Repository navigation
Conversation
This was referenced Oct 26, 2026
4 of 6 tasks
Member
Author
|
Folded into #58508, which now holds the whole /goal stack (idle hook, settled notifications, /goal, TUI and desktop) in one PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Refs #27167. Stacked on #59258, which is stacked on #58508 (the session
idlehook). Merge those first; this PR then retargets todev.Type of change
What does this PR do?
Adds
/goal <objective>. The Session keeps working, turn after turn, until a reviewer decides the objective is met. It's a built-in plugin (packages/core/src/plugin/goal.ts) that uses only public plugin APIs. The design draws on the harness notes inspecs/goal/.The loop
The whole goal runs as one execution. Each continuation is admitted from the
idlehook, so the Session never goes idle in between, and the user gets one notification at the end. (opencode runsends its message as a plain prompt and has no slash commands, so/goalisn't available there yet.)sequenceDiagram actor User participant Goal as Goal plugin participant Runner as Session runner participant Model participant Reviewer as Reviewer (session.generate) User->>Goal: /goal <objective> Goal->>Goal: save goal in Session metadata Goal->>Runner: kickoff (notice "Goal: <objective>") loop until done, paused or stalled Runner->>Model: steps (tools, edits, …) Runner->>Goal: idle hook { sessionID, background } alt background subagent still running Goal-->>Runner: return (its result wakes the Session later) else Goal->>Reviewer: has the objective been met? Reviewer-->>Goal: {"verdict": …, "reason": …} Goal->>Runner: hidden continuation (on "continue") end end Runner-->>User: one execution, one notificationGoal states
stateDiagram-v2 [*] --> Active: /goal <objective> Active --> Active: reviewer "continue" → hidden continuation Active --> Active: subagent running / reviewer "wait" → no-op Active --> Complete: reviewer "done" Active --> Paused: reviewer "blocked" or "paused" Active --> Paused: user stops · execution fails · 3 stalled rounds · 50 rounds Active --> Paused: no usable verdict twice · hook error · server restarted Active --> Paused: idle 30 min waiting on background work · reviewer request too large after compacting Paused --> Active: /goal resume (fresh round budget) Active --> [*]: /goal clear Paused --> [*]: /goal clear Complete --> Active: /goal <new objective>Details
ctx.session.generateruns the Session's own model on the full history, tool results included, and shares the cached prefix. It answers with one JSON line:done,continue,blocked,paused(the user asked to stop) orwait(offered only while background shells run, and never for long-running servers). An unusable answer is retried once. The reviewer runs after the last step, before the next step's automatic compaction, so its request can overflow. On a context overflow the plugin admits a compaction; the runner compacts and runs the hook again. A second overflow in the same round pauses.<untrusted_objective>, plus the reviewer's reason and next step. It has no description, so timelines don't show it; clients show the running goal from its metadata instead. User input admitted at the same boundary still goes first./goalwith attachments (files, agents, skills) sends the kickoff as the user's message instead, because only a prompt carries attachments. Titles come from the first user message, so an untitled Session started by/goalis named after the objective.opencode.goal, shaped bySessionGoal.Statein@opencode/schema/session-goal:{ id, sessionID, objective, status, reason, rounds, maxRounds, boot, updatedAt }. It's durable, and clients decode it withSessionGoal.fromMetadata. Children and forks copy metadata, so a goal only acts for thesessionIDthat set it.compactionhook asks the summary to keep the goal and its progress, as a note placed right before the summary prompt. It skips native compactions, where the provider may keep that message in the replay window after the goal is gone; the hook event now carriesnativefor this. Thecontexthook adds a one-line reminder of the objective when no kickoff or continuation is in view, for example in the middle of a round right after a compaction. The reminder isn't saved and goes near the tail, so the cached prefix stays warm, and it stops once the next continuation lands./goal pauseorclearsent mid-review wins. Every write the loop makes is guarded: it re-reads and applies only if the goal is still the one it decided on, so a user's command since then is never overwritten. Metadata has no compare-and-set, so a tiny window remains. User commands write unguarded, because they win. A failing hook pauses the goal instead of failing the execution./goal <objective>or/goal "<objective>"(replaces any earlier goal, max 4,000 characters),pause,resumeandclear. They run immediately, even mid-turn. A bare/goalreturns the status and usage as the command error until clients show it.Not in this PR
/goalstatus display.toolChoice: "none"forsession.generate. The reviewer request still carries the Session's tools, and the prompt says not to call them. A tool-call-only reply counts as no verdict.How did you verify your code works?
packages/core/test/plugin/goal.test.tsruns the real internal plugins, commands and runner against TestLLM:stallsstays 0/goal pauseduring review/goal "Ship the release"setsShip the releasepause/resume/clearclearkeeps other metadata keys.session-native-compaction.test.tschecks that compaction hooks seenative: true.bun run checkpasses, andbun run typecheck:tscpasses inpackages/core.packages/coresuite passes except 7 shell-syntax tests intool-shellandshell-scan, which fail the same way on this machine without this change.Visual
A looping animation for the desktop app's "What's new" dialog. GitHub only shows its source.
SVG source
Screenshots / recordings
How the loop looks with the clients in this stack (#59275 TUI, #59276 desktop). Real TUI and server, driven with
opencode-drive; only the model and reviewer replies are scripted.Checklist