prizmkit 1.1.124 → 1.1.126

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 (32) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +4 -6
  3. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +49 -3
  4. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +75 -14
  5. package/bundled/dev-pipeline/prizmkit_runtime/runtime_helper.py +37 -0
  6. package/bundled/dev-pipeline/prizmkit_runtime/task_checkout.py +267 -0
  7. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -4
  8. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  9. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +5 -16
  10. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -14
  11. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +8 -4
  12. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +9 -8
  13. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +12 -15
  14. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -16
  15. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +9 -8
  16. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +8 -4
  17. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +1 -1
  18. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +1 -1
  19. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +206 -3
  20. package/bundled/dev-pipeline/tests/test_runtime_helper.py +36 -0
  21. package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
  22. package/bundled/skills/_metadata.json +4 -4
  23. package/bundled/skills/prizmkit-code-review/SKILL.md +130 -103
  24. package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +3 -5
  25. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +89 -38
  26. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +44 -62
  27. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +249 -143
  28. package/package.json +1 -1
  29. package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +0 -179
  30. package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +0 -118
  31. package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +0 -380
  32. package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +0 -222
@@ -1,6 +1,6 @@
1
1
  # Reviewer Prompt Contract
2
2
 
3
- Use this complete template for every Reviewer execution in Phase 1. Fill documented placeholders only. Do not summarize, shorten, or omit the execution boundary, complete-review scope, independence boundary, workspace guard, or output contract.
3
+ Use this complete template for every Reviewer execution. Fill the documented placeholders with current-round context. Do not include previous-round findings, verdicts, filtering decisions, rejection reasons, or repair explanations.
4
4
 
5
5
  ```text
6
6
  You are the sole independent read-only Reviewer for this round of `/prizmkit-code-review`.
@@ -9,61 +9,60 @@ You are the sole independent read-only Reviewer for this round of `/prizmkit-cod
9
9
 
10
10
  Perform the complete review yourself.
11
11
 
12
- Do not create, schedule, resume, continue, request, or coordinate any separate downstream execution unit. Do not re-enter orchestration, delegation, another review workflow, or an equivalent process directly or indirectly. Do not ask the Main Agent to create a helper on your behalf.
12
+ Do not create, schedule, resume, continue, request, or coordinate any separate downstream execution unit. Do not re-enter orchestration, invoke a skill, delegate review work, or ask the Main Agent to create a helper.
13
13
 
14
- Do not modify, create, delete, rename, stage, or commit workspace files. Do not run a command or tool that changes the review view.
14
+ Do not modify, create, delete, rename, stage, or commit workspace files. Use only read-only inspection capabilities.
15
15
 
16
- If you cannot complete the review within this execution context, stop and return `REVIEW_INFRASTRUCTURE_BLOCKED`. Do not delegate or provide a partial PASS.
16
+ If you cannot complete the review in this execution context, return `BLOCKED` with the missing capability or context. Do not delegate and do not return a partial `PASS`.
17
17
 
18
18
  ## Independent Input Boundary
19
19
 
20
- Review only the current state supplied below. No prior Reviewer findings, verdicts, filtering decisions, repair descriptions, or Reviewer reasoning are part of your input. Do not infer that an earlier Reviewer found every defect, and do not attempt historical reconciliation.
20
+ Review only the current state supplied below. No previous-round findings, verdicts, filtering decisions, rejection reasons, repair descriptions, or Reviewer reasoning are part of your input.
21
21
 
22
- ## Spec Goals
22
+ ## Goals and Acceptance Criteria
23
23
 
24
- {goals and acceptance criteria from spec.md}
24
+ {goals and acceptance criteria from spec.md, or "No spec artifact found; infer the requested behavior from the user goal and current change."}
25
25
 
26
- ## Plan Decisions
26
+ ## Plan and Intended Change
27
27
 
28
- {architecture decisions and task list from plan.md}
28
+ {relevant architecture decisions and completed tasks from plan.md, or a concise current task description}
29
29
 
30
30
  ## Applicable Rules and Contracts
31
31
 
32
32
  {relevant Prizm docs, project rules, public and internal contracts, or "No additional project rules identified."}
33
33
 
34
- ## Scoped Test Evidence
34
+ ## Test Evidence
35
35
 
36
- {current test report summary and identities. If absent, write "No scoped /prizmkit-test report found." Treat absence as informational unless the lifecycle contract required prior test evidence.}
36
+ {current scoped test evidence, or "No scoped test report found."}
37
37
 
38
- ## Workspace Manifest
38
+ ## Current Change
39
39
 
40
- {repository identity, baseline identity, snapshot identity, path inventory, change states, staged and unstaged composition, rename/delete state, content identities, and supporting-context identities}
40
+ - Repository and Reviewer-visible checkout: {path or platform-visible location}
41
+ - Base revision: {base revision when known}
42
+ - Changed paths and states: {staged, unstaged, untracked, deleted, and renamed inventory}
43
+ - Review access: {active checkout, or forced worktree used only for baseline and surrounding context}
41
44
 
42
- ## Review Payload
45
+ ## Supplied Git Diff
43
46
 
44
- {complete logical representation of every manifest entry plus distinguishable unchanged caller and dependent context}
47
+ ### Staged
45
48
 
46
- ## Reviewer Strategy
49
+ {complete staged diff, or "No staged changes."}
47
50
 
48
- {concise diagnostic description of how this platform established observational access and made mutation, downstream execution, delegation-equivalent behavior, and orchestration re-entry unavailable}
51
+ ### Unstaged
49
52
 
50
- ## Workspace Equivalence Guard
53
+ {complete unstaged diff, or "No unstaged tracked changes."}
51
54
 
52
- Validate the manifest and payload before ordinary review:
55
+ ## Supplied Untracked Files
53
56
 
54
- 1. Every manifest path and state is represented.
55
- 2. Staged, unstaged, untracked, deleted, and renamed changes are preserved.
56
- 3. Content and snapshot identities match.
57
- 4. Supporting caller or dependent context is distinguishable from changed content.
58
- 5. No unexplained changed path, stale content, mixed-round content, or unverifiable entry exists.
57
+ {path and full content of every untracked file, or "No untracked files."}
59
58
 
60
- Physical path, checkout layout, host, or transport may differ. Content-equivalent current state is the invariant.
59
+ Treat the supplied diffs, path states, and untracked file contents as the authoritative current change. Use read-only file inspection for targeted surrounding context, callers, dependents, contracts, and tests. Do not depend on shell or Git execution being available.
61
60
 
62
- If equivalence cannot be proven, stop before ordinary review and return `WORKSPACE_MISMATCH` with concrete mismatch evidence. Do not return code findings from an unverified snapshot.
61
+ If a platform-forced worktree does not contain the active checkout's dirty or untracked changes, use the worktree only for baseline and surrounding-context reads. You are not required to calculate hashes or prove equivalence between physical checkouts. If an essential changed file or diff is missing from the supplied input, return `BLOCKED` and identify exactly what is missing.
63
62
 
64
- ## Complete Current-State Review
63
+ ## Complete Current-Change Review
65
64
 
66
- Review the complete current change set independently. Do not limit inspection to files changed by the latest repair or to previously identified categories.
65
+ Review the complete current change independently. Do not limit inspection to one file or one suspected issue.
67
66
 
68
67
  Evaluate applicable dimensions:
69
68
 
@@ -79,60 +78,43 @@ Evaluate applicable dimensions:
79
78
  10. Test adequacy and evidence compatibility
80
79
  11. Project-rule compliance
81
80
 
82
- Use both inspection directions inside this one review:
81
+ Use both directions in this review:
83
82
 
84
83
  - Code-forward: changed code → affected symbols → callers and dependents → concrete failure scenarios
85
84
  - Requirement-backward: requirements and contracts → required behavior → implementation path → side effects → test evidence
86
85
 
87
- Read unchanged callers, dependents, or shared patterns when needed to evaluate impact.
86
+ Read unchanged callers, dependents, shared patterns, or tests when needed. Prefer targeted read-only inspection. Do not perform a broad repository scan without a concrete cross-cutting reason.
88
87
 
