Browse Source

rename handoff feature to subtask

alvinreal 2 months ago
parent
commit
952c7091ae

+ 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 |
+| **[Subtask](docs/subtask.md)** | Spawn a child orchestrator with `/subtask`, 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 |

+ 14 - 14
docs/handoff.md → docs/subtask.md

@@ -1,26 +1,26 @@
-# Handoff
+# Subtask
 
-`/handoff` starts a boomerang-style worker session for the user’s requested
+`/subtask` 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>
+/subtask <what the worker should do>
 ```
 
-The command asks the current orchestrator to call `handoff_session` with the
+The command asks the current orchestrator to call `subtask` with the
 worker prompt and any clearly relevant files.
 
 ## Flow
 
-1. The main session calls `handoff_session`.
+1. The main session calls `subtask`.
 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>`.
+   the main session inside `<subtask_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
@@ -31,12 +31,12 @@ real child session. Existing session-depth and pane cleanup handling apply.
 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
+/subtask finish the docs for subtask and run the relevant checks
+/subtask investigate the flaky auth test and report what changed
+/subtask implement the small UI polish we discussed
 ```
 
-The handoff prompt intentionally avoids prescribing extra actions. It should do
+The subtask 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.
 
@@ -44,13 +44,13 @@ and any remaining risks or follow-up.
 
 | 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 |
+| `subtask` | Creates the child worker session and returns its summary |
+| `read_session` | Lets a subtask 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.
+- Nested subtasks are blocked: a subtask worker should finish its current task
+  and return a summary instead of spawning another subtask worker.
 - File context is restricted to the workspace real path, including symlink
   checks.
 - Binary files are skipped.

+ 8 - 8
docs/tools.md

@@ -34,23 +34,23 @@ Fast, structural code search and refactoring — more powerful than plain text g
 
 ---
 
-## Session Handoff
+## Session Subtask
 
 Run a boomerang-style worker session and return its summary to the caller.
 
 | 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 |
+| `/subtask <goal>` | Ask the current agent to run a focused child worker for the requested goal |
+| `subtask` | Runs a child subtask worker session and returns its summary to the caller |
+| `read_session` | Reads transcript details from the source session when the subtask summary 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
+Subtask prompts include `@file` references. Slim creates a real child session
+with the current session as `parentID`, lets the subtask worker read the provided
+context and files, then returns the worker's `<subtask_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 [Subtask](subtask.md) for the full workflow.
 
 ---
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "oh-my-opencode-slim",
-  "version": "1.0.7",
+  "version": "1.0.8",
   "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",

+ 1 - 1
src/codemap.md

@@ -34,7 +34,7 @@
 
 - Connects directly to `@opencode-ai/plugin`: returns the plugin object, mutates runtime agent configuration, handles event hooks, and routes RPC via `ctx.client`/`ctx.client.session`.
 - Integrates with host multiplexer backends through `src/multiplexer`, and with session lifecycle constraints through `SubagentDepthTracker`.
-- Hooks/handoff integration points now include:
+- Hooks/subtask integration points now include:
   - `createTaskSessionManagerHook` for resumable Task sessions,
   - `createTodoContinuationHook`, `createPhaseReminderHook`, `createFilterAvailableSkillsHook`, and `createPostFileToolNudgeHook` for chat/tool behavior,
   - `createInterviewManager` / `createPresetManager` command handlers.

+ 12 - 16
src/index.ts

@@ -41,11 +41,11 @@ import {
   ast_grep_replace,
   ast_grep_search,
   createCouncilTool,
-  createHandoffCommandManager,
-  createHandoffSessionTool,
-  createHandoffState,
   createPresetManager,
   createReadSessionTool,
+  createSubtaskCommandManager,
+  createSubtaskState,
+  createSubtaskTool,
   createWebfetchTool,
 } from './tools';
 import { recordTuiAgentModel, recordTuiAgentModels } from './tui-state';
@@ -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 subtaskCommandManager: ReturnType<typeof createSubtaskCommandManager>;
+  let subtaskState: ReturnType<typeof createSubtaskState>;
 
   // 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);
+    subtaskState = createSubtaskState();
+    subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
 
     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; // subtask, 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),
+      subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
+      read_session: createReadSessionTool(ctx.client, subtaskState),
     },
 
     mcp: mcps,
@@ -737,7 +733,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
 
       interviewManager.registerCommand(opencodeConfig);
       presetManager.registerCommand(opencodeConfig);
-      handoffCommandManager.registerCommand(opencodeConfig);
+      subtaskCommandManager.registerCommand(opencodeConfig);
     },
 
     event: async (input) => {
@@ -815,7 +811,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         },
       );
 
-      handoffCommandManager.handleEvent(
+      subtaskCommandManager.handleEvent(
         input as {
           event: {
             type: string;

+ 16 - 0
src/tools/codemap.md

@@ -7,6 +7,7 @@
 - AST-aware search/replace via `ast-grep` stack.
 - Remote fetch/transform utility via `smartfetch` (`webfetch` tool).
 - Council orchestration via `createCouncilTool` (`council.ts`).
+- Child-session subtasks via `subtask` and `/subtask` (`subtask/`).
 - Runtime preset switching via `/preset` hook via `createPresetManager` (`preset-manager.ts`).
 
 It is the bridge between plugin runtime integration (`src/index.ts`) and the lower-level
@@ -17,6 +18,8 @@ implementations in feature folders.
 - `ast_grep_search`, `ast_grep_replace` from `./ast-grep`
 - `createWebfetchTool`, `WEBFETCH_DESCRIPTION`, and related types from `./smartfetch`
 - `createCouncilTool`
+- `createSubtaskTool`, `createSubtaskCommandManager`, `createSubtaskState`, and
+  `createReadSessionTool` from `./subtask`
 - `createPresetManager` and `PresetManager` type
 
 ## Design patterns
@@ -57,6 +60,18 @@ implementations in feature folders.
   `displayName`).
 - In-memory `activePreset` supports immediate status display and updates after successful switches.
 
+### Subtask path
+
+- `createSubtaskCommandManager` registers `/subtask` and asks the current
+  agent to call the `subtask` tool with the worker prompt and relevant files.
+- `createSubtaskTool` creates a real child session with `parentID`, injects
+  referenced files as synthetic Read-tool context, waits for the worker to
+  finish, returns `<subtask_summary>`, then aborts the child for cleanup.
+- `createReadSessionTool` lets a subtask worker read only the source session
+  that spawned it when the summary prompt lacks details.
+- `SubtaskState` marks child sessions so nested subtasks can be blocked and
+  session.deleted events can clear stale markers.
+
 ### Smartfetch path
 
 - `createWebfetchTool` owns fetch orchestration, permission prompts, cache checks,
@@ -83,6 +98,7 @@ implementations in feature folders.
 - Tool registration:
   - `council` tools (only when `config.council` exists),
   - `webfetch`,
+  - `subtask`, `read_session`,
   - AST tools.
 - `presetManager` is initialized in plugin init and:
   - calls `registerCommand` during config hook,

+ 7 - 7
src/tools/index.ts

@@ -1,13 +1,13 @@
 // AST-grep tools
 export { ast_grep_replace, ast_grep_search } from './ast-grep';
 export { createCouncilTool } from './council';
-export type { HandoffCommandManager } from './handoff';
-export {
-  createHandoffCommandManager,
-  createHandoffSessionTool,
-  createHandoffState,
-  createReadSessionTool,
-} from './handoff';
 export type { PresetManager } from './preset-manager';
 export { createPresetManager } from './preset-manager';
 export { createWebfetchTool } from './smartfetch';
+export type { SubtaskCommandManager } from './subtask';
+export {
+  createReadSessionTool,
+  createSubtaskCommandManager,
+  createSubtaskState,
+  createSubtaskTool,
+} from './subtask';

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

@@ -1,6 +1,6 @@
 import { describe, expect, test } from 'bun:test';
-import { createHandoffCommandManager } from './command';
-import { createHandoffState } from './state';
+import { createSubtaskCommandManager } from './command';
+import { createSubtaskState } from './state';
 
 function createContext() {
   return {
@@ -9,26 +9,26 @@ function createContext() {
   } as any;
 }
 
-describe('createHandoffCommandManager', () => {
-  test('registers the /handoff command', () => {
-    const manager = createHandoffCommandManager(
+describe('createSubtaskCommandManager', () => {
+  test('registers the /subtask command', () => {
+    const manager = createSubtaskCommandManager(
       createContext(),
-      createHandoffState(),
+      createSubtaskState(),
     );
     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.subtask).toBeDefined();
+    expect(commands.subtask.template).toContain('subtask');
+    expect(commands.subtask.template).toContain('$ARGUMENTS');
   });
 
-  test('marks child sessions of handoff workers with the same source', () => {
-    const state = createHandoffState();
+  test('marks child sessions of subtask workers with the same source', () => {
+    const state = createSubtaskState();
     state.markSession('ses_worker', 'ses_source');
-    const manager = createHandoffCommandManager(createContext(), state);
+    const manager = createSubtaskCommandManager(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 = createSubtaskState();
+    const manager = createSubtaskCommandManager(createContext(), state);
 
     manager.handleEvent({
       event: {
@@ -51,13 +51,13 @@ describe('createHandoffCommandManager', () => {
       },
     });
 
-    expect(state.isHandoffSession('ses_child')).toBe(false);
+    expect(state.isSubtaskSession('ses_child')).toBe(false);
   });
 
-  test('unmarks deleted handoff sessions', () => {
-    const state = createHandoffState();
+  test('unmarks deleted subtask sessions', () => {
+    const state = createSubtaskState();
     state.markSession('ses_worker', 'ses_source');
-    const manager = createHandoffCommandManager(createContext(), state);
+    const manager = createSubtaskCommandManager(createContext(), state);
 
     manager.handleEvent({
       event: {
@@ -66,6 +66,6 @@ describe('createHandoffCommandManager', () => {
       },
     });
 
-    expect(state.isHandoffSession('ses_worker')).toBe(false);
+    expect(state.isSubtaskSession('ses_worker')).toBe(false);
   });
 });

+ 19 - 19
src/tools/handoff/command.ts → src/tools/subtask/command.ts

@@ -1,41 +1,41 @@
 /**
- * Command registration manager for handoff functionality.
+ * Command registration manager for subtask functionality.
  *
- * Manages the /handoff slash command registration and the HANDOFF_COMMAND
- * template that guides the AI in generating handoff prompts.
+ * Manages the /subtask slash command registration and the SUBTASK_COMMAND
+ * template that guides the AI in generating subtask prompts.
  */
 
 import type { PluginInput } from '@opencode-ai/plugin';
-import type { HandoffState } from './state';
+import type { SubtaskState } from './state';
 
-const COMMAND_NAME = 'handoff';
+const COMMAND_NAME = 'subtask';
 
 /**
- * The handoff command template that guides the AI in generating handoff
+ * The subtask command template that guides the AI in generating subtask
  * prompts.
  */
-const HANDOFF_COMMAND_TEMPLATE = `Start a handoff worker session.
+const SUBTASK_COMMAND_TEMPLATE = `Start a subtask 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.
 
 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 subtask with the worker prompt and any clearly relevant files:
+\`subtask(prompt="...", files=["src/foo.ts", "src/bar.ts", ...])\``;
 
 /**
- * Creates a handoff command manager.
+ * Creates a subtask 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 /subtask command and processing of chat
+ * messages to inject synthetic file parts for subtask sessions.
  */
-export function createHandoffCommandManager(
+export function createSubtaskCommandManager(
   _ctx: PluginInput,
-  state: HandoffState,
+  state: SubtaskState,
   _processedSessions?: Set<string>,
 ) {
   /**
-   * Register the /handoff command in the OpenCode config.
+   * Register the /subtask command in the OpenCode config.
    */
   function registerCommand(opencodeConfig: Record<string, unknown>): void {
     const configCommand = opencodeConfig.command as
@@ -46,8 +46,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: 'Create a focused subtask prompt for a new session',
+        template: SUBTASK_COMMAND_TEMPLATE,
       };
     }
   }
@@ -80,6 +80,6 @@ export function createHandoffCommandManager(
   };
 }
 
-export type HandoffCommandManager = ReturnType<
-  typeof createHandoffCommandManager
+export type SubtaskCommandManager = ReturnType<
+  typeof createSubtaskCommandManager
 >;

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

@@ -1,5 +1,5 @@
 /**
- * Tests for handoff file reference parsing.
+ * Tests for subtask 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 is the vendored behavior from opencode-subtask
     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-subtask-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-subtask-files-'));
     const outside = fs.mkdtempSync(
-      path.join(os.tmpdir(), 'omos-handoff-outside-'),
+      path.join(os.tmpdir(), 'omos-subtask-outside-'),
     );
     try {
       fs.writeFileSync(path.join(outside, 'secret.txt'), 'secret\n');

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

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

+ 6 - 6
src/tools/handoff/index.ts → src/tools/subtask/index.ts

@@ -1,23 +1,23 @@
 /**
- * Handoff functionality for session continuation.
+ * Subtask functionality for session continuation.
  *
- * Provides tools and commands for creating handoff prompts that allow
+ * Provides tools and commands for creating subtask prompts that allow
  * work to continue seamlessly in new sessions with preloaded context.
  */
 
 export {
-  createHandoffCommandManager,
-  type HandoffCommandManager,
+  createSubtaskCommandManager,
+  type SubtaskCommandManager,
 } from './command';
 export {
   buildSyntheticFileParts,
   FILE_REGEX,
   parseFileReferences,
 } from './files';
-export { createHandoffState, type HandoffState } from './state';
+export { createSubtaskState, type SubtaskState } from './state';
 export {
-  createHandoffSessionTool,
   createReadSessionTool,
+  createSubtaskTool,
   type OpencodeClient,
 } from './tools';
 export {

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

@@ -1,11 +1,11 @@
-export interface HandoffState {
+export interface SubtaskState {
   markSession(sessionID: string, sourceSessionID: string): void;
   unmarkSession(sessionID: string): void;
-  isHandoffSession(sessionID: string): boolean;
+  isSubtaskSession(sessionID: string): boolean;
   sourceFor(sessionID: string): string | undefined;
 }
 
-export function createHandoffState(): HandoffState {
+export function createSubtaskState(): SubtaskState {
   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 {
+    isSubtaskSession(sessionID: string): boolean {
       return sourceBySession.has(sessionID);
     },
     sourceFor(sessionID: string): string | undefined {

+ 19 - 19
src/tools/handoff/tools.test.ts → src/tools/subtask/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 { createSubtaskState } from './state';
+import { createReadSessionTool, createSubtaskTool } from './tools';
 
 function makeTempDir() {
-  return fs.mkdtempSync(path.join(os.tmpdir(), 'omos-handoff-tool-'));
+  return fs.mkdtempSync(path.join(os.tmpdir(), 'omos-subtask-tool-'));
 }
 
-describe('handoff_session tool', () => {
-  test('runs a worker child session and returns its handoff summary', async () => {
+describe('subtask tool', () => {
+  test('runs a worker child session and returns its subtask 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 = createSubtaskState();
+      const tool = createSubtaskTool(
         {
           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('<subtask_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: 'Subtask worker from ses_old',
         },
       });
       expect(sessionPrompt).toHaveBeenCalledTimes(1);
@@ -96,18 +96,18 @@ describe('handoff_session tool', () => {
     }
   });
 
-  test('blocks nested handoff calls from a handoff worker', async () => {
+  test('blocks nested subtask calls from a subtask worker', async () => {
     const directory = makeTempDir();
     try {
       let nestedResult = '';
-      const state = createHandoffState();
-      const tool = createHandoffSessionTool(
+      const state = createSubtaskState();
+      const tool = createSubtaskTool(
         {
           directory,
           client: {
             session: {
               abort: mock(async () => ({})),
-              create: mock(async () => ({ data: { id: 'ses_handoff' } })),
+              create: mock(async () => ({ data: { id: 'ses_subtask' } })),
               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 subtask' }, {
+                    sessionID: 'ses_subtask',
                   } as any),
                 );
               }),
@@ -130,11 +130,11 @@ describe('handoff_session tool', () => {
         new SubagentDepthTracker(),
       );
 
-      await tool.execute({ prompt: 'outer handoff' }, {
+      await tool.execute({ prompt: 'outer subtask' }, {
         sessionID: 'ses_old',
       } as any);
 
-      expect(nestedResult).toContain('Nested handoff is disabled');
+      expect(nestedResult).toContain('Nested subtask is disabled');
     } finally {
       fs.rmSync(directory, { recursive: true, force: true });
     }
@@ -159,7 +159,7 @@ describe('read_session tool', () => {
         },
       ],
     }));
-    const state = createHandoffState();
+    const state = createSubtaskState();
     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 = createSubtaskState();
     state.markSession('ses_worker', 'ses_old');
     const messages = mock(async () => ({ data: [] }));
 

+ 24 - 24
src/tools/handoff/tools.ts → src/tools/subtask/tools.ts

@@ -1,8 +1,8 @@
 /**
- * Tool definitions for handoff functionality.
+ * Tool definitions for subtask functionality.
  *
  * Factory functions that create tool definitions with injected dependencies:
- * - createHandoffSessionTool: Create a new session with handoff prompt
+ * - createSubtaskTool: Create a new session with subtask prompt
  * - 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 { SubtaskState } from './state';
 
 export type OpencodeClient = PluginInput['client'];
-const HANDOFF_TIMEOUT_MS = 5 * 60 * 1000;
+const SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
 
 /**
- * Create the handoff_session tool.
+ * Create the subtask tool.
  *
  * Takes the OpenCode client as a dependency for TUI and session operations.
  */
-export function createHandoffSessionTool(
+export function createSubtaskTool(
   ctx: PluginInput,
-  state: HandoffState,
+  state: SubtaskState,
   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 generated subtask prompt'),
       files: tool.schema
         .array(tool.schema.string())
         .optional()
@@ -50,15 +50,15 @@ 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.isSubtaskSession(sessionID)) {
+        return 'Nested subtask is disabled: this session is already a subtask 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 `Subtask 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.`;
@@ -80,7 +80,7 @@ export function createHandoffSessionTool(
           query: { directory },
           body: {
             parentID: sessionID === 'unknown' ? undefined : sessionID,
-            title: `Handoff worker from ${sessionID}`,
+            title: `Subtask 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('Subtask worker session did not return an id');
         }
         if (sessionID !== 'unknown' && depthTracker) {
           const registered = depthTracker.registerChild(
@@ -97,7 +97,7 @@ export function createHandoffSessionTool(
           );
           if (!registered) {
             throw new Error(
-              'Handoff worker blocked: max subagent depth exceeded',
+              'Subtask worker blocked: max subagent depth exceeded',
             );
           }
         }
@@ -121,7 +121,7 @@ export function createHandoffSessionTool(
               ],
             },
           },
-          HANDOFF_TIMEOUT_MS,
+          SUBTASK_TIMEOUT_MS,
         );
 
         const extraction = await extractSessionResult(client, childSessionID, {
@@ -129,15 +129,15 @@ export function createHandoffSessionTool(
           includeReasoning: false,
         });
         if (extraction.empty) {
-          throw new Error('Handoff worker returned no summary');
+          throw new Error('Subtask worker returned no summary');
         }
 
         return [
           `task_id: ${childSessionID}`,
           '',
-          '<handoff_summary>',
+          '<subtask_summary>',
           extraction.text,
-          '</handoff_summary>',
+          '</subtask_summary>',
         ].join('\n');
       } finally {
         if (childSessionID) {
@@ -148,7 +148,7 @@ export function createHandoffSessionTool(
             });
             state.unmarkSession(childSessionID);
           } catch {
-            // Keep the handoff marker if abort fails; session.deleted cleanup
+            // Keep the subtask marker if abort fails; session.deleted cleanup
             // will remove it when OpenCode eventually deletes the session.
           }
         }
@@ -235,11 +235,11 @@ function formatTranscript(
  */
 export function createReadSessionTool(
   client: OpencodeClient,
-  state: HandoffState,
+  state: SubtaskState,
 ): 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 conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the subtask summary.",
     args: {
       sessionID: tool.schema
         .string()
@@ -264,11 +264,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.isSubtaskSession(callerSessionID)) {
+        return 'read_session is only available from subtask 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 subtask worker.';
       }
 
       try {

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

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

+ 0 - 0
src/tools/handoff/vendor.ts → src/tools/subtask/vendor.ts