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
33 changes: 29 additions & 4 deletions autopilot/docs/init-templates/autopilot-executor.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: <short summary of the task>`,
Expand Down Expand Up @@ -283,18 +300,26 @@ jobs:
ISSUE: ${{ github.event.issue.number }}
run: |
MARKER="<!-- autopilot-executor-attempt-failed -->"
# 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: |
Expand Down
84 changes: 74 additions & 10 deletions autopilot/docs/init-templates/autopilot-gates.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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/<base> 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)

Expand All @@ -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}}"

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 "<the list of P0 issues found, or 'Audit gate: no P0 issues'>"
(GITHUB_TOKEN is already in your environment.)

- name: Enforce audit result
run: |
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 "<the list of Must Fix items found, or 'Anti-slop gate: no Must Fix items'>"
(GITHUB_TOKEN is already in your environment.)

- name: Enforce anti-slop result
run: |
Expand All @@ -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-')
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions autopilot/docs/init-templates/autopilot-metrics.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
14 changes: 10 additions & 4 deletions autopilot/skills/init/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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.