# Patch/Clarify 工具 + 工具目录 + 提示词优化 实现计划

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **Post-task review:** After each task commit, run the `simplify` skill to review changed code for reuse, quality, and efficiency.

**Goal:** 为 Neta Agent 引擎新增 patch（模糊补丁）和 clarify（阻塞式提问）工具，引入轻量工具目录，重写提示词提升工具触发率。

**Architecture:** 新建 `tools/catalog.ts` 管理工具 schema 元数据（不管理实例）。patch 工具依赖 `tools/fuzzy_match.ts` 九级匹配引擎。clarify 工具通过 `attempt.ts` 的 `onClarifyRequest` 回调实现阻塞，gateway 层用 Promise+Map 协调 WebSocket 消息，REST 入口降级为文本输出。

**Tech Stack:** TypeScript, @sinclair/typebox, Midway.js Socket.IO, Node.js fs/promises

**Base path:** `packages/backend/src/modules/netaclaw`

---

### Task 1: 工具目录 catalog.ts

**Files:**
- Create: `tools/catalog.ts`
- Modify: `runtime/prompt_builder.ts`

- [ ] **Step 1: 创建 tools/catalog.ts**

```typescript
// tools/catalog.ts
/**
 * 轻量工具目录 — 只注册 schema 元数据，不管理工具实例。
 * 用于 prompt_builder 查询可用工具名列表，替代硬编码 BASE_TOOLS。
 */

export interface ToolSchema {
  name: string;
  toolset: string;
  description: string;
}

const catalog = new Map<string, ToolSchema>();

export function registerSchema(schema: ToolSchema): void {
  catalog.set(schema.name, schema);
}

export function getToolNamesByToolsets(toolsets: string[]): string[] {
  const names: string[] = [];
  for (const entry of catalog.values()) {
    if (toolsets.includes(entry.toolset)) names.push(entry.name);
  }
  return names;
}

/** 默认启用的工具集（所有 Agent 都有） */
export const TOOLSET_DEFAULTS = ['base', 'planning', 'interaction'] as const;
```

- [ ] **Step 2: 修改 prompt_builder.ts — 从 catalog 查询工具名**

将 `runtime/prompt_builder.ts` 中硬编码的 `BASE_TOOLS` 和 `collectAvailableToolNames` 改为从 catalog 查询：

```typescript
// runtime/prompt_builder.ts — 替换原有的 BASE_TOOLS 和 collectAvailableToolNames

import { getToolNamesByToolsets, TOOLSET_DEFAULTS } from '../tools/catalog.js';

// 删除: const BASE_TOOLS = ['bash', 'read_file', 'write_file', 'list_dir', 'todo'];

export function collectAvailableToolNames(opts: CollectToolNamesOpts): string[] {
  const toolsets = [...TOOLSET_DEFAULTS];
  if (opts.memoryEnabled) toolsets.push('memory');
  if (opts.hasSkills) toolsets.push('skill');
  if (opts.crewRole === 'master') toolsets.push('crew');
  return getToolNamesByToolsets(toolsets);
}
```

- [ ] **Step 3: 现有工具注册到 catalog — bash.ts**

在 `tools/builtin/bash.ts` 文件末尾追加：

```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'bash', toolset: 'base', description: bashTool.description });
```

- [ ] **Step 4: 现有工具注册到 catalog — file.ts**

在 `tools/builtin/file.ts` 文件末尾追加：

```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'read_file', toolset: 'base', description: readFileTool.description });
registerSchema({ name: 'write_file', toolset: 'base', description: writeFileTool.description });
registerSchema({ name: 'list_dir', toolset: 'base', description: listDirTool.description });
```

- [ ] **Step 5: 现有工具注册到 catalog — memory.ts**

在 `tools/builtin/memory.ts` 文件末尾追加：

```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'memory_save', toolset: 'memory', description: '存储、更新或删除长期记忆。' });
registerSchema({ name: 'memory_recall', toolset: 'memory', description: '搜索长期记忆中的相关信息。' });
```

- [ ] **Step 6: 现有工具注册到 catalog — todo_tool.ts**

在 `tools/todo_tool.ts` 文件末尾追加：

```typescript
import { registerSchema } from './catalog.js';
registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description });
```

- [ ] **Step 7: 工厂工具注册到 catalog — skill 工具**

在以下文件末尾各追加 `registerSchema` 调用（schema 是静态的，只有 execute 需要运行时依赖）：

`tools/builtin/read_skill.ts` 末尾：
```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'read_skill', toolset: 'skill', description: '读取指定技能的 SKILL.md 内容' });
```

