diff --git a/autopilot/docs/init-templates/autopilot-executor.yml.template b/autopilot/docs/init-templates/autopilot-executor.yml.template index b03b1a0..6ec7e88 100644 --- a/autopilot/docs/init-templates/autopilot-executor.yml.template +++ b/autopilot/docs/init-templates/autopilot-executor.yml.template @@ -181,6 +181,7 @@ jobs: contents: write pull-requests: write issues: write + id-token: write # claude-code-action requires OIDC steps: # Executor-opened PRs must trigger the gates workflow, and the default # GITHUB_TOKEN never starts a workflow run from its own pushes/PRs — a @@ -213,9 +214,21 @@ jobs: GITHUB_TOKEN: ${{ secrets.AUTOPILOT_PAT }} with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Use the PAT as the action's own token too — otherwise it mints a + # claude[bot] app token, the PR lands authored by a bot, and the + # gate actions (which block non-human actors by default) refuse to run. + github_token: ${{ secrets.AUTOPILOT_PAT }} + # The action does not read .claude/settings.json — the autopilot + # action library must be installed explicitly. + plugin_marketplaces: "https://github.com/pcamarajr/content-stack.git" + plugins: | + content-ops@content-stack + astro-builder@content-stack + content-seo@content-stack claude_args: | + --model claude-sonnet-5 --max-turns ${{ needs.guard.outputs.max_turns }} - --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task,WebSearch,WebFetch" + --allowedTools "Bash,Read,Write,Edit,Glob,Grep,Task,Skill,WebSearch,WebFetch" prompt: | You are the autopilot executor. You implement exactly one task issue on this repo, open a PR for it, and stop — you never merge, and you @@ -237,7 +250,11 @@ jobs: 2. Create and switch to a branch named exactly `autopilot/task-${{ github.event.issue.number }}` before making any change. - 3. Do the work the skill produces, then commit it. + 3. Do the work the skill produces, then self-review before + committing: run /content-ops:review-content on every content + file you wrote and fix all Must Fix findings yourself — the + anti-slop gate applies the same standard and will reject the + PR otherwise. Then commit. 4. Push the branch and open a pull request against `{{DEFAULT_BRANCH}}` that: - has the title `autopilot: `, @@ -283,18 +300,26 @@ jobs: ISSUE: ${{ github.event.issue.number }} run: | MARKER="" + # Only count failures since the last time a human unblocked the + # task (removed autopilot:blocked) — otherwise stale markers from a + # long-fixed failure eat the retry budget forever. + LAST_UNBLOCK=$(gh api "repos/${{ github.repository }}/issues/$ISSUE/timeline" --paginate \ + --jq '[.[] | select(.event=="unlabeled" and .label.name=="autopilot:blocked") | .created_at] | max // ""') COUNT=$(gh issue view "$ISSUE" --json comments \ - --jq "[.comments[].body | select(startswith(\"$MARKER\"))] | length") + --jq "[.comments[] | select(.body | startswith(\"$MARKER\")) | select(.createdAt > \"$LAST_UNBLOCK\")] | length") MAX_RETRIES=$(yq '.limits.retries' .autopilot/config.yml) { echo "count=$COUNT" echo "max_retries=$MAX_RETRIES" } >> "$GITHUB_OUTPUT" + # The re-added label below must come from the PAT: a `labeled` event + # produced by the default GITHUB_TOKEN never triggers this workflow + # again, so a GITHUB_TOKEN retry would silently retry nothing. - name: Retry if: fromJSON(steps.attempts.outputs.count) < fromJSON(steps.attempts.outputs.max_retries) env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.AUTOPILOT_PAT }} ISSUE: ${{ github.event.issue.number }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | diff --git a/autopilot/docs/init-templates/autopilot-gates.yml.template b/autopilot/docs/init-templates/autopilot-gates.yml.template index b0141a9..708e863 100644 --- a/autopilot/docs/init-templates/autopilot-gates.yml.template +++ b/autopilot/docs/init-templates/autopilot-gates.yml.template @@ -27,7 +27,9 @@ jobs: - name: Reject changes to the autopilot control plane run: | - git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 + # fetch-depth: 0 above already has full history for every ref; a + # shallow re-fetch here would clobber origin/ and lose the + # merge base once the base branch advances. CHANGED=$(git diff --name-only "origin/${{ github.event.pull_request.base.ref }}...HEAD") PROTECTED=$(echo "$CHANGED" | grep -E '^(\.autopilot/|\.github/)' || true) @@ -46,6 +48,15 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + # Makes pnpm/yarn available per the repo's packageManager field; no-op for npm. + - name: Enable corepack + run: corepack enable + - name: Install dependencies run: "{{PACKAGE_MANAGER_INSTALL}}" @@ -55,8 +66,11 @@ jobs: audit: if: startsWith(github.head_ref, 'autopilot/task-') runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: read + pull-requests: write + id-token: write # claude-code-action requires OIDC steps: - name: Checkout uses: actions/checkout@v5 @@ -65,9 +79,15 @@ jobs: uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Task PRs may be authored/synchronized by a bot identity. + allowed_bots: "claude" + plugin_marketplaces: "https://github.com/pcamarajr/content-stack.git" + plugins: | + astro-builder@content-stack claude_args: | - --max-turns 20 - --allowedTools "Bash,Read,Grep,Glob,Task" + --model claude-sonnet-5 + --max-turns 40 + --allowedTools "Bash,Read,Grep,Glob,Task,Skill" prompt: | Run /astro-builder:audit against this checkout. Never auto-fix anything — you are a read-only reviewer here, the repo is not @@ -79,8 +99,10 @@ jobs: echo "FAIL" > /tmp/audit-result.txt (if any P0 issue was found) echo "PASS" > /tmp/audit-result.txt (otherwise) - Then, on the line above that command in your final message, list - every P0 issue found (or write "none"). + Before writing that file, post your findings so humans can see + them: run + gh pr comment ${{ github.event.pull_request.number }} --body "" + (GITHUB_TOKEN is already in your environment.) - name: Enforce audit result run: | @@ -93,8 +115,11 @@ jobs: anti-slop: if: startsWith(github.head_ref, 'autopilot/task-') runs-on: ubuntu-latest + timeout-minutes: 20 permissions: contents: read + pull-requests: write + id-token: write # claude-code-action requires OIDC steps: - name: Checkout uses: actions/checkout@v5 @@ -105,9 +130,15 @@ jobs: uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Task PRs may be authored/synchronized by a bot identity. + allowed_bots: "claude" + plugin_marketplaces: "https://github.com/pcamarajr/content-stack.git" + plugins: | + content-ops@content-stack claude_args: | - --max-turns 20 - --allowedTools "Bash,Read,Grep,Glob,Task" + --model claude-sonnet-5 + --max-turns 40 + --allowedTools "Bash,Read,Grep,Glob,Task,Skill" prompt: | Diff this PR's head against `origin/${{ github.event.pull_request.base.ref }}` to find the changed content files, then run @@ -121,8 +152,10 @@ jobs: echo "FAIL" > /tmp/anti-slop-result.txt (if any Must Fix was found) echo "PASS" > /tmp/anti-slop-result.txt (otherwise) - Then, on the line above that command in your final message, list - every Must Fix found (or write "none"). + Before writing that file, post your findings so humans can see + them: run + gh pr comment ${{ github.event.pull_request.number }} --body "" + (GITHUB_TOKEN is already in your environment.) - name: Enforce anti-slop result run: | @@ -132,6 +165,30 @@ jobs: exit 1 fi + # Red gates must not vanish into the Actions tab: tell the PR and the + # task issue what happened so a human (or the strategist) can triage. + report-failure: + needs: [path-guard, build, audit, anti-slop] + if: failure() && startsWith(github.head_ref, 'autopilot/task-') + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - name: Comment on PR and task issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + HEAD_REF: ${{ github.head_ref }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + gh pr comment "$PR" --body "Autopilot gates failed: $RUN_URL — see the gate comments above for findings." + TASK_NUM=$(echo "$HEAD_REF" | grep -oP 'autopilot/task-\K[0-9]+' || true) + if [ -n "$TASK_NUM" ]; then + gh issue comment "$TASK_NUM" --body "Gates failed on PR #$PR: $RUN_URL — needs triage or a revised attempt." + fi + auto-merge: needs: [path-guard, build, audit, anti-slop] if: success() && startsWith(github.head_ref, 'autopilot/task-') @@ -163,7 +220,14 @@ jobs: TASK_NUM=$(echo "$HEAD_REF" | grep -oP 'autopilot/task-\K[0-9]+' || true) if [ "$POLICY" = "auto" ]; then - gh pr merge --squash --auto "$PR" + # --auto requires the repo's allow_auto_merge setting, which + # private repos on the Free plan cannot enable. Gates are already + # green here (this job runs behind needs: [all gates]), so a + # direct squash merge is an equivalent fallback. + if ! gh pr merge --squash --auto "$PR"; then + echo "auto-merge unavailable on this repo — merging directly (gates are green)." + gh pr merge --squash "$PR" + fi if [ -n "$TASK_NUM" ]; then gh issue edit "$TASK_NUM" --add-label "autopilot:done" gh issue close "$TASK_NUM" --reason completed diff --git a/autopilot/docs/init-templates/autopilot-metrics.yml.template b/autopilot/docs/init-templates/autopilot-metrics.yml.template index f28df78..9f82e3d 100644 --- a/autopilot/docs/init-templates/autopilot-metrics.yml.template +++ b/autopilot/docs/init-templates/autopilot-metrics.yml.template @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write # claude-code-action requires OIDC steps: - name: Checkout uses: actions/checkout@v5 @@ -60,7 +61,11 @@ jobs: GOOGLE_APPLICATION_CREDENTIALS: ${{ runner.temp }}/gsc-service-account.json with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + plugin_marketplaces: "https://github.com/pcamarajr/content-stack.git" + plugins: | + content-seo@content-stack claude_args: | + --model claude-sonnet-5 --max-turns 15 --allowedTools "Bash,Read,Write,Task" prompt: | diff --git a/autopilot/skills/init/SKILL.md b/autopilot/skills/init/SKILL.md index e0b6ac7..58abf14 100644 --- a/autopilot/skills/init/SKILL.md +++ b/autopilot/skills/init/SKILL.md @@ -159,9 +159,12 @@ returns 200 without actually flipping the setting, so never trust the status cod gh api -X PATCH repos/{owner}/{repo} -f allow_auto_merge=true gh api repos/{owner}/{repo} --jq .allow_auto_merge ``` -The second command must print `true`. This requires admin on the repo. If it 403s OR -still reads `false` after the PATCH, don't stop the run — print it as a manual -follow-up instead (add it to the Step 5 checklist): +The second command must print `true`. This requires admin on the repo. Note: private +repos on the GitHub Free plan cannot enable this setting at all (the PATCH silently +no-ops) — the gates workflow handles it by falling back to a direct squash merge once +its own gates are green, so this is informational, not blocking. If it 403s OR still +reads `false` after the PATCH, don't stop the run — print it as a manual follow-up +instead (add it to the Step 5 checklist): ```text [ ] Enable "Allow auto-merge" in repo Settings → General (requires admin) — needed for the gates workflow's `gh pr merge --squash --auto` to succeed. @@ -303,6 +306,9 @@ Before the loop can run: -F restrictions=null \ -F allow_force_pushes=false \ -F allow_deletions=false + [ ] Baseline audit: run /astro-builder:audit on the repo and clear every P0 BEFORE + activating the loop — the audit gate reviews the whole repo, so any pre-existing + P0 blocks every task PR, no matter how clean the PR itself is [ ] Write and pin the first intention issue (goal / metric / horizon / constraints) [ ] Apply intention:approved to that issue — only a maintainer applies this label @@ -319,5 +325,5 @@ is the one action that turns proposals into work, and this skill never does it f - Never overwrite an existing file without asking first. Identical content is skipped silently; different content stops and asks (keep / overwrite / show diff). - Never create or approve an intention issue. This skill scaffolds infrastructure only — it does not author intentions and it never applies `intention:approved`. - The executor's machine identity (PAT or GitHub App) must never be granted permission to apply `intention:approved`. That label activates work and is reserved for a human maintainer — do not add it to any token's scope, workflow `permissions:` block, or automation this skill writes. -- Gate enforcement depends on branch protection; without it (see the Step 5 checklist) the path-guard/build/audit/anti-slop boundary is advisory, not enforced — anyone with push access can bypass it. +- Gate enforcement depends on branch protection; without it (see the Step 5 checklist) the path-guard/build/audit/anti-slop boundary is advisory, not enforced — anyone with push access can bypass it. Private repos on the GitHub Free plan cannot enable branch protection at all: acceptable for a single-maintainer private repo where the only writers are the maintainer and the executor PAT, but revisit before adding collaborators or going public. - Re-running this skill must be safe: always detect what already exists (Step 0) before creating anything, and always report what was skipped, not just what was created.