@laitszkin/apollo-toolkit 2.11.1 → 2.11.3
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/AGENTS.md +2 -2
- package/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/analyse-app-logs/README.md +12 -0
- package/analyse-app-logs/SKILL.md +6 -1
- package/analyse-app-logs/agents/openai.yaml +1 -1
- package/analyse-app-logs/scripts/filter_logs_by_time.py +64 -0
- package/analyse-app-logs/scripts/log_cli_utils.py +112 -0
- package/analyse-app-logs/scripts/search_logs.py +137 -0
- package/analyse-app-logs/tests/test_filter_logs_by_time.py +95 -0
- package/analyse-app-logs/tests/test_search_logs.py +100 -0
- package/commit-and-push/SKILL.md +17 -10
- package/develop-new-features/SKILL.md +17 -4
- package/develop-new-features/references/testing-e2e.md +1 -0
- package/enhance-existing-features/SKILL.md +20 -4
- package/enhance-existing-features/references/e2e-tests.md +1 -0
- package/generate-spec/SKILL.md +18 -3
- package/generate-spec/references/templates/checklist.md +30 -6
- package/generate-spec/references/templates/spec.md +7 -2
- package/maintain-skill-catalog/SKILL.md +3 -1
- package/open-github-issue/README.md +48 -6
- package/open-github-issue/SKILL.md +80 -5
- package/open-github-issue/agents/openai.yaml +2 -2
- package/open-github-issue/scripts/open_github_issue.py +174 -1
- package/open-github-issue/tests/test_open_github_issue.py +79 -0
- package/package.json +1 -1
- package/read-github-issue/SKILL.md +83 -0
- package/read-github-issue/agents/openai.yaml +4 -0
- package/{fix-github-issues/scripts/list_issues.py → read-github-issue/scripts/find_issues.py} +1 -1
- package/read-github-issue/scripts/read_issue.py +108 -0
- package/{fix-github-issues/tests/test_list_issues.py → read-github-issue/tests/test_find_issues.py} +3 -3
- package/read-github-issue/tests/test_read_issue.py +109 -0
- package/fix-github-issues/SKILL.md +0 -105
- package/fix-github-issues/agents/openai.yaml +0 -4
|
@@ -19,6 +19,18 @@ DEFAULT_REPRO_ZH = "尚未穩定重現;需補充更多執行期資料。"
|
|
|
19
19
|
DEFAULT_REPRO_EN = "Not yet reliably reproducible; more runtime evidence is required."
|
|
20
20
|
ISSUE_TYPE_PROBLEM = "problem"
|
|
21
21
|
ISSUE_TYPE_FEATURE = "feature"
|
|
22
|
+
ISSUE_TYPE_PERFORMANCE = "performance"
|
|
23
|
+
ISSUE_TYPE_SECURITY = "security"
|
|
24
|
+
ISSUE_TYPE_DOCS = "docs"
|
|
25
|
+
ISSUE_TYPE_OBSERVABILITY = "observability"
|
|
26
|
+
ISSUE_TYPES = [
|
|
27
|
+
ISSUE_TYPE_PROBLEM,
|
|
28
|
+
ISSUE_TYPE_FEATURE,
|
|
29
|
+
ISSUE_TYPE_PERFORMANCE,
|
|
30
|
+
ISSUE_TYPE_SECURITY,
|
|
31
|
+
ISSUE_TYPE_DOCS,
|
|
32
|
+
ISSUE_TYPE_OBSERVABILITY,
|
|
33
|
+
]
|
|
22
34
|
PROBLEM_BDD_MARKER_GROUPS = (
|
|
23
35
|
(
|
|
24
36
|
r"Expected Behavior\s*\(BDD\)",
|
|
@@ -43,7 +55,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
43
55
|
parser.add_argument("--title", required=True, help="Issue title")
|
|
44
56
|
parser.add_argument(
|
|
45
57
|
"--issue-type",
|
|
46
|
-
choices=
|
|
58
|
+
choices=ISSUE_TYPES,
|
|
47
59
|
default=ISSUE_TYPE_PROBLEM,
|
|
48
60
|
help="Structured issue type to publish.",
|
|
49
61
|
)
|
|
@@ -71,6 +83,27 @@ def parse_args() -> argparse.Namespace:
|
|
|
71
83
|
"--suggested-architecture",
|
|
72
84
|
help="Issue section content: suggested architecture for the feature",
|
|
73
85
|
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--impact",
|
|
88
|
+
help="Issue section content: concrete user, system, or business impact",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--evidence",
|
|
92
|
+
help="Issue section content: concrete evidence, metrics, logs, or repository facts",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--suggested-action",
|
|
96
|
+
help="Issue section content: suggested remediation, mitigation, or next action",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--severity",
|
|
100
|
+
choices=["critical", "high", "medium", "low"],
|
|
101
|
+
help="Issue section content: severity level for security-oriented issues",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--affected-scope",
|
|
105
|
+
help="Issue section content: affected endpoint, module, user cohort, or system surface",
|
|
106
|
+
)
|
|
74
107
|
parser.add_argument(
|
|
75
108
|
"--repo",
|
|
76
109
|
help="Target repository in owner/repo format. Defaults to origin remote.",
|
|
@@ -91,6 +124,35 @@ def validate_issue_content_args(args: argparse.Namespace) -> None:
|
|
|
91
124
|
raise SystemExit("Feature issues require --suggested-architecture.")
|
|
92
125
|
return
|
|
93
126
|
|
|
127
|
+
if args.issue_type == ISSUE_TYPE_PERFORMANCE:
|
|
128
|
+
require_non_empty(args.problem_description, "Performance issues require --problem-description.")
|
|
129
|
+
require_non_empty(args.impact, "Performance issues require --impact.")
|
|
130
|
+
require_non_empty(args.evidence, "Performance issues require --evidence.")
|
|
131
|
+
require_non_empty(args.suggested_action, "Performance issues require --suggested-action.")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if args.issue_type == ISSUE_TYPE_SECURITY:
|
|
135
|
+
require_non_empty(args.problem_description, "Security issues require --problem-description.")
|
|
136
|
+
require_non_empty(args.affected_scope, "Security issues require --affected-scope.")
|
|
137
|
+
require_non_empty(args.impact, "Security issues require --impact.")
|
|
138
|
+
require_non_empty(args.evidence, "Security issues require --evidence.")
|
|
139
|
+
require_non_empty(args.suggested_action, "Security issues require --suggested-action.")
|
|
140
|
+
require_non_empty(args.severity, "Security issues require --severity.")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if args.issue_type == ISSUE_TYPE_DOCS:
|
|
144
|
+
require_non_empty(args.problem_description, "Docs issues require --problem-description.")
|
|
145
|
+
require_non_empty(args.evidence, "Docs issues require --evidence.")
|
|
146
|
+
require_non_empty(args.suggested_action, "Docs issues require --suggested-action.")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if args.issue_type == ISSUE_TYPE_OBSERVABILITY:
|
|
150
|
+
require_non_empty(args.problem_description, "Observability issues require --problem-description.")
|
|
151
|
+
require_non_empty(args.impact, "Observability issues require --impact.")
|
|
152
|
+
require_non_empty(args.evidence, "Observability issues require --evidence.")
|
|
153
|
+
require_non_empty(args.suggested_action, "Observability issues require --suggested-action.")
|
|
154
|
+
return
|
|
155
|
+
|
|
94
156
|
if not (args.problem_description or "").strip():
|
|
95
157
|
raise SystemExit("Problem issues require --problem-description.")
|
|
96
158
|
if not (args.suspected_cause or "").strip():
|
|
@@ -102,6 +164,11 @@ def validate_issue_content_args(args: argparse.Namespace) -> None:
|
|
|
102
164
|
)
|
|
103
165
|
|
|
104
166
|
|
|
167
|
+
def require_non_empty(value: str | None, message: str) -> None:
|
|
168
|
+
if not (value or "").strip():
|
|
169
|
+
raise SystemExit(message)
|
|
170
|
+
|
|
171
|
+
|
|
105
172
|
def has_required_problem_bdd_sections(problem_description: str) -> bool:
|
|
106
173
|
normalized = problem_description.strip()
|
|
107
174
|
return any(
|
|
@@ -236,6 +303,11 @@ def build_issue_body(
|
|
|
236
303
|
proposal: str | None,
|
|
237
304
|
reason: str | None,
|
|
238
305
|
suggested_architecture: str | None,
|
|
306
|
+
impact: str | None = None,
|
|
307
|
+
evidence: str | None = None,
|
|
308
|
+
suggested_action: str | None = None,
|
|
309
|
+
severity: str | None = None,
|
|
310
|
+
affected_scope: str | None = None,
|
|
239
311
|
) -> str:
|
|
240
312
|
if issue_type == ISSUE_TYPE_FEATURE:
|
|
241
313
|
proposal_text = (proposal or title).strip()
|
|
@@ -261,6 +333,102 @@ def build_issue_body(
|
|
|
261
333
|
f"{architecture_text}\n"
|
|
262
334
|
)
|
|
263
335
|
|
|
336
|
+
if issue_type == ISSUE_TYPE_PERFORMANCE:
|
|
337
|
+
if language == "zh":
|
|
338
|
+
return (
|
|
339
|
+
"### 效能問題\n"
|
|
340
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
341
|
+
"### 影響\n"
|
|
342
|
+
f"{(impact or '').strip()}\n\n"
|
|
343
|
+
"### 證據\n"
|
|
344
|
+
f"{(evidence or '').strip()}\n\n"
|
|
345
|
+
"### 建議行動\n"
|
|
346
|
+
f"{(suggested_action or '').strip()}\n"
|
|
347
|
+
)
|
|
348
|
+
return (
|
|
349
|
+
"### Performance Problem\n"
|
|
350
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
351
|
+
"### Impact\n"
|
|
352
|
+
f"{(impact or '').strip()}\n\n"
|
|
353
|
+
"### Evidence\n"
|
|
354
|
+
f"{(evidence or '').strip()}\n\n"
|
|
355
|
+
"### Suggested Action\n"
|
|
356
|
+
f"{(suggested_action or '').strip()}\n"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if issue_type == ISSUE_TYPE_SECURITY:
|
|
360
|
+
if language == "zh":
|
|
361
|
+
return (
|
|
362
|
+
"### 安全風險\n"
|
|
363
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
364
|
+
"### 嚴重程度\n"
|
|
365
|
+
f"{(severity or '').strip()}\n\n"
|
|
366
|
+
"### 受影響範圍\n"
|
|
367
|
+
f"{(affected_scope or '').strip()}\n\n"
|
|
368
|
+
"### 影響\n"
|
|
369
|
+
f"{(impact or '').strip()}\n\n"
|
|
370
|
+
"### 證據\n"
|
|
371
|
+
f"{(evidence or '').strip()}\n\n"
|
|
372
|
+
"### 建議緩解\n"
|
|
373
|
+
f"{(suggested_action or '').strip()}\n"
|
|
374
|
+
)
|
|
375
|
+
return (
|
|
376
|
+
"### Security Risk\n"
|
|
377
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
378
|
+
"### Severity\n"
|
|
379
|
+
f"{(severity or '').strip()}\n\n"
|
|
380
|
+
"### Affected Scope\n"
|
|
381
|
+
f"{(affected_scope or '').strip()}\n\n"
|
|
382
|
+
"### Impact\n"
|
|
383
|
+
f"{(impact or '').strip()}\n\n"
|
|
384
|
+
"### Evidence\n"
|
|
385
|
+
f"{(evidence or '').strip()}\n\n"
|
|
386
|
+
"### Suggested Mitigation\n"
|
|
387
|
+
f"{(suggested_action or '').strip()}\n"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if issue_type == ISSUE_TYPE_DOCS:
|
|
391
|
+
if language == "zh":
|
|
392
|
+
return (
|
|
393
|
+
"### 文件缺口\n"
|
|
394
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
395
|
+
"### 證據\n"
|
|
396
|
+
f"{(evidence or '').strip()}\n\n"
|
|
397
|
+
"### 建議更新\n"
|
|
398
|
+
f"{(suggested_action or '').strip()}\n"
|
|
399
|
+
)
|
|
400
|
+
return (
|
|
401
|
+
"### Documentation Gap\n"
|
|
402
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
403
|
+
"### Evidence\n"
|
|
404
|
+
f"{(evidence or '').strip()}\n\n"
|
|
405
|
+
"### Suggested Update\n"
|
|
406
|
+
f"{(suggested_action or '').strip()}\n"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if issue_type == ISSUE_TYPE_OBSERVABILITY:
|
|
410
|
+
if language == "zh":
|
|
411
|
+
return (
|
|
412
|
+
"### 可觀測性缺口\n"
|
|
413
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
414
|
+
"### 影響\n"
|
|
415
|
+
f"{(impact or '').strip()}\n\n"
|
|
416
|
+
"### 證據\n"
|
|
417
|
+
f"{(evidence or '').strip()}\n\n"
|
|
418
|
+
"### 建議儀表化\n"
|
|
419
|
+
f"{(suggested_action or '').strip()}\n"
|
|
420
|
+
)
|
|
421
|
+
return (
|
|
422
|
+
"### Observability Gap\n"
|
|
423
|
+
f"{(problem_description or '').strip()}\n\n"
|
|
424
|
+
"### Impact\n"
|
|
425
|
+
f"{(impact or '').strip()}\n\n"
|
|
426
|
+
"### Evidence\n"
|
|
427
|
+
f"{(evidence or '').strip()}\n\n"
|
|
428
|
+
"### Suggested Instrumentation\n"
|
|
429
|
+
f"{(suggested_action or '').strip()}\n"
|
|
430
|
+
)
|
|
431
|
+
|
|
264
432
|
if language == "zh":
|
|
265
433
|
repro_text = (reproduction or DEFAULT_REPRO_ZH).strip()
|
|
266
434
|
return (
|
|
@@ -349,6 +517,11 @@ def main() -> int:
|
|
|
349
517
|
proposal=args.proposal,
|
|
350
518
|
reason=args.reason,
|
|
351
519
|
suggested_architecture=args.suggested_architecture,
|
|
520
|
+
impact=args.impact,
|
|
521
|
+
evidence=args.evidence,
|
|
522
|
+
suggested_action=args.suggested_action,
|
|
523
|
+
severity=args.severity,
|
|
524
|
+
affected_scope=args.affected_scope,
|
|
352
525
|
)
|
|
353
526
|
|
|
354
527
|
mode = "draft-only"
|
|
@@ -81,6 +81,11 @@ class OpenGitHubIssueTests(unittest.TestCase):
|
|
|
81
81
|
proposal="Allow exporting incident timelines",
|
|
82
82
|
reason="Support postmortem handoff",
|
|
83
83
|
suggested_architecture="Add shared exporters and UI action",
|
|
84
|
+
impact=None,
|
|
85
|
+
evidence=None,
|
|
86
|
+
suggested_action=None,
|
|
87
|
+
severity=None,
|
|
88
|
+
affected_scope=None,
|
|
84
89
|
)
|
|
85
90
|
|
|
86
91
|
self.assertIn("### 功能提案", zh_body)
|
|
@@ -88,6 +93,28 @@ class OpenGitHubIssueTests(unittest.TestCase):
|
|
|
88
93
|
self.assertIn("### Feature Proposal", en_body)
|
|
89
94
|
self.assertIn("### Suggested Architecture", en_body)
|
|
90
95
|
|
|
96
|
+
def test_build_issue_body_supports_security_issue_sections(self) -> None:
|
|
97
|
+
en_body = MODULE.build_issue_body(
|
|
98
|
+
issue_type=MODULE.ISSUE_TYPE_SECURITY,
|
|
99
|
+
language="en",
|
|
100
|
+
title="[Security] Missing auth",
|
|
101
|
+
problem_description="Endpoint misses admin check",
|
|
102
|
+
suspected_cause=None,
|
|
103
|
+
reproduction=None,
|
|
104
|
+
proposal=None,
|
|
105
|
+
reason=None,
|
|
106
|
+
suggested_architecture=None,
|
|
107
|
+
impact="Sensitive export may leak",
|
|
108
|
+
evidence="Reproduced request and code path review",
|
|
109
|
+
suggested_action="Add authz and tests",
|
|
110
|
+
severity="high",
|
|
111
|
+
affected_scope="/admin/export",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self.assertIn("### Security Risk", en_body)
|
|
115
|
+
self.assertIn("### Severity", en_body)
|
|
116
|
+
self.assertIn("### Suggested Mitigation", en_body)
|
|
117
|
+
|
|
91
118
|
def test_validate_issue_content_args_requires_feature_fields(self) -> None:
|
|
92
119
|
with self.assertRaises(SystemExit):
|
|
93
120
|
MODULE.validate_issue_content_args(
|
|
@@ -118,6 +145,43 @@ class OpenGitHubIssueTests(unittest.TestCase):
|
|
|
118
145
|
suggested_architecture="shared module",
|
|
119
146
|
problem_description=None,
|
|
120
147
|
suspected_cause=None,
|
|
148
|
+
impact=None,
|
|
149
|
+
evidence=None,
|
|
150
|
+
suggested_action=None,
|
|
151
|
+
severity=None,
|
|
152
|
+
affected_scope=None,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def test_validate_issue_content_args_requires_security_fields(self) -> None:
|
|
157
|
+
with self.assertRaises(SystemExit):
|
|
158
|
+
MODULE.validate_issue_content_args(
|
|
159
|
+
Namespace(
|
|
160
|
+
issue_type=MODULE.ISSUE_TYPE_SECURITY,
|
|
161
|
+
problem_description="auth missing",
|
|
162
|
+
affected_scope="",
|
|
163
|
+
impact="sensitive export may leak",
|
|
164
|
+
evidence="reproduced",
|
|
165
|
+
suggested_action="add authz",
|
|
166
|
+
severity="high",
|
|
167
|
+
reason=None,
|
|
168
|
+
suggested_architecture=None,
|
|
169
|
+
suspected_cause=None,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
MODULE.validate_issue_content_args(
|
|
174
|
+
Namespace(
|
|
175
|
+
issue_type=MODULE.ISSUE_TYPE_SECURITY,
|
|
176
|
+
problem_description="auth missing",
|
|
177
|
+
affected_scope="/admin/export",
|
|
178
|
+
impact="sensitive export may leak",
|
|
179
|
+
evidence="reproduced",
|
|
180
|
+
suggested_action="add authz",
|
|
181
|
+
severity="high",
|
|
182
|
+
reason=None,
|
|
183
|
+
suggested_architecture=None,
|
|
184
|
+
suspected_cause=None,
|
|
121
185
|
)
|
|
122
186
|
)
|
|
123
187
|
|
|
@@ -130,6 +194,11 @@ class OpenGitHubIssueTests(unittest.TestCase):
|
|
|
130
194
|
suggested_architecture=None,
|
|
131
195
|
problem_description="Repeated timeout warnings escalated into request failures.",
|
|
132
196
|
suspected_cause="handler.py:12",
|
|
197
|
+
impact=None,
|
|
198
|
+
evidence=None,
|
|
199
|
+
suggested_action=None,
|
|
200
|
+
severity=None,
|
|
201
|
+
affected_scope=None,
|
|
133
202
|
)
|
|
134
203
|
)
|
|
135
204
|
|
|
@@ -153,6 +222,11 @@ class OpenGitHubIssueTests(unittest.TestCase):
|
|
|
153
222
|
"- Difference/Impact: users still receive errors.\n"
|
|
154
223
|
),
|
|
155
224
|
suspected_cause="handler.py:12",
|
|
225
|
+
impact=None,
|
|
226
|
+
evidence=None,
|
|
227
|
+
suggested_action=None,
|
|
228
|
+
severity=None,
|
|
229
|
+
affected_scope=None,
|
|
156
230
|
)
|
|
157
231
|
)
|
|
158
232
|
|
|
@@ -181,6 +255,11 @@ class OpenGitHubIssueTests(unittest.TestCase):
|
|
|
181
255
|
suggested_architecture=None,
|
|
182
256
|
repo="owner/repo",
|
|
183
257
|
dry_run=True,
|
|
258
|
+
impact=None,
|
|
259
|
+
evidence=None,
|
|
260
|
+
suggested_action=None,
|
|
261
|
+
severity=None,
|
|
262
|
+
affected_scope=None,
|
|
184
263
|
)
|
|
185
264
|
|
|
186
265
|
with patch.object(MODULE, "parse_args", return_value=args), patch.object(
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: read-github-issue
|
|
3
|
+
description: Read and search remote GitHub issues via GitHub CLI (`gh`). Use when users ask to list issues, filter issue candidates, inspect a specific issue with comments, or gather issue context before planning follow-up work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Read GitHub Issue
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
- Required: none.
|
|
11
|
+
- Conditional: none.
|
|
12
|
+
- Optional: none.
|
|
13
|
+
- Fallback: If `gh` is unavailable or unauthenticated, stop and report the exact blocked command instead of guessing repository state.
|
|
14
|
+
|
|
15
|
+
## Standards
|
|
16
|
+
|
|
17
|
+
- Evidence: Verify repository context first, then read remote issue data directly from `gh issue list` / `gh issue view` instead of paraphrasing from memory.
|
|
18
|
+
- Execution: Confirm the target repo, find candidate issues with the bundled script, then inspect the chosen issue with comments and structured fields.
|
|
19
|
+
- Quality: Keep the skill focused on issue discovery and retrieval only; do not embed any hardcoded fixing, branching, PR, or push workflow.
|
|
20
|
+
- Output: Return candidate issues, selected issue details, comments summary, and any missing information needed before follow-up work.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
Use this skill to gather trustworthy GitHub issue context with `gh`: discover open or closed issues, narrow them with labels or search text, then inspect a chosen issue in detail before any separate planning or implementation workflow begins.
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- `gh` is installed and authenticated (`gh auth status`).
|
|
29
|
+
- The current directory is a git repository with the correct `origin`, or the user provides `--repo owner/name`.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
### 1) Verify repository and remote context
|
|
34
|
+
|
|
35
|
+
- Run `gh repo view --json nameWithOwner,isPrivate,defaultBranchRef` to confirm target repo.
|
|
36
|
+
- If the user specifies another repository, always use `--repo <owner>/<repo>` in issue commands.
|
|
37
|
+
|
|
38
|
+
### 2) Find candidate issues with the bundled script
|
|
39
|
+
|
|
40
|
+
- Preferred command:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3 scripts/find_issues.py --limit 50 --state open
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Optional filters:
|
|
47
|
+
- `--repo owner/name`
|
|
48
|
+
- `--label bug`
|
|
49
|
+
- `--search "panic in parser"`
|
|
50
|
+
- If the issue target is still unclear, present top candidates and ask which issue number or URL should be inspected next.
|
|
51
|
+
|
|
52
|
+
### 3) Read a specific issue in detail
|
|
53
|
+
|
|
54
|
+
- Preferred command:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python3 scripts/read_issue.py 123 --comments
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Optional flags:
|
|
61
|
+
- `--repo owner/name`
|
|
62
|
+
- `--json`
|
|
63
|
+
- `--comments`
|
|
64
|
+
- Use the returned title, body, labels, assignees, state, timestamps, and comments to summarize the issue precisely.
|
|
65
|
+
|
|
66
|
+
### 4) Summarize gaps before any follow-up action
|
|
67
|
+
|
|
68
|
+
- Identify missing acceptance criteria, repro details, affected components, or environment context.
|
|
69
|
+
- If issue text and comments are insufficient, state exactly what is missing instead of inventing a fix plan.
|
|
70
|
+
|
|
71
|
+
## Included Script
|
|
72
|
+
|
|
73
|
+
### `scripts/find_issues.py`
|
|
74
|
+
|
|
75
|
+
- Purpose: consistent remote issue listing via `gh issue list`.
|
|
76
|
+
- Outputs a readable table by default, or JSON with `--output json`.
|
|
77
|
+
- Uses only GitHub CLI so it reflects remote GitHub state.
|
|
78
|
+
|
|
79
|
+
### `scripts/read_issue.py`
|
|
80
|
+
|
|
81
|
+
- Purpose: deterministic issue detail retrieval via `gh issue view`.
|
|
82
|
+
- Outputs either a human-readable summary or full JSON for downstream automation.
|
|
83
|
+
- Can include issue comments so the agent can read the latest discussion before taking any other step.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Read GitHub Issue"
|
|
3
|
+
short_description: "Read and search remote GitHub issues"
|
|
4
|
+
default_prompt: "Use $read-github-issue to verify the target GitHub repository with gh, find candidate issues with the bundled issue-search script, inspect the selected issue with comments and structured fields, summarize the trustworthy issue context, and call out any missing details before any separate planning or implementation work."
|
package/{fix-github-issues/scripts/list_issues.py → read-github-issue/scripts/find_issues.py}
RENAMED
|
@@ -17,7 +17,7 @@ def positive_int(raw: str) -> int:
|
|
|
17
17
|
|
|
18
18
|
def parse_args() -> argparse.Namespace:
|
|
19
19
|
parser = argparse.ArgumentParser(
|
|
20
|
-
description="
|
|
20
|
+
description="Find remote GitHub issues with gh CLI and display table or JSON output."
|
|
21
21
|
)
|
|
22
22
|
parser.add_argument("--repo", help="Target repository in owner/name format.")
|
|
23
23
|
parser.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ISSUE_FIELDS = "number,title,body,state,author,labels,assignees,comments,createdAt,updatedAt,closedAt,url"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args() -> argparse.Namespace:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Read a remote GitHub issue with gh CLI and display summary or JSON output."
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("issue", help="Issue number or URL.")
|
|
18
|
+
parser.add_argument("--repo", help="Target repository in owner/name format.")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--comments",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Keep issue comments in formatted output.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--json",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help="Print raw JSON output.",
|
|
28
|
+
)
|
|
29
|
+
return parser.parse_args()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_command(args: argparse.Namespace) -> list[str]:
|
|
33
|
+
cmd = [
|
|
34
|
+
"gh",
|
|
35
|
+
"issue",
|
|
36
|
+
"view",
|
|
37
|
+
args.issue,
|
|
38
|
+
"--json",
|
|
39
|
+
ISSUE_FIELDS,
|
|
40
|
+
]
|
|
41
|
+
if args.repo:
|
|
42
|
+
cmd.extend(["--repo", args.repo])
|
|
43
|
+
return cmd
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def join_names(items: list[dict], key: str) -> str:
|
|
47
|
+
values = [item.get(key, "") for item in items if item.get(key)]
|
|
48
|
+
return ", ".join(values) if values else "-"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def print_summary(issue: dict, include_comments: bool) -> None:
|
|
52
|
+
print(f"Number: #{issue.get('number', '')}")
|
|
53
|
+
print(f"Title: {issue.get('title', '')}")
|
|
54
|
+
print(f"State: {issue.get('state', '')}")
|
|
55
|
+
print(f"URL: {issue.get('url', '')}")
|
|
56
|
+
print(f"Author: {(issue.get('author') or {}).get('login', '-')}")
|
|
57
|
+
print(f"Labels: {join_names(issue.get('labels', []), 'name')}")
|
|
58
|
+
print(f"Assignees: {join_names(issue.get('assignees', []), 'login')}")
|
|
59
|
+
print(f"Created: {issue.get('createdAt', '')}")
|
|
60
|
+
print(f"Updated: {issue.get('updatedAt', '')}")
|
|
61
|
+
print(f"Closed: {issue.get('closedAt', '') or '-'}")
|
|
62
|
+
print("")
|
|
63
|
+
print("Body:")
|
|
64
|
+
print(issue.get("body", "") or "-")
|
|
65
|
+
|
|
66
|
+
if include_comments:
|
|
67
|
+
comments = issue.get("comments", [])
|
|
68
|
+
print("")
|
|
69
|
+
print(f"Comments ({len(comments)}):")
|
|
70
|
+
if not comments:
|
|
71
|
+
print("-")
|
|
72
|
+
return
|
|
73
|
+
for comment in comments:
|
|
74
|
+
author = (comment.get("author") or {}).get("login", "-")
|
|
75
|
+
created = comment.get("createdAt", "")
|
|
76
|
+
body = comment.get("body", "") or "-"
|
|
77
|
+
print(f"- [{created}] {author}: {body}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main() -> int:
|
|
81
|
+
args = parse_args()
|
|
82
|
+
cmd = build_command(args)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
86
|
+
except FileNotFoundError:
|
|
87
|
+
print("Error: gh CLI is not installed or not in PATH.", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
except subprocess.CalledProcessError as exc:
|
|
90
|
+
print(exc.stderr.strip() or "gh issue view failed.", file=sys.stderr)
|
|
91
|
+
return exc.returncode
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
issue = json.loads(result.stdout)
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
print("Error: unable to parse gh output as JSON.", file=sys.stderr)
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
if args.json:
|
|
100
|
+
print(json.dumps(issue, indent=2))
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
print_summary(issue, include_comments=args.comments)
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
sys.exit(main())
|
package/{fix-github-issues/tests/test_list_issues.py → read-github-issue/tests/test_find_issues.py}
RENAMED
|
@@ -12,13 +12,13 @@ from pathlib import Path
|
|
|
12
12
|
from unittest.mock import patch
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "
|
|
16
|
-
SPEC = importlib.util.spec_from_file_location("
|
|
15
|
+
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "find_issues.py"
|
|
16
|
+
SPEC = importlib.util.spec_from_file_location("find_issues", SCRIPT_PATH)
|
|
17
17
|
MODULE = importlib.util.module_from_spec(SPEC)
|
|
18
18
|
SPEC.loader.exec_module(MODULE)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class
|
|
21
|
+
class FindIssuesTests(unittest.TestCase):
|
|
22
22
|
def test_build_command_with_filters(self) -> None:
|
|
23
23
|
args = Namespace(
|
|
24
24
|
repo="owner/repo",
|