ソースを参照

Add optional verifier agent

alvinreal 2 ヶ月 前
コミット
dfbfb31a44

+ 11 - 2
README.md

@@ -443,10 +443,19 @@ If any agent fails to respond, check your provider authentication and config fil
 
 ## Optional Agents
 
+### Verifier: Evidence Checker
+
+`verifier` is an optional read-only validation lane for V2. It is disabled by
+default, has no default MCPs, and uses explicit read/search permissions with
+`bash` set to ask for diagnostics. Enable it by removing `verifier` from
+`disabled_agents`, then use it only after implementation/background jobs are
+terminal and reconciled. Keep architecture, risk, maintainability, and YAGNI
+review routed to `oracle`.
+
 ### Observer: The Silent Witness
 
 > [!NOTE]
-> **Why a separate agent?** If your Orchestrator model is not multimodal, enable Observer to handle images, screenshots, PDFs, and other visual files. Observer is disabled by default and gives the Orchestrator a dedicated multimodal reader without forcing you to change your main reasoning model. Set `disabled_agents: []` and an `observer` model in your configuration. The bundled `opencode-go` install preset does this automatically because its GLM Orchestrator is not multimodal.
+> **Why a separate agent?** If your Orchestrator model is not multimodal, enable Observer to handle images, screenshots, PDFs, and other visual files. Observer is disabled by default and gives the Orchestrator a dedicated multimodal reader without forcing you to change your main reasoning model. Remove `observer` from `disabled_agents` and configure an `observer` model. The bundled `opencode-go` install preset enables Observer while keeping Verifier disabled.
 
 <table>
   <tr>
@@ -460,7 +469,7 @@ If any agent fails to respond, check your provider authentication and config fil
 
 - Images, screenshots, diagrams → `read` tool (native image support)
 - PDFs and binary documents → `read` tool (text + structure extraction)
-- **Disabled by default** — enable with `"disabled_agents": []` and configure a vision-capable model; installing with `--preset=opencode-go` enables it with `opencode-go/kimi-k2.6`
+- **Disabled by default** — remove `observer` from `disabled_agents` and configure a vision-capable model; installing with `--preset=opencode-go` enables it with `opencode-go/kimi-k2.6` while leaving `verifier` disabled
 
     </td>
   </tr>

+ 1 - 1
docs/configuration.md

@@ -102,7 +102,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `agents.<customAgent>.prompt` | string | — | Full execution prompt for a custom agent |
 | `agents.<customAgent>.orchestratorPrompt` | string | — | Exact `@agent` block injected into the orchestrator prompt; must start with `@<agent-name>` |
 | `agents.<agent>.displayName` | string | — | Custom user-facing alias for the agent in the active config |
-| `disabled_agents` | string[] | `["observer"]` | Agent names to disable globally. Set to `[]` to enable Observer; this is global, not per-preset |
+| `disabled_agents` | string[] | `["observer", "verifier"]` | Agent names to disable globally. Remove `observer` to enable visual analysis; remove `verifier` to enable the optional read-only validation lane. This is global, not per-preset |
 | `autoUpdate` | boolean | `true` | Automatically install plugin updates in the background; set to `false` for notification-only mode |
 | `multiplexer.type` | string | `"none"` | Multiplexer mode: `auto`, `tmux`, `zellij`, or `none` |
 | `multiplexer.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical` |

+ 4 - 1
docs/mcps.md

@@ -24,7 +24,10 @@ Built-in Model Context Protocol (MCP) servers ship with oh-my-opencode-slim and
 | `oracle` | none |
 | `explorer` | none |
 | `fixer` | none |
- | `councillor` | none |
+| `verifier` | none |
+| `observer` | none |
+| `council` | none |
+| `councillor` | none |
 
 ---
 

+ 3 - 3
docs/opencode-go-preset.md

@@ -34,11 +34,11 @@ If both presets are already in your config, switch from inside OpenCode:
 
 See [Preset Switching](preset-switching.md) for the full runtime switching
 workflow. If you originally installed with the default OpenAI preset, also add
-`"disabled_agents": []` to your config and restart OpenCode so Observer is
+`"disabled_agents": ["verifier"]` to your config and restart OpenCode so Observer is
 available before switching to `opencode-go`.
 
 `disabled_agents` is global, not per-preset. If you later switch back to OpenAI
-and restart while keeping `"disabled_agents": []`, Observer will remain enabled
+and restart while keeping `"disabled_agents": ["verifier"]`, Observer will remain enabled
 and use the default Observer model unless you configure one explicitly.
 
 ## Bundled Model Mapping
@@ -65,7 +65,7 @@ setting the top-level `preset` field:
 ```jsonc
 {
   "preset": "opencode-go",
-  "disabled_agents": [],
+  "disabled_agents": ["verifier"],
   "presets": {
     "opencode-go": {
       "orchestrator": { "model": "opencode-go/glm-5.1" },

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

@@ -165,6 +165,8 @@ Examples:
 
 - route UI review to `designer`,
 - route code review to `oracle`,
+- route optional read-only mechanical requirement/evidence checks to `verifier`
+  when enabled,
 - route test writing or test updates to `fixer`,
 - run final shell checks directly only when appropriate.
 

+ 8 - 1
oh-my-opencode-slim.schema.json

@@ -372,7 +372,7 @@
       }
     },
     "disabled_agents": {
-      "description": "Agent names to disable completely. Disabled agents are not instantiated and cannot be delegated to. Orchestrator and council internal agents (councillor) cannot be disabled. By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable.",
+      "description": "Agent names to disable completely. Disabled agents are not instantiated and cannot be delegated to. Orchestrator and council internal agents (councillor) cannot be disabled. By default, 'observer' and 'verifier' are disabled. Use ['verifier'] to enable only observer, ['observer'] to enable only verifier, or [] to enable both optional agents.",
       "type": "array",
       "items": {
         "type": "string"
@@ -661,6 +661,13 @@
               "items": {
                 "type": "string"
               }
+            },
+            "verifier": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
             }
           },
           "additionalProperties": {

+ 34 - 6
src/agents/index.test.ts

@@ -335,7 +335,7 @@ describe('agent classification', () => {
   });
 
   test('getAgentConfigs applies correct classification visibility and mode', () => {
-    // Enable all agents (including observer) for classification testing
+    // Enable all agents (including optional disabled-by-default agents) for classification testing
     const configs = getAgentConfigs({ disabled_agents: [] });
 
     // Primary agent
@@ -365,7 +365,7 @@ describe('createAgents', () => {
     expect(names).toContain('fixer');
   });
 
-  test('creates exactly 7 agents by default (observer disabled, council unconfigured)', () => {
+  test('creates exactly 7 agents by default (optional agents disabled, council unconfigured)', () => {
     const agents = createAgents();
     expect(agents.length).toBe(7);
   });
@@ -796,10 +796,10 @@ describe('disabled_agents', () => {
 
   test('agent count decreases when agents are disabled', () => {
     const agents = createAgents();
-    expect(agents.length).toBe(7); // observer disabled, council unconfigured
+    expect(agents.length).toBe(7); // observer/verifier disabled, council unconfigured
 
     const disabledConfig: PluginConfig = {
-      disabled_agents: ['observer', 'designer'],
+      disabled_agents: ['observer', 'verifier', 'designer'],
     };
     const disabledAgents = createAgents(disabledConfig);
     expect(disabledAgents.length).toBe(6);
@@ -840,14 +840,15 @@ describe('disabled_agents', () => {
     expect(enabled).not.toContain('janitor');
   });
 
-  test('empty disabled_agents creates observer but not unconfigured council', () => {
+  test('empty disabled_agents creates optional agents but not unconfigured council', () => {
     const config: PluginConfig = {
       disabled_agents: [],
     };
     const agents = createAgents(config);
     const names = agents.map((a) => a.name);
-    expect(agents.length).toBe(8);
+    expect(agents.length).toBe(9);
     expect(names).toContain('observer');
+    expect(names).toContain('verifier');
     expect(names).not.toContain('council');
   });
 });
@@ -891,3 +892,30 @@ describe('observer agent', () => {
     expect(DEFAULT_DISABLED_AGENTS).toContain('observer');
   });
 });
+
+describe('verifier agent', () => {
+  test('verifier is disabled by default and filtered from orchestrator prompt', () => {
+    const agents = createAgents();
+    const names = agents.map((a) => a.name);
+    const orchestrator = agents.find((a) => a.name === 'orchestrator');
+
+    expect(names).not.toContain('verifier');
+    expect(orchestrator?.config.prompt).not.toContain('@verifier');
+  });
+
+  test('verifier is enabled when removed from disabled_agents', () => {
+    const agents = createAgents({ disabled_agents: [] });
+    const verifier = agents.find((a) => a.name === 'verifier');
+
+    expect(verifier).toBeDefined();
+    expect(verifier?.config.model).toBe(DEFAULT_MODELS.verifier);
+    expect((verifier?.config.permission as any).read).toBe('allow');
+    expect((verifier?.config.permission as any).bash).toBe('ask');
+    expect((verifier?.config.permission as any).task).toBe('deny');
+    expect((verifier?.config.permission as any).write).toBe('deny');
+  });
+
+  test('DEFAULT_DISABLED_AGENTS contains verifier', () => {
+    expect(DEFAULT_DISABLED_AGENTS).toContain('verifier');
+  });
+});

+ 2 - 0
src/agents/index.ts

@@ -27,6 +27,7 @@ import {
   createOrchestratorAgent,
   resolvePrompt,
 } from './orchestrator';
+import { createVerifierAgent } from './verifier';
 
 export type { AgentDefinition } from './orchestrator';
 
@@ -208,6 +209,7 @@ const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
   oracle: createOracleAgent,
   designer: createDesignerAgent,
   fixer: createFixerAgent,
+  verifier: createVerifierAgent,
   observer: createObserverAgent,
   council: createCouncilAgent,
   councillor: createCouncillorAgent,

+ 16 - 2
src/agents/orchestrator.ts

@@ -77,6 +77,16 @@ const AGENT_DESCRIPTIONS: Record<string, string> = {
 - **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work • Sequential dependencies
 - **Rule of thumb:** Implementation are needed, schedule @fixer with clear scope. Bigger or lots of edits should be split by ownership and dispatched as parallel background fixer lanes when safe.`,
 
