Files
gstack/bin/gstack-render-claude.sh
Garry Tanandcapy-ai[bot] 9d9d4c31bb v1.93.0.0 wave(browse): #3700 #3773 #3779 #3817 #3842 (#3858)
* feat(loadouts): Opus 5.5 lite loadout with explicit model detection (GSTA-161)

modules/loadouts.json gains the lite loadout (eleven coaching preamble
pieces dropped; upgrade-check and brain-sync kept) and a models map that
sends only claude-opus-5-5 (and its dated snapshots) to it. Detection is
explicit: Capy passes --model to the installer, Claude Code opts in with
./setup --loadout lite; anything unmapped, invalid or unreadable serves full.
The checkout always stays the full render.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(loadouts): Opus 5.5 lite, detection order, Capy SKILL_ROOT, setup --loadout; troubleshooting anchors; changelog fragment (GSTA-161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(loadouts): claude_loadout read through CONFIG_BIN; capy skill context ceiling for the --model line (GSTA-161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (prune keeps a reused loadout folder, GSTACK_LOADOUT_SKILL_ROOT, claude_loadout guard test) (GSTA-161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (Capy skill passes the model as GSTACK_MODEL so an older installer ignores it) (GSTA-161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (prune only owned, unused folders; full root from the canonical Claude install; fallback reason and source) (GSTA-161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: loadout source example, install and resolve commands, Capy launch-file note (GSTA-161)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(autoplan): --ceo-only runs the CEO review alone with zero questions (GST-537)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: classify autoplan-ceo-only in the Codex host metadata

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: select autoplan-ceo-only on the skills it runs; say it needs Claude Code

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: run autoplan-ceo-only in the marathon lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog entry for /autoplan --ceo-only

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: count the autoplan-ceo-only paid file in the shard inventory

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat: gstack-autoplan next --ceo-only runs the CEO review alone and ends reviewed (GST-537)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: budget and close recover a budget-stopped CEO-only run; reviewed is at rest for checkpoints

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: CEO-only finish leaves owner-only cap items undecided, refuses marker phase flags and a plan at the report path, and replaces ceo-review.md atomically

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: the CEO-only report-path guard compares real paths and refuses at the finish when the report file is the plan

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: CEO_ONLY_CONFLICT covers a plan linked to the report path

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(browse): failing-first device and touch tests (GSTA-227 F3, #3058)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: the report-path guard keeps the lexical check, skips safe hard links and compares bigint file ids

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* browse: per-session tabs for parallel agents; tab pins stop moving the shared active tab (#1961)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* browse: keep the server.ts tab-pin anchor comment for source guards

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(browse): per-tab phone emulation and touch input ($B device, tap, swipe, pinch, touch; #3058, GSTA-227 F3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: copy lib/result-codes-autoplan.ts into the fixtures that copy the result-code tables, and keep it out of derived closures

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(qa, aside): touch pointer in /qa, Aside touch recipe verified on Aside 1.26, report Phones row (GSTA-227 F3, #3058)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* browse: drop the sessions paragraph from /browse to stay inside the parity cap

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: F3 phone emulation and touch entry

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* browse: hide the ps window in the session harness walk

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the decisions.jsonl status list names auto_kept

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(browse): session cap, LRU eviction, headed release and scoped auto-session rows

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* browse: sessions keep tab <id> picks, sidebar claude stays on the user's tab, pinned rebuild and state load rebind the scope

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (touch cancel with no finger held is a no-op)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: Step 11 review fixes (headed tab adoption, pinned viewport reset, CLI retry keeps --session, rebuild id remap, --json notice, evicted-session reset, device kept after annotated captures, render opts out of sessions, session: client ids reserved)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: headed viewport auto keeps the user's active tab; render's closetab opts out of sessions

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: sessions, phone emulation and touch in BROWSER.md; session: client ids reserved; TODOS #1961/#3058 shipped

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: copy lib/result-codes-browse.ts into the fixtures that copy the result-code modules

lib/result-codes.ts imports ./result-codes-browse, so fixtures with an explicit
lib list failed with 'Cannot find module'. pr-dependencies lists it with the other
reason-code modules as selection data. GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat: gstack-clean reclaims per-repo .gstack scratch and logs, dry run by default (#2627)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* browse: name the Aside touch recipe's coordinates <tap-x>/<tap-y> with a numeric grammar

The generated-bash lint (INV-3) rejected the free-text <x>/<y> placeholders. GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(clean): leave hard-linked and unreadable logs alone; cut through a no-follow descriptor checked against the evaluated inode

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(clean): tests that expect tmp removal see a quiet ps, so a codex elsewhere on the host cannot flake them

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(clean): an unlistable tmp/ skips its group; liveness asked once per group on apply; BROWSER.md says which logs reset at daemon start

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(clean): write output synchronously so --json reaches a pipe whole; test fixes for root, wrapper git and the ps stub

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(clean): every change rechecks the verified folder and the measured file; full writes; EPIPE ends output quietly

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(clean): the early-close test checks the bin's own exit status

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: list gstack-clean among the standalone CLIs

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(clean): copied-install fixtures and the dependency list carry lib/result-codes-clean.ts

The remote pregate failed 11 cases in gstack-doctor, plan-tune-cathedral and
timeline-stop-hook-slug: each copies an explicit list of lib/result-codes-*.ts
files into a temp install, and lib/result-codes.ts now imports
./result-codes-clean. Same lists GSTA-168 extended for result-codes-eval.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: regenerate gstack-doctor-components.sh for the gstack-clean result codes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* qa: fold the touch pointer into the Phases sentence to fit the parity size cap after the main merge

qa's union was 95988 bytes (ratio 1.142 > maxSizeRatio 1.142, train 75). Now 95975 (1.1419). GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.10.0 after the main merge

GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(clean): classify gstack-tasks rows files (tasks-*) as kept, after main added the producer

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.20.0 after the main merge

GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (restore the lib/result-codes-plan.ts ratchet row the base merge dropped)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(clean): the producer scan ends a name at a colon, so prose after a path names the file (train 78)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.30.0 after the main merge

GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* outside-voice-api: caller receipts, stop_reason, OpenAI max_completion_tokens

A caller can write its own egress receipt (sink, consent, payload class);
the default stays the outside-voice receipt, so existing callers send and
record the same bytes. The result carries the provider's stop reason, and
an OpenAI request gets max_completion_tokens only when the caller sets an
output limit (GST-537 PR 3).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* autoplan gate: item classes and the owner's answer provenance

Gate items carry item_class, ruleKept() names the keep-direction pick, and
answered decision rows record owner_mode, owner_named, rec_hash and
answered_role parsed from the owner's own words (GST-537 PR 3).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* autoplan: shadow-mode decider module, frozen prompt and spend accounting

The switch (off by default, fixed at init), the lease, one model call per
gate with a page retry and a truncation retry under one deadline, the
once-per-revision record with its journaled sha256, the 10% audit mark,
and shadow-labeled ledger rows that count against the cap, show as
shadow= on GATE_SPEND and stay out of the per-voice price (GST-537 PR 3).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* gstack-autoplan: shadow every published gate, the shadow export, config and docs

Every driver command that prints a gate records the shadow first and
checkpoints after it; answer waits for a live lease and records a dead
one retrospectively; gstack-autoplan shadow exports answered revisions
for taste-replay R2 (SHADOW_BEFORE_ANSWER otherwise); gstack-config
autoplan_shadow / autoplan_shadow_model; gstack-artifact validate checks
the record's hash (GST-537 PR 3).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: shadow mode for the CEO-only decider (GST-537 PR 3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* autoplan shadow: /ship review fixes

- register test/fixtures/shadow-fake-model.ts in FREE_FIXTURES (free-fixtures inventory failed)
- withRunLock: count dead-owner reclaims apart from waitMs iterations, so a long wait can still reclaim
- buildPage: fill the decider template in one pass, so slot names inside the plan stay plan text
- revise regate: the shared publish step prints the GATE and ESCALATE lines once

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* autoplan shadow: reply files never follow a link; the retry reserves against the current cap

- a model reply is written by temp file and rename, so a link at the reply path is replaced, not written through
- each shadow reservation reads the run's current spend cap, so a budget change during the call is not undone

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* outside-voice-api: write the egress receipt under the caller's env

callModel resolved the receipt's state root from process.env even when the caller passed env, so the shadow
tests wrote to the real ~/.gstack (the free-suite home-containment guard caught it). The receipt now uses
req.env; the shadow tests give the fake model a private HOME, GSTACK_HOME and GSTACK_STATE_ROOT, and the receipt
test no longer sets process.env to pass.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): CEO-only runs leave main's open Critical/High finding items to the owner (no decision row)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): shadow pages carry main's finding items; a finding item is never ceo_eligible

After merging #3767 (main 90a170c), open Critical/High findings are gate items
(item_class finding). The shadow records one row per gate item, so its pages now
include them. A CEO-only run leaves those items to the owner, so their rows are
ceo_eligible false (rule_pick was already null). The shadow fixtures follow
main's gate (items, counts, "Do it" tokens); the two merge adapters follow
main's doNext(out, rest, price) and its per-voice reservation (--estimate-usd).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.61.0 after the main merge

GSTA-227

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(investigate): drop the five-phase step procedure (GST-938)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: GST-938 fragment wording

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(investigate): keep the web-search sanitize guard; update BROWSER.md and docs/skills.md (review pass 1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: GST-938 wording

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(investigate): pin the web-search guard; changelog size (adversarial review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(clean): classify restamp-staging.json, the restamp producer main gained after #3779's base (wave browse)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (wave browse): scoped newtab keeps a concurrent tab switch; shadow validate catches a deleted record; a failed shadow publish is journaled

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: a failed shadow publish journals under the run lock and never throws (wave browse review pass 2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(unattended): shadow publish failures and the missing-record validate check (wave browse docs audit)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: wave browse integrator fixes fragment

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: shadow reader dedupes by run folder, so two runs started in the same second both count (wave browse CI free-suite 16)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: regenerate doctor result anchors in generator order (wave browse rebuild without #3818)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: name the wave fragment after the rebuilt wave

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-02 14:49:46 +00:00

234 lines
11 KiB
Bash

# gstack-render-claude.sh — the Claude skill render shared by ./setup and
# `gstack-config gbrain-refresh`. Source it; it only defines functions.
# bash-3.2-clean and safe under `set -euo pipefail`.
#
# Overlay selection is opt-in: the overlay is the family of the persisted
# `claude_overlay_model` (written only by `./setup --claude-model <id>`), or
# `claude` when that key is absent. The ID is re-resolved through
# scripts/models.ts at every render, so an ID that stops resolving after an
# upgrade falls back to `claude` with the reason printed.
#
# Each render keeps a plain-text activation record beside it
# (<render dir>.overlay: overlay, source, model, version, gbrain, render, and
# plan_mode=off for a render without the plan-mode preamble), so
# `./setup --status` and the change-only banner never need bun. No record
# means the default `claude` overlay on the committed render.
#
# Callers set _PLAN_MODE_ARG to --plan-mode=off when gstack-config
# plan_mode_preamble is off (#3168); empty or unset means on. The committed
# files carry the preamble, so with it off the default install is rendered.
#
# Callers set _LOADOUT_ARG with gstack_claude_loadout_arg (claude_loadout,
# written by ./setup --loadout; GSTA-161): a non-full loadout is rendered
# into the render dir like a pinned overlay, and the checkout stays full.
#
# Callers define `_swap_in_render` (setup and bin/gstack-config each keep
# their tested copy) and may define `log` and `bun_cmd`; plain echo and bun
# are used otherwise.
_gstack_render_say() {
if declare -F log >/dev/null 2>&1; then log "$@"; else echo "$@"; fi
}
_gstack_render_bun() {
if declare -F bun_cmd >/dev/null 2>&1; then bun_cmd "$@"; else bun "$@"; fi
}
# gstack_claude_overlay SOURCE_DIR CONFIG_BIN — resolve the requested overlay.
# Sets _GSTACK_OVERLAY (family), _GSTACK_OVERLAY_SOURCE (explicit|default),
# _GSTACK_OVERLAY_ID (the configured ID, or claude), _GSTACK_OVERLAY_GENERIC
# (1 when a specific ID uses the generic overlay) and _GSTACK_OVERLAY_NOTE
# (why a configured ID fell back to claude, else empty; printed here).
gstack_claude_overlay() {
local src="$1" CONFIG_BIN="$2" out rc=0
_GSTACK_OVERLAY=claude
_GSTACK_OVERLAY_SOURCE=default
_GSTACK_OVERLAY_ID=claude
_GSTACK_OVERLAY_GENERIC=0
_GSTACK_OVERLAY_NOTE=""
"$CONFIG_BIN" has claude_overlay_model >/dev/null 2>&1 || return 0
_GSTACK_OVERLAY_SOURCE=explicit
_GSTACK_OVERLAY_ID="$("$CONFIG_BIN" get claude_overlay_model 2>/dev/null || true)"
out="$(cd "$src" && _gstack_render_bun run scripts/models.ts claude-overlay "$_GSTACK_OVERLAY_ID" 2>&1)" || rc=$?
if [ "$rc" -eq 0 ]; then
_GSTACK_OVERLAY="$(printf '%s' "$out" | tail -1 | cut -f1)"
_GSTACK_OVERLAY_GENERIC="$(printf '%s' "$out" | tail -1 | cut -f2)"
return 0
fi
_GSTACK_OVERLAY=claude
_GSTACK_OVERLAY_NOTE="claude_overlay_model '$_GSTACK_OVERLAY_ID' no longer resolves to a Claude overlay ($(printf '%s\n' "$out" | sed -n 2p)); using the generic claude overlay. Fix: ./setup --claude-model <claude-model-id>, or ./setup --claude-model claude."
_gstack_render_say " note: $_GSTACK_OVERLAY_NOTE"
}
# gstack_claude_loadout_arg CONFIG_BIN — set _LOADOUT_ARG to --loadout=<name>
# for a saved non-full claude_loadout, else empty. A malformed value is
# ignored with a note, so the full skills are served.
gstack_claude_loadout_arg() {
local CONFIG_BIN="$1" value
_LOADOUT_ARG=""
value="$("$CONFIG_BIN" get claude_loadout 2>/dev/null || true)"
case "$value" in
''|full) return 0 ;;
esac
if printf '%s' "$value" | grep -Eq '^[a-z0-9-]{1,40}$'; then
_LOADOUT_ARG="--loadout=$value"
else
_gstack_render_say " note: claude_loadout is not a loadout name; serving the full skills. Fix: ./setup --loadout full"
fi
}
# gstack_claude_overlay_field RECORD KEY — one field of an activation record.
gstack_claude_overlay_field() {
[ -f "$1" ] || return 0
sed -n "s/^$2=//p" "$1" 2>/dev/null | head -1
}
# gstack_render_lock DIR / gstack_render_unlock DIR — serialize the swap and
# record write of concurrent renders (mkdir lock, no flock). A lock whose
# holder is gone is broken; after 30 s the swap proceeds unlocked.
gstack_render_lock() {
local lock="$1.lock" i=0 pid
mkdir -p "$(dirname "$1")" 2>/dev/null || true
while ! mkdir "$lock" 2>/dev/null; do
pid="$(cat "$lock/pid" 2>/dev/null || true)"
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
rm -rf "$lock"
continue
fi
i=$((i + 1))
[ "$i" -lt 300 ] || return 0
sleep 0.1
done
echo "$$" > "$lock/pid"
}
gstack_render_unlock() {
rm -rf "$1.lock"
}
# gstack_claude_render_action RENDER_DIR GBRAIN — what the default install's
# render dir needs: `user` (brain-aware :user render), `plain` (pinned
# overlay or plan-mode preamble off, no brain blocks), `remove` (serve the
# committed files) or `keep`.
# GBRAIN is ok, absent or unknown (detector missing or failed: no brain
# blocks, and a render is left alone unless an overlay or plan-mode change
# needs it).
gstack_claude_render_action() {
local render_dir="$1" gbrain="$2" previous
if [ "$gbrain" = ok ]; then _GSTACK_RENDER_ACTION=user; return 0; fi
if [ "$_GSTACK_OVERLAY" != claude ] || [ -n "${_PLAN_MODE_ARG:-}" ] || [ -n "${_LOADOUT_ARG:-}" ]; then _GSTACK_RENDER_ACTION=plain; return 0; fi
previous="$(gstack_claude_overlay_field "$render_dir.overlay" overlay)"
if [ "$gbrain" = unknown ] && [ "${previous:-claude}" = claude ] \
&& [ -z "$(gstack_claude_overlay_field "$render_dir.overlay" plan_mode)" ] \
&& [ -z "$(gstack_claude_overlay_field "$render_dir.overlay" loadout)" ]; then
_GSTACK_RENDER_ACTION=keep
else
_GSTACK_RENDER_ACTION=remove
fi
}
# gstack_claude_render_plain SOURCE_DIR RENDER_DIR — render the pinned overlay
# without brain blocks into a temp dir and swap it in only on success.
gstack_claude_render_plain() {
local src="$1" RENDER_DIR="$2" RENDER_TMP out rc=0
RENDER_TMP="$RENDER_DIR.tmp.$$"
rm -rf "$RENDER_TMP"
_gstack_render_say "Rendering Claude skills with the $_GSTACK_OVERLAY overlay${_PLAN_MODE_ARG:+ and no plan-mode preamble}${_LOADOUT_ARG:+ and the ${_LOADOUT_ARG#--loadout=} loadout} into $RENDER_DIR (source checkout stays clean)..."
out="$(cd "$src" && _gstack_render_bun run gen:skill-docs --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} ${_LOADOUT_ARG:+"$_LOADOUT_ARG"} 2>&1)" || rc=$?
if [ "$rc" -ne 0 ]; then
rm -rf "$RENDER_TMP"
printf '%s\n' "$out" | tail -3
return "$rc"
fi
gstack_render_lock "$RENDER_DIR"
_swap_in_render "$RENDER_DIR" "$RENDER_TMP"
gstack_render_unlock "$RENDER_DIR"
}
# gstack_claude_render_settle SOURCE_DIR RENDER_DIR GBRAIN OUTCOME [DEFAULT]
# Record what a render step left in place and report it. OUTCOME is rendered,
# removed or failed. DEFAULT is 1 (default install: a failed pinned render
# from an older gstack version falls back to the committed files) or 0 (a
# per-install render keeps its previous render). Prints the overlay banner
# only when the served overlay differs from the previous record's.
gstack_claude_render_settle() {
local src="$1" render_dir="$2" gbrain="$3" outcome="$4" default="${5:-1}"
local record="$2.overlay" version previous previous_version served render_kind
version="$(cat "$src/VERSION" 2>/dev/null | tr -d '[:space:]' || true)"
previous="$(gstack_claude_overlay_field "$record" overlay)"
previous_version="$(gstack_claude_overlay_field "$record" version)"
case "$outcome" in
rendered) served="$_GSTACK_OVERLAY"; render_kind=user ;;
removed) served=claude; render_kind=committed ;;
failed)
if [ "$default" = 1 ] && [ "$_GSTACK_OVERLAY" != claude ] && [ -d "$render_dir" ] \
&& [ "$previous_version" != "$version" ]; then
rm -rf "$render_dir"
_gstack_render_say " The $_GSTACK_OVERLAY overlay render failed and the previous render came from gstack ${previous_version:-unknown}, so the committed claude render is served. Rerun ./setup to retry."
served=claude; render_kind=committed
elif [ -d "$render_dir" ]; then
[ "$_GSTACK_OVERLAY" = claude ] \
|| _gstack_render_say " The $_GSTACK_OVERLAY overlay render failed; the previous render (${previous:-claude} overlay) stays in use. Rerun ./setup to retry."
return 0
else
_gstack_render_say " The $_GSTACK_OVERLAY overlay render failed; the committed claude render is served. Rerun ./setup to retry."
served=claude; render_kind=committed
fi
;;
*) return 0 ;;
esac
gstack_render_lock "$render_dir"
if [ "$render_kind" = committed ] && [ "$_GSTACK_OVERLAY_SOURCE" = default ]; then
rm -f "$record"
else
{
printf 'overlay=%s\nsource=%s\nmodel=%s\nversion=%s\ngbrain=%s\nrender=%s\n' \
"$served" "$_GSTACK_OVERLAY_SOURCE" "$_GSTACK_OVERLAY_ID" "${version:-unknown}" "$gbrain" "$render_kind"
[ "$render_kind" != user ] || [ -z "${_PLAN_MODE_ARG:-}" ] || printf 'plan_mode=off\n'
[ "$render_kind" != user ] || [ -z "${_LOADOUT_ARG:-}" ] || printf 'loadout=%s\n' "${_LOADOUT_ARG#--loadout=}"
} > "$record.tmp.$$" && mv "$record.tmp.$$" "$record"
fi
gstack_render_unlock "$render_dir"
if [ "$served" != "${previous:-claude}" ]; then
_gstack_render_say ""
_gstack_render_say "Claude skill overlay: $served (was ${previous:-claude})."
if [ "$_GSTACK_OVERLAY_SOURCE" = explicit ]; then
_gstack_render_say " Source: claude_overlay_model = $_GSTACK_OVERLAY_ID, set by ./setup --claude-model."
[ "$_GSTACK_OVERLAY_GENERIC" = 1 ] && _gstack_render_say " $_GSTACK_OVERLAY_ID has no overlay of its own, so it uses the generic claude overlay."
else
_gstack_render_say " Source: default (no claude_overlay_model set)."
fi
_gstack_render_say " Return to the generic overlay: ./setup --claude-model claude"
fi
}
# gstack_claude_overlay_status — `./setup --status` overlay lines. Reads only
# activation records and the registry: no bun, no writes.
gstack_claude_overlay_status() {
local default_render rows dest render record overlay source model version kind
default_render="${GSTACK_USER_RENDER_DIR:-$GSTACK_STATE_ROOT/render/claude}"
rows="$(gstack_install_registry_rows 2>/dev/null | awk -F '\t' '$1 == "claude" { print $4 "\t" $9 }')"
[ -n "$rows" ] || return 0
echo "Claude skill overlay:"
while IFS=' ' read -r dest render; do
[ -n "$dest" ] || continue
case "$render" in
''|-|committed) record="$default_render.overlay"; render="$default_render"; kind="user render" ;;
*) record="$render.overlay"; kind="per-install render" ;;
esac
overlay="$(gstack_claude_overlay_field "$record" overlay)"
[ -d "$render" ] || kind="committed render"
if [ -z "$overlay" ]; then
echo " $(_gstack_tilde "$dest"): claude (default, $kind)"
continue
fi
source="$(gstack_claude_overlay_field "$record" source)"
model="$(gstack_claude_overlay_field "$record" model)"
version="$(gstack_claude_overlay_field "$record" version)"
[ "$(gstack_claude_overlay_field "$record" render)" = committed ] && kind="committed render"
if [ "$source" = explicit ]; then source="explicit: $model"; fi
echo " $(_gstack_tilde "$dest"): $overlay ($source, $kind, gstack $version)"
done <<EOF
$rows
EOF
}