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.
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """Render review-report.md from completed structured review state."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ VERDICTS = {"PASS", "NEEDS_FIXES"}
13
+ EXECUTION_PROTOCOLS = {"BLOCKED", "COMPLIANT", "NOT_APPLICABLE", "VIOLATION"}
14
+ ATTESTATIONS = {"FAILED", "NOT_APPLICABLE", "VERIFIED"}
15
+ SNAPSHOT_STATUSES = {"FAILED", "NOT_APPLICABLE", "VERIFIED"}
16
+ BLOCKERS = {
17
+ "INVALID_REVIEW_RESULT",
18
+ "NONE",
19
+ "REVIEW_INFRASTRUCTURE_BLOCKED",
20
+ "REVIEW_ORCHESTRATION_VIOLATION",
21
+ "REVIEW_SAFETY_LIMIT_REACHED",
22
+ "WORKSPACE_MISMATCH",
23
+ }
24
+ REVIEW_MODES = {
25
+ "independent-reviewer-verified-snapshot",
26
+ "not-run-no-changes",
27
+ }
28
+
29
+
30
+ class ReportStateError(ValueError):
31
+ """Raised when persisted state cannot produce a trustworthy report."""
32
+
33
+
34
+ def _require_enum(state: dict[str, Any], name: str, allowed: set[str]) -> str:
35
+ value = state.get(name)
36
+ if value not in allowed:
37
+ raise ReportStateError(f"{name} has invalid value: {value!r}")
38
+ return value
39
+
40
+
41
+ def _require_count(state: dict[str, Any], name: str) -> int:
42
+ value = state.get(name)
43
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
44
+ raise ReportStateError(f"{name} must be a non-negative integer")
45
+ return value
46
+
47
+
48
+ def validate_state(state: Any) -> dict[str, Any]:
49
+ """Validate the minimum completed state required for report regeneration."""
50
+ if not isinstance(state, dict):
51
+ raise ReportStateError("state must be a JSON object")
52
+
53
+ _require_enum(state, "verdict", VERDICTS)
54
+ _require_enum(state, "review_mode", REVIEW_MODES)
55
+ _require_enum(state, "execution_protocol", EXECUTION_PROTOCOLS)
56
+ _require_enum(state, "capability_attestation", ATTESTATIONS)
57
+ _require_enum(state, "snapshot_verification", SNAPSHOT_STATUSES)
58
+ _require_enum(state, "blocker_code", BLOCKERS)
59
+ rounds_completed = _require_count(state, "rounds_completed")
60
+ reviewers_started = _require_count(state, "reviewers_started")
61
+ descendants = _require_count(state, "descendant_execution_units_observed")
62
+ delegation_depth = _require_count(state, "delegation_depth_observed")
63
+
64
+ rounds = state.get("rounds")
65
+ findings = state.get("findings")
66
+ if not isinstance(rounds, list) or not isinstance(findings, list):
67
+ raise ReportStateError("rounds and findings must be arrays")
68
+ if rounds_completed != len(rounds):
69
+ raise ReportStateError("rounds_completed must equal the rounds array length")
70
+ if reviewers_started < rounds_completed:
71
+ raise ReportStateError("reviewers_started cannot be below rounds_completed")
72
+
73
+ if state["review_mode"] == "not-run-no-changes":
74
+ if any((rounds_completed, reviewers_started, descendants, delegation_depth)):
75
+ raise ReportStateError("no-change review cannot contain execution activity")
76
+ if state["verdict"] != "PASS":
77
+ raise ReportStateError("no-change review must have PASS verdict")
78
+
79
+ if state["verdict"] == "PASS" and state["blocker_code"] != "NONE":
80
+ raise ReportStateError("PASS cannot include a blocker code")
81
+ if state["execution_protocol"] == "COMPLIANT":
82
+ if descendants != 0 or delegation_depth != 0:
83
+ raise ReportStateError("COMPLIANT execution requires zero descendants and depth")
84
+
85
+ for index, finding in enumerate(findings, start=1):
86
+ if not isinstance(finding, dict):
87
+ raise ReportStateError(f"finding {index} must be an object")
88
+ required = {"title", "severity", "dimension", "location", "problem", "failure_scenario", "status"}
89
+ missing = sorted(required - finding.keys())
90
+ if missing:
91
+ raise ReportStateError(
92
+ f"finding {index} missing fields: {', '.join(missing)}"
93
+ )
94
+
95
+ return state
96
+
97
+
98
+ def _finding_totals(findings: list[dict[str, Any]]) -> tuple[int, int, int]:
99
+ fixed = sum(finding["status"].startswith("fixed") for finding in findings)
100
+ rejected = sum(finding["status"].startswith("rejected") for finding in findings)
101
+ unresolved = len(findings) - fixed - rejected
102
+ return fixed, rejected, unresolved
103
+
104
+
105
+ def render_report(state: Any) -> str:
106
+ """Return deterministic Markdown without launching or resuming a Reviewer."""
107
+ valid = validate_state(state)
108
+ findings = valid["findings"]
109
+ fixed, rejected, unresolved = _finding_totals(findings)
110
+
111
+ lines = [
112
+ "# Review Report",
113
+ "",
114
+ f"## Verdict: {valid['verdict']}",
115
+ f"## Rounds Completed: {valid['rounds_completed']}",
116
+ f"## Reviewers Started: {valid['reviewers_started']}",
117
+ f"## Review Mode: {valid['review_mode']}",
118
+ f"## Reviewer Strategy: {valid.get('reviewer_strategy', 'not-applicable')}",
119
+ f"## Execution Protocol: {valid['execution_protocol']}",
120
+ f"## Capability Attestation: {valid['capability_attestation']}",
121
+ f"## Descendant Execution Units Observed: {valid['descendant_execution_units_observed']}",
122
+ f"## Delegation Depth Observed: {valid['delegation_depth_observed']}",
123
+ f"## Snapshot Verification: {valid['snapshot_verification']}",
124
+ f"## Snapshot Identity: {valid.get('snapshot_identity', 'not-applicable')}",
125
+ f"## Blocker Code: {valid['blocker_code']}",
126
+ f"## Total Findings: {len(findings)} → Fixed: {fixed}, Rejected: {rejected}, Unresolved: {unresolved}",
127
+ "",
128
+ ]
129
+
130
+ if valid["review_mode"] == "not-run-no-changes":
131
+ lines.extend(["## Round 0: PASS — no workspace changes to review", ""])
132
+ else:
133
+ for review_round in valid["rounds"]:
134
+ lines.extend(
135
+ [
136
+ f"## Round {review_round['round']}",
137
+ "",
138
+ f"- Snapshot Identity: {review_round['snapshot_identity']}",
139
+ f"- Reviewer Execution Identity: {review_round['reviewer_execution_identity']}",
140
+ f"- Result: {review_round['result']}",
141
+ f"- Findings: {review_round['findings_count']}",
142
+ f"- Accepted: {review_round['accepted_count']}",
143
+ f"- Rejected: {review_round['rejected_count']}",
144
+ "",
145
+ ]
146
+ )
147
+
148
+ for index, finding in enumerate(findings, start=1):
149
+ lines.extend(
150
+ [
151
+ f"### Finding {index}: {finding['title']}",
152
+ "",
153
+ f"- Severity: {finding['severity']}",
154
+ f"- Dimension: {finding['dimension']}",
155
+ f"- Location: {finding['location']}",
156
+ f"- Problem: {finding['problem']}",
157
+ f"- Failure Scenario: {finding['failure_scenario']}",
158
+ f"- Status: {finding['status']}",
159
+ "",
160
+ ]
161
+ )
162
+
163
+ lines.extend(["## Final Summary", "", valid["final_summary"], ""])
164
+ return "\n".join(lines)
165
+
166
+
167
+ def parse_args() -> argparse.Namespace:
168
+ parser = argparse.ArgumentParser(description=__doc__)
169
+ parser.add_argument("state", type=Path, help="completed review-result-state.json")
170
+ parser.add_argument("output", type=Path, help="review-report.md destination")
171
+ return parser.parse_args()
172
+
173
+
174
+ def main() -> int:
175
+ args = parse_args()
176
+ try:
177
+ state = json.loads(args.state.read_text(encoding="utf-8"))
178
+ report = render_report(state)
179
+ args.output.parent.mkdir(parents=True, exist_ok=True)
180
+ args.output.write_text(report, encoding="utf-8")
181
+ except (OSError, json.JSONDecodeError, ReportStateError, KeyError) as error:
182
+ print(json.dumps({"error": str(error)}, sort_keys=True))
183
+ return 1
184
+ return 0
185
+
186
+
187
+ if __name__ == "__main__":
188
+ sys.exit(main())
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """Generate a deterministic, path-safe workspace manifest for code review."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ SNAPSHOT_PROTOCOL_VERSION = 1
16
+
17
+
18
+ class SnapshotError(RuntimeError):
19
+ """Raised when the workspace snapshot cannot be generated safely."""
20
+
21
+
22
+ def _git(root: Path, *args: str) -> bytes:
23
+ try:
24
+ return subprocess.check_output(
25
+ ["git", "-c", "core.quotepath=false", *args],
26
+ cwd=root,
27
+ stderr=subprocess.PIPE,
28
+ )
29
+ except (OSError, subprocess.CalledProcessError) as error:
30
+ detail = getattr(error, "stderr", b"").decode("utf-8", "replace").strip()
31
+ raise SnapshotError(detail or f"git {' '.join(args)} failed") from error
32
+
33
+
34
+ def _decode(value: bytes) -> str:
35
+ return value.decode("utf-8", "surrogateescape")
36
+
37
+
38
+ def _sha256_bytes(value: bytes) -> str:
39
+ return hashlib.sha256(value).hexdigest()
40
+
41
+
42
+ def _file_identity(root: Path, relative_path: str) -> dict[str, Any] | None:
43
+ path = root / relative_path
44
+ if not path.is_symlink() and not path.is_file():
45
+ return None
46
+ if path.is_symlink():
47
+ content = os.readlink(path).encode("utf-8", "surrogateescape")
48
+ kind = "symlink"
49
+ else:
50
+ content = path.read_bytes()
51
+ kind = "file"
52
+ return {"kind": kind, "sha256": _sha256_bytes(content), "size": len(content)}
53
+
54
+
55
+ def _index_identity(root: Path, relative_path: str) -> str | None:
56
+ try:
57
+ return _decode(_git(root, "rev-parse", f":{relative_path}")).strip()
58
+ except SnapshotError:
59
+ return None
60
+
61
+
62
+ def _baseline_identity(root: Path, relative_path: str) -> str | None:
63
+ try:
64
+ return _decode(_git(root, "rev-parse", f"HEAD:{relative_path}")).strip()
65
+ except SnapshotError:
66
+ return None
67
+
68
+
69
+ def _parse_name_status(raw: bytes, layer: str, root: Path) -> list[dict[str, Any]]:
70
+ fields = raw.rstrip(b"\0").split(b"\0") if raw else []
71
+ entries: list[dict[str, Any]] = []
72
+ index = 0
73
+ while index < len(fields):
74
+ status = _decode(fields[index])
75
+ index += 1
76
+ code = status[:1]
77
+ if code in {"R", "C"}:
78
+ if index + 1 >= len(fields):
79
+ raise SnapshotError("malformed rename or copy record")
80
+ source = _decode(fields[index])
81
+ destination = _decode(fields[index + 1])
82
+ index += 2
83
+ entry = {
84
+ "layer": layer,
85
+ "status": code,
86
+ "similarity": status[1:] or None,
87
+ "source_path": source,
88
+ "path": destination,
89
+ }
90
+ else:
91
+ if index >= len(fields):
92
+ raise SnapshotError("malformed name-status record")
93
+ path = _decode(fields[index])
94
+ index += 1
95
+ entry = {"layer": layer, "status": code, "path": path}
96
+
97
+ entry["worktree_content"] = _file_identity(root, entry["path"])
98
+ baseline_path = entry.get("source_path", entry["path"])
99
+ entry["baseline_blob"] = _baseline_identity(root, baseline_path)
100
+ if layer == "staged" and code != "D":
101
+ entry["index_blob"] = _index_identity(root, entry["path"])
102
+ entries.append(entry)
103
+
104
+ return entries
105
+
106
+
107
+ def _untracked_entries(root: Path) -> list[dict[str, Any]]:
108
+ raw = _git(root, "ls-files", "--others", "--exclude-standard", "-z")
109
+ paths = [_decode(field) for field in raw.rstrip(b"\0").split(b"\0") if field]
110
+ return [
111
+ {
112
+ "layer": "untracked",
113
+ "status": "A",
114
+ "path": path,
115
+ "worktree_content": _file_identity(root, path),
116
+ "baseline_blob": None,
117
+ }
118
+ for path in paths
119
+ ]
120
+
121
+
122
+ def _repository_identity(root: Path) -> str:
123
+ try:
124
+ remote = _decode(_git(root, "config", "--get", "remote.origin.url")).strip()
125
+ except SnapshotError:
126
+ remote = ""
127
+ return remote or root.name
128
+
129
+
130
+ def _supporting_context(root: Path, paths: list[str]) -> list[dict[str, Any]]:
131
+ records: list[dict[str, Any]] = []
132
+ for relative_path in sorted(set(paths)):
133
+ candidate = (root / relative_path).resolve()
134
+ try:
135
+ candidate.relative_to(root)
136
+ except ValueError as error:
137
+ raise SnapshotError(
138
+ f"supporting context escapes repository: {relative_path}"
139
+ ) from error
140
+ identity = _file_identity(root, relative_path)
141
+ if identity is None:
142
+ raise SnapshotError(f"supporting context is not a file: {relative_path}")
143
+ records.append({"path": relative_path, "content": identity})
144
+ return records
145
+
146
+
147
+ def build_manifest(root: Path, supporting_paths: list[str] | None = None) -> dict[str, Any]:
148
+ """Return a deterministic manifest for the repository's current logical change."""
149
+ repository_root = Path(
150
+ _decode(_git(root, "rev-parse", "--show-toplevel")).strip()
151
+ ).resolve()
152
+ baseline = _decode(_git(repository_root, "rev-parse", "HEAD")).strip()
153
+
154
+ entries = [
155
+ *_parse_name_status(
156
+ _git(repository_root, "diff", "--cached", "--name-status", "-z", "-M"),
157
+ "staged",
158
+ repository_root,
159
+ ),
160
+ *_parse_name_status(
161
+ _git(repository_root, "diff", "--name-status", "-z", "-M"),
162
+ "unstaged",
163
+ repository_root,
164
+ ),
165
+ *_untracked_entries(repository_root),
166
+ ]
167
+ entries.sort(
168
+ key=lambda entry: (
169
+ entry["path"],
170
+ entry["layer"],
171
+ entry.get("source_path", ""),
172
+ )
173
+ )
174
+
175
+ manifest: dict[str, Any] = {
176
+ "protocol_version": SNAPSHOT_PROTOCOL_VERSION,
177
+ "repository_identity": _repository_identity(repository_root),
178
+ "baseline_identity": baseline,
179
+ "changes": entries,
180
+ "supporting_context": _supporting_context(
181
+ repository_root, supporting_paths or []
182
+ ),
183
+ }
184
+ canonical = json.dumps(
185
+ manifest,
186
+ ensure_ascii=True,
187
+ separators=(",", ":"),
188
+ sort_keys=True,
189
+ ).encode("utf-8")
190
+ manifest["snapshot_identity"] = _sha256_bytes(canonical)
191
+ return manifest
192
+
193
+
194
+ def parse_args() -> argparse.Namespace:
195
+ parser = argparse.ArgumentParser(description=__doc__)
196
+ parser.add_argument("--root", default=".", help="path inside the Git repository")
197
+ parser.add_argument(
198
+ "--supporting-context",
199
+ action="append",
200
+ default=[],
201
+ metavar="PATH",
202
+ help="unchanged repository-relative file required for review; repeat as needed",
203
+ )
204
+ return parser.parse_args()
205
+
206
+
207
+ def main() -> int:
208
+ args = parse_args()
209
+ try:
210
+ manifest = build_manifest(
211
+ Path(args.root).resolve(), args.supporting_context
212
+ )
213
+ except SnapshotError as error:
214
+ print(json.dumps({"error": str(error)}, sort_keys=True))
215
+ return 1
216
+
217
+ print(json.dumps(manifest, ensure_ascii=True, indent=2, sort_keys=True))
218
+ return 0
219
+
220
+
221
+ if __name__ == "__main__":
222
+ sys.exit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.123",
3
+ "version": "1.1.124",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/scaffold.js CHANGED
@@ -226,10 +226,12 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
226
226
 
