Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Goal X asks the assistant to refine that topic into a proposed goal. No durable
/goal-confirm
```

By default, the `/goal` draft command runs through OpenCode's `plan` agent and `/goal-confirm` switches execution to `build`.

If you already know the exact objective and want to start immediately, use `/goal-set` instead:

```text
Expand All @@ -49,6 +51,7 @@ The TUI plugin target, `opencode-goal-x/tui`, is optional. Load it through OpenC

- Keeps a persistent goal pool in `.opencode/goals/` with active markdown mirrors, archived markdown files, strict `state.json`, pending drafts, captured execution context, audit progress, and append-only `ledger.jsonl`.
- Makes `/goal <topic>` safe by default: it starts a draft/planning flow and never creates or executes a goal until explicit `/goal-confirm`.
- Registers draft commands with the `plan` agent and start/resume commands with the `build` agent by default, with plugin options for custom agent names.
- Provides `/goal-set <objective>` as the explicit immediate-start shortcut.
- Captures active agent/model/provider/variant context and reuses it for plugin-driven continuations, compaction followups, and audits unless explicit auditor config overrides it. Non-default variants such as `xhigh` are preserved by default.
- Runs guarded auto-continuation from `session.idle`, with budget limits for turns, runtime, tracked tokens, prompt failures, no-tool loops, low-progress loops, and stale focus.
Expand Down Expand Up @@ -181,6 +184,8 @@ Configure through OpenCode's plugin tuple form:
"maxRuntimeMs": 28800000,
"maxTokens": 2000000,
"minDelayMs": 1000,
"planningAgent": "plan",
"executionAgent": "build",
"noProgressTurnsBeforePause": 4,
"noToolCallTurnsBeforePause": 3,
"noProgressTokenThreshold": 40,
Expand All @@ -205,6 +210,8 @@ Useful options:

