upstream-docs
v0.4.0
Published
Claude Code plugin: enforce PRD/ADR before feature development
Maintainers
Readme
upstream
A Claude Code plugin that enforces PRD and ADR documentation before feature development begins.
upstream installs a hook into Claude Code that detects feature work and blocks it until a Product Requirements Document (PRD) exists. If the feature introduces architectural decisions — a new external dependency, a database migration, an API contract change — it also requires an Architecture Decision Record (ADR).
This keeps your team's reasoning in the repository, not scattered across memory.
How it works
After setup (via upstream init or claude plugin install upstream), the first prompt on a feature branch in each Claude Code session gets a context injection:
UPSTREAM: feature detected without PRD. Invoke upstream-guard before continuing.The hook fires at most once per session (tracked via a PPID-based cache file in /tmp). Subsequent prompts in the same session are silent.
Claude then runs the upstream-guard skill, which:
- Classifies the work — feature, bug, fix, chore, or incident
- Checks for a PRD — by filename or content match in
<docs_path>/ - Checks for an ADR — if the PRD describes architectural decisions
- Releases to development once docs are in place
PRDs and ADRs can be created in four ways: imported from an existing document, generated through a short interview, auto-drafted from git context, or linked to an external tool (Confluence, Google Docs).
Bypass branches (fix/, hotfix/, chore/, docs/) are skipped automatically.
Quick start
Option A — Claude Code plugin (personal install)
claude plugin install upstreamSkills and hook are loaded automatically. Add upstream.config.yaml to any repo you want guarded (see Configuration).
Option B — npm + upstream init (team setup)
# Install globally
npm install -g upstream-docs
# In your repo (platform engineer runs this once)
cd my-project
upstream init
git add .
git commit -m "feat: add upstream Claude Code plugin"
git pushupstream init runs an interactive wizard that configures upstream.config.yaml, scaffolds .claude/, optionally sets up a CODEOWNERS guardian, and automatically updates .gitignore to exclude .env/.env.local/.env.test.
Your team gets the plugin on their next git pull. No global install required on their machines — Claude Code picks up .claude/ automatically.
Which to choose? Use the plugin install for personal use across your own projects. Use
upstream initwhen your team should all be gated — the config and hook live in the repo and are enforced for everyone.
CLI reference
| Command | Description |
| --- | --- |
| upstream init | Interactive wizard: scaffold upstream into the current repo |
| upstream init --yes | Non-interactive: scaffold with all defaults |
| upstream init --from answers.json | Non-interactive: load all answers from a JSON file |
| upstream upgrade | Regenerate skills and hook, preserve config and docs |
| upstream auth google-docs | Connect Google Docs via OAuth (PKCE) |
| upstream auth confluence | Connect Confluence via OAuth (PKCE) |
| upstream auth status | Show authentication status for all providers |
| upstream logout <provider> | Remove stored token for a provider (or all) |
| upstream doctor | Check upstream installation health in the current repo |
| upstream doctor --fix | Repair missing or misconfigured files automatically |
| upstream status | Show PRD/ADR state for the current git branch |
| upstream list | Show PRD/ADR coverage for all local feature branches |
| upstream list --format json | Machine-readable coverage output ({ branches, unlinked }) |
| upstream validate | Check whether the current branch diff is aligned with its PRD/ADR |
| upstream validate --format json | Machine-readable alignment result ({ verdict, engine, findings }) |
| upstream validate --base <branch> | Override the base branch used for diff |
| upstream validate --report [path] | Write a JSON report artifact to disk (default: upstream-report.json) |
| upstream report summary | Print a Markdown summary of the last report artifact to stdout |
| upstream report summary --input <path> | Read report from a custom path |
| upstream stats | Show PRD/ADR coverage summary across all feature branches |
| upstream stats --format json | Machine-readable stats output |
| upstream stats --trend | Compare current coverage against the latest snapshot |
| upstream stats --adoption | Team adoption analytics — per-author coverage, skip log, and adoption score |
| upstream stats --adoption --since <date> | Scope adoption lookback window (default: 90 days, YYYY-MM-DD) |
| upstream stats --adoption --no-authors | Show skip log and adoption score without per-author table |
| upstream snapshot | Save current coverage stats as a local snapshot |
| upstream snapshot --ci | Exit non-zero if coverage regressed since last snapshot |
| upstream mcp | Start the upstream MCP server (called automatically by Claude Code) |
upstream init flags
| Flag | Description |
| --- | --- |
| --from <file> | Load answers from a JSON file (non-interactive, for CI/scripts) |
| --docs-storage <value> | local or link |
| --provider <id> | google-docs or confluence |
| --client-id <id> | OAuth client_id for the provider |
| --allowed-domain <domain> | Allowed domain (e.g. acme.com) |
| --guardian <handle> | GitHub handle or email written to .github/CODEOWNERS |
| --yes | Skip interactive Phase 2 (use org defaults) |
Configuration
upstream.config.yaml is created in your repo root on init. All fields have defaults.
version: 1
# Branch prefixes that bypass all checks
bypass_for:
- fix/
- hotfix/
- chore/
- docs/
# Fields that must be present in every PRD
prd_required_fields:
- problem_statement
- success_metrics
- out_of_scope
# Conditions that require an ADR
adr_triggers:
- new_external_dependency
- database_schema_change
- api_breaking_change
- infrastructure_change
- auth_change
# Directory for PRDs, ADRs, and the skip log
docs_path: docs/upstream/
# 'local': full document content in this repo
# 'link': stub file with URL; actual doc lives externally
docs_storage: local
# Alignment check (used by upstream validate)
align:
base_branch: main # branch to diff against
on_violation: warn # 'warn' (default) or 'block' (exits 1 when misaligned)
post_pr_comment: true # post verdict as a PR comment when GITHUB_TOKEN is setLink mode — external docs (Confluence, Google Docs)
If your team stores PRDs and ADRs in an external tool, set docs_storage: link. upstream saves a small stub file with the document URL and metadata instead of full content:
# PRD: user-auth
- **Status:** Linked
- **Storage:** external
- **Document:** https://docs.google.com/document/d/...
- **Date:** 2026-06-12OAuth and PKCE
upstream uses PKCE (Proof Key for Code Exchange, RFC 7636) for all OAuth flows. Only client_id and allowed_domain are stored in upstream.config.yaml — never in version control.
Both Google Docs and Confluence require a client_secret — neither supports public OAuth clients. Secrets are stored as env vars (UPSTREAM_GOOGLE_CLIENT_SECRET, UPSTREAM_CONFLUENCE_CLIENT_SECRET) and loaded at CLI startup. They are never stored in upstream.config.yaml or committed to the repo. upstream init writes the placeholder to .env, .env.local, and .env.example automatically.
Google Docs integration
Setup (platform engineer, done once per org):
- Create a project at console.cloud.google.com
- Enable the Google Drive API
- Create an OAuth 2.0 Client ID → type: Desktop app
- Desktop app type allows localhost automatically — no redirect URI configuration needed
- Copy the Client secret from the credential detail page
- Add
client_idandallowed_domaintoupstream.config.yamland commit:
integrations:
google_docs:
client_id: "xxx.apps.googleusercontent.com"
allowed_domain: "yourcompany.com"Add the client_secret to your .env / .env.local — never commit this file:
UPSTREAM_GOOGLE_CLIENT_SECRET=your-secret-here
upstream initcreates the placeholder automatically when you choose Google Docs during setup.
Each developer authenticates once:
upstream auth google-docsConfluence integration
Setup (platform engineer, done once per org):
- Create an app at developer.atlassian.com/console/myapps
- Enable OAuth 2.0 (3LO)
- In Authorization, add callback URL:
http://localhost:27182/callback - In Permissions, add scopes:
read:confluence-content.all,write:confluence-contentoffline_accessis not listed in Permissions — upstream requests it automatically at auth time
- Copy the Client ID and Client secret from the app's Settings tab
- Add
client_idandallowed_domaintoupstream.config.yamland commit:
integrations:
confluence:
client_id: "your-client-id"
allowed_domain: "yourorg.atlassian.net"
allowed_domainmust be your Atlassian site subdomain (e.g.acme.atlassian.net), not your company domain.
Add the client_secret to your .env / .env.local — never commit this file:
UPSTREAM_CONFLUENCE_CLIENT_SECRET=your-secret-here
upstream initcreates the placeholder in.env,.env.local, and.env.exampleautomatically.
Each developer authenticates once:
upstream auth confluenceEnforcement policy
Platform engineers can restrict which tools are accepted and require validation before a link can be saved:
link_policy:
allowed_providers: # only accept links from these tools
- google-docs
require_validation: true # block unvalidated links (e.g. unauthenticated)CODEOWNERS guardian
During upstream init, you can designate a GitHub handle or email as the guardian for upstream.config.yaml. upstream writes a .github/CODEOWNERS entry — any PR that modifies the config requires guardian approval.
Note: CODEOWNERS is only enforced when branch protection is enabled on the repository.
Alignment check — upstream validate
upstream validate checks whether the current branch's diff is aligned with its PRD and ADR. It runs automatically in CI via the scaffolded upstream-align.yml workflow, and can also be run locally.
upstream validate # human-readable output
upstream validate --format json # machine-readable (for CI scripts)Engines:
- LLM — uses
claude -pto analyse the diff against the PRD/ADR; provides a natural-language summary and per-dimension findings - Heuristic — fallback when Claude CLI is unavailable; detects out-of-scope changes, new dependencies, config drift, and missing test coverage from the diff
Verdict:
aligned— diff is consistent with the PRD/ADRwarning— minor concerns, but not blockingmisaligned— significant scope creep or deviation detected
Set align.on_violation: block in upstream.config.yaml to make upstream validate exit 1 on misaligned, blocking the CI pipeline.
--report [path]
Write a JSON report artifact to disk after the alignment check. Defaults to upstream-report.json in the current directory.
upstream validate --report # writes upstream-report.json
upstream validate --report ci-report.json # writes to custom pathThe artifact shape:
{
"branch": "feat/user-auth",
"verdict": "aligned",
"engine": "llm",
"coverage": { "prdPath": "docs/upstream/PRD-user-auth.md", "adrPath": null },
"findings": [{ "dimension": "problem_statement", "status": "pass", "detail": null }],
"snapshot": { "timestamp": "2026-06-30T12:00:00Z", "upstream_version": "0.3.1" },
"trend": { "vsLast": null }
}trend.vsLast is populated automatically when a previous snapshot exists (see upstream snapshot).
PR comments: when GITHUB_TOKEN and GITHUB_REPOSITORY are set (standard in GitHub Actions), upstream posts the verdict as a PR comment automatically. Set align.post_pr_comment: false to disable.
Link mode: when docs_storage: link, upstream validate fetches the full document from the provider (Google Docs or Confluence) before analysis. If the provider is unavailable or unauthenticated, it falls back to stub content with a warning.
upstream report
upstream report summary [--input <path>]
Reads a report artifact and prints a Markdown summary to stdout. Reads upstream-report.json by default; use --input to specify a different path.
upstream report summary # reads upstream-report.json
upstream report summary --input ci-report.json # reads custom path
upstream report summary >> $GITHUB_STEP_SUMMARY # pipe to GitHub Actions job summaryGitHub Actions integration
- name: Run alignment check
run: upstream validate --report upstream-report.json --format json
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_REPOSITORY: ${{ github.repository }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: upstream-report
path: upstream-report.json
- name: Write job summary
if: always()
run: upstream report summary --input upstream-report.json >> $GITHUB_STEP_SUMMARYCoverage stats — upstream stats
upstream stats shows a coverage summary across all local feature branches: how many have PRDs, ADRs, skips, or no docs at all.
upstream stats # human-readable table
upstream stats --format json # machine-readable
upstream stats --trend # compare against last snapshot--adoption — team adoption analytics
--adoption adds a per-author breakdown: which branches each author owns, their PRD/ADR coverage percentages, how many skips they've logged, and an overall adoption score.
upstream stats --adoption
upstream stats --adoption --since 2026-04-01 # scope to a specific date window
upstream stats --adoption --no-authors # skip log + score only, no per-author table
upstream stats --adoption --format json # machine-readable for CI/dashboardsExample output:
upstream adoption report
========================
Authors (last 90 days):
alice branches: 5 PRD: 5 (100%) ADR: 3 ( 60%) skips: 0
bob branches: 4 PRD: 2 ( 50%) ADR: 1 ( 25%) skips: 2
carol branches: 3 PRD: 3 (100%) ADR: 2 ( 67%) skips: 0
Skip log (last 90 days): 2 skips
bob feat/quick-fix 2026-06-10 "hotfix, no PRD needed"
bob feat/typo 2026-06-15 "one-liner, low risk"
Adoption score: 83% (PRD coverage weighted by branch author)Flags:
--since <date>— scope the report to branches and skips with activity after the given date (YYYY-MM-DD); defaults to 90 days ago--no-authors— suppress per-author table; shows only the skip log and adoption score--format json— emit the raw{ authors, skips, adoptionScore, since }object for CI or dashboards
All data is derived from local git history and SKIPS.md — no network calls.
upstream snapshot
Save the current stats as a local snapshot for trend comparison:
upstream snapshot # save snapshot
upstream snapshot --ci # exit 1 if coverage regressed vs last snapshot
upstream stats --trend # compare current stats against the saved snapshotSkipping
If a PRD or ADR genuinely isn't needed, developers can skip with a justification. The skip is logged to <docs_path>/SKIPS.md and a PR snippet is generated for transparency:
> ⚠️ upstream skip: PRD not created for `feat/quick-fix`.
> Reason: two-line CSS change, no product decisions involved.
> Logged in: <docs_path>/SKIPS.mdWhat gets committed to your repo
This applies to the
upstream init(team) setup. Withclaude plugin install, skills and the hook live in the Claude Code plugin directory and are not committed to your repo — onlyupstream.config.yamland your docs need to be tracked.
.claude/
hooks/
upstream-check.sh # UserPromptSubmit hook
plugins/upstream/
skills/
upstream-guard.md # orchestration skill
upstream-prd.md # PRD creation skill
upstream-adr.md # ADR creation skill
upstream-align.md # alignment check skill (used by upstream validate)
templates/
PRD.md # PRD template
ADR.md # ADR template
PRD-link.md # stub template for link mode
ADR-link.md # stub template for link mode
settings.json # MCP server registration (upstream mcp)
.github/
workflows/
upstream-align.yml # runs upstream validate on every PR
upstream.config.yaml # org configuration
.env.example # env var placeholders (shows required secrets, safe to commit)
.gitignore # updated by upstream init to exclude .env/.env.local/.env.test
.github/
CODEOWNERS # guardian entry (if configured)
<docs_path>/ # your PRDs, ADRs, and skip log
.gitkeep # created by upstream init to track the empty dirContributing
See CONTRIBUTING.md.
License
MIT
