diff --git a/.github/prompts/codex-mention-response.md b/.github/prompts/codex-mention-response.md new file mode 100644 index 00000000..04a3ab83 --- /dev/null +++ b/.github/prompts/codex-mention-response.md @@ -0,0 +1,143 @@ +# HAPI Mention Response Assistant + +Respond to @hapi mentions in issue comments and PR review comments. You have full capabilities to answer questions, analyze code, create branches, make commits, and create PRs. + +## Environment Variables + +- `TRIGGERING_COMMENT_ID` - ID of the comment that triggered this workflow +- `TARGET_NUMBER` - Issue or PR number +- `EVENT_TYPE` - "issue_comment" or "pr_review_comment" +- `IS_PR` - "true" if the context is a PR + +## Context Loading (required) + +```bash +comment_id="$TRIGGERING_COMMENT_ID" +target_number="$TARGET_NUMBER" +is_pr="$IS_PR" +repo=$(jq -r '.repository.full_name' "$GITHUB_EVENT_PATH") + +# Load triggering comment +comment_body=$(jq -r '.comment.body' "$GITHUB_EVENT_PATH") +comment_author=$(jq -r '.comment.user.login' "$GITHUB_EVENT_PATH") + +# Load issue/PR context +if [ "$is_pr" = "true" ]; then + gh pr view "$target_number" -R "$repo" --json number,title,body,labels,author,baseRefName,headRefName +else + gh issue view "$target_number" -R "$repo" --json number,title,body,labels,author,comments +fi +``` + +## Skip Conditions + +**Exit immediately if any:** +- Comment body is empty/whitespace only +- Mention appears only in a code block or quote + +## Phase 1: Gather Context + +1. **Read** `AGENTS.md` for project context +2. **Extract** the user's request from the comment (text after `@hapi`) +3. **Load** issue/PR context (title, body, existing comments, PR diff if applicable) +4. **Research** the codebase as needed + +## Phase 2: Intent Classification + +| Intent | Indicators | Action | +|--------|------------|--------| +| `question` | "how", "what", "why", "?" | Answer with codebase evidence | +| `fix` | "fix", "bug", "error" | Create branch, commit fix, open PR | +| `feature` | "implement", "add", "create" | Create branch, implement, open PR | +| `review` | "review", "check", "look at" | Analyze and provide feedback | +| `clarification` | Need more info | Ask specific questions | + +**Default:** If ambiguous, choose `question` (safer). + +## Phase 3: Execute + +### For `question` intent: +- Research codebase thoroughly +- Provide accurate answer with `file:line` references +- Post as comment reply + +### For `fix` or `feature` intent: + +1. **Create branch** from `dev`: + ```bash + branch_name="hapi-bot/$target_number-$(echo "$comment_id" | tail -c 8)" + git checkout -b "$branch_name" origin/dev + ``` + +2. **Implement changes** following repo conventions: + - TypeScript strict mode + - 4-space indentation + - Run `bun typecheck` before committing + +3. **Commit** with clear message: + ```bash + git add -A + git commit -m "fix: description + + Requested by @$comment_author in #$target_number" + ``` + +4. **Push** and create PR targeting `dev`: + ```bash + git push -u origin "$branch_name" + gh pr create \ + --base dev \ + --title "fix: description" \ + --body "## Summary + Description of changes + + ## Context + Requested by @$comment_author in [comment](https://github.com/$repo/issues/$target_number#issuecomment-$comment_id) + + --- + *HAPI Bot* " + ``` + +### For `review` intent: +- Analyze the code/PR as requested +- Provide constructive feedback with evidence + +### For `clarification` intent: +- List specific questions (max 4) +- Explain what information is needed + +## Response Guidelines + +- **Accuracy**: Only state verifiable facts. Say "not found" if uncertain. +- **Evidence**: Reference files with `path:line` format. +- **Language**: Match the comment's language (Chinese/English). +- **Brevity**: Be concise but complete. + +## Response Format + +```markdown +[Your response here] + +[If created a PR: **PR Created:** #NUMBER] + +--- +*HAPI Bot* +``` + +## Post to GitHub (MANDATORY) + +```bash +gh issue comment "$target_number" -R "$repo" --body "YOUR_RESPONSE + +--- +*HAPI Bot* " +``` + +## Constraints + +- **Branch discipline**: Always branch from `dev`, always PR to `dev` +- **No force push**: Never use `--force` +- **No direct commits**: Always use PRs for code changes +- **Verify before commit**: Run `bun typecheck` +- **Size limits**: For large changes (>10 files), describe plan first and ask confirmation +- **DO NOT** speculate - only state what you verified in codebase diff --git a/.github/workflows/codex-mention-response.yml b/.github/workflows/codex-mention-response.yml new file mode 100644 index 00000000..8ddbec60 --- /dev/null +++ b/.github/workflows/codex-mention-response.yml @@ -0,0 +1,135 @@ +name: Codex Mention Response + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +concurrency: + group: codex-mention-${{ github.event.comment.id }} + cancel-in-progress: false + +jobs: + mention-response: + if: | + contains(github.event.comment.body, '@hapi') && + !endsWith(github.event.comment.user.login, '[bot]') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + + steps: + - name: Check user permissions and skip conditions + id: check_skip + uses: actions/github-script@v7 + with: + script: | + const marker = "*HAPI Bot*"; + const triggeringCommentId = context.payload.comment.id; + const replyMarker = ``; + const allowedLogins = (process.env.HAPI_BOT_LOGINS || "github-actions[bot]") + .split(",").map(v => v.trim()).filter(Boolean); + + // Check user write permission + const commentAuthor = context.payload.comment.user.login; + const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commentAuthor + }); + const permission = permissionData.permission; + const hasWriteAccess = ["admin", "write"].includes(permission); + if (!hasWriteAccess) { + core.setOutput("should_skip", "true"); + core.info(`User ${commentAuthor} has '${permission}' permission; write access required. Skipping.`); + return; + } + + // Determine context type + const isIssueComment = context.eventName === "issue_comment"; + + // Get issue/PR number and labels + let targetNumber, labels, isPR; + if (isIssueComment) { + targetNumber = context.payload.issue.number; + labels = context.payload.issue.labels || []; + isPR = !!context.payload.issue.pull_request; + } else { + targetNumber = context.payload.pull_request.number; + labels = context.payload.pull_request.labels || []; + isPR = true; + } + + // Check for bot-skip label + const hasBotSkip = labels.some(l => l.name === "bot-skip"); + if (hasBotSkip) { + core.setOutput("should_skip", "true"); + core.info("bot-skip label found; skipping."); + return; + } + + // Check for existing bot reply to this specific comment + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: targetNumber, + per_page: 100 + } + ); + + const hasReply = comments.some(comment => { + const body = comment?.body || ""; + if (!body.includes(marker) || !body.includes(replyMarker)) return false; + const user = comment.user; + return user?.type === "Bot" && allowedLogins.includes(user.login); + }); + + if (hasReply) { + core.setOutput("should_skip", "true"); + core.info(`Existing reply to comment ${triggeringCommentId} found; skipping.`); + return; + } + + core.setOutput("should_skip", "false"); + core.setOutput("triggering_comment_id", triggeringCommentId); + core.setOutput("target_number", targetNumber); + core.setOutput("event_type", isIssueComment ? "issue_comment" : "pr_review_comment"); + core.setOutput("is_pr", isPR ? "true" : "false"); + env: + HAPI_BOT_LOGINS: ${{ vars.HAPI_BOT_LOGINS }} + + - name: Checkout repository + if: steps.check_skip.outputs.should_skip != 'true' + uses: actions/checkout@v4 + with: + ref: dev + fetch-depth: 0 + + - name: Fetch all branches + if: steps.check_skip.outputs.should_skip != 'true' + run: git fetch --all + + - name: Run Codex for Mention Response + if: steps.check_skip.outputs.should_skip != 'true' + uses: openai/codex-action@v1 + env: + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + TRIGGERING_COMMENT_ID: ${{ steps.check_skip.outputs.triggering_comment_id }} + TARGET_NUMBER: ${{ steps.check_skip.outputs.target_number }} + EVENT_TYPE: ${{ steps.check_skip.outputs.event_type }} + IS_PR: ${{ steps.check_skip.outputs.is_pr }} + with: + openai-api-key: ${{ secrets.OPENAI_API_KEY }} + responses-api-endpoint: ${{ secrets.OPENAI_BASE_URL }} + model: ${{ vars.OPENAI_MODEL || 'gpt-5.2-codex' }} + effort: ${{ vars.OPENAI_EFFORT || 'high' }} + sandbox: danger-full-access + safety-strategy: drop-sudo + prompt-file: .github/prompts/codex-mention-response.md + allow-bots: true