prizmkit 1.1.127 → 1.1.129

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 (50) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +2 -75
  3. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +14 -11
  4. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +88 -58
  5. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +4 -3
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +53 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +33 -17
  8. package/bundled/dev-pipeline/prizmkit_runtime/runtime_helper.py +0 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +28 -26
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -1
  11. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -3
  12. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -1
  13. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
  14. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  15. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +22 -21
  16. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -4
  17. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
  18. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
  19. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +11 -4
  20. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  21. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +3 -2
  22. package/bundled/dev-pipeline/templates/sections/directory-convention-full.md +3 -2
  23. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
  24. package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
  25. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
  26. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
  27. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +12 -5
  28. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -5
  29. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
  30. package/bundled/dev-pipeline/templates/sections/phase0-init.md +1 -1
  31. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +1 -1
  32. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +13 -6
  33. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  34. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +6 -4
  35. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +1 -1
  36. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +106 -7
  37. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
  38. package/bundled/dev-pipeline/tests/test_runtime_helper.py +9 -3
  39. package/bundled/dev-pipeline/tests/test_unified_cli.py +225 -24
  40. package/bundled/skills/_metadata.json +3 -3
  41. package/bundled/skills/feature-planner/SKILL.md +2 -2
  42. package/bundled/skills/prizmkit/SKILL.md +1 -1
  43. package/bundled/skills/prizmkit-code-review/SKILL.md +63 -122
  44. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +9 -36
  45. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +21 -111
  46. package/package.json +1 -1
  47. package/src/manifest.js +8 -3
  48. package/src/scaffold.js +5 -16
  49. package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +0 -19
  50. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +0 -120
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.127",
3
- "bundledAt": "2026-07-12T15:13:24.439Z",
4
- "bundledFrom": "d634625"
2
+ "frameworkVersion": "1.1.129",
3
+ "bundledAt": "2026-07-14T06:20:56.368Z",
4
+ "bundledFrom": "60410c0"
5
5
  }
@@ -7,6 +7,8 @@
7
7
  * - Only `description` in frontmatter
8
8
  * - `${SKILL_DIR}` references rewritten to relative paths
9
9
  * - Skills with assets/scripts use directory structure
10
+ *
11
+ * Code-review-specific named agents are retired; review runs in the Main Agent.
10
12
  */
11
13
 
12
14
  import { parseFrontmatter, buildMarkdown } from '../shared/frontmatter.js';
@@ -20,67 +22,6 @@ function toClaudePrizmkitCommand(sub) {
20
22
  return `prizmkit-${normalized}`;
21
23
  }
22
24
 
