prizmkit 1.1.105 → 1.1.107

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 (36) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +93 -6
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +9 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +421 -61
  8. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +102 -17
  9. package/bundled/dev-pipeline/scripts/continuation.py +0 -4
  10. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +7 -39
  11. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -31
  12. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -31
  13. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
  14. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +28 -28
  15. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +36 -52
  16. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +34 -54
  17. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  18. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  19. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -3
  20. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +2 -2
  21. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -9
  22. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -10
  23. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
  24. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -3
  25. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +49 -2
  26. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -0
  27. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +50 -1
  28. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +560 -1
  29. package/bundled/dev-pipeline/tests/test_unified_cli.py +208 -0
  30. package/bundled/skills/_metadata.json +1 -1
  31. package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
  32. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
  33. package/package.json +1 -1
  34. package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
  35. package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
  36. package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.105",
3
- "bundledAt": "2026-07-06T18:17:35.126Z",
4
- "bundledFrom": "e92eb9f"
2
+ "frameworkVersion": "1.1.107",
3
+ "bundledAt": "2026-07-07T11:02:06.367Z",
4
+ "bundledFrom": "93afdd5"
5
5
  }
@@ -565,7 +565,6 @@ def branch_ensure_return_plan(context: BranchContext) -> GitCommandPlan:
565
565
  ),
566
566
  _git("stash", "push", "-m", "pipeline-ensure-return-stash", allow_failure=True, description="stash checkout blockers"),
567
567
  _git("checkout", context.base_branch, allow_failure=True, description="return to original branch"),
568
- _git("stash", "pop", allow_failure=True, description="restore stashed checkout blockers"),
569
568
  ),
570
569
  never_fails=True,
571
570
  notes=("Always returns success to support trap/finally cleanup paths.",),
@@ -7,10 +7,11 @@ legacy ordering around branch creation and final status updates.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from collections.abc import Sequence
10
11
  from dataclasses import dataclass
11
12
  from pathlib import Path
12
13
 
13
- from .gitops import GitCommandResult, git_status_safe, run_git_command
14
+ from .gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES, GitCommandResult, git_status_safe, run_git_command
14
15
  from .runner_models import RunnerFamily
15
16
 
16
17
 
@@ -27,25 +28,95 @@ class BookkeepingResult:
27
28
  return self.result is None or self.result.return_code == 0
28
29
 
29
30
 
30
- def has_workspace_changes(project_root: Path) -> bool:
31
- """Return true when git status has non-hidden-tool worktree changes."""
32
- return bool(git_status_safe(project_root).strip())
31
+ def _bookkeeping_paths(project_root: Path, family: RunnerFamily, item_id: str) -> tuple[str, ...]:
32
+ return (
33
+ _repo_path(project_root, family.plan_path),
34
+ _repo_path(project_root, family.state_dir / item_id / "status.json"),
35
+ )
36
+
37
+
38
+ def _repo_path(project_root: Path, path: Path) -> str:
39
+ try:
40
+ return path.resolve().relative_to(project_root.resolve()).as_posix()
41
+ except ValueError:
42
+ return path.as_posix()
43
+
44
+
45
+ def _changed_paths_in(project_root: Path, paths: Sequence[str]) -> tuple[str, ...]:
46
+ result = run_git_command(project_root, ("status", "--porcelain", "--", *paths))
47
+ if result.return_code != 0:
48
+ return ()
49
+ changed: list[str] = []
50
+ for line in result.stdout.splitlines():
51
+ path = line[3:].strip()
52
+ if " -> " in path:
53
+ path = path.split(" -> ", 1)[1].strip()
54
+ if path:
55
+ changed.append(path)
56
+ return tuple(changed)
57
+
58
+
59
+ def commit_bookkeeping_changes(
60
+ project_root: Path,
61
+ family: RunnerFamily,
62
+ item_id: str,
63
+ message: str,
64
+ ) -> BookkeepingResult:
65
+ """Stage and commit only runner-owned bookkeeping paths when present."""
66
+ paths = _changed_paths_in(project_root, _bookkeeping_paths(project_root, family, item_id))
67
+ if not paths:
68
+ return BookkeepingResult(attempted=False, committed=False)
69
+ run_git_command(project_root, ("add", "-A", "--", *paths))
70
+ result = run_git_command(project_root, ("commit", "--no-verify", "-m", message))
71
+ return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
33
72
 
