Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/agent-coding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Coding Agent

# Triggers Cursor's Coding Cloud Agent via API when a Linear issue arrives
# in this repo (mirrored as a GitHub issue by Linear's GitHub integration).
# Replaces the manual cursor.com/agents Automation so the trigger config
# lives in code and propagates automatically via gsd new.

on:
issues:
types: [opened]

# Concurrency-guard against the rare case where the same issue gets edited
# in a way that re-fires the trigger before the previous run claimed work.
concurrency:
group: coding-agent-${{ github.event.issue.number }}
cancel-in-progress: false

jobs:
# Gate the agent on CURSOR_API_KEY being present. Secrets can't be used in
# job-level `if:`, so we surface presence as an output here and skip cleanly
# (neutral, not failed) on repos that haven't set the key.
guard:
name: Check Cursor API key
runs-on: ubuntu-latest
outputs:
has_key: ${{ steps.check.outputs.has_key }}
steps:
- id: check
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
if [ -n "$CURSOR_API_KEY" ]; then
echo "has_key=true" >> "$GITHUB_OUTPUT"
else
echo "has_key=false" >> "$GITHUB_OUTPUT"
echo "::notice title=Cursor agent skipped::CURSOR_API_KEY is not set on this repo — skipping. Set it with: gh secret set CURSOR_API_KEY --repo <owner>/<repo>"
fi

trigger:
name: Launch Cursor Coding Agent
needs: guard
if: >
needs.guard.outputs.has_key == 'true' &&
contains(github.event.issue.labels.*.name, 'agent-task') &&
startsWith(github.event.issue.title, '[CODING]')
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v7

