openfeel 1.0.9 → 1.1.1

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 (147) 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 +28 -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/init.d.ts +2 -1
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +7 -2
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/migrate.d.ts +10 -0
  19. package/dist/commands/migrate.d.ts.map +1 -0
  20. package/dist/commands/migrate.js +106 -0
  21. package/dist/commands/migrate.js.map +1 -0
  22. package/dist/commands/model.d.ts +10 -0
  23. package/dist/commands/model.d.ts.map +1 -0
  24. package/dist/commands/model.js +135 -0
  25. package/dist/commands/model.js.map +1 -0
  26. package/dist/commands/setup.d.ts +7 -0
  27. package/dist/commands/setup.d.ts.map +1 -0
  28. package/dist/commands/setup.js +31 -0
  29. package/dist/commands/setup.js.map +1 -0
  30. package/dist/commands/stage.d.ts.map +1 -1
  31. package/dist/commands/stage.js +50 -29
  32. package/dist/commands/stage.js.map +1 -1
  33. package/dist/commands/update.d.ts.map +1 -1
  34. package/dist/commands/update.js +13 -10
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/commands/view.d.ts +1 -1
  37. package/dist/commands/view.d.ts.map +1 -1
  38. package/dist/commands/view.js +2 -1
  39. package/dist/commands/view.js.map +1 -1
  40. package/dist/core/archive/merge.d.ts.map +1 -1
  41. package/dist/core/archive/merge.js +5 -4
  42. package/dist/core/archive/merge.js.map +1 -1
  43. package/dist/core/artifact-graph/resolver.js +2 -2
  44. package/dist/core/artifact-graph/resolver.js.map +1 -1
  45. package/dist/core/config.d.ts +8 -8
  46. package/dist/core/config.d.ts.map +1 -1
  47. package/dist/core/config.js +22 -17
  48. package/dist/core/config.js.map +1 -1
  49. package/dist/core/flow-manager.d.ts +25 -4
  50. package/dist/core/flow-manager.d.ts.map +1 -1
  51. package/dist/core/flow-manager.js +144 -79
  52. package/dist/core/flow-manager.js.map +1 -1
  53. package/dist/core/fs/atomic-write.d.ts +31 -0
  54. package/dist/core/fs/atomic-write.d.ts.map +1 -0
  55. package/dist/core/fs/atomic-write.js +80 -0
  56. package/dist/core/fs/atomic-write.js.map +1 -0
  57. package/dist/core/fs/file-lock.d.ts +34 -0
  58. package/dist/core/fs/file-lock.d.ts.map +1 -0
  59. package/dist/core/fs/file-lock.js +142 -0
  60. package/dist/core/fs/file-lock.js.map +1 -0
  61. package/dist/core/fs/sequence.d.ts +42 -0
  62. package/dist/core/fs/sequence.d.ts.map +1 -0
  63. package/dist/core/fs/sequence.js +69 -0
  64. package/dist/core/fs/sequence.js.map +1 -0
  65. package/dist/core/global-paths.d.ts +27 -0
  66. package/dist/core/global-paths.d.ts.map +1 -0
  67. package/dist/core/global-paths.js +61 -0
  68. package/dist/core/global-paths.js.map +1 -0
  69. package/dist/core/i18n-data/en.d.ts +3 -0
  70. package/dist/core/i18n-data/en.d.ts.map +1 -1
  71. package/dist/core/i18n-data/en.js +86 -1
  72. package/dist/core/i18n-data/en.js.map +1 -1
  73. package/dist/core/i18n-data/zh-CN.d.ts +3 -0
  74. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  75. package/dist/core/i18n-data/zh-CN.js +87 -1
  76. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  77. package/dist/core/i18n.js +4 -4
  78. package/dist/core/i18n.js.map +1 -1
  79. package/dist/core/init.d.ts +10 -15
  80. package/dist/core/init.d.ts.map +1 -1
  81. package/dist/core/init.js +65 -134
  82. package/dist/core/init.js.map +1 -1
  83. package/dist/core/managed-region.d.ts +54 -0
  84. package/dist/core/managed-region.d.ts.map +1 -0
  85. package/dist/core/managed-region.js +171 -0
  86. package/dist/core/managed-region.js.map +1 -0
  87. package/dist/core/metrics.d.ts.map +1 -1
  88. package/dist/core/metrics.js +6 -3
  89. package/dist/core/metrics.js.map +1 -1
  90. package/dist/core/migrate.d.ts +115 -0
  91. package/dist/core/migrate.d.ts.map +1 -0
  92. package/dist/core/migrate.js +558 -0
  93. package/dist/core/migrate.js.map +1 -0
  94. package/dist/core/model-config.d.ts +62 -0
  95. package/dist/core/model-config.d.ts.map +1 -0
  96. package/dist/core/model-config.js +336 -0
  97. package/dist/core/model-config.js.map +1 -0
  98. package/dist/core/opencode-config.d.ts +17 -0
  99. package/dist/core/opencode-config.d.ts.map +1 -0
  100. package/dist/core/opencode-config.js +135 -0
  101. package/dist/core/opencode-config.js.map +1 -0
  102. package/dist/core/plan/roadmap.d.ts.map +1 -1
  103. package/dist/core/plan/roadmap.js +3 -2
  104. package/dist/core/plan/roadmap.js.map +1 -1
  105. package/dist/core/plan/scheme.d.ts.map +1 -1
  106. package/dist/core/plan/scheme.js +35 -36
  107. package/dist/core/plan/scheme.js.map +1 -1
  108. package/dist/core/plan/stage.d.ts.map +1 -1
  109. package/dist/core/plan/stage.js +4 -3
  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/setup.d.ts +10 -0
  116. package/dist/core/setup.d.ts.map +1 -0
  117. package/dist/core/setup.js +75 -0
  118. package/dist/core/setup.js.map +1 -0
  119. package/dist/core/template-loader.d.ts +4 -5
  120. package/dist/core/template-loader.d.ts.map +1 -1
  121. package/dist/core/template-loader.js +3974 -3274
  122. package/dist/core/template-loader.js.map +1 -1
  123. package/dist/core/templates.d.ts +2 -2
  124. package/dist/core/templates.d.ts.map +1 -1
  125. package/dist/core/templates.js +2 -2
  126. package/dist/core/templates.js.map +1 -1
  127. package/dist/core/update-infos.d.ts +32 -0
  128. package/dist/core/update-infos.d.ts.map +1 -0
  129. package/dist/core/update-infos.js +153 -0
  130. package/dist/core/update-infos.js.map +1 -0
  131. package/dist/core/update-state.d.ts +18 -0
  132. package/dist/core/update-state.d.ts.map +1 -1
  133. package/dist/core/update-state.js +66 -3
  134. package/dist/core/update-state.js.map +1 -1
  135. package/dist/core/update.d.ts +12 -6
  136. package/dist/core/update.d.ts.map +1 -1
  137. package/dist/core/update.js +400 -550
  138. package/dist/core/update.js.map +1 -1
  139. package/dist/core/view/entry.js +4 -4
  140. package/dist/core/view/entry.js.map +1 -1
  141. package/dist/core/workspace/identity.d.ts.map +1 -1
  142. package/dist/core/workspace/identity.js +13 -10
  143. package/dist/core/workspace/identity.js.map +1 -1
  144. package/dist/core/workspace/knowledge.d.ts.map +1 -1
  145. package/dist/core/workspace/knowledge.js +24 -14
  146. package/dist/core/workspace/knowledge.js.map +1 -1
  147. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  * - advancePhase() 增加 to 参数的 PipelinePhaseSchema 校验