34
73
 
35
- def commit_all_changes(project_root: Path, message: str) -> BookkeepingResult:
36
- """Stage and commit current workspace changes when present."""
37
- if not has_workspace_changes(project_root):
74
+ def commit_task_branch_changes(
75
+ project_root: Path,
76
+ family: RunnerFamily,
77
+ item_id: str,
78
+ status: str,
79
+ ) -> BookkeepingResult:
80
+ """Commit task branch code, artifacts, and status without switching branches."""
81
+ message = f"chore({item_id}): save {family.kind} {status} state"
82
+ if status == "completed":
83
+ message = f"chore({item_id}): complete {family.kind} task"
84
+ if not git_status_safe(project_root).strip():
38
85
  return BookkeepingResult(attempted=False, committed=False)
39
- run_git_command(project_root, ("add", "-A", "--", "."))
86
+ run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
40
87
  result = run_git_command(project_root, ("commit", "--no-verify", "-m", message))
41
88
  return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
42
89
 
43
90
 
91
+ def commit_wip_changes(
92
+ project_root: Path,
93
+ family: RunnerFamily,
94
+ item_id: str,
95
+ status: str,
96
+ ) -> BookkeepingResult:
97
+ """Commit all visible task-branch changes as preserved WIP."""
98
+ if not git_status_safe(project_root).strip():
99
+ return BookkeepingResult(attempted=False, committed=False)
100
+ run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
101
+ result = run_git_command(
102
+ project_root,
103
+ (
104
+ "commit",
105
+ "--no-verify",
106
+ "-m",
107
+ f"wip({item_id}): preserve {family.kind} {status} state",
108
+ "-m",
109
+ "Pipeline preserved task branch work without merging it to the base branch.",
110
+ ),
111
+ )
112
+ return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
113
+
114
+
44
115
  def commit_pre_branch_bookkeeping(project_root: Path, family: RunnerFamily, item_id: str) -> BookkeepingResult:
45
- """Commit status/list changes before creating the item dev branch."""
46
- return commit_all_changes(project_root, f"chore({item_id}): mark in_progress")
116
+ """Commit in-progress status on the current task branch."""
117
+ return commit_bookkeeping_changes(project_root, family, item_id, f"chore({item_id}): mark in_progress")
47
118
 
48
119
 
49
120
  def commit_final_bookkeeping(project_root: Path, family: RunnerFamily, item_id: str, status: str) -> BookkeepingResult:
50
- """Commit final list/state changes after returning to the base branch."""
51
- return commit_all_changes(project_root, f"chore({item_id}): update {family.kind} status {status}")
121
+ """Commit final runner-owned status changes on the current task branch."""
122
+ return commit_bookkeeping_changes(project_root, family, item_id, f"chore({item_id}): update {family.kind} status {status}")
@@ -29,6 +29,10 @@ INFRA_MARKERS = (
29
29
  "authentication",
30
30
  "unauthorized",
31
31
  "api error",
32
+ "quota",
33
+ "usage limit",
34
+ "credit balance",
35
+ "billing",
32
36
  )
33
37
 
34
38
 
@@ -43,10 +47,25 @@ def classify_session(
43
47
  ) -> SessionClassification:
44
48
  """Classify raw AI session signals with durable success precedence."""
45
49
  fingerprint = progress_fingerprint(project_root, prompt)
46
- if _has_commit_since(project_root, base_head):
47
- return SessionClassification("success", reason="commit_since_base", progress_fingerprint=fingerprint)
50
+ has_commit_since_base = _has_commit_since(project_root, base_head)
51
+
52
+ if _is_terminal_success(result) and _has_completed_workflow_artifact(prompt):
53
+ return SessionClassification(
54
+ "success",
55
+ reason="terminal_success_with_completed_checkpoint",
56
+ progress_fingerprint=fingerprint,
57
+ )
58
+
59
+ if _has_late_runtime_error_after_completion(result, prompt, has_commit_since_base):
60
+ return SessionClassification(
61
+ "success",
62
+ reason="completed_checkpoint_with_late_runtime_error",
63
+ progress_fingerprint=fingerprint,
64
+ )
48
65
 
