From dd031e648850ed4409512548fa09bd0ca8d8060e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 09:22:40 +0000 Subject: docs(readme): reflect completed MyST migration The MyST migration is complete: source_suffix in docs/conf.py is now ['.md'] only, all canonical pages are .md, and the pre-migration RST originals are archived under docs/_rst_legacy/ (excluded from the build). Update the README accordingly: - Add a 'Source format' section stating pages are MyST Markdown and pointing at the _rst_legacy/ archive so newcomers know not to edit it. Mention that the VyOS command directives are written as MyST fenced blocks via myst_fence_as_directive, and that docs/_include/ *.txt snippets stay RST because cmdincludemd parses their content as RST (per 9c815d6). - Drop the stale 'MyST Markdown for migrated pages, RST for the rest' line from the Contributing section; rephrase to point at the actual topics AGENTS.md covers today (MyST conventions, command directives, linter suppression markers, Copilot/CodeRabbit workflow). --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 290c6ce1..17ddf8a6 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,19 @@ https://docs.vyos.io. The earlier wiki for VyOS 1.1.x and pre-1.2.0 docs is preserved on the [Wayback Machine](https://web.archive.org/web/20200225171529/https://wiki.vyos.net/wiki/Main_Page). +## Source format + +Pages are [MyST Markdown](https://myst-parser.readthedocs.io/) (`.md`) and +build with Sphinx — `source_suffix` in `docs/conf.py` lists `.md` only. +The pre-migration RST originals are archived under `docs/_rst_legacy/` +for reference; they are excluded from the build and should not be edited. + +VyOS-specific command directives (`cfgcmd`, `opcmd`, `cmdincludemd`) are +written as MyST fenced blocks and rendered as directives via +`myst_fence_as_directive`. Shared snippets under `docs/_include/*.txt` are +still RST — they are included into MyST pages through `cmdincludemd`, which +parses their content as RST so the legacy templates keep working unchanged. + ## Branches The documentation repository tracks the same branch convention as the VyOS @@ -60,6 +73,7 @@ Output lands in `docs/_build/html/`. ## Contributing -See [AGENTS.md](AGENTS.md) for the full contributor guide — source format -conventions (MyST Markdown for migrated pages, RST for the rest), CLI -directive syntax, IP-address rules, the linter, and the bot review workflow. +See [AGENTS.md](AGENTS.md) for the full contributor guide — MyST source +conventions, the VyOS command directives (`cfgcmd` / `opcmd` / +`cmdincludemd`), IP-address rules, the linter and its suppression markers, +and the Copilot/CodeRabbit bot review workflow. -- cgit v1.2.3 From 10d1e3ee6a47a1cda64680ce4cbd6def0c8d5d25 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 10:02:51 +0000 Subject: docs(readme): wrap pre-existing long lines for MyST doc-lint The doc-linter was extended to MyST .md files in 7a3aa29 (after the previous README refresh in 3a544f6), so this PR is the first README edit linted under MyST rules. The three pre-existing >80-char lines (badge, Wayback link, rolling table row) now fail doc-lint. Wrap them: - Switch the badge and Wayback links to reference-style. The badge destination/image URLs become [badge]/[rtd]/[wayback] references at the top of the file; renders identically on GitHub and in Sphinx. - Trim the Wayback snapshot path from web/20200225171529/ to web/2020/ (year-level capture, same era as the original snapshot) to fit the reference definition in 80 chars without a suppression marker. Using % stop_vyoslinter here would render literally on GitHub, since the README is GFM-rendered, not MyST. - Shorten the rolling-branch table row from 'all new contributions target this' to 'new contributions land here' (84 -> 78 chars). Also address two Copilot review findings on this PR: - Line 15 grammar: 'Pages are ... and build with Sphinx' -> 'Pages are ... and are built with Sphinx'. - AGENTS.md still describes the source format as mixed MyST/RST (AGENTS.md:7 and several follow-up sections). Updating AGENTS.md in this PR would pull ~30 pre-existing long-line violations into scope and require a bulk hard-wrap of the file, which is outside the original README request. Leaving AGENTS.md for a follow-up PR and noting it in the PR description. --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 17ddf8a6..a1364b36 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,19 @@ Source for the VyOS user documentation hosted on Read the Docs at https://docs.vyos.io. -[![Documentation Status](https://readthedocs.org/projects/vyos/badge/?version=rolling)](https://docs.vyos.io/en/rolling/?badge=rolling) +[![Documentation Status][badge]][rtd] The earlier wiki for VyOS 1.1.x and pre-1.2.0 docs is preserved on the -[Wayback Machine](https://web.archive.org/web/20200225171529/https://wiki.vyos.net/wiki/Main_Page). +[Wayback Machine][wayback]. + +[badge]: https://readthedocs.org/projects/vyos/badge/?version=rolling +[rtd]: https://docs.vyos.io/en/rolling/?badge=rolling +[wayback]: https://web.archive.org/web/2020/https://wiki.vyos.net/wiki/Main_Page ## Source format Pages are [MyST Markdown](https://myst-parser.readthedocs.io/) (`.md`) and -build with Sphinx — `source_suffix` in `docs/conf.py` lists `.md` only. +are built with Sphinx — `source_suffix` in `docs/conf.py` lists `.md` only. The pre-migration RST originals are archived under `docs/_rst_legacy/` for reference; they are excluded from the build and should not be edited. @@ -29,7 +33,7 @@ constellation: | Branch | VyOS version | Role | |--------|--------------|------| -| `rolling` | 1.5+ rolling | Default branch — all new contributions target this. | +| `rolling` | 1.5+ rolling | Default branch — new contributions land here. | | `circinus` | 1.5.x | LTS docs. | | `sagitta` | 1.4.x | Previous LTS docs. | | `equuleus` | 1.3.x | Legacy. | -- cgit v1.2.3 From e8d7e43b2c41f976bda91120242fadb8362f47e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 10:08:16 +0000 Subject: docs(agents): reflect completed MyST migration + bulk hard-wrap to 80 chars The previous README refresh sweep landed before the doc-linter was extended to MyST in 7a3aa29, so AGENTS.md was never wrapped to the 80-char rule its own 'Source conventions' section documents, and the source-format claims drifted out of sync once the RST->MyST migration finished. Content fixes (the inconsistency Copilot flagged on PR #2014): - '## Project': drop the 'MyST Markdown for migrated pages and RST for pages that haven't been migrated yet' framing. State plainly that sources are MyST Markdown only, source_suffix is ['.md'], and the RST archive under docs/_rst_legacy/ is not editable. - '### Sphinx config': source_suffix entry updated from ['.rst', '.md'] to ['.md']. - '### Source files': drop the 'docs/.rst' non-migrated bullet (no such pages exist). Add a 'docs/_include/.txt' bullet noting that the shared snippets stay RST because cmdincludemd parses their content as RST. - '**Editing rules:**': collapse 'Migrated page' / 'Non-migrated page' into one 'Existing page' rule. Add a bullet pointing at the _include/*.txt -> RST exception. - '### Command directives': rewrite to lead with MyST fenced blocks (the canonical form) and only mention RST forms for {eval-rst} blocks and _include/*.txt snippets. Avoid using literal quad-backtick code-in-code samples that would otherwise open phantom fences in the source. Linter compliance: - Hard-wrap every paragraph and bullet outside fenced code blocks to <=80 chars (~30 pre-existing violations). - Wrap the long line about the docutils inline-markup link by dropping the link entirely; the rule 'use double backticks' is self-contained and AGENTS.md doesn't need the reference. - Restructure '### Bot review workflow' table: shorten the CodeRabbit row to 79 chars and move the 'auto-reviews are disabled' note to a paragraph below the table. - Wrap the '8.8.8.8' real-public-IP bullet under '### IP addresses' with '% stop_vyoslinter' / '% start_vyoslinter' markers. The surrounding section is literally documenting suppression markers, so the meta-example reads naturally. - The '### Configuration page structure' bullet referring to MD fence syntax was using quad-backtick code-in-code; rewritten to '`{cfgcmd}` fenced code blocks (in MD)' to avoid opening a phantom fence in the linter's view of the source. --- AGENTS.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 321f8ba5..9fa05a59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,18 @@ # AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working +with code in this repository. ## Project -VyOS user documentation, built with Sphinx and hosted on Read the Docs at https://docs.vyos.io. Sources are MyST Markdown (`.md`) for migrated pages and RST (`.rst`) for pages that haven't been migrated yet. Both formats are first-class to Sphinx. +VyOS user documentation, built with Sphinx and hosted on Read the Docs +at https://docs.vyos.io. Sources are MyST Markdown (`.md`) — the +migration off RST is complete, `source_suffix` in `docs/conf.py` is +`['.md']` only, and all canonical pages are `.md`. -Pre-migration RST shadows of migrated pages are archived under `docs/_rst_legacy/` for reference only — they are excluded from the build and not consulted by Sphinx. +Pre-migration RST originals are archived under `docs/_rst_legacy/` for +reference only — they are excluded from the build, not consulted by +Sphinx, and not indexed by Context7. Do not edit them. ## Build @@ -40,7 +46,8 @@ suppression markers). ## Branches and versions -One long-lived branch per VyOS release line. Branch names are constellations sorted by area: +One long-lived branch per VyOS release line. Branch names are +constellations sorted by area: | Branch | VyOS version | |--------|--------------| @@ -50,44 +57,86 @@ One long-lived branch per VyOS release line. Branch names are constellations sor | `equuleus` | 1.3.x (legacy) | | `crux` | 1.2.x (legacy) | -PRs target `rolling`. After merge, request backports via a **post-merge comment** on the PR. Multiple branches go in a single command, space-separated: +PRs target `rolling`. After merge, request backports via a **post-merge +comment** on the PR. Multiple branches go in a single command, +space-separated: ```text @Mergifyio backport circinus sagitta ``` -Only **Maintainers team members** can invoke `@Mergifyio` commands — Mergify silently drops commands from anyone outside the team (no error reply). If a backport doesn't trigger, check team membership first. Ask a Maintainer to post the comment on your behalf. +Only **Maintainers team members** can invoke `@Mergifyio` commands — +Mergify silently drops commands from anyone outside the team (no error +reply). If a backport doesn't trigger, check team membership first. Ask +a Maintainer to post the comment on your behalf. -Mergify only reads commands from **PR comments** — mentions in the PR body are ignored. -Mergify is configured at the org level (no `.mergify.yml` in the repo). The PR template has a `## Backport` section to declare intent, but that does not trigger the backport; the comment does. +Mergify only reads commands from **PR comments** — mentions in the PR +body are ignored. +Mergify is configured at the org level (no `.mergify.yml` in the repo). +The PR template has a `## Backport` section to declare intent, but that +does not trigger the backport; the comment does. ## Architecture ### Sphinx config (`docs/conf.py`) -- `source_suffix = ['.rst', '.md']` — both formats build into the same site. +- `source_suffix = ['.md']` — Sphinx only picks up MyST Markdown + sources. The pre-migration RST originals under `docs/_rst_legacy/` + are not registered as a source extension and are excluded from the + build. - MyST extensions: `colon_fence`, `deflist`, `fieldlist`, `substitution`. -- `myst_fence_as_directive = ["cfgcmd", "opcmd", "cmdincludemd"]` — MyST fences with these names get parsed as if they were RST directives. This is how command pages stay format-portable. -- Custom modules live in `docs/_ext/` (only files listed in `extensions = [...]` in `conf.py` are actual Sphinx extensions; the others are support scripts loaded ad hoc): - - `vyos.py` (Sphinx extension, registered as `vyos`) — defines the `cfgcmd`, `opcmd`, `cmdinclude`, `cmdincludemd`, `cfgcmdlist`, `opcmdlist` directives and `cfgcmd`/`opcmd` roles that drive command coverage tracking. - - `autosectionlabel.py` (Sphinx extension, registered as `autosectionlabel`) — connects to `doctree-read` to register sections as labels. - - `testcoverage.py` — standalone helper that reads VyOS XML command definitions and exposes coverage stats; not a Sphinx extension. - - `releasenotes.py` — standalone release-notes/changelog generator script; not a Sphinx extension. +- `myst_fence_as_directive = ["cfgcmd", "opcmd", "cmdincludemd"]` — + MyST fences with these names get parsed as if they were RST + directives. This is how command pages stay format-portable. +- Custom modules live in `docs/_ext/` (only files listed in + `extensions = [...]` in `conf.py` are actual Sphinx extensions; the + others are support scripts loaded ad hoc): + - `vyos.py` (Sphinx extension, registered as `vyos`) — defines the + `cfgcmd`, `opcmd`, `cmdinclude`, `cmdincludemd`, `cfgcmdlist`, + `opcmdlist` directives and `cfgcmd`/`opcmd` roles that drive + command coverage tracking. + - `autosectionlabel.py` (Sphinx extension, registered as + `autosectionlabel`) — connects to `doctree-read` to register + sections as labels. + - `testcoverage.py` — standalone helper that reads VyOS XML command + definitions and exposes coverage stats; not a Sphinx extension. + - `releasenotes.py` — standalone release-notes/changelog generator + script; not a Sphinx extension. ### Source files -- `docs//.md` — canonical MD source for migrated pages. -- `docs/.rst` — canonical RST source for pages that have not been migrated yet (no `rst-` prefix, no MD sibling). -- `docs/_rst_legacy//rst-.rst` — archived pre-migration RST shadows. Excluded from the Sphinx build and from the Context7 index. Reference only. +- `docs//.md` — canonical MyST source for every page. + The migration off RST is complete. +- `docs/_include/.txt` — shared RST snippets included into MyST + pages via `cmdincludemd`. Their content is parsed as RST so the + legacy templates keep working unchanged. +- `docs/_rst_legacy//rst-.rst` — archived pre-migration + RST originals. Excluded from the Sphinx build and from the Context7 + index. Reference only. **Editing rules:** -- Migrated page (has `.md`): edit the `.md`. Do not touch the archived shadow under `_rst_legacy/`. -- Non-migrated page (RST-only): edit the `.rst`. -- New page: write it as `.md` from the start. The `md-` prefix that earlier MyST migration commits used is gone — never add it. + +- Existing page: edit the `.md`. Do not touch the archived original + under `_rst_legacy/`. +- New page: write it as `.md` from the start. The `md-` prefix that + earlier MyST migration commits used is gone — never add it. +- `_include/*.txt` snippets stay RST — see the next section. ### Command directives -`.. cfgcmd::`, `.. opcmd::`, and `.. cmdinclude::` are the VyOS-specific Sphinx directives in RST. They are tracked for command coverage — do **not** convert them to plain `.. code-block::`. In MyST the same directives are written as fenced blocks: ```` ```{cfgcmd} set system ... ```` (enabled by `myst_fence_as_directive`). The MyST include directive is named `cmdincludemd` (not `cmdinclude`) so that template parsing follows MyST rules in MD pages and RST rules in RST pages — pick `cmdinclude` in `.rst`, `cmdincludemd` in `.md`. +The VyOS-specific Sphinx directives are `cfgcmd`, `opcmd`, and +`cmdincludemd`. In MyST pages they are written as fenced code blocks +with `{cfgcmd}`, `{opcmd}`, or `{cmdincludemd}` as the info string +(enabled by `myst_fence_as_directive`). They are tracked for command +coverage — do **not** replace them with plain `text` or `bash` +fences. + +For RST contexts (`{eval-rst}` blocks and `_include/*.txt` snippets), +the directives are written `.. cfgcmd::`, `.. opcmd::`, and +`.. cmdinclude::`. `cmdinclude` is the RST-side include form; +`cmdincludemd` is the MyST-side form. They resolve to the same +include logic but follow the host file's parser, so pick +`cmdinclude` in `.txt`/RST contexts and `cmdincludemd` in `.md`. ## Source conventions @@ -102,15 +151,17 @@ Mergify is configured at the org level (no `.mergify.yml` in the repo). The PR t """"" Paragraphs ``` -The first heading in every RST file uses `#` overline+underline. Field lists (e.g., `:lastproofread:`) or labels may precede it. +The first heading in every RST file uses `#` overline+underline. Field +lists (e.g., `:lastproofread:`) or labels may precede it. ### Formatting -- 80-character line limit (exception: inside `.. code-block::` / fenced code blocks — `
` preserves source verbatim).
+- 80-character line limit (exception: inside `.. code-block::` /
+  fenced code blocks — `
` preserves source verbatim).
 - American English.
 - Indent with 2 spaces.
 - Blank lines around headings.
-- Inline code: use double backticks per RST convention (the [Inline markup](https://docutils.sourceforge.io/docs/user/rst/quickref.html#inline-markup) section of the docutils quick reference).
+- Inline code: use double backticks per RST convention.
 
 ### IP addresses (linter-enforced)
 
@@ -121,11 +172,15 @@ Allowed without suppression:
 - Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), `0.0.0.0/0`
 
 Allowed ASN: `64496-64511` (16-bit), `65536-65551` (32-bit).
-Allowed MAC ranges: `00-53-00`–`00-53-FF` (unicast), `90-10-00`–`90-10-FF` (multicast).
+Allowed MAC ranges: `00-53-00`–`00-53-FF` (unicast),
+`90-10-00`–`90-10-FF` (multicast).
 
 **Requires `stop/start_vyoslinter` suppression:**
+
+% stop_vyoslinter
 - Real public IPs (e.g., `8.8.8.8` for DNS examples).
 - NAT64 well-known prefix `64:ff9b::/96`.
+% start_vyoslinter
 - Lines over 80 chars (URLs, certificate fingerprints).
 
 ### Linter suppression markers
@@ -140,14 +195,23 @@ Allowed MAC ranges: `00-53-00`–`00-53-FF` (unicast), `90-10-00`–`90-10-FF` (
 .. start_vyoslinter
 ```
 
