prizmkit 1.1.159 → 1.1.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codebuddy/settings-adapter.js +76 -25
  3. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +9 -2
  4. package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +9 -6
  5. package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +155 -93
  6. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  7. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
  8. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  9. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +1 -1
  10. package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +9 -15
  11. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +9 -5
  12. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +4 -2
  13. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +4 -2
  14. package/bundled/dev-pipeline/tests/test_runtime_commit.py +228 -69
  15. package/bundled/dev-pipeline/tests/test_unified_cli.py +182 -10
  16. package/bundled/rules/prizm/prizm-commit-workflow.md +14 -7
  17. package/bundled/rules/prizm/prizm-documentation.md +24 -65
  18. package/bundled/rules/prizm/prizm-progressive-loading.md +9 -8
  19. package/bundled/skills/_metadata.json +1 -1
  20. package/bundled/skills/prizmkit-code-review/SKILL.md +5 -5
  21. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +4 -5
  22. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +4 -6
  23. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +1 -4
  24. package/bundled/skills/prizmkit-committer/SKILL.md +50 -59
  25. package/bundled/skills/prizmkit-deploy/SKILL.md +172 -198
  26. package/bundled/skills/prizmkit-deploy/references/capability-contract.md +137 -0
  27. package/bundled/skills/prizmkit-deploy/references/deploy-record-schema.json +1053 -0
  28. package/bundled/skills/prizmkit-deploy/references/deployment-declaration-schema.json +291 -0
  29. package/bundled/skills/prizmkit-deploy/references/legacy-migration.md +90 -0
  30. package/bundled/skills/prizmkit-deploy/references/policy-and-execution.md +170 -0
  31. package/bundled/skills/prizmkit-deploy/references/real-resource-test-policy.md +73 -0
  32. package/bundled/skills/prizmkit-deploy/references/verification-and-recovery.md +164 -0
  33. package/bundled/skills/prizmkit-deploy/scripts/validate-deploy-artifact.py +371 -0
  34. package/bundled/skills/prizmkit-implement/SKILL.md +2 -2
  35. package/bundled/skills/prizmkit-init/SKILL.md +17 -17
  36. package/bundled/skills/prizmkit-init/references/update-supplement.md +5 -5
  37. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +17 -11
  38. package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +95 -155
  39. package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +6 -5
  40. package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +4 -2
  41. package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +9 -10
  42. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +6 -5
  43. package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +13 -12
  44. package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -3
  45. package/bundled/skills/prizmkit-test/SKILL.md +28 -15
  46. package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +8 -4
  47. package/bundled/skills/prizmkit-test/references/independent-test-review.md +7 -3
  48. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +34 -11
  49. package/bundled/skills/prizmkit-test/references/test-report-template.md +33 -11
  50. package/bundled/skills/prizmkit-workflow/SKILL.md +4 -8
  51. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +10 -9
  52. package/bundled/templates/hooks/commit-intent-status.py +9 -41
  53. package/bundled/templates/hooks/commit-intent.json +3 -3
  54. package/bundled/templates/hooks/diff-prizm-docs.py +3 -1
  55. package/bundled/templates/hooks/prizm-pre-commit.py +32 -6
  56. package/bundled/templates/hooks/run-python-hook.cjs +44 -0
  57. package/bundled/templates/hooks/validate-prizm-docs.py +399 -173
  58. package/bundled/templates/project-memory-template.md +19 -19
  59. package/package.json +1 -1
  60. package/src/clean.js +6 -7
  61. package/src/gitignore-template.js +8 -12
  62. package/src/scaffold.js +125 -71
  63. package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +0 -89
  64. package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +0 -145
  65. package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +0 -109
  66. package/bundled/skills/prizmkit-deploy/references/data-safety-examples.md +0 -120
  67. package/bundled/skills/prizmkit-deploy/references/database-setup.md +0 -65
  68. package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +0 -150
  69. package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +0 -78
  70. package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +0 -49
  71. package/bundled/skills/prizmkit-deploy/references/deployment-modes.md +0 -50
  72. package/bundled/skills/prizmkit-deploy/references/direct-upload.md +0 -26
  73. package/bundled/skills/prizmkit-deploy/references/dns-setup.md +0 -42
  74. package/bundled/skills/prizmkit-deploy/references/docker-deploy.md +0 -31
  75. package/bundled/skills/prizmkit-deploy/references/firewall-setup.md +0 -37
  76. package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +0 -21
  77. package/bundled/skills/prizmkit-deploy/references/nginx-blue-green.md +0 -59
  78. package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +0 -164
  79. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +0 -221
  80. package/bundled/skills/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
  81. package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
  82. package/bundled/skills/prizmkit-deploy/references/ssh-takeover.md +0 -20
  83. package/bundled/skills/prizmkit-deploy/references/ssl-setup.md +0 -56
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.159",
3
- "bundledAt": "2026-07-28T08:35:59.394Z",
4
- "bundledFrom": "7908918"
2
+ "frameworkVersion": "1.1.161",
3
+ "bundledAt": "2026-07-29T09:12:57.978Z",
4
+ "bundledFrom": "5ba2929"
5
5
  }
