diff --git a/src/commands.ts b/src/commands.ts index 590b8e3..f2cbd9a 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -39,19 +39,18 @@ export function parseGoalCommand(commandName: string, defaultCommandName: string const tokensResult = splitCommandLine(rawArguments); if (tokensResult.ok === false) return tokensResult; const tokens = tokensResult.value; - const normalizedCommandName = normalizeGoalCommandName(commandName, defaultCommandName); - if (normalizedCommandName === `${defaultCommandName}-status`) return okCommand("status"); - if (normalizedCommandName === `${defaultCommandName}-list`) return okCommand("list"); - if (normalizedCommandName === `${defaultCommandName}-pause`) return okCommand("pause", { reason: rawArguments }); - if (normalizedCommandName === `${defaultCommandName}-resume`) return okCommand("resume"); - if (normalizedCommandName === `${defaultCommandName}-clear`) return okCommand("clear"); - if (normalizedCommandName === `${defaultCommandName}-abort`) return okCommand("abort", { reason: rawArguments }); - if (normalizedCommandName === `${defaultCommandName}-focus`) return okCommand("focus", { goalId: rawArguments.trim() }); - if (normalizedCommandName === `${defaultCommandName}-tweak`) return okCommand("tweak", { objective: rawArguments.trim() }); - if (normalizedCommandName === `${defaultCommandName}-confirm`) return okCommand("confirm", { goalId: rawArguments.trim() }); - if (normalizedCommandName === `${defaultCommandName}-reject`) return okCommand("reject", { goalId: rawArguments.trim() }); - if (normalizedCommandName === `${defaultCommandName}-set`) return parseStart(tokens); + if (commandName === `${defaultCommandName}-status`) return okCommand("status"); + if (commandName === `${defaultCommandName}-list`) return okCommand("list"); + if (commandName === `${defaultCommandName}-pause`) return okCommand("pause", { reason: rawArguments }); + if (commandName === `${defaultCommandName}-resume`) return okCommand("resume"); + if (commandName === `${defaultCommandName}-clear`) return okCommand("clear"); + if (commandName === `${defaultCommandName}-abort`) return okCommand("abort", { reason: rawArguments }); + if (commandName === `${defaultCommandName}-focus`) return okCommand("focus", { goalId: rawArguments.trim() }); + if (commandName === `${defaultCommandName}-tweak`) return okCommand("tweak", { objective: rawArguments.trim() }); + if (commandName === `${defaultCommandName}-confirm`) return okCommand("confirm", { goalId: rawArguments.trim() }); + if (commandName === `${defaultCommandName}-reject`) return okCommand("reject", { goalId: rawArguments.trim() }); + if (commandName === `${defaultCommandName}-set`) return parseStart(tokens); if (tokens.length === 0) return okCommand("status"); const first = tokens[0]; @@ -74,18 +73,6 @@ export function parseGoalCommand(commandName: string, defaultCommandName: string return parseDraft(tokens); } -export function goalCommandAliases(commandName: string): string[] { - return commandName.endsWith("s") ? [] : [`${commandName}s`]; -} - -function normalizeGoalCommandName(commandName: string, defaultCommandName: string): string { - for (const alias of goalCommandAliases(defaultCommandName)) { - if (commandName === alias) return defaultCommandName; - if (commandName.startsWith(`${alias}-`)) return `${defaultCommandName}${commandName.slice(alias.length)}`; - } - return commandName; -} - function parseDraft(tokens: string[]): OperationResult { const parsed = parseGoalDefinition(tokens, "draft"); if (parsed.ok === false) return parsed; diff --git a/src/runtime.ts b/src/runtime.ts index 3fb0a12..6f54223 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,6 +1,6 @@ import { tool, type Config, type Hooks, type PluginInput } from "@opencode-ai/plugin"; import { runCompletionAudit } from "./audit"; -import { goalCommandAliases, parseGoalCommand } from "./commands"; +import { parseGoalCommand } from "./commands"; import { createGoalDraft, findSessionDraft, formatGoalDraftConfirmation, formatGoalDraftHeaderLines, removeDraft, upsertDraft } from "./draft"; import { DEFAULT_OPTIONS, MEANINGFUL_PROGRESS_TOOLS, PLUGIN_NAME } from "./defaults"; import { errorMessage } from "./errors"; @@ -203,37 +203,35 @@ export class GoalRuntime { private registerCommands(config: Config): void { if (config.command === undefined) config.command = {}; const commandName = this.options.commandName; - this.registerCommandAliases(config, commandName, { + this.registerCommand(config, commandName, { description: "Draft a durable opencode-goal-x objective for explicit confirmation, or manage existing goals.", template: "$ARGUMENTS", }); - this.registerCommandAliases(config, `${commandName}-set`, { + this.registerCommand(config, `${commandName}-set`, { description: "Start a durable opencode-goal-x objective immediately.", template: "$ARGUMENTS", }); - this.registerCommandAliases(config, `${commandName}-confirm`, { + this.registerCommand(config, `${commandName}-confirm`, { description: "Confirm the latest drafted opencode-goal-x objective and start it.", template: "$ARGUMENTS", }); - this.registerCommandAliases(config, `${commandName}-reject`, { + this.registerCommand(config, `${commandName}-reject`, { description: "Discard the latest drafted opencode-goal-x objective without creating a goal.", template: "$ARGUMENTS", }); - this.registerCommandAliases(config, `${commandName}-status`, { description: "Show the focused goal status.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-list`, { description: "List open goals.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-focus`, { description: "Focus an open goal by number or id.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-pause`, { description: "Pause the focused goal.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-resume`, { description: "Resume the focused paused goal.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-tweak`, { description: "Revise the focused goal objective.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-abort`, { description: "Abort and archive the focused goal.", template: "$ARGUMENTS" }); - this.registerCommandAliases(config, `${commandName}-clear`, { description: "Clear and archive the focused goal.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-status`, { description: "Show the focused goal status.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-list`, { description: "List open goals.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-focus`, { description: "Focus an open goal by number or id.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-pause`, { description: "Pause the focused goal.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-resume`, { description: "Resume the focused paused goal.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-tweak`, { description: "Revise the focused goal objective.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-abort`, { description: "Abort and archive the focused goal.", template: "$ARGUMENTS" }); + this.registerCommand(config, `${commandName}-clear`, { description: "Clear and archive the focused goal.", template: "$ARGUMENTS" }); } - private registerCommandAliases(config: Config, command: string, definition: NonNullable[string]): void { + private registerCommand(config: Config, command: string, definition: NonNullable[string]): void { if (config.command === undefined) config.command = {}; config.command[command] = definition; - const suffix = command.slice(this.options.commandName.length); - for (const alias of goalCommandAliases(this.options.commandName)) config.command[`${alias}${suffix}`] = definition; } private async handleCommand(command: string, sessionID: string, rawArguments: string, parts: MutableTextPart[]): Promise { @@ -286,8 +284,7 @@ export class GoalRuntime { private ownsCommand(command: string): boolean { const base = this.options.commandName; - if (command === base || command.startsWith(`${base}-`)) return true; - return goalCommandAliases(base).some((alias) => command === alias || command.startsWith(`${alias}-`)); + return command === base || command.startsWith(`${base}-`); } private replaceCommandText(parts: MutableTextPart[], text: string): void { diff --git a/test/commands.test.ts b/test/commands.test.ts index a7bf392..91d1ebe 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -28,14 +28,13 @@ describe("goal command parser", () => { expect(command.budgetOverrides.maxTurns).toBe(2); }); - test("parses plural /goals aliases like /goal", () => { - const draft = requireParseSuccess(parseGoalCommand("goals", "goal", "draft with plural alias")); - const confirm = requireParseSuccess(parseGoalCommand("goals-confirm", "goal", "draft-1")); - - expect(draft.action).toBe("draft"); - expect(draft.objective).toBe("draft with plural alias"); - expect(confirm.action).toBe("confirm"); - expect(confirm.goalId).toBe("draft-1"); + test("does not normalize plural lifecycle command aliases", () => { + const parsed = parseGoalCommand("goals-confirm", "goal", "draft-1"); + + const command = requireParseSuccess(parsed); + expect(command.action).toBe("draft"); + expect(command.objective).toBe("draft-1"); + expect(command.goalId).toBeUndefined(); }); test("maps draft confirmation commands", () => { diff --git a/test/runtime.test.ts b/test/runtime.test.ts index 4265e7c..83e2c74 100644 --- a/test/runtime.test.ts +++ b/test/runtime.test.ts @@ -45,27 +45,37 @@ describe("GoalRuntime integration-style behavior", () => { await setup.runtime.disposeForTest(); }); - test("plural /goals alias is registered and creates a confirmable draft flow", async () => { + test("default command registration and ownership are singular-only", async () => { const setup = createRuntimeSetup({ requireAudit: false }); const commandHook = requireCommandHook(setup.hooks); const configHook = requireConfigHook(setup.hooks); const config: Config = {}; - const parts = textParts(); await configHook(config); - expect(config.command?.goals).toBeDefined(); - expect(config.command?.["goals-confirm"]).toBeDefined(); + expect(config.command?.goal).toBeDefined(); + expect(config.command?.["goal-confirm"]).toBeDefined(); + expect(config.command?.goals).toBeUndefined(); + expect(config.command?.["goals-confirm"]).toBeUndefined(); - await commandHook({ command: "goals", sessionID: "s1", arguments: "draft from plural alias" }, { parts }); - const afterDraft = loadStore(setup.paths); - expect(firstDraft(afterDraft).status).toBe("planning"); + const pluralDraftParts = textParts(); + await commandHook({ command: "goals", sessionID: "s1", arguments: "draft from plural alias" }, { parts: pluralDraftParts }); + const afterPluralDraft = loadStore(setup.paths); + expect(Object.values(afterPluralDraft.drafts ?? {})).toHaveLength(0); + expect(firstText(pluralDraftParts)).toBe(""); const proposeDraft = requireTool(setup.hooks, "propose_goal_draft"); await proposeDraft.execute({ objective: "draft from plural alias" }, toolContext(setup.directory)); - await commandHook({ command: "goals-confirm", sessionID: "s1", arguments: "" }, { parts }); + const pluralConfirmParts = textParts(); + await commandHook({ command: "goals-confirm", sessionID: "s1", arguments: "" }, { parts: pluralConfirmParts }); + const afterPluralConfirm = loadStore(setup.paths); + expect(firstText(pluralConfirmParts)).toBe(""); + expect(afterPluralConfirm.goals).toHaveLength(0); + + const singularConfirmParts = textParts(); + await commandHook({ command: "goal-confirm", sessionID: "s1", arguments: "" }, { parts: singularConfirmParts }); const store = loadStore(setup.paths); - expect(firstText(parts)).toContain("Goal draft confirmed"); + expect(firstText(singularConfirmParts)).toContain("Goal draft confirmed"); expect(store.goals).toHaveLength(1); await setup.runtime.disposeForTest(); });