feat: add session-scoped workflow approval gate commands

This commit is contained in:
William Valentin
2026-02-18 10:35:42 -08:00
parent 1508bcf9cb
commit f34a974210
13 changed files with 369 additions and 6 deletions
+45 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { createContextCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createTransferCommand } from './index.js';
import { createApproveCommand, createApprovalsCommand, createContextCommand, createDenyCommand, createElevateCommand, createModelCommand, createQueueCommand, createResearchCommand, createTransferCommand } from './index.js';
describe('builtin /model command', () => {
it('passes through the full argument string', async () => {
@@ -197,3 +197,47 @@ describe('builtin /transfer command', () => {
expect(result).toEqual({ handled: true, text: 'Transfer command is not available in this session.' });
});
});
describe('builtin approval commands', () => {
it('calls getApprovals for /approvals', async () => {
const cmd = createApprovalsCommand();
const getApprovals = vi.fn(() => '1 pending');
const result = await cmd.execute([], {
channel: 'test',
senderId: 'user',
sessionId: 's1',
rawInput: '/approvals',
services: { getApprovals },
});
expect(getApprovals).toHaveBeenCalledOnce();
expect(result).toEqual({ handled: true, text: '1 pending' });
});
it('passes raw input to approvePending for /approve', async () => {
const cmd = createApproveCommand();
const approvePending = vi.fn(() => 'approved');
const result = await cmd.execute(['abc-123'], {
channel: 'test',
senderId: 'user',
sessionId: 's1',
rawInput: '/approve abc-123',
services: { approvePending },
});
expect(approvePending).toHaveBeenCalledWith('abc-123');
expect(result).toEqual({ handled: true, text: 'approved' });
});
it('passes raw input to denyPending for /deny', async () => {
const cmd = createDenyCommand();
const denyPending = vi.fn(() => 'denied');
const result = await cmd.execute(['abc-123', 'too', 'risky'], {
channel: 'test',
senderId: 'user',
sessionId: 's1',
rawInput: '/deny abc-123 too risky',
services: { denyPending },
});
expect(denyPending).toHaveBeenCalledWith('abc-123 too risky');
expect(result).toEqual({ handled: true, text: 'denied' });
});
});
+51
View File
@@ -236,6 +236,54 @@ export function createTransferCommand(): CommandDefinition {
};
}
export function createApprovalsCommand(): CommandDefinition {
return {
name: 'approvals',
description: 'Show pending approval gates for this session',
execute: async (_args, ctx) => {
if (!ctx.services?.getApprovals) {
return notAvailable('Approvals command');
}
return {
handled: true,
text: await ctx.services.getApprovals(),
};
},
};
}
export function createApproveCommand(): CommandDefinition {
return {
name: 'approve',
description: 'Approve a pending gate (latest by default, or by id)',
execute: async (args, ctx) => {
if (!ctx.services?.approvePending) {
return notAvailable('Approve command');
}
return {
handled: true,
text: await ctx.services.approvePending(args.join(' ').trim()),
};
},
};
}
export function createDenyCommand(): CommandDefinition {
return {
name: 'deny',
description: 'Deny a pending gate (latest by default, optional id and reason)',
execute: async (args, ctx) => {
if (!ctx.services?.denyPending) {
return notAvailable('Deny command');
}
return {
handled: true,
text: await ctx.services.denyPending(args.join(' ').trim()),
};
},
};
}
export function registerBuiltinCommands(registry: CommandRegistry): void {
registry.register(createHelpCommand(registry));
registry.register(createStatusCommand());
@@ -248,4 +296,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
registry.register(createElevateCommand());
registry.register(createQueueCommand());
registry.register(createTransferCommand());
registry.register(createApprovalsCommand());
registry.register(createApproveCommand());
registry.register(createDenyCommand());
}
+3
View File
@@ -10,5 +10,8 @@ export {
createResetCommand,
createQueueCommand,
createTransferCommand,
createApprovalsCommand,
createApproveCommand,
createDenyCommand,
registerBuiltinCommands,
} from './builtin/index.js';
+3
View File
@@ -35,4 +35,7 @@ export interface CommandServices {
setQueue?: (input: string) => Promise<string> | string;
resetQueue?: () => Promise<string> | string;
transferSession?: (target: string) => Promise<string> | string;
getApprovals?: () => Promise<string> | string;
approvePending?: (input: string) => Promise<string> | string;
denyPending?: (input: string) => Promise<string> | string;
}