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