openfeel 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +2 -1
  3. package/README.zh-CN.md +13 -13
  4. package/bin/openfeel.js +2 -2
  5. package/dist/cli/index.d.ts +6 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +26 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/archive.d.ts +1 -1
  10. package/dist/commands/flow.d.ts +1 -1
  11. package/dist/commands/flow.d.ts.map +1 -1
  12. package/dist/commands/flow.js +20 -8
  13. package/dist/commands/flow.js.map +1 -1
  14. package/dist/commands/migrate.d.ts +10 -0
  15. package/dist/commands/migrate.d.ts.map +1 -0
  16. package/dist/commands/migrate.js +97 -0
  17. package/dist/commands/migrate.js.map +1 -0
  18. package/dist/commands/model.d.ts +10 -0
  19. package/dist/commands/model.d.ts.map +1 -0
  20. package/dist/commands/model.js +135 -0
  21. package/dist/commands/model.js.map +1 -0
  22. package/dist/commands/stage.d.ts.map +1 -1
  23. package/dist/commands/stage.js +50 -29
  24. package/dist/commands/stage.js.map +1 -1
  25. package/dist/commands/update.d.ts.map +1 -1
  26. package/dist/commands/update.js +12 -0
  27. package/dist/commands/update.js.map +1 -1
  28. package/dist/commands/view.d.ts +1 -1
  29. package/dist/commands/view.d.ts.map +1 -1
  30. package/dist/commands/view.js +2 -1
  31. package/dist/commands/view.js.map +1 -1
  32. package/dist/core/archive/merge.d.ts.map +1 -1
  33. package/dist/core/archive/merge.js +5 -4
  34. package/dist/core/archive/merge.js.map +1 -1
  35. package/dist/core/artifact-graph/resolver.js +2 -2
  36. package/dist/core/artifact-graph/resolver.js.map +1 -1
  37. package/dist/core/config.d.ts +8 -8
  38. package/dist/core/config.d.ts.map +1 -1
  39. package/dist/core/config.js +22 -17
  40. package/dist/core/config.js.map +1 -1
  41. package/dist/core/flow-manager.d.ts +25 -4
  42. package/dist/core/flow-manager.d.ts.map +1 -1
  43. package/dist/core/flow-manager.js +144 -79
  44. package/dist/core/flow-manager.js.map +1 -1
  45. package/dist/core/fs/atomic-write.d.ts +31 -0
  46. package/dist/core/fs/atomic-write.d.ts.map +1 -0
  47. package/dist/core/fs/atomic-write.js +80 -0
  48. package/dist/core/fs/atomic-write.js.map +1 -0
  49. package/dist/core/fs/file-lock.d.ts +34 -0
  50. package/dist/core/fs/file-lock.d.ts.map +1 -0
  51. package/dist/core/fs/file-lock.js +142 -0
  52. package/dist/core/fs/file-lock.js.map +1 -0
  53. package/dist/core/fs/sequence.d.ts +42 -0
  54. package/dist/core/fs/sequence.d.ts.map +1 -0
  55. package/dist/core/fs/sequence.js +69 -0
  56. package/dist/core/fs/sequence.js.map +1 -0
  57. package/dist/core/global-paths.d.ts +25 -0
  58. package/dist/core/global-paths.d.ts.map +1 -0
  59. package/dist/core/global-paths.js +57 -0
  60. package/dist/core/global-paths.js.map +1 -0
  61. package/dist/core/i18n-data/en.d.ts +2 -0
  62. package/dist/core/i18n-data/en.d.ts.map +1 -1
  63. package/dist/core/i18n-data/en.js +69 -1
  64. package/dist/core/i18n-data/en.js.map +1 -1
  65. package/dist/core/i18n-data/zh-CN.d.ts +2 -0
  66. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  67. package/dist/core/i18n-data/zh-CN.js +69 -1
  68. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  69. package/dist/core/i18n.js +4 -4
  70. package/dist/core/i18n.js.map +1 -1
  71. package/dist/core/init.d.ts +3 -3
  72. package/dist/core/init.d.ts.map +1 -1
  73. package/dist/core/init.js +50 -37
  74. package/dist/core/init.js.map +1 -1
  75. package/dist/core/managed-region.d.ts +54 -0
  76. package/dist/core/managed-region.d.ts.map +1 -0
  77. package/dist/core/managed-region.js +171 -0
  78. package/dist/core/managed-region.js.map +1 -0
  79. package/dist/core/metrics.d.ts.map +1 -1
  80. package/dist/core/metrics.js +6 -3
  81. package/dist/core/metrics.js.map +1 -1
  82. package/dist/core/migrate.d.ts +103 -0
  83. package/dist/core/migrate.d.ts.map +1 -0
  84. package/dist/core/migrate.js +533 -0
  85. package/dist/core/migrate.js.map +1 -0
  86. package/dist/core/model-config.d.ts +62 -0
  87. package/dist/core/model-config.d.ts.map +1 -0
  88. package/dist/core/model-config.js +336 -0
  89. package/dist/core/model-config.js.map +1 -0
  90. package/dist/core/opencode-config.d.ts +17 -0
  91. package/dist/core/opencode-config.d.ts.map +1 -0
  92. package/dist/core/opencode-config.js +123 -0
  93. package/dist/core/opencode-config.js.map +1 -0
  94. package/dist/core/plan/roadmap.d.ts.map +1 -1
  95. package/dist/core/plan/roadmap.js +3 -2
  96. package/dist/core/plan/roadmap.js.map +1 -1
  97. package/dist/core/plan/scheme.d.ts.map +1 -1
  98. package/dist/core/plan/scheme.js +35 -36
  99. package/dist/core/plan/scheme.js.map +1 -1
  100. package/dist/core/plan/stage.d.ts.map +1 -1
  101. package/dist/core/plan/stage.js +4 -3
  102. package/dist/core/plan/stage.js.map +1 -1
  103. package/dist/core/public-logger.d.ts +3 -3
  104. package/dist/core/public-logger.d.ts.map +1 -1
  105. package/dist/core/public-logger.js +36 -40
  106. package/dist/core/public-logger.js.map +1 -1
  107. package/dist/core/template-loader.d.ts +1 -1
  108. package/dist/core/template-loader.d.ts.map +1 -1
  109. package/dist/core/template-loader.js +2834 -2813
  110. package/dist/core/template-loader.js.map +1 -1
  111. package/dist/core/update-infos.d.ts +32 -0
  112. package/dist/core/update-infos.d.ts.map +1 -0
  113. package/dist/core/update-infos.js +153 -0
  114. package/dist/core/update-infos.js.map +1 -0
  115. package/dist/core/update-state.d.ts +18 -0
  116. package/dist/core/update-state.d.ts.map +1 -1
  117. package/dist/core/update-state.js +66 -3
  118. package/dist/core/update-state.js.map +1 -1
  119. package/dist/core/update.d.ts +12 -0
  120. package/dist/core/update.d.ts.map +1 -1
  121. package/dist/core/update.js +335 -482
  122. package/dist/core/update.js.map +1 -1
  123. package/dist/core/view/entry.js +4 -4
  124. package/dist/core/view/entry.js.map +1 -1
  125. package/dist/core/workspace/identity.d.ts.map +1 -1
  126. package/dist/core/workspace/identity.js +13 -10
  127. package/dist/core/workspace/identity.js.map +1 -1
  128. package/dist/core/workspace/knowledge.d.ts.map +1 -1
  129. package/dist/core/workspace/knowledge.js +24 -14
  130. package/dist/core/workspace/knowledge.js.map +1 -1
  131. package/package.json +1 -1
@@ -5,13 +5,19 @@
5
5
  * 变更摘要 (v3-stage-04 第二轮):
6
6
  * - 新增 instructions/core.md 创建(从 init.ts 迁移至此,职责归位适配器层)
7
7
  */
8
- import { writeFileSync, existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs';
9
- import { resolve, dirname, join } from 'node:path';
8
+ import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs';
9
+ import { resolve, dirname, join, basename, isAbsolute, relative } from 'node:path';
10
10
  import { loadAgentTemplate, listAgentIds, loadTemplate } from './template-loader.js';
11
11
  import { recordProjectLang, getGlobalConfig, getLang } from './workspace/identity.js';
12
12
  import { getCliLang } from './i18n.js';
13
- // 新增:update_state.json hash 比对与冲突检测
14
- import { hashContent, loadUpdateState, saveUpdateState, createUpdateState, updateFileHash, markFileConflict, getOpenfeelVersion, } from './update-state.js';
13
+ import { getOpencodeGlobalDir, getGlobalAgentsDir, getGlobalSkillsDir, getGlobalCoreMdPath, getGlobalOpencodeJsoncPath, getGlobalUpdateStatePath } from './global-paths.js';
14
+ import { mergeGlobalOpencodeJsonc, buildProjectOpencodeJsoncObj } from './opencode-config.js';
15
+ import { atomicWriteFileSync } from './fs/atomic-write.js';
16
+ import { withFileLock, globalLockPath } from './fs/file-lock.js';
17
+ import { detectFileType, normalize, splitFrontmatter, parseRegion, wrapRegion, replaceRegion, mergeFrontmatter, serializeFrontmatter } from './managed-region.js';
18
+ import { appendUpdateInfo } from './update-infos.js';
19
+ // 新增:update_state.json hash 比对与冲突检测(项目 state + 全局 state)
20
+ import { hashContent, loadUpdateState, saveUpdateState, createUpdateState, loadGlobalUpdateState, saveGlobalUpdateState, createGlobalUpdateState, updateFileHash, markFileConflict, getOpenfeelVersion, } from './update-state.js';
15
21
  /** AGENTS.md 语言冲突错误(由命令层捕获处理) */
16
22
  export class AgentsMdLangConflictError extends Error {
17
23
  projectLang;
@@ -28,7 +34,7 @@ export const supportedTools = [
28
34
  {
29
35
  id: 'opencode',
30
36
  name: 'OpenCode',
31
- description: 'OpenCode AI 编码助手 — 生成 Agent 定义和 /opfx:* 技能',
37
+ description: 'OpenCode AI 编码助手 — 生成 Agent 定义和 openfeel-* 技能',
32
38
  enabled: true,
33
39
  },
34
40
  // 后续扩展:
@@ -64,13 +70,13 @@ export async function selectTools() {
64
70
  // ─── Agent 定义由 template-loader.ts 统一管理 ──────────────────────────
65
71
  // ─── Skill 定义内容 ─────────────────────────────────────────────────
66
72
  // AUTO-GENERATED-BEGIN: SKILL_DEFINITIONS
67
- const SKILL_DEFINITIONS = {
68
- 'agent-model-check': `---
69
- name: agent-model-check
70
- description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或需要排查模型配置时使用。涵盖 auth.json 校验、provider key 匹配、模型能力确认、Vision 多模态专项指南。
73
+ export const SKILL_DEFINITIONS = {
74
+ 'openfeel-agent-model-check': `---
75
+ name: openfeel-agent-model-check
76
+ description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或需要排查模型配置时使用。涵盖 auth.json 校验、provider key 匹配、模型能力确认、openfeel-vision 多模态专项指南。
71
77
  ---
72
78
 
73
- # Skill: agent-model-check
79
+ # Skill: openfeel-agent-model-check
74
80
 
75
81
  # Agent 模型检查与修复
76
82
 
@@ -79,7 +85,7 @@ description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或
79
85
  - Agent 调度时报 \`Model not found: xxx\`
80
86
  - 需要验证某个 Agent 的模型是否可用
81
87
  - 新增 Agent 后需确认模型配置正确
82
- - 排查多模态(Vision)Agent 无法处理图片的问题
88
+ - 排查多模态(openfeel-vision)Agent 无法处理图片的问题
83
89
 
84
90
  ## 排查流程
85
91
 
@@ -113,22 +119,22 @@ cat ~/.local/share/opencode/auth.json
113
119
 
114
120
  | 能力需求 | 需确认的字段 | 示例 |
115
121
  |----------|-------------|------|
116
- | 视觉/图像分析 | Input = Yes | \`qwen3-vl-plus\` |
122
+ | 视觉/图像分析 | Input = Yes | \`deepseek-flash\` |
117
123
  | 工具调用 | Tool Call = Yes | \`qwen3.7-plus\` |
118
124
  | 结构化输出 | Structured = Yes | \`qwen3.7-flash\` |
119
125
  | 推理/思考 | Reasoning = Yes | \`qwq-plus\` |
120
126
 
121
127
  **常见陷阱**:
122
128
  - \`qwen3.7-plus\` 是纯文本模型,不支持图像输入
123
- - \`qwen3-vl-plus\` 是视觉模型,支持图像分析
124
- - 模型名中的 \`vl\` 表示 Vision-Language
129
+ - \`deepseek-flash\` 支持多模态图像输入(openfeel-vision 现用模型)
130
+ - 备选视觉模型:\`qwen3-vl-plus\`(模型名中的 \`vl\` 表示 Vision-Language)
125
131
 
126
132
  ### 第四步:检查 opencode.jsonc 配置
127
133
 
128
134
  \`\`\`jsonc
129
135
  {
130
136
  "agent": {
131
- "vision": {
137
+ "openfeel-vision": {
132
138
  "model": "{auth.json_key}/{model_id}" // 格式:provider_key/model_id
133
139
  }
134
140
  }
@@ -159,17 +165,18 @@ cat ~/.local/share/opencode/auth.json
159
165
  | opencode.jsonc 语法 | 检查 JSON 格式 | 无语法错误 |
160
166
  | 重启生效 | 重启 opencode 后重新测试 | 无 Model not found 报错 |
161
167
 
162
- ## 多模态(Vision)Agent 专项
168
+ ## 多模态(openfeel-vision)Agent 专项
163
169
 
164
- Vision Agent 必须配置多模态模型。Alibaba 系列视觉模型:
170
+ openfeel-vision Agent 必须配置多模态模型。当前默认使用 \`deepseek/deepseek-flash\`(已支持多模态图像输入):
165
171
 
166
- | 模型 ID | 完整引用(alibaba-cn) | 上下文 | 图像输入 |
167
- |---------|----------------------|--------|---------|
168
- | qwen3-vl-plus | \`alibaba-cn/qwen3-vl-plus\` | 262K | ✅ |
172
+ | 模型 ID | 完整引用 | 上下文 | 图像输入 |
173
+ |---------|---------|--------|---------|
174
+ | deepseek-flash | \`deepseek/deepseek-flash\` | — | ✅ |
175
+ | qwen3-vl-plus | \`alibaba-cn/qwen3-vl-plus\` | 262K | ✅(备选) |
169
176
  | qwen-vl-plus | \`alibaba-cn/qwen-vl-plus\` | 131K | ❌(旧版) |
170
177
  | qwen-vl-max | \`alibaba-cn/qwen-vl-max\` | 131K | ❌(旧版) |
171
178
 
172
- **推荐**:优先使用 \`qwen3-vl-plus\`,上下文最大且为最新视觉模型。
179
+ **推荐**:优先使用 \`deepseek-flash\`(已内置多模态);如需更大上下文可备选 \`qwen3-vl-plus\`。
173
180
 
174
181
  ## 常见错误与修复
175
182
 
@@ -178,10 +185,10 @@ Vision Agent 必须配置多模态模型。Alibaba 系列视觉模型:
178
185
  | \`Model not found: Alibaba(China)/xxx\` | 使用了自定义 provider name 而非 auth.json key | 改为 auth.json 中的实际 key |
179
186
  | \`Model not found: alibaba/xxx\` | 内置 key 与实际注册的 key 不一致 | 检查 auth.json,使用实际 key |
180
187
  | \`Model not found: xxx. Did you mean: yyy\` | 模型名拼写错误或不存在 | 使用 \`Did you mean\` 建议的名称 |
181
- | Agent 调度成功但无法处理图片 | 配置了纯文本模型 | 改为带 \`vl\` 后缀的视觉模型 |
188
+ | Agent 调度成功但无法处理图片 | 配置了纯文本模型 | 改为支持图像输入的模型(如 \`deepseek-flash\`) |
182
189
  `,
183
- 'bug-acceptance': `---
184
- name: bug-acceptance
190
+ 'openfeel-bug-acceptance': `---
191
+ name: openfeel-bug-acceptance
185
192
  description: 标准化 Bug 验收流程,供测试 Agent 或代码 Agent(自测后自查)调用。
186
193
  ---
187
194
 
@@ -233,8 +240,8 @@ description: 标准化 Bug 验收流程,供测试 Agent 或代码 Agent(自
233
240
 
234
241
  验收通过后,核心结论写入 \`.openfeel/bugs/{module}.md\`,并在公共日志简要记录。
235
242
  `,
236
- 'check-kb': `---
237
- name: check-kb
243
+ 'openfeel-check-kb': `---
244
+ name: openfeel-check-kb
238
245
  description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需求返回最相关的参考信息。精确匹配无结果时自动触发语义检索回退,避免一次加载全量内容。
239
246
  ---
240
247
 
@@ -284,7 +291,7 @@ description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需
284
291
 
285
292
  ### 5. 语义检索回退(自动)
286
293
 
287
- 当精确匹配未找到任何相关条目(无 \`[+]\` 条目或所有条目与当前任务无关)时,**本技能自行执行语义检索**,无需调用方另行加载 \`search-kb\`:
294
+ 当精确匹配未找到任何相关条目(无 \`[+]\` 条目或所有条目与当前任务无关)时,**本技能自行执行语义检索**,无需调用方另行加载 \`openfeel-search-kb\`:
288
295
 
289
296
  1. **索引就绪检查**:检查 \`.openfeel/tmp/vectors/index.json\` 是否存在
290
297
  2. **索引缺失时**:输出以下提示后结束,不再继续回退:
@@ -323,7 +330,7 @@ description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需
323
330
  💡 如需确保索引为最新,可运行: python scripts/build_kb_index.py --dry-run
324
331
  \`\`\`
325
332
 
326
- > 注:语义检索是 \`check-kb\` 的内置回退能力。调用方只需 \`skill("check-kb")\` 一次,无需再手动调用 \`search-kb\`。
333
+ > 注:语义检索是 \`openfeel-check-kb\` 的内置回退能力。调用方只需 \`skill("openfeel-check-kb")\` 一次,无需再手动调用 \`openfeel-search-kb\`。
327
334
 
328
335
  ### 6. 强制检索标记
329
336
 
@@ -335,8 +342,8 @@ Skill 加载后,根据步骤 3 的匹配结果决定是否追加提示:
335
342
 
336
343
  - **若步骤 3 无相关条目**:不追加强制提示,静默返回(已在步骤 4 输出"知识库中暂无相关记录,可继续执行")。
337
344
  `,
338
- 'get-bugs': `---
339
- name: get-bugs
345
+ 'openfeel-get-bugs': `---
346
+ name: openfeel-get-bugs
340
347
  description: 获取当前模块下状态为 open 或 fixing 的 Bug 列表,供代码 Agent 会话启动或承接时使用。
341
348
  ---
342
349
 
@@ -372,9 +379,9 @@ description: 获取当前模块下状态为 open 或 fixing 的 Bug 列表,供
372
379
 
373
380
  输出:\`模块 [模块名] 当前无待处理 Bug。\`
374
381
  `,
375
- 'get-stage-status': `---
376
- name: get-stage-status
377
- description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子计划状态、责任 Agent、是否允许自动推进以及下一步建议。用于 Reviewer/Executor/Feel Tester 在处理阶段任务前确认流程状态。
382
+ 'openfeel-get-stage-status': `---
383
+ name: openfeel-get-stage-status
384
+ description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子计划状态、责任 Agent、是否允许自动推进以及下一步建议。用于 openfeel-reviewer/openfeel-executor/openfeel-feel-tester 在处理阶段任务前确认流程状态。
378
385
  ---
379
386
 
380
387
  # 获取子计划状态
@@ -479,29 +486,29 @@ description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子
479
486
  {若依赖就绪且可自动推进,列出同批次可并行启动的其他阶段}
480
487
 
481
488
  ## 下一步建议
482
- {根据状态给出下一步,例如:启动 Code、等待用户、启动 Tester、停止流程;若存在并行候选则建议批量启动}
489
+ {根据状态给出下一步,例如:启动 Code、等待用户、启动 openfeel-feel-tester、停止流程;若存在并行候选则建议批量启动}
483
490
  \`\`\`
484
491
 
485
492
  ## 状态到下一步映射
486
493
 
487
494
  | 状态 | 下一步建议 |
488
495
  |------|------------|
489
- | \`planned\` | 等待用户确认或 Planner 细化计划 |
490
- | \`ready_for_code\` | Planner 可启动 Executor |
491
- | \`coding\` | Executor 正在开发 |
492
- | \`ready_for_review\` | Executor 可启动 Reviewer 审查,或等待用户触发 |
493
- | \`review_failed\` | Reviewer 可启动 Executor 修复审查问题 |
496
+ | \`planned\` | 等待用户确认或 openfeel-planner 细化计划 |
497
+ | \`ready_for_code\` | openfeel-planner 可启动 openfeel-executor |
498
+ | \`coding\` | openfeel-executor 正在开发 |
499
+ | \`ready_for_review\` | openfeel-executor 可启动 openfeel-reviewer 审查,或等待用户触发 |
500
+ | \`review_failed\` | openfeel-reviewer 可启动 openfeel-executor 修复审查问题 |
494
501
  | \`review_passed\` | Feel 可推进到 ready_for_test |
495
- | \`ready_for_test\` | Feel 可启动 Feel Tester |
496
- | \`test_writing\` | Feel Tester 正在写测试 |
497
- | \`testing\` | Feel Tester 正在测试 |
498
- | \`bug_found\` | Feel Tester 可启动 Executor 修复 Bug |
499
- | \`bug_fixing\` | Executor 正在修复 Bug |
502
+ | \`ready_for_test\` | Feel 可启动 openfeel-feel-tester |
503
+ | \`test_writing\` | openfeel-feel-tester 正在写测试 |
504
+ | \`testing\` | openfeel-feel-tester 正在测试 |
505
+ | \`bug_found\` | openfeel-feel-tester 可启动 openfeel-executor 修复 Bug |
506
+ | \`bug_fixing\` | openfeel-executor 正在修复 Bug |
500
507
  | \`done\` | 流程完成,停止 |
501
508
  | \`paused\` | 等待用户处理暂停原因 |
502
509
  `,
503
- 'health': `---
504
- name: health
510
+ 'openfeel-health': `---
511
+ name: openfeel-health
505
512
  description: 加载流水线健康检查结果,供 Agent 判断 flow.json 与工作区状态是否一致。
506
513
  ---
507
514
 
@@ -521,8 +528,8 @@ description: 加载流水线健康检查结果,供 Agent 判断 flow.json 与
521
528
 
522
529
  健康检查摘要:通过项数、失败项列表及原因,失败时给出修复建议
523
530
  `,
524
- 'model-check': `---
525
- name: model-check
531
+ 'openfeel-model-check': `---
532
+ name: openfeel-model-check
526
533
  description: Feel 自检时检查所有 Agent 的模型配置状态,识别期望模型 vs 实际模型的差距,引导用户在目标工具中完成配置。首次配置后存储为部署模板,新项目可直接复用。
527
534
  ---
528
535
 
@@ -567,14 +574,14 @@ Feel Agent 在以下时机加载本 Skill:
567
574
  | Agent 文件 | 默认模型角色 |
568
575
  |------------|-------------|
569
576
  | \`feel.md\` | \`primary_reasoning\`(主力推理) |
570
- | \`planner.md\` | \`reasoning\`(推理) |
571
- | \`executor.md\` | \`fast\`(快速) |
572
- | \`reviewer.md\` | \`cross_model\`(异种推理) |
573
- | \`archiver.md\` | \`reasoning\`(推理) |
574
- | \`schemer.md\` | \`reasoning\`(推理) |
575
- | \`feel-tester.md\` | \`reasoning\`(推理) |
576
- | \`utility.md\` | \`fast\`(快速) |
577
- | \`vision.md\` | \`multimodal\`(多模态) |
577
+ | \`openfeel-planner.md\` | \`reasoning\`(推理) |
578
+ | \`openfeel-executor.md\` | \`fast\`(快速) |
579
+ | \`openfeel-reviewer.md\` | \`cross_model\`(异种推理) |
580
+ | \`openfeel-archiver.md\` | \`reasoning\`(推理) |
581
+ | \`openfeel-schemer.md\` | \`reasoning\`(推理) |
582
+ | \`openfeel-feel-tester.md\` | \`reasoning\`(推理) |
583
+ | \`openfeel-utility.md\` | \`fast\`(快速) |
584
+ | \`openfeel-vision.md\` | \`multimodal\`(多模态) |
578
585
 
579
586
  ### 3. 检查 config.yaml 模型配置
580
587
 
@@ -589,14 +596,14 @@ models:
589
596
  base_url: https://api.deepseek.com
590
597
  api_key_env: DEEPSEEK_API_KEY
591
598
  agents: # Agent 级覆盖(可选)
592
- reviewer:
599
+ openfeel-reviewer:
593
600
  provider: anthropic
594
601
  model_name: claude-sonnet-4-20250514
595
602
  base_url: https://api.anthropic.com
596
603
  api_key_env: ANTHROPIC_API_KEY
597
- executor:
604
+ openfeel-executor:
598
605
  provider: deepseek
599
- model_name: deepseek-v4-flash
606
+ model_name: deepseek-flash
600
607
  roles: # 角色级覆盖(可选)
601
608
  cross_model:
602
609
  provider: openai
@@ -629,8 +636,8 @@ models:
629
636
  | Agent | 角色要求 | 实际模型 | 配置来源 | 状态 |
630
637
  |-------|----------|----------|----------|:--:|
631
638
  | Feel | 主力推理 | deepseek-v4-pro | default | ⚠️ |
632
- | Reviewer | **异种推理** | deepseek-v4-pro | default | ❌ 与主力相同! |
633
- | Executor | 快速 | deepseek-v4-pro | default | ⚠️ 未使用快速模型 |
639
+ | openfeel-reviewer | **异种推理** | deepseek-v4-pro | default | ❌ 与主力相同! |
640
+ | openfeel-executor | 快速 | deepseek-v4-pro | default | ⚠️ 未使用快速模型 |
634
641
  \`\`\`
635
642
 
636
643
  ### 5. 输出检查报告
@@ -673,8 +680,8 @@ models:
673
680
  | 触发条件 | 引导内容 |
674
681
  |----------|----------|
675
682
  | \`models\` 节不存在 | "未检测到模型配置。你需要为不同角色分配模型吗?" → 引导创建 |
676
- | Reviewer 使用与主力相同模型 | "⚠️ Reviewer 当前与 Feel 使用相同模型,异种交叉审查的核心优势无法发挥。建议为 Reviewer 配置不同的模型系列。" |
677
- | Executor 使用推理模型 | "⚠️ Executor 建议使用快速模型以节省成本。是否配置?" |
683
+ | openfeel-reviewer 使用与主力相同模型 | "⚠️ openfeel-reviewer 当前与 Feel 使用相同模型,异种交叉审查的核心优势无法发挥。建议为 openfeel-reviewer 配置不同的模型系列。" |
684
+ | openfeel-executor 使用推理模型 | "⚠️ openfeel-executor 建议使用快速模型以节省成本。是否配置?" |
678
685
  | 关键 Agent 无任何配置 | "以下 Agent 无模型配置:{列表}。请配置。" |
679
686
 
680
687
  ### 7. 写入配置
@@ -701,7 +708,7 @@ models:
701
708
  model_name: xxx
702
709
  ...
703
710
  agents:
704
- reviewer:
711
+ openfeel-reviewer:
705
712
  provider: xxx
706
713
  ...
707
714
  roles:
@@ -718,12 +725,12 @@ models:
718
725
  - 报告语言:中文
719
726
  - 每次检查后将结果摘要写入 \`.openfeel/log/\`(仅首次发现关键问题时)
720
727
  `,
721
- 'model-config': `---
722
- name: model-config
723
- description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或需要调整/新增 Agent 模型时使用。覆盖 opencode.jsonc 配置、模型名查找方法、多模态模型(Vision)特殊注意事项。
728
+ 'openfeel-model-config': `---
729
+ name: openfeel-model-config
730
+ description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或需要调整/新增 Agent 模型时使用。覆盖 opencode.jsonc 配置、模型名查找方法、多模态模型(openfeel-vision)特殊注意事项。
724
731
  ---
725
732
 
726
- # Skill: model-config
733
+ # Skill: openfeel-model-config
727
734
 
728
735
  # Agent 模型查找与配置
729
736
 
@@ -732,7 +739,7 @@ description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或
732
739
  - Agent 调用时报 \`Model not found: xxx\`
733
740
  - 需要为 Agent 更换或指定模型
734
741
  - 新增 Agent 后需要配置其模型
735
- - Vision / 多模态模型无法正常调用
742
+ - openfeel-vision / 多模态模型无法正常调用
736
743
 
737
744
  ## 配置位置
738
745
 
@@ -741,8 +748,8 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
741
748
  \`\`\`jsonc
742
749
  {
743
750
  "agent": {
744
- "vision": {
745
- "model": "qwen3-vl-plus" // 模型名格式:provider/model-id 或 model-id
751
+ "openfeel-vision": {
752
+ "model": "deepseek/deepseek-flash" // 模型名格式:provider/model-id 或 model-id
746
753
  }
747
754
  }
748
755
  }
@@ -750,6 +757,8 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
750
757
 
751
758
  > ⚠️ **配置修改后必须重启 opencode 才能生效**。运行中的会话使用启动时加载的配置。
752
759
 
760
+ > 💡 **推荐**:可用 \`openfeel model set/get/list --scope default|global|project\` 一键读写三层级模型(工具默认 / 全局 / 当前项目),无需手工定位文件。
761
+
753
762
  ## 查找可用模型名
754
763
 
755
764
  当收到 \`"Model not found: xxx. Did you mean: aaa, bbb?"\` 错误时:
@@ -759,17 +768,21 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
759
768
 
760
769
  ## Agent 定义文件中的 model 字段
761
770
 
762
- \`.opencode/agents/<name>.md\` 中的 \`model\` 字段是声明性的(供 Agent 自述),**不直接控制平台模型分配**。实际的模型绑定由 \`opencode.jsonc\` 的 \`agent.<name>.model\` 控制。
771
+ \`.opencode/agents/<name>.md\` 的 frontmatter \`model:\` 字段**是生效的,且优先于** \`opencode.jsonc\` 的 \`agent.<name>.model\`(stage-40 REV-1606 实测)。opencode 模型解析优先级链:
772
+
773
+ \`\`\`
774
+ 项目 agents frontmatter > 全局 agents frontmatter > 项目 opencode.jsonc agent.model > 全局 opencode.jsonc agent.model > opencode 默认
775
+ \`\`\`
763
776
 
764
- 因此修改模型需要两步:
765
- 1. 修改 \`opencode.jsonc\` 中的 \`agent.<name>.model\`
766
- 2. (可选)同步修改 \`.opencode/agents/<name>.md\` 和模板文件 \`src/core/templates-data/agents/\` 中的 \`model\` 字段,保持一致性
777
+ 因此修改模型时以 frontmatter 为准(只改 jsonc 会被 frontmatter 遮蔽),建议两处同步保持一致:
778
+ 1. 修改 \`.opencode/agents/<name>.md\` 的 frontmatter \`model:\`(模板源 \`src/core/templates-data/opencode/agents/\` 同步)
779
+ 2. (可选,保持一致)同步修改 \`opencode.jsonc\` 的 \`agent.<name>.model\`
767
780
 
768
- ## 多模态(Vision)模型特殊规则
781
+ ## 多模态(openfeel-vision)模型特殊规则
769
782
 
770
783
  - Feel 的主力模型(DeepSeek V4 Pro)不支持图片输入
771
- - 遇到图片输入时 Feel 会自动委托 Vision Agent
772
- - Vision Agent 需要配置多模态模型(如 \`qwen-vl-plus\`、\`qwen3-vl-plus\`)
784
+ - 遇到图片输入时 Feel 会自动委托 openfeel-vision Agent
785
+ - openfeel-vision Agent 需要配置多模态模型(现用 \`deepseek-flash\`,备选 \`qwen3-vl-plus\`)
773
786
  - 模型名不要随意添加前缀(如 \`alibaba/\`),以平台提示的可用名为准
774
787
  - **模型引用格式**:\`{auth.json中的key}/{模型ID}\`,不是 \`provider.name\` 也不是 \`provider.id\`
775
788
  - 读取 \`~/.local/share/opencode/auth.json\` 确认实际 provider key(常见:\`alibaba-cn\`、\`deepseek\`、\`zhipuai\`)
@@ -780,17 +793,17 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
780
793
  | Agent | 模型类型 | 备注 |
781
794
  |-------|---------|------|
782
795
  | Feel(总统领) | 推理模型 | DeepSeek V4 Pro — 不支持多模态 |
783
- | Planner | 推理模型 | — |
784
- | Schemer | 推理模型 | — |
785
- | Executor | 快速模型 (Flash) | — |
786
- | Reviewer | 异种推理模型 (GLM) | — |
787
- | Feel Tester | 推理模型 | — |
796
+ | openfeel-planner | 推理模型 | — |
797
+ | openfeel-schemer | 推理模型 | — |
798
+ | openfeel-executor | 快速模型 (Flash) | — |
799
+ | openfeel-reviewer | 异种推理模型 (GLM) | — |
800
+ | openfeel-feel-tester | 推理模型 | — |
788
801
  | 事务官 | 快速模型 (Flash) | — |
789
- | Vision | 多模态模型 | 需配 qwen3-vl-plus |
790
- | Archiver | 推理模型 | — |
802
+ | openfeel-vision | 多模态模型 | 需配 deepseek-flash |
803
+ | openfeel-archiver | 推理模型 | — |
791
804
  `,
792
- 'recover': `---
793
- name: recover
805
+ 'openfeel-recover': `---
806
+ name: openfeel-recover
794
807
  description: 跨会话上下文恢复,供 Agent 在会话启动时重建流水线状态。
795
808
  ---
796
809
 
@@ -810,8 +823,8 @@ description: 跨会话上下文恢复,供 Agent 在会话启动时重建流水
810
823
 
811
824
  恢复摘要:流水线状态 + 阻塞项 + 待处理任务列表
812
825
  `,
813
- 'roadmap': `---
814
- name: roadmap
826
+ 'openfeel-roadmap': `---
827
+ name: openfeel-roadmap
815
828
  description: 加载项目路线图,供 Agent 查看版本规划和里程碑。
816
829
  ---
817
830
 
@@ -831,8 +844,8 @@ description: 加载项目路线图,供 Agent 查看版本规划和里程碑。
831
844
 
832
845
  格式化路线图摘要:版本清单 + 各版本阶段进度
833
846
  `,
834
- 'search-kb': `---
835
- name: search-kb
847
+ 'openfeel-search-kb': `---
848
+ name: openfeel-search-kb
836
849
  description: 语义检索 .openfeel/kb/ 项目知识库。当精确匹配无结果或任务描述模糊时,通过向量相似度搜索语义相关的知识条目。支持图谱遍历返回关联条目。
837
850
  ---
838
851
 
@@ -951,16 +964,16 @@ description: 语义检索 .openfeel/kb/ 项目知识库。当精确匹配无结
951
964
 
952
965
  - 向量索引是缓存层,文件系统始终是 single source of truth。若检索结果与预期不符,检查索引是否过期(运行 \`--dry-run\` 查看变更文件)
953
966
  - 图谱链接亦是缓存层,Markdown 文件中 \`[[wikilink]]\` 是真实数据源;图谱可随时通过 \`python scripts/build_kb_index.py --graph\` 重建
954
- - 语义检索适合模糊查询和探索性搜索,精确关键词匹配优先使用 \`check-kb\`
955
- - 此技能是 \`check-kb\` 的回退方案——当 \`check-kb\` 精确匹配无结果时可自动调用
967
+ - 语义检索适合模糊查询和探索性搜索,精确关键词匹配优先使用 \`openfeel-check-kb\`
968
+ - 此技能是 \`openfeel-check-kb\` 的回退方案——当 \`openfeel-check-kb\` 精确匹配无结果时可自动调用
956
969
  - 图遍历返回的关联条目仅基于已建立的 wikilink 链接,若条目未引用或被引用其他条目,图谱中不会出现对应关联
957
970
  `,
958
- 'sync-status': `---
959
- name: sync-status
971
+ 'openfeel-sync-status': `---
972
+ name: openfeel-sync-status
960
973
  description: 聚合所有成员的任务进度视图,供任意 Agent 快速了解项目整体协作状态。
961
974
  ---
962
975
 
963
- # Skill: sync-status
976
+ # Skill: openfeel-sync-status
964
977
 
965
978
  # 聚合任务进度
966
979
 
@@ -1023,10 +1036,9 @@ description: 聚合所有成员的任务进度视图,供任意 Agent 快速了
1023
1036
 
1024
1037
  格式化后的 Markdown 进度摘要,不含文件修改。
1025
1038
 
1026
- Base directory for this skill: file:///C:/Code/AI/AI_Prompt/.kilo/skills/sync-status
1027
1039
  `,
1028
- 'update-stage-status': `---
1029
- name: update-stage-status
1040
+ 'openfeel-update-stage-status': `---
1041
+ name: openfeel-update-stage-status
1030
1042
  description: 标准化更新 .openfeel/plan/{series}/{stage}/status.md 的子计划状态、责任 Agent 和状态记录,避免各 Agent 随意改写状态文件。适用于自动闭环和人工流程中的阶段状态变更。
1031
1043
  ---
1032
1044
 
@@ -1124,10 +1136,10 @@ planned | ready_for_code | coding | ready_for_review | review_failed | review_pa
1124
1136
 
1125
1137
  ### 6. 合并状态处理
1126
1138
 
1127
- 当子计划状态变为 \`done\` 或 \`review_passed\`(且 Reviewer 验收完毕)时,根据 \`.openfeel/config.yaml\` 中的 \`merge_mode\` 决定合并行为:
1139
+ 当子计划状态变为 \`done\` 或 \`review_passed\`(且 openfeel-reviewer 验收完毕)时,根据 \`.openfeel/config.yaml\` 中的 \`merge_mode\` 决定合并行为:
1128
1140
 
1129
1141
  - **\`merge_mode=auto\`**:
1130
- 1. 将 \`合并状态\` 更新为 \`merged\`(实际合并由 Executor 执行 git 操作,Skill 仅更新状态字段)
1142
+ 1. 将 \`合并状态\` 更新为 \`merged\`(实际合并由 openfeel-executor 执行 git 操作,Skill 仅更新状态字段)
1131
1143
  2. 将 \`合并状态\` 更新为 \`cleaned\`
1132
1144
  3. 在状态记录中注明"自动合并"
1133
1145
  - **\`merge_mode=manual\`**:
@@ -1145,8 +1157,8 @@ planned | ready_for_code | coding | ready_for_review | review_failed | review_pa
1145
1157
  - 自动推进:保持 {enabled/disabled}
1146
1158
  \`\`\`
1147
1159
  `,
1148
- 'wizard': `---
1149
- name: wizard
1160
+ 'openfeel-wizard': `---
1161
+ name: openfeel-wizard
1150
1162
  description: 交互式流水线向导,供 Agent 在终端中逐步推进流水线阶段。
1151
1163
  ---
1152
1164
 
@@ -1170,301 +1182,97 @@ description: 交互式流水线向导,供 Agent 在终端中逐步推进流水
1170
1182
  `,
1171
1183
  };
1172
1184
  // AUTO-GENERATED-END: SKILL_DEFINITIONS
1173
- // ─── 新增的 Skill 名称列表 ─────────────────────────────────────────
1174
- const NEW_SKILL_NAMES = [
1175
- 'bug-acceptance',
1176
- 'check-kb',
1177
- 'get-bugs',
1178
- 'get-stage-status',
1179
- 'model-check',
1180
- 'search-kb',
1181
- 'sync-status',
1182
- 'update-stage-status',
1183
- ];
1184
1185
  // ─── 辅助函数 ──────────────────────────────────────────────────────
1186
+ /** 写全文时的受管内容组合:frontmatter 序列化 + 正文标记包裹 */
1187
+ function composeManagedContent(content, type) {
1188
+ const split = type === 'markdown' ? splitFrontmatter(content) : null;
1189
+ if (split) {
1190
+ return serializeFrontmatter(split.frontmatter) + wrapRegion(split.body, type);
1191
+ }
1192
+ return wrapRegion(content, type);
1193
+ }
1185
1194
  /**
1186
- * 带冲突检测的写入函数(替代原 writeIfChanged)
1187
- *
1188
- * 三态逻辑:
1189
- * - 文件不存在 → created
1190
- * - 文件已存在 + hash 匹配 update_state 中记录 → 安全覆盖 → updated
1191
- * - 文件已存在 + hash 不匹配 → 拒绝覆盖 → conflicts
1192
- * - 文件已存在 + 不在 update_state 管理中 → 安全覆盖 → updated(降级)
1193
- * - 文件已存在 + update_state 不存在或损坏 → 安全覆盖 → updated(降级为旧行为)
1194
- *
1195
- * REV-001:此函数仅负责分类(created/updated/skipped/conflicts),
1196
- * hash 的实际更新在 updateProject() 末尾统一处理,
1197
- * 确保"冲突路径非冲突文件 hash 同步更新"。
1195
+ * 受管文件写入(控制区优先 + hash 兜底 三态)。
1196
+ * 分派依据 detectFileType;jsonc 不走本函数(调用方走深度合并)。
1197
+ * target 供 update_infos 路径记录:全局资产 { isGlobal: true },项目资产 { isGlobal: false, projectRoot }。
1198
1198
  */
1199
- function writeWithMergeDetection(filePath, content, relativePath, updateState, created, updated, skipped, conflicts) {
1199
+ function writeManagedFile(filePath, content, stateKey, updateState, target) {
1200
+ const type = detectFileType(filePath);
1201
+ if (type === null || type === 'jsonc') {
1202
+ // 无策略 / JSONC:不应到达(jsonc 由 updateProject 单独走深度合并)
1203
+ throw new Error(`writeManagedFile: 不支持的类型 ${filePath}`);
1204
+ }
1205
+ // 拆 incoming 的 frontmatter + 正文(gitignore 无 frontmatter,body=content)
1206
+ const incomingSplit = type === 'markdown' ? splitFrontmatter(content) : null;
1207
+ const incomingFm = incomingSplit?.frontmatter ?? null;
1208
+ const incomingBody = incomingSplit?.body ?? content;
1209
+ // 不存在 → created(写全文:frontmatter + 标记包裹正文)
1200
1210
  if (!existsSync(filePath)) {
1201
- // 文件不存在 → 新建
1202
- mkdirSync(dirname(filePath), { recursive: true });
1203
- writeFileSync(filePath, content, 'utf-8');
1204
- created.push(relativePath);
1205
- return;
1211
+ atomicWriteFileSync(filePath, composeManagedContent(content, type));
1212
+ return 'created';
1206
1213
  }
1207
1214
  const existing = readFileSync(filePath, 'utf-8');
1208
- // 内容相同 → skip(快速路径:全文比对,避免 hash 计算)
1209
- if (existing === content) {
1210
- skipped.push(relativePath);
1211
- return;
1212
- }
1213
- // 检查 update_state 中是否有该文件的记录
1214
- const fileState = updateState?.files[relativePath];
1215
- if (fileState) {
1216
- // 有记录 → 比对 hash
1217
- const currentHash = hashContent(existing);
1218
- if (currentHash === fileState.hash) {
1219
- // hash 一致 → 用户未修改 → 安全覆盖
1220
- writeFileSync(filePath, content, 'utf-8');
1221
- updated.push(relativePath);
1222
- return;
1215
+ const existingSplit = type === 'markdown' ? splitFrontmatter(existing) : null;
1216
+ const existingFm = existingSplit?.frontmatter ?? null;
1217
+ const existingBody = existingSplit?.body ?? existing;
1218
+ const region = parseRegion(existingBody, type);
1219
+ if (region.status === 'ok') {
1220
+ // 含标记:frontmatter 合并 + 正文区内替换,全文比对(REV-901 skip 判定)
1221
+ // incoming 无 frontmatter(如 core.md/AGENTS.md 模板)时保留用户已有 frontmatter,避免写入丢失(REV-1101)
1222
+ const newFm = existingFm && incomingFm ? mergeFrontmatter(existingFm, incomingFm) : (existingFm ?? incomingFm);
1223
+ const newBody = replaceRegion(existingBody, incomingBody, type);
1224
+ const newContent = newFm ? serializeFrontmatter(newFm) + newBody : newBody;
1225
+ if (newContent === normalize(existing)) {
1226
+ return 'skipped';
1223
1227
  }
1224
- // hash 不一致 → 用户已修改 → 冲突!
1225
- conflicts.push(relativePath);
1226
- return;
1228
+ atomicWriteFileSync(filePath, newContent);
1229
+ return 'updated';
1227
1230
  }
1228
- // 无记录(降级路径:update_state 损坏或旧版本)→ 安全覆盖
1229
- writeFileSync(filePath, content, 'utf-8');
1230
- updated.push(relativePath);
1231
- }
1232
- /**
1233
- * 解析 JSONC 文本为 JavaScript 对象
1234
- * 先规范化换行符,再去除行注释(跳过字符串内部),最后解析
1235
- */
1236
- function parseJsonc(text) {
1237
- // 去除 \r 控制字符(避免 Windows CRLF 导致 JSON 解析报 "bad control character")
1238
- const normalized = text.replace(/\r/g, '');
1239
- // 逐字符处理,去除行注释(跳过字符串内部,避免匹配到 https:// 等 URL 中的 //)
1240
- let result = '';
1241
- let inString = false;
1242
- let inEscape = false;
1243
- let i = 0;
1244
- while (i < normalized.length) {
1245
- const ch = normalized[i];
1246
- const next = normalized[i + 1];
1247
- // 处理转义字符
1248
- if (inEscape) {
1249
- result += ch;
1250
- inEscape = false;
1251
- i++;
1252
- continue;
1253
- }
1254
- if (ch === '\\' && inString) {
1255
- result += ch;
1256
- inEscape = true;
1257
- i++;
1258
- continue;
1259
- }
1260
- // 字符串边界
1261
- if (ch === '"') {
1262
- inString = !inString;
1263
- result += ch;
1264
- i++;
1265
- continue;
1231
+ if (region.status === 'none') {
1232
+ // 无标记:hash 兜底(N3)
1233
+ const fileState = updateState?.files[stateKey];
1234
+ if (fileState && hashContent(existing) === fileState.hash) {
1235
+ // adopt:存量框架文件未改 → 直接写带标记的新框架内容(等价 created 记为 updated)
1236
+ atomicWriteFileSync(filePath, composeManagedContent(content, type));
1237
+ return 'updated';
1266
1238
  }
1267
- // 行注释:不在字符串内时遇到 //
1268
- if (!inString && ch === '/' && next === '/') {
1269
- // 跳过直到行尾
1270
- i += 2; // 跳过 //
1271
- while (i < normalized.length && normalized[i] !== '\n') {
1272
- i++;
1273
- }
1274
- // 保留换行符
1275
- if (i < normalized.length && normalized[i] === '\n') {
1276
- result += '\n';
1277
- i++;
1278
- }
1279
- continue;
1280
- }
1281
- result += ch;
1282
- i++;
1283
- }
1284
- return JSON.parse(result);
1285
- }
1286
- /**
1287
- * 获取 opencode.jsonc 更新后的完整内容
1288
- * 读取现有文件(若存在),合并更新后生成新的 JSONC 文本
1289
- */
1290
- function buildUpdatedJsonc(projectPath) {
1291
- const jsoncPath = resolve(projectPath, 'opencode.jsonc');
1292
- let jsoncObj;
1293
- let rawContent;
1294
- if (existsSync(jsoncPath)) {
1295
- // 已有文件 → 解析并合并,保留原始字符串用于后续补丁式更新
1296
- rawContent = readFileSync(jsoncPath, 'utf-8');
1297
- jsoncObj = parseJsonc(rawContent);
1298
- }
1299
- else {
1300
- // 新文件 → 基础结构
1301
- jsoncObj = {
1302
- $schema: 'https://opencode.openfeel/config.json',
1303
- default_agent: 'feel',
1304
- instructions: ['AGENTS.md', '.opencode/instructions/core.md'],
1305
- skills: {},
1306
- };
1307
- }
1308
- // 更新 default_agent
1309
- jsoncObj.default_agent = 'feel';
1310
- // 确保 instructions 存在
1311
- if (!jsoncObj.instructions) {
1312
- jsoncObj.instructions = ['AGENTS.md', '.opencode/instructions/core.md'];
1313
- }
1314
- // 合并 skills:保留原有 skill,添加新的 /opfx:* skill
1315
- const skills = jsoncObj.skills || {};
1316
- for (const name of NEW_SKILL_NAMES) {
1317
- skills[name] = `.opencode/skills/${name}`;
1239
+ // 追加受管区(标记包裹正文,不触碰 frontmatter),并记录待会话启动复核
1240
+ const appendedRegion = wrapRegion(incomingBody, type);
1241
+ const sep = existing.endsWith('\n') ? '' : '\n';
1242
+ atomicWriteFileSync(filePath, `${existing}${sep}${appendedRegion}`);
1243
+ appendUpdateInfo('appended', target.isGlobal ? { absolutePath: filePath } : { projectRoot: target.projectRoot, relativePath: stateKey });
1244
+ return 'appended';
1318
1245
  }
1319
- jsoncObj.skills = skills;
1320
- // 格式化为 JSONC 输出:若有原始内容则基于它做补丁式替换,否则从头构建
1321
- return formatJsonc(jsoncObj, rawContent);
1246
+ // malformed(>1 对 / 不成对):不追加、不写盘,仅记录异常条目待人工修复标记;
1247
+ // 修复后下次 update 走 ok(区内替换)/ none(hash 兜底)正常路径,杜绝重复追加死循环(REV-1001 修订)
1248
+ appendUpdateInfo('anomaly', target.isGlobal ? { absolutePath: filePath } : { projectRoot: target.projectRoot, relativePath: stateKey });
1249
+ return 'skipped';
1322
1250
  }
1323
1251
  /**
1324
- * 将对象格式化为美观的 JSONC 字符串
1325
- * 若提供了 originalContent,基于原始字符串做补丁式替换,
1326
- * 仅更新 default_agent 和 skills,其余字段(含注释和未识别字段)原样保留。
1252
+ * 写单个全局资产文件(REV-1205 最小侵入抽取,供 migrate 复用)。
1253
+ * 等价 writeManagedFile(filePath, content, filePath, state, { isGlobal: true }),
1254
+ * 即 stateKey = 绝对路径、target 为全局资产。
1255
+ * @returns 动作分类,供调用方报告。
1327
1256
  */
1328
- function formatJsonc(obj, originalContent) {
1329
- // 有原始内容 → 补丁模式:仅替换需要更新的字段,保留注释和未知字段
1330
- if (originalContent) {
1331
- let result = originalContent;
1332
- // 替换 "default_agent" 的值
1333
- result = result.replace(/("default_agent"\s*:\s*)("[^"]*")/, `$1"${obj.default_agent}"`);
1334
- // 替换 "skills" 块
1335
- const skills = obj.skills;
1336
- result = replaceSkillsFieldInJsonc(result, skills);
1337
- return result;
1338
- }
1339
- // 无原始内容 → 从头构建(新文件场景)
1340
- return buildJsoncFromObject(obj);
1257
+ export function deployGlobalAsset(filePath, content, state) {
1258
+ return writeManagedFile(filePath, content, filePath, state, { isGlobal: true });
1341
1259
  }
1342
- /**
1343
- * 在 JSONC 原始字符串中查找并替换 "skills" 字段的整个对象块
1344
- * 使用括号计数来正确处理嵌套对象,并自动处理末尾逗号
1345
- */
1346
- function replaceSkillsFieldInJsonc(content, skills) {
1347
- // 查找 "skills" 键及冒号
1348
- const keyRegex = /("skills"\s*:\s*)\{/;
1349
- const keyMatch = content.match(keyRegex);
1350
- if (!keyMatch || keyMatch.index === undefined) {
1351
- // skills 字段不存在于原始文件中,不需要替换
1352
- return content;
1353
- }
1354
- const keyStart = keyMatch.index;
1355
- const openBraceIdx = keyStart + keyMatch[0].length - 1; // { 的位置
1356
- // 找到匹配的 },处理字符串内的大括号
1357
- let depth = 1;
1358
- let closeBraceIdx = openBraceIdx;
1359
- let inString = false;
1360
- let inEscape = false;
1361
- for (let i = openBraceIdx + 1; i < content.length && depth > 0; i++) {
1362
- const ch = content[i];
1363
- if (inEscape) {
1364
- inEscape = false;
1365
- continue;
1366
- }
1367
- if (ch === '\\' && inString) {
1368
- inEscape = true;
1369
- continue;
1370
- }
1371
- if (ch === '"') {
1372
- inString = !inString;
1373
- continue;
1374
- }
1375
- if (!inString) {
1376
- if (ch === '{') {
1377
- depth++;
1378
- }
1379
- else if (ch === '}') {
1380
- depth--;
1381
- }
1382
- }
1383
- if (depth === 0) {
1384
- closeBraceIdx = i;
1260
+ /** 将 writeManagedFile 的动作分发到对应结果数组 */
1261
+ function pushAction(action, stateKey, created, updated, skipped, appended) {
1262
+ switch (action) {
1263
+ case 'created':
1264
+ created.push(stateKey);
1265
+ break;
1266
+ case 'updated':
1267
+ updated.push(stateKey);
1268
+ break;
1269
+ case 'skipped':
1270
+ skipped.push(stateKey);
1271
+ break;
1272
+ case 'appended':
1273
+ appended.push(stateKey);
1385
1274
  break;
1386
- }
1387
- }
1388
- // 检查 } 后面是否有逗号:若 } 后紧跟(仅跳过空白)有逗号,则将逗号及其后空白纳入 endIdx
1389
- let endIdx = closeBraceIdx + 1;
1390
- let scanIdx = endIdx;
1391
- while (scanIdx < content.length && /[ \t]/.test(content[scanIdx])) {
1392
- scanIdx++;
1393
- }
1394
- if (scanIdx < content.length && content[scanIdx] === ',') {
1395
- scanIdx++; // 跳过逗号
1396
- // 跳过逗号后的空白(包括换行)
1397
- while (scanIdx < content.length && /[\s\n\r]/.test(content[scanIdx])) {
1398
- scanIdx++;
1399
- }
1400
- endIdx = scanIdx;
1401
- }
1402
- // 确定替换后是否需要末尾逗号:检查 skills 之后是否还有其他顶层字段
1403
- let hasMoreFields = false;
1404
- if (endIdx < content.length) {
1405
- // 从 endIdx 找到内容末尾前最后一个 }(根对象闭合)
1406
- const remaining = content.slice(endIdx);
1407
- const rootCloseIdx = remaining.lastIndexOf('}');
1408
- if (rootCloseIdx > 0) {
1409
- const between = remaining.slice(0, rootCloseIdx);
1410
- hasMoreFields = /[^\s]/.test(between);
1411
- }
1412
- }
1413
- // 构建新的 skills 块内容
1414
- const skillEntries = Object.entries(skills);
1415
- const skillLines = skillEntries.map(([key, value], idx) => {
1416
- const comma = idx < skillEntries.length - 1 ? ',' : '';
1417
- return ` "${key}": "${value}"${comma}`;
1418
- });
1419
- const blockContent = skillLines.join('\n');
1420
- const newBlock = `"skills": {\n${blockContent}\n }${hasMoreFields ? ',' : ''}`;
1421
- return content.slice(0, keyStart) + newBlock + content.slice(endIdx);
1422
- }
1423
- /**
1424
- * 从对象从头构建 JSONC(无原始内容时的回退路径)
1425
- */
1426
- function buildJsoncFromObject(obj) {
1427
- const skills = obj.skills;
1428
- // 构建 skills 条目行
1429
- const skillEntries = Object.entries(skills);
1430
- const skillsLines = skillEntries.map(([key, value], index) => {
1431
- const comma = index < skillEntries.length - 1 ? ',' : '';
1432
- return ` "${key}": "${value}"${comma}`;
1433
- });
1434
- const instructions = obj.instructions;
1435
- const instructionLines = instructions.map((inst, index) => {
1436
- const comma = index < instructions.length - 1 ? ',' : '';
1437
- return ` "${inst}"${comma}`;
1438
- });
1439
- // 构建完整 JSONC
1440
- const hasExperimental = typeof obj.experimental === 'object' && obj.experimental !== null;
1441
- const lines = [
1442
- '{',
1443
- ' "$schema": "https://opencode.openfeel/config.json",',
1444
- ` "default_agent": "${obj.default_agent}",`,
1445
- ' "instructions": [',
1446
- ...instructionLines,
1447
- ' ],',
1448
- ' "skills": {',
1449
- ...skillsLines,
1450
- hasExperimental ? ' },' : ' }',
1451
- ];
1452
- // 如果有 experimental 字段,保留它
1453
- if (hasExperimental) {
1454
- const exp = obj.experimental;
1455
- const expEntries = Object.entries(exp);
1456
- const expLines = expEntries.map(([key, value], index) => {
1457
- const comma = index < expEntries.length - 1 ? ',' : '';
1458
- const val = typeof value === 'string' ? `"${value}"` : String(value);
1459
- return ` "${key}": ${val}${comma}`;
1460
- });
1461
- lines.push(' "experimental": {');
1462
- lines.push(...expLines);
1463
- lines.push(' }');
1464
1275
  }
1465
- lines.push('}');
1466
- lines.push(''); // 末尾换行符
1467
- return lines.join('\n');
1468
1276
  }
1469
1277
  // ─── 主函数 ────────────────────────────────────────────────────────
1470
1278
  /**
@@ -1478,22 +1286,33 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
1478
1286
  const created = [];
1479
1287
  const updated = [];
1480
1288
  const skipped = [];
1481
- // ── 增量更新:加载 update_state.json ──
1289
+ const appended = [];
1290
+ // ── 增量更新:加载项目 state(AGENTS.md / 项目 opencode.jsonc)与全局 state(框架资产)──
1482
1291
  const state = loadUpdateState(projectPath);
1292
+ const globalState = loadGlobalUpdateState();
1483
1293
  const conflicts = [];
1484
- const agentsDir = resolve(projectPath, '.opencode', 'agents');
1485
- const skillsDir = resolve(projectPath, '.opencode', 'skills');
1486
- // 确保目标目录存在
1487
- mkdirSync(agentsDir, { recursive: true });
1488
- mkdirSync(skillsDir, { recursive: true });
1489
1294
  // 过滤:仅处理选中的工具
1490
1295
  if (!selectedTools.includes('opencode')) {
1491
- return { created: [], updated: [], skipped: [], conflicts: [] };
1296
+ return { created: [], updated: [], skipped: [], conflicts: [], appended: [] };
1492
1297
  }
1298
+ // N8:检测项目内遗留 .opencode/{agents,skills,instructions},只提示不迁移
1299
+ const legacyDirs = ['agents', 'skills', 'instructions']
1300
+ .filter((d) => existsSync(resolve(projectPath, '.opencode', d)));
1301
+ if (legacyDirs.length > 0) {
1302
+ console.warn(`[update] 检测到项目内旧布局 .opencode/{${legacyDirs.join(',')}},请运行 openfeel migrate(stage-39 提供)迁移。`);
1303
+ }
1304
+ // 全局部署目标(D1)
1305
+ const agentsDir = getGlobalAgentsDir();
1306
+ const skillsDir = getGlobalSkillsDir();
1307
+ // 项目内旧布局 agents 目录:仅用于 AGENTS.md 首次部署判定,保持原语义(项目级)
1308
+ const projectLegacyAgentsDir = resolve(projectPath, '.opencode', 'agents');
1309
+ // 确保全局目标目录存在
1310
+ mkdirSync(agentsDir, { recursive: true });
1311
+ mkdirSync(skillsDir, { recursive: true });
1493
1312
  // ── AGENTS.md 语言同步逻辑 ──
1494
1313
  let agentsDirHasContent = false;
1495
1314
  try {
1496
- agentsDirHasContent = existsSync(agentsDir) && readdirSync(agentsDir).length > 0;
1315
+ agentsDirHasContent = existsSync(projectLegacyAgentsDir) && readdirSync(projectLegacyAgentsDir).length > 0;
1497
1316
  }
1498
1317
  catch {
1499
1318
  agentsDirHasContent = false;
@@ -1501,18 +1320,18 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
1501
1320
  const agentsMdPath = resolve(projectPath, 'AGENTS.md');
1502
1321
  const agentsMdExists = existsSync(agentsMdPath);
1503
1322
  if (!agentsDirHasContent && !agentsMdExists) {
1504
- // 情况 1:首次部署 → 使用全局默认语言部署 AGENTS.md
1323
+ // 情况 1:首次部署 → 使用全局默认语言部署 AGENTS.md(受管区标记包裹)
1505
1324
  const globalConfig = getGlobalConfig();
1506
1325
  const deployLang = lang ?? globalConfig.lang ?? 'zh-CN';
1507
1326
  const agentsMdContent = loadTemplate(deployLang, 'agents-md');
1508
- writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
1327
+ atomicWriteFileSync(agentsMdPath, composeManagedContent(agentsMdContent, 'markdown'));
1509
1328
  created.push('AGENTS.md');
1510
1329
  }
1511
1330
  else if (!agentsMdExists && options?.lang) {
1512
1331
  // 情况 2b:已有 agents 目录但 AGENTS.md 被手动删除 + --lang 指定
1513
- // → 用指定语言重新创建 AGENTS.md
1332
+ // → 用指定语言重新创建 AGENTS.md(受管区标记包裹)
1514
1333
  const agentsMdContent = loadTemplate(options.lang, 'agents-md');
1515
- writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
1334
+ atomicWriteFileSync(agentsMdPath, composeManagedContent(agentsMdContent, 'markdown'));
1516
1335
  created.push('AGENTS.md');
1517
1336
  }
1518
1337
  else if (agentsMdExists && options?.lang) {
@@ -1522,9 +1341,9 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
1522
1341
  if (requestedLang !== projectLang) {
1523
1342
  // 语言不同 → 提示确认
1524
1343
  if (options?.force) {
1525
- // --force → 跳过确认,直接覆盖
1344
+ // --force → 跳过确认,直接覆盖(受管区标记包裹)
1526
1345
  const agentsMdContent = loadTemplate(requestedLang, 'agents-md');
1527
- writeFileSync(agentsMdPath, agentsMdContent, 'utf-8');
1346
+ atomicWriteFileSync(agentsMdPath, composeManagedContent(agentsMdContent, 'markdown'));
1528
1347
  updated.push('AGENTS.md');
1529
1348
  }
1530
1349
  else if (options?.interactive) {
@@ -1539,29 +1358,17 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
1539
1358
  }
1540
1359
  }
1541
1360
  else {
1542
- // 语言相同 → 比较内容,模板更新时传播部署
1361
+ // 语言相同 → 走受管文件三态(含标记区内替换 / 无标记 hash 兜底 / 追加)
1543
1362
  const templateContent = loadTemplate(requestedLang, 'agents-md');
1544
- const existingContent = readFileSync(agentsMdPath, 'utf-8');
1545
- if (existingContent !== templateContent) {
1546
- writeFileSync(agentsMdPath, templateContent, 'utf-8');
1547
- updated.push('AGENTS.md');
1548
- }
1549
- else {
1550
- skipped.push('AGENTS.md (language unchanged)');
1551
- }
1363
+ const action = writeManagedFile(agentsMdPath, templateContent, 'AGENTS.md', state, { isGlobal: false, projectRoot: projectPath });
1364
+ pushAction(action, 'AGENTS.md', created, updated, skipped, appended);
1552
1365
  }
1553
1366
  }
1554
1367
  else if (agentsMdExists) {
1555
- // 情况 3:已有项目 + 无 --lang 参数 → 比较内容,模板更新时传播部署
1368
+ // 情况 3:已有项目 + 无 --lang 参数 → 走受管文件三态
1556
1369
  const templateContent = loadTemplate(lang, 'agents-md');
1557
- const existingContent = readFileSync(agentsMdPath, 'utf-8');
1558
- if (existingContent !== templateContent) {
1559
- writeFileSync(agentsMdPath, templateContent, 'utf-8');
1560
- updated.push('AGENTS.md');
1561
- }
1562
- else {
1563
- skipped.push('AGENTS.md (use existing)');
1564
- }
1370
+ const action = writeManagedFile(agentsMdPath, templateContent, 'AGENTS.md', state, { isGlobal: false, projectRoot: projectPath });
1371
+ pushAction(action, 'AGENTS.md', created, updated, skipped, appended);
1565
1372
  }
1566
1373
  // 记录项目语言映射到全局配置
1567
1374
  // 使用 options?.lang(用户通过 --lang 显式指定的语言)优先,
@@ -1572,76 +1379,123 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
1572
1379
  catch {
1573
1380
  // 记录失败不影响主流程
1574
1381
  }
1575
- // 0. 生成 instructions/core.md(适配器层核心指令,与 init 的核心层分离)
1576
- const instructionsDir = resolve(projectPath, '.opencode', 'instructions');
1577
- mkdirSync(instructionsDir, { recursive: true });
1578
- const coreInstructionsPath = join(instructionsDir, 'core.md');
1382
+ // 全局 state 可写实例:首次 null → 降级重建(后续全量写入全局资产,防静默不部署)
1383
+ const newGlobalState = globalState ?? createGlobalUpdateState({});
1384
+ // 0. 生成全局框架约束 core.md(~/.config/opencode/openfeel/core.md)
1385
+ const coreInstructionsPath = getGlobalCoreMdPath();
1579
1386
  const coreContent = loadTemplate(lang, 'core-instructions');
1580
- writeWithMergeDetection(coreInstructionsPath, coreContent, '.opencode/instructions/core.md', state, created, updated, skipped, conflicts);
1581
- // 1. 生成 Agent 定义文件(从 template-loader 加载模板,按语言选择)
1387
+ {
1388
+ const action = deployGlobalAsset(coreInstructionsPath, coreContent, newGlobalState);
1389
+ pushAction(action, coreInstructionsPath, created, updated, skipped, appended);
1390
+ }
1391
+ // 1. 生成 Agent 定义文件到全局 agents 目录(绝对路径作 state key)
1582
1392
  for (const name of listAgentIds(lang)) {
1583
1393
  const content = loadAgentTemplate(lang, name);
1584
1394
  const filePath = join(agentsDir, `${name}.md`);
1585
- const relPath = `.opencode/agents/${name}.md`;
1586
- writeWithMergeDetection(filePath, content, relPath, state, created, updated, skipped, conflicts);
1395
+ const action = deployGlobalAsset(filePath, content, newGlobalState);
1396
+ pushAction(action, filePath, created, updated, skipped, appended);
1587
1397
  }
1588
- // 2. 生成 Skill 定义文件(每个 Skill 一个子目录,包含 SKILL.md)
1398
+ // 2. 生成 Skill 定义文件到全局 skills 目录(每个 Skill 一个子目录,包含 SKILL.md)
1589
1399
  for (const [name, content] of Object.entries(SKILL_DEFINITIONS)) {
1590
1400
  const skillSubDir = join(skillsDir, name);
1591
1401
  mkdirSync(skillSubDir, { recursive: true });
1592
1402
  const filePath = join(skillSubDir, 'SKILL.md');
1593
- const relPath = `.opencode/skills/${name}/SKILL.md`;
1594
- writeWithMergeDetection(filePath, content, relPath, state, created, updated, skipped, conflicts);
1403
+ const action = deployGlobalAsset(filePath, content, newGlobalState);
1404
+ pushAction(action, filePath, created, updated, skipped, appended);
1405
+ }
1406
+ // 3. 全局 opencode.jsonc:深度合并(保留用户字段),加全局锁 + 原子写
1407
+ const globalJsoncPath = getGlobalOpencodeJsoncPath();
1408
+ // REV-1805:read-merge-write 全部置于锁内,消除与 model-config 锁内读写的 TOCTOU 竞态
1409
+ const globalJsoncNew = withFileLock(globalLockPath('global-opencode-jsonc'), () => {
1410
+ const current = existsSync(globalJsoncPath) ? readFileSync(globalJsoncPath, 'utf-8') : '{}\n';
1411
+ let merged;
1412
+ // 合并可能因块注释 parse 失败抛异常(parseJsonc 边界):降级为「跳过合并、保留原文件」并告警
1413
+ try {
1414
+ merged = mergeGlobalOpencodeJsonc(current);
1415
+ }
1416
+ catch (err) {
1417
+ console.warn(`[update] 全局 opencode.jsonc 解析失败(可能含块注释),跳过合并保留原文件: ${err.message}`);
1418
+ merged = current;
1419
+ }
1420
+ atomicWriteFileSync(globalJsoncPath, merged);
1421
+ return merged;
1422
+ });
1423
+ updateFileHash(newGlobalState, globalJsoncPath, globalJsoncNew);
1424
+ // 4. 项目 opencode.jsonc:不存在则写最小 { $schema };已存在则保留(旧非法字段清理属 stage-39)
1425
+ const projectJsoncPath = resolve(projectPath, 'opencode.jsonc');
1426
+ if (!existsSync(projectJsoncPath)) {
1427
+ atomicWriteFileSync(projectJsoncPath, JSON.stringify(buildProjectOpencodeJsoncObj(), null, 2) + '\n');
1428
+ created.push('opencode.jsonc');
1429
+ }
1430
+ else {
1431
+ skipped.push('opencode.jsonc (use existing)');
1595
1432
  }
1596
- // 3. 更新 opencode.jsonc
1597
- const jsoncPath = resolve(projectPath, 'opencode.jsonc');
1598
- const newContent = buildUpdatedJsonc(projectPath);
1599
- const relJsoncPath = 'opencode.jsonc';
1600
- writeWithMergeDetection(jsoncPath, newContent, relJsoncPath, state, created, updated, skipped, conflicts);
1601
- // 重启提醒(仅在 opencode agent 配置更新时,且为交互模式)
1602
- if (updated.some(f => f.startsWith('.opencode/agents/')) && process.stdout.isTTY) {
1603
- const lang = getCliLang(projectPath);
1604
- console.log(lang === 'en'
1605
- ? 'opencode agent configuration updated. Please restart opencode to load the new configuration.'
1606
- : 'opencode agent 配置已更新,请重启 opencode 以加载新配置。');
1433
+ // 重启提醒(仅在 opencode 全局 agent 配置更新时,且为交互模式)
1434
+ if (updated.some((f) => f.startsWith(getGlobalAgentsDir())) && process.stdout.isTTY) {
1435
+ const reminderLang = getCliLang(projectPath);
1436
+ console.log(reminderLang === 'en'
1437
+ ? 'opencode global configuration updated. Please restart opencode to load the new configuration.'
1438
+ : 'opencode 全局配置已更新,请重启 opencode 以加载新配置。');
1607
1439
  }
1608
1440
  // ── 写入冲突标记文件 ──
1609
1441
  if (conflicts.length > 0) {
1610
1442
  const openfeelVersion = getOpenfeelVersion();
1611
- for (const relPath of conflicts) {
1612
- const absPath = resolve(projectPath, relPath);
1613
- const currentContent = existsSync(absPath)
1614
- ? readFileSync(absPath, 'utf-8')
1615
- : '';
1616
- // 获取 incoming 内容:从模板/构建产物中重新读取
1617
- // 注意:由于 writeWithMergeDetection 未实际写入,需单独获取 incoming 内容
1618
- const incomingContent = getIncomingContent(projectPath, relPath, lang);
1619
- writeConflictFile(projectPath, relPath, currentContent, incomingContent, openfeelVersion);
1443
+ for (const conflictPath of conflicts) {
1444
+ // 全局冲突(绝对路径)写 ~/.openfeel/update_conflicts/;项目冲突写项目 .openfeel/update_conflicts/
1445
+ if (isAbsolute(conflictPath)) {
1446
+ const currentContent = existsSync(conflictPath) ? readFileSync(conflictPath, 'utf-8') : '';
1447
+ const incomingContent = getIncomingContent(conflictPath, lang);
1448
+ const relFromGlobal = relative(getOpencodeGlobalDir(), conflictPath);
1449
+ // 全局冲突根目录 ~/.openfeel/update_conflicts/(与项目 .openfeel/update_conflicts/ 对称)
1450
+ const globalConflictsBase = resolve(dirname(getGlobalUpdateStatePath()), 'update_conflicts');
1451
+ writeConflictFile(projectPath, relFromGlobal, currentContent, incomingContent, openfeelVersion, globalConflictsBase);
1452
+ }
1453
+ else {
1454
+ const absPath = resolve(projectPath, conflictPath);
1455
+ const currentContent = existsSync(absPath) ? readFileSync(absPath, 'utf-8') : '';
1456
+ // 获取 incoming 内容:冲突未实际写入,需单独获取
1457
+ const incomingContent = getIncomingContent(conflictPath, lang);
1458
+ writeConflictFile(projectPath, conflictPath, currentContent, incomingContent, openfeelVersion);
1459
+ }
1620
1460
  }
1621
1461
  }
1622
- // ── REV-001:统一更新 update_state.json ──
1462
+ // ── REV-001:统一更新双 update_state.json ──
1623
1463
  // 规则:
1624
1464
  // 1. 无论有无冲突,所有 created/updated 文件的 hash 都必须更新到 state
1625
1465
  // 2. 有冲突时,额外标记冲突文件
1626
1466
  // 3. 首次 update(state === null)→ 创建新 state
1627
1467
  const newState = state ?? createUpdateState(projectPath, {});
1628
- // 更新所有非冲突文件的 hash(created 和 updated)
1629
- for (const relPath of [...created, ...updated]) {
1630
- const absPath = resolve(projectPath, relPath);
1631
- if (existsSync(absPath)) {
1632
- const content = readFileSync(absPath, 'utf-8');
1633
- updateFileHash(newState, relPath, content);
1468
+ // 更新非冲突文件 hash:项目资产(相对路径)→ 项目 state;全局资产(绝对路径)→ 全局 state
1469
+ // appended 追加后同样记录 clean + 新 hash(D38-1:追加即建区,下次走区内替换而非误判无记录)
1470
+ for (const p of [...created, ...updated, ...appended]) {
1471
+ if (isAbsolute(p)) {
1472
+ if (existsSync(p)) {
1473
+ updateFileHash(newGlobalState, p, readFileSync(p, 'utf-8'));
1474
+ }
1475
+ }
1476
+ else {
1477
+ const absPath = resolve(projectPath, p);
1478
+ if (existsSync(absPath)) {
1479
+ updateFileHash(newState, p, readFileSync(absPath, 'utf-8'));
1480
+ }
1634
1481
  }
1635
1482
  }
1636
- // 标记冲突文件
1637
- for (const relPath of conflicts) {
1638
- markFileConflict(newState, relPath);
1483
+ // 标记冲突文件(按绝对/相对路由到对应 state)
1484
+ for (const p of conflicts) {
1485
+ if (isAbsolute(p)) {
1486
+ markFileConflict(newGlobalState, p);
1487
+ }
1488
+ else {
1489
+ markFileConflict(newState, p);
1490
+ }
1639
1491
  }
1640
1492
  // 更新时间戳
1641
1493
  newState.last_update = new Date().toISOString();
1642
- // 持久化
1494
+ newGlobalState.last_update = new Date().toISOString();
1495
+ // 持久化双 state
1643
1496
  saveUpdateState(projectPath, newState);
1644
- return { created, updated, skipped, conflicts };
1497
+ saveGlobalUpdateState(newGlobalState);
1498
+ return { created, updated, skipped, conflicts, appended };
1645
1499
  }
1646
1500
  /**
1647
1501
  * 写入冲突标记文件到 .openfeel/update_conflicts/{relativePath}
@@ -1649,10 +1503,11 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
1649
1503
  *
1650
1504
  * REV-006:update_conflicts/ 目录已加入 .gitignore,不纳入版本管理。
1651
1505
  */
1652
- function writeConflictFile(projectPath, relativePath, currentContent, incomingContent, openfeelVersion) {
1653
- const conflictsBase = resolve(projectPath, '.openfeel', 'update_conflicts');
1654
- mkdirSync(conflictsBase, { recursive: true });
1655
- const conflictPath = resolve(conflictsBase, relativePath);
1506
+ function writeConflictFile(projectPath, relativePath, currentContent, incomingContent, openfeelVersion, conflictsBase) {
1507
+ // 全局冲突传全局 base(~/.openfeel),项目冲突默认项目 .openfeel/update_conflicts
1508
+ const base = conflictsBase ?? resolve(projectPath, '.openfeel', 'update_conflicts');
1509
+ mkdirSync(base, { recursive: true });
1510
+ const conflictPath = resolve(base, relativePath);
1656
1511
  mkdirSync(dirname(conflictPath), { recursive: true });
1657
1512
  const conflictContent = [
1658
1513
  `<<<<<<< CURRENT (用户修改版)`,
@@ -1661,39 +1516,37 @@ function writeConflictFile(projectPath, relativePath, currentContent, incomingCo
1661
1516
  incomingContent,
1662
1517
  `>>>>>>> INCOMING (openfeel v${openfeelVersion} 更新)`,
1663
1518
  ].join('\n');
1664
- writeFileSync(conflictPath, conflictContent, 'utf-8');
1519
+ atomicWriteFileSync(conflictPath, conflictContent);
1665
1520
  }
1666
1521
  /**
1667
- * 根据相对路径获取 incoming(openfeel 期望写入)内容
1668
- * 与 updateProject() 中生成内容的逻辑一一对应:
1669
- * - .opencode/instructions/core.md → loadTemplate(lang, 'core-instructions')
1670
- * - .opencode/agents/{name}.md → loadAgentTemplate(lang, name)
1671
- * - .opencode/skills/{name}/SKILL.md → SKILL_DEFINITIONS[name]
1672
- * - opencode.jsonc → buildUpdatedJsonc(projectPath)
1522
+ * 根据路径获取 incoming(openfeel 期望写入)内容
1523
+ * relativePath 现为全局绝对路径(框架资产)或项目相对路径(AGENTS.md / opencode.jsonc):
1524
+ * - getGlobalCoreMdPath() → loadTemplate(lang, 'core-instructions')
1525
+ * - getGlobalAgentsDir()/{name}.md → loadAgentTemplate(lang, name)
1526
+ * - getGlobalSkillsDir()/{name}/SKILL.md → SKILL_DEFINITIONS[name]
1527
+ * - opencode.jsonc → buildProjectOpencodeJsoncObj()
1673
1528
  * - AGENTS.md → loadTemplate(lang, 'agents-md')
1674
1529
  */
1675
- function getIncomingContent(projectPath, relativePath, lang) {
1676
- // instructions/core.md
1677
- if (relativePath === '.opencode/instructions/core.md') {
1530
+ function getIncomingContent(relativePath, lang) {
1531
+ // 全局框架约束 core.md
1532
+ if (relativePath === getGlobalCoreMdPath()) {
1678
1533
  return loadTemplate(lang, 'core-instructions');
1679
1534
  }
1680
- // Agent 定义文件
1681
- const agentMatch = relativePath.match(/^\.opencode\/agents\/(.+)\.md$/);
1682
- if (agentMatch) {
1683
- const name = agentMatch[1];
1535
+ // 全局 agent:位于 getGlobalAgentsDir() 下的 {name}.md
1536
+ if (relativePath.startsWith(getGlobalAgentsDir())) {
1537
+ const name = basename(relativePath, '.md');
1684
1538
  return loadAgentTemplate(lang, name);
1685
1539
  }
1686
- // Skill 定义文件
1687
- const skillMatch = relativePath.match(/^\.opencode\/skills\/(.+)\/SKILL\.md$/);
1688
- if (skillMatch) {
1689
- const name = skillMatch[1];
1540
+ // 全局 skill:位于 getGlobalSkillsDir() 下的 {name}/SKILL.md(跨平台 basename 匹配)
1541
+ if (relativePath.startsWith(getGlobalSkillsDir()) && basename(relativePath) === 'SKILL.md') {
1542
+ const name = basename(dirname(relativePath));
1690
1543
  return SKILL_DEFINITIONS[name] ?? '';
1691
1544
  }
1692
- // opencode.jsonc
1545
+ // 项目 opencode.jsonc
1693
1546
  if (relativePath === 'opencode.jsonc') {
1694
- return buildUpdatedJsonc(projectPath);
1547
+ return JSON.stringify(buildProjectOpencodeJsoncObj(), null, 2) + '\n';
1695
1548
  }
1696
- // AGENTS.md
1549
+ // 项目 AGENTS.md
1697
1550
  if (relativePath === 'AGENTS.md') {
1698
1551
  return loadTemplate(lang, 'agents-md');
1699
1552
  }