227
227
  // Load Claude command adapter for skill conversion
228
228
  let convertSkillToCommand;
229
+ let getClaudeSkillAgentDefinitions;
229
230
  let convertSkillToCodex;
230
231
  if (platform === 'claude') {
231
232
  const commandAdapter = await loadAdapter('claude', 'command-adapter.js');
232
233
  convertSkillToCommand = commandAdapter.convertSkillToCommand;
234
+ getClaudeSkillAgentDefinitions = commandAdapter.getClaudeSkillAgentDefinitions;
233
235
  } else if (platform === 'codex') {
234
236
  const codexSkillAdapter = await loadAdapter('codex', 'skill-adapter.js');
235
237
  convertSkillToCodex = codexSkillAdapter.convertSkill;
@@ -321,6 +323,19 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
321
323
  await fs.copy(path.join(corePath, subdir), path.join(assetTargetDir, subdir));
322
324
  }
323
325
  }
326
+
327
+ const roleManifestPath = path.join(corePath, 'assets', 'reviewer-role.json');
328
+ const roleManifest = await fs.pathExists(roleManifestPath)
329
+ ? await fs.readJson(roleManifestPath)
330
+ : null;
331
+ const agentDefinitions = getClaudeSkillAgentDefinitions(skillName, roleManifest);
332
+ if (agentDefinitions.length > 0) {
333
+ const agentsDir = path.join(projectRoot, '.claude', 'agents');
334
+ await fs.ensureDir(agentsDir);
335
+ for (const definition of agentDefinitions) {
336
+ await fs.writeFile(path.join(agentsDir, definition.fileName), definition.content);
337
+ }
338
+ }
324
339
  console.log(chalk.green(` ✓ .claude/commands/${skillName}.md`));
325
340
  } else if (platform === 'codex') {
326
341
  const content = await fs.readFile(skillMdPath, 'utf8');