diff --git a/.agents/skills/openhands-handoff/SKILL.md b/.agents/skills/openhands-handoff/SKILL.md index 0e89aa654..a8a70fdb7 100644 --- a/.agents/skills/openhands-handoff/SKILL.md +++ b/.agents/skills/openhands-handoff/SKILL.md @@ -25,17 +25,18 @@ required summary artifacts keep local and cloud agents synchronized. ## Key Concepts -| Concept | Meaning | -| ---------------- | --------------------------------------------------------------------------------------------- | -| Actions agent | `.github/workflows/openhands-agent.yml`, used for short cloud runs. | -| VPS session | Long-running OpenHands Web UI/SDK deployment from `ops/openhands/docker-compose.yml`. | -| Model profile | `sonnet`, `gpt`, or `gemini`; maps to a LiteLLM model id in the workflow. | -| Literal model | Any LiteLLM-compatible `provider/model` string supplied with `model=...`. | -| Provider secret | `LLM_API_KEY_`, inferred from the model prefix, with `LLM_API_KEY` fallback. | -| Output mode | `pr-comment`, `respond-comments`, `thread-replies`, or `summary-only`. | -| Summary artifact | `.llm/tmp/openhands/summary.md`, required before the workflow exits. | -| Thread replies | Optional `.llm/tmp/openhands/replies.json` review-comment replies. | -| Chainable token | `PAT_TOKEN` or GitHub App token; required for cloud-created events to trigger more workflows. | +| Concept | Meaning | +| ---------------- | ----------------------------------------------------------------------------------------------- | +| Actions agent | `.github/workflows/openhands-agent.yml`, used for short cloud runs. | +| VPS session | Long-running OpenHands Web UI/SDK deployment from `ops/openhands/docker-compose.yml`. | +| Model profile | `sonnet`, `gpt`, or `gemini`; maps to a LiteLLM model id in the workflow. | +| Literal model | Any LiteLLM-compatible `provider/model` string supplied with `model=...`. | +| Provider secret | `LLM_API_KEY_`, inferred from the model prefix, with `LLM_API_KEY` fallback. | +| Output mode | `pr-comment`, `respond-comments`, `thread-replies`, or `summary-only`. | +| Summary artifact | `.llm/tmp/openhands/summary.md`, required before the workflow exits. | +| Status comment | One workflow-owned PR/issue comment that starts as running and is edited with the final result. | +| Thread replies | Optional `.llm/tmp/openhands/replies.json` review-comment replies. | +| Chainable token | `PAT_TOKEN` or GitHub App token; required for cloud-created events to trigger more workflows. | ## Workflow @@ -66,6 +67,8 @@ required summary artifacts keep local and cloud agents synchronized. `replies.json`. Use `respond-comments` for a single high-quality response when IDs are uncertain. - **Skipping the summary artifact**: GitHub comments and PR bodies come from `.llm/tmp/openhands/summary.md`; missing summaries produce weak handoffs. +- **Posting duplicate comments**: the workflow owns the status/final comment. Agents write + artifacts; they do not call `gh issue comment` directly. - **Using Actions for long sessions**: move multi-step, human-in-the-loop work to the VPS Web UI. ## Reference Files diff --git a/.github/workflows/openhands-agent.yml b/.github/workflows/openhands-agent.yml index 97fb82c0c..1cdf38350 100644 --- a/.github/workflows/openhands-agent.yml +++ b/.github/workflows/openhands-agent.yml @@ -195,11 +195,61 @@ jobs: core.setOutput('prompt', prompt); core.setOutput('issue_number', String(issueNumber)); core.setOutput('is_pr', String(isPr)); + core.setOutput('trigger_comment_id', String(payload.comment?.id || '')); core.setOutput('checkout_repo', checkoutRepo); core.setOutput('checkout_ref', checkoutRef); core.setOutput('base_branch', baseBranch); core.setOutput('branch_name', `openhands/${issueNumber || context.runId}-${context.runAttempt}`); + - name: Acknowledge trigger and post run link + id: status-comment + if: steps.request.outputs.output_mode != 'summary-only' && steps.request.outputs.issue_number != '' + uses: actions/github-script@v7 + env: + ISSUE_NUMBER: ${{ steps.request.outputs.issue_number }} + TRIGGER_COMMENT_ID: ${{ steps.request.outputs.trigger_comment_id }} + MODEL: ${{ steps.request.outputs.model }} + PROVIDER: ${{ steps.request.outputs.provider }} + OUTPUT_MODE: ${{ steps.request.outputs.output_mode }} + RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const issue_number = Number(process.env.ISSUE_NUMBER); + const triggerCommentId = process.env.TRIGGER_COMMENT_ID; + + if (triggerCommentId) { + try { + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: Number(triggerCommentId), + content: 'eyes', + }); + } catch (error) { + core.notice(`Could not add trigger reaction: ${error.message}`); + } + } + + const body = [ + '', + '## OpenHands Agent — Running', + '', + `Model: \`${process.env.MODEL}\``, + `Provider: \`${process.env.PROVIDER}\``, + `Output mode: \`${process.env.OUTPUT_MODE}\``, + '', + `Run: ${process.env.RUN_URL}`, + ].join('\n'); + + const { data: comment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body, + }); + core.setOutput('comment_id', String(comment.id)); + - name: Checkout repository uses: actions/checkout@v6 with: @@ -246,6 +296,7 @@ jobs: - Use rtk for read-heavy git/grep/gh/docker commands when it is available. - Preserve user changes and avoid destructive git commands. - Run the smallest validation that proves the change. + - Do not post GitHub issue or PR comments directly. The workflow owns GitHub comments. - Write .llm/tmp/openhands/summary.md before exit. Include Summary, Changes, Validation, Responses to review comments or issue comments when relevant, and Remaining risks. - If output_mode is thread-replies, optionally write .llm/tmp/openhands/replies.json as @@ -376,12 +427,18 @@ jobs: env: GH_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ steps.request.outputs.issue_number }} + STATUS_COMMENT_ID: ${{ steps.status-comment.outputs.comment_id }} MODEL: ${{ steps.request.outputs.model }} + PROVIDER: ${{ steps.request.outputs.provider }} + JOB_STATUS: ${{ job.status }} RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | if [ ! -f .llm/tmp/openhands/summary.md ]; then cat > .llm/tmp/openhands/summary.md <" + echo "## OpenHands Agent — ${JOB_STATUS}" + echo echo "Model: \`${MODEL}\`" + echo "Provider: \`${PROVIDER}\`" echo cat .llm/tmp/openhands/summary.md echo echo "Run: ${RUN_URL}" } > .llm/tmp/openhands/comment.md - gh issue comment "${ISSUE_NUMBER}" --body-file .llm/tmp/openhands/comment.md + if [ -n "${STATUS_COMMENT_ID}" ]; then + python - <<'PY' + import json + + with open(".llm/tmp/openhands/comment.md", encoding="utf-8") as source: + payload = {"body": source.read()} + with open(".llm/tmp/openhands/comment.json", "w", encoding="utf-8") as target: + json.dump(payload, target) + PY + gh api --method PATCH \ + "repos/${GITHUB_REPOSITORY}/issues/comments/${STATUS_COMMENT_ID}" \ + --input .llm/tmp/openhands/comment.json \ + --jq .html_url >/dev/null + else + gh issue comment "${ISSUE_NUMBER}" --body-file .llm/tmp/openhands/comment.md + fi - name: Post review-thread replies if: always() && steps.request.outputs.output_mode == 'thread-replies' && steps.request.outputs.is_pr == 'true' diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md index 4746b32c6..fdb377dee 100644 --- a/.openhands/microagents/repo.md +++ b/.openhands/microagents/repo.md @@ -36,6 +36,9 @@ Before finishing an OpenHands workflow run, write `.llm/tmp/openhands/summary.md - responses to relevant PR, issue, or review comments, - remaining risks or follow-up work. +Do not post GitHub issue or PR comments directly. The workflow reacts to the trigger, creates the +running status comment, and edits that comment with this summary artifact. + When the workflow output mode is `thread-replies`, optionally write `.llm/tmp/openhands/replies.json`: diff --git a/AGENTS-handoff.md b/AGENTS-handoff.md index 0116386e4..509a00531 100644 --- a/AGENTS-handoff.md +++ b/AGENTS-handoff.md @@ -56,6 +56,10 @@ provider-specific base URLs use the same suffix pattern, such as `LLM_BASE_URL_O The agent must write `.llm/tmp/openhands/summary.md` before exit. +The workflow owns GitHub comments: it reacts to the trigger comment, posts one running status +comment with the Actions URL, then edits that same comment with the final summary. Agents should not +post their own PR or issue comments during OpenHands runs. + ## Token Rule GitHub does not trigger follow-up workflows from events created with the default `GITHUB_TOKEN`. Use