prizmkit 1.1.105 → 1.1.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +83 -12
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +93 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +9 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +421 -61
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +102 -17
- package/bundled/dev-pipeline/scripts/continuation.py +0 -4
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +7 -39
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -31
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -31
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +28 -28
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +36 -52
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +34 -54
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -3
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +49 -2
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +17 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +50 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +560 -1
- package/bundled/dev-pipeline/tests/test_unified_cli.py +208 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +12 -13
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +10 -10
- package/package.json +1 -1
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
|
@@ -26,8 +26,13 @@ from .gitops import (
|
|
|
26
26
|
worktree_runtime_context,
|
|
27
27
|
worktree_save_wip,
|
|
28
28
|
)
|
|
29
|
-
from .runner_bookkeeping import
|
|
30
|
-
|
|
29
|
+
from .runner_bookkeeping import (
|
|
30
|
+
commit_final_bookkeeping,
|
|
31
|
+
commit_pre_branch_bookkeeping,
|
|
32
|
+
commit_task_branch_changes,
|
|
33
|
+
commit_wip_changes,
|
|
34
|
+
)
|
|
35
|
+
from .runner_classification import classify_session, has_branch_completion_evidence, has_completed_artifact_path
|
|
31
36
|
from .runner_models import RunnerEnvironment, RunnerFamily, RunnerInvocation, SessionPaths, family_for, parse_invocation
|
|
32
37
|
from .runner_prompts import PromptGenerationResult, generate_prompt
|
|
33
38
|
from .runner_recovery import run_recovery, run_recovery_detect
|
|
@@ -106,6 +111,14 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
106
111
|
return PipelineRunResult(False, processed, "get_next_failed", details=(next_result.text,))
|
|
107
112
|
marker = next_result.stdout.strip()
|
|
108
113
|
if marker == "PIPELINE_COMPLETE":
|
|
114
|
+
if _has_non_terminal_details(details):
|
|
115
|
+
return PipelineRunResult(
|
|
116
|
+
False,
|
|
117
|
+
processed,
|
|
118
|
+
"pipeline_complete_with_non_terminal_history",
|
|
119
|
+
_last_detail_status(details),
|
|
120
|
+
tuple(details),
|
|
121
|
+
)
|
|
109
122
|
return PipelineRunResult(True, processed, "pipeline_complete", details=tuple(details))
|
|
110
123
|
if marker == "PIPELINE_BLOCKED":
|
|
111
124
|
return PipelineRunResult(False, processed, "pipeline_blocked", details=tuple(details))
|
|
@@ -133,6 +146,37 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
133
146
|
return PipelineRunResult(False, processed, "item_failed", final_status, tuple(details))
|
|
134
147
|
|
|
135
148
|
|
|
149
|
+
_NON_TERMINAL_DETAIL_STATUSES = {
|
|
150
|
+
"pending",
|
|
151
|
+
"retryable",
|
|
152
|
+
"in_progress",
|
|
153
|
+
"infra_error",
|
|
154
|
+
"context_overflow",
|
|
155
|
+
"stalled_context_continuation",
|
|
156
|
+
"crashed",
|
|
157
|
+
"timed_out",
|
|
158
|
+
"failed",
|
|
159
|
+
"needs_info",
|
|
160
|
+
"merge_conflict",
|
|
161
|
+
"interrupted",
|
|
162
|
+
"unknown",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _detail_status(detail: str) -> str:
|
|
167
|
+
return detail.rsplit(":", 1)[-1] if ":" in detail else detail
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _last_detail_status(details: list[str]) -> str:
|
|
171
|
+
if not details:
|
|
172
|
+
return ""
|
|
173
|
+
return _detail_status(details[-1])
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _has_non_terminal_details(details: list[str]) -> bool:
|
|
177
|
+
return any(_detail_status(detail) in _NON_TERMINAL_DETAIL_STATUSES for detail in details)
|
|
178
|
+
|
|
179
|
+
|
|
136
180
|
_SEPARATOR = "─" * 52
|
|
137
181
|
|
|
138
182
|
|
|
@@ -180,7 +224,37 @@ def _is_recovery_side_effect_branch(branch: str, family: RunnerFamily, item_id:
|
|
|
180
224
|
|
|
181
225
|
|
|
182
226
|
def _is_current_task_branch(branch: str, family: RunnerFamily, item_id: str) -> bool:
|
|
183
|
-
|
|
227
|
+
if not branch or _is_recovery_side_effect_branch(branch, family, item_id):
|
|
228
|
+
return False
|
|
229
|
+
return branch == f"{family.branch_prefix}/{item_id}" or branch.startswith(f"{family.branch_prefix}/{item_id}-")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _with_recovery_metadata(family: RunnerFamily, item_id: str, metadata: dict[str, object]) -> dict[str, object]:
|
|
233
|
+
enriched = dict(metadata)
|
|
234
|
+
if enriched.get("active_dev_branch") and enriched.get("base_branch"):
|
|
235
|
+
return enriched
|
|
236
|
+
recovered = _latest_session_status(family.state_dir, item_id)
|
|
237
|
+
for key in ("active_dev_branch", "base_branch"):
|
|
238
|
+
if not enriched.get(key) and recovered.get(key):
|
|
239
|
+
enriched[key] = recovered[key]
|
|
240
|
+
return enriched
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _latest_session_status(state_dir: Path, item_id: str) -> dict[str, object]:
|
|
244
|
+
sessions_dir = state_dir / item_id / "sessions"
|
|
245
|
+
try:
|
|
246
|
+
sessions = sorted(child for child in sessions_dir.iterdir() if child.is_dir())
|
|
247
|
+
except OSError:
|
|
248
|
+
return {}
|
|
249
|
+
for session in reversed(sessions):
|
|
250
|
+
status_path = session / "session-status.json"
|
|
251
|
+
try:
|
|
252
|
+
data = json.loads(status_path.read_text(encoding="utf-8"))
|
|
253
|
+
except (OSError, json.JSONDecodeError):
|
|
254
|
+
continue
|
|
255
|
+
if isinstance(data, dict):
|
|
256
|
+
return data
|
|
257
|
+
return {}
|
|
184
258
|
|
|
185
259
|
|
|
186
260
|
def _resolve_base_branch(project_root: Path, metadata: dict[str, object], active_branch: str, family: RunnerFamily, item_id: str) -> str:
|
|
@@ -306,20 +380,15 @@ def _process_item(
|
|
|
306
380
|
raise RuntimeError("No supported AI CLI command was found")
|
|
307
381
|
|
|
308
382
|
active_branch = current_branch(paths.project_root)
|
|
309
|
-
|
|
310
|
-
|
|
383
|
+
metadata = _with_recovery_metadata(family, item_id, initial_metadata)
|
|
384
|
+
base_branch = _resolve_base_branch(paths.project_root, metadata, active_branch, family, item_id)
|
|
385
|
+
branch_name = _resolve_working_branch(family, item_id, metadata, env, active_branch)
|
|
311
386
|
branch_context = BranchContext(base_branch, branch_name, paths.project_root)
|
|
312
387
|
worktree_runtime = None
|
|
313
388
|
use_worktree = _use_worktree_for_family(env, family)
|
|
314
389
|
execution_root = paths.project_root
|
|
315
390
|
|
|
316
|
-
_emit_item_header(family, invocation, item_id, initial_metadata)
|
|
317
391
|
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
392
|
if use_worktree:
|
|
324
393
|
worktree_runtime = worktree_runtime_context(
|
|
325
394
|
branch_context,
|
|
@@ -332,29 +401,74 @@ def _process_item(
|
|
|
332
401
|
)
|
|
333
402
|
setup = ensure_linked_worktree(worktree_runtime)
|
|
334
403
|
if not setup.ok:
|
|
335
|
-
|
|
336
|
-
return _status_from_update(status)
|
|
404
|
+
return "infra_error"
|
|
337
405
|
worktree_runtime = setup.runtime or worktree_runtime
|
|
338
406
|
_emit_info(f"Dev branch: {branch_name}")
|
|
339
407
|
_emit_info(f"Worktree: {worktree_runtime.worktree_path}")
|
|
340
408
|
support_assets = materialize_worktree_support_assets(worktree_runtime)
|
|
341
409
|
if not support_assets.ok:
|
|
342
|
-
|
|
343
|
-
|
|
410
|
+
return _record_setup_failure(
|
|
411
|
+
family,
|
|
412
|
+
invocation,
|
|
413
|
+
item_id,
|
|
414
|
+
paths.project_root,
|
|
415
|
+
worktree_runtime.worktree_path,
|
|
416
|
+
base_branch,
|
|
417
|
+
branch_name,
|
|
418
|
+
worktree_runtime=worktree_runtime,
|
|
419
|
+
)
|
|
344
420
|
execution_root = worktree_runtime.worktree_path
|
|
345
421
|
else:
|
|
346
422
|
branch_result = run_git_plan(paths.project_root, branch_create_plan(branch_context))
|
|
347
423
|
_emit_branch_setup_result(branch_context, branch_result)
|
|
348
424
|
if not branch_result.ok:
|
|
349
|
-
|
|
350
|
-
|
|
425
|
+
return "infra_error"
|
|
426
|
+
|
|
427
|
+
_emit_item_header(family, invocation, item_id, metadata)
|
|
428
|
+
status_root = execution_root if use_worktree else paths.project_root
|
|
429
|
+
status_family = _family_for_execution_root(family, paths.project_root, status_root)
|
|
430
|
+
status_invocation = _invocation_for_execution_family(invocation, status_family)
|
|
431
|
+
recovered_session_id = _latest_session_id(family.state_dir, item_id)
|
|
432
|
+
recovery_artifact = _artifact_path_for_item(execution_root, family, item_id, metadata)
|
|
433
|
+
if recovered_session_id and has_completed_artifact_path(recovery_artifact) and has_branch_completion_evidence(execution_root, base_branch, branch_name, recovery_artifact):
|
|
434
|
+
session_paths = SessionPaths.for_item(family.state_dir, item_id, recovered_session_id)
|
|
435
|
+
_write_session_status(
|
|
436
|
+
session_paths,
|
|
437
|
+
status="success",
|
|
438
|
+
session_id=recovered_session_id,
|
|
439
|
+
task_id=item_id,
|
|
440
|
+
task_type=family.task_type,
|
|
441
|
+
base_branch=base_branch,
|
|
442
|
+
active_dev_branch=branch_name,
|
|
443
|
+
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
444
|
+
reason="completed_branch_recovered_from_late_runtime_error",
|
|
445
|
+
)
|
|
446
|
+
return _finalize_recovered_success(
|
|
447
|
+
family,
|
|
448
|
+
item_id,
|
|
449
|
+
paths,
|
|
450
|
+
status_family,
|
|
451
|
+
status_invocation,
|
|
452
|
+
status_root,
|
|
453
|
+
execution_root,
|
|
454
|
+
branch_context,
|
|
455
|
+
branch_name,
|
|
456
|
+
base_branch,
|
|
457
|
+
recovered_session_id,
|
|
458
|
+
session_paths,
|
|
459
|
+
worktree_runtime,
|
|
460
|
+
)
|
|
461
|
+
start = start_item(status_family, status_invocation, item_id, status_root)
|
|
462
|
+
if not start.ok:
|
|
463
|
+
raise RuntimeError(start.text)
|
|
464
|
+
commit_pre_branch_bookkeeping(status_root, status_family, item_id)
|
|
351
465
|
|
|
352
|
-
previous_fingerprint =
|
|
353
|
-
previous_no_progress_count = int(
|
|
354
|
-
continuation_pending = bool(
|
|
355
|
-
previous_session_id = str(
|
|
356
|
-
continuation_count = int(
|
|
357
|
-
context_overflow_count = int(
|
|
466
|
+
previous_fingerprint = metadata.get("last_progress_fingerprint")
|
|
467
|
+
previous_no_progress_count = int(metadata.get("no_progress_count") or 0)
|
|
468
|
+
continuation_pending = bool(metadata.get("continuation_pending"))
|
|
469
|
+
previous_session_id = str(metadata.get("previous_session_id") or metadata.get("last_context_overflow_session_id") or "")
|
|
470
|
+
continuation_count = int(metadata.get("continuation_count") or 0)
|
|
471
|
+
context_overflow_count = int(metadata.get("context_overflow_count") or 0)
|
|
358
472
|
|
|
359
473
|
while True:
|
|
360
474
|
session_id = _session_id(item_id)
|
|
@@ -368,7 +482,7 @@ def _process_item(
|
|
|
368
482
|
"task_type": family.task_type,
|
|
369
483
|
"active_dev_branch": branch_name,
|
|
370
484
|
"base_branch": base_branch,
|
|
371
|
-
"continuation_summary_path": _continuation_summary_path(
|
|
485
|
+
"continuation_summary_path": _continuation_summary_path(execution_root, family, item_id, None),
|
|
372
486
|
}
|
|
373
487
|
prompt = generate_prompt(
|
|
374
488
|
family,
|
|
@@ -417,14 +531,14 @@ def _process_item(
|
|
|
417
531
|
)
|
|
418
532
|
_emit_session_summary(session_result, session_paths, classification)
|
|
419
533
|
if classification.session_status == "context_overflow":
|
|
420
|
-
summary_path = _continuation_summary_path(
|
|
534
|
+
summary_path = _continuation_summary_path(execution_root, family, item_id, prompt)
|
|
421
535
|
update = update_item(
|
|
422
|
-
|
|
423
|
-
|
|
536
|
+
status_family,
|
|
537
|
+
status_invocation,
|
|
424
538
|
item_id,
|
|
425
539
|
"context_overflow",
|
|
426
540
|
session_id,
|
|
427
|
-
|
|
541
|
+
status_root,
|
|
428
542
|
active_dev_branch=branch_name,
|
|
429
543
|
base_branch=base_branch,
|
|
430
544
|
last_fatal_error_code=classification.fatal_error_code or "context_overflow",
|
|
@@ -444,32 +558,6 @@ def _process_item(
|
|
|
444
558
|
continue
|
|
445
559
|
|
|
446
560
|
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
561
|
|
|
474
562
|
_write_session_status(
|
|
475
563
|
session_paths,
|
|
@@ -485,12 +573,12 @@ def _process_item(
|
|
|
485
573
|
)
|
|
486
574
|
|
|
487
575
|
update = update_item(
|
|
488
|
-
|
|
489
|
-
|
|
576
|
+
status_family,
|
|
577
|
+
status_invocation,
|
|
490
578
|
item_id,
|
|
491
579
|
final_session_status,
|
|
492
580
|
session_id,
|
|
493
|
-
|
|
581
|
+
status_root,
|
|
494
582
|
active_dev_branch=branch_name,
|
|
495
583
|
base_branch=base_branch,
|
|
496
584
|
last_fatal_error_code=classification.fatal_error_code,
|
|
@@ -501,8 +589,32 @@ def _process_item(
|
|
|
501
589
|
raise RuntimeError(update.text)
|
|
502
590
|
_emit_update_summary(family, item_id, update)
|
|
503
591
|
new_status = _status_from_update(update)
|
|
504
|
-
|
|
505
|
-
|
|
592
|
+
if final_session_status != "success":
|
|
593
|
+
commit_final_bookkeeping(status_root, status_family, item_id, new_status)
|
|
594
|
+
_save_failure_wip(execution_root, family, item_id, new_status, worktree_runtime)
|
|
595
|
+
if use_worktree and worktree_runtime is not None:
|
|
596
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
597
|
+
else:
|
|
598
|
+
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
599
|
+
return new_status
|
|
600
|
+
|
|
601
|
+
return _complete_success_merge(
|
|
602
|
+
family,
|
|
603
|
+
item_id,
|
|
604
|
+
new_status,
|
|
605
|
+
paths,
|
|
606
|
+
status_family,
|
|
607
|
+
status_invocation,
|
|
608
|
+
status_root,
|
|
609
|
+
execution_root,
|
|
610
|
+
branch_context,
|
|
611
|
+
branch_name,
|
|
612
|
+
base_branch,
|
|
613
|
+
session_id,
|
|
614
|
+
session_paths,
|
|
615
|
+
worktree_runtime,
|
|
616
|
+
classification_fatal_error_code=classification.fatal_error_code,
|
|
617
|
+
)
|
|
506
618
|
except KeyboardInterrupt:
|
|
507
619
|
_emit_warning(f"Interrupted while processing {item_id}; preserving work and returning to {base_branch}")
|
|
508
620
|
if use_worktree and worktree_runtime is not None:
|
|
@@ -513,6 +625,172 @@ def _process_item(
|
|
|
513
625
|
raise
|
|
514
626
|
|
|
515
627
|
|
|
628
|
+
def _finalize_recovered_success(
|
|
629
|
+
family: RunnerFamily,
|
|
630
|
+
item_id: str,
|
|
631
|
+
paths,
|
|
632
|
+
status_family: RunnerFamily,
|
|
633
|
+
status_invocation: RunnerInvocation,
|
|
634
|
+
status_root: Path,
|
|
635
|
+
execution_root: Path,
|
|
636
|
+
branch_context: BranchContext,
|
|
637
|
+
branch_name: str,
|
|
638
|
+
base_branch: str,
|
|
639
|
+
session_id: str,
|
|
640
|
+
session_paths: SessionPaths,
|
|
641
|
+
worktree_runtime,
|
|
642
|
+
) -> str:
|
|
643
|
+
update = update_item(
|
|
644
|
+
status_family,
|
|
645
|
+
status_invocation,
|
|
646
|
+
item_id,
|
|
647
|
+
"success",
|
|
648
|
+
session_id,
|
|
649
|
+
status_root,
|
|
650
|
+
active_dev_branch=branch_name,
|
|
651
|
+
base_branch=base_branch,
|
|
652
|
+
)
|
|
653
|
+
if not update.ok:
|
|
654
|
+
raise RuntimeError(update.text)
|
|
655
|
+
_emit_update_summary(family, item_id, update)
|
|
656
|
+
return _complete_success_merge(
|
|
657
|
+
family,
|
|
658
|
+
item_id,
|
|
659
|
+
_status_from_update(update),
|
|
660
|
+
paths,
|
|
661
|
+
status_family,
|
|
662
|
+
status_invocation,
|
|
663
|
+
status_root,
|
|
664
|
+
execution_root,
|
|
665
|
+
branch_context,
|
|
666
|
+
branch_name,
|
|
667
|
+
base_branch,
|
|
668
|
+
session_id,
|
|
669
|
+
session_paths,
|
|
670
|
+
worktree_runtime,
|
|
671
|
+
classification_fatal_error_code="",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _complete_success_merge(
|
|
676
|
+
family: RunnerFamily,
|
|
677
|
+
item_id: str,
|
|
678
|
+
new_status: str,
|
|
679
|
+
paths,
|
|
680
|
+
status_family: RunnerFamily,
|
|
681
|
+
status_invocation: RunnerInvocation,
|
|
682
|
+
status_root: Path,
|
|
683
|
+
execution_root: Path,
|
|
684
|
+
branch_context: BranchContext,
|
|
685
|
+
branch_name: str,
|
|
686
|
+
base_branch: str,
|
|
687
|
+
session_id: str,
|
|
688
|
+
session_paths: SessionPaths,
|
|
689
|
+
worktree_runtime,
|
|
690
|
+
*,
|
|
691
|
+
classification_fatal_error_code: str = "",
|
|
692
|
+
) -> str:
|
|
693
|
+
env = RunnerEnvironment.from_env()
|
|
694
|
+
commit_final_bookkeeping(status_root, status_family, item_id, new_status)
|
|
695
|
+
commit_task_branch_changes(execution_root, family, item_id, new_status)
|
|
696
|
+
if worktree_runtime is not None:
|
|
697
|
+
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
698
|
+
merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
|
|
699
|
+
_emit_git_plan_output(merge)
|
|
700
|
+
if merge.ok:
|
|
701
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=True)
|
|
702
|
+
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
703
|
+
return new_status
|
|
704
|
+
else:
|
|
705
|
+
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
706
|
+
merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
|
|
707
|
+
_emit_git_plan_output(merge)
|
|
708
|
+
if merge.ok:
|
|
709
|
+
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
710
|
+
return new_status
|
|
711
|
+
|
|
712
|
+
_write_session_status(
|
|
713
|
+
session_paths,
|
|
714
|
+
status="merge_conflict",
|
|
715
|
+
session_id=session_id,
|
|
716
|
+
task_id=item_id,
|
|
717
|
+
task_type=family.task_type,
|
|
718
|
+
base_branch=base_branch,
|
|
719
|
+
active_dev_branch=branch_name,
|
|
720
|
+
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
721
|
+
reason="merge_conflict",
|
|
722
|
+
fatal_error_code=classification_fatal_error_code,
|
|
723
|
+
)
|
|
724
|
+
update = update_item(
|
|
725
|
+
status_family,
|
|
726
|
+
status_invocation,
|
|
727
|
+
item_id,
|
|
728
|
+
"merge_conflict",
|
|
729
|
+
session_id,
|
|
730
|
+
status_root,
|
|
731
|
+
active_dev_branch=branch_name,
|
|
732
|
+
base_branch=base_branch,
|
|
733
|
+
last_fatal_error_code=classification_fatal_error_code,
|
|
734
|
+
)
|
|
735
|
+
if not update.ok:
|
|
736
|
+
raise RuntimeError(update.text)
|
|
737
|
+
_emit_update_summary(family, item_id, update)
|
|
738
|
+
merged_status = _status_from_update(update)
|
|
739
|
+
commit_final_bookkeeping(status_root, status_family, item_id, merged_status)
|
|
740
|
+
_save_failure_wip(execution_root, family, item_id, merged_status, worktree_runtime)
|
|
741
|
+
if worktree_runtime is not None:
|
|
742
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
743
|
+
else:
|
|
744
|
+
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
745
|
+
return merged_status
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _save_failure_wip(execution_root: Path, family: RunnerFamily, item_id: str, status: str, worktree_runtime) -> None:
|
|
749
|
+
if worktree_runtime is not None:
|
|
750
|
+
worktree_save_wip(worktree_runtime)
|
|
751
|
+
else:
|
|
752
|
+
commit_wip_changes(execution_root, family, item_id, status)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _record_setup_failure(
|
|
756
|
+
family: RunnerFamily,
|
|
757
|
+
invocation: RunnerInvocation,
|
|
758
|
+
item_id: str,
|
|
759
|
+
project_root: Path,
|
|
760
|
+
status_root: Path,
|
|
761
|
+
base_branch: str,
|
|
762
|
+
branch_name: str,
|
|
763
|
+
*,
|
|
764
|
+
worktree_runtime=None,
|
|
765
|
+
) -> str:
|
|
766
|
+
"""Record setup-time infra failures on a task branch when one exists."""
|
|
767
|
+
if status_root.resolve() == project_root.resolve() and current_branch(project_root) != branch_name:
|
|
768
|
+
return "infra_error"
|
|
769
|
+
status_family = _family_for_execution_root(family, project_root, status_root)
|
|
770
|
+
status_invocation = _invocation_for_execution_family(invocation, status_family)
|
|
771
|
+
start = start_item(status_family, status_invocation, item_id, status_root)
|
|
772
|
+
if start.ok:
|
|
773
|
+
commit_pre_branch_bookkeeping(status_root, status_family, item_id)
|
|
774
|
+
update = update_item(
|
|
775
|
+
status_family,
|
|
776
|
+
status_invocation,
|
|
777
|
+
item_id,
|
|
778
|
+
"infra_error",
|
|
779
|
+
"",
|
|
780
|
+
status_root,
|
|
781
|
+
active_dev_branch=branch_name,
|
|
782
|
+
base_branch=base_branch,
|
|
783
|
+
)
|
|
784
|
+
new_status = _status_from_update(update) if update.ok else "infra_error"
|
|
785
|
+
if update.ok:
|
|
786
|
+
commit_final_bookkeeping(status_root, status_family, item_id, new_status)
|
|
787
|
+
_save_failure_wip(status_root, family, item_id, new_status, worktree_runtime)
|
|
788
|
+
if worktree_runtime is not None:
|
|
789
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
790
|
+
elif current_branch(project_root) == branch_name:
|
|
791
|
+
branch_ensure_return(project_root, base_branch, branch_name)
|
|
792
|
+
return new_status
|
|
793
|
+
|
|
516
794
|
def _test_cli(kind: str, paths) -> CommandResult:
|
|
517
795
|
"""Report the AI CLI resolution used by Python runner commands."""
|
|
518
796
|
config = load_runtime_config(paths)
|
|
@@ -539,6 +817,89 @@ def _use_worktree_for_family(env: RunnerEnvironment, family: RunnerFamily) -> bo
|
|
|
539
817
|
return env.use_worktree and family.kind in {"feature", "bugfix"}
|
|
540
818
|
|
|
541
819
|
|
|
820
|
+
def _family_for_execution_root(family: RunnerFamily, project_root: Path, execution_root: Path) -> RunnerFamily:
|
|
821
|
+
"""Return family paths rooted in the task execution checkout."""
|
|
822
|
+
if execution_root.resolve() == project_root.resolve():
|
|
823
|
+
return family
|
|
824
|
+
return RunnerFamily(
|
|
825
|
+
kind=family.kind,
|
|
826
|
+
id_prefix=family.id_prefix,
|
|
827
|
+
list_key=family.list_key,
|
|
828
|
+
list_arg=family.list_arg,
|
|
829
|
+
item_id_arg=family.item_id_arg,
|
|
830
|
+
state_dir=_path_in_execution_root(family.state_dir, project_root, execution_root),
|
|
831
|
+
plan_path=_path_in_execution_root(family.plan_path, project_root, execution_root),
|
|
832
|
+
updater_script=family.updater_script,
|
|
833
|
+
prompt_script=family.prompt_script,
|
|
834
|
+
branch_prefix=family.branch_prefix,
|
|
835
|
+
artifact_root_name=family.artifact_root_name,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _invocation_for_execution_family(invocation: RunnerInvocation, family: RunnerFamily) -> RunnerInvocation:
|
|
840
|
+
return RunnerInvocation(
|
|
841
|
+
action=invocation.action,
|
|
842
|
+
list_path=family.plan_path,
|
|
843
|
+
item_id=invocation.item_id,
|
|
844
|
+
item_filter=invocation.item_filter,
|
|
845
|
+
max_retries=invocation.max_retries,
|
|
846
|
+
max_infra_retries=invocation.max_infra_retries,
|
|
847
|
+
mode=invocation.mode,
|
|
848
|
+
critic=invocation.critic,
|
|
849
|
+
dry_run=invocation.dry_run,
|
|
850
|
+
clean=invocation.clean,
|
|
851
|
+
no_reset=invocation.no_reset,
|
|
852
|
+
resume_phase=invocation.resume_phase,
|
|
853
|
+
help_requested=invocation.help_requested,
|
|
854
|
+
unknown_args=invocation.unknown_args,
|
|
855
|
+
raw_args=invocation.raw_args,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _path_in_execution_root(path: Path, project_root: Path, execution_root: Path) -> Path:
|
|
860
|
+
try:
|
|
861
|
+
return execution_root / path.resolve().relative_to(project_root.resolve())
|
|
862
|
+
except ValueError:
|
|
863
|
+
return path
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _latest_session_id(state_dir: Path, item_id: str) -> str:
|
|
867
|
+
sessions_dir = state_dir / item_id / "sessions"
|
|
868
|
+
try:
|
|
869
|
+
sessions = [child.name for child in sessions_dir.iterdir() if child.is_dir()]
|
|
870
|
+
except OSError:
|
|
871
|
+
return ""
|
|
872
|
+
return sorted(sessions)[-1] if sessions else ""
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _artifact_path_for_item(project_root: Path, family: RunnerFamily, item_id: str, metadata: dict[str, object]) -> Path:
|
|
876
|
+
if family.kind == "feature":
|
|
877
|
+
title = str(metadata.get("title") or "")
|
|
878
|
+
if title:
|
|
879
|
+
return project_root / ".prizmkit" / "specs" / _feature_slug(item_id, title)
|
|
880
|
+
specs = project_root / ".prizmkit" / "specs"
|
|
881
|
+
numeric = item_id.replace("F-", "").replace("f-", "").zfill(3)
|
|
882
|
+
try:
|
|
883
|
+
matches = sorted(path for path in specs.iterdir() if path.is_dir() and path.name.startswith(f"{numeric}-"))
|
|
884
|
+
except OSError:
|
|
885
|
+
matches = []
|
|
886
|
+
if matches:
|
|
887
|
+
return matches[0]
|
|
888
|
+
return project_root / ".prizmkit" / family.artifact_root_name / item_id
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _feature_slug(feature_id: str, title: str) -> str:
|
|
892
|
+
import re
|
|
893
|
+
|
|
894
|
+
numeric = feature_id.replace("F-", "").replace("f-", "").zfill(3)
|
|
895
|
+
cleaned = re.sub(r"[^a-z0-9\s-]", "", title.lower())
|
|
896
|
+
cleaned = re.sub(r"[\s]+", "-", cleaned.strip())
|
|
897
|
+
cleaned = re.sub(r"-+", "-", cleaned).strip("-")
|
|
898
|
+
if not cleaned:
|
|
899
|
+
cleaned = "feature"
|
|
900
|
+
return f"{numeric}-{cleaned}"
|
|
901
|
+
|
|
902
|
+
|
|
542
903
|
def _write_session_status(
|
|
543
904
|
session_paths: SessionPaths,
|
|
544
905
|
*,
|
|
@@ -579,8 +940,7 @@ def _status_from_update(update_result) -> str:
|
|
|
579
940
|
|
|
580
941
|
|
|
581
942
|
def _default_branch_name(family: RunnerFamily, item_id: str) -> str:
|
|
582
|
-
|
|
583
|
-
return f"{family.branch_prefix}/{item_id}-{stamp}"
|
|
943
|
+
return f"{family.branch_prefix}/{item_id}-{time.strftime('%Y%m%d%H%M%S')}"
|
|
584
944
|
|
|
585
945
|
|
|
586
946
|
def _session_id(item_id: str) -> str:
|