prizmkit 1.1.123 → 1.1.125

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 (26) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +75 -0
  3. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +16 -5
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +121 -9
  5. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -4
  6. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  7. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +4 -3
  8. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -3
  9. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +7 -2
  10. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -4
  11. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +4 -4
  12. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -4
  13. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +6 -5
  14. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +2 -1
  15. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +197 -9
  16. package/bundled/dev-pipeline/tests/test_unified_cli.py +31 -16
  17. package/bundled/skills/_metadata.json +4 -4
  18. package/bundled/skills/prizmkit-code-review/SKILL.md +113 -110
  19. package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +19 -0
  20. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +24 -21
  21. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +95 -67
  22. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +162 -0
  23. package/package.json +1 -1
  24. package/src/scaffold.js +15 -0
  25. package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +0 -90
  26. package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +0 -186
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """Render the final review-report.md from minimal review data."""
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", "BLOCKED"}
13
+ DISPOSITIONS = ("fixed", "rejected", "unresolved")
14
+
15
+
16
+ class ReportStateError(ValueError):
17
+ """Raised when review data cannot produce a valid final report."""
18
+
19
+
20
+ def _require_count(data: dict[str, Any], name: str) -> int:
21
+ value = data.get(name)
22
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
23
+ raise ReportStateError(f"{name} must be a non-negative integer")
24
+ return value
25
+
26
+
27
+ def validate_data(data: Any) -> dict[str, Any]:
28
+ if not isinstance(data, dict):
29
+ raise ReportStateError("review data must be a JSON object")
30
+
31
+ verdict = data.get("verdict")
32
+ if verdict not in VERDICTS:
33
+ raise ReportStateError(f"verdict has invalid value: {verdict!r}")
34
+
35
+ rounds_completed = _require_count(data, "rounds_completed")
36
+ if rounds_completed > 10:
37
+ raise ReportStateError("rounds_completed cannot exceed ten")
38
+
39
+ findings = data.get("findings")
40
+ if not isinstance(findings, list):
41
+ raise ReportStateError("findings must be an array")
42
+
43
+ summary = data.get("summary")
44
+ if not isinstance(summary, str) or not summary.strip():
45
+ raise ReportStateError("summary must be a non-empty string")
46
+
47
+ for index, finding in enumerate(findings, start=1):
48
+ if not isinstance(finding, dict):
49
+ raise ReportStateError(f"finding {index} must be an object")
50
+ required = {
51
+ "title",
52
+ "severity",
53
+ "location",
54
+ "problem",
55
+ "failure_scenario",
56
+ "disposition",
57
+ }
58
+ missing = sorted(required - finding.keys())
59
+ if missing:
60
+ raise ReportStateError(
61
+ f"finding {index} missing fields: {', '.join(missing)}"
62
+ )
63
+ disposition = finding["disposition"]
64
+ if not isinstance(disposition, str) or not disposition.startswith(DISPOSITIONS):
65
+ raise ReportStateError(f"finding {index} has invalid disposition")
66
+
67
+ unresolved = sum(
68
+ finding["disposition"].startswith("unresolved") for finding in findings
69
+ )
70
+ blocker = data.get("blocker")
71
+
72
+ if verdict == "PASS" and unresolved:
73
+ raise ReportStateError("PASS cannot contain unresolved findings")
74
+ if verdict == "NEEDS_FIXES" and not unresolved:
75
+ raise ReportStateError("NEEDS_FIXES requires an unresolved finding")
76
+ if verdict == "BLOCKED":
77
+ if not isinstance(blocker, str) or not blocker.strip():
78
+ raise ReportStateError("BLOCKED requires a non-empty blocker")
79
+ if unresolved:
80
+ raise ReportStateError("BLOCKED cannot classify code findings as unresolved")
81
+ elif blocker is not None:
82
+ raise ReportStateError("only BLOCKED may include blocker")
83
+
84
+ return data
85
+
86
+
87
+ def _finding_totals(findings: list[dict[str, Any]]) -> tuple[int, int, int]:
88
+ fixed = sum(finding["disposition"].startswith("fixed") for finding in findings)
89
+ rejected = sum(
90
+ finding["disposition"].startswith("rejected") for finding in findings
91
+ )
92
+ unresolved = sum(
93
+ finding["disposition"].startswith("unresolved") for finding in findings
94
+ )
95
+ return fixed, rejected, unresolved
96
+
97
+
98
+ def render_report(data: Any) -> str:
99
+ valid = validate_data(data)
100
+ findings = valid["findings"]
101
+ fixed, rejected, unresolved = _finding_totals(findings)
102
+
103
+ lines = [
104
+ "# Review Report",
105
+ "",
106
+ f"## Verdict: {valid['verdict']}",
107
+ f"## Rounds Completed: {valid['rounds_completed']}",
108
+ f"## Total Findings: {len(findings)} → Fixed: {fixed}, Rejected: {rejected}, Unresolved: {unresolved}",
109
+ "",
110
+ ]
111
+
112
+ if findings:
113
+ lines.extend(["## Findings", ""])
114
+ for index, finding in enumerate(findings, start=1):
115
+ lines.extend(
116
+ [
117
+ f"### Finding {index}: {finding['title']}",
118
+ "",
119
+ f"- Severity: {finding['severity']}",
120
+ f"- Location: {finding['location']}",
121
+ f"- Problem: {finding['problem']}",
122
+ f"- Failure Scenario: {finding['failure_scenario']}",
123
+ f"- Disposition: {finding['disposition']}",
124
+ "",
125
+ ]
126
+ )
127
+
128
+ if valid["verdict"] == "BLOCKED":
129
+ lines.extend(["## Blocker", "", valid["blocker"], ""])
130
+
131
+ lines.extend(["## Summary", "", valid["summary"], ""])
132
+ return "\n".join(lines)
133
+
134
+
135
+ def parse_args() -> argparse.Namespace:
136
+ parser = argparse.ArgumentParser(description=__doc__)
137
+ parser.add_argument("output", type=Path, help="review-report.md destination")
138
+ parser.add_argument(
139
+ "input",
140
+ nargs="?",
141
+ type=Path,
142
+ help="optional JSON input path; reads standard input when omitted",
143
+ )
144
+ return parser.parse_args()
145
+
146
+
147
+ def main() -> int:
148
+ args = parse_args()
149
+ try:
150
+ raw = args.input.read_text(encoding="utf-8") if args.input else sys.stdin.read()
151
+ data = json.loads(raw)
152
+ report = render_report(data)
153
+ args.output.parent.mkdir(parents=True, exist_ok=True)
154
+ args.output.write_text(report, encoding="utf-8")
155
+ except (OSError, json.JSONDecodeError, ReportStateError, KeyError) as error:
156
+ print(json.dumps({"error": str(error)}, sort_keys=True))
157
+ return 1
158
+ return 0
159
+
160
+
161
+ if __name__ == "__main__":
162
+ 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.125",
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');
@@ -1,90 +0,0 @@
1
- # Reviewer Workspace Protocol
2
-
3
- Use this protocol whenever `/prizmkit-code-review` delegates review to an independent Reviewer. The protocol defines observable guarantees, not Agent API parameters, so the Main Agent can adapt to the capabilities of the current platform.
4
-
5
- ## Protocol Invariants
6
-
7
- 1. **Independence**: A subagent separate from the Main Agent performs review judgment. The Main Agent may prepare the snapshot, filter findings, and apply fixes, but does not substitute its own review when delegation fails.
8
- 2. **Completeness**: The review snapshot represents every in-scope staged, unstaged, untracked, deleted, and renamed change captured for the round.
9
- 3. **Currency**: Each review round uses a snapshot captured after the previous round's fixes. Never reuse a prior-round snapshot as evidence of current state.
10
- 4. **Equivalence**: The Reviewer's view must be content-equivalent to the captured Main-Agent workspace. Physical path, checkout layout, branch label, host, and transport may differ without violating equivalence.
11
- 5. **Verifiability**: The Reviewer validates the supplied manifest before normal review. Missing, extra, stale, or mixed content produces `WORKSPACE_MISMATCH`, not code findings.
12
- 6. **Read-only review**: The Reviewer does not modify files or apply fixes. This keeps review evidence stable and preserves clear fix ownership.
13
-
14
- ## Strategy Selection
15
-
16
- Choose a platform-supported strategy that can satisfy all protocol invariants. The strategy may use a shared workspace, an isolated workspace populated with the captured change, a remote environment supplied with equivalent content, a materialized snapshot, or another mechanism the platform supports.
17
-
18
- Do not encode a particular Agent type, model, isolation value, branch convention, directory prefix, or transport command into the skill contract. Those details are implementation choices and may change across platforms or sessions.
19
-
20
- Prefer the least elaborate strategy that can prove equivalence. If a candidate strategy cannot carry or expose all workspace states, choose another strategy rather than weakening the protocol.
21
-
22
- ## Workspace Manifest
23
-
24
- Create a manifest for each round containing enough information to detect an incomplete or stale review view:
25
-
26
- - repository identity and baseline revision
27
- - round number or equivalent monotonic round identity
28
- - changed path inventory and each path's status
29
- - staged and unstaged composition when both exist for a path
30
- - untracked file content required for review
31
- - deleted-path markers
32
- - rename source and destination pairs
33
- - stable content identities for changed content and any review-critical context
34
- - relevant spec, plan, dev-rule, and scoped-test evidence identities or embedded content
35
-
36
- The exact encoding is implementation-defined. Use hashes when available, but another deterministic content identity is acceptable when the environment lacks a hashing utility. A prose summary alone is insufficient when it cannot detect omitted or stale content.
37
-
38
- ## Review Payload
39
-
40
- Provide the Reviewer with both:
41
-
42
- 1. The manifest used for verification.
43
- 2. A reviewable representation of every manifest entry.
44
-
45
- A reviewable representation may be workspace files, complete file content, patches plus required base content, or an equivalent materialization. Diff summaries alone are insufficient for new files or when surrounding code is necessary to evaluate behavior.
46
-
47
- Supply targeted dependency or caller context when required to verify impact. Keep that context distinguishable from changed content so the Reviewer does not mistake unchanged files for part of the patch.
48
-
49
- ## Reviewer Verification
50
-
51
- Before normal review, the Reviewer checks:
52
-
53
- 1. Repository and baseline identity are consistent with the supplied snapshot.
54
- 2. Every changed path in the manifest is available in the review payload.
55
- 3. No unexplained changed path appears outside the manifest.
56
- 4. Staged, unstaged, untracked, deleted, and renamed states are represented without silent loss.
57
- 5. Content identities match the manifest.
58
- 6. The snapshot belongs to the current review round.
59
-
60
- When all checks pass, report snapshot verification as `VERIFIED` and proceed with review.
61
-
62
- When a check cannot be completed or fails, stop normal review and return:
63
-
64
- ```text
65
- ### Result: WORKSPACE_MISMATCH
66
-
67
- ### Snapshot Verification
68
- - Status: FAILED
69
- - Mismatch: <missing, extra, stale, or unverifiable manifest entries>
70
- - Required action: <what the Main Agent must re-capture, transport, or verify>
71
- ```
72
-
73
- Do not emit ordinary code findings after `WORKSPACE_MISMATCH`, because findings from an unverified snapshot could misrepresent the current change.
74
-
75
- ## Round Refresh
76
-
77
- After accepted fixes are applied:
78
-
79
- 1. Discard the previous manifest as current evidence.
80
- 2. Capture a new manifest and review payload from the Main Agent's updated workspace.
81
- 3. Establish a fresh independent Reviewer view using any valid strategy.
82
- 4. Repeat Reviewer verification before the next review.
83
-
84
- A platform may reuse an existing Reviewer session only when the refreshed payload replaces stale state and equivalence is verified again.
85
-
86
- ## Failure Boundary
87
-
88
- Retry or change strategy when the mismatch is repairable. Stop when the platform cannot provide an independent Reviewer with a verifiable equivalent snapshot.
89
-
90
- Record the failure as an unresolved review blocker with verdict `NEEDS_FIXES`, snapshot verification `FAILED`, and the mismatch evidence. Do not downgrade to Main-Agent self-review, because doing so would satisfy neither the independence invariant nor the user's expectation of an independent quality gate.
@@ -1,186 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Loop Exit Gatekeeper for prizmkit-code-review skill.
4
-
5
- Stateless design: all loop state is passed via stdin and returned via stdout.
6
- The caller (AI model) tracks `findings_history` and `max_rounds` between calls.
7
- No runtime files are created — the script is a pure function over JSON.
8
-
9
- The model supplies the current round in each call — the script does not auto-increment
10
- rounds, because the gate is called twice per loop iteration (after Reviewer returns
11
- and after Main Agent filters), and only the model knows when a full cycle completes.
12
-
13
- Usage:
14
- echo '{"reviewer_result":"NEEDS_FIXES","accepted_count":2,"findings_count":5,"round":1,"findings_history":[],"max_rounds":3,"filtering_done":true}' | python3 check_loop.py
15
-
16
- Input JSON schema:
17
- {
18
- "reviewer_result": "PASS" | "NEEDS_FIXES",
19
- "accepted_count": int,
20
- "findings_count": int,
21
- "round": int,
22
- "findings_history": [int, ...],
23
- "max_rounds": int,
24
- "filtering_done": bool
25
- }
26
-
27
- Output JSON schema:
28
- {
29
- "endLoop": bool,
30
- "reason": str,
31
- "verdict": "PASS" | "NEEDS_FIXES" | null,
32
- "round": int,
33
- "maxRounds": int,
34
- "divergenceWarning": bool,
35
- "findings_history": [int, ...]
36
- }
37
-
38
- Exit conditions (checked in order):
39
- 1. Reviewer returned PASS → endLoop=true, verdict=PASS
40
- 2. NEEDS_FIXES before filtering → endLoop=false, wait for filtering
41
- 3. Filtered findings all rejected → endLoop=true, verdict=PASS
42
- 4. Max rounds reached after filtering → endLoop=true, verdict=NEEDS_FIXES
43
- 5. Otherwise → endLoop=false
44
-
45
- Divergence detection:
46
- If findings_count strictly increases for 3 consecutive rounds, set divergenceWarning=true.
47
- This is a warning only — it does not force loop exit.
48
- """
49
-
50
- import json
51
- import sys
52
-
53
- DEFAULT_MAX_ROUNDS = 3
54
-
55
-
56
- def check_divergence(findings_history):
57
- """Return True if the last 3 entries in findings_history are strictly increasing."""
58
- if len(findings_history) < 3:
59
- return False
60
- last_three = findings_history[-3:]
61
- return last_three[0] < last_three[1] < last_three[2]
62
-
63
-
64
- def record_findings_count(findings_history, current_round, findings_count):
65
- """Record findings_count once per round index, updating duplicate calls for the same round."""
66
- if current_round <= 0:
67
- return findings_history
68
-
69
- target_index = current_round - 1
70
- while len(findings_history) < target_index:
71
- findings_history.append(0)
72
-
73
- if len(findings_history) == target_index:
74
- findings_history.append(findings_count)
75
- else:
76
- findings_history[target_index] = findings_count
77
-
78
- return findings_history
79
-
80
-
81
- def check_exit_condition(data):
82
- """Determine whether the loop should exit. Pure function — no side effects.
83
-
84
- Round is model-supplied — the script does not auto-increment it because
85
- the gate is called twice per loop iteration (after Step 2 and Step 3).
86
- """
87
- reviewer_result = data.get("reviewer_result")
88
- accepted_count = data.get("accepted_count", 0)
89
- findings_count = data.get("findings_count", 0)
90
- current_round = data.get("round", 0)
91
- findings_history = list(data.get("findings_history", []))
92
- max_rounds = data.get("max_rounds", DEFAULT_MAX_ROUNDS)
93
- filtering_done = data.get("filtering_done", reviewer_result == "PASS")
94
-
95
- findings_history = record_findings_count(
96
- findings_history, current_round, findings_count
97
- )
98
- divergence_warning = check_divergence(findings_history)
99
-
100
- # Condition 1: Reviewer returned PASS
101
- if reviewer_result == "PASS":
102
- return {
103
- "endLoop": True,
104
- "reason": f"Round {current_round}: Reviewer returned PASS",
105
- "verdict": "PASS",
106
- "round": current_round,
107
- "maxRounds": max_rounds,
108
- "divergenceWarning": False,
109
- "findings_history": findings_history,
110
- }
111
-
112
- # Condition 2: Reviewer returned findings, but filtering has not happened yet
113
- if reviewer_result == "NEEDS_FIXES" and not filtering_done:
114
- return {
115
- "endLoop": False,
116
- "reason": f"Round {current_round}: reviewer returned findings; filter before applying exit conditions",
117
- "verdict": None,
118
- "round": current_round,
119
- "maxRounds": max_rounds,
120
- "divergenceWarning": divergence_warning,
121
- "findings_history": findings_history,
122
- }
123
-
124
- # Condition 3: All findings rejected after filtering
125
- if reviewer_result == "NEEDS_FIXES" and accepted_count == 0:
126
- return {
127
- "endLoop": True,
128
- "reason": f"Round {current_round}: all findings rejected by main agent",
129
- "verdict": "PASS",
130
- "round": current_round,
131
- "maxRounds": max_rounds,
132
- "divergenceWarning": False,
133
- "findings_history": findings_history,
134
- }
135
-
136
- # Condition 4: Max rounds reached
137
- if current_round >= max_rounds:
138
- return {
139
- "endLoop": True,
140
- "reason": f"Hard limit: max {max_rounds} rounds reached",
141
- "verdict": "NEEDS_FIXES",
142
- "round": current_round,
143
- "maxRounds": max_rounds,
144
- "divergenceWarning": divergence_warning,
145
- "findings_history": findings_history,
146
- }
147
-
148
- # Condition 5: Continue loop
149
- next_round = current_round + 1
150
- return {
151
- "endLoop": False,
152
- "reason": f"Round {current_round}: {accepted_count} findings accepted, continuing to round {next_round}",
153
- "verdict": None,
154
- "round": current_round,
155
- "maxRounds": max_rounds,
156
- "divergenceWarning": divergence_warning,
157
- "findings_history": findings_history,
158
- }
159
-
160
-
161
- def main():
162
- # Read input from stdin
163
- try:
164
- data = json.load(sys.stdin)
165
- except json.JSONDecodeError as e:
166
- print(
167
- json.dumps(
168
- {
169
- "endLoop": False,
170
- "reason": f"Invalid JSON input: {e}",
171
- "verdict": None,
172
- "round": 0,
173
- "maxRounds": DEFAULT_MAX_ROUNDS,
174
- "divergenceWarning": False,
175
- "findings_history": [],
176
- }
177
- )
178
- )
179
- sys.exit(1)
180
-
181
- result = check_exit_condition(data)
182
- print(json.dumps(result))
183
-
184
-
185
- if __name__ == "__main__":
186
- main()