diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88037aeb0..09d3cac92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [1.93.12.0] - 2026-11-02
+
+**The Opus 5.5 `lite` skills now keep the install's gbrain blocks, disabled skills and the Claims Need Evidence rule (GST-944).** The lite tree Capy publishes was rendered without the install's settings. On a machine with gbrain detected, Opus 5.5 sessions lost the brain context-load and save-results blocks that the full install serves, and skills turned off with `disabled_skills` showed up again in the lite router. `./setup --loadout lite` was not affected.
+
+### Fixed
+- **`gstack-modules install`** renders lite with the state root's gbrain detection and `gstack-config` `disabled_skills`, and both are part of the folder's identity. A change in either publishes a new folder instead of reusing a stale one.
+
+### Changed
+- **`preamble:evidence` is pinned**, so `lite` drops ten coaching pieces instead of eleven. The honest-work design (W4) never retires the Claims Need Evidence rule while gstack reports evidence. It adds 804 bytes back to lite's shared preamble (16,175 → 16,979 bytes; full is 22,396).
+
+### Tests
+- `test/module-loadouts-install.test.ts`: with gbrain detected and `disabled_skills: qa`, the lite office-hours file carries the brain blocks, the router lists `/qa` as disabled, and the folder differs from the one published without those settings.
+- `test/module-loadouts.test.ts`: `preamble:evidence` is pinned and lite excludes the other ten pieces.
+
## [1.93.11.0] - 2026-11-02
**Capy sessions get their model's loadout again without a sentence in the skill (GST-982).** The browse wave (#3858) removed the Capy skill's instruction to run the installer with `GSTACK_MODEL`, to keep `capy/skill/gstack/SKILL.md` under its context budget. After that, every Capy session installed the full skills, including Opus 5.5 sessions that should get `lite`. Now the installer finds the model by itself.
diff --git a/VERSION b/VERSION
index bd312856c..7f209dd81 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.93.11.0
+1.93.12.0
diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md
index ad51d75a7..513f2a765 100644
--- a/agents-digest/gstack-AGENTS.md
+++ b/agents-digest/gstack-AGENTS.md
@@ -1,4 +1,4 @@
-# gstack digest v1.93.11.0 — regenerate/re-copy after upgrading gstack
+# gstack digest v1.93.12.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,
diff --git a/docs/loadouts.md b/docs/loadouts.md
index 1b44d99ae..50d4a24aa 100644
--- a/docs/loadouts.md
+++ b/docs/loadouts.md
@@ -9,7 +9,7 @@ One model has its own loadout: Claude Opus 5.5 gets `lite` ([below](#lite-on-cla
| Term | Meaning |
|---|---|
| Module | One of four kinds. `skill:
` is a whole skill. `section:/` is one entry of `/sections/manifest.json`. `overlay:` is one `model-overlays/.md`; excluding a base overlay also removes the base's text from every overlay that inherits it; the inheriting overlay's own text stays. `preamble:` is one piece of the shared preamble (`PREAMBLE_PIECES` in `scripts/resolvers/preamble.ts`). |
-| Pinned | A module no loadout can remove where `full` renders it: the start command, plan-mode rules, the question format, completion status, the unattended profile, /ship's review army and adversarial pass, the router and `/gstack-upgrade`. A skill's `preamble-tier` still applies, so a tier-1 skill has no question-format section in any loadout. |
+| Pinned | A module no loadout can remove where `full` renders it: the start command, plan-mode rules, the question format, completion status, the Claims Need Evidence rule, the unattended profile, /ship's review army and adversarial pass, the router and `/gstack-upgrade`. A skill's `preamble-tier` still applies, so a tier-1 skill has no question-format section in any loadout. |
| Rung | How much scaffolding a model gets: `frontier`, `strong`, `mid`, `light`, or `unknown`. Model ids are matched after lowercasing and stripping a `provider/` prefix (`anthropic/claude-haiku-5-5` is `mid`). Rungs are separate from the frontier/smart tiers in `lib/model-catalog.ts`, which choose which model runs a role. |
| Loadout | `{ base, exclude, include, preamble_tier_max, note }`. `full` (include `*`) is always present. An unknown model gets `default.unknown`, which is `full`. |
| Model map | `models` in `modules/loadouts.json`: an exact model id to a loadout, checked before rungs. The id also matches its dated snapshot (`-YYYYMMDD`) and `-latest`, never a point release or any other suffix (`claude-opus-5-5` matches `claude-opus-5-5-20261101`, not `claude-opus-5-50` or `claude-opus-5-5-1`). |
@@ -40,7 +40,7 @@ bin/gstack-modules install --full-root "$HOME/.claude/skills" --model claude-opu
## Lite on Claude Opus 5.5
-`lite` is `full` minus eleven coaching pieces of the shared preamble: voice, writing style, completeness, confusion, evidence, context health, question tuning, search before building, repo mode, brain health and context recovery. It keeps every pinned piece, the upgrade check (the PROACTIVE and skill-prefix rules) and brain sync (the privacy stop-gate). Only `claude-opus-5-5` maps to it, because only Opus 5.5 has been measured: office-hours showed no loss on Opus 5.5 (GSTA-161 preamble run), and the land-and-deploy confirmation (GSTA-161, PR 1b) compared full, lite and no gstack on the same tasks. Haiku 5.5 leaned slightly against a larger cut on the same office-hours run, so small models stay on `full`. Two skills point at a preamble section lite drops: office-hours and plan-ceo-review name ETHOS.md through the "Search Before Building" section, so under lite they find ETHOS.md in the gstack checkout instead.
+`lite` is `full` minus ten coaching pieces of the shared preamble: voice, writing style, completeness, confusion, context health, question tuning, search before building, repo mode, brain health and context recovery. It keeps every pinned piece, including the Claims Need Evidence rule (GST-944: the honest-work design never retires it while gstack reports evidence, and it adds 804 bytes back to lite's 16,175-byte shared preamble), the upgrade check (the PROACTIVE and skill-prefix rules) and brain sync (the privacy stop-gate). Only `claude-opus-5-5` maps to it, because only Opus 5.5 has been measured: office-hours showed no loss on Opus 5.5 (GSTA-161 preamble run), and the land-and-deploy confirmation (GSTA-161, PR 1b) compared full, lite and no gstack on the same tasks. Haiku 5.5 leaned slightly against a larger cut on the same office-hours run, so small models stay on `full`. Two skills point at a preamble section lite drops: office-hours and plan-ceo-review name ETHOS.md through the "Search Before Building" section, so under lite they find ETHOS.md in the gstack checkout instead.
### How gstack picks a loadout
@@ -64,7 +64,7 @@ SKILL_ROOT: /home/user/.gstack/loadouts/lite-3f2a9c01d4e7
Read skills from `SKILL_ROOT` (`/gstack/SKILL.md` routes, `//SKILL.md` runs) and source the printed `GSTACK_LAUNCH_FILE`, which adds `GSTACK_LOADOUT_SKILL_ROOT`. For `full`, `SKILL_ROOT` is `~/.claude/skills` and the launch file is the install verdict's own.
-The lite tree is published by `bin/gstack-modules install` under `/loadouts/-/`, beside the full install; the checkout and its committed SKILL.md files stay the full render. The digest covers the selection, the bound model, the full root, host, overlay, plan mode and the source tree (the git HEAD tree plus any uncommitted change), so an upgrade or a different model gets a new folder and an unchanged one is reused after its files are re-hashed against `loadout.lock.json`. Each folder is a complete skill tree: the rendered SKILL.md and section files plus every tracked runtime asset of each skill (templates, references, scripts). A published folder is never replaced. Pruning removes only folders it published (a `-` name with an install lock) beyond the three most recently used per loadout, and never one used in the last day. When `--full-root` is not `~/.claude/skills`, the tree's command paths point at that install's `gstack` folder. Two Capy threads with different models on one machine share one install verdict and get one launch file each (`.loadout--.env` for lite). Any failure serves `full` and prints `LOADOUT_FALLBACK`.
+The lite tree is published by `bin/gstack-modules install` under `/loadouts/-/`, beside the full install; the checkout and its committed SKILL.md files stay the full render. It carries over two of the full render's install settings: gbrain detection (`gbrain-detection.json` in the state root, so the brain context-load and save-results blocks appear when gbrain is detected) and `gstack-config` `disabled_skills` (the router omits those skills). The digest covers the selection, the bound model, the full root, host, overlay, plan mode, gbrain detection, disabled skills and the source tree (the git HEAD tree plus any uncommitted change), so an upgrade or a different model gets a new folder and an unchanged one is reused after its files are re-hashed against `loadout.lock.json`. Each folder is a complete skill tree: the rendered SKILL.md and section files plus every tracked runtime asset of each skill (templates, references, scripts). A published folder is never replaced. Pruning removes only folders it published (a `-` name with an install lock) beyond the three most recently used per loadout, and never one used in the last day. When `--full-root` is not `~/.claude/skills`, the tree's command paths point at that install's `gstack` folder. Two Capy threads with different models on one machine share one install verdict and get one launch file each (`.loadout--.env` for lite). Any failure serves `full` and prints `LOADOUT_FALLBACK`.
### The start line carries the model
diff --git a/modules/loadouts.json b/modules/loadouts.json
index 04204a69f..9d3ebbc99 100644
--- a/modules/loadouts.json
+++ b/modules/loadouts.json
@@ -42,6 +42,7 @@
"preamble:plan-mode-info",
"preamble:ask-user-format",
"preamble:completion-status",
+ "preamble:evidence",
"section:autoplan/unattended",
"section:ship/review-army",
"section:ship/adversarial"
@@ -58,7 +59,6 @@
"preamble:writing-style",
"preamble:completeness",
"preamble:confusion",
- "preamble:evidence",
"preamble:context-health",
"preamble:question-tuning",
"preamble:search-before-building",
@@ -66,7 +66,7 @@
"preamble:brain-health",
"preamble:context-recovery"
],
- "note": "Opus 5.5 only (GSTA-161). Drops eleven coaching preamble pieces and keeps upgrade-check and brain-sync. Evidence: the GSTA-161 office-hours preamble run (handoffs/GSTA-161/preamble-measurement.md) and the three-arm land-and-deploy confirmation cited in docs/loadouts.md."
+ "note": "Opus 5.5 only (GSTA-161). Drops ten coaching preamble pieces and keeps upgrade-check, brain-sync and the pinned Claims Need Evidence rule (GST-944). Evidence: the GSTA-161 office-hours preamble run (handoffs/GSTA-161/preamble-measurement.md) and the three-arm land-and-deploy confirmation cited in docs/loadouts.md."
}
},
"models": {
diff --git a/package.json b/package.json
index 7ca2b4061..098af7b3f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gstack",
- "version": "1.93.11",
+ "version": "1.93.12",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts
index 7869e377d..a644f7337 100644
--- a/scripts/gen-skill-docs.ts
+++ b/scripts/gen-skill-docs.ts
@@ -55,6 +55,8 @@ export interface GenerationOptions {
catalogMode?: 'trim' | 'full';
explainLevel?: 'default' | 'terse';
respectDetection?: boolean;
+ /** A detection result the caller already read; wins over respectDetection so one read serves identity and render. */
+ gbrainDetected?: boolean;
log?: (message: string) => void;
planMode?: boolean;
/** Module loadout (scripts/modules.ts selectModules); null/undefined = `full`. */
@@ -94,7 +96,7 @@ export interface GenerationResult {
}
/** Canonical generation never reads local detection state unless opted in. */
-function loadGbrainOverride(respectDetection: boolean): boolean {
+export function loadGbrainOverride(respectDetection: boolean): boolean {
if (!respectDetection) return false;
const stateDir = resolveStateRoot();
try {
@@ -989,7 +991,7 @@ export async function runGeneration(settings: GenerationOptions = {}): Promise n.replace(/^\//, '')).filter(n => n !== 'gstack' && n !== 'gstack-upgrade')
+ .map(n => n.replace(/^gstack-/, '')).filter(n => /^[a-z0-9-]+$/.test(n)))].sort();
const identity = {
loadout: selection.name,
excluded: [...selection.excluded].sort(),
@@ -157,6 +162,8 @@ export async function installLoadout(request: InstallRequest): Promise d.kind === 'error').map(d => d.message);
if (result.exitCode !== 0 || errors.length) {
throw new ModuleError('RENDER_FAILED', `gen-skill-docs failed: ${errors.join('; ') || 'exit 1'}`, 'fix the named template or module, then rerun; side effects: none, safe to retry');
diff --git a/test/module-loadouts-install.test.ts b/test/module-loadouts-install.test.ts
index 8eac52183..62b9095cb 100644
--- a/test/module-loadouts-install.test.ts
+++ b/test/module-loadouts-install.test.ts
@@ -6,7 +6,7 @@
* from the printed SKILL_ROOT; concurrent publishers of one identity agree;
* the committed checkout stays byte-identical.
*/
-import { afterAll, describe, expect, test } from 'bun:test';
+import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { spawnSync } from 'child_process';
import { createHash } from 'crypto';
import * as fs from 'fs';
@@ -19,7 +19,14 @@ import { LOCK_FILE } from '../scripts/modules-render';
const ROOT = path.resolve(import.meta.dir, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'loadout-install-'));
-afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
+const savedState = { GSTACK_HOME: process.env.GSTACK_HOME, GSTACK_STATE_ROOT: process.env.GSTACK_STATE_ROOT };
+beforeAll(() => {
+ process.env.GSTACK_HOME = process.env.GSTACK_STATE_ROOT = path.join(tmp, 'empty-state');
+});
+afterAll(() => {
+ for (const [key, value] of Object.entries(savedState)) if (value === undefined) delete process.env[key]; else process.env[key] = value;
+ fs.rmSync(tmp, { recursive: true, force: true });
+});
const FULL_ROOT = '/opt/claude/skills';
const lite = selectModules({ loadout: 'lite' })!;
const run = (...args: string[]) => {
@@ -64,7 +71,8 @@ describe('gstack-modules install', () => {
expect(landing).toContain(`${FULL_ROOT}/gstack/bin/`);
expect(landing).not.toContain('~/.claude/skills/gstack/bin/');
expect(lock).toMatchObject({ kind: 'install', loadout: 'lite', loadout_model: 'claude-opus-5-5', full_root: FULL_ROOT });
- expect(lock.excluded).toHaveLength(11);
+ expect(lock.excluded).toHaveLength(10);
+ expect(lock.excluded).not.toContain('preamble:evidence');
expect(lock.excluded).not.toContain('preamble:upgrade-check');
expect(lock.excluded).not.toContain('preamble:brain-sync');
expect(JSON.parse(fs.readFileSync(path.join(root, LOCK_FILE), 'utf-8')).identity.source_tree).toBe(sourceTree(ROOT));
@@ -81,6 +89,32 @@ describe('gstack-modules install', () => {
expect(explicit).not.toContain('--loadout');
}, 240_000);
+ test('the lite tree keeps the install settings the full render uses: gbrain detection and disabled_skills, both in the identity (GST-944)', () => {
+ const state = path.join(tmp, 'settings-state');
+ const store = path.join(tmp, 'settings');
+ fs.mkdirSync(state, { recursive: true });
+ const install = () => {
+ const r = spawnSync(process.execPath, [...bunIsolationArgs(), path.join(ROOT, 'bin/gstack-modules'), 'install', '--model', 'claude-opus-5-5', '--full-root', FULL_ROOT, '--store', store], { cwd: ROOT, encoding: 'utf-8', timeout: 120_000, env: { ...process.env, GSTACK_HOME: state, GSTACK_STATE_ROOT: state } });
+ expect(r.status, r.stderr).toBe(0);
+ expect(r.stderr).not.toContain('LOADOUT_FALLBACK');
+ return r.stdout.match(/^SKILL_ROOT: (.+)$/m)![1];
+ };
+ const plain = install();
+ fs.writeFileSync(path.join(state, 'config.yaml'), 'disabled_skills: qa\n');
+ const onlyDisabled = install();
+ expect(onlyDisabled).not.toBe(plain);
+ fs.writeFileSync(path.join(state, 'config.yaml'), 'disabled_skills: "/qa, gstack-qa gstack-upgrade"\n');
+ expect(install()).toBe(onlyDisabled);
+ fs.writeFileSync(path.join(state, 'gbrain-detection.json'), JSON.stringify({ gbrain_local_status: 'ok' }));
+ const set = install();
+ expect(set).not.toBe(onlyDisabled);
+ const officeHours = (root: string) => fs.readFileSync(path.join(root, 'office-hours/SKILL.md'), 'utf-8');
+ expect(officeHours(set).split('gbrain').length).toBeGreaterThan(officeHours(plain).split('gbrain').length);
+ const router = fs.readFileSync(path.join(set, 'SKILL.md'), 'utf-8');
+ expect(router).toContain('Disabled on this install (never invoke or suggest them): `/qa`.');
+ expect(fs.readFileSync(path.join(plain, 'SKILL.md'), 'utf-8')).not.toContain('Disabled on this install');
+ }, 240_000);
+
test('concurrent publishers of one identity agree on one folder; a tampered folder is refused, not reused', async () => {
const store = path.join(tmp, 'race');
const [x, y] = await Promise.all([1, 2].map(() => installLoadout({ selection: lite, boundModel: 'claude-opus-5-5', fullRoot: FULL_ROOT, store })));
diff --git a/test/module-loadouts.test.ts b/test/module-loadouts.test.ts
index eff886e88..909c2d83d 100644
--- a/test/module-loadouts.test.ts
+++ b/test/module-loadouts.test.ts
@@ -48,8 +48,9 @@ describe('modules/loadouts.json', () => {
expect(Object.values(manifest.default)).toEqual(['full', 'full', 'full', 'full', 'full']);
});
- test('lite drops the eleven coaching pieces, keeps upgrade-check and brain-sync, and only Opus 5.5 maps to it', () => {
- const coaching = ['voice', 'writing-style', 'completeness', 'confusion', 'evidence', 'context-health', 'question-tuning', 'search-before-building', 'repo-mode', 'brain-health', 'context-recovery'];
+ test('lite drops ten coaching pieces, keeps upgrade-check, brain-sync and the pinned Claims Need Evidence rule, and only Opus 5.5 maps to it', () => {
+ expect(manifest.pinned).toContain('preamble:evidence');
+ const coaching = ['voice', 'writing-style', 'completeness', 'confusion', 'context-health', 'question-tuning', 'search-before-building', 'repo-mode', 'brain-health', 'context-recovery'];
expect(manifest.loadouts.lite).toMatchObject({ base: 'full', exclude: coaching.map(id => `preamble:${id}`) });
expect(manifest.models).toEqual({ 'claude-opus-5-5': 'lite' });
const s = selectModules({ loadout: 'lite' })!;