diff --git a/docs/plans/state.json b/docs/plans/state.json index ddb9d7e..4a62337 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1486,6 +1486,15 @@ "src/cli/skills.test.ts" ], "test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing" + }, + "skills_execute_without_confirm_guardrails": { + "status": "completed", + "description": "Added explicit user-facing CLI guardrails for invalid execution opt-in (`--execute` without `--confirm`) on both install and execute commands, with deterministic error messaging and exit status coverage", + "files_modified": [ + "src/cli/skills.ts", + "src/cli/skills.test.ts" + ], + "test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing" } } } @@ -1514,7 +1523,7 @@ }, "overall_progress": { - "total_test_count": 1553, + "total_test_count": 1555, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -1534,7 +1543,7 @@ "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", "native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback", "remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 2/2 (100%) — component registry, confidence routing. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening", - "next_up": "Skills infrastructure Phase 3: add explicit user-facing error messaging/tests for invalid `--execute` usage patterns (for example, `--execute` without `--confirm`) and runner/result edge-case receipts" + "next_up": "Skills infrastructure Phase 3: add edge-case receipt tests for partial/missing runner results and deterministic fallback reasons across install/execute flows" }, "soul_md_and_cron_create": { "date": "2026-02-11", diff --git a/src/cli/skills.test.ts b/src/cli/skills.test.ts index 0f131da..6a9b92d 100644 --- a/src/cli/skills.test.ts +++ b/src/cli/skills.test.ts @@ -961,6 +961,33 @@ describe('skills CLI helpers', () => { rmSync(root, { recursive: true, force: true }); }); + it('skills install rejects --execute without --confirm', async () => { + const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); + const configPath = join(root, 'config.yaml'); + const managedDir = join(root, 'managed'); + const bundledDir = join(root, 'bundled'); + const workspaceDir = join(root, 'workspace'); + mkdirSync(managedDir, { recursive: true }); + mkdirSync(bundledDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir }); + + const program = new Command(); + registerSkillsCommand(program); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + process.exitCode = undefined; + + await program.parseAsync(['skills', 'install', '/tmp/any-skill', '--execute', '-c', configPath], { from: 'user' }); + + expect(errorSpy).toHaveBeenCalledWith('`--execute` requires `--confirm`. No installer commands were run.'); + expect(process.exitCode).toBe(1); + + errorSpy.mockRestore(); + process.exitCode = undefined; + rmSync(root, { recursive: true, force: true }); + }); + it('skills execute parses execute flags and emits execution-enabled JSON receipt', async () => { const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); const configPath = join(root, 'config.yaml'); @@ -1003,4 +1030,42 @@ describe('skills CLI helpers', () => { process.exitCode = undefined; rmSync(root, { recursive: true, force: true }); }); + + it('skills execute rejects --execute without --confirm', async () => { + const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); + const configPath = join(root, 'config.yaml'); + const managedDir = join(root, 'managed'); + const bundledDir = join(root, 'bundled'); + const workspaceDir = join(root, 'workspace'); + const skillDir = join(managedDir, 'cli-exec-skill'); + mkdirSync(skillDir, { recursive: true }); + mkdirSync(bundledDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions'); + writeFileSync( + join(skillDir, 'manifest.json'), + JSON.stringify({ + name: 'cli-exec-skill', + description: 'CLI execute parse', + version: '1.0.0', + }), + 'utf-8', + ); + + const program = new Command(); + registerSkillsCommand(program); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + process.exitCode = undefined; + + await program.parseAsync(['skills', 'execute', 'cli-exec-skill', '--execute', '-c', configPath], { from: 'user' }); + + expect(errorSpy).toHaveBeenCalledWith('`--execute` requires `--confirm`. No installer commands were run.'); + expect(process.exitCode).toBe(1); + + errorSpy.mockRestore(); + process.exitCode = undefined; + rmSync(root, { recursive: true, force: true }); + }); }); diff --git a/src/cli/skills.ts b/src/cli/skills.ts index 06ab014..53a5df3 100644 --- a/src/cli/skills.ts +++ b/src/cli/skills.ts @@ -815,6 +815,12 @@ export function registerSkillsCommand(program: Command): void { return; } + if ((opts.execute ?? false) && !(opts.confirm ?? false)) { + console.error('`--execute` requires `--confirm`. No installer commands were run.'); + process.exitCode = 1; + return; + } + const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills'); const installer = new SkillInstaller(loaded.config.skills.managed_dir ?? defaultManagedDir); @@ -952,6 +958,12 @@ export function registerSkillsCommand(program: Command): void { return; } + if ((opts.execute ?? false) && !(opts.confirm ?? false)) { + console.error('`--execute` requires `--confirm`. No installer commands were run.'); + process.exitCode = 1; + return; + } + runSkillExecuteAction(skill, { asJson: opts.json ?? false, confirmed: opts.confirm ?? false,