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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.123",
3
- "bundledAt": "2026-07-11T17:38:07.991Z",
4
- "bundledFrom": "9a8c719"
2
+ "frameworkVersion": "1.1.124",
3
+ "bundledAt": "2026-07-12T05:54:30.044Z",
4
+ "bundledFrom": "f15301f"
5
5
  }
@@ -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
- HIDDEN_TOOL_WORKTREE_EXCLUDES = (
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 HIDDEN_TOOL_WORKTREE_EXCLUDES, GitCommandResult, git_status_safe, run_git_command
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 pre-existing changes before task branch setup."""
89
- visible_status = _visible_status_result(project_root)
90
- if visible_status.return_code != 0:
91
- return BookkeepingResult(attempted=True, committed=False, result=visible_status)
92
- paths = _paths_from_status(visible_status.stdout)
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
- add_result = run_git_command(project_root, ("add", "-A", "-f", "--", *paths))
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}", "--", *paths),
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 = _visible_status_result(project_root)
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 test_preflight_ready_commit_excludes_hidden_support_assets_and_state(tmp_path):
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
- def test_process_item_creates_one_ready_commit_before_branch_setup_for_mixed_visible_changes(monkeypatch, tmp_path):
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", "dev/F-001-ready")
1899
- family = family_for("feature", paths)
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": paths.feature_plan})
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, "F-001", paths, initial_metadata={})
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 F-001",
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 test_headless_review_guidance_uses_workspace_equivalence_protocol():
815
- guidance_files = [
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 guidance_files:
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 = guidance_files[-1].read_text(encoding="utf-8")
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
- for path in guidance_files[:-1]:
838
- text = path.read_text(encoding="utf-8")
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 test_preflight_ready_commit_excludes_already_staged_hidden_support_assets(tmp_path):
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 "A .claude/agent.md" in _short_status(repo)
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):
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.123",
2
+ "version": "1.1.124",
3
3
  "skills": {
4
4
  "prizmkit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",