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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/codebuddy/settings-adapter.js +76 -25
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +9 -2
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +9 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +155 -93
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +9 -15
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +9 -5
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +4 -2
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +4 -2
- package/bundled/dev-pipeline/tests/test_runtime_commit.py +228 -69
- package/bundled/dev-pipeline/tests/test_unified_cli.py +182 -10
- package/bundled/rules/prizm/prizm-commit-workflow.md +14 -7
- package/bundled/rules/prizm/prizm-documentation.md +24 -65
- package/bundled/rules/prizm/prizm-progressive-loading.md +9 -8
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +5 -5
- package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +4 -5
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +4 -6
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +1 -4
- package/bundled/skills/prizmkit-committer/SKILL.md +50 -59
- package/bundled/skills/prizmkit-deploy/SKILL.md +172 -198
- package/bundled/skills/prizmkit-deploy/references/capability-contract.md +137 -0
- package/bundled/skills/prizmkit-deploy/references/deploy-record-schema.json +1053 -0
- package/bundled/skills/prizmkit-deploy/references/deployment-declaration-schema.json +291 -0
- package/bundled/skills/prizmkit-deploy/references/legacy-migration.md +90 -0
- package/bundled/skills/prizmkit-deploy/references/policy-and-execution.md +170 -0
- package/bundled/skills/prizmkit-deploy/references/real-resource-test-policy.md +73 -0
- package/bundled/skills/prizmkit-deploy/references/verification-and-recovery.md +164 -0
- package/bundled/skills/prizmkit-deploy/scripts/validate-deploy-artifact.py +371 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +2 -2
- package/bundled/skills/prizmkit-init/SKILL.md +17 -17
- package/bundled/skills/prizmkit-init/references/update-supplement.md +5 -5
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +17 -11
- package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +95 -155
- package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +6 -5
- package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +4 -2
- package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +9 -10
- package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +6 -5
- package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +13 -12
- package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -3
- package/bundled/skills/prizmkit-test/SKILL.md +28 -15
- package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +8 -4
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +7 -3
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +34 -11
- package/bundled/skills/prizmkit-test/references/test-report-template.md +33 -11
- package/bundled/skills/prizmkit-workflow/SKILL.md +4 -8
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +10 -9
- package/bundled/templates/hooks/commit-intent-status.py +9 -41
- package/bundled/templates/hooks/commit-intent.json +3 -3
- package/bundled/templates/hooks/diff-prizm-docs.py +3 -1
- package/bundled/templates/hooks/prizm-pre-commit.py +32 -6
- package/bundled/templates/hooks/run-python-hook.cjs +44 -0
- package/bundled/templates/hooks/validate-prizm-docs.py +399 -173
- package/bundled/templates/project-memory-template.md +19 -19
- package/package.json +1 -1
- package/src/clean.js +6 -7
- package/src/gitignore-template.js +8 -12
- package/src/scaffold.js +125 -71
- package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +0 -89
- package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +0 -145
- package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +0 -109
- package/bundled/skills/prizmkit-deploy/references/data-safety-examples.md +0 -120
- package/bundled/skills/prizmkit-deploy/references/database-setup.md +0 -65
- package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +0 -150
- package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +0 -78
- package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +0 -49
- package/bundled/skills/prizmkit-deploy/references/deployment-modes.md +0 -50
- package/bundled/skills/prizmkit-deploy/references/direct-upload.md +0 -26
- package/bundled/skills/prizmkit-deploy/references/dns-setup.md +0 -42
- package/bundled/skills/prizmkit-deploy/references/docker-deploy.md +0 -31
- package/bundled/skills/prizmkit-deploy/references/firewall-setup.md +0 -37
- package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +0 -21
- package/bundled/skills/prizmkit-deploy/references/nginx-blue-green.md +0 -59
- package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +0 -164
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +0 -221
- package/bundled/skills/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
- package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
- package/bundled/skills/prizmkit-deploy/references/ssh-takeover.md +0 -20
- package/bundled/skills/prizmkit-deploy/references/ssl-setup.md +0 -56
package/bundled/VERSION.json
CHANGED
|
@@ -1,15 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CodeBuddy
|
|
3
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
return RuntimeCommitResult("failed", "
|
|
133
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
|