prizmkit 1.1.128 → 1.1.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +88 -58
  3. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +4 -3
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +53 -1
  5. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +33 -17
  6. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +30 -28
  7. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +8 -3
  8. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -2
  9. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +11 -4
  10. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
  11. package/bundled/dev-pipeline/scripts/prompt_framework.py +4 -6
  12. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  13. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -19
  14. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
  15. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
  16. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +10 -2
  17. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  18. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +3 -2
  19. package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +3 -2
  20. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  21. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
  23. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +11 -3
  25. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -3
  26. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
  28. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +11 -3
  30. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +6 -4
  32. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
  33. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +192 -5
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +215 -9
  36. package/bundled/skills/_metadata.json +1 -1
  37. package/bundled/skills/feature-planner/SKILL.md +2 -2
  38. package/bundled/skills/prizmkit/SKILL.md +1 -1
  39. package/bundled/skills/prizmkit-code-review/SKILL.md +10 -1
  40. package/package.json +1 -1
  41. package/src/manifest.js +8 -3
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.128",
3
- "bundledAt": "2026-07-12T17:44:30.283Z",
4
- "bundledFrom": "341ff48"
2
+ "frameworkVersion": "1.1.130",
3
+ "bundledAt": "2026-07-15T08:17:59.816Z",
4
+ "bundledFrom": "81d54a2"
5
5
  }
@@ -11,7 +11,7 @@ from dataclasses import dataclass
11
11
  from pathlib import Path
12
12
 
13
13
 
14
- PIPELINE_TRANSIENT_EXCLUDES = (
14
+ PIPELINE_METADATA_EXCLUDES = (
15
15
  ":(top,exclude).prizmkit-worktree",
16
16
  ":(top,exclude,glob).*/worktree",
17
17
  ":(top,exclude,glob).*/worktree/**",
@@ -19,10 +19,11 @@ PIPELINE_TRANSIENT_EXCLUDES = (
19
19
  ":(top,exclude,glob).*/worktrees/**",
20
20
  ":(top,exclude).prizmkit/state",
21
21
  ":(top,exclude,glob).prizmkit/state/**",
22
+ ":(top,exclude).prizmkit/manifest.json",
22
23
  )
23
24
 
24
25
  HIDDEN_TOOL_WORKTREE_EXCLUDES = (
25
- *PIPELINE_TRANSIENT_EXCLUDES,
26
+ *PIPELINE_METADATA_EXCLUDES,
26
27
  ":(top,exclude).skills-lock.json",
27
28
  ":(top,exclude).AGENTS.md",
28
29
  ":(top,exclude).claude",
@@ -399,8 +400,23 @@ def ensure_linked_worktree(runtime: WorktreeRuntimeContext) -> WorktreeOperation
399
400
 
400
401
 
401
402
  def merge_linked_worktree(runtime: WorktreeRuntimeContext, *, auto_push: bool = False) -> WorktreeOperationResult:
402
- """Rebase the linked worktree branch and fast-forward merge it into base."""
403
+ """Rebase a clean linked-worktree branch and fast-forward merge it into base."""
403
404
  results: list[GitCommandResult] = []
405
+ clean = run_git_command(
406
+ runtime.worktree_path,
407
+ ("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES),
408
+ )
409
+ if clean.return_code == 0 and clean.stdout.strip():
410
+ clean = GitCommandResult(
411
+ args=clean.args,
412
+ return_code=1,
413
+ stdout=clean.stdout,
414
+ stderr="Task worktree is not clean; committed branch state is required",
415
+ )
416
+ results.append(clean)
417
+ if clean.return_code != 0:
418
+ return WorktreeOperationResult(False, "dirty_task_checkout", runtime, tuple(results))
419
+
404
420
  rebase = run_git_command(runtime.worktree_path, ("rebase", runtime.context.base_branch))
405
421
  results.append(rebase)
406
422
  if rebase.return_code != 0:
@@ -524,41 +540,33 @@ def branch_return_plan(context: BranchContext) -> GitCommandPlan:
524
540
 
525
541
 
526
542
  def branch_merge_plan(context: BranchContext, *, auto_push: bool = False) -> GitCommandPlan:
527
- """Plan shell-equivalent branch_merge safety invariants."""
543
+ """Plan a merge that consumes only a clean, committed task branch."""
528
544
  commands = [
529
- _git("diff", "--name-only", allow_failure=True, description="detect tracked dirty files"),
530
- _git("diff", "--cached", "--name-only", allow_failure=True, description="detect staged dirty files"),
531
- _git("stash", "push", "-m", "pipeline-merge-stash", allow_failure=True, description="stash tracked dirty files only"),
545
+ _git("status", "--porcelain", "--untracked-files=normal", description="require a clean task checkout"),
532
546
  _git("rebase", context.base_branch, context.working_branch, description="rebase dev onto original"),
533
547
  _git("checkout", context.base_branch, description="checkout original for fast-forward"),
534
548
  _git("merge", "--ff-only", context.working_branch, description="fast-forward original"),
535
549
  ]
536
550
  if auto_push:
537
551
  commands.append(_git("push", allow_failure=True, description="optional auto-push"))
538
- commands.extend(
539
- [
540
- _git("branch", "-d", context.working_branch, allow_failure=True, description="delete merged dev branch"),
541
- _git("stash", "pop", allow_failure=True, description="restore stashed tracked files"),
542
- ]
543
- )
552
+ commands.append(_git("branch", "-d", context.working_branch, allow_failure=True, description="delete merged dev branch"))
544
553
  return GitCommandPlan(
545
554
  name="branch_merge",
546
555
  commands=tuple(commands),
547
556
  notes=(
548
- "Tracked-only stash avoids capturing ignored state artifacts.",
549
- "Rebase then fast-forward preserves merge invariant.",
557
+ "The task checkout must be clean because committed branch state is the only merge input.",
558
+ "Rebase then fast-forward preserves merge history without pipeline-owned stashes.",
550
559
  ),
551
560
  )
552
561
 
553
562
 
554
563
  def branch_save_wip_plan(context: BranchContext) -> GitCommandPlan:
555
- """Plan shell-equivalent never-fail WIP preservation."""
564
+ """Plan WIP preservation that fails when task state cannot be committed."""
556
565
  return GitCommandPlan(
557
566
  name="branch_save_wip",
558
567
  commands=(
559
- _git("rev-parse", "--abbrev-ref", "HEAD", allow_failure=True, description="verify current branch"),
560
- _git("status", "--porcelain", allow_failure=True, description="detect uncommitted work"),
561
- _git("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES, allow_failure=True, description="stage non-ignored work excluding hidden tool worktrees"),
568
+ _git("rev-parse", "--abbrev-ref", "HEAD", description="verify current branch"),
569
+ _git("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES, description="stage non-ignored work excluding hidden tool worktrees"),
562
570
  _git(
563
571
  "commit",
564
572
  "--no-verify",
@@ -568,24 +576,26 @@ def branch_save_wip_plan(context: BranchContext) -> GitCommandPlan:
568
576
  "Pipeline was interrupted by signal. This commit preserves work-in-progress.",
569
577
  "-m",
570
578
  f"To resume: git checkout {context.working_branch}",
571
- allow_failure=True,
572
579
  description="create WIP commit",
573
580
  ),
581
+ _git(
582
+ "status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES,
583
+ description="require clean task checkout after WIP commit",
584
+ ),
574
585
  ),
575
- never_fails=True,
576
- notes=("Never-fail cleanup helper; errors are diagnostics only.",),
586
+ notes=("Task state stays on its branch; failed preservation is reported.",),
577
587
  )
578
588
 
579
589
 
580
590
  def branch_ensure_return_plan(context: BranchContext) -> GitCommandPlan:
581
- """Plan shell-equivalent guaranteed return cleanup."""
591
+ """Plan a fail-closed WIP commit followed by a clean base return."""
582
592
  return GitCommandPlan(
583
593
  name="branch_ensure_return",
584
594
  commands=(
585
595
  _git("rebase", "--abort", allow_failure=True, description="abort in-progress rebase"),
586
- _git("rev-parse", "--abbrev-ref", "HEAD", allow_failure=True, description="detect current branch"),
587
- _git("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES, allow_failure=True, description="detect WIP before checkout"),
588
- _git("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES, allow_failure=True, description="stage WIP before checkout"),
596
+ _git("rev-parse", "--abbrev-ref", "HEAD", description="detect current branch"),
597
+ _git("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES, description="detect WIP before checkout"),
598
+ _git("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES, description="stage WIP before checkout"),
589
599
  _git(
590
600
  "commit",
591
601
  "--no-verify",
@@ -598,11 +608,13 @@ def branch_ensure_return_plan(context: BranchContext) -> GitCommandPlan:
598
608
  allow_failure=True,
599
609
  description="save WIP before return when on dev branch",
600
610
  ),
601
- _git("stash", "push", "-m", "pipeline-ensure-return-stash", allow_failure=True, description="stash checkout blockers"),
602
- _git("checkout", context.base_branch, allow_failure=True, description="return to original branch"),
611
+ _git(
612
+ "status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES,
613
+ description="require clean task checkout before return",
614
+ ),
615
+ _git("checkout", context.base_branch, description="return to original branch"),
603
616
  ),
604
- never_fails=True,
605
- notes=("Always returns success to support trap/finally cleanup paths.",),
617
+ notes=("Return only after task state is committed and the checkout is clean.",),
606
618
  )
607
619
 
608
620
 
@@ -645,11 +657,24 @@ def worktree_save_wip(runtime: WorktreeRuntimeContext) -> GitPlanResult:
645
657
 
646
658
 
647
659
  def branch_ensure_return(project_root: Path, base_branch: str, working_branch: str = "") -> GitPlanResult:
648
- """Best-effort shell-compatible ensure-return helper that never reports failure."""
660
+ """Commit task WIP and return to base only from a clean checkout."""
649
661
  active = current_branch(project_root)
650
- if working_branch and active == working_branch:
651
- branch_save_wip(project_root, working_branch)
652
662
  context = BranchContext(base_branch=base_branch, working_branch=working_branch or active, project_root=project_root)
663
+ if not active or active == base_branch:
664
+ return GitPlanResult(plan=branch_ensure_return_plan(context), results=())
665
+ if working_branch and active != working_branch:
666
+ mismatch_plan = GitCommandPlan(
667
+ name="branch_ensure_return",
668
+ commands=(_git("checkout", base_branch, description="refuse return from an unrelated branch"),),
669
+ notes=("The active checkout must match the recorded task branch.",),
670
+ )
671
+ failure = GitCommandResult(
672
+ args=mismatch_plan.commands[0].args,
673
+ return_code=1,
674
+ stdout="",
675
+ stderr=f"Active branch {active} does not match task branch {working_branch}",
676
+ )
677
+ return GitPlanResult(plan=mismatch_plan, results=(failure,))
653
678
  return run_git_plan(project_root, branch_ensure_return_plan(context))
654
679
 
655
680
 
@@ -713,10 +738,22 @@ def run_git_command(project_root: Path, args: Sequence[str]) -> GitCommandResult
713
738
 
714
739
 
715
740
  def run_git_plan(project_root: Path, plan: GitCommandPlan) -> GitPlanResult:
716
- """Run a plan sequentially, stopping on non-allowed failures."""
741
+ """Run a plan sequentially, stopping on failures or dirty-state guards."""
717
742
  results: list[GitCommandResult] = []
718
743
  for command in plan.commands:
719
744
  result = run_git_command(project_root, command.args)
745
+ if (
746
+ command.args[:2] == ("status", "--porcelain")
747
+ and command.description.startswith("require clean")
748
+ and result.return_code == 0
749
+ and result.stdout.strip()
750
+ ):
751
+ result = GitCommandResult(
752
+ args=result.args,
753
+ return_code=1,
754
+ stdout=result.stdout,
755
+ stderr="Task checkout is not clean; committed branch state is required",
756
+ )
720
757
  results.append(result)
721
758
  if result.return_code != 0 and not (command.allow_failure or plan.never_fails):
722
759
  break
@@ -731,6 +768,8 @@ class ResetBranchCleanupResult:
731
768
  cleaned: bool
732
769
  deleted: bool
733
770
  returned_to: str
771
+ checkout_discarded: bool = False
772
+ branch_preserved: bool = False
734
773
  warnings: tuple[str, ...] = ()
735
774
 
736
775
 
@@ -765,16 +804,11 @@ def list_reset_branches(project_root: Path, prefix: str, item_id: str) -> tuple[
765
804
 
766
805
 
767
806
  def reset_cleanup_branch(project_root: Path, branch: str, *, return_branch: str, delete: bool) -> ResetBranchCleanupResult:
768
- """Discard uncommitted branch changes and optionally delete the branch.
769
-
770
- This is intentionally reset-specific: unlike interrupted runner cleanup, manual
771
- reset abandons uncommitted work on matching dev branches to recreate the
772
- shell helper contract.
773
- """
807
+ """Reset the current task checkout or operate directly on a non-current branch ref."""
774
808
  warnings: list[str] = []
775
809
  active = current_branch(project_root)
776
810
  restored = active
777
- stashed = False
811
+ cleaned = False
778
812
 
779
813
  if active == branch:
780
814
  run_git_command(project_root, ("reset", "HEAD"))
@@ -786,26 +820,22 @@ def reset_cleanup_branch(project_root: Path, branch: str, *, return_branch: str,
786
820
  restored = branch
787
821
  else:
788
822
  restored = return_branch
789
- else:
790
- dirty = run_git_command(project_root, ("status", "--porcelain"))
791
- if dirty.stdout.strip():
792
- stash = run_git_command(project_root, ("stash", "push", "--include-untracked", "-m", "reset-pipeline-temp"))
793
- stashed = stash.return_code == 0
794
- checkout = run_git_command(project_root, ("checkout", branch))
795
- if checkout.return_code == 0:
796
- run_git_command(project_root, ("reset", "HEAD"))
797
- run_git_command(project_root, ("checkout", "--", "."))
798
- run_git_command(project_root, ("clean", "-fd"))
799
- run_git_command(project_root, ("checkout", active or return_branch))
800
- restored = active or return_branch
801
- else:
802
- warnings.append(f"could not checkout {branch} to discard changes")
803
- if stashed:
804
- run_git_command(project_root, ("stash", "pop"))
823
+ cleaned = True
805
824
 
806
825
  deleted = False
807
826
  if delete:
808
827
  deleted = run_git_command(project_root, ("branch", "-D", branch)).return_code == 0
809
828
  if not deleted:
810
829
  warnings.append(f"failed to delete branch {branch}")
811
- return ResetBranchCleanupResult(branch=branch, cleaned=True, deleted=deleted, returned_to=restored, warnings=tuple(warnings))
830
+ elif active != branch:
831
+ cleaned = True
832
+
833
+ return ResetBranchCleanupResult(
834
+ branch=branch,
835
+ cleaned=cleaned or deleted,
836
+ deleted=deleted,
837
+ returned_to=restored,
838
+ checkout_discarded=cleaned and active == branch,
839
+ branch_preserved=active != branch and not delete,
840
+ warnings=tuple(warnings),
841
+ )
@@ -294,11 +294,12 @@ def _reset_one(family: RunnerFamily, item: ResetItem, options: ResetOptions, pro
294
294
 
295
295
  for branch in branches:
296
296
  cleanup = reset_cleanup_branch(project_root, branch, return_branch=return_branch, delete=options.clean)
297
- lines.append(f"Discarded uncommitted changes on dev branch: {cleanup.branch}")
297
+ if getattr(cleanup, "checkout_discarded", False):
298
+ lines.append(f"Discarded uncommitted changes on current dev branch: {cleanup.branch}")
299
+ elif getattr(cleanup, "branch_preserved", False):
300
+ lines.append(f"Preserved non-current dev branch without checkout: {cleanup.branch}")
298
301
  if cleanup.deleted:
299
302
  lines.append(f"Deleted dev branch: {cleanup.branch}")
300
- elif not options.clean:
301
- lines.append(f"Dev branch preserved for debugging: {cleanup.branch}")
302
303
  lines.extend(f"WARN: {warning}" for warning in cleanup.warnings)
303
304
  if cleanup.warnings:
304
305
  lines.append(f"ERROR: Reset {item.item_id} branch cleanup failed")
@@ -7,6 +7,7 @@ legacy ordering around branch creation and final status updates.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import json
10
11
  from collections.abc import Sequence
11
12
  from dataclasses import dataclass
12
13
  from pathlib import Path
@@ -27,6 +28,8 @@ LOCAL_ONLY_PLATFORM_PATHS = (
27
28
  ".codex/settings.local.json",
28
29
  ".agents/settings.local.json",
29
30
  )
31
+ PIPELINE_MANIFEST_PATH = ".prizmkit/manifest.json"
32
+ CONFLICT_MARKERS = (b"<<<<<<< ", b"=======", b">>>>>>> ")
30
33
 
31
34
 
32
35
  @dataclass(frozen=True)
@@ -39,7 +42,10 @@ class PreflightWorkspace:
39
42
 
40
43
  @property
41
44
  def preservable(self) -> tuple[str, ...]:
42
- return _unique_paths((*self.tracked, *self.visible_untracked))
45
+ return _unique_paths(
46
+ path for path in (*self.tracked, *self.visible_untracked)
47
+ if path != PIPELINE_MANIFEST_PATH
48
+ )
43
49
 
44
50
 
45
51
  @dataclass(frozen=True)
@@ -127,6 +133,49 @@ def _preflight_failure(message: str) -> GitCommandResult:
127
133
  return GitCommandResult(args=("status", "preflight"), return_code=1, stdout="", stderr=message)
128
134
 
129
135
 
136
+ def _preflight_integrity_failure(project_root: Path, workspace: PreflightWorkspace) -> GitCommandResult | None:
137
+ unmerged = run_git_command(project_root, ("ls-files", "-u"))
138
+ if unmerged.return_code != 0:
139
+ return unmerged
140
+ if unmerged.stdout.strip():
141
+ return _preflight_failure("Refusing to auto-commit unmerged Git paths")
142
+
143
+ manifest_path = project_root / PIPELINE_MANIFEST_PATH
144
+ if manifest_path.is_file():
145
+ try:
146
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
147
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
148
+ return _preflight_failure(
149
+ f"Refusing to continue because {PIPELINE_MANIFEST_PATH} is not valid JSON: {exc}"
150
+ )
151
+ if not isinstance(data, dict):
152
+ return _preflight_failure(
153
+ f"Refusing to continue because {PIPELINE_MANIFEST_PATH} is not a JSON object"
154
+ )
155
+ if PIPELINE_MANIFEST_PATH in workspace.tracked:
156
+ return _preflight_failure(
157
+ "Refusing to auto-commit installer-owned .prizmkit/manifest.json; "
158
+ "finish or revert the framework install/upgrade before starting a task"
159
+ )
160
+
161
+ marker_paths = []
162
+ for relative in workspace.preservable:
163
+ path = project_root / relative
164
+ if not path.is_file():
165
+ continue
166
+ try:
167
+ content = path.read_bytes()
168
+ except OSError as exc:
169
+ return _preflight_failure(f"Cannot inspect {relative} before auto-commit: {exc}")
170
+ if all(marker in content for marker in CONFLICT_MARKERS):
171
+ marker_paths.append(relative)
172
+ if marker_paths:
173
+ return _preflight_failure(
174
+ "Refusing to auto-commit unresolved conflict markers: " + ", ".join(marker_paths)
175
+ )
176
+ return None
177
+
178
+
130
179
  def _remaining_preflight_changes(project_root: Path) -> GitCommandResult:
131
180
  workspace, failure = _preflight_workspace(project_root)
132
181
  if failure is not None:
@@ -200,6 +249,9 @@ def commit_preflight_ready_changes(project_root: Path, item_id: str) -> Bookkeep
200
249
  "Refusing to auto-commit local-only platform settings: " + ", ".join(workspace.unsafe_local)
201
250
  ),
202
251
  )
252
+ integrity_failure = _preflight_integrity_failure(project_root, workspace)
253
+ if integrity_failure is not None:
254
+ return BookkeepingResult(attempted=True, committed=False, result=integrity_failure)
203
255
  paths = workspace.preservable
204
256
  if not paths:
205
257
  return BookkeepingResult(attempted=False, committed=False)
@@ -405,8 +405,8 @@ def _emit_prompt_summary(prompt: PromptGenerationResult, session_id: str) -> Non
405
405
  def _pipeline_mode_label(mode: str) -> str:
406
406
  labels = {
407
407
  "lite": "lite (Tier 1 — Single Agent)",
408
- "standard": "standard (Tier 2 — Orchestrator + Reviewer)",
409
- "full": "full (Tier 3 — Orchestrator + Reviewer)",
408
+ "standard": "standard (Tier 2 — Main-Agent Review Gates)",
409
+ "full": "full (Tier 3 — Main-Agent Full Guardrails)",
410
410
  }
411
411
  return labels.get(mode, mode)
412
412
 
@@ -729,9 +729,14 @@ def _process_item_locked(
729
729
 
730
730
  if final_session_status != "success":
731
731
  commit_final_bookkeeping(status_root, status_family, item_id, new_status)
732
- _save_failure_wip(execution_root, family, item_id, new_status, worktree_runtime)
732
+ wip = _save_failure_wip(execution_root, family, item_id, new_status, worktree_runtime)
733
+ if not getattr(wip, "ok", True):
734
+ _emit_warning(f"WIP preservation failed for {item_id}; leaving the task checkout active")
735
+ return new_status
733
736
  if not use_worktree:
734
- branch_ensure_return(paths.project_root, base_branch, branch_name)
737
+ returned = branch_ensure_return(paths.project_root, base_branch, branch_name)
738
+ if not getattr(returned, "ok", True):
739
+ _emit_warning(f"Could not safely return to {base_branch}; leaving {branch_name} active")
735
740
  return new_status
736
741
 
737
742
  return _complete_success_merge(
@@ -839,9 +844,12 @@ def _complete_success_merge(
839
844
  classification_fatal_error_code: str = "",
840
845
  ) -> str:
841
846
  env = RunnerEnvironment.from_env()
842
- commit_final_bookkeeping(status_root, status_family, item_id, new_status)
843
- commit_task_branch_changes(execution_root, family, item_id, new_status)
844
- if worktree_runtime is not None:
847
+ final_bookkeeping = commit_final_bookkeeping(status_root, status_family, item_id, new_status)
848
+ task_commit = commit_task_branch_changes(execution_root, family, item_id, new_status)
849
+ finalization_committed = getattr(final_bookkeeping, "ok", True) and getattr(task_commit, "ok", True)
850
+ if not finalization_committed:
851
+ _emit_warning(f"Task finalization commit failed for {item_id}; preserving task checkout without merge")
852
+ if finalization_committed and worktree_runtime is not None:
845
853
  _emit_info(f"Merging {branch_name} into {base_branch}...")
846
854
  merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
847
855
  _emit_git_plan_output(merge)
@@ -853,7 +861,7 @@ def _complete_success_merge(
853
861
  guarded_worktree_remove(worktree_runtime, delete_branch=True)
854
862
  _emit_success(f"Merged {branch_name} into {base_branch}")
855
863
  return new_status
856
- else:
864
+ elif finalization_committed:
857
865
  _emit_info(f"Merging {branch_name} into {base_branch}...")
858
866
  merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
859
867
  _emit_git_plan_output(merge)
@@ -893,17 +901,19 @@ def _complete_success_merge(
893
901
  _emit_update_summary(family, item_id, update)
894
902
  merged_status = _status_from_update(update)
895
903
  commit_final_bookkeeping(status_root, status_family, item_id, merged_status)
896
- _save_failure_wip(execution_root, family, item_id, merged_status, worktree_runtime)
897
- if worktree_runtime is None:
898
- branch_ensure_return(paths.project_root, base_branch, branch_name)
904
+ wip = _save_failure_wip(execution_root, family, item_id, merged_status, worktree_runtime)
905
+ if worktree_runtime is None and getattr(wip, "ok", True):
906
+ returned = branch_ensure_return(paths.project_root, base_branch, branch_name)
907
+ if not returned.ok:
908
+ _emit_warning(f"Could not safely return to {base_branch}; leaving {branch_name} active")
899
909
  return merged_status
900
910
 
901
911
 
902
- def _save_failure_wip(execution_root: Path, family: RunnerFamily, item_id: str, status: str, worktree_runtime) -> None:
912
+ def _save_failure_wip(execution_root: Path, family: RunnerFamily, item_id: str, status: str, worktree_runtime):
913
+ """Preserve failure work and return an observable bookkeeping result."""
903
914
  if worktree_runtime is not None:
904
- worktree_save_wip(worktree_runtime)
905
- else:
906
- commit_wip_changes(execution_root, family, item_id, status)
915
+ return worktree_save_wip(worktree_runtime)
916
+ return commit_wip_changes(execution_root, family, item_id, status)
907
917
 
908
918
 
909
919
  def _record_setup_failure(
@@ -938,8 +948,14 @@ def _record_setup_failure(
938
948
  new_status = _status_from_update(update) if update.ok else "infra_error"
939
949
  if update.ok:
940
950
  commit_final_bookkeeping(status_root, status_family, item_id, new_status)
941
- _save_failure_wip(status_root, family, item_id, new_status, worktree_runtime)
942
- if worktree_runtime is None and current_branch(project_root) == branch_name:
951
+ wip = _save_failure_wip(status_root, family, item_id, new_status, worktree_runtime)
952
+ else:
953
+ wip = None
954
+ if (
955
+ worktree_runtime is None
956
+ and current_branch(project_root) == branch_name
957
+ and getattr(wip, "ok", True)
958
+ ):
943
959
  branch_ensure_return(project_root, base_branch, branch_name)
944
960
  return new_status
945
961
 
@@ -1192,8 +1192,8 @@ def load_active_agent_prompts(templates_dir):
1192
1192
  """Load explicit active agent prompt templates.
1193
1193
 
1194
1194
  Retired Dev implementation, review-fix, and plan-challenge prompts are
1195
- intentionally not loaded; feature implementation and accepted review fixes
1196
- are performed by the main orchestrator and reviewer gate.
1195
+ intentionally not loaded; feature implementation, Code Review, and accepted
1196
+ review fixes are performed directly by the current Main Agent.
1197
1197
  """
1198
1198
  agent_prompts_dir = os.path.join(templates_dir, "agent-prompts")
1199
1199
  if not os.path.isdir(agent_prompts_dir):
@@ -1235,11 +1235,12 @@ def _tier_header(pipeline_mode):
1235
1235
  ),
1236
1236
  "full": (
1237
1237
  "# Dev-Pipeline Session Bootstrap — Tier 3 "
1238
- "(Orchestrator + Reviewer)\n",
1239
- "**Tier 3 — Orchestrator + Reviewer**: For complex "
1238
+ "(Main-Agent Full Guardrails)\n",
1239
+ "**Tier 3 — Main-Agent Full Guardrails**: For complex "
1240
1240
  "features, the main orchestrator handles context, planning, "
1241
- "and implementation directly. Reviewer reviews, and no top-level "
1242
- "Dev implementation subagent is spawned.\n",
1241
+ "implementation, and Code Review directly. No top-level Dev "
1242
+ "implementation subagent is spawned, and Code Review never uses "
1243
+ "direct or indirect delegation.\n",
1243
1244
  ),
1244
1245
  }
1245
1246
  return headers.get(pipeline_mode, headers["lite"])
@@ -1271,28 +1272,30 @@ def _tier_reminders(pipeline_mode):
1271
1272
  specific = [
1272
1273
  "- Tier 2: compatibility label only; feature prompts now render "
1273
1274
  "the unified full/tier3-equivalent guidance by default",
1274
- "- context-snapshot.md is append-only: orchestrator writes "
1275
- "Sections 1-4 + Implementation Log, Reviewer appends Review Notes",
1276
- "- Gate checks enforce Implementation Log (with Gate Evidence) "
1277
- "and Review Notes are written before proceeding",
1278
- "- Do NOT use `run_in_background=true` when spawning Reviewer",
1279
- "- On timeout: check snapshot + git diff HEAD → model:lite → "
1280
- "remaining steps only max 2 retries per phase "
1281
- "orchestrator fallback",
1275
+ "- context-snapshot.md is the implementation knowledge base; "
1276
+ "Code Review writes only review-report.md",
1277
+ "- Gate checks require the Implementation Log (with Gate Evidence) "
1278
+ "and a valid review-report.md Final Result before proceeding",
1279
+ "- Code Review runs only in the current Main Agent and never uses "
1280
+ "direct or indirect delegation",
1281
+ "- On timeout outside Code Review: check snapshot + git diff HEAD "
1282
+ " model:lite → remaining steps only → max 2 retries per phase → "
1283
+ "Main Agent fallback",
1282
1284
  ]
1283
1285
  else: # full
1284
1286
  specific = [
1285
- "- Tier 3: full orchestration — orchestrator implements "
1286
- "directly (no top-level Dev subagent), and Reviewer reviews",
1287
- "- context-snapshot.md is append-only: orchestrator writes "
1288
- "Sections 1-4 + Implementation Log, Reviewer appends "
1289
- "Review Notes",
1290
- "- Gate checks enforce Implementation Log and Review Notes are "
1291
- "written before proceeding",
1287
+ "- Tier 3: full orchestration — the Main Agent implements directly "
1288
+ "and owns the complete Code Review loop",
1289
+ "- context-snapshot.md is the implementation knowledge base; "
1290
+ "Code Review writes only review-report.md",
1291
+ "- Gate checks require the Implementation Log and a valid "
1292
+ "review-report.md Final Result before proceeding",
1293
+ "- Code Review is excluded from normal work delegation and never "
1294
+ "uses direct or indirect delegation or another review entry point",
1292
1295
  "- Do NOT use `run_in_background=true` when spawning normal work "
1293
- "agents",
1294
- "- If an Agent tool call fails with team/config/lock errors, retry "
1295
- "at most once; after a second failure, use the documented "
1296
+ "agents outside Code Review",
1297
+ "- If a normal work Agent call fails with team/config/lock errors, "
1298
+ "retry at most once; after a second failure, use the documented "
1296
1299
  "inline/recovery fallback or write `failure-log.md` rather than "
1297
1300
  "spawning variants or polling indefinitely",
1298
1301
  "- NEVER delete, modify, or touch any file under `.prizmkit/state/` "
@@ -1550,9 +1553,8 @@ def validate_rendered(content):
1550
1553
  # contain literal double braces like Jinja or Go templates)
1551
1554
  unreplaced = re.findall(r"\{\{[A-Z][A-Z_0-9]+\}\}", content)
1552
1555
  if unreplaced:
1553
- # Deduplicate
1554
1556
  unique = sorted(set(unreplaced))
1555
- warnings.append(
1557
+ errors.append(
1556
1558
  "Unreplaced placeholders: {}".format(", ".join(unique))
1557
1559
  )
1558
1560
 
@@ -1635,8 +1637,8 @@ def build_replacements(args, feature, features, global_context, script_dir):
1635
1637
  effective_resume = "6"
1636
1638
 
1637
1639
 
1638
- # Browser verification defaults to a mandatory attempt. It is disabled only
1639
- # by BROWSER_VERIFY=false or browser_interaction.enabled=false.
1640
+ # Browser verification is opt-in through a browser_interaction object and
1641
+ # can always be disabled globally with BROWSER_VERIFY=false.
1640
1642
  browser_enabled, browser_tool, browser_verify_steps, browser_blocking = browser_enabled_from_item(feature)
1641
1643
 
1642
1644
  if browser_tool not in ("playwright-cli", "opencli", "auto"):
@@ -356,13 +356,13 @@ def _bugfix_header(pipeline_mode):
356
356
  title = "# Dev-Pipeline Bug Fix Session Bootstrap — {}\n".format(pipeline_mode.title())
357
357
  desc = {
358
358
  "lite": "**Tier 1 — Single Agent**: direct root-cause fix by the main orchestrator.",
359
- "standard": "**Tier 2 — Orchestrator + Reviewer**: direct implementation with review gates.",
359
+ "standard": "**Tier 2 — Main-Agent Review Gates**: direct implementation with Main-Agent-only review gates.",
360
360
  "full": "**Tier 3 — Full Bugfix Guardrails**: direct implementation with stronger diagnosis and review gates.",
361
361
  }.get(pipeline_mode, "**Bugfix pipeline**")
362
362
  return title + "\n" + desc + "\n"
363
363
 
364
364
 
365
- def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manual_gate):
365
+ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manual_gate, browser_tool="auto"):
366
366
  sections = [
367
367
  ("header", _bugfix_header(pipeline_mode)),
368
368
  ("bugfix-session-context", load_section(sections_dir, "bugfix-session-context.md")),
@@ -380,7 +380,11 @@ def assemble_bugfix_sections(pipeline_mode, sections_dir, browser_enabled, manua
380
380
  ("bugfix-phase-review", load_section(sections_dir, "bugfix-phase-review.md")),
381
381
  ])
382
382
  if browser_enabled:
383
- sections.append(("phase-browser", load_section(sections_dir, "phase-browser-verification-auto.md")))
383
+ browser_section_file = {
384
+ "playwright-cli": "phase-browser-verification.md",
385
+ "opencli": "phase-browser-verification-opencli.md",
386
+ }.get(browser_tool, "phase-browser-verification-auto.md")
387
+ sections.append(("phase-browser", load_section(sections_dir, browser_section_file)))
384
388
  if manual_gate:
385
389
  sections.append(("bugfix-manual-gate", load_section(sections_dir, "bugfix-phase-manual-verification.md")))
386
390
  sections.extend([
@@ -463,6 +467,7 @@ def main():
463
467
  try:
464
468
  sections = assemble_bugfix_sections(
465
469
  pipeline_mode, sections_dir, browser_enabled, manual_gate,
470
+ browser_tool=replacements["{{BROWSER_TOOL}}"],
466
471
  )
467
472
  rendered = render_from_sections(sections, replacements)
468
473
  except FileNotFoundError as exc:
@@ -97,9 +97,10 @@ Implement the minimal fix (red → green):
97
97
  """\
98
98
  Verify fix quality:
99
99
  1. Run `/prizmkit-code-review` with the current bugfix artifact directory.
100
- 2. The Main Agent reviews the complete current change for up to ten rounds, directly repairs accepted findings, and verifies repairs.
100
+ 2. The current Main Agent is the only Code Review executor. It reviews the complete current change for up to ten rounds, directly repairs accepted findings, and verifies repairs.
101
101
  3. Converge only when accepted and unresolved findings are both zero; missing required evidence or round-ten non-convergence produces NEEDS_FIXES.
102
- 4. Do not spawn a code-review-specific subagent. Continue only after the report's last Final Result is valid; preserve append-only progress for recovery.""",
102
+ 4. Do not delegate review directly or indirectly. Do not invoke another review skill or review workflow. Do not relabel delegated work as a finder, verifier, audit, compatibility review, verification, or gap sweep.
103
+ 5. `review-report.md` is the only persisted review artifact. Continue only after its last Final Result is valid; preserve append-only progress for recovery.""",
103
104
  ),
104
105
  6: (
105
106
  "User Verification",