Every page lists the code it describes in its sources: frontmatter. When code changes, php artisan larecipe:affected maps the changed files to the pages that describe them, with no AI and no network. The coding agent that made the change then updates those pages before it pushes, so the docs land in the same pull request as the code.
You do not run any of this by hand. The installer sets it up, and your agent follows it.
Change the code
↓
Your agent updates the affected pages in the same pull request
↓
You review them with the code (or let them through, in auto mode)
↓
Every reader, human or agent, gets the current answerDeclare what a page documents
List the code a page documents as paths or globs from the project root:
---
title: Webhooks
description: Receive signed events from the app and verify them.
sources:
- app/Webhooks/**
- routes/webhooks.php
---A page without sources: is invisible to drift detection. larecipe:sources:suggest proposes them from what the page already mentions.
Glob syntax and regions
*stays inside one folder,**crosses folders,?is one character,{a,b}picks one of several and[abc]is a character class: the dialect of.gitignoreand GitHub Actions.- A path without wildcards matches that file, or everything inside it when it names a folder.
app/Billingandapp/Billing/**are the same. path#regionnarrows a source to the lines between#region webhooksand the next#endregionin that file, behind any comment prefix (// #region webhooks). Write it asroutes/api.php#webhooks. The page is affected only when a change touches those lines. When LaRecipe cannot find the region or the changed lines, the whole file counts: a false "stale" costs a look, a false "fine" costs a wrong page.- Changes inside the docs folder never count as code changes for other pages.
Docs ship with the code
Docs ship in the same push as the code they describe: as Markdown files for a repository set, as a docs migration for a database set. There is no separate docs pull request and no AI agent running in CI.
The reason is context. The agent that changed the code knows exactly what changed and why, right now. In an hour, or in a follow-up pull request written by someone else, that context is gone.
The layers
Four layers, each optional above the first. All of them ask the same question of the same code, so they never disagree about which pages are stale.
| Layer | What it is | When it runs | Writes |
|---|---|---|---|
| L0, the deterministic core | sources: frontmatter, larecipe:affected, larecipe:validate |
Always on | Nothing |
| L1, instructions in the repo | The LaRecipe block in AGENTS.md (or the Laravel Boost guideline), the four skills, the local MCP server |
At the start of every AI coding session | The agent writes the docs |
| L2, the pre-push check | An agent hook on git push and gh pr create that holds the push once and lists the stale pages; an optional git hook for people |
When a change is about to leave the machine | Nothing |
| L3, the optional CI check | larecipe:validate and larecipe:affected on each pull request |
On pull requests | Nothing |
- L1 is plain text every AI session reads. See agent instructions and skills.
- L2 catches the moment an agent forgets, and never calls an LLM. See the pre-push hook.
- L3 is a safety net for pushes made without an agent. See the CI check.
The agent writes the docs because it has the context. Nothing downstream writes them for it. To have an agent draft an update without a local session, run larecipe:sync on demand.
Recommended setups
| Team | Turn on | Install with |
|---|---|---|
| Solo developer with Claude Code or Codex | L0, L1, L2 (agent hook) | php artisan larecipe:install |
| Team with pull request review | L0, L1, L2 (agent hook, plus the git hook for people who push without an agent), L3 in comment or fail mode | php artisan larecipe:install --git-hook=warn --ci=comment |
| No local coding agents | L0, L2 (git hook), L3; larecipe:sync on demand when someone wants an agent to draft the update |
php artisan larecipe:install --agents=none --git-hook=warn --ci=fail |
| Database set | L0, L1, L2 (the local MCP server writes docs migrations into the same push), L3 | php artisan larecipe:install --driver=database --ci=fail |
Every flag is explained in installation. Add a layer later by running the installer again, or the command for that layer.
larecipe:affected
larecipe:affected answers one question: which docs pages does this change touch, and did the change update them? This is what your agent runs before it pushes:
php artisan larecipe:affected --uncommitted --base=mainLaRecipe: 4 files changed since main, plus uncommitted work.
Stale (the code changed, the page did not):
- docs/billing/webhooks.md (sources: app/Webhooks/**)
Updated in this change:
- docs/billing/refunds.md (sources: app/Billing/Refund.php)
Drifted earlier (what they document changed after they were last updated):
- docs/installation.md its sources changed in 3f2a9c1 (2026-10-02): Require PHP 8.4
Update the stale pages with the larecipe-update-docs skill, then run php artisan larecipe:validate.- Stale: a changed file matches the page's
sources:, and the page itself did not change in the same range. These are the pages to update. - Updated: the page changed in the same range as its code: its Markdown file, or, for a database set, a changed docs migration that names it.
- Drifted earlier: pages this change does not touch, whose sources changed in a commit after the page was last updated. LaRecipe names that commit, so you can see when the page fell behind.
--no-driftskips this part.
The same check is the affected_pages tool on the local MCP server.
Choose the range
| You run | It compares |
|---|---|
larecipe:affected |
Uncommitted work only: staged, unstaged and untracked files. |
larecipe:affected --base=main |
The commits on this branch since it left main (main...HEAD), without uncommitted work. |
larecipe:affected --base=main --uncommitted |
Both. This is what your agent runs before it pushes. |
larecipe:affected --base=main --head=feature/refunds |
The commits on another branch, without checking it out. |
When the base is not a known branch or commit, the report says so and suggests git fetch origin. In CI the base is the pull request's target, --base=origin/${{ github.base_ref }}.
Formats: text, json, github and markdown
--format picks who reads the report:
text(the default): for people, as above.json: for programs and agents. Asummarywith counts,changed_files, each affected page with itsstatus(staleorupdated),reason,matched_sourcesandchanged_files, thedriftlist with each page'sstale_sincecommit, and anynotes.github: GitHub Actions annotations. Each stale page becomes a warning on its file (an error with--fail-on-stale), drift becomes a notice, and inside a GitHub Actions run the command also adds a Markdown report to the job summary and sets astalestep output.markdown: a pull request comment, with a table of stale pages, the sources that matched and the files that changed. It also sets thestalestep output.
An abridged json report (each page also carries its version, locale and full sources):
{
"summary": { "changed_files": 4, "affected": 2, "stale": 1, "updated": 1, "drift": 1 },
"pages": [
{
"set": "docs",
"path": "billing/webhooks",
"title": "Webhooks",
"file": "docs/billing/webhooks.md",
"status": "stale",
"reason": "sources",
"matched_sources": ["app/Webhooks/**"],
"changed_files": ["app/Webhooks/Handler.php"]
}
]
}--fail-on-stale makes the command exit with 1 when the change leaves any page stale. The CI check uses it in fail mode.
Sets, versions and translations
--set=handbookchecks one doc set instead of all of them.--docs-version=2.xchecks another version, and--docs-version=allchecks every version. By default each set's default version is checked.--locale=archecks a language's pages against the default language:
php artisan larecipe:affected --uncommitted --base=main --locale=arA translation is stale when the code it documents changed (a translation without sources: of its own uses its original's), or when its original page changed in the range and the translation did not. Under drift, it lists translations whose original changed in a commit after the translation was last updated. See languages.
Database sets
A database set's pages are rows, so larecipe:affected reads their sources: from the local database. "Updated" means a changed docs migration in the migrations folder names the page, and for drift, "last updated" is the page's own update time. When the local database has no pages, the report says so. Refresh it from a production export with larecipe:pull (--from takes a bundle made on production with larecipe:source:export). See docs migrations.
Every option
Read live from this site's command registry:
larecipe:affectedList the docs pages a change affects and the ones it left stale
php artisan larecipe:affected [--base [BASE]] [--head [HEAD]] [--uncommitted] [--set [SET]] [--docs-version [DOCS-VERSION]] [--locale [LOCALE]] [--format [FORMAT]] [--fail-on-stale] [--no-drift]| Option | Description | Default |
|---|---|---|
--base=BASE | Compare with this branch or commit (from where the branch left it) | |
--head=HEAD | The end of the range | HEAD |
--uncommitted | Include staged, unstaged and untracked changes | |
--set=SET | Only this doc set | |
--docs-version=DOCS-VERSION | A version id, or "all" (default: each set's default version) | |
--locale=LOCALE | Check this locale's translations against their source pages | |
--format=FORMAT | text, json, github or markdown | text |
--fail-on-stale | Exit with 1 when the change leaves pages stale | |
--no-drift | Skip pages that drifted before this change |
Find sources for pages that have none
larecipe:sources:suggest proposes sources: for pages that declare none, from the classes, commands and routes they mention. Add --apply to write them into the pages:
php artisan larecipe:sources:suggestWhat it reads, and its options
It looks for:
- fully qualified class names in inline code, such as
App\Billing\InvoiceorApp\Billing\Invoice::refund(); - artisan commands in inline code (
orders:prune,php artisan orders:prune) and in shell code blocks; - route names, as
route('orders.store')or a bareorders.store; - live references:
<Route name="...">,<Artisan command="...">,<Model name="...">(short names inApp\Models) and<Event name="...">(short names inApp\Events).
It asks the running app where each one is defined (Composer's class map, the console kernel, the router) and keeps only files inside your project, never vendor/. Without --apply it prints its suggestions:
docs/billing/refunds.md
sources: ["app/Console/Commands/PruneRefunds.php","app/Http/Controllers/RefundController.php"]
app/Console/Commands/PruneRefunds.php `refunds:prune`
app/Http/Controllers/RefundController.php <Route name="refunds.store" />--applyadds the suggestions to repository pages that declare no sources yet. It inserts onesources:line and leaves the rest of the frontmatter exactly as it was.--dry-runshows what it would change.--page=billing/refundslimits it to one page,--setto one doc set, and--allalso suggests for pages that already declare sources.--jsonprints the suggestions for an agent.- It reads each set's default version and language.
--applynever edits database pages; update theirsources:through the MCP author tools.
larecipe:sources:suggestSuggest sources: for docs pages from the code they mention
php artisan larecipe:sources:suggest [--set [SET]] [--page [PAGE]] [--all] [--apply] [--dry-run] [--json]| Option | Description | Default |
|---|---|---|
--set=SET | Only this doc set | |
--page=PAGE | Only this page path | |
--all | Also suggest for pages that already declare sources | |
--apply | Add the suggestions to pages that declare no sources yet | |
--dry-run | With --apply, show what would change without writing | |
--json | Print the suggestions as JSON |
Next
- Agent instructions: the
AGENTS.mdblock every AI session reads. - The pre-push hook: what happens when an agent pushes stale docs.
- The CI check: go deeper with a safety net for pushes made without an agent.