- `commandName`: command prefix, default `goal`.
- `stateDir`: project-relative state directory, default `.opencode/goals`; absolute paths, traversal, and NUL bytes are rejected.
- `planningAgent`: agent assigned to `/goal`, default `plan`.
- `executionAgent`: agent assigned to `/goal-set`, `/goal-confirm`, and `/goal-resume`, default `build`.
- `maxTurns`, `maxRuntimeMs`, `maxTokens`, `minDelayMs`: autoContinue budgets.
- `noProgressTurnsBeforePause`, `noToolCallTurnsBeforePause`, `noProgressTokenThreshold`, `maxPromptFailures`: conservative loop guards.
- `requireAudit` / `completionAudit`: fail-closed audit gate, default `true`.
Expand Down Expand Up @@ -282,7 +289,7 @@ Manual OpenCode smoke tests before release:
1. Start OpenCode with this plugin loaded locally and restart after config changes.
2. Select a non-default model variant such as `xhigh`.
3. Run `/goal draft a small verified change`; verify no active goal appears before confirmation.
4. Confirm the draft with `/goal-confirm`; verify the same agent/model/variant remains selected.
4. Confirm the draft with `/goal-confirm`; verify execution switches to the configured execution agent while preserving model/variant context.
5. Run `/goal-set make a trivial documented test fixture --max-turns 2`; verify immediate persistence and autoContinue.
6. Force/simulate compaction; verify Goal X context survives and generic compaction auto-continue does not race the goal loop.
7. Trigger `complete_goal` with weak evidence; verify the audit rejects and pauses.
Expand Down
2 changes: 2 additions & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const DEFAULT_BUDGET: GoalBudget = {
export const DEFAULT_OPTIONS: GoalRuntimeOptions = {
commandName: "goal",
stateDir: DEFAULT_STATE_DIR,
planningAgent: "plan",
executionAgent: "build",
...DEFAULT_BUDGET,
requireAudit: true,
auditTimeoutMs: DEFAULT_AUDIT_TIMEOUT_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND,
Expand Down
5 changes: 4 additions & 1 deletion src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,17 @@ export class GoalRuntime {
const commandName = this.options.commandName;
this.registerCommand(config, commandName, {
description: "Draft a durable opencode-goal-x objective for explicit confirmation, or manage existing goals.",
agent: this.options.planningAgent,
template: "$ARGUMENTS",
});
this.registerCommand(config, `${commandName}-set`, {
description: "Start a durable opencode-goal-x objective immediately.",
agent: this.options.executionAgent,
template: "$ARGUMENTS",
});
this.registerCommand(config, `${commandName}-confirm`, {
description: "Confirm the latest drafted opencode-goal-x objective and start it.",
agent: this.options.executionAgent,
template: "$ARGUMENTS",
});
this.registerCommand(config, `${commandName}-reject`, {
Expand All @@ -223,7 +226,7 @@ export class GoalRuntime {
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}-resume`, { description: "Resume the focused paused goal.", agent: this.options.executionAgent, 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" });
Expand Down
4 changes: 4 additions & 0 deletions src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export const GoalStoreSnapshotSchema: z.ZodType<GoalStoreSnapshot> = z.object({
const PluginOptionsSchema = z.object({
commandName: OptionalNonEmptyStringSchema,
stateDir: OptionalNonEmptyStringSchema,
planningAgent: OptionalNonEmptyStringSchema,
executionAgent: OptionalNonEmptyStringSchema,
maxTurns: z.number().int().positive().optional(),
maxRuntimeMs: z.number().int().positive().optional(),
maxMinutes: z.number().positive().optional(),
Expand Down Expand Up @@ -190,6 +192,8 @@ export function normalizeOptions(rawOptions: unknown): GoalRuntimeOptions {
return {
commandName: normalizeCommandName(options.commandName ?? DEFAULT_OPTIONS.commandName),
stateDir: options.stateDir ?? DEFAULT_OPTIONS.stateDir,
planningAgent: options.planningAgent ?? DEFAULT_OPTIONS.planningAgent,
executionAgent: options.executionAgent ?? DEFAULT_OPTIONS.executionAgent,
maxTurns: options.maxTurns ?? DEFAULT_OPTIONS.maxTurns,
maxRuntimeMs,
maxTokens: options.maxTokens ?? DEFAULT_OPTIONS.maxTokens,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export interface GoalPaths {
export interface GoalRuntimeOptions extends GoalBudget {
commandName: string;
stateDir: string;
planningAgent: string;
executionAgent: string;
requireAudit: boolean;
auditTimeoutMs: number;
auditorModel?: string;
Expand Down
18 changes: 18 additions & 0 deletions test/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ describe("GoalRuntime integration-style behavior", () => {

await configHook(config);
expect(config.command?.goal).toBeDefined();
expect(config.command?.goal?.agent).toBe("plan");
expect(config.command?.["goal-set"]?.agent).toBe("build");
expect(config.command?.["goal-confirm"]).toBeDefined();
expect(config.command?.["goal-confirm"]?.agent).toBe("build");
expect(config.command?.["goal-resume"]?.agent).toBe("build");
expect(config.command?.goals).toBeUndefined();
expect(config.command?.["goals-confirm"]).toBeUndefined();

Expand All @@ -80,6 +84,20 @@ describe("GoalRuntime integration-style behavior", () => {
await setup.runtime.disposeForTest();
});

test("custom planning and execution agents are registered on goal commands", async () => {
const setup = createRuntimeSetup({ requireAudit: false, planningAgent: "goal-planner", executionAgent: "goal-builder" });
const configHook = requireConfigHook(setup.hooks);
const config: Config = {};

await configHook(config);

expect(config.command?.goal?.agent).toBe("goal-planner");
expect(config.command?.["goal-set"]?.agent).toBe("goal-builder");
expect(config.command?.["goal-confirm"]?.agent).toBe("goal-builder");
expect(config.command?.["goal-resume"]?.agent).toBe("goal-builder");
await setup.runtime.disposeForTest();
});

test("propose_goal_draft creates a finalized draft that /goal-confirm starts", async () => {
const setup = createRuntimeSetup({ requireAudit: false });
const proposeDraft = requireTool(setup.hooks, "propose_goal_draft");
Expand Down
7 changes: 7 additions & 0 deletions test/schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ describe("plugin option normalization", () => {
expect(normalizeOptions({ commandName: "/goals" }).commandName).toBe("goals");
expect(normalizeOptions({ commandName: "/" }).commandName).toBe(DEFAULT_OPTIONS.commandName);
});

test("normalizes command agent routing options", () => {
expect(normalizeOptions({}).planningAgent).toBe(DEFAULT_OPTIONS.planningAgent);
expect(normalizeOptions({}).executionAgent).toBe(DEFAULT_OPTIONS.executionAgent);
expect(normalizeOptions({ planningAgent: "goal-planner", executionAgent: "goal-builder" }).planningAgent).toBe("goal-planner");
expect(normalizeOptions({ planningAgent: "goal-planner", executionAgent: "goal-builder" }).executionAgent).toBe("goal-builder");
});
});
Loading