---
title: "Docs Update Agent"
slug: docs-update-agent
section: Governance & Operations
status: stable
description: "The Docs Update Agent is a Cloud Run service in the `vertex-agents` repo that, after an approved pull request merges on a tracked repo, proposes minimal citation-backed edits to the matching Nexus documentation pages and applies them thr…"
last-updated: 2026-10-07
---

# Docs Update Agent

The Docs Update Agent is a Cloud Run service in the `vertex-agents` repo that, after an approved pull request merges on a tracked repo, proposes minimal citation-backed edits to the matching Nexus documentation pages and applies them through the gateway as auto-merging doc PRs. [1] [2]

## Overview

The agent keeps these Nexus pages in step with the code that they describe. It holds no GitHub credential of its own: it reads the PR, its reviews, changed files and diff through the Alliance MCP Gateway under a dedicated robot identity, and writes docs only through the gateway's `ecosystem_update_doc` tool or through branches and PRs on `nexus-docs`. [2] [3]

Every edit must be backed by verbatim quotes from the PR diff, is checked by a second model, is blocked if it contains secret-like content, and is capped in size. Nothing is written to a page the agent did not read in the same run. [2] [4]

It is registered for the `development` environment and tracks `BackOfficePrototype`, `PPPToo`, `HostPortalPrototype` and `nexus-docs`. [1] The shared view of the whole agent fleet is on [Vertex Agents](/docs/vertex-agents), and the review pipeline that triggers it is on [PR Review Agents](/docs/pr-review-agents). [5] [6]

> **Code-verified baseline**
>
> Read against `main` of `vertex-agents` at commit `d1c8408` (2026-10-02): `docs-update-agent/agent.py`, `docs_logic.py`, `cloudbuild.yaml` and `registry.toml`. The agent README is partly stale (it still says "not deployed" and "creating new pages is not built"); this page follows the code. [2] [4] [7] [8]

## How a PR Reaches the Agent

1. A reviewed PR is merged on a repo that `pr-review-eligibility-agent` watches in `review_only` mode. GitHub sends a `pull_request` event with action `closed` and `merged` true. [9]
2. The eligibility agent runs its landed-PR path: once per PR (Firestore compare-and-swap), it creates the merge-record task and then calls `_dispatch_docs_update`. [9]
3. That function fetches a Google identity token for the docs agent's URL and posts `repo` and `number` to `POST /process-pr` with the shared API key. It is best-effort: an 8 second timeout, no retry, and a failure is only logged. It does nothing unless both `DOCS_AGENT_URL` and `DOCS_AGENT_API_KEY` are set. [9]
4. The docs agent answers `202 accepted` immediately and runs the pipeline as a background task, because the caller is a webhook handler with a short ack window. The service runs with CPU always allocated so background work is not starved. [2] [7]

