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