Use when reconciling Cargento's docs before a pull request or release, or whenever the repository documentation may be stale.
Resources
1Install
npx skillscat add spacedock-dev/cargento/sync-docs Install via the SkillsCat registry.
sync-docs
Keep the docs true to the code and free of bloat. This is a docs-only skill: it never changes
Python, manifests, workflow YAML, or version fields — only Markdown. Each run is a
diff-and-reconcile pass, not a rewrite.
Hard boundaries — this repository will reject you otherwise
- Never touch a version field. The plugin version appears in
cargento/.claude-plugin/plugin.json(the source of truth),cargento/.codex-plugin/plugin.json
andcargento/gemini-extension.json, and is owned by the tag-driven Release workflow. Theversion-guardcheck fails any PR that changes one. Never write a version literal into Markdown
either — it would drift permanently and unwatched. Illustrative tags (git tag v0.2.0) are fine. - Never change the plugin description casually. It must stay byte-identical across five
manifests;scripts/validate_plugins.pyenforces parity. If a correction genuinely requires a new
description, change all five in the same commit — or leave it and report it as unresolved. - Never edit application code, even to make a doc claim true. If a doc is right and the code is
wrong, report it; do not fix it here. The one exception is a comment that cites a document you
deleted or renamed — repointing that reference is part of the deletion (step 5). - Never weaken a validator rule to make an edit pass. If a doc fix and a validator rule are
jointly unsatisfiable, stop and surface it rather than leaving a required check red. - Respect the exclusion list below. Some Markdown here is an input to a program, not prose.
When to run
- Before opening a PR (the primary trigger). Per
AGENTS.md, invoking thesync-docsskill is a step in the
branch → commit → validation suite → sync-docs → push →gh pr createworkflow, so the doc
updates for a change ride in the same PR and the docs never drift between merges. Scope the pass
to what your branch touched (step 1). - Periodically, or before tagging a release, as a full reconcile even without a code change.
The doc-ownership map — consolidate, don't proliferate
| File | Owns | Rule |
|---|---|---|
README.md |
The front door: what Cargento is, prerequisites, per-harness install, the skill inventory table, a short "how it works", links out. | Keep short. No command suites and no reference tables — link to the owner. Validator-asserted: the literals /cargento:cargento and codex plugin add cargento@cargento-marketplace must appear verbatim; rewording either fails the required validate check. |
HOW_TO_USE.md |
What the reader configures by hand, one task per section: registering the stdio MCP server per harness, the hooks and status line the plugin cannot install, and resolving the installed plugin path every one of those snippets needs. | Task-oriented, and every command and snippet must have been run before it is written down: nothing in CI checks a command block, so an unrun one ships wrong. It may name repository paths and real config paths, which is exactly why it exists, since the shipped skill body is forbidden both. It owns no contract and no install steps. Frozen path once it is in ROOT_DOCS, and any heading COMPATIBILITY.md cites is frozen with it. |
AGENTS.md |
Canonical repository contract and the source of truth for process: architecture tree, doc map, commit conventions, PR workflow, the pre-PR command list, the quality gate, versioning/releases, portability rules. | Every process command list is defined here once; other docs link. CLAUDE.md imports it, so edits propagate. Never rename or move it — Codex and Claude both load it by name. |
CLAUDE.md |
Claude-Code-only addenda. | Line 1 must stay the bare @AGENTS.md import. Nothing that would also be true in Codex — that belongs in AGENTS.md. |
CONTRIBUTING.md |
The human contributor journey: clone → dev setup → run locally → the quality gate → tests → what the validator enforces → server.py design constraints → commits → PRs → adding a harness → releases. |
Task-oriented. Reference the pre-PR suite in AGENTS.md rather than re-listing it; duplicated command lists are how they drift. |
COMPATIBILITY.md |
Canonical cross-harness and cross-platform contract: the per-runtime surface matrix, the per-OS capability matrix, the platform caveats, the native per-runtime validators, and the Python floor (with the list of every other place it is restated). Carries the sync marker. | Matrices and their footnotes. Why a row reads the way it does belongs in docs/design-*.md. |
SECURITY.md |
Security posture: the invariants, the known-and-accepted exposures, and private reporting. Covers the whole shipped surface — server.py and notify_hook.py. |
Anything that weakens an invariant is a security bug and belongs here. Keep the contact address equal to the one in CODE_OF_CONDUCT.md; nothing checks it. |
cargento/skills/cargento/SKILL.md |
Canonical product surface: per-harness data sources, session states, start/stop, notifications, options, interpretation notes, common mistakes. | A shipped, validated artifact — see the constraints below. It is installed without the repository, so it must never contain a repo-relative link or repo process. |
docs/promise-map.md |
Canonical user-facing promise: the five questions of a user's day, one promise each, the shipped capability that backs it, and the limit that keeps it honest. | A promise may not enter it before the capability ships, and the wording of each promise is duplicated verbatim in docs/visibility-2x2/items.json (columns[].promise) and in the Linear project description. Change one, change all three. The "How work links to a promise" section is the only place the promise IDs and the move taxonomy are defined; every other file links to it. |
docs/design-runtime-architecture.md |
Canonical module map: what each runtime file owns, the inward-only dependency rule, top-level import identity, and the config/state/application split. | Other design docs link here for the module map instead of restating it. The import allowlist it describes is asserted by a test. |
docs/design-*.md |
The durable why/how per area — decisions that outlive the build, including alternatives that were tried and rejected and the reason why. | A decision earns a place here if re-deriving it would cost a day, or if a maintainer would otherwise re-attempt something already proven wrong. |
docs/plans/*.md |
Transient plans for unshipped work only. | Once the work ships, fold the durable what into the owning doc and the durable why into docs/design-*.md, then delete the plan file. |
.github/PULL_REQUEST_TEMPLATE.md |
The PR author's checklist. | Must mirror the real gate in AGENTS.md — if the gate gains a step, this gains a checkbox. Never rename; GitHub loads it by path. The HTML comments are deliberate hints. |
CODE_OF_CONDUCT.md |
Contributor Covenant 2.1, verbatim. | Never edit the text. Only the enforcement contact is ours. |
.claude/skills/*/SKILL.md and .agents/skills/* |
Repository development skills, including this one. Not shipped with the plugin. | .claude/skills/ is canonical. Each .agents/skills/ entry is a relative symlink to its matching canonical directory so Codex and Claude Code read one body. Each canonical skill directory contains its own agents/openai.yaml. The banned portability literals are legal here because this tree is not shipped; the validator link-checks the canonical files but does not marker-scan them. |
Constraints on cargento/skills/cargento/SKILL.md
scripts/validate_plugins.py scans every Markdown file under cargento/skills/, and
The dashboard test suite asserts the skill body against the code. Both are required checks:
- Frontmatter fields are exactly
name,description,license— nothing else, no duplicate keys.namemust be lowercase kebab-case and equal the directory name. descriptionis required, at most 300 characters (300 exactly passes), and may not contain<or>.- The body must stay host-neutral. These six literal substrings are rejected anywhere in the file:
${CLAUDE_PLUGIN_ROOT}, the user-cache skills path,mcp__claude_ai_,ToolSearch(,Skill(skill=,subagent_type. (That second one is why this skill's own path must never be
pasted into the skill body — write it out in prose instead.) - Every relative Markdown link and
#heading-anchormust resolve inside the repository. In
practice: do not add links to repo files at all, because the plugin is installed without the
repository around it. - The documentation-matches-code test additionally requires: every backticked
~/…path must
prefix-match (in either direction) a realresolve_store_rootsoutput, except~/.claude/settings*
which is carved out; the set of backticked store environment variables must equalSTORE_ENV_VARSin the code; the literalPython 3.11+must appear; and the body must containhttp://127.0.0.1:4553and must not contain the equivalentlocalhost:4553URL. The repository
validator applies that same ban to every prose doc — including this file, which is why the banned
spelling is not written out here.
After any edit under cargento/skills/, run python3 scripts/validate_plugins.py andpython3 -m unittest discover -s cargento/skills/cargento/tests -t . before committing.
Voice and tone
The prose docs are written for people, and they were humanized in one deliberate pass. An agent
topping them up in model-default voice, one sync at a time, is how that gets undone. Step 7 of the
Procedure exists to stop it, and check (e) in step 10 is the mechanical backstop, since nothing in CI
enforces tone.
This section is the contract, not the humanizer skill. That skill automates the pass and is
worth using where a harness has it, but this repository does not vendor it and a clean checkout will
not have it. Everything needed to apply the standard by hand is written out below, deliberately, so
the rule survives the tool going missing.
In scope, and the exact set check (e) greps:
README.md HOW_TO_USE.md CONTRIBUTING.md COMPATIBILITY.md SECURITY.md docs/promise-map.md docs/design-*.md docs/plans/*.mdOut of scope, deliberately: AGENTS.md and CLAUDE.md (agent contracts loaded verbatim as
instructions), cargento/skills/cargento/SKILL.md (a validated artifact with test-asserted
literals, shipped to other harnesses as agent instructions), and this file. Leave their voice alone.
The standard, in order of how often it is violated:
- No em dashes, en dashes, or curly quotes. This is the hard one, and the one check (e) catches.
Use a period, a comma, a colon, or parentheses instead. - No mechanical boldface, and no inline-header lists (
- **Thing:** the thing is...). A list is
fine; bolding the first two words of every item is the tell. - Sentence case in headings, not Title Case.
- Prefer
is/are/hasover "serves as", "stands as", "boasts", "features". - Cut the AI-vocabulary words: crucial, pivotal, seamless, robust, leverage, delve, showcase,
underscore, testament, landscape (figurative), tapestry. - No participle tails bolted on for depth ("..., ensuring reliability", "..., highlighting its
importance"). - No generic upbeat closers. End on the last concrete fact.
Two things not to do in the name of tone. Never drop a fact to make a sentence flow, and never add
one to make it land: the pass keeps the information and changes only the shape. And do not
flatten the specifics that make these docs worth reading. The measured numbers, the named failure
modes, and above all the rejected-alternatives lists in docs/design-*.md are the human signal, not
the noise.
Markdown that is not documentation — never reconcile these
Some Markdown here is consumed by a program, reproduced verbatim from upstream, or generated.
Editing it changes behavior or breaks a licence, silently.
| Path / pattern | Why it is off-limits | Allowed action |
|---|---|---|
CODE_OF_CONDUCT.md |
Verbatim Contributor Covenant 2.1. The version is pinned by the README badge and by the self-attestation near the end. | Fix the enforcement contact only. Never reword the covenant. |
LICENSE, NOTICE |
Apache-2.0 text and its §4(d) attribution. Extensionless, so no *.md sweep reaches them — but a "tidy the docs" pass can. |
None. Edits are legal changes. |
.github/ISSUE_TEMPLATE/*.yml |
GitHub issue forms: prose inside a validated schema. | Edit label / description strings only; never touch keys, ids or labels:. Keep the harness options aligned with the registry, but do not delete the non-harness catch-all option. |
.github/ISSUE_TEMPLATE/config.yml |
Chooser config. Its name/about strings do render, but the security-advisory URL is load-bearing. |
None — and if it must change, keep it consistent with SECURITY.md. |
coverage-comment.md |
Written by a CI step and read back by the next one; its marker comment must stay byte-identical to what the comment action matches on. Untracked. | Never. If it appears locally it is CI debris. |
Markdown under **/tests/** or created by a fixture |
Test input; its content is asserted on. | Never. New fixtures must live outside cargento/skills/, or the validator will scan them and fail the build. |
cargento/commands/*.md |
Rejected outright by the validator as legacy commands. | Never create Markdown there. |
.git/, .mypy_cache/, .ruff_cache/, __pycache__/, node_modules/, .venv/ |
Not source. | Never. |
Classifying Markdown this table does not list
Before editing any *.md you have not touched before, run the checks — never judge by filename:
F=<path>
# 1. Tracked? Untracked Markdown is generated or scratch until proven otherwise.
git ls-files --error-unmatch "$F" >/dev/null 2>&1 || echo "UNTRACKED — establish why before editing"
# 2. Does a program reach it? Search the basename outside Markdown.
grep -rn --exclude-dir={.git,.mypy_cache,.ruff_cache,__pycache__} -F "$(basename "$F")" \
--include='*.py' --include='*.yml' --include='*.yaml' --include='*.json' --include='*.toml' .Then apply, in order:
- A hit is a candidate, not a consumer. It is a consumer only if the path is opened, read,
globbed, counted, or asserted on there. A filename inside a comment or an error string is not.
Open the hit and read it. Then split the verdict by what is consumed: a file whose mere
existence or links are checked has a frozen path but editable content — deleting or renaming
it fails the build, editing it does not. A file whose strings are asserted has those specific
strings frozen. Every entry inROOT_DOCSinscripts/validate_plugins.pyis in the first
category;README.mdand the shipped skill body are in both. - Location outranks content. Anything under
cargento/skills/is a shipped artifact and gets
the full constraint set above, includingreferences/oroperations/files that do not exist
yet. Anything a required check asserts against code is system-consumed even when it reads like
prose — in this repository that is the skill body andREADME.md. - Convention-located files keep their exact path.
AGENTS.md,CLAUDE.md,SECURITY.md,CODE_OF_CONDUCT.md,.github/PULL_REQUEST_TEMPLATE.md,.github/ISSUE_TEMPLATE/*, and everySKILL.md. "Grep found no consumer" means nothing for this class. - Verbatim upstream is never-touch — but match the whole document, not a mention. The test is
whether the file is the upstream artifact, not whether it links to one.README.mdandCONTRIBUTING.mdboth link the licence and are ordinary docs. - A command block under a "Validation", "Test plan" or "Pre-PR" heading is executable content.
Every line must stay literally runnable, and the block must be complete relative to the real
gate. Do not reword a command for readability. - Stable in-document identifiers are part of the contract. Decision IDs (
D-1…D-6indocs/design-*.md) and heading anchors are cited from code comments and from other docs, and
only the anchors are gate-checked. Before renumbering or renaming one, grep the identifier across*.py,*.tomland*.mdand repoint every citation — or don't renumber. - Most restrictive wins; never-touch beats everything. Where two rules conflict irreconcilably,
stop and ask.
When in doubt, leave the file alone and list it under Unresolved: in the report. A missed doc
update is cheap; a silently broken fixture or system prompt is not.
Extract the "code truth"
Run these from the repository root; the output is what the docs MUST match.
S=cargento/skills/cargento/server.py
A=cargento/skills/cargento/cargento_runtime/aggregate.py
C=cargento/skills/cargento/cargento_runtime/config.py
R=cargento/skills/cargento/cargento_runtime/state.py
W=cargento/skills/cargento/cargento_runtime/web
G=cargento/skills/cargento/cargento_runtime/diagnostics.py
H=cargento/skills/cargento/cargento_runtime/http_api.py
L=cargento/skills/cargento/cargento_runtime/lifecycle.py
I=cargento/skills/cargento/cargento_runtime/cli.py
# HTTP routes the server actually serves, split by method, and who owns the
# listener. Read by AST over the three functions that dispatch, because no
# single grep survives this file: GET compares `url.path` in `_get_api` (not in
# `do_GET`, which only owns "/"), one GET and one POST route are prefix matches
# rather than comparisons, and `do_POST` dispatches through a DICT LITERAL. A
# comparison-only pattern therefore reports the GET routes and silently finds
# NONE of the nine POST ones, which is how SECURITY.md's "nine POST routes"
# came to be unverifiable from this block. Cross-check the count: SECURITY.md
# states it, and states how many of them carry a capability.
python3 - "$H" <<'ROUTES'
import ast
import pathlib
import re
import sys
source = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
lines = source.splitlines()
owners = {"do_GET": "GET", "_get_api": "GET", "do_POST": "POST"}
found: dict[str, set[str]] = {"GET": set(), "POST": set()}
for node in ast.walk(ast.parse(source)):
if isinstance(node, ast.FunctionDef) and node.name in owners:
body = "\n".join(lines[node.lineno - 1 : node.end_lineno])
found[owners[node.name]].update(re.findall(r'"(/(?:api/)?[a-z/<>-]*)"', body))
for method, routes in found.items():
print(f"{method}: {len(routes)}")
for route in sorted(routes):
print(" ", route)
ROUTES
grep -nE '^(class CargentoHTTPServer|class _RequestHandler)|^def (normalize_host|reuse_address_allowed|bind_error_message)' "$H"
# CLI flags and their defaults. These live in cli.build_parser now, not in the
# launcher: `server.py` is seven lines and greps of it silently find nothing.
grep -E -A4 'add_argument\(' "$I" | grep -E '"--|default='
# The launcher contract: one import of cli.main and nothing else.
cat "$S"
# The harness registry: one row per supported harness, in the order the page
# renders its chips. Read it from `aggregate.default_harnesses` rather than by
# matching source text, because the rows are multi-line. The third column is
# each callback's defining module. Every row must resolve under
# `cargento_runtime.collectors`, with no exception: Claude's used to be a
# wrapper the registry built to bind a popup notifier, and the popup decision
# now belongs to `Application` instead.
python3 - "$A" <<'PY'
import pathlib, sys
sys.path.insert(0, str(pathlib.Path(sys.argv[1]).parents[1]))
from cargento_runtime import aggregate
for spec in aggregate.default_harnesses():
print(f"{spec.key:10} {spec.label:10} {spec.collect.__module__}")
PY
# Lifecycle operations, and the one respawn contract --daemon uses on Windows
grep -nE '^def (tcp_port|cargento_home|state_path|log_path|write_state|read_state|probe_port|port_released|await_release|instance_status|render_status|stop_instance|fork_daemon|await_daemon|spawn_argv|spawn_detached|await_spawned|prepare_daemon_home|serve)' "$L"
# The application boundary and the registry contract it consumes
grep -nE '^(class Application|class HarnessSpec)|^def default_harnesses|^ def collect' "$A"
grep -nE '^HARNESSES|^def (_legacy_application|_bound_popup_notifier)' "$S"
# What --diagnose reports, and the pinned order it reports stores in
grep -nE '^(_REPORT_KEY_ORDER|def (store_primaries|candidate_report|diagnose|render_diagnosis))' "$G"
# Store-relocation environment variables the resolver advertises
grep -nE '^(STORE_ENV_VARS|CARGENTO_HOME_ENV) = ' "$C"
# Immutable configuration fields, builders, and locked defaults
grep -nE '^(class RuntimeConfig|def (build_runtime_config|resolve_store_roots|store_roots|primary_store))' "$C"
awk '/return RuntimeConfig\\(/,/^ \\)/' "$C"
# Mutable state fields, lock/dictionary factories, and builder/helper bodies
sed -n '/^class CollectMemoEntry/,$p' "$R"
# Python floor: the ruff target and the mypy pin must agree, and match the docs
grep -nE 'target-version|python_version|fail_under' pyproject.toml
# Frontend source ownership, assembly slots, and byte identity
grep -nE 'WEB_DIR|load_frontend' scripts/lint_embedded.py
grep -o '{{CARGENTO_STYLES}}\|{{CARGENTO_APP}}' "$W/index.html" | sort | uniq -c
python3 - "$W" <<'PY'
import base64
import hashlib
import importlib.util
import sys
from pathlib import Path
web = Path(sys.argv[1])
spec = importlib.util.spec_from_file_location("cargento_web_page", web / "page.py")
assert spec and spec.loader
page_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(page_mod)
# APP_PARTS first: test_next_page compares the tuple by equality, so a new part
# fails there as well as on its own size, and the order is part of the claim.
print("APP_PARTS =", page_mod.APP_PARTS)
for name in (*page_mod.APP_PARTS, "styles.css"):
payload = (web / name).read_bytes()
print(name, len(payload), hashlib.sha256(payload).hexdigest())
# Fonts are pinned DECODED, not as the .b64 file on disk. Printing the raw bytes
# here produced plausible figures that fail the test: the oracle joins the .b64
# lines and base64-decodes before measuring, so the two differ by about a third.
for name, _slot in page_mod.FONT_ASSETS:
encoded = "".join((web / name).read_text(encoding="ascii").splitlines())
payload = base64.b64decode(encoded, validate=True)
print(name, len(payload), hashlib.sha256(payload).hexdigest())
page = page_mod.load_page()
print("assembled page", len(page), hashlib.sha256(page).hexdigest())
print("NOTE: the assembled figures are pinned TWICE, in tests/test_next_page.py")
print(" and tests/test_next_flag.py. Update both.")
PY
# The real CI command surface
grep -nE '^\s+(- name:|run:| +[a-z].*)$' .github/workflows/quality-gate.yml | grep -E 'ruff|mypy|coverage|unittest|lint_embedded|validate_plugins'
grep -nE 'run: ' .github/workflows/validate.yml
# What the validator enforces on documentation
grep -nE 'PORTABILITY_MARKERS|SHARED_FRONTMATTER_FIELDS|ROOT_DOCS|BANNED_DOC_LITERALS|validate_repository_skills|maximum is 300' \
scripts/validate_plugins.pyCross-check every count a doc states — "eight harnesses", "five places", a port, a threshold in
minutes, a Python version. Stale counts are this repository's most common drift.
Procedure
Scope the diff. Find what landed since the last sync. Drive this off the
docs-synced-throughmarker at the bottom ofCOMPATIBILITY.md, not off the last commit
that touched the file — once any commit on this branch editsCOMPATIBILITY.md, that commit
is the last one to touch it and the range collapses to empty exactly when you need it:MARKER=$(grep -oE 'docs-synced-through: [0-9a-f]+' COMPATIBILITY.md | awk '{print $2}') git log --oneline "${MARKER:-origin/main}..HEAD" git log --stat "${MARKER:-origin/main}..HEAD" -- '*.py' '*.toml' '*.yml'The
--statoutput tells you which subsystems moved, so you know where to look; the extraction
above is the ground truth. If the marker is missing or unresolvable, fall back toorigin/main..HEADand say so in the report.Reconcile each doc against the code truth. Every real item must appear correctly wherever it
is documented, and every doc claim must still match the code. Build the full checklist of edits
before making any. Prune as you go: tighten entries that bloated into specs, collapse an
explanation repeated across docs down to the one owner plus a link, and delete shipped TODOs and
caveats that no longer apply.Diff the command lists against each other. The pre-PR suite, the CI workflows and the PR
template checklist drift apart quietly, and a short local copy is worse than none — a contributor
follows it, passes, and then fails the required gate.AGENTS.mdowns the canonical list; every
other mention must match it or link to it. Actually run any command you document for the first
time.Retire implemented plan docs. For each file under
docs/plans/: decide whether the work
shipped by checking the code, not the document's own status markers — a plan that says "DONE"
is a claim, not evidence. If it shipped, make sure its durable what reached the owning doc and
its durable why reacheddocs/design-*.md— especially the alternatives that were tried and
rejected, which are the expensive things to lose — then delete the plan file. Keep only
plans for genuinely unshipped work, trimmed to what remains.Fix cross-references. After deleting or renaming any doc, repoint everything that referenced
it, including non-Markdown files, which are the easy misses:grep -rn --exclude-dir={.git,.mypy_cache,.ruff_cache,__pycache__} \ -e 'docs/plans/' -e 'docs/design-' -e '<the-deleted-file>' .Comments in
.py,.tomland.ymlcount. Repointing them is allowed here even though this is
a docs-only skill: a comment naming a file you deleted is a doc reference, not behavior.Update the pointers. Keep the
AGENTS.mdarchitecture tree and doc map, andREADME.md's
links, current.CLAUDE.mdimportsAGENTS.md, so those edits propagate — but check that no
Claude-only bullet has become universally true (move it up) or obsolete (delete it).Hand the tracker to
sync-project. The Linear project overview, the milestone descriptions
and the shape of an issue are that skill's subject, not this one's. Invoke it in the same pass,
after this one, because a promise sentence is canonical indocs/promise-map.mdand the tracker
copies it: reconcile the source first, then the copies.This split exists because the two surfaces fail in opposite directions. Repository docs go
stale, so this skill diffs them against the code. Tracker descriptions accrete, because
every burndown leaves behind a paragraph that was true when written and nothing ever removes one.
A skill that diffs for staleness will not catch a page that is entirely accurate and four times
too long. Measured 2026-09-08: this step's own rules were followed and the overview still reached
about 25,000 characters, of which three lines were current state.One thing stays here, because it is about this repository rather than about Linear: the promise
wording is duplicated verbatim indocs/promise-map.md, indocs/visibility-2x2/items.json, and
in the Linear project description. Change one and change all three.sync-projectowns the third
copy; make sure the first two agree before handing over.Bring the tone back to the standard. The prose docs are written for humans, and the fastest
way for that to rot is an agent topping them up in model-default voice one sync at a time. Apply
the Voice and tone section to every human-facing doc this pass edited, and re-read the result: it
must keep every fact and lose only the tells. Do not touchAGENTS.md,CLAUDE.md, the shipped
skill body, or this file.The
humanizerskill automates this and is worth using if your harness has it, in file mode,
one doc at a time. It is a third-party skill that this repository does not vendor, so treat it as
an accelerant and never as a prerequisite. Voice and tone is the contract; a contributor with no
humanizer installed applies it by hand and is equally done.Stamp the sync only from a standalone main-based reconciliation. A feature branch leaves the
marker at the bottom ofCOMPATIBILITY.mdalone, because parallel branches cannot vouch for one
another and all editing it manufactures a conflict. After the relevant branches merge, a
standalone reconciliation updates<!-- docs-synced-through: <short-sha> (<YYYY-MM-DD>) -->on
its docs branch. Stamp theorigin/maintip that branch is based on, not its branchHEAD:git rev-parse --short "$(git merge-base origin/main HEAD)"mainis squash-merged, so a branch commit stops existing the moment the PR lands, and step 1
then cannot resolve the marker it is supposed to read. A merge-base sha is already onmainand
stays there. (This was learned twice the hard way: markers pointing ata4eb54candef480af
were both orphaned by the squash that shipped them.)Verify. All five checks, every run. Use
git status/git diff HEAD, not plain
git diff— a baregit diffshows neither your new untracked docs nor a staged deletion, so it
reports "clean" for exactly the changes this pass makes:
# a. Nothing but docs changed (comment-only repoints from step 5 are the allowed exception).
git status --porcelain -uall | awk '{print $NF}' | grep -vE '\.md$'
# b. No version field moved anywhere on this branch. `version-guard` compares the PR head
# against the MERGE BASE, so an already-committed bump is invisible to `git diff HEAD` and
# passes `bump_version.py --current` (which only checks five-way parity, not immutability).
git diff "$(git merge-base origin/main HEAD)"..HEAD \
-- '*plugin.json' '*marketplace.json' '*gemini-extension.json' | grep -E '^[+-].*"version"'
# c. The validator: link and anchor resolution across prose docs and bundled skill Markdown,
# the banned literals, description length, and the portability markers.
python3 scripts/validate_plugins.py
# d. If you touched the skill body, the documentation-matches-code assertions.
python3 -m unittest discover -s cargento/skills/cargento/tests -t .
# e. Tone: no em/en dashes or curly quotes in the human-facing prose docs. Nothing in CI
# enforces this, so it is the one anti-drift check that only exists here.
# `git ls-files` builds the list, for two reasons. Step 4 deletes plan docs, and a bare
# `docs/plans/*.md` that matches nothing stays literal and makes grep exit 2 on a
# "No such file" error. And `docs/plans/` is where per-issue deep-dive notes are kept
# locally, excluded per file in .git/info/exclude: those are somebody's scratch, not
# this repository's prose, and globbing them made the check red for text no reviewer
# will ever read. Tracked files are exactly the ones the standard governs.
# The explicit if/else is here because a bare `grep && echo` exits 1 when the docs are
# CLEAN, which reads as failure to anyone (or anything) checking the status.
# NOTE: the carve-out is for INLINE spans only, not fenced blocks. A dash inside a
# ``` fence turns this red, and HOW_TO_USE.md is made of config snippets, so it is
# the likeliest file here to trip that way. Snippets need ASCII quotes and hyphens.
# Inline code spans are stripped first. A backticked span is a quoted literal, not prose:
# the dashed model slot renders as a real em dash, and a doc that documents that string
# has to contain it. Matching inside code spans made the check permanently red, which
# ends with someone mangling a documented literal to quiet it.
# The per-file loop keeps the filename in the output; piping every doc through one sed
# would report a line number with nothing to open.
if for f in $(git ls-files -- README.md HOW_TO_USE.md CONTRIBUTING.md COMPATIBILITY.md \
SECURITY.md docs/promise-map.md 'docs/design-*.md' 'docs/plans/*.md'); do
sed 's/`[^`]*`//g' "$f" | grep -n '—\|–\|[“”‘’]' | sed "s|^|$f:|"
done | grep .; then
echo "TONE DRIFT: reapply Voice and tone to the files listed above"
else
echo "tone clean"
fiThen decide whether you owe the full gate, by who authors the PR:
- Pre-PR-gate run — checks a to e are enough. The suite in
AGENTS.md§ Pre-PR Checks ran on
this same tree minutes ago and the human author owns the result. - Standalone/periodic run — you are the PR author, so run the whole pre-PR suite from
AGENTS.mdbefore opening anything.quality-gateis a required check covering ruff, format,mypy --strict,lint_embedded.py, coverage againstfail_under, andplatform-testson
macOS and Windows; none of that is implied by a to e. Opening a PR you have not gated pushes your own
verification onto the reviewer.
Never open or update a PR on the strength of a to e alone.
11. Stage, commit in the right place, then report. Never commit to main. Stage explicitly —
new docs are untracked, so git commit -a would silently skip them:
bash git add -A -- '*.md' # plus any comment-only repoints from step 5, named individually git status --short # confirm the staged set is what you expect
- Pre-PR-gate run (invoked on an existing feat/…/fix/… branch before gh pr create):
commit onto that same branch with git commit -s — do not create a new branch or a second
PR; the doc updates ride in the PR you are about to open.
- Standalone/periodic run (started from main, no feature work in flight): create a
docs/… branch, commit, and open its own PR — having run the full suite per step 10.
**A standalone run is not finished when the PR opens; it is finished when the required checks are
green.** Watch them. If one goes red, either it is your doing — fix it — or it is unrelated to a
docs-only diff, in which case *prove* that before dismissing it: show the diff touches no code the
failing test exercises, name the failure mode, and re-run the job. "Probably flaky" without
evidence is how a real regression ships.
End with a one-line report:
`Added: … | Corrected: … | Retired (deleted): … | Unresolved: …`, plus the new sync marker.When NOT to change something
- A doc deliberately records a known gap or an accepted trade-off — the skill body's "Known gap"
note,COMPATIBILITY.md's unsupported-WSL-topology paragraph,SECURITY.md's "Known and
accepted" section. Preserve it. Remove such a note only when the underlying code changed; quietly
deleting an honest caveat is worse than the caveat. - A
docs/design-*.mdentry that explains why an approach was rejected. It describes code that
does not exist, which is exactly what makes it look stale and exactly why it is there. Never
prune it on those grounds. - Version and description fields, per the hard boundaries above.
- Any file in the exclusion table, or any file whose classification you are unsure of.