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
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "throughline",
"version": "3.45.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 (different model, fresh context) before the design PR. /build-tdds builds every TDD merged to the integration branch via sequenced workers on the latest top-tier model 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 on a DIFFERENT model (the prior generation's top tier) for reviewer diversity — 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.",
"version": "3.46.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. /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.46.0", "local_impacting": false },
{ "version": "3.45.0", "local_impacting": false },
{ "version": "3.44.0", "local_impacting": false },
{ "version": "3.14.0", "local_impacting": false },
Expand Down
9 changes: 5 additions & 4 deletions agents/design-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
name: design-reviewer
description: Independent critique of a design (PRD + TDD set + accepted ADRs) BEFORE the design PR is opened. Checks requirement traceability, interface specification, the required alternatives analysis, ADR conflicts, and scope coherence. Use at /tdd-author close-out.
tools: Read, Grep, Glob, Bash
model: sonnet
model: inherit
---
You are a senior architect doing an INDEPENDENT design review. You did NOT author
this design, and you are deliberately on a different model than the author — bring
genuinely independent judgment. There is NO code yet: review the DESIGN, not an
implementation.
this design. 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.
Bring genuinely independent judgment. There is NO code yet: review the DESIGN,
not an implementation.

**Pre-check already ran.** The skill that invokes you has already run
`scripts/lib/tdd-lint.sh` against this TDD set and is invoking you only because
Expand Down
2 changes: 1 addition & 1 deletion docs/tdd/0064-judgment-worker-defaults.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TDD 0064: Model roles and parent-model observation

Status: draft
Status: implemented
PRD refs: NFR-3, FR-52, FR-87, FR-10, FR-15, FR-50
PRD-rev: f6ef178
ADR constraints: 0004, 0005, 0006, 0010, 0011, 0013, 0015
Expand Down
321 changes: 281 additions & 40 deletions scripts/lib/models.sh
Original file line number Diff line number Diff line change
@@ -1,51 +1,292 @@
#!/usr/bin/env bash
# models.sh — the one greppable model-id binding site (TDD 0062 / ADR 0009 /
# NFR-3). Product names live ONLY in this file. Prose surfaces speak tiers.
# models.sh — model roles and parent-model observation (TDD 0064 / ADR 0015 /
# NFR-3: the best cost/performance model for each job, owned by the operator
# through the parent session's model and effort).
#
# tl_resolve_models [plan-text]
# prints: build=<id> review=<id> verify=<id>
# Judgment slots (implementer, FR-15(d) reviewer, nontrivial runtime-verify)
# resolve to `inherit`: the worker is dispatched with no model parameter and
# runs on the parent session's model. Pins win (FR-87). Exactly two models are
# named per harness, both maintained bindings on the four lines below: the
# LIGHT tier (mechanical runtime-verify, FR-52, never above the parent) and the
# ESCALATION target (FR-88, used by /build-tdds escalation). Rebinding one when
# a vendor ships a new generation is an implementation change, not an ADR.
# Review independence is a fresh worker, not a distinct model name.
#
# Defaults:
# build = latest top-tier on this harness
# review = prior-generation top-tier (author ≠ reviewer)
# verify = cheaper tier when <plan-text> is only exit-code / file / grep,
# else the build model
# Overrides: THROUGHLINE_BUILD_MODEL, THROUGHLINE_REVIEW_MODEL,
# THROUGHLINE_RUNTIME_VERIFY_MODEL
# Every model decision is a function here, so no skill re-derives a rule in
# prose. The parent's model is OBSERVED from the harness session artifact
# (tl_parent_model), never taken from the model's self-report.
#
# Harness: GROK_PLUGIN_ROOT set → Grok ids; else Claude ids.
# No top-level side effects.
# tl_model_harness claude | grok
# tl_light_model / tl_escalation_model the harness binding (escalation:
# THROUGHLINE_ESCALATION_MODEL wins)
# tl_model_family <id> family name (rc 1 on empty id)
# tl_model_tier <id> light | above | unknown
# tl_parent_model parent id; rc 1 + one stderr line
# `tl_parent_model: <reason>`
# tl_parent_effort `<level> env|settings` | `unknown -`
# tl_plan_class [tdd-path] mechanical | nontrivial
# tl_resolve_models [tdd] [parent] build=<v> review=<v> verify=<v>
# tl_model_sources [tdd] [parent] build_src=<s> review_src=<s> verify_src=<s>
#
# Harness: GROK_PLUGIN_ROOT non-empty → grok; else claude. Sourced, never
# executed: no shell options set; the only top-level effects are sourcing
# plan-classifier.sh and the four binding assignments.

