prizmkit 1.1.104 → 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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.104",
3
- "bundledAt": "2026-07-06T16:42:02.971Z",
4
- "bundledFrom": "05c5c13"
2
+ "frameworkVersion": "1.1.105",
3
+ "bundledAt": "2026-07-06T18:17:35.126Z",
4
+ "bundledFrom": "e92eb9f"
5
5
  }
@@ -678,10 +678,9 @@ cli.py feature run command
678
678
  +- AI CLI session # claude -p "$(cat prompt)" --dangerously-skip-permissions
679
679
  | | # cbc --print -y < prompt
680
680
  | | # codex --ask-for-approval never --sandbox danger-full-access exec --cd "$PROJECT_ROOT" --skip-git-repo-check - < prompt
681
- | +- prizm-dev-team # Multi-agent team implements the feature
682
- | +- Orchestrator # Main: init, context, plan, retrospective, commit
683
- | +- Dev x N # Implementation with TDD
684
- | +- Reviewer # Analyze + code review
681
+ | +- session orchestrator # Main: init, context, plan, direct implementation, review gate, retrospective, commit
682
+ | +- Critic # Optional plan challenge
683
+ | +- Reviewer # Code review via /prizmkit-code-review
685
684
  |
686
685
  +- check-session-status.py # Parse session outcome
687
686
  +- update-feature-status.py # Update feature state (completed/failed/retry)
@@ -696,8 +695,8 @@ The bootstrap prompt adapts based on feature complexity:
696
695
  | Tier | Template | Agents | Use Case |
697
696
  |------|----------|--------|----------|
698
697
  | Tier 1 (lite) | `bootstrap-tier1.md` | Single agent handles everything | Low complexity features |
699
- | Tier 2 (standard) | `bootstrap-tier2.md` | Orchestrator + Dev + Reviewer subagents | Medium complexity |
700
- | Tier 3 (full) | `bootstrap-tier3.md` | Full team with spec/plan/analyze/implement/review phases | High/critical complexity |
698
+ | Tier 2 (standard) | `bootstrap-tier2.md` | Orchestrator implements directly + optional Critic/Reviewer | Medium/high complexity |
699
+ | Tier 3 (full) | `bootstrap-tier3.md` | Orchestrator implements directly + Critic/Reviewer review phases | High/critical complexity |
701
700
 
702
701
  **Self-evolve mode:** Uses Tier 3 template with additional framework guardrails for developing the PrizmKit framework itself.
703
702
 
@@ -919,7 +918,7 @@ cli.py bugfix run command
919
918
  | Claude Code | `.claude/agents/prizm-dev-team-*.md` | `.claude/team-info.json` |
920
919
  | CodeBuddy | `.codebuddy/agents/prizm-dev-team-*.md` | `~/.codebuddy/teams/prizm-dev-team/config.json` |
921
920
 
922
- Agent files are 2 types: `prizm-dev-team-dev.md` (implementation) and `prizm-dev-team-reviewer.md` (review). Paths are auto-resolved by `generate-bootstrap-prompt.py`.
921
+ Agent files include Reviewer/Critic definitions used by feature prompt flows. `prizm-dev-team-dev.md` remains available for targeted fix/specialized handoffs such as the internal `/prizmkit-code-review` fix loop, but top-level feature implementation is performed directly by the main orchestrator with `/prizmkit-implement`. Paths are auto-resolved by `generate-bootstrap-prompt.py`.
923
922
 
924
923
  ---
925
924
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- dev-pipeline drives the prizm-dev-team multi-agent team through an outer shell loop. Each iteration spawns a new AI CLI session with a bootstrap prompt that instructs the agent to create and orchestrate the team for one feature.
5
+ dev-pipeline drives feature sessions through the unified Python outer loop. Each iteration spawns a new AI CLI session with a bootstrap prompt that makes the main session orchestrator responsible for implementation, while Critic and Reviewer agents remain available for challenge/review phases.
6
6
 
7
7
  ## Architecture
8
8
 
@@ -19,7 +19,7 @@ dev-pipeline (outer loop)
19
19
  ├── Phase 1-2: Context snapshot + Specify + Plan (Orchestrator)
20
20
  ├── Phase 3: Analyze (Reviewer agent) [tier2] / Phase 4: Analyze [tier3]
21
21
  ├── Phase 3.5: Plan Challenge (Critic agent(s), 1 or 3 parallel) [tier2+]
22
- ├── Phase 4: Implement (Dev agent) [tier2] / Phase 5: Implement [tier3]
22
+ ├── Phase 4: Implement (main orchestrator runs /prizmkit-implement directly)
23
23
  ├── Phase 5: Review (Reviewer agent)