PRs the hook never delivered are recovered by the daily [catch-up sweep](#catch-up-sweep). [2]

## Modes and Controls

| Setting | Values | Deployed value |
| --- | --- | --- |
| `DOCS_AGENT_MODE` | `observe` (default in code) computes and reports with zero writes; `write` applies edits | `write` [2] [7] |
| `DOCS_AGENT_NEW_PAGES` | `off`, `propose` (default in code), `create` | `create` [2] [7] |
| `DOCS_AGENT_UNMAPPED` | `propose` (default) stores drafts, never writes; `edit` writes search-picked pages | not set, so `propose` [2] [7] |
| `DOCS_AGENT_CITED_MAP` | on unless `0`; maps uncovered files from page citations | default on [2] |
| `DOCS_AGENT_VERIFY_CLAIMS` | on unless `0`; second-model claim check | default on [2] |
| `DOCS_AGENT_MODEL`, `DOCS_AGENT_VERIFY_MODEL` | drafting model and verifier model | `gemini-2.5-pro` and `gemini-2.5-flash` defaults [2] |

A request with `"source": "eval"` is a dry run for measuring page choice: it takes no dedupe claim, never writes, posts nothing to Platform Pulse and drafts no new-page proposals. `"source": "manual"` labels replays. [2]

`POST /pause` sets a Firestore kill switch that survives restarts; `POST /resume` clears it. A paused agent returns `{"status": "paused"}` to `/process-pr`. If the flag cannot be read the agent fails closed. [2] [4]

## Processing Pipeline

For each accepted PR the agent:

1. Loads the PR header and reviews. A PR that is not merged is ignored (`not_merged`). [2]
2. Applies the **approval gate**: the latest review that is `APPROVED` or `CHANGES_REQUESTED` must be `APPROVED`, must be on the PR's current head SHA, and a later `CHANGES_REQUESTED` cancels it. `COMMENTED` reviews never count. An optional `APPROVER_LOGINS` list restricts which reviewers count. Otherwise the PR is ignored with a `no_approved_verdict` reason. [2] [4]
3. Loads `content/docs-map.json` from `nexus-docs`. If it is missing the PR is silently ignored (`docs_map_missing`); if the repo is not in the map it is ignored (`repo_not_tracked`). [2]
4. Loads the changed files and diff (diff capped at `MAX_DIFF_CHARS`, default 120000; larger PRs fall back to a diff rebuilt from per-file patches and carry a `diff_truncated` flag). [2]
5. Classifies the PR with a decision ladder: `noop` (tests, dependencies, CI, refactors), `unmapped` (alert, no edit), `edit_section`, or `new_page`. [8] [4]
6. For `edit_section`, drafts and gates one edit per mapped page, running pages in parallel (4 workers) inside a 14 minute run budget; pages not reached are reported and covered by re-running `/process-pr`. [4]
7. Posts one Platform Pulse summary through `teams_send_bot_channel_message`, only for real runs (hook, sweep, scheduler) where a person can act or an edit was made. [2] [4]

Release, promotion and batch rollup PR titles never produce new pages or unmapped edits, because their parts were documented from their own PRs. [2] [4]

### Gates on Every Edit

Each page edit passes these checks in order; any failure blocks the edit and the draft is stored for review at `/proposals`. [2]

1. The page must be readable and not structured-only (a skip-list for pages with no extractable body). [2]
2. The model returns either `no_change` or the full new markdown with a list of claims, each with a verbatim quote and file. [2]
3. **Citation check:** every claim's quote must appear in the PR diff; an edit that cites nothing is refused. [2]
4. **Claim verifier** (below).
5. **Secret gate:** the edit is blocked, not redacted, if it matches secret patterns (GitHub tokens, private keys, cloud keys, bearer tokens, assigned secrets, JWTs). [4]
6. **Structure gate:** edits that damage the page structure are refused. [2]
7. **Deletion gate:** pages picked by search may never lose content. [2]
8. **Tidy cap:** unrelated clean-up is capped at 20 percent of the changed lines (the boy-scout rule). [4]
9. **Claim budget:** at most 12 changed lines per cited claim, so an edit cannot be larger than its evidence. [4]
10. In write mode the page is re-read and the edit is abandoned (`blocked_page_changed`) if it moved since drafting. [2]

The doc PR opened by `ecosystem_update_doc` is titled `docs(slug): sync with repo#number`, and its body is then replaced with one naming the source PR, its author and the change. [2]

### Claim Verifier

A second, cheaper model (`gemini-2.5-flash` by default) reads each claim with its quote and the diff and answers whether the quote really supports the claim. A refused claim triggers one redraft with the verifier's reasons fed back to the drafter (`CLAIM_REPAIR_ATTEMPTS` is 1); a second refusal blocks the edit as `blocked_unsupported_claims`. If the verifier itself fails the edit is blocked (`blocked_verifier`), so the check fails closed. Drafting retries up to 3 times with backoff. [2] [4]

## Docs Map

`content/docs-map.json` in `nexus-docs` maps repo paths to page slugs per repo, with `match` patterns and `slugs` lists; an empty `slugs` list marks a path as needing no page. `content/docs-style.md` is the style guide fed to the drafter. [2] [4] The agent loads both from `nexus-docs` at run time through the gateway. [2]

### Unmapped Code

Changed code files with no map entry are not ignored. The agent works through them in order:

1. **Confident entries added automatically.** A file whose page already names it, or whose directory is already mapped to one page, is added to the map. [2]
2. **Cited mapping.** The agent indexes every Nexus page's backticked paths, GitHub blob links and backticked directories. A file maps to a page when exactly one page cites it, or when exactly one page cites at least two files of its directory covering at least 15 percent of that directory's non-test files, in which case a `dir/*` entry is added. Each candidate must be confirmed by the second model, shown the citing lines and the page; failures fall back to the review flow. [8] [10]
3. **Model pick plus confirmation.** One model shortlists up to 3 existing pages (after an exact text search of `nexus-docs` for the file's distinctive identifiers); a second reads the page text and must confirm, and the page must textually contain an identifier from the file. Both must agree. [2]
4. **Internal files get an explicit decision.** A model judges whether the file is internal plumbing; paths such as seeds, migrations, fixtures, generated code and constants files are added as ignore entries automatically, other internal judgements go to review. [2] [4]
5. Anything left is reported as a **docs gap**. [2]

Auto-merging map PRs use the branch prefix `nexus-doc/docs-map-` and carry at most 12 entries; weaker proposals collect in one standing review PR per repo on branch `nexus-map/<repo>` that is never auto-merged. [2] [4]

Search-picked pages are drafted through the same gates but only stored (propose mode) until a human agrees to switch to `edit`. `GET /precision` reports the share of labelled proposals where the right page was picked; edit mode is only meant to be enabled when at least 30 proposals are labelled at 95 percent or better. Nothing flips automatically. [2] [4]

### New Pages

When a PR leaves at least 3 changed code files uncovered, no search-picked page has anything to update for it, and the title is not a rollup, the agent drafts a new-page proposal. In `create` mode with write enabled it turns a validated proposal into a page PR on `nexus-docs`. [2] [4]

Checks before the first write: the slug matches a lowercase-letters-and-hyphens pattern of at most 6 segments, the proposal has at least 3 cited facts, fewer than 2 pages were created today (`NEW_PAGE_DAILY_CAP`, failing closed if the counter cannot be read), the title does not overlap an existing manifest title, and the slug is not already registered. The body is drafted, validated (at most 200 lines and 6 sections) and each block must be supported by the cited facts according to the second model. [2] [4]

The PR (branch `nexus-new/<slug>-<timestamp>`, title `docs(slug): create page from repo#number`) adds the page MDX, a manifest entry with `status` set to `draft` and the `FileText` icon, and the `doc-layout.tsx` registration. The section is chosen by source repo: `BackOfficePrototype` to `globus-core`, `HostPortalPrototype` and `PPPToo` to `portals`, anything else to `portals`. The `nexus-docs` auto-merge workflow merges it once PR Verify passes. [2] [4]

## Catch-Up Sweep

`POST /sweep` lists PRs merged in the last 3 days in every repo named in the docs map and runs those with no dedupe claim through the normal pipeline, up to 10 PRs per run. A PR skipped for lack of an approved review verdict has its claim released, so a later verdict is honored, and is reported once in a Platform Pulse digest with the reason. A manual run can pass `lookback_days`, capped at 14. [2] [4] The README states the sweep runs daily from Cloud Scheduler as `docs-update-agent-sweep`. [8]

## Drift Check

`POST /drift-check` reports, and never edits. It reads every page named in the docs map, extracts backticked repo-relative file paths, and checks each still exists on the `DRIFT_BRANCH` (default `development`) of the repos that map to the page, up to 400 checks per run. Paths whose top-level directory is in none of those repos are counted as unverifiable, not flagged. [2]

It also flags backticked `UPPER_SNAKE` names that a code search of the page's repos cannot find, as "unconfirmed env var references": at most 25 per run, paced 7 seconds apart, and searches that fail are reported as could-not-verify. If every checked reference is missing the run is treated as a broken check and suppressed rather than posted. A scheduler run always posts, with a line of doc-PR statistics. Route references are deliberately not checked. [2] [8]

## Endpoints

All routes except `/health` require the `X-API-Key` header; the service is also deployed without unauthenticated access. [2] [7]

| Route | Purpose |
| --- | --- |
| `GET /health` | Liveness and mode |
| `POST /process-pr` | Accept one merged PR (202), deduped per PR in Firestore; 403 if the scope guard denies the environment |
| `POST /pause`, `POST /resume` | Kill switch |
| `GET /status` | Mode, paused flag, last run |
| `GET /runs` | Stored run records, optionally by repo and number |
| `GET /proposals` | Everything the agent drafted, including blocked drafts |
| `POST /proposals/label` | Record whether the page picked was the right one |
| `GET /precision` | Labelled-precision numbers |
| `POST /sweep` | Catch-up sweep |
| `POST /drift-check` | Drift report |
| `GET /doc-prs` | Doc PR counts: opened, merged, open, closed unmerged, reverted and revert rate |

Source: [2]

## Runtime and State

The service runs on Cloud Run in project `skilled-script-469819-f2`, region `us-central1`, deployed by a Cloud Build config that a GitHub Actions workflow submits on pushes that touch the agent, `shared/**` or `registry.toml`. It uses 512Mi memory, one CPU, concurrency 1, a single always-warm instance and CPU that is never throttled. [7] [11]

State lives in a dedicated Firestore database named `docs-update-agent`: a pause flag, per-PR dedupe claims, proposals, run records, the sweep digest list and the daily new-page counter. A claim records the instance and a heartbeat; a background watcher starts 90 seconds after boot, then every 5 minutes, and re-runs any claim still marked running by a different instance with no heartbeat for 10 minutes, so a redeploy cannot strand a PR. [2] [4] The pipeline never raises into the caller's path. [2]

## Known Gaps

- The agent README still describes an earlier state (not deployed, observe only, new pages not built). Code and `cloudbuild.yaml` are authoritative. [8] [7]
- Non-PR changes such as n8n API edits and Terraform applies cannot trigger the agent; the weekly drift check is the safety net. [8]
- Only the first 100 changed files and reviews of a PR are considered. [8]
- Lost-update protection is a pre-write re-read plus a single instance with concurrency 1, not a distributed lock. [8]

## References

[1]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/registry.toml "registry.toml docs-update-agent entry"
[2]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/docs-update-agent/agent.py "docs-update-agent/agent.py"
[3]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/docs-update-agent/gateway_mcp.py "docs-update-agent/gateway_mcp.py"
[4]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/docs-update-agent/docs_logic.py "docs-update-agent/docs_logic.py"
[5]: /docs/vertex-agents "Vertex Agents"
[6]: /docs/pr-review-agents "PR Review Agents"
[7]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/docs-update-agent/cloudbuild.yaml "docs-update-agent/cloudbuild.yaml"
[8]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/docs-update-agent/README.md "docs-update-agent/README.md"
[9]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/pr-review-eligibility-agent/agent.py "pr-review-eligibility-agent/agent.py"
[10]: https://github.com/Alliance-Strategies/vertex-agents/pull/246 "PR 246: map uncovered files from page citations, gated by the second-model verifier"
[11]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/.github/workflows/deploy-docs-update-agent.yml "deploy-docs-update-agent.yml"