49
66
  if _is_context_overflow(result):
67
+ if has_commit_since_base:
68
+ return SessionClassification("success", reason="commit_since_base", progress_fingerprint=fingerprint)
50
69
  no_progress_count = _next_no_progress_count(previous_fingerprint, fingerprint, previous_no_progress_count)
51
70
  if no_progress_count >= 2:
52
71
  return SessionClassification(
@@ -72,6 +91,8 @@ def classify_session(
72
91
  return SessionClassification("timed_out", reason="timeout", progress_fingerprint=fingerprint)
73
92
  if result.exit_code not in (0, None):
74
93
  return SessionClassification("crashed", reason=f"exit_code={result.exit_code}", progress_fingerprint=fingerprint)
94
+ if has_commit_since_base:
95
+ return SessionClassification("success", reason="commit_since_base", progress_fingerprint=fingerprint)
75
96
  if _has_workspace_changes(project_root):
76
97
  _auto_commit_workspace(project_root)
77
98
  return SessionClassification("success", reason="auto_commit_uncommitted_changes", progress_fingerprint=fingerprint)
@@ -100,6 +121,63 @@ def progress_fingerprint(project_root: Path, prompt: PromptGenerationResult | No
100
121
  }
101
122
 
102
123
 
124
+ def _is_terminal_success(result: AISessionResult) -> bool:
125
+ return (
126
+ result.exit_code in (0, None)
127
+ and not result.timed_out
128
+ and result.termination_reason not in {"timeout", "stale_session"}
129
+ and not result.fatal_error_code
130
+ and not result.progress_summary.fatal_error_code
131
+ and result.progress_summary.result_subtype == "success"
132
+ )
133
+
134
+
135
+ def _has_late_runtime_error_after_completion(
136
+ result: AISessionResult,
137
+ prompt: PromptGenerationResult | None,
138
+ has_commit_since_base: bool,
139
+ ) -> bool:
140
+ if not has_commit_since_base or not _has_completed_workflow_artifact(prompt):
141
+ return False
142
+ return _is_context_overflow(result) or _is_infra_error(result)
143
+
144
+
145
+ def _has_completed_workflow_artifact(prompt: PromptGenerationResult | None) -> bool:
146
+ if prompt is None:
147
+ return False
148
+ if prompt.checkpoint_path and prompt.checkpoint_path.is_file():
149
+ try:
150
+ data = json.loads(prompt.checkpoint_path.read_text(encoding="utf-8"))
151
+ except (OSError, json.JSONDecodeError):
152
+ data = {}
153
+ if isinstance(data, dict) and _checkpoint_cursor(data) == "complete":
154
+ return True
155
+ if prompt.artifact_path and has_completed_artifact_path(prompt.artifact_path):
156
+ return True
157
+ return False
158
+
159
+
160
+ def has_branch_completion_evidence(project_root: Path, base_branch: str, working_branch: str, artifact_path: Path | None) -> bool:
161
+ """Return true when a recorded task branch already has completed work to merge."""
162
+ if not base_branch or not working_branch or not has_completed_artifact_path(artifact_path):
163
+ return False
164
+ return _has_commit_range(project_root, base_branch, working_branch)
165
+
166
+
167
+ def has_completed_artifact_path(artifact_path: Path | None) -> bool:
168
+ if artifact_path is None:
169
+ return False
170
+ checkpoint = artifact_path / "workflow-checkpoint.json"
171
+ if checkpoint.is_file():
172
+ try:
173
+ data = json.loads(checkpoint.read_text(encoding="utf-8"))
174
+ except (OSError, json.JSONDecodeError):
175
+ data = {}
176
+ if isinstance(data, dict) and _checkpoint_cursor(data) == "complete":
177
+ return True
178
+ return (artifact_path / "completion-summary.json").is_file()
179
+
180
+
103
181
  def _next_no_progress_count(previous: object | None, current: object, count: int) -> int:
104
182
  if previous and previous == current:
105
183
  return max(0, count) + 1
@@ -125,11 +203,20 @@ def _is_infra_error(result: AISessionResult) -> bool:
125
203
  return bool(result.fatal_error_code and result.fatal_error_code != CONTEXT_OVERFLOW) or any(marker in text for marker in INFRA_MARKERS)
126
204
 
127
205
 
128
- def _has_commit_since(project_root: Path, base_head: str) -> bool:
129
- if not base_head:
206
+ def _has_commit_range(project_root: Path, start_ref: str, end_ref: str = "HEAD") -> bool:
207
+ if not start_ref or not end_ref:
130
208
  return False
131
- current = run_git_command(project_root, ("rev-parse", "HEAD")).stdout.strip()
132
- return bool(current and current != base_head)
209
+ result = run_git_command(project_root, ("rev-list", "--count", f"{start_ref}..{end_ref}"))
210
+ if result.return_code != 0:
211
+ return False
212
+ try:
213
+ return int(result.stdout.strip() or "0") > 0
214
+ except ValueError:
215
+ return False
216
+
217
+
218
+ def _has_commit_since(project_root: Path, base_head: str) -> bool:
219
+ return _has_commit_range(project_root, base_head, "HEAD")
133
220
 
134
221
 
135
222
  def _has_workspace_changes(project_root: Path) -> bool:
@@ -45,7 +45,6 @@ class RunnerEnvironment:
45
45
  heartbeat_interval_seconds: int = 30
46
46
  heartbeat_stale_threshold_seconds: int = 600
47
47
  stale_kill_threshold_seconds: int = 600
48
- max_log_size: int | None = None
49
48
  max_retries: int = 3
50
49
  strict_behavior_check: bool = True
51
50
 
@@ -67,7 +66,6 @@ class RunnerEnvironment:
67
66
  _int_value(values, "HEARTBEAT_TIMEOUT_SECONDS", 600),
68
67
  ),
69
68
  stale_kill_threshold_seconds=_int_value(values, "STALE_KILL_THRESHOLD", 600),
70
- max_log_size=_optional_int(values.get("MAX_LOG_SIZE")),
71
69
  max_retries=max(0, _int_value(values, "MAX_RETRIES", 3)),
72
70
  strict_behavior_check=not _falsey(values.get("STRICT_BEHAVIOR_CHECK")),
73
71
  )