10
10
  * - 新增 repair() 方法,自动检测并修复 flow.json 常见问题
11
11
  */
12
- import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, renameSync, readdirSync, unlinkSync } from 'node:fs';
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, unlinkSync } from 'node:fs';
13
13
  import { resolve } from 'node:path';
14
14
  import { execSync } from 'node:child_process';
15
15
  import { parse as parseYaml } from 'yaml';
@@ -17,6 +17,26 @@ import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSc
17
17
  import { PublicLogger, formatDate } from './public-logger.js';
18
18
  import { t, getCliLang } from './i18n.js';
19
19
  import { findStageStatusPath } from './plan/path.js';
20
+ import { atomicWriteFileSync } from './fs/atomic-write.js';
21
+ import { withFileLock, projectLockPath } from './fs/file-lock.js';
22
+ /** flow.json 乐观并发冲突:磁盘 revision 与本次加载时不一致(存在并发写入) */
23
+ export class FlowConcurrentModificationError extends Error {
24
+ /** 本次实例加载时的 revision */
25
+ expectedRevision;
26
+ /** 锁内读到的磁盘当前 revision */
27
+ actualRevision;
28
+ constructor(expected, actual) {
29
+ super(`flow.json 并发冲突:期望 revision=${expected},磁盘为 ${actual}(已被其它进程修改)`);
30
+ this.name = 'FlowConcurrentModificationError';
31
+ this.expectedRevision = expected;
32
+ this.actualRevision = actual;
33
+ }
34
+ }
35
+ /** 判断错误是否为 flow.json 并发冲突(供命令层统一识别) */
36
+ export function isFlowConcurrentError(err) {
37
+ return err instanceof FlowConcurrentModificationError
38
+ || (typeof err === 'object' && err !== null && err.name === 'FlowConcurrentModificationError');
39
+ }
20
40
  // ── 默认值 ──
