feat(skills): add pluggable no-op runner interface
This commit is contained in:
+11
-2
@@ -1424,6 +1424,15 @@
|
|||||||
"src/cli/skills.test.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"
|
"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_runner_interface_noop": {
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Added a pluggable installer command runner interface with policy-gated dispatch and a default no-op runner, preserving execution-disabled behavior while preparing a future real runner",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1452,7 +1461,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 1537,
|
"total_test_count": 1540,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (100%)",
|
"p1_completion": "4/4 (100%)",
|
||||||
@@ -1472,7 +1481,7 @@
|
|||||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
"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",
|
"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",
|
"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: introduce a pluggable installer command runner interface behind the existing execution policy gates (default no-op)"
|
"next_up": "Skills infrastructure Phase 3: add structured per-step execution result envelopes for the future real runner while keeping no-op default"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
renderSkillInstallerExecutionStub,
|
renderSkillInstallerExecutionStub,
|
||||||
toSkillInstallerExecutionStubFromPreflight,
|
toSkillInstallerExecutionStubFromPreflight,
|
||||||
evaluateInstallerExecutionPolicy,
|
evaluateInstallerExecutionPolicy,
|
||||||
|
runInstallerCommandsWithPolicy,
|
||||||
|
noOpSkillInstallerCommandRunner,
|
||||||
runSkillInstallAction,
|
runSkillInstallAction,
|
||||||
} from './skills.js';
|
} from './skills.js';
|
||||||
import type { Skill } from '../skills/index.js';
|
import type { Skill } from '../skills/index.js';
|
||||||
@@ -296,6 +298,36 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(policy.reason).toBe('execution_disabled');
|
expect(policy.reason).toBe('execution_disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not invoke command runner when policy disables execution', () => {
|
||||||
|
const runner = {
|
||||||
|
run: vi.fn((_commands: string[]) => ['should-not-run']),
|
||||||
|
};
|
||||||
|
|
||||||
|
const executed = runInstallerCommandsWithPolicy(
|
||||||
|
['brew install jq'],
|
||||||
|
{ confirmed: false, execution_enabled: false, reason: 'confirmation_required' },
|
||||||
|
runner,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(executed).toEqual([]);
|
||||||
|
expect(runner.run).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pluggable command runner when policy enables execution', () => {
|
||||||
|
const runner = {
|
||||||
|
run: vi.fn((commands: string[]) => commands),
|
||||||
|
};
|
||||||
|
|
||||||
|
const executed = runInstallerCommandsWithPolicy(
|
||||||
|
['brew install jq'],
|
||||||
|
{ confirmed: true, execution_enabled: true, reason: 'execution_disabled' },
|
||||||
|
runner,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(executed).toEqual(['brew install jq']);
|
||||||
|
expect(runner.run).toHaveBeenCalledWith(['brew install jq']);
|
||||||
|
});
|
||||||
|
|
||||||
it('summarizes refresh counts across status and tiers', () => {
|
it('summarizes refresh counts across status and tiers', () => {
|
||||||
const summary = summarizeSkillsRefresh([
|
const summary = summarizeSkillsRefresh([
|
||||||
buildSkill({ manifest: { name: 'a', description: 'a', version: '1.0.0', tier: 'bundled' } }),
|
buildSkill({ manifest: { name: 'a', description: 'a', version: '1.0.0', tier: 'bundled' } }),
|
||||||
@@ -436,6 +468,34 @@ describe('skills CLI helpers', () => {
|
|||||||
rmSync(root, { recursive: true, force: true });
|
rmSync(root, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses no-op command runner by default in install flow', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||||
|
const sourceDir = join(root, 'source-skill');
|
||||||
|
const managedDir = join(root, 'managed');
|
||||||
|
mkdirSync(sourceDir, { recursive: true });
|
||||||
|
writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
|
||||||
|
writeFileSync(
|
||||||
|
join(sourceDir, 'manifest.json'),
|
||||||
|
JSON.stringify({ name: 'install-skill', description: 'Install', version: '1.0.0' }),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const installer = new SkillInstaller(managedDir);
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
const runnerSpy = vi.spyOn(noOpSkillInstallerCommandRunner, 'run');
|
||||||
|
|
||||||
|
const result = runSkillInstallAction(installer, sourceDir, { mode: 'install', asJson: true, confirmed: true });
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(runnerSpy).not.toHaveBeenCalled();
|
||||||
|
const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
|
||||||
|
expect(payload.execution.executed).toEqual([]);
|
||||||
|
|
||||||
|
runnerSpy.mockRestore();
|
||||||
|
logSpy.mockRestore();
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
it('requires --yes confirmation for uninstall helper', () => {
|
it('requires --yes confirmation for uninstall helper', () => {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||||
const installer = new SkillInstaller(join(root, 'managed'));
|
const installer = new SkillInstaller(join(root, 'managed'));
|
||||||
|
|||||||
+35
-2
@@ -59,6 +59,16 @@ export interface SkillInstallerExecutionPolicy {
|
|||||||
reason: SkillInstallerExecutionReason;
|
reason: SkillInstallerExecutionReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkillInstallerCommandRunner {
|
||||||
|
run(commands: string[]): string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noOpSkillInstallerCommandRunner: SkillInstallerCommandRunner = {
|
||||||
|
run(_commands: string[]): string[] {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
||||||
return skills
|
return skills
|
||||||
.map((skill) => ({
|
.map((skill) => ({
|
||||||
@@ -271,6 +281,18 @@ export function evaluateInstallerExecutionPolicy(opts: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runInstallerCommandsWithPolicy(
|
||||||
|
commands: string[],
|
||||||
|
policy: SkillInstallerExecutionPolicy,
|
||||||
|
runner: SkillInstallerCommandRunner,
|
||||||
|
): string[] {
|
||||||
|
if (!policy.execution_enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return runner.run(commands);
|
||||||
|
}
|
||||||
|
|
||||||
export function renderSkillInstallerExecutionStub(view: SkillInstallerExecutionStubView): string {
|
export function renderSkillInstallerExecutionStub(view: SkillInstallerExecutionStubView): string {
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`Installer execution stub for '${view.skill.name}' (${view.skill.tier}, v${view.skill.version})`,
|
`Installer execution stub for '${view.skill.name}' (${view.skill.tier}, v${view.skill.version})`,
|
||||||
@@ -359,7 +381,12 @@ export function installSkillFromDirectory(installer: SkillInstaller, sourcePath:
|
|||||||
export function runSkillInstallAction(
|
export function runSkillInstallAction(
|
||||||
installer: SkillInstaller,
|
installer: SkillInstaller,
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
opts: { mode: SkillInstallActionMode; asJson: boolean; confirmed: boolean },
|
opts: {
|
||||||
|
mode: SkillInstallActionMode;
|
||||||
|
asJson: boolean;
|
||||||
|
confirmed: boolean;
|
||||||
|
commandRunner?: SkillInstallerCommandRunner;
|
||||||
|
},
|
||||||
): { ok: true } | { ok: false; error: string } {
|
): { ok: true } | { ok: false; error: string } {
|
||||||
const preflight = toSkillInstallPreflightView(sourcePath);
|
const preflight = toSkillInstallPreflightView(sourcePath);
|
||||||
|
|
||||||
@@ -422,12 +449,18 @@ export function runSkillInstallAction(
|
|||||||
mode: 'install' as const,
|
mode: 'install' as const,
|
||||||
confirmed: installPolicy.confirmed,
|
confirmed: installPolicy.confirmed,
|
||||||
execution_enabled: installPolicy.execution_enabled,
|
execution_enabled: installPolicy.execution_enabled,
|
||||||
executed: [],
|
executed: runInstallerCommandsWithPolicy([], installPolicy, opts.commandRunner ?? noOpSkillInstallerCommandRunner),
|
||||||
reason: installPolicy.reason,
|
reason: installPolicy.reason,
|
||||||
wouldRun: [],
|
wouldRun: [],
|
||||||
skipped: [],
|
skipped: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
execution.executed = runInstallerCommandsWithPolicy(
|
||||||
|
execution.wouldRun,
|
||||||
|
installPolicy,
|
||||||
|
opts.commandRunner ?? noOpSkillInstallerCommandRunner,
|
||||||
|
);
|
||||||
|
|
||||||
if (opts.asJson) {
|
if (opts.asJson) {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
|
|||||||
Reference in New Issue
Block a user