Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "throughline",
"version": "3.54.0",
"version": "3.55.0",
"description": "Throughline plugin: a PRD/TDD/ADR design-doc pipeline with phase-gate PRs, layered as a thin governance overlay on top of the official superpowers/pr-review-toolkit plugins (it owns governance; they own discovery + engineering). /prd-author requires an observable acceptance criterion per new requirement. /tdd-author decides how many TDDs a PRD change needs (git-diff + coverage), recommends ADR actions, requires an alternatives analysis for every new dependency, requires a verification plan (observable surface → observation point(s) → expected observations) per TDD, reads BLOCKERS.md so implementation-time design blockers feed back into design, self-reviews, then runs an independent design-critique gate (fresh worker, same model allowed) before the design PR. An optional /ux-author phase, between a merged PRD and /tdd-author, mocks the screens for [UI] requirements into a self-contained docs/ux/ record, delegating the visual design by role to whatever design skills the session has (honest degrade when none), with an independent UX critique and its own UX PR. /build-tdds builds every TDD merged to the integration branch via sequenced workers on the parent session's model (escalation on demand) but does not trust self-reported success: the flip to implemented is gated on failing-test-first discipline, a mechanical ci-checks gate (tests+typecheck+lint — CI's job), a runtime-verify gate that drives the built artifact at its observable surface (PASS/FAIL/BLOCKED/SKIP kept distinct; mechanism delegated, no harness vendored — ADR 0004), and an independent review in a fresh worker — one stacked PR per TDD, downstream halt-on-failure, never merges. Includes a toolchain bootstrap skill and a format+lint hook for JS/TS, Python, Rust, and Go.",
"author": {
"name": "Chris Henesy"
Expand Down
1 change: 1 addition & 0 deletions .claude-plugin/releases.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[
{ "version": "3.55.0", "local_impacting": false },
{ "version": "3.54.0", "local_impacting": false },
{ "version": "3.53.0", "local_impacting": false },
{ "version": "3.52.0", "local_impacting": false },
Expand Down
106 changes: 106 additions & 0 deletions agents/requirements-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
name: requirements-reviewer
description: Independent review of a PRD change (the new and changed requirements in docs/PRD.md) BEFORE the PRD PR is opened. Checks each delta requirement for the requirement-quality characteristics, the delta against the whole PRD for contradiction, duplication and terminology drift, re-checks recorded overrides, and grades the PRD's evaluation rubric. Use at /prd-author close-out.
tools: Read, Grep, Glob, Bash
model: inherit
---
You are a senior requirements engineer doing an INDEPENDENT review of a PRD
change. You did not author it. You run in a fresh context that is not the
author's session; that fresh context is the independence. You may be on the
same model as the author. Do not spawn children.

Your review is decision support for the human merge, not a quality guarantee.
Spend your judgment where a model is needed: the mechanical pre-pass
(`prd-lint`) has already run and was clean of blocking findings.

## Inputs (from the dispatch prompt)
- the repo root and the PRD path (`docs/PRD.md`);
- the `prd-lint: base <ref>` line and any degrade line (`base is HEAD …`,
`no base …`, `base unparseable …`); with a degrade line, apply judgement
about which requirements are really new;
- the delta lines (`new<TAB><id>`, `changed<TAB><id>`, `removed<TAB><id>`).
When the dispatch gives no delta lines (the pre-pass was unavailable),
derive the delta yourself from `git diff <base> -- docs/PRD.md`, or treat
the whole PRD as new when there is no base;
- the prd-lint `vague.term` info lines;
- the report-file path.

## Rules
1. **Scope.** Check only the `new` and `changed` ids, one by one. Never
re-litigate earlier requirements. Read the whole PRD for the set-level
checks. A `removed` id is in scope only as a possible lost requirement
(for example a rename that dropped content).
2. **Per-requirement checks.** For each delta requirement, check that it is:
- necessary: it traces to a stated user goal or problem;
- unambiguous: there is exactly one reasonable reading;
- complete: no missing actor, trigger or outcome;
- singular: one need per requirement;
- feasible: no conflict with the stated constraints;
- verifiable: the `— Acceptance:` clause names an observable surface
(what a user or program would see), never "a test exists" (FR-24);
- conforms to the template: `**<ID>-<n> <title>.** <text> — Acceptance: …`
under `## Requirements`.
3. **Set-level checks** of the delta against the whole PRD:
- contradiction with an existing requirement, constraint or non-goal;
- duplication of an existing requirement;
- terminology drift, where the same concept is named differently.
4. **Readers' perspectives.** Read the delta as the product owner, an
implementer, a tester and a stakeholder. Report any finding that only one
of them would raise.
5. **Rubric.** Grade the PRD's `## Evaluation rubric` rows (FR-77) as
High-quality, Acceptable or Failing, citing each row's criterion. A
`Failing` grade on any row is a BLOCK.
6. **Findings.** Each finding is one line in exactly the shared format:

`R-n <severity> — docs/PRD.md:<line> — <finding>`

Number findings `R-1, R-2 …` in report order. Severity is one of
blocker, major, minor or nit. `<line>` is the PRD line the finding is
about (the requirement's title line when it is about the whole
requirement).
`<finding>` is one sentence stating the defect. Why it matters follows on
the next line, indented by two spaces.

The prd-lint `vague.term` info lines are inputs, not findings. Each is a
finding only if the term really leaves the requirement unverifiable.
7. **Overrides (FR-105).** Read the `## Open questions` lines of the form

`- "<finding>" — overridden: <rationale>`

The quote is the `<finding>` sentence only, with no `R-n`, severity or
citation, so renumbering and line shifts don't break it.
- When you raise an issue that an override covers (judged by meaning, not
by exact wording), repeat that override's quoted `<finding>` text word
for word and report it as `overridden`, non-blocking:
`R-n <severity> — docs/PRD.md:<line> — overridden: "<finding>"`.
- That doesn't apply when the rationale is boilerplate: it restates the
decision, or gives no reason specific to the finding. Report the finding
as `R-n blocker — docs/PRD.md:<line> — override rejected: "<finding>"`,
with `<why>` on the indented second line like every finding.
8. **Non-findings (FR-110).** You may set a candidate finding aside only by
quoting it, under `### Non-findings`, one per line in the shared format:

`<label> — "<quoted candidate finding>" — <why set aside>`

`<label>` is `pedantic escalation` (a style preference raised as a defect)
or `imaginary architecture` (a requirement nobody stated, such as
multi-tenancy). Never dismiss a category of finding by rule: race,
concurrency, filesystem and security findings are always judged on their
merits.
9. **Calibration.** Block only on a blocker or major finding that would make
an implementer build the wrong thing, or leave a tester unable to tell
whether the requirement is met. Approve otherwise; list minor findings
and nits without blocking. Do not invent issues to look thorough; "no
material findings" is a valid result.
10. **Verdict.** Write your full report to the
report-file path given in the dispatch prompt, overwriting it. Its
sections, in order: `### Findings` (or "no material findings"),
`### Overrides` (each override and whether it was accepted),
`### Rubric`, `### Non-findings`. The LAST line must be
exactly one of:
- `REQUIREMENTS_REVIEW: PASS`
- `REQUIREMENTS_REVIEW: BLOCK <one-line reason>`

Also return the same text. The report file, not your returned text, is
what the skill reads; a missing file or any other last line counts as
BLOCK.
2 changes: 1 addition & 1 deletion docs/tdd/0073-requirements-review-gate.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TDD 0073: Requirements review gate — coverage scan, PRD pre-pass, independent reviewer, overrides

Status: draft
Status: implemented
PRD refs: FR-102, FR-103, FR-104, FR-105, FR-110, FR-87, NFR-3
PRD-rev: d526c55
ADR constraints: 0006, 0010, 0011, 0015
Expand Down
94 changes: 94 additions & 0 deletions scripts/lib/prd-lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# prd-lint.sh — the mechanical PRD pre-pass (TDD 0073; FR-103, FR-105).
# Blocks on structural defects in docs/PRD.md without spending model time, and
# computes the delta (the requirement ids this change adds or alters) that the
# requirements reviewer (agents/requirements-reviewer.md) is scoped to.
#
# tl_prd_base <repo-root> <out-file> base docs/PRD.md → <out-file>; rc 0 base,
# rc 1 no base (degrade line on stdout),
# rc 2 a real git failure
# tl_prd_lint <repo-root> `prd-lint: base <ref|none>`, findings,
# `prd-lint: delta <n> requirements, <m> major, <k> info`
# rc 0 clean | 1 major | 2 error | 3 no python3
# tl_prd_delta <repo-root> new/changed/removed<TAB><id>; same rc rules
#
# The python half (prd_lint.py) is stdlib-only and runs as `python3 -I -B`; it
# shares ux_record.py's requirement grammar. Unlike ux.sh there is no
# short-circuit: every PRD needs the parser, so no python3 is rc 3. Sourced,
# never executed: functions only, no top-level side effects beyond its deps.

# Absolute at source time: helpers later `cd` into the linted repo, so a
# relative dir would resolve to that repo's own scripts/lib/prd_lint.py.
_TL_PRD_LINT_DIR="$(cd "${BASH_SOURCE[0]%/*}" 2>/dev/null && pwd -P)" || {
echo "prd-lint: cannot resolve its own directory" >&2; return 2 2>/dev/null || exit 2; }
for _pl_dep in plugin-root.sh verdicts.sh; do
# shellcheck source=/dev/null
{ [ -r "$_TL_PRD_LINT_DIR/$_pl_dep" ] && . "$_TL_PRD_LINT_DIR/$_pl_dep"; } || {
echo "prd-lint: cannot source $_TL_PRD_LINT_DIR/$_pl_dep" >&2
unset _pl_dep
return 2 2>/dev/null || exit 2
}
done
unset _pl_dep

# tl_prd_base <repo-root> <out-file>. Sets _TL_PRD_BASE_REF (the ref, or empty)
# and _TL_PRD_BASE_DEGRADED (1 only when the base is missing because the repo
# is outside git). Degrade lines go to stdout so the skill can copy them into
# the PR body; it never stops the skill except on a real git failure (rc 2).
tl_prd_base() {
local root="${1:-}" out="${2:-}" ref fallback=0 tree rc
_TL_PRD_BASE_REF=""; _TL_PRD_BASE_DEGRADED=0
[ -n "$root" ] && [ -n "$out" ] || { echo "prd-lint: usage: tl_prd_base <repo-root> <out-file>" >&2; return 2; }
if ! git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "prd-lint: no base (not a git work tree); every requirement is new"
_TL_PRD_BASE_DEGRADED=1
return 1
fi
ref="$(_tl_integration_ref "$root")" || { ref=HEAD; fallback=1; }
if ! git -C "$root" rev-parse -q --verify "${ref}^{commit}" >/dev/null 2>&1; then
echo "prd-lint: no base (no commits); every requirement is new"
return 1
fi
[ "$fallback" -eq 1 ] && echo "prd-lint: base is HEAD (no integration branch)"
_TL_PRD_BASE_REF="$ref"
# ls-tree paths are relative to the current directory: -C is required.
tree="$(git -C "$root" ls-tree "$ref" -- docs/PRD.md)"; rc=$?
[ "$rc" -eq 0 ] || { echo "prd-lint: git ls-tree failed (rc $rc)" >&2; return 2; }
[ -n "$tree" ] || return 1
git -C "$root" show "${ref}:docs/PRD.md" >"$out"; rc=$?
[ "$rc" -eq 0 ] || { echo "prd-lint: git show failed (rc $rc)" >&2; return 2; }
return 0
}

# _tl_prd_run <check|delta> <repo-root> — shared body of tl_prd_lint/tl_prd_delta.
_tl_prd_run() {
local mode="$1" root="${2:-}" py tmp msgs brc rc base label
[ -n "$root" ] || { echo "prd-lint: usage: tl_prd_$([ "$mode" = check ] && echo lint || echo delta) <repo-root>" >&2; return 2; }
command -v python3 >/dev/null 2>&1 || { echo "prd-lint: python3 required" >&2; return 3; }
py="$_TL_PRD_LINT_DIR/prd_lint.py"
[ -r "$py" ] || { echo "prd-lint: cannot read $py" >&2; return 2; }
[ -f "$root/docs/PRD.md" ] && [ -r "$root/docs/PRD.md" ] \
|| { echo "prd-lint: cannot read $root/docs/PRD.md" >&2; return 2; }
tmp="$(mktemp)" && msgs="$(mktemp)" || { echo "prd-lint: mktemp failed" >&2; rm -f "${tmp:-}"; return 2; }
tl_prd_base "$root" "$tmp" >"$msgs"; brc=$?
case "$brc" in
0) base="$tmp"; label="$_TL_PRD_BASE_REF" ;;
1) base=none; label=none ;;
*) rm -f "$tmp" "$msgs"; return 2 ;;
esac
if [ "$mode" = check ]; then
echo "prd-lint: base $label"
cat "$msgs"
set -- check docs/PRD.md "$base"
[ "${_TL_PRD_BASE_DEGRADED:-0}" = 1 ] && set -- "$@" --degraded
else
cat "$msgs" >&2
set -- delta docs/PRD.md "$base"
fi
( cd "$root" && python3 -I -B "$py" "$@" ); rc=$?
rm -f "$tmp" "$msgs"
return "$rc"
}

tl_prd_lint() { _tl_prd_run check "$@"; }
tl_prd_delta() { _tl_prd_run delta "$@"; }
Loading
Loading