fix(#2589): use --raw/--pick for config/model/verify lookups, drop jq dep (#2673)

* fix(#2589): use --raw/--pick for config/model/verify lookups, drop jq dep

The reviewer/workflow config lookups resolved scalars and object fields with a
`gsd_run query <cmd> … | jq … 2>/dev/null || <default>` shape. On any machine
without jq (the default on Windows/Git-Bash) the jq stage fails with exit 127,
the failure is swallowed by 2>/dev/null + the trailing || default, and the
variable comes back EMPTY — the configured per-lane model/host/budget is
silently dropped and the lane falls back to CLI defaults with no diagnostic.

gsd-tools ships native flags that do the same job with no external dep:
  config-get <key> --raw          (strips JSON quotes off a scalar)
  resolve-model <id> --pick model (descends an object)
  resolve-execution … --pick <f>  (same)
  verification.status … --pick status

Replaced every jq-piped config/model/verify lookup across review.md (×23),
plan-phase.md, ship.md, debug.md (incl. the redundant boolean coercion — --raw
returns true/false as bare tokens natively), autonomous.md (×2),
ai-integration-phase.md (×4), and eval-review.md. The legitimate structured-JSON
jq sites that parse HTTP curl responses (.choices[0], jq -rs, jq -n --rawfile)
are untouched — only the jq-replaceable lookups moved to the native flags.

Adds tests/fix-2589-config-get-no-jq.test.cjs: a source-invariant guard asserting
no audited workflow pipes config-get/resolve-model/resolve-execution/verification.status
to jq (fails-first on the pre-fix text, passes after).

* test(#2589): update autonomous-converge jq assertion to --pick; regen golden fixtures

Two test consequences of the workflow-doc edits in the prior commit:

1. tests/autonomous-converge.test.cjs pinned the OLD jq-dependent shape
   (`verification.status … | jq -r '.status//empty'`) as the canonical routing
   contract. The test's INTENT is correct (route human validation through
   canonical verification.status) but it over-specified the MECHANISM (the jq
   pipe). Updated the assertion to match the new native --pick status shape;
   the contract being guarded (canonical verification.status read before the
   human_needed branch) is unchanged.

2. The golden-install-parity fixtures (19 runtimes) record a content hash of
   every installed workflow .md; the 7 edited workflows changed those hashes.
   Regenerated via `npm run gen:golden` (the test's own failure message
   instructs this). Only the 7 edited workflow hashes changed in each fixture.

* fix(#2589): declare jq a prerequisite for the lanes that still need it; repair test file

Three defects in the first cut of the #2589 fix:

1. tests/autonomous-converge.test.cjs was a JavaScript syntax error. The regex
   literal /...2>\/dev/null .../ left the second slash unescaped, terminating the
   literal early and parsing `null` as regex flags:
     SyntaxError: Invalid regular expression flags
   The whole file failed to load, so every assertion in it — including the #1522
   and #1526 guards — silently stopped running. Replaced with the string-compare
   form already used at line 202 for the sibling shell-snippet assertion.

2. lint:ci failed. tests/fix-2589-config-get-no-jq.test.cjs buckets into the
   capped `config` production module via its `config-get-...` effective prefix,
   making it a novel offender against the 2-file cap. The test is about workflow
   documents, not the config module, so it is renamed to
   fix-2589-workflow-jq-dependency.test.cjs (free prefix) rather than growing the
   allowlist with a module that does not actually need a 5th test file.

3. The fix deleted the repo's only jq-prerequisite declaration. review.md:244
   ("install jq if missing") was the anchor plan-review-convergence.md cites by
   line number, and it went away with the jq pipes — while the ollama, lm_studio,
   llama_cpp, opencode, and agy lanes still hard-require jq to parse HTTP
   /v1/chat/completions responses, opencode's JSONL event stream, and agy's
   conversation cache. On a jq-less host those five lanes swallow exit 127 into
   empty output: the same silent-degradation class #2589 exists to close.

   detect_clis now probes jq alongside the other prerequisites and emits
   jq:available / jq:missing, and the five dependent lanes are treated as
   undetected when it is absent, with an install hint. The six lanes that do not
   need jq stay selectable. plan-review-convergence.md now cites the section by
   name instead of a line number that moves.

Regression guards added to the renamed test file: review.md must keep the jq
probe and must name all five dependent lanes, and no workflow may cite review.md
by line number. Workflow-size baseline and the 19 install-parity goldens
regenerated for the review.md / plan-review-convergence.md edits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2589): decide the ship verification gate on a single verification.status read

Isolated review finding (medium). Pre-fix, ship.md captured verification.status
ONCE into $VERIFICATION and picked status / next_action / next_command off that
cached JSON with three jq calls. --pick takes a single dot-path field, so the
mechanical conversion issued three separate queries up front: three node spawns
that each re-read the phase VERIFICATION.md and re-derive the commit-time vs
mtime staleness comparison, on every ship — including the common passing path
that never uses the two message fields. It also meant the gate's verdict and the
message shown to the user were derived from three reads with no guarantee they
observed the same state.

The gate now reads `status` once and decides. The two message-only fields are
read on the blocking path only, after PHASE_VERIFICATION_INCOMPLETE is already
determined — so the passing path costs one query instead of three, and a
concurrent write between reads can no longer make the gate and its message
disagree, because the block/allow decision no longer depends on them.

Adding a multi-field --pick to gsd-tools would have collapsed this to one query,
but that changes the flag's output contract and belongs in its own change.

Regression guard in tests/fix-2589-workflow-jq-dependency.test.cjs: ship.md must
read verification.status exactly three times total, the block decision must
follow the status read, and next_action / next_command must both appear after
the blocking prose so they cannot drift back onto the passing path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* docs(#2589): document the jq prerequisite for the five reviewer lanes that need it

/gsd-review's ollama, lm_studio, llama_cpp, opencode, and agy lanes parse JSON
GSD does not produce (OpenAI-compatible /v1/chat/completions responses,
OpenCode's JSONL event stream, Antigravity's conversation cache), so they require
jq on PATH. Nothing in docs/ said so. Records which five lanes need it, which six
do not, that reading configured models/hosts/budgets no longer requires jq at
all, and what /gsd-review now does when jq is absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2589): backfill changeset pr number (#2673)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-26 16:17:02 -04:00
committed by GitHub
parent 89b673e40d
commit 920a5f3f06
32 changed files with 450 additions and 211 deletions

View File

@@ -33,10 +33,10 @@ Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded
Resolve agent models:
```bash
SELECTOR_MODEL=$(gsd_run query resolve-model gsd-framework-selector 2>/dev/null | jq -r '.model' 2>/dev/null || true)
RESEARCHER_MODEL=$(gsd_run query resolve-model gsd-ai-researcher 2>/dev/null | jq -r '.model' 2>/dev/null || true)
DOMAIN_MODEL=$(gsd_run query resolve-model gsd-domain-researcher 2>/dev/null | jq -r '.model' 2>/dev/null || true)
PLANNER_MODEL=$(gsd_run query resolve-model gsd-eval-planner 2>/dev/null | jq -r '.model' 2>/dev/null || true)
SELECTOR_MODEL=$(gsd_run query resolve-model gsd-framework-selector --pick model 2>/dev/null || true)
RESEARCHER_MODEL=$(gsd_run query resolve-model gsd-ai-researcher --pick model 2>/dev/null || true)
DOMAIN_MODEL=$(gsd_run query resolve-model gsd-domain-researcher --pick model 2>/dev/null || true)
PLANNER_MODEL=$(gsd_run query resolve-model gsd-eval-planner --pick model 2>/dev/null || true)
```
Check config:

View File

@@ -490,7 +490,7 @@ Skill(skill="gsd-code-review", args="${PHASE_NUM} --fix --auto")
After execute, read canonical verification:
```bash
VERIFY_STATUS=$(gsd_run query verification.status "${PHASE_DIR}" 2>/dev/null | jq -r '.status//empty')
VERIFY_STATUS=$(gsd_run query verification.status "${PHASE_DIR}" --pick status 2>/dev/null || true)
```
If `PHASE_DIR` is absent, re-fetch `init.phase-op ${PHASE_NUM}` and parse `phase_dir`.
@@ -548,7 +548,7 @@ Skill(skill="gsd-execute-phase", args="${PHASE_NUM} --no-transition")
Re-read verification status:
```bash
VERIFY_STATUS=$(gsd_run query verification.status "${PHASE_DIR}" 2>/dev/null | jq -r '.status//empty')
VERIFY_STATUS=$(gsd_run query verification.status "${PHASE_DIR}" --pick status 2>/dev/null || true)
```
If `passed` or `human_needed`: route normally.

View File

@@ -27,12 +27,12 @@ Extract `commit_docs` and `config.response_language` from init JSON. Extract `de
Resolve debugger model:
```bash
debugger_model=$(gsd_run query resolve-model gsd-debugger 2>/dev/null | jq -r '.model' 2>/dev/null || true)
debugger_model=$(gsd_run query resolve-model gsd-debugger --pick model 2>/dev/null || true)
```
Read TDD mode from config:
```bash
TDD_MODE=$(gsd_run query config-get workflow.tdd_mode 2>/dev/null | jq -r 'if type == "boolean" then tostring else . end' 2>/dev/null || echo "false")
TDD_MODE=$(gsd_run query config-get workflow.tdd_mode --raw 2>/dev/null || echo "false")
```
## 1a. LIST subcommand

View File

@@ -24,7 +24,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `commit_docs`.
```bash
AUDITOR_MODEL=$(gsd_run query resolve-model gsd-eval-auditor 2>/dev/null | jq -r '.model' 2>/dev/null || true)
AUDITOR_MODEL=$(gsd_run query resolve-model gsd-eval-auditor --pick model 2>/dev/null || true)
AGENT_SKILLS_AUDITOR=$(gsd_run query agent-skills gsd-eval-auditor)
```

View File

@@ -1292,7 +1292,7 @@ Skipping bounce step.
**Read pass count:**
```bash
BOUNCE_PASSES=$(gsd_run query config-get workflow.plan_bounce_passes 2>/dev/null || echo "2")
BOUNCE_SCRIPT=$(gsd_run query config-get workflow.plan_bounce_script 2>/dev/null | jq -r '.' 2>/dev/null || true)
BOUNCE_SCRIPT=$(gsd_run query config-get workflow.plan_bounce_script --raw 2>/dev/null || true)
```
Display banner:

View File

@@ -82,8 +82,9 @@ Then re-run: /gsd:plan-review-convergence {PHASE}
if [ -z "$REVIEWER_FLAGS" ]; then
DEFAULT_REVIEWERS_JSON=$(gsd_run query config-get review.default_reviewers 2>/dev/null || echo "")
if ! command -v jq >/dev/null 2>&1; then
# jq is a documented production dependency (review.md:244 — "install jq if
# missing"). If it is absent we cannot inspect the configured default, so
# jq is a documented production dependency (review.md, detect_clis — the
# "jq-dependent reviewer lanes" note). If it is absent we cannot inspect
# the configured default (it is a JSON array, not a --raw/--pick scalar), so
# fail safe with --codex and surface the reason rather than silently
# reproducing the #2315 override under degraded conditions.
echo "WARNING: jq not on PATH — cannot read review.default_reviewers; falling back to --codex (#2315)" >&2

View File

@@ -26,19 +26,44 @@ command -v cursor-agent >/dev/null 2>&1 && echo "cursor:available" || echo "curs
command -v agy >/dev/null 2>&1 && echo "antigravity:available" || echo "antigravity:missing"
# Check local model servers (OpenAI-compatible HTTP API — no CLI binary required)
OLLAMA_HOST=$(gsd_run query config-get review.ollama_host 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
OLLAMA_HOST=$(gsd_run query config-get review.ollama_host --raw 2>/dev/null || echo "")
if [ -z "$OLLAMA_HOST" ] || [ "$OLLAMA_HOST" = "null" ]; then OLLAMA_HOST="http://localhost:11434"; fi
curl -s --max-time 2 "${OLLAMA_HOST}/v1/models" >/dev/null 2>&1 && echo "ollama:available" || echo "ollama:missing"
LM_STUDIO_HOST=$(gsd_run query config-get review.lm_studio_host 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
LM_STUDIO_HOST=$(gsd_run query config-get review.lm_studio_host --raw 2>/dev/null || echo "")
if [ -z "$LM_STUDIO_HOST" ] || [ "$LM_STUDIO_HOST" = "null" ]; then LM_STUDIO_HOST="http://localhost:1234"; fi
curl -s --max-time 2 "${LM_STUDIO_HOST}/v1/models" >/dev/null 2>&1 && echo "lm_studio:available" || echo "lm_studio:missing"
LLAMA_CPP_HOST=$(gsd_run query config-get review.llama_cpp_host 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
LLAMA_CPP_HOST=$(gsd_run query config-get review.llama_cpp_host --raw 2>/dev/null || echo "")
if [ -z "$LLAMA_CPP_HOST" ] || [ "$LLAMA_CPP_HOST" = "null" ]; then LLAMA_CPP_HOST="http://localhost:8080"; fi
curl -s --max-time 2 "${LLAMA_CPP_HOST}/v1/models" >/dev/null 2>&1 && echo "llama_cpp:available" || echo "llama_cpp:missing"
# jq prerequisite (#2589). The config/model/budget lookups in this workflow no
# longer need jq — they use the native --raw/--pick flags. But the lanes listed
# under "jq-dependent reviewer lanes" below parse structured JSON that gsd-tools
# does not emit (OpenAI-compatible /v1/chat/completions responses, opencode's
# JSONL event stream, agy's conversation cache), so they cannot run without jq.
# Probe it here rather than letting each lane swallow exit 127 into empty output.
command -v jq >/dev/null 2>&1 && echo "jq:available" || echo "jq:missing"
```
**jq-dependent reviewer lanes.** `jq` is a production prerequisite for the
`ollama`, `lm_studio`, `llama_cpp`, `opencode`, and `antigravity` lanes only. If
`detect_clis` reports `jq:missing`, treat those five as **undetected** — they
follow the same "known-but-undetected" path as a missing CLI (info note, ignored;
and if they were the only selected reviewers, fail with the actionable message
below rather than producing an empty review). Tell the user to install jq:
```
NOTE: jq is not on PATH — the ollama, lm_studio, llama_cpp, opencode, and
antigravity reviewer lanes are unavailable. Install jq (https://jqlang.org/download/)
or select a lane that does not require it (--gemini, --claude, --codex,
--coderabbit, --qwen, --cursor).
```
The remaining lanes (`gemini`, `claude`, `codex`, `coderabbit`, `qwen`, `cursor`)
do not require jq and must stay selectable on a jq-less host.
Parse flags from `$ARGUMENTS`:
- `--gemini` → include Gemini
- `--claude` → include Claude
@@ -241,20 +266,21 @@ Note: `INSTRUCTIONS_BLOCK_FILE`, `ROADMAP_SECTION_FILE`, and `PHASE_DIR` come fr
Read model preferences from planning config. Null/missing values fall back to CLI defaults.
```bash
# JSON scalars from gsd-tools.cjs query; use jq -r to strip JSON string quotes (install jq if missing)
GEMINI_MODEL=$(gsd_run query config-get review.models.gemini 2>/dev/null | jq -r '.' 2>/dev/null || true)
CLAUDE_MODEL=$(gsd_run query config-get review.models.claude 2>/dev/null | jq -r '.' 2>/dev/null || true)
CODEX_MODEL=$(gsd_run query config-get review.models.codex 2>/dev/null | jq -r '.' 2>/dev/null || true)
OPENCODE_MODEL=$(gsd_run query config-get review.models.opencode 2>/dev/null | jq -r '.' 2>/dev/null || true)
# JSON scalars from gsd-tools.cjs query; --raw strips the JSON quotes natively
# (no jq dependency — jq is absent on stock Windows/Git-Bash, #2589)
GEMINI_MODEL=$(gsd_run query config-get review.models.gemini --raw 2>/dev/null || true)
CLAUDE_MODEL=$(gsd_run query config-get review.models.claude --raw 2>/dev/null || true)
CODEX_MODEL=$(gsd_run query config-get review.models.codex --raw 2>/dev/null || true)
OPENCODE_MODEL=$(gsd_run query config-get review.models.opencode --raw 2>/dev/null || true)
# review.models.agy, when set, is passed to agy as --model (escape hatch for a
# pinned model that 404s server-side); otherwise agy uses its persisted default.
AGY_MODEL=$(gsd_run query config-get review.models.agy 2>/dev/null | jq -r '.' 2>/dev/null || true)
AGY_MODEL=$(gsd_run query config-get review.models.agy --raw 2>/dev/null || true)
# Reasoning effort per reviewer (#2481). Empty unless the host's effortSurface
# axis is `argv`. Pass --attempt N to walk ADR-443's escalation ladder.
CLAUDE_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host claude 2>/dev/null | jq -r '.effort_argv_string // ""' 2>/dev/null || true)
CODEX_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host codex 2>/dev/null | jq -r '.effort_argv_string // ""' 2>/dev/null || true)
OPENCODE_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host opencode 2>/dev/null | jq -r '.effort_argv_string // ""' 2>/dev/null || true)
CLAUDE_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host claude --pick effort_argv_string 2>/dev/null || true)
CODEX_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host codex --pick effort_argv_string 2>/dev/null || true)
OPENCODE_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host opencode --pick effort_argv_string 2>/dev/null || true)
```
**No hook-trust bypass (#2479):** the codex invocations below deliberately pass no
@@ -644,9 +670,9 @@ prepare_trimmed_prompt_for_reviewer() {
}
# Resolve prompt budget for Ollama: per-reviewer override > global default > null
OLLAMA_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.ollama 2>/dev/null | jq -r '.' 2>/dev/null || echo "null")
OLLAMA_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.ollama --raw 2>/dev/null || echo "null")
if [ -z "$OLLAMA_REVIEWER_BUDGET" ] || [ "$OLLAMA_REVIEWER_BUDGET" = "null" ]; then
OLLAMA_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens 2>/dev/null | jq -r '.' 2>/dev/null || echo "null")
OLLAMA_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens --raw 2>/dev/null || echo "null")
fi
# Apply budget trim for Ollama if a budget is configured
@@ -670,9 +696,9 @@ if [ -n "$OLLAMA_REVIEWER_BUDGET" ] && [ "$OLLAMA_REVIEWER_BUDGET" != "null" ] &
fi
if [ "$OLLAMA_SKIP" != "1" ]; then
OLLAMA_HOST=$(gsd_run query config-get review.ollama_host 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
OLLAMA_HOST=$(gsd_run query config-get review.ollama_host --raw 2>/dev/null || echo "")
if [ -z "$OLLAMA_HOST" ] || [ "$OLLAMA_HOST" = "null" ]; then OLLAMA_HOST="http://localhost:11434"; fi
OLLAMA_MODEL=$(gsd_run query config-get review.models.ollama 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
OLLAMA_MODEL=$(gsd_run query config-get review.models.ollama --raw 2>/dev/null || echo "")
if [ -z "$OLLAMA_MODEL" ] || [ "$OLLAMA_MODEL" = "null" ]; then
OLLAMA_MODEL=$(curl -s --max-time 2 "${OLLAMA_HOST}/v1/models" 2>/dev/null | jq -r '.data[0].id // "llama3"' 2>/dev/null || echo "llama3")
fi
@@ -692,9 +718,9 @@ fi
**LM Studio (local, OpenAI-compatible):**
```bash
# Resolve prompt budget for LM Studio: per-reviewer override > global default > null
LM_STUDIO_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.lm_studio 2>/dev/null | jq -r '.' 2>/dev/null || echo "null")
LM_STUDIO_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.lm_studio --raw 2>/dev/null || echo "null")
if [ -z "$LM_STUDIO_REVIEWER_BUDGET" ] || [ "$LM_STUDIO_REVIEWER_BUDGET" = "null" ]; then
LM_STUDIO_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens 2>/dev/null | jq -r '.' 2>/dev/null || echo "null")
LM_STUDIO_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens --raw 2>/dev/null || echo "null")
fi
# Apply budget trim for LM Studio if a budget is configured
@@ -718,9 +744,9 @@ if [ -n "$LM_STUDIO_REVIEWER_BUDGET" ] && [ "$LM_STUDIO_REVIEWER_BUDGET" != "nul
fi
if [ "$LM_STUDIO_SKIP" != "1" ]; then
LM_STUDIO_HOST=$(gsd_run query config-get review.lm_studio_host 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
LM_STUDIO_HOST=$(gsd_run query config-get review.lm_studio_host --raw 2>/dev/null || echo "")
if [ -z "$LM_STUDIO_HOST" ] || [ "$LM_STUDIO_HOST" = "null" ]; then LM_STUDIO_HOST="http://localhost:1234"; fi
LM_STUDIO_MODEL=$(gsd_run query config-get review.models.lm_studio 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
LM_STUDIO_MODEL=$(gsd_run query config-get review.models.lm_studio --raw 2>/dev/null || echo "")
if [ -z "$LM_STUDIO_MODEL" ] || [ "$LM_STUDIO_MODEL" = "null" ]; then
LM_STUDIO_MODEL=$(curl -s --max-time 2 "${LM_STUDIO_HOST}/v1/models" 2>/dev/null | jq -r '.data[0].id // "local-model"' 2>/dev/null || echo "local-model")
fi
@@ -745,9 +771,9 @@ fi
**llama.cpp (local, OpenAI-compatible):**
```bash
# Resolve prompt budget for llama.cpp: per-reviewer override > global default > null
LLAMA_CPP_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.llama_cpp 2>/dev/null | jq -r '.' 2>/dev/null || echo "null")
LLAMA_CPP_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.llama_cpp --raw 2>/dev/null || echo "null")
if [ -z "$LLAMA_CPP_REVIEWER_BUDGET" ] || [ "$LLAMA_CPP_REVIEWER_BUDGET" = "null" ]; then
LLAMA_CPP_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens 2>/dev/null | jq -r '.' 2>/dev/null || echo "null")
LLAMA_CPP_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens --raw 2>/dev/null || echo "null")
fi
# Apply budget trim for llama.cpp if a budget is configured
@@ -771,9 +797,9 @@ if [ -n "$LLAMA_CPP_REVIEWER_BUDGET" ] && [ "$LLAMA_CPP_REVIEWER_BUDGET" != "nul
fi
if [ "$LLAMA_CPP_SKIP" != "1" ]; then
LLAMA_CPP_HOST=$(gsd_run query config-get review.llama_cpp_host 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
LLAMA_CPP_HOST=$(gsd_run query config-get review.llama_cpp_host --raw 2>/dev/null || echo "")
if [ -z "$LLAMA_CPP_HOST" ] || [ "$LLAMA_CPP_HOST" = "null" ]; then LLAMA_CPP_HOST="http://localhost:8080"; fi
LLAMA_CPP_MODEL=$(gsd_run query config-get review.models.llama_cpp 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
LLAMA_CPP_MODEL=$(gsd_run query config-get review.models.llama_cpp --raw 2>/dev/null || echo "")
if [ -z "$LLAMA_CPP_MODEL" ] || [ "$LLAMA_CPP_MODEL" = "null" ]; then
LLAMA_CPP_MODEL=$(curl -s --max-time 2 "${LLAMA_CPP_HOST}/v1/models" 2>/dev/null | jq -r '.data[0].id // "local-model"' 2>/dev/null || echo "local-model")
fi

View File

@@ -52,12 +52,19 @@ Verify the work is ready to ship:
1. **Verification passed?**
```bash
VERIFICATION=$(gsd_run query verification.status "${PHASE_DIR}" 2>/dev/null)
STATUS=$(printf '%s' "$VERIFICATION" | jq -r '.status' 2>/dev/null || echo "")
NEXT_ACTION=$(printf '%s' "$VERIFICATION" | jq -r '.next_action' 2>/dev/null || echo "")
NEXT_COMMAND=$(printf '%s' "$VERIFICATION" | jq -r '.next_command' 2>/dev/null || echo "")
# The gate decides on ONE read. --pick takes a single field, so the two
# human-facing fields are read only on the blocking path below — never on the
# passing path — rather than issuing three queries up front (#2589).
STATUS=$(gsd_run query verification.status "${PHASE_DIR}" --pick status 2>/dev/null || echo "")
```
Only `passed` may ship. If `$STATUS` is `passed`, verification is complete — continue to the next preflight check. Any other value (including `gaps_found`, `human_needed`, `missing`, and `unknown`) blocks with `PHASE_VERIFICATION_INCOMPLETE`: present `$NEXT_ACTION` to the user and, when `$NEXT_COMMAND` is non-empty, show it as the command to run next. The query already handles missing files and unexpected values, so no per-status arm is needed.
Only `passed` may ship. If `$STATUS` is `passed`, verification is complete — continue to the next preflight check; do not read any further verification field.
Any other value (including `gaps_found`, `human_needed`, `missing`, and `unknown`) blocks with `PHASE_VERIFICATION_INCOMPLETE`. Only then, read the two message fields:
```bash
NEXT_ACTION=$(gsd_run query verification.status "${PHASE_DIR}" --pick next_action 2>/dev/null || echo "")
NEXT_COMMAND=$(gsd_run query verification.status "${PHASE_DIR}" --pick next_command 2>/dev/null || echo "")
```
Present `$NEXT_ACTION` to the user and, when `$NEXT_COMMAND` is non-empty, show it as the command to run next. These two are message text only — the block/allow decision has already been made from `$STATUS`, so a concurrent write between the reads cannot change the gate's verdict. The query already handles missing files and unexpected values, so no per-status arm is needed.
2. **Clean working tree?**
```bash
@@ -346,7 +353,7 @@ Report: "PR #{number} created: {url}"
Before prompting the user, check if an external review command is configured:
```bash
REVIEW_CMD=$(gsd_run query config-get workflow.code_review_command 2>/dev/null | jq -r '.' 2>/dev/null || echo "")
REVIEW_CMD=$(gsd_run query config-get workflow.code_review_command --raw 2>/dev/null || echo "")
```
If `REVIEW_CMD` is non-empty and not `"null"`, run the external review: