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.
Files changed (23) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +6 -7
  3. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +3 -3
  4. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +1 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +4 -2
  6. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +357 -157
  7. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +146 -7
  8. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +37 -28
  9. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -3
  10. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +62 -38
  11. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +57 -49
  12. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
  13. package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -1
  14. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +2 -2
  15. package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +2 -2
  16. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -0
  17. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -0
  18. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +46 -0
  19. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +118 -10
  20. package/bundled/dev-pipeline/tests/test_unified_cli.py +76 -0
  21. package/bundled/skills/_metadata.json +1 -1
  22. package/package.json +1 -1
  23. 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
- status = _process_item(family, invocation, invocation.item_id, paths, initial_metadata={})
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
- final_status = _process_item(family, invocation, item_id, paths, initial_metadata=next_result.data)
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
- base_branch = str(initial_metadata.get("base_branch") or current_branch(paths.project_root) or "main")
143
- branch_name = str(initial_metadata.get("active_dev_branch") or env.dev_branch or _default_branch_name(family, item_id))
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
- while True:
182
- session_id = _session_id(item_id)
183
- session_paths = SessionPaths.for_item(family.state_dir, item_id, session_id)
184
- continuation = None
185
- if continuation_pending:
186
- continuation = {
187
- "previous_session_id": previous_session_id,
188
- "continuation_count": continuation_count,
189
- "context_overflow_count": context_overflow_count,
190
- "task_type": family.task_type,
191
- "active_dev_branch": branch_name,
192
- "base_branch": base_branch,
193
- "continuation_summary_path": _continuation_summary_path(paths.project_root, family, item_id, None),
194
- }
195
- prompt = generate_prompt(
196
- family,
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
- ).run()
227
- classification = classify_session(
228
- session_result,
229
- project_root=execution_root,
230
- base_head=base_head,
231
- prompt=prompt,
232
- previous_fingerprint=previous_fingerprint,
233
- previous_no_progress_count=previous_no_progress_count,
234
- )
235
- if classification.session_status == "context_overflow":
236
- summary_path = _continuation_summary_path(paths.project_root, family, item_id, prompt)
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
- "context_overflow",
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 or "context_overflow",
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
- previous_fingerprint = classification.progress_fingerprint
254
- previous_no_progress_count = classification.no_progress_count
255
- continuation_pending = True
256
- previous_session_id = session_id
257
- continuation_count += 1
258
- context_overflow_count += 1
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
- if final_session_status == "success":
264
- merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
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
- if final_session_status == "success":
275
- merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
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: