feat: expand slash-command goals from command templates
Resolves armed slash-command objectives from authoritative templates before dispatch Applies OpenCode argument expansion for goal metadata in UI and scheduled tasks Falls back to the raw invocation when command details are unavailable
This commit is contained in:
@@ -94,6 +94,32 @@ export const parseScheduledCommandPrompt = (prompt) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const expandCommandGoalObjective = (template, argumentsText) => {
|
||||
if (typeof template !== 'string' || !template.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawArguments = String(argumentsText ?? '');
|
||||
if (template.includes('$ARGUMENTS')) {
|
||||
return template.replaceAll('$ARGUMENTS', rawArguments);
|
||||
}
|
||||
|
||||
const positions = [...template.matchAll(/\$(\d+)/g)].map((match) => Number(match[1]));
|
||||
if (positions.length > 0) {
|
||||
const parsedArguments = [...rawArguments.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)]
|
||||
.map((match) => match[1] ?? match[2] ?? match[3] ?? '');
|
||||
const lastPosition = Math.max(...positions);
|
||||
return template.replace(/\$(\d+)/g, (_match, value) => {
|
||||
const position = Number(value);
|
||||
return position === lastPosition
|
||||
? parsedArguments.slice(position - 1).join(' ')
|
||||
: (parsedArguments[position - 1] ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
return rawArguments ? `${template}\n\n${rawArguments}` : template;
|
||||
};
|
||||
|
||||
export const computeNextRunAt = (task, nowMs = Date.now()) => {
|
||||
if (!task?.enabled) {
|
||||
return null;
|
||||
@@ -445,10 +471,10 @@ export const createScheduledTasksRuntime = (deps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const runScheduledCommandIfApplicable = async ({ client, projectPath, sessionID, task }) => {
|
||||
const resolveScheduledCommand = async ({ client, projectPath, task }) => {
|
||||
const parsed = parseScheduledCommandPrompt(task?.execution?.prompt);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
let commands = [];
|
||||
@@ -456,25 +482,24 @@ export const createScheduledTasksRuntime = (deps) => {
|
||||
const response = await client.command.list({ directory: projectPath });
|
||||
commands = Array.isArray(response?.data) ? response.data : [];
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasMatchingCommand = commands.some((command) => command?.name === parsed.command);
|
||||
if (!hasMatchingCommand) {
|
||||
return false;
|
||||
}
|
||||
const command = commands.find((candidate) => candidate?.name === parsed.command);
|
||||
return command ? { ...parsed, template: command.template } : null;
|
||||
};
|
||||
|
||||
const runScheduledCommand = async ({ client, projectPath, sessionID, task, command }) => {
|
||||
await client.session.command({
|
||||
sessionID,
|
||||
directory: projectPath,
|
||||
command: parsed.command,
|
||||
arguments: parsed.arguments,
|
||||
command: command.command,
|
||||
arguments: command.arguments,
|
||||
...(task.execution.agent ? { agent: task.execution.agent } : {}),
|
||||
model: `${task.execution.providerID}/${task.execution.modelID}`,
|
||||
...(task.execution.variant ? { variant: task.execution.variant } : {}),
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const runTaskWithWatchdog = async (projectID, task, reason) => {
|
||||
@@ -527,13 +552,18 @@ export const createScheduledTasksRuntime = (deps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const scheduledCommand = await resolveScheduledCommand({ client, projectPath, task });
|
||||
|
||||
if (task.execution.goalEnabled) {
|
||||
const commandObjective = scheduledCommand
|
||||
? expandCommandGoalObjective(scheduledCommand.template, scheduledCommand.arguments)
|
||||
: null;
|
||||
await createSessionGoal({
|
||||
baseUrl,
|
||||
authHeaders,
|
||||
sessionID,
|
||||
directory: projectPath,
|
||||
objective: expandSnippets(task.execution.prompt, projectPath),
|
||||
objective: commandObjective ?? expandSnippets(task.execution.prompt, projectPath),
|
||||
tokenBudget: task.execution.goalTokenBudget,
|
||||
providerID: task.execution.providerID,
|
||||
modelID: task.execution.modelID,
|
||||
@@ -541,13 +571,9 @@ export const createScheduledTasksRuntime = (deps) => {
|
||||
});
|
||||
}
|
||||
|
||||
const executedAsCommand = await runScheduledCommandIfApplicable({
|
||||
client,
|
||||
projectPath,
|
||||
sessionID,
|
||||
task,
|
||||
});
|
||||
if (!executedAsCommand) {
|
||||
if (scheduledCommand) {
|
||||
await runScheduledCommand({ client, projectPath, sessionID, task, command: scheduledCommand });
|
||||
} else {
|
||||
await runPromptAsync({
|
||||
baseUrl,
|
||||
authHeaders,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { computeNextRunAt, formatScheduledSessionTitle, parseScheduledCommandPrompt } from './runtime.js';
|
||||
import { computeNextRunAt, expandCommandGoalObjective, formatScheduledSessionTitle, parseScheduledCommandPrompt } from './runtime.js';
|
||||
|
||||
describe('scheduled-tasks runtime helpers', () => {
|
||||
it('computes next daily run in timezone', () => {
|
||||
@@ -97,4 +97,15 @@ describe('scheduled-tasks runtime helpers', () => {
|
||||
expect(parseScheduledCommandPrompt('Summarize open issues')).toBeNull();
|
||||
expect(parseScheduledCommandPrompt('/')).toBeNull();
|
||||
});
|
||||
|
||||
it('expands command arguments into the goal objective', () => {
|
||||
expect(expandCommandGoalObjective(
|
||||
'Run the issue pipeline for $ARGUMENTS. Verify $ARGUMENTS is represented by the PR.',
|
||||
'LIN-123 --draft',
|
||||
)).toBe('Run the issue pipeline for LIN-123 --draft. Verify LIN-123 --draft is represented by the PR.');
|
||||
expect(expandCommandGoalObjective(undefined, 'LIN-123')).toBeNull();
|
||||
expect(expandCommandGoalObjective('Move $1 to $2', '"src old" dist extra')).toBe('Move src old to dist extra');
|
||||
expect(expandCommandGoalObjective('Review the requested scope.', 'auth module'))
|
||||
.toBe('Review the requested scope.\n\nauth module');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user