Browse Source

Merge pull request #870 from kaze-gif/fix/reconcile-task-tool

fix: add reconcile_task tool to prevent self-amplifying task reconciliation loop
Alvin 2 weeks ago
parent
commit
31e9ef2e0a

+ 2 - 2
codemap.md

@@ -41,7 +41,7 @@ This codemap covers the plugin repository itself and excludes the nested `openco
 | `src/hooks/json-error-recovery/` | JSON/tool-output recovery helpers for malformed model responses. | [View Map](src/hooks/json-error-recovery/codemap.md) |
 | `src/hooks/json-error-recovery/` | JSON/tool-output recovery helpers for malformed model responses. | [View Map](src/hooks/json-error-recovery/codemap.md) |
 | `src/hooks/phase-reminder/` | Message-transform reminder enforcing orchestrator workflow phases. | [View Map](src/hooks/phase-reminder/codemap.md) |
 | `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/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/task-session-manager/` | Resumable `task` session tracking, short alias resolution, prompt injection, stale-session cleanup, and terminal task reconciliation. | [View Map](src/hooks/task-session-manager/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/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/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) |
 | `src/multiplexer/` | Terminal multiplexer abstraction layer with backend selection, session mirroring, polling fallback, and shutdown lifecycle orchestration. | [View Map](src/multiplexer/codemap.md) |
@@ -97,7 +97,7 @@ This codemap covers the plugin repository itself and excludes the nested `openco
   multiplexer behavior remains on the upstream path.
   multiplexer behavior remains on the upstream path.
 - Council mode is implemented in `src/agents/`; the orchestrator dispatches councillors as subagents and the council agent synthesizes responses.
 - Council mode is implemented in `src/agents/`; the orchestrator dispatches councillors as subagents and the council agent synthesizes responses.
 - `src/tools/preset-manager.ts` hooks command execution and updates runtime agent models from configured presets.
 - `src/tools/preset-manager.ts` hooks command execution and updates runtime agent models from configured presets.
-- `src/hooks/task-session-manager/` depends on `src/utils/background-job-board.ts` and `src/utils/task.ts` to support background task tracking, task output parsing, and safe alias reuse.
+- `src/hooks/task-session-manager/` depends on `src/utils/background-job-board.ts` and `src/utils/task.ts` to support background task tracking, task output parsing, terminal task reconciliation, and safe alias reuse.
 - `src/hooks/filter-available-skills/` and agent permission logic rely on shared skill names from the CLI/config layer.
 - `src/hooks/filter-available-skills/` and agent permission logic rely on shared skill names from the CLI/config layer.
 - `src/interview/` hooks into plugin command/event surfaces exposed by `src/index.ts`.
 - `src/interview/` hooks into plugin command/event surfaces exposed by `src/index.ts`.
 
 

+ 6 - 0
docs/background-orchestration.md

@@ -37,6 +37,7 @@ The required native/background-control tools are:
 | `task(..., background: true)` | Start a specialist in the background and immediately return a task ID |
 | `task(..., background: true)` | Start a specialist in the background and immediately return a task ID |
 | hook-driven completion | OpenCode injects terminal background task results automatically |
 | hook-driven completion | OpenCode injects terminal background task results automatically |
 | `cancel_task` | Plugin-provided tool to cancel a tracked background task by task ID or Background Job Board alias |
 | `cancel_task` | Plugin-provided tool to cancel a tracked background task by task ID or Background Job Board alias |
+| `reconcile_task` | Plugin-provided tool to mark a terminal task as reconciled in the Background Job Board (state-only, no specialist invocation) |
 | `wait_for_user` | Plugin-provided orchestrator tool that pauses automatic continuation while the user performs external manual work |
 | `wait_for_user` | Plugin-provided orchestrator tool that pauses automatic continuation while the user performs external manual work |
 
 
 If these are not available, the scheduler cannot use the default background
 If these are not available, the scheduler cannot use the default background
@@ -161,6 +162,11 @@ running lane is obsolete, wrong, or conflicts with a safer replacement plan.
 Cancellation is not rollback: if cancelling a writer, inspect and reconcile
 Cancellation is not rollback: if cancelling a writer, inspect and reconcile
 partial file changes before launching a replacement lane.
 partial file changes before launching a replacement lane.
 
 
