Skip to content
Open
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
6 changes: 6 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
"source": "./plugins/bitwarden-testing-tools",
"version": "1.0.0",
"description": "Testing tools for Bitwarden — analyzing and improving test quality across its repositories."
},
{
"name": "bitwarden-doc-parity",
"source": "./plugins/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."
}
]
}
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"cvss",
"Dashlane",
"dast",
"diceware",
"docstrings",
"dread",
"duedate",
Expand Down Expand Up @@ -97,6 +98,7 @@
"pushback",
"pyproject",
"pytest",
"rawfile",
"remotelink",
"Rescope",
"resolutiondate",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A curated collection of plugins for AI-assisted development at Bitwarden. Enable
| [bitwarden-designer](plugins/bitwarden-designer/) | 0.1.0 | Product designer persona: Code of Conduct and 30/60/90 critique, critique facilitation; dispatches into bitwarden-design-tools |
| [bitwarden-design-tools](plugins/bitwarden-design-tools/) | 0.1.0 | Design toolkit: content style guide, Figma Dev Mode MCP, Bitwarden brand application, handoff prep, Design System governance, Product and Design Jira |
| [bitwarden-devops-engineer](plugins/bitwarden-devops-engineer/) | 0.1.5 | DevOps engineering assistant: workflow compliance linting, action security auditing, and org-wide CI/CD remediation |
| [bitwarden-doc-parity](plugins/bitwarden-doc-parity/) | 1.0.0 | Keeps docs current with the code they describe: session instructions, a Stop-hook tripwire, a semantic verification skill, and a CI review face |
| [bitwarden-init](plugins/bitwarden-init/) | 1.2.2 | Initialize and enhance CLAUDE.md files with Bitwarden's standardized template format |
| [bitwarden-product-analyst](plugins/bitwarden-product-analyst/) | 0.1.5 | Product analyst agent for creating comprehensive Bitwarden requirements documents from multiple sources |
| [bitwarden-security-engineer](plugins/bitwarden-security-engineer/) | 1.3.0 | Application security engineering: vulnerability triage, threat modeling, and secure code analysis |
Expand Down
19 changes: 19 additions & 0 deletions plugins/bitwarden-doc-parity/.claude-plugin/plugin.json
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"
]
}
13 changes: 13 additions & 0 deletions plugins/bitwarden-doc-parity/CHANGELOG.md
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.
51 changes: 51 additions & 0 deletions plugins/bitwarden-doc-parity/README.md
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.

Copy link
Copy Markdown
Contributor

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-review version that does not exist yet.

Details and fix

bitwarden-code-review is at 1.13.1 in .claude-plugin/marketplace.json (this PR does not change it), and nothing in plugins/bitwarden-code-review/ references verifying-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-review reviewer agent is planned; until then, wire the skill into your review 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.
151 changes: 151 additions & 0 deletions plugins/bitwarden-doc-parity/hooks/doc-parity-check.sh
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ DEBT: The tripwire now arms on CLAUDE.md, but the skill it hands off to does not treat that as a documented scope.

Details and fix

The comment on line 113-114 says CLAUDE.md aligns "with SKILL.md's scope definition", but SKILL.md:31 defines a documented scope as a directory containing "a README.md, a docs/ directory, or diagram sources" — CLAUDE.md appears nowhere in SKILL.md (or in the plugin README.md).

Consequence for a directory that carries a CLAUDE.md but no README.md/docs/ (common for component-level agent guidance in monorepos): the hook blocks and names that directory under "Documented scopes involved", then Step 2 of the skill does not enumerate it, so Step 4's per-scope report cannot attest to the scope the block called out. The developer gets a gate with nothing to answer.

The reverse gap exists too: SKILL.md counts bare diagram sources as a documented scope, while documented_ancestors only recognizes README/CLAUDE.md/docs/.

Pick one definition and use it in both places — either add CLAUDE.md to SKILL.md:31, or drop it from line 120. Either way, the file-header trigger rule on lines 7-8 ("contains a README.md or a docs/ directory") is now stale and should match.

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 }'
24 changes: 24 additions & 0 deletions plugins/bitwarden-doc-parity/hooks/doc-parity-context.sh
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
26 changes: 26 additions & 0 deletions plugins/bitwarden-doc-parity/hooks/hooks.json
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
}
]
}
]
}
}
Loading
Loading