diff --git a/README.md b/README.md index 69e3079..99f920e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,32 @@ The plugin registers one tool, `session_restart`, that the agent can call when t - Aborts the current session by default, so only the restarted session stays active. - Returns the new session ID so the agent can tell you where to switch. +## Static handoff text + +A static text that is always included in the restart prompt, when defined: + +- **Per agent (overrides the global default):** add a `handoff_text` key to any agent in `opencode.json` (or any frontmatter key in `agent/*.md` — unknown keys fold into the agent's `options` and are read from there). The text of the agent the restarted session runs under is used. + + ```json + { + "agent": { + "build": { + "handoff_text": "Always start by reviewing the open PRs on git.jilits.se before touching any code." + } + } + } + ``` + +- **Global fallback:** set the `static_text` plugin option. It applies to every restart whenever the agent defines no `handoff_text` of its own. + + ```json + { + "plugin": [["/path/to/opencode-session-restart", { "static_text": "..." }]] + } + ``` + +Either way the text is prepended to the handoff prompt under a `STANDING INSTRUCTIONS` section. Precedence: agent `handoff_text` (direct key, or md-frontmatter `handoff_text`) → `static_text` plugin option → nothing. + ### Tool arguments | argument | type | default | description | diff --git a/index.ts b/index.ts index d59f561..8dc60d6 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,10 @@ -import { type Plugin, type PluginOptions, tool } from "@opencode-ai/plugin" +import { type Plugin, type PluginInput, type PluginOptions, tool } from "@opencode-ai/plugin" import type { TextPartInput } from "@opencode-ai/sdk" export const id = "opencode-session-restart" +type PluginClient = PluginInput["client"] + export type RestartPluginOptions = { /** * Abort the current session once the new session has been started. @@ -16,6 +18,12 @@ export type RestartPluginOptions = { * Default: true. */ disable_compaction_continue: boolean + /** + * Static text always prepended to every restart prompt, for agents that do + * not define their own `handoff_text`. An agent-defined `handoff_text` + * overrides this. Default: none. + */ + static_text?: string } const DEFAULT_OPTIONS: RestartPluginOptions = { @@ -25,12 +33,15 @@ const DEFAULT_OPTIONS: RestartPluginOptions = { const MAX_TITLE_LENGTH = 80 -function buildHandoff(task: string, notes: string | undefined) { +function buildHandoff(task: string, notes: string | undefined, staticText: string | undefined) { const state = notes?.trim() return [ "You are continuing work that was restarted into this fresh session with a clean context window.", "", - "Do not reconstruct or repeat the earlier conversation. Only the notes below and the current on-disk state (working tree, git status, staged or uncommitted changes) are available context.", + "Do not reconstruct or repeat the earlier conversation. Only the standing instructions and state notes below, plus the current on-disk state (working tree, git status, staged or uncommitted changes), are available context.", + ...(staticText + ? ["", "STANDING INSTRUCTIONS (always apply):", staticText] + : []), "", "STATE NOTES:", state && state.length > 0 ? state : "None provided — orient yourself by inspecting git status, the diff, and open files.", @@ -40,6 +51,27 @@ function buildHandoff(task: string, notes: string | undefined) { ].join("\n") } +async function resolveStaticText( + client: PluginClient, + currentAgent: string, + targetAgent: string | undefined, + fallback: string | undefined, +) { + const agentName = targetAgent ?? currentAgent + try { + const cfg = await client.config.get() + const agent = cfg.data?.agent?.[agentName] + const direct = agent?.handoff_text + if (typeof direct === "string" && direct.trim()) return direct + const opts = agent?.options as Record | undefined + const folded = opts?.handoff_text + if (typeof folded === "string" && folded.trim()) return folded + } catch { + // config lookup failed — fall through to the plugin-level static_text + } + return fallback +} + function normalizeModel(value: string) { const slash = value.indexOf("/") if (slash <= 0 || slash === value.length - 1) { @@ -91,7 +123,8 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => { const restartedID = created.data?.id if (!restartedID) throw new Error("session.create returned no session id") - const parts: TextPartInput[] = [{ type: "text", text: buildHandoff(args.task, args.notes) }] + const staticText = await resolveStaticText(client, ctx.agent, args.agent, options.static_text) + const parts: TextPartInput[] = [{ type: "text", text: buildHandoff(args.task, args.notes, staticText) }] await client.session.promptAsync({ path: { id: restartedID }, body: {