@openhands/extensions 0.26.0 → 0.28.0
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/skills/custom-codereview-guide.md +31 -0
- package/.claude-plugin/marketplace.json +9 -0
- package/.codex-plugin/marketplace.json +9 -0
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/deprecation-check.yml +1 -1
- package/.github/workflows/pypi-publish.yml +2 -2
- package/.github/workflows/tests.yml +1 -1
- package/.plugin/marketplace.json +9 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/README.md +7 -6
- package/automations/bundle-index.js +5 -5
- package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
- package/automations/catalog/github-issue-triage/manifest.json +2 -2
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/automations/index.d.ts +9 -1
- package/automations/interface.json +9 -0
- package/automations/interface.schema.json +37 -2
- package/marketplaces/openhands-extensions.json +9 -0
- package/package.json +1 -1
- package/plugins/openhands/skills/openhands-automation/README.md +6 -4
- package/plugins/openhands/skills/openhands-automation/SKILL.md +103 -21
- package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +3 -4
- package/plugins/openhands/skills/openhands-sdk/SKILL.md +2 -1
- package/plugins/qa-changes/README.md +4 -0
- package/plugins/qa-changes/skills/qa-changes/README.md +3 -1
- package/plugins/qa-changes/skills/qa-changes/SKILL.md +42 -0
- package/plugins/vulnerability-remediation/skills/github/README.md +5 -1
- package/plugins/vulnerability-remediation/skills/github/SKILL.md +27 -0
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +51 -4
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/scripts/sync_openhands_sdk_skill.py +16 -13
- package/skills/azure-devops/README.md +3 -1
- package/skills/azure-devops/SKILL.md +4 -0
- package/skills/bitbucket/README.md +3 -1
- package/skills/bitbucket/SKILL.md +4 -2
- package/skills/bitbucket-cloud/README.md +2 -0
- package/skills/bitbucket-cloud/SKILL.md +2 -0
- package/skills/bitbucket-data-center/README.md +2 -0
- package/skills/bitbucket-data-center/SKILL.md +2 -0
- package/skills/github/README.md +5 -1
- package/skills/github/SKILL.md +27 -0
- package/skills/github/scripts/agent_conversation.py +51 -4
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +51 -4
- package/skills/github-issue-triage/scripts/agent_conversation.py +51 -4
- package/skills/github-pr-reviewer/README.md +5 -0
- package/skills/github-pr-reviewer/SKILL.md +12 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +51 -4
- package/skills/github-pr-reviewer/scripts/main.py +20 -6
- package/skills/github-pr-reviewer/scripts/worker.py +77 -15
- package/skills/github-pr-reviewer/tests/test_main.py +23 -0
- package/skills/gitlab/README.md +3 -1
- package/skills/gitlab/SKILL.md +4 -0
- package/skills/index.js +23 -11
- package/skills/linear/README.md +2 -0
- package/skills/linear/SKILL.md +6 -2
- package/skills/openhands-automation/README.md +6 -4
- package/skills/openhands-automation/SKILL.md +103 -21
- package/skills/openhands-automation/references/custom-automation.md +3 -4
- package/skills/openhands-sdk/SKILL.md +2 -1
- package/skills/qa-changes/README.md +3 -1
- package/skills/qa-changes/SKILL.md +42 -0
- package/skills/simplified-technical-english/.claude-plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/.codex-plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/.plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/README.md +21 -0
- package/skills/simplified-technical-english/SKILL.md +65 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +130 -0
- package/tests/test_github_reviewer_delivery.py +279 -2
- package/tests/test_interface_manifest.py +23 -1
|
@@ -61,6 +61,11 @@ This skill is activated by:
|
|
|
61
61
|
checkout when the review ends, so nothing accumulates between runs
|
|
62
62
|
- Publishes a real pull request review, with inline comments where a finding
|
|
63
63
|
maps to a changed line, and verifies on GitHub that it landed
|
|
64
|
+
- Requires live evidence from the real app before approving a user-visible UI
|
|
65
|
+
change when the repository's guidance demands it: unit tests, CSS-token
|
|
66
|
+
assertions, generated mockups, and reconstructed captures cannot substitute.
|
|
67
|
+
Missing evidence yields a COMMENT review that names the gap, so no approval
|
|
68
|
+
and no maintainer handoff
|
|
64
69
|
- Posts acknowledgement comments with AI disclosure
|
|
65
70
|
- Configurable review tone and polling schedule
|
|
66
71
|
- Optional human handoff after an exact-head approval. The scanner ranks the
|
|
@@ -189,6 +189,18 @@ handler recognizes the verdict and requests one configured maintainer through th
|
|
|
189
189
|
same handoff used after an approval. An in-scope change continues the existing
|
|
190
190
|
review unchanged.
|
|
191
191
|
|
|
192
|
+
Before approving a change to user-visible UI behavior, the prompt requires live
|
|
193
|
+
evidence from a real running application when the repository's guidance demands
|
|
194
|
+
it: a screenshot, screen recording, or equivalent capture of the running app
|
|
195
|
+
that exercises the production-facing path. Unit tests, CSS-token or contract
|
|
196
|
+
assertions, generated mockups, and reconstructed captures may support the review
|
|
197
|
+
but cannot substitute for that evidence. When the required evidence is missing,
|
|
198
|
+
the reviewer publishes one `event: COMMENT` review naming exactly what is
|
|
199
|
+
missing and ends with `🔄 CHANGES REQUESTED`, so approval is withheld and the
|
|
200
|
+
deterministic maintainer handoff does not fire. Non-UI changes keep the existing
|
|
201
|
+
requirement of the real command and its observed output, with tests insufficient
|
|
202
|
+
as sole proof.
|
|
203
|
+
|
|
192
204
|
The script prepares each review's workspace before the agent starts: the pull
|
|
193
205
|
request's head commit is downloaded as a tarball and extracted to a directory of
|
|
194
206
|
its own, which becomes the conversation's working directory. The agent is told
|
|
@@ -32,6 +32,11 @@ _CLOUD_START_GRACE_SECONDS = 10 * 60
|
|
|
32
32
|
# How long to wait for a paused conversation's sandbox to come back up.
|
|
33
33
|
_CLOUD_RESUME_TIMEOUT_SECONDS = 120
|
|
34
34
|
_CLOUD_POLL_SECONDS = 3
|
|
35
|
+
# How many times a matched delivery whose conversation ended in ERROR is sent
|
|
36
|
+
# again. The delivery key never changes for a stable subject, so without a
|
|
37
|
+
# retry its work is stranded; without a bound, a conversation that fails the
|
|
38
|
+
# same way every time would be re-run on every scan.
|
|
39
|
+
_MAX_ERROR_RETRIES = 2
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def _register_tools() -> None:
|
|
@@ -204,14 +209,23 @@ class AgentConversationDispatcher:
|
|
|
204
209
|
|
|
205
210
|
same_delivery = record.get("delivery") == delivery
|
|
206
211
|
same_head = bool(head) and record.get("head") == head
|
|
212
|
+
# Error retries count against one delivery; a new delivery starts over.
|
|
213
|
+
error_retries = int(record.get("error_retries") or 0) if same_delivery else 0
|
|
214
|
+
can_retry = error_retries < _MAX_ERROR_RETRIES
|
|
207
215
|
|
|
208
216
|
if self._cloud:
|
|
209
217
|
disposition, conversation_id = self._deliver_cloud(
|
|
210
|
-
record,
|
|
218
|
+
record,
|
|
219
|
+
conversation_id,
|
|
220
|
+
subject,
|
|
221
|
+
prompt,
|
|
222
|
+
same_delivery,
|
|
223
|
+
same_head,
|
|
224
|
+
can_retry,
|
|
211
225
|
)
|
|
212
226
|
else:
|
|
213
227
|
disposition = self._deliver_local(
|
|
214
|
-
conversation_id, prompt, same_delivery, same_head
|
|
228
|
+
conversation_id, prompt, same_delivery, same_head, can_retry
|
|
215
229
|
)
|
|
216
230
|
|
|
217
231
|
if disposition in ("deduplicated", "in_progress"):
|
|
@@ -228,6 +242,10 @@ class AgentConversationDispatcher:
|
|
|
228
242
|
}
|
|
229
243
|
if disposition == "created" and self._cloud:
|
|
230
244
|
new_record["started_at"] = time.time()
|
|
245
|
+
if disposition == "retried":
|
|
246
|
+
error_retries += 1
|
|
247
|
+
if error_retries:
|
|
248
|
+
new_record["error_retries"] = error_retries
|
|
231
249
|
_kv_request(state_key, "PUT", new_record)
|
|
232
250
|
return {
|
|
233
251
|
"disposition": disposition,
|
|
@@ -235,7 +253,14 @@ class AgentConversationDispatcher:
|
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
def _deliver_cloud(
|
|
238
|
-
self,
|
|
256
|
+
self,
|
|
257
|
+
record,
|
|
258
|
+
conversation_id,
|
|
259
|
+
subject,
|
|
260
|
+
prompt,
|
|
261
|
+
same_delivery,
|
|
262
|
+
same_head,
|
|
263
|
+
can_retry,
|
|
239
264
|
):
|
|
240
265
|
"""Deliver through the OpenHands API; return the disposition and the id.
|
|
241
266
|
|
|
@@ -272,11 +297,21 @@ class AgentConversationDispatcher:
|
|
|
272
297
|
if status in ("idle", "paused"):
|
|
273
298
|
self._cloud.run(conversation)
|
|
274
299
|
return "resumed", current_id
|
|
300
|
+
if status == "error" and can_retry:
|
|
301
|
+
# The delivery matched but its conversation died before the work
|
|
302
|
+
# finished, so send the revision again rather than strand it.
|
|
303
|
+
# The API reports an execution status only while the sandbox
|
|
304
|
+
# runs, so an errored conversation whose sandbox was already
|
|
305
|
+
# paused is not seen here and keeps reporting deduplicated.
|
|
306
|
+
self._cloud.send(conversation, prompt)
|
|
307
|
+
return "retried", current_id
|
|
275
308
|
return "deduplicated", current_id
|
|
276
309
|
self._cloud.send(conversation, prompt)
|
|
277
310
|
return "resumed", current_id
|
|
278
311
|
|
|
279
|
-
def _deliver_local(
|
|
312
|
+
def _deliver_local(
|
|
313
|
+
self, conversation_id, prompt, same_delivery, same_head, can_retry
|
|
314
|
+
) -> str:
|
|
280
315
|
if self._workspace is None:
|
|
281
316
|
raise RuntimeError("AgentConversationDispatcher must be used as a context")
|
|
282
317
|
|
|
@@ -321,6 +356,18 @@ class AgentConversationDispatcher:
|
|
|
321
356
|
):
|
|
322
357
|
conversation.update_secrets(self._secrets)
|
|
323
358
|
conversation.run(blocking=False)
|
|
359
|
+
elif (
|
|
360
|
+
conversation.state.execution_status
|
|
361
|
+
== ConversationExecutionStatus.ERROR
|
|
362
|
+
and can_retry
|
|
363
|
+
):
|
|
364
|
+
# The delivery matched but its conversation died before
|
|
365
|
+
# the work finished, so send the revision again rather
|
|
366
|
+
# than strand it.
|
|
367
|
+
conversation.update_secrets(self._secrets)
|
|
368
|
+
conversation.send_message(prompt)
|
|
369
|
+
conversation.run(blocking=False)
|
|
370
|
+
disposition = "retried"
|
|
324
371
|
else:
|
|
325
372
|
disposition = "deduplicated"
|
|
326
373
|
else:
|
|
@@ -847,7 +847,21 @@ def _build_review_prompt(
|
|
|
847
847
|
"credential, configuration, or external state stayed unchanged. Do not add speculative or "
|
|
848
848
|
"out-of-scope notes: every blocking or non-blocking observation must identify demonstrated "
|
|
849
849
|
"behavior on the current head and explain why it matters to the merge decision.\n"
|
|
850
|
-
|
|
850
|
+
"7. LIVE EVIDENCE GATE - if the change alters user-visible UI behavior and the repository "
|
|
851
|
+
"guidance requires live evidence for it, you must not approve unless that evidence is from a "
|
|
852
|
+
"real running application and exercises the production-facing path. Live evidence means a "
|
|
853
|
+
"screenshot, screen recording, or equivalent capture of the running app, not the source, a "
|
|
854
|
+
"diff, or a description of what it should render. Unit tests, CSS-token or contract "
|
|
855
|
+
"assertions, generated mockups, and reconstructed or hand-built captures may support the "
|
|
856
|
+
"review but never substitute for the required live evidence. When the repository guidance "
|
|
857
|
+
"requires that evidence and only non-live evidence is available, do not approve: publish "
|
|
858
|
+
"exactly one review with `event: COMMENT` that names exactly which live evidence is missing, "
|
|
859
|
+
"and end its body with the verdict on its own line: `🔄 CHANGES REQUESTED`. State the gap as "
|
|
860
|
+
"a blocking finding, not a note, and never let a passing test suite stand in for the missing "
|
|
861
|
+
"capture. A UI change that does supply compliant live evidence, and any non-UI change "
|
|
862
|
+
"(backend, API, CLI, script) that follows the existing requirement of the real command and "
|
|
863
|
+
"its observed output, continues to step 8.\n"
|
|
864
|
+
f"8. Publish one review with `POST /repos/{repo}/pulls/{number}/reviews`, using "
|
|
851
865
|
"`commit_id` equal to the Head SHA above. Use `event: APPROVE` when there are "
|
|
852
866
|
"no material findings; otherwise use `event: COMMENT`. Never use "
|
|
853
867
|
"`REQUEST_CHANGES`.\n"
|
|
@@ -858,19 +872,19 @@ def _build_review_prompt(
|
|
|
858
872
|
" Put the overall assessment in `body`, and each line-specific finding in the `comments` "
|
|
859
873
|
"array with `path`, `line`, `side: RIGHT`, and `body`.\n"
|
|
860
874
|
" Only create inline comments for actionable findings; do not open praise or nitpick threads.\n"
|
|
861
|
-
"
|
|
875
|
+
"9. If a finding cannot be attached to a changed line, put it in the review body instead. "
|
|
862
876
|
"If the API rejects the inline positions, retry with every finding in the body and no `comments` array. "
|
|
863
877
|
"If GitHub forbids the configured bot from approving its own PR, retry the clean review with "
|
|
864
878
|
"`event: COMMENT` and keep the approved verdict.\n"
|
|
865
|
-
"
|
|
879
|
+
"10. Begin the review body with this disclosure: "
|
|
866
880
|
"`_This review was posted by an AI agent (OpenHands)._`\n"
|
|
867
|
-
"
|
|
881
|
+
"11. End the review body with a verdict on its own line: either `✅ APPROVED` "
|
|
868
882
|
"or `🔄 CHANGES REQUESTED`.\n"
|
|
869
|
-
"
|
|
883
|
+
"12. If there are no material issues, still publish a review saying so, with the "
|
|
870
884
|
"disclosure and the verdict.\n"
|
|
871
885
|
f"\nReview instructions:\n{tone}{extra}{guide_section}\n\n"
|
|
872
886
|
"After GitHub accepts the review, output exactly `GITHUB_REVIEW_POSTED`. "
|
|
873
|
-
"If publishing still fails after the fallback in step
|
|
887
|
+
"If publishing still fails after the fallback in step 9, output the complete review text "
|
|
874
888
|
"so it can be posted as a comment instead."
|
|
875
889
|
)
|
|
876
890
|
|
|
@@ -27,6 +27,11 @@ from maintainer_handoff import (
|
|
|
27
27
|
# every current-head check and workflow run, so a red head still blocks.
|
|
28
28
|
CHECK_GATE_MARKER = "<!-- openhands-review-gate:"
|
|
29
29
|
NON_BLOCKING_CHECK_CONCLUSIONS = frozenset({"success", "neutral", "skipped"})
|
|
30
|
+
# GitHub parks a fork head's workflow runs at this conclusion until a maintainer
|
|
31
|
+
# approves them. Such a run is waiting on a person, not failing, so the gate
|
|
32
|
+
# lists it as waiting with this note and its explanation asks for the approval.
|
|
33
|
+
AWAITING_APPROVAL_CONCLUSION = "action_required"
|
|
34
|
+
AWAITING_APPROVAL_NOTE = " (awaiting maintainer approval)"
|
|
30
35
|
# The gate is deterministic; this disclosure is what tells a reader no model ran.
|
|
31
36
|
WORKFLOW_DISCLOSURE = "no AI was used to generate this comment"
|
|
32
37
|
|
|
@@ -98,7 +103,8 @@ class ReviewIntake:
|
|
|
98
103
|
flush=True,
|
|
99
104
|
)
|
|
100
105
|
continue
|
|
101
|
-
|
|
106
|
+
# A retried conversation runs again, so it uses a slot like a new one.
|
|
107
|
+
if result["disposition"] in ("created", "retried"):
|
|
102
108
|
self._started += 1
|
|
103
109
|
if failures:
|
|
104
110
|
raise RuntimeError(
|
|
@@ -283,7 +289,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
283
289
|
f"second result.{self_review_note}"
|
|
284
290
|
)
|
|
285
291
|
|
|
286
|
-
def _finish_completed_review(self, pr, trigger, label=None):
|
|
292
|
+
def _finish_completed_review(self, pr, trigger, label=None, submitted_review=None):
|
|
287
293
|
"""Complete an exact-head review, including an optional human handoff.
|
|
288
294
|
|
|
289
295
|
`trigger` is the event the current work is keyed on. It is None for an
|
|
@@ -291,7 +297,11 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
291
297
|
is the whole key, so any submitted review by this account on the current
|
|
292
298
|
head is the completion of that work. That is what lets an unrequested
|
|
293
299
|
review the scan started be reconciled and handed off on a later scan
|
|
294
|
-
instead of being restarted.
|
|
300
|
+
instead of being restarted. `submitted_review` is the review a
|
|
301
|
+
`submitted` delivery reports. It can predate the latest
|
|
302
|
+
`review_requested` event, so it is found by id rather than by the
|
|
303
|
+
trigger window; this account's later reviews of the head still count, so
|
|
304
|
+
a newer verdict supersedes it. A dismissed review completes nothing.
|
|
295
305
|
"""
|
|
296
306
|
head_sha = pr["head"]["sha"]
|
|
297
307
|
if trigger is not None:
|
|
@@ -299,14 +309,35 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
299
309
|
else:
|
|
300
310
|
triggered_at = ""
|
|
301
311
|
reviews = self.gh_pages(f"/pulls/{pr['number']}/reviews")
|
|
302
|
-
|
|
312
|
+
mine = [
|
|
303
313
|
review
|
|
304
314
|
for review in reviews
|
|
305
315
|
if review.get("commit_id") == head_sha
|
|
306
316
|
and ((review.get("user") or {}).get("login") or "").lower()
|
|
307
317
|
== self.github_login.lower()
|
|
308
|
-
and (review.get("
|
|
318
|
+
and (review.get("state") or "").upper() != "DISMISSED"
|
|
309
319
|
]
|
|
320
|
+
if submitted_review and submitted_review.get("id") is not None:
|
|
321
|
+
reported = next(
|
|
322
|
+
(review for review in mine if review.get("id") == submitted_review["id"]),
|
|
323
|
+
None,
|
|
324
|
+
)
|
|
325
|
+
reported_at = (reported or {}).get("submitted_at") or ""
|
|
326
|
+
completed = (
|
|
327
|
+
[]
|
|
328
|
+
if reported is None
|
|
329
|
+
else [
|
|
330
|
+
review
|
|
331
|
+
for review in mine
|
|
332
|
+
if (review.get("submitted_at") or "") >= reported_at
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
completed = [
|
|
337
|
+
review
|
|
338
|
+
for review in mine
|
|
339
|
+
if (review.get("submitted_at") or "") > triggered_at
|
|
340
|
+
]
|
|
310
341
|
if not completed:
|
|
311
342
|
return False
|
|
312
343
|
approved = None
|
|
@@ -482,16 +513,19 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
482
513
|
|
|
483
514
|
A completed run whose conclusion is neither blocking nor explicitly
|
|
484
515
|
non-blocking fails closed, so an unknown conclusion cannot silently
|
|
485
|
-
approve a PR. A run that has not completed
|
|
486
|
-
approval.
|
|
516
|
+
approve a PR. A run that has not completed, or one awaiting a
|
|
517
|
+
maintainer's approval to run, means waiting, never approval.
|
|
487
518
|
"""
|
|
488
519
|
blocking, pending = [], []
|
|
489
520
|
for run in reporters:
|
|
490
521
|
name = run.get("name") or "unnamed check"
|
|
491
522
|
status = (run.get("status") or "").lower()
|
|
523
|
+
conclusion = (run.get("conclusion") or "").lower()
|
|
492
524
|
if status != "completed":
|
|
493
525
|
pending.append(name)
|
|
494
|
-
elif
|
|
526
|
+
elif conclusion == AWAITING_APPROVAL_CONCLUSION:
|
|
527
|
+
pending.append(f"{name}{AWAITING_APPROVAL_NOTE}")
|
|
528
|
+
elif conclusion in NON_BLOCKING_CHECK_CONCLUSIONS:
|
|
495
529
|
continue
|
|
496
530
|
else:
|
|
497
531
|
blocking.append(name)
|
|
@@ -650,10 +684,10 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
650
684
|
short = sha[:12]
|
|
651
685
|
listed = "\n".join(f"- `{name}`" for name in names)
|
|
652
686
|
if state == "blocked":
|
|
653
|
-
heading = "### ⚠️ Review paused: current-head checks
|
|
687
|
+
heading = "### ⚠️ Review paused: current-head checks did not pass"
|
|
654
688
|
lead = (
|
|
655
|
-
f"The current head `{short}` has
|
|
656
|
-
"conversation was started:"
|
|
689
|
+
f"The current head `{short}` has checks that did not pass, so no "
|
|
690
|
+
"review conversation was started:"
|
|
657
691
|
)
|
|
658
692
|
action = (
|
|
659
693
|
"Fix the checks above and push. The scheduled scan then starts "
|
|
@@ -665,6 +699,25 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
665
699
|
"second request while the first is still outstanding. The review "
|
|
666
700
|
"starts on the updated head."
|
|
667
701
|
)
|
|
702
|
+
elif any(name.endswith(AWAITING_APPROVAL_NOTE) for name in names):
|
|
703
|
+
heading = "### ⏳ Review waiting on workflow approval"
|
|
704
|
+
lead = (
|
|
705
|
+
f"The current head `{short}` has workflow runs that GitHub holds "
|
|
706
|
+
"until a maintainer approves them, or checks that have not "
|
|
707
|
+
"finished, so no review conversation was started:"
|
|
708
|
+
)
|
|
709
|
+
action = (
|
|
710
|
+
"A maintainer needs to approve the workflow runs marked above. "
|
|
711
|
+
"The scheduled scan retries once every check on the head reports "
|
|
712
|
+
"a conclusion."
|
|
713
|
+
if scheduled
|
|
714
|
+
else "A maintainer needs to approve the workflow runs marked "
|
|
715
|
+
"above. Once every check on the head reports a conclusion, "
|
|
716
|
+
f"remove the outstanding `{self.trigger_reviewer}` request and "
|
|
717
|
+
f"request `{self.trigger_reviewer}` again: GitHub will not "
|
|
718
|
+
"accept a second request while the first is still outstanding. "
|
|
719
|
+
"That starts the review."
|
|
720
|
+
)
|
|
668
721
|
else:
|
|
669
722
|
heading = "### ⏳ Review waiting on checks"
|
|
670
723
|
lead = (
|
|
@@ -858,6 +911,11 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
858
911
|
for candidate in prs:
|
|
859
912
|
try:
|
|
860
913
|
pr = self.gh("GET", f"/pulls/{candidate['number']}")
|
|
914
|
+
submitted_review = (
|
|
915
|
+
payload.get("review")
|
|
916
|
+
if event_mode and payload.get("action") == "submitted"
|
|
917
|
+
else None
|
|
918
|
+
)
|
|
861
919
|
has_label = label in {
|
|
862
920
|
item["name"] for item in pr.get("labels", [])
|
|
863
921
|
}
|
|
@@ -894,7 +952,11 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
894
952
|
delivery_key = self._unrequested_head(pr)
|
|
895
953
|
unrequested_candidate = True
|
|
896
954
|
if trigger is None and delivery_key is None:
|
|
897
|
-
|
|
955
|
+
# A submitted review is its own completion signal: its
|
|
956
|
+
# request can predate the issue-event window, and that
|
|
957
|
+
# review must still be reconciled.
|
|
958
|
+
if submitted_review is None:
|
|
959
|
+
continue
|
|
898
960
|
# A head this account already reviewed, with no clarifying
|
|
899
961
|
# comment since, is done: another conversation would publish a
|
|
900
962
|
# second review of identical code. This must come before the
|
|
@@ -918,7 +980,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
918
980
|
),
|
|
919
981
|
flush=True,
|
|
920
982
|
)
|
|
921
|
-
if trigger is not None:
|
|
983
|
+
if trigger is not None or submitted_review is not None:
|
|
922
984
|
# Reconcile the completed review (clearing the label and
|
|
923
985
|
# running the maintainer handoff) where the review is
|
|
924
986
|
# this trigger's result. When it predates the trigger -
|
|
@@ -927,7 +989,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
927
989
|
# otherwise the label stays and every later scan re-reads
|
|
928
990
|
# this head and re-logs with no effect.
|
|
929
991
|
if not self._finish_completed_review(
|
|
930
|
-
pr, trigger, trigger_label
|
|
992
|
+
pr, trigger, trigger_label, submitted_review
|
|
931
993
|
) and trigger_label:
|
|
932
994
|
self.gh(
|
|
933
995
|
"DELETE",
|
|
@@ -936,7 +998,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
936
998
|
)
|
|
937
999
|
continue
|
|
938
1000
|
if delivery_key is None and self._finish_completed_review(
|
|
939
|
-
pr, trigger, trigger_label
|
|
1001
|
+
pr, trigger, trigger_label, submitted_review
|
|
940
1002
|
):
|
|
941
1003
|
continue
|
|
942
1004
|
if event_mode and payload.get("action") == "submitted":
|
|
@@ -538,6 +538,29 @@ class TestRepoReviewGuide(unittest.TestCase):
|
|
|
538
538
|
self.assertIn("Do not add speculative or out-of-scope notes", prompt)
|
|
539
539
|
self.assertIn("forbids the configured bot from approving its own PR", prompt)
|
|
540
540
|
|
|
541
|
+
def test_prompt_requires_live_ui_evidence_before_approval(self):
|
|
542
|
+
prompt = main._build_review_prompt(
|
|
543
|
+
"owner/repo",
|
|
544
|
+
self._pr(),
|
|
545
|
+
"0123456789abcdef",
|
|
546
|
+
{"id": "1", "created_at": "t"},
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# The gate sits after the finding-quality rules and before the review is
|
|
550
|
+
# published, so a missing capture is a blocking finding, not a note.
|
|
551
|
+
gate = prompt.index("LIVE EVIDENCE GATE")
|
|
552
|
+
self.assertLess(prompt.index("Ground every finding"), gate)
|
|
553
|
+
self.assertLess(gate, prompt.index("Publish one review"))
|
|
554
|
+
self.assertIn("user-visible UI behavior", prompt)
|
|
555
|
+
self.assertIn("real running application", prompt)
|
|
556
|
+
self.assertIn("production-facing path", prompt)
|
|
557
|
+
self.assertIn("CSS-token", prompt)
|
|
558
|
+
self.assertIn("generated mockups", prompt)
|
|
559
|
+
self.assertIn("reconstructed", prompt)
|
|
560
|
+
self.assertIn("never substitute for the required live evidence", prompt)
|
|
561
|
+
self.assertIn("names exactly which live evidence is missing", prompt)
|
|
562
|
+
self.assertIn("`🔄 CHANGES REQUESTED`", prompt)
|
|
563
|
+
|
|
541
564
|
def test_prompt_requires_refreshing_mutable_github_state(self):
|
|
542
565
|
"""A resumed conversation must not trust an earlier turn's observations."""
|
|
543
566
|
prompt = main._build_review_prompt(
|
package/skills/gitlab/README.md
CHANGED
|
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated GitLab MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `GITLAB_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
|
|
15
17
|
the GitLab API.
|
|
16
18
|
|
|
@@ -34,4 +36,4 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
|
|
34
36
|
```bash
|
|
35
37
|
git remote -v && git branch # to find the current org, repo and branch
|
|
36
38
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
37
|
-
```
|
|
39
|
+
```
|
package/skills/gitlab/SKILL.md
CHANGED
|
@@ -5,6 +5,10 @@ triggers:
|
|
|
5
5
|
- gitlab
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## Connection selection
|
|
9
|
+
|
|
10
|
+
Use authenticated GitLab MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when GitLab MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
11
|
+
|
|
8
12
|
You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
|
|
9
13
|
the GitLab API.
|
|
10
14
|
|