feat(skills): add per-step no-op execution envelopes
This commit is contained in:
+11
-2
@@ -1433,6 +1433,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_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": {
|
"overall_progress": {
|
||||||
"total_test_count": 1540,
|
"total_test_count": 1541,
|
||||||
"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%)",
|
||||||
@@ -1481,7 +1490,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: 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": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
+107
-4
@@ -19,6 +19,7 @@ import {
|
|||||||
renderSkillInstallerExecutionStub,
|
renderSkillInstallerExecutionStub,
|
||||||
toSkillInstallerExecutionStubFromPreflight,
|
toSkillInstallerExecutionStubFromPreflight,
|
||||||
evaluateInstallerExecutionPolicy,
|
evaluateInstallerExecutionPolicy,
|
||||||
|
toInstallerExecutionStepEnvelopes,
|
||||||
runInstallerCommandsWithPolicy,
|
runInstallerCommandsWithPolicy,
|
||||||
noOpSkillInstallerCommandRunner,
|
noOpSkillInstallerCommandRunner,
|
||||||
runSkillInstallAction,
|
runSkillInstallAction,
|
||||||
@@ -238,6 +239,17 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(view.execution_enabled).toBe(false);
|
expect(view.execution_enabled).toBe(false);
|
||||||
expect(view.executed).toEqual([]);
|
expect(view.executed).toEqual([]);
|
||||||
expect(view.reason).toBe('execution_disabled');
|
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.length).toBe(1);
|
||||||
expect(view.wouldRun[0]).toContain('download https://example.com/tool.tgz');
|
expect(view.wouldRun[0]).toContain('download https://example.com/tool.tgz');
|
||||||
});
|
});
|
||||||
@@ -251,6 +263,8 @@ describe('skills CLI helpers', () => {
|
|||||||
execution_enabled: false,
|
execution_enabled: false,
|
||||||
executed: [],
|
executed: [],
|
||||||
reason: 'execution_disabled',
|
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'],
|
wouldRun: ['brew install jq'],
|
||||||
skipped: [{ installerType: 'node', reason: 'neither pnpm nor npm available in PATH' }],
|
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.execution_enabled).toBe(false);
|
||||||
expect(view.executed).toEqual([]);
|
expect(view.executed).toEqual([]);
|
||||||
expect(view.reason).toBe('execution_disabled');
|
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']);
|
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', () => {
|
it('marks install execution policy as confirmation_required when not confirmed', () => {
|
||||||
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false });
|
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, 'SKILL.md'), '# Plan Skill\nInstructions');
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(sourceDir, 'manifest.json'),
|
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',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -416,7 +468,12 @@ describe('skills CLI helpers', () => {
|
|||||||
writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions');
|
writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions');
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(sourceDir, 'manifest.json'),
|
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',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -433,6 +490,20 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(payload.execution.execution_enabled).toBe(false);
|
expect(payload.execution.execution_enabled).toBe(false);
|
||||||
expect(payload.execution.executed).toEqual([]);
|
expect(payload.execution.executed).toEqual([]);
|
||||||
expect(payload.execution.reason).toBe('execution_disabled');
|
expect(payload.execution.reason).toBe('execution_disabled');
|
||||||
|
expect(payload.execution.attempted).toEqual([
|
||||||
|
{
|
||||||
|
installer_type: 'download',
|
||||||
|
command: 'download https://example.com/plan.tgz -> <default destination>',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(payload.execution.results).toEqual([
|
||||||
|
{
|
||||||
|
installer_type: 'download',
|
||||||
|
command: 'download https://example.com/plan.tgz -> <default destination>',
|
||||||
|
status: 'skipped',
|
||||||
|
reason: 'execution_disabled',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
rmSync(root, { recursive: true, force: true });
|
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, 'SKILL.md'), '# Install Skill\nInstructions');
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(sourceDir, 'manifest.json'),
|
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',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -463,6 +539,20 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(payload.execution.execution_enabled).toBe(false);
|
expect(payload.execution.execution_enabled).toBe(false);
|
||||||
expect(payload.execution.executed).toEqual([]);
|
expect(payload.execution.executed).toEqual([]);
|
||||||
expect(payload.execution.reason).toBe('confirmation_required');
|
expect(payload.execution.reason).toBe('confirmation_required');
|
||||||
|
expect(payload.execution.attempted).toEqual([
|
||||||
|
{
|
||||||
|
installer_type: 'download',
|
||||||
|
command: 'download https://example.com/install.tgz -> <default destination>',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(payload.execution.results).toEqual([
|
||||||
|
{
|
||||||
|
installer_type: 'download',
|
||||||
|
command: 'download https://example.com/install.tgz -> <default destination>',
|
||||||
|
status: 'blocked',
|
||||||
|
reason: 'confirmation_required',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
rmSync(root, { recursive: true, force: true });
|
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, 'SKILL.md'), '# Install Skill\nInstructions');
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(sourceDir, 'manifest.json'),
|
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',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -490,6 +585,14 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(runnerSpy).not.toHaveBeenCalled();
|
expect(runnerSpy).not.toHaveBeenCalled();
|
||||||
const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
|
const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
|
||||||
expect(payload.execution.executed).toEqual([]);
|
expect(payload.execution.executed).toEqual([]);
|
||||||
|
expect(payload.execution.results).toEqual([
|
||||||
|
{
|
||||||
|
installer_type: 'download',
|
||||||
|
command: 'download https://example.com/install-confirmed.tgz -> <default destination>',
|
||||||
|
status: 'skipped',
|
||||||
|
reason: 'execution_disabled',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
runnerSpy.mockRestore();
|
runnerSpy.mockRestore();
|
||||||
logSpy.mockRestore();
|
logSpy.mockRestore();
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export interface SkillInstallerExecutionStubView {
|
|||||||
execution_enabled: boolean;
|
execution_enabled: boolean;
|
||||||
executed: string[];
|
executed: string[];
|
||||||
reason: SkillInstallerExecutionReason;
|
reason: SkillInstallerExecutionReason;
|
||||||
|
attempted: Array<{ installer_type: string; command: string }>;
|
||||||
|
results: Array<{ installer_type: string; command: string; status: 'blocked' | 'skipped'; reason: SkillInstallerExecutionReason }>;
|
||||||
wouldRun: string[];
|
wouldRun: string[];
|
||||||
skipped: SkillInstallerPlanView['skipped'];
|
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[] {
|
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
||||||
return skills
|
return skills
|
||||||
.map((skill) => ({
|
.map((skill) => ({
|
||||||
@@ -230,6 +257,7 @@ export function renderSkillInstallPreflight(view: SkillInstallPreflightView): st
|
|||||||
export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerExecutionStubView {
|
export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerExecutionStubView {
|
||||||
const plan = toSkillInstallerPlanView(skill);
|
const plan = toSkillInstallerPlanView(skill);
|
||||||
const policy = evaluateInstallerExecutionPolicy({ mode: 'stub', confirmed: false });
|
const policy = evaluateInstallerExecutionPolicy({ mode: 'stub', confirmed: false });
|
||||||
|
const stepEnvelopes = toInstallerExecutionStepEnvelopes(plan.steps, policy);
|
||||||
return {
|
return {
|
||||||
skill: plan.skill,
|
skill: plan.skill,
|
||||||
execution: 'stub',
|
execution: 'stub',
|
||||||
@@ -238,6 +266,8 @@ export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerE
|
|||||||
execution_enabled: policy.execution_enabled,
|
execution_enabled: policy.execution_enabled,
|
||||||
executed: [],
|
executed: [],
|
||||||
reason: policy.reason,
|
reason: policy.reason,
|
||||||
|
attempted: stepEnvelopes.attempted,
|
||||||
|
results: stepEnvelopes.results,
|
||||||
wouldRun: plan.steps.map((step) => step.command),
|
wouldRun: plan.steps.map((step) => step.command),
|
||||||
skipped: plan.skipped,
|
skipped: plan.skipped,
|
||||||
};
|
};
|
||||||
@@ -249,6 +279,7 @@ export function toSkillInstallerExecutionStubFromPreflight(
|
|||||||
): SkillInstallerExecutionStubView {
|
): SkillInstallerExecutionStubView {
|
||||||
const mode = options?.mode ?? 'stub';
|
const mode = options?.mode ?? 'stub';
|
||||||
const policy = evaluateInstallerExecutionPolicy({ mode, confirmed: options?.confirmed ?? false });
|
const policy = evaluateInstallerExecutionPolicy({ mode, confirmed: options?.confirmed ?? false });
|
||||||
|
const stepEnvelopes = toInstallerExecutionStepEnvelopes(preflight.steps, policy);
|
||||||
return {
|
return {
|
||||||
skill: preflight.skill,
|
skill: preflight.skill,
|
||||||
execution: 'stub',
|
execution: 'stub',
|
||||||
@@ -257,6 +288,8 @@ export function toSkillInstallerExecutionStubFromPreflight(
|
|||||||
execution_enabled: policy.execution_enabled,
|
execution_enabled: policy.execution_enabled,
|
||||||
executed: [],
|
executed: [],
|
||||||
reason: policy.reason,
|
reason: policy.reason,
|
||||||
|
attempted: stepEnvelopes.attempted,
|
||||||
|
results: stepEnvelopes.results,
|
||||||
wouldRun: preflight.steps.map((step) => step.command),
|
wouldRun: preflight.steps.map((step) => step.command),
|
||||||
skipped: preflight.skipped,
|
skipped: preflight.skipped,
|
||||||
};
|
};
|
||||||
@@ -451,6 +484,8 @@ export function runSkillInstallAction(
|
|||||||
execution_enabled: installPolicy.execution_enabled,
|
execution_enabled: installPolicy.execution_enabled,
|
||||||
executed: runInstallerCommandsWithPolicy([], installPolicy, opts.commandRunner ?? noOpSkillInstallerCommandRunner),
|
executed: runInstallerCommandsWithPolicy([], installPolicy, opts.commandRunner ?? noOpSkillInstallerCommandRunner),
|
||||||
reason: installPolicy.reason,
|
reason: installPolicy.reason,
|
||||||
|
attempted: [],
|
||||||
|
results: [],
|
||||||
wouldRun: [],
|
wouldRun: [],
|
||||||
skipped: [],
|
skipped: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user