-In MyST `.md` files use the comment form `% stop_vyoslinter` / `% start_vyoslinter` for top-level Markdown content. Inside `{eval-rst}` blocks (where the embedded content is parsed as RST) keep the RST form `.. stop_vyoslinter` / `.. start_vyoslinter` — the linter scans the source line literally and only the form that matches the surrounding parser is recognized. Likewise, `.txt` template files (included via `{include}` or `cmdincludemd`) keep the RST form.
+In MyST `.md` files use the comment form `% stop_vyoslinter` /
+`% start_vyoslinter` for top-level Markdown content. Inside
+`{eval-rst}` blocks (where the embedded content is parsed as RST)
+keep the RST form `.. stop_vyoslinter` / `.. start_vyoslinter` — the
+linter scans the source line literally and only the form that matches
+the surrounding parser is recognized. Likewise, `.txt` template files
+(included via `{include}` or `cmdincludemd`) keep the RST form.
 
-Markers must always come in pairs. Indentation may match the surrounding directive (indented inside a block) or sit at column 0 (top-level) — both are valid.
+Markers must always come in pairs. Indentation may match the
+surrounding directive (indented inside a block) or sit at column 0
+(top-level) — both are valid.
 
 ### Configuration page structure
 
 1. **Theory** — what it is, when to use it, relevant RFCs.
-2. **Configuration** — all CLI options as `.. cfgcmd::` (RST) or ```` ```{cfgcmd} ```` (MD).
+2. **Configuration** — all CLI options as `.. cfgcmd::` directives
+   (in RST contexts) or `{cfgcmd}` fenced code blocks (in MD).
 3. **Examples** — practical configurations with topology diagrams.
 4. **Known issues** — problems and workarounds.
 5. **Debugging** — log collection, `show` commands, state indicators.
@@ -155,8 +219,10 @@ Markers must always come in pairs. Indentation may match the surrounding directi
 ### `.. TODO::` markers
 
 Two valid uses:
-1. **Tracking** marker on pages that still need `cfgcmd`/`opcmd` conversion — intentional.
-2. **Stale** marker on pages that already have full content — should be removed.
+1. **Tracking** marker on pages that still need `cfgcmd`/`opcmd`
+   conversion — intentional.
+2. **Stale** marker on pages that already have full content — should
+   be removed.
 
 A PR that both adds and removes TODOs is not contradictory; intent matters.
 
@@ -180,9 +246,10 @@ Files are shipped at the docs root for each version
   The render lives in `_write_llms_txt(app, exception)` in `docs/conf.py`,
   wired via `app.connect('build-finished', ...)`.
 
-When adding new top-level sections to the docs, add a corresponding bullet in
-`docs/_templates/llms.txt.j2`. Branch-specific differences (e.g. sagitta has
-no `vpp/index.md` or `contributing/index.md`) live in that branch's copy of the template.
+When adding new top-level sections to the docs, add a corresponding
+bullet in `docs/_templates/llms.txt.j2`. Branch-specific differences
+(e.g. sagitta has no `vpp/index.md` or `contributing/index.md`) live
+in that branch's copy of the template.
 
 ## Read the Docs Layout
 
@@ -209,8 +276,11 @@ serves and crawlers skip the redirect hop.
 
 ## CI
 
-- **vyoslinter** (`doc-linter.py` from the `vyos/.github` repo, run via `lint-doc.yml`) — line length and IP rules, on changed files only.
-- **Sphinx build** — runs on Read the Docs for every PR; preview URL appears as a check.
+- **vyoslinter** (`doc-linter.py` from the `vyos/.github` repo, run
+  via `lint-doc.yml`) — line length and IP rules, on changed files
+  only.
+- **Sphinx build** — runs on Read the Docs for every PR; preview URL
+  appears as a check.
 - **CLA check** — contributors must sign the VyOS CLA before merge.
 - **Conflict check** — fails the PR if it doesn't merge cleanly into base.
 
@@ -220,13 +290,17 @@ Two bots run at separate stages — do not mix them:
 
 | Bot | When to trigger | How |
 |-----|-----------------|-----|
-| **Copilot** | **Draft PRs only** | Comment `@copilot review` |
-| **CodeRabbit** | **Ready-for-review PRs only** | Comment `@coderabbitai review` (auto-reviews are disabled on this repo) |
+| **Copilot** | Draft PRs only | Comment `@copilot review` |
+| **CodeRabbit** | Ready-for-review PRs only | Comment `@coderabbitai review` |
+
+Auto-reviews are disabled on this repo — both bots are triggered
+manually via the comments shown above.
 
 Workflow:
 1. Open PR as draft (`gh pr create --draft`).
 2. Iterate; when complete, comment `@copilot review`.
-3. Address Copilot threads, re-request after each fix round until Copilot is silent.
+3. Address Copilot threads, re-request after each fix round until
+   Copilot is silent.
 4. Flip to ready (`gh pr ready `), then comment `@coderabbitai review`.
 5. Address CodeRabbit threads the same way.
 
-- 
cgit v1.2.3


From 93a5bdc70468ff92196e00783e6871de1891282f Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 13 May 2026 10:14:12 +0000
Subject: docs(agents): address Copilot review on stale RST-era conventions

Four follow-up findings from Copilot on commit e8d7e43, all valid:

- '### Formatting' bullet 'Inline code: use double backticks per RST
  convention' was carried over from the RST era. The .md sources are
  overwhelmingly single-backtick (~1900 single- vs ~80 double-backtick
  inline-code spans across docs/). Rewrite to 'single backticks in
  MyST (the canonical form). Double backticks only inside
  {eval-rst} blocks and _include/*.txt snippets, per RST convention.'

- '### RST heading hierarchy' section read like a general authoring
  rule. Add a preface scoping it explicitly to RST contexts
  (_include/*.txt and {eval-rst} blocks) and noting that canonical
  MyST pages use ATX (# / ## / ...) headings. Reword the post-block
  prose to 'every embedded RST snippet that introduces a title'
  instead of 'every RST file'.

- '### `.. TODO::` markers' heading and prose described the RST
  form only. The .md sources use `{todo}` fenced directives
  exclusively (6 triple-backtick + 1 colon-fenced occurrences across
  docs/, zero `.. TODO::` in .md). Rename the section to
  '`{todo}` markers', describe the MyST fenced form first, and
  mention the RST form only for RST contexts.

- Drop the '% stop_vyoslinter' / '% start_vyoslinter' markers I
  added around the '8.8.8.8' DNS example bullet under
  '### IP addresses'. Replace the literal IP with a description-only
  example ('a DNS server's address in a DNS forwarder example, or an
  upstream peer's address in an EBGP example'). The bullet still
  illustrates the rule (real public IPs need suppression) without
  rendering bare '% stop_vyoslinter' lines in GitHub views.

Local doc-linter still exits 0 on AGENTS.md and README.md.
---
 AGENTS.md | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 9fa05a59..32c5885d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -142,6 +142,11 @@ include logic but follow the host file's parser, so pick
 
 ### RST heading hierarchy
 
+Applies only to RST contexts — `_include/*.txt` snippets and
+`{eval-rst}` blocks inside MyST pages. Canonical pages are MyST and
+use ATX `#` / `##` / `###` etc. headings; this hierarchy does not
+apply to them.
+
 ```
 ##### Title (overline+underline, one per file)
 ***** Chapters
@@ -151,8 +156,9 @@ include logic but follow the host file's parser, so pick
 """"" Paragraphs
 ```
 
-The first heading in every RST file uses `#` overline+underline. Field
-lists (e.g., `:lastproofread:`) or labels may precede it.
+The first heading in every embedded RST snippet that introduces a
+title uses `#` overline+underline. Field lists (e.g.,
+`:lastproofread:`) or labels may precede it.
 
 ### Formatting
 
@@ -161,7 +167,9 @@ lists (e.g., `:lastproofread:`) or labels may precede it.
 - American English.
 - Indent with 2 spaces.
 - Blank lines around headings.
-- Inline code: use double backticks per RST convention.
+- Inline code: single backticks in MyST (the canonical form). Double
+  backticks only inside `{eval-rst}` blocks and `_include/*.txt`
+  snippets, per RST convention.
 
 ### IP addresses (linter-enforced)
 
@@ -177,10 +185,9 @@ Allowed MAC ranges: `00-53-00`–`00-53-FF` (unicast),
 
 **Requires `stop/start_vyoslinter` suppression:**
 
-% stop_vyoslinter
-- Real public IPs (e.g., `8.8.8.8` for DNS examples).
+- Real public IPs (e.g., a DNS server's address in a DNS forwarder
+  example, or an upstream peer's address in an EBGP example).
 - NAT64 well-known prefix `64:ff9b::/96`.
-% start_vyoslinter
 - Lines over 80 chars (URLs, certificate fingerprints).
 
 ### Linter suppression markers
@@ -216,9 +223,13 @@ surrounding directive (indented inside a block) or sit at column 0
 4. **Known issues** — problems and workarounds.
 5. **Debugging** — log collection, `show` commands, state indicators.
 
-### `.. TODO::` markers
+### `{todo}` markers
+
+In MyST pages, write TODO markers as `{todo}` fenced directives
+(triple-backtick or `:::` fenced blocks with `{todo}` as the info
+string). In RST contexts (`{eval-rst}` blocks, `_include/*.txt`
+snippets) use the RST form `.. TODO::`. Two valid uses:
 
-Two valid uses:
 1. **Tracking** marker on pages that still need `cfgcmd`/`opcmd`
    conversion — intentional.
 2. **Stale** marker on pages that already have full content — should
-- 
cgit v1.2.3


From 65a8e9fc9394ff4c9e9fca4000b8ef2681e8d95c Mon Sep 17 00:00:00 2001
From: Yuriy Andamasov 
Date: Wed, 13 May 2026 05:36:00 -0700
Subject: ci(doc-linter): scope to docs/ only — skip repo-root meta files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The linter targets published documentation sources; the auto-discover
fallback already walks `docs/` only. CI was passing root-level meta
files (README.md, AGENTS.md, .github/copilot-instructions.md — the
last is a symlink to AGENTS.md) which forced docs-publication
conventions (80-char wrap, RFC IP rules, suppression markers) onto
project meta that has no business obeying them.

Add an `is_docs_path()` guard in `main()` so the explicit-file-list
path matches the auto-discover behavior — only files under `docs/`
are linted. AGENTS.md and the Copilot-instruction symlink are now
out of scope.

Verified:
- `python3 scripts/doc-linter.py "['AGENTS.md', 'README.md', '.github/copilot-instructions.md']"` → exit 0 (all skipped).
- `python3 scripts/doc-linter.py "['docs/_test_lint.md']"` with a real public IP → still errors as expected.

🤖 Generated by [robots](https://vyos.io)
---
 scripts/doc-linter.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py
index 5ccff488..c86ff29a 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -29,6 +29,16 @@ NUMBER = r"([\s']\d+[\s'])"
 
 SUPPORTED_EXTS = ('.md', '.rst', '.txt')
 
+# Linter only applies to published documentation sources under docs/. Repo-root
+# files (README.md, AGENTS.md, .github/copilot-instructions.md) are project
+# meta, not docs content, and are out of scope.
+DOCS_ROOT = 'docs' + os.sep
+
+
+def is_docs_path(path):
+    norm = os.path.normpath(path)
+    return norm == 'docs' or norm.startswith(DOCS_ROOT)
+
 # MyST / Markdown fenced code block: leading whitespace + 3+ backticks or 3+ colons.
 # Same character and length-or-greater closes.
 MD_FENCE_RE = re.compile(r'^(\s*)(`{3,}|:{3,})(.*)$')
@@ -220,7 +230,11 @@ def main():
     try:
         files = ast.literal_eval(sys.argv[1])
         for file in files:
-                if file.endswith(SUPPORTED_EXTS) and "_build" not in file:
+                if (
+                    file.endswith(SUPPORTED_EXTS)
+                    and "_build" not in file
+                    and is_docs_path(file)
+                ):
                     if handle_file_action(file) is False:
                         bool_error = False
     except Exception as e:
-- 
cgit v1.2.3


From 28224f3ad5a62f6e67ce438c243f38c63583f0b6 Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 13 May 2026 12:43:31 +0000
Subject: docs(agents): doc-linter is in-repo, scoped to docs/ — fix stale
 references
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Two stale claims flagged by Copilot in PR #2014 (one comment marked the
other as 'same issue earlier in the file'):

- '## Lint' (L40-45): said 'vyoslinter (doc-linter.py from the
  vyos/.github repo, via the lint-doc.yml workflow) on changed files
  only'. The cross-repo dependency was dropped in 1bf386e — the
  linter is now scripts/doc-linter.py in this repo, invoked from
  .github/workflows/lint-doc.yml.
- '## CI' (L290-292): same stale claim in bullet form.

Also reflect 65a8e9f's scoping decision: the linter applies only to
files under docs/. Repo-root meta files (README.md, AGENTS.md,
.github/copilot-instructions.md) are explicitly out of scope, since
they are project meta rather than published documentation and don't
need to obey docs-publication conventions (80-char wrap, RFC IP
rules, suppression markers). Mention that exclusion in the CI bullet
so contributors know the linter won't catch issues there.
---
 AGENTS.md | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 32c5885d..f445a7c7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,12 +37,12 @@ Output: `docs/_build/html/`.
 
 ## Lint
 
-The repo doesn't ship a local lint config or pin a linter binary. CI runs
-`vyoslinter` (`doc-linter.py` from the `vyos/.github` repo, via the
-`lint-doc.yml` workflow) on changed files only — see the CI section
-below. For local checks, manually grep for the rules in
-[Source conventions](#source-conventions) (line length, address space,
-suppression markers).
+The repo doesn't ship a local lint config or pin a linter binary. CI
+runs `scripts/doc-linter.py` (in-repo, invoked from
+`.github/workflows/lint-doc.yml`) on changed files only, scoped to
+`docs/` — see the CI section below. For local checks, manually grep
+for the rules in [Source conventions](#source-conventions) (line
+length, address space, suppression markers).
 
 ## Branches and versions
 
@@ -287,9 +287,11 @@ serves and crawlers skip the redirect hop.
 
 ## CI
 
-- **vyoslinter** (`doc-linter.py` from the `vyos/.github` repo, run
-  via `lint-doc.yml`) — line length and IP rules, on changed files
-  only.
+- **doc-linter** (`scripts/doc-linter.py` in-repo, invoked via
+  `.github/workflows/lint-doc.yml`) — line length and IP rules, on
+  changed files under `docs/` only. Repo-root meta files
+  (README.md, AGENTS.md, `.github/copilot-instructions.md`) are out
+  of scope.
 - **Sphinx build** — runs on Read the Docs for every PR; preview URL
   appears as a check.
 - **CLA check** — contributors must sign the VyOS CLA before merge.
-- 
cgit v1.2.3


From ab497bf93ecc769f621b59220d9df1c13d3d2fa1 Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 13 May 2026 19:46:51 +0000
Subject: doc-linter: handle absolute paths in is_docs_path()

CodeRabbit review on 28224f3 flagged that is_docs_path() introduced
in 65a8e9f only matched repo-relative path strings. An absolute path
to docs/... (e.g., from a local invocation that pre-resolves paths,
or from tooling that uses git ls-files --full-path) would silently
fail the docs/ check and the file would be skipped.

Rewrite the helper to use os.path.commonpath against an absolute
docs/ root computed on each call. Both inputs are normalized to
absolute form, so repo-relative and absolute callers produce the
same result. ValueError from commonpath (mixed Windows drives or
empty input) is caught and treated as 'not a docs path'.

abs_docs is recomputed per call rather than captured at import time
so the helper picks up the actual cwd at invocation, matching the
existing assumption that CI / local runs invoke the linter from the
repo root.

Verified against 12 edge cases:
- repo-relative docs paths (docs, docs/foo.md, docs/sub/dir/foo.md,
  ./docs/foo.md) -> True.
- repo-relative meta paths (AGENTS.md, README.md,
  .github/copilot-instructions.md, docs_other/foo.md) -> False.
- absolute paths inside docs/ -> True; inside repo root but outside
  docs/ -> False.
- traversal attempts (../other/foo.md, docs/../AGENTS.md) -> False.

CI behavior unchanged: tj-actions/changed-files passes repo-relative
paths, which were already handled by the previous logic.
---
 scripts/doc-linter.py | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py
index c86ff29a..f580fcdc 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -32,12 +32,23 @@ SUPPORTED_EXTS = ('.md', '.rst', '.txt')
 # Linter only applies to published documentation sources under docs/. Repo-root
 # files (README.md, AGENTS.md, .github/copilot-instructions.md) are project
 # meta, not docs content, and are out of scope.
-DOCS_ROOT = 'docs' + os.sep
+DOCS_ROOT = 'docs'
 
 
 def is_docs_path(path):
-    norm = os.path.normpath(path)
-    return norm == 'docs' or norm.startswith(DOCS_ROOT)
+    """Return True iff `path` resolves under the repo's `docs/` tree.
+
+    Accepts both repo-relative and absolute paths. Paths are normalized
+    against the linter's working directory (CI invokes it from the repo
+    root; the same is expected for local runs).
+    """
+    abs_path = os.path.abspath(path)
+    abs_docs = os.path.abspath(DOCS_ROOT)
+    try:
+        return os.path.commonpath([abs_path, abs_docs]) == abs_docs
+    except ValueError:
+        # commonpath raises on mixed drives (Windows) or empty input.
+        return False
 
 # MyST / Markdown fenced code block: leading whitespace + 3+ backticks or 3+ colons.
 # Same character and length-or-greater closes.
-- 
cgit v1.2.3


From bad55da626f59fd84e85e8f6d9a826ae1f0b8860 Mon Sep 17 00:00:00 2001
From: Yuriy Andamasov 
Date: Wed, 13 May 2026 23:30:02 +0300
Subject: docs(linter): add one-line docstrings to clear coverage warning
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

CodeRabbit Pre-merge Docstring Coverage check reported 50% on
scripts/doc-linter.py (threshold 80%). Add minimal one-line docstrings
to each public function; no behavior change.

🤖 Generated by [robots](https://vyos.io)
---
 scripts/doc-linter.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py
index f580fcdc..9f1e6f57 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -88,6 +88,7 @@ def is_suppression_marker(line, kind, in_md_fence, in_rst_codeblock,
 
 
 def lint_mac(cnt, line):
+    """Flag MAC addresses outside the RFC 7042 documentation range."""
     mac = re.search(MAC, line, re.I)
     if mac is not None:
         mac = mac.group()
@@ -98,6 +99,7 @@ def lint_mac(cnt, line):
 
 
 def lint_ipv4(cnt, line):
+    """Flag IPv4 addresses outside RFC 5737 / private / multicast ranges."""
     ip = re.search(IPV4ADDR, line, re.I)
     if ip is not None:
         ip = ipaddress.ip_address(ip.group().strip(' '))
@@ -112,6 +114,7 @@ def lint_ipv4(cnt, line):
 
 
 def lint_ipv6(cnt, line):
+    """Flag IPv6 addresses outside RFC 3849 / private / multicast ranges."""
     ip = re.search(IPV6ADDR, line, re.I)
     if ip is not None:
         ip = ipaddress.ip_address(ip.group().strip(' '))
@@ -125,6 +128,7 @@ def lint_ipv6(cnt, line):
 
 
 def lint_AS(cnt, line):
+    """Placeholder for future AS-number documentation-range checks (RFC 5398)."""
     number = re.search(NUMBER, line, re.I)
     if number:
         pass
@@ -132,11 +136,13 @@ def lint_AS(cnt, line):
 
 
 def lint_linelen(cnt, line):
+    """Warn when a line exceeds the 80-character docs convention."""
     line = line.rstrip()
     if len(line) > 80:
         return (f"Line too long: len={len(line)}", cnt, 'warning')
 
 def handle_file_action(filepath):
+    """Run all lint checks on one file, respecting fence/code-block and suppression context."""
     errors = []
     file_ext = os.path.splitext(filepath)[1].lower()
     # Stack of open MD/MyST fences: (char, min_len). Supports nesting like
@@ -236,6 +242,7 @@ def handle_file_action(filepath):
 
 
 def main():
+    """Entry point: lint the changed-file list from argv, or fall back to walking `docs/`."""
     bool_error = True
     print('start')
     try:
-- 
cgit v1.2.3


From 5d1c2d74d1e18e80b788e5778b10b94053343c2c Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 13 May 2026 20:35:38 +0000
Subject: doc-linter: realpath() resolution, DOCS_ROOT in walker, indent fix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Three Copilot findings on ab497bf:

1. is_docs_path() docstring claimed paths 'resolve' under docs/, but
   the implementation only normalized via abspath() — a symlink under
   docs/ that points outside the tree would be treated as in-scope.
   Switch both inputs to os.path.realpath() so symlinks are followed
   to their real targets. The reverse case is also handled: if docs/
   is itself a symlink (some CI checkouts), realpath() resolves it
   consistently for both sides of the commonpath comparison.

   Verified with a synthetic case: docs/poison.md -> /etc/hosts now
   returns False (with abspath() it returned True).

2. The auto-discover fallback in main() still hardcoded
   os.walk('docs') instead of using the new DOCS_ROOT constant.
   Use DOCS_ROOT in both paths so the docs root is configured in
   exactly one place.

3. Indentation inside 'for file in files:' was double-indented (8
   spaces under the for, instead of 4) — pre-existing oddity from
   before 65a8e9f, preserved through the is_docs_path() addition.
   Normalize to a single indent level under the loop.

CI behavior unchanged: tj-actions/changed-files passes repo-relative
paths with no symlinks under docs/, which were already handled. The
realpath() switch only changes behavior in the symlink-escape case,
which was a bug.
---
 scripts/doc-linter.py | 31 ++++++++++++++++++-------------
 1 file changed, 18 insertions(+), 13 deletions(-)

diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py
index 9f1e6f57..c3304718 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -38,12 +38,17 @@ DOCS_ROOT = 'docs'
 def is_docs_path(path):
     """Return True iff `path` resolves under the repo's `docs/` tree.
 
-    Accepts both repo-relative and absolute paths. Paths are normalized
-    against the linter's working directory (CI invokes it from the repo
-    root; the same is expected for local runs).
+    Accepts both repo-relative and absolute paths. Both `path` and
+    `DOCS_ROOT` are resolved with `os.path.realpath`, so symlinks are
+    followed to their real targets — a symlink under `docs/` that
+    points outside the tree is correctly treated as out-of-scope, and
+    a `docs/` that is itself a symlink (e.g., in some CI checkouts) is
+    correctly treated as the docs root. Paths are normalized against
+    the linter's working directory (CI invokes it from the repo root;
+    the same is expected for local runs).
     """
-    abs_path = os.path.abspath(path)
-    abs_docs = os.path.abspath(DOCS_ROOT)
+    abs_path = os.path.realpath(path)
+    abs_docs = os.path.realpath(DOCS_ROOT)
     try:
         return os.path.commonpath([abs_path, abs_docs]) == abs_docs
     except ValueError:
@@ -248,15 +253,15 @@ def main():
     try:
         files = ast.literal_eval(sys.argv[1])
         for file in files:
-                if (
-                    file.endswith(SUPPORTED_EXTS)
-                    and "_build" not in file
-                    and is_docs_path(file)
-                ):
-                    if handle_file_action(file) is False:
-                        bool_error = False
+            if (
+                file.endswith(SUPPORTED_EXTS)
+                and "_build" not in file
+                and is_docs_path(file)
+            ):
+                if handle_file_action(file) is False:
+                    bool_error = False
     except Exception as e:
-        for root, dirs, files in os.walk("docs"):
+        for root, dirs, files in os.walk(DOCS_ROOT):
             path = root.split(os.sep)
             for file in files:
                 if file.endswith(SUPPORTED_EXTS) and "_build" not in path:
-- 
cgit v1.2.3


From 36d8f3ee1e0caa54b041e2ee1b16425cf7a3044a Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 13 May 2026 20:40:31 +0000
Subject: doc-linter: rename unused walker var to _dirs

CodeRabbit nit (Ruff B007): the dirs variable from os.walk(DOCS_ROOT)
in the auto-discover fallback is unused. Renaming to _dirs makes the
intent explicit and silences the warning.
---
 scripts/doc-linter.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py
index c3304718..b1977f84 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -261,7 +261,7 @@ def main():
                 if handle_file_action(file) is False:
                     bool_error = False
     except Exception as e:
-        for root, dirs, files in os.walk(DOCS_ROOT):
+        for root, _dirs, files in os.walk(DOCS_ROOT):
             path = root.split(os.sep)
             for file in files:
                 if file.endswith(SUPPORTED_EXTS) and "_build" not in path:
-- 
cgit v1.2.3


From 8be6993d1496b0cec62c3a485519f6f915a62ecf Mon Sep 17 00:00:00 2001
From: Claude 
Date: Wed, 13 May 2026 20:45:14 +0000
Subject: doc-linter: narrow argv parsing exception scope to (IndexError,
 SyntaxError, ValueError)

CodeRabbit / Ruff BLE001: the previous 'except Exception as e:' on the
explicit-file-list path caught any error, masking runtime failures
from handle_file_action() as silent fallback behavior. Only input
validation errors from ast.literal_eval(sys.argv[1]) should trigger
the fallback walk.

Refactor:

- Wrap only the parse step in try/except, catching just IndexError
  (missing argv[1]), SyntaxError (malformed literal), and ValueError
  (non-literal input).
- On parse failure, set files = None and dispatch to the DOCS_ROOT
  walk via an explicit 'else' branch.
- On parse success, run the file loop outside the try so any errors
  from handle_file_action() propagate normally and CI fails loudly.

Also drops the unused 'as e' (Ruff BLE001 noise) and the implicit
catch of TypeError (e.g. ast.literal_eval('42') returns an int and
'for file in 42:' would have been silently swallowed -> fallback
walk; now it raises clearly).

Verified scenarios:
- explicit file list (CI normal path) -> exit 0.
- no argv -> IndexError caught -> walks DOCS_ROOT.
- malformed argv ('not-a-list') -> SyntaxError caught -> walks DOCS_ROOT.
- explicit list with a non-existent file -> FileNotFoundError
  propagates (previously silently triggered a fallback walk).
- explicit list with a non-list literal ('42') -> TypeError
  propagates (programming error stays visible).
---
 scripts/doc-linter.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py
index b1977f84..8e74600f 100644
--- a/scripts/doc-linter.py
+++ b/scripts/doc-linter.py
@@ -250,8 +250,16 @@ def main():
     """Entry point: lint the changed-file list from argv, or fall back to walking `docs/`."""
     bool_error = True
     print('start')
+    # Only the argv-parsing step is wrapped in try/except. Errors raised by
+    # handle_file_action() must propagate so CI failures stay visible instead
+    # of silently triggering a full docs/ walk.
     try:
         files = ast.literal_eval(sys.argv[1])
+    except (IndexError, SyntaxError, ValueError):
+        # No argv or malformed list -> fall back to walking DOCS_ROOT.
+        files = None
+
+    if files is not None:
         for file in files:
             if (
                 file.endswith(SUPPORTED_EXTS)
@@ -260,7 +268,7 @@ def main():
             ):
                 if handle_file_action(file) is False:
                     bool_error = False
-    except Exception as e:
+    else:
         for root, _dirs, files in os.walk(DOCS_ROOT):
             path = root.split(os.sep)
             for file in files:
-- 
cgit v1.2.3