guide for the X mcp server #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Guide draft | |
| on: | |
| issues: | |
| types: [labeled] | |
| jobs: | |
| draft: | |
| if: github.event.label.name == 'guide:draft' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 180 | |
| concurrency: | |
| group: guide-draft-issue-${{ github.event.issue.number }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| ISSUE_NUMBER: ${{ github.event.issue.number }} | |
| ISSUE_TITLE: ${{ github.event.issue.title }} | |
| ISSUE_BODY: ${{ github.event.issue.body }} | |
| GH_TOKEN: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| steps: | |
| - name: Ensure factory labels | |
| run: | | |
| set -euo pipefail | |
| # Repo labels must exist before gh issue edit --add-label (missing label = hard fail). | |
| ensure_label() { | |
| local name="$1" color="$2" desc="$3" | |
| if gh label list --limit 100 --json name --jq '.[].name' | grep -Fxq "$name"; then | |
| return 0 | |
| fi | |
| gh label create "$name" --color "$color" --description "$desc" | |
| } | |
| ensure_label "guide:draft" "1D76DB" "Trigger guide draft factory" | |
| ensure_label "guide:in-progress" "FBCA04" "Guide draft factory running" | |
| ensure_label "guide:blocked" "D73A4A" "Guide draft factory blocked" | |
| - name: Preflight existing factory PR | |
| id: preflight | |
| run: | | |
| set -euo pipefail | |
| # Resume from an open factory PR for this issue (guide/issue-N-*) instead | |
| # of refusing — clarifications should iterate on prior research/setup. | |
| existing=$(gh pr list \ | |
| --state open \ | |
| --search "in:body \"#${ISSUE_NUMBER}\"" \ | |
| --json number,url,body,author,headRefName \ | |
| --jq "[.[] | select(.body | test(\"(?i)(closes|fixes|resolves)\\\\s+#${ISSUE_NUMBER}\\\\b\"))]") | |
| count=$(echo "$existing" | jq 'length') | |
| resume=false | |
| resume_pr_url="" | |
| resume_pr_number="" | |
| resume_branch="" | |
| refused=false | |
| refused_pr_url="" | |
| for i in $(seq 0 $((count - 1))); do | |
| author=$(echo "$existing" | jq -r ".[$i].author.login") | |
| head=$(echo "$existing" | jq -r ".[$i].headRefName") | |
| url=$(echo "$existing" | jq -r ".[$i].url") | |
| num=$(echo "$existing" | jq -r ".[$i].number") | |
| if ! gh api "repos/${GH_REPO}/collaborators/${author}" --silent 2>/dev/null; then | |
| continue | |
| fi | |
| if [[ "$head" == guide/issue-${ISSUE_NUMBER}-* ]]; then | |
| resume=true | |
| resume_pr_url="$url" | |
| resume_pr_number="$num" | |
| resume_branch="$head" | |
| break | |
| fi | |
| # Non-factory collaborator PR still blocks (avoid stomping human work). | |
| refused=true | |
| refused_pr_url="$url" | |
| break | |
| done | |
| echo "refused=$refused" >> "$GITHUB_OUTPUT" | |
| echo "refused_pr_url=$refused_pr_url" >> "$GITHUB_OUTPUT" | |
| echo "resume=$resume" >> "$GITHUB_OUTPUT" | |
| echo "resume_pr_url=$resume_pr_url" >> "$GITHUB_OUTPUT" | |
| echo "resume_pr_number=$resume_pr_number" >> "$GITHUB_OUTPUT" | |
| echo "resume_branch=$resume_branch" >> "$GITHUB_OUTPUT" | |
| - name: Refuse non-factory PR | |
| if: steps.preflight.outputs.refused == 'true' | |
| env: | |
| REFUSED_PR_URL: ${{ steps.preflight.outputs.refused_pr_url }} | |
| run: | | |
| gh issue edit "$ISSUE_NUMBER" --remove-label "guide:draft" || true | |
| gh issue edit "$ISSUE_NUMBER" --add-label "guide:blocked" || true | |
| gh issue comment "$ISSUE_NUMBER" --body "Refused to run: $REFUSED_PR_URL already targets this issue and is not a factory branch (\`guide/issue-${ISSUE_NUMBER}-*\`). Close it or finish that PR first, then re-add \`guide:draft\`." | |
| - name: Transition labels | |
| if: steps.preflight.outputs.refused != 'true' | |
| run: | | |
| gh issue edit "$ISSUE_NUMBER" --remove-label "guide:draft" || true | |
| gh issue edit "$ISSUE_NUMBER" --remove-label "guide:blocked" || true | |
| gh issue edit "$ISSUE_NUMBER" --add-label "guide:in-progress" | |
| - name: Checkout main (first run) | |
| if: steps.preflight.outputs.refused != 'true' && steps.preflight.outputs.resume != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| token: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }} | |
| - name: Checkout factory branch (resume) | |
| if: steps.preflight.outputs.refused != 'true' && steps.preflight.outputs.resume == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.preflight.outputs.resume_branch }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.AGENT_PAT || secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| if: steps.preflight.outputs.refused != 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: scripts/cursor-sdk/package-lock.json | |
| - name: Install dependencies | |
| if: steps.preflight.outputs.refused != 'true' | |
| working-directory: scripts/cursor-sdk | |
| run: npm ci | |
| - name: Distill issue intent | |
| id: distill | |
| if: steps.preflight.outputs.refused != 'true' | |
| working-directory: scripts/cursor-sdk | |
| env: | |
| CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${CURSOR_API_KEY:-}" ]; then | |
| echo "CURSOR_API_KEY secret is not set" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| # Fold issue comments into distill context so clarifications on the | |
| # ticket (not only the body) reach --notes on retry. | |
| comments=$(gh api "repos/${GH_REPO}/issues/${ISSUE_NUMBER}/comments" \ | |
| --jq '[.[].body] | join("\n\n---\n\n")' \ | |
| 2>/dev/null || echo "") | |
| if [ -n "${comments}" ]; then | |
| # Keep every line of this assignment indented — bare "## ..." at | |
| # column 0 ends the YAML `run: |` block and breaks the workflow. | |
| { | |
| printf '%s\n\n' "${ISSUE_BODY:-}" | |
| echo "## Issue thread (for clarifications)" | |
| printf '%s\n' "${comments}" | |
| } > "${RUNNER_TEMP}/issue-body-with-thread.txt" | |
| export ISSUE_BODY="$(cat "${RUNNER_TEMP}/issue-body-with-thread.txt")" | |
| fi | |
| set +e | |
| npm run resolve-issue -- --output "${RUNNER_TEMP}/resolved.json" | |
| code=$? | |
| set -e | |
| if [ ! -f "${RUNNER_TEMP}/resolved.json" ]; then | |
| echo "resolve-issue produced no resolved.json (exit ${code})" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| status=$(jq -r '.status' "${RUNNER_TEMP}/resolved.json") | |
| if [ "$status" = "ok" ]; then | |
| slug=$(jq -r '.slug' "${RUNNER_TEMP}/resolved.json") | |
| provider=$(jq -r '.provider' "${RUNNER_TEMP}/resolved.json") | |
| persona=$(jq -r '.persona // "it-admin"' "${RUNNER_TEMP}/resolved.json") | |
| notes=$(jq -r '.notes // ""' "${RUNNER_TEMP}/resolved.json") | |
| echo "slug=$slug" >> "$GITHUB_OUTPUT" | |
| echo "provider=$provider" >> "$GITHUB_OUTPUT" | |
| echo "persona=$persona" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "notes<<EOF" | |
| echo "$notes" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$status" = "needs_clarification" ]; then | |
| reason=$(jq -r '.reason' "${RUNNER_TEMP}/resolved.json") | |
| candidates=$(jq -r '(.candidates // []) | join(", ")' "${RUNNER_TEMP}/resolved.json") | |
| { | |
| echo "Distill needs clarification before drafting." | |
| echo | |
| echo "**Reason:** ${reason}" | |
| if [ -n "$candidates" ]; then | |
| echo | |
| echo "**Candidates:** ${candidates}" | |
| fi | |
| echo | |
| echo "Reply on this issue (or edit the body) clarifying which MCP server, then re-add \`guide:draft\`." | |
| } > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| echo "Unexpected distill status: ${status}" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| - name: Comment resolved intent | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| PROVIDER: ${{ steps.distill.outputs.provider }} | |
| PERSONA: ${{ steps.distill.outputs.persona }} | |
| NOTES: ${{ steps.distill.outputs.notes }} | |
| RESUME: ${{ steps.preflight.outputs.resume }} | |
| RESUME_PR_URL: ${{ steps.preflight.outputs.resume_pr_url }} | |
| run: | | |
| set -euo pipefail | |
| body_file="${RUNNER_TEMP}/resolved-comment.md" | |
| { | |
| if [ "${RESUME}" = "true" ]; then | |
| echo "Resuming on existing factory PR: ${RESUME_PR_URL}" | |
| echo | |
| echo "Resolved as \`${SLUG}\` (${PROVIDER}), persona \`${PERSONA}\`." | |
| echo "Prior \`guides/${SLUG}/\` stays on the branch — research/draft will revise from those artifacts (lock skips when inputs match)." | |
| else | |
| echo "Resolved as \`${SLUG}\` (${PROVIDER}), persona \`${PERSONA}\`." | |
| fi | |
| if [ -n "${NOTES}" ]; then | |
| echo | |
| echo "Notes handed to the pipeline:" | |
| echo | |
| echo "> ${NOTES}" | |
| fi | |
| echo | |
| echo "Starting \`draft-guide\`…" | |
| } > "$body_file" | |
| gh issue comment "$ISSUE_NUMBER" --body-file "$body_file" | |
| - name: Create branch | |
| id: branch | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| RESUME: ${{ steps.preflight.outputs.resume }} | |
| RESUME_BRANCH: ${{ steps.preflight.outputs.resume_branch }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "guide-factory[bot]" | |
| git config user.email "guide-factory[bot]@users.noreply.github.com" | |
| if [ "${RESUME}" = "true" ]; then | |
| echo "name=${RESUME_BRANCH}" >> "$GITHUB_OUTPUT" | |
| echo "Resuming on ${RESUME_BRANCH}" | |
| exit 0 | |
| fi | |
| branch="guide/issue-${ISSUE_NUMBER}-${SLUG}" | |
| echo "name=$branch" >> "$GITHUB_OUTPUT" | |
| git checkout -b "$branch" | |
| - name: Draft guide | |
| id: draft | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| working-directory: scripts/cursor-sdk | |
| env: | |
| CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| PERSONA: ${{ steps.distill.outputs.persona }} | |
| NOTES: ${{ steps.distill.outputs.notes }} | |
| run: | | |
| set -euo pipefail | |
| args=(--overwrite) | |
| if [ -n "${PERSONA}" ] && [ "${PERSONA}" != "it-admin" ]; then | |
| args+=(--persona "${PERSONA}") | |
| fi | |
| if [ -n "${NOTES}" ]; then | |
| args+=(--notes "${NOTES}") | |
| fi | |
| set +e | |
| npm run draft-guide -- "${SLUG}" "${args[@]}" | |
| code=$? | |
| set -e | |
| # Prefer the newest run record for this slug (written even on unconverged). | |
| record="" | |
| if compgen -G "${GITHUB_WORKSPACE}/retro/runs/*-${SLUG}.json" > /dev/null; then | |
| record=$(ls -t "${GITHUB_WORKSPACE}"/retro/runs/*-"${SLUG}".json | head -n1) | |
| echo "record=${record}" >> "$GITHUB_OUTPUT" | |
| cp "$record" "${RUNNER_TEMP}/run-record.json" | |
| fi | |
| # CLI: 0 = converged, 2 = unconverged/blocked/failed guide status, | |
| # 1 = hard failure. Exit 2 still wrote files — open a PR for humans. | |
| if [ "$code" -eq 0 ]; then | |
| echo "outcome=converged" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$code" -eq 2 ]; then | |
| if [ -d "${GITHUB_WORKSPACE}/guides/${SLUG}" ]; then | |
| echo "outcome=unconverged" >> "$GITHUB_OUTPUT" | |
| echo "draft-guide exited 2 (unconverged/blocked/failed). Opening a draft PR with whatever was written for human review." > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 0 | |
| fi | |
| echo "draft-guide exited 2 and guides/${SLUG}/ is missing" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| echo "draft-guide exited ${code} (hard failure; see workflow logs)" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit "$code" | |
| - name: Commit and push | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| BRANCH: ${{ steps.branch.outputs.name }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| set -euo pipefail | |
| git add "guides/${SLUG}/" || true | |
| # Matching run records for this slug (any timestamp prefix). | |
| if compgen -G "retro/runs/*-${SLUG}.json" > /dev/null; then | |
| git add retro/runs/*-"${SLUG}".json | |
| fi | |
| if git diff --cached --quiet; then | |
| echo "No guide or run-record changes to commit" > "${RUNNER_TEMP}/failure_reason.txt" | |
| exit 1 | |
| fi | |
| git commit -m "Draft guide: ${SLUG} (issue #${ISSUE_NUMBER})" | |
| # force-with-lease keeps resume pushes from clobbering unexpected remote edits | |
| git push --force-with-lease origin "$BRANCH" | |
| - name: Open or update draft PR | |
| id: open_pr | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| BRANCH: ${{ steps.branch.outputs.name }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| PROVIDER: ${{ steps.distill.outputs.provider }} | |
| OUTCOME: ${{ steps.draft.outputs.outcome }} | |
| RESUME: ${{ steps.preflight.outputs.resume }} | |
| RESUME_PR_NUMBER: ${{ steps.preflight.outputs.resume_pr_number }} | |
| RESUME_PR_URL: ${{ steps.preflight.outputs.resume_pr_url }} | |
| run: | | |
| set -euo pipefail | |
| title="Draft guide: ${PROVIDER} (#${ISSUE_NUMBER})" | |
| if [ "${OUTCOME}" = "unconverged" ]; then | |
| title="Draft guide (unconverged): ${PROVIDER} (#${ISSUE_NUMBER})" | |
| fi | |
| title="${title:0:256}" | |
| body_file="${RUNNER_TEMP}/pr-body.md" | |
| { | |
| echo "Closes #${ISSUE_NUMBER}" | |
| echo | |
| echo "Factory draft of \`guides/${SLUG}/\` via \`draft-guide\` (Cursor SDK)." | |
| echo | |
| if [ "${OUTCOME}" = "unconverged" ]; then | |
| echo "**Pipeline status:** unconverged (reviewers still had blockers after max rounds)." | |
| echo "See the issue comment **Pipeline review** for unresolved blockers and open questions." | |
| echo "Do not merge until those are settled." | |
| echo | |
| fi | |
| if [ "${RESUME}" = "true" ]; then | |
| echo "_Updated by a factory re-run (resume) — prior research/setup on this branch were reused where the lock allowed._" | |
| echo | |
| fi | |
| echo "Human review still required — agents never commit; this Action did." | |
| } > "$body_file" | |
| if [ -f "${RUNNER_TEMP}/run-record.json" ]; then | |
| echo >> "$body_file" | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "${RUNNER_TEMP}/run-record.json" "" \ | |
| "${GITHUB_WORKSPACE}/guides/${SLUG}" >> "$body_file" | |
| fi | |
| if [ "${RESUME}" = "true" ] && [ -n "${RESUME_PR_NUMBER}" ]; then | |
| gh pr edit "${RESUME_PR_NUMBER}" --title "$title" --body-file "$body_file" | |
| echo "pr_url=${RESUME_PR_URL}" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| pr_url=$(gh pr create --draft --base main --head "$BRANCH" --title "$title" --body-file "$body_file" | tail -n1) | |
| echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT" | |
| - name: Comment pipeline review on issue | |
| if: steps.preflight.outputs.refused != 'true' && success() | |
| env: | |
| PR_URL: ${{ steps.open_pr.outputs.pr_url }} | |
| OUTCOME: ${{ steps.draft.outputs.outcome }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| set -euo pipefail | |
| body_file="${RUNNER_TEMP}/pipeline-review-comment.md" | |
| if [ -f "${RUNNER_TEMP}/run-record.json" ]; then | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "${RUNNER_TEMP}/run-record.json" "${PR_URL}" \ | |
| "${GITHUB_WORKSPACE}/guides/${SLUG}" > "$body_file" | |
| else | |
| { | |
| echo "## Pipeline review" | |
| echo | |
| if [ "${OUTCOME}" = "unconverged" ]; then | |
| echo "Draft PR opened (pipeline **unconverged**): ${PR_URL}" | |
| else | |
| echo "Draft PR opened: ${PR_URL}" | |
| fi | |
| echo | |
| echo "_No run record found to summarize blockers / open questions._" | |
| } > "$body_file" | |
| fi | |
| gh issue comment "$ISSUE_NUMBER" --body-file "$body_file" | |
| - name: Mark blocked on failure | |
| if: steps.preflight.outputs.refused != 'true' && failure() | |
| env: | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| SLUG: ${{ steps.distill.outputs.slug }} | |
| run: | | |
| reason="(no reason file written; check workflow logs)" | |
| if [ -f "${RUNNER_TEMP}/failure_reason.txt" ]; then | |
| reason=$(cat "${RUNNER_TEMP}/failure_reason.txt") | |
| fi | |
| body_file="${RUNNER_TEMP}/failure-comment.md" | |
| { | |
| echo "\`guide:draft\` run failed (no PR opened)." | |
| echo | |
| echo "$reason" | |
| echo | |
| echo "**Workflow run:** ${RUN_URL}" | |
| echo | |
| } > "$body_file" | |
| # If a run record exists from a partial draft, surface blockers here too. | |
| if [ -f "${RUNNER_TEMP}/run-record.json" ]; then | |
| echo >> "$body_file" | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "${RUNNER_TEMP}/run-record.json" "" \ | |
| "${GITHUB_WORKSPACE}/guides/${SLUG}" >> "$body_file" || true | |
| elif [ -n "${SLUG}" ] && compgen -G "${GITHUB_WORKSPACE}/retro/runs/*-${SLUG}.json" > /dev/null; then | |
| record=$(ls -t "${GITHUB_WORKSPACE}"/retro/runs/*-"${SLUG}".json | head -n1) | |
| echo >> "$body_file" | |
| bash "${GITHUB_WORKSPACE}/scripts/ci/format-pipeline-review.sh" \ | |
| "$record" "" "${GITHUB_WORKSPACE}/guides/${SLUG}" >> "$body_file" || true | |
| else | |
| { | |
| echo "Reply on this issue with clarifications, then re-add \`guide:draft\`." | |
| } >> "$body_file" | |
| fi | |
| gh issue edit "$ISSUE_NUMBER" --add-label "guide:blocked" || true | |
| gh issue comment "$ISSUE_NUMBER" --body-file "$body_file" | |
| - name: Always remove in-progress | |
| if: always() && steps.preflight.outputs.refused != 'true' | |
| run: gh issue edit "$ISSUE_NUMBER" --remove-label "guide:in-progress" || true |