@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.
- package/framework/commands/review.md +5 -20
- package/framework/skills/gh/SKILL.md +157 -0
- package/framework/skills/gh/examples.md +10 -0
- package/framework/skills/ponytail/SKILL.md +145 -0
- package/framework/skills/ponytail/references/ponytail-audit.md +18 -0
- package/framework/skills/ponytail/references/ponytail-debt.md +21 -0
- package/framework/skills/ponytail/references/ponytail-gain.md +25 -0
- package/framework/skills/ponytail/references/ponytail-help.md +18 -0
- package/framework/skills/ponytail/references/ponytail-mode.md +33 -0
- package/framework/skills/ponytail/references/ponytail-review.md +27 -0
- package/framework/skills/ponytail/references/ponytail-rules.md +31 -0
- package/framework/skills/ponytail/references/principle-boundary-discipline.md +7 -0
- package/framework/skills/ponytail/references/principle-encode-lessons-in-structure.md +13 -0
- package/framework/skills/ponytail/references/principle-fix-root-causes.md +17 -0
- package/framework/skills/ponytail/references/principle-make-operations-idempotent.md +12 -0
- package/framework/skills/ponytail/references/principle-model-the-domain.md +7 -0
- package/framework/skills/ponytail/references/principle-prove-it-works.md +27 -0
- package/framework/skills/ponytail/references/principle-sequence-verifiable-units.md +7 -0
- package/framework/skills/review/SKILL.md +106 -11
- package/framework/skills/review/examples.md +4 -3
- package/framework/skills/review/scripts/render_review.py +95 -0
- package/framework/skills/review/scripts/resolve_spec_context.py +723 -0
- package/framework/skills/review/scripts/validate_review_json.py +348 -0
- package/package.json +1 -1
- 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
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: '
|
|
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: '
|
|
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
|
},
|