diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 0f0723cfa..2fea1425b 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -18,3 +18,7 @@ bd8ac3ebd916eafbce1040cea39ff9d966bb3493
bb94c967934cf3c355a143435bc2b292a917275f
05ab77d55f9b0c073708b17a0e3c34580b92ac78
295041e71071b2fa66bbc3c8517a1697c9d6c00b
+
+# P2-06 纯搬迁与 include 改写(2026-09-28)
+51c37dea8be09e73b823f0799cef301352fbf2ff
+f095f76256a47f86a403a66fde160e8d96782039
diff --git a/CLAUDE.md b/CLAUDE.md
index 6db9fa94f..699f4cacd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -291,6 +291,8 @@ CLI `--worktree [name]` / `-w`(TUI):启动即建 worktree,name 可为 PR 引用(
## Skills, Memory, And Project Instructions
+P2-06 module boundaries: generic YAML frontmatter lives in `src/utils/frontmatter.*`; TUI skill command registration lives in `src/tui/commands/skill_commands.*`; reusable command expansion lives under `src/skills/`. Provider-dependent hook payload builders live in `src/agent/hook_bridge/hook_events.*`, while `src/hooks/hook_payload.*` owns the generic startup payload and `src/hooks/hook_seeds.*` owns read-only hook seed definitions.
+
[src/skills/](src/skills) discovers `SKILL.md` files from configured global, project, and external skill directories. Skill metadata is read from YAML frontmatter at startup; full bodies are loaded lazily through skill invocation or the `skill_view` tool.
Proactive skill discovery (`inject-skill-index-into-context`): a compact skill index (name + description + optional `whenToUse` frontmatter, grouped by category) is injected each request into the session-context system-reminder built by `build_session_context_prompt` — the same per-request, never-persisted block that carries project instructions and the memory index. Budget is 1% of the context window in chars (4 chars/token, 8000-char fallback), 250 chars per entry (UTF-8-safe truncation); over budget degrades to names-only, then tail-cut with a `(+N more — call skills_list)` marker. The static `# Skills` prompt section points at this index ("err on the side of loading"); `skills_list` remains the fallback enumerator only. Without this index the model has zero visibility into installed skills and never invokes them proactively.
@@ -564,7 +566,7 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `
` → `{builtins:[{name,description}][, skills:[{name,description}]]}` | InputBar 斜杠下拉的命令清单。builtins **硬编码白名单 = init + compact**(描述与 TUI `register_builtin_commands` / `register_init_command` 对齐)。**`workspace` 参数(由 `expand-webui-skill-commands` 引入)**:缺省 → 不返回 `skills` 字段(向后兼容旧客户端);提供 → handler 用 `acecode::initialize_skill_registry(tmp, *cfg, workspace_cwd)` 临时构造一个 SkillRegistry 扫该 workspace 的项目链(`.agent/skills`、`.acecode/skills` + 全局 + external_dirs),与 daemon 全局 SkillRegistry 合并(workspace local 优先,first-wins by name),按字典序输出 skills 字段。`/init` `/compact` 在 web 端选中后只插入输入框 + chip 高亮,daemon 端**不**做特殊执行(原 add-webui-slash-commands 决策);`/
args` 由下面新加的 expander 真展开。实现:`src/web/handlers/commands_handler.{hpp,cpp}`(纯函数 `build_commands_payload` + gtest case)|
-| `POST /api/sessions/:id/messages`(行为扩展) | `expand-webui-skill-commands` 引入:在 `send_input` 之前调 `try_expand_skill_command(text, registry)`(`src/web/handlers/skill_command_expander.{hpp,cpp}`)。命中已知 skill 名(按 session 的 workspace cwd 临时 scan)→ text 被替换为 `build_skill_invocation_hint(meta, args)` 的**轻量提示**:`[SYSTEM: User invoked / skill] + Description + Use skill_view(name=...) to load full SKILL.md + User's request: `。**不**注入 SKILL.md body / supporting_files,LLM 第一次看到提示后主动 invoke `skill_view` tool 把 SKILL.md 拉一次进 context,后续重复同名 `/skill` 调用不再注入(避免 context 膨胀)。TUI `src/skills/skill_commands.cpp::cmd.execute` 也走同一个 `build_skill_invocation_hint`,跨端行为统一。Builtin (`/init`/`/compact`) 不在 SkillRegistry 中 → 透传走普通 user message;未知命令 (`/foobar`) 同样透传。**不**新增执行端点,**不**改 AgentLoop API |
+| `POST /api/sessions/:id/messages`(行为扩展) | `expand-webui-skill-commands` 引入:在 `send_input` 之前调 `try_expand_skill_command(text, registry)`(`src/skills/skill_command_expander.{hpp,cpp}`)。命中已知 skill 名(按 session 的 workspace cwd 临时 scan)→ text 被替换为 `build_skill_invocation_hint(meta, args)` 的**轻量提示**:`[SYSTEM: User invoked / skill] + Description + Use skill_view(name=...) to load full SKILL.md + User's request: `。**不**注入 SKILL.md body / supporting_files,LLM 第一次看到提示后主动 invoke `skill_view` tool 把 SKILL.md 拉一次进 context,后续重复同名 `/skill` 调用不再注入(避免 context 膨胀)。TUI `src/tui/commands/skill_commands.cpp::cmd.execute` 也走同一个 `build_skill_invocation_hint`,跨端行为统一。Builtin (`/init`/`/compact`) 不在 SkillRegistry 中 → 透传走普通 user message;未知命令 (`/foobar`) 同样透传。**不**新增执行端点,**不**改 AgentLoop API |
| `POST /api/open-in-explorer` body `{path}` → `{ok:true}` / 400 `{ok:false,error}` / 501 | webapp 兼容模式(Edge --app,无 webview bridge)右键菜单「在资源管理器中打开」的 REST 通路。门控 = `WebServerDeps.open_in_explorer` 回调是否注入(worker.cpp 仅在 `native_folder_picker_enabled` 时填,复用 `desktop::open_path_in_file_manager`:接受任意现存本地绝对普通文件或目录,不做 workspace/managed-root 边界判断)。Windows 文件用 Explorer `/select` 选中,目录保持直接打开。前端 `DesktopContextMenu::openTargetInExplorer` bridge 优先、REST 兜底 |
| `GET /api/fs/roots` → `{host, os, home, roots[{path,label,drive_type,total_bytes?,free_bytes?}], quick[{kind,path}], workspaces[{hash,name,path}]}` / `GET /api/fs/list?path=&show_hidden=1` → `{path, parent, truncated, entries[{name,path,kind,hidden,size?,modified_ms?,link_target?}]}` | Web「选择文件 / 文件夹」弹窗(`add-web-path-picker`)的服务端目录浏览器:**已鉴权即可只读浏览 daemon 全盘**,不走 `/api/files` 的 workspace 白名单,噪音目录不过滤,junction 照常列出并附 `link_target`;路径只做词法归一**不 canonical**(经 `C:\Users\shao` junction 进入返回的仍是 `C:/...`,与原生对话框一致,否则同一目录会注册出两个 workspace hash);5000 条上限置 `truncated`;`ec.message()` 在中文 Windows 是 GBK,入 JSON 前必须 `ensure_utf8`,否则 dump 抛异常整个请求变裸 500。触发策略:有 Desktop bridge 走原生对话框,没有(普通浏览器 / Edge --app 兼容模式 / 远程 Web)一律 web 弹窗,不再调用 daemon 的原生 REST 对话框(那会在 daemon 所在机器桌面弹框并把请求挂死)。实现:`src/web/handlers/fs_browser_handler.{hpp,cpp}` 纯函数 + `routes_fs.cpp`;前端 `lib/pathPicker.js`(纯逻辑)+ `lib/pathPickerHost.js`(Promise 形态的单实例请求)+ `components/PathPickerModal.jsx` / `PathPickerHost.jsx`(App 顶层挂载),三处接入点 `lib/workspacePicker.js` / `lib/environmentSettings.js::pickEnvironmentPath` / `lib/desktopPreviewFilePicker.js::pickPreviewFile` |
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c76f8d221..9019d7a43 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -182,7 +182,7 @@ set(ACECODE_TUI_DIRS
${CMAKE_SOURCE_DIR}/src/markdown)
set(ACECODE_TUI_TESTABLE_SUBSETS
commands/ resume/ path_reference/ markdown/
- drag_scroll.cpp text_input_ops.cpp skill_commands.cpp slash_command_usage.cpp)
+ drag_scroll.cpp text_input_ops.cpp slash_command_usage.cpp)
set(ACECODE_TUI_SOURCES)
foreach(_tui_dir IN LISTS ACECODE_TUI_DIRS)
if(NOT IS_DIRECTORY "${_tui_dir}")
diff --git a/docs/skills-implementation.md b/docs/skills-implementation.md
index 3a1f4d3d6..7dd0c553b 100644
--- a/docs/skills-implementation.md
+++ b/docs/skills-implementation.md
@@ -48,7 +48,7 @@
## 3. SKILL.md frontmatter
-acecode 只识别 YAML frontmatter 的一个子集,解析器位于 `src/skills/frontmatter.{hpp,cpp}`。仅读取文件首 8KB 足以抓到 frontmatter。
+acecode 只识别 YAML frontmatter 的一个子集,解析器位于 `src/utils/frontmatter.{hpp,cpp}`。仅读取文件首 8KB 足以抓到 frontmatter。
| 字段 | 类型 | 用途 |
|------|------|------|
@@ -108,7 +108,7 @@ struct SkillMetadata {
## 5. 激活路径 A — 用户输入 `/`
-相关代码:`src/skills/skill_commands.{hpp,cpp}`、`src/skills/skill_activation.{hpp,cpp}`、`main.cpp` 对 `CommandRegistry` 的集成。
+相关代码:`src/tui/commands/skill_commands.{hpp,cpp}`、`src/skills/skill_activation.{hpp,cpp}`、`main.cpp` 对 `CommandRegistry` 的集成。
### 注册流程
@@ -232,11 +232,11 @@ struct SkillMetadata {
| 文件 | 职责 |
|------|------|
| `src/skills/skill_metadata.hpp` | `SkillMetadata` 数据结构 |
-| `src/skills/frontmatter.{hpp,cpp}` | 受限 YAML frontmatter 解析 |
+| `src/utils/frontmatter.{hpp,cpp}` | 受限 YAML frontmatter 解析 |
| `src/skills/skill_loader.{hpp,cpp}` | 单个 `SKILL.md` 目录 → `SkillMetadata`;name 归一化;平台匹配 |
| `src/skills/skill_registry.{hpp,cpp}` | 多根递归扫描、去重、查询、支持文件定位 |
| `src/skills/skill_activation.{hpp,cpp}` | 用户 `/` 触发时向 agent 投递的消息模板 |
-| `src/skills/skill_commands.{hpp,cpp}` | 把每个 skill 注册/反注册为 slash 命令;`/skills reload` |
+| `src/tui/commands/skill_commands.{hpp,cpp}` | 把每个 skill 注册/反注册为 slash 命令;`/skills reload` |
| `src/tool/skills_tool.{hpp,cpp}` | `skills_list` 工具(tier-1) |
| `src/tool/skill_view_tool.{hpp,cpp}` | `skill_view` 工具(tier-2/3,含路径安全校验) |
| `src/prompt/system_prompt.cpp` | 系统提示词 Skills 段 |
diff --git "a/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md" "b/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md"
index 64c83fd64..69448ea13 100644
--- "a/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md"
+++ "b/docs/\347\253\236\345\223\201\350\260\203\347\240\224\346\212\245\345\221\212_\345\256\214\346\225\264\347\211\210_2026-08.md"
@@ -56,7 +56,7 @@
| 2 | 计划模式 `/plan` + 权限模式 `/mode` | `builtin_commands.cpp:1964,1968`、`src/tool/plan_mode_tool.cpp` |
| 3 | 子代理:spawn_subagent 工具 + `/tasks` 后台任务 + Web Swarm | `src/tool/spawn_subagent_tool.cpp`、`builtin_commands.cpp:2022`、`web/src/components/SwarmModeIcon.jsx` |
| 4 | TodoWrite 工具 | `src/tool/todo_write_tool.cpp` |
-| 5 | opencode 式 markdown 自定义命令(`$ARGUMENTS` 插值,注册为斜杠命令) | `src/commands/opencode_command.cpp`、`opencode_command_registry.cpp` |
+| 5 | opencode 式 markdown 自定义命令(`$ARGUMENTS` 插值,注册为斜杠命令) | `src/skills/opencode_command.cpp`、`opencode_command_registry.cpp` |
| 6 | OAuth:GitHub Copilot Device Flow + Grok Coding Plan(xAI) | `src/provider/auth/github_auth.{hpp,cpp}`、`xai_auth.{hpp,cpp}`、`web/routes/routes_models.cpp` |
| 7 | 自更新 `acecode upgrade` / `update` | `src/upgrade/`(apply / manifest / package / version)、`src/tui/cli_dispatch.cpp` |
| 8 | 复制助手消息(鼠标选区 OSC52 / 系统剪贴板) | `src/main.cpp` clipboard_copy_status_message 路径、`src/tui/clipboard_helpers.cpp` |
diff --git a/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md b/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md
index ea5a572f0..2be00f9c7 100644
--- a/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md
+++ b/openspec/changes/refactor20260927-restructure-src-layers/branch-inventory.md
@@ -464,3 +464,9 @@ Base: `master` (`e15df9eadfaf05a170fd8b83fb26939452e2d3e9`). Read-only snapshot.
P2-05 不保留旧路径转发头。合并本阶段主线前,先在各自的迁移分支上运行 `normalize_includes.py --scope src` 与 `--scope tests`,再按既有方案演练 rebase / patch。不要改写或删除原 ref。
本阶段除映射表中的文件搬迁外,还拆出了 `config/vocab/permission_mode.hpp`、`provider/models_dev_catalog_cache`、`provider/model_probe_cache`、`tool/{safe_text_write,text_file_errors}`、`tool/web_search/region_cache`、`desktop/workspace_state` 与 `tui/slash_command_usage`。涉及 `state_file`、编码报错或 models.dev 全局缓存的调用方需显式包含对应模块的新头,不能仅依靠旧的间接 include。原状态字段和模型工具名映射保持。
+
+## P2-06 迁移提示(2026-09-28)
+
+本任务把 frontmatter 移到 utils,把可复用的 opencode / skill 命令展开器归到 skills,把技能斜杠命令注册归到 tui/commands,对应五个测试文件同步归位。旧路径不保留转发头。上表九个遗留 ref 继续按映射运行 `migrate_branch.py --apply-map` 或 include 规范化脚本,原 ref 的功能内容与去留仍按 P3-03 逐项处理。
+
+hook 模型事件构造器与只读种子表属于 extract 行,不能用整文件路径替换代替职责拆分:模型事件调用方引用 `agent/hook_bridge/hook_events.hpp`,种子消费者引用 `hooks/hook_seeds.hpp`。原 `hooks/hook_payload.hpp` 只提供无模型依赖的启动事件,种子安装事务仍由 skills 的统一安装器协调。
diff --git a/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-06-hooks-and-skills.md b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-06-hooks-and-skills.md
new file mode 100644
index 000000000..2ca546f07
--- /dev/null
+++ b/openspec/changes/refactor20260927-restructure-src-layers/verification/P2-06-hooks-and-skills.md
@@ -0,0 +1,76 @@
+# P2-06 hooks 与 skills 拆分验证
+
+任务认领由 PR #82 合入 master,基线 `d993ae5f00c2d4c475a04c05d5509ffbbe3b4d8e`,已包含 P2-03 / P2-04 / P2-05 的验收。工作分支为 `refactor20260927/P2-06`,复用已合入且无活动进程、无未提交改动的原 P2-03 工作区;构建使用新目录。
+
+## 实现边界
+
+- `51c37dea`:13 个文件的纯 `git mv`,逐项 R100,0 行增删。frontmatter 归 utils,opencode_command 与 skill_command_expander 归 skills,skill_commands 归 tui/commands;五个测试同步镜像归位。
+- `f095f762`:19 个源/测试文件的 20 处 include 路径改写;CMake 删除失效的根级 skill_commands.cpp 子集项,已登记的 commands/ 子集继续确保该实现参与 acecode_testable。
+- 模型注册源、模型加载完成和助手消息完成三个事件构造器及其私有辅助移到 agent/hook_bridge/hook_events。hooks/hook_payload 仅保留无 provider 依赖的启动前事件;调用方直接引用实际定义头,旧路径无转发壳。
+- DefaultHookSeed 与只读种子表移到 hooks/hook_seeds。注册表只依赖 hooks;skills 安装器继续协调完整种子事务并读取该表,既有版本/哈希/迁移规则保持。
+- 两类启动事件复用已有 platform 进程 ID 原语,保留原来的 int 转换。移除终端探测对 hooks 配置的无用 include。
+- 同步 CLAUDE.md、技能说明和对应测试注释中的路径;help-source 生成器未引用此次移动路径,无须改写生成内容。
+
+## 当前检查
+
+src / tests include 规范化第二次检查为 0 改动;映射 strict、分层已有阻断项、行数和所有权 strict 通过。分层违规从 P2-05 的 87 项降为 83 项,本任务相关 hooks→provider、hooks→skills 和 environment→hooks 反向依赖消除。其余项目仍由后续 P2-07 / P2-08 处理。
+
+前端首次测试因本工作区未安装 node_modules,缺少 @babel/core 而未能启动完整套件;已执行 pnpm install --frozen-lockfile,不改锁文件。依赖齐备后的完整 `pnpm test` 与 `pnpm build` 均通过。
+
+对照机械提交前后的函数文本,三个模型事件构造器除了等价的进程 ID 原语调用外保持原样;hook 种子表函数完全相同。
+
+## 本机原生验证
+
+固定源码 `df9ef9567bb24eabb7b6294ea16f49fd991f845d`,使用 MSVC 2022 x64 / Ninja / Release,全新目录 `build/p2-06-hooks-skills`,BUILD_TESTING 与 Desktop 均开启。acecode、acecode-desktop、acecode_unit_tests 及五个 EXCLUDE_FROM_ALL 冒烟目标全部构建成功。
+
+CMake File API 为 59 个目标、3637 个元组;对照已验收 P2-05 的 59 / 3623,目标和原有源文件归属、编译参数全部相同,无删除元组。14 个新增元组仅对应 hook_events 与 hook_seeds 的源、头及生成对象。
+
+- 定向用例 351 / 351 通过,0 SKIP,包括三个 SkillCommandsReload、11 个 TerminalResolver 及 hooks / skills / seeder 用例。
+- 独立 HOME/TEMP 下完整用例清单 5117,实际运行 5116,9 SKIP,0 失败,退出码 0。
+- 对照 P2-05 完整本机记录,用例名和 SKIP 集合完全一致,未通过过滤隐藏失败。
+
+`migrate_branch.py --check` 已执行,总体仍为非零:documentation 79、layers 2331(含尚未进行的最终分组检查)、migration_paths 60;include_normalization、map、ownership、seed 均为 0。此报告不代表 P3 已完成;过渡布局的实际分层违规仍是上文的 83 项。九个遗留分支的迁移通知已记录在 branch-inventory.md。
+
+## 四平台验证
+
+[refactor-matrix 36341836305](https://github.com/tmoonlight/acecode/actions/runs/36341836305) 固定上述源码提交,Windows / Linux / macOS / Deepin 全部完成构建与采集。PR #84 合入的窗口预告随后已合并到本分支,仅改 AGENTS.md / CLAUDE.md,src、tests、CMake、scripts、assets、web 相对验证源码无差异。
+
+| 平台 | targets / tuples | 用例清单 | 实际执行 / SKIP | 失败 |
+|---|---:|---:|---:|---:|
+| Windows x64 | 59 / 3637 | 5117 | 5110 / 10 | 4(既有基线) |
+| Linux x64 | 50 / 3551 | 5037 | 5028 / 16 | 0 |
+| macOS arm64 | 57 / 3696 | 5042 | 5035 / 14 | 10(既有基线) |
+| Deepin x64 | 13 / 1400 | 不运行单测 | 不适用 | 构建成功 |
+
+CI 使用 CTest 逐用例运行,实际执行统计包含脚本测试,与本机直接运行 gtest 的计数口径不同。四个平台全部保留既有 target 和源文件归属、编译参数;对照 P2-05,三个完整平台仅增加本轮两个拆出实现的 14 个元组,Deepin 增加 6 个。三个测试平台的用例与 SKIP 集合相对 P2-05 完全相同;相对 G0 仅有 P2-05 已登记的两个 TextFileToolErrors 新增用例。
+
+Windows 与 macOS 的失败名称均在 G0 中,断言或异常特征也逐项对照此前 P2-05 日志一致。保留实际失败,未放宽断言、增加过滤或重跑取绿。Windows 既有失败为:
+
+
+- `HookRunner.ShellCommandReceivesExactEnvironmentOverride`
+- `McpManagerAsync.ProjectOverridesHaveIsolatedToolDiscoveryAndDispatch`
+- `dev_desktop_script_unit`
+- `verify_package_python_unit`
+
+macOS 既有失败为:
+
+- `BuiltinToolRegistry.NativeBrowserToolsCanBeUnregisteredAsOneGroup`
+- `BuiltinToolRegistry.RegistersSharedCoreAndPlatformBrowserTools`
+- `ManagedRemoteWebProxy.AutomaticPortFallsBackWhenAdjacentPortIsBusy`
+- `ManagedRemoteWebProxy.StartsRealChildAndStopsWithoutOrphaningIt`
+- `RemoteWebTcpProxy.TransparentlyForwardsPersistentBidirectionalBytes`
+- `SandboxPolicy.ResolveAccessPrefersDeepestEntryThenDenyWriteRead`
+- `SettingsCenterRender.NarrowTopRailFollowsDeepLinkedTab`
+- `StateFileTest.ClaimFlagIsGrantedExactlyOnceAcrossProcesses`
+- `WebServerHttp.ModelRoutesRejectUnauthenticatedRemoteAccess`
+- `WebServerHttp.RealProxyRequiresTokenAndForwardsAuthenticatedHealth`
+
+P2-04 曾记录的 `AgentLoopTurnSteering.InterruptStartsStructuredTurnBeforeOrdinaryQueue` 和已知 macOS SSE 时序失败,本轮均未出现。
+
+三个完整平台的 provenance 源码 SHA 与固定提交一致。Deepin 的 source_revision 仍因容器 Git 归属检查为空;额外核对构建 job `108683316416` 的 checkout 命令、HEAD 提示及紧接 `git log -1 --format=%H` 的完整 SHA,均为 `df9ef9567bb24eabb7b6294ea16f49fd991f845d`,未将空 provenance 当作源码证明。
+
+## 不变量与合入条件
+
+本轮完整测试实际执行并通过 `AgentLoopTermination.RequestPrefixIsByteStableAcrossIterationsInATurn`、`SystemPromptTest.NonGptModelStateIsByteIdenticalToLegacyPrompt`、`HookAgentLoop.DispatchesAssistantCompletedAfterTextMessageCommit`、`HookAgentLoop.SessionStartAdditionalContextReachesNextRequestOnly`、`HookAgentLoop.PermissionResolvedPairsWithHookApprovalExactlyOnce` 与三个 `SkillCommandsReloadTest`。CMake 逐元组对照同时覆盖 Desktop 链接边界与按源编译属性。
+
+P2-06 的实现、构建归属、测试清单、静态检查和已知失败复核已完成。任务复选框保持未勾选,PR #83 实际合入后立即登记验收并认领下一项。
diff --git a/src/agent/hook_bridge/hook_events.cpp b/src/agent/hook_bridge/hook_events.cpp
new file mode 100644
index 000000000..da87c9e81
--- /dev/null
+++ b/src/agent/hook_bridge/hook_events.cpp
@@ -0,0 +1,136 @@
+#include "hook_events.hpp"
+
+#include "hooks/hook_config.hpp"
+#include "platform/process/os_process.hpp"
+#include "utils/uuid.hpp"
+
+#include
+
+namespace acecode {
+namespace {
+
+std::string registry_source_kind_to_string(RegistrySource::Kind kind) {
+ switch (kind) {
+ case RegistrySource::Kind::Empty: return "empty";
+ case RegistrySource::Kind::Bundled: return "bundled";
+ case RegistrySource::Kind::UserOverride: return "user_override";
+ case RegistrySource::Kind::Network: return "network";
+ }
+ return "unknown";
+}
+
+nlohmann::json model_profile_to_json(const ModelProfile& profile) {
+ nlohmann::json j = {
+ {"name", profile.name},
+ {"provider", profile.provider},
+ {"model", profile.model},
+ };
+ if (!profile.base_url.empty()) j["base_url"] = profile.base_url;
+ if (profile.models_dev_provider_id.has_value()) {
+ j["models_dev_provider_id"] = *profile.models_dev_provider_id;
+ }
+ if (profile.stream_timeout_ms.has_value()) {
+ j["stream_timeout_ms"] = *profile.stream_timeout_ms;
+ }
+ return j;
+}
+
+nlohmann::json assistant_tool_calls_to_json(const nlohmann::json& tool_calls) {
+ nlohmann::json out = nlohmann::json::array();
+ if (!tool_calls.is_array()) return out;
+ for (const auto& item : tool_calls) {
+ nlohmann::json one;
+ if (item.contains("id") && item["id"].is_string()) {
+ one["id"] = item["id"].get();
+ }
+ if (item.contains("function") && item["function"].is_object()) {
+ const auto& fn = item["function"];
+ one["name"] = fn.value("name", std::string{});
+ one["arguments"] = fn.value("arguments", std::string{});
+ }
+ if (!one.empty()) out.push_back(std::move(one));
+ }
+ return out;
+}
+
+} // namespace
+
+nlohmann::json hook_registry_source_to_json(const RegistrySource& source) {
+ nlohmann::json j = {
+ {"kind", registry_source_kind_to_string(source.kind)},
+ {"path_or_url", source.path_or_url},
+ };
+ if (source.seed_dir.has_value()) j["seed_dir"] = *source.seed_dir;
+ if (source.manifest.has_value() && source.manifest->is_object()) {
+ const auto& m = *source.manifest;
+ nlohmann::json manifest = nlohmann::json::object();
+ for (const char* key : {"generated_at", "source_url", "etag", "sha256"}) {
+ if (m.contains(key)) manifest[key] = m[key];
+ }
+ if (!manifest.empty()) j["manifest"] = std::move(manifest);
+ }
+ return j;
+}
+
+nlohmann::json build_startup_models_loaded_payload(
+ const std::string& cwd,
+ const ModelProfile& active_profile,
+ const std::shared_ptr& provider) {
+ nlohmann::json active = model_profile_to_json(active_profile);
+ if (provider) {
+ active["provider"] = provider->name();
+ active["model"] = provider->model();
+ }
+
+ return {
+ {"schema_version", 1},
+ {"event", kHookEventStartupModelsLoaded},
+ {"timestamp", iso_timestamp()},
+ {"process", {
+ {"pid", static_cast(daemon::current_pid())},
+ {"cwd", cwd},
+ }},
+ {"models", {
+ {"registry_source", hook_registry_source_to_json(current_registry_source())},
+ {"active", std::move(active)},
+ }},
+ };
+}
+
+nlohmann::json build_assistant_message_completed_payload(
+ const std::string& cwd,
+ const std::string& session_id,
+ const std::string& provider_name,
+ const std::string& model,
+ const ChatMessage& assistant_message) {
+ const bool has_tool_calls =
+ assistant_message.tool_calls.is_array() && !assistant_message.tool_calls.empty();
+
+ nlohmann::json assistant = {
+ {"kind", has_tool_calls ? "tool_calls" : "text"},
+ {"role", assistant_message.role},
+ {"content", assistant_message.content},
+ {"has_tool_calls", has_tool_calls},
+ {"tool_calls", assistant_tool_calls_to_json(assistant_message.tool_calls)},
+ };
+ if (!assistant_message.reasoning_content.empty()) {
+ assistant["reasoning_content"] = assistant_message.reasoning_content;
+ }
+
+ return {
+ {"schema_version", 1},
+ {"event", kHookEventAssistantMessageCompleted},
+ {"timestamp", iso_timestamp()},
+ {"session", {
+ {"id", session_id},
+ {"cwd", cwd},
+ }},
+ {"model", {
+ {"provider", provider_name},
+ {"model", model},
+ }},
+ {"assistant", std::move(assistant)},
+ };
+}
+
+} // namespace acecode
diff --git a/src/agent/hook_bridge/hook_events.hpp b/src/agent/hook_bridge/hook_events.hpp
new file mode 100644
index 000000000..555493f58
--- /dev/null
+++ b/src/agent/hook_bridge/hook_events.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+// 模型和 provider 信息转成 hook 事件载荷,由 agent 编排层负责。
+
+#include "config/saved_models.hpp"
+#include "llm/llm_provider.hpp"
+#include "provider/models_dev_registry.hpp"
+
+#include
+#include
+
+#include
+
+namespace acecode {
+
+nlohmann::json hook_registry_source_to_json(const RegistrySource& source);
+
+nlohmann::json build_startup_models_loaded_payload(
+ const std::string& cwd,
+ const ModelProfile& active_profile,
+ const std::shared_ptr& provider);
+
+nlohmann::json build_assistant_message_completed_payload(
+ const std::string& cwd,
+ const std::string& session_id,
+ const std::string& provider_name,
+ const std::string& model,
+ const ChatMessage& assistant_message);
+
+} // namespace acecode
diff --git a/src/agent_loop.cpp b/src/agent_loop.cpp
index 71b749e93..35a2e4456 100644
--- a/src/agent_loop.cpp
+++ b/src/agent_loop.cpp
@@ -43,7 +43,7 @@
#include "web/tool_event_payload.hpp"
#include "hooks/hook_config.hpp"
#include "hooks/hook_manager.hpp"
-#include "hooks/hook_payload.hpp"
+#include "agent/hook_bridge/hook_events.hpp"
#include "permissions/interaction_mode.hpp"
#include "pa/pa_context_budget.hpp"
#include "pa/pa_overflow_rescue.hpp"
diff --git a/src/commands/builtin_commands.cpp b/src/commands/builtin_commands.cpp
index aa897573d..847f492bf 100644
--- a/src/commands/builtin_commands.cpp
+++ b/src/commands/builtin_commands.cpp
@@ -24,7 +24,7 @@
#include "tool/mcp_manager.hpp"
#include "tool/tool_executor.hpp"
#include "skills/skill_registry.hpp"
-#include "skills/skill_commands.hpp"
+#include "tui/commands/skill_commands.hpp"
#include "session/session_manager.hpp"
#include "session/session_resume_restore.hpp"
#include "session/session_storage.hpp"
diff --git a/src/commands/command_registry.cpp b/src/commands/command_registry.cpp
index 701edc39a..a40d40c14 100644
--- a/src/commands/command_registry.cpp
+++ b/src/commands/command_registry.cpp
@@ -1,7 +1,7 @@
#include "command_registry.hpp"
#include "opencode_command_registry.hpp"
#include "skills/skill_activation.hpp"
-#include "skills/skill_commands.hpp"
+#include "tui/commands/skill_commands.hpp"
#include "skills/skill_registry.hpp"
#include "utils/logger.hpp"
diff --git a/src/commands/opencode_command_registry.cpp b/src/commands/opencode_command_registry.cpp
index 2ba177ded..462e738df 100644
--- a/src/commands/opencode_command_registry.cpp
+++ b/src/commands/opencode_command_registry.cpp
@@ -1,7 +1,7 @@
#include "opencode_command_registry.hpp"
#include "command_registry.hpp"
-#include "opencode_command.hpp"
+#include "skills/opencode_command.hpp"
#include "agent_loop.hpp"
#include "tui_state.hpp"
#include "utils/logger.hpp"
diff --git a/src/daemon/worker.cpp b/src/daemon/worker.cpp
index 88d5c485c..44c8c157f 100644
--- a/src/daemon/worker.cpp
+++ b/src/daemon/worker.cpp
@@ -27,7 +27,7 @@
#include "provider/provider_factory.hpp"
#include "hooks/hook_config.hpp"
#include "hooks/hook_manager.hpp"
-#include "hooks/hook_payload.hpp"
+#include "agent/hook_bridge/hook_events.hpp"
#include "platform/process/process_runner.hpp"
#include "loop/loop_scheduler.hpp"
#include "loop/loop_store.hpp"
diff --git a/src/environment/terminal_resolver.cpp b/src/environment/terminal_resolver.cpp
index b0135fbb0..eb4b234f6 100644
--- a/src/environment/terminal_resolver.cpp
+++ b/src/environment/terminal_resolver.cpp
@@ -1,6 +1,5 @@
#include "terminal_resolver.hpp"
-#include "hooks/hook_config.hpp"
#include "platform/process/process_runner.hpp"
#include "utils/logger.hpp"
diff --git a/src/headless/headless_runner.cpp b/src/headless/headless_runner.cpp
index 59bac2889..9e3c2fedb 100644
--- a/src/headless/headless_runner.cpp
+++ b/src/headless/headless_runner.cpp
@@ -38,7 +38,7 @@
#include "utils/paths.hpp"
#include "platform/power_inhibitor.hpp"
#include "utils/utf8_path.hpp"
-#include "web/handlers/skill_command_expander.hpp"
+#include "skills/skill_command_expander.hpp"
#include
diff --git a/src/hooks/hook_payload.cpp b/src/hooks/hook_payload.cpp
index 92c1c7fba..9f15c6de3 100644
--- a/src/hooks/hook_payload.cpp
+++ b/src/hooks/hook_payload.cpp
@@ -1,78 +1,12 @@
#include "hook_payload.hpp"
#include "hook_config.hpp"
-#include "config/config.hpp"
+#include "platform/process/os_process.hpp"
+#include "utils/paths.hpp"
#include "utils/utf8_path.hpp"
#include "utils/uuid.hpp"
-#ifdef _WIN32
-#ifndef WIN32_LEAN_AND_MEAN
-#define WIN32_LEAN_AND_MEAN
-#endif
-#ifndef NOMINMAX
-#define NOMINMAX
-#endif
-#include
-#else
-#include
-#endif
-
namespace acecode {
-namespace {
-
-std::string registry_source_kind_to_string(RegistrySource::Kind kind) {
- switch (kind) {
- case RegistrySource::Kind::Empty: return "empty";
- case RegistrySource::Kind::Bundled: return "bundled";
- case RegistrySource::Kind::UserOverride: return "user_override";
- case RegistrySource::Kind::Network: return "network";
- }
- return "unknown";
-}
-
-int current_pid() {
-#ifdef _WIN32
- return static_cast(GetCurrentProcessId());
-#else
- return static_cast(getpid());
-#endif
-}
-
-nlohmann::json model_profile_to_json(const ModelProfile& profile) {
- nlohmann::json j = {
- {"name", profile.name},
- {"provider", profile.provider},
- {"model", profile.model},
- };
- if (!profile.base_url.empty()) j["base_url"] = profile.base_url;
- if (profile.models_dev_provider_id.has_value()) {
- j["models_dev_provider_id"] = *profile.models_dev_provider_id;
- }
- if (profile.stream_timeout_ms.has_value()) {
- j["stream_timeout_ms"] = *profile.stream_timeout_ms;
- }
- return j;
-}
-
-nlohmann::json assistant_tool_calls_to_json(const nlohmann::json& tool_calls) {
- nlohmann::json out = nlohmann::json::array();
- if (!tool_calls.is_array()) return out;
- for (const auto& item : tool_calls) {
- nlohmann::json one;
- if (item.contains("id") && item["id"].is_string()) {
- one["id"] = item["id"].get();
- }
- if (item.contains("function") && item["function"].is_object()) {
- const auto& fn = item["function"];
- one["name"] = fn.value("name", std::string{});
- one["arguments"] = fn.value("arguments", std::string{});
- }
- if (!one.empty()) out.push_back(std::move(one));
- }
- return out;
-}
-
-} // namespace
nlohmann::json build_startup_before_model_load_payload(const std::string& cwd) {
std::string config_path = path_to_utf8(path_from_utf8(get_acecode_dir()) / "config.json");
@@ -81,7 +15,7 @@ nlohmann::json build_startup_before_model_load_payload(const std::string& cwd) {
{"event", kHookEventStartupBeforeModelLoad},
{"timestamp", iso_timestamp()},
{"process", {
- {"pid", current_pid()},
+ {"pid", static_cast(daemon::current_pid())},
{"cwd", cwd},
}},
{"config", {
@@ -90,82 +24,4 @@ nlohmann::json build_startup_before_model_load_payload(const std::string& cwd) {
};
}
-nlohmann::json hook_registry_source_to_json(const RegistrySource& source) {
- nlohmann::json j = {
- {"kind", registry_source_kind_to_string(source.kind)},
- {"path_or_url", source.path_or_url},
- };
- if (source.seed_dir.has_value()) j["seed_dir"] = *source.seed_dir;
- if (source.manifest.has_value() && source.manifest->is_object()) {
- const auto& m = *source.manifest;
- nlohmann::json manifest = nlohmann::json::object();
- for (const char* key : {"generated_at", "source_url", "etag", "sha256"}) {
- if (m.contains(key)) manifest[key] = m[key];
- }
- if (!manifest.empty()) j["manifest"] = std::move(manifest);
- }
- return j;
-}
-
-nlohmann::json build_startup_models_loaded_payload(
- const std::string& cwd,
- const ModelProfile& active_profile,
- const std::shared_ptr& provider) {
- nlohmann::json active = model_profile_to_json(active_profile);
- if (provider) {
- active["provider"] = provider->name();
- active["model"] = provider->model();
- }
-
- return {
- {"schema_version", 1},
- {"event", kHookEventStartupModelsLoaded},
- {"timestamp", iso_timestamp()},
- {"process", {
- {"pid", current_pid()},
- {"cwd", cwd},
- }},
- {"models", {
- {"registry_source", hook_registry_source_to_json(current_registry_source())},
- {"active", std::move(active)},
- }},
- };
-}
-
-nlohmann::json build_assistant_message_completed_payload(
- const std::string& cwd,
- const std::string& session_id,
- const std::string& provider_name,
- const std::string& model,
- const ChatMessage& assistant_message) {
- const bool has_tool_calls =
- assistant_message.tool_calls.is_array() && !assistant_message.tool_calls.empty();
-
- nlohmann::json assistant = {
- {"kind", has_tool_calls ? "tool_calls" : "text"},
- {"role", assistant_message.role},
- {"content", assistant_message.content},
- {"has_tool_calls", has_tool_calls},
- {"tool_calls", assistant_tool_calls_to_json(assistant_message.tool_calls)},
- };
- if (!assistant_message.reasoning_content.empty()) {
- assistant["reasoning_content"] = assistant_message.reasoning_content;
- }
-
- return {
- {"schema_version", 1},
- {"event", kHookEventAssistantMessageCompleted},
- {"timestamp", iso_timestamp()},
- {"session", {
- {"id", session_id},
- {"cwd", cwd},
- }},
- {"model", {
- {"provider", provider_name},
- {"model", model},
- }},
- {"assistant", std::move(assistant)},
- };
-}
-
} // namespace acecode
diff --git a/src/hooks/hook_payload.hpp b/src/hooks/hook_payload.hpp
index 07c3509da..ddc0ed6eb 100644
--- a/src/hooks/hook_payload.hpp
+++ b/src/hooks/hook_payload.hpp
@@ -1,30 +1,11 @@
#pragma once
-#include "config/saved_models.hpp"
-#include "llm/llm_provider.hpp"
-#include "provider/models_dev_registry.hpp"
-
-#include
#include
#include
namespace acecode {
-nlohmann::json hook_registry_source_to_json(const RegistrySource& source);
-
nlohmann::json build_startup_before_model_load_payload(const std::string& cwd);
-nlohmann::json build_startup_models_loaded_payload(
- const std::string& cwd,
- const ModelProfile& active_profile,
- const std::shared_ptr& provider);
-
-nlohmann::json build_assistant_message_completed_payload(
- const std::string& cwd,
- const std::string& session_id,
- const std::string& provider_name,
- const std::string& model,
- const ChatMessage& assistant_message);
-
} // namespace acecode
diff --git a/src/hooks/hook_registry.cpp b/src/hooks/hook_registry.cpp
index bc0cfa6ae..71364c40e 100644
--- a/src/hooks/hook_registry.cpp
+++ b/src/hooks/hook_registry.cpp
@@ -1,6 +1,6 @@
#include "hook_registry.hpp"
-#include "skills/default_skill_seeder.hpp"
+#include "hooks/hook_seeds.hpp"
#include "utils/atomic_file.hpp"
#include "utils/encoding.hpp"
#include "utils/paths.hpp"
diff --git a/src/hooks/hook_seeds.cpp b/src/hooks/hook_seeds.cpp
new file mode 100644
index 000000000..0bb1fe938
--- /dev/null
+++ b/src/hooks/hook_seeds.cpp
@@ -0,0 +1,20 @@
+#include "hook_seeds.hpp"
+
+namespace acecode {
+
+const std::vector& default_hook_seeds() {
+ static const std::vector seeds = {
+ {"agent-reporting",
+ "acecode:managed-hook/agent-reporting@2026-09-17.1",
+ "agent-reporting",
+ "90403a8d660fc4086bca9c7c979c8fb8e98321dcebc63aefd1e995addc2fab1e",
+ {
+ "daeb5ce4f3ff42d1717c9997b9627bc6daf00df9ec24643203253da2aae30644",
+ "b731118b927bb32a5c43083f5d3279ecd4ce3d96137b351c2d105ac3548d9f2f",
+ "e139bd9bc1314dfe82cd7a7c018332e7a33c42eb451c8ce1db99f58d18d6ff4e",
+ }},
+ };
+ return seeds;
+}
+
+} // namespace acecode
diff --git a/src/hooks/hook_seeds.hpp b/src/hooks/hook_seeds.hpp
new file mode 100644
index 000000000..f8156baee
--- /dev/null
+++ b/src/hooks/hook_seeds.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace acecode {
+
+// 内置 hook 的只读种子定义,供注册表识别与统一安装事务复用。
+struct DefaultHookSeed {
+ std::string name;
+ std::string source_id;
+ std::filesystem::path relative_path;
+ std::string definition_sha256;
+ std::vector previous_definition_sha256s;
+};
+
+const std::vector& default_hook_seeds();
+
+} // namespace acecode
diff --git a/src/main.cpp b/src/main.cpp
index eacf8e65f..41b3e93c8 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -86,11 +86,12 @@
#include "session/permission_prompter.hpp"
#include "skills/skill_init.hpp"
#include "skills/skill_registry.hpp"
-#include "skills/skill_commands.hpp"
+#include "tui/commands/skill_commands.hpp"
#include "skills/default_skill_seeder.hpp"
#include "commands/opencode_command_registry.hpp"
#include "hooks/hook_config.hpp"
#include "hooks/hook_manager.hpp"
+#include "agent/hook_bridge/hook_events.hpp"
#include "hooks/hook_payload.hpp"
#include "memory/memory_paths.hpp"
#include "memory/memory_registry.hpp"
diff --git a/src/memory/memory_frontmatter.cpp b/src/memory/memory_frontmatter.cpp
index 73f810fee..fb151bccb 100644
--- a/src/memory/memory_frontmatter.cpp
+++ b/src/memory/memory_frontmatter.cpp
@@ -1,6 +1,6 @@
#include "memory_frontmatter.hpp"
-#include "skills/frontmatter.hpp"
+#include "utils/frontmatter.hpp"
#include "utils/encoding.hpp"
#include "utils/logger.hpp"
#include "utils/utf8_path.hpp"
diff --git a/src/skills/default_skill_seeder.cpp b/src/skills/default_skill_seeder.cpp
index e48676b64..5736aab02 100644
--- a/src/skills/default_skill_seeder.cpp
+++ b/src/skills/default_skill_seeder.cpp
@@ -1,5 +1,7 @@
#include "default_skill_seeder.hpp"
+#include "hooks/hook_seeds.hpp"
+
#include "utils/atomic_file.hpp"
#include "utils/encoding.hpp"
#include "utils/logger.hpp"
@@ -1308,21 +1310,6 @@ const std::vector& default_expert_seeds() {
return seeds;
}
-const std::vector& default_hook_seeds() {
- static const std::vector seeds = {
- {"agent-reporting",
- "acecode:managed-hook/agent-reporting@2026-09-17.1",
- "agent-reporting",
- "90403a8d660fc4086bca9c7c979c8fb8e98321dcebc63aefd1e995addc2fab1e",
- {
- "daeb5ce4f3ff42d1717c9997b9627bc6daf00df9ec24643203253da2aae30644",
- "b731118b927bb32a5c43083f5d3279ecd4ce3d96137b351c2d105ac3548d9f2f",
- "e139bd9bc1314dfe82cd7a7c018332e7a33c42eb451c8ce1db99f58d18d6ff4e",
- }},
- };
- return seeds;
-}
-
std::optional find_default_skill_seed_dir(
const std::string& argv0_dir) {
const std::string env = getenv_utf8("ACECODE_SEED_SKILLS_DIR");
diff --git a/src/skills/default_skill_seeder.hpp b/src/skills/default_skill_seeder.hpp
index ea9f51682..4ce966ed4 100644
--- a/src/skills/default_skill_seeder.hpp
+++ b/src/skills/default_skill_seeder.hpp
@@ -19,14 +19,6 @@ struct DefaultExpertSeed {
std::filesystem::path relative_path;
};
-struct DefaultHookSeed {
- std::string name;
- std::string source_id;
- std::filesystem::path relative_path;
- std::string definition_sha256;
- std::vector previous_definition_sha256s;
-};
-
struct DefaultSkillSeedOutcome {
std::string name;
std::string source_id;
@@ -61,7 +53,6 @@ struct DefaultSkillSeedInstallResult {
const std::vector& default_skill_seeds();
const std::vector& default_expert_seeds();
-const std::vector& default_hook_seeds();
std::optional find_default_skill_seed_dir(
const std::string& argv0_dir = "");
diff --git a/src/commands/opencode_command.cpp b/src/skills/opencode_command.cpp
similarity index 100%
rename from src/commands/opencode_command.cpp
rename to src/skills/opencode_command.cpp
diff --git a/src/commands/opencode_command.hpp b/src/skills/opencode_command.hpp
similarity index 100%
rename from src/commands/opencode_command.hpp
rename to src/skills/opencode_command.hpp
diff --git a/src/web/handlers/skill_command_expander.cpp b/src/skills/skill_command_expander.cpp
similarity index 100%
rename from src/web/handlers/skill_command_expander.cpp
rename to src/skills/skill_command_expander.cpp
diff --git a/src/web/handlers/skill_command_expander.hpp b/src/skills/skill_command_expander.hpp
similarity index 100%
rename from src/web/handlers/skill_command_expander.hpp
rename to src/skills/skill_command_expander.hpp
diff --git a/src/skills/skill_loader.cpp b/src/skills/skill_loader.cpp
index cbd158c86..de91846f6 100644
--- a/src/skills/skill_loader.cpp
+++ b/src/skills/skill_loader.cpp
@@ -1,6 +1,6 @@
#include "skill_loader.hpp"
-#include "frontmatter.hpp"
+#include "utils/frontmatter.hpp"
#include "utils/encoding.hpp"
#include "utils/logger.hpp"
#include "utils/utf8_path.hpp"
diff --git a/src/skills/skill_registry.cpp b/src/skills/skill_registry.cpp
index 7a25b3d27..295031a96 100644
--- a/src/skills/skill_registry.cpp
+++ b/src/skills/skill_registry.cpp
@@ -1,6 +1,6 @@
#include "skill_registry.hpp"
-#include "frontmatter.hpp"
+#include "utils/frontmatter.hpp"
#include "skill_loader.hpp"
#include "utils/encoding.hpp"
#include "utils/logger.hpp"
diff --git a/src/tool/spawn_subagent_tool.cpp b/src/tool/spawn_subagent_tool.cpp
index cf3308e1e..86d0fdc83 100644
--- a/src/tool/spawn_subagent_tool.cpp
+++ b/src/tool/spawn_subagent_tool.cpp
@@ -7,7 +7,7 @@
#include "skills/skill_registry.hpp"
#include "utils/encoding.hpp"
#include "utils/logger.hpp"
-#include "web/handlers/skill_command_expander.hpp"
+#include "skills/skill_command_expander.hpp"
#include "worktree/worktree_core.hpp"
#include "worktree/worktree_manager.hpp"
diff --git a/src/skills/skill_commands.cpp b/src/tui/commands/skill_commands.cpp
similarity index 98%
rename from src/skills/skill_commands.cpp
rename to src/tui/commands/skill_commands.cpp
index 0ae29a59e..fa98271c0 100644
--- a/src/skills/skill_commands.cpp
+++ b/src/tui/commands/skill_commands.cpp
@@ -1,7 +1,7 @@
#include "skill_commands.hpp"
-#include "skill_activation.hpp"
-#include "skill_registry.hpp"
+#include "skills/skill_activation.hpp"
+#include "skills/skill_registry.hpp"
#include "agent_loop.hpp"
#include "commands/command_registry.hpp"
#include "tui_state.hpp"
diff --git a/src/skills/skill_commands.hpp b/src/tui/commands/skill_commands.hpp
similarity index 100%
rename from src/skills/skill_commands.hpp
rename to src/tui/commands/skill_commands.hpp
diff --git a/src/tui/settings/management_center.cpp b/src/tui/settings/management_center.cpp
index 6bb365f70..a188d5147 100644
--- a/src/tui/settings/management_center.cpp
+++ b/src/tui/settings/management_center.cpp
@@ -11,7 +11,7 @@
#include "config/config_mutation.hpp"
#include "hooks/hook_manager.hpp"
#include "hooks/hook_registry.hpp"
-#include "skills/skill_commands.hpp"
+#include "tui/commands/skill_commands.hpp"
#include "skills/skill_init.hpp"
#include "skills/skill_registry.hpp"
#include "tool/mcp_manager.hpp"
diff --git a/src/skills/frontmatter.cpp b/src/utils/frontmatter.cpp
similarity index 100%
rename from src/skills/frontmatter.cpp
rename to src/utils/frontmatter.cpp
diff --git a/src/skills/frontmatter.hpp b/src/utils/frontmatter.hpp
similarity index 100%
rename from src/skills/frontmatter.hpp
rename to src/utils/frontmatter.hpp
diff --git a/src/web/handlers/commands_handler.cpp b/src/web/handlers/commands_handler.cpp
index 87e954fa3..ade452288 100644
--- a/src/web/handlers/commands_handler.cpp
+++ b/src/web/handlers/commands_handler.cpp
@@ -1,6 +1,6 @@
#include "commands_handler.hpp"
-#include "commands/opencode_command.hpp"
+#include "skills/opencode_command.hpp"
#include "config/config.hpp"
#include "skills/skill_init.hpp"
#include "skills/skill_activation.hpp"
diff --git a/src/web/handlers/opencode_command_expander.cpp b/src/web/handlers/opencode_command_expander.cpp
index ce42012ea..a4023af2a 100644
--- a/src/web/handlers/opencode_command_expander.cpp
+++ b/src/web/handlers/opencode_command_expander.cpp
@@ -1,6 +1,6 @@
#include "opencode_command_expander.hpp"
-#include "commands/opencode_command.hpp"
+#include "skills/opencode_command.hpp"
#include "config/config.hpp"
namespace acecode::web {
diff --git a/src/web/server_impl.hpp b/src/web/server_impl.hpp
index 2ef5162b3..f005fb990 100644
--- a/src/web/server_impl.hpp
+++ b/src/web/server_impl.hpp
@@ -59,7 +59,7 @@
#include "web/handlers/builtin_command_handler.hpp"
#include "web/handlers/commands_handler.hpp"
#include "web/handlers/opencode_command_expander.hpp"
-#include "web/handlers/skill_command_expander.hpp"
+#include "skills/skill_command_expander.hpp"
#include "web/handlers/session_list_handler.hpp"
#include "web/handlers/side_chat_handler.hpp"
#include "web/handlers/skills_handler.hpp"
diff --git a/tests/experts/expert_registry_test.cpp b/tests/experts/expert_registry_test.cpp
index 170bf107f..d217d8b40 100644
--- a/tests/experts/expert_registry_test.cpp
+++ b/tests/experts/expert_registry_test.cpp
@@ -8,7 +8,7 @@
#include "session/session_storage.hpp"
#include "tool/tool_executor.hpp"
#include "utils/utf8_path.hpp"
-#include "web/handlers/skill_command_expander.hpp"
+#include "skills/skill_command_expander.hpp"
#include
#include
diff --git a/tests/hooks/hook_registry_test.cpp b/tests/hooks/hook_registry_test.cpp
index 2a28c937c..95f856a92 100644
--- a/tests/hooks/hook_registry_test.cpp
+++ b/tests/hooks/hook_registry_test.cpp
@@ -1,7 +1,7 @@
#include
#include "hooks/hook_registry.hpp"
-#include "skills/default_skill_seeder.hpp"
+#include "hooks/hook_seeds.hpp"
#include "test_support/repo_root.hpp"
#include "utils/utf8_path.hpp"
diff --git a/tests/skills/default_skill_seeder_test.cpp b/tests/skills/default_skill_seeder_test.cpp
index 3a26f8ff2..cb6b529d6 100644
--- a/tests/skills/default_skill_seeder_test.cpp
+++ b/tests/skills/default_skill_seeder_test.cpp
@@ -1,3 +1,4 @@
+#include "hooks/hook_seeds.hpp"
#include "skills/default_skill_seeder.hpp"
#include "skills/skill_registry.hpp"
#include "test_support/repo_root.hpp"
diff --git a/tests/commands/opencode_command_test.cpp b/tests/skills/opencode_command_test.cpp
similarity index 99%
rename from tests/commands/opencode_command_test.cpp
rename to tests/skills/opencode_command_test.cpp
index 30e063429..dc78739a4 100644
--- a/tests/commands/opencode_command_test.cpp
+++ b/tests/skills/opencode_command_test.cpp
@@ -1,6 +1,6 @@
#include
-#include "commands/opencode_command.hpp"
+#include "skills/opencode_command.hpp"
#include "config/config.hpp"
#include
diff --git a/tests/web/skill_command_expander_test.cpp b/tests/skills/skill_command_expander_test.cpp
similarity index 98%
rename from tests/web/skill_command_expander_test.cpp
rename to tests/skills/skill_command_expander_test.cpp
index 77be78d9c..9585baa0b 100644
--- a/tests/web/skill_command_expander_test.cpp
+++ b/tests/skills/skill_command_expander_test.cpp
@@ -1,4 +1,4 @@
-// 覆盖 src/web/handlers/skill_command_expander.cpp::try_expand_skill_command。
+// 覆盖 src/skills/skill_command_expander.cpp::try_expand_skill_command。
//
// 这个函数是 daemon 端 sendInput 路径上的关键 hook — 把 `/ args`
// 形式的 user message rewrite 成 Codex linked Skill mention,与 TUI 行为
@@ -19,7 +19,7 @@
#include "config/config.hpp"
#include "skills/skill_init.hpp"
#include "skills/skill_registry.hpp"
-#include "web/handlers/skill_command_expander.hpp"
+#include "skills/skill_command_expander.hpp"
#include
#include
diff --git a/tests/skill_registry_test.cpp b/tests/skills/skill_registry_test.cpp
similarity index 100%
rename from tests/skill_registry_test.cpp
rename to tests/skills/skill_registry_test.cpp
diff --git a/tests/skills/skill_commands_reload_test.cpp b/tests/tui/commands/skill_commands_reload_test.cpp
similarity index 97%
rename from tests/skills/skill_commands_reload_test.cpp
rename to tests/tui/commands/skill_commands_reload_test.cpp
index 10b431050..f128c9509 100644
--- a/tests/skills/skill_commands_reload_test.cpp
+++ b/tests/tui/commands/skill_commands_reload_test.cpp
@@ -1,4 +1,4 @@
-// 覆盖 src/skills/skill_commands.cpp::reload_skill_commands 在「会话进行中磁盘上
+// 覆盖 src/tui/commands/skill_commands.cpp::reload_skill_commands 在「会话进行中磁盘上
// 新增/删除 skill」时的重绑行为。
//
// 背景:用户(或 agent 用 skill-creator)在 ACECode 运行期间往扫描根里写了个新
@@ -17,7 +17,7 @@
#include "commands/command_registry.hpp"
#include "config/config.hpp"
-#include "skills/skill_commands.hpp"
+#include "tui/commands/skill_commands.hpp"
#include "skills/skill_init.hpp"
#include "skills/skill_registry.hpp"
diff --git a/tests/skills/frontmatter_test.cpp b/tests/utils/frontmatter_test.cpp
similarity index 97%
rename from tests/skills/frontmatter_test.cpp
rename to tests/utils/frontmatter_test.cpp
index b455fedc8..4ab58c46d 100644
--- a/tests/skills/frontmatter_test.cpp
+++ b/tests/utils/frontmatter_test.cpp
@@ -1,4 +1,4 @@
-// 覆盖 src/skills/frontmatter.cpp 的 YAML 块标量解析。
+// 覆盖 src/utils/frontmatter.cpp 的 YAML 块标量解析。
//
// 背景 bug:claude-code 生态的 skill(pdf / docx / kimi-webbridge 等)常用
// `description: >` 或 `description: |` 写多行描述。旧解析器把 ">" / "|"
@@ -8,7 +8,7 @@
#include
-#include "skills/frontmatter.hpp"
+#include "utils/frontmatter.hpp"
namespace {