Skip to content

feat: add reusable workflow that keeps AGENTS.md accurate in the PR that changed it - #311

Open
FilipMasar wants to merge 8 commits into
mainfrom
feat/claude-md-pr-workflow
Open

feat: add reusable workflow that keeps AGENTS.md accurate in the PR that changed it#311
FilipMasar wants to merge 8 commits into
mainfrom
feat/claude-md-pr-workflow

Conversation

@FilipMasar

@FilipMasar FilipMasar commented Aug 17, 2026

Copy link
Copy Markdown

What

A new reusable workflow that keeps AGENTS.md accurate.

When a PR makes AGENTS.md wrong, a bot commits the fix to that PR's own branch and comments what it changed.

Why

We already have claude-md-maintenance.yml, which runs after merge and opens its own PR. Nobody merges those PRs, so the doc goes stale anyway. Fixing the doc in the PR that broke it means the change lands together with the code.

How it works

  1. Someone opens or updates a PR.
  2. A shell gate decides whether to run. It skips drafts, forks, bot commits, and PRs where the branch has already moved on. If the repo doesn't use the standard layout, the run fails and the error says how to fix it.
  3. Claude reads what the PR changed and compares it against the current AGENTS.md. It edits the file only if the PR made something in it wrong — a renamed script, a removed dependency, a new top-level directory.
  4. If the file changed, the bot commits it to the PR branch (signed) and posts a comment saying what it changed and why.
  5. If nothing was wrong, it does nothing. Its reasoning still goes into the run summary, so a quiet run is not a silent one.

The agent only reads and writes files. Committing and commenting are ordinary steps after it finishes, so when something breaks it's clear which part did it.

How to use it

Nothing happens until a repo opts in — this is workflow_call only. Onboarding is two steps.

1. The layout. Two regular files in the repo root, neither a symlink.

AGENTS.md — the whole doc
CLAUDE.md — first line is a pointer to AGENTS.md

@AGENTS.md

2. The workflow file.

name: Keep AGENTS.md accurate

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

permissions:
  contents: write
  pull-requests: write

jobs:
  update-agents-md:
    uses: apify/workflows/.github/workflows/agents-md-maintenance.yml@main
    secrets:
      ANTHROPIC_API_KEY: ${{ secrets.YOUR_ANTHROPIC_API_KEY }}

For our own repos, both steps are apify/integrations-team#113 — it has the per-repo migration commands, the stub and the script that distributes it.

Fixes apify/integrations-team#98

Keeps CLAUDE.md up to date inside the PR that made it wrong, instead of
opening a separate PR after merge that nobody merges.

Refs apify/integrations-team#98

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the t-integrations Issues with this label are in the ownership of the integrations team. label Aug 17, 2026
AGENTS.md now holds the doc and CLAUDE.md is a one-line `@AGENTS.md` pointer at it.
This is the right way round: claude-code-action deletes and re-checks-out its
SENSITIVE_PATHS from the base branch before the agent starts, and CLAUDE.md is on
that list while AGENTS.md is not. So the agent now reads the branch's own version of
the file it is judging.

The gate hard-fails a repo that isn't laid out this way, printing the fix, instead of
skipping with a warning. A skip is indistinguishable from "found nothing to fix",
which is how a repo silently stops being maintained.

Two things the old layout forced, both now gone:

- The base-branch blob comparison in prep. With AGENTS.md never reset, `git status`
  answers "did the agent change anything" directly.
- The per-commit API loop that bailed out when a human had edited the doc in the PR.
  It existed because the reset hid their edit from the agent, so committing reverted
  their work. The agent now sees it and edits on top, which is correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FilipMasar FilipMasar changed the title feat: add claude-md-pr reusable workflow feat: add reusable workflow that keeps AGENTS.md accurate in the PR that changed it Aug 17, 2026
FilipMasar and others added 6 commits August 17, 2026 14:48
The gate no longer lists the PR's changed files. Nothing judged that list any more,
and the agent's own `gh pr diff` is already in the log, so it was an API call per run
for a duplicate. The gate now needs no token and touches no network — every decision
it makes reads the checkout on disk.

fetch-depth drops to 1. Nothing reads a parent commit: the gate checks the tip's
author and the only comparison left is the working tree against HEAD.

The post-agent guard also refuses an empty AGENTS.md, not just a missing one. An
Edit/Write agent cannot unlink a file but can truncate one, and that would have been
committed as a "docs: update" that wipes the repo's doc.

Comments that described the previous version of this unmerged file are rewritten as
what a future reader needs: what to add back, and when.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A present-but-empty AGENTS.md passed the layout check. The agent is forbidden
to create the file, so it would have had no claims to check and would report
"nothing to do" on a repo with no doc at all — silence that means the opposite
of what it looks like. Same -s predicate as the post-agent guard already uses.

Found by lab/test-layout-gate.sh in apify/claude-md-lab, which covers 17 layout
shapes locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The layout error told the reader to run `printf '@AGENTS.md\n' > CLAUDE.md`.
Where CLAUDE.md is a symlink to AGENTS.md the redirect follows the link
and truncates the doc to that single line -- so the gate would fail the
run, hand back an instruction, and the instruction would destroy the file
it was meant to move. apify-core and apify/activepieces are both in
exactly that shape, and apify-hubspot-integration and
apify-integrations-backend are in the inverse one, with AGENTS.md as the
link.

The message now states the required end state, gives the command for the
common case, and names the two shapes where a shortcut loses work: a
symlink must be removed rather than written through, and two real files
have diverged and need a human merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-md-pr.yml -> agents-md-maintenance.yml. The doc has lived in AGENTS.md
since the reset problem in claude-code-action made CLAUDE.md unusable as the
source of truth, so the old name pointed at the pointer file rather than the
document.

Free to do now and not later: the workflow is not on main yet, so nothing pins
claude-md-pr.yml@main and the rename breaks no callers. The comment claiming a
rename would break every stub was written for the state after rollout; it now
says so explicitly, and warns off renaming once stubs exist.

claude-md-maintenance.yml keeps its name. It is the superseded post-merge
workflow that already exists in the target repos, and onboarding deletes it by
that exact filename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
135 comment lines out of 358 was more than this needed. Cuts the verbose ones
and drops two that said nothing the code or the error messages next to them did
not already say:

  - the gate's opening restatement of the required layout, which the two
    ::error:: echoes immediately below it state twice more
  - "the agent's reasoning goes to the run summary either way", which the step's
    own always() comment three lines up already explains

Everything load-bearing stays, because each of these was written down after
something went wrong: `'' == false` comparing equal on a payload-less event, %an
rather than %cn on API commits, check-ignore consulting the index, tr -d '\r' on
a CRLF checkout, concurrency on the job rather than the workflow, and why the doc
must live in AGENTS.md and not on claude-code-action's SENSITIVE_PATHS. The two
"deliberately nothing here" blocks stay for the same reason: they document code
that is absent on purpose, which is the only kind of decision a comment can
carry at all.

Comment-only. Verified by stripping comment lines from both versions and
diffing: every remaining line is byte-identical, and the agent prompt -- whose
markdown ## headings a naive comment filter would have eaten -- is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-integrations Issues with this label are in the ownership of the integrations team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants