From 5e5d96523ed694735522514e4ca1f70a8ca650f6 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 12 Feb 2026 19:03:27 -0800 Subject: [PATCH] feat(skills): add per-step no-op execution envelopes --- docs/plans/state.json | 13 ++++- src/cli/skills.test.ts | 111 +++++++++++++++++++++++++++++++++++++++-- src/cli/skills.ts | 35 +++++++++++++ 3 files changed, 153 insertions(+), 6 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 4ab7bb8..8ea4002 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1433,6 +1433,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" + }, + "installer_step_execution_envelopes_noop": { + "status": "completed", + "description": "Added structured per-step execution envelopes (`attempted` + `results`) to install/plan/stub JSON receipts with policy-derived blocked/skipped statuses while keeping command execution disabled", + "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" } } } @@ -1461,7 +1470,7 @@ }, "overall_progress": { - "total_test_count": 1540, + "total_test_count": 1541, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -1481,7 +1490,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 structured per-step execution result envelopes for the future real runner while keeping no-op default" + "next_up": "Skills infrastructure Phase 3: define real-runner-compatible per-step terminal statuses and map runner return values into structured execution envelopes (execution still policy-gated by default)" }, "soul_md_and_cron_create": { "date": "2026-02-11", diff --git a/src/cli/skills.test.ts b/src/cli/skills.test.ts index 6622eb1..78f6d74 100644 --- a/src/cli/skills.test.ts +++ b/src/cli/skills.test.ts @@ -19,6 +19,7 @@ import { renderSkillInstallerExecutionStub, toSkillInstallerExecutionStubFromPreflight, evaluateInstallerExecutionPolicy, + toInstallerExecutionStepEnvelopes, runInstallerCommandsWithPolicy, noOpSkillInstallerCommandRunner, runSkillInstallAction, @@ -238,6 +239,17 @@ describe('skills CLI helpers', () => { expect(view.execution_enabled).toBe(false); expect(view.executed).toEqual([]); expect(view.reason).toBe('execution_disabled'); + expect(view.attempted.length).toBe(1); + expect(view.attempted[0]).toEqual({ + installer_type: 'download', + command: expect.stringContaining('download https://example.com/tool.tgz'), + }); + expect(view.results[0]).toEqual({ + installer_type: 'download', + command: expect.stringContaining('download https://example.com/tool.tgz'), + status: 'skipped', + reason: 'execution_disabled', + }); expect(view.wouldRun.length).toBe(1); expect(view.wouldRun[0]).toContain('download https://example.com/tool.tgz'); }); @@ -251,6 +263,8 @@ describe('skills CLI helpers', () => { execution_enabled: false, executed: [], reason: 'execution_disabled', + attempted: [{ installer_type: 'brew', command: 'brew install jq' }], + results: [{ installer_type: 'brew', command: 'brew install jq', status: 'skipped', reason: 'execution_disabled' }], wouldRun: ['brew install jq'], skipped: [{ installerType: 'node', reason: 'neither pnpm nor npm available in PATH' }], }); @@ -279,9 +293,42 @@ describe('skills CLI helpers', () => { expect(view.execution_enabled).toBe(false); expect(view.executed).toEqual([]); expect(view.reason).toBe('execution_disabled'); + expect(view.attempted).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/a.tgz -> /tmp/a.tgz', + }, + ]); + expect(view.results).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/a.tgz -> /tmp/a.tgz', + status: 'skipped', + reason: 'execution_disabled', + }, + ]); expect(view.wouldRun).toEqual(['download https://example.com/a.tgz -> /tmp/a.tgz']); }); + it('builds blocked step envelopes when confirmation is required', () => { + const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false }); + + const envelopes = toInstallerExecutionStepEnvelopes( + [{ installerType: 'brew', command: 'brew install jq' }], + policy, + ); + + expect(envelopes.attempted).toEqual([{ installer_type: 'brew', command: 'brew install jq' }]); + expect(envelopes.results).toEqual([ + { + installer_type: 'brew', + command: 'brew install jq', + status: 'blocked', + reason: 'confirmation_required', + }, + ]); + }); + it('marks install execution policy as confirmation_required when not confirmed', () => { const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false }); @@ -395,7 +442,12 @@ describe('skills CLI helpers', () => { writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions'); writeFileSync( join(sourceDir, 'manifest.json'), - JSON.stringify({ name: 'plan-skill', description: 'Plan only', version: '1.0.0' }), + JSON.stringify({ + name: 'plan-skill', + description: 'Plan only', + version: '1.0.0', + installers: [{ type: 'download', url: 'https://example.com/plan.tgz' }], + }), 'utf-8', ); @@ -416,7 +468,12 @@ describe('skills CLI helpers', () => { writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions'); writeFileSync( join(sourceDir, 'manifest.json'), - JSON.stringify({ name: 'plan-skill', description: 'Plan only', version: '1.0.0' }), + JSON.stringify({ + name: 'plan-skill', + description: 'Plan only', + version: '1.0.0', + installers: [{ type: 'download', url: 'https://example.com/plan.tgz' }], + }), 'utf-8', ); @@ -433,6 +490,20 @@ describe('skills CLI helpers', () => { expect(payload.execution.execution_enabled).toBe(false); expect(payload.execution.executed).toEqual([]); expect(payload.execution.reason).toBe('execution_disabled'); + expect(payload.execution.attempted).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/plan.tgz -> ', + }, + ]); + expect(payload.execution.results).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/plan.tgz -> ', + status: 'skipped', + reason: 'execution_disabled', + }, + ]); logSpy.mockRestore(); rmSync(root, { recursive: true, force: true }); @@ -446,7 +517,12 @@ describe('skills CLI helpers', () => { writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions'); writeFileSync( join(sourceDir, 'manifest.json'), - JSON.stringify({ name: 'install-skill', description: 'Install', version: '1.0.0' }), + JSON.stringify({ + name: 'install-skill', + description: 'Install', + version: '1.0.0', + installers: [{ type: 'download', url: 'https://example.com/install.tgz' }], + }), 'utf-8', ); @@ -463,6 +539,20 @@ describe('skills CLI helpers', () => { expect(payload.execution.execution_enabled).toBe(false); expect(payload.execution.executed).toEqual([]); expect(payload.execution.reason).toBe('confirmation_required'); + expect(payload.execution.attempted).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/install.tgz -> ', + }, + ]); + expect(payload.execution.results).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/install.tgz -> ', + status: 'blocked', + reason: 'confirmation_required', + }, + ]); logSpy.mockRestore(); rmSync(root, { recursive: true, force: true }); @@ -476,7 +566,12 @@ describe('skills CLI helpers', () => { writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions'); writeFileSync( join(sourceDir, 'manifest.json'), - JSON.stringify({ name: 'install-skill', description: 'Install', version: '1.0.0' }), + JSON.stringify({ + name: 'install-skill', + description: 'Install', + version: '1.0.0', + installers: [{ type: 'download', url: 'https://example.com/install-confirmed.tgz' }], + }), 'utf-8', ); @@ -490,6 +585,14 @@ describe('skills CLI helpers', () => { expect(runnerSpy).not.toHaveBeenCalled(); const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0])); expect(payload.execution.executed).toEqual([]); + expect(payload.execution.results).toEqual([ + { + installer_type: 'download', + command: 'download https://example.com/install-confirmed.tgz -> ', + status: 'skipped', + reason: 'execution_disabled', + }, + ]); runnerSpy.mockRestore(); logSpy.mockRestore(); diff --git a/src/cli/skills.ts b/src/cli/skills.ts index ab48dce..d2ec6c8 100644 --- a/src/cli/skills.ts +++ b/src/cli/skills.ts @@ -46,6 +46,8 @@ export interface SkillInstallerExecutionStubView { execution_enabled: boolean; executed: string[]; reason: SkillInstallerExecutionReason; + attempted: Array<{ installer_type: string; command: string }>; + results: Array<{ installer_type: string; command: string; status: 'blocked' | 'skipped'; reason: SkillInstallerExecutionReason }>; wouldRun: string[]; skipped: SkillInstallerPlanView['skipped']; } @@ -69,6 +71,31 @@ export const noOpSkillInstallerCommandRunner: SkillInstallerCommandRunner = { }, }; +export function toInstallerExecutionStepEnvelopes( + steps: Array<{ installerType: string; command: string }>, + policy: SkillInstallerExecutionPolicy, +): { + attempted: SkillInstallerExecutionStubView['attempted']; + results: SkillInstallerExecutionStubView['results']; +} { + const attempted = steps.map((step) => ({ + installer_type: step.installerType, + command: step.command, + })); + + const status: SkillInstallerExecutionStubView['results'][number]['status'] = + policy.reason === 'confirmation_required' ? 'blocked' : 'skipped'; + + const results = attempted.map((step) => ({ + installer_type: step.installer_type, + command: step.command, + status, + reason: policy.reason, + })); + + return { attempted, results }; +} + export function toSkillListRows(skills: Skill[]): SkillListRow[] { return skills .map((skill) => ({ @@ -230,6 +257,7 @@ export function renderSkillInstallPreflight(view: SkillInstallPreflightView): st export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerExecutionStubView { const plan = toSkillInstallerPlanView(skill); const policy = evaluateInstallerExecutionPolicy({ mode: 'stub', confirmed: false }); + const stepEnvelopes = toInstallerExecutionStepEnvelopes(plan.steps, policy); return { skill: plan.skill, execution: 'stub', @@ -238,6 +266,8 @@ export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerE execution_enabled: policy.execution_enabled, executed: [], reason: policy.reason, + attempted: stepEnvelopes.attempted, + results: stepEnvelopes.results, wouldRun: plan.steps.map((step) => step.command), skipped: plan.skipped, }; @@ -249,6 +279,7 @@ export function toSkillInstallerExecutionStubFromPreflight( ): SkillInstallerExecutionStubView { const mode = options?.mode ?? 'stub'; const policy = evaluateInstallerExecutionPolicy({ mode, confirmed: options?.confirmed ?? false }); + const stepEnvelopes = toInstallerExecutionStepEnvelopes(preflight.steps, policy); return { skill: preflight.skill, execution: 'stub', @@ -257,6 +288,8 @@ export function toSkillInstallerExecutionStubFromPreflight( execution_enabled: policy.execution_enabled, executed: [], reason: policy.reason, + attempted: stepEnvelopes.attempted, + results: stepEnvelopes.results, wouldRun: preflight.steps.map((step) => step.command), skipped: preflight.skipped, }; @@ -451,6 +484,8 @@ export function runSkillInstallAction( execution_enabled: installPolicy.execution_enabled, executed: runInstallerCommandsWithPolicy([], installPolicy, opts.commandRunner ?? noOpSkillInstallerCommandRunner), reason: installPolicy.reason, + attempted: [], + results: [], wouldRun: [], skipped: [], };