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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +93 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +9 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +421 -61
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +102 -17
- package/bundled/dev-pipeline/scripts/continuation.py +0 -4
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +7 -39
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -31
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -31
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +28 -28
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +36 -52
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +34 -54
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -3
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +49 -2
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +50 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +560 -1
- package/bundled/dev-pipeline/tests/test_unified_cli.py +208 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
- package/package.json +1 -1
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
package/bundled/VERSION.json
CHANGED
|
@@ -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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
46
|
-
return
|
|
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
|
|
51
|
-
return
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
129
|
-
if not
|
|
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
|
-
|
|
132
|
-
|
|
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()
|