`tools/builtin/read_skill_file.ts` 末尾：
```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'read_skill_file', toolset: 'skill', description: '读取技能的附属文件内容' });
```

`tools/builtin/skill_manage.ts` 末尾：
```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'skill_manage', toolset: 'skill', description: '创建、更新或删除技能' });
```

- [ ] **Step 8: 工厂工具注册到 catalog — crew 工具**

`tools/builtin/delegate_task.ts` 末尾：
```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'delegate_task', toolset: 'crew', description: '委派任务给指定成员 Agent' });
```

`tools/builtin/delegate_parallel.ts` 末尾：
```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'delegate_parallel', toolset: 'crew', description: '并行委派多个任务给不同成员' });
```

`tools/builtin/escalate.ts` 末尾：
```typescript
import { registerSchema } from '../catalog.js';
registerSchema({ name: 'escalate', toolset: 'crew', description: '将问题升级给用户或上级 Agent' });
```

- [ ] **Step 9: catalog.ts 自注册所有工具文件**

> 消除 side-effect import 维护负担：catalog.ts 自身 import 所有工具文件触发注册。

在 `tools/catalog.ts` 底部追加：

```typescript
// --- 集中注册入口：import 触发各工具文件的 registerSchema ---
import './builtin/bash.js';
import './builtin/file.js';
import './builtin/patch.js';
import './builtin/clarify.js';
import './builtin/memory.js';
import './builtin/read_skill.js';
import './builtin/read_skill_file.js';
import './builtin/skill_manage.js';
import './builtin/delegate_task.js';
import './builtin/delegate_parallel.js';
import './builtin/escalate.js';
import './todo_tool.js';
```

> 注意：这些 import 放在 catalog.ts 底部（registerSchema 函数定义之后），确保函数已可用。消费方只需 `import { getToolNamesByToolsets } from '../tools/catalog.js'`。

- [ ] **Step 10: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 编译成功，无类型错误

- [ ] **Step 11: Commit**

```bash
git add packages/backend/src/modules/netaclaw/tools/catalog.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/bash.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/file.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/memory.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/read_skill_file.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/delegate_task.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/delegate_parallel.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/escalate.ts \
  packages/backend/src/modules/netaclaw/tools/todo_tool.ts \
  packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts \
  packages/backend/src/modules/netaclaw/gateway/server.ts \
  packages/backend/src/modules/netaclaw/service/agent_executor.ts
git commit -m "feat(netaclaw): add tool catalog for schema-based tool name resolution"
```

- [ ] **Step 12: 运行 simplify skill 审查本次变更**

---

### Task 2: 九级模糊匹配引擎 fuzzy_match.ts

**Files:**
- Create: `tools/fuzzy_match.ts`

- [ ] **Step 1: 创建 tools/fuzzy_match.ts — 类型定义和策略框架**

```typescript
// tools/fuzzy_match.ts
/**
 * 九级模糊匹配引擎
 * 按优先级依次尝试 9 种策略，首个成功即返回。
 */

export interface FuzzyMatchResult {
  strategy: string;
  startIndex: number;
  endIndex: number;
  matchedText: string;
}

type Strategy = {
  name: string;
  find: (content: string, search: string) => FuzzyMatchResult[];
};

/** 在 content 中查找所有匹配 search 的位置，按策略优先级 */
export function fuzzyFindAll(content: string, search: string): FuzzyMatchResult[] {
  for (const strategy of strategies) {
    const results = strategy.find(content, search);
    if (results.length > 0) return results;
  }
  return [];
}

// --- 辅助函数 ---

/** Levenshtein 距离 */
function levenshtein(a: string, b: string): number {
  const m = a.length, n = b.length;
  const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
    Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
  );
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      dp[i][j] = a[i - 1] === b[j - 1]
        ? dp[i - 1][j - 1]
        : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
    }
  }
  return dp[m][n];
}

/** 两字符串相似度 0-1 */
function similarity(a: string, b: string): number {
  if (a === b) return 1;
  const maxLen = Math.max(a.length, b.length);
  if (maxLen === 0) return 1;
  return 1 - levenshtein(a, b) / maxLen;
}

/** 将 content 按 normalizer 转换后查找 search，映射回原始索引 */
function findByNormalization(
  content: string,
  search: string,
  normalizer: (s: string) => string,
  strategyName: string,
): FuzzyMatchResult[] {
  const normContent = normalizer(content);
  const normSearch = normalizer(search);
  if (normSearch.length === 0) return [];

  // 构建字符映射：normIndex → origIndex
  // 逐字符对比原始和标准化后的内容，建立位置映射
  const charMap = buildCharMap(content, normContent);

  const results: FuzzyMatchResult[] = [];
  let pos = 0;
  while (true) {
    const idx = normContent.indexOf(normSearch, pos);
    if (idx === -1) break;
    const endIdx = idx + normSearch.length;
    const origStart = charMap[idx] ?? 0;
    const origEnd = (endIdx < charMap.length ? charMap[endIdx] : content.length);
    results.push({
      strategy: strategyName,
      startIndex: origStart,
      endIndex: origEnd,
      matchedText: content.slice(origStart, origEnd),
    });
    pos = idx + 1;
  }
  return results;
}

/**
 * 构建标准化索引 → 原始索引的映射表。
 * 对于行级 normalizer（不改变行数），按行对齐映射。
 * 对于可能改变行数的 normalizer，逐字符扫描。
 */
function buildCharMap(original: string, normalized: string): number[] {
  // 简单实现：如果行数相同，按行内偏移映射
  const origLines = original.split('\n');
  const normLines = normalized.split('\n');

  if (origLines.length === normLines.length) {
    // 行级映射：每行内按比例映射
    const map: number[] = [];
    let origOffset = 0;
    let normOffset = 0;
    for (let i = 0; i < normLines.length; i++) {
      const origLen = origLines[i].length;
      const normLen = normLines[i].length;
      for (let j = 0; j < normLen; j++) {
        map[normOffset + j] = origOffset + Math.round((j / Math.max(normLen, 1)) * origLen);
      }
      // 换行符映射
      if (i < normLines.length - 1) {
        map[normOffset + normLen] = origOffset + origLen;
      }
      origOffset += origLen + 1;
      normOffset += normLen + 1;
    }
    return map;
  }

  // 行数不同（如 whitespace_normalized）：按字符比例映射
  const map: number[] = [];
  const ratio = original.length / Math.max(normalized.length, 1);
  for (let i = 0; i < normalized.length; i++) {
    map[i] = Math.round(i * ratio);
  }
  return map;
}
```

- [ ] **Step 2: 实现 9 个策略**

在 `tools/fuzzy_match.ts` 底部追加策略数组：