tl_resolve_models() {
local plan="${1:-}" build review verify cheap
if [ -n "${GROK_PLUGIN_ROOT:-}" ]; then
build="${THROUGHLINE_BUILD_MODEL:-grok-4.6}"
cheap="grok-4.5"
if [ -n "${THROUGHLINE_REVIEW_MODEL:-}" ]; then
review="$THROUGHLINE_REVIEW_MODEL"
else
review="grok-4.5"
fi
case "${BASH_SOURCE[0]}" in
*/*) _tl_models_pc="${BASH_SOURCE[0]%/*}/plan-classifier.sh" ;;
*) _tl_models_pc="./plan-classifier.sh" ;;
esac
# shellcheck source=scripts/lib/plan-classifier.sh
{ [ -r "$_tl_models_pc" ] && . "$_tl_models_pc"; } || {
echo "FATAL: cannot source $_tl_models_pc (partial install or perms)" >&2
unset _tl_models_pc
return 1 2>/dev/null || exit 1
}
unset _tl_models_pc

_TL_CLAUDE_LIGHT=sonnet
_TL_CLAUDE_ESCALATION=fable
_TL_GROK_LIGHT=grok-4.5 # unverified this pass (see Failure modes)
_TL_GROK_ESCALATION=grok-4.6 # unverified this pass

tl_model_harness() {
if [ -n "${GROK_PLUGIN_ROOT:-}" ]; then printf 'grok\n'; else printf 'claude\n'; fi
}

tl_light_model() {
if [ "$(tl_model_harness)" = grok ]; then printf '%s\n' "$_TL_GROK_LIGHT"
else printf '%s\n' "$_TL_CLAUDE_LIGHT"; fi
}

tl_escalation_model() {
if [ -n "${THROUGHLINE_ESCALATION_MODEL:-}" ]; then
printf '%s\n' "$THROUGHLINE_ESCALATION_MODEL"
elif [ "$(tl_model_harness)" = grok ]; then printf '%s\n' "$_TL_GROK_ESCALATION"
else printf '%s\n' "$_TL_CLAUDE_ESCALATION"; fi
}

# tl_model_family <id> — Claude: the first known family name that is a
# substring of the lowercased id, else the lowercased id. Grok: lowercased id.
tl_model_family() {
local id f
[ -n "${1:-}" ] || return 1
id="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
if [ "$(tl_model_harness)" = claude ]; then
for f in mythos fable opus sonnet haiku; do
case "$id" in *"$f"*) printf '%s\n' "$f"; return 0 ;; esac
done
fi
printf '%s\n' "$id"
}

# tl_model_tier <id> — `light` when the id is in the light binding's family
# (or, on Claude, the haiku family); `above` otherwise; `unknown` when empty.
# Membership by family name only: no rank table, no live fetch (ADR 0015).
tl_model_tier() {
local fam
fam="$(tl_model_family "${1:-}")" || { printf 'unknown\n'; return 0; }
if [ "$fam" = "$(tl_model_family "$(tl_light_model)")" ]; then
printf 'light\n'
elif [ "$(tl_model_harness)" = claude ] && [ "$fam" = haiku ]; then
printf 'light\n'
else
build="${THROUGHLINE_BUILD_MODEL:-fable}"
cheap="sonnet"
if [ -n "${THROUGHLINE_REVIEW_MODEL:-}" ]; then
review="$THROUGHLINE_REVIEW_MODEL"
else
case "$build" in
*opus*) review="sonnet" ;;
*) review="opus" ;;
esac
fi
printf 'above\n'
fi
if [ -n "${THROUGHLINE_RUNTIME_VERIFY_MODEL:-}" ]; then
verify="$THROUGHLINE_RUNTIME_VERIFY_MODEL"
}

# _tl_tac <file> — the file's lines newest-first (tac; BSD `tail -r`).
_tl_tac() {
if command -v tac >/dev/null 2>&1; then tac "$1"; else tail -r "$1"; fi
}

# _tl_transcript_model <jsonl> — print the newest assistant `.message.model`
# that is neither empty nor `<synthetic>`. Every line is PARSED as JSON,
# tolerantly (a truncated line is skipped); never a regex on the raw line,
# whose tool_use inputs can carry their own "model" key. rc 3 = no parser.
_tl_transcript_model() {
if command -v jq >/dev/null 2>&1; then
_tl_tac "$1" | jq -nRr 'first(inputs | fromjson? | objects
| select(.type == "assistant") | .message | objects | .model | strings
| select(. != "" and . != "<synthetic>"))' 2>/dev/null
elif command -v python3 >/dev/null 2>&1; then
_tl_tac "$1" | python3 -c '
import json, sys
for line in sys.stdin.buffer:
try:
d = json.loads(line)
except Exception:
continue
if not isinstance(d, dict) or d.get("type") != "assistant":
continue
m = d.get("message")
v = m.get("model") if isinstance(m, dict) else None
if isinstance(v, str) and v and v != "<synthetic>":
print(v)
break
' 2>/dev/null
else
return 3
fi
return 0
}

# _tl_json_top_string <file> <key> — print the top-level string field <key>
# of the JSON document <file> (empty when absent / not a string / malformed).
# jq → python3 cascade; rc 3 = no parser.
_tl_json_top_string() {
if command -v jq >/dev/null 2>&1; then
jq -r --arg k "$2" '.[$k]? | strings' "$1" 2>/dev/null
elif command -v python3 >/dev/null 2>&1; then
python3 -c '
import json, sys
try:
d = json.load(open(sys.argv[1]))
except Exception:
sys.exit(0)
v = d.get(sys.argv[2]) if isinstance(d, dict) else None
if isinstance(v, str):
print(v)
' "$1" "$2" 2>/dev/null
else
case "$plan" in
*[Ee]xit-code*|*grep*|*file\ exist*|*rc\ 0*|*rc=0*) verify="$cheap" ;;
'') verify="$cheap" ;;
*) verify="$build" ;;
return 3
fi
return 0
}

# _tl_pct_encode <string> — percent-encode every byte outside A-Za-z0-9._~-
# (so `/` → %2F, including the leading slash).
_tl_pct_encode() {
local LC_ALL=C s="${1:-}" out="" c hex i
for (( i = 0; i < ${#s}; i++ )); do
c="${s:i:1}"
case "$c" in
[A-Za-z0-9._~-]) out="$out$c" ;;
*) printf -v hex '%%%02X' "'$c"; out="$out$hex" ;;
esac
done
printf '%s' "$out"
}

# _tl_grok_summary <sessions-dir> <dir> <sid> — print the summary.json path
# for <dir> (percent-encoded) if it exists. Rejects any `..` so the path stays
# under <sessions-dir>.
_tl_grok_summary() {
local rel
rel="$(_tl_pct_encode "$2")/$3"
case "$rel" in *..*) return 1 ;; esac
[ -f "$1/$rel/summary.json" ] || return 1
printf '%s\n' "$1/$rel/summary.json"
}

_tl_pm_fail() { printf 'tl_parent_model: %s\n' "$1" >&2; return 1; }

# tl_parent_model — the parent session's model id, observed from the harness
# session artifact. rc 1 (no stdout, one stderr line) when unreadable; an
# unreadable model is reported, never guessed (ADR 0015).
tl_parent_model() {
local out rc f="" cand top what
if [ "$(tl_model_harness)" = grok ]; then
local sid="${GROK_SESSION_ID:-}" base
[ -n "$sid" ] || { _tl_pm_fail "no session id"; return 1; }
case "$sid" in *..*) _tl_pm_fail "invalid session id"; return 1 ;; esac
base="${GROK_HOME:-${HOME:-}/.grok}/sessions"
f="$(_tl_grok_summary "$base" "$PWD" "$sid")" || {
top="$(git rev-parse --show-toplevel 2>/dev/null)" || top=""
f=""
[ -z "$top" ] || f="$(_tl_grok_summary "$base" "$top" "$sid")" || f=""
}
[ -n "$f" ] || { _tl_pm_fail "summary not found"; return 1; }
what=summary
out="$(_tl_json_top_string "$f" current_model_id)" && rc=0 || rc=$?
else
local id="${CLAUDE_CODE_SESSION_ID:-}"
case "$id" in ''|*[!A-Za-z0-9-]*) _tl_pm_fail "no session id"; return 1 ;; esac
for cand in "${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}"/projects/*/"$id".jsonl; do
[ -f "$cand" ] && { f="$cand"; break; }
done
[ -n "$f" ] || { _tl_pm_fail "transcript not found"; return 1; }
what=transcript
out="$(_tl_transcript_model "$f")" && rc=0 || rc=$?
fi
printf 'build=%s review=%s verify=%s\n' "$build" "$review" "$verify"
[ "$rc" -ne 3 ] || { _tl_pm_fail "no json parser"; return 1; }
out="${out%%$'\n'*}"
[ -n "$out" ] || { _tl_pm_fail "no model in $what"; return 1; }
printf '%s\n' "$out"
}

