prizmkit 1.1.122 → 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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/command-adapter.js +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +16 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +121 -9
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +46 -38
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +365 -22
- package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +118 -109
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +21 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +33 -17
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +109 -63
- package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +179 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +80 -52
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +348 -154
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +188 -0
- package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +222 -0
- package/package.json +1 -1
- package/src/scaffold.js +15 -0
|
@@ -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
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');
|