- name: Render prompt and call Cursor API
id: trigger
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
REPO: ${{ github.repository }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_URL: ${{ github.event.issue.html_url }}
run: |
set -euo pipefail
PROMPT=$(jq -Rs \
--arg repo "$REPO" \
--arg t "$ISSUE_TITLE" \
--arg b "${ISSUE_BODY:-}" \
--arg u "$ISSUE_URL" \
'
gsub("\\{\\{repo\\}\\}"; $repo)
| gsub("\\{\\{issue.title\\}\\}"; $t)
| gsub("\\{\\{issue.body\\}\\}"; $b)
| gsub("\\{\\{issue.url\\}\\}"; $u)
' .cursor/agent-prompts/coding.md)

PAYLOAD=$(jq -n \
--arg prompt "$PROMPT" \
--arg repo "$REPO" \
'{
prompt: { text: $prompt },
source: { repository: ("github.com/" + $repo), ref: "staging" },
target: { autoCreatePr: true, openAsCursorGithubApp: true },
model: "composer-2.5"
}')

response=$(curl -sS -X POST "https://api.cursor.com/v0/agents" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
echo "$response" | jq .
AGENT_ID=$(echo "$response" | jq -er '.id')
echo "agent_id=$AGENT_ID" >> "$GITHUB_OUTPUT"

- name: Comment agent run link on issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
ISSUE: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
run: |
gh issue comment "$ISSUE" --repo "$REPO" \
--body "🤖 Cursor coding agent started: [\`$AGENT_ID\`](https://cursor.com/agents/$AGENT_ID) — agent will open a PR against \`staging\` when done."
132 changes: 132 additions & 0 deletions .github/workflows/agent-devops.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: DevOps Agent

# Triggers Cursor's DevOps Cloud Agent via API after a qa-passed PR merges
# into staging. The workflow waits for the agent and posts its report as
# a comment on the merged PR (Cursor's sandbox token can't comment).

on:
pull_request:
types: [closed]

jobs:
# Gate the agent on CURSOR_API_KEY being present. Secrets can't be used in
# job-level `if:`, so we surface presence as an output here and skip cleanly
# (neutral, not failed) on repos that haven't set the key.
guard:
name: Check Cursor API key
runs-on: ubuntu-latest
outputs:
has_key: ${{ steps.check.outputs.has_key }}
steps:
- id: check
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
if [ -n "$CURSOR_API_KEY" ]; then
echo "has_key=true" >> "$GITHUB_OUTPUT"
else
echo "has_key=false" >> "$GITHUB_OUTPUT"
echo "::notice title=Cursor agent skipped::CURSOR_API_KEY is not set on this repo — skipping. Set it with: gh secret set CURSOR_API_KEY --repo <owner>/<repo>"
fi

trigger:
name: Run DevOps Agent
needs: guard
if: >
needs.guard.outputs.has_key == 'true' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'staging' &&
contains(github.event.pull_request.labels.*.name, 'qa-passed')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
ref: staging

- name: Trigger Cursor agent
id: trigger
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
REPO: ${{ github.repository }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_BASE: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
PROMPT=$(jq -Rs \
--arg repo "$REPO" \
--arg t "$PR_TITLE" \
--arg u "$PR_URL" \
--arg base "$PR_BASE" \
'
gsub("\\{\\{repo\\}\\}"; $repo)
| gsub("\\{\\{pr.title\\}\\}"; $t)
| gsub("\\{\\{pr.url\\}\\}"; $u)
| gsub("\\{\\{pr.base_ref\\}\\}"; $base)
' .cursor/agent-prompts/devops.md)

PAYLOAD=$(jq -n \
--arg prompt "$PROMPT" \
--arg repo "$REPO" \
'{
prompt: { text: $prompt },
source: { repository: ("github.com/" + $repo), ref: "staging" },
target: { autoCreatePr: false, openAsCursorGithubApp: true },
model: "composer-2.5"
}')

response=$(curl -sS -X POST "https://api.cursor.com/v0/agents" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
echo "$response" | jq .
AGENT_ID=$(echo "$response" | jq -er '.id')
echo "agent_id=$AGENT_ID" >> "$GITHUB_OUTPUT"

- name: Wait for agent to finish
id: wait
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
run: |
set -euo pipefail
for i in $(seq 1 30); do
agent_status=$(curl -sS "https://api.cursor.com/v0/agents/$AGENT_ID" \
-H "Authorization: Bearer $CURSOR_API_KEY" | jq -r '.status')
echo "[$i/30] status: $agent_status"
if [[ "$agent_status" != "RUNNING" && "$agent_status" != "CREATING" ]]; then
echo "final_status=$agent_status" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 30
done
echo "final_status=TIMEOUT" >> "$GITHUB_OUTPUT"

- name: Post agent report as PR comment
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
AGENT_STATUS: ${{ steps.wait.outputs.final_status }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
REPORT=$(curl -sS "https://api.cursor.com/v0/agents/$AGENT_ID/conversation" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
| jq -r '[.messages[] | select(.type == "assistant_message") | .text] | last')

{
echo "## DevOps verification (Cursor DevOps agent — IaC)"
echo ""
echo "**Agent run:** [\`$AGENT_ID\`](https://cursor.com/agents/$AGENT_ID)"
echo "**Status:** \`$AGENT_STATUS\`"
echo ""
echo "$REPORT"
} > /tmp/devops-comment.md
gh pr comment "$PR" --repo "$REPO" --body-file /tmp/devops-comment.md
145 changes: 145 additions & 0 deletions .github/workflows/agent-qa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: QA Agent

# Triggers Cursor's QA Cloud Agent via API when a PR is labeled needs-qa,
# waits for the agent to finish, then posts the report as a PR comment
# and updates labels. The workflow does the comment + label step (not
# the agent) because Cursor's sandbox token lacks pull-requests:write.

on:
pull_request:
types: [labeled]

jobs:
# Gate the agent on CURSOR_API_KEY being present. Secrets can't be used in
# job-level `if:`, so we surface presence as an output here and skip cleanly
# (neutral, not failed) on repos that haven't set the key.
guard:
name: Check Cursor API key
runs-on: ubuntu-latest
outputs:
has_key: ${{ steps.check.outputs.has_key }}
steps:
- id: check
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
if [ -n "$CURSOR_API_KEY" ]; then
echo "has_key=true" >> "$GITHUB_OUTPUT"
else
echo "has_key=false" >> "$GITHUB_OUTPUT"
echo "::notice title=Cursor agent skipped::CURSOR_API_KEY is not set on this repo — skipping. Set it with: gh secret set CURSOR_API_KEY --repo <owner>/<repo>"
fi

trigger:
name: Run QA Agent
needs: guard
if: needs.guard.outputs.has_key == 'true' && github.event.label.name == 'needs-qa'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- name: Trigger Cursor agent
id: trigger
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
REPO: ${{ github.repository }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_HEAD: ${{ github.event.pull_request.head.ref }}
PR_BASE: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
PROMPT=$(jq -Rs \
--arg repo "$REPO" \
--arg t "$PR_TITLE" \
--arg b "${PR_BODY:-}" \
--arg u "$PR_URL" \
--arg h "$PR_HEAD" \
--arg base "$PR_BASE" \
'
gsub("\\{\\{repo\\}\\}"; $repo)
| gsub("\\{\\{pr.title\\}\\}"; $t)
| gsub("\\{\\{pr.body\\}\\}"; $b)
| gsub("\\{\\{pr.url\\}\\}"; $u)
| gsub("\\{\\{pr.head_ref\\}\\}"; $h)
| gsub("\\{\\{pr.base_ref\\}\\}"; $base)
' .cursor/agent-prompts/qa.md)

PAYLOAD=$(jq -n \
--arg prompt "$PROMPT" \
--arg repo "$REPO" \
--arg ref "$PR_HEAD" \
'{
prompt: { text: $prompt },
source: { repository: ("github.com/" + $repo), ref: $ref },
target: { autoCreatePr: false, openAsCursorGithubApp: true },
model: "composer-2.5"
}')

response=$(curl -sS -X POST "https://api.cursor.com/v0/agents" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
echo "$response" | jq .
AGENT_ID=$(echo "$response" | jq -er '.id')
echo "agent_id=$AGENT_ID" >> "$GITHUB_OUTPUT"

- name: Wait for agent to finish
id: wait
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
run: |
set -euo pipefail
for i in $(seq 1 30); do
agent_status=$(curl -sS "https://api.cursor.com/v0/agents/$AGENT_ID" \
-H "Authorization: Bearer $CURSOR_API_KEY" | jq -r '.status')
echo "[$i/30] status: $agent_status"
if [[ "$agent_status" != "RUNNING" && "$agent_status" != "CREATING" ]]; then
echo "final_status=$agent_status" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 30
done
echo "final_status=TIMEOUT" >> "$GITHUB_OUTPUT"

- name: Post agent report as PR comment + update labels
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
AGENT_ID: ${{ steps.trigger.outputs.agent_id }}
AGENT_STATUS: ${{ steps.wait.outputs.final_status }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
REPORT=$(curl -sS "https://api.cursor.com/v0/agents/$AGENT_ID/conversation" \
-H "Authorization: Bearer $CURSOR_API_KEY" \
| jq -r '[.messages[] | select(.type == "assistant_message") | .text] | last')

{
echo "## QA report (Cursor QA agent — IaC)"
echo ""
echo "**Agent run:** [\`$AGENT_ID\`](https://cursor.com/agents/$AGENT_ID)"
echo "**Status:** \`$AGENT_STATUS\`"
echo ""
echo "$REPORT"
} > /tmp/qa-comment.md
gh pr comment "$PR" --repo "$REPO" --body-file /tmp/qa-comment.md

# Outcome detection: if the report indicates success, flip labels
if [[ "$AGENT_STATUS" == "FINISHED" ]] && \
echo "$REPORT" | grep -qiE "all (checks )?(✅|green|passed)"; then
gh pr edit "$PR" --repo "$REPO" --remove-label needs-qa --add-label qa-passed
echo "Labels: needs-qa → qa-passed"
else
gh pr edit "$PR" --repo "$REPO" --add-label needs-human || true
echo "Labels: added needs-human (status=$AGENT_STATUS, not all-green)"
exit 1
fi
Loading
Loading