@@ -349,9 +347,3 @@ def _safe_int(value: str | None, default: int) -> int:
349
347
 
350
348
  def _int_value(values: Mapping[str, str], name: str, default: int) -> int:
351
349
  return _safe_int(values.get(name), default)
352
-
353
-
354
- def _optional_int(value: str | None) -> int | None:
355
- if value is None or str(value).strip() == "":
356
- return None
357
- return _safe_int(value, 0)
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from .config import load_runtime_config
13
13
  from .paths import RuntimePaths
14
14
  from .runner_models import RunnerEnvironment, SessionPaths
15
- from .sessions import AISessionConfig, AISessionLauncher
15
+ from .sessions import AISessionConfig, AISessionLauncher, detect_stream_json_support
16
16
 
17
17
 
18
18
  @dataclass(frozen=True)
@@ -122,6 +122,9 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
122
122
  if not config.ai_client.command:
123
123
  return 1, "", "No supported AI CLI command was found for recovery\n"
124
124
  env = RunnerEnvironment.from_env()
125
+ stream_json = False
126
+ if env.live_output:
127
+ stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
125
128
  launcher = AISessionLauncher(
126
129
  AISessionConfig(
127
130
  cli_command=config.ai_client.command,
@@ -134,7 +137,12 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
134
137
  heartbeat_path=session_paths.heartbeat_json,
135
138
  effort=config.effort,
136
139
  verbose=env.verbose,
140
+ live_output=env.live_output,
141
+ use_stream_json=stream_json,
137
142
  stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
143
+ live_progress_interval_seconds=env.heartbeat_interval_seconds,
144
+ live_banner=False,
145
+ suppress_stream_output=True,
138
146
  )
139
147
  )
140
148
  result = launcher.run()