+Before final response, use `reconcile_task` to mark any terminal jobs shown in
+the Background Job Board as reconciled. Idle-based auto-reconciliation also
+handles this when the orchestrator session goes idle, but explicit reconciliation
+via `reconcile_task` is preferred for deterministic state management.
+
 **Note on reconciliation:** Idle-based reconciliation is a heuristic. A job marked
 **Note on reconciliation:** Idle-based reconciliation is a heuristic. A job marked
 as reconciled means its terminal result was injected into an orchestrator turn
 as reconciled means its terminal result was injected into an orchestrator turn
 that completed and the parent returned to idle; it is not proof the result was
 that completed and the parent returned to idle; it is not proof the result was

+ 7 - 0
docs/tools.md

@@ -39,6 +39,7 @@ Fast, structural code search and refactoring - more powerful than plain text gre
 | Tool | Description |
 | Tool | Description |
 |------|-------------|
 |------|-------------|
 | `cancel_task` | Cancel a tracked background specialist task by native task ID or Background Job Board alias |
 | `cancel_task` | Cancel a tracked background specialist task by native task ID or Background Job Board alias |
+| `reconcile_task` | Mark a terminal (completed/error/cancelled) background task as reconciled so it no longer appears as unreconciled |
 | `wait_for_user` | Pause automatic incomplete-todo continuation until the next distinct external user message |
 | `wait_for_user` | Pause automatic incomplete-todo continuation until the next distinct external user message |
 
 
 `cancel_task` is orchestrator-only. It only cancels background tasks tracked for
 `cancel_task` is orchestrator-only. It only cancels background tasks tracked for
@@ -46,6 +47,12 @@ the current orchestrator session, and it does not roll back partial edits. After
 cancelling a write-capable task, inspect and reconcile file changes before
 cancelling a write-capable task, inspect and reconcile file changes before
 launching replacement work.
 launching replacement work.
 
 
+`reconcile_task` is orchestrator-only. It marks a terminal task as reconciled
+in the Background Job Board without invoking any specialist session. This is a
+state-only operation — use it before final response when a terminal result has
+been received and consumed. Idle-based auto-reconciliation also handles this
+when the orchestrator session goes idle.
+
 `wait_for_user` is also orchestrator-only. The orchestrator uses it as the final
 `wait_for_user` is also orchestrator-only. The orchestrator uses it as the final
 tool action after providing concrete instructions for external manual work. Its
 tool action after providing concrete instructions for external manual work. Its
 `reason` is diagnostic text only; the plugin does not parse assistant prose to
 `reason` is diagnostic text only; the plugin does not parse assistant prose to

+ 1 - 1
src/agents/orchestrator.ts

@@ -210,7 +210,7 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
 - Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
 - Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
 - Before local edits or another writer task, compare against running task scopes.
 - Before local edits or another writer task, compare against running task scopes.
 - Parallel background tasks are allowed only when their write scopes do not conflict.
 - Parallel background tasks are allowed only when their write scopes do not conflict.