24
24
  └── Phase 6: Retrospective & Commit (Orchestrator)
25
25
 
@@ -30,7 +30,7 @@ dev-pipeline (outer loop)
30
30
 
31
31
  | Agent | Definition Path | Type |
32
32
  |-------|----------------|------|
33
- | Dev | `.claude/agents/prizm-dev-team-dev.md` (or `.codebuddy/agents/`) | prizm-dev-team-dev |
33
+ | Dev | `.claude/agents/prizm-dev-team-dev.md` (or `.codebuddy/agents/`) | prizm-dev-team-dev; reserved for targeted fix/specialized handoffs, not top-level feature implementation |
34
34
  | Reviewer | `.claude/agents/prizm-dev-team-reviewer.md` (or `.codebuddy/agents/`) | prizm-dev-team-reviewer |
35
35
  | Critic | `.claude/agents/prizm-dev-team-critic.md` (or `.codebuddy/agents/`) | prizm-dev-team-critic |
36
36
 
@@ -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,184 +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
- live_output=env.live_output,
224
- use_stream_json=stream_json,
225
- 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
+ ),
226
332
  )
227
- ).run()
228
- classification = classify_session(
229
- session_result,
230
- project_root=execution_root,
231
- base_head=base_head,
232
- prompt=prompt,
233
- previous_fingerprint=previous_fingerprint,
234
- previous_no_progress_count=previous_no_progress_count,
235
- )
236
- if classification.session_status == "context_overflow":
237
- 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
+
238
487
  update = update_item(
239
488
  family,
240
489
  invocation,
241
490
  item_id,
242
- "context_overflow",
491
+ final_session_status,
243
492
  session_id,
244
493
  paths.project_root,
245
494
  active_dev_branch=branch_name,
246
495
  base_branch=base_branch,
247
- last_fatal_error_code=classification.fatal_error_code or "context_overflow",
248
- continuation_summary_path=summary_path,
496
+ last_fatal_error_code=classification.fatal_error_code,
249
497
  no_progress_count=classification.no_progress_count,
250
498
  progress_fingerprint=classification.progress_fingerprint,
251
499
  )
252
500
  if not update.ok:
253
501
  raise RuntimeError(update.text)
254
- previous_fingerprint = classification.progress_fingerprint
255
- previous_no_progress_count = classification.no_progress_count
256
- continuation_pending = True
257
- previous_session_id = session_id
258
- continuation_count += 1
259
- context_overflow_count += 1
260
- continue
261
-
262
- 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}")
263
508
  if use_worktree and worktree_runtime is not None:
264
- if final_session_status == "success":
265
- merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
266
- if merge.ok:
267
- guarded_worktree_remove(worktree_runtime, delete_branch=True)
268
- else:
269
- final_session_status = "merge_conflict"
270
- guarded_worktree_remove(worktree_runtime, delete_branch=False)
271
- else:
272
- worktree_save_wip(worktree_runtime)
273
- guarded_worktree_remove(worktree_runtime, delete_branch=False)
509
+ worktree_save_wip(worktree_runtime)
510
+ guarded_worktree_remove(worktree_runtime, delete_branch=False)
274
511
  else:
275
- if final_session_status == "success":
276
- merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
277
- if not merge.ok:
278
- branch_ensure_return(paths.project_root, base_branch, branch_name)
279
- final_session_status = "merge_conflict"
280
- else:
281
- branch_ensure_return(paths.project_root, base_branch, branch_name)
282
-
283
- _write_session_status(
284
- session_paths,
285
- status=final_session_status,
286
- session_id=session_id,
287
- task_id=item_id,
288
- task_type=family.task_type,
289
- base_branch=base_branch,
290
- active_dev_branch=branch_name,
291
- worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
292
- reason=classification.reason,
293
- fatal_error_code=classification.fatal_error_code,
294
- )
295
-
296
- update = update_item(
297
- family,
298
- invocation,
299
- item_id,
300
- final_session_status,
301
- session_id,
302
- paths.project_root,
303
- active_dev_branch=branch_name,
304
- base_branch=base_branch,
305
- last_fatal_error_code=classification.fatal_error_code,
306
- no_progress_count=classification.no_progress_count,
307
- progress_fingerprint=classification.progress_fingerprint,
308
- )
309
- if not update.ok:
310
- raise RuntimeError(update.text)
311
- new_status = _status_from_update(update)
312
- commit_final_bookkeeping(paths.project_root, family, item_id, new_status)
313
- return new_status
314
-
512
+ branch_ensure_return(paths.project_root, base_branch, branch_name)
513
+ raise
315
514
 
316
515
 
317
516
  def _test_cli(kind: str, paths) -> CommandResult: