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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +88 -58
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +4 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +53 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +33 -17
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +30 -28
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +8 -3
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -2
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +11 -4
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
- package/bundled/dev-pipeline/scripts/prompt_framework.py +4 -6
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -19
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +10 -2
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
- package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +3 -2
- package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +3 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +11 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -3
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +11 -3
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +6 -4
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +192 -5
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
- package/bundled/dev-pipeline/tests/test_unified_cli.py +215 -9
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/feature-planner/SKILL.md +2 -2
- package/bundled/skills/prizmkit/SKILL.md +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +10 -1
- package/package.json +1 -1
- package/src/manifest.js +8 -3
package/bundled/VERSION.json
CHANGED
|
@@ -11,7 +11,7 @@ from dataclasses import dataclass
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
543
|
+
"""Plan a merge that consumes only a clean, committed task branch."""
|
|
528
544
|
commands = [
|
|
529
|
-
_git("
|
|
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.
|
|
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
|
-
"
|
|
549
|
-
"Rebase then fast-forward preserves merge
|
|
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
|
|
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",
|
|
560
|
-
_git("
|
|
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
|
-
|
|
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
|
|
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",
|
|
587
|
-
_git("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
588
|
-
_git("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
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(
|
|
602
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 —
|
|
409
|
-
"full": "full (Tier 3 —
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1196
|
-
are performed by the
|
|
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
|
-
"(
|
|
1239
|
-
"**Tier 3 —
|
|
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
|
|
1242
|
-
"
|
|
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
|
|
1275
|
-
"
|
|
1276
|
-
"- Gate checks
|
|
1277
|
-
"and
|
|
1278
|
-
"-
|
|
1279
|
-
"
|
|
1280
|
-
"
|
|
1281
|
-
"
|
|
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 —
|
|
1286
|
-
"
|
|
1287
|
-
"- context-snapshot.md is
|
|
1288
|
-
"
|
|
1289
|
-
"
|
|
1290
|
-
"-
|
|
1291
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
1639
|
-
#
|
|
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 —
|
|
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
|
-
|
|
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
|
|
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",
|