-
Notifications
You must be signed in to change notification settings - Fork 15
Feat/doc parity plugin #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
25207b2
11973b9
7df6ee4
5fd746f
ac8b594
c4aa127
feb01ac
ae4746d
3c32d17
fe7972c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "bitwarden-doc-parity", | ||
| "version": "1.0.0", | ||
| "description": "Enforces the documentation standard's base obligations — docs and diagrams update in the same change as the code they describe. Session instructions, a Stop-hook tripwire, a semantic verification skill, and a CI review face.", | ||
| "author": { | ||
| "name": "Bitwarden", | ||
| "url": "https://github.com/bitwarden" | ||
| }, | ||
| "homepage": "https://github.com/bitwarden/ai-plugins/tree/main/plugins/bitwarden-doc-parity", | ||
| "repository": "https://github.com/bitwarden/ai-plugins", | ||
| "keywords": [ | ||
| "documentation", | ||
| "doc-parity", | ||
| "doc-drift", | ||
| "hooks", | ||
| "standards", | ||
| "review" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to the bitwarden-doc-parity plugin will be documented in this file. | ||
|
|
||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [1.0.0] - 2026-08-04 | ||
|
|
||
| ### Added | ||
|
|
||
| - Initial release of the `bitwarden-doc-parity` plugin, enforcing the documentation standard's base obligations to monitor for drift between code and documentation. Operates within-repo only when fired as a stop-hook and includes out-of-repo validation against contributing-docs when being executed in a PR review. | ||
| - Behavior evals for `verifying-doc-parity` against the bitwarden/server Seeder subsystem as the first ground-truth corpus. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # bitwarden-doc-parity | ||
|
|
||
| Enforces the [documentation standard's](https://contributing.bitwarden.com/contributing/documentation) requirement that docs and diagrams update in the same change as the code they describe. Changes that invalidate out-of-repo docs get called out at review. | ||
|
|
||
| ## Design | ||
|
|
||
| Four layers. Each layer covers the weakness of the one before it. | ||
|
|
||
| ### Layer 1: instruction fragment | ||
|
|
||
| A `SessionStart` hook injects the [base documentation obligations](./hooks/doc-parity-instructions.md) into every session. | ||
|
|
||
| The fragment closes with a pointer to the published standard. This layer is prevention, so most sessions never reach the gate. | ||
|
|
||
| ### Layer 2: Stop hook | ||
|
|
||
| A [deterministic tripwire](./hooks/doc-parity-check.sh) on the session-end event. The hook's scope is in-repo documentation only, since a file-path tripwire can only see the tree; out-of-repo docs belong to the CI face. | ||
|
|
||
| The hook blocks once per session and its message directs the agent to run the verification skill. | ||
|
|
||
| ### Layer 3: semantic verification skill | ||
|
|
||
| `verifying-doc-parity` is the intelligence the hook invokes, also invocable on demand: read the diff, then read the documentation at every documented ancestor of the change, since references may occur at any level. | ||
|
|
||
| The outcome, per documented scope, is either an update or an explicit attestation that nothing documented there drifted. | ||
|
|
||
| ### Layer 4: CI face | ||
|
|
||
| The same skill logic run as a PR reviewer through the consuming repository's ai-review workflow, covering human-authored changes that no in-session layer sees. This layer also owns out-of-repo discovery. Search terms are derived from the diff and used to search contributing-docs for references that need to be addressed. The callout triggers the standard's external-docs flow: a work item before merge and a stale marker on the page. | ||
|
|
||
| This layer is inert until the review pipeline invokes `verifying-doc-parity`. When installed alongside `bitwarden-code-review` (from 1.14.0), the reviewer agent's Cross-Plugin Enrichment picks the skill up automatically. If the consuming repository uses a different review workflow, wire the skill into that workflow's documentation pass. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| /plugin install bitwarden-doc-parity@bitwarden-marketplace | ||
| ``` | ||
|
|
||
| Restart Claude Code after installing so the hooks load. | ||
|
|
||
| ## Usage | ||
|
|
||
| The plugin needs no invocation in normal work: the fragment loads at session start, and the Stop hook fires only when the trigger rule matches. When the hook blocks, follow its message and run the skill. To verify documentation parity on demand: | ||
|
|
||
| ```text | ||
| Do I need to update any docs for my current changes? | ||
| ``` | ||
|
|
||
| ## Requirements | ||
|
|
||
| - `bash`, `git`, and `jq` on `PATH`. The hooks fail open: when a requirement is missing or the working directory is not a git repository, sessions proceed unaffected. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| #!/bin/bash | ||
| # doc-parity-check.sh | ||
| # Stop hook: deterministic tripwire for stale in-repo documentation. | ||
| # | ||
| # Trigger rule: fire when a changed file has a documented ancestor scope | ||
| # below the repo root and no documentation along its ancestor chain was | ||
| # touched. A directory is a documented scope when it contains a README.md | ||
| # or a docs/ directory. Root-scope docs do not arm the tripwire, since the | ||
| # root is an ancestor of every file; their verification belongs to the | ||
| # semantic layer. | ||
| # | ||
| # Behavior: blocks exactly once per session with a message directing the | ||
| # agent to the verifying-doc-parity skill, then allows through. | ||
| # Generalizes bitwarden/server's .claude/hooks/seeder-docs-check.sh. | ||
| # | ||
| # Fail-open: any environment problem (no jq, no git repo) lets the turn end without blocking. | ||
|
|
||
| set -uo pipefail | ||
|
|
||
| command -v jq >/dev/null 2>&1 || exit 0 | ||
|
|
||
| INPUT=$(cat) | ||
|
|
||
| # Guard: if a Stop hook already blocked this turn, allow through. | ||
| STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false') | ||
| if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Guard: block at most once per session. Markers live under a plugin-owned | ||
| # subdirectory of TMPDIR to keep them off the world-writable top level, and | ||
| # the -O check skips markers we do not own so a stray file cannot silently | ||
| # suppress the session's only block. | ||
| SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' | tr -cd 'a-zA-Z0-9_-') | ||
| MARKER="" | ||
| if [[ -n "$SESSION_ID" ]]; then | ||
| MARKER_DIR="${TMPDIR:-/tmp}/doc-parity" | ||
| mkdir -p "$MARKER_DIR" 2>/dev/null || true | ||
| MARKER="${MARKER_DIR}/blocked-${SESSION_ID}" | ||
| if [[ -f "$MARKER" && -O "$MARKER" ]]; then | ||
| exit 0 | ||
| fi | ||
| fi | ||
|
|
||
| CWD=$(echo "$INPUT" | jq -r '.cwd // empty') | ||
| if [[ -z "$CWD" ]]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| REPO_ROOT=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null) || exit 0 | ||
|
|
||
| # Gather all changed files (staged, unstaged, and untracked) relative to repo root. | ||
| # core.quotePath=false so non-ASCII filenames come through unquoted and match the | ||
| # filesystem checks below. | ||
| DIFF_HEAD=$(git -C "$REPO_ROOT" -c core.quotePath=false diff --name-only HEAD 2>/dev/null || true) | ||
| UNTRACKED=$(git -C "$REPO_ROOT" -c core.quotePath=false ls-files --others --exclude-standard 2>/dev/null || true) | ||
| ALL_CHANGED=$(printf "%s\n%s" "$DIFF_HEAD" "$UNTRACKED" | sort -u | grep -v '^$' || true) | ||
|
|
||
| if [[ -z "$ALL_CHANGED" ]]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # A file is documentation when it is a markdown or diagram source file, or | ||
| # when it lives under a docs/ directory. | ||
| is_doc_file() { | ||
| case "$1" in | ||
| *.md | *.mdx | *.mmd | *.mermaid) return 0 ;; | ||
| docs/* | */docs/*) return 0 ;; | ||
| esac | ||
| return 1 | ||
| } | ||
|
|
||
| # The scope a documentation file describes: the directory holding it, or the | ||
| # parent of its docs/ directory when it lives under one. | ||
| doc_scope() { | ||
| local path="$1" | ||
| case "$path" in | ||
| docs/*) echo "." ;; | ||
| */docs/*) echo "${path%%/docs/*}" ;; | ||
| *) dirname "$path" ;; | ||
| esac | ||
| } | ||
|
|
||
| # Partition the change set and collect the scope of every touched doc. | ||
| CHANGED_CODE="" | ||
| TOUCHED_DOC_SCOPES="" | ||
| while IFS= read -r file; do | ||
| if is_doc_file "$file"; then | ||
| TOUCHED_DOC_SCOPES="${TOUCHED_DOC_SCOPES}$(doc_scope "$file")"$'\n' | ||
| else | ||
| CHANGED_CODE="${CHANGED_CODE}${file}"$'\n' | ||
| fi | ||
| done <<<"$ALL_CHANGED" | ||
|
|
||
| if [[ -z "$CHANGED_CODE" ]]; then | ||
| exit 0 | ||
| fi | ||
| TOUCHED_DOC_SCOPES=$(printf '%s' "$TOUCHED_DOC_SCOPES" | sort -u) | ||
|
|
||
| # A touched doc covers a file when the doc's scope is an ancestor of the file. | ||
| is_covered() { | ||
| local file="$1" scope | ||
| while IFS= read -r scope; do | ||
| [[ -z "$scope" ]] && continue | ||
| if [[ "$file" == "$scope/"* ]]; then | ||
| return 0 | ||
| fi | ||
| done <<<"$TOUCHED_DOC_SCOPES" | ||
| return 1 | ||
| } | ||
|
|
||
| # Documented ancestor scopes of a file, strictly below the repo root. | ||
| # Match README case-insensitively; CLAUDE.md and docs/ are the other markers, | ||
| # aligning with SKILL.md's scope definition. | ||
| documented_ancestors() { | ||
| local dir | ||
| dir=$(dirname "$1") | ||
| while [[ "$dir" != "." && "$dir" != "/" ]]; do | ||
| if compgen -G "$REPO_ROOT/$dir/[Rr][Ee][Aa][Dd][Mm][Ee].md" >/dev/null 2>&1 \ | ||
| || [[ -f "$REPO_ROOT/$dir/CLAUDE.md" || -d "$REPO_ROOT/$dir/docs" ]]; then | ||
|
Comment on lines
+112
to
+120
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♻️ DEBT: The tripwire now arms on Details and fixThe comment on line 113-114 says Consequence for a directory that carries a The reverse gap exists too: Pick one definition and use it in both places — either add |
||
| echo "$dir" | ||
| fi | ||
| dir=$(dirname "$dir") | ||
| done | ||
| } | ||
|
|
||
| VIOLATIONS="" | ||
| VIOLATION_SCOPES="" | ||
| while IFS= read -r file; do | ||
| [[ -z "$file" ]] && continue | ||
| ancestors=$(documented_ancestors "$file") | ||
| [[ -z "$ancestors" ]] && continue | ||
| if ! is_covered "$file"; then | ||
| VIOLATIONS="${VIOLATIONS} - ${file}"$'\n' | ||
| VIOLATION_SCOPES="${VIOLATION_SCOPES}${ancestors}"$'\n' | ||
| fi | ||
| done <<<"$CHANGED_CODE" | ||
|
|
||
| if [[ -z "$VIOLATIONS" ]]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| VIOLATION_SCOPES=$(printf '%s' "$VIOLATION_SCOPES" | sort -u | sed 's/^/ - /') | ||
|
|
||
| if [[ -n "$MARKER" ]]; then | ||
| touch "$MARKER" 2>/dev/null || true | ||
| fi | ||
|
|
||
| REASON=$(printf 'Code changed inside documented scopes, but no documentation along the changed files'\'' ancestor chains was touched.\n\nChanged files without a documentation update:\n%s\nDocumented scopes involved:\n%s\n\nRun the bitwarden-doc-parity:verifying-doc-parity skill now: read the diff, then verify or update the documentation at every documented ancestor scope of the change. If nothing documented at a scope drifted, say so explicitly per scope. Do not make a token documentation edit to satisfy this check.' "$VIOLATIONS" "$VIOLATION_SCOPES") | ||
|
|
||
| jq -n --arg reason "$REASON" '{ "decision": "block", "reason": $reason }' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| #!/bin/bash | ||
| # doc-parity-context.sh | ||
| # SessionStart hook: injects the base documentation obligations into every | ||
| # session as additional context, so no repo has to hand-carry them in its | ||
| # root CLAUDE.md. The canonical obligation text lives in | ||
| # doc-parity-instructions.md next to this script. | ||
| # | ||
| # Fail-open: if the fragment or jq is unavailable, the session proceeds | ||
| # without the context rather than erroring. | ||
|
|
||
| set -uo pipefail | ||
|
|
||
| FRAGMENT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/doc-parity-instructions.md" | ||
|
|
||
| if [[ ! -f "$FRAGMENT" ]] || ! command -v jq >/dev/null 2>&1; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| jq -n --rawfile ctx "$FRAGMENT" '{ | ||
| hookSpecificOutput: { | ||
| hookEventName: "SessionStart", | ||
| additionalContext: $ctx | ||
| } | ||
| }' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Documentation parity obligations | ||
|
|
||
| Before modifying code, read in-code documentation associated with it, any `README.md` or `docs/`, and any diagrams that may reference it. When you change code that has associated documentation or diagrams, update that documentation in the same change. If behavior contradicts documentation and the code is correct, fix the documentation. Do not leave them disagreeing. | ||
|
|
||
| These obligations come from the documentation standard: https://contributing.bitwarden.com/contributing/documentation |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/doc-parity-context.sh\"", | ||
| "timeout": 5 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "Stop": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/doc-parity-check.sh\"", | ||
| "timeout": 15 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
♻️ DEBT: This documents automatic reviewer pickup against a
bitwarden-code-reviewversion that does not exist yet.Details and fix
bitwarden-code-reviewis at1.13.1in.claude-plugin/marketplace.json(this PR does not change it), and nothing inplugins/bitwarden-code-review/referencesverifying-doc-parity— its Cross-Plugin Enrichment section enumerates specific skills by name, so there is no generic pickup path.The PR description confirms the wiring is a planned follow-up, but the sentence reads as present tense for anyone installing today: "(from 1.14.0), the reviewer agent's Cross-Plugin Enrichment picks the skill up automatically." A reader installing both plugins now would assume Layer 4 is live and get no review coverage.
Either land the enrichment entry (plus version bump) in this PR, or mark the integration as planned, e.g. "Automatic pickup by the
bitwarden-code-reviewreviewer agent is planned; until then, wire the skill into your review workflow's documentation pass."