Browse Source

rename handoff flow to fork session

Alvin Unreal 3 months ago
parent
commit
cbc5f1a658

+ 1 - 1
README.md

@@ -490,7 +490,7 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[Session Management](docs/session-management.md)** | Reuse recent child-agent sessions with short aliases instead of starting over |
 | **[Todo Continuation](docs/todo-continuation.md)** | Auto-continue orchestrator sessions with cooldowns and safety checks |
 | **[Preset Switching](docs/preset-switching.md)** | Switch agent model presets at runtime with `/preset` |
-| **[Handoff](docs/handoff.md)** | Spawn a child orchestrator with `/handoff`, do the requested work, and return a summary to the main session |
+| **[Fork](docs/fork.md)** | Fork the current orchestrator context with `/fork-session`, do the requested work, and return a summary to the main session |
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 | **[Interview](docs/interview.md)** | Turn rough ideas into a structured markdown spec through a browser-based Q&A flow |
 | **[Divoom Display](docs/divoom.md)** | Mirror orchestrator and specialist-agent activity to a Divoom MiniToo Bluetooth display |

+ 63 - 0
docs/fork.md

@@ -0,0 +1,63 @@
+# Fork
+
+`/fork-session` starts a boomerang-style fork of the current orchestrator
+session. The fork receives the best compact context the parent can provide, does
+the requested work, then returns a compact completion summary to the original
+session.
+
+## Usage
+
+```text
+/fork-session <what the forked orchestrator should do>
+```
+
+The command asks the current orchestrator to call `fork_session` with a compact
+worker prompt and clearly relevant files.
+
+## Flow
+
+1. The main session calls `fork_session`.
+2. Slim creates a real child session with `parentID` set to the main session.
+3. The child runs as `orchestrator`, so it can use the normal specialist-agent
+   workflow and delegate through `task` when useful.
+4. The parent passes current context, decisions, constraints, and file
+   references into the fork prompt.
+5. Referenced files are loaded into the child as synthetic Read-tool context.
+6. When the child finishes, Slim extracts its assistant output and returns it to
+   the main session inside `<fork_summary>`.
+7. The child session is aborted for cleanup after the summary is extracted.
+
+In tmux or zellij, the fork appears like other delegated work because it is a
+real child session. Existing session-depth and pane cleanup handling apply.
+
+## Prompt style
+
+The user prompt controls scope. Keep it direct:
+
+```text
+/fork-session finish the docs and run the relevant checks
+/fork-session investigate the flaky auth test and report what changed
+/fork-session implement the small UI polish we discussed
+```
+
+The fork prompt should stay compact: pass what the fork needs to act without
+re-discovering the thread, then let the fork do the requested work and summarize
+what happened.
+
+## Tools
+
+| Tool | Purpose |
+|------|---------|
+| `fork_session` | Creates the child worker session and returns its summary |
+| `read_session` | Lets a fork worker read details from its source session |
+
+## Safety
+
+- Nested forks are blocked: a fork worker should finish its current task and
+  return a summary instead of spawning another fork worker.
+- File context is restricted to the workspace real path, including symlink
+  checks.
+- Binary files are skipped.
+- Large files are capped before being injected as context.
+- Child sessions use normal OpenCode session lifecycle events, so multiplexer
+  cleanup remains consistent with other delegated agents.

+ 0 - 59
docs/handoff.md

