Skip to content

Making the communication rule checkable โ€‹

Issue: #950

The voice rule in .agents/rules/project/communication-voice.md states three things as absolute and had no way to tell whether any of them held. This adds one: a stated mode in the message, and a script that reads the transcript afterwards.

Why post-hoc, and not a hook โ€‹

Every other guardrail in this repo runs at write time, because every other guardrail watches a tool call. A message to the operator is not a tool call. There is no PreToolUse surface to attach to, no file being written, nothing for a hook to inspect. The transcript is the only place a sent message exists as data, and it only exists after the fact.

So the check is post-hoc by design, not by compromise.

The three checks โ€‹

tooling/scripts/check-transcript-voice.mjs reads a session transcript, walks every assistant text block the operator actually saw, and reports file:record:line rule detail. It exits 1 on a hit.

CheckWhat it flagsWhat passes
bare-issue-numberA number the reader has to decode alone: see #887, PR #845, Closes #807, a link labelled only [#950]A title with the number after it
unlinked-documentA .md or .mdc name in prose with no link, backticked ones includedThe name inside a markdown link, or inside a fenced code block
status-over-budgetA status running past 23 linesAnything in explanation mode, at any length

Sidechain records are skipped: a subagent talking to its parent is not a message to a person. Thinking blocks and tool calls are skipped for the same reason.

Two strictnesses, because the literal rule is noisier than the intent โ€‹

The rule writes the good form as a title with the number in parentheses after. Held to the letter, trademark #140 fails alongside Closes #807, and a run over 16 real transcripts produced 572 hits across 620 messages. A checker at that volume gets ignored.

So the default passes a number preceded by a real word and fails one preceded by a connector (issue, PR, see, that, closes, and the rest of the list in the script). That is the rule's intent: the reader can decode it. --strict drops the connector allowance and holds to the exact stated form, and also fails a message with no stated mode.

ModeBare numbersUnlinked docsOver-budget statusNo stated mode
default42213337reported, does not fail
--strict57213337620

Both columns are the same 16 transcripts, all of them predating the rule, so the counts are a baseline rather than a verdict.

The stated mode โ€‹

One line was added to the voice rule requiring a message to open with Mode: status or Mode: explanation.

It fixes the failure that recurred on 2026-08-22, short questions drawing explanation-length answers, and it is what makes the length check honest. Without a declaration the script has to infer the mode from whether a TLDR heading happens to be present, which misses a status-shaped answer to a question, the exact case that recurred. With a declaration the script reads it, and says so in the finding when it had to guess instead.

Where the line reaches, and where it does not โ€‹

npm run sync:agents regenerates four surfaces. The line appears verbatim in three:

SurfaceCarries the line
.github/instructions/communication-voice.instructions.mdyes
.cursor/rules/communication-voice.mdcyes
.claude/output-styles/operator-voice.mdyes
AGENTS.mdno, it renders as a router pointer

The voice rule carries agentsMd: router in its frontmatter, so AGENTS.md gets a one-paragraph pointer instead of the body. That is deliberate, and it means Gemini reaches the mode line only by following the pointer to the source. The router's own summary still says "know whether you are in status mode or explanation mode", which now understates it. Left alone, because changing it is a change to the voice rule and this project's brief was one line.

Deliberately not a gate โ€‹

Not in npm run validate, not in CI, no npm script. Transcripts live under ~/.claude/projects/ on one machine, per session. A shared gate would have nothing to read. Run it by hand:

bash
node tooling/scripts/check-transcript-voice.mjs            # newest session
node tooling/scripts/check-transcript-voice.mjs --all      # every session for this repo
node tooling/scripts/check-transcript-voice.mjs --strict

Open questions โ€‹

  • Does the mode line survive contact with a running session? A session already running keeps the output style it booted with, so this only binds sessions started after it lands.
  • Is the connector list the right cut? It is a judgement call made against 16 transcripts on one machine, and it is the piece most likely to need tuning.
  • Should the router summary in AGENTS.md name the mode line? Out of scope here, worth deciding before #952 merges.

Built with VitePress