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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/command-adapter.js +2 -75
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +14 -11
- 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/prizmkit_runtime/runtime_helper.py +0 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +28 -26
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +22 -21
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -4
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +11 -4
- 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 +12 -5
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -5
- 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 +13 -6
- 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 +106 -7
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +139 -11
- package/bundled/dev-pipeline/tests/test_runtime_helper.py +9 -3
- package/bundled/dev-pipeline/tests/test_unified_cli.py +225 -24
- package/bundled/skills/_metadata.json +3 -3
- 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 +63 -122
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +9 -36
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +21 -111
- package/package.json +1 -1
- package/src/manifest.js +8 -3
- package/src/scaffold.js +5 -16
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +0 -19
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +0 -120
package/bundled/VERSION.json
CHANGED
|
@@ -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
|
|
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`
|
|
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,
|
|
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
|
-
+--
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|