@@ -1,15 +1,78 @@
1
1
  /**
2
- * CodeBuddy Settings Adapter
3
- * Generates .codebuddy/settings.json with PrizmKit hooks and permissions.
2
+ * CodeBuddy settings adapter.
3
+ * Replaces only deterministic PrizmKit-managed hook entries and preserves user settings.
4
4
  */
5
5
 
6
6
  import { existsSync, mkdirSync } from 'node:fs';
7
7
  import { readFile, writeFile } from 'node:fs/promises';
8
- import path from 'path';
8
+ import path from 'node:path';
9
+
10
+ const MANAGED_MARKERS = [
11
+ '.prizmkit/scripts/commit-intent-status.py',
12
+ '.prizmkit/scripts/post-command-prizm-drift.py',
13
+ 'PRIZMKIT_DOC_UPDATE_REQUIRED',
14
+ ];
15
+
16
+ function isManaged(entry) {
17
+ let serialized;
18
+ try {
19
+ serialized = JSON.stringify(entry);
20
+ } catch {
21
+ return false;
22
+ }
23
+ return MANAGED_MARKERS.some(marker => serialized.includes(marker));
24
+ }
25
+
26
+ function mergeManagedHooks(existingHooks = {}, managedHooks = {}) {
27
+ if (!existingHooks || typeof existingHooks !== 'object' || Array.isArray(existingHooks)) {
28
+ throw new Error('Cannot preserve malformed CodeBuddy hooks: hooks must be an object');
29
+ }
30
+ const merged = {};
31
+ for (const [event, entries] of Object.entries(existingHooks)) {
32
+ if (!Array.isArray(entries)) {
33
+ throw new Error(`Cannot preserve malformed CodeBuddy hooks: ${event} must be an array`);
34
+ }
35
+ merged[event] = entries.filter(entry => !isManaged(entry));
36
+ }
37
+ for (const [event, entries] of Object.entries(managedHooks)) {
38
+ if (!Array.isArray(entries)) {
39
+ throw new Error(`Managed CodeBuddy hook template is invalid: ${event} must be an array`);
40
+ }
41
+ merged[event] = [
42
+ ...(Array.isArray(merged[event]) ? merged[event] : []),
43
+ ...entries,
44
+ ];
45
+ }
46
+ return merged;
47
+ }
48
+
49
+ const MANAGED_HOOKS = {
50
+ UserPromptSubmit: [
51
+ {
52
+ matcher: '(?i)\\b(commit|push|finish|ship|merge|pull request|pr)\\b|提交|推送|收尾|完成任务|合并|发版|发布',
53
+ hooks: [
54
+ {
55
+ type: 'command',
56
+ command: 'node .prizmkit/scripts/run-python-hook.cjs .prizmkit/scripts/commit-intent-status.py',
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ PostToolUse: [
62
+ {
63
+ matcher: '.*',
64
+ hooks: [
65
+ {
66
+ type: 'command',
67
+ command: 'node .prizmkit/scripts/run-python-hook.cjs .prizmkit/scripts/post-command-prizm-drift.py',
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ };
9
73
 
10
74
  /**
11
- * Generate CodeBuddy settings.json for a PrizmKit-enabled project.
12
- * @param {string} targetRoot - Target project root
75
+ * @param {string} targetRoot
13
76
  * @param {Object} options - { hooks: boolean }
14
77
  */
15
78
  export async function generateSettings(targetRoot, options = {}) {
@@ -20,28 +83,16 @@ export async function generateSettings(targetRoot, options = {}) {
20
83
  if (existsSync(settingsPath)) {
21
84
  try {
22
85
  existing = JSON.parse(await readFile(settingsPath, 'utf8'));
23
- } catch {
24
- existing = {};
86
+ } catch (error) {
87
+ throw new Error(`Cannot preserve malformed CodeBuddy settings: ${error.message}`);
25
88
  }
26
- }
27
-
28
- // Merge PrizmKit hooks
29
- if (options.hooks !== false) {
30
- if (!existing.hooks) existing.hooks = {};
31
- if (!existing.hooks.UserPromptSubmit) {
32
- existing.hooks.UserPromptSubmit = [
33
- {
34
- matcher: '(?i)\\b(commit|push|finish|ship|merge|pull request|pr)\\b|提交|推送|收尾|完成任务|合并|发版|发布',
35
- hooks: [
36
- {
37
- type: 'prompt',
38
- prompt: 'You are a commit-intent detector for PrizmKit. Analyze the user prompt and determine if the user intends to COMMIT, PUSH, FINISH, SHIP, MERGE, or create a PULL REQUEST.\n\nUser prompt: $ARGUMENTS\n\nIF commit intent detected, respond with:\n{"ok": true, "reason": "PRIZMKIT_DOC_UPDATE_REQUIRED: Before committing, you MUST update .prizmkit/prizm-docs/ per Prizm auto-update protocol. Steps: 1) Run git diff --cached --name-status. 2) Map changed files to modules via root.prizm MODULE_INDEX. 3) Read and update affected .prizm files (only changed sections). 4) Append to changelog.prizm. 5) Stage .prizm files with git add .prizmkit/prizm-docs/. 6) Then proceed with commit using prizmkit-committer workflow. RULES: Never rewrite entire .prizm files. Never add prose. Only update affected sections."}\n\nIF no commit intent, respond with:\n{"ok": true}\n\nRespond with JSON only. No explanation.',
39
- },
40
- ],
41
- },
42
- ];
89
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
90
+ throw new Error('Cannot preserve malformed CodeBuddy settings: root must be an object');
43
91
  }
44
92
  }
45
93
 
46
- await writeFile(settingsPath, JSON.stringify(existing, null, 2));
94
+ const settings = options.hooks === false
95
+ ? existing
96
+ : { ...existing, hooks: mergeManagedHooks(existing.hooks, MANAGED_HOOKS) };
97
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2));
47
98
  }
@@ -17,8 +17,15 @@ PIPELINE_METADATA_EXCLUDES = (
17
17
  ":(top,exclude,glob).*/worktree/**",
18
18
  ":(top,exclude,glob).*/worktrees",
19
19
  ":(top,exclude,glob).*/worktrees/**",
20
- ":(top,exclude).prizmkit",
21
- ":(top,exclude,glob).prizmkit/**",
20
+ ":(top,exclude).prizmkit/dev-pipeline",
21
+ ":(top,exclude,glob).prizmkit/dev-pipeline/**",
22
+ ":(top,exclude).prizmkit/state",
23
+ ":(top,exclude,glob).prizmkit/state/**",
24
+ ":(top,exclude).prizmkit/scripts",
25
+ ":(top,exclude,glob).prizmkit/scripts/**",
26
+ ":(top,exclude).prizmkit/manifest.json",
27
+ ":(top,exclude,glob).prizmkit/specs/**/workflow-checkpoint.json",
28
+ ":(top,exclude,glob).prizmkit/specs/**/runtime-commit-request.json",
22
29
  )
23
30
 
24
31
  HIDDEN_TOOL_WORKTREE_EXCLUDES = (
@@ -59,17 +59,19 @@ def _file_size(path: Path | None) -> int:
59
59
  return 0
60
60
 
61
61
 
62
- def _progress_signature(path: Path | None) -> tuple[str, str]:
62
+ def _progress_signature(path: Path | None) -> tuple[str, str, bool]:
63
63
  if path is None or not path.is_file():
64
- return "", ""
64
+ return "", "", False
65
65
  try:
66
66
  data = json.loads(path.read_text(encoding="utf-8"))
67
67
  except (OSError, json.JSONDecodeError):
68
- return "", ""
68
+ return "", "", False
69
+ current_tool = data.get("current_tool")
70
+ tool_active = isinstance(current_tool, str) and bool(current_tool.strip())
69
71
  child_signature = str(data.get("child_activity_signature") or "")
70
72
  signature = json.dumps(
71
73
  [
72
- data.get("current_tool") or "",
74
+ current_tool or "",
73
75
  int(data.get("message_count") or 0),
74
76
  int(data.get("total_tool_calls") or 0),
75
77
  int(data.get("active_subagent_count") or 0),
@@ -81,7 +83,7 @@ def _progress_signature(path: Path | None) -> tuple[str, str]:
81
83
  ],
82
84
  separators=(",", ":"),
83
85
  )
84
- return signature, child_signature
86
+ return signature, child_signature, tool_active
85
87
 
86
88
 
87
89
  def observe_heartbeat_state(
@@ -91,13 +93,14 @@ def observe_heartbeat_state(
91
93
  """Observe log/progress activity and compute staleness."""
92
94
  now = time.time()
93
95
  log_size = _file_size(config.session_log)
94
- progress_signature, child_signature = _progress_signature(config.progress_file)
96
+ progress_signature, child_signature, tool_active = _progress_signature(config.progress_file)
95
97
  prev_log_size = previous.log_size if previous else 0
96
98
  progressed = (
97
99
  previous is None
98
100
  or log_size > prev_log_size
99
101
  or (progress_signature and progress_signature != previous.progress_signature)
100
102
  or (child_signature and child_signature != previous.child_activity_signature)
103
+ or tool_active
101
104
  )
102
105
  if progressed:
103
106
  last_seen_epoch = now
@@ -9,17 +9,15 @@ import subprocess
9
9
  import tempfile
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path, PurePosixPath
12
- from typing import Mapping, Sequence
12
+ from typing import Mapping
13
13
 
14
14
  from .checkpoint_state import (
15
15
  load_checkpoint_state,
16
16
  semantic_state_key,
17
17
  validate_checkpoint_data,
18
18
  )
19
- from .gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES
20
-
21
19
  REQUEST_FILENAME = "runtime-commit-request.json"
22
- REQUEST_SCHEMA_VERSION = 1
20
+ REQUEST_SCHEMA_VERSION = 2
23
21
  _HEX_COMMIT = re.compile(r"^[0-9a-fA-F]{40,64}$")
24
22
  _FORBIDDEN_EXACT_PATHS = {
25
23
  ".claude/settings.local.json",
@@ -37,8 +35,6 @@ _SENSITIVE_NAMES = {
37
35
  "secrets.json",
38
36
  }
39
37
  _SENSITIVE_SUFFIXES = {".pem", ".p12", ".pfx"}
40
- _SUPPORT_ROOT_FILES = {"agents.md", "skills-lock.json"}
41
- _SUPPORT_ROOT_DIRS = {".claude", ".codebuddy", ".codex", ".agents", ".pi"}
42
38
 
43
39
 
44
40
  @dataclass(frozen=True)
@@ -48,7 +44,6 @@ class RuntimeCommitRequest:
48
44
  artifact_dir: str
49
45
  base_head: str
50
46
  commit_message: str
51
- intended_paths: tuple[str, ...]
52
47
  runtime_commit_hash: str = ""
53
48
 
54
49
 
@@ -106,9 +101,9 @@ def finalize_runtime_commit(
106
101
  return RuntimeCommitResult("not_ready", "runtime_commit_already_finalized")
107
102
  return RuntimeCommitResult("failed", error or "invalid_commit_request")
108
103
 
109
- if any(_is_tracked(root, path) for path in (request_path, checkpoint)):
110
- return RuntimeCommitResult("failed", "runtime_metadata_must_be_untracked")
111
-
104
+ # Request/checkpoint paths are excluded by their exact Runtime bookkeeping role.
105
+ # They may be project-tracked; commit verification below ensures their pre/post-
106
+ # commit writes never enter the prepared snapshot.
112
107
  current_head = _git(root, "rev-parse", "HEAD")
113
108
  if current_head.return_code != 0:
114
109
  return RuntimeCommitResult("failed", "git_head_unavailable")
@@ -120,21 +115,58 @@ def finalize_runtime_commit(
120
115
  return RuntimeCommitResult("failed", "premature_runtime_commit_hash")
121
116
  if state.semantic_complete:
122
117
  return RuntimeCommitResult("failed", "completed_checkpoint_without_commit")
123
- changed, status_error = _task_visible_changed_paths(
118
+ unmerged = _git(root, "ls-files", "-u")
119
+ if unmerged.return_code != 0:
120
+ return RuntimeCommitResult("failed", "git_unmerged_state_unavailable")
121
+ if unmerged.stdout.strip():
122
+ return RuntimeCommitResult("failed", "unresolved_merge_state")
123
+ changed, status_error = _workspace_changed_paths(
124
124
  root,
125
- artifact_dir=artifact,
126
125
  checkpoint_path=checkpoint,
127
126
  request_path=request_path,
128
127
  )
129
128
  if status_error:
130
129
  return RuntimeCommitResult("failed", status_error)
131
- if set(changed) != set(request.intended_paths):
132
- return RuntimeCommitResult("failed", "intended_paths_mismatch")
133
- staged = _stage_intended_paths(root, request.intended_paths)
130
+ if not changed:
131
+ return RuntimeCommitResult("failed", "empty_workspace_change")
132
+ unsafe = next((path for path in changed if _sensitive_commit_path(path)), "")
133
+ if unsafe:
134
+ return RuntimeCommitResult("failed", "sensitive_workspace_path")
135
+ bookkeeping_staged = _staged_bookkeeping_paths(
136
+ root,
137
+ checkpoint_path=checkpoint,
138
+ request_path=request_path,
139
+ )
140
+ if bookkeeping_staged is None:
141
+ return RuntimeCommitResult("failed", "git_staged_set_unavailable")
142
+ if bookkeeping_staged:
143
+ return RuntimeCommitResult("failed", "runtime_bookkeeping_staged")
144
+ staged = _stage_workspace(
145
+ root,
146
+ checkpoint_path=checkpoint,
147
+ request_path=request_path,
148
+ )
134
149
  if staged.return_code != 0:
135
150
  return RuntimeCommitResult("failed", "git_stage_failed")
136
- if _git(root, "diff", "--cached", "--quiet", "--exit-code").return_code == 0:
151
+ staged_paths = _git(
152
+ root, "diff", "--cached", "--name-only", "--no-renames", "-z",
153
+ )
154
+ if staged_paths.return_code != 0:
155
+ return RuntimeCommitResult("failed", "git_staged_set_unavailable")
156
+ staged_set = {path for path in staged_paths.stdout.split("\0") if path}
157
+ if staged_set != set(changed):
158
+ return RuntimeCommitResult("failed", "staged_workspace_mismatch")
159
+ if not staged_set:
137
160
  return RuntimeCommitResult("failed", "empty_staged_change")
161
+ unstaged_candidates = _workspace_has_unstaged_candidates(
162
+ root,
163
+ checkpoint_path=checkpoint,
164
+ request_path=request_path,
165
+ )
166
+ if unstaged_candidates is None:
167
+ return RuntimeCommitResult("failed", "git_workspace_verification_failed")
168
+ if unstaged_candidates:
169
+ return RuntimeCommitResult("failed", "workspace_changed_during_staging")
138
170
  committed = _git(
139
171
  root,
140
172
  "-c", "core.hooksPath=/dev/null",
@@ -196,7 +228,7 @@ def _load_request(
196
228
  if not isinstance(raw, Mapping) or raw.get("schema_version") != REQUEST_SCHEMA_VERSION:
197
229
  return None, "commit_request_invalid_schema"
198
230
  required_keys = {
199
- "schema_version", "artifact_dir", "base_head", "commit_message", "intended_paths",
231
+ "schema_version", "artifact_dir", "base_head", "commit_message",
200
232
  }
201
233
  allowed_keys = required_keys | {"runtime_commit_hash"}
202
234
  if not required_keys.issubset(raw) or not set(raw).issubset(allowed_keys):
@@ -215,18 +247,6 @@ def _load_request(
215
247
  if not message or "\n" in message or len(message) > 240:
216
248
  return None, "commit_request_message_invalid"
217
249
 
218
- raw_paths = raw.get("intended_paths")
219
- if not isinstance(raw_paths, list) or not raw_paths:
220
- return None, "commit_request_paths_invalid"
221
- intended: list[str] = []
222
- for value in raw_paths:
223
- relative = _safe_relative_path(value)
224
- if relative is None or _forbidden_commit_path(relative, expected_artifact):
225
- return None, "commit_request_path_unsafe"
226
- intended.append(relative)
227
- if len(intended) != len(set(intended)):
228
- return None, "commit_request_paths_duplicate"
229
-
230
250
  runtime_commit_hash = str(raw.get("runtime_commit_hash") or "").strip()
231
251
  if runtime_commit_hash and not _HEX_COMMIT.fullmatch(runtime_commit_hash):
232
252
  return None, "runtime_commit_hash_invalid"
@@ -235,45 +255,25 @@ def _load_request(
235
255
  artifact_dir=expected_artifact,
236
256
  base_head=base_head.lower(),
237
257
  commit_message=message,
238
- intended_paths=tuple(intended),
239
258
  runtime_commit_hash=runtime_commit_hash.lower(),
240
259
  ), ""
241
260
 
242
261
 
243
- def _safe_relative_path(value: object) -> str | None:
244
- text = str(value or "").strip().replace("\\", "/")
245
- if not text or text.startswith("/") or re.match(r"^[A-Za-z]:/", text):
246
- return None
247
- path = PurePosixPath(text)
248
- if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
249
- return None
250
- return path.as_posix()
251
-
252
-
253
- def _forbidden_commit_path(path: str, artifact_dir: str) -> bool:
262
+ def _sensitive_commit_path(path: str) -> bool:
254
263
  lower = path.lower()
255
264
  name = PurePosixPath(lower).name
256
265
  if lower in _FORBIDDEN_EXACT_PATHS:
257
266
  return True
258
267
  if lower == ".git" or lower.startswith(".git/"):
259
268
  return True
260
- if _known_support_path(lower):
261
- return True
262
- artifact_prefix = artifact_dir.lower().rstrip("/") + "/"
263
- if lower in {
264
- artifact_prefix + REQUEST_FILENAME,
265
- artifact_prefix + "workflow-checkpoint.json",
266
- }:
267
- return True
268
269
  if name in _SENSITIVE_NAMES or (name.startswith(".env.") and name != ".env.example"):
269
270
  return True
270
271
  return PurePosixPath(lower).suffix in _SENSITIVE_SUFFIXES
271
272
 
272
273
 
273
- def _task_visible_changed_paths(
274
+ def _workspace_changed_paths(
274
275
  project_root: Path,
275
276
  *,
276
- artifact_dir: Path,
277
277
  checkpoint_path: Path,
278
278
  request_path: Path,
279
279
  ) -> tuple[tuple[str, ...], str]:
@@ -285,42 +285,31 @@ def _task_visible_changed_paths(
285
285
  "--untracked-files=all",
286
286
  "--",
287
287
  ".",
288
- *HIDDEN_TOOL_WORKTREE_EXCLUDES,
289
288
  )
290
289
  if result.return_code != 0:
291
290
  return (), "git_status_failed"
292
- local_paths = {
293
- checkpoint_path.relative_to(project_root).as_posix(),
294
- request_path.relative_to(project_root).as_posix(),
295
- }
296
- visible = []
297
- for path in _status_record_paths(result.stdout):
298
- if path in local_paths:
299
- continue
300
- if _known_support_path(path.lower()) or path.lower() in _FORBIDDEN_EXACT_PATHS:
301
- continue
302
- visible.append(path)
291
+ local_paths = _runtime_bookkeeping_paths(
292
+ project_root,
293
+ checkpoint_path=checkpoint_path,
294
+ request_path=request_path,
295
+ )
296
+ visible = [
297
+ path for path in _status_record_paths(result.stdout)
298
+ if path not in local_paths
299
+ ]
303
300
  return tuple(dict.fromkeys(visible)), ""
304
301
 
305
302
 
306
- def _known_support_path(lower_path: str) -> bool:
307
- path = PurePosixPath(lower_path)
308
- parts = path.parts
309
- if lower_path in _SUPPORT_ROOT_FILES:
310
- return True
311
- if parts and parts[0] in _SUPPORT_ROOT_DIRS:
312
- return True
313
- if lower_path == ".prizmkit-worktree" or lower_path.startswith(
314
- ".prizmkit-worktree/"
315
- ):
316
- return True
317
- if (
318
- len(parts) >= 2
319
- and parts[0].startswith(".")
320
- and parts[1] in {"worktree", "worktrees"}
321
- ):
322
- return True
323
- return lower_path == ".prizmkit" or lower_path.startswith(".prizmkit/")
303
+ def _runtime_bookkeeping_paths(
304
+ project_root: Path,
305
+ *,
306
+ checkpoint_path: Path,
307
+ request_path: Path,
308
+ ) -> set[str]:
309
+ return {
310
+ checkpoint_path.relative_to(project_root).as_posix(),
311
+ request_path.relative_to(project_root).as_posix(),
312
+ }
324
313
 
325
314
 
326
315
  def _status_record_paths(stdout: str) -> tuple[str, ...]:
@@ -341,9 +330,85 @@ def _status_record_paths(stdout: str) -> tuple[str, ...]:
341
330
  return tuple(dict.fromkeys(path for path in changed if path))
342
331
 
343
332
 
344
- def _stage_intended_paths(project_root: Path, paths: Sequence[str]) -> _GitResult:
345
- pathspecs = tuple(f":(top,literal){path}" for path in paths)
346
- return _git(project_root, "add", "-A", "--", *pathspecs)
333
+ def _staged_bookkeeping_paths(
334
+ project_root: Path,
335
+ *,
336
+ checkpoint_path: Path,
337
+ request_path: Path,
338
+ ) -> tuple[str, ...] | None:
339
+ staged = _git(
340
+ project_root,
341
+ "diff",
342
+ "--cached",
343
+ "--name-only",
344
+ "--no-renames",
345
+ "-z",
346
+ )
347
+ if staged.return_code != 0:
348
+ return None
349
+ local_paths = _runtime_bookkeeping_paths(
350
+ project_root,
351
+ checkpoint_path=checkpoint_path,
352
+ request_path=request_path,
353
+ )
354
+ return tuple(
355
+ path for path in staged.stdout.split("\0")
356
+ if path and path in local_paths
357
+ )
358
+
359
+
360
+ def _stage_workspace(
361
+ project_root: Path,
362
+ *,
363
+ checkpoint_path: Path,
364
+ request_path: Path,
365
+ ) -> _GitResult:
366
+ staged = _git(project_root, "add", "-A", "--", ".")
367
+ if staged.return_code != 0:
368
+ return staged
369
+ local_pathspecs = [
370
+ f":(top,literal){path}"
371
+ for path in sorted(
372
+ _runtime_bookkeeping_paths(
373
+ project_root,
374
+ checkpoint_path=checkpoint_path,
375
+ request_path=request_path,
376
+ )
377
+ )
378
+ ]
379
+ return _git(project_root, "reset", "-q", "HEAD", "--", *local_pathspecs)
380
+
381
+
382
+ def _workspace_has_unstaged_candidates(
383
+ project_root: Path,
384
+ *,
385
+ checkpoint_path: Path,
386
+ request_path: Path,
387
+ ) -> bool | None:
388
+ local_paths = _runtime_bookkeeping_paths(
389
+ project_root,
390
+ checkpoint_path=checkpoint_path,
391
+ request_path=request_path,
392
+ )
393
+ unstaged = _git(project_root, "diff", "--name-only", "--no-renames", "-z")
394
+ untracked = _git(
395
+ project_root,
396
+ "ls-files",
397
+ "--others",
398
+ "--exclude-standard",
399
+ "-z",
400
+ "--",
401
+ ".",
402
+ )
403
+ if unstaged.return_code != 0 or untracked.return_code != 0:
404
+ return None
405
+ candidates = {
406
+ path
407
+ for output in (unstaged.stdout, untracked.stdout)
408
+ for path in output.split("\0")
409
+ if path and path not in local_paths
410
+ }
411
+ return bool(candidates)
347
412
 
348
413
 
349
414
  def _matches_prepared_commit(project_root: Path, request: RuntimeCommitRequest, head: str) -> bool:
@@ -357,12 +422,17 @@ def _matches_prepared_commit(project_root: Path, request: RuntimeCommitRequest,
357
422
  return False
358
423
  parent_list = parents.stdout.strip().split()
359
424
  changed = {path for path in paths.stdout.split("\0") if path}
425
+ artifact_prefix = request.artifact_dir.rstrip("/") + "/"
426
+ causal_bookkeeping = {
427
+ artifact_prefix + REQUEST_FILENAME,
428
+ artifact_prefix + "workflow-checkpoint.json",
429
+ }
360
430
  return (
361
431
  len(parent_list) == 1
362
432
  and parent_list[0].lower() == request.base_head.lower()
363
433
  and subject.stdout.strip() == request.commit_message
364
434
  and bool(changed)
365
- and changed == set(request.intended_paths)
435
+ and changed.isdisjoint(causal_bookkeeping)
366
436
  )
367
437
 
368
438
 
@@ -378,14 +448,6 @@ def _record_runtime_commit_hash(path: Path, commit_hash: str) -> str:
378
448
  return "commit_request_hash_record_failed" if error else ""
379
449
 
380
450
 
381
- def _is_tracked(project_root: Path, path: Path) -> bool:
382
- try:
383
- relative = path.relative_to(project_root).as_posix()
384
- except ValueError:
385
- return True
386
- return _git(project_root, "ls-files", "--error-unmatch", "--", relative).return_code == 0
387
-
388
-
389
451
  def _finalize_checkpoint(
390
452
  path: Path,
391
453
  *,
@@ -38,7 +38,7 @@ Complete the feature in headless non-interactive mode using the active atomic sk
38
38
  5. Invoke `/prizmkit-test scope=this-change artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/`. Validate its report/result pair. For `TEST_NEEDS_FIXES`, classify the concrete correction into an allowed `repair_scope`; unknown/unsafe scope blocks, while a safe in-budget route re-enters Implement and requires fresh Review, configured verification, and Test. Do not duplicate the Skill's internal repair loop.
39
39
  6. Write `completion-summary.json` before retrospective and commit preparation.
40
40
  7. Invoke `/prizmkit-retrospective` with the exact artifact root, non-`.prizmkit/` `change_paths`, and `change_summary`; validate `outcome=RETRO_COMPLETE`.
41
- 8. Before commit preparation, all `.prizmkit/**` paths are PrizmKit-internal and stay outside `intended_paths` together with recognized platform support; project-specific transient data must already be Git-ignored, while unknown Git-visible paths block. After validating every required artifact, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/ evidence_paths=<exact paths> excluded_paths=.prizmkit/specs/{{FEATURE_SLUG}}/workflow-checkpoint.json request_path=.prizmkit/specs/{{FEATURE_SLUG}}/runtime-commit-request.json`. Require `COMMIT_REQUEST_READY`; this session then records `COMMIT_PENDING` and stops. The Python runtime executes and verifies the local commit.
41
+ 8. Before commit preparation, validate the complete Git-visible workspace regardless of when or where each correct change originated. Final Review/Test authority must cover every behavior-affecting visible change; ignored paths remain local, while merge conflicts, Secrets, sensitive content, stale evidence, and known incorrect or unverifiable content block. After validating every required artifact, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/ evidence_paths=<exact paths> request_path=.prizmkit/specs/{{FEATURE_SLUG}}/runtime-commit-request.json`. Require a schema-v2 pathless request and `COMMIT_REQUEST_READY`; this session then records `COMMIT_PENDING` and stops. The Python runtime re-enumerates and commits the complete non-ignored workspace with only exact causal request/checkpoint bookkeeping omitted.
42
42
 
43
43
  ## Failure Capture
44
44
 
@@ -448,9 +448,9 @@ Invoke `/prizmkit-retrospective` with the exact artifact root, exact non-`.prizm
448
448
 
449
449
  **6c. Runtime commit preparation**
450
450
 
451
- Before commit preparation, all `.prizmkit/**` paths are PrizmKit-internal and stay outside `intended_paths`, together with `AGENTS.md`, `skills-lock.json`, installed platform directories, and local platform settings. Project-specific transient data must already be covered by Git ignore rules. Unknown Git-visible paths are not silently ignored: justify and include them as task output or block.
451
+ Before commit preparation, validate the complete Git-visible workspace regardless of when or where each correct change originated. Code Review and Test authority must cover every behavior-affecting visible change. Ignored paths remain naturally absent; merge conflicts, Secrets, sensitive content, stale evidence, and known incorrect or unverifiable content block. Never force-add or change/interpret project ignore policy.
452
452
 
453
- After this injected session validates every required artifact, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/ evidence_paths=<exact paths> excluded_paths=.prizmkit/specs/{{FEATURE_SLUG}}/workflow-checkpoint.json request_path=.prizmkit/specs/{{FEATURE_SLUG}}/runtime-commit-request.json`. Require `COMMIT_REQUEST_READY`; this session records `COMMIT_PENDING` and stops. The Python runtime validates the request, executes/verifies the local commit, and finalizes checkpoint `COMMITTED`.
453
+ After this injected session validates every required artifact, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/specs/{{FEATURE_SLUG}}/ evidence_paths=<exact paths> request_path=.prizmkit/specs/{{FEATURE_SLUG}}/runtime-commit-request.json`. Require a schema-v2 pathless request and `COMMIT_REQUEST_READY`; this session records `COMMIT_PENDING` and stops. The Python runtime re-enumerates and commits the complete non-ignored workspace, omitting only exact causal request/checkpoint bookkeeping, then verifies the local commit and finalizes checkpoint `COMMITTED`.
454
454
 
455
455
  ## Critical Paths
456
456
 
@@ -245,7 +245,7 @@ Invoke `/prizmkit-retrospective` with the exact artifact root, exact non-`.prizm
245
245
 
246
246
  **c. Runtime commit preparation**
247
247
 
248
- After this injected session validates every required artifact, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/bugfix/{{BUG_ID}}/ evidence_paths=<exact paths> excluded_paths=.prizmkit/bugfix/{{BUG_ID}}/workflow-checkpoint.json request_path=.prizmkit/bugfix/{{BUG_ID}}/runtime-commit-request.json`. Require `COMMIT_REQUEST_READY`; this session records `COMMIT_PENDING` and stops. The Python runtime validates the request, executes/verifies the local commit, finalizes checkpoint `COMMITTED`, and controls any later publication.
248
+ After this injected session validates every required artifact and confirms final Review/Test authority covers the complete behavior-affecting Git-visible workspace, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/bugfix/{{BUG_ID}}/ evidence_paths=<exact paths> request_path=.prizmkit/bugfix/{{BUG_ID}}/runtime-commit-request.json`. Require a schema-v2 pathless request and `COMMIT_REQUEST_READY`; this session records `COMMIT_PENDING` and stops. Ignored files remain local. The Python runtime re-enumerates and commits the complete non-ignored workspace with only exact causal request/checkpoint bookkeeping omitted, verifies the local commit, finalizes checkpoint `COMMITTED`, and controls any later publication.
249
249
 
250
250
  ---
251
251
 
@@ -210,7 +210,7 @@ Write `.prizmkit/refactor/{{REFACTOR_ID}}/completion-summary.json` for downstrea
210
210
 
211
211
  Complete `refactor-report` and `completion-summary` before retrospective. Then invoke `/prizmkit-retrospective` with the exact artifact root, exact non-`.prizmkit/` `change_paths`, and a concise `change_summary`. Validate `outcome=RETRO_COMPLETE`; do not stage or commit.
212
212
 
213
- After this injected session validates every required artifact, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/refactor/{{REFACTOR_ID}}/ evidence_paths=<exact paths> excluded_paths=.prizmkit/refactor/{{REFACTOR_ID}}/workflow-checkpoint.json request_path=.prizmkit/refactor/{{REFACTOR_ID}}/runtime-commit-request.json`. Require `COMMIT_REQUEST_READY`; this session records `COMMIT_PENDING` and stops. The Python runtime executes/verifies the local commit, finalizes checkpoint `COMMITTED`, and controls any later publication.
213
+ After this injected session validates every required artifact and confirms final Review/Test authority covers the complete behavior-affecting Git-visible workspace, invoke `/prizmkit-committer operation=prepare-runtime-commit artifact_dir=.prizmkit/refactor/{{REFACTOR_ID}}/ evidence_paths=<exact paths> request_path=.prizmkit/refactor/{{REFACTOR_ID}}/runtime-commit-request.json`. Require a schema-v2 pathless request and `COMMIT_REQUEST_READY`; this session records `COMMIT_PENDING` and stops. Ignored files remain local. The Python runtime re-enumerates and commits the complete non-ignored workspace with only exact causal request/checkpoint bookkeeping omitted, verifies the local commit, finalizes checkpoint `COMMITTED`, and controls any later publication.
214
214
 
215
215
  ---
216
216