diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 22794d5..d4b8e87 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" }, diff --git a/.claude-plugin/releases.json b/.claude-plugin/releases.json index 49dc658..ac62b1d 100644 --- a/.claude-plugin/releases.json +++ b/.claude-plugin/releases.json @@ -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 }, diff --git a/agents/design-reviewer.md b/agents/design-reviewer.md index e0a5d97..13c6d3f 100644 --- a/agents/design-reviewer.md +++ b/agents/design-reviewer.md @@ -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 diff --git a/docs/tdd/0064-judgment-worker-defaults.md b/docs/tdd/0064-judgment-worker-defaults.md index e7dfca4..197cc13 100644 --- a/docs/tdd/0064-judgment-worker-defaults.md +++ b/docs/tdd/0064-judgment-worker-defaults.md @@ -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 diff --git a/scripts/lib/models.sh b/scripts/lib/models.sh index e64def5..1763924 100644 --- a/scripts/lib/models.sh +++ b/scripts/lib/models.sh @@ -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= review= verify= +# 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 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 family name (rc 1 on empty id) +# tl_model_tier light | above | unknown +# tl_parent_model parent id; rc 1 + one stderr line +# `tl_parent_model: ` +# tl_parent_effort ` env|settings` | `unknown -` +# tl_plan_class [tdd-path] mechanical | nontrivial +# tl_resolve_models [tdd] [parent] build= review= verify= +# tl_model_sources [tdd] [parent] build_src= review_src= verify_src= +# +# 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 — 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 — `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 — 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 — print the newest assistant `.message.model` +# that is neither empty nor ``. 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 . != ""))' 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 != "": + print(v) + break +' 2>/dev/null + else + return 3 + fi + return 0 +} + +# _tl_json_top_string — print the top-level string field +# of the JSON document (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 — 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 — print the summary.json path +# for (percent-encoded) if it exists. Rejects any `..` so the path stays +# under . +_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 — ` env` | ` 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 [tdd-path] [parent-id] — print +# ` `; 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= review= +# verify=`; 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= +# review_src= verify_src=`; ∈ parent, pin:, 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##* }" } diff --git a/skills/tdd-author/SKILL.md b/skills/tdd-author/SKILL.md index cc7794d..69b4e38 100644 --- a/skills/tdd-author/SKILL.md +++ b/skills/tdd-author/SKILL.md @@ -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 `. diff --git a/tests/build-tdds-skill.test.sh b/tests/build-tdds-skill.test.sh index 500458f..411ccd8 100755 --- a/tests/build-tdds-skill.test.sh +++ b/tests/build-tdds-skill.test.sh @@ -50,18 +50,21 @@ echo "[C] skill names require_flip, next_gate, tokens, no nested spawn" || bad "missing 'must not spawn children'" ) || true -# --- [D] models.sh is the ADR 0009 binding site ------------------------------ -echo "[D] models.sh binds tl_resolve_models" +# --- [D] models.sh is the ADR 0015 binding site (TDD 0064) ------------------- +# No pins (incl. CLAUDE_CODE_SUBAGENT_MODEL), no session id, no args → every +# judgment slot inherits the parent session's model. +echo "[D] models.sh binds tl_resolve_models (inherit by default)" ( bash -n "$MODELS" 2>"$ROOT/D.err" \ && ok "models.sh parses" \ || bad "models.sh bash -n failed: $(cat "$ROOT/D.err" 2>/dev/null)" if env -u GROK_PLUGIN_ROOT -u THROUGHLINE_BUILD_MODEL \ -u THROUGHLINE_REVIEW_MODEL -u THROUGHLINE_RUNTIME_VERIFY_MODEL \ + -u CLAUDE_CODE_SUBAGENT_MODEL -u CLAUDE_CODE_SESSION_ID \ bash -c "source \"$MODELS\"; tl_resolve_models" >"$ROOT/D.out" 2>"$ROOT/D2.err"; then out="$(cat "$ROOT/D.out")" - printf '%s' "$out" | grep -qE '^build=.+ review=.+ verify=.+$' \ - && ok "prints build= review= verify= ($out)" \ - || bad "unexpected resolve line: $out" + [ "$out" = "build=inherit review=inherit verify=inherit" ] \ + && ok "no-arg line is build=inherit review=inherit verify=inherit" \ + || bad "unexpected resolve line: '$out' want 'build=inherit review=inherit verify=inherit'" else bad "tl_resolve_models failed: $(cat "$ROOT/D2.err")" fi diff --git a/tests/implement-gate.test.sh b/tests/implement-gate.test.sh index 7ba2bca..05b45c34 100755 --- a/tests/implement-gate.test.sh +++ b/tests/implement-gate.test.sh @@ -6,7 +6,8 @@ # # Coprocess/runner evals were removed with the implement.sh body (TDD 0063+). # This aggregator keeps the 0063 stub/status observations and the live -# 0060–0062 helpers: plugin-root, verdicts, run-record, build-tdds-skill. +# 0060–0062 helpers: plugin-root, verdicts, run-record, build-tdds-skill, +# and the 0064 model-roles eval. # # Run: bash tests/implement-gate.test.sh set -uo pipefail @@ -189,9 +190,10 @@ PASS="$(grep -c '^ok$' "$RESULTS" 2>/dev/null)"; PASS="${PASS:-0}" FAIL="$(grep -c '^fail$' "$RESULTS" 2>/dev/null)"; FAIL="${FAIL:-0}" echo "=== 0063 observations: $PASS passed, $FAIL failed ===" -# 0060–0062 evals (kept). Each file is invoked only when it exists. +# 0060–0062 evals (kept) + 0064 model-roles. Each file is invoked only when +# it exists. EVAL_FAIL=0 -for ev in plugin-root verdicts run-record build-tdds-skill; do +for ev in plugin-root verdicts run-record build-tdds-skill model-roles; do f="$(dirname "$0")/${ev}.test.sh" if [ -f "$f" ]; then echo diff --git a/tests/model-roles.test.sh b/tests/model-roles.test.sh new file mode 100755 index 0000000..feb55a6 --- /dev/null +++ b/tests/model-roles.test.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# model-roles.test.sh — eval for TDD 0064 / NFR-3, FR-52, FR-87, FR-10, +# FR-15(d), FR-50: EXECUTES every scripts/lib/models.sh function in a clean +# `env -i HOME= PATH="$PATH" bash -c '. models.sh; …'` shell against +# fixture TDDs, Claude transcripts and a Grok summary.json (no network), and +# pins the fresh-worker independence wording (design-reviewer, /tdd-author 7b, +# plugin.json). Observation numbers [1]–[15] are the TDD's Verification plan. +# +# Written red-first. Run: bash tests/model-roles.test.sh +set -uo pipefail +REPO="$(cd "$(dirname "$0")/.." && pwd)" +MODELS="$REPO/scripts/lib/models.sh" +DR="$REPO/agents/design-reviewer.md" +TA="$REPO/skills/tdd-author/SKILL.md" +PJ="$REPO/.claude-plugin/plugin.json" +RESULTS="$(mktemp)"; export RESULTS +ok() { printf 'ok\n' >>"$RESULTS"; printf ' ok — %s\n' "$1"; } +bad() { printf 'fail\n' >>"$RESULTS"; printf ' FAIL — %s\n' "$1"; } + +ROOT="$(mktemp -d)"; trap 'rm -rf "$ROOT" "$RESULTS"' EXIT +H="$ROOT/home"; mkdir -p "$H" +q() { printf '%q' "$1"; } + +# m [VAR=val ...] '' — source models.sh then run in a CLEAN shell +# (env -i: no caller variable or function leaks in). A later PATH=/HOME= in the +# VAR list overrides the default. Sets OUT, ERR, RC, NL (stdout line count). +m() { + local cmd="${!#}" + local -a envs=("${@:1:$#-1}") + env -i HOME="$H" PATH="$PATH" ${envs[@]+"${envs[@]}"} \ + bash -c ". $(q "$MODELS") || exit 97; $cmd" >"$ROOT/out" 2>"$ROOT/err" + RC=$? + OUT="$(cat "$ROOT/out")"; ERR="$(cat "$ROOT/err")" + NL="$(wc -l <"$ROOT/out" | tr -d ' ')" +} +# eq