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.
- 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/runners.py +357 -158
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +98 -9
- 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 +100 -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
package/bundled/VERSION.json
CHANGED
|
@@ -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
|
-
| +-
|
|
682
|
-
| +-
|
|
683
|
-
| +-
|
|
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
|
|
700
|
-
| Tier 3 (full) | `bootstrap-tier3.md` |
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
-
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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:
|