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.
- package/README.en.md +11 -11
- package/README.md +2 -1
- package/README.zh-CN.md +13 -13
- package/bin/openfeel.js +2 -2
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/archive.d.ts +1 -1
- package/dist/commands/flow.d.ts +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +20 -8
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/migrate.d.ts +10 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +97 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/model.d.ts +10 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +135 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/plan.js +2 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/stage.d.ts.map +1 -1
- package/dist/commands/stage.js +66 -47
- package/dist/commands/stage.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +12 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.d.ts.map +1 -1
- package/dist/commands/view.js +2 -1
- package/dist/commands/view.js.map +1 -1
- package/dist/core/archive/merge.d.ts.map +1 -1
- package/dist/core/archive/merge.js +5 -4
- package/dist/core/archive/merge.js.map +1 -1
- package/dist/core/artifact-graph/resolver.js +2 -2
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/config.d.ts +8 -8
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -17
- package/dist/core/config.js.map +1 -1
- package/dist/core/flow-manager.d.ts +27 -6
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +152 -103
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/fs/atomic-write.d.ts +31 -0
- package/dist/core/fs/atomic-write.d.ts.map +1 -0
- package/dist/core/fs/atomic-write.js +80 -0
- package/dist/core/fs/atomic-write.js.map +1 -0
- package/dist/core/fs/file-lock.d.ts +34 -0
- package/dist/core/fs/file-lock.d.ts.map +1 -0
- package/dist/core/fs/file-lock.js +142 -0
- package/dist/core/fs/file-lock.js.map +1 -0
- package/dist/core/fs/sequence.d.ts +42 -0
- package/dist/core/fs/sequence.d.ts.map +1 -0
- package/dist/core/fs/sequence.js +69 -0
- package/dist/core/fs/sequence.js.map +1 -0
- package/dist/core/global-paths.d.ts +25 -0
- package/dist/core/global-paths.d.ts.map +1 -0
- package/dist/core/global-paths.js +57 -0
- package/dist/core/global-paths.js.map +1 -0
- package/dist/core/i18n-data/en.d.ts +2 -0
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +69 -1
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts +2 -0
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +69 -1
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/i18n.js +4 -4
- package/dist/core/i18n.js.map +1 -1
- package/dist/core/init.d.ts +3 -3
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +58 -42
- package/dist/core/init.js.map +1 -1
- package/dist/core/managed-region.d.ts +54 -0
- package/dist/core/managed-region.d.ts.map +1 -0
- package/dist/core/managed-region.js +171 -0
- package/dist/core/managed-region.js.map +1 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +6 -3
- package/dist/core/metrics.js.map +1 -1
- package/dist/core/migrate.d.ts +103 -0
- package/dist/core/migrate.d.ts.map +1 -0
- package/dist/core/migrate.js +533 -0
- package/dist/core/migrate.js.map +1 -0
- package/dist/core/model-config.d.ts +62 -0
- package/dist/core/model-config.d.ts.map +1 -0
- package/dist/core/model-config.js +336 -0
- package/dist/core/model-config.js.map +1 -0
- package/dist/core/opencode-config.d.ts +17 -0
- package/dist/core/opencode-config.d.ts.map +1 -0
- package/dist/core/opencode-config.js +123 -0
- package/dist/core/opencode-config.js.map +1 -0
- package/dist/core/plan/path.d.ts +64 -0
- package/dist/core/plan/path.d.ts.map +1 -0
- package/dist/core/plan/path.js +150 -0
- package/dist/core/plan/path.js.map +1 -0
- package/dist/core/plan/roadmap.d.ts.map +1 -1
- package/dist/core/plan/roadmap.js +3 -2
- package/dist/core/plan/roadmap.js.map +1 -1
- package/dist/core/plan/scheme.d.ts +4 -3
- package/dist/core/plan/scheme.d.ts.map +1 -1
- package/dist/core/plan/scheme.js +113 -110
- package/dist/core/plan/scheme.js.map +1 -1
- package/dist/core/plan/stage.d.ts +2 -2
- package/dist/core/plan/stage.d.ts.map +1 -1
- package/dist/core/plan/stage.js +47 -32
- package/dist/core/plan/stage.js.map +1 -1
- package/dist/core/public-logger.d.ts +3 -3
- package/dist/core/public-logger.d.ts.map +1 -1
- package/dist/core/public-logger.js +36 -40
- package/dist/core/public-logger.js.map +1 -1
- package/dist/core/template-loader.d.ts +1 -1
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +2849 -2826
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update-infos.d.ts +32 -0
- package/dist/core/update-infos.d.ts.map +1 -0
- package/dist/core/update-infos.js +153 -0
- package/dist/core/update-infos.js.map +1 -0
- package/dist/core/update-state.d.ts +18 -0
- package/dist/core/update-state.d.ts.map +1 -1
- package/dist/core/update-state.js +66 -3
- package/dist/core/update-state.js.map +1 -1
- package/dist/core/update.d.ts +12 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +340 -485
- package/dist/core/update.js.map +1 -1
- package/dist/core/view/entry.js +4 -4
- package/dist/core/view/entry.js.map +1 -1
- package/dist/core/workspace/identity.d.ts.map +1 -1
- package/dist/core/workspace/identity.js +13 -10
- package/dist/core/workspace/identity.js.map +1 -1
- package/dist/core/workspace/knowledge.d.ts.map +1 -1
- package/dist/core/workspace/knowledge.js +24 -14
- package/dist/core/workspace/knowledge.js.map +1 -1
- 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,
|
|
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
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
1147
|
+
/** 查找 status.md 的路径(三级回退:plan/{series}/ 精确 → plan 递归 → stages 兜底) */
|
|
1108
1148
|
findStatusPath(stageId) {
|
|
1109
|
-
|
|
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
|
-
* 当
|
|
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
|
-
//
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
1790
|
-
// 校验临时文件
|
|
1821
|
+
const lockPath = projectLockPath(this.projectPath, 'flow');
|
|
1791
1822
|
try {
|
|
1792
|
-
|
|
1793
|
-
|
|
1823
|
+
withFileLock(lockPath, () => {
|
|
1824
|
+
// recovered 场景 .bak 已是有效恢复来源,避免被损坏文件覆盖(保持既有语义)
|
|
1825
|
+
atomicWriteFileSync(this.filePath, content, { backup: !recovered });
|
|
1826
|
+
});
|
|
1794
1827
|
}
|
|
1795
|
-
catch (
|
|
1796
|
-
|
|
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
|
-
|
|
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/{
|
|
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
|
-
//
|
|
2061
|
-
|
|
2062
|
-
if (!
|
|
2063
|
-
|
|
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
|
-
|
|
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 流水线阶段
|