```typescript
// --- 9 级策略 ---

const strategies: Strategy[] = [
  // 1. exact
  {
    name: 'exact',
    find(content, search) {
      const results: FuzzyMatchResult[] = [];
      let pos = 0;
      while (true) {
        const idx = content.indexOf(search, pos);
        if (idx === -1) break;
        results.push({ strategy: 'exact', startIndex: idx, endIndex: idx + search.length, matchedText: search });
        pos = idx + 1;
      }
      return results;
    },
  },

  // 2. line_trimmed — 每行 trim 后匹配
  {
    name: 'line_trimmed',
    find(content, search) {
      return findByNormalization(content, search, s => s.split('\n').map(l => l.trim()).join('\n'), 'line_trimmed');
    },
  },

  // 3. whitespace_normalized — 连续空白合并为单空格
  {
    name: 'whitespace_normalized',
    find(content, search) {
      return findByNormalization(content, search, s => s.replace(/\s+/g, ' '), 'whitespace_normalized');
    },
  },

  // 4. indent_flexible — 去除行首所有空白
  {
    name: 'indent_flexible',
    find(content, search) {
      return findByNormalization(content, search, s => s.split('\n').map(l => l.trimStart()).join('\n'), 'indent_flexible');
    },
  },

  // 5. escape_normalized — \\n → \n, \\t → \t
  {
    name: 'escape_normalized',
    find(content, search) {
      const norm = (s: string) => s.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
      return findByNormalization(content, search, norm, 'escape_normalized');
    },
  },

  // 6. trimmed_boundary — 仅首尾行 trim
  {
    name: 'trimmed_boundary',
    find(content, search) {
      const norm = (s: string) => {
        const lines = s.split('\n');
        if (lines.length > 0) lines[0] = lines[0].trim();
        if (lines.length > 1) lines[lines.length - 1] = lines[lines.length - 1].trim();
        return lines.join('\n');
      };
      return findByNormalization(content, search, norm, 'trimmed_boundary');
    },
  },

  // 7. unicode_normalized — 智能引号/em-dash/省略号 → ASCII
  {
    name: 'unicode_normalized',
    find(content, search) {
      const norm = (s: string) => s
        .replace(/[\u2018\u2019\u201A]/g, "'")
        .replace(/[\u201C\u201D\u201E]/g, '"')
        .replace(/[\u2013\u2014]/g, '--')
        .replace(/\u2026/g, '...');
      return findByNormalization(content, search, norm, 'unicode_normalized');
    },
  },

  // 8. block_anchor — 首尾行锚定 + 中间 Levenshtein ≥60%
  {
    name: 'block_anchor',
    find(content, search) {
      const searchLines = search.split('\n');
      if (searchLines.length < 3) return [];
      const firstLine = searchLines[0].trim();
      const lastLine = searchLines[searchLines.length - 1].trim();
      const contentLines = content.split('\n');
      const results: FuzzyMatchResult[] = [];

      for (let i = 0; i < contentLines.length; i++) {
        if (contentLines[i].trim() !== firstLine) continue;
        for (let j = i + searchLines.length - 1; j < contentLines.length && j < i + searchLines.length + 5; j++) {
          if (contentLines[j].trim() !== lastLine) continue;
          const candidateMiddle = contentLines.slice(i + 1, j).join('\n');
          const searchMiddle = searchLines.slice(1, -1).join('\n');
          if (similarity(candidateMiddle, searchMiddle) >= 0.6) {
            const startIndex = content.split('\n').slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
            const matchedLines = contentLines.slice(i, j + 1);
            const matchedText = matchedLines.join('\n');
            results.push({ strategy: 'block_anchor', startIndex, endIndex: startIndex + matchedText.length, matchedText });
          }
        }
      }
      return results;
    },
  },

  // 9. context_aware — 逐行相似度 ≥80%，整体 ≥50%
  {
    name: 'context_aware',
    find(content, search) {
      const searchLines = search.split('\n');
      const contentLines = content.split('\n');
      if (searchLines.length === 0) return [];
      const results: FuzzyMatchResult[] = [];

      for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
        let matchCount = 0;
        for (let j = 0; j < searchLines.length; j++) {
          if (similarity(contentLines[i + j].trim(), searchLines[j].trim()) >= 0.8) matchCount++;
        }
        if (matchCount / searchLines.length >= 0.5) {
          const startIndex = contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
          const matchedText = contentLines.slice(i, i + searchLines.length).join('\n');
          results.push({ strategy: 'context_aware', startIndex, endIndex: startIndex + matchedText.length, matchedText });
        }
      }
      return results;
    },
  },
];
```

- [ ] **Step 3: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 编译成功

- [ ] **Step 4: Commit**

```bash
git add packages/backend/src/modules/netaclaw/tools/fuzzy_match.ts
git commit -m "feat(netaclaw): add 9-level fuzzy match engine for patch tool"
```

- [ ] **Step 5: 运行 simplify skill 审查本次变更**

---

### Task 3: Patch 工具

**Files:**
- Create: `tools/builtin/patch.ts`
- Modify: `gateway/server.ts`
- Modify: `service/agent_executor.ts`

- [ ] **Step 1: 创建 tools/builtin/patch.ts**

```typescript
// tools/builtin/patch.ts
import { Type } from '@sinclair/typebox';
import * as fs from 'fs/promises';
import { AgentToolWithMeta, textResult } from '../common.js';
import { fuzzyFindAll } from '../fuzzy_match.js';
import { registerSchema } from '../catalog.js';

const PatchParams = Type.Object({
  path: Type.String({ description: '文件绝对路径' }),
  old_string: Type.String({ description: '要查找的文本片段' }),
  new_string: Type.String({ description: '替换为的文本' }),
  replace_all: Type.Optional(Type.Boolean({ description: '替换所有匹配，默认 false' })),
});

export const patchTool: AgentToolWithMeta<typeof PatchParams, unknown> = {
  name: 'patch',
  label: '模糊补丁',
  description: '对文件进行局部查找替换，支持模糊匹配。比 write_file 更安全、更省 token。',
  parameters: PatchParams,
  async execute(_id, params) {
    try {
      const content = await fs.readFile(params.path, 'utf-8');
      const matches = fuzzyFindAll(content, params.old_string);

      if (matches.length === 0) {
        return textResult(`未找到匹配。请检查 old_string 是否正确，或提供更多上下文。`);
      }
      if (matches.length > 1 && !params.replace_all) {
        return textResult(`找到 ${matches.length} 处匹配（策略: ${matches[0].strategy}），请提供更多上下文确保唯一匹配，或设置 replace_all=true。`);
      }

      // 从后往前替换，避免索引偏移
      let result = content;
      const sorted = [...matches].sort((a, b) => b.startIndex - a.startIndex);
      for (const m of sorted) {
        result = result.slice(0, m.startIndex) + params.new_string + result.slice(m.endIndex);
      }

      await fs.writeFile(params.path, result, 'utf-8');

      const count = params.replace_all ? matches.length : 1;
      return textResult(`已替换 ${count} 处（策略: ${matches[0].strategy}）: ${params.path}`);
    } catch (err: any) {
      return textResult(`patch 失败: ${err.message}`);
    }
  },
};

registerSchema({ name: 'patch', toolset: 'base', description: patchTool.description });
```

- [ ] **Step 2: gateway/server.ts — defaultTools 加入 patchTool**

```typescript
// gateway/server.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改 defaultTools（第 50 行）
private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
```

- [ ] **Step 3: agent_executor.ts — defaultTools 加入 patchTool**

```typescript
// service/agent_executor.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改 defaultTools（第 40 行）
private readonly defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
```

- [ ] **Step 3.5: crew_orchestrator.ts + crew_delegate.ts — builtinTools 加入 patchTool**

> Crew 子 Agent 也需要 patch 进行文件编辑（但不加 clarify，Crew 无用户交互通道）。

```typescript
// service/crew_orchestrator.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改第 24 行 BUILTIN_TOOL_NAMES 追加 'patch'
const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'delegate_task', 'delegate_parallel', 'escalate'];

// 修改第 175 行 builtinTools 追加 patchTool
const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
```

```typescript
// service/crew_delegate.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改第 17 行 BUILTIN_TOOL_NAMES 追加 'patch'
const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch'];

// 修改第 40 行 builtinTools 追加 patchTool
const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
```

- [ ] **Step 4: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 编译成功

- [ ] **Step 5: Commit**

```bash
git add packages/backend/src/modules/netaclaw/tools/builtin/patch.ts \
  packages/backend/src/modules/netaclaw/gateway/server.ts \
  packages/backend/src/modules/netaclaw/service/agent_executor.ts \
  packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts \
  packages/backend/src/modules/netaclaw/service/crew_delegate.ts
git commit -m "feat(netaclaw): add patch tool with fuzzy matching"
```

- [ ] **Step 6: 运行 simplify skill 审查本次变更**

---

### Task 4: Clarify 工具 + WebSocket 协议 + 阻塞机制

**Files:**
- Create: `tools/builtin/clarify.ts`
- Modify: `gateway/protocol.ts`
- Modify: `runtime/attempt.ts`
- Modify: `runtime/agent.ts`
- Modify: `gateway/server.ts`

- [ ] **Step 1: 创建 tools/builtin/clarify.ts**

```typescript
// tools/builtin/clarify.ts
import { Type } from '@sinclair/typebox';
import { AgentToolWithMeta, textResult } from '../common.js';
import { registerSchema } from '../catalog.js';

const ClarifyParams = Type.Object({
  question: Type.String({ description: '要问用户的问题' }),
  choices: Type.Optional(Type.Array(Type.String(), { maxItems: 4, description: '预设选项（最多4个）' })),
});

/** 降级 execute：当无 WebSocket 回调时，返回问题文本让 Agent 作为回复输出 */
export const clarifyTool: AgentToolWithMeta<typeof ClarifyParams, unknown> = {
  name: 'clarify',
  label: '向用户提问',
  description: '当任务需求不明确时，向用户提出澄清问题。支持选择题和开放式问题。',
  parameters: ClarifyParams,
  async execute(_id, params) {
    const choicesText = params.choices?.length
      ? `\n选项: ${params.choices.map((c, i) => `${i + 1}. ${c}`).join(', ')}`
      : '';
    return textResult(`[需要用户确认] ${params.question}${choicesText}`);
  },
};

registerSchema({ name: 'clarify', toolset: 'interaction', description: clarifyTool.description });
```

- [ ] **Step 2: 扩展 gateway/protocol.ts**

在 `protocol.ts` 的 `ClientMessage` 和 `ServerEvent` 联合类型中追加：

```typescript
// --- 客户端 → 服务端 追加 ---

export interface ClientClarifyResponseMessage {
  type: 'clarify_response';
  sessionId: string;
  requestId: string;
  answer: string;
}

export type ClientMessage = ClientChatMessage | ClientPingMessage | ClientSetThinkingLevelMessage | ClientClarifyResponseMessage;

// --- 服务端 → 客户端 追加 ---

export interface ServerClarifyRequestEvent {
  type: 'clarify_request';
  sessionId: string;
  data: { requestId: string; question: string; choices?: string[] };
}

// ServerEvent 联合类型追加 ServerClarifyRequestEvent
```