@@ -1,59 +0,0 @@
-# Handoff
-
-`/handoff` starts a boomerang-style worker session for the user’s requested
-goal, then returns a compact completion summary to the original session.
-
-## Usage
-
-```text
-/handoff <what the worker should do>
-```
-
-The command asks the current orchestrator to call `handoff_session` with the
-worker prompt and any clearly relevant files.
-
-## Flow
-
-1. The main session calls `handoff_session`.
-2. Slim creates a real child session with `parentID` set to the main session.
-3. The child runs as `orchestrator`, so it can use the normal specialist-agent
-   workflow and delegate through `task` when useful.
-4. Referenced files are loaded into the child as synthetic Read-tool context.
-5. When the child finishes, Slim extracts its assistant output and returns it to
-   the main session inside `<handoff_summary>`.
-6. The child session is aborted for cleanup after the summary is extracted.
-
-In tmux or zellij, the child appears like other delegated work because it is a
-real child session. Existing session-depth and pane cleanup handling apply.
-
-## What to put in the prompt
-
-The user prompt controls scope. Keep it direct:
-
-```text
-/handoff finish the docs for handoff and run the relevant checks
-/handoff investigate the flaky auth test and report what changed
-/handoff implement the small UI polish we discussed
-```
-
-The handoff prompt intentionally avoids prescribing extra actions. It should do
-what the user asks, then summarize what happened, files changed, validation run,
-and any remaining risks or follow-up.
-
-## Tools
-
-| Tool | Purpose |
-|------|---------|
-| `handoff_session` | Creates the child worker session and returns its summary |
-| `read_session` | Lets a handoff worker read details from the parent/source session |
-
-## Safety
-
-- Nested handoffs are blocked: a handoff worker should finish its current task
-  and return a summary instead of spawning another handoff worker.
-- File context is restricted to the workspace real path, including symlink
-  checks.
-- Binary files are skipped.
-- Large files are capped before being injected as context.
-- Child sessions use normal OpenCode session lifecycle events, so multiplexer
-  cleanup remains consistent with other delegated agents.

+ 11 - 11
docs/tools.md

@@ -34,23 +34,23 @@ Fast, structural code search and refactoring — more powerful than plain text g
 
 ---
 
-## Session Handoff
+## Fork
 
-Run a boomerang-style worker session and return its summary to the caller.
+Fork the current orchestrator context into a boomerang-style worker session.
 
 | Command / Tool | Description |
 |----------------|-------------|
-| `/handoff <goal>` | Ask the current agent to summarize context, relevant files, decisions, and next steps for a new session |
-| `handoff_session` | Runs a child handoff worker session and returns its summary to the caller |
-| `read_session` | Reads transcript details from the source session when the handoff summary is missing specifics |
+| `/fork-session <goal>` | Ask the current orchestrator to pass compact context into a forked worker session |
+| `fork_session` | Runs a child fork worker session and returns its summary to the caller |
+| `read_session` | Reads transcript details from the source session when the fork prompt is missing specifics |
 
-Handoff prompts include `@file` references. Slim creates a real child session
-with the current session as `parentID`, lets the handoff worker read the provided
-context and files, then returns the worker's `<handoff_summary>` back to the
-main session as normal tool output. In tmux/zellij this appears like other child
-agent work: a pane can open for the worker and close when the summary returns.
+Fork prompts include `@file` references. Slim creates a real child session with
+the current session as `parentID`, lets the forked orchestrator use the provided
+context and files, then returns the worker's `<fork_summary>` back to the main
+session as normal tool output. In tmux/zellij this appears like other child agent
+work: a pane can open for the worker and close when the summary returns.
 
-See [Handoff](handoff.md) for the full workflow.
+See [Fork](fork.md) for the full workflow.
 
 ---
 

+ 12 - 16
src/index.ts