23
- const REVIEWER_ROLE_SKILL = 'prizmkit-code-review';
24
- const REVIEWER_ROLE_FILE = 'prizmkit-code-reviewer.md';
25
- const REVIEWER_REQUIRED_UNAVAILABLE = new Set([
26
- 'workspace-mutation',
27
- 'shell-execution',
28
- 'downstream-execution-creation',
29
- 'delegation-equivalent-behavior',
30
- 'orchestration-reentry',
31
- 'skill-invocation',
32
- ]);
33
- const CLAUDE_OBSERVATIONAL_TOOLS = new Map([
34
- ['read-files', 'Read'],
35
- ['search-content', 'Grep'],
36
- ['search-paths', 'Glob'],
37
- ]);
38
-
39
- function buildReviewerRoleDefinition(roleManifest) {
40
- if (!roleManifest || roleManifest.role_name !== 'prizmkit-code-reviewer') {
41
- throw new Error('prizmkit-code-review requires assets/reviewer-role.json');
42
- }
43
- const unavailable = new Set(roleManifest.unavailable_capabilities || []);
44
- for (const capability of REVIEWER_REQUIRED_UNAVAILABLE) {
45
- if (!unavailable.has(capability)) {
46
- throw new Error(`Reviewer role must make ${capability} unavailable`);
47
- }
48
- }
49
- if (roleManifest.execution_mode !== 'foreground-independent-review') {
50
- throw new Error('Reviewer role must use foreground independent review');
51
- }
52
-
53
- const tools = (roleManifest.observational_capabilities || []).map(capability => {
54
- const tool = CLAUDE_OBSERVATIONAL_TOOLS.get(capability);
55
- if (!tool) throw new Error(`Unsupported Reviewer observational capability: ${capability}`);
56
- return tool;
57
- });
58
- if (tools.length === 0) throw new Error('Reviewer role requires observational capabilities');
59
-
60
- return `---
61
- name: ${roleManifest.role_name}
62
- description: ${roleManifest.description}
63
- tools: ${tools.join(', ')}
64
- permissionMode: dontAsk
65
- maxTurns: 80
66
- ---
67
-
68
- You are a capability-restricted read-only Reviewer execution role.
69
-
70
- Read and follow the complete prompt supplied by the Main Agent. Perform the complete review yourself. Your tool allowlist intentionally excludes workspace mutation, shell execution, skill invocation, downstream execution creation, delegation-equivalent behavior, and orchestration re-entry.
71
-
72
- If the supplied review cannot be completed with the available read-only capabilities and current context, return \`BLOCKED\` with the missing capability or context. Do not delegate, request a helper, mutate the workspace, or claim \`PASS\` from incomplete work.
73
- `;
74
- }
75
-
76
- export function getClaudeSkillAgentDefinitions(skillName, roleManifest = null) {
77
- if (skillName !== REVIEWER_ROLE_SKILL) return [];
78
- return [{
79
- fileName: REVIEWER_ROLE_FILE,
80
- content: buildReviewerRoleDefinition(roleManifest),
81
- }];
82
- }
83
-
84
25
  /**
85
26
  * Convert a core SKILL.md to Claude Code command.md format.
86
27
  * @param {string} skillContent - Content of the core SKILL.md
@@ -158,20 +99,6 @@ export async function installCommand(corePath, targetRoot) {
158
99
  for (const subdir of subdirs) {
159
100
  cpSync(path.join(corePath, subdir), path.join(targetDir, subdir), { recursive: true });
160
101
  }
161
-
162
- // Install platform-native capability roles generated from canonical skill semantics.
163
- const roleManifestPath = path.join(corePath, 'assets', 'reviewer-role.json');
164
- const roleManifest = existsSync(roleManifestPath)
165
- ? JSON.parse(await readFile(roleManifestPath, 'utf8'))
166
- : null;
167
- const agentDefinitions = getClaudeSkillAgentDefinitions(skillName, roleManifest);
168
- if (agentDefinitions.length > 0) {
169
- const agentsDir = path.join(targetRoot, '.claude', 'agents');
170
- mkdirSync(agentsDir, { recursive: true });
171
- for (const definition of agentDefinitions) {
172
- await writeFile(path.join(agentsDir, definition.fileName), definition.content);
173
- }
174
- }
175
102
  } else {
176
103
  // Single file command
177
104
  const targetDir = path.join(targetRoot, COMMANDS_DIR);
@@ -2,18 +2,19 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `dev-pipeline` drives feature, bugfix, and refactor sessions through the unified Python outer loop. Each iteration spawns a new AI CLI session with a bootstrap prompt that makes the main session orchestrator responsible for planning, implementation, review coordination, testing, retrospective, and commit.
5
+ `dev-pipeline` drives feature, bugfix, and refactor sessions through the unified Python outer loop. Each iteration starts an AI CLI session with a bootstrap prompt that makes the main session orchestrator responsible for planning, implementation, review, testing, retrospective, and commit.
6
+
7
+ Skill behavior is owned by canonical skills:
6
8
 
7
- Implementation and review behavior is owned by skills:
8
9
  - `/prizmkit-implement` executes plan tasks directly and may use its local implementation-subagent reference for narrow active-checkout delegation.
9
- - `/prizmkit-code-review` owns its internal inline reviewer subagent review loop through its local reviewer prompt reference.
10
+ - `/prizmkit-code-review` runs its complete bounded review and repair loop directly in the Main Agent.
10
11
 
11
12
  ## Architecture
12
13
 
13
14
  ```text
14
15
  dev-pipeline (outer loop)
15
16
  |
16
- +-- cli.py feature/bugfix/refactor Python runtime CLI — picks next item, spawns CLI
17
+ +-- cli.py feature/bugfix/refactor Python runtime CLI — picks next item, starts CLI
17
18
  +-- scripts/ Python state and prompt management scripts
18
19
  +-- templates/ Session prompt templates and sections
19
20
  |
@@ -22,7 +23,7 @@ dev-pipeline (outer loop)
22
23
  +-- Context snapshot + specify + plan (orchestrator)
23
24
  +-- Local plan/spec review loop inside prizmkit-plan
24
25
  +-- Implement through /prizmkit-implement
25
- +-- Code review through /prizmkit-code-review inline reviewer loop
26
+ +-- Main-Agent code review through /prizmkit-code-review
26
27
  +-- Scoped test gate and browser attempt
27
28
  +-- Retrospective & commit (orchestrator)
28
29
  ```
@@ -30,24 +31,26 @@ dev-pipeline (outer loop)
30
31
  ## Skill-Owned References
31
32
 
32
33
  | Skill | Reference | Purpose |
33
- |-------|-----------|---------|
34
+ |---|---|---|
34
35
  | `/prizmkit-implement` | skill-local `references/implementation-subagent-procedure.md` | Optional narrow implementation delegation contract |
35
- | `/prizmkit-code-review` | skill-local `references/reviewer-agent-prompt.md` | Required independent reviewer-loop prompt |
36
+ | `/prizmkit-code-review` | skill-local `references/review-report-template.md` | Main-Agent review progress and terminal-result contract |
36
37
 
37
- The orchestrator role is handled by the main agent directly no separate platform agent definition or team topology is installed.
38
+ The orchestrator role is handled by the Main Agent directly. Code review does not install or invoke a separate platform agent definition.
38
39
 
39
40
  ## Active Checkout Contract
40
41
 
41
- Any skill-level implementation or review subagent must run in the expected active git top-level. The prompt contract forbids git worktrees, tool-created worktree isolation, copied repository checkouts, remote isolated checkouts, branch switching, and operating outside the expected active checkout.
42
+ Any optional implementation subagent must run in the expected active git top-level. Its prompt contract forbids git worktrees, tool-created worktree isolation, copied repository checkouts, remote isolated checkouts, branch switching, and operating outside the expected active checkout.
43
+
44
+ Code review always runs in the Main Agent's current workspace so staged, unstaged, and untracked content remains authoritative.
42
45
 
43
- Pipeline runner-owned linked worktrees are an outer runtime option. They do not authorize skill-level subagents to create or enter their own worktrees.
46
+ Pipeline runner-owned linked worktrees are an outer runtime option. They do not authorize a skill-level execution unit to create or enter another worktree.
44
47
 
45
48
  ## Pipeline Scripts
46
49
 
47
50
  Located at `.prizmkit/dev-pipeline/scripts/`:
48
51
 
49
52
  | Script | Purpose |
50
- |--------|---------|
53
+ |---|---|
51
54
  | `init-pipeline.py` | Initialize feature pipeline state |
52
55
  | `init-bugfix-pipeline.py` | Initialize bugfix pipeline state |
53
56
  | `init-refactor-pipeline.py` | Initialize refactor pipeline state |
@@ -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
 
@@ -258,7 +258,6 @@ def _text_final_verdict(args: argparse.Namespace) -> HelperResult:
258
258
  markers = {
259
259
  "PASS": "REVIEW_PASS",
260
260
  "NEEDS_FIXES": "REVIEW_NEEDS_FIXES",
261
- "BLOCKED": "REVIEW_BLOCKED",
262
261
  }
263
262
  marker = markers.get(verdict)
264
263
  if not marker: