Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions .claude/skills/taos-development-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,16 +428,28 @@ normalized form (`1.0.0bN`). Only pyproject vs uv.lock is test-gated

## Documentation gate

A gate blocks PRs that add or remove certain feature code without a matching doc update
(configured in `docs/doc-gate.toml`):

| Change | Requires editing |
|--------|-----------------|
| Desktop app under `desktop/src/apps/` added/removed | `README.md` |
| Route module under `tinyagentos/routes/` added/removed | `docs/agent-coordination.md` |
| Installer under `tinyagentos/installers/` or `scripts/install*` added/removed | `README.md` |
| Manifest under `app-catalog/` added/removed | `README.md` |
| `tinyagentos/auth_middleware.py` (agent-token route allowlist) changed | `docs/agent-coordination.md` |
A gate blocks PRs that change certain feature code without a matching doc update
(configured in `docs/doc-gate.toml`). Rules marked **any change** also fire on a plain
modification; the rest fire only when a matching file is added or deleted. Test files
(`test_*.py`, `*.test.*`, `*.spec.*`, `__tests__/`) never trigger any rule.

| Change | Fires on | Requires editing |
|--------|----------|-----------------|
| Desktop app under `desktop/src/apps/` | add/remove | `README.md` |
| Route module under `tinyagentos/routes/` | any change | `docs/agent-coordination.md` |
| Installer under `tinyagentos/installers/` or `scripts/install*` | any change | `README.md` |
| Manifest under `app-catalog/` | any change | `README.md` |
| `tinyagentos/auth_middleware.py` (agent-token route allowlist) | any change | `docs/agent-coordination.md` |
| Anything under `tinyagentos/` or `desktop/src/` | any change | `CHANGELOG.md` or a `changelog.d/*.md` fragment |
| Agent registry, token auth, scope-requests store, `routes/agent_*.py`, `tinyagentos/mcp/` | any change | `docs/agent-manual/*.md` or `docs/agent-coordination.md` |
| `.github/workflows/*.yml`, `pyproject.toml`, `CONTRIBUTING.md` | any change | this skill or `docs/*.md` |
| `routes/desktop.py`, `routes/desktop_control.py`, `routes/taos_agent.py` | any change | `.claude/skills/taos-agent/*.md` or `docs/agent-manual/*.md` |
| `update_runner.py`, `auto_update.py`, `restart_orchestrator.py`, `scripts/collate_changelog.py` | any change | `docs/RELEASING.md`, a runbook, or another `docs/*.md` |
| `tinyagentos/worker/` | add/remove | `tinyagentos/worker/README.md` |

The changelog rule is the one that catches most PRs: any non-test change under
`tinyagentos/` or `desktop/src/` needs a `changelog.d/<pr>-<slug>.md` fragment (preferred
over editing `CHANGELOG.md` directly, which conflicts between PRs).

`.github/workflows/doc-gate.yml` is authoritative (a local `--no-verify` does not bypass it) and
also runs `scripts/check_schema_migrations.py` (the SCHEMA-before-migrations guard, see Pitfalls).
Expand All @@ -446,6 +458,10 @@ If your PR trips a rule and there is genuinely nothing to document, add a traile
```
Docs-Reviewed: no user-facing change, internal refactor only
```
The trailer passes **every** rule for that PR, so it is an escape hatch, not a shortcut:
the gate prints `doc-gate: trailer override used in <sha> by <author>: <why>` in its CI
log for each commit that carries one, and that line is reviewable. A reviewer may ask for
a real doc instead.

Run `scripts/install-git-hooks.sh` to enable local hooks (`.githooks/pre-commit` and
`.githooks/commit-msg`) so the gate runs before you push.
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio

## [Unreleased]

### Added

- Doc-gate now triggers on plain modifications (not only add/delete) for
behaviour-bearing trees: routes, installers, app-catalog, and auth_middleware.
A modified route file now requires `docs/agent-coordination.md` to be touched
in the same PR.

- A new CHANGELOG rule covers every code change under `tinyagentos/` or
`desktop/src/` that is not test-only: such changes require a `CHANGELOG.md`
edit or a new `changelog.d/` fragment.

- Agent-facing coverage is broadened: changes to the agent identity and scope
surface (`agent_scope_requests_store.py`, `agent_auth_requests.py`, and
related token-auth files) now require `docs/agent-manual/` to be touched.

### Changed

- The Docs-Reviewed trailer override is now logged in CI output: when a commit
carries the trailer, the gate prints the commit hash, author, and trailer
text. The escape hatch still works exactly as before.

## [1.0.0-beta.48] - 2026-08-11

### Added
Expand Down
53 changes: 44 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,24 +253,59 @@ Applied to your own changes:

## Documentation gate

A gate blocks PRs that add or remove certain feature code without a matching doc update. It only fires on structural changes (a file added or deleted), never on a plain edit, and only for a small set of conservative rules configured in `docs/doc-gate.toml`:
A gate blocks PRs that change feature code without a matching doc update, unless
a commit in the PR carries a non-empty `Docs-Reviewed: <why>` trailer. The gate
is configured in `docs/doc-gate.toml`; rules are data, no code changes needed to
add new ones.

### What triggers the gate

The gate fires on **additions, deletions, and modifications** (plain edits count)
for the following trees:

| Change | Requires editing one of |
|--------|--------------------------|
| A desktop app under `desktop/src/apps/` is added or removed | `README.md` |
| A route module under `tinyagentos/routes/` is added or removed | `docs/agent-coordination.md` |
| An installer under `tinyagentos/installers/` or `scripts/install*` is added or removed | `README.md` |
| A manifest under `app-catalog/` is added or removed | `README.md` |
| A route module under `tinyagentos/routes/` is added, removed, or modified | `docs/agent-coordination.md` |
| An installer under `tinyagentos/installers/` or `scripts/install*` is added, removed, or modified | `README.md` |
| A manifest under `app-catalog/` is added, removed, or modified | `README.md` |
| `tinyagentos/auth_middleware.py` is added, removed, or modified | `docs/agent-coordination.md` |

If your PR trips a rule and there is genuinely nothing to document (or you already covered it elsewhere), add a trailer line to a commit message instead of editing a doc:
In addition, **any non-test change under `tinyagentos/` or `desktop/src/`**
(docs and assets under those trees included, not only code) requires a
changelog entry: either a line in `CHANGELOG.md`
or a new file under `changelog.d/` (preferred, avoids merge conflicts on the
shared `[Unreleased]` anchor).

```
Agent-facing changes (agent registry, token auth, the scope-requests store,
`tinyagentos/routes/agent_*.py`, and the MCP surface) also require a doc:
either a page under `docs/agent-manual/` or `docs/agent-coordination.md`
satisfies the rule.

### Test-file exemption

Test files are never structural changes and do not trip the gate. This covers
Python `test_*.py` modules, co-located `__tests__/` directories, and frontend
`*.test.*` / `*.spec.*` files.

### Docs-Reviewed trailer

If your PR trips a rule and there is genuinely nothing to document (or you
already covered it elsewhere), add a trailer line to a commit message instead of
editing a doc:

```text
Docs-Reviewed: no user-facing change, internal refactor only
```

The trailer must have non-empty text after the colon; a bare `Docs-Reviewed:` does not count.
The trailer must have non-empty text after the colon; a bare `Docs-Reviewed:`
does not count. When CI detects the trailer it logs which commit used it and who
authored it, then passes all rules for that PR.

Run `scripts/install-git-hooks.sh` once to enable local hooks (`.githooks/pre-commit` and `.githooks/commit-msg`) so the gate runs before you push instead of after you open the PR. Local hooks are a convenience only: `.github/workflows/doc-gate.yml` is the authoritative check and runs on every PR regardless of local setup or `--no-verify`.
Run `scripts/install-git-hooks.sh` once to enable local hooks (`.githooks/pre-commit`
and `.githooks/commit-msg`) so the gate runs before you push instead of after you
open the PR. Local hooks are a convenience only: `.github/workflows/doc-gate.yml`
is the authoritative check and runs on every PR regardless of local setup or
`--no-verify`.

To add a new rule, edit `docs/doc-gate.toml` -- rules are data, no code changes needed.

Expand Down
35 changes: 25 additions & 10 deletions docs/doc-gate.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
# Read by scripts/check_doc_gate.py. Rules are data: add more [[rules]]
# entries here to cover new feature areas without touching the script.
#
# Precision over recall: `when_changed` only triggers a rule on a structural
# change (a file added or deleted, or a plain modification when the rule sets
# `on_modify = true`), and every glob below is scoped to the specific tree it
# names. This is deliberate -- a noisy gate gets disabled, so false positives
# are worse than an occasional missed doc update.
# Two trigger modes:
# when_changed -- fires on structural add/delete by default; set
# `on_modify = true` to also count plain modifications.
# require_doc -- any changed file matching these globs satisfies the rule.
#
# Test paths (test_*.py, *.test.*, *.spec.*, __tests__/) are never structural
# and are always excluded from triggering.
#
# The Docs-Reviewed: trailer is an explicit escape hatch: when present in a
# commit message, the gate prints which commit used it and who authored it,
# then passes all rules for that PR.

[gate]
trailer = "Docs-Reviewed:"
Expand Down Expand Up @@ -41,27 +47,31 @@ hint = "a desktop app was added or removed"

[[rules]]
name = "routes"
on_modify = true
when_changed = ["tinyagentos/routes/*.py"]
require_doc = ["docs/agent-coordination.md"]
hint = "an API route module was added or removed"
hint = "an API route module was added, removed, or modified"

[[rules]]
name = "installers"
on_modify = true
when_changed = ["tinyagentos/installers/*", "scripts/install*"]
require_doc = ["README.md"]
hint = "an installer was added or removed"
hint = "an installer was added, removed, or modified"

[[rules]]
# The repo's catalog tree is app-catalog/, not catalog/ (there is no
# catalog/ directory in this repo). Scoped to the real path so the rule can
# actually fire instead of being permanently dead config.
name = "catalog"
on_modify = true
when_changed = ["app-catalog/**"]
require_doc = ["README.md"]
hint = "a catalog manifest was added or removed"
hint = "a catalog manifest was added, removed, or modified"

[[rules]]
name = "agent-api"
on_modify = true
when_changed = ["tinyagentos/auth_middleware.py"]
require_doc = ["docs/agent-coordination.md"]
hint = "the agent-token route allowlist changed; update the agent-facing API surface (or add a Docs-Reviewed trailer explaining why not)"
Expand All @@ -81,18 +91,23 @@ hint = "the agent-token route allowlist changed; update the agent-facing API sur
# PR that merely touched the README satisfy the rule. Verified by probe: with the
# doc inside the directory the gate went green on a change carrying no changelog
# at all.
#
# Broadened to cover every NON-TEST change under tinyagentos/ or desktop/src/,
# not only code: a doc or asset under those trees triggers it too (the test
# suite pins tinyagentos/README.md as a trigger). Any such change requires a
# CHANGELOG.md edit or a new changelog.d/ fragment in the same PR.
[[rules]]
name = "user-visible-changelog"
on_modify = true
when_changed = ["tinyagentos/routes/*.py", "desktop/src/apps/*/**", "desktop/src/App.tsx", "desktop/src/components/**", "desktop/src/stores/**", "tinyagentos/installers/*"]
when_changed = ["tinyagentos/**", "desktop/src/**"]
require_doc = ["CHANGELOG.md", "changelog.d/*.md"]
hint = "user-visible behaviour changed; add a changelog.d/<pr>-<slug>.md fragment (preferred) or a CHANGELOG.md line (or add a Docs-Reviewed trailer explaining why not)"

# Agent-facing behaviour needs the compiled agent manual reviewed.
[[rules]]
name = "agent-manual"
on_modify = true
when_changed = ["tinyagentos/agent_registry*.py", "tinyagentos/agent_token_auth.py", "tinyagentos/routes/agent_*.py", "tinyagentos/mcp/**"]
when_changed = ["tinyagentos/agent_registry*.py", "tinyagentos/agent_token_auth.py", "tinyagentos/agent_scope_requests_store.py", "tinyagentos/routes/agent_*.py", "tinyagentos/mcp/**"]
require_doc = ["docs/agent-manual/*.md", "docs/agent-coordination.md"]
hint = "agent-facing behaviour changed; review the agent manual or coordination doc (or add a Docs-Reviewed trailer explaining why not)"

Expand Down
35 changes: 34 additions & 1 deletion scripts/check_doc_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,37 @@ def _git_commit_messages(base_ref: str) -> list[str]:
return [m for m in out.split("\x00") if m.strip()]


def _git_commits_with_messages(base_ref: str) -> list[tuple[str, str, str]]:
"""Return (hash, author_name, message_body) for each commit in the range."""
# %x1e terminates each commit record and %x1f separates the three fields
# inside it. A record terminator distinct from the field separator is what
# makes this parseable: with one separator for both, the flat split cannot
# tell a new commit's hash from the previous commit's body.
out = _run_git(["log", f"{base_ref}..HEAD", "--format=%H%x1f%an%x1f%B%x1e"])
commits: list[tuple[str, str, str]] = []
for record in out.split("\x1e"):
if not record.strip():
continue
fields = record.lstrip("\n").split("\x1f")
if len(fields) < 3:
continue
commit_hash, author, body = fields[0], fields[1], fields[2]
commits.append((commit_hash.strip(), author, body))
return commits


def _log_trailer_usage(commits: list[tuple[str, str, str]], trailer: str) -> None:
"""Print a log line for each commit that carries a non-empty trailer."""
for commit_hash, author, message in commits:
for line in message.splitlines():
stripped = line.strip()
if stripped.startswith(trailer) and stripped[len(trailer):].strip():
short_hash = commit_hash[:8]
why = stripped[len(trailer):].strip()
print(f"doc-gate: trailer override used in {short_hash} by {author}: {why}")
break


def get_trailer(config: dict) -> str:
"""Single source of truth for the commit-message trailer prefix, shared
by the diff-gate check and the hooks (via the print-trailer command)."""
Expand Down Expand Up @@ -365,7 +396,9 @@ def main(argv: list[str] | None = None) -> int:
commit_messages: list[str] = []
else:
changed = _git_changed_base(args.base)
commit_messages = _git_commit_messages(args.base)
commits_meta = _git_commits_with_messages(args.base)
commit_messages = [msg for _hash, _author, msg in commits_meta]
_log_trailer_usage(commits_meta, get_trailer(config))

failures = evaluate_rules(changed, commit_messages, config)
return _report(failures)
Expand Down
Loading
Loading