Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba88e94
docs(roadmap): renumber watchkeeper series to v0.12.x and un-defer re…
tcballard Jun 12, 2026
60fe824
docs(decisions): record ADR-042 watchkeeper revision materialization …
tcballard Jun 12, 2026
e7057f3
feat(watchkeeper): add repository comparison service [roadmap:v0.12.0]
tcballard Jun 12, 2026
c0f58c8
feat(watchkeeper): add git revision materialization [roadmap:v0.12.0]
tcballard Jun 12, 2026
c1705da
feat(watchkeeper): expose rac watchkeeper comparison CLI [roadmap:v0.…
tcballard Jun 12, 2026
29c6af6
test(watchkeeper): add comparison fixtures, batteries, and goldens [r…
tcballard Jun 12, 2026
e6c96de
docs(watchkeeper): document rac watchkeeper usage [roadmap:v0.12.0]
tcballard Jun 12, 2026
8018787
feat(watchkeeper): add deterministic intent analysis service [roadmap…
tcballard Jun 12, 2026
a850152
feat(watchkeeper): surface intent findings in watchkeeper output [roa…
tcballard Jun 12, 2026
cd5d2e6
test(watchkeeper): cover intent checks and pin finding contract [road…
tcballard Jun 12, 2026
24bd19e
docs(watchkeeper): document intent findings [roadmap:v0.12.1]
tcballard Jun 12, 2026
dbcfb44
feat(watchkeeper): add deterministic review recommendations [roadmap:…
tcballard Jun 12, 2026
608619c
feat(watchkeeper): add github format and fail-on exit policy [roadmap…
tcballard Jun 12, 2026
47cdfdf
test(watchkeeper): pin recommendation, annotation, and exit contracts…
tcballard Jun 12, 2026
94c6e79
docs(watchkeeper): document review workflow and formats [roadmap:v0.1…
tcballard Jun 12, 2026
d603926
feat(watchkeeper): add composite GitHub Action [roadmap:v0.12.3]
tcballard Jun 12, 2026
8cc6e2e
feat(watchkeeper): add reusable workflow and PR dogfood job [roadmap:…
tcballard Jun 12, 2026
56be4cb
test(watchkeeper): pin action and workflow contracts [roadmap:v0.12.3]
tcballard Jun 12, 2026
6b8e23a
docs(watchkeeper): document action usage and version pinning [roadmap…
tcballard Jun 12, 2026
4dcc8d1
docs(decisions): renumber watchkeeper ADR to 043 [roadmap:v0.12.0]
tcballard Jun 13, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,19 @@ jobs:
tests/test_ci_batteries.py tests/test_corpus.py
tests/test_operations.py
tests/test_golden.py tests/test_dogfood.py

# Watchkeeper dogfood (v0.12.3): every pull request here gets a product
# knowledge review from the local action in source mode — which is also
# the live end-to-end test of action.yml itself.
watchkeeper:
name: watchkeeper (dogfood, source install)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: ./
with:
path: rac
install-from: source
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ jobs:
paths: "tests/test_validate.py tests/test_parser.py tests/test_schema.py tests/test_identity.py tests/test_frontmatter.py tests/test_metadata_identity.py tests/test_idgen.py tests/test_ci_batteries.py tests/test_corpus.py tests/test_operations.py"
- name: cli
paths: "tests/test_cli.py"
- name: compare
paths: "tests/test_compare.py"
- name: artifacts
paths: "tests/test_design.py tests/test_roadmap.py tests/test_prompt.py tests/test_decision_metadata.py"
- name: create
Expand All @@ -77,6 +79,8 @@ jobs:
paths: "tests/test_init.py"
- name: inspect
paths: "tests/test_inspect.py"
- name: intent
paths: "tests/test_intent.py"
- name: migrate
paths: "tests/test_migrate.py"
- name: portfolio
Expand All @@ -93,10 +97,14 @@ jobs:
paths: "tests/test_resolve.py"
- name: review
paths: "tests/test_review.py"
- name: revisions
paths: "tests/test_revisions.py"
- name: skill
paths: "tests/test_skill.py"
- name: stats
paths: "tests/test_stats.py"
- name: watchkeeper
paths: "tests/test_watchkeeper.py"
# Trust gates (v0.7.9): dogfood validates RAC's own rac/ corpus,
# golden pins CLI stdout byte-for-byte against committed files.
- name: dogfood
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/watchkeeper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Reusable Watchkeeper workflow (v0.12.3).
#
# Consumers call this with `uses:` to get product knowledge review on pull
# requests without writing checkout/base-ref wiring themselves:
#
# jobs:
# watchkeeper:
# uses: tcballard/requirements-as-code/.github/workflows/watchkeeper.yml@<tag>
# with:
# path: rac
#
# Pin an exact release tag. No moving major tag is published: package
# versions derive from git tags via setuptools-scm, and a floating tag
# would corrupt version derivation (see docs/watchkeeper.md).
name: Watchkeeper

on:
workflow_call:
inputs:
path:
description: "Corpus directory to compare."
type: string
required: false
default: "rac"
base:
description: "Base revision (empty: origin/<PR base branch>)."
type: string
required: false
default: ""
fail-on:
description: "Failure policy: error, warning, or none."
type: string
required: false
default: "error"
annotate:
description: "Emit inline annotations."
type: boolean
required: false
default: true
rac-version:
description: "Exact requirements-as-code version from PyPI (empty: latest)."
type: string
required: false
default: ""

permissions:
contents: read

jobs:
watchkeeper:
name: watchkeeper
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# The comparison needs the base revision's history, and a
# source-mode install needs git metadata for setuptools-scm.
fetch-depth: 0

# Reusable workflows run in the caller's repository, so the action is
# referenced by full path. Update this ref when cutting a release tag;
# until the first v0.12.x release it tracks main.
- uses: tcballard/requirements-as-code@main
with:
path: ${{ inputs.path }}
base: ${{ inputs.base }}
fail-on: ${{ inputs.fail-on }}
annotate: ${{ inputs.annotate }}
rac-version: ${{ inputs.rac-version }}
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,51 @@ details, release history over commit history.

### Added

- Watchkeeper GitHub Action and reusable workflow (v0.12.3): a composite
`action.yml` at the repository root (`uses:
tcballard/requirements-as-code@<tag>`) and a callable
`.github/workflows/watchkeeper.yml` bring product knowledge review to
pull requests — failed check per the `fail-on` policy, inline
annotations on the artifacts needing attention, and a step-summary
report. The action is logic-free (install RAC, resolve the base ref,
run one `rac watchkeeper --format github`, propagate the exit code) and
this repository's own PR checks run it from source as the live
end-to-end test. Pin exact release tags; no moving major tag is
published (setuptools-scm derives versions from git tags). See
`docs/watchkeeper.md`.

- Watchkeeper review verdict, GitHub format, and CI policy (v0.12.2):
`rac watchkeeper` now ends with a deterministic review recommendation —
validation regressions, broken relationships, and clarity-regression
findings recommend human review with Core-owned reasons; ambiguity and
unlinked scope inform but never recommend alone. `--fail-on
error|warning|none` turns the verdict into CI policy, and `--format
github` writes a Markdown step-summary report to stdout and
workflow-command annotations (with repository-relative paths) to stderr
— no GitHub API involved. JSON gains an additive `review` block.

- Watchkeeper intent analysis (v0.12.1): the `rac watchkeeper` report now
ends with deterministic intent findings — specificity regressions
(numbers vanishing from requirements), ambiguous wording arriving,
mandatory language weakening or disappearing, acceptance criteria or
success measures being removed, new scope with no relationships, and
the relationship impact of modified or removed artifacts. Every check
is token-boundary text matching or parsed-section comparison — no
semantic scoring — and each finding carries a one-sentence detail plus
diff-style evidence. JSON gains an additive `findings[]` array.

- Watchkeeper repository comparison (v0.12.0): `rac watchkeeper [directory]
--base REF [--head REF] [--json]` reviews product knowledge changes
between two repository states — added/modified/removed artifacts (with
requirement-level diffs), validation deltas (including newly invalid
artifacts), relationship deltas (including references broken purely by a
removal elsewhere), and per-type artifact count deltas. Base and head
each accept a git revision or a plain directory; revisions are
materialized read-only via `git archive` (ADR-043) and nothing ever
mutates the repository. JSON output is a stable contract
(`schema_version: "1"`) that grows additively across the v0.12.x series
(intent findings and review recommendations follow).

- Portal export (v0.11.0): `rac export` turns a repository's corpus
into shareable artifacts. The default mode prints a deterministic
JSON payload to stdout — artifacts with stable ids, aliases, type,
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ to the corpus artifact and they load through the imports below.

## Working corpus

- Current series: `rac/roadmaps/v0.11.x-portal/` (next up: v0.11.0)
- Previous series: `rac/roadmaps/v0.10.x-guide/` (complete through v0.10.5; v0.10.7 docs site in flight on PR #78)
- Current series: `rac/roadmaps/v0.12.x-watchkeeper/` (next up: v0.12.0)
- Previous series: `rac/roadmaps/v0.11.x-portal/` (complete through v0.11.0)
- Decisions (ADRs): `rac/decisions/`
122 changes: 122 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# RAC Watchkeeper composite action (v0.12.3).
#
# A thin wrapper: install RAC, resolve the base revision, run one
# `rac watchkeeper --format github` invocation, and route its outputs —
# stdout to the step summary, stderr (workflow-command annotations) to the
# step log. All analysis, rendering, and failure policy live in the Python
# package (ADR-015); the action never reinterprets exit codes.
name: "RAC Watchkeeper"
description: >-
Review product knowledge changes (requirements, decisions, roadmaps,
designs, prompts) on pull requests: changed artifacts, validation and
relationship deltas, deterministic intent findings, and a review verdict.
author: "Tom Ballard"

branding:
icon: "eye"
color: "purple"

inputs:
path:
description: >-
Corpus directory to compare (passed to `rac watchkeeper`).
required: false
default: "rac"
base:
description: >-
Base state: a git revision or a directory. Empty (the default)
resolves to `origin/<pull request base branch>`.
required: false
default: ""
fail-on:
description: >-
Failure policy forwarded to `rac watchkeeper --fail-on`:
`error` (fail when review is recommended), `warning` (also fail on
any warning finding), or `none` (never fail, still report).
required: false
default: "error"
annotate:
description: >-
Emit inline annotations via workflow commands (`true` or `false`).
required: false
default: "true"
rac-version:
description: >-
Exact requirements-as-code version to install from PyPI. Empty
installs the latest release. Ignored when `install-from` is `source`.
required: false
default: ""
install-from:
description: >-
Where to install RAC from: `pypi` (the default for consumers) or
`source` (the action checkout itself — used by this repository's own
dogfood job; requires `uses: ./` on a full checkout).
required: false
default: "pypi"

runs:
using: "composite"
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install RAC
shell: bash
env:
INSTALL_FROM: ${{ inputs.install-from }}
RAC_VERSION: ${{ inputs.rac-version }}
run: |
python -m pip install --quiet --upgrade pip
if [ "$INSTALL_FROM" = "source" ]; then
# The action checkout itself; setuptools-scm needs git metadata,
# so this path requires `uses: ./` on a real checkout.
python -m pip install --quiet "$GITHUB_ACTION_PATH"
elif [ -n "$RAC_VERSION" ]; then
python -m pip install --quiet "requirements-as-code==$RAC_VERSION"
else
python -m pip install --quiet requirements-as-code
fi

- name: Resolve base revision
shell: bash
env:
INPUT_BASE: ${{ inputs.base }}
PR_BASE_REF: ${{ github.base_ref }}
run: |
BASE="$INPUT_BASE"
if [ -z "$BASE" ]; then
BASE="origin/$PR_BASE_REF"
fi
# Make sure the base ref exists locally; harmless when it already
# does, best-effort when the base is a directory or a plain SHA
# (rac fails with exit 2 and a clear message if it cannot resolve).
case "$BASE" in
origin/*)
REF="${BASE#origin/}"
git fetch --quiet --no-tags origin \
"+refs/heads/$REF:refs/remotes/origin/$REF" || true
;;
esac
echo "WATCHKEEPER_BASE=$BASE" >> "$GITHUB_ENV"

- name: Run watchkeeper
shell: bash
env:
INPUT_PATH: ${{ inputs.path }}
FAIL_ON: ${{ inputs.fail-on }}
ANNOTATE: ${{ inputs.annotate }}
run: |
ANNOTATE_FLAG=""
if [ "$ANNOTATE" != "true" ]; then
ANNOTATE_FLAG="--no-annotate"
fi
# stdout becomes the job's step summary; stderr stays in the live
# step log where the runner parses the annotation commands. The
# exit code propagates unchanged — policy lives in --fail-on.
rac watchkeeper "$INPUT_PATH" \
--base "$WATCHKEEPER_BASE" \
--format github \
--fail-on "$FAIL_ON" \
$ANNOTATE_FLAG \
> "$GITHUB_STEP_SUMMARY"
Loading
Loading