# tl_parent_effort — `<level> env` | `<level> settings` | `unknown -`.
# Effort is session-wide on these harnesses; Grok exposes none.
tl_parent_effort() {
local lvl="" f
if [ "$(tl_model_harness)" = grok ]; then printf 'unknown -\n'; return 0; fi
if [ -n "${CLAUDE_CODE_EFFORT_LEVEL:-}" ]; then
printf '%s env\n' "$CLAUDE_CODE_EFFORT_LEVEL"; return 0
fi
f="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}/settings.json"
if [ -r "$f" ]; then lvl="$(_tl_json_top_string "$f" effortLevel)" || lvl=""; fi
lvl="${lvl%%$'\n'*}"
if [ -n "$lvl" ]; then printf '%s settings\n' "$lvl"; else printf 'unknown -\n'; fi
}

# tl_plan_class [tdd-path] — the FR-52 plan class via tl_classify_plan;
# no path or a classifier failure → nontrivial (conservative).
tl_plan_class() {
local c
[ -n "${1:-}" ] || { printf 'nontrivial\n'; return 0; }
c="$(tl_classify_plan "$1")" || c=""
case "$c" in
mechanical) printf 'mechanical\n' ;;
*) printf 'nontrivial\n' ;;
esac
}

# _tl_resolve_slot <build|review|verify> [tdd-path] [parent-id] — print
# `<value> <source>`; the one rule both public resolvers share. The source is
# a single token, so callers split on the LAST space.
_tl_resolve_slot() {
local slot="$1" tdd="${2:-}" parent="${3:-}" pin
case "$slot" in
build|review)
if [ "$slot" = build ]; then pin=THROUGHLINE_BUILD_MODEL; else pin=THROUGHLINE_REVIEW_MODEL; fi
if [ -n "${!pin:-}" ]; then
printf '%s pin:%s\n' "${!pin}" "$pin"
elif [ "$(tl_model_harness)" = claude ] && [ -n "${CLAUDE_CODE_SUBAGENT_MODEL:-}" ]; then
# Claude Code applies it to every dispatch without a model: a pin.
printf '%s pin:CLAUDE_CODE_SUBAGENT_MODEL\n' "$CLAUDE_CODE_SUBAGENT_MODEL"
else
printf 'inherit parent\n'
fi ;;
verify)
if [ -n "${THROUGHLINE_RUNTIME_VERIFY_MODEL:-}" ]; then
printf '%s pin:THROUGHLINE_RUNTIME_VERIFY_MODEL\n' "$THROUGHLINE_RUNTIME_VERIFY_MODEL"
elif [ "$(tl_plan_class "$tdd")" = nontrivial ]; then
_tl_resolve_slot build "$tdd" "$parent"
elif [ "$(tl_model_tier "$parent")" = above ]; then
printf '%s light\n' "$(tl_light_model)"
else
printf 'inherit parent-cap\n' # light or unknown parent: never above it
fi ;;
*) printf '_tl_resolve_slot: unknown slot %s\n' "$slot" >&2; return 2 ;;
esac
}