89
- ## Review Efficiency
88
+ ## Finding Standard
90
89
 
91
- Complete the review directly instead of delegating it. Prefer direct read-only inspection and targeted verification.
92
-
93
- Use existing valid test evidence when it answers the question. Prefer the narrowest verification that can establish or refute a concrete issue. Do not rerun an equivalent check when relevant code and inputs have not changed.
94
-
95
- Avoid broad builds, tests, lint, or type checks as exploratory defaults. A broad command remains valid when a concrete cross-cutting risk genuinely requires it; state why narrower evidence is insufficient.
96
-
97
- Non-normative examples:
98
-
99
- - A repository-wide test or build without a concrete reason is often redundant with the formal Test Gate.
100
- - A focused module or test-case invocation is usually better when investigating one failure scenario.
101
-
102
- These examples do not prohibit command names. The governing rule is targeted, non-duplicative evidence gathering.
103
-
104
- ## Finding Coverage
105
-
106
- Report every concrete issue you find, including uncertain or low-severity issues. Do not filter for importance or confidence; the Main Agent performs downstream filtering. Exclude pure style or naming preferences that cannot cause incorrect behavior, contract drift, regression, security weakness, or misleading evidence.
90
+ Report every concrete defect you find, including low-severity issues. Exclude pure style preferences and speculative concerns that lack a concrete wrong behavior, contract violation, regression path, security weakness, data-integrity risk, or misleading test result.
107
91
 
108
92
  ## Output Contract
109
93
 
110
94
  Respond with exactly this structure:
111
95
 
112
- ### Result: PASS | NEEDS_FIXES | WORKSPACE_MISMATCH | REVIEW_INFRASTRUCTURE_BLOCKED
96
+ ### Result: PASS | FINDINGS | BLOCKED
113
97
 
114
- ### Execution Compliance
115
- - Status: COMPLIANT | BLOCKED
116
- - Descendant execution units created: 0
117
- - Delegation depth: 0
98
+ ### Findings
118
99
 
119
- ### Snapshot Verification
120
- - Status: VERIFIED | FAILED
121
- - Identity: <snapshot identity or mismatch summary>
100
+ If the result is `PASS`, write:
122
101
 
123
- ### Findings
124
- (If PASS, write "No issues found." If WORKSPACE_MISMATCH or REVIEW_INFRASTRUCTURE_BLOCKED, include only blocker evidence and required action.)
102
+ No issues found.
125
103
 
126
- #### Finding N
104
+ If the result is `BLOCKED`, write only the missing capability or essential context and the required action.
105
+
106
+ If the result is `FINDINGS`, repeat this block for every concrete issue:
107
+
108
+ #### Finding N: <short title>
127
109
  - Severity: high | medium | low
128
- - Confidence: high | medium | low
129
- - Dimension: requirement-alignment | defect | completeness | error-handling | security | data-integrity | concurrency | contract | compatibility | test-quality | rules-compliance
130
110
  - Location: filepath:line (or "project-level")
131
111
  - Problem: concrete wrong behavior and why it matters
132
112
  - Failure scenario: inputs or state that produce the wrong result
133
- - Suggestion: recommended repair approach
113
+ - Reasoning: concise evidence connecting the current code to the failure
114
+ - Suggested repair: smallest safe repair approach
134
115
  - Verification: how to prove the repair
135
116
 
136
117
  ### Summary
137
- One or two sentences describing the verified current state and overall result. Do not expose private reasoning traces.
118
+
119
+ One or two sentences describing the current change and result. Do not expose private reasoning traces.
138
120
  ```
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Render review-report.md from completed structured review state."""
2
+ """Initialize and append validated sections to review-report.md."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
@@ -9,180 +9,286 @@ import sys
9
9
  from pathlib import Path
10
10
  from typing import Any
11
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",
12
+ VERDICTS = {"PASS", "NEEDS_FIXES", "BLOCKED"}
13
+ RISKS = {"low", "medium", "high"}
14
+ EVENTS = {
15
+ "risk-assessment",
16
+ "main-review-round",
17
+ "repair-verification",
18
+ "reviewer-running",
19
+ "reviewer-result",
20
+ "infrastructure-failure",
21
+ "final-verification",
27
22
  }
23
+ FINAL_HEADING = "## Final Result"
28
24
 
29
25
 
30
26
  class ReportStateError(ValueError):
31
- """Raised when persisted state cannot produce a trustworthy report."""
27
+ """Raised when an operation would produce an invalid report lifecycle."""
32
28
 
33
29
 
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
30
+ def _require_object(data: Any) -> dict[str, Any]:
31
+ if not isinstance(data, dict):
32
+ raise ReportStateError("input must be a JSON object")
33
+ return data
34
+
39
35
 
36
+ def _require_text(data: dict[str, Any], name: str) -> str:
37
+ value = data.get(name)
38
+ if not isinstance(value, str) or not value.strip():
39
+ raise ReportStateError(f"{name} must be a non-empty string")
40
+ return value.strip()
40
41
 
41
- def _require_count(state: dict[str, Any], name: str) -> int:
42
- value = state.get(name)
42
+
43
+ def _require_count(data: dict[str, Any], name: str, *, maximum: int | None = None) -> int:
44
+ value = data.get(name)
43
45
  if not isinstance(value, int) or isinstance(value, bool) or value < 0:
44
46
  raise ReportStateError(f"{name} must be a non-negative integer")
47
+ if maximum is not None and value > maximum:
48
+ raise ReportStateError(f"{name} cannot exceed {maximum}")
45
49
  return value
46
50
 
47
51
 
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(
52
+ def _require_risk(data: dict[str, Any]) -> str:
53
+ risk = data.get("risk")
54
+ if risk not in RISKS:
55
+ raise ReportStateError(f"risk has invalid value: {risk!r}")
56
+ return risk
57
+
58
+
59
+ def _report_text(path: Path) -> str:
60
+ try:
61
+ return path.read_text(encoding="utf-8")
62
+ except FileNotFoundError as error:
63
+ raise ReportStateError("report must be initialized before append") from error
64
+
65
+
66
+ def _ensure_appendable(path: Path) -> None:
67
+ text = _report_text(path)
68
+ if "## Status: IN_PROGRESS" not in text:
69
+ raise ReportStateError("report is not an initialized in-progress execution")
70
+ if FINAL_HEADING in text:
71
+ raise ReportStateError("cannot append after Final Result")
72
+
73
+
74
+ def _append_section(path: Path, section: str) -> None:
75
+ with path.open("a", encoding="utf-8") as report:
76
+ report.write(section.rstrip() + "\n\n")
77
+
78
+
79
+ def initialize_report(path: Path) -> None:
80
+ """Start a new execution and remove stale prior execution content."""
81
+ path.parent.mkdir(parents=True, exist_ok=True)
82
+ path.write_text("# Review Report\n\n## Status: IN_PROGRESS\n\n", encoding="utf-8")
83
+
84
+
85
+ def _render_event(data: dict[str, Any]) -> str:
86
+ event = data.get("event")
87
+ if event not in EVENTS:
88
+ raise ReportStateError(f"event has invalid value: {event!r}")
89
+
90
+ if event == "risk-assessment":
91
+ risk = _require_risk(data)
92
+ reasons = data.get("reasons")
93
+ if not isinstance(reasons, list) or not reasons or not all(
94
+ isinstance(reason, str) and reason.strip() for reason in reasons
95
+ ):
96
+ raise ReportStateError("reasons must be a non-empty string array")
97
+ limit = {"low": 0, "medium": 1, "high": 2}[risk]
98
+ lines = [
99
+ "## Risk Assessment",
100
+ "",
101
+ f"- Risk: {risk}",
102
+ f"- Independent Reviewer Limit: {limit}",
103
+ "- Reasons:",
104
+ ]
105
+ lines.extend(f" - {reason.strip()}" for reason in reasons)
106
+ return "\n".join(lines)
107
+
108
+ if event == "main-review-round":
109
+ round_number = _require_count(data, "round", maximum=10)
110
+ if round_number < 1:
111
+ raise ReportStateError("round must be at least 1")
112
+ findings = _require_count(data, "findings")
113
+ accepted = _require_count(data, "accepted")
114
+ rejected = _require_count(data, "rejected")
115
+ if accepted + rejected != findings:
116
+ raise ReportStateError("accepted + rejected must equal findings")
117
+ return "\n".join(
118
+ [
119
+ f"## Main Review Round {round_number}",
120
+ "",
121
+ "- Status: COMPLETED",
122
+ f"- Findings: {findings}",
123
+ f"- Accepted: {accepted}",
124
+ f"- Rejected: {rejected}",
125
+ f"- Next: {_require_text(data, 'next')}",
126
+ ]
127
+ )
128
+
129
+ if event == "repair-verification":
130
+ source = _require_text(data, "source")
131
+ if source not in {"main", "reviewer-1", "reviewer-2"}:
132
+ raise ReportStateError(f"source has invalid value: {source!r}")
133
+ return "\n".join(
134
+ [
135
+ "## Repair Verification",
136
+ "",
137
+ f"- Source: {source}",
138
+ f"- Fixed Findings: {_require_count(data, 'fixed_findings')}",
139
+ f"- Verification: {_require_text(data, 'verification')}",
140
+ f"- Next: {_require_text(data, 'next')}",
141
+ ]
142
+ )
143
+
144
+ if event == "reviewer-running":
145
+ reviewer = _require_count(data, "reviewer", maximum=2)
146
+ if reviewer < 1:
147
+ raise ReportStateError("reviewer must be 1 or 2")
148
+ risk = _require_risk(data)
149
+ if risk == "low" or (risk == "medium" and reviewer == 2):
150
+ raise ReportStateError("reviewer exceeds the semantic risk limit")
151
+ attempt = data.get("attempt", 1)
152
+ if attempt not in {1, 2}:
153
+ raise ReportStateError("attempt must be 1 or 2")
154
+ return "\n".join(
150
155
  [
151
- f"### Finding {index}: {finding['title']}",
156
+ f"## Independent Reviewer {reviewer}",
152
157
  "",
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']}",
158
+ "- Status: RUNNING",
159
+ f"- Risk Trigger: {risk}",
160
+ f"- Attempt: {attempt}",
161
+ ]
162
+ )
163
+
164
+ if event == "reviewer-result":
165
+ reviewer = _require_count(data, "reviewer", maximum=2)
166
+ if reviewer < 1:
167
+ raise ReportStateError("reviewer must be 1 or 2")
168
+ result = data.get("result")
169
+ if result not in {"PASS", "FINDINGS", "BLOCKED"}:
170
+ raise ReportStateError(f"result has invalid value: {result!r}")
171
+ findings = _require_count(data, "findings")
172
+ accepted = _require_count(data, "accepted")
173
+ rejected = _require_count(data, "rejected")
174
+ accepted_high = _require_count(data, "accepted_high")
175
+ if accepted + rejected != findings or accepted_high > accepted:
176
+ raise ReportStateError("reviewer finding counts are inconsistent")
177
+ status = "BLOCKED" if result == "BLOCKED" else "COMPLETED"
178
+ return "\n".join(
179
+ [
180
+ f"## Independent Reviewer {reviewer} Result",
181
+ "",
182
+ f"- Status: {status}",
183
+ f"- Result: {result}",
184
+ f"- Findings: {findings}",
185
+ f"- Accepted: {accepted}",
186
+ f"- Rejected: {rejected}",
187
+ f"- Accepted High: {accepted_high}",
188
+ f"- Next: {_require_text(data, 'next')}",
189
+ ]
190
+ )
191
+
192
+ if event == "infrastructure-failure":
193
+ reviewer = _require_count(data, "reviewer", maximum=2)
194
+ attempt = _require_count(data, "attempt", maximum=2)
195
+ if reviewer < 1 or attempt < 1:
196
+ raise ReportStateError("reviewer and attempt must be at least 1")
197
+ return "\n".join(
198
+ [
199
+ f"## Independent Reviewer {reviewer} Infrastructure Failure",
159
200
  "",
201
+ f"- Attempt: {attempt}",
202
+ f"- Evidence: {_require_text(data, 'evidence')}",
203
+ f"- Next: {_require_text(data, 'next')}",
160
204
  ]
161
205
  )
162
206
 
163
- lines.extend(["## Final Summary", "", valid["final_summary"], ""])
164
- return "\n".join(lines)
207
+ return "\n".join(
208
+ [
209
+ "## Final Verification",
210
+ "",
211
+ f"- Status: {_require_text(data, 'status')}",
212
+ f"- Evidence: {_require_text(data, 'evidence')}",
213
+ ]
214
+ )
215
+
216
+
217
+ def append_event(path: Path, data: Any) -> None:
218
+ """Append one validated progress event to an active execution."""
219
+ _ensure_appendable(path)
220
+ _append_section(path, _render_event(_require_object(data)))
221
+
222
+
223
+ def append_final_result(path: Path, data: Any) -> None:
224
+ """Append the sole terminal result for the current execution."""
225
+ _ensure_appendable(path)
226
+ valid = _require_object(data)
227
+ verdict = valid.get("verdict")
228
+ if verdict not in VERDICTS:
229
+ raise ReportStateError(f"verdict has invalid value: {verdict!r}")
230
+ risk = _require_risk(valid)
231
+ main_rounds = _require_count(valid, "main_review_rounds", maximum=10)
232
+ reviewers = _require_count(valid, "independent_reviewers_started", maximum=2)
233
+ if reviewers > {"low": 0, "medium": 1, "high": 2}[risk]:
234
+ raise ReportStateError("independent_reviewers_started exceeds risk limit")
235
+ accepted = _require_count(valid, "accepted_findings")
236
+ fixed = _require_count(valid, "fixed_findings")
237
+ rejected = _require_count(valid, "rejected_findings")
238
+ unresolved = _require_count(valid, "unresolved_findings")
239
+ if verdict == "PASS" and unresolved:
240
+ raise ReportStateError("PASS cannot contain unresolved findings")
241
+ if verdict == "NEEDS_FIXES" and not unresolved:
242
+ raise ReportStateError("NEEDS_FIXES requires unresolved findings")
243
+
244
+ section = "\n".join(
245
+ [
246
+ FINAL_HEADING,
247
+ "",
248
+ f"- Verdict: {verdict}",
249
+ f"- Risk: {risk}",
250
+ f"- Main Review Rounds: {main_rounds}",
251
+ f"- Independent Reviewers Started: {reviewers}",
252
+ f"- Accepted Findings: {accepted}",
253
+ f"- Fixed Findings: {fixed}",
254
+ f"- Rejected Findings: {rejected}",
255
+ f"- Unresolved Findings: {unresolved}",
256
+ f"- Summary: {_require_text(valid, 'summary')}",
257
+ ]
258
+ )
259
+ _append_section(path, section)
260
+
261
+
262
+ def _read_json_input() -> Any:
263
+ try:
264
+ return json.loads(sys.stdin.read())
265
+ except json.JSONDecodeError as error:
266
+ raise ReportStateError(f"invalid JSON input: {error}") from error
165
267
 
166
268
 
167
269
  def parse_args() -> argparse.Namespace:
168
270
  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")
271
+ subparsers = parser.add_subparsers(dest="command", required=True)
272
+ for command in ("init", "append", "finalize"):
273
+ subparser = subparsers.add_parser(command)
274
+ subparser.add_argument("report", type=Path)
171
275
  return parser.parse_args()
172
276
 
173
277
 
174
278
  def main() -> int:
175
279
  args = parse_args()
176
280
  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:
281
+ if args.command == "init":
282
+ initialize_report(args.report)
283
+ elif args.command == "append":
284
+ append_event(args.report, _read_json_input())
285
+ else:
286
+ append_final_result(args.report, _read_json_input())
287
+ except (OSError, ReportStateError, KeyError) as error:
182
288
  print(json.dumps({"error": str(error)}, sort_keys=True))
183
289
  return 1
184
290
  return 0
185
291
 
186
292
 
187
293
  if __name__ == "__main__":
188
- sys.exit(main())
294
+ raise SystemExit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.124",
3
+ "version": "1.1.126",
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": {