prizmkit 1.1.106 → 1.1.108
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/bin/create-prizmkit.js +4 -0
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +81 -14
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +19 -8
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/launch_profiles.py +18 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +22 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +56 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +11 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +298 -63
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +243 -37
- package/bundled/dev-pipeline/scripts/continuation.py +0 -5
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +10 -82
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +7 -64
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +40 -63
- package/bundled/dev-pipeline/scripts/utils.py +123 -0
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +4 -3
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +5 -6
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +36 -35
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +53 -80
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +43 -78
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +5 -17
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +6 -18
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +14 -48
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +7 -14
- package/bundled/dev-pipeline/templates/sections/feature-context.md +0 -4
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -14
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +0 -2
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +6 -8
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +3 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +4 -16
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +4 -17
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -8
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +15 -16
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -10
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +3 -10
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +0 -7
- package/bundled/dev-pipeline/templates/sections/phase0-test-baseline.md +2 -15
- package/bundled/dev-pipeline/tests/conftest.py +0 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +395 -21
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +50 -14
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +154 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +431 -3
- package/bundled/dev-pipeline/tests/test_unified_cli.py +857 -5
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/app-planner/SKILL.md +1 -1
- package/bundled/skills/app-planner/references/architecture-decisions.md +1 -1
- package/bundled/skills/bug-fix-workflow/SKILL.md +174 -128
- package/bundled/skills/bug-planner/SKILL.md +101 -15
- package/bundled/skills/bug-planner/references/severity-rules.md +20 -8
- package/bundled/skills/feature-planner/SKILL.md +87 -8
- package/bundled/skills/feature-planner/assets/planning-guide.md +34 -10
- package/bundled/skills/feature-workflow/SKILL.md +100 -81
- package/bundled/skills/{prizm-kit → prizmkit}/SKILL.md +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/bundled/skills/refactor-planner/SKILL.md +88 -7
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -1
- package/bundled/skills/refactor-planner/references/planning-phases.md +29 -11
- package/bundled/skills/refactor-workflow/SKILL.md +116 -57
- package/bundled/templates/project-memory-template.md +1 -1
- package/package.json +1 -1
- package/src/ai-cli-launch.js +194 -0
- package/src/config.js +26 -14
- package/src/index.js +11 -44
- package/src/platforms.js +10 -3
- package/src/prompts.js +60 -2
- package/src/scaffold.js +26 -14
- package/bundled/dev-pipeline/scripts/prizmkit-test-gate.py +0 -446
- 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/dev-pipeline/templates/sections/log-size-awareness.md +0 -20
- package/bundled/dev-pipeline/tests/test_prizmkit_test_gate.py +0 -107
- package/bundled/skills/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
|
@@ -29,10 +29,11 @@ from .gitops import (
|
|
|
29
29
|
from .runner_bookkeeping import (
|
|
30
30
|
commit_final_bookkeeping,
|
|
31
31
|
commit_pre_branch_bookkeeping,
|
|
32
|
+
commit_preflight_ready_changes,
|
|
32
33
|
commit_task_branch_changes,
|
|
33
34
|
commit_wip_changes,
|
|
34
35
|
)
|
|
35
|
-
from .runner_classification import classify_session
|
|
36
|
+
from .runner_classification import classify_session, has_branch_completion_evidence, has_completed_artifact_path
|
|
36
37
|
from .runner_models import RunnerEnvironment, RunnerFamily, RunnerInvocation, SessionPaths, family_for, parse_invocation
|
|
37
38
|
from .runner_prompts import PromptGenerationResult, generate_prompt
|
|
38
39
|
from .runner_recovery import run_recovery, run_recovery_detect
|
|
@@ -111,6 +112,14 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
111
112
|
return PipelineRunResult(False, processed, "get_next_failed", details=(next_result.text,))
|
|
112
113
|
marker = next_result.stdout.strip()
|
|
113
114
|
if marker == "PIPELINE_COMPLETE":
|
|
115
|
+
if _has_non_terminal_details(details):
|
|
116
|
+
return PipelineRunResult(
|
|
117
|
+
False,
|
|
118
|
+
processed,
|
|
119
|
+
"pipeline_complete_with_non_terminal_history",
|
|
120
|
+
_last_detail_status(details),
|
|
121
|
+
tuple(details),
|
|
122
|
+
)
|
|
114
123
|
return PipelineRunResult(True, processed, "pipeline_complete", details=tuple(details))
|
|
115
124
|
if marker == "PIPELINE_BLOCKED":
|
|
116
125
|
return PipelineRunResult(False, processed, "pipeline_blocked", details=tuple(details))
|
|
@@ -138,6 +147,37 @@ def _run_pipeline(family: RunnerFamily, invocation: RunnerInvocation, paths) ->
|
|
|
138
147
|
return PipelineRunResult(False, processed, "item_failed", final_status, tuple(details))
|
|
139
148
|
|
|
140
149
|
|
|
150
|
+
_NON_TERMINAL_DETAIL_STATUSES = {
|
|
151
|
+
"pending",
|
|
152
|
+
"retryable",
|
|
153
|
+
"in_progress",
|
|
154
|
+
"infra_error",
|
|
155
|
+
"context_overflow",
|
|
156
|
+
"stalled_context_continuation",
|
|
157
|
+
"crashed",
|
|
158
|
+
"timed_out",
|
|
159
|
+
"failed",
|
|
160
|
+
"needs_info",
|
|
161
|
+
"merge_conflict",
|
|
162
|
+
"interrupted",
|
|
163
|
+
"unknown",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _detail_status(detail: str) -> str:
|
|
168
|
+
return detail.rsplit(":", 1)[-1] if ":" in detail else detail
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _last_detail_status(details: list[str]) -> str:
|
|
172
|
+
if not details:
|
|
173
|
+
return ""
|
|
174
|
+
return _detail_status(details[-1])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _has_non_terminal_details(details: list[str]) -> bool:
|
|
178
|
+
return any(_detail_status(detail) in _NON_TERMINAL_DETAIL_STATUSES for detail in details)
|
|
179
|
+
|
|
180
|
+
|
|
141
181
|
_SEPARATOR = "─" * 52
|
|
142
182
|
|
|
143
183
|
|
|
@@ -190,6 +230,34 @@ def _is_current_task_branch(branch: str, family: RunnerFamily, item_id: str) ->
|
|
|
190
230
|
return branch == f"{family.branch_prefix}/{item_id}" or branch.startswith(f"{family.branch_prefix}/{item_id}-")
|
|
191
231
|
|
|
192
232
|
|
|
233
|
+
def _with_recovery_metadata(family: RunnerFamily, item_id: str, metadata: dict[str, object]) -> dict[str, object]:
|
|
234
|
+
enriched = dict(metadata)
|
|
235
|
+
if enriched.get("active_dev_branch") and enriched.get("base_branch"):
|
|
236
|
+
return enriched
|
|
237
|
+
recovered = _latest_session_status(family.state_dir, item_id)
|
|
238
|
+
for key in ("active_dev_branch", "base_branch"):
|
|
239
|
+
if not enriched.get(key) and recovered.get(key):
|
|
240
|
+
enriched[key] = recovered[key]
|
|
241
|
+
return enriched
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _latest_session_status(state_dir: Path, item_id: str) -> dict[str, object]:
|
|
245
|
+
sessions_dir = state_dir / item_id / "sessions"
|
|
246
|
+
try:
|
|
247
|
+
sessions = sorted(child for child in sessions_dir.iterdir() if child.is_dir())
|
|
248
|
+
except OSError:
|
|
249
|
+
return {}
|
|
250
|
+
for session in reversed(sessions):
|
|
251
|
+
status_path = session / "session-status.json"
|
|
252
|
+
try:
|
|
253
|
+
data = json.loads(status_path.read_text(encoding="utf-8"))
|
|
254
|
+
except (OSError, json.JSONDecodeError):
|
|
255
|
+
continue
|
|
256
|
+
if isinstance(data, dict):
|
|
257
|
+
return data
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
|
|
193
261
|
def _resolve_base_branch(project_root: Path, metadata: dict[str, object], active_branch: str, family: RunnerFamily, item_id: str) -> str:
|
|
194
262
|
recorded = str(metadata.get("base_branch") or "").strip()
|
|
195
263
|
if recorded:
|
|
@@ -313,14 +381,29 @@ def _process_item(
|
|
|
313
381
|
raise RuntimeError("No supported AI CLI command was found")
|
|
314
382
|
|
|
315
383
|
active_branch = current_branch(paths.project_root)
|
|
316
|
-
|
|
317
|
-
|
|
384
|
+
metadata = _with_recovery_metadata(family, item_id, initial_metadata)
|
|
385
|
+
base_branch = _resolve_base_branch(paths.project_root, metadata, active_branch, family, item_id)
|
|
386
|
+
branch_name = _resolve_working_branch(family, item_id, metadata, env, active_branch)
|
|
318
387
|
branch_context = BranchContext(base_branch, branch_name, paths.project_root)
|
|
319
388
|
worktree_runtime = None
|
|
320
389
|
use_worktree = _use_worktree_for_family(env, family)
|
|
321
390
|
execution_root = paths.project_root
|
|
322
391
|
|
|
323
392
|
try:
|
|
393
|
+
preflight = commit_preflight_ready_changes(paths.project_root, item_id)
|
|
394
|
+
if preflight.committed:
|
|
395
|
+
_emit_info(f"Preflight: preserved existing user-visible changes before {family.kind} task {item_id}")
|
|
396
|
+
elif preflight.attempted:
|
|
397
|
+
detail = ""
|
|
398
|
+
if preflight.result is not None:
|
|
399
|
+
detail = (preflight.result.stderr or preflight.result.stdout).strip()
|
|
400
|
+
_emit_warning(f"Preflight: failed to preserve existing changes before {family.kind} task {item_id}")
|
|
401
|
+
if detail:
|
|
402
|
+
_emit_warning(detail.splitlines()[0][:500])
|
|
403
|
+
raise RuntimeError(f"Preflight dirty workspace preservation failed before {family.kind} task {item_id}")
|
|
404
|
+
else:
|
|
405
|
+
_emit_info(f"Preflight: no user-visible changes to preserve before {family.kind} task {item_id}")
|
|
406
|
+
|
|
324
407
|
if use_worktree:
|
|
325
408
|
worktree_runtime = worktree_runtime_context(
|
|
326
409
|
branch_context,
|
|
@@ -356,21 +439,51 @@ def _process_item(
|
|
|
356
439
|
if not branch_result.ok:
|
|
357
440
|
return "infra_error"
|
|
358
441
|
|
|
359
|
-
_emit_item_header(family, invocation, item_id,
|
|
442
|
+
_emit_item_header(family, invocation, item_id, metadata)
|
|
360
443
|
status_root = execution_root if use_worktree else paths.project_root
|
|
361
444
|
status_family = _family_for_execution_root(family, paths.project_root, status_root)
|
|
362
445
|
status_invocation = _invocation_for_execution_family(invocation, status_family)
|
|
446
|
+
recovered_session_id = _latest_session_id(family.state_dir, item_id)
|
|
447
|
+
recovery_artifact = _artifact_path_for_item(execution_root, family, item_id, metadata)
|
|
448
|
+
if recovered_session_id and has_completed_artifact_path(recovery_artifact) and has_branch_completion_evidence(execution_root, base_branch, branch_name, recovery_artifact):
|
|
449
|
+
session_paths = SessionPaths.for_item(family.state_dir, item_id, recovered_session_id)
|
|
450
|
+
_write_session_status(
|
|
451
|
+
session_paths,
|
|
452
|
+
status="success",
|
|
453
|
+
session_id=recovered_session_id,
|
|
454
|
+
task_id=item_id,
|
|
455
|
+
task_type=family.task_type,
|
|
456
|
+
base_branch=base_branch,
|
|
457
|
+
active_dev_branch=branch_name,
|
|
458
|
+
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
459
|
+
reason="completed_branch_recovered_from_late_runtime_error",
|
|
460
|
+
)
|
|
461
|
+
return _finalize_recovered_success(
|
|
462
|
+
family,
|
|
463
|
+
item_id,
|
|
464
|
+
paths,
|
|
465
|
+
status_family,
|
|
466
|
+
status_invocation,
|
|
467
|
+
status_root,
|
|
468
|
+
execution_root,
|
|
469
|
+
branch_context,
|
|
470
|
+
branch_name,
|
|
471
|
+
base_branch,
|
|
472
|
+
recovered_session_id,
|
|
473
|
+
session_paths,
|
|
474
|
+
worktree_runtime,
|
|
475
|
+
)
|
|
363
476
|
start = start_item(status_family, status_invocation, item_id, status_root)
|
|
364
477
|
if not start.ok:
|
|
365
478
|
raise RuntimeError(start.text)
|
|
366
479
|
commit_pre_branch_bookkeeping(status_root, status_family, item_id)
|
|
367
480
|
|
|
368
|
-
previous_fingerprint =
|
|
369
|
-
previous_no_progress_count = int(
|
|
370
|
-
continuation_pending = bool(
|
|
371
|
-
previous_session_id = str(
|
|
372
|
-
continuation_count = int(
|
|
373
|
-
context_overflow_count = int(
|
|
481
|
+
previous_fingerprint = metadata.get("last_progress_fingerprint")
|
|
482
|
+
previous_no_progress_count = int(metadata.get("no_progress_count") or 0)
|
|
483
|
+
continuation_pending = bool(metadata.get("continuation_pending"))
|
|
484
|
+
previous_session_id = str(metadata.get("previous_session_id") or metadata.get("last_context_overflow_session_id") or "")
|
|
485
|
+
continuation_count = int(metadata.get("continuation_count") or 0)
|
|
486
|
+
context_overflow_count = int(metadata.get("context_overflow_count") or 0)
|
|
374
487
|
|
|
375
488
|
while True:
|
|
376
489
|
session_id = _session_id(item_id)
|
|
@@ -401,7 +514,9 @@ def _process_item(
|
|
|
401
514
|
if invocation.dry_run:
|
|
402
515
|
return "dry_run"
|
|
403
516
|
_emit_prompt_summary(prompt, session_id)
|
|
404
|
-
stream_json =
|
|
517
|
+
stream_json = False
|
|
518
|
+
if getattr(config.ai_client, "launch_profile", None) is None:
|
|
519
|
+
stream_json = detect_stream_json_support(config.ai_client.command, config.ai_client.platform).enabled
|
|
405
520
|
base_head = run_git_command(execution_root, ("rev-parse", "HEAD")).stdout.strip()
|
|
406
521
|
session_result = AISessionLauncher(
|
|
407
522
|
AISessionConfig(
|
|
@@ -417,7 +532,9 @@ def _process_item(
|
|
|
417
532
|
verbose=env.verbose,
|
|
418
533
|
live_output=env.live_output,
|
|
419
534
|
use_stream_json=stream_json,
|
|
535
|
+
launch_profile=getattr(config.ai_client, "launch_profile", None),
|
|
420
536
|
stale_kill_threshold_seconds=env.stale_kill_threshold_seconds,
|
|
537
|
+
heartbeat_stale_threshold_seconds=env.heartbeat_stale_threshold_seconds,
|
|
421
538
|
live_progress_interval_seconds=env.heartbeat_interval_seconds,
|
|
422
539
|
live_banner=False,
|
|
423
540
|
suppress_stream_output=True,
|
|
@@ -500,62 +617,23 @@ def _process_item(
|
|
|
500
617
|
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
501
618
|
return new_status
|
|
502
619
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
_emit_git_plan_output(merge)
|
|
509
|
-
if merge.ok:
|
|
510
|
-
guarded_worktree_remove(worktree_runtime, delete_branch=True)
|
|
511
|
-
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
512
|
-
return new_status
|
|
513
|
-
final_session_status = "merge_conflict"
|
|
514
|
-
else:
|
|
515
|
-
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
516
|
-
merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
|
|
517
|
-
_emit_git_plan_output(merge)
|
|
518
|
-
if merge.ok:
|
|
519
|
-
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
520
|
-
return new_status
|
|
521
|
-
final_session_status = "merge_conflict"
|
|
522
|
-
|
|
523
|
-
_write_session_status(
|
|
524
|
-
session_paths,
|
|
525
|
-
status=final_session_status,
|
|
526
|
-
session_id=session_id,
|
|
527
|
-
task_id=item_id,
|
|
528
|
-
task_type=family.task_type,
|
|
529
|
-
base_branch=base_branch,
|
|
530
|
-
active_dev_branch=branch_name,
|
|
531
|
-
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
532
|
-
reason="merge_conflict",
|
|
533
|
-
fatal_error_code=classification.fatal_error_code,
|
|
534
|
-
)
|
|
535
|
-
update = update_item(
|
|
620
|
+
return _complete_success_merge(
|
|
621
|
+
family,
|
|
622
|
+
item_id,
|
|
623
|
+
new_status,
|
|
624
|
+
paths,
|
|
536
625
|
status_family,
|
|
537
626
|
status_invocation,
|
|
538
|
-
item_id,
|
|
539
|
-
final_session_status,
|
|
540
|
-
session_id,
|
|
541
627
|
status_root,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
628
|
+
execution_root,
|
|
629
|
+
branch_context,
|
|
630
|
+
branch_name,
|
|
631
|
+
base_branch,
|
|
632
|
+
session_id,
|
|
633
|
+
session_paths,
|
|
634
|
+
worktree_runtime,
|
|
635
|
+
classification_fatal_error_code=classification.fatal_error_code,
|
|
547
636
|
)
|
|
548
|
-
if not update.ok:
|
|
549
|
-
raise RuntimeError(update.text)
|
|
550
|
-
_emit_update_summary(family, item_id, update)
|
|
551
|
-
new_status = _status_from_update(update)
|
|
552
|
-
commit_final_bookkeeping(status_root, status_family, item_id, new_status)
|
|
553
|
-
_save_failure_wip(execution_root, family, item_id, new_status, worktree_runtime)
|
|
554
|
-
if use_worktree and worktree_runtime is not None:
|
|
555
|
-
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
556
|
-
else:
|
|
557
|
-
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
558
|
-
return new_status
|
|
559
637
|
except KeyboardInterrupt:
|
|
560
638
|
_emit_warning(f"Interrupted while processing {item_id}; preserving work and returning to {base_branch}")
|
|
561
639
|
if use_worktree and worktree_runtime is not None:
|
|
@@ -566,6 +644,126 @@ def _process_item(
|
|
|
566
644
|
raise
|
|
567
645
|
|
|
568
646
|
|
|
647
|
+
def _finalize_recovered_success(
|
|
648
|
+
family: RunnerFamily,
|
|
649
|
+
item_id: str,
|
|
650
|
+
paths,
|
|
651
|
+
status_family: RunnerFamily,
|
|
652
|
+
status_invocation: RunnerInvocation,
|
|
653
|
+
status_root: Path,
|
|
654
|
+
execution_root: Path,
|
|
655
|
+
branch_context: BranchContext,
|
|
656
|
+
branch_name: str,
|
|
657
|
+
base_branch: str,
|
|
658
|
+
session_id: str,
|
|
659
|
+
session_paths: SessionPaths,
|
|
660
|
+
worktree_runtime,
|
|
661
|
+
) -> str:
|
|
662
|
+
update = update_item(
|
|
663
|
+
status_family,
|
|
664
|
+
status_invocation,
|
|
665
|
+
item_id,
|
|
666
|
+
"success",
|
|
667
|
+
session_id,
|
|
668
|
+
status_root,
|
|
669
|
+
active_dev_branch=branch_name,
|
|
670
|
+
base_branch=base_branch,
|
|
671
|
+
)
|
|
672
|
+
if not update.ok:
|
|
673
|
+
raise RuntimeError(update.text)
|
|
674
|
+
_emit_update_summary(family, item_id, update)
|
|
675
|
+
return _complete_success_merge(
|
|
676
|
+
family,
|
|
677
|
+
item_id,
|
|
678
|
+
_status_from_update(update),
|
|
679
|
+
paths,
|
|
680
|
+
status_family,
|
|
681
|
+
status_invocation,
|
|
682
|
+
status_root,
|
|
683
|
+
execution_root,
|
|
684
|
+
branch_context,
|
|
685
|
+
branch_name,
|
|
686
|
+
base_branch,
|
|
687
|
+
session_id,
|
|
688
|
+
session_paths,
|
|
689
|
+
worktree_runtime,
|
|
690
|
+
classification_fatal_error_code="",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _complete_success_merge(
|
|
695
|
+
family: RunnerFamily,
|
|
696
|
+
item_id: str,
|
|
697
|
+
new_status: str,
|
|
698
|
+
paths,
|
|
699
|
+
status_family: RunnerFamily,
|
|
700
|
+
status_invocation: RunnerInvocation,
|
|
701
|
+
status_root: Path,
|
|
702
|
+
execution_root: Path,
|
|
703
|
+
branch_context: BranchContext,
|
|
704
|
+
branch_name: str,
|
|
705
|
+
base_branch: str,
|
|
706
|
+
session_id: str,
|
|
707
|
+
session_paths: SessionPaths,
|
|
708
|
+
worktree_runtime,
|
|
709
|
+
*,
|
|
710
|
+
classification_fatal_error_code: str = "",
|
|
711
|
+
) -> str:
|
|
712
|
+
env = RunnerEnvironment.from_env()
|
|
713
|
+
commit_final_bookkeeping(status_root, status_family, item_id, new_status)
|
|
714
|
+
commit_task_branch_changes(execution_root, family, item_id, new_status)
|
|
715
|
+
if worktree_runtime is not None:
|
|
716
|
+
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
717
|
+
merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
|
|
718
|
+
_emit_git_plan_output(merge)
|
|
719
|
+
if merge.ok:
|
|
720
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=True)
|
|
721
|
+
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
722
|
+
return new_status
|
|
723
|
+
else:
|
|
724
|
+
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
725
|
+
merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
|
|
726
|
+
_emit_git_plan_output(merge)
|
|
727
|
+
if merge.ok:
|
|
728
|
+
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
729
|
+
return new_status
|
|
730
|
+
|
|
731
|
+
_write_session_status(
|
|
732
|
+
session_paths,
|
|
733
|
+
status="merge_conflict",
|
|
734
|
+
session_id=session_id,
|
|
735
|
+
task_id=item_id,
|
|
736
|
+
task_type=family.task_type,
|
|
737
|
+
base_branch=base_branch,
|
|
738
|
+
active_dev_branch=branch_name,
|
|
739
|
+
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
740
|
+
reason="merge_conflict",
|
|
741
|
+
fatal_error_code=classification_fatal_error_code,
|
|
742
|
+
)
|
|
743
|
+
update = update_item(
|
|
744
|
+
status_family,
|
|
745
|
+
status_invocation,
|
|
746
|
+
item_id,
|
|
747
|
+
"merge_conflict",
|
|
748
|
+
session_id,
|
|
749
|
+
status_root,
|
|
750
|
+
active_dev_branch=branch_name,
|
|
751
|
+
base_branch=base_branch,
|
|
752
|
+
last_fatal_error_code=classification_fatal_error_code,
|
|
753
|
+
)
|
|
754
|
+
if not update.ok:
|
|
755
|
+
raise RuntimeError(update.text)
|
|
756
|
+
_emit_update_summary(family, item_id, update)
|
|
757
|
+
merged_status = _status_from_update(update)
|
|
758
|
+
commit_final_bookkeeping(status_root, status_family, item_id, merged_status)
|
|
759
|
+
_save_failure_wip(execution_root, family, item_id, merged_status, worktree_runtime)
|
|
760
|
+
if worktree_runtime is not None:
|
|
761
|
+
guarded_worktree_remove(worktree_runtime, delete_branch=False)
|
|
762
|
+
else:
|
|
763
|
+
branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
764
|
+
return merged_status
|
|
765
|
+
|
|
766
|
+
|
|
569
767
|
def _save_failure_wip(execution_root: Path, family: RunnerFamily, item_id: str, status: str, worktree_runtime) -> None:
|
|
570
768
|
if worktree_runtime is not None:
|
|
571
769
|
worktree_save_wip(worktree_runtime)
|
|
@@ -684,6 +882,43 @@ def _path_in_execution_root(path: Path, project_root: Path, execution_root: Path
|
|
|
684
882
|
return path
|
|
685
883
|
|
|
686
884
|
|
|
885
|
+
def _latest_session_id(state_dir: Path, item_id: str) -> str:
|
|
886
|
+
sessions_dir = state_dir / item_id / "sessions"
|
|
887
|
+
try:
|
|
888
|
+
sessions = [child.name for child in sessions_dir.iterdir() if child.is_dir()]
|
|
889
|
+
except OSError:
|
|
890
|
+
return ""
|
|
891
|
+
return sorted(sessions)[-1] if sessions else ""
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _artifact_path_for_item(project_root: Path, family: RunnerFamily, item_id: str, metadata: dict[str, object]) -> Path:
|
|
895
|
+
if family.kind == "feature":
|
|
896
|
+
title = str(metadata.get("title") or "")
|
|
897
|
+
if title:
|
|
898
|
+
return project_root / ".prizmkit" / "specs" / _feature_slug(item_id, title)
|
|
899
|
+
specs = project_root / ".prizmkit" / "specs"
|
|
900
|
+
numeric = item_id.replace("F-", "").replace("f-", "").zfill(3)
|
|
901
|
+
try:
|
|
902
|
+
matches = sorted(path for path in specs.iterdir() if path.is_dir() and path.name.startswith(f"{numeric}-"))
|
|
903
|
+
except OSError:
|
|
904
|
+
matches = []
|
|
905
|
+
if matches:
|
|
906
|
+
return matches[0]
|
|
907
|
+
return project_root / ".prizmkit" / family.artifact_root_name / item_id
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _feature_slug(feature_id: str, title: str) -> str:
|
|
911
|
+
import re
|
|
912
|
+
|
|
913
|
+
numeric = feature_id.replace("F-", "").replace("f-", "").zfill(3)
|
|
914
|
+
cleaned = re.sub(r"[^a-z0-9\s-]", "", title.lower())
|
|
915
|
+
cleaned = re.sub(r"[\s]+", "-", cleaned.strip())
|
|
916
|
+
cleaned = re.sub(r"-+", "-", cleaned).strip("-")
|
|
917
|
+
if not cleaned:
|
|
918
|
+
cleaned = "feature"
|
|
919
|
+
return f"{numeric}-{cleaned}"
|
|
920
|
+
|
|
921
|
+
|
|
687
922
|
def _write_session_status(
|
|
688
923
|
session_paths: SessionPaths,
|
|
689
924
|
*,
|