- [ ] **Step 3: 修改 runtime/attempt.ts — 新增 onClarifyRequest 回调**

```typescript
// attempt.ts — AttemptParams 接口追加字段
export interface AttemptParams {
  // ... 现有字段不变 ...
  onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}

// attempt.ts — 工具执行循环中（第 69-79 行区域），在 tool.execute 之前插入 clarify 分支
if (tc.name === 'clarify' && params.onClarifyRequest) {
  const args = JSON.parse(tc.arguments);
  try {
    resultText = await params.onClarifyRequest(args.question, args.choices);
  } catch {
    resultText = '用户未回答，请自行判断并继续。';
  }
} else if (!tool) {
  resultText = `错误: 工具 "${tc.name}" 不存在`;
} else {
  try {
    const result = await tool.execute(tc.id, JSON.parse(tc.arguments));
    resultText = typeof result === 'string' ? result : JSON.stringify(result);
  } catch (err: any) {
    resultText = `工具执行错误: ${err.message}`;
  }
}
```

- [ ] **Step 4: 修改 runtime/agent.ts — 透传 onClarifyRequest**

```typescript
// agent.ts — AgentRunParams 接口追加
export interface AgentRunParams {
  // ... 现有字段不变 ...
  onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}

// agent.ts — runAttempt 调用处（第 86-96 行）追加透传
return runAttempt({
  // ... 现有参数不变 ...
  onClarifyRequest: params.onClarifyRequest,
});
```

- [ ] **Step 5: 修改 gateway/server.ts — 注入 clarify 阻塞回调**

> **Critical**: Midway.js `@WSController` 是 request-scope（每连接一个实例），`clarifyResolvers` 必须是**模块级** Map，否则 clarify_response 可能路由到不同实例导致 resolve 永远不被调用。超时设为 20s（前端适配前快速降级）。

```typescript
// gateway/server.ts — 顶部 import 追加
import { randomUUID } from 'crypto';
import { clarifyTool } from '../tools/builtin/clarify.js';

// 模块级 Map（不是实例属性！@WSController 每连接一个实例）
const clarifyResolvers = new Map<string, { resolve: (answer: string) => void; timer: NodeJS.Timeout }>();

// defaultTools 追加 clarifyTool
private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool, clarifyTool];

// runAgent 调用处（第 219 行区域）追加 onClarifyRequest
onClarifyRequest: async (question, choices) => {
  const requestId = randomUUID();
  this.send({ type: 'clarify_request', sessionId: sid, data: { requestId, question, choices } });
  return new Promise<string>((resolve) => {
    const timer = setTimeout(() => {
      clarifyResolvers.delete(requestId);
      resolve('用户未在规定时间内回答。请根据已有信息自行判断并继续执行。');
    }, 20_000);  // 前端适配前用短超时快速降级
    clarifyResolvers.set(requestId, { resolve, timer });
  });
},

// onMessage 方法中（第 80 行区域）追加 clarify_response 处理
if (msg.type === 'clarify_response') {
  const entry = clarifyResolvers.get(msg.requestId);
  if (entry) {
    clearTimeout(entry.timer);
    clarifyResolvers.delete(msg.requestId);
    entry.resolve(msg.answer);
  }
}
```

- [ ] **Step 6: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 编译成功

- [ ] **Step 7: Commit**

```bash
git add packages/backend/src/modules/netaclaw/tools/builtin/clarify.ts \
  packages/backend/src/modules/netaclaw/gateway/protocol.ts \
  packages/backend/src/modules/netaclaw/runtime/attempt.ts \
  packages/backend/src/modules/netaclaw/runtime/agent.ts \
  packages/backend/src/modules/netaclaw/gateway/server.ts
git commit -m "feat(netaclaw): add clarify tool with blocking WebSocket interaction"
```

- [ ] **Step 8: 运行 simplify skill 审查本次变更**

---

### Task 5: 提示词重写 + todo TypeBox 迁移

**Files:**
- Modify: `runtime/prompt_guidance.ts`
- Modify: `tools/todo_tool.ts`

- [ ] **Step 1: 重写 prompt_guidance.ts — TOOL_USE_ENFORCEMENT 改为动态函数**

> **Critical**: 原设计硬编码了 clarify/patch 工具名，Crew 子 Agent 没有这些工具会被误导调用不存在的工具。改为根据实际可用工具列表动态生成。

