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:
Bohdan Triapitsyn
2026-07-29 00:47:29 +03:00
parent 7331ee626e
commit b0028283fb
8 changed files with 221 additions and 65 deletions
@@ -2,7 +2,7 @@ import express from 'express';
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
import { createWorktree } from '../git/index.js';
import { expandSnippets } from '../opencode/snippets.js';
import { parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js';
import { expandCommandGoalObjective, parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js';
import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js';
import { OpenChamberControlError, asControlError } from '../openchamber-control/error.js';
@@ -369,13 +369,27 @@ export const createOpenChamberSessionService = (dependencies) => {
}
const expandedPrompt = expandSnippets(prompt, directory);
const parsedCommand = parseScheduledCommandPrompt(prompt);
let resolvedCommand = null;
if (parsedCommand) {
try {
const response = await client.command.list({ directory });
const commands = Array.isArray(response?.data) ? response.data : [];
const command = commands.find((candidate) => candidate?.name === parsedCommand.command);
if (command) resolvedCommand = { ...parsedCommand, template: command.template };
} catch {
}
}
if (goalInput.enabled) {
const commandObjective = resolvedCommand
? expandCommandGoalObjective(resolvedCommand.template, resolvedCommand.arguments)
: null;
await (createSessionGoalOverride || createSessionGoal)({
baseUrl,
authHeaders,
sessionID,
directory,
objective: expandedPrompt,
objective: commandObjective ?? expandedPrompt,
tokenBudget: goalInput.tokenBudget,
providerID: model.providerID,
modelID: model.modelID,
@@ -388,35 +402,21 @@ export const createOpenChamberSessionService = (dependencies) => {
return error;
};
let dispatchedAsCommand = false;
const parsedCommand = parseScheduledCommandPrompt(prompt);
if (parsedCommand) {
let commandExists = false;
if (resolvedCommand) {
try {
const response = await client.command.list({ directory });
const commands = Array.isArray(response?.data) ? response.data : [];
commandExists = commands.some((command) => command?.name === parsedCommand.command);
} catch {
await client.session.command({
sessionID,
directory,
command: resolvedCommand.command,
arguments: resolvedCommand.arguments,
...(agent ? { agent } : {}),
model: `${model.providerID}/${model.modelID}`,
...(variant ? { variant } : {}),
});
} catch (error) {
throw markGoalPartial(error);
}
if (commandExists) {
try {
await client.session.command({
sessionID,
directory,
command: parsedCommand.command,
arguments: parsedCommand.arguments,
...(agent ? { agent } : {}),
model: `${model.providerID}/${model.modelID}`,
...(variant ? { variant } : {}),
});
} catch (error) {
throw markGoalPartial(error);
}
dispatchedAsCommand = true;
}
}
if (!dispatchedAsCommand) {
} else {
try {
await runPromptAsync({
baseUrl,
@@ -440,7 +440,7 @@ export const createOpenChamberSessionService = (dependencies) => {
}
}
return { model, agent, variant, promptDispatched: true, dispatchedAsCommand };
return { model, agent, variant, promptDispatched: true, dispatchedAsCommand: Boolean(resolvedCommand) };
};
const create = async (payload = {}) => {
@@ -361,6 +361,44 @@ describe('openchamber session routes', () => {
}
});
it('uses the expanded slash-command template as the goal objective before command dispatch', async () => {
const originalFetch = globalThis.fetch;
const createSessionGoal = vi.fn(async () => undefined);
commandListMock.mockResolvedValue({
data: [{
name: 'issue--to-pr',
template: 'Take $ARGUMENTS from issue through a verified pull request. Confirm the PR covers $ARGUMENTS.',
}],
});
globalThis.fetch = vi.fn();
try {
const { app } = createApp({ createSessionGoal });
const response = await request(app)
.post('/api/openchamber/sessions/ses_source/send')
.send({
directory: '/repo/app',
prompt: '/issue--to-pr LIN-123',
model: 'openai/gpt-5.5',
agent: 'build',
goal: true,
})
.expect(200);
expect(createSessionGoal).toHaveBeenCalledWith(expect.objectContaining({
objective: 'Take LIN-123 from issue through a verified pull request. Confirm the PR covers LIN-123.',
}));
expect(sessionCommandMock).toHaveBeenCalledWith(expect.objectContaining({
command: 'issue--to-pr',
arguments: 'LIN-123',
}));
expect(createSessionGoal.mock.invocationCallOrder[0]).toBeLessThan(sessionCommandMock.mock.invocationCallOrder[0]);
expect(response.body).toMatchObject({ goalEnabled: true, dispatchedAsCommand: true });
expect(globalThis.fetch).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
it('reuses the previous session selection when send omits model, agent, and variant', async () => {
const originalFetch = globalThis.fetch;
const fetchMock = vi.fn(async () => ({ ok: true, text: async () => '' }));