-- Before final response, reconcile any terminal jobs shown in the Background Job Board.
+- Before final response, use \`reconcile_task\` to mark any terminal jobs shown in the Background Job Board as reconciled.
 - Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
 - Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
 - Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
 - Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
 
 

+ 1 - 1
src/hooks/__snapshots__/cache-payload.snapshot.test.ts.snap

@@ -155,7 +155,7 @@ Balance: respect dependencies, avoid parallelizing what must be sequential, and
 - Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
 - Continue orchestration only on non-overlapping work; otherwise briefly report what was launched and stop.
 - Before local edits or another writer task, compare against running task scopes.
 - Before local edits or another writer task, compare against running task scopes.
 - Parallel background tasks are allowed only when their write scopes do not conflict.
 - Parallel background tasks are allowed only when their write scopes do not conflict.
-- Before final response, reconcile any terminal jobs shown in the Background Job Board.
+- Before final response, use \`reconcile_task\` to mark any terminal jobs shown in the Background Job Board as reconciled.
 - Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
 - Use \`cancel_task\` only when the user asks, or when a running lane is obsolete, wrong, or conflicts with a safer replacement plan.
 - Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
 - Cancellation is not rollback: if cancelling a writer, inspect and reconcile partial file changes before launching a replacement lane.
 
 

+ 10 - 1
src/index.ts

@@ -61,6 +61,7 @@ import {
   ast_grep_search,
   ast_grep_search,
   createAcpRunTool,
   createAcpRunTool,
   createCancelTaskTool,
   createCancelTaskTool,
+  createReconcileTaskTool,
   createWaitForUserTool,
   createWaitForUserTool,
   createWebfetchTool,
   createWebfetchTool,
 } from './tools';
 } from './tools';
@@ -102,13 +103,14 @@ async function appLog(
 const HEALTH_CHECK = {
 const HEALTH_CHECK = {
   minAgents: 5,
   minAgents: 5,
   // Default tool set when council and ACP agents are not configured:
   // Default tool set when council and ACP agents are not configured:
-  // cancel_task, wait_for_user, webfetch, ast_grep_search, ast_grep_replace.
+  // cancel_task, reconcile_task, wait_for_user, webfetch, ast_grep_search, ast_grep_replace.
   minTools: 5,
   minTools: 5,
   minMcps: 1,
   minMcps: 1,
 } as const;
 } as const;
 
 
 const BASELINE_TOOL_NAMES = new Set([
 const BASELINE_TOOL_NAMES = new Set([
   'cancel_task',
   'cancel_task',
+  'reconcile_task',
   'wait_for_user',
   'wait_for_user',
   'webfetch',
   'webfetch',
   'ast_grep_search',
   'ast_grep_search',
@@ -198,6 +200,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   let interviewManager: ReturnType<typeof createInterviewManager>;
   let interviewManager: ReturnType<typeof createInterviewManager>;
   let companionManager: CompanionManager;
   let companionManager: CompanionManager;
   let cancelTaskTools: ReturnType<typeof createCancelTaskTool>;
   let cancelTaskTools: ReturnType<typeof createCancelTaskTool>;
+  let reconcileTaskTools: ReturnType<typeof createReconcileTaskTool>;
   let waitForUserTools: ReturnType<typeof createWaitForUserTool>;
   let waitForUserTools: ReturnType<typeof createWaitForUserTool>;
   let acpRunTools: Record<string, ReturnType<typeof createAcpRunTool>>;
   let acpRunTools: Record<string, ReturnType<typeof createAcpRunTool>>;
   let webfetch: ReturnType<typeof createWebfetchTool>;
   let webfetch: ReturnType<typeof createWebfetchTool>;
@@ -440,6 +443,11 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       shouldManageSession: (sessionID) =>
       shouldManageSession: (sessionID) =>
         sessionAgentMap.get(sessionID) === 'orchestrator',
         sessionAgentMap.get(sessionID) === 'orchestrator',
     });
     });
+    reconcileTaskTools = createReconcileTaskTool({
+      backgroundJobBoard: backgroundJobCoordinator,
+      shouldManageSession: (sessionID) =>
+        sessionAgentMap.get(sessionID) === 'orchestrator',
+    });
     waitForUserTools = createWaitForUserTool({
     waitForUserTools = createWaitForUserTool({
       shouldManageSession: (sessionID) =>
       shouldManageSession: (sessionID) =>
         sessionAgentMap.get(sessionID) === 'orchestrator',
         sessionAgentMap.get(sessionID) === 'orchestrator',
@@ -453,6 +461,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
 
 
     tools = {
     tools = {
       ...cancelTaskTools,
       ...cancelTaskTools,
+      ...reconcileTaskTools,
       ...waitForUserTools,
       ...waitForUserTools,
       ...acpRunTools,
       ...acpRunTools,
       webfetch,
       webfetch,

+ 15 - 2
src/tools/codemap.md

@@ -27,7 +27,7 @@ Each tool is implemented as a factory function that returns a `ToolDefinition` r
 | Tool Family | Purpose | Key Components |
 | Tool Family | Purpose | Key Components |
 |------------|---------|----------------|
 |------------|---------|----------------|
 | **Council** | Multi-LLM consensus synthesis (orchestrator dispatches councillors as subagents) | `agents/council.ts`, `agents/index.ts` |
 | **Council** | Multi-LLM consensus synthesis (orchestrator dispatches councillors as subagents) | `agents/council.ts`, `agents/index.ts` |
-| **Task Management** | Background task lifecycle and HITL continuation control | `cancel-task.ts`, `wait-for-user.ts`, `background-job-board.ts` |
+| **Task Management** | Background task lifecycle and HITL continuation control | `cancel-task.ts`, `reconcile-task.ts`, `wait-for-user.ts`, `background-job-board.ts` |
 | **ACP Integration** | External agent protocol execution | `acp-run.ts`, ACP client implementation |
 | **ACP Integration** | External agent protocol execution | `acp-run.ts`, ACP client implementation |
 | **Code Intelligence** | AST-based code manipulation | `ast-grep/` directory, `tools.ts` |
 | **Code Intelligence** | AST-based code manipulation | `ast-grep/` directory, `tools.ts` |
 | **Web Fetching** | Intelligent web content retrieval | `smartfetch/` directory, `tool.ts` |
 | **Web Fetching** | Intelligent web content retrieval | `smartfetch/` directory, `tool.ts` |
@@ -80,6 +80,17 @@ Each tool is implemented as a factory function that returns a `ToolDefinition` r
    └─> Returns cancellation confirmation
    └─> Returns cancellation confirmation
 ```
 ```
 
 
+### Task Reconciliation Flow
+
+```
+1. Orchestrator invokes reconcile_task tool
+   ├─> Validates calling agent is 'orchestrator'
+   ├─> Resolves task_id/alias to BackgroundJobBoard entry
+   ├─> Validates job is terminal-unreconciled
+   ├─> Calls BackgroundJobBoard.markReconciled()
+   └─> Returns reconciliation confirmation
+```
+
 ### Explicit User-Wait Flow
 ### Explicit User-Wait Flow
 
 
 ```
 ```
@@ -163,7 +174,8 @@ Each tool is implemented as a factory function that returns a `ToolDefinition` r
 ```
 ```
 Tools Layer → Background Layer
 Tools Layer → Background Layer
 ├─ cancel_task tool → BackgroundJobBoard.resolve() → abortSessionWithTimeout()
 ├─ cancel_task tool → BackgroundJobBoard.resolve() → abortSessionWithTimeout()
-└─> Returns cancellation status
+├─ reconcile_task tool → BackgroundJobBoard.resolve() → BackgroundJobBoard.markReconciled()
+└─> Returns reconciliation status
 
 
 Tools Layer → Config Layer
 Tools Layer → Config Layer
 ├─ acp_run tool → AcpAgentsConfig from config system
 ├─ acp_run tool → AcpAgentsConfig from config system
@@ -204,6 +216,7 @@ export { ast_grep_replace, ast_grep_search } from './ast-grep';
 
 
 // Task management
 // Task management
 export { createCancelTaskTool } from './cancel-task';
 export { createCancelTaskTool } from './cancel-task';
+export { createReconcileTaskTool } from './reconcile-task';
 export { createWaitForUserTool } from './wait-for-user';
 export { createWaitForUserTool } from './wait-for-user';
 
 
 // Preset management
 // Preset management

+ 1 - 0
src/tools/index.ts

@@ -2,5 +2,6 @@
 export { createAcpRunTool } from './acp-run';
 export { createAcpRunTool } from './acp-run';
 export { ast_grep_replace, ast_grep_search } from './ast-grep';
 export { ast_grep_replace, ast_grep_search } from './ast-grep';
 export { createCancelTaskTool } from './cancel-task';
 export { createCancelTaskTool } from './cancel-task';
+export { createReconcileTaskTool } from './reconcile-task';
 export { createWebfetchTool } from './smartfetch';
 export { createWebfetchTool } from './smartfetch';
 export { createWaitForUserTool } from './wait-for-user';
 export { createWaitForUserTool } from './wait-for-user';

+ 169 - 0
src/tools/reconcile-task.test.ts

@@ -0,0 +1,169 @@
+import { describe, expect, test } from 'bun:test';
+import { BackgroundJobBoard } from '../utils/background-job-board';
+import { createReconcileTaskTool } from './reconcile-task';
+
+function createTool(overrides?: {
+  shouldManageSession?: (sessionID: string) => boolean;
+}) {
+  const board = new BackgroundJobBoard();
+  const tools = createReconcileTaskTool({
+    backgroundJobBoard: board,
+    shouldManageSession: overrides?.shouldManageSession ?? (() => true),
+  });
+
+  return { board, reconcileTask: tools.reconcile_task };
+}
+
+const context = { sessionID: 'parent-1', agent: 'orchestrator' } as any;
+
+describe('reconcile_task tool', () => {
+  test('reconciles a terminal-unreconciled task by task ID', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-1',
+      agent: 'explorer',
+    });
+    board.updateStatus({ taskID: 'ses_1', state: 'completed' });
+
+    const output = await reconcileTask.execute({ task_id: 'ses_1' }, context);
+
+    expect(String(output)).toContain('state: reconciled');
+    expect(String(output)).toContain('Task reconciled');
+    expect(board.get('ses_1')?.state).toBe('reconciled');
+    expect(board.get('ses_1')?.terminalUnreconciled).toBe(false);
+  });
+
+  test('reconciles a terminal-unreconciled task by alias', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-1',
+      agent: 'oracle',
+    });
+    board.updateStatus({ taskID: 'ses_1', state: 'completed' });
+
+    const output = await reconcileTask.execute({ task_id: 'ora-1' }, context);
+
+    expect(String(output)).toContain('state: reconciled');
+    expect(board.get('ses_1')?.state).toBe('reconciled');
+  });
+
+  test('reconciles an errored task', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-1',
+      agent: 'fixer',
+    });
+    board.updateStatus({ taskID: 'ses_1', state: 'error' });
+
+    const output = await reconcileTask.execute({ task_id: 'ses_1' }, context);
+
+    expect(String(output)).toContain('state: reconciled');
+    expect(board.get('ses_1')?.state).toBe('reconciled');
+  });
+
+  test('reconciles a cancelled task', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-1',
+      agent: 'fixer',
+    });
+    board.markCancelled('ses_1', 'obsolete');
+
+    const output = await reconcileTask.execute({ task_id: 'ses_1' }, context);
+
+    expect(String(output)).toContain('state: reconciled');
+    expect(board.get('ses_1')?.state).toBe('reconciled');
+  });
+
+  test('includes reason in output when provided', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-1',
+      agent: 'explorer',
+    });
+    board.updateStatus({ taskID: 'ses_1', state: 'completed' });
+
+    const output = await reconcileTask.execute(
+      { task_id: 'ses_1', reason: 'result consumed' },
+      context,
+    );
+
+    expect(String(output)).toContain('Reason: result consumed');
+  });
+
+  test('returns already-reconciled message for non-terminal tasks', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-1',
+      agent: 'explorer',
+    });
+
+    const output = await reconcileTask.execute({ task_id: 'ses_1' }, context);
+
+    expect(String(output)).toContain('already reconciled');
+    expect(String(output)).toContain('state: running');
+  });
+
+  test('returns error for unknown task ID', async () => {
+    const { reconcileTask } = createTool();
+
+    const output = await reconcileTask.execute(
+      { task_id: 'ses_unknown' },
+      context,
+    );
+
+    expect(String(output)).toContain('state: unknown');
+    expect(String(output)).toContain('unknown or unowned');
+  });
+
+  test('does not reconcile tasks owned by a different parent', async () => {
+    const { board, reconcileTask } = createTool();
+    board.registerLaunch({
+      taskID: 'ses_1',
+      parentSessionID: 'parent-2',
+      agent: 'explorer',
+    });
+    board.updateStatus({ taskID: 'ses_1', state: 'completed' });
+
+    const output = await reconcileTask.execute({ task_id: 'ses_1' }, context);
+
+    expect(String(output)).toContain('state: unknown');
+    expect(board.get('ses_1')?.state).toBe('completed');
+    expect(board.get('ses_1')?.terminalUnreconciled).toBe(true);
+  });
+
+  test('denies non-orchestrator agents', async () => {
+    const { reconcileTask } = createTool();
+
+    await expect(
+      reconcileTask.execute({ task_id: 'ses_1' }, {
+        sessionID: 'parent-1',
+        agent: 'fixer',
+      } as any),
+    ).rejects.toThrow('orchestrator');
+  });
+
+  test('denies unmanaged sessions', async () => {
+    const { reconcileTask } = createTool({
+      shouldManageSession: () => false,
+    });
+
+    await expect(
+      reconcileTask.execute({ task_id: 'ses_1' }, context),
+    ).rejects.toThrow('orchestrator sessions');
+  });
+
+  test('requires task_id', async () => {
+    const { reconcileTask } = createTool();
+
+    await expect(
+      reconcileTask.execute({ task_id: '  ' }, context),
+    ).rejects.toThrow('requires task_id');
+  });
+});

+ 117 - 0
src/tools/reconcile-task.ts

@@ -0,0 +1,117 @@
+import { type ToolDefinition, tool } from '@opencode-ai/plugin';
+import type { BackgroundJobStore } from '../utils/background-job-store';
+import { log } from '../utils/logger';
+
+const z = tool.schema;
+
+interface ReconcileTaskToolOptions {
+  backgroundJobBoard: BackgroundJobStore;
+  shouldManageSession: (sessionID: string) => boolean;
+}
+
+export function createReconcileTaskTool(
+  options: ReconcileTaskToolOptions,
+): Record<string, ToolDefinition> {
+  const reconcile_task = tool({
+    description: `Reconcile a completed background specialist task in the Background Job Board.
+
+Marks a terminal (completed, error, or cancelled) task as reconciled so it no longer appears as unreconciled.
+This is a state-only operation — it does not invoke or resume any specialist session.
+Use when a terminal task result has been received and consumed, before issuing a final response.
+
+Accepts either the native task_id/session ID or the parent-scoped alias shown in the Background Job Board.`,
+    args: {
+      task_id: z
+        .string()
+        .describe(
+          'Background task ID or Background Job Board alias to reconcile',
+        ),
+      reason: z
+        .string()
+        .optional()
+        .describe('Optional short reason for reconciliation'),
+    },
+    async execute(args, toolContext) {
+      const parentSessionID = toolContext?.sessionID;
+      if (!parentSessionID)
+        throw new Error('reconcile_task requires sessionID');
+      if (toolContext.agent && toolContext.agent !== 'orchestrator') {
+        throw new Error('reconcile_task can only be used by orchestrator');
+      }
+      if (!options.shouldManageSession(parentSessionID)) {
+        throw new Error(
+          'reconcile_task can only be used in orchestrator sessions',
+        );
+      }
+
+      const requested = args.task_id.trim();
+      if (!requested) throw new Error('reconcile_task requires task_id');
+
+      const job = options.backgroundJobBoard.resolve(
+        parentSessionID,
+        requested,
+      );
+      log('[reconcile-task] request received', {
+        parentSessionID,
+        requested,
+        resolvedTaskID: job?.taskID,
+        alias: job
+          ? options.backgroundJobBoard.field(job.taskID, 'alias')
+          : undefined,
+        state: job
+          ? options.backgroundJobBoard.field(job.taskID, 'state')
+          : undefined,
+        terminalUnreconciled: job
+          ? options.backgroundJobBoard.field(job.taskID, 'terminalUnreconciled')
+          : undefined,
+      });
+
+      if (!job) {
+        log('[reconcile-task] unknown or unowned task', {
+          parentSessionID,
+          requested,
+        });
+        return [
+          `task_id: ${requested}`,
+          'state: unknown',
+          '',
+          '<task_error>',
+          'unknown or unowned background task',
+          '</task_error>',
+        ].join('\n');
+      }
+
+      if (!job.terminalUnreconciled) {
+        const state = options.backgroundJobBoard.getState(job.taskID);
+        log('[reconcile-task] task already reconciled or not terminal', {
+          taskID: job.taskID,
+          state,
+        });
+        return [
+          `task_id: ${job.taskID}`,
+          `state: ${state ?? 'unknown'}`,
+          '',
+          'Task is already reconciled or not in a terminal state.',
+        ].join('\n');
+      }
+
+      const updated = options.backgroundJobBoard.markReconciled(job.taskID);
+      log('[reconcile-task] marked reconciled', {
+        taskID: job.taskID,
+        alias: options.backgroundJobBoard.field(job.taskID, 'alias'),
+        previousTerminalState: job.terminalState,
+        reason: args.reason,
+      });
+
+      const finalState = updated?.state ?? 'reconciled';
+      return [
+        `task_id: ${job.taskID}`,
+        `state: ${finalState}`,
+        '',
+        `Task reconciled.${args.reason ? ` Reason: ${args.reason}` : ''}`,
+      ].join('\n');
+    },
+  });
+
+  return { reconcile_task };
+}