删除 `prompt_guidance.ts` 第 29-34 行的 `export const TOOL_USE_ENFORCEMENT = ...`，替换为：

```typescript
/**
 * 根据实际可用工具列表动态生成工具使用规范。
 * Crew 子 Agent 没有 clarify/patch，不应在提示词中出现这些工具。
 */
export function getToolUseEnforcement(toolNames: string[]): string {
  const rules = `# 工具使用规范

你必须通过工具采取行动 - 不要只描述你打算做什么。

## 强制规则
- 当你说"我来检查"、"让我执行"时，必须在同一回复中立即发起工具调用
- 不要以"下一步我会..."结束回复 - 现在就执行
- 持续工作直到任务真正完成，不要停在计划阶段
- 每条回复要么包含推进任务的工具调用，要么向用户交付最终结果`;

  const scenarios: string[] = [];
  if (toolNames.includes('read_file')) scenarios.push('需要读取文件内容时 → read_file（不要凭记忆猜测）');
  if (toolNames.includes('patch')) scenarios.push('需要修改已有文件时 → patch（局部修改）或 write_file（新建/全量重写）');
  else if (toolNames.includes('write_file')) scenarios.push('需要修改文件时 → write_file');
  if (toolNames.includes('list_dir')) scenarios.push('需要了解目录结构时 → list_dir');
  if (toolNames.includes('bash')) scenarios.push('需要执行命令时 → bash');
  if (toolNames.includes('clarify')) scenarios.push('任务需求不明确时 → clarify（主动提问，不要猜测）');
  if (toolNames.includes('todo')) scenarios.push('复杂任务开始前 → todo（创建任务列表）');

  return scenarios.length > 0
    ? `${rules}\n\n## 必须使用工具的场景\n${scenarios.map(s => `- ${s}`).join('\n')}\n\n## 操作前先确认\n- 修改文件前先 read_file 确认当前内容\n- 执行命令前确认工作目录`
    : rules;
}
```

同时修改 `runtime/prompt_builder.ts` 中 Layer 2 的调用：

```typescript
// prompt_builder.ts — 将 TOOL_USE_ENFORCEMENT 常量引用改为函数调用
// 旧: import { TOOL_USE_ENFORCEMENT, ... } from './prompt_guidance.js';
// 新: import { getToolUseEnforcement, ... } from './prompt_guidance.js';

// Layer 2 中：
// 旧: parts.push(TOOL_USE_ENFORCEMENT);
// 新: parts.push(getToolUseEnforcement(params.availableToolNames));
```

- [ ] **Step 2: 重写 prompt_guidance.ts — TOOL_BEHAVIOR**

替换 `prompt_guidance.ts` 第 104-117 行的 `TOOL_BEHAVIOR` 对象：

```typescript
const TOOL_BEHAVIOR: Record<string, string> = {
  memory_save: `## 记忆使用策略
使用 memory_save 存储对未来对话有价值的持久信息：用户偏好、环境细节、项目约束。
保持记忆紧凑，聚焦于减少用户未来重复纠正的事实。
不要存储任务进度、会话结果、临时 TODO 状态。更新已有记忆而非创建重复条目。`,

  todo: `## 任务规划策略
收到用户请求后，先评估任务复杂度：
**必须使用 todo：** 涉及 2 个以上步骤、修改多个文件、需要先调研再实施、用户一次提出多个需求。
**无需使用 todo：** 单步操作（查看文件、回答问题、一条命令）、简单单文件小修改。
列表顺序=优先级。同一时间只标记一个 in_progress。完成立即标记 completed。
任务列表是你的工作契约 - 它让用户看到你的计划并跟踪进度。`,

  skill_manage: `## 技能管理策略
完成复杂任务（5步以上工具调用）后，考虑用 skill_manage 将方法保存为技能以便复用。
使用技能时如发现过时或错误，立即修补，不要等用户要求。`,

  patch: `## 文件编辑策略
修改已有文件时，优先使用 patch 进行局部替换，而非 write_file 全量重写。
**用 patch：** 修改函数、修复 bug、调整配置、添加/删除代码片段。
**用 write_file：** 创建新文件、文件需要完全重写。
old_string 提供足够上下文确保唯一匹配。不需要精确匹配缩进和空白。`,

  clarify: `## 主动提问策略
任务需求不明确、存在多种合理解读、或缺少关键信息时，使用 clarify 向用户提问。
**应该提问：** 指令模糊有多种解读、缺少关键参数、操作有风险需确认。
**不应提问：** 任务明确可直接执行、可从上下文推断、琐碎实现细节。
提供 choices 选项让用户快速选择。一次只问一个问题。`,
};
```

- [ ] **Step 3: todo_tool.ts — TypeBox 迁移**

替换 `tools/todo_tool.ts` 的 schema 定义（第 3-41 行）：

```typescript
import { Type, Static } from '@sinclair/typebox';
import { TodoStore } from '../runtime/todo_store.js';
import { registerSchema } from './catalog.js';

