mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
v1.91.158.0 feat(config): plan_mode_preamble=off drops the plan-mode preamble from installed skills (port of #3213) (#3308)
* feat(gen-skill-docs): add --plan-mode=off to omit the plan-mode preamble (#3168)
Generated skills ship a ~1.8 KB "## Plan Mode Safe Operations" +
"## Skill Invocation During Plan Mode" block on every skill, for hosts
and users who never enter plan mode. This adds a --plan-mode=off flag to
gen-skill-docs that drops that block at gen time. Default builds
(planMode undefined or true) keep the block byte-identically.
The "PLAN MODE EXCEPTION — ALWAYS RUN" markers inside skill bodies
still apply when the flag is off.
(cherry picked from commit 51edee9915)
* style(types): fix stray indentation on TemplateContext.installRoot
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* 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>
* fix(setup,relink): plan_mode_preamble=off survives builds and failed renders; alias follows the router
Review findings on the plan_mode_preamble wiring:
- a build renders env hosts without --plan-mode=off, so with the key off
setup no longer skips their re-render after a build;
- with the key off the gbrain-absent branch leaves the render to the plain
re-render, so a failed render keeps the previous one;
- gstack-relink refreshes the _gstack-command alias after re-rendering the
router (also fixes the same staleness for disabled_skills).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: xingzihai <xingzihai@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
17 files changed
+644
-44
No files matched your search
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.158.0] - 2026-10-19
|
||||
|
||||
**`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`). When setup has to build, `scripts/build.sh` renders Factory, OpenCode, Cursor, Copilot and Antigravity without the flag, so with the key off setup renders those hosts again afterwards.
|
||||
- **The default Claude install gets a render when the key is off.** Without a working gbrain (absent, or the detector failed), `gstack_claude_render_action` returns `plain` instead of serving the committed files; with gbrain the brain-aware render already runs and now carries the flag too. The activation record (`~/.gstack/render/claude.overlay`) gains `plan_mode=off`. Turning the key back on removes that render on the next setup when gbrain is absent or its detector fails (a failed detector otherwise keeps a render), and re-renders the brain-aware render with the sections when gbrain is present. A failed render with the key off keeps the previous render. Records written with the key on are unchanged.
|
||||
|
||||
### Fixed
|
||||
- **`gstack-relink` refreshes the `_gstack-command` router alias after it re-renders the router.** It used to copy the router before the re-render, so the alias kept the previous router (with `disabled_skills`, the full skill list) until the next relink.
|
||||
|
||||
### 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.157.0] - 2026-10-19
|
||||
|
||||
**Claude SKILL.md files no longer carry a "When to invoke" body section, which saves about 20 KB across 56 skills.** Claude Code reads a skill's body only after the skill is invoked, so routing prose there never routed anything. Routing still comes from the catalog description and the CLAUDE.md routing block, and voice triggers move into the root router's routing block. From #3129 by @thanhnguyentung95.
|
||||
|
||||
@@ -797,6 +797,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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.157.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.158.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+23
-4
@@ -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'."
|
||||
|
||||
+10
-3
@@ -461,19 +461,26 @@ 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
|
||||
_link_root_skill_alias
|
||||
[ -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
@@ -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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.157",
|
||||
"version": "1.91.158",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface GenerationOptions {
|
||||
explainLevel?: 'default' | 'terse';
|
||||
respectDetection?: boolean;
|
||||
log?: (message: string) => void;
|
||||
planMode?: boolean;
|
||||
}
|
||||
|
||||
interface RenderOptions {
|
||||
@@ -63,6 +64,7 @@ interface RenderOptions {
|
||||
catalogMode: 'trim' | 'full';
|
||||
explainLevel: 'default' | 'terse';
|
||||
gbrainDetected: boolean;
|
||||
planMode: boolean;
|
||||
}
|
||||
|
||||
export interface GeneratedArtifact {
|
||||
@@ -130,6 +132,11 @@ function parseGenerationArgs(args: string[]): GenerationOptions {
|
||||
if (explainLevel !== 'default' && explainLevel !== 'terse') {
|
||||
throw new Error(`Unknown explain level: ${explainLevel}. Use 'default' or 'terse'.`);
|
||||
}
|
||||
const planModeValue = value('--plan-mode');
|
||||
if (planModeValue !== undefined && planModeValue !== 'on' && planModeValue !== 'off') {
|
||||
throw new Error(`Unknown plan mode: ${planModeValue}. Use 'on' or 'off'.`);
|
||||
}
|
||||
const planMode = planModeValue === 'off' ? false : true;
|
||||
const outDir = value('--out-dir');
|
||||
const linkRoot = value('--link-root');
|
||||
const disabledValue = value('--disabled-skills');
|
||||
@@ -143,7 +150,7 @@ function parseGenerationArgs(args: string[]): GenerationOptions {
|
||||
// Swap-in callers use --link-root for the FINAL serving path (#2692).
|
||||
// Direct --out-dir callers retain their existing links into the render.
|
||||
return {
|
||||
host, model, catalogMode, explainLevel,
|
||||
host, model, catalogMode, explainLevel, planMode,
|
||||
dryRun: args.includes('--dry-run'),
|
||||
respectDetection: args.includes('--respect-detection'),
|
||||
outputRoot: outDir === undefined ? ROOT : path.resolve(outDir),
|
||||
@@ -687,7 +694,7 @@ function buildContext(
|
||||
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined;
|
||||
return {
|
||||
skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host],
|
||||
preambleTier, model: options.model ?? getHostConfig(host).defaultModel, interactive, explainLevel: options.explainLevel, installRoot: options.installRoot,
|
||||
preambleTier, model: options.model ?? getHostConfig(host).defaultModel, interactive, explainLevel: options.explainLevel, installRoot: options.installRoot, planMode: options.planMode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -920,6 +927,7 @@ export async function runGeneration(settings: GenerationOptions = {}): Promise<G
|
||||
catalogMode: settings.catalogMode ?? 'trim',
|
||||
explainLevel: settings.explainLevel ?? 'default',
|
||||
gbrainDetected: loadGbrainOverride(settings.respectDetection ?? false),
|
||||
planMode: settings.planMode ?? true,
|
||||
};
|
||||
const hosts = settings.host === 'all' ? ALL_HOST_NAMES as Host[] : [settings.host ?? 'claude'];
|
||||
const log = settings.log ?? (() => {});
|
||||
|
||||
@@ -25,6 +25,7 @@ import { getHostConfig } from '../../../hosts/index';
|
||||
*/
|
||||
export function generatePlanModeInfo(ctx: TemplateContext): string {
|
||||
if (!getHostConfig(ctx.host).capabilities.planMode) return '';
|
||||
if (ctx.planMode === false) return '';
|
||||
const entryOrder = ctx.skillName === 'plan-eng-review'
|
||||
? 'starting from the Scope gate, then follow its Startup sequence'
|
||||
: 'starting from the Scope gate (current target announcement or answered question), then preamble and Step 0';
|
||||
|
||||
@@ -117,6 +117,14 @@ export interface TemplateContext {
|
||||
* null/undefined = the host's default root (committed render, unchanged bytes).
|
||||
*/
|
||||
installRoot?: string | null;
|
||||
/**
|
||||
* Plan-mode preamble block. Defaults to true (block ships).
|
||||
* false (opt-in via `--plan-mode=off`): the "## Plan Mode Safe Operations" +
|
||||
* "## Skill Invocation During Plan Mode" section is dropped from the
|
||||
* preamble. "PLAN MODE EXCEPTION — ALWAYS RUN" markers inside skill bodies
|
||||
* still apply.
|
||||
*/
|
||||
planMode?: boolean;
|
||||
}
|
||||
|
||||
/** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */
|
||||
|
||||
@@ -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,58 +2080,60 @@ 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
|
||||
|
||||
# 1c-1g: a build (NEEDS_BUILD=1) already rendered these hosts, but without
|
||||
# --plan-mode=off, so plan_mode_preamble=off renders them again.
|
||||
# 1c. Generate .factory/ Factory Droid skill docs
|
||||
if [ "$INSTALL_FACTORY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
if [ "$INSTALL_FACTORY" -eq 1 ] && { [ "$NEEDS_BUILD" -eq 0 ] || [ -n "$_PLAN_MODE_ARG" ]; }; then
|
||||
log "Generating .factory/ skill docs..."
|
||||
(
|
||||
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
|
||||
|
||||
# 1d. Generate .opencode/ OpenCode skill docs
|
||||
if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
if [ "$INSTALL_OPENCODE" -eq 1 ] && { [ "$NEEDS_BUILD" -eq 0 ] || [ -n "$_PLAN_MODE_ARG" ]; }; then
|
||||
log "Generating .opencode/ skill docs..."
|
||||
(
|
||||
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
|
||||
|
||||
# 1e. Generate .cursor/ Cursor skill docs
|
||||
if [ "$INSTALL_CURSOR" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
if [ "$INSTALL_CURSOR" -eq 1 ] && { [ "$NEEDS_BUILD" -eq 0 ] || [ -n "$_PLAN_MODE_ARG" ]; }; then
|
||||
log "Generating .cursor/ skill docs..."
|
||||
(
|
||||
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
|
||||
|
||||
# 1f. Generate .copilot/ GitHub Copilot skill docs
|
||||
if [ "$INSTALL_COPILOT" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
if [ "$INSTALL_COPILOT" -eq 1 ] && { [ "$NEEDS_BUILD" -eq 0 ] || [ -n "$_PLAN_MODE_ARG" ]; }; then
|
||||
log "Generating .copilot/ skill docs..."
|
||||
(
|
||||
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
|
||||
|
||||
# 1g. Generate .agy/ Antigravity CLI skill docs
|
||||
if [ "$INSTALL_AGY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
if [ "$INSTALL_AGY" -eq 1 ] && { [ "$NEEDS_BUILD" -eq 0 ] || [ -n "$_PLAN_MODE_ARG" ]; }; then
|
||||
log "Generating .agy/ skill docs..."
|
||||
(
|
||||
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 +2744,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 +3033,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 +3076,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 +3224,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 +3334,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 +3408,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 +3651,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 +4186,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"
|
||||
@@ -4206,7 +4213,7 @@ if [ -x "$DETECT_BIN" ]; then
|
||||
# A render from a previous gbrain install would shadow canonical files
|
||||
# on the next link/relink — drop it and restore canonical links. A
|
||||
# pinned overlay re-renders it without brain blocks instead (below).
|
||||
if [ -d "$_GSTACK_RENDER_DIR" ] && [ -z "${GSTACK_SKIP_GBRAIN_REGEN:-}" ] && [ "$INSTALL_CLAUDE" -eq 1 ] && [ "$_GSTACK_OVERLAY" = claude ]; then
|
||||
if [ -d "$_GSTACK_RENDER_DIR" ] && [ -z "${GSTACK_SKIP_GBRAIN_REGEN:-}" ] && [ "$INSTALL_CLAUDE" -eq 1 ] && [ "$_GSTACK_OVERLAY" = claude ] && [ -z "$_PLAN_MODE_ARG" ]; then
|
||||
rm -rf "$_GSTACK_RENDER_DIR"
|
||||
gstack_claude_render_settle "$SOURCE_GSTACK_DIR" "$_GSTACK_RENDER_DIR" absent removed
|
||||
if [ "${_CLAUDE_SKILLS_LINKED:-0}" -eq 1 ]; then
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Unit tests for the --plan-mode=off flag (#3168).
|
||||
*
|
||||
* `--plan-mode=off` drops the "## Plan Mode Safe Operations" +
|
||||
* "## Skill Invocation During Plan Mode" preamble block from generated
|
||||
* skills, for hosts and users who never enter plan mode. Default builds
|
||||
* (planMode undefined or true) keep the block byte-identically.
|
||||
*
|
||||
* Pure-function tests against the resolvers — fast, free, no subprocess.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import type { TemplateContext } from '../scripts/resolvers/types';
|
||||
import { generatePlanModeInfo, generateCompletionStatus } from '../scripts/resolvers/preamble/generate-completion-status';
|
||||
import { generatePreamble } from '../scripts/resolvers/preamble';
|
||||
|
||||
function makeCtx(planMode?: boolean, tier: number = 4): TemplateContext {
|
||||
return {
|
||||
skillName: 'test-skill',
|
||||
tmplPath: '/tmp/test/SKILL.md.tmpl',
|
||||
host: 'claude',
|
||||
paths: {
|
||||
skillRoot: '~/.claude/skills/gstack',
|
||||
localSkillRoot: '.claude/skills',
|
||||
binDir: '~/.claude/skills/gstack/bin',
|
||||
browseDir: '~/.claude/skills/gstack/browse/dist',
|
||||
designDir: '~/.claude/skills/gstack/design/dist',
|
||||
makePdfDir: '~/.claude/skills/gstack/make-pdf/dist',
|
||||
},
|
||||
preambleTier: tier,
|
||||
planMode,
|
||||
};
|
||||
}
|
||||
|
||||
describe('plan-mode off — generatePlanModeInfo', () => {
|
||||
test('planMode undefined: emits both plan-mode sections (default behavior)', () => {
|
||||
const out = generatePlanModeInfo(makeCtx(undefined));
|
||||
expect(out).toContain('## Plan Mode Safe Operations');
|
||||
expect(out).toContain('## Skill Invocation During Plan Mode');
|
||||
});
|
||||
|
||||
test('planMode true: emits both plan-mode sections', () => {
|
||||
const out = generatePlanModeInfo(makeCtx(true));
|
||||
expect(out).toContain('## Plan Mode Safe Operations');
|
||||
expect(out).toContain('## Skill Invocation During Plan Mode');
|
||||
});
|
||||
|
||||
test('planMode false: returns empty string', () => {
|
||||
expect(generatePlanModeInfo(makeCtx(false))).toBe('');
|
||||
});
|
||||
|
||||
test('planMode false: no plan-mode heading survives', () => {
|
||||
const out = generatePlanModeInfo(makeCtx(false));
|
||||
expect(out).not.toContain('Plan Mode Safe Operations');
|
||||
expect(out).not.toContain('Skill Invocation During Plan Mode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plan-mode off — generatePreamble integration', () => {
|
||||
test('default tier-2 preamble includes the plan-mode block', () => {
|
||||
const out = generatePreamble(makeCtx(undefined, 2));
|
||||
expect(out).toContain('## Plan Mode Safe Operations');
|
||||
expect(out).toContain('## Skill Invocation During Plan Mode');
|
||||
});
|
||||
|
||||
test('planMode false tier-2 preamble drops the plan-mode block', () => {
|
||||
const out = generatePreamble(makeCtx(false, 2));
|
||||
expect(out).not.toContain('## Plan Mode Safe Operations');
|
||||
expect(out).not.toContain('## Skill Invocation During Plan Mode');
|
||||
});
|
||||
|
||||
test('planMode false preamble is measurably smaller than default', () => {
|
||||
const defaultLen = generatePreamble(makeCtx(undefined, 2)).length;
|
||||
const offLen = generatePreamble(makeCtx(false, 2)).length;
|
||||
// The plan-mode block is ~1.8 KB; assert at least 1 KB saved.
|
||||
expect(defaultLen - offLen).toBeGreaterThan(1024);
|
||||
});
|
||||
|
||||
test('planMode undefined preamble is byte-identical to planMode true', () => {
|
||||
expect(generatePreamble(makeCtx(undefined, 2))).toBe(generatePreamble(makeCtx(true, 2)));
|
||||
});
|
||||
|
||||
test('planMode false: PLAN MODE EXCEPTION markers inside skill bodies still apply (completion status keeps its telemetry exception)', () => {
|
||||
const out = generateCompletionStatus(makeCtx(false));
|
||||
expect(out).toContain('PLAN MODE EXCEPTION — ALWAYS RUN');
|
||||
});
|
||||
});
|
||||
@@ -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'.");
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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"');
|
||||
});
|
||||
|
||||
test('setup: a build (which renders without the arg) does not skip the host re-renders when the key is off', () => {
|
||||
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
|
||||
const guards = setup.split('\n').filter(line => /^if \[ "\$INSTALL_[A-Z]+" -eq 1 \] && .*NEEDS_BUILD/.test(line));
|
||||
expect(guards.length).toBe(5);
|
||||
for (const line of guards) expect(line, line).toContain('{ [ "$NEEDS_BUILD" -eq 0 ] || [ -n "$_PLAN_MODE_ARG" ]; }');
|
||||
});
|
||||
|
||||
test('setup: with the key off the gbrain-absent branch leaves the render for the plain re-render (a failed render keeps it)', () => {
|
||||
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf8');
|
||||
const removal = setup.split('\n').filter(line => /if \[ -d "\$_GSTACK_RENDER_DIR" \].*\$_GSTACK_OVERLAY" = claude/.test(line));
|
||||
expect(removal.length).toBe(1);
|
||||
expect(removal[0]).toContain('[ -z "$_PLAN_MODE_ARG" ]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-relink router re-render', () => {
|
||||
const BINS = ['gstack-config', 'gstack-relink', 'gstack-patch-names', 'gstack-state-root.sh', 'gstack-install-registry.sh'];
|
||||
const SAFE = '## Plan Mode Safe Operations';
|
||||
|
||||
function relinkFixture() {
|
||||
const install = path.join(tmp, 'gstack');
|
||||
fs.mkdirSync(path.join(install, 'bin'), { recursive: true });
|
||||
for (const b of BINS) {
|
||||
fs.copyFileSync(path.join(ROOT, 'bin', b), path.join(install, 'bin', b));
|
||||
fs.chmodSync(path.join(install, 'bin', b), 0o755);
|
||||
}
|
||||
fs.symlinkSync(path.join(ROOT, 'scripts'), path.join(install, 'scripts'));
|
||||
fs.copyFileSync(path.join(ROOT, 'SKILL.md'), path.join(install, 'SKILL.md'));
|
||||
fs.mkdirSync(path.join(install, 'ship'));
|
||||
fs.writeFileSync(path.join(install, 'ship', 'SKILL.md'), '---\nname: ship\ndescription: test\n---\n# ship\n');
|
||||
const env = {
|
||||
...process.env,
|
||||
GSTACK_HOME: path.join(tmp, 'state'),
|
||||
GSTACK_STATE_ROOT: '',
|
||||
GSTACK_STATE_DIR: '',
|
||||
GSTACK_INSTALL_DIR: install,
|
||||
GSTACK_SKILLS_DIR: path.join(tmp, 'skills'),
|
||||
GSTACK_SETUP_RUNNING: '1',
|
||||
};
|
||||
const run = (bin: string, ...args: string[]) => {
|
||||
const r = spawnSync('bash', [path.join(install, 'bin', bin), ...args], { encoding: 'utf8', timeout: 60_000, env });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
return r;
|
||||
};
|
||||
const router = () => fs.readFileSync(path.join(install, 'SKILL.md'), 'utf8');
|
||||
const alias = () => fs.readFileSync(path.join(tmp, 'skills', '_gstack-command', 'SKILL.md'), 'utf8');
|
||||
const asAlias = (text: string) => text.replace(/^name: gstack$/m, 'name: _gstack-command');
|
||||
return { run, router, alias, asAlias };
|
||||
}
|
||||
|
||||
test('off strips the router and its _gstack-command alias; on restores both to the committed bytes', () => {
|
||||
const { run, router, alias, asAlias } = relinkFixture();
|
||||
const committed = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf8');
|
||||
run('gstack-relink');
|
||||
expect(router()).toBe(committed);
|
||||
expect(alias()).toBe(asAlias(committed));
|
||||
|
||||
run('gstack-config', 'set', 'plan_mode_preamble', 'off');
|
||||
run('gstack-relink');
|
||||
expect(router()).not.toContain(SAFE);
|
||||
expect(router()).toContain('PLAN MODE EXCEPTION');
|
||||
expect(alias()).toBe(asAlias(router()));
|
||||
|
||||
run('gstack-config', 'set', 'plan_mode_preamble', 'on');
|
||||
run('gstack-relink');
|
||||
expect(router()).toBe(committed);
|
||||
expect(alias()).toBe(asAlias(committed));
|
||||
});
|
||||
|
||||
test('disabled_skills: the alias follows the re-rendered router in the same run', () => {
|
||||
const { run, router, alias, asAlias } = relinkFixture();
|
||||
run('gstack-config', 'set', 'disabled_skills', 'ship');
|
||||
run('gstack-relink');
|
||||
expect(router()).toContain('Disabled on this install (never invoke or suggest them): `/ship`.');
|
||||
expect(alias()).toBe(asAlias(router()));
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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, failableBun, fakeDetect, record, served, setup } from './helpers/claude-overlay-fixture';
|
||||
import { rmSync } from 'node:fs';
|
||||
import { ROOT, cleanupFixtures, cleanupSeed, makeFixture, makeSource, put } from './helpers/install-fixture';
|
||||
|
||||
afterEach(cleanupFixtures);
|
||||
afterAll(cleanupSeed);
|
||||
|
||||
const SAFE = '## Plan Mode Safe Operations';
|
||||
const alias = (skills: string) => readFileSync(join(skills, '_gstack-command/SKILL.md'), 'utf8');
|
||||
const asAlias = (router: string) => router.replace(/^name: gstack$/m, 'name: _gstack-command');
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('./setup with plan_mode_preamble', () => {
|
||||
test('off renders the default install without the preamble and keeps it through a failed render; on (detector failing) serves the committed files again', () => {
|
||||
const f = makeFixture();
|
||||
failableBun(f);
|
||||
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(alias(skills)).toBe(asAlias(readFileSync(join(src, 'SKILL.md'), 'utf8')));
|
||||
|
||||
put(join(f.home, 'fail-render'), '1');
|
||||
const failed = setup(f, src);
|
||||
expect(failed.status, failed.out).toBe(0);
|
||||
expect(record(defaultRender(f))).toMatchObject({ plan_mode: 'off', render: 'user' });
|
||||
expect(served(skills)).not.toContain(SAFE);
|
||||
rmSync(join(f.home, 'fail-render'));
|
||||
|
||||
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);
|
||||
expect(alias(skills)).toBe(asAlias(committedRouter));
|
||||
}, 240_000);
|
||||
});
|
||||
Reference in new issue
Block a user