# tl_resolve_models [tdd-path] [parent-id] — one line `build=<v> review=<v>
# verify=<v>`; <v> is `inherit` (dispatch with no model) or a model id.
tl_resolve_models() {
local b r v
b="$(_tl_resolve_slot build "${1:-}" "${2:-}")"
r="$(_tl_resolve_slot review "${1:-}" "${2:-}")"
v="$(_tl_resolve_slot verify "${1:-}" "${2:-}")"
printf 'build=%s review=%s verify=%s\n' "${b% *}" "${r% *}" "${v% *}"
}

# tl_model_sources [tdd-path] [parent-id] — one line `build_src=<s>
# review_src=<s> verify_src=<s>`; <s> ∈ parent, pin:<ENV-NAME>, light,
# parent-cap.
tl_model_sources() {
local b r v
b="$(_tl_resolve_slot build "${1:-}" "${2:-}")"
r="$(_tl_resolve_slot review "${1:-}" "${2:-}")"
v="$(_tl_resolve_slot verify "${1:-}" "${2:-}")"
printf 'build_src=%s review_src=%s verify_src=%s\n' "${b##* }" "${r##* }" "${v##* }"
}
6 changes: 3 additions & 3 deletions skills/tdd-author/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,9 +530,9 @@ scope concern this gate missed.

**7b. Independent design critique (gate — do not skip).** Before opening the design
PR, get an INDEPENDENT critique of the whole authored set. Spawn the
`design-reviewer` subagent — it runs in fresh context on a
different model than you authored in, so it does not share your blind spots. It
reads the PRD, the TDD(s), and the accepted ADRs and checks requirement
`design-reviewer` subagent — it runs in a fresh worker that is not this session
and inherits this session's model (FR-10); independence is the fresh context,
not a distinct model name. It reads the PRD, the TDD(s), and the accepted ADRs and checks requirement
traceability, interface specification, the REQUIRED alternatives analysis, ADR
conflicts, and scope coherence, ending with `DESIGN_REVIEW: PASS` or
`DESIGN_REVIEW: BLOCK <reason>`.
Expand Down
Loading
Loading