const TodoItemSchema = Type.Object({
  id: Type.String({ description: '唯一标识' }),
  content: Type.String({ description: '任务描述' }),
  status: Type.Union([
    Type.Literal('pending'),
    Type.Literal('in_progress'),
    Type.Literal('completed'),
    Type.Literal('cancelled'),
  ], { description: '当前状态' }),
});

const TodoParams = Type.Object({
  todos: Type.Optional(Type.Array(TodoItemSchema, { description: '任务项数组。省略则读取当前列表。' })),
  merge: Type.Optional(Type.Boolean({ description: 'true=按 id 增量更新。false(默认)=全量替换。', default: false })),
});

export const todoToolSchema = {
  name: 'todo' as const,
  description:
    '管理当前会话的任务列表。用于复杂任务或用户提供多个任务时。\n'
    + '不传参数=读取当前列表。传 todos 数组=写入。\n'
    + 'merge=false(默认)全量替换，merge=true 按 id 增量更新。\n'
    + '列表顺序=优先级。同一时间只能有一个 in_progress。',
  parameters: TodoParams,
};

type TodoExecArgs = Static<typeof TodoParams>;

/** 执行 todo 工具 */
export function executeTodo(
  store: TodoStore,
  args: TodoExecArgs,
): { todos: any[]; summary: any } {
  if (args.todos && args.todos.length > 0) {
    store.write(args.todos, args.merge ?? false);
  }
  return {
    todos: store.read(),
    summary: store.getSummary(),
  };
}

registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description });
```

- [ ] **Step 4: 验证构建**

Run: `cd packages/backend && npm run build`
Expected: 编译成功

- [ ] **Step 5: Commit**

```bash
git add packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts \
  packages/backend/src/modules/netaclaw/tools/todo_tool.ts
git commit -m "feat(netaclaw): rewrite tool prompts and migrate todo schema to TypeBox"
```

- [ ] **Step 6: 运行 simplify skill 审查本次变更**

---

### Task 6: 集成验证

**Files:** 无新文件，验证现有改动的集成正确性

- [ ] **Step 1: 完整构建验证**

Run: `cd packages/backend && npm run build`
Expected: 零错误，零警告

- [ ] **Step 2: 验证 catalog 工具名解析**

在构建产物中检查 `collectAvailableToolNames` 是否正确返回所有工具名。手动验证方式：

Run: `cd packages/backend && node -e "import('./dist/modules/netaclaw/tools/builtin/bash.js').then(() => import('./dist/modules/netaclaw/tools/builtin/file.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/patch.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/clarify.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/memory.js')).then(() => import('./dist/modules/netaclaw/tools/todo_tool.js')).then(() => import('./dist/modules/netaclaw/runtime/prompt_builder.js')).then(m => console.log(m.collectAvailableToolNames({ memoryEnabled: true, hasSkills: true, crewRole: 'master' })))"`

Expected: `['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'todo', 'clarify', 'memory_save', 'memory_recall', 'read_skill', 'read_skill_file', 'skill_manage', 'delegate_task', 'delegate_parallel', 'escalate']`

- [ ] **Step 3: 验证 prompt_guidance 输出**

Run: `cd packages/backend && node -e "import('./dist/modules/netaclaw/runtime/prompt_guidance.js').then(m => { console.log('=== ENFORCEMENT ==='); console.log(m.getToolUseEnforcement(['bash','read_file','write_file','list_dir','patch','todo','clarify']).slice(0, 300)); console.log('=== BEHAVIOR ==='); console.log(m.getToolBehaviorGuidance(['todo', 'patch', 'clarify']).slice(0, 300)); })"`

Expected: ENFORCEMENT 输出包含所有 7 个工具的场景描述；BEHAVIOR 输出包含 todo/patch/clarify 策略

- [ ] **Step 4: 最终 Commit（如有修复）**

```bash
git add -A
git commit -m "fix(netaclaw): integration fixes for tool catalog and prompts"
```
