From 1996ac25a6ab2918fa84d14cb7d74084f61f32ba Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sun, 5 Jul 2026 01:04:57 +0300 Subject: [PATCH] fix(framework): default the CLI to bypassPermissions so the headless loop can build/verify Every framework turn is a headless `claude -p`, which can't answer an interactive approval. The driver's library default (acceptEdits) auto-approves edits but not Bash, so installs/builds/tests were silently denied: the production-grade checklist tried `npm run build` / dev-boot, hit 'Build needs interactive approval which isn't available', failed pass 1, and the loop ground on unverifiable blockers. The framework CLI now defaults its Claude Code driver to bypassPermissions so the full loop runs unattended and the checklist verifies for real. Overridable with --permission-mode (e.g. acceptEdits for the old behavior); --dangerously-skip-permissions still wins. The ClaudeCodeDriver library default is unchanged; only the CLI opts up. Resolution extracted to claudeDriverOptions() with unit coverage. Closes #225. --- .changeset/headless-permission-default.md | 23 +++++++++++++++++++++++ packages/framework/src/cli.test.ts | 14 ++++++++++++++ packages/framework/src/cli.ts | 23 ++++++++++++++++++----- 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 .changeset/headless-permission-default.md diff --git a/.changeset/headless-permission-default.md b/.changeset/headless-permission-default.md new file mode 100644 index 0000000..f94ee30 --- /dev/null +++ b/.changeset/headless-permission-default.md @@ -0,0 +1,23 @@ +--- +'@gemstack/framework': patch +--- + +Default the CLI to bypassPermissions so the headless loop can build/verify + +Every framework turn is a headless `claude -p` invocation, which can't answer an +interactive approval. The driver's library default (`acceptEdits`) auto-approves +edits but not Bash, so installs/builds/tests were silently denied: the +production-grade checklist tried `npm run build` / dev-boot, hit "Build needs +interactive approval which isn't available," failed pass 1 as "could not be +executed this session," and the loop ground on listing blockers it couldn't +verify. + +The `framework` CLI now defaults its Claude Code driver to `bypassPermissions` so +the full loop (install, build, test, dev-boot) runs unattended and the checklist +verifies for real. This is a permissive default appropriate to a headless +autonomous builder; `--permission-mode ` still overrides it (e.g. +`--permission-mode acceptEdits` for the old, conservative behavior), and +`--dangerously-skip-permissions` still takes precedence. The `ClaudeCodeDriver` +library default is unchanged (still `acceptEdits`); only the CLI opts up. + +Closes #225. diff --git a/packages/framework/src/cli.test.ts b/packages/framework/src/cli.test.ts index 36c66fb..5b2c7fc 100644 --- a/packages/framework/src/cli.test.ts +++ b/packages/framework/src/cli.test.ts @@ -3,6 +3,7 @@ import { test } from 'node:test' import { buildDeployTarget, chooseSessionLink, + claudeDriverOptions, CLAUDE_CODE_SESSION_LIST, parseArgs, runCli, @@ -35,6 +36,19 @@ test('parseArgs reads permission-mode and skip-permissions', () => { assert.equal(opts.skipPermissions, true) }) +test('claudeDriverOptions defaults the headless CLI to bypassPermissions (#225)', () => { + // Default run: acceptEdits would deny installs/builds/tests headlessly, so the CLI opts up. + assert.deepEqual(claudeDriverOptions({ skipPermissions: false }), { permissionMode: 'bypassPermissions' }) + // An explicit --permission-mode still wins. + assert.deepEqual(claudeDriverOptions({ permissionMode: 'acceptEdits', skipPermissions: false }), { + permissionMode: 'acceptEdits', + }) + // --dangerously-skip-permissions takes precedence over the mode. + assert.deepEqual(claudeDriverOptions({ permissionMode: 'plan', skipPermissions: true }), { + dangerouslySkipPermissions: true, + }) +}) + test('parseArgs persists by default and reads --resume / --no-persist (#211)', () => { const dflt = parseArgs(['x']) assert.equal(dflt.persist, true) diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index 581bd7e..35832e6 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -67,7 +67,8 @@ Options: auto-activate either way (default: off, hand-rolled + Prisma). --max-passes Full-fledged loop pass budget (default: 5). --permission-mode Claude Code permission mode: default | acceptEdits | - bypassPermissions | plan (default: acceptEdits). + bypassPermissions | plan (default: bypassPermissions, + so the headless loop can run installs/builds/tests). --dangerously-skip-permissions Bypass all agent permission checks (sandboxes only). --serve Gate the loop on the app actually running (e.g. "npm run dev"), then keep it serving with a preview link on the dashboard. @@ -261,6 +262,21 @@ export function parseArgs(argv: string[]): CliOptions { return opts } +/** + * Resolve the Claude Code driver options for a live CLI run. The CLI is a + * headless autonomous builder: every turn is `claude -p`, which cannot answer an + * interactive approval. The driver's library default (`acceptEdits`) silently + * denies installs/builds/tests, so the production-grade checklist can never + * verify the app actually builds/runs (#225). Default the CLI to + * `bypassPermissions` so the full loop runs unattended; `--permission-mode` and + * `--dangerously-skip-permissions` still override. + */ +export function claudeDriverOptions(opts: Pick): ClaudeCodeDriverOptions { + return opts.skipPermissions + ? { dangerouslySkipPermissions: true } + : { permissionMode: opts.permissionMode ?? 'bypassPermissions' } +} + /** * The `framework` command. Wires the parsed options into {@link runFramework} * over a live dashboard + terminal narration, and resolves with an exit code. @@ -310,10 +326,7 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise