feat: add doc-sentinel plugin - #36
Merged
Merged
Conversation
New plugin that scans docs for drift against the codebase, runs automatically via post-commit and stop hooks, and resolves findings through a drift-resolver agent.
…ut fixes
Consolidated fixes making the Stop hook reliably auto-dispatch the
drift-resolver subagent, plus portability/correctness fixes discovered
during debugging. Bumps plugin to 1.0.5.
Bugs fixed:
1. systemMessage placement — Stop hook was emitting systemMessage nested
under hookSpecificOutput; Stop hooks need it at the top level.
2. systemMessage is informational — even at the top level, systemMessage
is shown as a UI status line and is NOT auto-injected into the model's
next-turn context, so the dispatch directive never reached Claude.
3. decision:"block" JSON unreliable — the JSON payload parses fine but
the installed Claude Code version did not actually block on it.
4. exit 2 + stderr is the universal blocking contract — emit_block now
does `printf '%s\n' "$reason" >&2; exit 2`. Session marker write moved
before emit_block since exit 2 short-circuits.
5. Session marker safety valve — .doc-sentinel-last-session records the
session_id from the hook stdin envelope and blocks at most once per
session, preventing infinite loops if the drift-resolver can't fully
clear the drift file.
6. timeout units — hooks.json `timeout` is seconds, not ms; corrected
5000 → 10 on both PostToolUse and Stop hooks.
7. CWD handling — hooks may run from any subdirectory. Both scripts now
resolve $CLAUDE_PROJECT_DIR with a `git rev-parse --show-toplevel`
fallback and prefix DRIFT_FILE / CONFIG_FILE / DOC_ROOT with
$PROJECT_ROOT.
8. BSD sed delimiter bug — post-commit-drift.sh glob→regex conversion
used `/` as sed delimiter with `/` in the replacement, which fails on
macOS sed. Switched to `#` delimiter.
9. Directive wording — Stop hook reason now includes a numbered 5-step
plan telling Claude exactly how to invoke the drift-resolver and to
commit with a `docs:` prefix (the post-commit hook already skips
`^docs(\(.+\))?:` commits, closing the feedback loop).
Test plan:
- Empty project: `echo '{"session_id":"x"}' | CLAUDE_PROJECT_DIR=/tmp/fake \
bash stop-drift-report.sh` → exit 0, silent.
- Populated drift file: exit 2, full directive on stderr.
- Second call same session_id: exit 0 (marker honored).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
doc-sentinelplugin — a proactive documentation drift detector — and consolidates the fix work required to make its Stop hook reliably auto-dispatch thedrift-resolversubagent. Plugin version is 1.0.5.The first commit adds the plugin. The second commit collapses a multi-stage debugging session into a single fix covering bugs #1-#9 below.
Bugs fixed in 1.0.1 → 1.0.5
systemMessageplacement — Stop hook was emittingsystemMessagenested underhookSpecificOutput; Stop hooks require it at the top level.systemMessageis informational only — even at the top level,systemMessagerenders as a UI status line and is not injected into the model's next-turn context. Empirically confirmed: the user had to paste the directive manually for Claude to act on it.decision: "block"JSON is unreliable — 1.0.4 emitted{"decision":"block","reason":"..."}; the payload parses fine but the installed Claude Code version did not actually block the Stop. Per the docs both mechanisms should work; only the older one does in practice.emit_blocknow doesprintf '%s\n' "$reason" >&2; exit 2. This is the same mechanism PreToolUse hooks use. The session-marker write happens beforeemit_blocksinceexit 2short-circuits..doc-sentinel-last-sessionrecords thesession_idfrom the hook stdin envelope and blocks at most once per session, so a drift-resolver run that can't fully clear the drift file can't trap Claude in a loop.hooks.jsontimeoutis seconds, not milliseconds. Corrected5000→10on both PostToolUse and Stop hooks.$CLAUDE_PROJECT_DIRwith agit rev-parse --show-toplevelfallback and prefixDRIFT_FILE/CONFIG_FILE/DOC_ROOTwith$PROJECT_ROOT.post-commit-drift.shglob→regex conversion used/as the sed delimiter with a/in the replacement, which fails on macOS sed. Switched to#.docs:prefix. The post-commit hook already skips^docs(\(.+\))?:commits, which closes the feedback loop.Why exit-2 (empirical evidence)
Per the Claude Code docs, Stop hooks expose two blocking mechanisms: JSON
{"decision":"block","reason":"..."}on stdout, or exit code 2 with the reason on stderr. In the version of Claude Code currently installed, only the exit-2 mechanism actually prevents the model from stopping — the JSON payload is parsed without error but the Stop proceeds anyway. Exit-2 is the older universal contract and matches how PreToolUse hooks block tool calls, so the Stop hook now uses it exclusively.Test plan
echo '{"session_id":"x"}' | CLAUDE_PROJECT_DIR=/tmp/fake bash plugins/doc-sentinel/hooks/stop-drift-report.sh→ exit 0, silent.session_id: exit 0 (marker honored, no re-block).docs:prefix, let the Stop hook fire, verify Claude dispatches the drift-resolver subagent autonomously.Plugin version
1.0.5in bothplugins/doc-sentinel/.claude-plugin/plugin.jsonand.claude-plugin/marketplace.json. No release tag — that's a manual step after merge.