@@ -41,9 +41,9 @@ import {
   ast_grep_replace,
   ast_grep_search,
   createCouncilTool,
-  createHandoffCommandManager,
-  createHandoffSessionTool,
-  createHandoffState,
+  createForkCommandManager,
+  createForkSessionTool,
+  createForkState,
   createPresetManager,
   createReadSessionTool,
   createWebfetchTool,
@@ -146,8 +146,8 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   let rewriteDisplayNameMentions: ReturnType<
     typeof createDisplayNameMentionRewriter
   >;
-  let handoffCommandManager: ReturnType<typeof createHandoffCommandManager>;
-  let handoffState: ReturnType<typeof createHandoffState>;
+  let forkCommandManager: ReturnType<typeof createForkCommandManager>;
+  let forkState: ReturnType<typeof createForkState>;
 
   // Counters for post-init health check (set inside try, checked outside)
   let toolCount = 0;
@@ -318,15 +318,15 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     presetManager = createPresetManager(ctx, config);
     divoomManager = createDivoomManager(config.divoom);
 
-    handoffState = createHandoffState();
-    handoffCommandManager = createHandoffCommandManager(ctx, handoffState);
+    forkState = createForkState();
+    forkCommandManager = createForkCommandManager(ctx, forkState);
 
     toolCount =
       Object.keys(councilTools).length +
       Object.keys(todoContinuationHook.tool).length +
       1 + // webfetch
       2 + // ast_grep_search, ast_grep_replace
-      2; // handoff_session, read_session
+      2; // fork_session, read_session
   } catch (err) {
     // Plugin init failed: log visibly before re-throwing so the user
     // sees something actionable instead of a silent "loaded but empty".
@@ -396,12 +396,8 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ...todoContinuationHook.tool,
       ast_grep_search,
       ast_grep_replace,
-      handoff_session: createHandoffSessionTool(
-        ctx,
-        handoffState,
-        depthTracker,
-      ),
-      read_session: createReadSessionTool(ctx.client, handoffState),
+      fork_session: createForkSessionTool(ctx, forkState, depthTracker),
+      read_session: createReadSessionTool(ctx.client, forkState),
     },
 
     mcp: mcps,
@@ -737,7 +733,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
 
       interviewManager.registerCommand(opencodeConfig);
       presetManager.registerCommand(opencodeConfig);
-      handoffCommandManager.registerCommand(opencodeConfig);
+      forkCommandManager.registerCommand(opencodeConfig);
     },
 
     event: async (input) => {
@@ -815,7 +811,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         },
       );
 
-      handoffCommandManager.handleEvent(
+      forkCommandManager.handleEvent(
         input as {
           event: {
             type: string;

+ 19 - 19
src/tools/handoff/command.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, test } from 'bun:test';
-import { createHandoffCommandManager } from './command';
-import { createHandoffState } from './state';
+import { createForkCommandManager } from './command';
+import { createForkState } from './state';
 
 function createContext() {
   return {
@@ -9,26 +9,26 @@ function createContext() {
   } as any;
 }
 
-describe('createHandoffCommandManager', () => {
-  test('registers the /handoff command', () => {
-    const manager = createHandoffCommandManager(
+describe('createForkCommandManager', () => {
+  test('registers the /fork-session command', () => {
+    const manager = createForkCommandManager(
       createContext(),
-      createHandoffState(),
+      createForkState(),
     );
     const config: Record<string, unknown> = {};
 
     manager.registerCommand(config);
 
     const commands = config.command as Record<string, { template: string }>;
-    expect(commands.handoff).toBeDefined();
-    expect(commands.handoff.template).toContain('handoff_session');
-    expect(commands.handoff.template).toContain('$ARGUMENTS');
+    expect(commands['fork-session']).toBeDefined();
+    expect(commands['fork-session'].template).toContain('fork_session');
+    expect(commands['fork-session'].template).toContain('$ARGUMENTS');
   });
 
-  test('marks child sessions of handoff workers with the same source', () => {
-    const state = createHandoffState();
+  test('marks child sessions of fork workers with the same source', () => {
+    const state = createForkState();
     state.markSession('ses_worker', 'ses_source');
-    const manager = createHandoffCommandManager(createContext(), state);
+    const manager = createForkCommandManager(createContext(), state);
 
     manager.handleEvent({
       event: {
@@ -41,8 +41,8 @@ describe('createHandoffCommandManager', () => {
   });
 
   test('does not mark unrelated child sessions', () => {
-    const state = createHandoffState();
-    const manager = createHandoffCommandManager(createContext(), state);
+    const state = createForkState();
+    const manager = createForkCommandManager(createContext(), state);
 
     manager.handleEvent({
       event: {
@@ -51,13 +51,13 @@ describe('createHandoffCommandManager', () => {
       },
     });
 
-    expect(state.isHandoffSession('ses_child')).toBe(false);
+    expect(state.isForkSession('ses_child')).toBe(false);
   });
 
-  test('unmarks deleted handoff sessions', () => {
-    const state = createHandoffState();
+  test('unmarks deleted fork sessions', () => {
+    const state = createForkState();
     state.markSession('ses_worker', 'ses_source');
-    const manager = createHandoffCommandManager(createContext(), state);
+    const manager = createForkCommandManager(createContext(), state);
 
     manager.handleEvent({
       event: {
@@ -66,6 +66,6 @@ describe('createHandoffCommandManager', () => {
       },
     });
 
-    expect(state.isHandoffSession('ses_worker')).toBe(false);
+    expect(state.isForkSession('ses_worker')).toBe(false);
   });
 });

+ 18 - 22
src/tools/handoff/command.ts

@@ -1,41 +1,39 @@
 /**
- * Command registration manager for handoff functionality.
+ * Command registration manager for fork functionality.
  *
- * Manages the /handoff slash command registration and the HANDOFF_COMMAND
- * template that guides the AI in generating handoff prompts.
+ * Manages the /fork-session slash command registration and template.
  */
 
 import type { PluginInput } from '@opencode-ai/plugin';
-import type { HandoffState } from './state';
+import type { ForkState } from './state';
 
-const COMMAND_NAME = 'handoff';
+const COMMAND_NAME = 'fork-session';
 
 /**
- * The handoff command template that guides the AI in generating handoff
- * prompts.
+ * Compact command template that lets the user request drive fork behavior.
  */
-const HANDOFF_COMMAND_TEMPLATE = `Start a handoff worker session.
+const FORK_COMMAND_TEMPLATE = `Fork the current orchestrator context into a worker session.
 
-Use the user's request below as the source of truth for what the worker should do. Keep scope and emphasis exactly aligned with the user's request.
+Use the user's request as the source of truth. Pass the best current context, decisions, constraints, and file references the fork needs. Keep it compact.
 
 USER: $ARGUMENTS
 
-Call handoff_session with the worker prompt and any clearly relevant files:
-\`handoff_session(prompt="...", files=["src/foo.ts", "src/bar.ts", ...])\``;
+Call fork_session with the worker prompt and clearly relevant files:
+\`fork_session(prompt="...", files=["src/foo.ts", "src/bar.ts", ...])\``;
 
 /**
- * Creates a handoff command manager.
+ * Creates a fork command manager.
  *
- * Handles registration of the /handoff command and processing of chat
- * messages to inject synthetic file parts for handoff sessions.
+ * Handles registration of the /fork-session command and fork session state
+ * events.
  */
-export function createHandoffCommandManager(
+export function createForkCommandManager(
   _ctx: PluginInput,
-  state: HandoffState,
+  state: ForkState,
   _processedSessions?: Set<string>,
 ) {
   /**
-   * Register the /handoff command in the OpenCode config.
+   * Register the /fork-session command in the OpenCode config.
    */
   function registerCommand(opencodeConfig: Record<string, unknown>): void {
     const configCommand = opencodeConfig.command as
@@ -46,8 +44,8 @@ export function createHandoffCommandManager(
         opencodeConfig.command = {};
       }
       (opencodeConfig.command as Record<string, unknown>)[COMMAND_NAME] = {
-        description: 'Create a focused handoff prompt for a new session',
-        template: HANDOFF_COMMAND_TEMPLATE,
+        description: 'Fork the orchestrator context into a worker session',
+        template: FORK_COMMAND_TEMPLATE,
       };
     }
   }
@@ -80,6 +78,4 @@ export function createHandoffCommandManager(
   };
 }
 
-export type HandoffCommandManager = ReturnType<
-  typeof createHandoffCommandManager
->;
+export type ForkCommandManager = ReturnType<typeof createForkCommandManager>;

+ 5 - 5
src/tools/handoff/files.test.ts

@@ -1,5 +1,5 @@
 /**
- * Tests for handoff file reference parsing.
+ * Tests for fork file reference parsing.
  */
 
 import { describe, expect, it } from 'bun:test';
@@ -77,7 +77,7 @@ describe('parseFileReferences', () => {
 
   it('should handle references with trailing punctuation', () => {
     // Note: The regex includes trailing punctuation as part of the path
-    // This is the vendored behavior from opencode-handoff
+    // This preserves the vendored parser behavior.
     const text = 'See @src/file.ts, @src/other.ts. And @src/more.ts!';
     const refs = parseFileReferences(text);
     // The regex captures the trailing punctuation, so these won't match
@@ -109,7 +109,7 @@ describe('FILE_REGEX', () => {
 
 describe('buildSyntheticFileParts', () => {
   it('loads readable files inside the workspace', async () => {
-    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-handoff-files-'));
+    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-fork-files-'));
     try {
       fs.writeFileSync(path.join(dir, 'file.ts'), 'const x = 1;\n');
 
@@ -124,9 +124,9 @@ describe('buildSyntheticFileParts', () => {
   });
 
   it('skips path traversal and symlinks outside the workspace', async () => {
-    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-handoff-files-'));
+    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-fork-files-'));
     const outside = fs.mkdtempSync(
-      path.join(os.tmpdir(), 'omos-handoff-outside-'),
+      path.join(os.tmpdir(), 'omos-fork-outside-'),
     );
     try {
       fs.writeFileSync(path.join(outside, 'secret.txt'), 'secret\n');

+ 2 - 2
src/tools/handoff/files.ts

@@ -1,7 +1,7 @@
 /**
- * File reference parsing and synthetic file parts for handoff sessions.
+ * File reference parsing and synthetic file parts for fork sessions.
  *
- * Handles extraction of @file references from handoff prompts and
+ * Handles extraction of @file references from fork prompts and
  * building synthetic text parts that match OpenCode's Read tool output
  * format.
  */

+ 6 - 7
src/tools/handoff/index.ts

@@ -1,22 +1,21 @@
 /**
- * Handoff functionality for session continuation.
+ * Fork functionality for orchestrator worker sessions.
  *
- * Provides tools and commands for creating handoff prompts that allow
- * work to continue seamlessly in new sessions with preloaded context.
+ * Provides tools and commands for forking current context into child workers.
  */
 
 export {
-  createHandoffCommandManager,
-  type HandoffCommandManager,
+  createForkCommandManager,
+  type ForkCommandManager,
 } from './command';
 export {
   buildSyntheticFileParts,
   FILE_REGEX,
   parseFileReferences,
 } from './files';
-export { createHandoffState, type HandoffState } from './state';
+export { createForkState, type ForkState } from './state';
 export {
-  createHandoffSessionTool,
+  createForkSessionTool,
   createReadSessionTool,
   type OpencodeClient,
 } from './tools';

+ 4 - 4
src/tools/handoff/state.ts

@@ -1,11 +1,11 @@
-export interface HandoffState {
+export interface ForkState {
   markSession(sessionID: string, sourceSessionID: string): void;
   unmarkSession(sessionID: string): void;
-  isHandoffSession(sessionID: string): boolean;
+  isForkSession(sessionID: string): boolean;
   sourceFor(sessionID: string): string | undefined;
 }
 
-export function createHandoffState(): HandoffState {
+export function createForkState(): ForkState {
   const sourceBySession = new Map<string, string>();
 
   return {
@@ -15,7 +15,7 @@ export function createHandoffState(): HandoffState {
     unmarkSession(sessionID: string): void {
       sourceBySession.delete(sessionID);
     },
-    isHandoffSession(sessionID: string): boolean {
+    isForkSession(sessionID: string): boolean {
       return sourceBySession.has(sessionID);
     },
     sourceFor(sessionID: string): string | undefined {

+ 20 - 20
src/tools/handoff/tools.test.ts

@@ -3,15 +3,15 @@ import * as fs from 'node:fs';
 import * as os from 'node:os';
 import * as path from 'node:path';
 import { SubagentDepthTracker } from '../../utils/subagent-depth';
-import { createHandoffState } from './state';
-import { createHandoffSessionTool, createReadSessionTool } from './tools';
+import { createForkState } from './state';
+import { createForkSessionTool, createReadSessionTool } from './tools';
 
 function makeTempDir() {
-  return fs.mkdtempSync(path.join(os.tmpdir(), 'omos-handoff-tool-'));
+  return fs.mkdtempSync(path.join(os.tmpdir(), 'omos-fork-tool-'));
 }
 
-describe('handoff_session tool', () => {
-  test('runs a worker child session and returns its handoff summary', async () => {
+describe('fork_session tool', () => {
+  test('runs a worker child session and returns its fork summary', async () => {
     const directory = makeTempDir();
     try {
       fs.mkdirSync(path.join(directory, 'src'));
@@ -28,8 +28,8 @@ describe('handoff_session tool', () => {
         ],
       }));
       const sessionAbort = mock(async () => ({}));
-      const state = createHandoffState();
-      const tool = createHandoffSessionTool(
+      const state = createForkState();
+      const tool = createForkSessionTool(
         {
           directory,
           client: {
@@ -51,7 +51,7 @@ describe('handoff_session tool', () => {
       );
 
       expect(result).toContain('task_id: ses_new');
-      expect(result).toContain('<handoff_summary>');
+      expect(result).toContain('<fork_summary>');
       expect(result).toContain('Summary from worker');
       expect(sessionCreate).toHaveBeenCalledWith({
         responseStyle: 'data',
@@ -59,7 +59,7 @@ describe('handoff_session tool', () => {
         query: { directory },
         body: {
           parentID: 'ses_old',
-          title: 'Handoff worker from ses_old',
+          title: 'Fork worker from ses_old',
         },
       });
       expect(sessionPrompt).toHaveBeenCalledTimes(1);
@@ -77,7 +77,7 @@ describe('handoff_session tool', () => {
       expect(promptCall.body.parts[0]).toMatchObject({
         type: 'text',
         text: expect.stringContaining(
-          'Work on behalf of parent session ses_old',
+          'fork of parent orchestrator session ses_old',
         ),
       });
       expect(promptCall.body.parts).toContainEqual(
@@ -96,18 +96,18 @@ describe('handoff_session tool', () => {
     }
   });
 
-  test('blocks nested handoff calls from a handoff worker', async () => {
+  test('blocks nested fork calls from a fork worker', async () => {
     const directory = makeTempDir();
     try {
       let nestedResult = '';
-      const state = createHandoffState();
-      const tool = createHandoffSessionTool(
+      const state = createForkState();
+      const tool = createForkSessionTool(
         {
           directory,
           client: {
             session: {
               abort: mock(async () => ({})),
-              create: mock(async () => ({ data: { id: 'ses_handoff' } })),
+              create: mock(async () => ({ data: { id: 'ses_fork' } })),
               messages: mock(async () => ({
                 data: [
                   {
@@ -118,8 +118,8 @@ describe('handoff_session tool', () => {
               })),
               prompt: mock(async () => {
                 nestedResult = String(
-                  await tool.execute({ prompt: 'nested handoff' }, {
-                    sessionID: 'ses_handoff',
+                  await tool.execute({ prompt: 'nested fork' }, {
+                    sessionID: 'ses_fork',
                   } as any),
                 );
               }),
@@ -130,11 +130,11 @@ describe('handoff_session tool', () => {
         new SubagentDepthTracker(),
       );
 
-      await tool.execute({ prompt: 'outer handoff' }, {
+      await tool.execute({ prompt: 'outer fork' }, {
         sessionID: 'ses_old',
       } as any);
 
-      expect(nestedResult).toContain('Nested handoff is disabled');
+      expect(nestedResult).toContain('Nested fork is disabled');
     } finally {
       fs.rmSync(directory, { recursive: true, force: true });
     }
@@ -159,7 +159,7 @@ describe('read_session tool', () => {
         },
       ],
     }));
-    const state = createHandoffState();
+    const state = createForkState();
     state.markSession('ses_worker', 'ses_old');
 
     const result = await createReadSessionTool(
@@ -174,7 +174,7 @@ describe('read_session tool', () => {
   });
 
   test('blocks reads outside the source session', async () => {
-    const state = createHandoffState();
+    const state = createForkState();
     state.markSession('ses_worker', 'ses_old');
     const messages = mock(async () => ({ data: [] }));
 

+ 26 - 28
src/tools/handoff/tools.ts

@@ -1,8 +1,8 @@
 /**
- * Tool definitions for handoff functionality.
+ * Tool definitions for fork functionality.
  *
  * Factory functions that create tool definitions with injected dependencies:
- * - createHandoffSessionTool: Create a new session with handoff prompt
+ * - createForkSessionTool: Create a fork worker session
  * - createReadSessionTool: Read conversation transcript from a session
  */
 
@@ -11,19 +11,19 @@ import { tool } from '@opencode-ai/plugin';
 import { extractSessionResult, promptWithTimeout } from '../../utils/session';
 import type { SubagentDepthTracker } from '../../utils/subagent-depth';
 import { buildSyntheticFileParts, parseFileReferences } from './files';
-import type { HandoffState } from './state';
+import type { ForkState } from './state';
 
 export type OpencodeClient = PluginInput['client'];
-const HANDOFF_TIMEOUT_MS = 5 * 60 * 1000;
+const FORK_TIMEOUT_MS = 5 * 60 * 1000;
 
 /**
- * Create the handoff_session tool.
+ * Create the fork_session tool.
  *
  * Takes the OpenCode client as a dependency for TUI and session operations.
  */
-export function createHandoffSessionTool(
+export function createForkSessionTool(
   ctx: PluginInput,
-  state: HandoffState,
+  state: ForkState,
   depthTracker?: SubagentDepthTracker,
 ): ToolDefinition {
   const client = ctx.client;
@@ -32,7 +32,7 @@ export function createHandoffSessionTool(
     description:
       'Run a child worker session and return its completion summary to the caller',
     args: {
-      prompt: tool.schema.string().describe('The generated handoff prompt'),
+      prompt: tool.schema.string().describe('The prompt for the fork worker'),
       files: tool.schema
         .array(tool.schema.string())
         .optional()
@@ -50,18 +50,18 @@ export function createHandoffSessionTool(
         context && typeof context === 'object' && 'sessionID' in context
           ? (context as { sessionID: string }).sessionID
           : 'unknown';
-      if (state.isHandoffSession(sessionID)) {
-        return 'Nested handoff is disabled: this session is already a handoff worker. Finish this worker and return its summary to the parent session instead.';
+      if (state.isForkSession(sessionID)) {
+        return 'Nested fork is disabled: this session is already a fork worker. Finish this worker and return its summary to the parent session instead.';
       }
       if (
         sessionID !== 'unknown' &&
         depthTracker &&
         depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth
       ) {
-        return `Handoff worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
+        return `Fork worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
       }
 
-      const sessionReference = `Work on behalf of parent session ${sessionID}. When you lack specific information you can use read_session to get it.`;
+      const sessionReference = `You are a fork of parent orchestrator session ${sessionID}. Use the provided context from the parent. If needed, use read_session for source-session details.`;
       const files = new Set([
         ...parseFileReferences(args.prompt),
         ...(args.files ?? []).map((file) => file.replace(/^@/, '')),
@@ -80,7 +80,7 @@ export function createHandoffSessionTool(
           query: { directory },
           body: {
             parentID: sessionID === 'unknown' ? undefined : sessionID,
-            title: `Handoff worker from ${sessionID}`,
+            title: `Fork worker from ${sessionID}`,
           },
         });
 
@@ -88,7 +88,7 @@ export function createHandoffSessionTool(
           (session as { data?: { id?: string }; id?: string })?.data?.id ??
           (session as { data?: { id?: string }; id?: string })?.id;
         if (!childSessionID) {
-          throw new Error('Handoff worker session did not return an id');
+          throw new Error('Fork worker session did not return an id');
         }
         if (sessionID !== 'unknown' && depthTracker) {
           const registered = depthTracker.registerChild(
@@ -96,9 +96,7 @@ export function createHandoffSessionTool(
             childSessionID,
           );
           if (!registered) {
-            throw new Error(
-              'Handoff worker blocked: max subagent depth exceeded',
-            );
+            throw new Error('Fork worker blocked: max subagent depth exceeded');
           }
         }
         state.markSession(childSessionID, sessionID);
@@ -115,13 +113,13 @@ export function createHandoffSessionTool(
               parts: [
                 {
                   type: 'text',
-                  text: `${fullPrompt}\n\nDo the requested work. When finished, return a concise summary of what you did, files changed, validation run, and any remaining risks or follow-up. Let the user's prompt determine scope and emphasis.`,
+                  text: `${fullPrompt}\n\nDo the requested work. When finished, return a compact summary of what changed, validation run, and any remaining risks or follow-up.`,
                 },
                 ...(await buildSyntheticFileParts(directory, files)),
               ],
             },
           },
-          HANDOFF_TIMEOUT_MS,
+          FORK_TIMEOUT_MS,
         );
 
         const extraction = await extractSessionResult(client, childSessionID, {
@@ -129,15 +127,15 @@ export function createHandoffSessionTool(
           includeReasoning: false,
         });
         if (extraction.empty) {
-          throw new Error('Handoff worker returned no summary');
+          throw new Error('Fork worker returned no summary');
         }
 
         return [
           `task_id: ${childSessionID}`,
           '',
-          '<handoff_summary>',
+          '<fork_summary>',
           extraction.text,
-          '</handoff_summary>',
+          '</fork_summary>',
         ].join('\n');
       } finally {
         if (childSessionID) {
@@ -148,7 +146,7 @@ export function createHandoffSessionTool(
             });
             state.unmarkSession(childSessionID);
           } catch {
-            // Keep the handoff marker if abort fails; session.deleted cleanup
+            // Keep the fork marker if abort fails; session.deleted cleanup
             // will remove it when OpenCode eventually deletes the session.
           }
         }
@@ -235,11 +233,11 @@ function formatTranscript(
  */
 export function createReadSessionTool(
   client: OpencodeClient,
-  state: HandoffState,
+  state: ForkState,
 ): ToolDefinition {
   return tool({
     description:
-      "Read the conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the handoff summary.",
+      'Read the source-session transcript for a fork worker. Use this only when the fork prompt did not include a specific detail you need.',
     args: {
       sessionID: tool.schema
         .string()
@@ -264,11 +262,11 @@ export function createReadSessionTool(
         context && typeof context === 'object' && 'sessionID' in context
           ? (context as { sessionID?: string }).sessionID
           : undefined;
-      if (!callerSessionID || !state.isHandoffSession(callerSessionID)) {
-        return 'read_session is only available from handoff worker sessions.';
+      if (!callerSessionID || !state.isForkSession(callerSessionID)) {
+        return 'read_session is only available from fork worker sessions.';
       }
       if (state.sourceFor(callerSessionID) !== args.sessionID) {
-        return 'read_session can only read the source session for this handoff worker.';
+        return 'read_session can only read the source session for this fork worker.';
       }
 
       try {

+ 1 - 1
src/tools/handoff/vendor.test.ts

@@ -1,5 +1,5 @@
 /**
- * Tests for handoff vendor helpers.
+ * Tests for fork vendor helpers.
  */
 
 import { describe, expect, it } from 'bun:test';

+ 4 - 4
src/tools/index.ts

@@ -1,11 +1,11 @@
 // AST-grep tools
 export { ast_grep_replace, ast_grep_search } from './ast-grep';
 export { createCouncilTool } from './council';
-export type { HandoffCommandManager } from './handoff';
+export type { ForkCommandManager } from './handoff';
 export {
-  createHandoffCommandManager,
-  createHandoffSessionTool,
-  createHandoffState,
+  createForkCommandManager,
+  createForkSessionTool,
+  createForkState,
   createReadSessionTool,
 } from './handoff';
 export type { PresetManager } from './preset-manager';