feat(config): plan_mode_preamble key wires --plan-mode=off into setup and every installed render

gstack-config plan_mode_preamble (on|off, default on) makes ./setup pass
--plan-mode=off to every gen:skill-docs render, gbrain-refresh and the
gstack-relink router render. The default Claude install, which otherwise
serves the committed SKILL.md files, gets a plain render with the key off;
the activation record notes plan_mode=off so switching back on removes the
render, including on the gbrain=unknown keep branch. With the key on or
unset every path and record is byte-for-byte unchanged.

Fixes #3168. Finishes #3213 by @xingzihai.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-19 19:07:43 +00:00
1 parent 14506c8e4a
commit cff65f7229
10 files changed
+433 -33

No files matched your search

+13
View File
@@ -1,5 +1,18 @@
# Changelog
## [Unreleased]
**`gstack-config set plan_mode_preamble off` leaves the plan-mode preamble out of every installed skill, for people who never use plan mode.** The "Plan Mode Safe Operations" and "Skill Invocation During Plan Mode" sections add about 1.6 KB to each of 50 Claude skills. With the key off, the next `./setup` renders every host's skills with `gen-skill-docs --plan-mode=off`, and the default Claude install serves that render instead of the committed files. The default is on, and with it on every render and the committed SKILL.md files are byte-for-byte unchanged.
### Added
- **`gen-skill-docs --plan-mode=off`** drops the two sections at generation time. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" keep their marker. Hosts whose capabilities declare no plan mode already render the sections empty, so in practice the flag matters for Claude, Copilot and OpenCode. Fixes #3168. From #3213 by @xingzihai, finished with the config wiring.
- **The `plan_mode_preamble` config key** (`on` | `off`, default `on`). It is a closed domain like `explain_level`: an unknown value warns and stores `on`. Setting it outside `./setup` says it applies on the next setup run and prints the `./setup --refresh-registered` command that applies it now.
- **Every installed render carries the choice:** all 16 `gen:skill-docs` call sites in `setup`, the Claude render helper, `gstack-config gbrain-refresh`, and the router that `gstack-relink` re-renders in place (as it already does for `disabled_skills`).
- **The default Claude install gets a render when the key is off.** `gstack_claude_render_action` returns `plain` whatever gbrain detection says, and the activation record (`~/.gstack/render/claude.overlay`) gains `plan_mode=off`. Turning the key back on removes that render on the next setup, including when the gbrain detector fails, which otherwise keeps a render. Records written with the key on are unchanged.
### Changed
- **Gate integrity:** new tests `test/plan-mode-off-build.test.ts` (from #3213, unchanged), `test/plan-mode-off-generation.test.ts`, `test/plan-mode-preamble-config.test.ts`, `test/plan-mode-preamble-render.test.ts` and `test/plan-mode-preamble-setup.test.ts`. No existing test was edited, and no threshold, trial count, case kind or judge changed.
## [1.91.149.0] - 2026-10-19
**Paid eval cases can now run on the Codex host, and the Monday census runs two arms: Claude Opus 5.5 and Codex gpt-6.1-sol.** `EVALS_HOST=codex` runs the 52 adapter-only gate cases through a full Codex runtime per case. On its first pilot gpt-6.1-sol passed 48 of 52 with no retries, at $4.63 for the 52 cases, against $28.93 for the same cases on Fable (0.16×). Claude-only cases are skipped on Codex with a recorded reason and never count as failed. GSTA-49 TM2.
+2
View File
@@ -796,6 +796,8 @@ types into that element; bare `browse type <text>` types into whatever has focus
**Don't use some skills?** `gstack-config set disabled_skills make-pdf,pair-agent` stops registering them on every host, so their descriptions stop loading into each session. Claude Code updates right away; other hosts on their next `./setup --host <name>` or `/gstack-upgrade`. Their files stay installed, so a skill that calls a disabled one still works (you get a warning). Typos are rejected with the closest skill name, `gstack-upgrade` can't be disabled, and `gstack-config set disabled_skills ""` re-enables everything.
**Never use plan mode?** `gstack-config set plan_mode_preamble off` leaves the "Plan Mode Safe Operations" and "Skill Invocation During Plan Mode" sections (about 1.6 KB per skill) out of every installed skill on the next `./setup` run; the command prints the exact `./setup --refresh-registered` line that applies it. Claude Code then serves a render under `~/.gstack/render/claude` instead of the committed files. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" keep their marker. `gstack-config set plan_mode_preamble on` (the default) and another `./setup` bring the sections back. It matters for Claude Code, Copilot and OpenCode: hosts without plan mode (Codex and others) never carry these sections.
**Want namespaced commands?** `cd ~/.claude/skills/gstack && ./setup --prefix` — switches from `/qa` to `/gstack-qa`. Useful if you run other skill packs alongside gstack.
**Codex says "Skipped loading skill(s) due to invalid SKILL.md"?** Your Codex skill descriptions are stale. `${CODEX_HOME:-~/.codex}/skills/gstack` is a runtime directory, not the checkout: `./setup --status` shows the Codex row's source checkout. Fix: `cd <that source> && git pull && ./setup --host codex` — for a repo-local install, run it from inside the project.
+23 -4
View File
@@ -94,6 +94,13 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
# # (for power users who know the terms)
# # Unknown values default to "default" with a warning.
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
# plan_mode_preamble: on # on = every skill carries the plan-mode preamble
# # off = rendered skills omit the "Plan Mode Safe
# # Operations" and "Skill Invocation During
# # Plan Mode" sections (~1.8 KB each), for
# # users who never enter plan mode
# # Applies on the next ./setup run. Unknown values
# # default to "on" with a warning.
#
# ─── Artifacts sync (renamed from gbrain_sync_mode in v1.27.0.0) ─────
# artifacts_sync_mode: off # off | artifacts-only | full
@@ -279,6 +286,7 @@ lookup_default() {
update_check) echo "true" ;;
skill_prefix) echo "false" ;;
explain_level) echo "default" ;;
plan_mode_preamble) echo "on" ;; # on | off — off drops the plan-mode preamble on the next ./setup (#3168)
codex_reviews) echo "enabled" ;;
design_detector) echo "auto" ;; # auto | off — impeccable engine pre-pass in the design skills
design_detector_install_prompted) echo "false" ;; # true once the user answered the one-time engine install offer with "never ask again"
@@ -734,6 +742,10 @@ $_gstack_legacy_root"
echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
VALUE="default"
fi
if [ "$KEY" = "plan_mode_preamble" ] && [ "$VALUE" != "on" ] && [ "$VALUE" != "off" ]; then
echo "Warning: plan_mode_preamble '$VALUE' not recognized. Valid values: on, off. Using on." >&2
VALUE="on"
fi
if [ "$KEY" = "artifacts_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
echo "Warning: artifacts_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
VALUE="off"
@@ -985,6 +997,11 @@ $_gstack_legacy_root"
gstack_install_registry_rows | awk -F '\t' '$1 != "claude" { print " apply to " $1 " (" $4 "): cd " $6 " && ./setup --host " $1 }' )
fi
fi
# Installed skills are rendered by ./setup, so the preamble choice lands
# on its next run (every registered host).
if [ "$KEY" = "plan_mode_preamble" ] && [ -z "${GSTACK_SETUP_RUNNING:-}" ]; then
printf 'gstack-config: plan_mode_preamble=%s applies on the next ./setup run. Apply now: cd %q && ./setup --refresh-registered\n' "$VALUE" "$(cd "$(dirname "$0")/.." && pwd)"
fi
;;
unset)
KEY="${2:?Usage: gstack-config unset <key>}"
@@ -1009,7 +1026,7 @@ $_gstack_legacy_root"
echo "# ─── Active values (including defaults for unset keys) ───"
report_root_disagreement
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix explain_level \
skill_prefix explain_level plan_mode_preamble \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted artifacts_sync_removals plan_tune_hooks \
timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \
@@ -1034,7 +1051,7 @@ $_gstack_legacy_root"
defaults)
echo "# gstack-config defaults"
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix explain_level \
skill_prefix explain_level plan_mode_preamble \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted artifacts_sync_removals plan_tune_hooks \
timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \
@@ -1065,6 +1082,8 @@ $_gstack_legacy_root"
# generated SKILL.md files match your installation state.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/gstack-render-claude.sh" 2>/dev/null || { echo "$0: $SCRIPT_DIR/gstack-render-claude.sh is missing. fix: reinstall with ./setup or /gstack-upgrade" >&2; exit 1; }
_PLAN_MODE_ARG=""
[ "$("$0" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"
DETECT_BIN="$SCRIPT_DIR/gstack-gbrain-detect"
DETECTION_FILE="$STATE_DIR/gbrain-detection.json"
mkdir -p "$STATE_DIR"
@@ -1133,7 +1152,7 @@ $_gstack_legacy_root"
gstack_claude_overlay "$INSTALL_DIR" "$0"
RENDER_TMP="$RENDER_DIR.tmp.$$"
rm -rf "$RENDER_TMP"
if ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" >/dev/null 2>&1 ); then
if ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1 ); then
gstack_render_lock "$RENDER_DIR"
_swap_in_render "$RENDER_DIR" "$RENDER_TMP"
gstack_render_unlock "$RENDER_DIR"
@@ -1159,7 +1178,7 @@ $_gstack_legacy_root"
# brain blocks, so removing gbrain keeps the chosen overlay.
INSTALL_DIR="$HOME/.claude/skills/gstack"
RENDER_DIR="${GSTACK_USER_RENDER_DIR:-$STATE_DIR/render/claude}"
if { [ -d "$RENDER_DIR" ] || "$0" has claude_overlay_model; } \
if { [ -d "$RENDER_DIR" ] || "$0" has claude_overlay_model || [ -n "$_PLAN_MODE_ARG" ]; } \
&& [ -d "$INSTALL_DIR" ] && [ ! -L "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/VERSION" ] && [ -f "$INSTALL_DIR/package.json" ]; then
if ! command -v bun >/dev/null 2>&1; then
echo "Skip: bun not on PATH — the Claude render at $RENDER_DIR was not refreshed. Install bun, then re-run 'gstack-config gbrain-refresh'."
+9 -3
View File
@@ -461,19 +461,25 @@ done
# (in place, like gstack-patch-names). Clearing the list restores the committed
# bytes. Only a generated router is rewritten, and only from a checkout that
# carries the generator; a failed render keeps the current router.
# plan_mode_preamble=off (#3168) renders it without the plan-mode preamble the
# same way, and turning it back on restores the committed bytes.
_ROUTER="$INSTALL_DIR/SKILL.md"
_GEN_SRC="$(cd "$SCRIPT_DIR/.." && pwd)"
if { [ "$DISABLED" != " " ] || grep -q '^Disabled on this install' "$_ROUTER" 2>/dev/null; } \
_PLAN_MODE_ARG=""
[ "$("$GSTACK_CONFIG" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"
if { [ "$DISABLED" != " " ] || grep -q '^Disabled on this install' "$_ROUTER" 2>/dev/null \
|| [ -n "$_PLAN_MODE_ARG" ] \
|| { grep -q 'PLAN MODE EXCEPTION' "$_ROUTER" 2>/dev/null && ! grep -q '^## Plan Mode Safe Operations' "$_ROUTER" 2>/dev/null; }; } \
&& grep -q '<!-- AUTO-GENERATED from' "$_ROUTER" 2>/dev/null && [ -f "$_GEN_SRC/scripts/gen-skill-docs.ts" ]; then
_ROUTER_TMP="$(mktemp -d "${TMPDIR:-/tmp}/gstack-router.XXXXXX")" || _ROUTER_TMP=""
_DISABLED_CSV="$(printf '%s' "$DISABLED" | xargs | tr ' ' ',')"
if [ -n "$_ROUTER_TMP" ] && command -v bun >/dev/null 2>&1 \
&& (cd "$_GEN_SRC" && bun run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} >/dev/null 2>&1) \
&& (cd "$_GEN_SRC" && bun run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1) \
&& [ -s "$_ROUTER_TMP/SKILL.md" ] && cp "$_ROUTER_TMP/SKILL.md" "$_ROUTER.tmp.$$" && mv "$_ROUTER.tmp.$$" "$_ROUTER"; then
[ -n "$_DISABLED_CSV" ] && echo "Router: disabled skills left out of $_ROUTER"
else
rm -f "$_ROUTER.tmp.$$"
echo "gstack-relink: could not re-render the router for disabled_skills (bun missing or generation failed); $_ROUTER still lists every skill. Fix: install Bun, then run $0" >&2
echo "gstack-relink: could not re-render the router for disabled_skills or plan_mode_preamble (bun missing or generation failed); $_ROUTER is unchanged. Fix: install Bun, then run $0" >&2
fi
[ -z "$_ROUTER_TMP" ] || rm -rf "$_ROUTER_TMP"
fi
+20 -10
View File
@@ -9,10 +9,15 @@
# 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), so
# (<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 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.
@@ -81,15 +86,18 @@ gstack_render_unlock() {
# gstack_claude_render_action RENDER_DIR GBRAIN — what the default install's
# render dir needs: `user` (brain-aware :user render), `plain` (pinned
# overlay, no brain blocks), `remove` (serve the committed files) or `keep`.
# 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 change needs it).
# 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 ]; then _GSTACK_RENDER_ACTION=plain; return 0; fi
if [ "$_GSTACK_OVERLAY" != claude ] || [ -n "${_PLAN_MODE_ARG:-}" ]; then _GSTACK_RENDER_ACTION=plain; return 0; fi
previous="$(gstack_claude_overlay_field "$render_dir.overlay" overlay)"
if [ "$gbrain" = unknown ] && [ "${previous:-claude}" = claude ]; then
if [ "$gbrain" = unknown ] && [ "${previous:-claude}" = claude ] \
&& [ -z "$(gstack_claude_overlay_field "$render_dir.overlay" plan_mode)" ]; then
_GSTACK_RENDER_ACTION=keep
else
_GSTACK_RENDER_ACTION=remove
@@ -102,8 +110,8 @@ 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 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"} 2>&1)" || rc=$?
_gstack_render_say "Rendering Claude skills with the $_GSTACK_OVERLAY overlay${_PLAN_MODE_ARG:+ and no plan-mode preamble} 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"} 2>&1)" || rc=$?
if [ "$rc" -ne 0 ]; then
rm -rf "$RENDER_TMP"
printf '%s\n' "$out" | tail -3
@@ -150,9 +158,11 @@ gstack_claude_render_settle() {
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" \
> "$record.tmp.$$" && mv "$record.tmp.$$" "$record"
{
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'
} > "$record.tmp.$$" && mv "$record.tmp.$$" "$record"
fi
gstack_render_unlock "$render_dir"
if [ "$served" != "${previous:-claude}" ]; then
+21 -16
View File
@@ -1168,14 +1168,14 @@ _render_install() {
rm -rf "$tmp"
rc=0
if [ "$host" = claude ]; then
out="$(cd "$SOURCE_GSTACK_DIR" && bun_cmd run gen:skill-docs:user --host claude --out-dir "$tmp" --link-root "$render" --install-root "$named" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} 2>&1)" || rc=$?
out="$(cd "$SOURCE_GSTACK_DIR" && bun_cmd run gen:skill-docs:user --host claude --out-dir "$tmp" --link-root "$render" --install-root "$named" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} 2>&1)" || rc=$?
want="$(find "$SOURCE_GSTACK_DIR" -mindepth 2 -maxdepth 2 -name SKILL.md | wc -l)"
got="$(find "$tmp" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l)"
[ "$rc" -ne 0 ] || { [ "$want" -eq "$got" ] && grep -qF "$named/bin/gstack-skill-start --skill" "$tmp/SKILL.md" 2>/dev/null; } \
|| { rc=1; out="render incomplete: $got of $want skills, or a start line that does not name $named"; }
[ "$rc" -ne 0 ] || "$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$tmp" "$SKILL_PREFIX" >/dev/null || rc=$?
else
out="$(cd "$SOURCE_GSTACK_DIR" && bun_cmd run gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL" --install-root "$named" --out-dir "$tmp" --link-root "$render" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} 2>&1)" || rc=$?
out="$(cd "$SOURCE_GSTACK_DIR" && bun_cmd run gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL" --install-root "$named" --out-dir "$tmp" --link-root "$render" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} 2>&1)" || rc=$?
want="$(find "$SOURCE_GSTACK_DIR/.agents/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l)"
got="$(find "$tmp/.agents/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l)"
[ "$rc" -ne 0 ] || { [ "$got" -gt 0 ] && { [ "$want" -eq 0 ] || [ "$want" -eq "$got" ]; } && grep -qF "GSTACK_ROOT=\"$named\"" "$tmp/.agents/skills/gstack-review/SKILL.md" 2>/dev/null; } \
@@ -1374,6 +1374,11 @@ _DISABLED_SKILLS="$(gstack_disabled_normalize "$("$GSTACK_CONFIG" get disabled_s
# per-install renders) omits disabled skills (C8); the default Claude router is
# rendered in place by gstack-relink.
_DISABLED_CSV="$(printf '%s' "$_DISABLED_SKILLS" | xargs | tr ' ' ',')"
# plan_mode_preamble=off (#3168): every render below omits the plan-mode
# preamble; the default Claude install then serves a render instead of the
# committed files (bin/gstack-render-claude.sh).
_PLAN_MODE_ARG=""
[ "$("$GSTACK_CONFIG" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"
# _remove_disabled_host_entry ENTRY — drop a non-Claude host's gstack-owned
# entry for a disabled skill (a link, or a generated copy); anything else stays.
_remove_disabled_host_entry() {
@@ -2075,7 +2080,7 @@ if [ "$NEEDS_AGENTS_GEN" -eq 1 ]; then
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
bun_cmd run gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}
)
_prune_stale_generated "$SOURCE_GSTACK_DIR" "$AGENTS_DIR" "$CODEX_SKILLS"
fi
@@ -2086,7 +2091,7 @@ if [ "$INSTALL_FACTORY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host factory ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
bun_cmd run gen:skill-docs --host factory ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}
)
fi
@@ -2096,7 +2101,7 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host opencode ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
bun_cmd run gen:skill-docs --host opencode ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}
)
fi
@@ -2106,7 +2111,7 @@ if [ "$INSTALL_CURSOR" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host cursor ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
bun_cmd run gen:skill-docs --host cursor ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}
)
fi
@@ -2116,7 +2121,7 @@ if [ "$INSTALL_COPILOT" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host copilot ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
bun_cmd run gen:skill-docs --host copilot ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}
)
fi
@@ -2126,7 +2131,7 @@ if [ "$INSTALL_AGY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host agy ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
bun_cmd run gen:skill-docs --host agy ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}
)
fi
@@ -2737,7 +2742,7 @@ link_codex_skill_dirs() {
if [ ! -d "$agents_dir" ]; then
echo " Generating .agents/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host codex --model "$CODEX_GENERATION_MODEL" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
fi
if [ ! -d "$agents_dir" ]; then
@@ -3026,7 +3031,7 @@ link_factory_skill_dirs() {
if [ ! -d "$factory_dir" ]; then
echo " Generating .factory/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host factory ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host factory ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
fi
if [ ! -d "$factory_dir" ]; then
@@ -3069,7 +3074,7 @@ link_opencode_skill_dirs() {
if [ ! -d "$opencode_dir" ]; then
echo " Generating .opencode/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host opencode ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host opencode ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
fi
if [ ! -d "$opencode_dir" ]; then
@@ -3217,7 +3222,7 @@ link_cursor_skill_dirs() {
if [ ! -d "$cursor_dir" ]; then
echo " Generating .cursor/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host cursor ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host cursor ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
fi
if [ ! -d "$cursor_dir" ]; then
@@ -3327,7 +3332,7 @@ link_copilot_skill_dirs() {
if [ ! -d "$copilot_dir" ]; then
echo " Generating .copilot/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host copilot ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host copilot ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
fi
if [ ! -d "$copilot_dir" ]; then
echo " warning: .copilot/skills/ generation failed — run 'bun run gen:skill-docs --host copilot' manually" >&2
@@ -3401,7 +3406,7 @@ link_agy_skill_dirs() {
if [ ! -d "$agy_dir" ]; then
echo " Generating .agy/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host agy ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host agy ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
fi
if [ ! -d "$agy_dir" ]; then
echo " warning: .agy/skills/ generation failed — run 'bun run gen:skill-docs --host agy' manually" >&2
@@ -3644,7 +3649,7 @@ if [ "$INSTALL_KIRO" -eq 1 ]; then
KIRO_GSTACK="$KIRO_SKILLS/gstack"
# Host identity controls outside-review routing as well as skill availability.
# Never borrow .agents or temporarily replace a live Codex model profile.
( cd "$SOURCE_GSTACK_DIR" && bun_cmd run gen:skill-docs --host kiro ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
( cd "$SOURCE_GSTACK_DIR" && bun_cmd run gen:skill-docs --host kiro ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} )
mkdir -p "$KIRO_SKILLS"
if _sidecar_root_user_owned "$KIRO_GSTACK"; then
echo " left in place (existing Kiro runtime root is not gstack-managed): $KIRO_GSTACK" >&2
@@ -4179,7 +4184,7 @@ if [ -x "$DETECT_BIN" ]; then
# No pipe before the || guard: `cmd | tail -3` reports TAIL's exit
# status, so a generator crash read as success (same masking the
# main gen:skill-docs site had). Capture, show the tail, propagate.
_GEN_USER_OUT=$(bun_cmd run gen:skill-docs:user --host claude --out-dir "$_GSTACK_RENDER_TMP" --link-root "$_GSTACK_RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} 2>&1)
_GEN_USER_OUT=$(bun_cmd run gen:skill-docs:user --host claude --out-dir "$_GSTACK_RENDER_TMP" --link-root "$_GSTACK_RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} 2>&1)
_GEN_USER_RC=$?
printf '%s\n' "$_GEN_USER_OUT" | tail -3
exit "$_GEN_USER_RC"
+83
View File
@@ -0,0 +1,83 @@
/**
* --plan-mode=off end to end through the generator, Claude host (#3168).
*
* Default output must stay byte-identical to the committed SKILL.md files.
* With planMode false, every skill that carries the preamble's
* "## Plan Mode Safe Operations" and "## Skill Invocation During Plan Mode"
* sections loses exactly those two sections and nothing else; skills
* without them are unchanged.
*/
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runGeneration } from '../scripts/gen-skill-docs';
const ROOT = path.resolve(import.meta.dir, '..');
const SAFE = '## Plan Mode Safe Operations\n';
const INVOCATION = '## Skill Invocation During Plan Mode\n\n';
let tmp: string;
let skills: string[];
beforeAll(async () => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-plan-mode-gen-'));
const on = await runGeneration({ host: 'claude', outputRoot: path.join(tmp, 'on'), contentLinkRoot: null });
const off = await runGeneration({ host: 'claude', outputRoot: path.join(tmp, 'off'), contentLinkRoot: null, planMode: false });
expect(on.exitCode).toBe(0);
expect(off.exitCode).toBe(0);
skills = on.artifacts.filter(a => a.kind === 'skill').map(a => a.relativePath);
});
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
const read = (root: string, rel: string) => fs.readFileSync(path.join(root, rel), 'utf8');
/** The committed text with the two plan-mode sections (each heading plus one paragraph) cut out. */
function withoutPlanMode(text: string): string {
const start = text.indexOf(SAFE);
const invocation = text.indexOf(INVOCATION, start);
expect(invocation).toBeGreaterThan(start);
const end = text.indexOf('\n\n', invocation + INVOCATION.length) + 2;
return text.slice(0, start) + text.slice(end);
}
describe('gen-skill-docs --plan-mode=off (claude host)', () => {
test('default output is byte-identical to every committed SKILL.md', () => {
expect(skills.length).toBeGreaterThan(40);
for (const rel of skills) expect(read(path.join(tmp, 'on'), rel), rel).toBe(read(ROOT, rel));
});
test('off drops exactly the two plan-mode sections and leaves other skills untouched', () => {
let stripped = 0;
for (const rel of skills) {
const committed = read(ROOT, rel);
const off = read(path.join(tmp, 'off'), rel);
expect(off, rel).not.toContain(SAFE);
expect(off, rel).not.toContain('## Skill Invocation During Plan Mode');
if (!committed.includes(SAFE)) {
expect(off, rel).toBe(committed);
continue;
}
expect(off, rel).toBe(withoutPlanMode(committed));
stripped++;
}
expect(stripped).toBeGreaterThan(40);
expect(read(path.join(tmp, 'off'), 'SKILL.md')).toContain('PLAN MODE EXCEPTION');
});
test('the CLI flag parses: --plan-mode=off renders without the sections, an unknown value is refused', () => {
const cli = (...args: string[]) => spawnSync('bun', ['run', 'scripts/gen-skill-docs.ts', '--host', 'claude', ...args], {
cwd: ROOT, encoding: 'utf8', timeout: 120_000,
});
const out = path.join(tmp, 'cli');
const ok = cli('--out-dir', out, '--plan-mode=off');
expect(ok.status, ok.stderr).toBe(0);
expect(read(out, 'SKILL.md')).toBe(read(path.join(tmp, 'off'), 'SKILL.md'));
expect(read(out, 'review/SKILL.md')).not.toContain(SAFE);
const bad = cli('--out-dir', path.join(tmp, 'bad'), '--plan-mode=maybe');
expect(bad.status).not.toBe(0);
expect(bad.stderr).toContain("Unknown plan mode: maybe. Use 'on' or 'off'.");
});
});
+74
View File
@@ -0,0 +1,74 @@
/**
* gstack-config plan_mode_preamble (#3168): on | off, default on.
*
* off makes ./setup render every installed skill without the plan-mode
* preamble (--plan-mode=off). The key is a closed domain like explain_level:
* an unknown value warns and stores on. Setting it outside ./setup says the
* change lands on the next setup run and prints the command that applies it.
*/
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runBin } from './helpers/run-bin';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN_CONFIG = path.join(ROOT, 'bin', 'gstack-config');
let tmpHome: string;
beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-plan-mode-cfg-'));
});
afterEach(() => {
fs.rmSync(tmpHome, { recursive: true, force: true });
});
function run(args: string[], env: Record<string, string | undefined> = { GSTACK_SETUP_RUNNING: undefined }) {
return runBin(BIN_CONFIG, args, { gstackHome: tmpHome, cwd: ROOT, trim: true, env });
}
describe('gstack-config plan_mode_preamble', () => {
test('defaults to on, in get, list and defaults', () => {
expect(run(['get', 'plan_mode_preamble']).stdout).toBe('on');
expect(run(['defaults']).stdout).toMatch(/plan_mode_preamble:\s+on$/m);
expect(run(['list']).stdout).toMatch(/plan_mode_preamble:\s+on \(default\)$/m);
});
test('accepts off and on', () => {
expect(run(['set', 'plan_mode_preamble', 'off']).status).toBe(0);
expect(run(['get', 'plan_mode_preamble']).stdout).toBe('off');
expect(run(['list']).stdout).toMatch(/plan_mode_preamble:\s+off \(set\)$/m);
expect(run(['set', 'plan_mode_preamble', 'on']).status).toBe(0);
expect(run(['get', 'plan_mode_preamble']).stdout).toBe('on');
});
test('an unknown value warns and stores on', () => {
run(['set', 'plan_mode_preamble', 'off']);
const junk = run(['set', 'plan_mode_preamble', 'false']);
expect(junk.status).toBe(0);
expect(junk.stderr).toContain("plan_mode_preamble 'false' not recognized. Valid values: on, off. Using on.");
expect(run(['get', 'plan_mode_preamble']).stdout).toBe('on');
});
test('outside setup, set says it applies on the next ./setup run and gives the command', () => {
const r = run(['set', 'plan_mode_preamble', 'off']);
expect(r.status).toBe(0);
expect(r.stdout).toBe(`gstack-config: plan_mode_preamble=off applies on the next ./setup run. Apply now: cd ${ROOT} && ./setup --refresh-registered`);
});
test('inside setup, set prints nothing', () => {
const r = run(['set', 'plan_mode_preamble', 'off'], { GSTACK_SETUP_RUNNING: '1' });
expect(r.status).toBe(0);
expect(r.stdout).toBe('');
});
test('the config header documents the key and its values', () => {
run(['set', 'plan_mode_preamble', 'on']);
const cfg = fs.readFileSync(path.join(tmpHome, 'config.yaml'), 'utf-8');
expect(cfg).toContain('# plan_mode_preamble: on');
expect(cfg).toContain('# off = rendered skills omit the "Plan Mode Safe');
});
});
+134
View File
@@ -0,0 +1,134 @@
/**
* plan_mode_preamble=off reaches every installed render (#3168).
*
* Installed gstack never runs the generator by hand, so the key only works if
* every render ./setup and the bin/ helpers run carries --plan-mode=off. The
* default Claude install serves the committed SKILL.md files (which carry the
* preamble) with no render at all, so with the key off it must get a render,
* and turning the key back on must drop that render even where a detector
* failure (gbrain=unknown) would otherwise keep it.
*/
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const ROOT = path.resolve(import.meta.dir, '..');
const PLAN_ARG = '${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}';
const GENERATOR = /(?:\bbun|\bbun_cmd|_gstack_render_bun) run (?:gen:skill-docs(?::user)?|scripts\/gen-skill-docs\.ts) --[a-z]/;
const CONFIG_READ = '[ "$("$GSTACK_CONFIG" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"';
/** Generator invocations in a file: not comments, and not echoed or logged instructions. */
function renders(rel: string): string[] {
return fs.readFileSync(path.join(ROOT, rel), 'utf8').split('\n')
.filter(line => GENERATOR.test(line) && !/^\s*(#|echo\b|log\b)/.test(line));
}
describe('every installed-skill render carries the plan-mode arg', () => {
test('setup: every gen:skill-docs call site, with the key read once', () => {
const lines = renders('setup');
expect(lines.length).toBeGreaterThanOrEqual(16);
for (const line of lines) expect(line, line).toContain(PLAN_ARG);
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
expect(setup.split(CONFIG_READ).length - 1).toBe(1);
expect(setup.indexOf(CONFIG_READ)).toBeLessThan(setup.indexOf('bun_cmd run gen:skill-docs --host factory'));
});
test('the Claude render helper, gbrain-refresh and the relink router render', () => {
for (const rel of ['bin/gstack-render-claude.sh', 'bin/gstack-config', 'bin/gstack-relink']) {
const lines = renders(rel);
expect(lines.length, rel).toBeGreaterThanOrEqual(1);
for (const line of lines) expect(line, `${rel}: ${line}`).toContain(PLAN_ARG);
}
expect(fs.readFileSync(path.join(ROOT, 'bin/gstack-relink'), 'utf8')).toContain(CONFIG_READ);
expect(fs.readFileSync(path.join(ROOT, 'bin/gstack-config'), 'utf8'))
.toContain('[ "$("$0" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"');
});
});
let tmp: string;
beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-plan-mode-render-')); });
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
/**
* Source the real helper with a stub generator (bun_cmd) that logs its
* arguments and writes one SKILL.md into --out-dir, then run SCRIPT.
* PLAN is the caller's _PLAN_MODE_ARG; OVERLAY a pinned overlay or claude.
*/
function helper(script: string, plan: string, overlay = 'claude') {
const src = path.join(tmp, 'src');
fs.mkdirSync(src, { recursive: true });
fs.writeFileSync(path.join(src, 'VERSION'), '0.1.2.3\n');
const prelude = `set -euo pipefail
. ${JSON.stringify(path.join(ROOT, 'bin/gstack-render-claude.sh'))}
_swap_in_render() { rm -rf "$1"; mv "$2" "$1"; }
bun_cmd() {
printf '%s\\n' "$*" >> "$TMPROOT/bun.log"
local out="" prev=""
for a in "$@"; do [ "$prev" = --out-dir ] && out="$a"; prev="$a"; done
mkdir -p "$out/autoplan" && echo rendered > "$out/autoplan/SKILL.md"
}
SRC="$TMPROOT/src"; R="$TMPROOT/render/claude"
_GSTACK_OVERLAY="$OVERLAY"; _GSTACK_OVERLAY_ID="$OVERLAY"; _GSTACK_OVERLAY_GENERIC=0
_GSTACK_OVERLAY_SOURCE=default; [ "$OVERLAY" = claude ] || _GSTACK_OVERLAY_SOURCE=explicit
_PLAN_MODE_ARG="$PLAN"
`;
const r = spawnSync('bash', ['-c', prelude + script], {
encoding: 'utf8',
timeout: 30_000,
env: { PATH: process.env.PATH ?? '/usr/bin:/bin', TMPROOT: tmp, PLAN: plan, OVERLAY: overlay },
});
expect(r.status, r.stderr).toBe(0);
return r.stdout;
}
const action = (gbrain: string, plan: string, overlay = 'claude') =>
helper(`gstack_claude_render_action "$R" ${gbrain}; echo "$_GSTACK_RENDER_ACTION"`, plan, overlay).trim();
const record = () => fs.readFileSync(path.join(tmp, 'render/claude.overlay'), 'utf8');
const bunLog = () => fs.readFileSync(path.join(tmp, 'bun.log'), 'utf8');
describe('gstack_claude_render_action / render_plain / render_settle', () => {
test('key on or unset: the default install keeps today\'s actions', () => {
expect(action('unknown', '')).toBe('keep');
expect(action('absent', '')).toBe('remove');
expect(action('ok', '')).toBe('user');
expect(action('absent', '', 'opus-4-7')).toBe('plain');
});
test('key off: the default install renders (plain) whatever detection says, brain renders stay user', () => {
expect(action('unknown', '--plan-mode=off')).toBe('plain');
expect(action('absent', '--plan-mode=off')).toBe('plain');
expect(action('ok', '--plan-mode=off')).toBe('user');
});
test('key off: the plain render passes --plan-mode=off and the record says so', () => {
const out = helper('gstack_claude_render_plain "$SRC" "$R" && gstack_claude_render_settle "$SRC" "$R" absent rendered', '--plan-mode=off');
expect(out).toContain('Rendering Claude skills with the claude overlay and no plan-mode preamble into');
expect(bunLog()).toMatch(/run gen:skill-docs --host claude --out-dir \S+ --link-root \S+ --model claude --plan-mode=off$/m);
expect(fs.existsSync(path.join(tmp, 'render/claude/autoplan/SKILL.md'))).toBe(true);
expect(record()).toBe('overlay=claude\nsource=default\nmodel=claude\nversion=0.1.2.3\ngbrain=absent\nrender=user\nplan_mode=off\n');
expect(out).not.toContain('Claude skill overlay:');
});
test('switching back on drops the stale render, including the gbrain=unknown keep branch', () => {
helper('gstack_claude_render_plain "$SRC" "$R" && gstack_claude_render_settle "$SRC" "$R" unknown rendered', '--plan-mode=off');
expect(record()).toContain('plan_mode=off');
expect(action('unknown', '')).toBe('remove');
expect(action('absent', '')).toBe('remove');
helper('rm -rf "$R"; gstack_claude_render_settle "$SRC" "$R" unknown removed', '');
expect(fs.existsSync(path.join(tmp, 'render/claude.overlay'))).toBe(false);
expect(action('unknown', '')).toBe('keep');
});
test('a brain render made with the key on is still kept on gbrain=unknown', () => {
helper('mkdir -p "$R" && gstack_claude_render_settle "$SRC" "$R" ok rendered', '');
expect(record()).not.toContain('plan_mode');
expect(action('unknown', '')).toBe('keep');
});
test('key on: a pinned render and its record are byte-for-byte unchanged', () => {
helper('gstack_claude_render_plain "$SRC" "$R" && gstack_claude_render_settle "$SRC" "$R" absent rendered', '', 'opus-4-7');
expect(bunLog()).toMatch(/run gen:skill-docs --host claude --out-dir \S+ --link-root \S+ --model opus-4-7$/m);
expect(record()).toBe('overlay=opus-4-7\nsource=explicit\nmodel=opus-4-7\nversion=0.1.2.3\ngbrain=absent\nrender=user\n');
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* plan_mode_preamble through the real ./setup on a default Claude install (#3168).
*
* The default install serves the committed SKILL.md files with no render, so
* with the key off setup must render the skills without the plan-mode
* preamble and serve that render (router included, via gstack-relink); with
* the key back on, the render goes away, including when the gbrain detector
* fails (the gbrain=unknown branch that otherwise keeps a render).
* Fixture: test/helpers/install-fixture.ts, overlay helpers:
* test/helpers/claude-overlay-fixture.ts.
*/
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { config, defaultRender, fakeDetect, record, served, setup } from './helpers/claude-overlay-fixture';
import { ROOT, cleanupFixtures, cleanupSeed, makeFixture, makeSource, put } from './helpers/install-fixture';
afterEach(cleanupFixtures);
afterAll(cleanupSeed);
const SAFE = '## Plan Mode Safe Operations';
describe.skipIf(process.platform === 'win32')('./setup with plan_mode_preamble', () => {
test('off renders the default install without the preamble; on (detector failing) serves the committed files again', () => {
const f = makeFixture();
const src = makeSource(f, join(f.home, '.claude/skills/gstack'));
fakeDetect(src);
const skills = join(f.home, '.claude/skills');
const committedSkill = readFileSync(join(ROOT, 'autoplan/SKILL.md'), 'utf8');
const committedRouter = readFileSync(join(ROOT, 'SKILL.md'), 'utf8');
expect(committedSkill).toContain(SAFE);
expect(config(f, src, 'set', 'plan_mode_preamble', 'off').status).toBe(0);
const off = setup(f, src);
expect(off.status, off.out).toBe(0);
expect(off.out).toContain('and no plan-mode preamble');
expect(record(defaultRender(f))).toMatchObject({ overlay: 'claude', source: 'default', gbrain: 'absent', render: 'user', plan_mode: 'off' });
expect(served(skills)).not.toContain(SAFE);
expect(served(skills)).not.toContain('## Skill Invocation During Plan Mode');
expect(readFileSync(join(src, 'SKILL.md'), 'utf8')).not.toContain(SAFE);
expect(off.out).not.toContain('Claude skill overlay:');
expect(config(f, src, 'set', 'plan_mode_preamble', 'on').status).toBe(0);
put(join(f.home, 'gbrain-mode'), 'fail');
const on = setup(f, src);
expect(on.status, on.out).toBe(0);
expect(on.out).toContain('gstack-gbrain-detect failed');
expect(existsSync(defaultRender(f))).toBe(false);
expect(existsSync(`${defaultRender(f)}.overlay`)).toBe(false);
expect(served(skills)).toBe(readFileSync(join(src, 'autoplan/SKILL.md'), 'utf8'));
expect(served(skills)).toBe(committedSkill);
expect(readFileSync(join(src, 'SKILL.md'), 'utf8')).toBe(committedRouter);
}, 240_000);
});