From fa931ca63f9216ba12efa75cab25569a53cdd898 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 12:16:05 +0000 Subject: [PATCH 01/17] feat(ci): add issue auto-implement action and workflow - Add workflow issue-auto-implement.yml (label, comment, PR review triggers) - Add composite action: team check, labels, assess script, implement placeholder, verify loop, create PR - Add assess script (TypeScript/tsx, Claude, fixtures) with vitest tests - Add workflow issue-auto-implement-test.yml to run assess tests on PR - Add README (usage, inputs, testing) and AGENTS.md (flows, verification, next steps) - Use AUTO_IMPLEMENT_* prefix for secrets/vars; require github_allowed_trigger_team Made-with: Cursor --- .../actions/issue-auto-implement/AGENTS.md | 71 + .../actions/issue-auto-implement/README.md | 49 + .../actions/issue-auto-implement/action.yml | 211 ++ .../assess/fixtures/issue-comment.json | 11 + .../assess/fixtures/issue-labeled.json | 11 + .../assess/fixtures/pull_request_review.json | 14 + .../issue-auto-implement/assess/index.test.ts | 63 + .../issue-auto-implement/assess/index.ts | 154 ++ .../assess/normalize.test.ts | 92 + .../issue-auto-implement/assess/normalize.ts | 77 + .../assess/package-lock.json | 2379 +++++++++++++++++ .../issue-auto-implement/assess/package.json | 17 + .../issue-auto-implement/assess/tsconfig.json | 14 + .../assess/vitest.config.ts | 8 + .../workflows/issue-auto-implement-test.yml | 28 + .github/workflows/issue-auto-implement.yml | 40 + 16 files changed, 3239 insertions(+) create mode 100644 .github/actions/issue-auto-implement/AGENTS.md create mode 100644 .github/actions/issue-auto-implement/README.md create mode 100644 .github/actions/issue-auto-implement/action.yml create mode 100644 .github/actions/issue-auto-implement/assess/fixtures/issue-comment.json create mode 100644 .github/actions/issue-auto-implement/assess/fixtures/issue-labeled.json create mode 100644 .github/actions/issue-auto-implement/assess/fixtures/pull_request_review.json create mode 100644 .github/actions/issue-auto-implement/assess/index.test.ts create mode 100644 .github/actions/issue-auto-implement/assess/index.ts create mode 100644 .github/actions/issue-auto-implement/assess/normalize.test.ts create mode 100644 .github/actions/issue-auto-implement/assess/normalize.ts create mode 100644 .github/actions/issue-auto-implement/assess/package-lock.json create mode 100644 .github/actions/issue-auto-implement/assess/package.json create mode 100644 .github/actions/issue-auto-implement/assess/tsconfig.json create mode 100644 .github/actions/issue-auto-implement/assess/vitest.config.ts create mode 100644 .github/workflows/issue-auto-implement-test.yml create mode 100644 .github/workflows/issue-auto-implement.yml diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md new file mode 100644 index 00000000..5a471d59 --- /dev/null +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -0,0 +1,71 @@ +# AGENTS.md — Issue auto-implement action + +For agents making changes to this action. This file summarizes flows, design decisions, and implementation details. + +## Flows + +### 1. Issue to first PR + +- **Triggers:** `issues.labeled` (prefixed trigger label), `issue_comment.created` on a labeled issue when **no PR exists yet**. +- **Flow:** Normalize event → assess (enough info?) → if `request_info`: post comment, add needs-info label, exit. If `implement`: implement step (push to branch `auto-implement-issue-`) → verify → on fail retry (cap `max_implement_retries`); on pass create PR with "Closes #N", add pr-created label, optional comment. + +### 2. Issue comment when PR already exists + +- **Trigger:** `issue_comment.created` on an issue that **already has an open PR** for that issue. +- **Flow:** Post a short reply on the issue directing the user to the PR; exit. No assessment or implement. + +### 3. PR review → iteration + +- **Triggers:** `pull_request_review.submitted`, `pull_request_review_comment.created` when the PR is from an automation branch or body contains "Closes #N". +- **Flow:** Resolve issue number from PR (body "Closes #N"/"Fixes #N" or head branch `auto-implement-issue-`) → assess with issue + review content → implement ("address review feedback"), push to same branch → verify → on pass: do **not** create PR; post comment on the PR summarizing the new commit(s). + +## Event normalization + +From the workflow event payload, derive: + +- **Issue number:** For `issues` or `issue_comment`: `event.issue.number`. For `pull_request_review` or `pull_request_review_comment`: parse PR body for "Closes #N" or "Fixes #N", or PR head branch for `auto-implement-issue-`. +- **PR exists for issue (issue_comment only):** Check whether an open PR exists for that issue (e.g. head branch `auto-implement-issue-` or body "Closes #"). + +## Assess script + +- **Path:** `assess/index.ts` (TypeScript), run with `npx tsx assess/index.ts` (no build). +- **Input:** Reads event from `GITHUB_EVENT_PATH`; optional context files from input. +- **Output:** JSON with `action` (`implement` | `request_info`), `comment_body` (if request_info), `verification_notes` (optional). Written to file or GITHUB_OUTPUT. +- **When triggered by PR review:** Include PR review body and review comments in the payload sent to Claude. + +## Branch and PR + +- Branch name: `auto-implement-issue-`. +- PR title or body must include "Closes #N" (or "Fixes #N") so merging auto-closes the issue. +- On iteration (PR already exists): do not create a new PR; post a comment on the PR summarizing the new commit(s). + +## Restricting who can trigger + +Only members of the `github_allowed_trigger_team` (input; set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`) may trigger the flow. First step checks `github.actor` against that team; if the variable is unset or the actor is not a member, fail immediately. Token must have `read:org`. + +## Labels + +All automation labels use `label_prefix` (default `automation`): `{prefix}/auto-implement`, `{prefix}/needs-info`, `{prefix}/pr-created`. Create via API if missing. + +## Verification + +When changing this action or the assess script: + +1. **Run the assess unit tests locally** before committing: `cd .github/actions/issue-auto-implement/assess && npm ci && npm test`. Do not rely on CI alone—catch failures locally first, then push. +2. **CI** runs the same tests in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches `.github/actions/issue-auto-implement/**`. +3. **Optional:** Run the assess script with a fixture and `ANTHROPIC_API_KEY` for end-to-end assessment behavior. +4. **Full workflow:** Trigger manually on a test issue (trigger label or comment). Ensure repo secrets/variables are set. Inspect the Actions run and the issue/PR; re-run after changes to the implement or verify loop. + +## Next steps (implementation backlog) + +Recommended order: + +1. **Context files in assess** — Pass the `context_files` input into the assess script (e.g. via env) and include those file contents (AGENTS.md, REFERENCE.md, etc.) in the Claude assessment prompt so the model has full repo guidance. +2. **Fetch all issue comments** — For `issues` and `issue_comment` events, optionally call the GitHub API to list all comments on the issue and include them in the assessment payload (not only the single comment from the event). +3. **Implement step (real)** — Replace the placeholder with Claude generating and applying code changes. Options: call Anthropic API to produce a patch or file edits from the issue body + verification_notes (and repo context), then apply, commit, and push; or integrate a Claude Code Action / external tool. Branch is already checked out; implement must make commits and push. +4. **True implement–verify loop** — On verify failure, re-run the **implement** step with the verify failure output in the prompt (then verify again), up to `max_implement_retries`. Currently only the verify command is retried; the plan requires re-implementing with failure context. +5. **PR review iteration path** — When the trigger is `pull_request_review` or `pull_request_review_comment`: after verify passes, do **not** create a new PR; instead post a comment on the existing PR summarizing the changes in the new commit(s). Detect "PR already exists" (e.g. from event type or by checking for an open PR for this branch) and branch the flow: create PR vs. comment on PR. +6. **Optional comment when PR is created** — Add an input (e.g. `post_pr_comment`) and, when creating a PR, optionally post a short comment on the issue linking to the new PR. +7. **Comment when retries exhausted** — When the verify loop fails after all retries, post a comment on the issue so the run is visible and explainable from the issue thread. Add a step that runs on failure (e.g. `if: failure() && steps.assess.outputs.action == 'implement'`) and posts a comment on the issue: e.g. "The auto-implement run could not complete: verification failed after N attempts. See the [workflow run](link) for logs. You can address the failure and re-trigger by adding a comment or re-applying the label." +8. **Secrets and variables** — Document in README: add `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` as a repo secret; set `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM` (required) as a repo variable; note that the default `GITHUB_TOKEN` may need to be replaced with a PAT that has `read:org` for the team check. +9. **Local run with fixture and Claude** — Add an npm script or small wrapper (e.g. `npm run assess:fixture issue-labeled`) to run the assess script with a fixture and real `ANTHROPIC_API_KEY` for manual end-to-end assessment testing. diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md new file mode 100644 index 00000000..dae491a4 --- /dev/null +++ b/.github/actions/issue-auto-implement/README.md @@ -0,0 +1,49 @@ +# Issue auto-implement action + +Reusable composite action for label-triggered issue automation: assess (request more info or implement), implement-verify loop, then create PR or iterate on an existing PR after review. + +## Usage + +Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_key` (e.g. from repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), `github_allowed_trigger_team` (e.g. from repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`), and `github_token` from the workflow. + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `anthropic_api_key` | Yes | - | Claude API key. Set via repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` so multiple actions can use different keys. | +| `github_token` | Yes | - | Token with contents, issues, pull-requests, read:org | +| `context_files` | No | AGENTS.md,REFERENCE.md | Comma-separated paths for assessment context | +| `assessment_reference_issue` | No | 192 | Reference issue number for "enough information" | +| `label_prefix` | No | automation | Prefix for labels (e.g. automation/auto-implement) | +| `verify_commands` | No | go test ./... | Commands run for verification | +| `max_implement_retries` | No | 3 | Max retries on verify failure (cap 5) | +| `github_allowed_trigger_team` | Yes | - | GitHub Team slug (e.g. org/team); only members can trigger. Set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. | + +Secrets and variables use an action-specific prefix (e.g. `AUTO_IMPLEMENT_`) so each action can have its own keys/variables and it's clear which workflow uses which. This also avoids clashing with platform-reserved names (e.g. `GITHUB_*`). + +## Triggers + +- **issues.labeled** — prefixed trigger label (e.g. `automation/auto-implement`) +- **issue_comment.created** — on an issue with that label (redirect to PR if PR exists) +- **pull_request_review.submitted** / **pull_request_review_comment.created** — PR from automation branch or with "Closes #N" + +## Labels + +The action ensures these labels exist (creates them if missing): `{prefix}/auto-implement`, `{prefix}/needs-info`, `{prefix}/pr-created`. + +## Testing + +From the repo root, run the assess script tests: + +```bash +cd .github/actions/issue-auto-implement/assess && npm ci && npm test +``` + +CI runs these in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches this action. To run the assess script with a real Claude call, set `ANTHROPIC_API_KEY` and use a fixture: + +```bash +cd .github/actions/issue-auto-implement/assess +GITHUB_EVENT_PATH=./fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues npx tsx index.ts +``` + +For implementation details, verification steps, and the implementation backlog, see `AGENTS.md`. diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml new file mode 100644 index 00000000..da054858 --- /dev/null +++ b/.github/actions/issue-auto-implement/action.yml @@ -0,0 +1,211 @@ +name: 'Issue auto-implement' +description: 'Assess labeled issues (or PR reviews), request more info or implement with verify loop; create PR or iterate on existing PR.' +inputs: + anthropic_api_key: + description: 'API key for Claude (assessment and implement step)' + required: true + github_token: + description: 'GitHub token (contents, issues, pull-requests, read:org)' + required: true + context_files: + description: 'Comma-separated paths to repo files to include in assessment (e.g. AGENTS.md, REFERENCE.md)' + required: false + default: 'AGENTS.md,REFERENCE.md' + assessment_reference_issue: + description: 'Reference issue number for "enough information" example (e.g. 192)' + required: false + default: '192' + label_prefix: + description: 'Prefix for automation labels (e.g. automation/auto-implement, automation/needs-info, automation/pr-created)' + required: false + default: 'automation' + verify_commands: + description: 'Shell commands to run for verification (e.g. go test ./..., go build)' + required: false + default: 'go test ./...' + max_implement_retries: + description: 'Max retries for implement step on verify failure (default 3, max 5)' + required: false + default: '3' + github_allowed_trigger_team: + description: 'GitHub Team slug whose members may trigger the flow (e.g. hookdeck/automation-triggers). Set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM.' + required: true +runs: + using: 'composite' + steps: + - name: Check allowed trigger team + shell: bash + run: | + TEAM="${{ inputs.github_allowed_trigger_team }}" + if [ -z "$TEAM" ]; then + echo "::error::github_allowed_trigger_team must be set (e.g. repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM). Only team members can trigger this workflow." + exit 1 + fi + ORG="${TEAM%/*}" + SLUG="${TEAM#*/}" + HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer ${{ inputs.github_token }}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/orgs/${ORG}/teams/${SLUG}/members/${{ github.actor }}") + if [ "$HTTP" != "204" ]; then + echo "::error::Actor ${{ github.actor }} is not in team $TEAM (HTTP $HTTP). Only team members can trigger this workflow." + exit 1 + fi + echo "Actor ${{ github.actor }} is in team $TEAM." + - name: Ensure labels exist + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + LABEL_PREFIX: ${{ inputs.label_prefix }} + run: | + for LABEL in auto-implement needs-info pr-created; do + NAME="${LABEL_PREFIX}/${LABEL}" + curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/labels" \ + -d "{\"name\":\"$NAME\",\"color\":\"ededed\"}" | grep -q 201 || true + done + - name: Install assess script dependencies + shell: bash + run: cd .github/actions/issue-auto-implement/assess && npm ci --omit=dev + - id: assess + name: Run assess + shell: bash + env: + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + GITHUB_TOKEN: ${{ inputs.github_token }} + GITHUB_REPOSITORY: ${{ github.repository }} + ASSESSMENT_REFERENCE_ISSUE: ${{ inputs.assessment_reference_issue }} + run: | + RESULT=$(cd .github/actions/issue-auto-implement/assess && npx tsx index.ts) + echo "action=$(echo "$RESULT" | jq -r '.action')" >> $GITHUB_OUTPUT + echo "issue_number=$(echo "$RESULT" | jq -r '.issue_number // empty')" >> $GITHUB_OUTPUT + echo "pr_url=$(echo "$RESULT" | jq -r '.pr_url // empty')" >> $GITHUB_OUTPUT + echo "comment_body<> $GITHUB_OUTPUT + echo "$RESULT" | jq -r '.comment_body // empty' >> $GITHUB_OUTPUT + echo "BODY_EOF" >> $GITHUB_OUTPUT + echo "verification_notes<> $GITHUB_OUTPUT + echo "$RESULT" | jq -r '.verification_notes // empty' >> $GITHUB_OUTPUT + echo "NOTES_EOF" >> $GITHUB_OUTPUT + - name: Handle redirect to PR + if: steps.assess.outputs.action == 'redirect_to_pr' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + PR_URL: ${{ steps.assess.outputs.pr_url }} + run: | + BODY="A PR is open for this issue. Please review and comment on the PR: $PR_URL" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "{\"body\":$(echo "$BODY" | jq -Rs .)}" + echo "Redirected user to PR. Exiting." + exit 0 + - name: Handle request more info + if: steps.assess.outputs.action == 'request_info' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + LABEL_PREFIX: ${{ inputs.label_prefix }} + run: | + COMMENT_BODY="${{ steps.assess.outputs.comment_body }}" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "$(jq -n --arg b "$COMMENT_BODY" '{body: $b}')" + NEEDS_LABEL="${LABEL_PREFIX}/needs-info" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" \ + -d "{\"labels\":[\"$NEEDS_LABEL\"]}" + echo "Posted request for more info. Exiting." + exit 0 + - name: Checkout branch for implement + if: steps.assess.outputs.action == 'implement' + shell: bash + env: + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + run: | + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + git fetch origin main + if git show-ref --verify --quiet refs/remotes/origin/"$BRANCH"; then + git checkout "$BRANCH" + git merge origin/main --no-edit + else + git checkout -b "$BRANCH" origin/main + fi + - name: Implement + if: steps.assess.outputs.action == 'implement' + shell: bash + env: + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + VERIFICATION_NOTES: ${{ steps.assess.outputs.verification_notes }} + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + run: | + echo "Implement step: would run Claude Code Action or API here." + echo "Verification notes: $VERIFICATION_NOTES" + # Placeholder until Claude implement is wired; verify step will run next. + - name: Push branch + if: steps.assess.outputs.action == 'implement' + shell: bash + env: + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + run: | + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + git push -u origin "$BRANCH" || true + - id: verify_loop + name: Verify (with retries) + if: steps.assess.outputs.action == 'implement' + shell: bash + env: + MAX_RETRIES: ${{ inputs.max_implement_retries }} + VERIFY_CMDS: ${{ inputs.verify_commands }} + run: | + MAX=${MAX_RETRIES:-3} + for i in $(seq 1 "$MAX"); do + echo "Verify attempt $i of $MAX" + if eval "$VERIFY_CMDS"; then + echo "verified=true" >> $GITHUB_OUTPUT + exit 0 + fi + done + echo "verified=false" >> $GITHUB_OUTPUT + echo "::error::Verification failed after $MAX attempts" + exit 1 + - name: Create PR + if: steps.assess.outputs.action == 'implement' && steps.verify_loop.outcome == 'success' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + LABEL_PREFIX: ${{ inputs.label_prefix }} + run: | + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + TITLE="Auto-implement issue #${ISSUE_NUMBER}" + BODY="Closes #${ISSUE_NUMBER}" + PR_JSON=$(curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/pulls" \ + -d "{\"title\":\"$TITLE\",\"body\":\"$BODY\",\"head\":\"$BRANCH\",\"base\":\"main\"}") + PR_URL=$(echo "$PR_JSON" | jq -r '.html_url // empty') + if [ -n "$PR_URL" ]; then + PR_NUM=$(echo "$PR_JSON" | jq -r '.number') + NEEDS_LABEL="${LABEL_PREFIX}/pr-created" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" \ + -d "{\"labels\":[\"$NEEDS_LABEL\"]}" + echo "Created PR: $PR_URL" + fi diff --git a/.github/actions/issue-auto-implement/assess/fixtures/issue-comment.json b/.github/actions/issue-auto-implement/assess/fixtures/issue-comment.json new file mode 100644 index 00000000..495653b5 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/fixtures/issue-comment.json @@ -0,0 +1,11 @@ +{ + "action": "created", + "issue": { + "number": 42, + "title": "Example issue", + "body": "Description", + "labels": [{ "name": "automation/auto-implement" }] + }, + "comment": { "body": "Here is more context for implementation." }, + "repository": { "full_name": "hookdeck/hookdeck-cli" } +} diff --git a/.github/actions/issue-auto-implement/assess/fixtures/issue-labeled.json b/.github/actions/issue-auto-implement/assess/fixtures/issue-labeled.json new file mode 100644 index 00000000..7c29f204 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/fixtures/issue-labeled.json @@ -0,0 +1,11 @@ +{ + "action": "labeled", + "issue": { + "number": 192, + "title": "Fix rule-filter-headers storing JSON as escaped string", + "body": "When using --rule-filter-headers the value is stored as an escaped JSON string...", + "labels": [{ "name": "automation/auto-implement" }] + }, + "label": { "name": "automation/auto-implement" }, + "repository": { "full_name": "hookdeck/hookdeck-cli" } +} diff --git a/.github/actions/issue-auto-implement/assess/fixtures/pull_request_review.json b/.github/actions/issue-auto-implement/assess/fixtures/pull_request_review.json new file mode 100644 index 00000000..df81f386 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/fixtures/pull_request_review.json @@ -0,0 +1,14 @@ +{ + "action": "submitted", + "pull_request": { + "number": 99, + "body": "Closes #192", + "head": { "ref": "auto-implement-issue-192" }, + "html_url": "https://github.com/hookdeck/hookdeck-cli/pull/99" + }, + "review": { + "body": "Please add a test for the new behavior.", + "state": "changes_requested" + }, + "repository": { "full_name": "hookdeck/hookdeck-cli" } +} diff --git a/.github/actions/issue-auto-implement/assess/index.test.ts b/.github/actions/issue-auto-implement/assess/index.test.ts new file mode 100644 index 00000000..4f4c468f --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/index.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { assess } from './index.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = resolve(__dirname, 'fixtures'); + +function loadFixture(name: string): unknown { + const raw = readFileSync(resolve(FIXTURES_DIR, name), 'utf-8'); + return JSON.parse(raw); +} + +describe('assess', () => { + it('returns implement or request_info with valid shape when using mock client', async () => { + const mockClient = { + messages: { + create: vi.fn().mockResolvedValue({ + content: [ + { + type: 'text', + text: '{"action":"implement","verification_notes":"Run go test ./..."}', + }, + ], + }), + }, + } as unknown as import('@anthropic-ai/sdk').Anthropic; + + const payload = loadFixture('issue-labeled.json'); + const result = await assess('issues', payload, { + referenceIssue: '192', + anthropicClient: mockClient, + }); + + expect(result.action).toMatch(/^(implement|request_info)$/); + if (result.action === 'request_info') { + expect(typeof result.comment_body).toBe('string'); + } + if (result.action === 'implement' && result.verification_notes !== undefined) { + expect(typeof result.verification_notes).toBe('string'); + } + }); + + it('returns valid output shape for issue_comment fixture with mock client', async () => { + const mockClient = { + messages: { + create: vi.fn().mockResolvedValue({ + content: [{ type: 'text', text: '{"action":"request_info","comment_body":"Please add steps to reproduce."}' }], + }), + }, + } as unknown as import('@anthropic-ai/sdk').Anthropic; + + const payload = loadFixture('issue-comment.json'); + const result = await assess('issue_comment', payload, { + referenceIssue: '192', + anthropicClient: mockClient, + }); + + expect(['implement', 'request_info', 'redirect_to_pr']).toContain(result.action); + if (result.action === 'request_info') expect(typeof result.comment_body).toBe('string'); + }); +}); diff --git a/.github/actions/issue-auto-implement/assess/index.ts b/.github/actions/issue-auto-implement/assess/index.ts new file mode 100644 index 00000000..07cba48d --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/index.ts @@ -0,0 +1,154 @@ +#!/usr/bin/env -S npx tsx +/** + * Assess script: read GitHub event, normalize, optionally check redirect, call Claude, output JSON. + * Output: { action: 'implement' | 'request_info' | 'redirect_to_pr', comment_body?, verification_notes?, pr_url? } + * Run: GITHUB_EVENT_PATH=... GITHUB_EVENT_NAME=... [ANTHROPIC_API_KEY=...] npx tsx index.ts + */ + +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import Anthropic from '@anthropic-ai/sdk'; +import { normalizeEvent } from './normalize.js'; + +const EVENT_PATH = process.env.GITHUB_EVENT_PATH || ''; +const EVENT_NAME = process.env.GITHUB_EVENT_NAME || ''; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ''; +const REPO = process.env.GITHUB_REPOSITORY || ''; + +export type AssessmentOutput = { + action: 'implement' | 'request_info' | 'redirect_to_pr'; + issue_number?: number; + comment_body?: string; + verification_notes?: string; + pr_url?: string; +}; + +async function checkExistingPr(owner: string, repo: string, issueNumber: number): Promise<{ pr_url: string } | null> { + if (!GITHUB_TOKEN) return null; + const branch = `auto-implement-issue-${issueNumber}`; + const url = `https://api.github.com/repos/${owner}/${repo}/pulls?head=${owner}:${branch}&state=open`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${GITHUB_TOKEN}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) return null; + const data = (await res.json()) as { html_url?: string }[]; + const pr = data?.[0]; + return pr?.html_url ? { pr_url: pr.html_url } : null; +} + +function loadPayload(): { eventName: string; payload: unknown } { + if (!EVENT_PATH) { + throw new Error('GITHUB_EVENT_PATH is not set'); + } + const raw = readFileSync(EVENT_PATH, 'utf-8'); + const payload = JSON.parse(raw) as unknown; + const eventName = EVENT_NAME || (payload as Record)?.action ? inferEventName(payload) : ''; + if (!eventName) throw new Error('Could not determine event name (set GITHUB_EVENT_NAME or use a standard payload)'); + return { eventName, payload }; +} + +function inferEventName(payload: unknown): string { + const p = payload as Record; + if (p.issue && p.comment) return 'issue_comment'; + if (p.pull_request && p.review) return 'pull_request_review'; + if (p.pull_request && (p as { comment?: unknown }).comment) return 'pull_request_review_comment'; + if (p.issue && p.label) return 'issues'; + return ''; +} + +function buildAssessmentPrompt(payload: unknown, eventName: string, referenceIssue: string): string { + const p = payload as Record; + const issue = p.issue as { title?: string; body?: string; number?: number } | undefined; + const parts: string[] = [ + 'You are assessing a GitHub issue to decide if there is enough information to implement a fix or feature.', + 'Reply with a single JSON object only, no markdown or extra text, with these keys:', + '- action: either "implement" or "request_info"', + '- comment_body: (required if action is request_info) a short message to post on the issue asking for the missing information', + '- verification_notes: (optional if action is implement) free-form notes for the implementer, e.g. "run go test ./pkg/... and ensure build passes"', + '', + 'Issue:', + `Title: ${issue?.title ?? 'N/A'}`, + `Body: ${issue?.body ?? 'N/A'}`, + '', + `Reference example of "enough information": GitHub issue #${referenceIssue} (use similar clarity and specificity).`, + ]; + + const comment = p.comment as { body?: string } | undefined; + if (comment?.body) { + parts.push('', 'Latest comment:', comment.body); + } + if (Array.isArray(p.comments) && p.comments.length) { + parts.push('', 'Comments:', JSON.stringify(p.comments, null, 2)); + } + if (eventName === 'pull_request_review' || eventName === 'pull_request_review_comment') { + const review = (p.review as { body?: string }) ?? {}; + parts.push('', 'PR review (address this feedback):', review.body ?? 'N/A'); + } + + parts.push('', 'Output only the JSON object:'); + return parts.join('\n'); +} + +async function callClaude(prompt: string, client?: Anthropic): Promise { + const api = client ?? new Anthropic({ apiKey: ANTHROPIC_API_KEY }); + if (!client && !ANTHROPIC_API_KEY) { + throw new Error('ANTHROPIC_API_KEY is not set'); + } + const response = await api.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + const text = response.content?.[0]?.type === 'text' ? response.content[0].text : ''; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('Claude did not return valid JSON: ' + text.slice(0, 200)); + } + const parsed = JSON.parse(jsonMatch[0]) as AssessmentOutput; + if (parsed.action !== 'implement' && parsed.action !== 'request_info') { + parsed.action = 'request_info'; + } + return parsed; +} + +async function main(): Promise { + const { eventName, payload } = loadPayload(); + const result = await assess(eventName, payload, { + repo: REPO, + token: GITHUB_TOKEN, + referenceIssue: process.env.ASSESSMENT_REFERENCE_ISSUE || '192', + }); + console.log(JSON.stringify(result)); +} + +/** Exported for tests: run assessment with given payload and optional mock client */ +export async function assess( + eventName: string, + payload: unknown, + opts: { repo?: string; token?: string; referenceIssue?: string; anthropicClient?: Anthropic } +): Promise { + const normalized = normalizeEvent(eventName, payload); + if (!normalized) throw new Error('Could not normalize event'); + + if (eventName === 'issue_comment' && opts.repo && opts.token) { + const [owner, repo] = opts.repo.split('/'); + if (owner && repo) { + const existing = await checkExistingPr(owner, repo, normalized.issueNumber); + if (existing) return { action: 'redirect_to_pr', issue_number: normalized.issueNumber, pr_url: existing.pr_url }; + } + } + + const referenceIssue = opts.referenceIssue ?? '192'; + const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue); + const result = await callClaude(prompt, opts.anthropicClient); + result.issue_number = normalized.issueNumber; + return result; +} + +if (process.env.GITHUB_EVENT_PATH) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/.github/actions/issue-auto-implement/assess/normalize.test.ts b/.github/actions/issue-auto-implement/assess/normalize.test.ts new file mode 100644 index 00000000..4d0a3c00 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/normalize.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest'; +import { + parseClosesIssueNumber, + parseIssueNumberFromBranch, + issueNumberFromPrPayload, + normalizeEvent, +} from './normalize'; + +describe('parseClosesIssueNumber', () => { + it('extracts issue number from Closes #123', () => { + expect(parseClosesIssueNumber('Closes #123')).toBe(123); + expect(parseClosesIssueNumber('Closes #1')).toBe(1); + }); + it('extracts issue number from Fixes #456', () => { + expect(parseClosesIssueNumber('Fixes #456')).toBe(456); + }); + it('returns first match', () => { + expect(parseClosesIssueNumber('Closes #10 and Fixes #20')).toBe(10); + }); + it('returns null for empty or no match', () => { + expect(parseClosesIssueNumber('')).toBeNull(); + expect(parseClosesIssueNumber('No issue here')).toBeNull(); + expect(parseClosesIssueNumber(null as unknown as string)).toBeNull(); + }); +}); + +describe('parseIssueNumberFromBranch', () => { + it('extracts issue number from auto-implement-issue-', () => { + expect(parseIssueNumberFromBranch('auto-implement-issue-42')).toBe(42); + expect(parseIssueNumberFromBranch('auto-implement-issue-1')).toBe(1); + }); + it('returns null for non-matching branch', () => { + expect(parseIssueNumberFromBranch('main')).toBeNull(); + expect(parseIssueNumberFromBranch('feature/foo')).toBeNull(); + expect(parseIssueNumberFromBranch('auto-implement-issue-')).toBeNull(); + }); +}); + +describe('issueNumberFromPrPayload', () => { + it('prefers body Closes #N over branch', () => { + expect( + issueNumberFromPrPayload({ + body: 'Closes #99', + head: { ref: 'auto-implement-issue-42' }, + }) + ).toBe(99); + }); + it('uses branch when body has no Closes/Fixes', () => { + expect( + issueNumberFromPrPayload({ + body: 'Some description', + head: { ref: 'auto-implement-issue-42' }, + }) + ).toBe(42); + }); + it('returns null when neither present', () => { + expect(issueNumberFromPrPayload({})).toBeNull(); + expect(issueNumberFromPrPayload({ body: '', head: { ref: 'main' } })).toBeNull(); + }); +}); + +describe('normalizeEvent', () => { + it('extracts issue number from issues payload', () => { + const r = normalizeEvent('issues', { issue: { number: 192 } }); + expect(r).toEqual({ eventName: 'issues', issueNumber: 192 }); + }); + + it('extracts issue number from issue_comment payload', () => { + const r = normalizeEvent('issue_comment', { issue: { number: 5 } }); + expect(r).toEqual({ eventName: 'issue_comment', issueNumber: 5, redirectToPr: false }); + }); + + it('extracts issue number from pull_request_review (body)', () => { + const r = normalizeEvent('pull_request_review', { + pull_request: { body: 'Fixes #10', head: { ref: 'auto-implement-issue-10' } }, + }); + expect(r).toEqual({ eventName: 'pull_request_review', issueNumber: 10, headRef: 'auto-implement-issue-10' }); + }); + + it('extracts issue number from pull_request_review (branch only)', () => { + const r = normalizeEvent('pull_request_review', { + pull_request: { head: { ref: 'auto-implement-issue-7' } }, + }); + expect(r).toEqual({ eventName: 'pull_request_review', issueNumber: 7, headRef: 'auto-implement-issue-7' }); + }); + + it('returns null for unknown event or missing data', () => { + expect(normalizeEvent('push', {})).toBeNull(); + expect(normalizeEvent('issues', {})).toBeNull(); + expect(normalizeEvent('pull_request_review', { pull_request: {} })).toBeNull(); + }); +}); diff --git a/.github/actions/issue-auto-implement/assess/normalize.ts b/.github/actions/issue-auto-implement/assess/normalize.ts new file mode 100644 index 00000000..f6fb39d3 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/normalize.ts @@ -0,0 +1,77 @@ +/** + * Event normalization: derive issue number (and related) from GitHub workflow event payloads. + * Used for: issues, issue_comment, pull_request_review, pull_request_review_comment. + */ + +export interface NormalizedEvent { + eventName: string; + issueNumber: number; + /** For issue_comment: should we redirect to PR instead of assessing? (PR exists for this issue) */ + redirectToPr?: boolean; + prUrl?: string; + /** Pull request head ref (e.g. auto-implement-issue-123) for PR events */ + headRef?: string; +} + +/** Match "Closes #123" or "Fixes #456" in text; returns first match or null */ +export function parseClosesIssueNumber(text: string): number | null { + if (!text || typeof text !== 'string') return null; + const match = text.match(/(?:Closes|Fixes)\s+#(\d+)/i); + return match ? parseInt(match[1], 10) : null; +} + +/** Match branch name auto-implement-issue-; returns N or null */ +export function parseIssueNumberFromBranch(branchName: string): number | null { + if (!branchName || typeof branchName !== 'string') return null; + const match = branchName.match(/^auto-implement-issue-(\d+)$/); + return match ? parseInt(match[1], 10) : null; +} + +/** Derive issue number from a PR payload (body or head ref) */ +export function issueNumberFromPrPayload(pr: { body?: string | null; head?: { ref?: string } }): number | null { + const fromBody = pr?.body != null ? parseClosesIssueNumber(pr.body) : null; + if (fromBody != null) return fromBody; + const ref = pr?.head?.ref; + return ref != null ? parseIssueNumberFromBranch(ref) : null; +} + +/** Minimal payload shapes we need from GitHub Actions event */ +export type IssuePayload = { issue: { number: number }; label?: { name: string } }; +export type IssueCommentPayload = { issue: { number: number } }; +export type PullRequestReviewPayload = { pull_request: { number: number; body?: string | null; head?: { ref?: string }; html_url?: string }; review?: { body?: string | null } }; + +export function normalizeEvent(eventName: string, payload: unknown): NormalizedEvent | null { + if (!payload || typeof payload !== 'object') return null; + + const p = payload as Record; + + if (eventName === 'issues') { + const issue = (p.issue as { number: number })?.number; + if (issue == null) return null; + return { eventName: 'issues', issueNumber: issue }; + } + + if (eventName === 'issue_comment') { + const issue = (p.issue as { number: number })?.number; + if (issue == null) return null; + return { + eventName: 'issue_comment', + issueNumber: issue, + redirectToPr: false, // caller must set from API if PR exists + }; + } + + if (eventName === 'pull_request_review' || eventName === 'pull_request_review_comment') { + const pr = p.pull_request as { body?: string | null; head?: { ref?: string }; html_url?: string } | undefined; + if (!pr) return null; + const issueNumber = issueNumberFromPrPayload(pr); + if (issueNumber == null) return null; + return { + eventName, + issueNumber, + headRef: pr.head?.ref, + }; + } + + return null; +} diff --git a/.github/actions/issue-auto-implement/assess/package-lock.json b/.github/actions/issue-auto-implement/assess/package-lock.json new file mode 100644 index 00000000..f252d3af --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/package-lock.json @@ -0,0 +1,2379 @@ +{ + "name": "issue-auto-implement-assess", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "issue-auto-implement-assess", + "dependencies": { + "@anthropic-ai/sdk": "^0.32.1" + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.6" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz", + "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/.github/actions/issue-auto-implement/assess/package.json b/.github/actions/issue-auto-implement/assess/package.json new file mode 100644 index 00000000..c9e247db --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/package.json @@ -0,0 +1,17 @@ +{ + "name": "issue-auto-implement-assess", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "start": "tsx index.ts" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.32.1" + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.6" + } +} diff --git a/.github/actions/issue-auto-implement/assess/tsconfig.json b/.github/actions/issue-auto-implement/assess/tsconfig.json new file mode 100644 index 00000000..4eb7a2af --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/.github/actions/issue-auto-implement/assess/vitest.config.ts b/.github/actions/issue-auto-implement/assess/vitest.config.ts new file mode 100644 index 00000000..056e0aa1 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['**/*.test.ts'], + globals: false, + }, +}); diff --git a/.github/workflows/issue-auto-implement-test.yml b/.github/workflows/issue-auto-implement-test.yml new file mode 100644 index 00000000..e9ade7cb --- /dev/null +++ b/.github/workflows/issue-auto-implement-test.yml @@ -0,0 +1,28 @@ +# Run issue-auto-implement assess script tests (unit tests, no secrets). +name: Issue auto-implement (assess tests) + +on: + pull_request: + branches: [main] + paths: + - '.github/actions/issue-auto-implement/**' + push: + branches: [main] + paths: + - '.github/actions/issue-auto-implement/**' + +jobs: + assess: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install and test assess script + run: | + cd .github/actions/issue-auto-implement/assess + npm ci + npm test diff --git a/.github/workflows/issue-auto-implement.yml b/.github/workflows/issue-auto-implement.yml new file mode 100644 index 00000000..cf6f974c --- /dev/null +++ b/.github/workflows/issue-auto-implement.yml @@ -0,0 +1,40 @@ +# Label-triggered issue automation: assess, implement-verify loop, create PR or iterate on PR. +# Triggers: issue labeled (automation/auto-implement), issue comment, PR review. +# Require repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM (org/team slug). Token may need read:org (PAT) for team check. +name: Issue auto-implement + +on: + issues: + types: [labeled] + issue_comment: + types: [created] + pull_request_review: + types: [submitted] + pull_request_review_comment: + types: [created] + +# Only run for the trigger label on issues (action will further filter issue_comment and PR events) +jobs: + run: + runs-on: ubuntu-latest + if: | + (github.event_name == 'issues' && github.event.label.name == 'automation/auto-implement') || + github.event_name == 'issue_comment' || + github.event_name == 'pull_request_review' || + github.event_name == 'pull_request_review_comment' + permissions: + contents: write + issues: write + pull-requests: write + # Team check requires read:org; use a PAT as github_token if GITHUB_TOKEN lacks it + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Issue auto-implement + uses: ./.github/actions/issue-auto-implement + with: + anthropic_api_key: ${{ secrets.AUTO_IMPLEMENT_ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_allowed_trigger_team: ${{ vars.AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM }} From be2fe945ba78e884a90796a01466de0a3a85d76c Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 12:23:01 +0000 Subject: [PATCH 02/17] feat(ci): pass context_files into assess and include in Claude prompt - Add CONTEXT_FILES and GITHUB_WORKSPACE to assess step env - Load repo files (AGENTS.md, REFERENCE.md, etc.) and append to assessment prompt - Skip missing files; REPO_ROOT from GITHUB_WORKSPACE or cwd for local runs Made-with: Cursor --- .../actions/issue-auto-implement/action.yml | 2 + .../issue-auto-implement/assess/index.ts | 39 +++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml index da054858..a3055413 100644 --- a/.github/actions/issue-auto-implement/action.yml +++ b/.github/actions/issue-auto-implement/action.yml @@ -77,7 +77,9 @@ runs: ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} GITHUB_TOKEN: ${{ inputs.github_token }} GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_WORKSPACE: ${{ github.workspace }} ASSESSMENT_REFERENCE_ISSUE: ${{ inputs.assessment_reference_issue }} + CONTEXT_FILES: ${{ inputs.context_files }} run: | RESULT=$(cd .github/actions/issue-auto-implement/assess && npx tsx index.ts) echo "action=$(echo "$RESULT" | jq -r '.action')" >> $GITHUB_OUTPUT diff --git a/.github/actions/issue-auto-implement/assess/index.ts b/.github/actions/issue-auto-implement/assess/index.ts index 07cba48d..5c00695c 100644 --- a/.github/actions/issue-auto-implement/assess/index.ts +++ b/.github/actions/issue-auto-implement/assess/index.ts @@ -15,6 +15,8 @@ const EVENT_NAME = process.env.GITHUB_EVENT_NAME || ''; const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ''; const REPO = process.env.GITHUB_REPOSITORY || ''; +const CONTEXT_FILES = process.env.CONTEXT_FILES || ''; +const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); export type AssessmentOutput = { action: 'implement' | 'request_info' | 'redirect_to_pr'; @@ -57,7 +59,28 @@ function inferEventName(payload: unknown): string { return ''; } -function buildAssessmentPrompt(payload: unknown, eventName: string, referenceIssue: string): string { +function loadContextFiles(): string { + if (!CONTEXT_FILES.trim()) return ''; + const paths = CONTEXT_FILES.split(',').map((s) => s.trim()).filter(Boolean); + const chunks: string[] = []; + for (const rel of paths) { + try { + const full = resolve(REPO_ROOT, rel); + const content = readFileSync(full, 'utf-8'); + chunks.push(`--- ${rel} ---\n${content}`); + } catch { + // Skip missing files (e.g. REFERENCE.md may not exist in all repos) + } + } + return chunks.length ? ['Repository context:', '', ...chunks].join('\n') : ''; +} + +function buildAssessmentPrompt( + payload: unknown, + eventName: string, + referenceIssue: string, + contextBlock: string +): string { const p = payload as Record; const issue = p.issue as { title?: string; body?: string; number?: number } | undefined; const parts: string[] = [ @@ -73,6 +96,9 @@ function buildAssessmentPrompt(payload: unknown, eventName: string, referenceIss '', `Reference example of "enough information": GitHub issue #${referenceIssue} (use similar clarity and specificity).`, ]; + if (contextBlock) { + parts.push('', contextBlock); + } const comment = p.comment as { body?: string } | undefined; if (comment?.body) { @@ -126,7 +152,13 @@ async function main(): Promise { export async function assess( eventName: string, payload: unknown, - opts: { repo?: string; token?: string; referenceIssue?: string; anthropicClient?: Anthropic } + opts: { + repo?: string; + token?: string; + referenceIssue?: string; + anthropicClient?: Anthropic; + contextFilesContent?: string; + } ): Promise { const normalized = normalizeEvent(eventName, payload); if (!normalized) throw new Error('Could not normalize event'); @@ -140,7 +172,8 @@ export async function assess( } const referenceIssue = opts.referenceIssue ?? '192'; - const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue); + const contextBlock = opts.contextFilesContent ?? loadContextFiles(); + const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue, contextBlock); const result = await callClaude(prompt, opts.anthropicClient); result.issue_number = normalized.issueNumber; return result; From 4bff7cbcb6e457936eac392b856d6d191b285c6f Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 12:24:03 +0000 Subject: [PATCH 03/17] feat(ci): fetch all issue comments for assessment - For issues and issue_comment events, call GitHub API for issue comments - Include all comments in Claude prompt (author, date, body) - Fall back to payload comment/comments when API not used Made-with: Cursor --- .../issue-auto-implement/assess/index.ts | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/actions/issue-auto-implement/assess/index.ts b/.github/actions/issue-auto-implement/assess/index.ts index 5c00695c..c3434c1e 100644 --- a/.github/actions/issue-auto-implement/assess/index.ts +++ b/.github/actions/issue-auto-implement/assess/index.ts @@ -39,6 +39,23 @@ async function checkExistingPr(owner: string, repo: string, issueNumber: number) return pr?.html_url ? { pr_url: pr.html_url } : null; } +type IssueComment = { body?: string; user?: { login?: string }; created_at?: string }; + +async function fetchIssueComments( + owner: string, + repo: string, + issueNumber: number, + token: string +): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) return []; + const data = (await res.json()) as IssueComment[]; + return Array.isArray(data) ? data : []; +} + function loadPayload(): { eventName: string; payload: unknown } { if (!EVENT_PATH) { throw new Error('GITHUB_EVENT_PATH is not set'); @@ -79,7 +96,8 @@ function buildAssessmentPrompt( payload: unknown, eventName: string, referenceIssue: string, - contextBlock: string + contextBlock: string, + issueComments: IssueComment[] = [] ): string { const p = payload as Record; const issue = p.issue as { title?: string; body?: string; number?: number } | undefined; @@ -99,12 +117,20 @@ function buildAssessmentPrompt( if (contextBlock) { parts.push('', contextBlock); } - + if (issueComments.length > 0) { + parts.push( + '', + 'All issue comments (from API):', + issueComments + .map((c) => `[${c.user?.login ?? 'unknown'} @ ${c.created_at ?? 'N/A'}]: ${c.body ?? ''}`) + .join('\n\n') + ); + } const comment = p.comment as { body?: string } | undefined; - if (comment?.body) { - parts.push('', 'Latest comment:', comment.body); + if (comment?.body && !issueComments.some((c) => c.body === comment.body)) { + parts.push('', 'Latest event comment:', comment.body); } - if (Array.isArray(p.comments) && p.comments.length) { + if (Array.isArray(p.comments) && p.comments.length && issueComments.length === 0) { parts.push('', 'Comments:', JSON.stringify(p.comments, null, 2)); } if (eventName === 'pull_request_review' || eventName === 'pull_request_review_comment') { @@ -173,7 +199,14 @@ export async function assess( const referenceIssue = opts.referenceIssue ?? '192'; const contextBlock = opts.contextFilesContent ?? loadContextFiles(); - const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue, contextBlock); + let issueComments: IssueComment[] = []; + if ((eventName === 'issues' || eventName === 'issue_comment') && opts.repo && opts.token) { + const [owner, repo] = opts.repo.split('/'); + if (owner && repo) { + issueComments = await fetchIssueComments(owner, repo, normalized.issueNumber, opts.token); + } + } + const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue, contextBlock, issueComments); const result = await callClaude(prompt, opts.anthropicClient); result.issue_number = normalized.issueNumber; return result; From 4b27fdd480bfe515d72a8007f9e9e14d23cef111 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 12:28:45 +0000 Subject: [PATCH 04/17] fix(ci): stabilize issue-auto-implement assess tests workflow - Set GITHUB_WORKSPACE in job env so assess script has repo root in CI - Use working-directory instead of cd for install/test step - Add npm cache keyed by assess package-lock.json Made-with: Cursor --- .github/workflows/issue-auto-implement-test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-auto-implement-test.yml b/.github/workflows/issue-auto-implement-test.yml index e9ade7cb..d8fdcb8d 100644 --- a/.github/workflows/issue-auto-implement-test.yml +++ b/.github/workflows/issue-auto-implement-test.yml @@ -14,6 +14,8 @@ on: jobs: assess: runs-on: ubuntu-latest + env: + GITHUB_WORKSPACE: ${{ github.workspace }} steps: - name: Checkout uses: actions/checkout@v4 @@ -21,8 +23,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' + cache: 'npm' + cache-dependency-path: .github/actions/issue-auto-implement/assess/package-lock.json - name: Install and test assess script + working-directory: .github/actions/issue-auto-implement/assess run: | - cd .github/actions/issue-auto-implement/assess npm ci npm test From 876526d0b08e43a76679237f7862a2032e6b417c Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 12:29:59 +0000 Subject: [PATCH 05/17] fix(ci): do not run main() when assess script is loaded by Vitest In CI the runner sets GITHUB_EVENT_PATH, so the script was invoking main() on import; loadPayload() then threw and process.exit(1) caused Vitest to fail with an unhandled error. Only run main() when GITHUB_EVENT_PATH is set and VITEST is not (i.e. when invoked as script, not by tests). Made-with: Cursor --- .github/actions/issue-auto-implement/assess/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/issue-auto-implement/assess/index.ts b/.github/actions/issue-auto-implement/assess/index.ts index c3434c1e..f9dab1b4 100644 --- a/.github/actions/issue-auto-implement/assess/index.ts +++ b/.github/actions/issue-auto-implement/assess/index.ts @@ -212,7 +212,9 @@ export async function assess( return result; } -if (process.env.GITHUB_EVENT_PATH) { +// Only run main when invoked as script (not when imported by tests). In CI, the runner sets +// GITHUB_EVENT_PATH, so we must skip main() when Vitest is running to avoid unhandled process.exit. +if (process.env.GITHUB_EVENT_PATH && !process.env.VITEST) { main().catch((err) => { console.error(err); process.exit(1); From f75cf43f3c3046c1d31c4ac4d4534f238f7c7a64 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 12:47:56 +0000 Subject: [PATCH 06/17] feat(ci): wire Claude implement step and commit-and-push - Add assess/implement.ts: fetch issue, call Claude for edits JSON, apply files, write commit message - Action: run implement script with env; commit and push using generated commit message - Add .gitignore for .commit_msg; document implement script in AGENTS.md Made-with: Cursor --- .../actions/issue-auto-implement/.gitignore | 2 + .../actions/issue-auto-implement/AGENTS.md | 6 + .../actions/issue-auto-implement/action.yml | 24 +++- .../issue-auto-implement/assess/implement.ts | 126 ++++++++++++++++++ 4 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 .github/actions/issue-auto-implement/.gitignore create mode 100644 .github/actions/issue-auto-implement/assess/implement.ts diff --git a/.github/actions/issue-auto-implement/.gitignore b/.github/actions/issue-auto-implement/.gitignore new file mode 100644 index 00000000..ce1b4f0d --- /dev/null +++ b/.github/actions/issue-auto-implement/.gitignore @@ -0,0 +1,2 @@ +# Commit message file written by implement script (never commit) +.commit_msg diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md index 5a471d59..263377d7 100644 --- a/.github/actions/issue-auto-implement/AGENTS.md +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -33,6 +33,12 @@ From the workflow event payload, derive: - **Output:** JSON with `action` (`implement` | `request_info`), `comment_body` (if request_info), `verification_notes` (optional). Written to file or GITHUB_OUTPUT. - **When triggered by PR review:** Include PR review body and review comments in the payload sent to Claude. +## Implement script + +- **Path:** `assess/implement.ts`, run with `npx tsx implement.ts` from the assess directory. +- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY` (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE` (optional). +- **Flow:** Fetches issue title/body from GitHub API, loads context files, calls Claude for JSON `{ edits: [{ path, contents }], commit_message }`, applies edits under repo root, writes commit message to `IMPLEMENT_COMMIT_MSG_FILE`. Paths in edits must be relative; script validates they stay inside repo root. + ## Branch and PR - Branch name: `auto-implement-issue-`. diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml index a3055413..ab594330 100644 --- a/.github/actions/issue-auto-implement/action.yml +++ b/.github/actions/issue-auto-implement/action.yml @@ -150,20 +150,32 @@ runs: shell: bash env: ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} - VERIFICATION_NOTES: ${{ steps.assess.outputs.verification_notes }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ inputs.github_token }} + GITHUB_WORKSPACE: ${{ github.workspace }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + VERIFICATION_NOTES: ${{ steps.assess.outputs.verification_notes }} + CONTEXT_FILES: ${{ inputs.context_files }} + IMPLEMENT_COMMIT_MSG_FILE: ${{ github.workspace }}/.github/actions/issue-auto-implement/.commit_msg run: | - echo "Implement step: would run Claude Code Action or API here." - echo "Verification notes: $VERIFICATION_NOTES" - # Placeholder until Claude implement is wired; verify step will run next. - - name: Push branch + cd .github/actions/issue-auto-implement/assess && npx tsx implement.ts + - name: Commit and push if: steps.assess.outputs.action == 'implement' shell: bash env: ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} run: | + COMMIT_MSG_FILE=".github/actions/issue-auto-implement/.commit_msg" + git add -A + git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true + if git diff --staged --quiet; then + echo "No changes to commit (implement produced no edits)." + exit 0 + fi + git commit -F "$COMMIT_MSG_FILE" + rm -f "$COMMIT_MSG_FILE" BRANCH="auto-implement-issue-${ISSUE_NUMBER}" - git push -u origin "$BRANCH" || true + git push -u origin "$BRANCH" - id: verify_loop name: Verify (with retries) if: steps.assess.outputs.action == 'implement' diff --git a/.github/actions/issue-auto-implement/assess/implement.ts b/.github/actions/issue-auto-implement/assess/implement.ts new file mode 100644 index 00000000..c05dfeb5 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/implement.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env -S npx tsx +/** + * Implement script: fetch issue, call Claude for file edits, apply and write commit message. + * Env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, ANTHROPIC_API_KEY, VERIFICATION_NOTES, + * GITHUB_WORKSPACE, CONTEXT_FILES. Writes commit message to IMPLEMENT_COMMIT_MSG_FILE. + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import Anthropic from '@anthropic-ai/sdk'; + +const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); +const COMMIT_MSG_FILE = process.env.IMPLEMENT_COMMIT_MSG_FILE || resolve(REPO_ROOT, '.github/actions/issue-auto-implement/.commit_msg'); + +type Edit = { path: string; contents: string }; +type ImplementOutput = { edits: Edit[]; commit_message: string }; + +async function fetchIssue(owner: string, repo: string, issueNumber: number, token: string): Promise<{ title: string; body: string }> { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) throw new Error(`Failed to fetch issue: ${res.status}`); + const data = (await res.json()) as { title?: string; body?: string }; + return { title: data.title ?? '', body: data.body ?? '' }; +} + +function loadContextFiles(): string { + const contextFiles = process.env.CONTEXT_FILES || ''; + if (!contextFiles.trim()) return ''; + const paths = contextFiles.split(',').map((s) => s.trim()).filter(Boolean); + const chunks: string[] = []; + for (const rel of paths) { + try { + const full = resolve(REPO_ROOT, rel); + const content = readFileSync(full, 'utf-8'); + chunks.push(`--- ${rel} ---\n${content}`); + } catch { + // Skip missing files + } + } + return chunks.length ? ['Repository context:', '', ...chunks].join('\n') : ''; +} + +function buildPrompt(issueTitle: string, issueBody: string, verificationNotes: string, contextBlock: string): string { + const parts = [ + 'You are implementing a GitHub issue. Produce a single JSON object with no markdown or extra text.', + 'Keys: "edits" (array of { "path": "relative/path/from/repo/root", "contents": "full file content" }), "commit_message" (short conventional commit message).', + 'Only include files you change or create. Paths must be relative to the repo root. Output full file contents for each edited file.', + '', + 'Issue title:', + issueTitle, + '', + 'Issue body:', + issueBody, + '', + ]; + if (verificationNotes) { + parts.push('Verification notes (e.g. run tests):', verificationNotes, ''); + } + if (contextBlock) { + parts.push('', contextBlock); + } + parts.push('', 'Output only the JSON object:'); + return parts.join('\n'); +} + +function safePath(relativePath: string): string { + const normalized = resolve(REPO_ROOT, relativePath); + if (!normalized.startsWith(REPO_ROOT)) { + throw new Error(`Path escapes repo root: ${relativePath}`); + } + return normalized; +} + +function applyEdits(edits: Edit[]): void { + for (const { path: rel, contents } of edits) { + const full = safePath(rel); + const dir = dirname(full); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(full, contents, 'utf-8'); + } +} + +async function main(): Promise { + const issueNumber = process.env.ISSUE_NUMBER; + const repo = process.env.GITHUB_REPOSITORY; + const token = process.env.GITHUB_TOKEN; + const apiKey = process.env.ANTHROPIC_API_KEY; + const verificationNotes = process.env.VERIFICATION_NOTES || ''; + + if (!issueNumber || !repo || !token || !apiKey) { + throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, ANTHROPIC_API_KEY'); + } + + const [owner, repoName] = repo.split('/'); + if (!owner || !repoName) throw new Error('Invalid GITHUB_REPOSITORY'); + + const { title, body } = await fetchIssue(owner, repoName, parseInt(issueNumber, 10), token); + const contextBlock = loadContextFiles(); + const prompt = buildPrompt(title, body, verificationNotes, contextBlock); + + const client = new Anthropic({ apiKey }); + const response = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 16384, + messages: [{ role: 'user', content: prompt }], + }); + const text = response.content?.[0]?.type === 'text' ? response.content[0].text : ''; + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) throw new Error('Claude did not return valid JSON: ' + text.slice(0, 300)); + + const parsed = JSON.parse(jsonMatch[0]) as ImplementOutput; + if (!Array.isArray(parsed.edits)) throw new Error('Missing or invalid "edits" array'); + const commitMessage = typeof parsed.commit_message === 'string' && parsed.commit_message.trim() + ? parsed.commit_message.trim() + : `fix: implement issue #${issueNumber}`; + + applyEdits(parsed.edits); + writeFileSync(COMMIT_MSG_FILE, commitMessage, 'utf-8'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); From d3aa1920042b9fdb14322aa64f61bb14ade502ed Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 13:22:00 +0000 Subject: [PATCH 07/17] feat(ci): true implement-verify loop with PREVIOUS_VERIFY_OUTPUT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement script accepts PREVIOUS_VERIFY_OUTPUT and adds it to Claude prompt on retry - Single step loops: implement → commit/push → verify; on verify failure retry with output - Create PR uses implement_verify_loop.outcome; document loop in AGENTS.md Made-with: Cursor --- .../actions/issue-auto-implement/AGENTS.md | 8 ++- .../actions/issue-auto-implement/action.yml | 52 +++++++++---------- .../issue-auto-implement/assess/implement.ts | 22 +++++++- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md index 263377d7..9e5bb82c 100644 --- a/.github/actions/issue-auto-implement/AGENTS.md +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -36,8 +36,12 @@ From the workflow event payload, derive: ## Implement script - **Path:** `assess/implement.ts`, run with `npx tsx implement.ts` from the assess directory. -- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY` (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE` (optional). -- **Flow:** Fetches issue title/body from GitHub API, loads context files, calls Claude for JSON `{ edits: [{ path, contents }], commit_message }`, applies edits under repo root, writes commit message to `IMPLEMENT_COMMIT_MSG_FILE`. Paths in edits must be relative; script validates they stay inside repo root. +- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY` (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE`, `PREVIOUS_VERIFY_OUTPUT` (optional). +- **Flow:** Fetches issue title/body from GitHub API, loads context files, calls Claude for JSON `{ edits: [{ path, contents }], commit_message }`, applies edits under repo root, writes commit message to `IMPLEMENT_COMMIT_MSG_FILE`. Paths in edits must be relative; script validates they stay inside repo root. When `PREVIOUS_VERIFY_OUTPUT` is set (e.g. after a failed verify run), it is included in the prompt so Claude can fix the implementation. + +## Implement–verify loop + +- **Single step** `implement_verify_loop`: for each attempt from 1 to `max_implement_retries`, run implement (with `PREVIOUS_VERIFY_OUTPUT` from the previous failure, if any), commit and push, then run `verify_commands`. If verify passes, exit success. If it fails, set the verify output as `PREVIOUS_VERIFY_OUTPUT` and retry. After all attempts, fail. Create PR only when this step succeeds. ## Branch and PR diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml index ab594330..0cb3ce47 100644 --- a/.github/actions/issue-auto-implement/action.yml +++ b/.github/actions/issue-auto-implement/action.yml @@ -145,7 +145,8 @@ runs: else git checkout -b "$BRANCH" origin/main fi - - name: Implement + - id: implement_verify_loop + name: Implement and verify (loop) if: steps.assess.outputs.action == 'implement' shell: bash env: @@ -157,46 +158,41 @@ runs: VERIFICATION_NOTES: ${{ steps.assess.outputs.verification_notes }} CONTEXT_FILES: ${{ inputs.context_files }} IMPLEMENT_COMMIT_MSG_FILE: ${{ github.workspace }}/.github/actions/issue-auto-implement/.commit_msg - run: | - cd .github/actions/issue-auto-implement/assess && npx tsx implement.ts - - name: Commit and push - if: steps.assess.outputs.action == 'implement' - shell: bash - env: - ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} - run: | - COMMIT_MSG_FILE=".github/actions/issue-auto-implement/.commit_msg" - git add -A - git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true - if git diff --staged --quiet; then - echo "No changes to commit (implement produced no edits)." - exit 0 - fi - git commit -F "$COMMIT_MSG_FILE" - rm -f "$COMMIT_MSG_FILE" - BRANCH="auto-implement-issue-${ISSUE_NUMBER}" - git push -u origin "$BRANCH" - - id: verify_loop - name: Verify (with retries) - if: steps.assess.outputs.action == 'implement' - shell: bash - env: MAX_RETRIES: ${{ inputs.max_implement_retries }} VERIFY_CMDS: ${{ inputs.verify_commands }} run: | + set -e MAX=${MAX_RETRIES:-3} + COMMIT_MSG_FILE=".github/actions/issue-auto-implement/.commit_msg" + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + VERIFY_FAILURE="" for i in $(seq 1 "$MAX"); do - echo "Verify attempt $i of $MAX" - if eval "$VERIFY_CMDS"; then + echo "Implement–verify attempt $i of $MAX" + export PREVIOUS_VERIFY_OUTPUT="$VERIFY_FAILURE" + cd .github/actions/issue-auto-implement/assess && npx tsx implement.ts + cd "$GITHUB_WORKSPACE" + git add -A + git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true + if ! git diff --staged --quiet; then + git commit -F "$COMMIT_MSG_FILE" + rm -f "$COMMIT_MSG_FILE" + git push -u origin "$BRANCH" + else + echo "No changes to commit (attempt $i)." + fi + VERIFY_OUTPUT=$(eval "$VERIFY_CMDS" 2>&1); VERIFY_EXIT=$? + if [ "$VERIFY_EXIT" -eq 0 ]; then echo "verified=true" >> $GITHUB_OUTPUT exit 0 fi + VERIFY_FAILURE="$VERIFY_OUTPUT" + echo "::warning::Verification failed (attempt $i of $MAX)" done echo "verified=false" >> $GITHUB_OUTPUT echo "::error::Verification failed after $MAX attempts" exit 1 - name: Create PR - if: steps.assess.outputs.action == 'implement' && steps.verify_loop.outcome == 'success' + if: steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'success' shell: bash env: GITHUB_TOKEN: ${{ inputs.github_token }} diff --git a/.github/actions/issue-auto-implement/assess/implement.ts b/.github/actions/issue-auto-implement/assess/implement.ts index c05dfeb5..61d85315 100644 --- a/.github/actions/issue-auto-implement/assess/implement.ts +++ b/.github/actions/issue-auto-implement/assess/implement.ts @@ -42,7 +42,13 @@ function loadContextFiles(): string { return chunks.length ? ['Repository context:', '', ...chunks].join('\n') : ''; } -function buildPrompt(issueTitle: string, issueBody: string, verificationNotes: string, contextBlock: string): string { +function buildPrompt( + issueTitle: string, + issueBody: string, + verificationNotes: string, + contextBlock: string, + previousVerifyOutput: string +): string { const parts = [ 'You are implementing a GitHub issue. Produce a single JSON object with no markdown or extra text.', 'Keys: "edits" (array of { "path": "relative/path/from/repo/root", "contents": "full file content" }), "commit_message" (short conventional commit message).', @@ -58,6 +64,17 @@ function buildPrompt(issueTitle: string, issueBody: string, verificationNotes: s if (verificationNotes) { parts.push('Verification notes (e.g. run tests):', verificationNotes, ''); } + if (previousVerifyOutput.trim()) { + parts.push( + '', + 'The previous implementation was applied but verification failed. Fix the implementation based on the following output:', + '', + '--- Verification output ---', + previousVerifyOutput.trim(), + '--- End verification output ---', + '' + ); + } if (contextBlock) { parts.push('', contextBlock); } @@ -88,6 +105,7 @@ async function main(): Promise { const token = process.env.GITHUB_TOKEN; const apiKey = process.env.ANTHROPIC_API_KEY; const verificationNotes = process.env.VERIFICATION_NOTES || ''; + const previousVerifyOutput = process.env.PREVIOUS_VERIFY_OUTPUT || ''; if (!issueNumber || !repo || !token || !apiKey) { throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, ANTHROPIC_API_KEY'); @@ -98,7 +116,7 @@ async function main(): Promise { const { title, body } = await fetchIssue(owner, repoName, parseInt(issueNumber, 10), token); const contextBlock = loadContextFiles(); - const prompt = buildPrompt(title, body, verificationNotes, contextBlock); + const prompt = buildPrompt(title, body, verificationNotes, contextBlock, previousVerifyOutput); const client = new Anthropic({ apiKey }); const response = await client.messages.create({ From 27bb3812c49810d53ec0d514664eff2a8e5708e5 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 13:42:10 +0000 Subject: [PATCH 08/17] refactor(issue-auto-implement): move assess into src/ and test/ - Main code: assess/src/ (index.ts, implement.ts, normalize.ts) - Tests: assess/test/ (index.test.ts, normalize.test.ts) - Fixtures stay at assess/fixtures/ - Update package.json scripts and action.yml to use src/ - Add .env.example, setup-local-env.sh, .gitignore .env; update README/AGENTS paths Made-with: Cursor --- .../actions/issue-auto-implement/.env.example | 23 ++++++++ .../actions/issue-auto-implement/.gitignore | 3 + .../actions/issue-auto-implement/AGENTS.md | 12 ++-- .../actions/issue-auto-implement/README.md | 54 +++++++++++++++++- .../actions/issue-auto-implement/action.yml | 57 +++++++++++++++++-- .../assess/package-lock.json | 14 +++++ .../issue-auto-implement/assess/package.json | 5 +- .../assess/{ => src}/implement.ts | 15 +++-- .../assess/{ => src}/index.ts | 23 +++++++- .../assess/{ => src}/normalize.ts | 0 .../assess/{ => test}/index.test.ts | 4 +- .../assess/{ => test}/normalize.test.ts | 2 +- .../scripts/setup-local-env.sh | 52 +++++++++++++++++ 13 files changed, 240 insertions(+), 24 deletions(-) create mode 100644 .github/actions/issue-auto-implement/.env.example rename .github/actions/issue-auto-implement/assess/{ => src}/implement.ts (86%) rename .github/actions/issue-auto-implement/assess/{ => src}/index.ts (89%) rename .github/actions/issue-auto-implement/assess/{ => src}/normalize.ts (100%) rename .github/actions/issue-auto-implement/assess/{ => test}/index.test.ts (95%) rename .github/actions/issue-auto-implement/assess/{ => test}/normalize.test.ts (99%) create mode 100755 .github/actions/issue-auto-implement/scripts/setup-local-env.sh diff --git a/.github/actions/issue-auto-implement/.env.example b/.github/actions/issue-auto-implement/.env.example new file mode 100644 index 00000000..782ec25a --- /dev/null +++ b/.github/actions/issue-auto-implement/.env.example @@ -0,0 +1,23 @@ +# Issue auto-implement — local development +# Copy to .env and fill in secrets. Do not commit .env. + +# Required for assess + implement (Claude). Get from https://console.anthropic.com/ or use ANTHROPIC_API_KEY. +AUTO_IMPLEMENT_ANTHROPIC_API_KEY= + +# Required for implement; optional for assess (redirect-to-PR, fetch comments). Run: gh auth token +GITHUB_TOKEN= + +# Required for implement. Example: hookdeck/hookdeck-cli +GITHUB_REPOSITORY= + +# Required for implement. Issue number to implement. +# ISSUE_NUMBER= + +# Optional. Repo root; default inferred from cwd when run from assess/. +# GITHUB_WORKSPACE= + +# Optional. Comma-separated paths (relative to repo root) for Claude context. +# CONTEXT_FILES=AGENTS.md,REFERENCE.md + +# Optional. Passed from assess step into implement prompt. +# VERIFICATION_NOTES= diff --git a/.github/actions/issue-auto-implement/.gitignore b/.github/actions/issue-auto-implement/.gitignore index ce1b4f0d..5e102f5c 100644 --- a/.github/actions/issue-auto-implement/.gitignore +++ b/.github/actions/issue-auto-implement/.gitignore @@ -1,2 +1,5 @@ # Commit message file written by implement script (never commit) .commit_msg + +# Local env (secrets); do not commit +.env diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md index 9e5bb82c..7ba978a5 100644 --- a/.github/actions/issue-auto-implement/AGENTS.md +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -28,20 +28,20 @@ From the workflow event payload, derive: ## Assess script -- **Path:** `assess/index.ts` (TypeScript), run with `npx tsx assess/index.ts` (no build). +- **Path:** `assess/src/index.ts` (TypeScript), run with `npx tsx src/index.ts` from the assess directory (no build). - **Input:** Reads event from `GITHUB_EVENT_PATH`; optional context files from input. - **Output:** JSON with `action` (`implement` | `request_info`), `comment_body` (if request_info), `verification_notes` (optional). Written to file or GITHUB_OUTPUT. - **When triggered by PR review:** Include PR review body and review comments in the payload sent to Claude. ## Implement script -- **Path:** `assess/implement.ts`, run with `npx tsx implement.ts` from the assess directory. -- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY` (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE`, `PREVIOUS_VERIFY_OUTPUT` (optional). +- **Path:** `assess/src/implement.ts`, run with `npx tsx src/implement.ts` from the assess directory. +- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (or `ANTHROPIC_API_KEY`) (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE`, `PREVIOUS_VERIFY_OUTPUT` (optional). - **Flow:** Fetches issue title/body from GitHub API, loads context files, calls Claude for JSON `{ edits: [{ path, contents }], commit_message }`, applies edits under repo root, writes commit message to `IMPLEMENT_COMMIT_MSG_FILE`. Paths in edits must be relative; script validates they stay inside repo root. When `PREVIOUS_VERIFY_OUTPUT` is set (e.g. after a failed verify run), it is included in the prompt so Claude can fix the implementation. ## Implement–verify loop -- **Single step** `implement_verify_loop`: for each attempt from 1 to `max_implement_retries`, run implement (with `PREVIOUS_VERIFY_OUTPUT` from the previous failure, if any), commit and push, then run `verify_commands`. If verify passes, exit success. If it fails, set the verify output as `PREVIOUS_VERIFY_OUTPUT` and retry. After all attempts, fail. Create PR only when this step succeeds. +- **Single step** `implement_verify_loop`: for each attempt from 1 to `max_implement_retries`, run implement (with `PREVIOUS_VERIFY_OUTPUT` from the previous failure, if any), commit and push, then run `verify_commands`. If verify passes, exit success. If it fails, set the verify output as `PREVIOUS_VERIFY_OUTPUT` and retry. After all attempts, fail. When this step succeeds: if trigger was `pull_request_review` or `pull_request_review_comment`, post a comment on the existing PR (no new PR); otherwise create PR. ## Branch and PR @@ -57,6 +57,10 @@ Only members of the `github_allowed_trigger_team` (input; set via repo variable All automation labels use `label_prefix` (default `automation`): `{prefix}/auto-implement`, `{prefix}/needs-info`, `{prefix}/pr-created`. Create via API if missing. +## Local development + +Scripts load a `.env` file from the action root or cwd (see README **Local runs**). Key env: `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (or `ANTHROPIC_API_KEY`), `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, `ISSUE_NUMBER` (implement). Copy `.env.example` to `.env` and fill; optional `./scripts/setup-local-env.sh --with-gh` to set `GITHUB_TOKEN` from `gh auth token`. + ## Verification When changing this action or the assess script: diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md index dae491a4..b4a965e8 100644 --- a/.github/actions/issue-auto-implement/README.md +++ b/.github/actions/issue-auto-implement/README.md @@ -18,9 +18,16 @@ Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_ke | `verify_commands` | No | go test ./... | Commands run for verification | | `max_implement_retries` | No | 3 | Max retries on verify failure (cap 5) | | `github_allowed_trigger_team` | Yes | - | GitHub Team slug (e.g. org/team); only members can trigger. Set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. | +| `post_pr_comment` | No | false | When true, post a comment on the issue linking to the new PR when one is created. | Secrets and variables use an action-specific prefix (e.g. `AUTO_IMPLEMENT_`) so each action can have its own keys/variables and it's clear which workflow uses which. This also avoids clashing with platform-reserved names (e.g. `GITHUB_*`). +## Secrets and variables (repo setup) + +- **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret) — Claude API key for the assess and implement steps. Add under Settings → Secrets and variables → Actions. +- **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable, required) — GitHub Team slug (e.g. `org/team-name`) whose members may trigger the workflow. Add under Settings → Secrets and variables → Actions. The first step checks `github.actor` against this team; if unset or not a member, the run fails. +- **Token for team check** — The workflow passes `github_token` (usually `secrets.GITHUB_TOKEN`) to the action. The team check needs `read:org`. If your default `GITHUB_TOKEN` does not have `read:org`, use a PAT with that scope and pass it as the token (e.g. a repo secret) instead. + ## Triggers - **issues.labeled** — prefixed trigger label (e.g. `automation/auto-implement`) @@ -39,11 +46,52 @@ From the repo root, run the assess script tests: cd .github/actions/issue-auto-implement/assess && npm ci && npm test ``` -CI runs these in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches this action. To run the assess script with a real Claude call, set `ANTHROPIC_API_KEY` and use a fixture: +CI runs these in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches this action. + +## Local runs (Claude) + +Scripts load a **local `.env`** file so you don't have to pass secrets on the command line. They look for `.env` in (1) the action root (`.github/actions/issue-auto-implement/.env`) and (2) the current working directory (e.g. `assess/.env`). Later overrides earlier; shell env still wins. + +### Env vars (local) + +| Variable | Required for | How to get it | +|----------|--------------|----------------| +| `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` | Assess, Implement | [Anthropic console](https://console.anthropic.com/) → API keys. Or use `ANTHROPIC_API_KEY` (e.g. from Claude CLI). | +| `GITHUB_TOKEN` | Implement; optional for Assess | `gh auth token`, or a PAT with `repo`, `read:org`. | +| `GITHUB_REPOSITORY` | Implement | `owner/repo` (e.g. `hookdeck/hookdeck-cli`). | +| `ISSUE_NUMBER` | Implement | The issue number to implement. | +| `GITHUB_EVENT_PATH` | Assess (when not using fixture) | Path to event JSON; for fixture: `./fixtures/issue-labeled.json`. | +| `GITHUB_EVENT_NAME` | Assess | e.g. `issues` or `issue_comment`. | +| `GITHUB_WORKSPACE` | Optional | Repo root; default inferred from cwd when run from `assess/`. | +| `CONTEXT_FILES` | Optional | Comma-separated paths (relative to repo root) for Claude context. | +| `VERIFICATION_NOTES` | Optional (Implement) | Notes from assess step. | +| `PREVIOUS_VERIFY_OUTPUT` | Optional (Implement retries) | Previous verify failure output. | + +### One-time setup: `.env` + +1. From the action root: `cp .env.example .env` +2. Edit `.env` and set at least `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` and (for implement) `GITHUB_TOKEN`. Optionally run `./scripts/setup-local-env.sh --with-gh` to fill `GITHUB_TOKEN` from `gh auth token`; use `--template-only` to only create `.env` from `.env.example`. + +### Assess (issue → implement vs request_info) + +Uses a fixture as the GitHub event. Claude decides whether to `implement` or `request_info`; output is JSON to stdout. + +```bash +cd .github/actions/issue-auto-implement/assess +npm run assess:fixture +``` + +With `.env` in place, no need to pass the key on the command line. Optional: set `GITHUB_TOKEN` and `GITHUB_REPOSITORY` to exercise redirect-to-PR and fetch-comments. Set `ASSESS_DEBUG=1` to log the prompt sent to Claude and the raw response to stderr. Other fixtures: `GITHUB_EVENT_PATH=./fixtures/issue-comment.json GITHUB_EVENT_NAME=issue_comment npx tsx src/index.ts`. + +### Implement (issue → Claude edits → files on disk) + +Fetches the issue from the GitHub API, calls Claude for file edits, and **writes changes** under the repo root and a commit message file. Use a branch you can discard or reset. ```bash cd .github/actions/issue-auto-implement/assess -GITHUB_EVENT_PATH=./fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues npx tsx index.ts +npm run implement:issue ``` -For implementation details, verification steps, and the implementation backlog, see `AGENTS.md`. +With `.env` set (e.g. `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), no need to pass them inline. Override any var on the command line if needed (e.g. `ISSUE_NUMBER=42 npm run implement:issue`). Then from the repo root inspect `git status` and the commit message at `.github/actions/issue-auto-implement/.commit_msg`. Optionally set `VERIFICATION_NOTES` and `CONTEXT_FILES`. + +For implementation details and verification steps, see `AGENTS.md`. diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml index 0cb3ce47..0dcc0dd5 100644 --- a/.github/actions/issue-auto-implement/action.yml +++ b/.github/actions/issue-auto-implement/action.yml @@ -30,6 +30,10 @@ inputs: github_allowed_trigger_team: description: 'GitHub Team slug whose members may trigger the flow (e.g. hookdeck/automation-triggers). Set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM.' required: true + post_pr_comment: + description: 'When true, post a comment on the issue linking to the new PR when one is created' + required: false + default: 'false' runs: using: 'composite' steps: @@ -74,14 +78,14 @@ runs: name: Run assess shell: bash env: - ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + AUTO_IMPLEMENT_ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} GITHUB_TOKEN: ${{ inputs.github_token }} GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_WORKSPACE: ${{ github.workspace }} ASSESSMENT_REFERENCE_ISSUE: ${{ inputs.assessment_reference_issue }} CONTEXT_FILES: ${{ inputs.context_files }} run: | - RESULT=$(cd .github/actions/issue-auto-implement/assess && npx tsx index.ts) + RESULT=$(cd .github/actions/issue-auto-implement/assess && npx tsx src/index.ts) echo "action=$(echo "$RESULT" | jq -r '.action')" >> $GITHUB_OUTPUT echo "issue_number=$(echo "$RESULT" | jq -r '.issue_number // empty')" >> $GITHUB_OUTPUT echo "pr_url=$(echo "$RESULT" | jq -r '.pr_url // empty')" >> $GITHUB_OUTPUT @@ -154,7 +158,7 @@ runs: GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_TOKEN: ${{ inputs.github_token }} GITHUB_WORKSPACE: ${{ github.workspace }} - ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + AUTO_IMPLEMENT_ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} VERIFICATION_NOTES: ${{ steps.assess.outputs.verification_notes }} CONTEXT_FILES: ${{ inputs.context_files }} IMPLEMENT_COMMIT_MSG_FILE: ${{ github.workspace }}/.github/actions/issue-auto-implement/.commit_msg @@ -169,7 +173,7 @@ runs: for i in $(seq 1 "$MAX"); do echo "Implement–verify attempt $i of $MAX" export PREVIOUS_VERIFY_OUTPUT="$VERIFY_FAILURE" - cd .github/actions/issue-auto-implement/assess && npx tsx implement.ts + cd .github/actions/issue-auto-implement/assess && npx tsx src/implement.ts cd "$GITHUB_WORKSPACE" git add -A git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true @@ -191,14 +195,47 @@ runs: echo "verified=false" >> $GITHUB_OUTPUT echo "::error::Verification failed after $MAX attempts" exit 1 + - name: Comment on issue when verify retries exhausted + if: failure() && steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'failure' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + MAX_RETRIES: ${{ inputs.max_implement_retries }} + run: | + BODY="The auto-implement run could not complete: verification failed after ${MAX_RETRIES} attempts. See the [workflow run]($RUN_URL) for logs. You can address the failure and re-trigger by adding a comment or re-applying the label." + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "$(jq -n --arg b "$BODY" '{body: $b}')" + echo "Posted comment on issue #$ISSUE_NUMBER (verify exhausted)" + - name: Comment on PR (review iteration) + if: steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'success' && (github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment') + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + BODY="Addressed review feedback. New commit(s) pushed; verification passed." + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$PR_NUMBER/comments" \ + -d "$(jq -n --arg b "$BODY" '{body: $b}')" + echo "Posted comment on PR #$PR_NUMBER" - name: Create PR - if: steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'success' + if: steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'success' && github.event_name != 'pull_request_review' && github.event_name != 'pull_request_review_comment' shell: bash env: GITHUB_TOKEN: ${{ inputs.github_token }} REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} LABEL_PREFIX: ${{ inputs.label_prefix }} + POST_PR_COMMENT: ${{ inputs.post_pr_comment }} run: | BRANCH="auto-implement-issue-${ISSUE_NUMBER}" TITLE="Auto-implement issue #${ISSUE_NUMBER}" @@ -210,7 +247,6 @@ runs: -d "{\"title\":\"$TITLE\",\"body\":\"$BODY\",\"head\":\"$BRANCH\",\"base\":\"main\"}") PR_URL=$(echo "$PR_JSON" | jq -r '.html_url // empty') if [ -n "$PR_URL" ]; then - PR_NUM=$(echo "$PR_JSON" | jq -r '.number') NEEDS_LABEL="${LABEL_PREFIX}/pr-created" curl -s -X POST \ -H "Authorization: Bearer $GITHUB_TOKEN" \ @@ -218,4 +254,13 @@ runs: "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" \ -d "{\"labels\":[\"$NEEDS_LABEL\"]}" echo "Created PR: $PR_URL" + if [ "$POST_PR_COMMENT" = "true" ]; then + COMMENT_BODY="Opened PR: $PR_URL" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "$(jq -n --arg b "$COMMENT_BODY" '{body: $b}')" + echo "Posted comment on issue #$ISSUE_NUMBER" + fi fi diff --git a/.github/actions/issue-auto-implement/assess/package-lock.json b/.github/actions/issue-auto-implement/assess/package-lock.json index f252d3af..570b4039 100644 --- a/.github/actions/issue-auto-implement/assess/package-lock.json +++ b/.github/actions/issue-auto-implement/assess/package-lock.json @@ -9,6 +9,7 @@ "@anthropic-ai/sdk": "^0.32.1" }, "devDependencies": { + "dotenv": "^16.4.5", "tsx": "^4.19.2", "typescript": "^5.7.2", "vitest": "^2.1.6" @@ -1106,6 +1107,19 @@ "node": ">=0.4.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/.github/actions/issue-auto-implement/assess/package.json b/.github/actions/issue-auto-implement/assess/package.json index c9e247db..829b31fe 100644 --- a/.github/actions/issue-auto-implement/assess/package.json +++ b/.github/actions/issue-auto-implement/assess/package.json @@ -4,12 +4,15 @@ "type": "module", "scripts": { "test": "vitest run", - "start": "tsx index.ts" + "start": "tsx src/index.ts", + "assess:fixture": "GITHUB_EVENT_PATH=./fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues tsx src/index.ts", + "implement:issue": "tsx src/implement.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.32.1" }, "devDependencies": { + "dotenv": "^16.4.5", "tsx": "^4.19.2", "typescript": "^5.7.2", "vitest": "^2.1.6" diff --git a/.github/actions/issue-auto-implement/assess/implement.ts b/.github/actions/issue-auto-implement/assess/src/implement.ts similarity index 86% rename from .github/actions/issue-auto-implement/assess/implement.ts rename to .github/actions/issue-auto-implement/assess/src/implement.ts index 61d85315..1b35df71 100644 --- a/.github/actions/issue-auto-implement/assess/implement.ts +++ b/.github/actions/issue-auto-implement/assess/src/implement.ts @@ -1,14 +1,21 @@ #!/usr/bin/env -S npx tsx /** * Implement script: fetch issue, call Claude for file edits, apply and write commit message. - * Env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, ANTHROPIC_API_KEY, VERIFICATION_NOTES, - * GITHUB_WORKSPACE, CONTEXT_FILES. Writes commit message to IMPLEMENT_COMMIT_MSG_FILE. + * Env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, AUTO_IMPLEMENT_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY), + * VERIFICATION_NOTES, GITHUB_WORKSPACE (optional, default: infer from cwd), CONTEXT_FILES. + * Writes commit message to IMPLEMENT_COMMIT_MSG_FILE. */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { resolve, dirname } from 'path'; +import { config } from 'dotenv'; import Anthropic from '@anthropic-ai/sdk'; +// Load .env from action root then cwd (cwd is assess/ when run from there). No-op if files missing. +config({ path: resolve(process.cwd(), '../.env') }); +config({ path: resolve(process.cwd(), '.env') }); + +// Default repo root: infer from cwd when run from assess/ (e.g. ../../..); set GITHUB_WORKSPACE only if needed const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); const COMMIT_MSG_FILE = process.env.IMPLEMENT_COMMIT_MSG_FILE || resolve(REPO_ROOT, '.github/actions/issue-auto-implement/.commit_msg'); @@ -103,12 +110,12 @@ async function main(): Promise { const issueNumber = process.env.ISSUE_NUMBER; const repo = process.env.GITHUB_REPOSITORY; const token = process.env.GITHUB_TOKEN; - const apiKey = process.env.ANTHROPIC_API_KEY; + const apiKey = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY; const verificationNotes = process.env.VERIFICATION_NOTES || ''; const previousVerifyOutput = process.env.PREVIOUS_VERIFY_OUTPUT || ''; if (!issueNumber || !repo || !token || !apiKey) { - throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, ANTHROPIC_API_KEY'); + throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, AUTO_IMPLEMENT_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY)'); } const [owner, repoName] = repo.split('/'); diff --git a/.github/actions/issue-auto-implement/assess/index.ts b/.github/actions/issue-auto-implement/assess/src/index.ts similarity index 89% rename from .github/actions/issue-auto-implement/assess/index.ts rename to .github/actions/issue-auto-implement/assess/src/index.ts index f9dab1b4..ddb837a7 100644 --- a/.github/actions/issue-auto-implement/assess/index.ts +++ b/.github/actions/issue-auto-implement/assess/src/index.ts @@ -2,18 +2,23 @@ /** * Assess script: read GitHub event, normalize, optionally check redirect, call Claude, output JSON. * Output: { action: 'implement' | 'request_info' | 'redirect_to_pr', comment_body?, verification_notes?, pr_url? } - * Run: GITHUB_EVENT_PATH=... GITHUB_EVENT_NAME=... [ANTHROPIC_API_KEY=...] npx tsx index.ts + * Run: GITHUB_EVENT_PATH=... GITHUB_EVENT_NAME=... [AUTO_IMPLEMENT_ANTHROPIC_API_KEY=...] npx tsx src/index.ts */ import { readFileSync } from 'fs'; import { resolve } from 'path'; +import { config } from 'dotenv'; import Anthropic from '@anthropic-ai/sdk'; import { normalizeEvent } from './normalize.js'; +// Load .env from action root then cwd (cwd is assess/ when run from there). No-op if files missing. +config({ path: resolve(process.cwd(), '../.env') }); +config({ path: resolve(process.cwd(), '.env') }); + const EVENT_PATH = process.env.GITHUB_EVENT_PATH || ''; const EVENT_NAME = process.env.GITHUB_EVENT_NAME || ''; const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; -const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ''; +const ANTHROPIC_API_KEY = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || ''; const REPO = process.env.GITHUB_REPOSITORY || ''; const CONTEXT_FILES = process.env.CONTEXT_FILES || ''; const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); @@ -142,10 +147,17 @@ function buildAssessmentPrompt( return parts.join('\n'); } +const DEBUG = process.env.ASSESS_DEBUG === '1' || process.env.ASSESS_DEBUG === 'true'; + async function callClaude(prompt: string, client?: Anthropic): Promise { const api = client ?? new Anthropic({ apiKey: ANTHROPIC_API_KEY }); if (!client && !ANTHROPIC_API_KEY) { - throw new Error('ANTHROPIC_API_KEY is not set'); + throw new Error('AUTO_IMPLEMENT_ANTHROPIC_API_KEY or ANTHROPIC_API_KEY is not set'); + } + if (DEBUG) { + process.stderr.write('--- ASSESS PROMPT (sent to Claude) ---\n'); + process.stderr.write(prompt); + process.stderr.write('\n--- END PROMPT ---\n'); } const response = await api.messages.create({ model: 'claude-sonnet-4-20250514', @@ -153,6 +165,11 @@ async function callClaude(prompt: string, client?: Anthropic): Promise { it('extracts issue number from Closes #123', () => { diff --git a/.github/actions/issue-auto-implement/scripts/setup-local-env.sh b/.github/actions/issue-auto-implement/scripts/setup-local-env.sh new file mode 100755 index 00000000..50efccd5 --- /dev/null +++ b/.github/actions/issue-auto-implement/scripts/setup-local-env.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Create or update .env for local runs. Run from action root: .github/actions/issue-auto-implement/ +# +# Usage: +# ./scripts/setup-local-env.sh # Create .env from .env.example if missing; optionally fill GITHUB_TOKEN from gh +# ./scripts/setup-local-env.sh --with-gh # Same, and run 'gh auth token' to set GITHUB_TOKEN (prompts if not authenticated) +# ./scripts/setup-local-env.sh --template-only # Only create .env from .env.example, do not run gh + +set -e +ACTION_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ACTION_ROOT" +ENV_FILE="$ACTION_ROOT/.env" +EXAMPLE_FILE="$ACTION_ROOT/.env.example" + +if [[ "$1" == "--template-only" ]]; then + WITH_GH=false +else + WITH_GH=false + [[ "$1" == "--with-gh" ]] && WITH_GH=true +fi + +if [[ ! -f "$EXAMPLE_FILE" ]]; then + echo "Missing .env.example" >&2 + exit 1 +fi + +if [[ ! -f "$ENV_FILE" ]]; then + cp "$EXAMPLE_FILE" "$ENV_FILE" + echo "Created .env from .env.example" +else + echo ".env already exists; leaving it as-is" +fi + +if [[ "$WITH_GH" == true ]]; then + if command -v gh &>/dev/null; then + TOKEN=$(gh auth token 2>/dev/null) || true + if [[ -n "$TOKEN" ]]; then + if grep -q '^GITHUB_TOKEN=' "$ENV_FILE" 2>/dev/null; then + sed -i.bak "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$TOKEN|" "$ENV_FILE" && rm -f "$ENV_FILE.bak" + else + echo "GITHUB_TOKEN=$TOKEN" >> "$ENV_FILE" + fi + echo "Set GITHUB_TOKEN from gh auth token" + else + echo "gh auth token returned empty; run 'gh auth login' if needed. GITHUB_TOKEN not updated in .env" + fi + else + echo "gh not found; install GitHub CLI to fill GITHUB_TOKEN automatically" + fi +fi + +echo "Edit .env to set AUTO_IMPLEMENT_ANTHROPIC_API_KEY (and ISSUE_NUMBER, GITHUB_REPOSITORY when running implement)." From 77b46cded2ceb83ae2bbea90b3d0a1463c5d1344 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 13:52:46 +0000 Subject: [PATCH 09/17] refactor(issue-auto-implement): consolidate tests under test/ (unit, integration, fixtures) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test/unit/ — unit tests (npm test), no API - test/integration/ — Claude API tests (npm run test:integration), not in CI - test/fixtures/ — shared event JSONs for both and assess:fixture script - Remove top-level fixtures/ and test-integration/; update vitest configs and paths Made-with: Cursor --- .../actions/issue-auto-implement/README.md | 4 +- .../issue-auto-implement/assess/package.json | 3 +- .../{ => test}/fixtures/issue-comment.json | 0 .../{ => test}/fixtures/issue-labeled.json | 0 .../fixtures/pull_request_review.json | 0 .../integration/assess.integration.test.ts | 77 +++++++++++++++++++ .../assess/test/{ => unit}/index.test.ts | 2 +- .../assess/test/{ => unit}/normalize.test.ts | 2 +- .../assess/vitest.config.ts | 2 +- .../assess/vitest.integration.config.ts | 11 +++ 10 files changed, 96 insertions(+), 5 deletions(-) rename .github/actions/issue-auto-implement/assess/{ => test}/fixtures/issue-comment.json (100%) rename .github/actions/issue-auto-implement/assess/{ => test}/fixtures/issue-labeled.json (100%) rename .github/actions/issue-auto-implement/assess/{ => test}/fixtures/pull_request_review.json (100%) create mode 100644 .github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts rename .github/actions/issue-auto-implement/assess/test/{ => unit}/index.test.ts (97%) rename .github/actions/issue-auto-implement/assess/test/{ => unit}/normalize.test.ts (99%) create mode 100644 .github/actions/issue-auto-implement/assess/vitest.integration.config.ts diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md index b4a965e8..e2b06bf5 100644 --- a/.github/actions/issue-auto-implement/README.md +++ b/.github/actions/issue-auto-implement/README.md @@ -48,6 +48,8 @@ cd .github/actions/issue-auto-implement/assess && npm ci && npm test CI runs these in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches this action. +**Integration tests (Claude API):** Tests in `assess/test/integration/` call the real Anthropic API. They do not run with `npm test`. From the assess directory, run `npm run test:integration` (requires `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` or `ANTHROPIC_API_KEY` in `.env` or env). Unit tests live in `assess/test/unit/`; shared fixtures in `assess/test/fixtures/`. You can add integration tests to CI later with the secret configured. + ## Local runs (Claude) Scripts load a **local `.env`** file so you don't have to pass secrets on the command line. They look for `.env` in (1) the action root (`.github/actions/issue-auto-implement/.env`) and (2) the current working directory (e.g. `assess/.env`). Later overrides earlier; shell env still wins. @@ -81,7 +83,7 @@ cd .github/actions/issue-auto-implement/assess npm run assess:fixture ``` -With `.env` in place, no need to pass the key on the command line. Optional: set `GITHUB_TOKEN` and `GITHUB_REPOSITORY` to exercise redirect-to-PR and fetch-comments. Set `ASSESS_DEBUG=1` to log the prompt sent to Claude and the raw response to stderr. Other fixtures: `GITHUB_EVENT_PATH=./fixtures/issue-comment.json GITHUB_EVENT_NAME=issue_comment npx tsx src/index.ts`. +With `.env` in place, no need to pass the key on the command line. Optional: set `GITHUB_TOKEN` and `GITHUB_REPOSITORY` to exercise redirect-to-PR and fetch-comments. Set `ASSESS_DEBUG=1` to log the prompt sent to Claude and the raw response to stderr. Other fixtures: `GITHUB_EVENT_PATH=./test/fixtures/issue-comment.json GITHUB_EVENT_NAME=issue_comment npx tsx src/index.ts`. ### Implement (issue → Claude edits → files on disk) diff --git a/.github/actions/issue-auto-implement/assess/package.json b/.github/actions/issue-auto-implement/assess/package.json index 829b31fe..4944a813 100644 --- a/.github/actions/issue-auto-implement/assess/package.json +++ b/.github/actions/issue-auto-implement/assess/package.json @@ -4,8 +4,9 @@ "type": "module", "scripts": { "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", "start": "tsx src/index.ts", - "assess:fixture": "GITHUB_EVENT_PATH=./fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues tsx src/index.ts", + "assess:fixture": "GITHUB_EVENT_PATH=./test/fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues tsx src/index.ts", "implement:issue": "tsx src/implement.ts" }, "dependencies": { diff --git a/.github/actions/issue-auto-implement/assess/fixtures/issue-comment.json b/.github/actions/issue-auto-implement/assess/test/fixtures/issue-comment.json similarity index 100% rename from .github/actions/issue-auto-implement/assess/fixtures/issue-comment.json rename to .github/actions/issue-auto-implement/assess/test/fixtures/issue-comment.json diff --git a/.github/actions/issue-auto-implement/assess/fixtures/issue-labeled.json b/.github/actions/issue-auto-implement/assess/test/fixtures/issue-labeled.json similarity index 100% rename from .github/actions/issue-auto-implement/assess/fixtures/issue-labeled.json rename to .github/actions/issue-auto-implement/assess/test/fixtures/issue-labeled.json diff --git a/.github/actions/issue-auto-implement/assess/fixtures/pull_request_review.json b/.github/actions/issue-auto-implement/assess/test/fixtures/pull_request_review.json similarity index 100% rename from .github/actions/issue-auto-implement/assess/fixtures/pull_request_review.json rename to .github/actions/issue-auto-implement/assess/test/fixtures/pull_request_review.json diff --git a/.github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts b/.github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts new file mode 100644 index 00000000..da8e6504 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts @@ -0,0 +1,77 @@ +/** + * Integration tests: call real Claude API. Require AUTO_IMPLEMENT_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY). + * Run with: npm run test:integration + * Not run with: npm test (unit tests only) + */ +import { config } from 'dotenv'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { readFileSync } from 'fs'; +import { describe, it, expect } from 'vitest'; +import { assess } from '../../src/index.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Load .env from action root then cwd so integration tests see the same env as local runs +config({ path: resolve(process.cwd(), '../.env') }); +config({ path: resolve(process.cwd(), '.env') }); + +const hasApiKey = !!( + process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY +); + +const FIXTURES_DIR = resolve(__dirname, '../fixtures'); + +function loadFixture(name: string): unknown { + const raw = readFileSync(resolve(FIXTURES_DIR, name), 'utf-8'); + return JSON.parse(raw); +} + +describe.skipIf(!hasApiKey)('assess (integration with Claude)', () => { + it('returns valid assessment shape for issue-labeled fixture (real API)', async () => { + const payload = loadFixture('issue-labeled.json'); + const result = await assess('issues', payload, { + referenceIssue: '192', + // No anthropicClient: use real API + }); + + expect(result).toBeDefined(); + expect(['implement', 'request_info']).toContain(result.action); + expect(typeof result.issue_number).toBe('number'); + expect(result.issue_number).toBe(192); + + if (result.action === 'request_info') { + expect(typeof result.comment_body).toBe('string'); + expect(result.comment_body.length).toBeGreaterThan(0); + } + if (result.action === 'implement' && result.verification_notes !== undefined) { + expect(typeof result.verification_notes).toBe('string'); + } + }, 45_000); + + it('returns valid assessment shape for issue-comment fixture (real API)', async () => { + const payload = loadFixture('issue-comment.json'); + const result = await assess('issue_comment', payload, { + referenceIssue: '192', + }); + + expect(result).toBeDefined(); + expect(['implement', 'request_info', 'redirect_to_pr']).toContain(result.action); + expect(typeof result.issue_number).toBe('number'); + + if (result.action === 'request_info') { + expect(typeof result.comment_body).toBe('string'); + } + }, 45_000); + + it('returns valid assessment shape for pull_request_review fixture (real API)', async () => { + const payload = loadFixture('pull_request_review.json'); + const result = await assess('pull_request_review', payload, { + referenceIssue: '192', + }); + + expect(result).toBeDefined(); + expect(['implement', 'request_info']).toContain(result.action); + expect(typeof result.issue_number).toBe('number'); + }, 45_000); +}); diff --git a/.github/actions/issue-auto-implement/assess/test/index.test.ts b/.github/actions/issue-auto-implement/assess/test/unit/index.test.ts similarity index 97% rename from .github/actions/issue-auto-implement/assess/test/index.test.ts rename to .github/actions/issue-auto-implement/assess/test/unit/index.test.ts index ddde11bc..f097edef 100644 --- a/.github/actions/issue-auto-implement/assess/test/index.test.ts +++ b/.github/actions/issue-auto-implement/assess/test/unit/index.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest'; import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { assess } from '../src/index.js'; +import { assess } from '../../src/index.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const FIXTURES_DIR = resolve(__dirname, '../fixtures'); diff --git a/.github/actions/issue-auto-implement/assess/test/normalize.test.ts b/.github/actions/issue-auto-implement/assess/test/unit/normalize.test.ts similarity index 99% rename from .github/actions/issue-auto-implement/assess/test/normalize.test.ts rename to .github/actions/issue-auto-implement/assess/test/unit/normalize.test.ts index 56f34676..8ae254a2 100644 --- a/.github/actions/issue-auto-implement/assess/test/normalize.test.ts +++ b/.github/actions/issue-auto-implement/assess/test/unit/normalize.test.ts @@ -4,7 +4,7 @@ import { parseIssueNumberFromBranch, issueNumberFromPrPayload, normalizeEvent, -} from '../src/normalize.js'; +} from '../../src/normalize.js'; describe('parseClosesIssueNumber', () => { it('extracts issue number from Closes #123', () => { diff --git a/.github/actions/issue-auto-implement/assess/vitest.config.ts b/.github/actions/issue-auto-implement/assess/vitest.config.ts index 056e0aa1..7a9682ec 100644 --- a/.github/actions/issue-auto-implement/assess/vitest.config.ts +++ b/.github/actions/issue-auto-implement/assess/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['**/*.test.ts'], + include: ['test/unit/**/*.test.ts'], globals: false, }, }); diff --git a/.github/actions/issue-auto-implement/assess/vitest.integration.config.ts b/.github/actions/issue-auto-implement/assess/vitest.integration.config.ts new file mode 100644 index 00000000..91e85b38 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/vitest.integration.config.ts @@ -0,0 +1,11 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/integration/**/*.test.ts'], + globals: false, + testTimeout: 60_000, + hookTimeout: 10_000, + }, +}); From 44839d7337cd16d3c0db9a0be7f2c82754d358a9 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 17:38:34 +0000 Subject: [PATCH 10/17] chore: ignore auto-implement worktrees Made-with: Cursor --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 92c473aa..c70c663d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ test-scripts/.install-test/ # Claude Code temporary worktrees .claude/worktrees/ + +# Auto-implement local worktrees (run-local-assess creates these) +.worktrees/ From 16d47d9a1e4ca087845e352cb2ef39af05540fd1 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 17:38:52 +0000 Subject: [PATCH 11/17] fix(issue-auto-implement): use Claude Code CLI only, single API key - Implement runs only Claude Code CLI (no API path); pass AUTO_IMPLEMENT_ANTHROPIC_API_KEY to CLI - Assess uses only AUTO_IMPLEMENT_ANTHROPIC_API_KEY - Action installs Claude Code CLI in CI when assess outcome is implement - README: add CI/CD checklist; docs for implement and env - AGENTS.md: implement flow and backlog updated Made-with: Cursor --- .../actions/issue-auto-implement/.env.example | 3 +- .../actions/issue-auto-implement/AGENTS.md | 26 ++-- .../actions/issue-auto-implement/README.md | 24 +++- .../actions/issue-auto-implement/action.yml | 19 ++- .../assess/src/implement.ts | 134 +++++++++++------- .../issue-auto-implement/assess/src/index.ts | 6 +- 6 files changed, 130 insertions(+), 82 deletions(-) diff --git a/.github/actions/issue-auto-implement/.env.example b/.github/actions/issue-auto-implement/.env.example index 782ec25a..076a2d06 100644 --- a/.github/actions/issue-auto-implement/.env.example +++ b/.github/actions/issue-auto-implement/.env.example @@ -1,7 +1,8 @@ # Issue auto-implement — local development # Copy to .env and fill in secrets. Do not commit .env. -# Required for assess + implement (Claude). Get from https://console.anthropic.com/ or use ANTHROPIC_API_KEY. +# Required for assess and implement. Get from https://console.anthropic.com/ +# Implement passes this to Claude Code CLI (claude on PATH). AUTO_IMPLEMENT_ANTHROPIC_API_KEY= # Required for implement; optional for assess (redirect-to-PR, fetch comments). Run: gh auth token diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md index 7ba978a5..963ae513 100644 --- a/.github/actions/issue-auto-implement/AGENTS.md +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -36,8 +36,8 @@ From the workflow event payload, derive: ## Implement script - **Path:** `assess/src/implement.ts`, run with `npx tsx src/implement.ts` from the assess directory. -- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (or `ANTHROPIC_API_KEY`) (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE`, `PREVIOUS_VERIFY_OUTPUT` (optional). -- **Flow:** Fetches issue title/body from GitHub API, loads context files, calls Claude for JSON `{ edits: [{ path, contents }], commit_message }`, applies edits under repo root, writes commit message to `IMPLEMENT_COMMIT_MSG_FILE`. Paths in edits must be relative; script validates they stay inside repo root. When `PREVIOUS_VERIFY_OUTPUT` is set (e.g. after a failed verify run), it is included in the prompt so Claude can fix the implementation. +- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE`, `PREVIOUS_VERIFY_OUTPUT` (optional). +- **Flow:** Fetches issue title/body from GitHub API, loads context files, then runs **Claude Code CLI** (`claude` on PATH) in the repo root with a prompt; the script passes `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` to the CLI as `ANTHROPIC_API_KEY`. The CLI implements in-repo (Read/Edit/Bash), then the script ensures commit/PR meta files exist (`.commit_msg`, `.pr_title`, `.pr_body` under the action dir). When `PREVIOUS_VERIFY_OUTPUT` is set (e.g. after a failed verify run), it is included in the prompt so the CLI can fix the implementation. In CI, the action installs the CLI (`npm install -g @anthropic-ai/claude-code`) before the implement step when the assess outcome is `implement`. ## Implement–verify loop @@ -59,7 +59,7 @@ All automation labels use `label_prefix` (default `automation`): `{prefix}/auto- ## Local development -Scripts load a `.env` file from the action root or cwd (see README **Local runs**). Key env: `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (or `ANTHROPIC_API_KEY`), `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, `ISSUE_NUMBER` (implement). Copy `.env.example` to `.env` and fill; optional `./scripts/setup-local-env.sh --with-gh` to set `GITHUB_TOKEN` from `gh auth token`. +Scripts load a `.env` file from the action root or cwd (see README **Local runs**). Key env: `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`, `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, `ISSUE_NUMBER` (implement). Copy `.env.example` to `.env` and fill; optional `./scripts/setup-local-env.sh --with-gh` to set `GITHUB_TOKEN` from `gh auth token`. ## Verification @@ -67,19 +67,15 @@ When changing this action or the assess script: 1. **Run the assess unit tests locally** before committing: `cd .github/actions/issue-auto-implement/assess && npm ci && npm test`. Do not rely on CI alone—catch failures locally first, then push. 2. **CI** runs the same tests in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches `.github/actions/issue-auto-implement/**`. -3. **Optional:** Run the assess script with a fixture and `ANTHROPIC_API_KEY` for end-to-end assessment behavior. +3. **Optional:** Run the assess script with a fixture and `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` for end-to-end assessment behavior. 4. **Full workflow:** Trigger manually on a test issue (trigger label or comment). Ensure repo secrets/variables are set. Inspect the Actions run and the issue/PR; re-run after changes to the implement or verify loop. ## Next steps (implementation backlog) -Recommended order: - -1. **Context files in assess** — Pass the `context_files` input into the assess script (e.g. via env) and include those file contents (AGENTS.md, REFERENCE.md, etc.) in the Claude assessment prompt so the model has full repo guidance. -2. **Fetch all issue comments** — For `issues` and `issue_comment` events, optionally call the GitHub API to list all comments on the issue and include them in the assessment payload (not only the single comment from the event). -3. **Implement step (real)** — Replace the placeholder with Claude generating and applying code changes. Options: call Anthropic API to produce a patch or file edits from the issue body + verification_notes (and repo context), then apply, commit, and push; or integrate a Claude Code Action / external tool. Branch is already checked out; implement must make commits and push. -4. **True implement–verify loop** — On verify failure, re-run the **implement** step with the verify failure output in the prompt (then verify again), up to `max_implement_retries`. Currently only the verify command is retried; the plan requires re-implementing with failure context. -5. **PR review iteration path** — When the trigger is `pull_request_review` or `pull_request_review_comment`: after verify passes, do **not** create a new PR; instead post a comment on the existing PR summarizing the changes in the new commit(s). Detect "PR already exists" (e.g. from event type or by checking for an open PR for this branch) and branch the flow: create PR vs. comment on PR. -6. **Optional comment when PR is created** — Add an input (e.g. `post_pr_comment`) and, when creating a PR, optionally post a short comment on the issue linking to the new PR. -7. **Comment when retries exhausted** — When the verify loop fails after all retries, post a comment on the issue so the run is visible and explainable from the issue thread. Add a step that runs on failure (e.g. `if: failure() && steps.assess.outputs.action == 'implement'`) and posts a comment on the issue: e.g. "The auto-implement run could not complete: verification failed after N attempts. See the [workflow run](link) for logs. You can address the failure and re-trigger by adding a comment or re-applying the label." -8. **Secrets and variables** — Document in README: add `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` as a repo secret; set `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM` (required) as a repo variable; note that the default `GITHUB_TOKEN` may need to be replaced with a PAT that has `read:org` for the team check. -9. **Local run with fixture and Claude** — Add an npm script or small wrapper (e.g. `npm run assess:fixture issue-labeled`) to run the assess script with a fixture and real `ANTHROPIC_API_KEY` for manual end-to-end assessment testing. +Possible future improvements: + +1. **Fetch all issue comments** — For `issues` and `issue_comment` events, optionally call the GitHub API to list all comments on the issue and include them in the assessment payload (not only the single comment from the event). +2. **Optional comment when PR is created** — Input `post_pr_comment` exists; when true, post a short comment on the issue linking to the new PR when one is created. +3. **Local run with fixture and Claude** — `npm run assess:fixture` exists; optional end-to-end testing with real `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`. + +Done: context files in assess, implement step (Claude Code CLI), implement–verify loop with re-implement on failure, PR review iteration (comment on PR, no new PR), comment when retries exhausted, secrets/variables and README docs, local assess script and TEST_PLAN. diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md index e2b06bf5..7358b94d 100644 --- a/.github/actions/issue-auto-implement/README.md +++ b/.github/actions/issue-auto-implement/README.md @@ -22,6 +22,18 @@ Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_ke Secrets and variables use an action-specific prefix (e.g. `AUTO_IMPLEMENT_`) so each action can have its own keys/variables and it's clear which workflow uses which. This also avoids clashing with platform-reserved names (e.g. `GITHUB_*`). +## CI/CD: what you need to run this workflow + +To use this action in GitHub Actions: + +1. **Workflow** — Call the action from a workflow (e.g. `.github/workflows/issue-auto-implement.yml`) on `issues.labeled`, `issue_comment`, `pull_request_review`, and/or `pull_request_review_comment`. The job needs `contents: write`, `issues: write`, `pull-requests: write`. +2. **Secrets** — Add **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret). Used for the assess step and passed to the Claude Code CLI in the implement step. +3. **Variables** — Add **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable, required): GitHub Team slug (e.g. `org/team-name`) whose members may trigger the run. +4. **Token** — Pass `github_token` (e.g. `secrets.GITHUB_TOKEN`) to the action. The team check needs `read:org`; if `GITHUB_TOKEN` lacks it, use a PAT with `read:org` and pass it as the token. +5. **Implement in CI** — The action installs the Claude Code CLI (`@anthropic-ai/claude-code`) when the assess outcome is `implement`, so the workflow does not need to install it. Implement runs in the repo with Read/Edit/Bash; the CLI uses `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`. + +No other setup is required. Optionally set `verify_commands` (default `go test ./...`) and `context_files` (default `AGENTS.md,REFERENCE.md`) to match your repo. + ## Secrets and variables (repo setup) - **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret) — Claude API key for the assess and implement steps. Add under Settings → Secrets and variables → Actions. @@ -48,7 +60,7 @@ cd .github/actions/issue-auto-implement/assess && npm ci && npm test CI runs these in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches this action. -**Integration tests (Claude API):** Tests in `assess/test/integration/` call the real Anthropic API. They do not run with `npm test`. From the assess directory, run `npm run test:integration` (requires `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` or `ANTHROPIC_API_KEY` in `.env` or env). Unit tests live in `assess/test/unit/`; shared fixtures in `assess/test/fixtures/`. You can add integration tests to CI later with the secret configured. +**Integration tests (Claude API):** Tests in `assess/test/integration/` call the real Anthropic API. They do not run with `npm test`. From the assess directory, run `npm run test:integration` (requires `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` in `.env` or env). Unit tests live in `assess/test/unit/`; shared fixtures in `assess/test/fixtures/`. You can add integration tests to CI later with the secret configured. ## Local runs (Claude) @@ -58,7 +70,7 @@ Scripts load a **local `.env`** file so you don't have to pass secrets on the co | Variable | Required for | How to get it | |----------|--------------|----------------| -| `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` | Assess, Implement | [Anthropic console](https://console.anthropic.com/) → API keys. Or use `ANTHROPIC_API_KEY` (e.g. from Claude CLI). | +| `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` | Assess, Implement | [Anthropic console](https://console.anthropic.com/) → API keys. Assess uses it directly; implement passes it to Claude Code CLI (`claude` on PATH). | | `GITHUB_TOKEN` | Implement; optional for Assess | `gh auth token`, or a PAT with `repo`, `read:org`. | | `GITHUB_REPOSITORY` | Implement | `owner/repo` (e.g. `hookdeck/hookdeck-cli`). | | `ISSUE_NUMBER` | Implement | The issue number to implement. | @@ -85,9 +97,9 @@ npm run assess:fixture With `.env` in place, no need to pass the key on the command line. Optional: set `GITHUB_TOKEN` and `GITHUB_REPOSITORY` to exercise redirect-to-PR and fetch-comments. Set `ASSESS_DEBUG=1` to log the prompt sent to Claude and the raw response to stderr. Other fixtures: `GITHUB_EVENT_PATH=./test/fixtures/issue-comment.json GITHUB_EVENT_NAME=issue_comment npx tsx src/index.ts`. -### Implement (issue → Claude edits → files on disk) +### Implement (issue → Claude Code CLI → files on disk) -Fetches the issue from the GitHub API, calls Claude for file edits, and **writes changes** under the repo root and a commit message file. Use a branch you can discard or reset. +Fetches the issue from the GitHub API, then runs **Claude Code CLI** in the repo (`claude` on PATH with Read/Edit/Bash). The CLI implements the issue in-repo and writes commit/PR meta files. Use a branch you can discard or reset. Requires Claude Code CLI installed and `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` set (passed to the CLI). ```bash cd .github/actions/issue-auto-implement/assess @@ -97,3 +109,7 @@ npm run implement:issue With `.env` set (e.g. `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), no need to pass them inline. Override any var on the command line if needed (e.g. `ISSUE_NUMBER=42 npm run implement:issue`). Then from the repo root inspect `git status` and the commit message at `.github/actions/issue-auto-implement/.commit_msg`. Optionally set `VERIFICATION_NOTES` and `CONTEXT_FILES`. For implementation details and verification steps, see `AGENTS.md`. + +### Local run against a real issue (no workflow events) + +To test the full flow locally against a real GitHub issue and create a PR, use the **local assess** script (fetches the issue from the API and runs the same assess logic). With **APPLY=1** the script applies the outcome: posts the request-for-more-info or redirect comment on the issue, or runs implement and push (creates/updates the PR). When the outcome is implement, the script creates or reuses a **worktree** at `.worktrees/auto-implement-issue-` so your current branch is left untouched; implement runs there, then commit/push/PR is done in TypeScript (`assess/src/push-and-open-pr.ts`). The workflow does not need to run; you trigger each step locally and optionally pass **COMMENT_BODY** (after you add a comment on the issue) or **REVIEW_BODY** (after you review the PR). See **[TEST_PLAN.md](TEST_PLAN.md)** for the **Live flow walkthrough** and full test plan. diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml index 0dcc0dd5..c1ac7471 100644 --- a/.github/actions/issue-auto-implement/action.yml +++ b/.github/actions/issue-auto-implement/action.yml @@ -2,7 +2,7 @@ name: 'Issue auto-implement' description: 'Assess labeled issues (or PR reviews), request more info or implement with verify loop; create PR or iterate on existing PR.' inputs: anthropic_api_key: - description: 'API key for Claude (assessment and implement step)' + description: 'API key for Claude (assessment uses it; implement passes it to Claude Code CLI as AUTO_IMPLEMENT_ANTHROPIC_API_KEY)' required: true github_token: description: 'GitHub token (contents, issues, pull-requests, read:org)' @@ -20,7 +20,7 @@ inputs: required: false default: 'automation' verify_commands: - description: 'Shell commands to run for verification (e.g. go test ./..., go build)' + description: 'Shell commands to run for verification. Should run tests and ensure the application builds (e.g. go test ./... && go build ./..., or npm test && npm run build). Repo-specific.' required: false default: 'go test ./...' max_implement_retries: @@ -149,6 +149,10 @@ runs: else git checkout -b "$BRANCH" origin/main fi + - name: Install Claude Code CLI + if: steps.assess.outputs.action == 'implement' + shell: bash + run: npm install -g @anthropic-ai/claude-code - id: implement_verify_loop name: Implement and verify (loop) if: steps.assess.outputs.action == 'implement' @@ -238,13 +242,18 @@ runs: POST_PR_COMMENT: ${{ inputs.post_pr_comment }} run: | BRANCH="auto-implement-issue-${ISSUE_NUMBER}" - TITLE="Auto-implement issue #${ISSUE_NUMBER}" - BODY="Closes #${ISSUE_NUMBER}" + PR_DIR=".github/actions/issue-auto-implement" + if [ -f "$PR_DIR/.pr_title" ]; then TITLE=$(cat "$PR_DIR/.pr_title"); else TITLE="Implement issue #${ISSUE_NUMBER}"; fi + if [ -f "$PR_DIR/.pr_body" ]; then + PAYLOAD=$(jq -n --arg t "$TITLE" --rawfile b "$PR_DIR/.pr_body" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}') + else + PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "Closes #${ISSUE_NUMBER}" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}') + fi PR_JSON=$(curl -s -X POST \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/$REPO/pulls" \ - -d "{\"title\":\"$TITLE\",\"body\":\"$BODY\",\"head\":\"$BRANCH\",\"base\":\"main\"}") + -d "$PAYLOAD") PR_URL=$(echo "$PR_JSON" | jq -r '.html_url // empty') if [ -n "$PR_URL" ]; then NEEDS_LABEL="${LABEL_PREFIX}/pr-created" diff --git a/.github/actions/issue-auto-implement/assess/src/implement.ts b/.github/actions/issue-auto-implement/assess/src/implement.ts index 1b35df71..70b73e0b 100644 --- a/.github/actions/issue-auto-implement/assess/src/implement.ts +++ b/.github/actions/issue-auto-implement/assess/src/implement.ts @@ -1,26 +1,27 @@ #!/usr/bin/env -S npx tsx /** - * Implement script: fetch issue, call Claude for file edits, apply and write commit message. - * Env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, AUTO_IMPLEMENT_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY), - * VERIFICATION_NOTES, GITHUB_WORKSPACE (optional, default: infer from cwd), CONTEXT_FILES. - * Writes commit message to IMPLEMENT_COMMIT_MSG_FILE. + * Implement script: fetch issue, then run Claude Code CLI in the repo to implement it. + * + * Env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN; VERIFICATION_NOTES, GITHUB_WORKSPACE (optional), CONTEXT_FILES. + * Claude Code CLI must be on PATH. AUTO_IMPLEMENT_ANTHROPIC_API_KEY is passed to the CLI as ANTHROPIC_API_KEY. + * Writes commit message and PR meta to ACTION_DIR for push-and-open-pr. */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { resolve, dirname } from 'path'; +import { spawnSync } from 'child_process'; import { config } from 'dotenv'; -import Anthropic from '@anthropic-ai/sdk'; // Load .env from action root then cwd (cwd is assess/ when run from there). No-op if files missing. config({ path: resolve(process.cwd(), '../.env') }); config({ path: resolve(process.cwd(), '.env') }); -// Default repo root: infer from cwd when run from assess/ (e.g. ../../..); set GITHUB_WORKSPACE only if needed -const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); -const COMMIT_MSG_FILE = process.env.IMPLEMENT_COMMIT_MSG_FILE || resolve(REPO_ROOT, '.github/actions/issue-auto-implement/.commit_msg'); - -type Edit = { path: string; contents: string }; -type ImplementOutput = { edits: Edit[]; commit_message: string }; +// Default repo root: in CI GITHUB_WORKSPACE is set; when run from assess/ locally, cwd is assess/ so repo root is 4 levels up +const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../../..'); +const ACTION_DIR = '.github/actions/issue-auto-implement'; +const COMMIT_MSG_FILE = process.env.IMPLEMENT_COMMIT_MSG_FILE || resolve(REPO_ROOT, ACTION_DIR + '/.commit_msg'); +const PR_TITLE_FILE = resolve(REPO_ROOT, ACTION_DIR + '/.pr_title'); +const PR_BODY_FILE = resolve(REPO_ROOT, ACTION_DIR + '/.pr_body'); async function fetchIssue(owner: string, repo: string, issueNumber: number, token: string): Promise<{ title: string; body: string }> { const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`; @@ -49,17 +50,28 @@ function loadContextFiles(): string { return chunks.length ? ['Repository context:', '', ...chunks].join('\n') : ''; } -function buildPrompt( +/** + * Prompt for Claude Code CLI: implement in-repo with Read/Edit/Bash; write meta files when done. + */ +function buildClaudeCliPrompt( issueTitle: string, issueBody: string, verificationNotes: string, contextBlock: string, - previousVerifyOutput: string + previousVerifyOutput: string, + issueNumber: number ): string { + const metaDir = ACTION_DIR; const parts = [ - 'You are implementing a GitHub issue. Produce a single JSON object with no markdown or extra text.', - 'Keys: "edits" (array of { "path": "relative/path/from/repo/root", "contents": "full file content" }), "commit_message" (short conventional commit message).', - 'Only include files you change or create. Paths must be relative to the repo root. Output full file contents for each edited file.', + 'Implement this GitHub issue in the current repository. You have full access to read and edit files and run commands.', + '', + 'Rules:', + '- Only change what is necessary to implement the issue. Preserve existing exported symbols and call sites unless the issue explicitly asks to remove or replace them.', + '- Consider the broader codebase—other code may depend on the files you edit; make minimal, targeted edits and keep the public API intact.', + '- When you are done, you MUST write three files (create the directory if needed):', + ` 1. ${metaDir}/.commit_msg — one line, conventional commit message (e.g. "fix: correct version comparison for beta").`, + ` 2. ${metaDir}/.pr_title — one-line PR title.`, + ` 3. ${metaDir}/.pr_body — markdown body: brief problem summary, then "How it was solved" or "Solution". Do NOT include "Closes #N" (it will be appended).`, '', 'Issue title:', issueTitle, @@ -69,40 +81,70 @@ function buildPrompt( '', ]; if (verificationNotes) { - parts.push('Verification notes (e.g. run tests):', verificationNotes, ''); + parts.push('Verification (run these to confirm):', verificationNotes, ''); } if (previousVerifyOutput.trim()) { parts.push( '', - 'The previous implementation was applied but verification failed. Fix the implementation based on the following output:', + 'The previous implementation was applied but verification failed. Fix based on:', '', '--- Verification output ---', previousVerifyOutput.trim(), - '--- End verification output ---', + '--- End ---', '' ); } if (contextBlock) { parts.push('', contextBlock); } - parts.push('', 'Output only the JSON object:'); + parts.push('', `After implementing, write ${metaDir}/.commit_msg, .pr_title, and .pr_body as above.`); return parts.join('\n'); } -function safePath(relativePath: string): string { - const normalized = resolve(REPO_ROOT, relativePath); - if (!normalized.startsWith(REPO_ROOT)) { - throw new Error(`Path escapes repo root: ${relativePath}`); +/** + * Run Claude Code CLI in REPO_ROOT with prompt on stdin. Throws if CLI is not found or exits non-zero. + * Uses AUTO_IMPLEMENT_ANTHROPIC_API_KEY (passed to the CLI as ANTHROPIC_API_KEY). + */ +function runClaudeCli(prompt: string): void { + const apiKey = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error('AUTO_IMPLEMENT_ANTHROPIC_API_KEY must be set for the Claude Code CLI.'); + } + const env = { ...process.env, ANTHROPIC_API_KEY: apiKey }; + + const result = spawnSync( + 'claude', + ['-p', '--allowedTools', 'Read,Edit,Bash'], + { + cwd: REPO_ROOT, + input: prompt, + stdio: ['pipe', 'inherit', 'inherit'], + encoding: 'utf-8', + timeout: 25 * 60 * 1000, // 25 minutes + env, + } + ); + if (result.error && (result.error as NodeJS.ErrnoException).code === 'ENOENT') { + throw new Error('Claude Code CLI not found (claude not on PATH). Install it and set AUTO_IMPLEMENT_ANTHROPIC_API_KEY.'); + } + if (result.error) throw result.error; + if (result.status !== 0) { + process.exit(result.status ?? 1); } - return normalized; } -function applyEdits(edits: Edit[]): void { - for (const { path: rel, contents } of edits) { - const full = safePath(rel); - const dir = dirname(full); - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); - writeFileSync(full, contents, 'utf-8'); +/** Ensure commit message and PR meta files exist; write defaults if missing. */ +function ensureMetaFiles(issueNumber: number): void { + const metaDir = dirname(COMMIT_MSG_FILE); + if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true }); + if (!existsSync(COMMIT_MSG_FILE)) { + writeFileSync(COMMIT_MSG_FILE, `fix: implement issue #${issueNumber}`, 'utf-8'); + } + if (!existsSync(PR_TITLE_FILE)) { + writeFileSync(PR_TITLE_FILE, `Implement issue #${issueNumber}`, 'utf-8'); + } + if (!existsSync(PR_BODY_FILE)) { + writeFileSync(PR_BODY_FILE, `Closes #${issueNumber}`, 'utf-8'); } } @@ -110,39 +152,23 @@ async function main(): Promise { const issueNumber = process.env.ISSUE_NUMBER; const repo = process.env.GITHUB_REPOSITORY; const token = process.env.GITHUB_TOKEN; - const apiKey = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY; const verificationNotes = process.env.VERIFICATION_NOTES || ''; const previousVerifyOutput = process.env.PREVIOUS_VERIFY_OUTPUT || ''; - if (!issueNumber || !repo || !token || !apiKey) { - throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN, AUTO_IMPLEMENT_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY)'); + if (!issueNumber || !repo || !token) { + throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN'); } const [owner, repoName] = repo.split('/'); if (!owner || !repoName) throw new Error('Invalid GITHUB_REPOSITORY'); - const { title, body } = await fetchIssue(owner, repoName, parseInt(issueNumber, 10), token); + const issueNum = parseInt(issueNumber, 10); + const { title, body } = await fetchIssue(owner, repoName, issueNum, token); const contextBlock = loadContextFiles(); - const prompt = buildPrompt(title, body, verificationNotes, contextBlock, previousVerifyOutput); - - const client = new Anthropic({ apiKey }); - const response = await client.messages.create({ - model: 'claude-sonnet-4-20250514', - max_tokens: 16384, - messages: [{ role: 'user', content: prompt }], - }); - const text = response.content?.[0]?.type === 'text' ? response.content[0].text : ''; - const jsonMatch = text.match(/\{[\s\S]*\}/); - if (!jsonMatch) throw new Error('Claude did not return valid JSON: ' + text.slice(0, 300)); - - const parsed = JSON.parse(jsonMatch[0]) as ImplementOutput; - if (!Array.isArray(parsed.edits)) throw new Error('Missing or invalid "edits" array'); - const commitMessage = typeof parsed.commit_message === 'string' && parsed.commit_message.trim() - ? parsed.commit_message.trim() - : `fix: implement issue #${issueNumber}`; - applyEdits(parsed.edits); - writeFileSync(COMMIT_MSG_FILE, commitMessage, 'utf-8'); + const prompt = buildClaudeCliPrompt(title, body, verificationNotes, contextBlock, previousVerifyOutput, issueNum); + runClaudeCli(prompt); + ensureMetaFiles(issueNum); } main().catch((err) => { diff --git a/.github/actions/issue-auto-implement/assess/src/index.ts b/.github/actions/issue-auto-implement/assess/src/index.ts index ddb837a7..d6601ed7 100644 --- a/.github/actions/issue-auto-implement/assess/src/index.ts +++ b/.github/actions/issue-auto-implement/assess/src/index.ts @@ -18,7 +18,7 @@ config({ path: resolve(process.cwd(), '.env') }); const EVENT_PATH = process.env.GITHUB_EVENT_PATH || ''; const EVENT_NAME = process.env.GITHUB_EVENT_NAME || ''; const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; -const ANTHROPIC_API_KEY = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || ''; +const ANTHROPIC_API_KEY = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || ''; const REPO = process.env.GITHUB_REPOSITORY || ''; const CONTEXT_FILES = process.env.CONTEXT_FILES || ''; const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); @@ -111,7 +111,7 @@ function buildAssessmentPrompt( 'Reply with a single JSON object only, no markdown or extra text, with these keys:', '- action: either "implement" or "request_info"', '- comment_body: (required if action is request_info) a short message to post on the issue asking for the missing information', - '- verification_notes: (optional if action is implement) free-form notes for the implementer, e.g. "run go test ./pkg/... and ensure build passes"', + '- verification_notes: (optional if action is implement) free-form notes for the implementer. Include running the test suite and ensuring the application builds; infer the repo\'s usual test and build commands from the repository context (e.g. go test ./... && go build ., npm test && npm run build, make test, etc.).', '', 'Issue:', `Title: ${issue?.title ?? 'N/A'}`, @@ -152,7 +152,7 @@ const DEBUG = process.env.ASSESS_DEBUG === '1' || process.env.ASSESS_DEBUG === ' async function callClaude(prompt: string, client?: Anthropic): Promise { const api = client ?? new Anthropic({ apiKey: ANTHROPIC_API_KEY }); if (!client && !ANTHROPIC_API_KEY) { - throw new Error('AUTO_IMPLEMENT_ANTHROPIC_API_KEY or ANTHROPIC_API_KEY is not set'); + throw new Error('AUTO_IMPLEMENT_ANTHROPIC_API_KEY is not set'); } if (DEBUG) { process.stderr.write('--- ASSESS PROMPT (sent to Claude) ---\n'); From caca558ac5f4bded7b9b1352d54031cbacaab6f4 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 17:39:05 +0000 Subject: [PATCH 12/17] fix(issue-auto-implement): create new PR when previous closed; TS push-and-open-pr - push-and-open-pr.ts: check gh pr view --json state; only skip create when OPEN - run-local-assess.ts: worktree flow, calls pushAndOpenPr, default context - load-dotenv.ts: used by run-local-assess - scripts/push-and-open-pr.sh: optional standalone script Made-with: Cursor --- .../assess/src/load-dotenv.ts | 12 + .../assess/src/push-and-open-pr.ts | 102 ++++++++ .../assess/src/run-local-assess.ts | 224 ++++++++++++++++++ .../scripts/push-and-open-pr.sh | 54 +++++ 4 files changed, 392 insertions(+) create mode 100644 .github/actions/issue-auto-implement/assess/src/load-dotenv.ts create mode 100644 .github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts create mode 100644 .github/actions/issue-auto-implement/assess/src/run-local-assess.ts create mode 100755 .github/actions/issue-auto-implement/scripts/push-and-open-pr.sh diff --git a/.github/actions/issue-auto-implement/assess/src/load-dotenv.ts b/.github/actions/issue-auto-implement/assess/src/load-dotenv.ts new file mode 100644 index 00000000..6769ea04 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/load-dotenv.ts @@ -0,0 +1,12 @@ +/** + * Load .env from action root and assess dir before any other module that reads process.env. + * Import this first in run-local-assess so env is set before index.js loads. + */ +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { config } from 'dotenv'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const actionRoot = resolve(__dirname, '../..'); +config({ path: resolve(actionRoot, '.env'), override: true }); +config({ path: resolve(__dirname, '../.env'), override: true }); diff --git a/.github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts b/.github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts new file mode 100644 index 00000000..af66833a --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env -S npx tsx +/** + * Commit implement output, push branch, create PR if missing. + * Caller must ensure repoRoot is the implementation branch (e.g. after checkout or worktree add). + * + * Env: GITHUB_TOKEN (for push and gh). IMPLEMENT_COMMIT_MSG_FILE overrides default path. + * When run as script: ISSUE_NUMBER and GITHUB_WORKSPACE (or cwd = assess/ with repo root 4 levels up). + */ +import { resolve } from 'path'; +import { unlinkSync, existsSync, readFileSync } from 'fs'; +import { execSync } from 'child_process'; + +const REL_DIR = '.github/actions/issue-auto-implement'; +const REL_COMMIT_MSG = `${REL_DIR}/.commit_msg`; +const REL_PR_TITLE = `${REL_DIR}/.pr_title`; +const REL_PR_BODY = `${REL_DIR}/.pr_body`; + +export function pushAndOpenPr(repoRoot: string, issueNumber: number, token?: string): void { + const commitMsgFile = resolve(repoRoot, REL_COMMIT_MSG); + if (!existsSync(commitMsgFile)) { + throw new Error(`Missing ${commitMsgFile}; run implement step first.`); + } + const branch = `auto-implement-issue-${issueNumber}`; + const env = { ...process.env, GH_TOKEN: token || process.env.GITHUB_TOKEN }; + + execSync('git add -A', { cwd: repoRoot, stdio: 'inherit' }); + for (const rel of [REL_COMMIT_MSG, REL_PR_TITLE, REL_PR_BODY]) { + try { + execSync(`git reset -- ${rel}`, { cwd: repoRoot, stdio: 'pipe' }); + } catch { + // ignore if path not staged + } + } + + let hasStaged: boolean; + try { + execSync('git diff --staged --quiet', { cwd: repoRoot, stdio: 'pipe' }); + hasStaged = false; + } catch { + hasStaged = true; + } + + if (hasStaged) { + execSync(`git commit -F ${REL_COMMIT_MSG}`, { cwd: repoRoot, stdio: 'inherit' }); + unlinkSync(commitMsgFile); + execSync(`git push -u origin ${branch} --force-with-lease`, { cwd: repoRoot, stdio: 'inherit', env }); + } else { + console.error('No changes to commit.'); + } + + const prTitleFile = resolve(repoRoot, REL_PR_TITLE); + const prBodyFile = resolve(repoRoot, REL_PR_BODY); + + let shouldCreatePr = true; + try { + const out = execSync('gh pr view --json state', { cwd: repoRoot, encoding: 'utf-8', env }); + const { state } = JSON.parse(out) as { state: string }; + if (state === 'OPEN') { + shouldCreatePr = false; + console.error('PR already exists; branch pushed.'); + } + // If state is CLOSED or MERGED, create a new PR for this branch (GitHub allows that). + } catch { + // No PR for this branch; create one. + } + + if (shouldCreatePr) { + const title = existsSync(prTitleFile) ? readFileSync(prTitleFile, 'utf-8').trim() : `Implement issue #${issueNumber}`; + const bodyPath = existsSync(prBodyFile) ? prBodyFile : null; + const createArgs = bodyPath + ? `--title ${JSON.stringify(title)} --body-file ${JSON.stringify(prBodyFile)}` + : `--title ${JSON.stringify(title)} --body ${JSON.stringify(`Closes #${issueNumber}`)}`; + execSync(`gh pr create ${createArgs}`, { + cwd: repoRoot, + stdio: 'inherit', + env, + }); + if (existsSync(prTitleFile)) unlinkSync(prTitleFile); + if (existsSync(prBodyFile)) unlinkSync(prBodyFile); + console.error('PR created.'); + } else { + if (existsSync(prTitleFile)) unlinkSync(prTitleFile); + if (existsSync(prBodyFile)) unlinkSync(prBodyFile); + } +} + +function main(): void { + const repoRoot = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../../..'); + const issueNumber = process.env.ISSUE_NUMBER; + if (!issueNumber) { + console.error('Set ISSUE_NUMBER'); + process.exit(1); + } + pushAndOpenPr(repoRoot, parseInt(issueNumber, 10)); +} + +const isMain = + typeof process.argv[1] === 'string' && + (process.argv[1].endsWith('push-and-open-pr.ts') || process.argv[1].endsWith('push-and-open-pr.js')); +if (isMain) { + main(); +} diff --git a/.github/actions/issue-auto-implement/assess/src/run-local-assess.ts b/.github/actions/issue-auto-implement/assess/src/run-local-assess.ts new file mode 100644 index 00000000..cacd0084 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/run-local-assess.ts @@ -0,0 +1,224 @@ +#!/usr/bin/env -S npx tsx +/** + * Run assess against a real issue by fetching it from GitHub (no event file). + * Optionally APPLY=1: post comments to the issue or run implement + push (same as the workflow would). + * + * Local runs force the same default context as CI so assess and implement see the same repo guidance. + * Env: ISSUE_NUMBER (required), GITHUB_REPOSITORY, GITHUB_TOKEN, AUTO_IMPLEMENT_ANTHROPIC_API_KEY. + * Optional: EVENT_TYPE=issues|issue_comment|pull_request_review; COMMENT_BODY (for issue_comment); REVIEW_BODY (for pull_request_review). + * Optional: APPLY=1 — post comment on issue (request_info/redirect_to_pr) or run implement then push-and-open-pr (implement). + * Optional: CONTEXT_FILES — overrides default; default matches action.yml context_files (AGENTS.md,REFERENCE.md). + * + * Output: same JSON as index.ts (action, issue_number, comment_body?, verification_notes?, pr_url?). + */ +import './load-dotenv.js'; + +/** Must match action.yml inputs.context_files default so local and CI use the same repo context. */ +const DEFAULT_CONTEXT_FILES = 'AGENTS.md,REFERENCE.md'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { existsSync } from 'fs'; +import { execSync } from 'child_process'; +import { assess } from './index.js'; +import { pushAndOpenPr } from './push-and-open-pr.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const ISSUE_NUMBER = process.env.ISSUE_NUMBER; +const REPO = process.env.GITHUB_REPOSITORY || ''; +const TOKEN = process.env.GITHUB_TOKEN || ''; +const EVENT_TYPE = (process.env.EVENT_TYPE || 'issues') as 'issues' | 'issue_comment' | 'pull_request_review'; +const COMMENT_BODY = process.env.COMMENT_BODY || ''; +const REVIEW_BODY = process.env.REVIEW_BODY || ''; +const APPLY = process.env.APPLY === '1' || process.env.APPLY === 'true'; +const LABEL_PREFIX = process.env.LABEL_PREFIX || 'automation'; + +async function fetchIssue(owner: string, repo: string, issueNumber: number): Promise<{ title: string; body: string; number: number; labels: { name: string }[] }> { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) throw new Error(`Failed to fetch issue: ${res.status} ${await res.text()}`); + const data = (await res.json()) as { title?: string; body?: string; number?: number; labels?: { name: string }[] }; + return { + title: data.title ?? '', + body: data.body ?? '', + number: data.number ?? issueNumber, + labels: data.labels ?? [], + }; +} + +async function fetchIssueComments(owner: string, repo: string, issueNumber: number): Promise<{ body?: string; user?: { login?: string }; created_at?: string }[]> { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) return []; + const data = (await res.json()) as { body?: string; user?: { login?: string }; created_at?: string }[]; + return Array.isArray(data) ? data : []; +} + +async function postIssueComment(owner: string, repo: string, issueNumber: number, body: string): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${TOKEN}`, + Accept: 'application/vnd.github+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ body }), + }); + if (!res.ok) throw new Error(`Failed to post comment: ${res.status} ${await res.text()}`); +} + +async function addLabel(owner: string, repo: string, issueNumber: number, label: string): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/labels`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${TOKEN}`, + Accept: 'application/vnd.github+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ labels: [label] }), + }); + if (!res.ok) { + // 422 = label may not exist; ignore + if (res.status !== 422) throw new Error(`Failed to add label: ${res.status} ${await res.text()}`); + } +} + +async function main(): Promise { + if (!process.env.CONTEXT_FILES?.trim()) { + process.env.CONTEXT_FILES = DEFAULT_CONTEXT_FILES; + } + + const missing: string[] = []; + if (!ISSUE_NUMBER) missing.push('ISSUE_NUMBER'); + if (!REPO) missing.push('GITHUB_REPOSITORY'); + if (!TOKEN) missing.push('GITHUB_TOKEN'); + if (missing.length) { + throw new Error( + `Missing or empty: ${missing.join(', ')}. Set in .env (action root or assess/) or in the environment.` + ); + } + const issueNumber = parseInt(ISSUE_NUMBER, 10); + if (Number.isNaN(issueNumber)) throw new Error('Invalid ISSUE_NUMBER'); + + const [owner, repoName] = REPO.split('/'); + if (!owner || !repoName) throw new Error('Invalid GITHUB_REPOSITORY (use owner/repo)'); + + if (EVENT_TYPE === 'pull_request_review' && !REVIEW_BODY.trim()) { + throw new Error('Set REVIEW_BODY when EVENT_TYPE=pull_request_review (paste the review you left on the PR)'); + } + + const issue = await fetchIssue(owner, repoName, issueNumber); + const comments = await fetchIssueComments(owner, repoName, issueNumber); + + let eventName: string; + let payload: unknown; + + if (EVENT_TYPE === 'pull_request_review') { + eventName = 'pull_request_review'; + payload = { + pull_request: { + body: `Closes #${issueNumber}`, + head: { ref: `auto-implement-issue-${issueNumber}` }, + }, + review: { body: REVIEW_BODY }, + issue: { number: issue.number, title: issue.title, body: issue.body, labels: issue.labels }, + }; + } else if (EVENT_TYPE === 'issue_comment' || COMMENT_BODY.trim()) { + eventName = 'issue_comment'; + payload = { + action: 'created', + issue: { + number: issue.number, + title: issue.title, + body: issue.body, + labels: issue.labels.length ? issue.labels : [{ name: `${LABEL_PREFIX}/auto-implement` }], + }, + comment: { body: COMMENT_BODY.trim() || '(new comment)' }, + repository: { full_name: REPO }, + }; + } else { + eventName = 'issues'; + payload = { + action: 'labeled', + issue: { + number: issue.number, + title: issue.title, + body: issue.body, + labels: issue.labels.length ? issue.labels : [{ name: `${LABEL_PREFIX}/auto-implement` }], + }, + repository: { full_name: REPO }, + }; + } + + const result = await assess(eventName, payload, { + repo: REPO, + token: TOKEN, + referenceIssue: process.env.ASSESSMENT_REFERENCE_ISSUE || '192', + }); + + console.log(JSON.stringify(result)); + + if (!APPLY) return; + + if (result.action === 'request_info' && result.comment_body) { + await postIssueComment(owner, repoName, issueNumber, result.comment_body); + await addLabel(owner, repoName, issueNumber, `${LABEL_PREFIX}/needs-info`); + console.error('Posted request for more info on issue #' + issueNumber); + } else if (result.action === 'redirect_to_pr' && result.pr_url) { + const body = `A PR is open for this issue. Please review and comment on the PR: ${result.pr_url}`; + await postIssueComment(owner, repoName, issueNumber, body); + console.error('Posted redirect to PR on issue #' + issueNumber); + } else if (result.action === 'implement') { + const assessDir = process.cwd(); + const repoRoot = resolve(assessDir, '../../../..'); + const branch = `auto-implement-issue-${issueNumber}`; + const worktreePath = resolve(repoRoot, '.worktrees', branch); + + // Always start from a clean branch: remove existing worktree and local branch, then create fresh from origin/main + if (existsSync(worktreePath)) { + try { + execSync(`git worktree remove "${worktreePath}" --force`, { cwd: repoRoot, stdio: 'inherit' }); + } catch (e) { + console.error('Failed to remove existing worktree:', e); + throw e; + } + } + try { + execSync(`git branch -D ${branch}`, { cwd: repoRoot, stdio: 'pipe' }); + } catch { + // local branch may not exist + } + try { + execSync('git fetch origin main', { cwd: repoRoot, stdio: 'pipe' }); + } catch { + // ignore + } + execSync(`git worktree add "${worktreePath}" -b "${branch}" origin/main`, { + cwd: repoRoot, + stdio: 'inherit', + }); + + const env = { + ...process.env, + ISSUE_NUMBER: String(issueNumber), + VERIFICATION_NOTES: result.verification_notes || '', + GITHUB_REPOSITORY: REPO, + GITHUB_TOKEN: TOKEN, + GITHUB_WORKSPACE: worktreePath, + }; + execSync('npx tsx src/implement.ts', { cwd: assessDir, env, stdio: 'inherit' }); + pushAndOpenPr(worktreePath, issueNumber, TOKEN); + console.error('Ran implement in worktree and pushed; PR created or updated.'); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh b/.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh new file mode 100755 index 00000000..2f261a2b --- /dev/null +++ b/.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# From repo root: commit implement output, push branch, open PR if missing. +# Requires: ISSUE_NUMBER set (e.g. export ISSUE_NUMBER=192). +# Requires: gh CLI and GITHUB_TOKEN (or gh auth). +# +# Prefer the TypeScript implementation when running from the assess flow: +# assess/src/push-and-open-pr.ts (used by run-local-assess with APPLY=1). +# This script is still valid for manual "commit and open PR" from repo root +# when you have already run implement and are on the implementation branch. +# +# Usage from repo root: +# ISSUE_NUMBER=192 ./.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh +set -e +if [[ -z "$ISSUE_NUMBER" ]]; then + echo "Set ISSUE_NUMBER (e.g. export ISSUE_NUMBER=192)" >&2 + exit 1 +fi +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" +BRANCH="auto-implement-issue-${ISSUE_NUMBER}" +COMMIT_MSG_FILE=".github/actions/issue-auto-implement/.commit_msg" +if [[ ! -f "$COMMIT_MSG_FILE" ]]; then + echo "Missing $COMMIT_MSG_FILE (run implement step first from assess dir)" >&2 + exit 1 +fi +# Create or checkout branch +if git show-ref --verify --quiet refs/heads/"$BRANCH"; then + git checkout "$BRANCH" + git merge origin/main --no-edit 2>/dev/null || true +elif git show-ref --verify --quiet refs/remotes/origin/"$BRANCH"; then + git fetch origin "$BRANCH" + git checkout -b "$BRANCH" origin/"$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git merge origin/main --no-edit 2>/dev/null || true +else + git fetch origin main 2>/dev/null || true + git checkout -b "$BRANCH" origin/main 2>/dev/null || git checkout -b "$BRANCH" main +fi +# Stage all, unstage commit message file, commit, push +git add -A +git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true +if git diff --staged --quiet; then + echo "No changes to commit." +else + git commit -F "$COMMIT_MSG_FILE" + rm -f "$COMMIT_MSG_FILE" +fi +git push -u origin "$BRANCH" +# Open PR if none exists for this branch +if ! gh pr view --json number 2>/dev/null; then + gh pr create --fill --body "Closes #${ISSUE_NUMBER}" + echo "PR created." +else + echo "PR already exists; branch pushed." +fi From 27a5e56a798b1fa2b195d5d37e7a312e132a91e5 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 17:44:11 +0000 Subject: [PATCH 13/17] docs(issue-auto-implement): quick start, remove TEST_PLAN refs, add assess:local - README: add How to use (workflow, secrets, trigger label, trigger) - Remove TEST_PLAN.md references from README and AGENTS.md - package.json: add assess:local script Made-with: Cursor --- .github/actions/issue-auto-implement/AGENTS.md | 2 +- .github/actions/issue-auto-implement/README.md | 11 +++++++++-- .../actions/issue-auto-implement/assess/package.json | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md index 963ae513..0e58571f 100644 --- a/.github/actions/issue-auto-implement/AGENTS.md +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -78,4 +78,4 @@ Possible future improvements: 2. **Optional comment when PR is created** — Input `post_pr_comment` exists; when true, post a short comment on the issue linking to the new PR when one is created. 3. **Local run with fixture and Claude** — `npm run assess:fixture` exists; optional end-to-end testing with real `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`. -Done: context files in assess, implement step (Claude Code CLI), implement–verify loop with re-implement on failure, PR review iteration (comment on PR, no new PR), comment when retries exhausted, secrets/variables and README docs, local assess script and TEST_PLAN. +Done: context files in assess, implement step (Claude Code CLI), implement–verify loop with re-implement on failure, PR review iteration (comment on PR, no new PR), comment when retries exhausted, secrets/variables and README docs, local assess script. diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md index 7358b94d..a949382d 100644 --- a/.github/actions/issue-auto-implement/README.md +++ b/.github/actions/issue-auto-implement/README.md @@ -2,7 +2,14 @@ Reusable composite action for label-triggered issue automation: assess (request more info or implement), implement-verify loop, then create PR or iterate on an existing PR after review. -## Usage +## How to use (quick start) + +1. **Workflow** — Ensure `.github/workflows/issue-auto-implement.yml` exists and calls this action (see the workflow in this repo for the exact `on:` and `uses:`). +2. **Secrets and variable** — In the repo: Settings → Secrets and variables → Actions. Add secret **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (Anthropic API key). Add variable **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (e.g. `your-org/your-team`); only members of this team can trigger the action. +3. **Trigger label** — Create a label **`automation/auto-implement`** (or `{your-prefix}/auto-implement` if you set `label_prefix`) in the repo so you can add it to issues. The action will create the other labels (`needs-info`, `pr-created`) when it runs. +4. **Trigger** — On an issue, add the label `automation/auto-implement`. The workflow runs: it assesses the issue (request more info vs implement), and if implement, runs the Claude Code CLI and opens a PR. You can also comment on the issue (to add context and re-trigger) or review the PR (to iterate). + +## Usage (reference) Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_key` (e.g. from repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), `github_allowed_trigger_team` (e.g. from repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`), and `github_token` from the workflow. @@ -112,4 +119,4 @@ For implementation details and verification steps, see `AGENTS.md`. ### Local run against a real issue (no workflow events) -To test the full flow locally against a real GitHub issue and create a PR, use the **local assess** script (fetches the issue from the API and runs the same assess logic). With **APPLY=1** the script applies the outcome: posts the request-for-more-info or redirect comment on the issue, or runs implement and push (creates/updates the PR). When the outcome is implement, the script creates or reuses a **worktree** at `.worktrees/auto-implement-issue-` so your current branch is left untouched; implement runs there, then commit/push/PR is done in TypeScript (`assess/src/push-and-open-pr.ts`). The workflow does not need to run; you trigger each step locally and optionally pass **COMMENT_BODY** (after you add a comment on the issue) or **REVIEW_BODY** (after you review the PR). See **[TEST_PLAN.md](TEST_PLAN.md)** for the **Live flow walkthrough** and full test plan. +To test the full flow locally against a real GitHub issue and create a PR, use the **local assess** script (fetches the issue from the API and runs the same assess logic). With **APPLY=1** the script applies the outcome: posts the request-for-more-info or redirect comment on the issue, or runs implement and push (creates/updates the PR). When the outcome is implement, the script creates or reuses a **worktree** at `.worktrees/auto-implement-issue-` so your current branch is left untouched; implement runs there, then commit/push/PR is done in TypeScript (`assess/src/push-and-open-pr.ts`). The workflow does not need to run; you trigger each step locally and optionally pass **COMMENT_BODY** (after you add a comment on the issue) or **REVIEW_BODY** (after you review the PR). diff --git a/.github/actions/issue-auto-implement/assess/package.json b/.github/actions/issue-auto-implement/assess/package.json index 4944a813..7835c682 100644 --- a/.github/actions/issue-auto-implement/assess/package.json +++ b/.github/actions/issue-auto-implement/assess/package.json @@ -7,6 +7,7 @@ "test:integration": "vitest run --config vitest.integration.config.ts", "start": "tsx src/index.ts", "assess:fixture": "GITHUB_EVENT_PATH=./test/fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues tsx src/index.ts", + "assess:local": "tsx src/run-local-assess.ts", "implement:issue": "tsx src/implement.ts" }, "dependencies": { From 38dbe179676531cdf14c5d9620c2068c47ea1e5a Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 18:57:43 +0000 Subject: [PATCH 14/17] feat(issue-auto-implement): add setup workflow to create labels - Add issue-auto-implement-setup.yml (workflow_dispatch) to create automation/auto-implement, automation/needs-info, automation/pr-created - README: quick start step 3 mentions running setup workflow - REMOTE_TEST_PLAN: prerequisites reference setup workflow Made-with: Cursor --- .../actions/issue-auto-implement/README.md | 2 +- .../issue-auto-implement/REMOTE_TEST_PLAN.md | 70 +++++++++++++++++++ .../workflows/issue-auto-implement-setup.yml | 36 ++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md create mode 100644 .github/workflows/issue-auto-implement-setup.yml diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md index a949382d..e9c9145e 100644 --- a/.github/actions/issue-auto-implement/README.md +++ b/.github/actions/issue-auto-implement/README.md @@ -6,7 +6,7 @@ Reusable composite action for label-triggered issue automation: assess (request 1. **Workflow** — Ensure `.github/workflows/issue-auto-implement.yml` exists and calls this action (see the workflow in this repo for the exact `on:` and `uses:`). 2. **Secrets and variable** — In the repo: Settings → Secrets and variables → Actions. Add secret **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (Anthropic API key). Add variable **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (e.g. `your-org/your-team`); only members of this team can trigger the action. -3. **Trigger label** — Create a label **`automation/auto-implement`** (or `{your-prefix}/auto-implement` if you set `label_prefix`) in the repo so you can add it to issues. The action will create the other labels (`needs-info`, `pr-created`) when it runs. +3. **Trigger label** — Create the labels once so you can add them to issues. Either run the **Issue auto-implement setup** workflow (Actions → Issue auto-implement setup → Run workflow), which creates `automation/auto-implement`, `automation/needs-info`, and `automation/pr-created`; or create the trigger label **`automation/auto-implement`** manually in the repo (Settings or Issues → Labels). The main action also ensures these labels exist when it runs, but the trigger label must exist before you can add it to an issue. 4. **Trigger** — On an issue, add the label `automation/auto-implement`. The workflow runs: it assesses the issue (request more info vs implement), and if implement, runs the Claude Code CLI and opens a PR. You can also comment on the issue (to add context and re-trigger) or review the PR (to iterate). ## Usage (reference) diff --git a/.github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md b/.github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md new file mode 100644 index 00000000..8f74d7bb --- /dev/null +++ b/.github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md @@ -0,0 +1,70 @@ +# Remote test plan (throwaway) + +Use this to verify the issue-auto-implement action works on GitHub after merging PR #238. Delete this file once you're done. + +## Prerequisites (after merge) + +1. **Merge** [PR #238](https://github.com/hookdeck/hookdeck-cli/pull/238) (`feature/issue-auto-implement` → `main`). +2. **Delete** the implement branch [auto-implement-issue-232](https://github.com/hookdeck/hookdeck-cli/tree/auto-implement-issue-232) from remote (it was from a local implement run; no longer needed): + - GitHub: repo → Branches → find `auto-implement-issue-232` → delete. + - Or: `git push origin --delete auto-implement-issue-232` +3. **Secrets/variable** already set: `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (secret), `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM` (variable). +4. **Labels** created: run **Issue auto-implement setup** (Actions → Issue auto-implement setup → Run workflow) once to create `automation/auto-implement`, `automation/needs-info`, `automation/pr-created`. Or create `automation/auto-implement` manually in the repo. + +--- + +## Test 1: Label triggers workflow + +1. Open or create a **test issue** (e.g. "Test: auto-implement workflow" with a short, clear description). +2. Add the label **`automation/auto-implement`** to the issue. +3. **Expect:** Actions tab shows a run for "Issue auto-implement" (workflow `issue-auto-implement.yml`). +4. **Check:** Run completes; either "request_info" (comment posted, needs-info label) or "implement" (implement step runs, then verify, then PR created). If implement runs, confirm the "Install Claude Code CLI" step and the implement step succeed. + +**Pass:** Workflow runs when label is added; no "workflow not triggered" or permission errors. + +--- + +## Test 2: Implement path (full flow on GitHub) + +Use an issue with **enough context** so assess returns `implement` (e.g. clear title + body, or reference a small, well-defined task). + +1. Add `automation/auto-implement` to that issue. +2. **Expect:** Assess → implement → verify loop → create PR (or comment on issue if request_info). +3. **Check:** Actions run shows: Checkout → Install assess deps → Run assess → Checkout branch for implement → **Install Claude Code CLI** → Implement and verify (loop) → Create PR step (if verify passed). +4. **Check:** A PR is created from branch `auto-implement-issue-` with "Closes #". +5. **Check:** PR is open and points at the correct issue. + +**Pass:** Implement runs in CI, CLI installs, PR is created. + +--- + +## Test 3: PR review iteration (optional) + +1. On the PR from Test 2, submit a **review** (e.g. "Please add a unit test for the new function"). +2. **Expect:** Workflow runs again (pull_request_review trigger). +3. **Check:** Assess runs with review context → implement runs → push to same branch → verify → **no new PR**; instead a comment on the existing PR (e.g. "Addressed review feedback..."). + +**Pass:** Review triggers run; same PR updated; comment posted on PR. + +--- + +## Test 4: Team check (optional) + +1. Use an account that is **not** in `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. +2. Add the label to an issue (or trigger in another way). +3. **Expect:** Workflow run fails early with an error that the actor is not in the team. + +**Pass:** Non-members cannot run the action. + +--- + +## Quick checklist + +- [ ] PR #238 merged to main +- [ ] Branch `auto-implement-issue-232` deleted from remote +- [ ] Label `automation/auto-implement` exists +- [ ] Test 1: Adding label triggers workflow +- [ ] Test 2: Implement path runs in CI and creates PR (if issue has enough context) +- [ ] Test 3 (optional): PR review triggers iteration +- [ ] Test 4 (optional): Team check blocks non-members +- [ ] Delete this file when done diff --git a/.github/workflows/issue-auto-implement-setup.yml b/.github/workflows/issue-auto-implement-setup.yml new file mode 100644 index 00000000..c49edced --- /dev/null +++ b/.github/workflows/issue-auto-implement-setup.yml @@ -0,0 +1,36 @@ +# One-time setup: create the labels required by issue-auto-implement. +# Run manually (Actions → Issue auto-implement setup → Run workflow) once per repo. +# After this runs, the "automation/auto-implement" label will exist so you can add it to issues. +name: Issue auto-implement setup + +on: + workflow_dispatch: + +jobs: + create-labels: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Create labels + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + LABEL_PREFIX: automation + run: | + for LABEL in auto-implement needs-info pr-created; do + NAME="${LABEL_PREFIX}/${LABEL}" + CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/labels" \ + -d "{\"name\":\"$NAME\",\"color\":\"ededed\",\"description\":\"Issue auto-implement: $LABEL\"}") + if [ "$CODE" = "201" ]; then + echo "Created label: $NAME" + elif [ "$CODE" = "422" ]; then + echo "Label already exists: $NAME" + else + echo "::warning::Unexpected response $CODE for $NAME" + fi + done + echo "Done. You can now add the label $LABEL_PREFIX/auto-implement to issues." From 803b3b1d2452948922a5a485175abb08757838ff Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 19:01:49 +0000 Subject: [PATCH 15/17] chore: remove REMOTE_TEST_PLAN.md from source control Made-with: Cursor --- .../issue-auto-implement/REMOTE_TEST_PLAN.md | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 .github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md diff --git a/.github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md b/.github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md deleted file mode 100644 index 8f74d7bb..00000000 --- a/.github/actions/issue-auto-implement/REMOTE_TEST_PLAN.md +++ /dev/null @@ -1,70 +0,0 @@ -# Remote test plan (throwaway) - -Use this to verify the issue-auto-implement action works on GitHub after merging PR #238. Delete this file once you're done. - -## Prerequisites (after merge) - -1. **Merge** [PR #238](https://github.com/hookdeck/hookdeck-cli/pull/238) (`feature/issue-auto-implement` → `main`). -2. **Delete** the implement branch [auto-implement-issue-232](https://github.com/hookdeck/hookdeck-cli/tree/auto-implement-issue-232) from remote (it was from a local implement run; no longer needed): - - GitHub: repo → Branches → find `auto-implement-issue-232` → delete. - - Or: `git push origin --delete auto-implement-issue-232` -3. **Secrets/variable** already set: `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (secret), `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM` (variable). -4. **Labels** created: run **Issue auto-implement setup** (Actions → Issue auto-implement setup → Run workflow) once to create `automation/auto-implement`, `automation/needs-info`, `automation/pr-created`. Or create `automation/auto-implement` manually in the repo. - ---- - -## Test 1: Label triggers workflow - -1. Open or create a **test issue** (e.g. "Test: auto-implement workflow" with a short, clear description). -2. Add the label **`automation/auto-implement`** to the issue. -3. **Expect:** Actions tab shows a run for "Issue auto-implement" (workflow `issue-auto-implement.yml`). -4. **Check:** Run completes; either "request_info" (comment posted, needs-info label) or "implement" (implement step runs, then verify, then PR created). If implement runs, confirm the "Install Claude Code CLI" step and the implement step succeed. - -**Pass:** Workflow runs when label is added; no "workflow not triggered" or permission errors. - ---- - -## Test 2: Implement path (full flow on GitHub) - -Use an issue with **enough context** so assess returns `implement` (e.g. clear title + body, or reference a small, well-defined task). - -1. Add `automation/auto-implement` to that issue. -2. **Expect:** Assess → implement → verify loop → create PR (or comment on issue if request_info). -3. **Check:** Actions run shows: Checkout → Install assess deps → Run assess → Checkout branch for implement → **Install Claude Code CLI** → Implement and verify (loop) → Create PR step (if verify passed). -4. **Check:** A PR is created from branch `auto-implement-issue-` with "Closes #". -5. **Check:** PR is open and points at the correct issue. - -**Pass:** Implement runs in CI, CLI installs, PR is created. - ---- - -## Test 3: PR review iteration (optional) - -1. On the PR from Test 2, submit a **review** (e.g. "Please add a unit test for the new function"). -2. **Expect:** Workflow runs again (pull_request_review trigger). -3. **Check:** Assess runs with review context → implement runs → push to same branch → verify → **no new PR**; instead a comment on the existing PR (e.g. "Addressed review feedback..."). - -**Pass:** Review triggers run; same PR updated; comment posted on PR. - ---- - -## Test 4: Team check (optional) - -1. Use an account that is **not** in `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. -2. Add the label to an issue (or trigger in another way). -3. **Expect:** Workflow run fails early with an error that the actor is not in the team. - -**Pass:** Non-members cannot run the action. - ---- - -## Quick checklist - -- [ ] PR #238 merged to main -- [ ] Branch `auto-implement-issue-232` deleted from remote -- [ ] Label `automation/auto-implement` exists -- [ ] Test 1: Adding label triggers workflow -- [ ] Test 2: Implement path runs in CI and creates PR (if issue has enough context) -- [ ] Test 3 (optional): PR review triggers iteration -- [ ] Test 4 (optional): Team check blocks non-members -- [ ] Delete this file when done From dead048a638fd71e86f78ae31a244a97cda7e705 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 19:26:37 +0000 Subject: [PATCH 16/17] feat(issue-auto-implement): add permission-based trigger gate (works with default token) - Add optional github_allowed_trigger_min_permission input (triage, push, maintain, admin). When set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION, the action checks repo collaborator permission via API; works with default GITHUB_TOKEN (no read:org). - Keep team check (AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM) as alternative; token needs read:org. - Exactly one gate required per run; if both variables set, permission check is used. - Update README and AGENTS.md with both options. Made-with: Cursor --- .../actions/issue-auto-implement/AGENTS.md | 7 ++- .../actions/issue-auto-implement/README.md | 22 +++++--- .../actions/issue-auto-implement/action.yml | 55 ++++++++++++++++--- .github/workflows/issue-auto-implement.yml | 5 +- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md index 0e58571f..5e164751 100644 --- a/.github/actions/issue-auto-implement/AGENTS.md +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -51,7 +51,12 @@ From the workflow event payload, derive: ## Restricting who can trigger -Only members of the `github_allowed_trigger_team` (input; set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`) may trigger the flow. First step checks `github.actor` against that team; if the variable is unset or the actor is not a member, fail immediately. Token must have `read:org`. +The first step enforces one of two gates (exactly one must be configured; no bypass): + +1. **Permission check** — If `github_allowed_trigger_min_permission` is set (repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION`: `triage`, `push`, `maintain`, or `admin`), the action calls the repo collaborator permission API and requires the actor to have at least that permission. Works with the default `GITHUB_TOKEN`. +2. **Team check** — Otherwise, if `github_allowed_trigger_team` is set (repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`, e.g. `org/team`), the action checks `github.actor` against that team; if unset or not a member, fail. Token must have `read:org` (PAT if `GITHUB_TOKEN` lacks it). + +If neither variable is set, the step fails. When both are set, the permission check is used and the team value is ignored. ## Labels diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md index e9c9145e..cb9f6f77 100644 --- a/.github/actions/issue-auto-implement/README.md +++ b/.github/actions/issue-auto-implement/README.md @@ -5,28 +5,31 @@ Reusable composite action for label-triggered issue automation: assess (request ## How to use (quick start) 1. **Workflow** — Ensure `.github/workflows/issue-auto-implement.yml` exists and calls this action (see the workflow in this repo for the exact `on:` and `uses:`). -2. **Secrets and variable** — In the repo: Settings → Secrets and variables → Actions. Add secret **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (Anthropic API key). Add variable **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (e.g. `your-org/your-team`); only members of this team can trigger the action. +2. **Secrets and variables** — In the repo: Settings → Secrets and variables → Actions. Add secret **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (Anthropic API key). For who can trigger, set **one** of: **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION`** (e.g. `push` or `maintain`; works with default token) or **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (e.g. `org/team`; token needs `read:org`). 3. **Trigger label** — Create the labels once so you can add them to issues. Either run the **Issue auto-implement setup** workflow (Actions → Issue auto-implement setup → Run workflow), which creates `automation/auto-implement`, `automation/needs-info`, and `automation/pr-created`; or create the trigger label **`automation/auto-implement`** manually in the repo (Settings or Issues → Labels). The main action also ensures these labels exist when it runs, but the trigger label must exist before you can add it to an issue. 4. **Trigger** — On an issue, add the label `automation/auto-implement`. The workflow runs: it assesses the issue (request more info vs implement), and if implement, runs the Claude Code CLI and opens a PR. You can also comment on the issue (to add context and re-trigger) or review the PR (to iterate). ## Usage (reference) -Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_key` (e.g. from repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), `github_allowed_trigger_team` (e.g. from repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`), and `github_token` from the workflow. +Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_key` (e.g. from repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), one of `github_allowed_trigger_min_permission` or `github_allowed_trigger_team` (repo variables), and `github_token` from the workflow. ## Inputs | Input | Required | Default | Description | |-------|----------|---------|-------------| | `anthropic_api_key` | Yes | - | Claude API key. Set via repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` so multiple actions can use different keys. | -| `github_token` | Yes | - | Token with contents, issues, pull-requests, read:org | +| `github_token` | Yes | - | Token (contents, issues, pull-requests; read:org only if using team check) | | `context_files` | No | AGENTS.md,REFERENCE.md | Comma-separated paths for assessment context | | `assessment_reference_issue` | No | 192 | Reference issue number for "enough information" | | `label_prefix` | No | automation | Prefix for labels (e.g. automation/auto-implement) | | `verify_commands` | No | go test ./... | Commands run for verification | | `max_implement_retries` | No | 3 | Max retries on verify failure (cap 5) | -| `github_allowed_trigger_team` | Yes | - | GitHub Team slug (e.g. org/team); only members can trigger. Set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. | +| `github_allowed_trigger_team` | No* | - | Team slug (e.g. org/team); only members can trigger. Repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. Ignored if min_permission is set. Token needs read:org. | +| `github_allowed_trigger_min_permission` | No* | - | Require actor has at least this repo permission: triage, push, maintain, or admin. Repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION`. Works with default GITHUB_TOKEN. | | `post_pr_comment` | No | false | When true, post a comment on the issue linking to the new PR when one is created. | +*One of `github_allowed_trigger_min_permission` or `github_allowed_trigger_team` must be set (via repo variables). + Secrets and variables use an action-specific prefix (e.g. `AUTO_IMPLEMENT_`) so each action can have its own keys/variables and it's clear which workflow uses which. This also avoids clashing with platform-reserved names (e.g. `GITHUB_*`). ## CI/CD: what you need to run this workflow @@ -35,8 +38,10 @@ To use this action in GitHub Actions: 1. **Workflow** — Call the action from a workflow (e.g. `.github/workflows/issue-auto-implement.yml`) on `issues.labeled`, `issue_comment`, `pull_request_review`, and/or `pull_request_review_comment`. The job needs `contents: write`, `issues: write`, `pull-requests: write`. 2. **Secrets** — Add **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret). Used for the assess step and passed to the Claude Code CLI in the implement step. -3. **Variables** — Add **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable, required): GitHub Team slug (e.g. `org/team-name`) whose members may trigger the run. -4. **Token** — Pass `github_token` (e.g. `secrets.GITHUB_TOKEN`) to the action. The team check needs `read:org`; if `GITHUB_TOKEN` lacks it, use a PAT with `read:org` and pass it as the token. +3. **Variables (trigger gate)** — Set **one** of: + - **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION`** (repo variable): `triage`, `push`, `maintain`, or `admin`. Only users with at least this repo permission can trigger. Works with default `GITHUB_TOKEN`. + - **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable): org/team slug (e.g. `org/team-name`). Only team members can trigger. Token must have `read:org` (use a PAT if `GITHUB_TOKEN` lacks it). +4. **Token** — Pass `github_token` (e.g. `secrets.GITHUB_TOKEN`). If using the team check, the token needs `read:org`; the permission check works with the default token. 5. **Implement in CI** — The action installs the Claude Code CLI (`@anthropic-ai/claude-code`) when the assess outcome is `implement`, so the workflow does not need to install it. Implement runs in the repo with Read/Edit/Bash; the CLI uses `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`. No other setup is required. Optionally set `verify_commands` (default `go test ./...`) and `context_files` (default `AGENTS.md,REFERENCE.md`) to match your repo. @@ -44,8 +49,9 @@ No other setup is required. Optionally set `verify_commands` (default `go test . ## Secrets and variables (repo setup) - **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret) — Claude API key for the assess and implement steps. Add under Settings → Secrets and variables → Actions. -- **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable, required) — GitHub Team slug (e.g. `org/team-name`) whose members may trigger the workflow. Add under Settings → Secrets and variables → Actions. The first step checks `github.actor` against this team; if unset or not a member, the run fails. -- **Token for team check** — The workflow passes `github_token` (usually `secrets.GITHUB_TOKEN`) to the action. The team check needs `read:org`. If your default `GITHUB_TOKEN` does not have `read:org`, use a PAT with that scope and pass it as the token (e.g. a repo secret) instead. +- **Trigger gate (set one):** + - **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION`** (repo variable) — Require the triggering user to have at least this repo permission: `triage`, `push`, `maintain`, or `admin`. Works with the default `GITHUB_TOKEN`. Add under Settings → Secrets and variables → Actions → Variables. + - **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable) — GitHub Team slug (e.g. `org/team-name`) whose members may trigger. The first step checks `github.actor` against this team. The token needs `read:org`; if `GITHUB_TOKEN` lacks it, use a PAT and pass it as `github_token`. ## Triggers diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml index c1ac7471..8fd056c5 100644 --- a/.github/actions/issue-auto-implement/action.yml +++ b/.github/actions/issue-auto-implement/action.yml @@ -28,8 +28,11 @@ inputs: required: false default: '3' github_allowed_trigger_team: - description: 'GitHub Team slug whose members may trigger the flow (e.g. hookdeck/automation-triggers). Set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM.' - required: true + description: 'GitHub Team slug whose members may trigger (e.g. org/team). Requires token with read:org. Set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM. Ignored if github_allowed_trigger_min_permission is set.' + required: false + github_allowed_trigger_min_permission: + description: 'Alternative to team check: require actor has at least this repo permission (triage, push, maintain, admin). Works with default GITHUB_TOKEN. Set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION.' + required: false post_pr_comment: description: 'When true, post a comment on the issue linking to the new PR when one is created' required: false @@ -37,25 +40,59 @@ inputs: runs: using: 'composite' steps: - - name: Check allowed trigger team + - name: Check allowed trigger (permission or team) shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + MIN_PERM: ${{ inputs.github_allowed_trigger_min_permission }} + TEAM: ${{ inputs.github_allowed_trigger_team }} run: | - TEAM="${{ inputs.github_allowed_trigger_team }}" + # Option A: Require minimum repo permission (works with default GITHUB_TOKEN, no read:org) + if [ -n "$MIN_PERM" ]; then + case "$MIN_PERM" in + triage|push|maintain|admin) ;; + *) echo "::error::github_allowed_trigger_min_permission must be one of: triage, push, maintain, admin"; exit 1 ;; + esac + RESP=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/collaborators/$ACTOR/permission") + HTTP=$(echo "$RESP" | tail -n1) + BODY=$(echo "$RESP" | sed '$d') + if [ "$HTTP" != "200" ]; then + echo "::error::Actor $ACTOR has no collaborator permission on this repo (HTTP $HTTP). Only users with at least $MIN_PERM can trigger." + exit 1 + fi + PERM=$(echo "$BODY" | jq -r '.permission // "read"') + # Order: read/pull < triage < push < maintain < admin + level() { case "$1" in read|pull) echo 1;; triage) echo 2;; push) echo 3;; maintain) echo 4;; admin) echo 5;; *) echo 0;; esac; } + ACTOR_LEVEL=$(level "$PERM") + MIN_LEVEL=$(level "$MIN_PERM") + if [ "$ACTOR_LEVEL" -lt "$MIN_LEVEL" ]; then + echo "::error::Actor $ACTOR has permission $PERM; at least $MIN_PERM is required to trigger this workflow." + exit 1 + fi + echo "Actor $ACTOR has permission $PERM (>= $MIN_PERM)." + exit 0 + fi + # Option B: Require team membership (token needs read:org) if [ -z "$TEAM" ]; then - echo "::error::github_allowed_trigger_team must be set (e.g. repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM). Only team members can trigger this workflow." + echo "::error::Set one of: repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION (e.g. push) or AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM (e.g. org/team)." exit 1 fi ORG="${TEAM%/*}" SLUG="${TEAM#*/}" HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer ${{ inputs.github_token }}" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ - "https://api.github.com/orgs/${ORG}/teams/${SLUG}/members/${{ github.actor }}") + "https://api.github.com/orgs/${ORG}/teams/${SLUG}/members/$ACTOR") if [ "$HTTP" != "204" ]; then - echo "::error::Actor ${{ github.actor }} is not in team $TEAM (HTTP $HTTP). Only team members can trigger this workflow." + echo "::error::Actor $ACTOR is not in team $TEAM (HTTP $HTTP). Only team members can trigger. Token may need read:org." exit 1 fi - echo "Actor ${{ github.actor }} is in team $TEAM." + echo "Actor $ACTOR is in team $TEAM." - name: Ensure labels exist shell: bash env: diff --git a/.github/workflows/issue-auto-implement.yml b/.github/workflows/issue-auto-implement.yml index cf6f974c..343ae388 100644 --- a/.github/workflows/issue-auto-implement.yml +++ b/.github/workflows/issue-auto-implement.yml @@ -1,6 +1,6 @@ # Label-triggered issue automation: assess, implement-verify loop, create PR or iterate on PR. # Triggers: issue labeled (automation/auto-implement), issue comment, PR review. -# Require repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM (org/team slug). Token may need read:org (PAT) for team check. +# Gate: set one of AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION (e.g. push; works with default token) or AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM (org/team; token needs read:org). name: Issue auto-implement on: @@ -26,7 +26,7 @@ jobs: contents: write issues: write pull-requests: write - # Team check requires read:org; use a PAT as github_token if GITHUB_TOKEN lacks it + # read:org only needed if using team check (AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM) steps: - name: Checkout uses: actions/checkout@v4 @@ -37,4 +37,5 @@ jobs: with: anthropic_api_key: ${{ secrets.AUTO_IMPLEMENT_ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} + github_allowed_trigger_min_permission: ${{ vars.AUTO_IMPLEMENT_ALLOWED_TRIGGER_MIN_PERMISSION }} github_allowed_trigger_team: ${{ vars.AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM }} From 8f562dd58f3807b0a906dcbc65326c12059a61b2 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Mon, 16 Mar 2026 21:22:15 +0000 Subject: [PATCH 17/17] feat(ci): dotenv in deps for issue-auto-implement CI, upgrade to actions/checkout@v6 - Move dotenv to dependencies so npm ci --omit=dev installs it (fixes ERR_MODULE_NOT_FOUND in CI) - Upgrade actions/checkout@v4 to @v6 (Node 24, removes deprecation warning) - issue-auto-implement-test: checkout@v6, Node 22 for assess tests Made-with: Cursor --- .../actions/issue-auto-implement/assess/package-lock.json | 5 ++--- .github/actions/issue-auto-implement/assess/package.json | 4 ++-- .github/workflows/issue-auto-implement-test.yml | 4 ++-- .github/workflows/issue-auto-implement.yml | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/actions/issue-auto-implement/assess/package-lock.json b/.github/actions/issue-auto-implement/assess/package-lock.json index 570b4039..9b84e9bb 100644 --- a/.github/actions/issue-auto-implement/assess/package-lock.json +++ b/.github/actions/issue-auto-implement/assess/package-lock.json @@ -6,10 +6,10 @@ "": { "name": "issue-auto-implement-assess", "dependencies": { - "@anthropic-ai/sdk": "^0.32.1" + "@anthropic-ai/sdk": "^0.32.1", + "dotenv": "^16.4.5" }, "devDependencies": { - "dotenv": "^16.4.5", "tsx": "^4.19.2", "typescript": "^5.7.2", "vitest": "^2.1.6" @@ -1111,7 +1111,6 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/.github/actions/issue-auto-implement/assess/package.json b/.github/actions/issue-auto-implement/assess/package.json index 7835c682..92d99bfa 100644 --- a/.github/actions/issue-auto-implement/assess/package.json +++ b/.github/actions/issue-auto-implement/assess/package.json @@ -11,10 +11,10 @@ "implement:issue": "tsx src/implement.ts" }, "dependencies": { - "@anthropic-ai/sdk": "^0.32.1" + "@anthropic-ai/sdk": "^0.32.1", + "dotenv": "^16.4.5" }, "devDependencies": { - "dotenv": "^16.4.5", "tsx": "^4.19.2", "typescript": "^5.7.2", "vitest": "^2.1.6" diff --git a/.github/workflows/issue-auto-implement-test.yml b/.github/workflows/issue-auto-implement-test.yml index d8fdcb8d..e5c670d0 100644 --- a/.github/workflows/issue-auto-implement-test.yml +++ b/.github/workflows/issue-auto-implement-test.yml @@ -18,11 +18,11 @@ jobs: GITHUB_WORKSPACE: ${{ github.workspace }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' cache-dependency-path: .github/actions/issue-auto-implement/assess/package-lock.json - name: Install and test assess script diff --git a/.github/workflows/issue-auto-implement.yml b/.github/workflows/issue-auto-implement.yml index 343ae388..36e69ca1 100644 --- a/.github/workflows/issue-auto-implement.yml +++ b/.github/workflows/issue-auto-implement.yml @@ -29,7 +29,7 @@ jobs: # read:org only needed if using team check (AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM) steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Issue auto-implement