prizmkit 1.1.123 → 1.1.124
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 +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +16 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +121 -9
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +197 -9
- package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +118 -109
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +21 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +33 -17
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +109 -63
- package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +179 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +80 -52
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +348 -154
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +188 -0
- package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +222 -0
- package/package.json +1 -1
- package/src/scaffold.js +15 -0
package/bundled/VERSION.json
CHANGED
|
@@ -20,6 +20,69 @@ function toClaudePrizmkitCommand(sub) {
|
|
|
20
20
|
return `prizmkit-${normalized}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
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-single-authority'
|
|
50
|
+
|| roleManifest.max_descendant_execution_units !== 0
|
|
51
|
+
|| roleManifest.max_delegation_depth !== 0) {
|
|
52
|
+
throw new Error('Reviewer role violates the single-authority execution topology');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tools = (roleManifest.observational_capabilities || []).map(capability => {
|
|
56
|
+
const tool = CLAUDE_OBSERVATIONAL_TOOLS.get(capability);
|
|
57
|
+
if (!tool) throw new Error(`Unsupported Reviewer observational capability: ${capability}`);
|
|
58
|
+
return tool;
|
|
59
|
+
});
|
|
60
|
+
if (tools.length === 0) throw new Error('Reviewer role requires observational capabilities');
|
|
61
|
+
|
|
62
|
+
return `---
|
|
63
|
+
name: ${roleManifest.role_name}
|
|
64
|
+
description: ${roleManifest.description}
|
|
65
|
+
tools: ${tools.join(', ')}
|
|
66
|
+
permissionMode: dontAsk
|
|
67
|
+
maxTurns: 200
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
You are a capability-restricted read-only Reviewer execution role.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
If the supplied review cannot be completed with the available observational capabilities and current execution context, return \`REVIEW_INFRASTRUCTURE_BLOCKED\`. Do not delegate, request a helper, mutate the workspace, or claim \`PASS\` from incomplete work.
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getClaudeSkillAgentDefinitions(skillName, roleManifest = null) {
|
|
79
|
+
if (skillName !== REVIEWER_ROLE_SKILL) return [];
|
|
80
|
+
return [{
|
|
81
|
+
fileName: REVIEWER_ROLE_FILE,
|
|
82
|
+
content: buildReviewerRoleDefinition(roleManifest),
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
|
|
23
86
|
/**
|
|
24
87
|
* Convert a core SKILL.md to Claude Code command.md format.
|
|
25
88
|
* @param {string} skillContent - Content of the core SKILL.md
|
|
@@ -97,6 +160,20 @@ export async function installCommand(corePath, targetRoot) {
|
|
|
97
160
|
for (const subdir of subdirs) {
|
|
98
161
|
cpSync(path.join(corePath, subdir), path.join(targetDir, subdir), { recursive: true });
|
|
99
162
|
}
|
|
163
|
+
|
|
164
|
+
// Install platform-native capability roles generated from canonical skill semantics.
|
|
165
|
+
const roleManifestPath = path.join(corePath, 'assets', 'reviewer-role.json');
|
|
166
|
+
const roleManifest = existsSync(roleManifestPath)
|
|
167
|
+
? JSON.parse(await readFile(roleManifestPath, 'utf8'))
|
|
168
|
+
: null;
|
|
169
|
+
const agentDefinitions = getClaudeSkillAgentDefinitions(skillName, roleManifest);
|
|
170
|
+
if (agentDefinitions.length > 0) {
|
|
171
|
+
const agentsDir = path.join(targetRoot, '.claude', 'agents');
|
|
172
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
173
|
+
for (const definition of agentDefinitions) {
|
|
174
|
+
await writeFile(path.join(agentsDir, definition.fileName), definition.content);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
100
177
|
} else {
|
|
101
178
|
// Single file command
|
|
102
179
|
const targetDir = path.join(targetRoot, COMMANDS_DIR);
|
|
@@ -11,14 +11,20 @@ from dataclasses import dataclass
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
PIPELINE_TRANSIENT_EXCLUDES = (
|
|
15
15
|
":(top,exclude).prizmkit-worktree",
|
|
16
|
-
":(top,exclude).skills-lock.json",
|
|
17
|
-
":(top,exclude).AGENTS.md",
|
|
18
16
|
":(top,exclude,glob).*/worktree",
|
|
19
17
|
":(top,exclude,glob).*/worktree/**",
|
|
20
18
|
":(top,exclude,glob).*/worktrees",
|
|
21
19
|
":(top,exclude,glob).*/worktrees/**",
|
|
20
|
+
":(top,exclude).prizmkit/state",
|
|
21
|
+
":(top,exclude,glob).prizmkit/state/**",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
HIDDEN_TOOL_WORKTREE_EXCLUDES = (
|
|
25
|
+
*PIPELINE_TRANSIENT_EXCLUDES,
|
|
26
|
+
":(top,exclude).skills-lock.json",
|
|
27
|
+
":(top,exclude).AGENTS.md",
|
|
22
28
|
":(top,exclude).claude",
|
|
23
29
|
":(top,exclude,glob).claude/**",
|
|
24
30
|
":(top,exclude).codebuddy",
|
|
@@ -31,12 +37,17 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
|
|
|
31
37
|
":(top,exclude,glob).prizmkit/prizm-docs/**",
|
|
32
38
|
":(top,exclude).prizmkit/config.json",
|
|
33
39
|
":(top,exclude).prizmkit/manifest.json",
|
|
34
|
-
":(top,exclude).prizmkit/state",
|
|
35
|
-
":(top,exclude,glob).prizmkit/state/**",
|
|
36
40
|
":(top,exclude).prizmkit/dev-pipeline",
|
|
37
41
|
":(top,exclude,glob).prizmkit/dev-pipeline/**",
|
|
38
42
|
)
|
|
39
43
|
|
|
44
|
+
UNTRACKED_TRANSIENT_EXCLUDES = (
|
|
45
|
+
*HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
46
|
+
":(top,exclude,glob)**/__pycache__",
|
|
47
|
+
":(top,exclude,glob)**/__pycache__/**",
|
|
48
|
+
":(top,exclude,glob)**/*.py[cod]",
|
|
49
|
+
)
|
|
50
|
+
|
|
40
51
|
PIPELINE_FALLBACK_GIT_NAME = "PrizmKit Pipeline"
|
|
41
52
|
PIPELINE_FALLBACK_GIT_EMAIL = "prizmkit-pipeline@example.invalid"
|
|
42
53
|
|
|
@@ -11,10 +11,37 @@ from collections.abc import Sequence
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from .gitops import
|
|
14
|
+
from .gitops import (
|
|
15
|
+
HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
16
|
+
UNTRACKED_TRANSIENT_EXCLUDES,
|
|
17
|
+
GitCommandResult,
|
|
18
|
+
git_status_safe,
|
|
19
|
+
run_git_command,
|
|
20
|
+
)
|
|
15
21
|
from .runner_models import RunnerFamily
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
LOCAL_ONLY_PLATFORM_PATHS = (
|
|
25
|
+
".claude/settings.local.json",
|
|
26
|
+
".codebuddy/settings.local.json",
|
|
27
|
+
".codex/settings.local.json",
|
|
28
|
+
".agents/settings.local.json",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class PreflightWorkspace:
|
|
34
|
+
"""Exact Git paths participating in startup preservation."""
|
|
35
|
+
|
|
36
|
+
tracked: tuple[str, ...]
|
|
37
|
+
visible_untracked: tuple[str, ...]
|
|
38
|
+
unsafe_local: tuple[str, ...]
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def preservable(self) -> tuple[str, ...]:
|
|
42
|
+
return _unique_paths((*self.tracked, *self.visible_untracked))
|
|
43
|
+
|
|
44
|
+
|
|
18
45
|
@dataclass(frozen=True)
|
|
19
46
|
class BookkeepingResult:
|
|
20
47
|
"""Result from an optional bookkeeping commit boundary."""
|
|
@@ -42,6 +69,81 @@ def _repo_path(project_root: Path, path: Path) -> str:
|
|
|
42
69
|
return path.as_posix()
|
|
43
70
|
|
|
44
71
|
|
|
72
|
+
def _tracked_status_result(project_root: Path) -> GitCommandResult:
|
|
73
|
+
return run_git_command(project_root, ("status", "--porcelain=v1", "-z", "--untracked-files=no", "--", "."))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _untracked_status_result(project_root: Path) -> GitCommandResult:
|
|
77
|
+
return run_git_command(
|
|
78
|
+
project_root,
|
|
79
|
+
("ls-files", "--others", "--exclude-standard", "-z", "--", ".", *UNTRACKED_TRANSIENT_EXCLUDES),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _unique_paths(paths: Sequence[str]) -> tuple[str, ...]:
|
|
84
|
+
return tuple(dict.fromkeys(path for path in paths if path))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _literal_pathspecs(paths: Sequence[str]) -> tuple[str, ...]:
|
|
88
|
+
return tuple(f":(top,literal){path}" for path in paths)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _status_record_paths(stdout: str) -> tuple[str, ...]:
|
|
92
|
+
"""Decode exact paths from NUL-delimited porcelain-v1 records."""
|
|
93
|
+
records = stdout.split("\0")
|
|
94
|
+
changed: list[str] = []
|
|
95
|
+
index = 0
|
|
96
|
+
while index < len(records):
|
|
97
|
+
record = records[index]
|
|
98
|
+
index += 1
|
|
99
|
+
if not record:
|
|
100
|
+
continue
|
|
101
|
+
if len(record) < 4 or record[2] != " ":
|
|
102
|
+
continue
|
|
103
|
+
status = record[:2]
|
|
104
|
+
changed.append(record[3:])
|
|
105
|
+
if "R" in status or "C" in status:
|
|
106
|
+
if index < len(records) and records[index]:
|
|
107
|
+
changed.append(records[index])
|
|
108
|
+
index += 1
|
|
109
|
+
return _unique_paths(changed)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _preflight_workspace(project_root: Path) -> tuple[PreflightWorkspace | None, GitCommandResult | None]:
|
|
113
|
+
tracked_result = _tracked_status_result(project_root)
|
|
114
|
+
if tracked_result.return_code != 0:
|
|
115
|
+
return None, tracked_result
|
|
116
|
+
untracked_result = _untracked_status_result(project_root)
|
|
117
|
+
if untracked_result.return_code != 0:
|
|
118
|
+
return None, untracked_result
|
|
119
|
+
|
|
120
|
+
tracked = _status_record_paths(tracked_result.stdout)
|
|
121
|
+
visible_untracked = _unique_paths(untracked_result.stdout.split("\0"))
|
|
122
|
+
unsafe_local = tuple(path for path in tracked if path in LOCAL_ONLY_PLATFORM_PATHS)
|
|
123
|
+
return PreflightWorkspace(tracked, visible_untracked, unsafe_local), None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _preflight_failure(message: str) -> GitCommandResult:
|
|
127
|
+
return GitCommandResult(args=("status", "preflight"), return_code=1, stdout="", stderr=message)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _remaining_preflight_changes(project_root: Path) -> GitCommandResult:
|
|
131
|
+
workspace, failure = _preflight_workspace(project_root)
|
|
132
|
+
if failure is not None:
|
|
133
|
+
return failure
|
|
134
|
+
assert workspace is not None
|
|
135
|
+
if workspace.unsafe_local:
|
|
136
|
+
return _preflight_failure(
|
|
137
|
+
"Refusing to auto-commit local-only platform settings: " + ", ".join(workspace.unsafe_local)
|
|
138
|
+
)
|
|
139
|
+
return GitCommandResult(
|
|
140
|
+
args=("status", "preflight"),
|
|
141
|
+
return_code=0,
|
|
142
|
+
stdout="\0".join(workspace.preservable),
|
|
143
|
+
stderr="",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
45
147
|
def _visible_status_result(project_root: Path) -> GitCommandResult:
|
|
46
148
|
return run_git_command(project_root, ("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
47
149
|
|
|
@@ -85,23 +187,33 @@ def commit_bookkeeping_changes(
|
|
|
85
187
|
|
|
86
188
|
|
|
87
189
|
def commit_preflight_ready_changes(project_root: Path, item_id: str) -> BookkeepingResult:
|
|
88
|
-
"""Preserve visible
|
|
89
|
-
|
|
90
|
-
if
|
|
91
|
-
return BookkeepingResult(attempted=True, committed=False, result=
|
|
92
|
-
|
|
190
|
+
"""Preserve tracked changes and visible untracked files before task branch setup."""
|
|
191
|
+
workspace, failure = _preflight_workspace(project_root)
|
|
192
|
+
if failure is not None:
|
|
193
|
+
return BookkeepingResult(attempted=True, committed=False, result=failure)
|
|
194
|
+
assert workspace is not None
|
|
195
|
+
if workspace.unsafe_local:
|
|
196
|
+
return BookkeepingResult(
|
|
197
|
+
attempted=True,
|
|
198
|
+
committed=False,
|
|
199
|
+
result=_preflight_failure(
|
|
200
|
+
"Refusing to auto-commit local-only platform settings: " + ", ".join(workspace.unsafe_local)
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
paths = workspace.preservable
|
|
93
204
|
if not paths:
|
|
94
205
|
return BookkeepingResult(attempted=False, committed=False)
|
|
95
|
-
|
|
206
|
+
pathspecs = _literal_pathspecs(paths)
|
|
207
|
+
add_result = run_git_command(project_root, ("add", "-A", "-f", "--", *pathspecs))
|
|
96
208
|
if add_result.return_code != 0:
|
|
97
209
|
return BookkeepingResult(attempted=True, committed=False, result=add_result)
|
|
98
210
|
result = run_git_command(
|
|
99
211
|
project_root,
|
|
100
|
-
("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", *
|
|
212
|
+
("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", *pathspecs),
|
|
101
213
|
)
|
|
102
214
|
if result.return_code != 0:
|
|
103
215
|
return BookkeepingResult(attempted=True, committed=False, result=result)
|
|
104
|
-
clean_result =
|
|
216
|
+
clean_result = _remaining_preflight_changes(project_root)
|
|
105
217
|
if clean_result.return_code != 0 or clean_result.stdout.strip():
|
|
106
218
|
return BookkeepingResult(attempted=True, committed=False, result=clean_result)
|
|
107
219
|
return BookkeepingResult(attempted=True, committed=True, result=result)
|
|
@@ -1835,7 +1835,180 @@ def test_preflight_ready_commit_preserves_staged_unstaged_and_mixed_visible_chan
|
|
|
1835
1835
|
assert visible_status == ""
|
|
1836
1836
|
|
|
1837
1837
|
|
|
1838
|
-
def
|
|
1838
|
+
def test_preflight_ready_commit_handles_exact_special_and_renamed_paths(tmp_path):
|
|
1839
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1840
|
+
|
|
1841
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1842
|
+
old_path = tmp_path / "old name -> café.txt"
|
|
1843
|
+
old_path.write_text("old\n", encoding="utf-8")
|
|
1844
|
+
subprocess.run(["git", "add", old_path.name], cwd=tmp_path, check=True)
|
|
1845
|
+
subprocess.run(["git", "commit", "-m", "track special path"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1846
|
+
new_path = tmp_path / 'new "quoted" \\ 名称.txt'
|
|
1847
|
+
old_path.rename(new_path)
|
|
1848
|
+
untracked_path = tmp_path / "untracked -> line\nbreak.txt"
|
|
1849
|
+
untracked_path.write_text("new\n", encoding="utf-8")
|
|
1850
|
+
colon_path = tmp_path / ":(glob)**.txt"
|
|
1851
|
+
colon_path.write_text("literal pathspec\n", encoding="utf-8")
|
|
1852
|
+
|
|
1853
|
+
result = commit_preflight_ready_changes(tmp_path, "R-001")
|
|
1854
|
+
committed_files = set(
|
|
1855
|
+
filter(
|
|
1856
|
+
None,
|
|
1857
|
+
subprocess.run(
|
|
1858
|
+
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "HEAD"],
|
|
1859
|
+
cwd=tmp_path,
|
|
1860
|
+
check=True,
|
|
1861
|
+
stdout=subprocess.PIPE,
|
|
1862
|
+
).stdout.decode().split("\0"),
|
|
1863
|
+
)
|
|
1864
|
+
)
|
|
1865
|
+
|
|
1866
|
+
assert result.committed is True
|
|
1867
|
+
assert old_path.name in committed_files
|
|
1868
|
+
assert new_path.name in committed_files
|
|
1869
|
+
assert untracked_path.name in committed_files
|
|
1870
|
+
assert colon_path.name in committed_files
|
|
1871
|
+
assert _head_message(tmp_path) == "chore: ready for R-001"
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
@pytest.mark.parametrize("staged", [False, True])
|
|
1875
|
+
def test_preflight_ready_commit_rejects_tracked_local_settings_before_checkout(tmp_path, staged):
|
|
1876
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1877
|
+
|
|
1878
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1879
|
+
local_settings = tmp_path / ".claude" / "settings.local.json"
|
|
1880
|
+
local_settings.parent.mkdir(parents=True)
|
|
1881
|
+
local_settings.write_text('{"permissions": []}\n', encoding="utf-8")
|
|
1882
|
+
subprocess.run(["git", "add", "-f", ".claude/settings.local.json"], cwd=tmp_path, check=True)
|
|
1883
|
+
subprocess.run(["git", "commit", "-m", "track local settings"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1884
|
+
local_settings.write_text('{"permissions": ["secret"]}\n', encoding="utf-8")
|
|
1885
|
+
if staged:
|
|
1886
|
+
subprocess.run(["git", "add", ".claude/settings.local.json"], cwd=tmp_path, check=True)
|
|
1887
|
+
|
|
1888
|
+
result = commit_preflight_ready_changes(tmp_path, "B-001")
|
|
1889
|
+
|
|
1890
|
+
assert result.attempted is True
|
|
1891
|
+
assert result.committed is False
|
|
1892
|
+
assert result.result is not None
|
|
1893
|
+
assert "Refusing to auto-commit local-only platform settings" in result.result.stderr
|
|
1894
|
+
assert _head_message(tmp_path) == "track local settings"
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
def test_preflight_ready_commit_excludes_untracked_python_cache_outside_support_dirs(tmp_path):
|
|
1898
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1899
|
+
|
|
1900
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1901
|
+
(tmp_path / "visible.txt").write_text("visible\n", encoding="utf-8")
|
|
1902
|
+
cache_paths = [
|
|
1903
|
+
tmp_path / "__pycache__" / "top.cpython-312.pyc",
|
|
1904
|
+
tmp_path / "src" / "package" / "__pycache__" / "module.cpython-312.pyc",
|
|
1905
|
+
tmp_path / "src" / "standalone.pyo",
|
|
1906
|
+
]
|
|
1907
|
+
for path in cache_paths:
|
|
1908
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1909
|
+
path.write_text("compiled\n", encoding="utf-8")
|
|
1910
|
+
|
|
1911
|
+
result = commit_preflight_ready_changes(tmp_path, "F-001")
|
|
1912
|
+
committed_files = _head_files(tmp_path)
|
|
1913
|
+
|
|
1914
|
+
assert result.committed is True
|
|
1915
|
+
assert committed_files == {"visible.txt"}
|
|
1916
|
+
assert all(path.is_file() for path in cache_paths)
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
def test_tracked_python_cache_remains_visible_to_task_commit(tmp_path):
|
|
1920
|
+
from prizmkit_runtime.runner_bookkeeping import commit_task_branch_changes
|
|
1921
|
+
from prizmkit_runtime.runner_models import family_for
|
|
1922
|
+
|
|
1923
|
+
paths = _make_paths(tmp_path)
|
|
1924
|
+
_init_bookkeeping_repo(paths.project_root)
|
|
1925
|
+
tracked_cache = paths.project_root / "src" / "__pycache__" / "module.pyc"
|
|
1926
|
+
tracked_cache.parent.mkdir(parents=True)
|
|
1927
|
+
tracked_cache.write_text("baseline\n", encoding="utf-8")
|
|
1928
|
+
subprocess.run(["git", "add", tracked_cache.relative_to(paths.project_root).as_posix()], cwd=paths.project_root, check=True)
|
|
1929
|
+
subprocess.run(["git", "commit", "-m", "track cache"], cwd=paths.project_root, check=True, stdout=subprocess.DEVNULL)
|
|
1930
|
+
tracked_cache.write_text("task change\n", encoding="utf-8")
|
|
1931
|
+
|
|
1932
|
+
result = commit_task_branch_changes(paths.project_root, family_for("bugfix", paths), "B-001", "completed")
|
|
1933
|
+
|
|
1934
|
+
assert result.committed is True
|
|
1935
|
+
assert tracked_cache.relative_to(paths.project_root).as_posix() in _head_files(paths.project_root)
|
|
1936
|
+
assert subprocess.run(
|
|
1937
|
+
["git", "status", "--short", "--untracked-files=no"],
|
|
1938
|
+
cwd=paths.project_root,
|
|
1939
|
+
check=True,
|
|
1940
|
+
stdout=subprocess.PIPE,
|
|
1941
|
+
text=True,
|
|
1942
|
+
).stdout == ""
|
|
1943
|
+
|
|
1944
|
+
|
|
1945
|
+
def test_preflight_ready_commit_includes_tracked_state_but_excludes_untracked_state(tmp_path):
|
|
1946
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1947
|
+
|
|
1948
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1949
|
+
tracked_state = tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "tracked.json"
|
|
1950
|
+
tracked_state.parent.mkdir(parents=True)
|
|
1951
|
+
tracked_state.write_text("baseline\n", encoding="utf-8")
|
|
1952
|
+
subprocess.run(["git", "add", "-f", tracked_state.relative_to(tmp_path).as_posix()], cwd=tmp_path, check=True)
|
|
1953
|
+
subprocess.run(["git", "commit", "-m", "track state"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1954
|
+
tracked_state.write_text("changed\n", encoding="utf-8")
|
|
1955
|
+
untracked_state = tracked_state.parent / "untracked.json"
|
|
1956
|
+
untracked_state.write_text("transient\n", encoding="utf-8")
|
|
1957
|
+
|
|
1958
|
+
result = commit_preflight_ready_changes(tmp_path, "B-001")
|
|
1959
|
+
|
|
1960
|
+
assert result.committed is True
|
|
1961
|
+
assert tracked_state.relative_to(tmp_path).as_posix() in _head_files(tmp_path)
|
|
1962
|
+
assert untracked_state.relative_to(tmp_path).as_posix() not in _head_files(tmp_path)
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def test_preflight_ready_commit_includes_tracked_framework_support_but_excludes_untracked_support_and_state(tmp_path):
|
|
1966
|
+
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1967
|
+
|
|
1968
|
+
_init_bookkeeping_repo(tmp_path)
|
|
1969
|
+
tracked_support_paths = [
|
|
1970
|
+
tmp_path / ".prizmkit" / "manifest.json",
|
|
1971
|
+
tmp_path / ".prizmkit" / "dev-pipeline" / "prizmkit_runtime" / "runners.py",
|
|
1972
|
+
tmp_path / ".claude" / "commands" / "prizmkit.md",
|
|
1973
|
+
]
|
|
1974
|
+
for path in tracked_support_paths:
|
|
1975
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1976
|
+
path.write_text("installed\n", encoding="utf-8")
|
|
1977
|
+
subprocess.run(
|
|
1978
|
+
["git", "add", *(path.relative_to(tmp_path).as_posix() for path in tracked_support_paths)],
|
|
1979
|
+
cwd=tmp_path,
|
|
1980
|
+
check=True,
|
|
1981
|
+
)
|
|
1982
|
+
subprocess.run(["git", "commit", "-m", "track support"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
|
|
1983
|
+
for path in tracked_support_paths:
|
|
1984
|
+
path.write_text("upgraded\n", encoding="utf-8")
|
|
1985
|
+
|
|
1986
|
+
untracked_support_paths = [
|
|
1987
|
+
tmp_path / ".claude" / "settings.local.json",
|
|
1988
|
+
tmp_path / ".prizmkit" / "dev-pipeline" / "scripts" / "__pycache__" / "runtime.pyc",
|
|
1989
|
+
tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "status.json",
|
|
1990
|
+
]
|
|
1991
|
+
for path in untracked_support_paths:
|
|
1992
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1993
|
+
path.write_text("transient\n", encoding="utf-8")
|
|
1994
|
+
|
|
1995
|
+
result = commit_preflight_ready_changes(tmp_path, "B-001")
|
|
1996
|
+
committed_files = _head_files(tmp_path)
|
|
1997
|
+
|
|
1998
|
+
assert result.committed is True
|
|
1999
|
+
assert {path.relative_to(tmp_path).as_posix() for path in tracked_support_paths}.issubset(committed_files)
|
|
2000
|
+
assert not {path.relative_to(tmp_path).as_posix() for path in untracked_support_paths}.intersection(committed_files)
|
|
2001
|
+
assert _head_message(tmp_path) == "chore: ready for B-001"
|
|
2002
|
+
assert subprocess.run(
|
|
2003
|
+
["git", "status", "--short", "--untracked-files=no"],
|
|
2004
|
+
cwd=tmp_path,
|
|
2005
|
+
check=True,
|
|
2006
|
+
stdout=subprocess.PIPE,
|
|
2007
|
+
text=True,
|
|
2008
|
+
).stdout == ""
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def test_preflight_ready_commit_excludes_untracked_support_assets_and_state(tmp_path):
|
|
1839
2012
|
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
1840
2013
|
|
|
1841
2014
|
_init_bookkeeping_repo(tmp_path)
|
|
@@ -1882,23 +2055,38 @@ def test_preflight_ready_commit_noops_when_workspace_has_no_visible_dirty_conten
|
|
|
1882
2055
|
assert _head_commit_count(tmp_path) == before_count
|
|
1883
2056
|
|
|
1884
2057
|
|
|
1885
|
-
|
|
2058
|
+
@pytest.mark.parametrize(
|
|
2059
|
+
("kind", "item_id", "branch"),
|
|
2060
|
+
[
|
|
2061
|
+
("feature", "F-001", "dev/F-001-ready"),
|
|
2062
|
+
("bugfix", "B-001", "bugfix/B-001-ready"),
|
|
2063
|
+
("refactor", "R-001", "refactor/R-001-ready"),
|
|
2064
|
+
],
|
|
2065
|
+
)
|
|
2066
|
+
def test_process_item_creates_one_ready_commit_before_branch_setup_for_all_families(
|
|
2067
|
+
monkeypatch,
|
|
2068
|
+
tmp_path,
|
|
2069
|
+
kind,
|
|
2070
|
+
item_id,
|
|
2071
|
+
branch,
|
|
2072
|
+
):
|
|
1886
2073
|
from prizmkit_runtime import runner_bookkeeping, runners
|
|
1887
2074
|
from prizmkit_runtime.gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES
|
|
1888
2075
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
1889
2076
|
|
|
1890
|
-
paths = _make_paths(tmp_path)
|
|
2077
|
+
paths = _make_paths(tmp_path / kind)
|
|
1891
2078
|
_init_bookkeeping_repo(paths.project_root)
|
|
1892
2079
|
(paths.project_root / "staged.txt").write_text("staged before run\n", encoding="utf-8")
|
|
1893
2080
|
subprocess.run(["git", "add", "staged.txt"], cwd=paths.project_root, check=True)
|
|
1894
2081
|
(paths.project_root / "unstaged.txt").write_text("unstaged before run\n", encoding="utf-8")
|
|
2082
|
+
(paths.project_root / "untracked.txt").write_text("untracked before run\n", encoding="utf-8")
|
|
1895
2083
|
before_count = _head_commit_count(paths.project_root)
|
|
1896
2084
|
branch_events = []
|
|
1897
2085
|
|
|
1898
|
-
monkeypatch.setenv("DEV_BRANCH",
|
|
1899
|
-
family = family_for(
|
|
2086
|
+
monkeypatch.setenv("DEV_BRANCH", branch)
|
|
2087
|
+
family = family_for(kind, paths)
|
|
1900
2088
|
invocation = parse_invocation(family, "run", ())
|
|
1901
|
-
invocation = invocation.__class__(**{**invocation.__dict__, "list_path":
|
|
2089
|
+
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": family.plan_path})
|
|
1902
2090
|
_install_runner_session_fakes(monkeypatch, runners)
|
|
1903
2091
|
monkeypatch.setattr(runners, "commit_preflight_ready_changes", runner_bookkeeping.commit_preflight_ready_changes)
|
|
1904
2092
|
|
|
@@ -1921,18 +2109,18 @@ def test_process_item_creates_one_ready_commit_before_branch_setup_for_mixed_vis
|
|
|
1921
2109
|
|
|
1922
2110
|
monkeypatch.setattr(runners, "run_git_plan", fake_run_git_plan)
|
|
1923
2111
|
|
|
1924
|
-
status = runners._process_item(family, invocation,
|
|
2112
|
+
status = runners._process_item(family, invocation, item_id, paths, initial_metadata={})
|
|
1925
2113
|
|
|
1926
2114
|
assert status == "completed"
|
|
1927
2115
|
assert branch_events == [
|
|
1928
2116
|
{
|
|
1929
2117
|
"commit_count": before_count + 1,
|
|
1930
|
-
"message": "chore: ready for
|
|
2118
|
+
"message": f"chore: ready for {item_id}",
|
|
1931
2119
|
"visible_status": "",
|
|
1932
2120
|
}
|
|
1933
2121
|
]
|
|
1934
2122
|
assert _head_commit_count(paths.project_root) == before_count + 1
|
|
1935
|
-
assert {"staged.txt", "unstaged.txt"}.issubset(_head_files(paths.project_root))
|
|
2123
|
+
assert {"staged.txt", "unstaged.txt", "untracked.txt"}.issubset(_head_files(paths.project_root))
|
|
1936
2124
|
|
|
1937
2125
|
|
|
1938
2126
|
|
|
@@ -811,14 +811,21 @@ def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(
|
|
|
811
811
|
assert str(prompt) not in "\n".join(lines[:6])
|
|
812
812
|
|
|
813
813
|
|
|
814
|
-
def
|
|
815
|
-
|
|
814
|
+
def test_headless_review_guidance_uses_workspace_and_execution_protocols():
|
|
815
|
+
headless_guidance_files = [
|
|
816
816
|
REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-review-full.md",
|
|
817
817
|
REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
|
|
818
|
-
REPO_ROOT / "core" / "skills" / "prizmkit-skill" / "prizmkit-code-review" / "SKILL.md",
|
|
819
818
|
]
|
|
819
|
+
skill_path = (
|
|
820
|
+
REPO_ROOT
|
|
821
|
+
/ "core"
|
|
822
|
+
/ "skills"
|
|
823
|
+
/ "prizmkit-skill"
|
|
824
|
+
/ "prizmkit-code-review"
|
|
825
|
+
/ "SKILL.md"
|
|
826
|
+
)
|
|
820
827
|
|
|
821
|
-
for path in
|
|
828
|
+
for path in headless_guidance_files:
|
|
822
829
|
text = path.read_text(encoding="utf-8")
|
|
823
830
|
assert "platform-supported" in text, path
|
|
824
831
|
assert "snapshot" in text and "equivalent" in text, path
|
|
@@ -828,15 +835,17 @@ def test_headless_review_guidance_uses_workspace_equivalence_protocol():
|
|
|
828
835
|
assert ".prizmkit/state/worktrees/..." not in text, path
|
|
829
836
|
assert "USE_WORKTREE" not in text, path
|
|
830
837
|
assert "isolation:" not in text, path
|
|
838
|
+
assert "Read existing source files in the modules this plan touches" not in text, path
|
|
831
839
|
|
|
832
|
-
skill_text =
|
|
840
|
+
skill_text = skill_path.read_text(encoding="utf-8")
|
|
833
841
|
assert "reviewer-workspace-protocol.md" in skill_text
|
|
842
|
+
assert "reviewer-execution-protocol.md" in skill_text
|
|
834
843
|
assert "staged, unstaged, untracked, deleted, and renamed" in skill_text
|
|
844
|
+
assert "content-equivalent" in skill_text
|
|
835
845
|
assert "WORKSPACE_MISMATCH" in skill_text
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
assert "Read existing source files in the modules this plan touches" not in text, path
|
|
846
|
+
assert "REVIEW_INFRASTRUCTURE_BLOCKED" in skill_text
|
|
847
|
+
assert "do not substitute Main-Agent self-review" in skill_text
|
|
848
|
+
assert "platform-specific role" in skill_text
|
|
840
849
|
|
|
841
850
|
|
|
842
851
|
def test_progress_parser_adds_required_metadata(tmp_path):
|
|
@@ -2109,7 +2118,7 @@ def test_preflight_ready_commit_skips_hidden_support_asset_only_changes(tmp_path
|
|
|
2109
2118
|
assert (repo / ".claude" / "settings.local.json").is_file()
|
|
2110
2119
|
|
|
2111
2120
|
|
|
2112
|
-
def
|
|
2121
|
+
def test_preflight_ready_commit_includes_explicitly_staged_support_assets(tmp_path):
|
|
2113
2122
|
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
2114
2123
|
|
|
2115
2124
|
repo = tmp_path / "repo"
|
|
@@ -2123,8 +2132,8 @@ def test_preflight_ready_commit_excludes_already_staged_hidden_support_assets(tm
|
|
|
2123
2132
|
|
|
2124
2133
|
assert result.committed
|
|
2125
2134
|
assert _head_commit_message(repo) == "chore: ready for F-005"
|
|
2126
|
-
assert _head_files(repo) == {"visible.txt"}
|
|
2127
|
-
assert
|
|
2135
|
+
assert _head_files(repo) == {".claude/agent.md", "visible.txt"}
|
|
2136
|
+
assert _short_status(repo) == ""
|
|
2128
2137
|
|
|
2129
2138
|
|
|
2130
2139
|
def test_preflight_ready_commit_excludes_ignored_tool_worktree_directories(tmp_path):
|