21
41
  /** 创建默认的 Checkpoints */
22
42
  function defaultCheckpoints() {
@@ -35,6 +55,7 @@ function defaultFlowData() {
35
55
  version: '1.0',
36
56
  project: 'OpenFeel',
37
57
  updated: new Date().toISOString(),
58
+ revision: 0,
38
59
  },
39
60
  pipeline: {
40
61
  phase: 'active',
@@ -46,10 +67,17 @@ function defaultFlowData() {
46
67
  log: [],
47
68
  };
48
69
  }
70
+ /** 从 FlowData 提取 revision(缺失/非整数 → 0,兼容存量文件) */
71
+ function extractRevision(data) {
72
+ const rev = data?.meta?.revision;
73
+ return typeof rev === 'number' && Number.isInteger(rev) ? rev : 0;
74
+ }
49
75
  // ── 核心类 ──
50
76
  export class FlowManager {
51
77
  projectPath;
52
78
  data;
79
+ /** 本次实例加载(或最近一次成功 save)时的 flow.json revision,用于乐观并发校验 */
80
+ loadedRevision = 0;
53
81
  filePath;
54
82
  /** 从 pipeline.yaml 加载的流水线配置(含后备默认值) */
55
83
  pipelineConfig = null;
@@ -68,11 +96,14 @@ export class FlowManager {
68
96
  load() {
69
97
  if (!existsSync(this.filePath)) {
70
98
  this.data = null;
99
+ this.loadedRevision = 0;
71
100
  return;
72
101
  }
73
102
  try {
74
103
  const raw = readFileSync(this.filePath, 'utf-8');
75
104
  this.data = JSON.parse(raw);
105
+ // 记录加载基线 revision(缺失/非法视为 0,兼容存量文件)
106
+ this.loadedRevision = extractRevision(this.data);
76
107
  // 从 stages 的键名恢复 op 的 id 字段(运行时便利字段,磁盘不存储)
77
108
  if (this.data && this.data.stages) {
78
109
  for (const [, stage] of Object.entries(this.data.stages)) {
@@ -87,9 +118,10 @@ export class FlowManager {
87
118
  }
88
119
  catch {
89
120
  this.data = null;
121
+ this.loadedRevision = 0;
90
122
  }
91
123
  }
92
- /** 保存 flow.json(自动更新 meta.updated,序列化时去除 op 中的 id 字段以与键名保持一致) */
124
+ /** 保存 flow.json(自动更新 meta.updated;锁内做乐观并发校验、备份旧版本并原子写) */
93
125
  save() {
94
126
  if (!this.data) {
95
127
  return;
@@ -102,46 +134,42 @@ export class FlowManager {
102
134
  delete op.id;
103
135
  }
104
136
  }
105
- const content = JSON.stringify(serializable, null, 2) + '\n';
106
- const tmpPath = this.filePath + '.tmp';
107
- const bakPath = this.filePath + '.bak';
108
- // 备份旧文件(写入前)
109
- if (existsSync(this.filePath)) {
110
- try {
111
- copyFileSync(this.filePath, bakPath);
112
- }
113
- catch {
114
- // 备份失败不阻塞写入
115
- }
116
- }
137
+ const lockPath = projectLockPath(this.projectPath, 'flow');
117
138
  try {
118
- // 写入临时文件
119
- writeFileSync(tmpPath, content, 'utf-8');
120
- // 校验临时文件 JSON 合法性
121
- try {
122
- const tmpContent = readFileSync(tmpPath, 'utf-8');
123
- JSON.parse(tmpContent);
124
- }
125
- catch (parseErr) {
126
- // 清理失败的临时文件
127
- try {
128
- writeFileSync(tmpPath, '', 'utf-8');
129
- }
130
- catch {
131
- // 清理失败也不阻塞
139
+ withFileLock(lockPath, () => {
140
+ // 乐观并发校验:锁内读磁盘 revision,与加载时比对;不一致说明有并发写,拒绝覆盖
141
+ const diskRevision = this.readDiskRevision();
142
+ if (diskRevision !== this.loadedRevision) {
143
+ throw new FlowConcurrentModificationError(this.loadedRevision, diskRevision);
132
144
  }
133
- throw new Error(`flow.json 写入校验失败: ${parseErr.message}。备份已保留在 ${bakPath}`);
134
- }
135
- // 校验通过,rename 临时文件到正式文件
136
- renameSync(tmpPath, this.filePath);
137
- // 同步备份
138
- writeFileSync(this.filePath + '.bak', content, 'utf-8');
145
+ const nextRevision = this.loadedRevision + 1;
146
+ serializable.meta.revision = nextRevision;
147
+ // S5:写前复制旧文件为 .bak;写成功后不覆盖 .bak,使其始终保留上一版本
148
+ const content = JSON.stringify(serializable, null, 2) + '\n';
149
+ atomicWriteFileSync(this.filePath, content, { backup: true });
150
+ // 同步内存与加载基线,避免同实例连续 save 误判冲突
151
+ this.data.meta.revision = nextRevision;
152
+ this.loadedRevision = nextRevision;
153
+ });
139
154
  }
140
155
  catch (err) {
141
156
  console.error(`[ERROR] flow.json 保存失败: ${err instanceof Error ? err.message : err}`);
142
157
  throw err;
143
158
  }
144
159
  }
160
+ /** 读取磁盘当前 revision(不存在/损坏/缺失 → 0,兼容存量文件) */
161
+ readDiskRevision() {
162
+ try {
163
+ const raw = readFileSync(this.filePath, 'utf-8');
164
+ const parsed = JSON.parse(raw);
165
+ const rev = parsed?.meta?.revision;
166
+ return typeof rev === 'number' && Number.isInteger(rev) ? rev : 0;
167
+ }
168
+ catch {
169
+ // 文件不存在或损坏:视为 0(首次写入 / 由 repair 兜底)
170
+ return 0;
171
+ }
172
+ }
145
173
  /** 数据是否已加载 */
146
174
  isLoaded() {
147
175
  return this.data !== null;
@@ -176,7 +204,8 @@ export class FlowManager {
176
204
  const timestamp = this.formatCheckpointTimestamp(new Date());
177
205
  const filename = `${stageId}-${timestamp}-${phase}.json`;
178
206
  const filePath = resolve(dir, filename);
179
- writeFileSync(filePath, JSON.stringify(serializable, null, 2) + '\n', 'utf-8');
207
+ // REV-003:快照唯一文件名 + best-effort,原子写防半写;不接全局锁(避免长序列化纳入临界区)
208
+ atomicWriteFileSync(filePath, JSON.stringify(serializable, null, 2) + '\n');
180
209
  // 清理超限的最旧快照
181
210
  this.cleanupCheckpoints(stageId);
182
211
  }
@@ -223,16 +252,26 @@ export class FlowManager {
223
252
  const content = readFileSync(filePath, 'utf-8');
224
253
  // 校验快照 JSON 合法性,非法内容拒绝恢复
225
254
  JSON.parse(content);
226
- // 备份当前 flow.json(失败不阻塞恢复)
227
- if (existsSync(this.filePath)) {
228
- try {
229
- copyFileSync(this.filePath, this.filePath + '.bak');
230
- }
231
- catch {
232
- // 备份失败忽略
255
+ const lockPath = projectLockPath(this.projectPath, 'flow');
256
+ let conflict = false;
257
+ withFileLock(lockPath, () => {
258
+ // 乐观并发校验:磁盘被并发修改时拒绝恢复,防止覆盖他人写入
259
+ const diskRevision = this.readDiskRevision();
260
+ if (diskRevision !== this.loadedRevision) {
261
+ conflict = true;
262
+ return;
233
263
  }
264
+ // 快照 revision 重定基为 diskRevision+1,保证单调递增
265
+ const restored = JSON.parse(content);
266
+ restored.meta.revision = diskRevision + 1;
267
+ const restoredContent = JSON.stringify(restored, null, 2) + '\n';
268
+ // REV-002:与 save() 同锁、同 S5 语义(写前复制旧文件为 .bak,写后不覆盖)
269
+ atomicWriteFileSync(this.filePath, restoredContent, { backup: true });
270
+ });
271
+ if (conflict) {
272
+ console.warn('[WARN] flow.json 已被其它进程修改,拒绝从快照恢复(请重新加载后重试)');
273
+ return false;
234
274
  }
235
- writeFileSync(this.filePath, content, 'utf-8');
236
275
  this.load();
237
276
  return true;
238
277
  }
@@ -1176,22 +1215,22 @@ export class FlowManager {
1176
1215
  }
1177
1216
  return result;
1178
1217
  }
1179
- /** 将 PipelinePhase 映射为负责 Agent 标识 */
1218
+ /** 将 PipelinePhase 映射为负责 Agent 标识(返回新名,供新写入使用) */
1180
1219
  mapPhaseToAgent(phase) {
1181
1220
  const prefix = phase.split('_')[0];
1182
1221
  switch (prefix) {
1183
1222
  case 'plan':
1184
- return 'planner';
1223
+ return 'openfeel-planner';
1185
1224
  case 'scheme':
1186
- return 'schemer';
1225
+ return 'openfeel-schemer';
1187
1226
  case 'exec':
1188
- return 'executor';
1227
+ return 'openfeel-executor';
1189
1228
  case 'review':
1190
- return 'reviewer';
1229
+ return 'openfeel-reviewer';
1191
1230
  case 'test':
1192
- return 'feel-tester';
1231
+ return 'openfeel-feel-tester';
1193
1232
  case 'archiving':
1194
- return 'archiver';
1233
+ return 'openfeel-archiver';
1195
1234
  case 'done':
1196
1235
  return 'none';
1197
1236
  default:
@@ -1221,7 +1260,7 @@ export class FlowManager {
1221
1260
  this.data.pipeline.retry = 0;
1222
1261
  this.appendLog({
1223
1262
  time: '',
1224
- agent: 'executor',
1263
+ agent: 'openfeel-executor',
1225
1264
  action: 'attempt_pass',
1226
1265
  detail: { opId, attempts: op.attempts },
1227
1266
  });
@@ -1239,7 +1278,7 @@ export class FlowManager {
1239
1278
  this.data.pipeline.retry += 1;
1240
1279
  this.appendLog({
1241
1280
  time: '',
1242
- agent: 'executor',
1281
+ agent: 'openfeel-executor',
1243
1282
  action: 'attempt_fail_retry',
1244
1283
  detail: { opId, attempts: op.attempts, maxAttempts: op.max_attempts },
1245
1284
  });
@@ -1256,7 +1295,7 @@ export class FlowManager {
1256
1295
  this.data.pipeline.retry += 1;
1257
1296
  this.appendLog({
1258
1297
  time: '',
1259
- agent: 'executor',
1298
+ agent: 'openfeel-executor',
1260
1299
  action: 'attempt_fail_exhausted',
1261
1300
  detail: { opId, attempts: op.attempts, maxAttempts: op.max_attempts },
1262
1301
  });
@@ -1301,7 +1340,7 @@ export class FlowManager {
1301
1340
  }
1302
1341
  /**
1303
1342
  * 添加自动修复审查条目
1304
- * 当 Reviewer 认为问题可直接修复时调用,REV 条目状态直接设为 resolved,
1343
+ * 当 openfeel-reviewer 认为问题可直接修复时调用,REV 条目状态直接设为 resolved,
1305
1344
  * pipeline.phase 跳过 review_failed→scheme_pending,直接推进到 exec_running。
1306
1345
  * @param item 审查条目(canAutoFix 自动设为 true)
1307
1346
  * @param opId 关联的操作 ID
@@ -1660,6 +1699,12 @@ export class FlowManager {
1660
1699
  changes.push('已补全缺失的 meta.updated');
1661
1700
  modified = true;
1662
1701
  }
1702
+ // (乐观并发修订)补全缺失的 meta.revision(存量兼容,视为 0)
1703
+ if (typeof flowData.meta.revision !== 'number') {
1704
+ flowData.meta.revision = 0;
1705
+ changes.push('已补全缺失的 meta.revision(存量兼容,视为 0)');
1706
+ modified = true;
1707
+ }
1663
1708
  }
1664
1709
  // 修复缺失的 pipeline 字段
1665
1710
  if (!flowData.pipeline) {
@@ -1754,17 +1799,11 @@ export class FlowManager {
1754
1799
  }
1755
1800
  // 写入修复后的数据(recovered 场景下磁盘文件仍损坏,即使无字段修改也必须写回恢复内容)
1756
1801
  if ((modified || recovered) && !dryRun) {
1757
- // 备份当前文件(recovered 场景下 .bak 已是有效恢复来源,避免被损坏文件覆盖)
1758
- if (!recovered) {
1759
- try {
1760
- copyFileSync(this.filePath, this.filePath + '.bak');
1761
- }
1762
- catch {
1763
- // 备份失败不阻塞
1764
- }
1765
- }
1766
- // 使用安全写入
1767
- const tmpPath = this.filePath + '.tmp';
1802
+ // (乐观并发修订)恢复/修复亦视为一次写入:先在源对象上递增 revision,
1803
+ // 再序列化深拷贝,确保 revision 随内容一起落盘(避免陈旧实例后续 save 误判冲突)
1804
+ const meta = flowData.meta ?? defaults.meta;
1805
+ flowData.meta = meta;
1806
+ meta.revision = (typeof meta.revision === 'number' ? meta.revision : 0) + 1;
1768
1807
  const serializable = JSON.parse(JSON.stringify(flowData));
1769
1808
  // 去除 op 中的 id 字段
1770
1809
  if (serializable.stages && typeof serializable.stages === 'object') {
@@ -1779,23 +1818,19 @@ export class FlowManager {
1779
1818
  }
1780
1819
  }
1781
1820
  const content = JSON.stringify(serializable, null, 2) + '\n';
1782
- writeFileSync(tmpPath, content, 'utf-8');
1783
- // 校验临时文件
1821
+ const lockPath = projectLockPath(this.projectPath, 'flow');
1784
1822
  try {
1785
- const tmpContent = readFileSync(tmpPath, 'utf-8');
1786
- JSON.parse(tmpContent);
1823
+ withFileLock(lockPath, () => {
1824
+ // recovered 场景 .bak 已是有效恢复来源,避免被损坏文件覆盖(保持既有语义)
1825
+ atomicWriteFileSync(this.filePath, content, { backup: !recovered });
1826
+ });
1787
1827
  }
1788
- catch (parseErr) {
1789
- try {
1790
- writeFileSync(tmpPath, '', 'utf-8');
1791
- }
1792
- catch {
1793
- // 清理失败不阻塞
1794
- }
1795
- changes.push(`修复后写入校验失败: ${parseErr.message}`);
1828
+ catch (err) {
1829
+ changes.push(`修复后写入失败: ${err.message}`);
1796
1830
  return { fixed: false, changes, recovered };
1797
1831
  }
1798
- renameSync(tmpPath, this.filePath);
1832
+ // 写盘成功后重新加载,同步 this.data 与 this.loadedRevision 基线
1833
+ this.load();
1799
1834
  }
1800
1835
  // changes 在无问题时保持空数组,CLI 层通过 changes.length === 0 判断"没问题"
1801
1836
  return { fixed: modified || recovered, changes, recovered };
@@ -2268,7 +2303,8 @@ export class FlowManager {
2268
2303
  return; // 已存在则不覆盖
2269
2304
  }
2270
2305
  const data = defaultFlowData();
2271
- writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
2306
+ // 首次创建(existsSync 守卫)、无并发读者,故不加锁,仅原子写;默认 revision:0
2307
+ atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
2272
2308
  }
2273
2309
  /**
2274
2310
  * 内嵌数据(仅供测试使用)
@@ -2276,6 +2312,8 @@ export class FlowManager {
2276
2312
  */
2277
2313
  setData(data) {
2278
2314
  this.data = data;
2315
+ // 测试专用:注入数据视为「当前磁盘状态」,同步并发校验基线,避免后续 save 误判冲突
2316
+ this.loadedRevision = this.readDiskRevision();
2279
2317
  }
2280
2318
  // ═══ 流水线配置加载 ═══
2281
2319
  /**
@@ -2348,6 +2386,33 @@ export class FlowManager {
2348
2386
  }
2349
2387
  }
2350
2388
  // ── 辅助函数 ──
2389
+ /** 旧 agent 名 → 新名映射(读取兼容用;skill 名不在此列) */
2390
+ const LEGACY_AGENT_NAME_MAP = {
2391
+ planner: 'openfeel-planner',
2392
+ schemer: 'openfeel-schemer',
2393
+ executor: 'openfeel-executor',
2394
+ reviewer: 'openfeel-reviewer',
2395
+ 'feel-tester': 'openfeel-feel-tester',
2396
+ utility: 'openfeel-utility',
2397
+ vision: 'openfeel-vision',
2398
+ archiver: 'openfeel-archiver',
2399
+ };
2400
+ /**
2401
+ * 归一化 agent 名:旧名 → 新名(读取兼容 P5)。
2402
+ * 内部 toLowerCase() 归一大小写不一致(REV-304)。
2403
+ * 幂等:已是 openfeel-* 原样返回;feel / none / unknown / 非 agent 值(如 flow-manager)原样保留。
2404
+ */
2405
+ export function normalizeAgentName(name) {
2406
+ if (!name) {
2407
+ return name;
2408
+ }
2409
+ const key = name.trim().toLowerCase();
2410
+ // 已是新名(openfeel- 前缀)→ 原样返回,避免二次前缀化
2411
+ if (key.startsWith('openfeel-')) {
2412
+ return key;
2413
+ }
2414
+ return LEGACY_AGENT_NAME_MAP[key] ?? key;
2415
+ }
2351
2416
  /**
2352
2417
  * 将 PipelinePhase 映射为 stage 状态
2353
2418
  * @param phase 流水线阶段