Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/headless-permission-default.md
Original file line number Diff line number Diff line change
@@ -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 <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.
14 changes: 14 additions & 0 deletions packages/framework/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { test } from 'node:test'
import {
buildDeployTarget,
chooseSessionLink,
claudeDriverOptions,
CLAUDE_CODE_SESSION_LIST,
parseArgs,
runCli,
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 18 additions & 5 deletions packages/framework/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ Options:
auto-activate either way (default: off, hand-rolled + Prisma).
--max-passes <n> Full-fledged loop pass budget (default: 5).
--permission-mode <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 <cmd> Gate the loop on the app actually running (e.g. "npm run dev"),
then keep it serving with a preview link on the dashboard.
Expand Down Expand Up @@ -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<CliOptions, 'permissionMode' | 'skipPermissions'>): 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.
Expand Down Expand Up @@ -310,10 +326,7 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise<num
}
}

const claudeOpts: ClaudeCodeDriverOptions = {
...(opts.permissionMode ? { permissionMode: opts.permissionMode } : {}),
...(opts.skipPermissions ? { dangerouslySkipPermissions: true } : {}),
}
const claudeOpts = claudeDriverOptions(opts)
const driver: Driver = fake ? fakeDriver() : new ClaudeCodeDriver(claudeOpts)
const cwd = opts.cwd ?? (fake ? join(tmpdir(), 'framework-fake-workspace') : process.cwd())
// The fake demo defaults to a Cloudflare deploy decision so the flow ends with
Expand Down
Loading