Browse Source

Remove auto-continue feature

Alvin Unreal 2 months ago
parent
commit
e986dabfa1

+ 1 - 1
.slim/clonedeps.json

@@ -1,6 +1,6 @@
 {
   "version": "1.0.0",
-  "updatedAt": "2026-05-12T00:00:00.000Z",
+  "updatedAt": "2026-05-25T00:00:00.000Z",
   "dependencies": [
     {
       "name": "@opencode-ai/plugin",

+ 0 - 3
.slim/codemap.json

@@ -95,8 +95,6 @@
     "src/hooks/phase-reminder/index.ts": "55c78ab86f3b26a071c8e2f639831b8b",
     "src/hooks/post-file-tool-nudge/index.ts": "7a23d01b3396c4018015e0e90629c45d",
     "src/hooks/task-session-manager/index.ts": "a9d701588ceefa4b40454e27c1ed2ea1",
-    "src/hooks/todo-continuation/index.ts": "4bc29a79ce7d85acc120c87a02cd09d2",
-    "src/hooks/todo-continuation/todo-hygiene.ts": "08374710c80e24ca55b78a1ff5ed7a81",
     "src/index.ts": "fa92b5f3491ff9a2ba8a1ccf3e631ba2",
     "src/interview/dashboard.ts": "dbe6703d036ff16952c98f5cc0066e5c",
     "src/interview/document.ts": "c8c35c9042fdef497925c89ce1dba1b4",
@@ -174,7 +172,6 @@
     "src/hooks/phase-reminder": "80f01bd7edd895fcd3950a44b21a4d3a",
     "src/hooks/post-file-tool-nudge": "e01c0aa6e649ec049c068d6a1b2006f9",
     "src/hooks/task-session-manager": "0c73238a14d84eb1d204f9cc0044180d",
-    "src/hooks/todo-continuation": "f3622b6cc650fcd74043cde96b56b9f3",
     "src/interview": "3920f8d94c932173803d6fdd8506b9b3",
     "src/mcp": "5f5fc5fbb54bf9944063483cee8be88f",
     "src/multiplexer": "f543dda4ba0043e6c5e4ea0c07e11a77",

+ 0 - 1
README.ja-JP.md

@@ -501,7 +501,6 @@ ping all agents
 | **[Council](docs/council.md)** | 複数のモデルを並列実行し、`@council` で 1 つの回答に統合します |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | エージェントの動作を Tmux や Zellij のペインでライブ表示します |
 | **[Session Management](docs/session-management.md)** | 短いエイリアスで最近の子エージェントセッションを再利用し、最初からやり直さずに済みます |
-| **[Todo Continuation](docs/todo-continuation.md)** | クールダウンと安全チェック付きで Orchestrator セッションを自動継続します |
 | **[Preset Switching](docs/preset-switching.md)** | `/preset` で実行時にエージェントモデルのプリセットを切り替えます |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
 | **[Subtask](docs/subtask.md)** | `/subtask` で境界が明確な子ワーカーを実行し、構造化された要約をメインセッションに返します |

+ 0 - 1
README.md

@@ -503,7 +503,6 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[V2 Background Orchestration](docs/v2-background-orchestration.md)** | Scheduler-first orchestrator model built around native background subagents |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux or Zellij panes |
 | **[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` |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | Define your own specialists with custom prompts, models, MCP access, and Orchestrator delegation rules |
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |

+ 0 - 1
README.zh-CN.md

@@ -495,7 +495,6 @@ ping all agents
 | **[Council (议会) (docs/council.md)](docs/council.md)** | 使用 `@council` 并行运行多个模型并合成单一答案 |
 | **[多路复用器集成 (docs/multiplexer-integration.md)](docs/multiplexer-integration.md)** | 在 Tmux 或 Zellij 窗格中实时观看智能体的工作过程 |
 | **[会话管理 (docs/session-management.md)](docs/session-management.md)** | 使用短别名复用最近的子智能体会话,而不是重新开始 |
-| **[待办事项持续执行 (docs/todo-continuation.md)](docs/todo-continuation.md)** | 具备冷却时间和安全检查的编排者会话自动持续执行 |
 | **[运行时预设切换 (docs/preset-switching.md)](docs/preset-switching.md)** | 在运行时使用 `/preset` 切换智能体模型预设 |
 | **[自定义智能体 (docs/configuration.md#custom-agents)](docs/configuration.md#custom-agents)** | 自定义专家智能体:配置独特的提示词、模型、MCP 权限和编排者委派规则 |
 | **[子任务 (docs/subtask.md)](docs/subtask.md)** | 使用 `/subtask` 运行受限的子工作器,并将结构化总结返回到主会话 |

+ 0 - 1
codemap.md

@@ -42,7 +42,6 @@ This codemap intentionally covers the plugin repository itself and excludes the
 | `src/hooks/phase-reminder/` | Message-transform reminder enforcing orchestrator workflow phases. | [View Map](src/hooks/phase-reminder/codemap.md) |
 | `src/hooks/post-file-tool-nudge/` | Post-read/write reminder path that nudges delegation-aware next steps. | [View Map](src/hooks/post-file-tool-nudge/codemap.md) |
 | `src/hooks/task-session-manager/` | Resumable `task` session tracking, short alias resolution, prompt injection, and stale-session cleanup. | [View Map](src/hooks/task-session-manager/codemap.md) |
-| `src/hooks/todo-continuation/` | Auto-continue behavior for outstanding todo execution. | [View Map](src/hooks/todo-continuation/codemap.md) |
 | `src/interview/` | `/interview` feature: per-session and dashboard prompt/state orchestration, persistence, local UI, and cross-process coordination. | [View Map](src/interview/codemap.md) |
 | `src/mcp/` | Built-in MCP registry and per-provider MCP definitions. | [View Map](src/mcp/codemap.md) |
 | `src/multiplexer/` | Terminal multiplexer abstraction layer with backend selection, session mirroring, polling fallback, and shutdown lifecycle orchestration. | [View Map](src/multiplexer/codemap.md) |

+ 1 - 7
docs/authors-preset.md

@@ -32,12 +32,6 @@ This is the exact configuration the author runs day-to-day.
         "gamma": { "model": "fireworks-ai/accounts/fireworks/routers/kimi-k2p5-turbo" }
       }
     }
-  },
-  "todoContinuation": {
-    "maxContinuations": 50,
-    "cooldownMs": 3000,
-    "autoEnable": false,
-    "autoEnableThreshold": 4
-  } 
+  }
 }
 ```

+ 0 - 4
docs/configuration.md

@@ -136,10 +136,6 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `council.timeout` | number | `180000` | Per-councillor timeout (ms) |
 | `council.councillor_execution_mode` | string | `"parallel"` | Run councillors in `parallel` or `serial`; use `serial` for single-model setups |
 | `council.councillor_retries` | number | `3` | Max retries per councillor on empty provider response (0–5) |
-| `todoContinuation.maxContinuations` | integer | `5` | Max consecutive auto-continuations before stopping (1–50) |
-| `todoContinuation.cooldownMs` | integer | `3000` | Delay in ms before auto-continuing — gives user time to abort (0–30000) |
-| `todoContinuation.autoEnable` | boolean | `false` | Automatically enable auto-continue when session has enough todos |
-| `todoContinuation.autoEnableThreshold` | integer | `4` | Number of todos that triggers auto-enable (only used when `autoEnable` is true, 1–50) |
 | `interview.maxQuestions` | integer | `2` | Max questions per interview round (1–10) |
 | `interview.outputFolder` | string | `"interview"` | Directory where interview markdown files are written (relative to project root) |
 | `interview.autoOpenBrowser` | boolean | `true` | Automatically open the interview UI in your default browser during interactive runs; suppressed in tests and CI |

+ 0 - 1
docs/quick-reference.md

@@ -16,7 +16,6 @@
 | [V2 Background Orchestration](v2-background-orchestration.md) | Scheduler-first orchestrator model for native background subagents |
 | [Interview](interview.md) | `/interview` command, browser UI, dashboard mode, multi-session coordination |
 | [Multiplexer Integration](multiplexer-integration.md) | Real-time pane monitoring, layouts, troubleshooting |
-| [Todo Continuation](todo-continuation.md) | `auto_continue`, `/auto-continue`, cooldowns, safety gates |
 | [Preset Switching](preset-switching.md) | `/preset` command for runtime agent model switching |
 | [Codemap Skill](codemap.md) | Hierarchical codemap generation |
 

+ 0 - 45
docs/todo-continuation.md

@@ -1,45 +0,0 @@
-# Todo Continuation
-
-Auto-continue the orchestrator when it stops with incomplete todos. Opt-in only — nothing resumes automatically unless you enable it.
-
-## Controls
-
-| Tool / Command | Description |
-|----------------|-------------|
-| `auto_continue` | Toggle auto-continuation. Call with `{ enabled: true }` to activate, `{ enabled: false }` to disable |
-| `/auto-continue` | Slash command shortcut. Accepts `on`, `off`, or toggles with no argument |
-
-## How It Works
-
-1. When the orchestrator goes idle with incomplete todos, a countdown notification appears
-2. After the cooldown (default 3s), a continuation prompt is injected and the orchestrator resumes work
-3. Press Esc×2 during the cooldown or after injection to stop it
-
-## Safety Gates
-
-All of these must pass before continuation happens:
-
-- Auto-continue is enabled
-- The session is the orchestrator
-- Incomplete todos exist
-- The last assistant message is not a question
-- The consecutive continuation count is under the limit
-- The session is not in the post-abort suppress window (5s)
-- No pending injection is already in flight
-
-## Configuration
-
-Configure it in `~/.config/opencode/oh-my-opencode-slim.json` or `~/.config/opencode/oh-my-opencode-slim.jsonc`:
-
-```jsonc
-{
-  "todoContinuation": {
-    "maxContinuations": 5,      // Max consecutive auto-continuations (1–50)
-    "cooldownMs": 3000,         // Delay before each continuation (0–30000)
-    "autoEnable": false,        // Auto-enable when session has enough todos
-    "autoEnableThreshold": 4    // Number of todos to trigger auto-enable
-  }
-}
-```
-
-> See [Configuration](configuration.md) for the full option reference.

+ 0 - 8
docs/tools.md

@@ -59,11 +59,3 @@ Includes Prettier, Biome, `gofmt`, `rustfmt`, `ruff`, and 20+ others.
 > See the [official OpenCode docs](https://opencode.ai/docs/formatters/#built-in) for the complete list.
 
 ---
-
-## Todo Continuation
-
-Auto-continue has its own guide now:
-
-- [Todo Continuation](todo-continuation.md) — controls, safety gates, behavior, and config
-
----

+ 0 - 2
docs/v2-background-orchestration.md

@@ -316,8 +316,6 @@ Important areas:
   notifications, Divoom, multiplexer, and cleanup behavior.
 - `src/multiplexer/` — verify panes stay attached to running background child
   sessions while the parent continues.
-- `src/hooks/todo-continuation/` — avoid marking workflows complete before
-  relevant background tasks have terminal results.
 
 ---
 

+ 3 - 40
docs/v2_core.md

@@ -8,8 +8,7 @@ Scope for this pass:
 - scheduler/job-board behavior,
 - `task` and `task_status` integration,
 - task-session-manager changes,
-- tmux/zellij multiplexer compatibility,
-- todo-continuation guardrails.
+- tmux/zellij multiplexer compatibility.
 
 Out of scope for this pass:
 
@@ -151,7 +150,6 @@ formatForPrompt(parentSessionID)
 Then pass the shared state into:
 
 - task-session-manager,
-- todo-continuation,
 - any future prompt/system-context hook that needs scheduler state.
 
 ### Reconciliation rule
@@ -316,7 +314,7 @@ Current behavior:
 
 - `src/index.ts` creates one shared `BackgroundJobBoard` using
   `backgroundJobs` caps/context config and passes it to task-session-manager,
-  todo-continuation, cancel-task, and multiplexer integration.
+  cancel-task, and multiplexer integration.
 - `tool.execute.before(task)` validates `subagent_type`, strips stale/invalid
   `task_id` aliases when they cannot safely resolve, and only resolves reusable
   aliases for matching completed/reconciled jobs.
@@ -498,34 +496,6 @@ Potential later improvement:
 
 ---
 
-## Todo Continuation Guardrails
-
-Primary file:
-
-- `src/hooks/todo-continuation/index.ts`
-
-Risk:
-
-- parent orchestrator becomes idle while background jobs are still running,
-- auto-continuation assumes the workflow can proceed or finish,
-- dependent work advances too early.
-
-V2 rule:
-
-```text
-If relevant background jobs are running, continuation should poll/reconcile them
-instead of treating the workflow as complete.
-```
-
-Implementation direction:
-
-- expose a `hasRunningBackgroundJobs(parentSessionID)` query from the scheduler
-  state,
-- expose `hasTerminalUnreconciledJobs(parentSessionID)`,
-- have continuation reminders nudge toward `task_status` and reconciliation.
-
----
-
 ## Agent Lane Reframing
 
 V2 should describe specialists as execution lanes, not optional helpers.
@@ -594,13 +564,6 @@ prompt can rely on visible scheduler state.
 - add tests for delayed completion,
 - adjust close-on-idle only if native events prove insufficient.
 
-### Phase 8 — Todo Continuation Safety
-
-- prevent auto-continuation from finalizing while jobs run,
-- nudge the orchestrator to poll terminal states and reconcile.
-
----
-
 ## First Code Targets
 
 Start here:
@@ -641,7 +604,7 @@ Core V2 is working when:
 - dependent work waits for terminal results,
 - prompt-level advisory ownership reduces conflicting background workers,
 - multiplexer panes show background child sessions while parent continues,
-- todo-continuation does not finalize with unresolved background jobs.
+- final responses do not depend on unresolved background jobs.
 
 The core invariant:
 

+ 0 - 31
oh-my-opencode-slim.schema.json

@@ -568,37 +568,6 @@
         }
       }
     },
-    "todoContinuation": {
-      "type": "object",
-      "properties": {
-        "maxContinuations": {
-          "default": 5,
-          "description": "Maximum consecutive auto-continuations before stopping to ask user",
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 50
-        },
-        "cooldownMs": {
-          "default": 3000,
-          "description": "Delay in ms before auto-continuing (gives user time to abort)",
-          "type": "integer",
-          "minimum": 0,
-          "maximum": 30000
-        },
-        "autoEnable": {
-          "default": false,
-          "description": "Automatically enable auto-continue when the orchestrator session has enough todos",
-          "type": "boolean"
-        },
-        "autoEnableThreshold": {
-          "default": 4,
-          "description": "Number of todos that triggers auto-enable (only used when autoEnable is true)",
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 50
-        }
-      }
-    },
     "fallback": {
       "type": "object",
       "properties": {

+ 0 - 7
src/agents/orchestrator.ts

@@ -231,13 +231,6 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
 - If multiple remembered sessions fit, prefer the most recently used matching session.
 - Prefer re-uses over creating new sessions all the time
 
-### Auto-Continue
-When working through multi-step tasks, consider enabling auto-continue to avoid stopping between batches:
-- **Enable when:** User requests autonomous/batch work, or you create 4+ todos in a session
-- **Don't enable when:** User is in an interactive/conversational flow, or each step needs explicit review
-- Use the \`auto_continue\` tool with \`enabled: true\` to activate. The system will automatically resume you when incomplete todos remain after you stop.
-- The user can toggle this anytime via the \`/auto-continue\` command.
-
 ### Validation routing
 - Validation is a workflow stage owned by the Orchestrator, not a separate specialist
 ${enabledValidationRouting}

+ 0 - 40
src/config/schema.ts

@@ -213,45 +213,6 @@ export const DivoomConfigSchema = z.object({
 
 export type DivoomConfig = z.infer<typeof DivoomConfigSchema>;
 
-// Todo continuation configuration
-export const TodoContinuationConfigSchema = z.object({
-  maxContinuations: z
-    .number()
-    .int()
-    .min(1)
-    .max(50)
-    .default(5)
-    .describe(
-      'Maximum consecutive auto-continuations before stopping to ask user',
-    ),
-  cooldownMs: z
-    .number()
-    .int()
-    .min(0)
-    .max(30_000)
-    .default(3000)
-    .describe('Delay in ms before auto-continuing (gives user time to abort)'),
-  autoEnable: z
-    .boolean()
-    .default(false)
-    .describe(
-      'Automatically enable auto-continue when the orchestrator session has enough todos',
-    ),
-  autoEnableThreshold: z
-    .number()
-    .int()
-    .min(1)
-    .max(50)
-    .default(4)
-    .describe(
-      'Number of todos that triggers auto-enable (only used when autoEnable is true)',
-    ),
-});
-
-export type TodoContinuationConfig = z.infer<
-  typeof TodoContinuationConfigSchema
->;
-
 export const FailoverConfigSchema = z.object({
   enabled: z.boolean().default(true),
   timeoutMs: z.number().min(0).default(15000),
@@ -334,7 +295,6 @@ export const PluginConfigSchema = z
     interview: InterviewConfigSchema.optional(),
     backgroundJobs: BackgroundJobsConfigSchema.optional(),
     divoom: DivoomConfigSchema.optional(),
-    todoContinuation: TodoContinuationConfigSchema.optional(),
     fallback: FailoverConfigSchema.optional(),
     council: CouncilConfigSchema.optional(),
   })

+ 4 - 8
src/hooks/codemap.md

@@ -51,11 +51,11 @@ and managers for all hook-based runtime behaviors used by
 | `tool.execute.before` | Pre-process tool inputs | `apply-patch`, `task-session-manager` |
 | `tool.execute.after` | Post-process tool outputs | `delegate-task-retry`, `json-error-recovery`, `post-file-tool-nudge`, `task-session-manager` |
 | `experimental.chat.messages.transform` | Rewrite outbound user content | `filter-available-skills`, `phase-reminder` |
-| `experimental.chat.system.transform` | Inject system-level directives | `todo-continuation`, `post-file-tool-nudge`, `task-session-manager` |
+| `experimental.chat.system.transform` | Inject system-level directives | `post-file-tool-nudge`, `task-session-manager` |
 | `chat.headers` | Mutate request headers | `chat-headers` |
-| `chat.message` | Track runtime session/agent mapping | `todo-continuation` |
-| `command.execute.before` | Handle slash-command UX | `todo-continuation` (`auto-continue`) |
-| `event` | React to session lifecycle and runtime failures | `foreground-fallback`, `todo-continuation`, `post-file-tool-nudge`, `auto-update-checker`, multiplexer managers, `task-session-manager` |
+| `chat.message` | Track runtime session/agent mapping | `src/index.ts` session map |
+| `command.execute.before` | Handle slash-command UX | `interview`, `preset-manager`, `deepwork` |
+| `event` | React to session lifecycle and runtime failures | `foreground-fallback`, `post-file-tool-nudge`, `auto-update-checker`, multiplexer managers, `task-session-manager` |
 
 ## Implementation Notes
 
@@ -64,10 +64,6 @@ and managers for all hook-based runtime behaviors used by
 - `ForegroundFallbackManager` listens to event traffic and remediates
   foreground rate-limit failures by aborting the current prompt and re-queuing the
   latest user message on the next model in a per-agent chain.
-- `createTodoContinuationHook` spans multiple surfaces: message transform,
-  system transform, command interception, tool-after, and events. It owns
-  auto-injection state, cooldown, suppress windows, and orchestration session
-  tracking.
 - `createTaskSessionManagerHook` tracks V2 background jobs and reusable completed sessions: generates
   user-facing aliases, resolves alias/task IDs before delegation, remembers fresh
   task IDs after completion, and drops stale entries on missing-session failure,

+ 0 - 1
src/hooks/index.ts

@@ -14,4 +14,3 @@ export { createJsonErrorRecoveryHook } from './json-error-recovery';
 export { createPhaseReminderHook } from './phase-reminder';
 export { createPostFileToolNudgeHook } from './post-file-tool-nudge';
 export { createTaskSessionManagerHook } from './task-session-manager';
-export { createTodoContinuationHook } from './todo-continuation';

+ 0 - 77
src/hooks/todo-continuation/codemap.md

@@ -1,77 +0,0 @@
-# src/hooks/todo-continuation/
-
-## Responsibility
-
-Implements orchestrator-only auto-continuation for incomplete todo lists with
-strict safety controls so automation does not loop or fight the user. It also
-hosts todo-state hygiene reminders after relevant tool actions.
-
-## Design
-
-- `index.ts` exports `createTodoContinuationHook(ctx, config?)`, returning:
-  - `handleMessagesTransform`
-  - `handleChatSystemTransform`
-  - `handleToolExecuteAfter`
-  - `handleEvent`
-  - `handleChatMessage`
-  - `handleCommandExecuteBefore`
-  - `tool` map containing `auto_continue`
-- State model (`ContinuationState`) tracks:
-  - enabled flag, consecutive continuation count, cooldown timer
-  - suppression window after abort, orchestrator session IDs
-  - in-flight notification and injection guards
-- `todo-hygiene.ts` owns lightweight reminder arming/injection using
-  todo-queue transitions and message-context signals.
-- Request signatures are used in `handleMessagesTransform` to avoid duplicate
-  per-request work.
-
-## Flow
-
-### Auto-continuation path
-
-1. `handleMessagesTransform` identifies the latest external user message,
-   infers session/agent, and starts a new continuation cycle for orchestrator
-   sessions.
-2. On `session.idle`/idle `session.status`, if enabled, the hook validates:
-   incomplete todos, non-question last assistant message, max-continuation limit,
-   suppress/notification guard, and timer/injection state.
-3. If all guards pass, it schedules a cooldown timer and sends a lightweight
-   no-reply notification via `session.prompt`.
-4. After cooldown, it injects `CONTINUATION_PROMPT` via `session.prompt`, updates
-   `consecutiveContinuations`, and logs progress.
-5. Event handling resets counters, clears pending timers, or applies a short
-   suppression window on abort-like errors.
-6. On `session.deleted`, orchestrator session state is torn down and notification
-   state is cleared.
-
-### Command path
-
-1. `handleCommandExecuteBefore` intercepts `/auto-continue` before runtime
-   execution.
-2. It toggles enabled state (`on`, `off`, or flip), clears timers as needed,
-   and injects a direct status response into output parts.
-3. When enabling and todos are pending, it appends continuation-ready status
-   text; when no todos remain, it reports that state.
-
-### Todo hygiene path
-
-1. `createTodoHygiene.handleToolExecuteAfter` arms reminders after supported
-   tooling activity, with reset/ignore rules for specific tools.
-2. `createTodoHygiene.handleChatSystemTransform` injects one reminder per request
-   when open todos remain (`TODO_HYGIENE_REMINDER` or
-   `TODO_FINAL_ACTIVE_REMINDER`).
-3. `handleEvent` clears hygiene state on `session.deleted`.
-
-## Integration
-
-- Registered in `src/index.ts` across:
-  - `experimental.chat.messages.transform`
-  - `experimental.chat.system.transform`
-  - `chat.message`
-  - `command.execute.before`
-  - `event`
-  - `tool.execute.after`
-- Uses shared utilities: `log`, `createInternalAgentTextPart`, and
-  `SLIM_INTERNAL_INITIATOR_MARKER`.
-- Session/agent identity is coordinated with `session.message` events and
-  maintained in the plugin for serve-mode routing consistency.

+ 0 - 3127
src/hooks/todo-continuation/index.test.ts

@@ -1,3127 +0,0 @@
-import { describe, expect, mock, test } from 'bun:test';
-import {
-  BackgroundJobBoard,
-  SLIM_INTERNAL_INITIATOR_MARKER,
-} from '../../utils';
-import { createTodoContinuationHook } from './index';
-import {
-  TODO_FINAL_ACTIVE_REMINDER,
-  TODO_HYGIENE_REMINDER,
-} from './todo-hygiene';
-
-describe('createTodoContinuationHook', () => {
-  function createMockContext(overrides?: {
-    todoResult?: {
-      data?: Array<{
-        id: string;
-        content: string;
-        status: string;
-        priority: string;
-      }>;
-    };
-    messagesResult?: {
-      data?: Array<{
-        info?: { role?: string };
-        parts?: Array<{ type?: string; text?: string }>;
-      }>;
-    };
-  }) {
-    return {
-      client: {
-        session: {
-          todo: mock(async () => overrides?.todoResult ?? { data: [] }),
-          messages: mock(async () => overrides?.messagesResult ?? { data: [] }),
-          prompt: mock(async () => ({})),
-        },
-      },
-    } as any;
-  }
-
-  async function delay(ms: number): Promise<void> {
-    await new Promise((resolve) => setTimeout(resolve, ms));
-  }
-
-  // Notification prompts (noReply:true, no marker) fire immediately when
-  // scheduling a continuation. These helpers check only for actual
-  // continuation prompts (with SLIM_INTERNAL_INITIATOR_MARKER).
-  function hasContinuation(m: ReturnType<typeof mock>): boolean {
-    return m.mock.calls.some((c: any[]) =>
-      (c[0]?.body?.parts as any[])?.some((p: any) =>
-        p.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER),
-      ),
-    );
-  }
-  function contCount(m: ReturnType<typeof mock>): number {
-    return m.mock.calls.filter((c: any[]) =>
-      (c[0]?.body?.parts as any[])?.some((p: any) =>
-        p.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER),
-      ),
-    ).length;
-  }
-  function contCall(m: ReturnType<typeof mock>): any[] {
-    const call = m.mock.calls.find((c: any[]) =>
-      (c[0]?.body?.parts as any[])?.some((p: any) =>
-        p.text?.includes(SLIM_INTERNAL_INITIATOR_MARKER),
-      ),
-    );
-    if (!call) {
-      throw new Error('No continuation call found');
-    }
-    return call;
-  }
-
-  function userMessages(
-    text: string,
-    sessionID = 'main1',
-    agent?: string,
-    parts?: Array<{ type: string; text?: string }>,
-    id?: string,
-  ) {
-    return {
-      messages: [
-        {
-          info: { id, role: 'user', agent, sessionID },
-          parts: parts ?? [{ type: 'text', text }],
-        },
-      ],
-    };
-  }
-
-  function allMessageText(output: {
-    messages: Array<{ parts: Array<{ type?: string; text?: string }> }>;
-  }) {
-    return output.messages
-      .flatMap((message) => message.parts)
-      .filter((part) => part.type === 'text' && typeof part.text === 'string')
-      .map((part) => part.text)
-      .join('\n');
-  }
-
-  describe('tool toggle', () => {
-    test('calling auto_continue execute with { enabled: true } sets state', async () => {
-      const ctx = createMockContext();
-      const hook = createTodoContinuationHook(ctx);
-
-      const result = await hook.tool.auto_continue.execute({ enabled: true });
-
-      expect(result).toContain('Auto-continue enabled');
-      expect(result).toContain('up to 5');
-    });
-
-    test('calling auto_continue execute with { enabled: false } disables', async () => {
-      const ctx = createMockContext();
-      const hook = createTodoContinuationHook(ctx);
-
-      const result = await hook.tool.auto_continue.execute({ enabled: false });
-
-      expect(result).toBe('Auto-continue disabled.');
-    });
-  });
-
-  describe('todo hygiene routing', () => {
-    test('does not inject hygiene reminder for unknown non-orchestrator session', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const toolOutput = { output: 'task result' };
-
-      await hook.handleMessagesTransform(
-        userMessages('continue previous work', 'sub1', 'explorer'),
-      );
-      await hook.handleToolExecuteAfter(
-        { tool: 'task', sessionID: 'sub1' },
-        toolOutput,
-      );
-
-      expect(toolOutput.output).toBe('task result');
-      expect(toolOutput.output).not.toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('does not expose a system transform handler', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-
-      expect('handleChatSystemTransform' in hook).toBe(false);
-    });
-
-    test('injects hygiene reminder into latest user message after todowrite activity', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = userMessages('primera request', 'main1', 'orchestrator');
-      const toolOutput = { output: 'read result' };
-
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter(
-        { tool: 'read', sessionID: 'main1' },
-        toolOutput,
-      );
-      await hook.handleMessagesTransform(output);
-
-      expect(toolOutput.output).toBe('read result');
-      expect(allMessageText(output)).toContain(TODO_HYGIENE_REMINDER);
-      expect(allMessageText(output)).toContain(
-        '<instruction name="todo_hygiene">',
-      );
-    });
-
-    test('skips hygiene reminder when todo state lookup times out', async () => {
-      const ctx = createMockContext();
-      ctx.client.session.todo = mock(() => new Promise(() => {}));
-      const hook = createTodoContinuationHook(ctx);
-      const output = userMessages('primera request', 'main1', 'orchestrator');
-
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-      await hook.handleMessagesTransform(output);
-
-      expect(allMessageText(output)).not.toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('compaction-like transform does not consume pending reminder', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const live = userMessages('primera request', 'main1', 'orchestrator');
-      const compactionClone = structuredClone(live);
-
-      await hook.handleMessagesTransform(live);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-
-      await hook.handleMessagesTransform(compactionClone);
-      expect(allMessageText(compactionClone)).toContain(TODO_HYGIENE_REMINDER);
-
-      await hook.handleMessagesTransform(live);
-      expect(allMessageText(live)).toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('new request clears stale pending reminder state', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const first = userMessages('primera request', 'main1', 'orchestrator');
-      const blocked = userMessages(
-        'segunda request distinta',
-        'main1',
-        'orchestrator',
-      );
-      const allowed = userMessages(
-        'segunda request distinta',
-        'main1',
-        'orchestrator',
-      );
-
-      await hook.handleMessagesTransform(first);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-
-      await hook.handleMessagesTransform(blocked);
-      expect(allMessageText(blocked)).not.toContain(TODO_HYGIENE_REMINDER);
-
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-      await hook.handleMessagesTransform(allowed);
-
-      expect(allMessageText(allowed)).toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('attachment-only requests reset stale state without synthetic text parts', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const first = userMessages('primera request', 'main1', 'orchestrator');
-      const attachmentOnly = userMessages('', 'main1', 'orchestrator', [
-        { type: 'image' },
-      ]);
-
-      await hook.handleMessagesTransform(first);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-
-      await hook.handleMessagesTransform(attachmentOnly);
-
-      expect(attachmentOnly.messages[0].parts).toHaveLength(1);
-      expect(allMessageText(attachmentOnly)).not.toContain(
-        TODO_HYGIENE_REMINDER,
-      );
-    });
-
-    test('falls back to known orchestrator session when transform message lacks sessionID', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = {
-        messages: [
-          {
-            info: { role: 'user', agent: 'orchestrator' },
-            parts: [{ type: 'text', text: 'new request boundary' }],
-          },
-        ],
-      };
-
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleMessagesTransform(output);
-
-      expect(allMessageText(output)).toContain(TODO_FINAL_ACTIVE_REMINDER);
-    });
-
-    test('does not promote sessions with missing agent metadata to orchestrator', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const toolOutput = { output: 'task result' };
-
-      await hook.handleMessagesTransform(
-        userMessages('continue previous work', 'sub1'),
-      );
-      await hook.handleToolExecuteAfter(
-        { tool: 'task', sessionID: 'sub1' },
-        toolOutput,
-      );
-
-      expect(toolOutput.output).toBe('task result');
-      expect(toolOutput.output).not.toContain(TODO_HYGIENE_REMINDER);
-      expect(toolOutput.output).not.toContain(TODO_FINAL_ACTIVE_REMINDER);
-    });
-
-    test('known orchestrator sessions still process request boundaries when agent metadata is missing', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = userMessages('new request boundary', 'main1');
-
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleMessagesTransform(output);
-
-      expect(allMessageText(output)).toContain(TODO_FINAL_ACTIVE_REMINDER);
-    });
-
-    test('the same user message id consumes pending reminder even if array index shifts', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const shifted = {
-        messages: [
-          {
-            info: { role: 'assistant', sessionID: 'main1' },
-            parts: [{ type: 'text', text: 'intermediate output' }],
-          },
-          {
-            info: {
-              id: 'u1',
-              role: 'user',
-              agent: 'orchestrator',
-              sessionID: 'main1',
-            },
-            parts: [{ type: 'text', text: 'request boundary' }],
-          },
-        ],
-      };
-
-      await hook.handleMessagesTransform(
-        userMessages(
-          'request boundary',
-          'main1',
-          'orchestrator',
-          undefined,
-          'u1',
-        ),
-      );
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-      await hook.handleMessagesTransform(shifted);
-
-      expect(allMessageText(shifted)).toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('a new user message id resets the request even if text is unchanged', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const blocked = userMessages(
-        'same text',
-        'main1',
-        'orchestrator',
-        undefined,
-        'u2',
-      );
-      const allowed = userMessages(
-        'same text',
-        'main1',
-        'orchestrator',
-        undefined,
-        'u2',
-      );
-
-      await hook.handleMessagesTransform(
-        userMessages('same text', 'main1', 'orchestrator', undefined, 'u1'),
-      );
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-
-      await hook.handleMessagesTransform(blocked);
-      expect(allMessageText(blocked)).not.toContain(TODO_HYGIENE_REMINDER);
-
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-      await hook.handleMessagesTransform(allowed);
-
-      expect(allMessageText(allowed)).toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('a repeated text without message ids resets when a later user turn appears', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const blocked = {
-        messages: [
-          {
-            info: { role: 'user', agent: 'orchestrator', sessionID: 'main1' },
-            parts: [{ type: 'text', text: 'same text' }],
-          },
-          {
-            info: { role: 'assistant', sessionID: 'main1' },
-            parts: [{ type: 'text', text: 'intermediate output' }],
-          },
-          {
-            info: { role: 'user', agent: 'orchestrator', sessionID: 'main1' },
-            parts: [{ type: 'text', text: 'same text' }],
-          },
-        ],
-      };
-      const allowed = structuredClone(blocked);
-
-      await hook.handleMessagesTransform(
-        userMessages('same text', 'main1', 'orchestrator'),
-      );
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-
-      await hook.handleMessagesTransform(blocked);
-      expect(allMessageText(blocked)).not.toContain(TODO_HYGIENE_REMINDER);
-
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-      await hook.handleMessagesTransform(allowed);
-
-      expect(allMessageText(allowed)).toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('messages without inferable sessionID clear stale state for known orchestrators', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const unknown = {
-        messages: [
-          {
-            info: { role: 'user', agent: 'orchestrator' },
-            parts: [{ type: 'text', text: 'boundary without session id' }],
-          },
-        ],
-      };
-
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-      hook.handleChatMessage({ sessionID: 'main2', agent: 'orchestrator' });
-      await hook.handleMessagesTransform(
-        userMessages('first request', 'main1', 'orchestrator', undefined, 'u1'),
-      );
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 'main1' });
-
-      await hook.handleMessagesTransform(unknown);
-
-      expect(allMessageText(unknown)).not.toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('does not inject from continuation-like wording alone', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const toolOutput = { output: 'read result' };
-
-      await hook.handleMessagesTransform(
-        userMessages(
-          'sigue este formato pero empieza de cero',
-          'main1',
-          'orchestrator',
-        ),
-      );
-      await hook.handleToolExecuteAfter(
-        { tool: 'read', sessionID: 'main1' },
-        toolOutput,
-      );
-
-      expect(toolOutput.output).toBe('read result');
-      expect(toolOutput.output).not.toContain(TODO_HYGIENE_REMINDER);
-      expect(toolOutput.output).not.toContain(TODO_FINAL_ACTIVE_REMINDER);
-    });
-
-    test('rearms on activity after todowrite even if request wording is continuation-like', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = userMessages(
-        'finish the previous work',
-        'main1',
-        'orchestrator',
-      );
-
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleMessagesTransform(output);
-
-      expect(allMessageText(output)).toContain(TODO_FINAL_ACTIVE_REMINDER);
-    });
-
-    test('final active todo after todowrite uses the stronger finishing reminder', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = userMessages('haz esto', 'main1', 'orchestrator');
-
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleMessagesTransform(output);
-
-      expect(allMessageText(output)).toContain(TODO_FINAL_ACTIVE_REMINDER);
-      expect(allMessageText(output)).not.toContain(TODO_HYGIENE_REMINDER);
-    });
-
-    test('todo hygiene reminder includes unreconciled background results', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'in_progress',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const board = new BackgroundJobBoard();
-      board.registerLaunch({
-        taskID: 'child-1',
-        parentSessionID: 'main1',
-        agent: 'oracle',
-        description: 'review plan',
-      });
-      board.updateStatus({ taskID: 'child-1', state: 'completed' });
-      const hook = createTodoContinuationHook(ctx, {
-        backgroundJobBoard: board,
-      });
-      const output = userMessages('haz esto', 'main1', 'orchestrator');
-
-      await hook.handleMessagesTransform(output);
-      await hook.handleToolExecuteAfter({
-        tool: 'todowrite',
-        sessionID: 'main1',
-      });
-      await hook.handleMessagesTransform(output);
-
-      expect(allMessageText(output)).toContain(
-        'Background jobs have terminal results: reconcile the Background Job Board results before finalizing.',
-      );
-    });
-  });
-
-  describe('continuation scheduling', () => {
-    test('session idle + enabled + incomplete todos → schedules continuation', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-            { id: '2', content: 'todo2', status: 'completed', priority: 'low' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Here is the result' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-      });
-
-      // Enable auto-continue
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Fire session.idle event
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait for cooldown
-      await delay(60);
-
-      // Verify session.prompt was called with continuation prompt
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      const promptCall = contCall(ctx.client.session.prompt);
-      expect(promptCall[0].path.id).toBe('session-123');
-      expect(promptCall[0].body.parts[0].text).toContain(
-        '[Auto-continue: enabled - there are incomplete todos remaining.',
-      );
-      expect(promptCall[0].body.parts[0].text).toContain(
-        SLIM_INTERNAL_INITIATOR_MARKER,
-      );
-    });
-
-    test('continuation prompt nudges toward running background job status', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Continuing' }],
-            },
-          ],
-        },
-      });
-      const board = new BackgroundJobBoard();
-      board.registerLaunch({
-        taskID: 'child-1',
-        parentSessionID: 'session-123',
-        agent: 'fixer',
-        description: 'implement change',
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        cooldownMs: 50,
-        backgroundJobBoard: board,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-      await delay(60);
-
-      const promptCall = contCall(ctx.client.session.prompt);
-      expect(promptCall[0].body.parts[0].text).toContain(
-        'Background jobs are still running: call task_status',
-      );
-    });
-
-    test('disabled → no continuation', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Done' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      // Do NOT enable auto-continue
-
-      // Fire session.idle event
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait for cooldown
-      await delay(60);
-
-      // Verify session.prompt was NOT called
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('last message is a question → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [
-                { type: 'text', text: 'Should I proceed with the next step?' },
-              ],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      // Enable auto-continue
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Fire session.idle event
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait for cooldown
-      await delay(60);
-
-      // Verify continuation NOT scheduled
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('question detection with question mark → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Ready to continue?' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('question detection with "would you like" phrase → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [
-                {
-                  type: 'text',
-                  text: 'Would you like me to proceed?',
-                },
-              ],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('max continuations reached → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 2,
-        cooldownMs: 50,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Fire idle events up to maxContinuations
-      for (let i = 0; i < 2; i++) {
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 'session-123' },
-          },
-        });
-        await delay(60);
-      }
-
-      // Reset mock for the 3rd attempt
-      ctx.client.session.prompt.mockClear();
-
-      // On the N+1th idle, verify no continuation scheduled
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('abort suppress window → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      // Seed orchestrator session
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Fire session.error with MessageAbortedError
-      await hook.handleEvent({
-        event: {
-          type: 'session.error',
-          properties: {
-            sessionID: 'session-123',
-            error: { name: 'MessageAbortedError' },
-          },
-        },
-      });
-
-      // Immediately fire session.idle
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait less than suppress window (5s) - just enough to verify it's working
-      await delay(100);
-
-      // Verify no continuation within suppress window
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('session busy → cancel pending timer', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 500,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Schedule a continuation
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // After the notification grace but before cooldown expires, fire busy.
-      await delay(300);
-      await hook.handleEvent({
-        event: {
-          type: 'session.status',
-          properties: {
-            sessionID: 'session-123',
-            status: { type: 'busy' },
-          },
-        },
-      });
-
-      // Advance past original cooldown
-      await delay(250);
-
-      // Verify timer was cancelled and prompt NOT called
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-    });
-
-    test('sub-agent session.busy does NOT cancel orchestrator timer', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 100,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Schedule a continuation for orchestrator session
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // A sub-agent (different session) goes busy
-      await delay(50);
-      await hook.handleEvent({
-        event: {
-          type: 'session.status',
-          properties: {
-            sessionID: 'sub-agent-456',
-            status: { type: 'busy' },
-          },
-        },
-      });
-
-      // Advance past original cooldown
-      await delay(250);
-
-      // Orchestrator timer should still fire — prompt was called
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('all todos complete → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'completed',
-              priority: 'high',
-            },
-            { id: '2', content: 'todo2', status: 'cancelled', priority: 'low' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'All done' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('non-orchestrator session → skip', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // First idle from session A (becomes orchestrator)
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-A' },
-        },
-      });
-
-      await delay(60);
-
-      // Verify prompt was called for session A
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-
-      // Reset mock
-      ctx.client.session.prompt.mockClear();
-
-      // Second idle from session B (different sessionID)
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-B' },
-        },
-      });
-
-      await delay(60);
-
-      // Verify no continuation for session B
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('cooldownMs from config', async () => {
-      const customCooldownMs = 150;
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: customCooldownMs,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Advance timer by well under the custom cooldown to avoid timer jitter
-      await delay(60);
-
-      // Verify prompt not called yet
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-
-      // Advance timer past the configured cooldown
-      await delay(100);
-
-      // Now prompt should be called
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-  });
-
-  describe('event handling - session.error', () => {
-    test('MessageAbortedError sets suppress window', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      // Seed orchestrator session
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Fire session.error with MessageAbortedError
-      await hook.handleEvent({
-        event: {
-          type: 'session.error',
-          properties: {
-            sessionID: 'session-123',
-            error: { name: 'MessageAbortedError' },
-          },
-        },
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait less than suppress window
-      await delay(100);
-
-      // Verify no continuation within suppress window
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('AbortError sets suppress window', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      // Seed orchestrator session (disabled, so no continuation fires)
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.error',
-          properties: {
-            sessionID: 'session-123',
-            error: { name: 'AbortError' },
-          },
-        },
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait less than suppress window
-      await delay(100);
-
-      // Verify no continuation within suppress window
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('other errors do not set suppress window', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.error',
-          properties: {
-            sessionID: 'session-123',
-            error: { name: 'NetworkError' },
-          },
-        },
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      // Prompt should be called immediately (no suppress window)
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-  });
-
-  describe('event handling - session.deleted', () => {
-    test('clears pending timer on session delete', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 100,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Schedule continuation
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Delete session before timer fires
-      await delay(50);
-      await hook.handleEvent({
-        event: {
-          type: 'session.deleted',
-          properties: {
-            sessionID: 'session-123',
-          },
-        },
-      });
-
-      // Advance past original cooldown
-      await delay(250);
-
-      // Verify timer was cancelled and prompt NOT called
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-    });
-
-    test('sub-agent session.deleted does NOT cancel orchestrator timer', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 100,
-      });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Schedule continuation for orchestrator session
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // A sub-agent (different session) gets deleted
-      await delay(50);
-      await hook.handleEvent({
-        event: {
-          type: 'session.deleted',
-          properties: {
-            sessionID: 'sub-agent-456',
-          },
-        },
-      });
-
-      // Advance past original cooldown
-      await delay(250);
-
-      // Orchestrator timer should still fire — prompt was called
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('resets orchestrator session when deleted session matches', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // First idle sets orchestrator
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-A' },
-        },
-      });
-
-      await delay(60);
-
-      // Delete orchestrator session
-      await hook.handleEvent({
-        event: {
-          type: 'session.deleted',
-          properties: {
-            sessionID: 'session-A',
-          },
-        },
-      });
-
-      // Second idle from new session should become orchestrator
-      ctx.client.session.prompt.mockClear();
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-B' },
-        },
-      });
-
-      await delay(60);
-
-      // Prompt should be called for session-B (new orchestrator)
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      const promptCall = contCall(ctx.client.session.prompt);
-      expect(promptCall[0].path.id).toBe('session-B');
-    });
-  });
-
-  describe('error handling', () => {
-    test('fetch todos failure → skips continuation', async () => {
-      const ctx = createMockContext({
-        todoResult: undefined as any,
-      });
-      ctx.client.session.todo = mock(async () => {
-        throw new Error('Failed to fetch todos');
-      });
-
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('fetch messages failure → skips continuation', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-      });
-      ctx.client.session.messages = mock(async () => {
-        throw new Error('Failed to fetch messages');
-      });
-
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-  });
-
-  describe('command.execute.before interception', () => {
-    test('unrelated command → no interception', async () => {
-      const ctx = createMockContext();
-      const hook = createTodoContinuationHook(ctx);
-      const output = { parts: [] as Array<{ type: string; text?: string }> };
-
-      await hook.handleCommandExecuteBefore(
-        { command: 'help', sessionID: 'session-123', arguments: '' },
-        output,
-      );
-
-      expect(output.parts).toHaveLength(0);
-    });
-
-    test('/auto-continue enables and injects continuation when incomplete todos', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'pending',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = { parts: [] as Array<{ type: string; text?: string }> };
-
-      await hook.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 'session-123', arguments: '' },
-        output,
-      );
-
-      expect(output.parts).toHaveLength(1);
-      expect(output.parts[0].text).toContain(
-        '[Auto-continue: enabled - there are incomplete todos remaining.',
-      );
-      expect(output.parts[0].text).toContain(SLIM_INTERNAL_INITIATOR_MARKER);
-    });
-
-    test('/auto-continue enables but no continuation when all todos complete', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'completed',
-              priority: 'high',
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = { parts: [] as Array<{ type: string; text?: string }> };
-
-      await hook.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 'session-123', arguments: '' },
-        output,
-      );
-
-      expect(output.parts).toHaveLength(1);
-      expect(output.parts[0].text).toContain('No incomplete todos right now');
-    });
-
-    test('/auto-continue toggles off when already enabled', async () => {
-      const ctx = createMockContext();
-      const hook = createTodoContinuationHook(ctx);
-      const output = { parts: [] as Array<{ type: string; text?: string }> };
-
-      // Enable via tool
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Toggle off via command
-      await hook.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 'session-123', arguments: '' },
-        output,
-      );
-
-      expect(output.parts).toHaveLength(1);
-      expect(output.parts[0].text).toContain('disabled by user command');
-    });
-
-    test('/auto-continue resets consecutive continuations on toggle', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            {
-              id: '1',
-              content: 'todo1',
-              status: 'pending',
-              priority: 'high',
-            },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 2,
-        cooldownMs: 50,
-      });
-
-      // Enable and run up to max
-      await hook.tool.auto_continue.execute({ enabled: true });
-      for (let i = 0; i < 2; i++) {
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 'session-123' },
-          },
-        });
-        await delay(60);
-      }
-
-      // Toggle off then on via command (resets count)
-      const outputOff = {
-        parts: [] as Array<{ type: string; text?: string }>,
-      };
-      await hook.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 'session-123', arguments: '' },
-        outputOff,
-      );
-      expect(outputOff.parts[0].text).toContain('disabled');
-
-      const outputOn = {
-        parts: [] as Array<{ type: string; text?: string }>,
-      };
-      await hook.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 'session-123', arguments: '' },
-        outputOn,
-      );
-      // Should have continuation prompt again (count was reset)
-      expect(outputOn.parts[0].text).toContain(
-        '[Auto-continue: enabled - there are incomplete todos remaining.',
-      );
-    });
-
-    test('/auto-continue with todo fetch failure → enables without continuation', async () => {
-      const ctx = createMockContext();
-      ctx.client.session.todo = mock(async () => {
-        throw new Error('Network error');
-      });
-      const hook = createTodoContinuationHook(ctx);
-      const output = { parts: [] as Array<{ type: string; text?: string }> };
-
-      await hook.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 'session-123', arguments: '' },
-        output,
-      );
-
-      // Should still enable but skip continuation (no todos fetched)
-      expect(output.parts).toHaveLength(1);
-      expect(output.parts[0].text).toContain('No incomplete todos right now');
-    });
-  });
-
-  describe('config defaults', () => {
-    test('default config: maxContinuations = 5, cooldownMs = 3000', async () => {
-      const ctx = createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-      const hook = createTodoContinuationHook(ctx); // No config passed
-
-      const result = await hook.tool.auto_continue.execute({ enabled: true });
-
-      expect(result).toContain('up to 5');
-
-      // Test default cooldown - we'll just verify it waits before calling
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-123' },
-        },
-      });
-
-      // Wait less than default cooldown
-      await delay(100);
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-
-      // Wait past default cooldown
-      await delay(2900);
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-  });
-
-  describe('council review findings', () => {
-    describe('CRITICAL-1: counter bypass via session.status→busy', () => {
-      test('counter persists when busy fires during auto-injection', async () => {
-        let promptResolve!: () => void;
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-
-        // Make prompt hang so isAutoInjecting stays true
-        ctx.client.session.prompt = mock(async () => {
-          await new Promise<void>((r) => {
-            promptResolve = r;
-          });
-        });
-
-        const hook = createTodoContinuationHook(ctx, {
-          maxContinuations: 2,
-          cooldownMs: 50,
-        });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Cycle 1: idle → timer → prompt hangs
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        // Session goes busy from prompt — isAutoInjecting is true,
-        // so counter should NOT be reset
-        await hook.handleEvent({
-          event: {
-            type: 'session.status',
-            properties: {
-              sessionID: 's1',
-              status: { type: 'busy' },
-            },
-          },
-        });
-
-        // Resolve prompt → counter = 1
-        promptResolve();
-        await delay(10);
-
-        // Cycle 2: idle → timer → prompt hangs
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        // Session goes busy again — counter still not reset
-        await hook.handleEvent({
-          event: {
-            type: 'session.status',
-            properties: {
-              sessionID: 's1',
-              status: { type: 'busy' },
-            },
-          },
-        });
-
-        // Resolve prompt → counter = 2
-        promptResolve();
-        await delay(10);
-
-        // Cycle 3: counter = 2 >= maxContinuations = 2 → BLOCKED
-        ctx.client.session.prompt = mock(async () => ({}));
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-      });
-    });
-
-    describe('CRITICAL-2: disable cancels pending timer', () => {
-      test('tool disable during cooldown prevents injection', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 100 });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Fire idle → timer scheduled (100ms cooldown)
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-
-        // Disable before timer fires
-        await delay(50);
-        await hook.tool.auto_continue.execute({ enabled: false });
-
-        // Wait past original cooldown
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-      });
-
-      test('command disable during cooldown prevents injection', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 100 });
-
-        // Enable via command
-        const outputOn = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: 'on',
-          },
-          outputOn,
-        );
-
-        // Fire idle → timer scheduled
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-
-        // Disable via command before timer fires
-        await delay(50);
-        const outputOff = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: 'off',
-          },
-          outputOff,
-        );
-
-        // Wait past original cooldown
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-      });
-    });
-
-    describe('MAJOR-1: session.deleted resets counter', () => {
-      test('deleted orchestrator session resets counter for next session', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, {
-          maxContinuations: 2,
-          cooldownMs: 50,
-        });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Cycle 1: idle → inject → counter = 1
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        // Delete orchestrator session → counter should reset
-        await hook.handleEvent({
-          event: {
-            type: 'session.deleted',
-            properties: { sessionID: 's1' },
-          },
-        });
-
-        // New session becomes orchestrator — counter starts from 0
-        ctx.client.session.prompt.mockClear();
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's2' },
-          },
-        });
-        await delay(60); // counter = 1
-
-        // One more cycle → counter = 2 (reaches max)
-        ctx.client.session.prompt.mockClear();
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's2' },
-          },
-        });
-        await delay(60);
-
-        // Third cycle blocked (counter = 2 >= max = 2)
-        ctx.client.session.prompt.mockClear();
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's2' },
-          },
-        });
-        await delay(60);
-
-        expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-      });
-    });
-
-    describe('MAJOR-2: suppressUntil cleared on re-enable', () => {
-      test('tool re-enable clears suppress window', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Fire abort → sets suppress window
-        await hook.handleEvent({
-          event: {
-            type: 'session.error',
-            properties: {
-              sessionID: 's1',
-              error: { name: 'AbortError' },
-            },
-          },
-        });
-
-        // Re-enable within suppress window → clears suppressUntil
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Fire idle → should NOT be suppressed
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      });
-
-      test('command re-enable clears suppress window', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Fire abort → sets suppress window
-        await hook.handleEvent({
-          event: {
-            type: 'session.error',
-            properties: {
-              sessionID: 's1',
-              error: { name: 'AbortError' },
-            },
-          },
-        });
-
-        // Re-enable via command → clears suppressUntil
-        const output = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: 'on',
-          },
-          output,
-        );
-
-        // Fire idle → should NOT be suppressed
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      });
-    });
-
-    describe('error paths', () => {
-      test('prompt failure in timer callback is handled gracefully', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        ctx.client.session.prompt = mock(async () => {
-          throw new Error('API error');
-        });
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-
-        // Seed orchestrator session
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        // Error caught; isAutoInjecting should be cleared via finally.
-        // Verify by checking a second idle still works.
-        ctx.client.session.prompt = mock(async () => ({}));
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      });
-    });
-
-    describe('edge cases', () => {
-      test('session.idle with missing sessionID returns early', async () => {
-        const ctx = createMockContext();
-        const hook = createTodoContinuationHook(ctx);
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Fire idle without sessionID — should not throw
-        await hook.handleEvent({
-          event: { type: 'session.idle', properties: {} },
-        });
-
-        expect(ctx.client.session.todo).not.toHaveBeenCalled();
-      });
-
-      test('session.deleted with properties.info.id path', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Set orchestrator via idle
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-
-        // Delete via info.id path (alternative shape from session store)
-        await hook.handleEvent({
-          event: {
-            type: 'session.deleted',
-            properties: { info: { id: 's1' } },
-          },
-        });
-
-        // New session should become orchestrator
-        ctx.client.session.prompt.mockClear();
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's2' },
-          },
-        });
-        await delay(60);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-        expect(contCall(ctx.client.session.prompt)[0].path.id).toBe('s2');
-      });
-
-      test('cooldownMs = 0 fires on next tick', async () => {
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        const hook = createTodoContinuationHook(ctx, {
-          cooldownMs: 0,
-          maxContinuations: 5,
-        });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(10);
-
-        expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      });
-    });
-
-    describe('MAJOR-3: double-fire prevention', () => {
-      test('rapid idle events during prompt delivery — single continuation', async () => {
-        let promptResolve!: () => void;
-        const ctx = createMockContext({
-          todoResult: {
-            data: [
-              {
-                id: '1',
-                content: 't1',
-                status: 'pending',
-                priority: 'high',
-              },
-            ],
-          },
-          messagesResult: {
-            data: [
-              {
-                info: { role: 'assistant' },
-                parts: [{ type: 'text', text: 'Work' }],
-              },
-            ],
-          },
-        });
-        ctx.client.session.prompt = mock(async () => {
-          await new Promise<void>((r) => {
-            promptResolve = r;
-          });
-        });
-
-        const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // Fire idle → timer → prompt hangs (isAutoInjecting = true)
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        // Fire another idle while prompt is in flight
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-
-        // Only one prompt call (blocked by isAutoInjecting gate)
-        expect(contCount(ctx.client.session.prompt)).toBe(1);
-
-        // Resolve prompt
-        promptResolve();
-        await delay(10);
-
-        // Now idle should schedule a new timer
-        ctx.client.session.prompt = mock(async () => ({}));
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 's1' },
-          },
-        });
-        await delay(60);
-
-        expect(contCount(ctx.client.session.prompt)).toBe(1);
-      });
-    });
-
-    describe('MAJOR-4: command explicit on|off arguments', () => {
-      test('command "on" keeps enabled state when already enabled', async () => {
-        const ctx = createMockContext();
-        const hook = createTodoContinuationHook(ctx);
-
-        // Enable via tool
-        await hook.tool.auto_continue.execute({ enabled: true });
-
-        // /auto-continue on → should KEEP enabled (not toggle to off)
-        const output = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: 'on',
-          },
-          output,
-        );
-
-        expect(output.parts[0].text).not.toContain('disabled');
-      });
-
-      test('command "off" keeps disabled state when already disabled', async () => {
-        const ctx = createMockContext();
-        const hook = createTodoContinuationHook(ctx);
-
-        // Start disabled (default)
-        const output = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: 'off',
-          },
-          output,
-        );
-
-        expect(output.parts[0].text).toContain('disabled');
-      });
-
-      test('command with no argument toggles state', async () => {
-        const ctx = createMockContext();
-        const hook = createTodoContinuationHook(ctx);
-
-        // First toggle: disabled → enabled
-        const output1 = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: '',
-          },
-          output1,
-        );
-        expect(output1.parts[0].text).not.toContain('disabled');
-
-        // Second toggle: enabled → disabled
-        const output2 = {
-          parts: [] as Array<{ type: string; text?: string }>,
-        };
-        await hook.handleCommandExecuteBefore(
-          {
-            command: 'auto-continue',
-            sessionID: 's1',
-            arguments: '',
-          },
-          output2,
-        );
-        expect(output2.parts[0].text).toContain('disabled');
-      });
-    });
-  });
-
-  describe('session routing and notification cancellation', () => {
-    function createPendingCtx() {
-      return createMockContext({
-        todoResult: {
-          data: [
-            { id: '1', content: 'todo1', status: 'pending', priority: 'high' },
-          ],
-        },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Work in progress' }],
-            },
-          ],
-        },
-      });
-    }
-
-    test('chat.message registers orchestrator sessions without first-idle lockout', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, {
-        cooldownMs: 50,
-      });
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      hook.handleChatMessage({ sessionID: 'sub1', agent: 'fixer' });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-      hook.handleChatMessage({ sessionID: 'main2', agent: 'orchestrator' });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'sub1' },
-        },
-      });
-      await delay(60);
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(false);
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'main2' },
-        },
-      });
-      await delay(60);
-
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      expect(contCall(ctx.client.session.prompt)[0].path.id).toBe('main2');
-    });
-
-    test('chat.message without agent does not block legacy first-idle fallback', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, {
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 1,
-      });
-
-      hook.handleChatMessage({ sessionID: 'main1' });
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'main1' },
-        },
-      });
-      await delay(60);
-
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('subagent chat.message prevents first-idle fallback registration', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, {
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 1,
-      });
-
-      hook.handleChatMessage({ sessionID: 'sub1', agent: 'fixer' });
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'sub1' },
-        },
-      });
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('session.status idle triggers continuation like session.idle', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-      await hook.tool.auto_continue.execute({ enabled: true });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.status',
-          properties: { sessionID: 'main1', status: { type: 'idle' } },
-        },
-      });
-      await delay(60);
-
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('deleting another orchestrator does not cancel the active session timer', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-      await hook.tool.auto_continue.execute({ enabled: true });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-      hook.handleChatMessage({ sessionID: 'main2', agent: 'orchestrator' });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'main1' },
-        },
-      });
-      await hook.handleEvent({
-        event: {
-          type: 'session.deleted',
-          properties: { sessionID: 'main2' },
-        },
-      });
-      await delay(60);
-
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      expect(contCall(ctx.client.session.prompt)[0].path.id).toBe('main1');
-    });
-
-    test('deleting all orchestrators restores legacy first-idle fallback', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, {
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 1,
-      });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-      hook.handleChatMessage({ sessionID: 'main2', agent: 'orchestrator' });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.deleted',
-          properties: { sessionID: 'main1' },
-        },
-      });
-      await hook.handleEvent({
-        event: {
-          type: 'session.deleted',
-          properties: { sessionID: 'main2' },
-        },
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'legacy-main' },
-        },
-      });
-      await delay(60);
-
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-      expect(contCall(ctx.client.session.prompt)[0].path.id).toBe(
-        'legacy-main',
-      );
-    });
-
-    test('countdown notification busy status does not reset max-continuation counter', async () => {
-      const ctx = createPendingCtx();
-      const releaseNotifications: Array<() => void> = [];
-      ctx.client.session.prompt = mock(async (args: any) => {
-        if (args?.body?.noReply === true) {
-          await new Promise<void>((resolve) => {
-            releaseNotifications.push(resolve);
-          });
-        }
-        return {};
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        cooldownMs: 50,
-        maxContinuations: 2,
-      });
-      await hook.tool.auto_continue.execute({ enabled: true });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-
-      for (let i = 0; i < 2; i++) {
-        await hook.handleEvent({
-          event: {
-            type: 'session.idle',
-            properties: { sessionID: 'main1' },
-          },
-        });
-        await hook.handleEvent({
-          event: {
-            type: 'session.status',
-            properties: { sessionID: 'main1', status: { type: 'busy' } },
-          },
-        });
-        await delay(60);
-        releaseNotifications.shift()?.();
-        await delay(10);
-      }
-
-      ctx.client.session.prompt.mockClear();
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'main1' },
-        },
-      });
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('late countdown notification busy status does not cancel continuation timer', async () => {
-      const ctx = createPendingCtx();
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-      await hook.tool.auto_continue.execute({ enabled: true });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'main1' },
-        },
-      });
-      await delay(10);
-      await hook.handleEvent({
-        event: {
-          type: 'session.status',
-          properties: { sessionID: 'main1', status: { type: 'busy' } },
-        },
-      });
-      await delay(60);
-
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('countdown notification busy status does not cancel continuation timer', async () => {
-      const ctx = createPendingCtx();
-      let callCount = 0;
-      ctx.client.session.prompt = mock(async () => {
-        callCount++;
-        return {};
-      });
-      const hook = createTodoContinuationHook(ctx, { cooldownMs: 50 });
-      await hook.tool.auto_continue.execute({ enabled: true });
-      hook.handleChatMessage({ sessionID: 'main1', agent: 'orchestrator' });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'main1' },
-        },
-      });
-      await hook.handleEvent({
-        event: {
-          type: 'session.status',
-          properties: { sessionID: 'main1', status: { type: 'busy' } },
-        },
-      });
-      await delay(60);
-
-      expect(callCount).toBeGreaterThanOrEqual(2);
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-  });
-
-  describe('auto-enable on todo count', () => {
-    function createAutoEnableCtx(
-      todos: Array<{
-        id: string;
-        content: string;
-        status: string;
-        priority: string;
-      }>,
-    ) {
-      return createMockContext({
-        todoResult: { data: todos },
-        messagesResult: {
-          data: [
-            {
-              info: { role: 'assistant' },
-              parts: [{ type: 'text', text: 'Working...' }],
-            },
-          ],
-        },
-      });
-    }
-
-    test('autoEnable=true, todos >= threshold → auto-enables and continues', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'pending', priority: 'high' },
-        { id: '2', content: 't2', status: 'pending', priority: 'high' },
-        { id: '3', content: 't3', status: 'pending', priority: 'high' },
-        { id: '4', content: 't4', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      // Do NOT manually enable — auto-enable should trigger
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // Should have scheduled continuation (auto-enabled)
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('autoEnable=true, todos < threshold → does NOT auto-enable', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'pending', priority: 'high' },
-        { id: '2', content: 't2', status: 'pending', priority: 'high' },
-        { id: '3', content: 't3', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // Should NOT auto-enable or continue
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('autoEnable=false (default) → never auto-enables regardless of todo count', async () => {
-      const ctx = createAutoEnableCtx(
-        Array.from({ length: 10 }, (_, i) => ({
-          id: String(i),
-          content: `t${i}`,
-          status: 'pending',
-          priority: 'high',
-        })),
-      );
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        // autoEnable defaults to false
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('auto-enable does not re-enable if already manually enabled', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'pending', priority: 'high' },
-        { id: '2', content: 't2', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      // Manually enable first
-      await hook.tool.auto_continue.execute({ enabled: true });
-
-      // Only 2 todos (< threshold) — but already enabled, so should continue
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // Continues because already manually enabled (auto-enable check skipped)
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('auto-enable respects custom threshold', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'pending', priority: 'high' },
-        { id: '2', content: 't2', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 2,
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // 2 todos >= threshold 2 → auto-enables
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('auto-enable skipped for non-orchestrator session', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'pending', priority: 'high' },
-        { id: '2', content: 't2', status: 'pending', priority: 'high' },
-        { id: '3', content: 't3', status: 'pending', priority: 'high' },
-        { id: '4', content: 't4', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      // First idle sets orchestrator to session-A
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-A' },
-        },
-      });
-      await delay(60);
-
-      // Reset mock
-      ctx.client.session.prompt.mockClear();
-
-      // Second idle from session-B — not orchestrator, should skip
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 'session-B' },
-        },
-      });
-      await delay(60);
-
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('auto-enable with todo fetch failure → no auto-enable, no crash', async () => {
-      const ctx = createMockContext();
-      ctx.client.session.todo = mock(async () => {
-        throw new Error('Network error');
-      });
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      // Should not throw
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // No auto-enable, no continuation
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-
-    test('auto-enable resets consecutive counter and suppress window', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'pending', priority: 'high' },
-        { id: '2', content: 't2', status: 'pending', priority: 'high' },
-        { id: '3', content: 't3', status: 'pending', priority: 'high' },
-        { id: '4', content: 't4', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      // Manually enable, run a continuation, disable
-      await hook.tool.auto_continue.execute({ enabled: true });
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-      await delay(60);
-
-      // Fire abort to set suppress window
-      await hook.handleEvent({
-        event: {
-          type: 'session.error',
-          properties: {
-            sessionID: 's1',
-            error: { name: 'AbortError' },
-          },
-        },
-      });
-
-      // Disable
-      await hook.tool.auto_continue.execute({ enabled: false });
-
-      // Reset mock
-      ctx.client.session.prompt.mockClear();
-
-      // Fire idle again — auto-enable should trigger (4 todos >= 4),
-      // resetting counter and suppress window
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // Should continue (suppressed window was cleared by auto-enable)
-      expect(hasContinuation(ctx.client.session.prompt)).toBe(true);
-    });
-
-    test('auto-enable counts incomplete todos only, not completed', async () => {
-      const ctx = createAutoEnableCtx([
-        { id: '1', content: 't1', status: 'completed', priority: 'high' },
-        { id: '2', content: 't2', status: 'completed', priority: 'high' },
-        { id: '3', content: 't3', status: 'pending', priority: 'high' },
-        { id: '4', content: 't4', status: 'pending', priority: 'high' },
-      ]);
-      const hook = createTodoContinuationHook(ctx, {
-        maxContinuations: 5,
-        cooldownMs: 50,
-        autoEnable: true,
-        autoEnableThreshold: 4,
-      });
-
-      await hook.handleEvent({
-        event: {
-          type: 'session.idle',
-          properties: { sessionID: 's1' },
-        },
-      });
-
-      await delay(60);
-
-      // Only 2 incomplete todos < threshold 4 → does NOT auto-enable
-      expect(ctx.client.session.prompt).not.toHaveBeenCalled();
-    });
-  });
-});

+ 0 - 913
src/hooks/todo-continuation/index.ts

@@ -1,913 +0,0 @@
-import type { PluginInput } from '@opencode-ai/plugin';
-import { tool } from '@opencode-ai/plugin';
-import {
-  type BackgroundJobBoard,
-  createInternalAgentTextPart,
-  log,
-  SLIM_INTERNAL_INITIATOR_MARKER,
-  withTimeout,
-} from '../../utils';
-import { createTodoHygiene } from './todo-hygiene';
-
-const HOOK_NAME = 'todo-continuation';
-const COMMAND_NAME = 'auto-continue';
-const TODO_STATE_TIMEOUT_MS = 500;
-
-const CONTINUATION_PROMPT =
-  '[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]';
-const TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
-const TODO_HYGIENE_INSTRUCTION_CLOSE = '</instruction>';
-
-// Suppress window after user abort (Esc/Ctrl+C) to avoid immediately
-// re-continuing something the user explicitly stopped
-const SUPPRESS_AFTER_ABORT_MS = 5_000;
-const NOTIFICATION_BUSY_GRACE_MS = 250;
-
-const QUESTION_PHRASES = [
-  'would you like',
-  'should i',
-  'do you want',
-  'please review',
-  'let me know',
-  'what do you think',
-  'can you confirm',
-  'would you prefer',
-  'shall i',
-  'any thoughts',
-];
-
-// Statuses that indicate a todo is terminal (won't be worked on further).
-// Uses denylist approach: any status not listed here is considered incomplete.
-const TERMINAL_TODO_STATUSES = ['completed', 'cancelled'];
-
-interface ContinuationState {
-  enabled: boolean;
-  consecutiveContinuations: number;
-  pendingTimer: ReturnType<typeof setTimeout> | null;
-  pendingTimerSessionId: string | null;
-  suppressUntil: number;
-  orchestratorSessionIds: Set<string>;
-  sawChatMessage: boolean;
-  // True while our auto-injection prompt is in flight — prevents counter reset
-  // on session.status→busy and blocks duplicate injections
-  isAutoInjecting: boolean;
-  // session IDs with an in-flight noReply countdown notification.
-  notifyingSessionIds: Set<string>;
-  // sessionID → timestamp until which just-completed noReply countdown
-  // notification busy transitions are ignored, covering HTTP/SSE reordering.
-  notificationBusyUntilBySession: Map<string, number>;
-}
-
-function isQuestion(text: string): boolean {
-  const lowerText = text.toLowerCase().trim();
-  // Match trailing '?' with optional whitespace after it
-  if (/\?\s*$/.test(lowerText)) {
-    return true;
-  }
-  return QUESTION_PHRASES.some((phrase) => lowerText.includes(phrase));
-}
-
-interface TodoItem {
-  id: string;
-  content: string;
-  status: string;
-  priority: string;
-}
-
-interface MessageInfo {
-  role?: string;
-  [key: string]: unknown;
-}
-
-interface MessagePart {
-  type?: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface ChatTransformMessage {
-  info: {
-    id?: string;
-    role?: string;
-    agent?: string;
-    sessionID?: string;
-  };
-  parts: MessagePart[];
-}
-
-interface LastExternalUserMessage {
-  sessionID?: string;
-  agent?: string;
-  signature: string;
-  message: ChatTransformMessage;
-}
-
-interface Message {
-  info?: MessageInfo;
-  parts?: MessagePart[];
-}
-
-function cancelPendingTimer(state: ContinuationState): void {
-  if (state.pendingTimer) {
-    clearTimeout(state.pendingTimer);
-    state.pendingTimer = null;
-  }
-  state.pendingTimerSessionId = null;
-}
-
-function resetState(state: ContinuationState): void {
-  cancelPendingTimer(state);
-  state.consecutiveContinuations = 0;
-  state.suppressUntil = 0;
-  state.isAutoInjecting = false;
-  state.notifyingSessionIds.clear();
-  state.notificationBusyUntilBySession.clear();
-}
-
-function stripTodoHygieneInstruction(text: string): string {
-  const trimmed = text.trimEnd();
-  if (!trimmed.endsWith(TODO_HYGIENE_INSTRUCTION_CLOSE)) {
-    return trimmed;
-  }
-
-  const start = trimmed.lastIndexOf(TODO_HYGIENE_INSTRUCTION_OPEN);
-  if (start === -1) {
-    return trimmed;
-  }
-
-  return trimmed.slice(0, start).trimEnd();
-}
-
-function appendTodoHygieneInstruction(
-  message: ChatTransformMessage,
-  reminder: string,
-): void {
-  const textPart = [...message.parts]
-    .reverse()
-    .find((part) => part.type === 'text' && typeof part.text === 'string');
-  if (!textPart) return;
-
-  const baseText = stripTodoHygieneInstruction(textPart.text ?? '');
-  const instruction = `${TODO_HYGIENE_INSTRUCTION_OPEN}\n${reminder}\n${TODO_HYGIENE_INSTRUCTION_CLOSE}`;
-  textPart.text = baseText ? `${baseText}\n\n${instruction}` : instruction;
-}
-
-function stripTodoHygieneInstructionFromMessage(
-  message: ChatTransformMessage,
-): void {
-  const textPart = [...message.parts]
-    .reverse()
-    .find((part) => part.type === 'text' && typeof part.text === 'string');
-  if (!textPart) return;
-
-  textPart.text = stripTodoHygieneInstruction(textPart.text ?? '');
-}
-
-export function createTodoContinuationHook(
-  ctx: PluginInput,
-  config?: {
-    maxContinuations?: number;
-    cooldownMs?: number;
-    autoEnable?: boolean;
-    autoEnableThreshold?: number;
-    backgroundJobBoard?: BackgroundJobBoard;
-  },
-): {
-  tool: Record<string, unknown>;
-  handleToolExecuteAfter: (
-    input: {
-      tool: string;
-      sessionID?: string;
-    },
-    output?: { output?: unknown },
-  ) => Promise<void>;
-  handleMessagesTransform: (output: {
-    messages: ChatTransformMessage[];
-  }) => Promise<void>;
-  handleEvent: (input: {
-    event: { type: string; properties?: Record<string, unknown> };
-  }) => Promise<void>;
-  handleChatMessage: (input: { sessionID: string; agent?: string }) => void;
-  handleCommandExecuteBefore: (
-    input: {
-      command: string;
-      sessionID: string;
-      arguments: string;
-    },
-    output: { parts: Array<{ type: string; text?: string }> },
-  ) => Promise<void>;
-} {
-  const maxContinuations = config?.maxContinuations ?? 5;
-  const cooldownMs = config?.cooldownMs ?? 3000;
-  const autoEnable = config?.autoEnable ?? false;
-  const autoEnableThreshold = config?.autoEnableThreshold ?? 4;
-  const backgroundJobBoard = config?.backgroundJobBoard;
-  const requestSignatureBySession = new Map<string, string>();
-
-  const state: ContinuationState = {
-    enabled: false,
-    consecutiveContinuations: 0,
-    pendingTimer: null,
-    pendingTimerSessionId: null,
-    suppressUntil: 0,
-    orchestratorSessionIds: new Set<string>(),
-    sawChatMessage: false,
-    isAutoInjecting: false,
-    notifyingSessionIds: new Set<string>(),
-    notificationBusyUntilBySession: new Map<string, number>(),
-  };
-
-  async function fetchTodos(sessionID: string): Promise<TodoItem[]> {
-    const result = await withTimeout(
-      ctx.client.session.todo({
-        path: { id: sessionID },
-      }),
-      TODO_STATE_TIMEOUT_MS,
-      `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`,
-    );
-    return result.data as TodoItem[];
-  }
-
-  const hygiene = createTodoHygiene({
-    getTodoState: async (sessionID) => {
-      const todos = await fetchTodos(sessionID);
-      const openTodos = todos.filter(
-        (todo) => !TERMINAL_TODO_STATUSES.includes(todo.status),
-      );
-      return {
-        hasOpenTodos: openTodos.length > 0,
-        openCount: openTodos.length,
-        inProgressCount: openTodos.filter(
-          (todo) => todo.status === 'in_progress',
-        ).length,
-        pendingCount: openTodos.filter((todo) => todo.status === 'pending')
-          .length,
-      };
-    },
-    shouldInject: (sessionID) => isOrchestratorSession(sessionID),
-    log: (message, meta) => log(`[${HOOK_NAME}] ${message}`, meta),
-  });
-
-  function inferSessionID(
-    messages: ChatTransformMessage[],
-    index: number,
-  ): string | undefined {
-    const direct = messages[index]?.info.sessionID;
-    if (direct) {
-      return direct;
-    }
-
-    for (let i = index - 1; i >= 0; i--) {
-      const sessionID = messages[i]?.info.sessionID;
-      if (sessionID) {
-        return sessionID;
-      }
-    }
-
-    for (let i = index + 1; i < messages.length; i++) {
-      const sessionID = messages[i]?.info.sessionID;
-      if (sessionID) {
-        return sessionID;
-      }
-    }
-
-    if (state.orchestratorSessionIds.size === 1) {
-      return Array.from(state.orchestratorSessionIds)[0];
-    }
-
-    return undefined;
-  }
-
-  function isExternalUserMessage(message: ChatTransformMessage): boolean {
-    if (message.info.role !== 'user') {
-      return false;
-    }
-
-    const visibleText = message.parts
-      .filter(
-        (part) =>
-          part.type === 'text' &&
-          typeof part.text === 'string' &&
-          !part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER),
-      )
-      .map((part) => part.text?.trim() ?? '')
-      .filter(Boolean)
-      .join('\n');
-    const hasNonTextPart = message.parts.some((part) => part.type !== 'text');
-
-    return !(
-      !visibleText &&
-      !hasNonTextPart &&
-      message.parts.some(
-        (part) =>
-          part.type === 'text' &&
-          typeof part.text === 'string' &&
-          part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER),
-      )
-    );
-  }
-
-  function getLastExternalUserMessage(
-    messages: ChatTransformMessage[],
-  ): LastExternalUserMessage | null {
-    for (let i = messages.length - 1; i >= 0; i--) {
-      const message = messages[i];
-      if (!isExternalUserMessage(message)) {
-        continue;
-      }
-
-      const sessionID = inferSessionID(messages, i);
-
-      const partSignature = message.parts
-        .map((part) => {
-          if (part.type === 'text' && typeof part.text === 'string') {
-            const text = stripTodoHygieneInstruction(part.text);
-            return `${part.type}:${text.includes(SLIM_INTERNAL_INITIATOR_MARKER) ? '<internal>' : text.trim()}`;
-          }
-          return part.type ?? 'unknown';
-        })
-        .join('|');
-      const ordinal = messages
-        .slice(0, i + 1)
-        .filter((item) => isExternalUserMessage(item)).length;
-
-      return {
-        sessionID,
-        agent: message.info.agent,
-        message,
-        signature: message.info.id
-          ? `${message.info.id}:${partSignature}`
-          : `${ordinal}:${partSignature}`,
-      };
-    }
-
-    return null;
-  }
-
-  async function handleMessagesTransform(output: {
-    messages: ChatTransformMessage[];
-  }): Promise<void> {
-    const lastUserMessage = getLastExternalUserMessage(output.messages);
-    if (!lastUserMessage) {
-      return;
-    }
-
-    if (lastUserMessage.agent && lastUserMessage.agent !== 'orchestrator') {
-      return;
-    }
-
-    if (!lastUserMessage.sessionID) {
-      for (const sessionID of state.orchestratorSessionIds) {
-        requestSignatureBySession.delete(sessionID);
-        hygiene.handleRequestStart({ sessionID });
-      }
-      return;
-    }
-
-    const knownOrchestrator = isOrchestratorSession(lastUserMessage.sessionID);
-    if (lastUserMessage.agent === 'orchestrator') {
-      registerOrchestratorSession(lastUserMessage.sessionID);
-    } else if (!knownOrchestrator) {
-      return;
-    }
-
-    if (
-      requestSignatureBySession.get(lastUserMessage.sessionID) ===
-      lastUserMessage.signature
-    ) {
-      const reminder = hygiene.getPendingReminder(lastUserMessage.sessionID);
-      const guardrail = backgroundGuardrail(lastUserMessage.sessionID);
-      const combinedReminder = [reminder, guardrail]
-        .filter((item): item is string => Boolean(item))
-        .join(' ');
-      if (combinedReminder) {
-        appendTodoHygieneInstruction(lastUserMessage.message, combinedReminder);
-      } else {
-        stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
-      }
-      return;
-    }
-
-    requestSignatureBySession.set(
-      lastUserMessage.sessionID,
-      lastUserMessage.signature,
-    );
-    stripTodoHygieneInstructionFromMessage(lastUserMessage.message);
-    hygiene.handleRequestStart({ sessionID: lastUserMessage.sessionID });
-  }
-
-  function markNotificationStarted(sessionID: string): void {
-    state.notifyingSessionIds.add(sessionID);
-  }
-
-  function markNotificationFinished(sessionID: string): void {
-    state.notifyingSessionIds.delete(sessionID);
-    state.notificationBusyUntilBySession.set(
-      sessionID,
-      Date.now() + NOTIFICATION_BUSY_GRACE_MS,
-    );
-  }
-
-  function clearNotificationState(sessionID: string): void {
-    state.notifyingSessionIds.delete(sessionID);
-    state.notificationBusyUntilBySession.delete(sessionID);
-  }
-
-  function isNotificationBusy(sessionID: string): boolean {
-    if (state.notifyingSessionIds.has(sessionID)) {
-      return true;
-    }
-
-    const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
-    if (until <= Date.now()) {
-      state.notificationBusyUntilBySession.delete(sessionID);
-      return false;
-    }
-    return true;
-  }
-
-  function isOrchestratorSession(sessionID: string): boolean {
-    return state.orchestratorSessionIds.has(sessionID);
-  }
-
-  function registerOrchestratorSession(sessionID: string): void {
-    state.orchestratorSessionIds.add(sessionID);
-  }
-
-  function backgroundGuardrail(sessionID: string): string | undefined {
-    if (!backgroundJobBoard) return undefined;
-
-    const hasRunning = backgroundJobBoard.hasRunning(sessionID);
-    const hasTerminal = backgroundJobBoard.hasTerminalUnreconciled(sessionID);
-    if (hasRunning && hasTerminal) {
-      return 'Background jobs are still unresolved: call task_status for running jobs and reconcile terminal Background Job Board results before dependent work or finalizing.';
-    }
-    if (hasTerminal) {
-      return 'Background jobs have terminal results: reconcile the Background Job Board results before finalizing.';
-    }
-    if (hasRunning) {
-      return 'Background jobs are still running: call task_status before dependent work or finalizing.';
-    }
-
-    return undefined;
-  }
-
-  function continuationPrompt(sessionID: string): string {
-    const guardrail = backgroundGuardrail(sessionID);
-    if (!guardrail) return CONTINUATION_PROMPT;
-
-    return `${CONTINUATION_PROMPT} ${guardrail}`;
-  }
-
-  function handleChatMessage(input: {
-    sessionID: string;
-    agent?: string;
-  }): void {
-    if (!input.agent) {
-      return;
-    }
-
-    state.sawChatMessage = true;
-    if (input.agent === 'orchestrator') {
-      registerOrchestratorSession(input.sessionID);
-    }
-  }
-
-  const autoContinue = tool({
-    description:
-      'Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.',
-    args: { enabled: tool.schema.boolean() },
-    execute: async (args) => {
-      const enabled = args.enabled;
-      state.enabled = enabled;
-      state.consecutiveContinuations = 0;
-
-      if (enabled) {
-        state.suppressUntil = 0;
-        log(`[${HOOK_NAME}] Auto-continue enabled`, { maxContinuations });
-        return `Auto-continue enabled. Will auto-continue for up to ${maxContinuations} consecutive injections.`;
-      }
-
-      // Cancel any pending timer on disable
-      cancelPendingTimer(state);
-      log(`[${HOOK_NAME}] Auto-continue disabled`);
-      return 'Auto-continue disabled.';
-    },
-  });
-
-  async function handleEvent(input: {
-    event: { type: string; properties?: Record<string, unknown> };
-  }): Promise<void> {
-    const { event } = input;
-    const properties = event.properties ?? {};
-
-    hygiene.handleEvent({
-      type: event.type,
-      properties: {
-        info: properties.info as { id?: string } | undefined,
-        sessionID: properties.sessionID as string | undefined,
-      },
-    });
-
-    if (
-      event.type === 'session.idle' ||
-      (event.type === 'session.status' &&
-        (properties.status as { type?: string } | undefined)?.type === 'idle')
-    ) {
-      const sessionID = properties.sessionID as string;
-      if (!sessionID) {
-        return;
-      }
-
-      log(`[${HOOK_NAME}] Session idle`, { sessionID });
-
-      // Backward compatibility: if no chat.message has identified the
-      // orchestrator yet, fall back to the first idle session.
-      if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
-        registerOrchestratorSession(sessionID);
-        log(`[${HOOK_NAME}] Tracked orchestrator session`, {
-          sessionID,
-        });
-      }
-
-      // Gate: session is orchestrator (needed before auto-enable check)
-      if (!isOrchestratorSession(sessionID)) {
-        log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
-          sessionID,
-        });
-        return;
-      }
-
-      // Auto-enable check: if configured, not yet enabled, and enough
-      // todos exist, automatically enable auto-continue.
-      if (autoEnable && !state.enabled) {
-        try {
-          const todos = await fetchTodos(sessionID);
-          const incompleteCount = todos.filter(
-            (t) => !TERMINAL_TODO_STATUSES.includes(t.status),
-          ).length;
-          if (incompleteCount >= autoEnableThreshold) {
-            state.enabled = true;
-            state.consecutiveContinuations = 0;
-            state.suppressUntil = 0;
-            log(
-              `[${HOOK_NAME}] Auto-enabled: ${incompleteCount} incomplete todos >= threshold ${autoEnableThreshold}`,
-              { sessionID },
-            );
-          } else {
-            log(
-              `[${HOOK_NAME}] Auto-enable skipped: ${incompleteCount} incomplete todos < threshold ${autoEnableThreshold}`,
-              { sessionID },
-            );
-          }
-        } catch (error) {
-          log(
-            `[${HOOK_NAME}] Warning: failed to fetch todos for auto-enable check`,
-            {
-              sessionID,
-              error: error instanceof Error ? error.message : String(error),
-            },
-          );
-        }
-      }
-
-      // Safety gate 1: enabled
-      if (!state.enabled) {
-        log(`[${HOOK_NAME}] Skipped: auto-continue not enabled`, {
-          sessionID,
-        });
-        return;
-      }
-
-      // Safety gate 2: incomplete todos exist
-      let hasIncompleteTodos = false;
-      let incompleteCount = 0;
-      try {
-        const todos = await fetchTodos(sessionID);
-        incompleteCount = todos.filter(
-          (t) => !TERMINAL_TODO_STATUSES.includes(t.status),
-        ).length;
-        hasIncompleteTodos = incompleteCount > 0;
-        log(`[${HOOK_NAME}] Fetched todos`, {
-          sessionID,
-          hasIncompleteTodos,
-          total: todos.length,
-        });
-      } catch (error) {
-        log(`[${HOOK_NAME}] Warning: failed to fetch todos`, {
-          sessionID,
-          error: error instanceof Error ? error.message : String(error),
-        });
-        return;
-      }
-
-      if (!hasIncompleteTodos) {
-        log(`[${HOOK_NAME}] Skipped: no incomplete todos`, { sessionID });
-        return;
-      }
-
-      // Safety gate 3: last assistant message is not a question
-      let lastAssistantIsQuestion = false;
-      try {
-        const messagesResult = await ctx.client.session.messages({
-          path: { id: sessionID },
-        });
-        const messages = messagesResult.data as Message[];
-        const lastAssistantMessage = messages
-          .slice()
-          .reverse()
-          .find((m) => m.info?.role === 'assistant');
-        if (lastAssistantMessage?.parts) {
-          const lastText = lastAssistantMessage.parts
-            .map((p) => p.text ?? '')
-            .join(' ');
-          lastAssistantIsQuestion = isQuestion(lastText);
-        }
-        log(`[${HOOK_NAME}] Fetched messages`, {
-          sessionID,
-          lastAssistantIsQuestion,
-        });
-      } catch (error) {
-        log(`[${HOOK_NAME}] Warning: failed to fetch messages`, {
-          sessionID,
-          error: error instanceof Error ? error.message : String(error),
-        });
-        return;
-      }
-
-      if (lastAssistantIsQuestion) {
-        log(`[${HOOK_NAME}] Skipped: last message is question`, {
-          sessionID,
-        });
-        return;
-      }
-
-      // Safety gate 4: below max continuations
-      if (state.consecutiveContinuations >= maxContinuations) {
-        log(`[${HOOK_NAME}] Skipped: max continuations reached`, {
-          sessionID,
-          consecutive: state.consecutiveContinuations,
-          max: maxContinuations,
-        });
-        return;
-      }
-
-      // Safety gate 5: not in suppress window
-      const now = Date.now();
-      if (now < state.suppressUntil) {
-        log(`[${HOOK_NAME}] Skipped: in suppress window`, {
-          sessionID,
-          suppressUntil: state.suppressUntil,
-        });
-        return;
-      }
-
-      // Safety gate 6: no pending timer AND no injection in flight
-      if (state.pendingTimer !== null || state.isAutoInjecting) {
-        log(`[${HOOK_NAME}] Skipped: timer pending or injection in flight`, {
-          sessionID,
-        });
-        return;
-      }
-
-      // Schedule continuation
-      log(`[${HOOK_NAME}] Scheduling continuation`, {
-        sessionID,
-        delayMs: cooldownMs,
-      });
-
-      // Show countdown notification (noReply = agent doesn't respond)
-      markNotificationStarted(sessionID);
-      ctx.client.session
-        .prompt({
-          path: { id: sessionID },
-          body: {
-            noReply: true,
-            parts: [
-              {
-                type: 'text',
-                text: [
-                  `⎔ Auto-continue: ${incompleteCount} incomplete todos remaining — resuming in ${cooldownMs / 1000}s — Esc×2 to cancel`,
-                  '',
-                  '[system status: continue without acknowledging this notification]',
-                ].join('\n'),
-              },
-            ],
-          },
-        })
-        .catch(() => {
-          /* best-effort notification */
-        })
-        .finally(() => {
-          markNotificationFinished(sessionID);
-        });
-
-      state.pendingTimerSessionId = sessionID;
-      state.pendingTimer = setTimeout(async () => {
-        state.pendingTimer = null;
-        state.pendingTimerSessionId = null;
-        clearNotificationState(sessionID);
-
-        // Guard: may have been disabled during cooldown
-        if (!state.enabled) {
-          log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
-            sessionID,
-          });
-          return;
-        }
-
-        state.isAutoInjecting = true;
-        try {
-          await ctx.client.session.prompt({
-            path: { id: sessionID },
-            body: {
-              parts: [
-                createInternalAgentTextPart(continuationPrompt(sessionID)),
-              ],
-            },
-          });
-          state.consecutiveContinuations++;
-          log(`[${HOOK_NAME}] Continuation injected`, {
-            sessionID,
-            consecutive: state.consecutiveContinuations,
-          });
-        } catch (error) {
-          log(`[${HOOK_NAME}] Error: failed to inject continuation`, {
-            sessionID,
-            error: error instanceof Error ? error.message : String(error),
-          });
-        } finally {
-          state.isAutoInjecting = false;
-        }
-      }, cooldownMs);
-    } else if (event.type === 'session.status') {
-      const status = properties.status as { type: string };
-      const sessionID = properties.sessionID as string;
-      if (status?.type === 'busy') {
-        const isOrchestrator = isOrchestratorSession(sessionID);
-        const isNotification = isNotificationBusy(sessionID);
-
-        // Only cancel timer for orchestrator session — sub-agents going
-        // busy must not silently kill the orchestrator's continuation.
-        if (
-          isOrchestrator &&
-          !isNotification &&
-          state.pendingTimerSessionId === sessionID
-        ) {
-          cancelPendingTimer(state);
-        }
-
-        // Only reset consecutive counter for user-initiated activity,
-        // not for our own auto-injection prompt. Scope to orchestrator only.
-        if (
-          !state.isAutoInjecting &&
-          !isNotification &&
-          isOrchestrator &&
-          state.consecutiveContinuations > 0
-        ) {
-          state.consecutiveContinuations = 0;
-          log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
-            sessionID,
-          });
-        }
-      }
-    } else if (event.type === 'session.error') {
-      const error = properties.error as { name?: string };
-      const sessionID = properties.sessionID as string;
-      const errorName = error?.name;
-      const isOrchestrator = isOrchestratorSession(sessionID);
-      if (
-        isOrchestrator &&
-        (errorName === 'MessageAbortedError' || errorName === 'AbortError')
-      ) {
-        state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
-        log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
-          sessionID,
-          errorName,
-        });
-      }
-      if (isOrchestrator) {
-        cancelPendingTimer(state);
-        log(`[${HOOK_NAME}] Cancelled pending timer on error`, {
-          sessionID,
-        });
-      }
-    } else if (event.type === 'session.deleted') {
-      // OpenCode sends sessionID in two shapes:
-      // properties.info.id (from session store) or properties.sessionID (from event)
-      const deletedSessionId =
-        (properties.info as { id?: string })?.id ??
-        (properties.sessionID as string);
-
-      if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
-        requestSignatureBySession.delete(deletedSessionId);
-        if (state.pendingTimerSessionId === deletedSessionId) {
-          cancelPendingTimer(state);
-          log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
-            sessionID: deletedSessionId,
-          });
-        }
-
-        state.orchestratorSessionIds.delete(deletedSessionId);
-        clearNotificationState(deletedSessionId);
-        if (state.orchestratorSessionIds.size === 0) {
-          resetState(state);
-          state.sawChatMessage = false;
-        }
-        log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
-          sessionID: deletedSessionId,
-        });
-      }
-    }
-  }
-
-  async function handleCommandExecuteBefore(
-    input: {
-      command: string;
-      sessionID: string;
-      arguments: string;
-    },
-    output: { parts: Array<{ type: string; text?: string }> },
-  ): Promise<void> {
-    if (input.command !== COMMAND_NAME) {
-      return;
-    }
-
-    // Seed orchestrator session from slash command (more reliable than
-    // first-idle heuristic — slash commands only fire in main chat)
-    registerOrchestratorSession(input.sessionID);
-
-    // Clear template text — hook handles everything directly
-    output.parts.length = 0;
-
-    // Accept explicit on/off argument, toggle only when no arg
-    const arg = input.arguments.trim().toLowerCase();
-    let newEnabled: boolean;
-    if (arg === 'on') {
-      newEnabled = true;
-    } else if (arg === 'off') {
-      newEnabled = false;
-    } else {
-      newEnabled = !state.enabled;
-    }
-
-    state.enabled = newEnabled;
-    state.consecutiveContinuations = 0;
-
-    if (!newEnabled) {
-      // Cancel any pending timer on disable
-      cancelPendingTimer(state);
-      output.parts.push(
-        createInternalAgentTextPart(
-          '[Auto-continue: disabled by user command.]',
-        ),
-      );
-      log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME} command`);
-      return;
-    }
-
-    // Clear suppress window on explicit re-enable
-    state.suppressUntil = 0;
-
-    log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME} command`, {
-      maxContinuations,
-    });
-
-    // Check for incomplete todos to decide on immediate continuation
-    let hasIncompleteTodos = false;
-    try {
-      const todos = await fetchTodos(input.sessionID);
-      hasIncompleteTodos = todos.some(
-        (t) => !TERMINAL_TODO_STATUSES.includes(t.status),
-      );
-    } catch (error) {
-      log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
-        sessionID: input.sessionID,
-        error: error instanceof Error ? error.message : String(error),
-      });
-    }
-
-    if (hasIncompleteTodos) {
-      output.parts.push(
-        createInternalAgentTextPart(
-          `${continuationPrompt(input.sessionID)} [Auto-continue enabled: up to ${maxContinuations} continuations.]`,
-        ),
-      );
-    } else {
-      output.parts.push(
-        createInternalAgentTextPart(
-          `[Auto-continue: enabled for up to ${maxContinuations} continuations. No incomplete todos right now.]`,
-        ),
-      );
-    }
-  }
-
-  return {
-    tool: { auto_continue: autoContinue },
-    handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
-    handleMessagesTransform,
-    handleEvent,
-    handleChatMessage,
-    handleCommandExecuteBefore,
-  };
-}

+ 0 - 204
src/hooks/todo-continuation/todo-hygiene.test.ts

@@ -1,204 +0,0 @@
-import { describe, expect, test } from 'bun:test';
-import {
-  createTodoHygiene,
-  TODO_FINAL_ACTIVE_REMINDER,
-  TODO_HYGIENE_REMINDER,
-} from './todo-hygiene';
-
-function createState(
-  overrides?: Partial<{
-    hasOpenTodos: boolean;
-    openCount: number;
-    inProgressCount: number;
-    pendingCount: number;
-  }>,
-) {
-  return {
-    hasOpenTodos: overrides?.hasOpenTodos ?? true,
-    openCount: overrides?.openCount ?? 1,
-    inProgressCount: overrides?.inProgressCount ?? 0,
-    pendingCount: overrides?.pendingCount ?? 1,
-  };
-}
-
-describe('todo hygiene', () => {
-  test('new request clears pending state from the previous turn', async () => {
-    const hook = createTodoHygiene({
-      getTodoState: async () => createState(),
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-    hook.handleRequestStart({ sessionID: 's1' });
-
-    expect(hook.getPendingReminder('s1')).toBeNull();
-
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-
-    expect(hook.getPendingReminder('s1')).toBe(TODO_HYGIENE_REMINDER);
-  });
-
-  test('does not arm before the current request calls todowrite', async () => {
-    const hook = createTodoHygiene({
-      getTodoState: async () => createState(),
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-
-    expect(hook.getPendingReminder('s1')).toBeNull();
-  });
-
-  test('arms after the first relevant tool following todowrite', async () => {
-    const hook = createTodoHygiene({
-      getTodoState: async () => createState(),
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-
-    expect(hook.getPendingReminder('s1')).toBe(TODO_HYGIENE_REMINDER);
-    expect(hook.getPendingReminder('s1')).toBe(TODO_HYGIENE_REMINDER);
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    expect(hook.getPendingReminder('s1')).toBeNull();
-  });
-
-  test('upgrades to final-active on a later round', async () => {
-    let call = 0;
-    const hook = createTodoHygiene({
-      getTodoState: async () => {
-        call++;
-        if (call <= 3) return createState();
-        return createState({
-          openCount: 1,
-          inProgressCount: 1,
-          pendingCount: 0,
-        });
-      },
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-    expect(hook.getPendingReminder('s1')).toBe(TODO_HYGIENE_REMINDER);
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-    expect(hook.getPendingReminder('s1')).toBe(TODO_FINAL_ACTIVE_REMINDER);
-  });
-
-  test('todowrite can arm final-active immediately', async () => {
-    const hook = createTodoHygiene({
-      getTodoState: async () =>
-        createState({
-          openCount: 1,
-          inProgressCount: 1,
-          pendingCount: 0,
-        }),
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-
-    expect(hook.getPendingReminder('s1')).toBe(TODO_FINAL_ACTIVE_REMINDER);
-  });
-
-  test('once final-active is armed, later tools skip extra todo lookups in the same round', async () => {
-    let calls = 0;
-    const hook = createTodoHygiene({
-      getTodoState: async () => {
-        calls++;
-        return createState({
-          openCount: 1,
-          inProgressCount: 1,
-          pendingCount: 0,
-        });
-      },
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'grep', sessionID: 's1' });
-
-    expect(calls).toBe(1);
-  });
-
-  test('shouldInject rejection prevents reset lookup and reminders', async () => {
-    let calls = 0;
-    const hook = createTodoHygiene({
-      getTodoState: async () => {
-        calls++;
-        return createState({
-          openCount: 1,
-          inProgressCount: 1,
-          pendingCount: 0,
-        });
-      },
-      shouldInject: () => false,
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-
-    expect(calls).toBe(0);
-    expect(hook.getPendingReminder('s1')).toBeNull();
-  });
-
-  test('reading a pending reminder does not inspect todos', async () => {
-    let fail = false;
-    const hook = createTodoHygiene({
-      getTodoState: async () => {
-        if (fail) throw new Error('boom');
-        return createState();
-      },
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-    fail = true;
-
-    expect(hook.getPendingReminder('s1')).toBe(TODO_HYGIENE_REMINDER);
-  });
-
-  test('todowrite lookup failures do not disable the current request', async () => {
-    let fail = false;
-    const hook = createTodoHygiene({
-      getTodoState: async () => {
-        if (fail) throw new Error('boom');
-        return createState();
-      },
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    fail = true;
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    fail = false;
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-
-    expect(hook.getPendingReminder('s1')).toBe(TODO_HYGIENE_REMINDER);
-  });
-
-  test('session.deleted clears all state', async () => {
-    const hook = createTodoHygiene({
-      getTodoState: async () => createState(),
-    });
-
-    hook.handleRequestStart({ sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'todowrite', sessionID: 's1' });
-    await hook.handleToolExecuteAfter({ tool: 'read', sessionID: 's1' });
-    hook.handleEvent({
-      type: 'session.deleted',
-      properties: { info: { id: 's1' } },
-    });
-
-    expect(hook.getPendingReminder('s1')).toBeNull();
-  });
-});

+ 0 - 207
src/hooks/todo-continuation/todo-hygiene.ts

@@ -1,207 +0,0 @@
-export const TODO_HYGIENE_REMINDER =
-  'If the active task changed or finished, update the todo list to match the current work state.';
-export const TODO_FINAL_ACTIVE_REMINDER =
-  'If you are finishing now, do not leave the active todo in_progress. Mark it completed, or move unfinished work back to pending.';
-
-const RESET = new Set(['todowrite']);
-const IGNORE = new Set(['auto_continue']);
-
-type Reason = 'general' | 'final_active';
-
-interface ToolInput {
-  tool: string;
-  sessionID?: string;
-}
-
-interface EventInput {
-  type: string;
-  properties?: {
-    info?: { id?: string };
-    sessionID?: string;
-  };
-}
-
-interface RequestStartInput {
-  sessionID: string;
-}
-
-interface Options {
-  getTodoState: (sessionID: string) => Promise<{
-    hasOpenTodos: boolean;
-    openCount: number;
-    inProgressCount: number;
-    pendingCount: number;
-  }>;
-  shouldInject?: (sessionID: string) => boolean;
-  log?: (message: string, meta?: Record<string, unknown>) => void;
-}
-
-export function createTodoHygiene(options: Options) {
-  const pending = new Map<string, Set<Reason>>();
-  const active = new Set<string>();
-
-  function clearCycle(sessionID: string): void {
-    pending.delete(sessionID);
-  }
-
-  function clear(sessionID: string): void {
-    clearCycle(sessionID);
-    active.delete(sessionID);
-  }
-
-  function isFinalActive(state: {
-    openCount: number;
-    inProgressCount: number;
-    pendingCount: number;
-  }): boolean {
-    return (
-      state.inProgressCount === 1 &&
-      state.pendingCount === 0 &&
-      state.openCount === 1
-    );
-  }
-
-  function mark(sessionID: string, reason: Reason): void {
-    const reasons = pending.get(sessionID) ?? new Set<Reason>();
-    reasons.add(reason);
-    pending.set(sessionID, reasons);
-  }
-
-  function pick(reasons: Set<Reason>): string {
-    if (reasons.has('final_active')) {
-      return TODO_FINAL_ACTIVE_REMINDER;
-    }
-
-    return TODO_HYGIENE_REMINDER;
-  }
-
-  return {
-    handleRequestStart(input: RequestStartInput): void {
-      clear(input.sessionID);
-    },
-
-    async handleToolExecuteAfter(
-      input: ToolInput,
-      _output?: unknown,
-    ): Promise<void> {
-      if (!input.sessionID) {
-        return;
-      }
-
-      const tool = input.tool.toLowerCase();
-      if (IGNORE.has(tool)) {
-        return;
-      }
-
-      try {
-        if (RESET.has(tool)) {
-          if (options.shouldInject && !options.shouldInject(input.sessionID)) {
-            clear(input.sessionID);
-            return;
-          }
-
-          active.add(input.sessionID);
-          clearCycle(input.sessionID);
-          const state = await options.getTodoState(input.sessionID);
-          if (!state.hasOpenTodos) {
-            active.delete(input.sessionID);
-            options.log?.('Cleared todo hygiene cycle', {
-              sessionID: input.sessionID,
-              tool,
-            });
-            return;
-          }
-
-          if (!isFinalActive(state)) {
-            options.log?.('Reset todo hygiene cycle', {
-              sessionID: input.sessionID,
-              tool,
-            });
-            return;
-          }
-
-          mark(input.sessionID, 'final_active');
-          options.log?.('Armed final-active todo hygiene reminder', {
-            sessionID: input.sessionID,
-            tool,
-          });
-          return;
-        }
-
-        if (!active.has(input.sessionID)) {
-          return;
-        }
-
-        if (pending.get(input.sessionID)?.has('final_active')) {
-          return;
-        }
-
-        if (options.shouldInject && !options.shouldInject(input.sessionID)) {
-          clear(input.sessionID);
-          return;
-        }
-
-        const state = await options.getTodoState(input.sessionID);
-        if (!state.hasOpenTodos) {
-          clear(input.sessionID);
-          return;
-        }
-
-        if (isFinalActive(state)) {
-          mark(input.sessionID, 'final_active');
-        } else {
-          mark(input.sessionID, 'general');
-        }
-
-        options.log?.('Armed todo hygiene reminder', {
-          sessionID: input.sessionID,
-          tool,
-          reasons: Array.from(pending.get(input.sessionID) ?? []),
-        });
-      } catch (error) {
-        options.log?.(
-          'Skipped todo hygiene reminder: failed to inspect todos',
-          {
-            sessionID: input.sessionID,
-            tool,
-            error: error instanceof Error ? error.message : String(error),
-          },
-        );
-      }
-    },
-
-    getPendingReminder(sessionID: string): string | null {
-      const reasons = pending.get(sessionID);
-      if (!reasons || reasons.size === 0) {
-        return null;
-      }
-
-      if (options.shouldInject && !options.shouldInject(sessionID)) {
-        clear(sessionID);
-        return null;
-      }
-
-      const reminder = pick(reasons);
-      options.log?.('Read todo hygiene reminder', {
-        sessionID,
-        reminder,
-        reasons: Array.from(reasons),
-      });
-      return reminder;
-    },
-
-    handleEvent(event: EventInput): void {
-      if (event.type !== 'session.deleted') {
-        return;
-      }
-
-      const sessionID =
-        event.properties?.sessionID ?? event.properties?.info?.id;
-      if (!sessionID) {
-        return;
-      }
-
-      clear(sessionID);
-    },
-  };
-}

+ 0 - 61
src/index.ts

@@ -27,7 +27,6 @@ import {
   createPhaseReminderHook,
   createPostFileToolNudgeHook,
   createTaskSessionManagerHook,
-  createTodoContinuationHook,
   ForegroundFallbackManager,
 } from './hooks';
 import { processImageAttachments } from './hooks/image-hook';
@@ -135,7 +134,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   let applyPatchHook: ReturnType<typeof createApplyPatchHook>;
   let jsonErrorRecoveryHook: ReturnType<typeof createJsonErrorRecoveryHook>;
   let foregroundFallback: ForegroundFallbackManager;
-  let todoContinuationHook: ReturnType<typeof createTodoContinuationHook>;
   let deepworkCommandHook: ReturnType<typeof createDeepworkCommandHook>;
   let taskSessionManagerHook: ReturnType<typeof createTaskSessionManagerHook>;
   let backgroundJobBoard: BackgroundJobBoard;
@@ -305,15 +303,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         Object.keys(runtimeChains).length > 0,
     );
 
-    // Initialize todo-continuation hook (opt-in auto-continue for
-    // incomplete todos)
-    todoContinuationHook = createTodoContinuationHook(ctx, {
-      maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
-      cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
-      autoEnable: config.todoContinuation?.autoEnable ?? false,
-      autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
-      backgroundJobBoard,
-    });
     deepworkCommandHook = createDeepworkCommandHook();
     taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
       maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
@@ -336,7 +325,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     toolCount =
       Object.keys(councilTools).length +
       Object.keys(cancelTaskTools).length +
-      Object.keys(todoContinuationHook.tool).length +
       1 + // webfetch
       2; // ast_grep_search, ast_grep_replace
   } catch (err) {
@@ -406,7 +394,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ...councilTools,
       ...cancelTaskTools,
       webfetch,
-      ...todoContinuationHook.tool,
       ast_grep_search,
       ast_grep_replace,
     },
@@ -725,23 +712,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         agentConfigEntry.permission = agentPermission;
       }
 
-      // Register /auto-continue command so OpenCode recognizes it.
-      // Actual handling is done by command.execute.before hook below
-      // (no LLM round-trip — injected directly into output.parts).
-      const configCommand = opencodeConfig.command as
-        | Record<string, unknown>
-        | undefined;
-      if (!configCommand?.['auto-continue']) {
-        if (!opencodeConfig.command) {
-          opencodeConfig.command = {};
-        }
-        (opencodeConfig.command as Record<string, unknown>)['auto-continue'] = {
-          template: 'Call the auto_continue tool with enabled=true',
-          description:
-            'Enable auto-continuation — orchestrator keeps working through incomplete todos',
-        };
-      }
-
       interviewManager.registerCommand(opencodeConfig);
       deepworkCommandHook.registerCommand(opencodeConfig);
       presetManager.registerCommand(opencodeConfig);
@@ -802,9 +772,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       // Runtime model fallback for foreground agents (rate-limit detection)
       await foregroundFallback.handleEvent(input.event);
 
-      // Todo-continuation: auto-continue orchestrator on incomplete todos
-      await todoContinuationHook.handleEvent(input);
-
       // Handle auto-update checking
       await autoUpdateChecker.event(input);
 
@@ -923,18 +890,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       }
     },
 
-    // Direct interception of /auto-continue command — bypasses LLM
-    // round-trip
     'command.execute.before': async (input, output) => {
-      await todoContinuationHook.handleCommandExecuteBefore(
-        input as {
-          command: string;
-          sessionID: string;
-          arguments: string;
-        },
-        output as { parts: Array<{ type: string; text?: string }> },
-      );
-
       await interviewManager.handleCommandExecuteBefore(
         input as {
           command: string;
@@ -987,10 +943,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       if (agent) {
         sessionAgentMap.set(input.sessionID, agent);
       }
-      todoContinuationHook.handleChatMessage({
-        sessionID: input.sessionID,
-        agent,
-      });
     },
 
     // Inject orchestrator system prompt for serve-mode sessions. In serve
@@ -1084,9 +1036,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         log,
       });
 
-      await todoContinuationHook.handleMessagesTransform({
-        messages: typedOutput.messages,
-      });
       await taskSessionManagerHook['experimental.chat.messages.transform'](
         input,
         typedOutput,
@@ -1148,16 +1097,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         ),
       );
 
-      await runPostToolHook('todo-continuation', () =>
-        todoContinuationHook.handleToolExecuteAfter(
-          input as {
-            tool: string;
-            sessionID?: string;
-          },
-          output as { output?: unknown },
-        ),
-      );
-
       await runPostToolHook('post-file-tool-nudge', () =>
         postFileToolNudgeHook['tool.execute.after'](
           input as {

+ 1 - 1
src/tools/preset-manager.test.ts

@@ -63,7 +63,7 @@ describe('createPresetManager', () => {
       const output = createOutput();
 
       await manager.handleCommandExecuteBefore(
-        { command: 'auto-continue', sessionID: 's1', arguments: 'on' },
+        { command: 'unknown-command', sessionID: 's1', arguments: 'on' },
         output,
       );