prizmkit 1.1.102 → 1.1.105
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 +6 -7
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +4 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +357 -157
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +146 -7
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +37 -28
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +62 -38
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +57 -49
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -1
- package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +2 -2
- package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -0
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +46 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +118 -10
- package/bundled/dev-pipeline/tests/test_unified_cli.py +76 -0
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +0 -71
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import sys
|
|
6
7
|
import time
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -15,6 +16,7 @@ from .gitops import (
|
|
|
15
16
|
branch_ensure_return,
|
|
16
17
|
branch_merge_plan,
|
|
17
18
|
current_branch,
|
|
19
|
+
default_branch,
|
|
18
20
|
ensure_linked_worktree,
|
|
19
21
|
guarded_worktree_remove,
|
|
20
22
|
materialize_worktree_support_assets,
|
|
@@ -92,7 +94,10 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
92
94
|
processed = 0
|
|
93
95
|
details: list[str] = []
|
|
94
96
|
if invocation.item_id:
|
|
95
|
-
|
|
97
|
+
try:
|
|
98
|
+
status = _process_item(family, invocation, invocation.item_id, paths, initial_metadata={})
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
return PipelineRunResult(False, 1, "interrupted", "interrupted", tuple(details))
|
|
96
101
|
return PipelineRunResult(status == "completed", 1, "single_item_done", status, tuple(details))
|
|
97
102
|
|
|
98
103
|
while True:
|
|
@@ -109,10 +114,16 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
109
114
|
item_id = str(next_result.data.get(f"{family.kind}_id") or next_result.data.get("feature_id") or next_result.data.get("bug_id") or next_result.data.get("refactor_id") or "")
|
|
110
115
|
if not item_id:
|
|
111
116
|
return PipelineRunResult(False, processed, "missing_item_id", details=(marker[:500],))
|
|
112
|
-
|
|
117
|
+
try:
|
|
118
|
+
final_status = _process_item(family, invocation, item_id, paths, initial_metadata=next_result.data)
|
|
119
|
+
except KeyboardInterrupt:
|
|
120
|
+
processed += 1
|
|
121
|
+
details.append(f"{item_id}:interrupted")
|
|
122
|
+
return PipelineRunResult(False, processed, "interrupted", "interrupted", tuple(details))
|
|
113
123
|
processed += 1
|
|
114
124
|
details.append(f"{item_id}:{final_status}")
|
|
115
125
|
if final_status == "completed":
|
|
126
|
+
_emit_info(f"Pausing 5s before next {family.kind}...")
|
|
116
127
|
continue
|
|
117
128
|
if final_status == "pending":
|
|
118
129
|
continue
|
|
@@ -122,6 +133,165 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
122
133
|
return PipelineRunResult(False, processed, "item_failed", final_status, tuple(details))
|
|
123
134
|
|
|
124
135
|
|
|
136
|
+
_SEPARATOR = "─" * 52
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _emit_separator() -> None:
|
|
140
|
+
print(_SEPARATOR, file=sys.stderr, flush=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _emit_info(message: str, *, level: str = "INFO") -> None:
|
|
144
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
145
|
+
print(f"{f'[{level}]'.ljust(10)}{timestamp} {message}", file=sys.stderr, flush=True)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _emit_success(message: str) -> None:
|
|
149
|
+
_emit_info(message, level="SUCCESS")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _emit_warning(message: str) -> None:
|
|
153
|
+
_emit_info(message, level="WARN")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _family_label(family: RunnerFamily) -> str:
|
|
157
|
+
return {"feature": "Feature", "bugfix": "Bug", "refactor": "Refactor"}.get(family.kind, family.kind.title())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _max_retries(invocation: RunnerInvocation) -> int:
|
|
161
|
+
if invocation.max_retries is not None:
|
|
162
|
+
return max(0, invocation.max_retries)
|
|
163
|
+
return RunnerEnvironment.from_env().max_retries
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _emit_item_header(family: RunnerFamily, invocation: RunnerInvocation, item_id: str, metadata: dict[str, object]) -> None:
|
|
167
|
+
title = str(metadata.get("title") or metadata.get("description") or "").strip()
|
|
168
|
+
title_text = f" — {title}" if title else ""
|
|
169
|
+
retry_count = int(metadata.get("retry_count") or 0)
|
|
170
|
+
infra_count = int(metadata.get("infra_error_count") or 0)
|
|
171
|
+
_emit_separator()
|
|
172
|
+
_emit_info(f"{_family_label(family)}: {item_id}{title_text}")
|
|
173
|
+
_emit_info(f"Code retry: {retry_count} / {_max_retries(invocation)}")
|
|
174
|
+
_emit_info(f"Infrastructure retry: {infra_count} / {invocation.max_infra_retries}")
|
|
175
|
+
_emit_separator()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _is_recovery_side_effect_branch(branch: str, family: RunnerFamily, item_id: str) -> bool:
|
|
179
|
+
return bool(branch) and branch.startswith(f"{family.branch_prefix}/{item_id}-") and "-recovery-" in branch
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _is_current_task_branch(branch: str, family: RunnerFamily, item_id: str) -> bool:
|
|
183
|
+
return bool(branch) and branch.startswith(f"{family.branch_prefix}/{item_id}-") and not _is_recovery_side_effect_branch(branch, family, item_id)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _resolve_base_branch(project_root: Path, metadata: dict[str, object], active_branch: str, family: RunnerFamily, item_id: str) -> str:
|
|
187
|
+
recorded = str(metadata.get("base_branch") or "").strip()
|
|
188
|
+
if recorded:
|
|
189
|
+
return recorded
|
|
190
|
+
detected_default = default_branch(project_root)
|
|
191
|
+
if _is_current_task_branch(active_branch, family, item_id) or _is_recovery_side_effect_branch(active_branch, family, item_id):
|
|
192
|
+
return detected_default
|
|
193
|
+
return active_branch or detected_default
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _resolve_working_branch(family: RunnerFamily, item_id: str, metadata: dict[str, object], env: RunnerEnvironment, active_branch: str) -> str:
|
|
197
|
+
recorded = str(metadata.get("active_dev_branch") or "").strip()
|
|
198
|
+
if recorded and not _is_recovery_side_effect_branch(recorded, family, item_id):
|
|
199
|
+
return recorded
|
|
200
|
+
if env.dev_branch and not _is_recovery_side_effect_branch(env.dev_branch, family, item_id):
|
|
201
|
+
return env.dev_branch
|
|
202
|
+
if _is_current_task_branch(active_branch, family, item_id):
|
|
203
|
+
return active_branch
|
|
204
|
+
return _default_branch_name(family, item_id)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _emit_branch_setup_result(branch_context: BranchContext, result) -> None:
|
|
208
|
+
results = getattr(result, "results", ()) or ()
|
|
209
|
+
created = len(results) > 1 and results[1].return_code == 0
|
|
210
|
+
reused = len(results) > 2 and results[2].return_code == 0 and not created
|
|
211
|
+
if created:
|
|
212
|
+
_emit_info(f"Created and checked out branch: {branch_context.working_branch} (from {branch_context.base_branch})")
|
|
213
|
+
elif reused:
|
|
214
|
+
_emit_info(f"Reusing branch: {branch_context.working_branch} (from {branch_context.base_branch})")
|
|
215
|
+
else:
|
|
216
|
+
_emit_info(f"Dev branch: {branch_context.working_branch}")
|
|
217
|
+
_emit_info(f"Dev branch: {branch_context.working_branch}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _emit_git_plan_output(result) -> None:
|
|
221
|
+
for command_result in getattr(result, "results", ()) or ():
|
|
222
|
+
for text in (getattr(command_result, "stdout", ""), getattr(command_result, "stderr", "")):
|
|
223
|
+
stripped = str(text or "").strip()
|
|
224
|
+
if stripped:
|
|
225
|
+
print(stripped, file=sys.stderr, flush=True)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _bytes_text(size: int) -> str:
|
|
229
|
+
if size >= 1024 * 1024:
|
|
230
|
+
return f"{round(size / (1024 * 1024))}MB"
|
|
231
|
+
if size >= 1024:
|
|
232
|
+
return f"{round(size / 1024)}KB"
|
|
233
|
+
return f"{size}B"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _session_log_summary(session_log: Path) -> tuple[int, str]:
|
|
237
|
+
try:
|
|
238
|
+
size = session_log.stat().st_size
|
|
239
|
+
with session_log.open("r", encoding="utf-8", errors="ignore") as handle:
|
|
240
|
+
line_count = sum(1 for _ in handle)
|
|
241
|
+
return line_count, _bytes_text(size)
|
|
242
|
+
except OSError:
|
|
243
|
+
return 0, "0B"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _progress_data(path: Path) -> dict[str, object]:
|
|
247
|
+
if not path.is_file():
|
|
248
|
+
return {}
|
|
249
|
+
try:
|
|
250
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
251
|
+
except (OSError, json.JSONDecodeError):
|
|
252
|
+
return {}
|
|
253
|
+
return data if isinstance(data, dict) else {}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _emit_prompt_summary(prompt: PromptGenerationResult, session_id: str) -> None:
|
|
257
|
+
if prompt.pipeline_mode:
|
|
258
|
+
_emit_info(f"Pipeline mode: {_pipeline_mode_label(prompt.pipeline_mode)}")
|
|
259
|
+
if prompt.agent_count:
|
|
260
|
+
critic = "enabled" if prompt.critic_enabled else "disabled"
|
|
261
|
+
_emit_info(f"Agents: {prompt.agent_count} (critic: {critic})")
|
|
262
|
+
_emit_info(f"Spawning AI CLI session: {session_id}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _pipeline_mode_label(mode: str) -> str:
|
|
266
|
+
labels = {
|
|
267
|
+
"lite": "lite (Tier 1 — Single Agent)",
|
|
268
|
+
"standard": "standard (Tier 2 — Orchestrator + Critic/Reviewer)",
|
|
269
|
+
"full": "full (Tier 3 — Orchestrator + Critic/Reviewer)",
|
|
270
|
+
}
|
|
271
|
+
return labels.get(mode, mode)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _emit_session_summary(session_result, session_paths: SessionPaths, classification) -> None:
|
|
275
|
+
lines, size = _session_log_summary(session_paths.session_log)
|
|
276
|
+
_emit_info(f"Session log: {lines} lines, {size}")
|
|
277
|
+
_emit_info(f"exit_code={getattr(session_result, 'exit_code', None)}")
|
|
278
|
+
_emit_info(f"Session result: {classification.session_status}")
|
|
279
|
+
progress = _progress_data(session_paths.progress_json)
|
|
280
|
+
subagent_calls = int(progress.get("subagent_spawn_count") or 0)
|
|
281
|
+
if subagent_calls:
|
|
282
|
+
_emit_info(f"Subagent calls detected in session: {subagent_calls}")
|
|
283
|
+
if classification.session_status == "success":
|
|
284
|
+
_emit_info("CHECKPOINT: All workflow steps completed")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _emit_update_summary(family: RunnerFamily, item_id: str, update) -> None:
|
|
288
|
+
if not isinstance(getattr(update, "data", None), dict):
|
|
289
|
+
return
|
|
290
|
+
new_status = str(update.data.get("new_status") or "")
|
|
291
|
+
if family.kind == "feature" and new_status == "completed":
|
|
292
|
+
_emit_info(f"Propagated completion notes for {item_id} to feature-list.json")
|
|
293
|
+
|
|
294
|
+
|
|
125
295
|
def _process_item(
|
|
126
296
|
family: RunnerFamily,
|
|
127
297
|
invocation: RunnerInvocation,
|
|
@@ -134,183 +304,213 @@ def _process_item(
|
|
|
134
304
|
config = load_runtime_config(paths)
|
|
135
305
|
if not config.ai_client.command:
|
|
136
306
|
raise RuntimeError("No supported AI CLI command was found")
|
|
137
|
-
start = start_item(family, invocation, item_id, paths.project_root)
|
|
138
|
-
if not start.ok:
|
|
139
|
-
raise RuntimeError(start.text)
|
|
140
|
-
commit_pre_branch_bookkeeping(paths.project_root, family, item_id)
|
|
141
307
|
|
|
142
|
-
|
|
143
|
-
|
|
308
|
+
active_branch = current_branch(paths.project_root)
|
|
309
|
+
base_branch = _resolve_base_branch(paths.project_root, initial_metadata, active_branch, family, item_id)
|
|
310
|
+
branch_name = _resolve_working_branch(family, item_id, initial_metadata, env, active_branch)
|
|
144
311
|
branch_context = BranchContext(base_branch, branch_name, paths.project_root)
|
|
145
312
|
worktree_runtime = None
|
|
146
313
|
use_worktree = _use_worktree_for_family(env, family)
|
|
147
314
|
execution_root = paths.project_root
|
|
148
|
-
if use_worktree:
|
|
149
|
-
worktree_runtime = worktree_runtime_context(
|
|
150
|
-
branch_context,
|
|
151
|
-
WorktreePolicy(
|
|
152
|
-
use_worktree=True,
|
|
153
|
-
cleanup_on_success=True,
|
|
154
|
-
preserve_on_failure=True,
|
|
155
|
-
worktree_root=paths.state_dir / "worktrees",
|
|
156
|
-
),
|
|
157
|
-
)
|
|
158
|
-
setup = ensure_linked_worktree(worktree_runtime)
|
|
159
|
-
if not setup.ok:
|
|
160
|
-
status = update_item(family, invocation, item_id, "infra_error", "", paths.project_root)
|
|
161
|
-
return _status_from_update(status)
|
|
162
|
-
worktree_runtime = setup.runtime or worktree_runtime
|
|
163
|
-
support_assets = materialize_worktree_support_assets(worktree_runtime)
|
|
164
|
-
if not support_assets.ok:
|
|
165
|
-
status = update_item(family, invocation, item_id, "infra_error", "", paths.project_root)
|
|
166
|
-
return _status_from_update(status)
|
|
167
|
-
execution_root = worktree_runtime.worktree_path
|
|
168
|
-
else:
|
|
169
|
-
branch_result = run_git_plan(paths.project_root, branch_create_plan(branch_context))
|
|
170
|
-
if not branch_result.ok:
|
|
171
|
-
status = update_item(family, invocation, item_id, "infra_error", "", paths.project_root)
|
|
172
|
-
return _status_from_update(status)
|
|
173
|
-
|
|
174
|
-
previous_fingerprint = initial_metadata.get("last_progress_fingerprint")
|
|
175
|
-
previous_no_progress_count = int(initial_metadata.get("no_progress_count") or 0)
|
|
176
|
-
continuation_pending = bool(initial_metadata.get("continuation_pending"))
|
|
177
|
-
previous_session_id = str(initial_metadata.get("previous_session_id") or initial_metadata.get("last_context_overflow_session_id") or "")
|
|
178
|
-
continuation_count = int(initial_metadata.get("continuation_count") or 0)
|
|
179
|
-
context_overflow_count = int(initial_metadata.get("context_overflow_count") or 0)
|
|
180
315
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
invocation,
|
|
198
|
-
item_id=item_id,
|
|
199
|
-
session_id=session_id,
|
|
200
|
-
run_id="run-python",
|
|
201
|
-
retry_count=0,
|
|
202
|
-
session_paths=session_paths,
|
|
203
|
-
project_root=paths.project_root,
|
|
204
|
-
execution_root=execution_root,
|
|
205
|
-
continuation=continuation,
|
|
206
|
-
)
|
|
207
|
-
if invocation.dry_run:
|
|
208
|
-
return "dry_run"
|
|
209
|
-
stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
|
|
210
|
-
base_head = run_git_command(execution_root, ("rev-parse", "HEAD")).stdout.strip()
|
|
211
|
-
session_result = AISessionLauncher(
|
|
212
|
-
AISessionConfig(
|
|
213
|
-
cli_command=config.ai_client.command,
|
|
214
|
-
platform=config.ai_client.platform or "codebuddy",
|
|
215
|
-
model=prompt.model or config.model,
|
|
216
|
-
prompt_path=prompt.output_path,
|
|
217
|
-
cwd=execution_root,
|
|
218
|
-
log_path=session_paths.session_log,
|
|
219
|
-
progress_path=session_paths.progress_json,
|
|
220
|
-
heartbeat_path=session_paths.heartbeat_json,
|
|
221
|
-
effort=config.effort,
|
|
222
|
-
verbose=env.verbose,
|
|
223
|
-
use_stream_json=stream_json,
|
|
224
|
-
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
316
|
+
_emit_item_header(family, invocation, item_id, initial_metadata)
|
|
317
|
+
try:
|
|
318
|
+
start = start_item(family, invocation, item_id, paths.project_root)
|
|
319
|
+
if not start.ok:
|
|
320
|
+
raise RuntimeError(start.text)
|
|
321
|
+
commit_pre_branch_bookkeeping(paths.project_root, family, item_id)
|
|
322
|
+
|
|
323
|
+
if use_worktree:
|
|
324
|
+
worktree_runtime = worktree_runtime_context(
|
|
325
|
+
branch_context,
|
|
326
|
+
WorktreePolicy(
|
|
327
|
+
use_worktree=True,
|
|
328
|
+
cleanup_on_success=True,
|
|
329
|
+
preserve_on_failure=True,
|
|
330
|
+
worktree_root=paths.state_dir / "worktrees",
|
|
331
|
+
),
|
|
225
332
|
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
333
|
+
setup = ensure_linked_worktree(worktree_runtime)
|
|
334
|
+
if not setup.ok:
|
|
335
|
+
status = update_item(family, invocation, item_id, "infra_error", "", paths.project_root)
|
|
336
|
+
return _status_from_update(status)
|
|
337
|
+
worktree_runtime = setup.runtime or worktree_runtime
|
|
338
|
+
_emit_info(f"Dev branch: {branch_name}")
|
|
339
|
+
_emit_info(f"Worktree: {worktree_runtime.worktree_path}")
|
|
340
|
+
support_assets = materialize_worktree_support_assets(worktree_runtime)
|
|
341
|
+
if not support_assets.ok:
|
|
342
|
+
status = update_item(family, invocation, item_id, "infra_error", "", paths.project_root)
|
|
343
|
+
return _status_from_update(status)
|
|
344
|
+
execution_root = worktree_runtime.worktree_path
|
|
345
|
+
else:
|
|
346
|
+
branch_result = run_git_plan(paths.project_root, branch_create_plan(branch_context))
|
|
347
|
+
_emit_branch_setup_result(branch_context, branch_result)
|
|
348
|
+
if not branch_result.ok:
|
|
349
|
+
status = update_item(family, invocation, item_id, "infra_error", "", paths.project_root)
|
|
350
|
+
return _status_from_update(status)
|
|
351
|
+
|
|
352
|
+
previous_fingerprint = initial_metadata.get("last_progress_fingerprint")
|
|
353
|
+
previous_no_progress_count = int(initial_metadata.get("no_progress_count") or 0)
|
|
354
|
+
continuation_pending = bool(initial_metadata.get("continuation_pending"))
|
|
355
|
+
previous_session_id = str(initial_metadata.get("previous_session_id") or initial_metadata.get("last_context_overflow_session_id") or "")
|
|
356
|
+
continuation_count = int(initial_metadata.get("continuation_count") or 0)
|
|
357
|
+
context_overflow_count = int(initial_metadata.get("context_overflow_count") or 0)
|
|
358
|
+
|
|
359
|
+
while True:
|
|
360
|
+
session_id = _session_id(item_id)
|
|
361
|
+
session_paths = SessionPaths.for_item(family.state_dir, item_id, session_id)
|
|
362
|
+
continuation = None
|
|
363
|
+
if continuation_pending:
|
|
364
|
+
continuation = {
|
|
365
|
+
"previous_session_id": previous_session_id,
|
|
366
|
+
"continuation_count": continuation_count,
|
|
367
|
+
"context_overflow_count": context_overflow_count,
|
|
368
|
+
"task_type": family.task_type,
|
|
369
|
+
"active_dev_branch": branch_name,
|
|
370
|
+
"base_branch": base_branch,
|
|
371
|
+
"continuation_summary_path": _continuation_summary_path(paths.project_root, family, item_id, None),
|
|
372
|
+
}
|
|
373
|
+
prompt = generate_prompt(
|
|
374
|
+
family,
|
|
375
|
+
invocation,
|
|
376
|
+
item_id=item_id,
|
|
377
|
+
session_id=session_id,
|
|
378
|
+
run_id="run-python",
|
|
379
|
+
retry_count=0,
|
|
380
|
+
session_paths=session_paths,
|
|
381
|
+
project_root=paths.project_root,
|
|
382
|
+
execution_root=execution_root,
|
|
383
|
+
continuation=continuation,
|
|
384
|
+
)
|
|
385
|
+
if invocation.dry_run:
|
|
386
|
+
return "dry_run"
|
|
387
|
+
_emit_prompt_summary(prompt, session_id)
|
|
388
|
+
stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
|
|
389
|
+
base_head = run_git_command(execution_root, ("rev-parse", "HEAD")).stdout.strip()
|
|
390
|
+
session_result = AISessionLauncher(
|
|
391
|
+
AISessionConfig(
|
|
392
|
+
cli_command=config.ai_client.command,
|
|
393
|
+
platform=config.ai_client.platform or "codebuddy",
|
|
394
|
+
model=prompt.model or config.model,
|
|
395
|
+
prompt_path=prompt.output_path,
|
|
396
|
+
cwd=execution_root,
|
|
397
|
+
log_path=session_paths.session_log,
|
|
398
|
+
progress_path=session_paths.progress_json,
|
|
399
|
+
heartbeat_path=session_paths.heartbeat_json,
|
|
400
|
+
effort=config.effort,
|
|
401
|
+
verbose=env.verbose,
|
|
402
|
+
live_output=env.live_output,
|
|
403
|
+
use_stream_json=stream_json,
|
|
404
|
+
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
405
|
+
live_progress_interval_seconds=env.heartbeat_interval_seconds,
|
|
406
|
+
live_banner=False,
|
|
407
|
+
suppress_stream_output=True,
|
|
408
|
+
)
|
|
409
|
+
).run()
|
|
410
|
+
classification = classify_session(
|
|
411
|
+
session_result,
|
|
412
|
+
project_root=execution_root,
|
|
413
|
+
base_head=base_head,
|
|
414
|
+
prompt=prompt,
|
|
415
|
+
previous_fingerprint=previous_fingerprint,
|
|
416
|
+
previous_no_progress_count=previous_no_progress_count,
|
|
417
|
+
)
|
|
418
|
+
_emit_session_summary(session_result, session_paths, classification)
|
|
419
|
+
if classification.session_status == "context_overflow":
|
|
420
|
+
summary_path = _continuation_summary_path(paths.project_root, family, item_id, prompt)
|
|
421
|
+
update = update_item(
|
|
422
|
+
family,
|
|
423
|
+
invocation,
|
|
424
|
+
item_id,
|
|
425
|
+
"context_overflow",
|
|
426
|
+
session_id,
|
|
427
|
+
paths.project_root,
|
|
428
|
+
active_dev_branch=branch_name,
|
|
429
|
+
base_branch=base_branch,
|
|
430
|
+
last_fatal_error_code=classification.fatal_error_code or "context_overflow",
|
|
431
|
+
continuation_summary_path=summary_path,
|
|
432
|
+
no_progress_count=classification.no_progress_count,
|
|
433
|
+
progress_fingerprint=classification.progress_fingerprint,
|
|
434
|
+
)
|
|
435
|
+
if not update.ok:
|
|
436
|
+
raise RuntimeError(update.text)
|
|
437
|
+
_emit_update_summary(family, item_id, update)
|
|
438
|
+
previous_fingerprint = classification.progress_fingerprint
|
|
439
|
+
previous_no_progress_count = classification.no_progress_count
|
|
440
|
+
continuation_pending = True
|
|
441
|
+
previous_session_id = session_id
|
|
442
|
+
continuation_count += 1
|
|
443
|
+
context_overflow_count += 1
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
final_session_status = classification.session_status
|
|
447
|
+
if use_worktree and worktree_runtime is not None:
|
|
448
|
+
if final_session_status == "success":
|
|
449
|
+
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
450
|
+
merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
|
|
451
|
+
_emit_git_plan_output(merge)
|
|
452
|
+
if merge.ok:
|
|
453
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=True)
|
|
454
|
+
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
455
|
+
else:
|
|
456
|
+
final_session_status = "merge_conflict"
|
|
457
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
458
|
+
else:
|
|
459
|
+
worktree_save_wip(worktree_runtime)
|
|
460
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
461
|
+
else:
|
|
462
|
+
if final_session_status == "success":
|
|
463
|
+
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
464
|
+
merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
|
|
465
|
+
_emit_git_plan_output(merge)
|
|
466
|
+
if merge.ok:
|
|
467
|
+
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
468
|
+
else:
|
|
469
|
+
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
470
|
+
final_session_status = "merge_conflict"
|
|
471
|
+
else:
|
|
472
|
+
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
473
|
+
|
|
474
|
+
_write_session_status(
|
|
475
|
+
session_paths,
|
|
476
|
+
status=final_session_status,
|
|
477
|
+
session_id=session_id,
|
|
478
|
+
task_id=item_id,
|
|
479
|
+
task_type=family.task_type,
|
|
480
|
+
base_branch=base_branch,
|
|
481
|
+
active_dev_branch=branch_name,
|
|
482
|
+
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
483
|
+
reason=classification.reason,
|
|
484
|
+
fatal_error_code=classification.fatal_error_code,
|
|
485
|
+
)
|
|
486
|
+
|
|
237
487
|
update = update_item(
|
|
238
488
|
family,
|
|
239
489
|
invocation,
|
|
240
490
|
item_id,
|
|
241
|
-
|
|
491
|
+
final_session_status,
|
|
242
492
|
session_id,
|
|
243
493
|
paths.project_root,
|
|
244
494
|
active_dev_branch=branch_name,
|
|
245
495
|
base_branch=base_branch,
|
|
246
|
-
last_fatal_error_code=classification.fatal_error_code
|
|
247
|
-
continuation_summary_path=summary_path,
|
|
496
|
+
last_fatal_error_code=classification.fatal_error_code,
|
|
248
497
|
no_progress_count=classification.no_progress_count,
|
|
249
498
|
progress_fingerprint=classification.progress_fingerprint,
|
|
250
499
|
)
|
|
251
500
|
if not update.ok:
|
|
252
501
|
raise RuntimeError(update.text)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
continue
|
|
260
|
-
|
|
261
|
-
final_session_status = classification.session_status
|
|
502
|
+
_emit_update_summary(family, item_id, update)
|
|
503
|
+
new_status = _status_from_update(update)
|
|
504
|
+
commit_final_bookkeeping(paths.project_root, family, item_id, new_status)
|
|
505
|
+
return new_status
|
|
506
|
+
except KeyboardInterrupt:
|
|
507
|
+
_emit_warning(f"Interrupted while processing {item_id}; preserving work and returning to {base_branch}")
|
|
262
508
|
if use_worktree and worktree_runtime is not None:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if merge.ok:
|
|
266
|
-
guarded_worktree_remove(worktree_runtime, delete_branch=True)
|
|
267
|
-
else:
|
|
268
|
-
final_session_status = "merge_conflict"
|
|
269
|
-
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
270
|
-
else:
|
|
271
|
-
worktree_save_wip(worktree_runtime)
|
|
272
|
-
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
509
|
+
worktree_save_wip(worktree_runtime)
|
|
510
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
273
511
|
else:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if not merge.ok:
|
|
277
|
-
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
278
|
-
final_session_status = "merge_conflict"
|
|
279
|
-
else:
|
|
280
|
-
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
281
|
-
|
|
282
|
-
_write_session_status(
|
|
283
|
-
session_paths,
|
|
284
|
-
status=final_session_status,
|
|
285
|
-
session_id=session_id,
|
|
286
|
-
task_id=item_id,
|
|
287
|
-
task_type=family.task_type,
|
|
288
|
-
base_branch=base_branch,
|
|
289
|
-
active_dev_branch=branch_name,
|
|
290
|
-
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
291
|
-
reason=classification.reason,
|
|
292
|
-
fatal_error_code=classification.fatal_error_code,
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
update = update_item(
|
|
296
|
-
family,
|
|
297
|
-
invocation,
|
|
298
|
-
item_id,
|
|
299
|
-
final_session_status,
|
|
300
|
-
session_id,
|
|
301
|
-
paths.project_root,
|
|
302
|
-
active_dev_branch=branch_name,
|
|
303
|
-
base_branch=base_branch,
|
|
304
|
-
last_fatal_error_code=classification.fatal_error_code,
|
|
305
|
-
no_progress_count=classification.no_progress_count,
|
|
306
|
-
progress_fingerprint=classification.progress_fingerprint,
|
|
307
|
-
)
|
|
308
|
-
if not update.ok:
|
|
309
|
-
raise RuntimeError(update.text)
|
|
310
|
-
new_status = _status_from_update(update)
|
|
311
|
-
commit_final_bookkeeping(paths.project_root, family, item_id, new_status)
|
|
312
|
-
return new_status
|
|
313
|
-
|
|
512
|
+
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
513
|
+
raise
|
|
314
514
|
|
|
315
515
|
|
|
316
516
|
def _test_cli(kind: str, paths) -> CommandResult:
|