feat: finalize configurable model tiers and verified release repairs

This commit is contained in:
garrytan committed 2026-10-08 04:39:33 +00:00
1 parent b4ba71b28d
commit f823ca5c49
13 files changed
+158 -60

No files matched your search

+11
View File
@@ -1,5 +1,16 @@
# Changelog
## [1.91.35.0] - 2026-10-07
### Added
- Choose separate model tiers for planning and implementation. Independent plan reviews default to Fable 5.1 or GPT-6 Astra; implementation handoffs recommend Opus 5.5 or GPT-6.1 Sol without switching your session. Explicit model choices still win, and `plan_review_tier smart` or `host` provides an alternative to the frontier default.
- Inspect effective models, their sources and copyable pin/reset commands with `gstack-models`. Six settings use the existing configuration store, with fail-closed validation and a notice before the first affected review. See the [model-policy guide](docs/model-policy.md).
- Get weekly advisory checks of official model recommendations and retirement notices. One tracking issue retains source evidence and lifecycle history; upgrades remain human-reviewed, with no automatic model changes or paid benchmarking.
### Fixed
- Codex model probes retain up to sixteen selections, serialize concurrent misses and preserve newer entries when another probe finishes later. Plan-review readiness and execution share one deadline, and failed model choices point to the setting that actually selected them.
- `/review` and `/ship` explicitly require prerequisite QA instructions to be read in earlier responses before a probe, including its evidence capture.
## [1.91.34.0] - 2026-10-07
**/autoplan works again on current Claude Code, including long sessions and `claude --bg`.**
+1 -1
View File
@@ -1 +1 @@
1.91.34.0
1.91.35.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.34.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.35.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,
+6 -5
View File
@@ -851,11 +851,12 @@ If token count is not available, display: `Tokens: unknown`
None of these is an auth or network failure, and the auth probe cannot catch them.
Recovery, in order:
1. Read the `CODEX_MODEL:` line: it names the model and where it came from.
2. Fix that source: name another model for this request, update
`GSTACK_CODEX_MODEL`, or change `model` (or `review_model`) in the Codex
`config.toml`. With none of these set, gstack uses `gpt-6-astra`; set any of
them to a model the account can use.
3. If Codex printed `[notice.model_migrations]`, use that replacement model.
2. Fix that source and relay its HINT/Repair lines. With `--role plan-review`,
tier pins outrank native settings; native `model`/`review_model` applies
only in `host` mode. Without a role, request, `GSTACK_CODEX_MODEL`, then
native settings choose the model, falling back to `gpt-6-astra`.
3. If Codex printed `[notice.model_migrations]`, use that replacement for
no-role calls. For a role, offer an explicit override; do not silently substitute.
Never present this as a model stall or a PASS — it is a fail-closed gate result.
- **`VERDICT: unavailable`:** the shared validator found the run did not execute (for
example `Codex's sandbox could not start here`). Relay its line and fix verbatim; it is
+6 -5
View File
@@ -317,11 +317,12 @@ If token count is not available, display: `Tokens: unknown`
None of these is an auth or network failure, and the auth probe cannot catch them.
Recovery, in order:
1. Read the `CODEX_MODEL:` line: it names the model and where it came from.
2. Fix that source: name another model for this request, update
`GSTACK_CODEX_MODEL`, or change `model` (or `review_model`) in the Codex
`config.toml`. With none of these set, gstack uses `gpt-6-astra`; set any of
them to a model the account can use.
3. If Codex printed `[notice.model_migrations]`, use that replacement model.
2. Fix that source and relay its HINT/Repair lines. With `--role plan-review`,
tier pins outrank native settings; native `model`/`review_model` applies
only in `host` mode. Without a role, request, `GSTACK_CODEX_MODEL`, then
native settings choose the model, falling back to `gpt-6-astra`.
3. If Codex printed `[notice.model_migrations]`, use that replacement for
no-role calls. For a role, offer an explicit override; do not silently substitute.
Never present this as a model stall or a PASS — it is a fail-closed gate result.
- **`VERDICT: unavailable`:** the shared validator found the run did not execute (for
example `Codex's sandbox could not start here`). Relay its line and fix verbatim; it is
+9 -5
View File
@@ -1148,10 +1148,10 @@ This is my **second opinion mode**.
`/codex` brings OpenAI Codex CLI to review the same diff independently. It is available on every harness except Codex itself. External harnesses install it as `/gstack-codex`. Compare its findings with the native review to distinguish corroborated findings from issues only one reviewer caught.
gstack-owned Codex calls default to `gpt-6-astra`, including resumed consult
sessions. Set `GSTACK_CODEX_MODEL=<model>` to change the default, or name a
model in your request to override it for that invocation. Generated commands
pass the selection through `-c model=...`, overriding the CLI's configured model.
Without a role, gstack-owned Codex calls select a request-specific model, then
`GSTACK_CODEX_MODEL`, then Codex native settings, falling back to `gpt-6-astra`.
This also applies to resumed consultations. Generated commands pass the selection
through `-c model=...`, overriding the CLI's configured model for that call.
Native review also sets `-c review_model=...` to that selection, overriding any
separate review-model pin.
@@ -1161,6 +1161,10 @@ review, challenge, and consult calls preserve Claude's configured model.
sessions; a model named in your request takes precedence. Harness routing is
independent of model selection.
An explicit `--role plan-review` uses the configured plan-review tier instead;
request and environment overrides still win, and `host` mode restores native
selection. See [model policy](model-policy.md) for pins, precedence and recovery.
### Three modes
**Review** — run `codex review` against the current diff. Codex reads every changed file, classifies findings by severity (P1 critical, P2 high, P3 medium), and returns a PASS/FAIL verdict. Any P1 finding = FAIL. The review is fully independent — Codex doesn't see Claude's review.
@@ -1200,7 +1204,7 @@ Claude Code provides the outside reviewer when gstack runs in Codex. Other non-C
**Review** supplies the branch diff for a read-only pass/fail review. **Challenge** asks Claude Code to find concrete failure cases in the same diff. **Consult** supports read-only repository exploration and resumes the session saved in `.context/claude-session-id`. Review and challenge receive context from the parent workflow and run without tools; consultation can read and search files.
The Claude Code CLI must be installed and authenticated. Its existing model configuration and `GSTACK_CLAUDE_BIN` / `GSTACK_CLAUDE_BIN_ARGS` executable overrides are honored. Errors, timeouts, and invalid responses report missing outside coverage instead of a clean review. Automatic reviews start fresh; consult session continuity is explicit.
The Claude Code CLI must be installed and authenticated. Without a role, its existing model configuration is honored; explicit `--role plan-review` follows the [model policy](model-policy.md). `GSTACK_CLAUDE_BIN` / `GSTACK_CLAUDE_BIN_ARGS` executable overrides remain in force. Errors, timeouts, and invalid responses report missing outside coverage instead of a clean review. Automatic reviews start fresh; consult session continuity is explicit.
Outside-review routing follows the harness, independently of the configured model. Generic second-opinion requests choose `/claude-code` on Codex and `/codex` elsewhere; explicit provider requests keep that provider. The existing `codex_reviews` setting controls the selected automatic reviewer in workflows that already use that setting. Existing opt-in and skip controls still apply in office hours, design, and spec workflows.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.34",
"version": "1.91.35",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+40 -40
View File
@@ -1,5 +1,5 @@
{
"version": 2,
"version": 3,
"patterns": [
"test/helpers/agent-sdk-runner.ts",
"test/helpers/codex-session-runner.ts",
@@ -10,44 +10,44 @@
"test/helpers/llm-judge.ts"
],
"files": {
"scripts/lib/paid-cases.ts": {"blob":"9bacd52446192a7ecf3dfaae901ea443cf9c9ae2","since":2},
"scripts/lib/paid-plan.ts": {"blob":"45721ce128a112666a0caed6c13c66637b74ca54","since":2},
"scripts/lib/paid-report.ts": {"blob":"3fda96a8b1f7f3d55797934847353f254cc4d054","since":2},
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":2},
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":2},
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":2},
"scripts/test-pr-profile.ts": {"blob":"88ecbce2b71c1e388c4dbca3f7013c71013bf8d3","since":2,"nonBehavioral":"test-pr-profile classifies the dispatch-only measure-journal-read workflow as a free-lane path; selection of paid cases and every verdict are unchanged"},
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":2},
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":2},
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":2},
"test/helpers/claude-pty-runner.ts": {"blob":"20cc5894532873aa6145fc6617fdeec8cef94a2d","since":2},
"test/helpers/codex-session-runner.ts": {"blob":"4eea35931a35a0afb006363402e3d4191e379737","since":2},
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":2},
"test/helpers/e2e-helpers.ts": {"blob":"2d4a2a53063cfc1f003db366d2c861ab1e379e6e","since":2},
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":2},
"test/helpers/eval-store.ts": {"blob":"609c6d099108ff2696eb014b47aa318a618dd2af","since":2},
"test/helpers/hermetic-env.ts": {"blob":"6473f78285384f4420f48cbe5928658919078e0b","since":2},
"test/helpers/llm-judge.ts": {"blob":"54e3306b494fb5d7f132ccdaaae5c680465028f9","since":2},
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":2},
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":2},
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":2},
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":2},
"test/helpers/pty/boundaries.ts": {"blob":"53eb76e57c0558c00254366d6334a1c833126575","since":2},
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":2},
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":2},
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":2},
"test/helpers/pty/launch.ts": {"blob":"4ef0bcb7b5e7c35d2aac0cbffd7f8a303440b6fe","since":2},
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":2},
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":2},
"test/helpers/pty/runners/floor.ts": {"blob":"465f01641fb3a18ae1c9d2613a9c2b26f8fb503e","since":2},
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":2},
"test/helpers/pty/screen.ts": {"blob":"ee005f36ffa48617e6832ccbeb35e8e96873df9b","since":2},
"test/helpers/pty/session.ts": {"blob":"58533cea9d81cc87bb3b499bdfe0db8e3a8709c5","since":2},
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":2},
"test/helpers/session-ledger.ts": {"blob":"6e3c9e837e46f98addba2d12eaebff802da320bb","since":2},
"test/helpers/session-runner.ts": {"blob":"d40eb29e8b8fdc0675b511693e4a90823b5963fc","since":2},
"test/helpers/skill-fixture.ts": {"blob":"b188064594eb4201a54a80ba1bc6f28e31d7f49e","since":2},
"test/helpers/test-selection.ts": {"blob":"51655f89f66be7a5ec487e2abfe4744300ebd5f6","since":2},
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":2}
"scripts/lib/paid-cases.ts": {"blob":"9bacd52446192a7ecf3dfaae901ea443cf9c9ae2","since":3},
"scripts/lib/paid-plan.ts": {"blob":"45721ce128a112666a0caed6c13c66637b74ca54","since":3},
"scripts/lib/paid-report.ts": {"blob":"3fda96a8b1f7f3d55797934847353f254cc4d054","since":3},
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":3},
"scripts/lib/shard-engine.ts": {"blob":"3cd0b1aaa84018b00bf9ea0ce48093f3c8492f66","since":3},
"scripts/test-paid-shards.ts": {"blob":"8ddee5f11ed7856876135520dd8025ab989e2db9","since":3},
"scripts/test-pr-profile.ts": {"blob":"54363c3d3aad9743b9e79219da7ec8a0f4ab0832","since":3},
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":3},
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":3},
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":3},
"test/helpers/claude-pty-runner.ts": {"blob":"20cc5894532873aa6145fc6617fdeec8cef94a2d","since":3},
"test/helpers/codex-session-runner.ts": {"blob":"4eea35931a35a0afb006363402e3d4191e379737","since":3},
"test/helpers/e2e-gate.ts": {"blob":"efb7e197dd5f66c3ad0e34074b14e640754f7a45","since":3},
"test/helpers/e2e-helpers.ts": {"blob":"2d4a2a53063cfc1f003db366d2c861ab1e379e6e","since":3},
"test/helpers/eval-budgets.ts": {"blob":"667d0e09b20fe5d7366f57b77b5fb9b04baa37ef","since":3},
"test/helpers/eval-store.ts": {"blob":"609c6d099108ff2696eb014b47aa318a618dd2af","since":3},
"test/helpers/hermetic-env.ts": {"blob":"6473f78285384f4420f48cbe5928658919078e0b","since":3},
"test/helpers/llm-judge.ts": {"blob":"54e3306b494fb5d7f132ccdaaae5c680465028f9","since":3},
"test/helpers/paid-test-set.ts": {"blob":"ff9bfe43c43ec2bdd11114f7b8ec7c5736116f75","since":3},
"test/helpers/pty-screen.ts": {"blob":"4f77569f9ce11f9cee9f347216e468cd7c956820","since":3},
"test/helpers/pty/auq.ts": {"blob":"16e5ea2bb5b9f28f44808a1a09024dbd3e6f28c4","since":3},
"test/helpers/pty/binary.ts": {"blob":"404d7515076d5c00804f89e23ea69296312c00e1","since":3},
"test/helpers/pty/boundaries.ts": {"blob":"53eb76e57c0558c00254366d6334a1c833126575","since":3},
"test/helpers/pty/classify.ts": {"blob":"3f2daa12ff6484f96b8d28d910a50231c4bb3ae8","since":3},
"test/helpers/pty/fake-session.ts": {"blob":"554e064ef259b8444280eddfe9b0db93c66a8202","since":3},
"test/helpers/pty/judge.ts": {"blob":"81c5bb4c48ab928aca2f186a1ade9df7169d7fbe","since":3},
"test/helpers/pty/launch.ts": {"blob":"ca874247a5b6bf6076d3cd34da01af9c8cc292b7","since":3},
"test/helpers/pty/plan-native.ts": {"blob":"ddba9cb68c7c0da935cff7c1cea258dee2dff740","since":3},
"test/helpers/pty/runners/counting.ts": {"blob":"8c5c05e54abf961259d34d9c3d77db67138d7794","since":3},
"test/helpers/pty/runners/floor.ts": {"blob":"465f01641fb3a18ae1c9d2613a9c2b26f8fb503e","since":3},
"test/helpers/pty/runners/observation.ts": {"blob":"1c2b75fbb984708725abaa8939125f7cab969f9f","since":3},
"test/helpers/pty/screen.ts": {"blob":"ee005f36ffa48617e6832ccbeb35e8e96873df9b","since":3},
"test/helpers/pty/session.ts": {"blob":"58533cea9d81cc87bb3b499bdfe0db8e3a8709c5","since":3},
"test/helpers/session-drain-policy.ts": {"blob":"6211239dd0485e6495a700b824a7e0e61d1bcd97","since":3},
"test/helpers/session-ledger.ts": {"blob":"6e3c9e837e46f98addba2d12eaebff802da320bb","since":3},
"test/helpers/session-runner.ts": {"blob":"d40eb29e8b8fdc0675b511693e4a90823b5963fc","since":3},
"test/helpers/skill-fixture.ts": {"blob":"b188064594eb4201a54a80ba1bc6f28e31d7f49e","since":3},
"test/helpers/test-selection.ts": {"blob":"51655f89f66be7a5ec487e2abfe4744300ebd5f6","since":3},
"test/helpers/touchfiles.ts": {"blob":"15f369056d41b9dc42dcd86f81f60c878074704c","since":3}
}
}
+1 -1
View File
@@ -947,7 +947,7 @@ describe('codex broken-install detection (#2742)', () => {
expect(src).toContain('echo "CODEX_MODE: $_CODEX_MODE"');
}
const role = fs.readFileSync(PROBE, 'utf-8').match(/_gstack_codex_role_ready\(\) \{[\s\S]*?\n\}/)![0];
expect(role.trim().split('\n').at(-2)!.trim()).toBe('_gstack_codex_model_probe "$1"');
expect(role).toContain('_gstack_codex_model_probe "$1" || return $?');
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-role-broken-'));
try {
const bin = path.join(home, 'bin');
+1
View File
@@ -493,6 +493,7 @@ async function closePty(pty: PtyProcess, wallTimer: ReturnType<typeof setTimeout
} finally {
clearTimeout(cleanupTimer);
clearTimeout(wallTimer);
pty.proc.terminal?.close?.();
}
}
+8
View File
@@ -552,6 +552,14 @@ describe('Claude plan-review invocation emits one selected model or none', () =>
describe('manual /codex entry: explicit role only, paid probe only for the dispatched model', () => {
const skill = fs.readFileSync(path.join(ROOT, 'codex', 'SKILL.md.tmpl'), 'utf8');
test('manual model recovery distinguishes role pins from native no-role settings', () => {
const recovery = skill.slice(skill.indexOf('Recovery, in order:'), skill.indexOf('- **`VERDICT: unavailable`:'));
expect(recovery).toContain('With `--role plan-review`');
expect(recovery).toContain('tier pins outrank native settings');
expect(recovery).toContain('only in `host` mode');
expect(recovery).toContain('Without a role');
expect(recovery).toContain('do not silently substitute');
});
const preflight = skill.match(/```bash\n(_CODEX_PROBE=[^\n]*\n_CODEX_ROLE=''[\s\S]*?)\n```/)![1]!
.replace('{{OUTSIDE_SELF_GUARD:codex}}', RESOLVERS.OUTSIDE_SELF_GUARD(ctxFor('codex', 'claude', HOST_PATHS.claude), ['codex']));
const withRole = (text: string, role: string) => text.replaceAll("_CODEX_ROLE=''", `_CODEX_ROLE='${role}'`);
+72
View File
@@ -4,6 +4,78 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { launchClaudePty, runPlanSkillCounting, type ClaudePtySession } from './helpers/claude-pty-runner';
test.each(['exited', 'running', 'unresponsive'])('closes the owned terminal once after %s child cleanup', async mode => {
const originalBinary = process.env.BROWSE_TERMINAL_BINARY;
const events: string[] = [];
let resolveExit!: (code: number) => void;
const exited = new Promise<number>(resolve => { resolveExit = resolve; });
if (mode === 'exited') resolveExit(0);
const spawn = Bun.spawn;
const spawnSpy = spyOn(Bun, 'spawn').mockImplementation(((...args: any[]) =>
args[1]?.env?.GSTACK_TEST_TERMINAL_CLEANUP === mode ? {
exited,
kill(signal: string) {
events.push(signal);
if (mode === 'running') resolveExit(0);
},
terminal: { write() {}, close() { events.push('close'); } },
} : (spawn as any)(...args)) as typeof Bun.spawn);
let session: ClaudePtySession | undefined;
try {
process.env.BROWSE_TERMINAL_BINARY = process.execPath;
session = await launchClaudePty({ timeoutMs: 200, sessionLedger: false,
env: { GSTACK_TEST_TERMINAL_CLEANUP: mode } });
await Promise.all([session.close(), session.close()]);
await session.close();
expect(events.filter(event => event === 'close')).toHaveLength(1);
expect(events.at(-1)).toBe('close');
if (mode === 'exited') expect(events).toEqual(['close']);
if (mode === 'running') expect(events).toEqual(['SIGINT', 'close']);
if (mode === 'unresponsive') expect(events).toContain('SIGKILL');
} finally {
try { await session?.close(); }
finally {
spawnSpy.mockRestore();
if (originalBinary === undefined) delete process.env.BROWSE_TERMINAL_BINARY;
else process.env.BROWSE_TERMINAL_BINARY = originalBinary;
}
}
});
test.skipIf(process.platform !== 'linux')('closes the owned terminal without retaining native PTY descriptors', async () => {
const descriptors = () => fs.readdirSync('/proc/self/fd').filter(fd => {
try { return /\/dev\/(?:pts\/)?ptmx$/.test(fs.readlinkSync(`/proc/self/fd/${fd}`)); }
catch { return false; }
}).sort();
const before = descriptors();
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-pty-close-'));
const fake = path.join(dir, 'fake-claude');
fs.writeFileSync(fake, `#!${process.execPath}
process.on('SIGINT', () => process.exit(0));
process.stdin.resume();
process.stdout.write('TERMINAL_READY');
`, { mode: 0o755 });
const originalBinary = process.env.BROWSE_TERMINAL_BINARY;
let session: ClaudePtySession | undefined;
try {
process.env.BROWSE_TERMINAL_BINARY = fake;
session = await launchClaudePty({ cwd: dir, model: 'fixture', timeoutMs: 5000, sessionLedger: false });
await session.waitFor('TERMINAL_READY', { timeoutMs: 3000 });
expect(descriptors().length).toBeGreaterThan(before.length);
await session.close();
expect(session.exited()).toBe(true);
expect(descriptors()).toEqual(before);
} finally {
try { await session?.close(); }
finally {
if (originalBinary === undefined) delete process.env.BROWSE_TERMINAL_BINARY;
else process.env.BROWSE_TERMINAL_BINARY = originalBinary;
fs.rmSync(dir, { recursive: true, force: true });
}
}
}, 10000);
test.skipIf(process.platform === 'win32')('PTY output and exit wake observers without leaving deadline timers', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-pty-output-'));
const fake = path.join(dir, 'fake-claude');
+1 -1
View File
@@ -30,7 +30,7 @@ function fixture(copy: boolean) {
put(path.join(root, 'VERSION'), '2.0.0.0\n');
put(path.join(root, 'ETHOS.md'), 'Fixture ethos\n');
put(path.join(root, 'package.json'), JSON.stringify({ type: 'module', scripts: { 'gen:skill-docs': 'bun run scripts/gen-skill-docs.ts' } }));
for (const bin of ['gstack-config', 'gstack-patch-names', 'gstack-relink', 'gstack-migrate-claude-code', 'gstack-claude-code', 'gstack-state-root.sh', 'gstack-bun-version.sh', 'gstack-install-registry.sh', 'gstack-render-claude.sh']) {
for (const bin of ['gstack-config', 'gstack-patch-names', 'gstack-relink', 'gstack-migrate-claude-code', 'gstack-claude-code', 'gstack-models', 'gstack-state-root.sh', 'gstack-bun-version.sh', 'gstack-install-registry.sh', 'gstack-render-claude.sh']) {
const target = path.join(root, 'bin', bin);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(path.join(ROOT, 'bin', bin), target);