v1.93.12.0 fix(loadouts): lite install keeps gbrain detection and disabled_skills; pin the evidence rule (GST-944) (#3874)

* fix(loadouts): lite install keeps gbrain detection and disabled_skills; pin the Claims Need Evidence rule (GST-944)

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

* fix(loadouts): one gbrain detection read serves identity and render; normalize disabled_skills like gstack-config; isolate install tests from ~/.gstack (GST-944 review)

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

* test(loadouts): set the empty state root in beforeAll (GST-944)

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

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-02 18:05:23 +00:00
1 parent 24a47db3d9
commit 8f08cb76ac
10 files changed
+75 -17

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
# Changelog # 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 ## [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. **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.
+1 -1
View File
@@ -1 +1 @@
1.93.11.0 1.93.12.0
+1 -1
View File
@@ -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 Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows, for agent hosts without a full skill install. The full skills add workflows,
+3 -3
View File
@@ -9,7 +9,7 @@ One model has its own loadout: Claude Opus 5.5 gets `lite` ([below](#lite-on-cla
| Term | Meaning | | Term | Meaning |
|---|---| |---|---|
| Module | One of four kinds. `skill:<dir>` is a whole skill. `section:<skill>/<id>` is one entry of `<skill>/sections/manifest.json`. `overlay:<name>` is one `model-overlays/<name>.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:<id>` is one piece of the shared preamble (`PREAMBLE_PIECES` in `scripts/resolvers/preamble.ts`). | | Module | One of four kinds. `skill:<dir>` is a whole skill. `section:<skill>/<id>` is one entry of `<skill>/sections/manifest.json`. `overlay:<name>` is one `model-overlays/<name>.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:<id>` 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. | | 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`. | | 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`). | | 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 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 ### How gstack picks a loadout
@@ -64,7 +64,7 @@ SKILL_ROOT: /home/user/.gstack/loadouts/lite-3f2a9c01d4e7
Read skills from `SKILL_ROOT` (`<root>/gstack/SKILL.md` routes, `<root>/<skill>/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. Read skills from `SKILL_ROOT` (`<root>/gstack/SKILL.md` routes, `<root>/<skill>/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 `<state root>/loadouts/<loadout>-<digest12>/`, 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 `<loadout>-<digest12>` 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 (`<verdict>.loadout-<name>-<digest>.env` for lite). Any failure serves `full` and prints `LOADOUT_FALLBACK`. The lite tree is published by `bin/gstack-modules install` under `<state root>/loadouts/<loadout>-<digest12>/`, 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 `<loadout>-<digest12>` 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 (`<verdict>.loadout-<name>-<digest>.env` for lite). Any failure serves `full` and prints `LOADOUT_FALLBACK`.
### The start line carries the model ### The start line carries the model
+2 -2
View File
@@ -42,6 +42,7 @@
"preamble:plan-mode-info", "preamble:plan-mode-info",
"preamble:ask-user-format", "preamble:ask-user-format",
"preamble:completion-status", "preamble:completion-status",
"preamble:evidence",
"section:autoplan/unattended", "section:autoplan/unattended",
"section:ship/review-army", "section:ship/review-army",
"section:ship/adversarial" "section:ship/adversarial"
@@ -58,7 +59,6 @@
"preamble:writing-style", "preamble:writing-style",
"preamble:completeness", "preamble:completeness",
"preamble:confusion", "preamble:confusion",
"preamble:evidence",
"preamble:context-health", "preamble:context-health",
"preamble:question-tuning", "preamble:question-tuning",
"preamble:search-before-building", "preamble:search-before-building",
@@ -66,7 +66,7 @@
"preamble:brain-health", "preamble:brain-health",
"preamble:context-recovery" "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": { "models": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "gstack", "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.", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+4 -2
View File
@@ -55,6 +55,8 @@ export interface GenerationOptions {
catalogMode?: 'trim' | 'full'; catalogMode?: 'trim' | 'full';
explainLevel?: 'default' | 'terse'; explainLevel?: 'default' | 'terse';
respectDetection?: boolean; 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; log?: (message: string) => void;
planMode?: boolean; planMode?: boolean;
/** Module loadout (scripts/modules.ts selectModules); null/undefined = `full`. */ /** 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. */ /** 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; if (!respectDetection) return false;
const stateDir = resolveStateRoot(); const stateDir = resolveStateRoot();
try { try {
@@ -989,7 +991,7 @@ export async function runGeneration(settings: GenerationOptions = {}): Promise<G
model: settings.model ?? null, model: settings.model ?? null,
catalogMode: settings.catalogMode ?? 'trim', catalogMode: settings.catalogMode ?? 'trim',
explainLevel: settings.explainLevel ?? 'default', explainLevel: settings.explainLevel ?? 'default',
gbrainDetected: loadGbrainOverride(settings.respectDetection ?? false), gbrainDetected: settings.gbrainDetected ?? loadGbrainOverride(settings.respectDetection ?? false),
planMode: settings.planMode ?? true, planMode: settings.planMode ?? true,
modules: settings.modules ?? null, modules: settings.modules ?? null,
}; };
+9 -2
View File
@@ -26,7 +26,8 @@ import { createHash, randomBytes } from 'node:crypto';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as os from 'node:os'; import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { runGeneration } from './gen-skill-docs'; import { readConfigKey } from '../lib/state-root';
import { loadGbrainOverride, runGeneration } from './gen-skill-docs';
import { resolveModel } from './models'; import { resolveModel } from './models';
import { ModuleError, type ModuleSelection } from './modules'; import { ModuleError, type ModuleSelection } from './modules';
import { LOCK_FILE, danglingReferences } from './modules-render'; import { LOCK_FILE, danglingReferences } from './modules-render';
@@ -148,6 +149,10 @@ export async function installLoadout(request: InstallRequest): Promise<InstallRe
const planMode = request.planMode ?? true; const planMode = request.planMode ?? true;
const model = request.boundModel ? resolveModel(request.boundModel) : null; const model = request.boundModel ? resolveModel(request.boundModel) : null;
const selection: ModuleSelection = { ...request.selection, boundModel: request.boundModel ?? undefined, fullRoot: request.boundModel ? request.fullRoot : undefined }; const selection: ModuleSelection = { ...request.selection, boundModel: request.boundModel ?? undefined, fullRoot: request.boundModel ? request.fullRoot : undefined };
const gbrainDetected = loadGbrainOverride(true);
const disabledSkills = [...new Set((readConfigKey('disabled_skills') ?? '').replace(/["']/g, '').split(/[\s,]+/)
.map(n => n.replace(/^\//, '')).filter(n => n !== 'gstack' && n !== 'gstack-upgrade')
.map(n => n.replace(/^gstack-/, '')).filter(n => /^[a-z0-9-]+$/.test(n)))].sort();
const identity = { const identity = {
loadout: selection.name, loadout: selection.name,
excluded: [...selection.excluded].sort(), excluded: [...selection.excluded].sort(),
@@ -157,6 +162,8 @@ export async function installLoadout(request: InstallRequest): Promise<InstallRe
host, host,
overlay_model: model, overlay_model: model,
plan_mode: planMode, plan_mode: planMode,
gbrain_detected: gbrainDetected,
disabled_skills: disabledSkills,
source_tree: sourceTree(source), source_tree: sourceTree(source),
}; };
const identitySha = sha256(JSON.stringify(identity)); const identitySha = sha256(JSON.stringify(identity));
@@ -174,7 +181,7 @@ export async function installLoadout(request: InstallRequest): Promise<InstallRe
} }
const stage = `${final}.stage-${process.pid}-${randomBytes(4).toString('hex')}`; const stage = `${final}.stage-${process.pid}-${randomBytes(4).toString('hex')}`;
try { try {
const result = await runGeneration({ host, outputRoot: stage, contentLinkRoot: final, installRoot: request.fullRoot && request.fullRoot !== path.join(os.homedir(), '.claude/skills') ? path.join(request.fullRoot, 'gstack') : null, model, planMode, modules: selection }); const result = await runGeneration({ host, outputRoot: stage, contentLinkRoot: final, installRoot: request.fullRoot && request.fullRoot !== path.join(os.homedir(), '.claude/skills') ? path.join(request.fullRoot, 'gstack') : null, model, planMode, modules: selection, gbrainDetected, disabledSkills });
const errors = result.diagnostics.filter(d => d.kind === 'error').map(d => d.message); const errors = result.diagnostics.filter(d => d.kind === 'error').map(d => d.message);
if (result.exitCode !== 0 || errors.length) { 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'); 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');
+37 -3
View File
@@ -6,7 +6,7 @@
* from the printed SKILL_ROOT; concurrent publishers of one identity agree; * from the printed SKILL_ROOT; concurrent publishers of one identity agree;
* the committed checkout stays byte-identical. * 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 { spawnSync } from 'child_process';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
@@ -19,7 +19,14 @@ import { LOCK_FILE } from '../scripts/modules-render';
const ROOT = path.resolve(import.meta.dir, '..'); const ROOT = path.resolve(import.meta.dir, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'loadout-install-')); 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 FULL_ROOT = '/opt/claude/skills';
const lite = selectModules({ loadout: 'lite' })!; const lite = selectModules({ loadout: 'lite' })!;
const run = (...args: string[]) => { const run = (...args: string[]) => {
@@ -64,7 +71,8 @@ describe('gstack-modules install', () => {
expect(landing).toContain(`${FULL_ROOT}/gstack/bin/`); expect(landing).toContain(`${FULL_ROOT}/gstack/bin/`);
expect(landing).not.toContain('~/.claude/skills/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).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:upgrade-check');
expect(lock.excluded).not.toContain('preamble:brain-sync'); 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)); 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'); expect(explicit).not.toContain('--loadout');
}, 240_000); }, 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 () => { 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 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 }))); const [x, y] = await Promise.all([1, 2].map(() => installLoadout({ selection: lite, boundModel: 'claude-opus-5-5', fullRoot: FULL_ROOT, store })));
+3 -2
View File
@@ -48,8 +48,9 @@ describe('modules/loadouts.json', () => {
expect(Object.values(manifest.default)).toEqual(['full', 'full', 'full', 'full', 'full']); 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', () => { 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', () => {
const coaching = ['voice', 'writing-style', 'completeness', 'confusion', 'evidence', 'context-health', 'question-tuning', 'search-before-building', 'repo-mode', 'brain-health', 'context-recovery']; 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.loadouts.lite).toMatchObject({ base: 'full', exclude: coaching.map(id => `preamble:${id}`) });
expect(manifest.models).toEqual({ 'claude-opus-5-5': 'lite' }); expect(manifest.models).toEqual({ 'claude-opus-5-5': 'lite' });
const s = selectModules({ loadout: 'lite' })!; const s = selectModules({ loadout: 'lite' })!;