@mohammadhprp/system-prompt 0.12.4 → 0.12.5

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 (25) hide show
  1. package/framework/commands/review.md +5 -20
  2. package/framework/skills/gh/SKILL.md +157 -0
  3. package/framework/skills/gh/examples.md +10 -0
  4. package/framework/skills/ponytail/SKILL.md +145 -0
  5. package/framework/skills/ponytail/references/ponytail-audit.md +18 -0
  6. package/framework/skills/ponytail/references/ponytail-debt.md +21 -0
  7. package/framework/skills/ponytail/references/ponytail-gain.md +25 -0
  8. package/framework/skills/ponytail/references/ponytail-help.md +18 -0
  9. package/framework/skills/ponytail/references/ponytail-mode.md +33 -0
  10. package/framework/skills/ponytail/references/ponytail-review.md +27 -0
  11. package/framework/skills/ponytail/references/ponytail-rules.md +31 -0
  12. package/framework/skills/ponytail/references/principle-boundary-discipline.md +7 -0
  13. package/framework/skills/ponytail/references/principle-encode-lessons-in-structure.md +13 -0
  14. package/framework/skills/ponytail/references/principle-fix-root-causes.md +17 -0
  15. package/framework/skills/ponytail/references/principle-make-operations-idempotent.md +12 -0
  16. package/framework/skills/ponytail/references/principle-model-the-domain.md +7 -0
  17. package/framework/skills/ponytail/references/principle-prove-it-works.md +27 -0
  18. package/framework/skills/ponytail/references/principle-sequence-verifiable-units.md +7 -0
  19. package/framework/skills/review/SKILL.md +106 -11
  20. package/framework/skills/review/examples.md +4 -3
  21. package/framework/skills/review/scripts/render_review.py +95 -0
  22. package/framework/skills/review/scripts/resolve_spec_context.py +723 -0
  23. package/framework/skills/review/scripts/validate_review_json.py +348 -0
  24. package/package.json +1 -1
  25. package/src/catalog.js +4 -2
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env python3
2
+ """Validate a review.json artifact against an annotated PR diff.
3
+
4
+ This script is packaged with the review-pr skill and must work when the skill
5
+ is copied into a consuming repository without the full oz-for-oss source tree.
6
+ Keep it self-contained: do not import helpers from the repository package.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import sys
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any, TypedDict
18
+
19
+
20
+ class ReviewComment(TypedDict, total=False):
21
+ """Normalized review comment accepted by GitHub's create-review API."""
22
+
23
+ path: str
24
+ line: int
25
+ side: str
26
+ body: str
27
+ start_line: int
28
+ start_side: str
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ReviewValidationResult:
33
+ """Validated review fields plus any comment-location errors."""
34
+
35
+ body: str
36
+ comments: list[ReviewComment]
37
+ errors: list[str]
38
+
39
+
40
+ SUGGESTION_BLOCK_PATTERN = re.compile(
41
+ r"```suggestion[^\n]*\r?\n(?P<content>.*?)\r?\n```",
42
+ re.DOTALL,
43
+ )
44
+ ANNOTATED_OLD_PATTERN = re.compile(r"^\[OLD:(?P<old>\d+)\] ?(?P<text>.*)$")
45
+ ANNOTATED_NEW_PATTERN = re.compile(r"^\[NEW:(?P<new>\d+)\] ?(?P<text>.*)$")
46
+ ANNOTATED_CONTEXT_PATTERN = re.compile(
47
+ r"^\[OLD:(?P<old>\d+),NEW:(?P<new>\d+)\] ?(?P<text>.*)$"
48
+ )
49
+
50
+
51
+ def normalize_review_path(value: Any) -> str:
52
+ path = str(value or "").strip()
53
+ path = re.sub(r"^(a/|b/|\./)", "", path)
54
+ return path
55
+
56
+
57
+ def build_diff_maps_from_annotated_diff(
58
+ diff_text: str,
59
+ ) -> tuple[dict[str, dict[str, set[int]]], dict[str, dict[str, dict[int, str]]]]:
60
+ """Build validation maps from the annotated diff shown to review agents."""
61
+ diff_line_map: dict[str, dict[str, set[int]]] = {}
62
+ diff_content_map: dict[str, dict[str, dict[int, str]]] = {}
63
+ current_path = ""
64
+ old_path = ""
65
+
66
+ def ensure_path(path: str) -> None:
67
+ diff_line_map.setdefault(path, {"LEFT": set(), "RIGHT": set()})
68
+ diff_content_map.setdefault(path, {"LEFT": {}, "RIGHT": {}})
69
+
70
+ for raw_line in diff_text.splitlines():
71
+ if raw_line.startswith("diff --git "):
72
+ current_path = ""
73
+ old_path = ""
74
+ continue
75
+ if raw_line.startswith("--- "):
76
+ candidate = raw_line[4:].strip()
77
+ old_path = (
78
+ "" if candidate == "/dev/null" else normalize_review_path(candidate)
79
+ )
80
+ continue
81
+ if raw_line.startswith("+++ "):
82
+ candidate = raw_line[4:].strip()
83
+ if candidate == "/dev/null":
84
+ current_path = old_path
85
+ else:
86
+ current_path = normalize_review_path(candidate)
87
+ if current_path:
88
+ ensure_path(current_path)
89
+ continue
90
+ if not current_path:
91
+ continue
92
+ old_match = ANNOTATED_OLD_PATTERN.match(raw_line)
93
+ if old_match:
94
+ line = int(old_match.group("old"))
95
+ text = old_match.group("text")
96
+ diff_line_map[current_path]["LEFT"].add(line)
97
+ diff_content_map[current_path]["LEFT"][line] = text
98
+ continue
99
+ new_match = ANNOTATED_NEW_PATTERN.match(raw_line)
100
+ if new_match:
101
+ line = int(new_match.group("new"))
102
+ text = new_match.group("text")
103
+ diff_line_map[current_path]["RIGHT"].add(line)
104
+ diff_content_map[current_path]["RIGHT"][line] = text
105
+ continue
106
+ context_match = ANNOTATED_CONTEXT_PATTERN.match(raw_line)
107
+ if context_match:
108
+ old_line = int(context_match.group("old"))
109
+ new_line = int(context_match.group("new"))
110
+ text = context_match.group("text")
111
+ diff_line_map[current_path]["LEFT"].add(old_line)
112
+ diff_line_map[current_path]["RIGHT"].add(new_line)
113
+ diff_content_map[current_path]["LEFT"][old_line] = text
114
+ diff_content_map[current_path]["RIGHT"][new_line] = text
115
+
116
+ return diff_line_map, diff_content_map
117
+
118
+
119
+ def _extract_suggestion_blocks(body: str | None) -> list[list[str]]:
120
+ blocks: list[list[str]] = []
121
+ for match in SUGGESTION_BLOCK_PATTERN.finditer(body or ""):
122
+ content = match.group("content")
123
+ lines = [line.rstrip("\r") for line in content.split("\n")]
124
+ blocks.append(lines)
125
+ return blocks
126
+
127
+
128
+ def _validate_suggestion_blocks(
129
+ comment: dict[str, Any],
130
+ diff_content_map: dict[str, dict[str, dict[int, str]]],
131
+ ) -> list[str]:
132
+ errors: list[str] = []
133
+ body = comment.get("body") or ""
134
+ blocks = _extract_suggestion_blocks(body)
135
+ if not blocks:
136
+ return errors
137
+
138
+ path = comment.get("path") or ""
139
+ side = comment.get("side") or "RIGHT"
140
+ start_side = comment.get("start_side") or side
141
+ line_no = comment.get("line")
142
+ if not isinstance(line_no, int):
143
+ return errors
144
+ start_line = comment.get("start_line") or line_no
145
+ content_for_start_side = diff_content_map.get(path, {}).get(start_side, {})
146
+ content_for_end_side = diff_content_map.get(path, {}).get(side, {})
147
+
148
+ for block_index, block_lines in enumerate(blocks):
149
+ if not block_lines or block_lines == [""]:
150
+ continue
151
+ prev_context = content_for_start_side.get(start_line - 1)
152
+ next_context = content_for_end_side.get(line_no + 1)
153
+ first_line = block_lines[0]
154
+ last_line = block_lines[-1]
155
+ if prev_context is not None and first_line == prev_context:
156
+ errors.append(
157
+ f"suggestion block {block_index} duplicates the context line immediately above "
158
+ f"`start_line` ({start_line - 1}); that line is not replaced and will appear twice after the suggestion is applied"
159
+ )
160
+ if next_context is not None and last_line == next_context:
161
+ errors.append(
162
+ f"suggestion block {block_index} duplicates the context line immediately below "
163
+ f"`line` ({line_no + 1}); that line is not replaced and will appear twice after the suggestion is applied"
164
+ )
165
+ return errors
166
+
167
+
168
+ def validate_review_payload(
169
+ review: Any,
170
+ diff_line_map: dict[str, dict[str, set[int]]],
171
+ diff_content_map: dict[str, dict[str, dict[int, str]]] | None = None,
172
+ ) -> ReviewValidationResult:
173
+ """Validate a review.json payload against the annotated PR diff."""
174
+ if not isinstance(review, dict):
175
+ raise ValueError("Review payload must be a JSON object.")
176
+
177
+ raw_body = review.get("body")
178
+ if raw_body is None:
179
+ raw_body = review.get("summary") or ""
180
+ if not isinstance(raw_body, str):
181
+ raise ValueError("Review payload `body` must be a string.")
182
+
183
+ raw_comments = review.get("comments") or []
184
+ if not isinstance(raw_comments, list):
185
+ raise ValueError("Review payload `comments` must be a list.")
186
+
187
+ normalized_comments: list[ReviewComment] = []
188
+ errors: list[str] = []
189
+
190
+ for index, raw_comment in enumerate(raw_comments):
191
+ if not isinstance(raw_comment, dict):
192
+ errors.append(f"`comments[{index}]` must be an object.")
193
+ continue
194
+
195
+ path = normalize_review_path(raw_comment.get("path"))
196
+ line = raw_comment.get("line")
197
+ body_value = raw_comment.get("body")
198
+ body = body_value.strip() if isinstance(body_value, str) else ""
199
+ side = raw_comment.get("side")
200
+
201
+ if not path:
202
+ errors.append(f"`comments[{index}]` is missing `path`.")
203
+ continue
204
+ if path not in diff_line_map:
205
+ errors.append(
206
+ f"`comments[{index}]` references `{path}`, which is not part of the PR diff. Move that feedback to top-level `body` instead."
207
+ )
208
+ continue
209
+ if not isinstance(line, int) or line <= 0:
210
+ errors.append(
211
+ f"`comments[{index}]` for `{path}` must include a positive integer `line`."
212
+ )
213
+ continue
214
+ if side not in {"LEFT", "RIGHT"}:
215
+ errors.append(
216
+ f"`comments[{index}]` for `{path}:{line}` must include `side` set to `LEFT` or `RIGHT`."
217
+ )
218
+ continue
219
+ if not body:
220
+ errors.append(f"`comments[{index}]` for `{path}` is missing `body`.")
221
+ continue
222
+
223
+ allowed_lines = diff_line_map[path][side]
224
+ if line not in allowed_lines:
225
+ errors.append(
226
+ f"`comments[{index}]` references `{path}:{line}` on `{side}`, which is not commentable in the PR diff."
227
+ )
228
+ continue
229
+
230
+ normalized_comment: ReviewComment = {
231
+ "path": path,
232
+ "line": line,
233
+ "side": side,
234
+ "body": body,
235
+ }
236
+
237
+ if "start_line" in raw_comment and raw_comment.get("start_line") is not None:
238
+ start_line = raw_comment.get("start_line")
239
+ if not isinstance(start_line, int) or start_line <= 0:
240
+ errors.append(
241
+ f"`comments[{index}]` for `{path}` has invalid `start_line`; it must be a positive integer."
242
+ )
243
+ continue
244
+ start_side = raw_comment.get("start_side")
245
+ if start_side not in {"LEFT", "RIGHT"}:
246
+ errors.append(
247
+ f"`comments[{index}]` for `{path}` has `start_line` but is missing `start_side`; set `start_side` to `LEFT` or `RIGHT`."
248
+ )
249
+ continue
250
+ if start_side == side and start_line >= line:
251
+ errors.append(
252
+ f"`comments[{index}]` for `{path}` has invalid `start_line`; when `start_side` matches `side`, it must be smaller than `line`."
253
+ )
254
+ continue
255
+ if start_line not in diff_line_map[path][start_side]:
256
+ errors.append(
257
+ f"`comments[{index}]` references `{path}:{start_line}` on `{start_side}` as `start_line`, which is not commentable in the PR diff."
258
+ )
259
+ continue
260
+ normalized_comment["start_line"] = start_line
261
+ normalized_comment["start_side"] = start_side
262
+ elif raw_comment.get("start_side") is not None:
263
+ errors.append(
264
+ f"`comments[{index}]` for `{path}:{line}` has `start_side` without `start_line`."
265
+ )
266
+ continue
267
+
268
+ if diff_content_map is not None:
269
+ suggestion_errors = _validate_suggestion_blocks(
270
+ normalized_comment, diff_content_map
271
+ )
272
+ if suggestion_errors:
273
+ for err in suggestion_errors:
274
+ errors.append(
275
+ f"`comments[{index}]` for `{path}:{line}` on `{side}` has an invalid suggestion block: {err}."
276
+ )
277
+ continue
278
+
279
+ normalized_comments.append(normalized_comment)
280
+
281
+ return ReviewValidationResult(
282
+ body=raw_body.strip(),
283
+ comments=normalized_comments,
284
+ errors=errors,
285
+ )
286
+
287
+
288
+ def _load_json(path: Path) -> Any:
289
+ try:
290
+ return json.loads(path.read_text(encoding="utf-8"))
291
+ except FileNotFoundError:
292
+ raise SystemExit(f"review validation failed: {path} does not exist")
293
+ except json.JSONDecodeError as exc:
294
+ raise SystemExit(f"review validation failed: {path} is invalid JSON: {exc}")
295
+
296
+
297
+ def _validate_verdict(payload: Any) -> list[str]:
298
+ if not isinstance(payload, dict):
299
+ return ["review.json must decode to a JSON object."]
300
+ verdict = payload.get("verdict")
301
+ if verdict not in {"APPROVE", "REJECT"}:
302
+ return ['`verdict` must be exactly "APPROVE" or "REJECT".']
303
+ return []
304
+
305
+
306
+ def main() -> int:
307
+ parser = argparse.ArgumentParser(
308
+ description="Validate review.json comments against annotated pr_diff.txt."
309
+ )
310
+ parser.add_argument(
311
+ "--review-json",
312
+ default="review.json",
313
+ type=Path,
314
+ help="Path to the review.json artifact to validate.",
315
+ )
316
+ parser.add_argument(
317
+ "--diff",
318
+ default="pr_diff.txt",
319
+ type=Path,
320
+ help="Path to the annotated PR diff consumed during review.",
321
+ )
322
+ args = parser.parse_args()
323
+
324
+ payload = _load_json(args.review_json)
325
+ try:
326
+ diff_text = args.diff.read_text(encoding="utf-8")
327
+ except FileNotFoundError:
328
+ print(f"review validation failed: {args.diff} does not exist", file=sys.stderr)
329
+ return 1
330
+
331
+ diff_line_map, diff_content_map = build_diff_maps_from_annotated_diff(diff_text)
332
+ result = validate_review_payload(payload, diff_line_map, diff_content_map)
333
+ errors = _validate_verdict(payload) + result.errors
334
+ if errors:
335
+ print("review validation failed:", file=sys.stderr)
336
+ for error in errors:
337
+ print(f"- {error}", file=sys.stderr)
338
+ return 1
339
+
340
+ print(
341
+ "review validation passed: "
342
+ f"{len(result.comments)} inline comment(s), {len(diff_line_map)} diff file(s)"
343
+ )
344
+ return 0
345
+
346
+
347
+ if __name__ == "__main__":
348
+ raise SystemExit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohammadhprp/system-prompt",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "AI Coding Agent Framework — interactive bootstrap CLI",
5
5
  "keywords": [
6
6
  "ai",
package/src/catalog.js CHANGED
@@ -16,6 +16,7 @@ export const categories = {
16
16
  { id: 'effective-html', name: 'Effective HTML', description: 'Create self-contained HTML artifacts with routed guidance for design, wireframes, prototypes, plans, and diagrams' },
17
17
  { id: 'find-skills', name: 'Find Skills', description: 'Discover, evaluate, and install agent skills for specialized tasks' },
18
18
  { id: 'frontend-design', name: 'Frontend Design', description: 'Distinctive, intentional visual design for new UI or reshaping existing UI' },
19
+ { id: 'gh', name: 'GitHub CLI', description: 'Work with GitHub via the gh CLI for repositories, issues, pull requests, Actions, releases, and APIs' },
19
20
  { id: 'glab', name: 'Glab', description: 'Work with GitLab via the glab CLI for MRs, issues, and pipelines' },
20
21
  { id: 'humanizer', name: 'Humanizer', description: 'Remove signs of AI-generated writing to make text sound more natural and human' },
21
22
  { id: 'improve', name: 'Improve', description: 'Audit repositories and produce prioritized, read-only implementation plans for another agent' },
@@ -24,9 +25,10 @@ export const categories = {
24
25
  { id: 'laravel-best-practices', name: 'Laravel Best Practices', description: 'Laravel patterns for Eloquent, validation, testing' },
25
26
  { id: 'merge-request', name: 'Merge Request', description: 'Create a GitLab merge request (MR) for the current branch' },
26
27
  { id: 'perf-web-optimization', name: 'Web Performance Optimization', description: 'Optimize web performance: bundle size, images, caching, lazy loading, and overall page speed' },
28
+ { id: 'ponytail', name: 'Ponytail', description: 'Lazy senior developer workflow focused on YAGNI, reuse, and the smallest working change' },
27
29
  { id: 'pull-request', name: 'Pull Request', description: 'Create or update a GitHub pull request (PR) for the current branch' },
28
30
  { id: 'release', name: 'Release', description: 'Prepare and tag semantic-versioned releases' },
29
- { id: 'review', name: 'Review', description: 'Perform comprehensive code quality review' },
31
+ { id: 'review', name: 'Review', description: 'Review local, GitHub, or GitLab changes and write review.json' },
30
32
  { id: 'security-best-practices', name: 'Security Best Practices', description: 'Language and framework specific security best-practice reviews and secure-by-default coding help' },
31
33
  { id: 'sentry', name: 'Sentry', description: 'Inspect Sentry issues, summarize production errors, and pull health data via the Sentry API' },
32
34
  { id: 'show-me', name: 'Show Me', description: 'Explain the current topic visually with diagrams, code-shape sketches, and focused HTML artifacts' },
@@ -61,7 +63,7 @@ export const categories = {
61
63
  { id: 'pr', name: 'PR', description: 'Create a GitHub PR for the current branch' },
62
64
  { id: 'mr', name: 'MR', description: 'Create a GitLab MR for the current branch' },
63
65
  { id: 'release', name: 'Release', description: 'Tag releases, update changelog, and bump versions' },
64
- { id: 'review', name: 'Review', description: 'Perform comprehensive code quality review' },
66
+ { id: 'review', name: 'Review', description: 'Review local, GitHub, or GitLab changes and write review.json' },
65
67
  { id: 'summarize-changes', name: 'Summarize Changes', description: 'Summarize uncommitted changes and flag risks' },
66
68
  ],
67
69
  },