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
@@ -9,13 +9,34 @@
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';
16
16
  import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, } from './pipeline-schema.js';
17
17
  import { PublicLogger, formatDate } from './public-logger.js';
18
18
  import { t, getCliLang } from './i18n.js';
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
+ }
19
40
  // ── 默认值 ──
20
41
  /** 创建默认的 Checkpoints */
21
42
  function defaultCheckpoints() {
@@ -34,6 +55,7 @@ function defaultFlowData() {
34
55
  version: '1.0',
35
56
  project: 'OpenFeel',
36
57
  updated: new Date().toISOString(),
58
+ revision: 0,
37
59
  },
38
60
  pipeline: {
39
61
  phase: 'active',
@@ -45,10 +67,17 @@ function defaultFlowData() {
45
67
  log: [],
46
68
  };
47
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
+ }
48
75
  // ── 核心类 ──
49
76
  export class FlowManager {
50
77
  projectPath;
51
78
  data;
79
+ /** 本次实例加载(或最近一次成功 save)时的 flow.json revision,用于乐观并发校验 */
80
+ loadedRevision = 0;
52
81
  filePath;
53
82
  /** 从 pipeline.yaml 加载的流水线配置(含后备默认值) */
54
83
  pipelineConfig = null;
@@ -67,11 +96,14 @@ export class FlowManager {
67
96
  load() {
68
97
  if (!existsSync(this.filePath)) {
69
98
  this.data = null;
99
+ this.loadedRevision = 0;
70
100
  return;
71
101
  }
72
102
  try {
73
103
  const raw = readFileSync(this.filePath, 'utf-8');
74
104
  this.data = JSON.parse(raw);
105
+ // 记录加载基线 revision(缺失/非法视为 0,兼容存量文件)
106
+ this.loadedRevision = extractRevision(this.data);
75
107
  // 从 stages 的键名恢复 op 的 id 字段(运行时便利字段,磁盘不存储)
76
108
  if (this.data && this.data.stages) {
77
109
  for (const [, stage] of Object.entries(this.data.stages)) {
@@ -86,9 +118,10 @@ export class FlowManager {
86
118
  }
87
119
  catch {
88
120
  this.data = null;
121
+ this.loadedRevision = 0;
89
122
  }
90
123
  }
91
- /** 保存 flow.json(自动更新 meta.updated,序列化时去除 op 中的 id 字段以与键名保持一致) */
124
+ /** 保存 flow.json(自动更新 meta.updated;锁内做乐观并发校验、备份旧版本并原子写) */
92
125
  save() {
93
126
  if (!this.data) {
94
127
  return;
@@ -101,46 +134,42 @@ export class FlowManager {
101
134
  delete op.id;
102
135
  }
103
136
  }
104
- const content = JSON.stringify(serializable, null, 2) + '\n';
105
- const tmpPath = this.filePath + '.tmp';
106
- const bakPath = this.filePath + '.bak';
107
- // 备份旧文件(写入前)
108
- if (existsSync(this.filePath)) {
109
- try {
110
- copyFileSync(this.filePath, bakPath);
111
- }
112
- catch {
113
- // 备份失败不阻塞写入
114
- }
115
- }
137
+ const lockPath = projectLockPath(this.projectPath, 'flow');
116
138
  try {
117
- // 写入临时文件
118
- writeFileSync(tmpPath, content, 'utf-8');
119
- // 校验临时文件 JSON 合法性
120
- try {
121
- const tmpContent = readFileSync(tmpPath, 'utf-8');
122
- JSON.parse(tmpContent);
123
- }
124
- catch (parseErr) {
125
- // 清理失败的临时文件
126
- try {
127
- writeFileSync(tmpPath, '', 'utf-8');
139
+ withFileLock(lockPath, () => {
140
+ // 乐观并发校验:锁内读磁盘 revision,与加载时比对;不一致说明有并发写,拒绝覆盖
141
+ const diskRevision = this.readDiskRevision();
142
+ if (diskRevision !== this.loadedRevision) {
143
+ throw new FlowConcurrentModificationError(this.loadedRevision, diskRevision);
128
144
  }
129
- catch {
130
- // 清理失败也不阻塞
131
- }
132
- throw new Error(`flow.json 写入校验失败: ${parseErr.message}。备份已保留在 ${bakPath}`);
133
- }
134
- // 校验通过,rename 临时文件到正式文件
135
- renameSync(tmpPath, this.filePath);
136
- // 同步备份
137
- 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
+ });
138
154
  }
139
155
  catch (err) {
140
156
  console.error(`[ERROR] flow.json 保存失败: ${err instanceof Error ? err.message : err}`);
141
157
  throw err;
142
158
  }
143
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
+ }
144
173
  /** 数据是否已加载 */
145
174
  isLoaded() {
146
175
  return this.data !== null;
@@ -175,7 +204,8 @@ export class FlowManager {
175
204
  const timestamp = this.formatCheckpointTimestamp(new Date());
176
205
  const filename = `${stageId}-${timestamp}-${phase}.json`;
177
206
  const filePath = resolve(dir, filename);
178
- writeFileSync(filePath, JSON.stringify(serializable, null, 2) + '\n', 'utf-8');
207
+ // REV-003:快照唯一文件名 + best-effort,原子写防半写;不接全局锁(避免长序列化纳入临界区)
208
+ atomicWriteFileSync(filePath, JSON.stringify(serializable, null, 2) + '\n');
179
209
  // 清理超限的最旧快照
180
210
  this.cleanupCheckpoints(stageId);
181
211
  }
@@ -222,16 +252,26 @@ export class FlowManager {
222
252
  const content = readFileSync(filePath, 'utf-8');
223
253
  // 校验快照 JSON 合法性,非法内容拒绝恢复
224
254
  JSON.parse(content);
225
- // 备份当前 flow.json(失败不阻塞恢复)
226
- if (existsSync(this.filePath)) {
227
- try {
228
- copyFileSync(this.filePath, this.filePath + '.bak');
229
- }
230
- catch {
231
- // 备份失败忽略
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;
232
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;
233
274
  }
234
- writeFileSync(this.filePath, content, 'utf-8');
235
275
  this.load();
236
276
  return true;
237
277
  }
@@ -1104,17 +1144,9 @@ export class FlowManager {
1104
1144
  }
1105
1145
  return { configDefaults, statusOverrides, effective };
1106
1146
  }
1107
- /** 查找 status.md 的路径(先查 stages 目录,再回退 plan 目录) */
1147
+ /** 查找 status.md 的路径(三级回退:plan/{series}/ 精确 → plan 递归 → stages 兜底) */
1108
1148
  findStatusPath(stageId) {
1109
- const stagesDir = resolve(this.projectPath, '.openfeel', 'stages', stageId, 'status.md');
1110
- if (existsSync(stagesDir)) {
1111
- return stagesDir;
1112
- }
1113
- const planDir = resolve(this.projectPath, '.openfeel', 'plan', stageId, 'status.md');
1114
- if (existsSync(planDir)) {
1115
- return planDir;
1116
- }
1117
- return null;
1149
+ return findStageStatusPath(this.projectPath, stageId);
1118
1150
  }
1119
1151
  /** 从 status.md 的状态记录表提取最近 N 条变更 */
1120
1152
  extractRecentChanges(maxChanges) {
@@ -1183,22 +1215,22 @@ export class FlowManager {
1183
1215
  }
1184
1216
  return result;
1185
1217
  }
1186
- /** 将 PipelinePhase 映射为负责 Agent 标识 */
1218
+ /** 将 PipelinePhase 映射为负责 Agent 标识(返回新名,供新写入使用) */
1187
1219
  mapPhaseToAgent(phase) {
1188
1220
  const prefix = phase.split('_')[0];
1189
1221
  switch (prefix) {
1190
1222
  case 'plan':
1191
- return 'planner';
1223
+ return 'openfeel-planner';
1192
1224
  case 'scheme':
1193
- return 'schemer';
1225
+ return 'openfeel-schemer';
1194
1226
  case 'exec':
1195
- return 'executor';
1227
+ return 'openfeel-executor';
1196
1228
  case 'review':
1197
- return 'reviewer';
1229
+ return 'openfeel-reviewer';
1198
1230
  case 'test':
1199
- return 'feel-tester';
1231
+ return 'openfeel-feel-tester';
1200
1232
  case 'archiving':
1201
- return 'archiver';
1233
+ return 'openfeel-archiver';
1202
1234
  case 'done':
1203
1235
  return 'none';
1204
1236
  default:
@@ -1228,7 +1260,7 @@ export class FlowManager {
1228
1260
  this.data.pipeline.retry = 0;
1229
1261
  this.appendLog({
1230
1262
  time: '',
1231
- agent: 'executor',
1263
+ agent: 'openfeel-executor',
1232
1264
  action: 'attempt_pass',
1233
1265
  detail: { opId, attempts: op.attempts },
1234
1266
  });
@@ -1246,7 +1278,7 @@ export class FlowManager {
1246
1278
  this.data.pipeline.retry += 1;
1247
1279
  this.appendLog({
1248
1280
  time: '',
1249
- agent: 'executor',
1281
+ agent: 'openfeel-executor',
1250
1282
  action: 'attempt_fail_retry',
1251
1283
  detail: { opId, attempts: op.attempts, maxAttempts: op.max_attempts },
1252
1284
  });
@@ -1263,7 +1295,7 @@ export class FlowManager {
1263
1295
  this.data.pipeline.retry += 1;
1264
1296
  this.appendLog({
1265
1297
  time: '',
1266
- agent: 'executor',
1298
+ agent: 'openfeel-executor',
1267
1299
  action: 'attempt_fail_exhausted',
1268
1300
  detail: { opId, attempts: op.attempts, maxAttempts: op.max_attempts },
1269
1301
  });
@@ -1308,7 +1340,7 @@ export class FlowManager {
1308
1340
  }
1309
1341
  /**
1310
1342
  * 添加自动修复审查条目
1311
- * 当 Reviewer 认为问题可直接修复时调用,REV 条目状态直接设为 resolved,
1343
+ * 当 openfeel-reviewer 认为问题可直接修复时调用,REV 条目状态直接设为 resolved,
1312
1344
  * pipeline.phase 跳过 review_failed→scheme_pending,直接推进到 exec_running。
1313
1345
  * @param item 审查条目(canAutoFix 自动设为 true)
1314
1346
  * @param opId 关联的操作 ID
@@ -1667,6 +1699,12 @@ export class FlowManager {
1667
1699
  changes.push('已补全缺失的 meta.updated');
1668
1700
  modified = true;
1669
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
+ }
1670
1708
  }
1671
1709
  // 修复缺失的 pipeline 字段
1672
1710
  if (!flowData.pipeline) {
@@ -1761,17 +1799,11 @@ export class FlowManager {
1761
1799
  }
1762
1800
  // 写入修复后的数据(recovered 场景下磁盘文件仍损坏,即使无字段修改也必须写回恢复内容)
1763
1801
  if ((modified || recovered) && !dryRun) {
1764
- // 备份当前文件(recovered 场景下 .bak 已是有效恢复来源,避免被损坏文件覆盖)
1765
- if (!recovered) {
1766
- try {
1767
- copyFileSync(this.filePath, this.filePath + '.bak');
1768
- }
1769
- catch {
1770
- // 备份失败不阻塞
1771
- }
1772
- }
1773
- // 使用安全写入
1774
- 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;
1775
1807
  const serializable = JSON.parse(JSON.stringify(flowData));
1776
1808
  // 去除 op 中的 id 字段
1777
1809
  if (serializable.stages && typeof serializable.stages === 'object') {
@@ -1786,23 +1818,19 @@ export class FlowManager {
1786
1818
  }
1787
1819
  }
1788
1820
  const content = JSON.stringify(serializable, null, 2) + '\n';
1789
- writeFileSync(tmpPath, content, 'utf-8');
1790
- // 校验临时文件
1821
+ const lockPath = projectLockPath(this.projectPath, 'flow');
1791
1822
  try {
1792
- const tmpContent = readFileSync(tmpPath, 'utf-8');
1793
- JSON.parse(tmpContent);
1823
+ withFileLock(lockPath, () => {
1824
+ // recovered 场景 .bak 已是有效恢复来源,避免被损坏文件覆盖(保持既有语义)
1825
+ atomicWriteFileSync(this.filePath, content, { backup: !recovered });
1826
+ });
1794
1827
  }
1795
- catch (parseErr) {
1796
- try {
1797
- writeFileSync(tmpPath, '', 'utf-8');
1798
- }
1799
- catch {
1800
- // 清理失败不阻塞
1801
- }
1802
- changes.push(`修复后写入校验失败: ${parseErr.message}`);
1828
+ catch (err) {
1829
+ changes.push(`修复后写入失败: ${err.message}`);
1803
1830
  return { fixed: false, changes, recovered };
1804
1831
  }
1805
- renameSync(tmpPath, this.filePath);
1832
+ // 写盘成功后重新加载,同步 this.data 与 this.loadedRevision 基线
1833
+ this.load();
1806
1834
  }
1807
1835
  // changes 在无问题时保持空数组,CLI 层通过 changes.length === 0 判断"没问题"
1808
1836
  return { fixed: modified || recovered, changes, recovered };
@@ -2046,25 +2074,18 @@ export class FlowManager {
2046
2074
  }
2047
2075
  }
2048
2076
  }
2049
- /** 2. 检查 flow.json 与 plan/{stage}/status.md 跨文件一致性 */
2077
+ /** 2. 检查 flow.json 与 plan/{series}/stage-NN/status.md 跨文件一致性 */
2050
2078
  checkCrossFileConsistency(items) {
2051
2079
  if (!this.data) {
2052
2080
  return;
2053
2081
  }
2054
- const planDir = resolve(this.projectPath, '.openfeel', 'plan');
2055
- // 同时检查 .openfeel/stages/ 目录(代码实际使用路径)
2056
- const stagesDir = resolve(this.projectPath, '.openfeel', 'stages');
2057
2082
  let total = 0;
2058
2083
  let consistent = 0;
2059
2084
  for (const [stageId, stage] of Object.entries(this.data.stages)) {
2060
- // 尝试 .openfeel/stages/{stageId}/status.md(代码实际路径)
2061
- let statusPath = resolve(stagesDir, stageId, 'status.md');
2062
- if (!existsSync(statusPath)) {
2063
- // 回退到 .openfeel/plan/{stageId}/status.md(文档声明路径)
2064
- statusPath = resolve(planDir, stageId, 'status.md');
2065
- if (!existsSync(statusPath)) {
2066
- continue;
2067
- }
2085
+ // 三级回退查找 status.md(plan/{series}/ 精确 → plan 递归 → stages 兜底)
2086
+ const statusPath = findStageStatusPath(this.projectPath, stageId);
2087
+ if (!statusPath) {
2088
+ continue;
2068
2089
  }
2069
2090
  total++;
2070
2091
  try {
@@ -2100,8 +2121,6 @@ export class FlowManager {
2100
2121
  return;
2101
2122
  }
2102
2123
  // 僵尸阶段检测:各 stage 在 status.md 中声明的状态与 stage.status 不一致
2103
- const planDir = resolve(this.projectPath, '.openfeel', 'plan');
2104
- const stagesDir = resolve(this.projectPath, '.openfeel', 'stages');
2105
2124
  for (const [stageId, stage] of Object.entries(this.data.stages)) {
2106
2125
  // 所有 REV 已 closed 但 stage 仍为 review_failed
2107
2126
  if (stage.status === 'review_failed') {
@@ -2284,7 +2303,8 @@ export class FlowManager {
2284
2303
  return; // 已存在则不覆盖
2285
2304
  }
2286
2305
  const data = defaultFlowData();
2287
- writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
2306
+ // 首次创建(existsSync 守卫)、无并发读者,故不加锁,仅原子写;默认 revision:0
2307
+ atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
2288
2308
  }
2289
2309
  /**
2290
2310
  * 内嵌数据(仅供测试使用)
@@ -2292,6 +2312,8 @@ export class FlowManager {
2292
2312
  */
2293
2313
  setData(data) {
2294
2314
  this.data = data;
2315
+ // 测试专用:注入数据视为「当前磁盘状态」,同步并发校验基线,避免后续 save 误判冲突
2316
+ this.loadedRevision = this.readDiskRevision();
2295
2317
  }
2296
2318
  // ═══ 流水线配置加载 ═══
2297
2319
  /**
@@ -2364,6 +2386,33 @@ export class FlowManager {
2364
2386
  }
2365
2387
  }
2366
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
+ }
2367
2416
  /**
2368
2417
  * 将 PipelinePhase 映射为 stage 状态
2369
2418
  * @param phase 流水线阶段