+  verifier: `@verifier
+- Lane: Optional mechanical validation after implementation
+- Role: Read-only evidence checker that validates completed work against explicit requirements
+- Permissions: Read/search files; bash diagnostics require approval; no edits or delegation
+- Stats: Cheap focused verification lane; best after task_status jobs are terminal and reconciled
+- Capabilities: Checks files, diffs, tests, diagnostics, and specialist outputs for PASS/FAIL/INCONCLUSIVE evidence
+- **Delegate when:** Implementation is complete and you need independent mechanical validation • Multiple independent scopes can be checked without contending for global checks • Acceptance criteria need evidence mapping
+- **Don't delegate when:** Jobs are still running • You need architecture/risk/maintainability review (use @oracle) • You need edits/fixes (use @fixer) • Validation is trivial enough to run directly
+- **Rule of thumb:** Orchestrator owns verification decisions. Use @verifier only as an optional read-only validation lane when enabled and evidence can be checked mechanically.`,
+
   council: `@council
 - Lane: High-stakes multi-model decision support
 - Role: Multi-LLM consensus engine that runs several councillors, synthesizes their views, and returns a structured council report.
@@ -105,6 +115,7 @@ const AGENT_DESCRIPTIONS: Record<string, string> = {
 const VALIDATION_ROUTING = [
   '- Route UI/UX validation and review to @designer',
   '- Route code review, simplification, maintainability review, and YAGNI checks to @oracle',
+  '- Route optional mechanical requirement/evidence checks to @verifier only after relevant jobs are terminal and reconciled',
   '- Route implementation to @fixer or multiple @fixer instances for maximum parallel execution',
   '- Route visual/media analysis and interpretation to @observer',
   '- If a request spans multiple lanes, delegate only the lanes that add clear value',
@@ -115,6 +126,7 @@ const PARALLEL_DELEGATION_EXAMPLES = [
   '- Multiple @explorer searches across different domains?',
   '- @explorer + @librarian research in parallel?',
   '- Multiple @fixer instances for faster, scoped implementation?',
+  '- Independent @verifier checks after scoped implementation jobs finish?',
   '- @observer + @explorer in parallel (visual analysis + code search)?',
 ];
 
@@ -227,12 +239,14 @@ When working through multi-step tasks, consider enabling auto-continue to avoid
 - 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
+- Verification decisions remain owned by the Orchestrator; specialists provide evidence, not final responsibility
+- Optional read-only mechanical validation can be delegated when that specialist is enabled and relevant background jobs are terminal/reconciled
+- Keep @oracle for architecture, risk, maintainability, simplification, and YAGNI review
 ${enabledValidationRouting}
 
 ## 6. Verify
 - Run relevant checks/diagnostics for the change
-- Use validation routing when applicable instead of doing all review work yourself
+- Use validation routing when applicable; do direct checks for simple cases
 - If test files are involved, prefer @fixer for bounded test changes and @oracle only for test strategy or quality review
 - Confirm specialists completed successfully
 - Verify solution meets requirements

+ 72 - 0
src/agents/verifier.ts

@@ -0,0 +1,72 @@
+import type { AgentDefinition } from './orchestrator';
+
+const VERIFIER_PROMPT = `You are Verifier - a read-only validation specialist.
+
+**Role**: Mechanically validate completed work against explicit requirements using evidence from files, diffs, tests, diagnostics, and provided task results.
+
+**Behavior**:
+- Check only the stated acceptance criteria and relevant implementation evidence
+- Inspect files, diffs, and diagnostic output as needed
+- Run diagnostics only when useful and safe; ask before bash execution
+- Report PASS, FAIL, or INCONCLUSIVE with concise evidence
+- Include exact file paths, line numbers, commands, and outputs when relevant
+
+**Constraints**:
+- READ-ONLY: Never edit, patch, write, format, or generate files
+- Do not redesign, refactor, or perform broad architecture review
+- Do not spawn subagents or delegate work
+- Do not replace @oracle for risk/design/maintainability review
+- If evidence is missing or jobs are still running, return INCONCLUSIVE and say what is needed
+
+**Output Format**:
+<verification>
+status: PASS | FAIL | INCONCLUSIVE
+evidence:
+- path:line or command/result evidence
+issues:
+- concrete mismatch or blocker, if any
+next:
+- minimal next action, if any
+</verification>`;
+
+export function createVerifierAgent(
+  model: string,
+  customPrompt?: string,
+  customAppendPrompt?: string,
+): AgentDefinition {
+  let prompt = VERIFIER_PROMPT;
+
+  if (customPrompt) {
+    prompt = customPrompt;
+  } else if (customAppendPrompt) {
+    prompt = `${VERIFIER_PROMPT}\n\n${customAppendPrompt}`;
+  }
+
+  return {
+    name: 'verifier',
+    description:
+      'Optional read-only validation specialist. Use after implementation is complete to check requirements against evidence, tests, and diagnostics.',
+    config: {
+      model,
+      temperature: 0.1,
+      prompt,
+      permission: {
+        '*': 'deny',
+        read: 'allow',
+        glob: 'allow',
+        grep: 'allow',
+        lsp: 'allow',
+        list: 'allow',
+        codesearch: 'allow',
+        ast_grep_search: 'allow',
+        bash: 'ask',
+        question: 'deny',
+        task: 'deny',
+        edit: 'deny',
+        write: 'deny',
+        patch: 'deny',
+        apply_patch: 'deny',
+      },
+    },
+  };
+}

+ 4 - 1
src/cli/config-io.test.ts

@@ -428,7 +428,7 @@ describe('config-io', () => {
 
     const saved = JSON.parse(readFileSync(litePath, 'utf-8'));
     expect(saved.preset).toBe('opencode-go');
-    expect(saved.disabled_agents).toEqual([]);
+    expect(saved.disabled_agents).toEqual(['verifier']);
     expect(saved.presets.openai).toBeDefined();
     expect(saved.presets['opencode-go'].orchestrator.model).toBe(
       'opencode-go/glm-5.1',
@@ -436,6 +436,9 @@ describe('config-io', () => {
     expect(saved.presets['opencode-go'].observer.model).toBe(
       'opencode-go/kimi-k2.6',
     );
+    expect(saved.presets['opencode-go'].verifier.model).toBe(
+      'opencode-go/deepseek-v4-flash',
+    );
   });
 
   test('disableDefaultAgents disables explore and general agents', () => {

+ 3 - 1
src/cli/providers.test.ts

@@ -69,7 +69,7 @@ describe('providers', () => {
     });
 
     expect(config.preset).toBe('opencode-go');
-    expect(config.disabled_agents).toEqual([]);
+    expect(config.disabled_agents).toEqual(['verifier']);
     expect((config.presets as any).openai).toBeDefined();
     const agents = (config.presets as any)['opencode-go'];
     expect(agents).toBeDefined();
@@ -83,6 +83,8 @@ describe('providers', () => {
     expect(agents.designer.model).toBe('opencode-go/kimi-k2.6');
     expect(agents.fixer.model).toBe('opencode-go/deepseek-v4-flash');
     expect(agents.fixer.variant).toBe('high');
+    expect(agents.verifier.model).toBe('opencode-go/deepseek-v4-flash');
+    expect(agents.verifier.variant).toBe('high');
     expect(agents.observer.model).toBe('opencode-go/kimi-k2.6');
   });
 

+ 2 - 1
src/cli/providers.ts

@@ -52,6 +52,7 @@ export const MODEL_MAPPINGS = {
     explorer: { model: 'opencode-go/minimax-m2.7' },
     designer: { model: 'opencode-go/kimi-k2.6', variant: 'medium' },
     fixer: { model: 'opencode-go/deepseek-v4-flash', variant: 'high' },
+    verifier: { model: 'opencode-go/deepseek-v4-flash', variant: 'high' },
     observer: { model: 'opencode-go/kimi-k2.6' },
   },
 } as const;
@@ -94,7 +95,7 @@ export function generateLiteConfig(
   };
 
   if (preset === 'opencode-go') {
-    config.disabled_agents = [];
+    config.disabled_agents = ['verifier'];
   }
 
   const createAgentConfig = (

+ 4 - 0
src/config/agent-mcps.test.ts

@@ -2,6 +2,10 @@ import { describe, expect, test } from 'bun:test';
 import { DEFAULT_AGENT_MCPS, parseList } from './agent-mcps';
 
 describe('parseList', () => {
+  test('verifier has no default MCPs', () => {
+    expect(DEFAULT_AGENT_MCPS.verifier).toEqual([]);
+  });
+
   test('empty list returns empty array', () => {
     expect(parseList([], ['mcp1', 'mcp2'])).toEqual([]);
   });

+ 1 - 0
src/config/agent-mcps.ts

@@ -14,6 +14,7 @@ export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
   librarian: ['websearch', 'context7', 'grep_app'],
   explorer: [],
   fixer: [],
+  verifier: [],
   observer: [],
   council: [],
   councillor: [],

+ 5 - 1
src/config/constants.ts

@@ -10,6 +10,7 @@ export const SUBAGENT_NAMES = [
   'oracle',
   'designer',
   'fixer',
+  'verifier',
   'observer',
   'council',
   'councillor',
@@ -36,6 +37,7 @@ export const ORCHESTRATABLE_AGENTS = [
   'oracle',
   'designer',
   'fixer',
+  'verifier',
   'observer',
   'council',
 ] as const;
@@ -56,6 +58,7 @@ export function getOrchestratableAgents(
 export const SUBAGENT_DELEGATION_RULES: Record<AgentName, readonly string[]> = {
   orchestrator: ORCHESTRATABLE_AGENTS,
   fixer: [],
+  verifier: [],
   designer: [],
   explorer: [],
   librarian: [],
@@ -74,6 +77,7 @@ export const DEFAULT_MODELS: Record<AgentName, string | undefined> = {
   explorer: 'openai/gpt-5.4-mini',
   designer: 'openai/gpt-5.4-mini',
   fixer: 'openai/gpt-5.4-mini',
+  verifier: 'openai/gpt-5.4-mini',
   observer: 'openai/gpt-5.4-mini',
   council: 'openai/gpt-5.4-mini',
   councillor: 'openai/gpt-5.4-mini',
@@ -106,4 +110,4 @@ export const STABLE_POLLS_THRESHOLD = 3;
 
 /** Agents that are disabled by default. Users must explicitly enable them
  *  by removing from disabled_agents and configuring an appropriate model. */
-export const DEFAULT_DISABLED_AGENTS: string[] = ['observer'];
+export const DEFAULT_DISABLED_AGENTS: string[] = ['observer', 'verifier'];

+ 3 - 1
src/config/schema.ts

@@ -9,6 +9,7 @@ const FALLBACK_AGENT_NAMES = [
   'explorer',
   'librarian',
   'fixer',
+  'verifier',
 ] as const;
 
 const MANUAL_AGENT_NAMES = [
@@ -74,6 +75,7 @@ const FallbackChainsSchema = z
     explorer: AgentModelChainSchema.optional(),
     librarian: AgentModelChainSchema.optional(),
     fixer: AgentModelChainSchema.optional(),
+    verifier: AgentModelChainSchema.optional(),
   })
   .catchall(AgentModelChainSchema);
 
@@ -322,7 +324,7 @@ export const PluginConfigSchema = z
         'Agent names to disable completely. ' +
           'Disabled agents are not instantiated and cannot be delegated to. ' +
           'Orchestrator and council internal agents (councillor) cannot be disabled. ' +
-          "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable.",
+          "By default, 'observer' and 'verifier' are disabled. Use ['verifier'] to enable only observer, ['observer'] to enable only verifier, or [] to enable both optional agents.",
       ),
     disabled_mcps: z.array(z.string()).optional(),
     // Multiplexer config (new unified config - preferred)

+ 1 - 0
src/hooks/task-session-manager/index.ts

@@ -35,6 +35,7 @@ const AGENT_NAME_SET = new Set<AgentName>([
   'explorer',
   'librarian',
   'fixer',
+  'verifier',
   'observer',
   'council',
   'councillor',

+ 1 - 0
src/utils/background-job-board.ts

@@ -47,6 +47,7 @@ const AGENT_PREFIX: Record<string, string> = {
   designer: 'des',
   explorer: 'exp',
   fixer: 'fix',
+  verifier: 'ver',
   librarian: 'lib',
   observer: 'obs',
   oracle: 'ora',

+ 2 - 0
src/utils/session-manager.ts

@@ -39,6 +39,8 @@ function aliasPrefix(agentType: AgentName): string {
       return 'des';
     case 'fixer':
       return 'fix';
+    case 'verifier':
+      return 'ver';
     case 'observer':
       return 'obs';
     case 'council':