openfeel 1.0.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 (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +140 -0
  3. package/README.md +17 -0
  4. package/README.zh-CN.md +140 -0
  5. package/bin/openfeel.js +13 -0
  6. package/dist/cli/index.d.ts +11 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +119 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/repl.d.ts +10 -0
  11. package/dist/cli/repl.d.ts.map +1 -0
  12. package/dist/cli/repl.js +74 -0
  13. package/dist/cli/repl.js.map +1 -0
  14. package/dist/commands/archive.d.ts +7 -0
  15. package/dist/commands/archive.d.ts.map +1 -0
  16. package/dist/commands/archive.js +38 -0
  17. package/dist/commands/archive.js.map +1 -0
  18. package/dist/commands/config.d.ts +15 -0
  19. package/dist/commands/config.d.ts.map +1 -0
  20. package/dist/commands/config.js +208 -0
  21. package/dist/commands/config.js.map +1 -0
  22. package/dist/commands/flow.d.ts +24 -0
  23. package/dist/commands/flow.d.ts.map +1 -0
  24. package/dist/commands/flow.js +1024 -0
  25. package/dist/commands/flow.js.map +1 -0
  26. package/dist/commands/init.d.ts +9 -0
  27. package/dist/commands/init.d.ts.map +1 -0
  28. package/dist/commands/init.js +62 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/instructions.d.ts +7 -0
  31. package/dist/commands/instructions.d.ts.map +1 -0
  32. package/dist/commands/instructions.js +36 -0
  33. package/dist/commands/instructions.js.map +1 -0
  34. package/dist/commands/knowledge.d.ts +7 -0
  35. package/dist/commands/knowledge.d.ts.map +1 -0
  36. package/dist/commands/knowledge.js +174 -0
  37. package/dist/commands/knowledge.js.map +1 -0
  38. package/dist/commands/lint.d.ts +12 -0
  39. package/dist/commands/lint.d.ts.map +1 -0
  40. package/dist/commands/lint.js +192 -0
  41. package/dist/commands/lint.js.map +1 -0
  42. package/dist/commands/plan.d.ts +7 -0
  43. package/dist/commands/plan.d.ts.map +1 -0
  44. package/dist/commands/plan.js +73 -0
  45. package/dist/commands/plan.js.map +1 -0
  46. package/dist/commands/project.d.ts +7 -0
  47. package/dist/commands/project.d.ts.map +1 -0
  48. package/dist/commands/project.js +189 -0
  49. package/dist/commands/project.js.map +1 -0
  50. package/dist/commands/roadmap.d.ts +7 -0
  51. package/dist/commands/roadmap.d.ts.map +1 -0
  52. package/dist/commands/roadmap.js +26 -0
  53. package/dist/commands/roadmap.js.map +1 -0
  54. package/dist/commands/stage.d.ts +17 -0
  55. package/dist/commands/stage.d.ts.map +1 -0
  56. package/dist/commands/stage.js +291 -0
  57. package/dist/commands/stage.js.map +1 -0
  58. package/dist/commands/update.d.ts +8 -0
  59. package/dist/commands/update.d.ts.map +1 -0
  60. package/dist/commands/update.js +87 -0
  61. package/dist/commands/update.js.map +1 -0
  62. package/dist/commands/view.d.ts +7 -0
  63. package/dist/commands/view.d.ts.map +1 -0
  64. package/dist/commands/view.js +77 -0
  65. package/dist/commands/view.js.map +1 -0
  66. package/dist/core/archive/merge.d.ts +20 -0
  67. package/dist/core/archive/merge.d.ts.map +1 -0
  68. package/dist/core/archive/merge.js +118 -0
  69. package/dist/core/archive/merge.js.map +1 -0
  70. package/dist/core/artifact-graph/graph.d.ts +62 -0
  71. package/dist/core/artifact-graph/graph.d.ts.map +1 -0
  72. package/dist/core/artifact-graph/graph.js +184 -0
  73. package/dist/core/artifact-graph/graph.js.map +1 -0
  74. package/dist/core/artifact-graph/index.d.ts +10 -0
  75. package/dist/core/artifact-graph/index.d.ts.map +1 -0
  76. package/dist/core/artifact-graph/index.js +8 -0
  77. package/dist/core/artifact-graph/index.js.map +1 -0
  78. package/dist/core/artifact-graph/instruction-loader.d.ts +35 -0
  79. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -0
  80. package/dist/core/artifact-graph/instruction-loader.js +358 -0
  81. package/dist/core/artifact-graph/instruction-loader.js.map +1 -0
  82. package/dist/core/artifact-graph/resolver.d.ts +10 -0
  83. package/dist/core/artifact-graph/resolver.d.ts.map +1 -0
  84. package/dist/core/artifact-graph/resolver.js +32 -0
  85. package/dist/core/artifact-graph/resolver.js.map +1 -0
  86. package/dist/core/artifact-graph/state.d.ts +9 -0
  87. package/dist/core/artifact-graph/state.d.ts.map +1 -0
  88. package/dist/core/artifact-graph/state.js +24 -0
  89. package/dist/core/artifact-graph/state.js.map +1 -0
  90. package/dist/core/artifact-graph/types.d.ts +22 -0
  91. package/dist/core/artifact-graph/types.d.ts.map +1 -0
  92. package/dist/core/artifact-graph/types.js +2 -0
  93. package/dist/core/artifact-graph/types.js.map +1 -0
  94. package/dist/core/config.d.ts +256 -0
  95. package/dist/core/config.d.ts.map +1 -0
  96. package/dist/core/config.js +393 -0
  97. package/dist/core/config.js.map +1 -0
  98. package/dist/core/flow-manager.d.ts +492 -0
  99. package/dist/core/flow-manager.d.ts.map +1 -0
  100. package/dist/core/flow-manager.js +2379 -0
  101. package/dist/core/flow-manager.js.map +1 -0
  102. package/dist/core/i18n-data/en.d.ts +26 -0
  103. package/dist/core/i18n-data/en.d.ts.map +1 -0
  104. package/dist/core/i18n-data/en.js +492 -0
  105. package/dist/core/i18n-data/en.js.map +1 -0
  106. package/dist/core/i18n-data/types.d.ts +18 -0
  107. package/dist/core/i18n-data/types.d.ts.map +1 -0
  108. package/dist/core/i18n-data/types.js +8 -0
  109. package/dist/core/i18n-data/types.js.map +1 -0
  110. package/dist/core/i18n-data/zh-CN.d.ts +28 -0
  111. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -0
  112. package/dist/core/i18n-data/zh-CN.js +524 -0
  113. package/dist/core/i18n-data/zh-CN.js.map +1 -0
  114. package/dist/core/i18n.d.ts +40 -0
  115. package/dist/core/i18n.d.ts.map +1 -0
  116. package/dist/core/i18n.js +135 -0
  117. package/dist/core/i18n.js.map +1 -0
  118. package/dist/core/init.d.ts +22 -0
  119. package/dist/core/init.d.ts.map +1 -0
  120. package/dist/core/init.js +292 -0
  121. package/dist/core/init.js.map +1 -0
  122. package/dist/core/metrics.d.ts +60 -0
  123. package/dist/core/metrics.d.ts.map +1 -0
  124. package/dist/core/metrics.js +137 -0
  125. package/dist/core/metrics.js.map +1 -0
  126. package/dist/core/pipeline-schema.d.ts +78 -0
  127. package/dist/core/pipeline-schema.d.ts.map +1 -0
  128. package/dist/core/pipeline-schema.js +57 -0
  129. package/dist/core/pipeline-schema.js.map +1 -0
  130. package/dist/core/plan/roadmap.d.ts +17 -0
  131. package/dist/core/plan/roadmap.d.ts.map +1 -0
  132. package/dist/core/plan/roadmap.js +71 -0
  133. package/dist/core/plan/roadmap.js.map +1 -0
  134. package/dist/core/plan/scheme.d.ts +34 -0
  135. package/dist/core/plan/scheme.d.ts.map +1 -0
  136. package/dist/core/plan/scheme.js +262 -0
  137. package/dist/core/plan/scheme.js.map +1 -0
  138. package/dist/core/plan/stage.d.ts +20 -0
  139. package/dist/core/plan/stage.d.ts.map +1 -0
  140. package/dist/core/plan/stage.js +120 -0
  141. package/dist/core/plan/stage.js.map +1 -0
  142. package/dist/core/public-logger.d.ts +84 -0
  143. package/dist/core/public-logger.d.ts.map +1 -0
  144. package/dist/core/public-logger.js +344 -0
  145. package/dist/core/public-logger.js.map +1 -0
  146. package/dist/core/schema.d.ts +70 -0
  147. package/dist/core/schema.d.ts.map +1 -0
  148. package/dist/core/schema.js +61 -0
  149. package/dist/core/schema.js.map +1 -0
  150. package/dist/core/template-loader.d.ts +22 -0
  151. package/dist/core/template-loader.d.ts.map +1 -0
  152. package/dist/core/template-loader.js +2848 -0
  153. package/dist/core/template-loader.js.map +1 -0
  154. package/dist/core/templates.d.ts +18 -0
  155. package/dist/core/templates.d.ts.map +1 -0
  156. package/dist/core/templates.js +50 -0
  157. package/dist/core/templates.js.map +1 -0
  158. package/dist/core/update.d.ts +40 -0
  159. package/dist/core/update.d.ts.map +1 -0
  160. package/dist/core/update.js +1569 -0
  161. package/dist/core/update.js.map +1 -0
  162. package/dist/core/view/entry.d.ts +37 -0
  163. package/dist/core/view/entry.d.ts.map +1 -0
  164. package/dist/core/view/entry.js +108 -0
  165. package/dist/core/view/entry.js.map +1 -0
  166. package/dist/core/workspace/identity.d.ts +60 -0
  167. package/dist/core/workspace/identity.d.ts.map +1 -0
  168. package/dist/core/workspace/identity.js +169 -0
  169. package/dist/core/workspace/identity.js.map +1 -0
  170. package/dist/core/workspace/knowledge.d.ts +55 -0
  171. package/dist/core/workspace/knowledge.d.ts.map +1 -0
  172. package/dist/core/workspace/knowledge.js +230 -0
  173. package/dist/core/workspace/knowledge.js.map +1 -0
  174. package/dist/core/workspace/structure.d.ts +9 -0
  175. package/dist/core/workspace/structure.d.ts.map +1 -0
  176. package/dist/core/workspace/structure.js +59 -0
  177. package/dist/core/workspace/structure.js.map +1 -0
  178. package/dist/index.d.ts +7 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +7 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/utils/kb-dedup.d.ts +46 -0
  183. package/dist/utils/kb-dedup.d.ts.map +1 -0
  184. package/dist/utils/kb-dedup.js +169 -0
  185. package/dist/utils/kb-dedup.js.map +1 -0
  186. package/dist/utils/path.d.ts +22 -0
  187. package/dist/utils/path.d.ts.map +1 -0
  188. package/dist/utils/path.js +37 -0
  189. package/dist/utils/path.js.map +1 -0
  190. package/package.json +61 -0
  191. package/schemas/spec-driven/schema.yaml +50 -0
  192. package/schemas/spec-driven/templates/design.md +43 -0
  193. package/schemas/spec-driven/templates/plan.md +46 -0
  194. package/schemas/spec-driven/templates/proposal.md +35 -0
  195. package/schemas/spec-driven/templates/tasks.md +39 -0
  196. package/scripts/patch-inquirer.js +70 -0
@@ -0,0 +1,2379 @@
1
+ /**
2
+ * FlowManager — 流水线状态管理核心类
3
+ * 负责 flow.json 的读写、状态推进、重试计数、审查管理、日志记录与校验。
4
+ *
5
+ * 变更摘要 (stage-01: flow.json 鲁棒性加固):
6
+ * - PipelinePhase 类型从动态 string 硬化为 Zod enum(从 pipeline-schema 导入)
7
+ * - validate() 增强: 使用 PipelinePhaseSchema 校验 + 模糊匹配自动修正非法 phase
8
+ * - save() 增加备份与临时文件写入机制,防止 JSON 损坏导致状态丢失
9
+ * - advancePhase() 增加 to 参数的 PipelinePhaseSchema 校验
10
+ * - 新增 repair() 方法,自动检测并修复 flow.json 常见问题
11
+ */
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, renameSync, readdirSync, unlinkSync } from 'node:fs';
13
+ import { resolve } from 'node:path';
14
+ import { execSync } from 'node:child_process';
15
+ import { parse as parseYaml } from 'yaml';
16
+ import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, } from './pipeline-schema.js';
17
+ import { PublicLogger, formatDate } from './public-logger.js';
18
+ import { t, getCliLang } from './i18n.js';
19
+ // ── 默认值 ──
20
+ /** 创建默认的 Checkpoints */
21
+ function defaultCheckpoints() {
22
+ return {
23
+ plan: 'pending',
24
+ scheme: 'pending',
25
+ exec: { attempts: 0, self: 'pending' },
26
+ review: 'pending',
27
+ test: 'pending',
28
+ };
29
+ }
30
+ /** 创建默认的 FlowData */
31
+ function defaultFlowData() {
32
+ return {
33
+ meta: {
34
+ version: '1.0',
35
+ project: 'OpenFeel',
36
+ updated: new Date().toISOString(),
37
+ },
38
+ pipeline: {
39
+ phase: 'active',
40
+ current: { stage: '-', op: 'init' },
41
+ retry: 0,
42
+ },
43
+ stages: {},
44
+ reviews: [],
45
+ log: [],
46
+ };
47
+ }
48
+ // ── 核心类 ──
49
+ export class FlowManager {
50
+ projectPath;
51
+ data;
52
+ filePath;
53
+ /** 从 pipeline.yaml 加载的流水线配置(含后备默认值) */
54
+ pipelineConfig = null;
55
+ /** 公共日志写入器(审计链) */
56
+ publicLogger;
57
+ constructor(projectPath) {
58
+ this.projectPath = projectPath;
59
+ this.data = null;
60
+ this.filePath = resolve(projectPath, '.openfeel', 'flow.json');
61
+ this.publicLogger = PublicLogger.getInstance(projectPath);
62
+ this.loadPipelineConfig();
63
+ this.load();
64
+ }
65
+ // ═══ 数据读写 ═══
66
+ /** 加载 flow.json */
67
+ load() {
68
+ if (!existsSync(this.filePath)) {
69
+ this.data = null;
70
+ return;
71
+ }
72
+ try {
73
+ const raw = readFileSync(this.filePath, 'utf-8');
74
+ this.data = JSON.parse(raw);
75
+ // 从 stages 的键名恢复 op 的 id 字段(运行时便利字段,磁盘不存储)
76
+ if (this.data && this.data.stages) {
77
+ for (const [, stage] of Object.entries(this.data.stages)) {
78
+ for (const [opKey, op] of Object.entries(stage.ops)) {
79
+ op.id = opKey;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ this.data = null;
86
+ }
87
+ }
88
+ /** 保存 flow.json(自动更新 meta.updated,序列化时去除 op 中的 id 字段以与键名保持一致) */
89
+ save() {
90
+ if (!this.data) {
91
+ return;
92
+ }
93
+ this.data.meta.updated = new Date().toISOString();
94
+ // 创建清理后的副本:去除 op 中的 id 字段(id 由 stages.{stageId}.ops 的键名决定)
95
+ const serializable = JSON.parse(JSON.stringify(this.data));
96
+ for (const stage of Object.values(serializable.stages)) {
97
+ for (const op of Object.values(stage.ops)) {
98
+ delete op.id;
99
+ }
100
+ }
101
+ const content = JSON.stringify(serializable, null, 2) + '\n';
102
+ const tmpPath = this.filePath + '.tmp';
103
+ const bakPath = this.filePath + '.bak';
104
+ // 备份旧文件(写入前)
105
+ if (existsSync(this.filePath)) {
106
+ try {
107
+ copyFileSync(this.filePath, bakPath);
108
+ }
109
+ catch {
110
+ // 备份失败不阻塞写入
111
+ }
112
+ }
113
+ try {
114
+ // 写入临时文件
115
+ writeFileSync(tmpPath, content, 'utf-8');
116
+ // 校验临时文件 JSON 合法性
117
+ try {
118
+ const tmpContent = readFileSync(tmpPath, 'utf-8');
119
+ JSON.parse(tmpContent);
120
+ }
121
+ catch (parseErr) {
122
+ // 清理失败的临时文件
123
+ try {
124
+ writeFileSync(tmpPath, '', 'utf-8');
125
+ }
126
+ catch {
127
+ // 清理失败也不阻塞
128
+ }
129
+ throw new Error(`flow.json 写入校验失败: ${parseErr.message}。备份已保留在 ${bakPath}`);
130
+ }
131
+ // 校验通过,rename 临时文件到正式文件
132
+ renameSync(tmpPath, this.filePath);
133
+ // 同步备份
134
+ writeFileSync(this.filePath + '.bak', content, 'utf-8');
135
+ }
136
+ catch (err) {
137
+ console.error(`[ERROR] flow.json 保存失败: ${err instanceof Error ? err.message : err}`);
138
+ throw err;
139
+ }
140
+ }
141
+ /** 数据是否已加载 */
142
+ isLoaded() {
143
+ return this.data !== null;
144
+ }
145
+ // ═══ Checkpoint 快照 ═══
146
+ /** 快照保留上限:超过该数量自动清理最旧的 */
147
+ static CHECKPOINT_MAX_KEEP = 20;
148
+ /**
149
+ * 保存 flow.json 快照到 .openfeel/checkpoints/
150
+ * 命名格式: {stageId}-{yyyyMMddTHHmmss}-{phase}.json
151
+ * 保留最近 CHECKPOINT_MAX_KEEP 个快照,超出自动清理最旧的。
152
+ * try-catch 包裹:快照失败不阻塞 phase 推进(调用方保证)。
153
+ * @param stageId 阶段 ID(如 v5.3-stage-01)
154
+ * @param phase 推进后的目标 phase(如 exec_running)
155
+ */
156
+ saveCheckpoint(stageId, phase) {
157
+ try {
158
+ if (!this.data) {
159
+ return;
160
+ }
161
+ const dir = resolve(this.projectPath, '.openfeel', 'checkpoints');
162
+ if (!existsSync(dir)) {
163
+ mkdirSync(dir, { recursive: true });
164
+ }
165
+ // 序列化时去除 op 中的 id 字段(与 save() 保持一致)
166
+ const serializable = JSON.parse(JSON.stringify(this.data));
167
+ for (const stage of Object.values(serializable.stages)) {
168
+ for (const op of Object.values(stage.ops)) {
169
+ delete op.id;
170
+ }
171
+ }
172
+ const timestamp = this.formatCheckpointTimestamp(new Date());
173
+ const filename = `${stageId}-${timestamp}-${phase}.json`;
174
+ const filePath = resolve(dir, filename);
175
+ writeFileSync(filePath, JSON.stringify(serializable, null, 2) + '\n', 'utf-8');
176
+ // 清理超限的最旧快照
177
+ this.cleanupCheckpoints(stageId);
178
+ }
179
+ catch {
180
+ // 快照失败静默跳过,不阻塞 phase 推进
181
+ }
182
+ }
183
+ /**
184
+ * 列出 Checkpoint 快照文件名(按时间升序)
185
+ * @param stageId 可选阶段 ID,提供时仅列出该阶段的快照
186
+ * @returns 快照文件名列表,目录不存在或读取失败时返回空数组
187
+ */
188
+ listCheckpoints(stageId) {
189
+ try {
190
+ const dir = resolve(this.projectPath, '.openfeel', 'checkpoints');
191
+ if (!existsSync(dir)) {
192
+ return [];
193
+ }
194
+ return readdirSync(dir)
195
+ .filter((f) => f.endsWith('.json') && (!stageId || f.startsWith(stageId + '-')))
196
+ .sort();
197
+ }
198
+ catch {
199
+ return [];
200
+ }
201
+ }
202
+ /**
203
+ * 从 Checkpoint 快照恢复 flow.json
204
+ * 恢复前将当前 flow.json 备份为 .bak;恢复成功后重新加载数据。
205
+ * @param filename 快照文件名(仅允许纯文件名,防路径穿越)
206
+ * @returns 是否恢复成功
207
+ */
208
+ restoreCheckpoint(filename) {
209
+ // 安全校验:拒绝含路径分隔符或 '..' 的文件名,防止路径穿越
210
+ if (!filename || filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
211
+ return false;
212
+ }
213
+ try {
214
+ const dir = resolve(this.projectPath, '.openfeel', 'checkpoints');
215
+ const filePath = resolve(dir, filename);
216
+ if (!existsSync(filePath)) {
217
+ return false;
218
+ }
219
+ const content = readFileSync(filePath, 'utf-8');
220
+ // 校验快照 JSON 合法性,非法内容拒绝恢复
221
+ JSON.parse(content);
222
+ // 备份当前 flow.json(失败不阻塞恢复)
223
+ if (existsSync(this.filePath)) {
224
+ try {
225
+ copyFileSync(this.filePath, this.filePath + '.bak');
226
+ }
227
+ catch {
228
+ // 备份失败忽略
229
+ }
230
+ }
231
+ writeFileSync(this.filePath, content, 'utf-8');
232
+ this.load();
233
+ return true;
234
+ }
235
+ catch {
236
+ return false;
237
+ }
238
+ }
239
+ /** 格式化快照时间戳: yyyyMMddTHHmmssSSS(毫秒级,避免同秒多次推进覆盖快照) */
240
+ formatCheckpointTimestamp(date) {
241
+ const pad = (n) => String(n).padStart(2, '0');
242
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` +
243
+ `T${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}` +
244
+ `${String(date.getMilliseconds()).padStart(3, '0')}`;
245
+ }
246
+ /**
247
+ * 清理超限快照:按文件名(含时间戳)升序,删除最旧的直至不超过 CHECKPOINT_MAX_KEEP
248
+ * @param stageId 阶段 ID,仅清理该阶段的快照
249
+ */
250
+ cleanupCheckpoints(stageId) {
251
+ try {
252
+ const dir = resolve(this.projectPath, '.openfeel', 'checkpoints');
253
+ if (!existsSync(dir)) {
254
+ return;
255
+ }
256
+ const entries = readdirSync(dir)
257
+ .filter((f) => f.startsWith(stageId + '-') && f.endsWith('.json'))
258
+ .sort();
259
+ const excess = entries.length - FlowManager.CHECKPOINT_MAX_KEEP;
260
+ if (excess > 0) {
261
+ for (let i = 0; i < excess; i++) {
262
+ try {
263
+ unlinkSync(resolve(dir, entries[i]));
264
+ }
265
+ catch {
266
+ // 单个文件删除失败跳过
267
+ }
268
+ }
269
+ }
270
+ }
271
+ catch {
272
+ // 清理失败静默跳过
273
+ }
274
+ }
275
+ /** 获取原始数据(供测试用) */
276
+ getData() {
277
+ return this.data;
278
+ }
279
+ // ═══ 查询 ═══
280
+ /** 获取全局宏观状态(MetaPhase:active/paused/done) */
281
+ getPhase() {
282
+ if (!this.data) {
283
+ return null;
284
+ }
285
+ return this.data.pipeline.phase;
286
+ }
287
+ /** 获取当前操作 */
288
+ getCurrent() {
289
+ if (!this.data) {
290
+ return null;
291
+ }
292
+ const cur = this.data.pipeline.current;
293
+ if (!cur.stage || !cur.op) {
294
+ return null;
295
+ }
296
+ return { stage: cur.stage, op: cur.op };
297
+ }
298
+ /** 解析 opId(格式 "stage-xx.op-xxx") */
299
+ parseOpId(opId) {
300
+ const dotIdx = opId.lastIndexOf('.');
301
+ if (dotIdx === -1) {
302
+ return null;
303
+ }
304
+ const stageId = opId.substring(0, dotIdx);
305
+ const opLocalId = opId.substring(dotIdx + 1);
306
+ // 排除空字符串情况
307
+ if (!stageId || !opLocalId) {
308
+ return null;
309
+ }
310
+ return { stageId, opLocalId };
311
+ }
312
+ /** 根据 opId 查找 Op 对象 */
313
+ getOp(opId) {
314
+ if (!this.data) {
315
+ return null;
316
+ }
317
+ const parts = this.parseOpId(opId);
318
+ if (!parts) {
319
+ return null;
320
+ }
321
+ const stage = this.data.stages[parts.stageId];
322
+ if (!stage) {
323
+ return null;
324
+ }
325
+ return stage.ops[parts.opLocalId] ?? null;
326
+ }
327
+ /** 获取 op 的执行状态 */
328
+ getOpState(opId) {
329
+ const op = this.getOp(opId);
330
+ return op ? op.state : null;
331
+ }
332
+ /** 获取 op 的检查点 */
333
+ getOpCheckpoints(opId) {
334
+ const op = this.getOp(opId);
335
+ return op ? op.checkpoints : null;
336
+ }
337
+ /**
338
+ * 获取 ready 状态(pending 或 executing)的操作
339
+ * 可选按 stageId 过滤
340
+ */
341
+ getReadyOps(stageId) {
342
+ if (!this.data) {
343
+ return [];
344
+ }
345
+ const result = [];
346
+ for (const [sid, stage] of Object.entries(this.data.stages)) {
347
+ if (stageId && sid !== stageId) {
348
+ continue;
349
+ }
350
+ for (const op of Object.values(stage.ops)) {
351
+ if (op.state === 'pending' || op.state === 'executing') {
352
+ result.push({ ...op, id: `${sid}.${op.id}` });
353
+ }
354
+ }
355
+ }
356
+ return result;
357
+ }
358
+ /** 获取审查条目,可按 opId 过滤 */
359
+ getReviewItems(opId) {
360
+ if (!this.data) {
361
+ return [];
362
+ }
363
+ if (!opId) {
364
+ return [...this.data.reviews];
365
+ }
366
+ return this.data.reviews.filter((r) => r.op === opId);
367
+ }
368
+ /** 获取指定 op 的重试次数 */
369
+ getRetryCount(opId) {
370
+ const op = this.getOp(opId);
371
+ return op ? op.attempts : 0;
372
+ }
373
+ /** 返回人类可读的流水线摘要(支持 i18n) */
374
+ summary(lang = 'zh-CN') {
375
+ if (!this.data) {
376
+ return t('common.noInit', lang);
377
+ }
378
+ const stagesCount = Object.keys(this.data.stages).length;
379
+ let opsCount = 0;
380
+ for (const stage of Object.values(this.data.stages)) {
381
+ opsCount += Object.keys(stage.ops).length;
382
+ }
383
+ const openReviews = this.data.reviews.filter((r) => r.status === 'open').length;
384
+ const cur = this.data.pipeline.current;
385
+ const stagePhase = (cur.stage && this.data.stages[cur.stage])
386
+ ? this.data.stages[cur.stage].phase
387
+ : t('common.none', lang);
388
+ const lines = [
389
+ t('flow.status.title', lang),
390
+ `${t('flow.status.globalStatus', lang)}: ${this.data.pipeline.phase}`,
391
+ `${t('flow.status.currentStageLabel', lang)}: ${cur.stage || t('common.none', lang)} — ${t('flow.status.stagePhase', lang)}: ${stagePhase}`,
392
+ `${t('flow.status.currentOp', lang)}: ${cur.stage ? `${cur.stage}.${cur.op}` : t('common.none', lang)}`,
393
+ `${t('flow.status.retryCount', lang)}: ${this.data.pipeline.retry}`,
394
+ `${t('flow.status.stagesCount', lang)}: ${stagesCount}`,
395
+ `${t('flow.status.opsCount', lang)}: ${opsCount}`,
396
+ `${t('flow.status.reviewPending', lang)}: ${openReviews}`,
397
+ `${t('flow.status.logTotal', lang)}: ${this.data.log.length}`,
398
+ `${t('flow.status.lastUpdated', lang)}: ${this.data.meta.updated}`,
399
+ ];
400
+ return lines.join('\n');
401
+ }
402
+ /** 获取 PipelineSummary 结构化摘要 */
403
+ getSummary() {
404
+ if (!this.data) {
405
+ return {
406
+ phase: 'uninitialized',
407
+ currentOp: null,
408
+ retryCount: 0,
409
+ stagesCount: 0,
410
+ opsCount: 0,
411
+ reviewItemsOpen: 0,
412
+ recentLogs: 0,
413
+ };
414
+ }
415
+ let opsCount = 0;
416
+ for (const stage of Object.values(this.data.stages)) {
417
+ opsCount += Object.keys(stage.ops).length;
418
+ }
419
+ return {
420
+ phase: this.data.pipeline.phase,
421
+ currentOp: this.data.pipeline.current.stage
422
+ ? `${this.data.pipeline.current.stage}.${this.data.pipeline.current.op}`
423
+ : null,
424
+ retryCount: this.data.pipeline.retry,
425
+ stagesCount: Object.keys(this.data.stages).length,
426
+ opsCount,
427
+ reviewItemsOpen: this.data.reviews.filter((r) => r.status === 'open').length,
428
+ recentLogs: this.data.log.length,
429
+ };
430
+ }
431
+ /**
432
+ * 将阶段注册到 flow.json 的 stages 中(若不存在)
433
+ * @param stageName 阶段名(如 stage-01)
434
+ * @param deps 依赖阶段列表(可选)
435
+ */
436
+ registerStage(stageName, deps = []) {
437
+ if (!this.data) {
438
+ return;
439
+ }
440
+ if (this.data.stages[stageName]) {
441
+ return; // 已注册,跳过
442
+ }
443
+ this.data.stages[stageName] = {
444
+ name: stageName,
445
+ phase: 'plan_pending',
446
+ status: 'planned',
447
+ deps,
448
+ ops: {},
449
+ };
450
+ }
451
+ // ═══ 阶段耗时统计 ═══
452
+ /**
453
+ * 记录阶段开始时间
454
+ * @param stageId 阶段 ID(如 stage-01)
455
+ */
456
+ startStage(stageId) {
457
+ if (!this.data) {
458
+ return;
459
+ }
460
+ const stage = this.data.stages[stageId];
461
+ if (!stage) {
462
+ return;
463
+ }
464
+ if (!stage.stats) {
465
+ stage.stats = { start_time: '', end_time: '', duration_ms: 0 };
466
+ }
467
+ stage.stats.start_time = new Date().toISOString();
468
+ }
469
+ /**
470
+ * 记录阶段结束时间并计算耗时
471
+ * @param stageId 阶段 ID(如 stage-01)
472
+ */
473
+ endStage(stageId) {
474
+ if (!this.data) {
475
+ return;
476
+ }
477
+ const stage = this.data.stages[stageId];
478
+ if (!stage) {
479
+ return;
480
+ }
481
+ // 确保 stats 已初始化
482
+ if (!stage.stats) {
483
+ stage.stats = { start_time: '', end_time: '', duration_ms: 0 };
484
+ }
485
+ stage.stats.end_time = new Date().toISOString();
486
+ // 计算耗时(毫秒)
487
+ if (stage.stats.start_time) {
488
+ const startMs = new Date(stage.stats.start_time).getTime();
489
+ const endMs = new Date(stage.stats.end_time).getTime();
490
+ stage.stats.duration_ms = Math.max(0, endMs - startMs);
491
+ }
492
+ // 公域日志:阶段完成时汇总写入一条里程碑记录
493
+ this.publicLogger.logMilestone(`阶段 ${stageId} 完成`, {
494
+ action: 'stage_completed',
495
+ stageId,
496
+ durationMs: stage.stats?.duration_ms ?? 0,
497
+ finalPhase: stage.phase,
498
+ });
499
+ }
500
+ /**
501
+ * 获取阶段耗时统计
502
+ * @param stageId 阶段 ID(如 stage-01)
503
+ * @returns 阶段耗时数据,不存在时返回 null
504
+ */
505
+ getStageStats(stageId) {
506
+ if (!this.data) {
507
+ return null;
508
+ }
509
+ const stage = this.data.stages[stageId];
510
+ if (!stage || !stage.stats) {
511
+ return null;
512
+ }
513
+ return { ...stage.stats };
514
+ }
515
+ /**
516
+ * 获取所有阶段的耗时统计映射
517
+ */
518
+ getAllStageStats() {
519
+ if (!this.data) {
520
+ return {};
521
+ }
522
+ const result = {};
523
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
524
+ if (stage.stats) {
525
+ result[stageId] = { ...stage.stats };
526
+ }
527
+ }
528
+ return result;
529
+ }
530
+ // ═══ 日志骨架创建 ═══
531
+ /**
532
+ * 在私域日志目录创建带日期前缀的骨架文件
533
+ * Agent 只需填充内容,无需手动创建文件
534
+ * @param stageId 阶段 ID(如 v4.4-stage-02)
535
+ * @param label 骨架标签(如 exec_running / review_pending / test_pending)
536
+ */
537
+ createLogSkeleton(stageId, label) {
538
+ const username = this.getUsername();
539
+ const now = new Date();
540
+ const dateStr = this.formatDateStr(now);
541
+ const logDir = resolve(this.projectPath, '.openfeel', 'users', username, 'log');
542
+ // 确保私域日志目录存在
543
+ if (!existsSync(logDir)) {
544
+ mkdirSync(logDir, { recursive: true });
545
+ }
546
+ // 计算 NNN 序号(基于当日已有文件数 + 1)
547
+ const nnn = this.computeSkeletonNnn(logDir, dateStr);
548
+ const fileName = `${dateStr}-${String(nnn).padStart(3, '0')}.md`;
549
+ const filePath = resolve(logDir, fileName);
550
+ // 仅当文件不存在时创建(幂等性)
551
+ if (!existsSync(filePath)) {
552
+ const content = `# ${fileName.replace('.md', '')}
553
+ - **时间**:${now.toISOString()}
554
+ - **阶段**:${stageId}
555
+ - **节点**:${label}
556
+ - **状态**:待填充
557
+
558
+ > 此文件由流水线自动创建。请在此记录操作详情。
559
+ `;
560
+ writeFileSync(filePath, content, 'utf-8');
561
+ }
562
+ }
563
+ /** 获取当前用户名(委托给 PublicLogger) */
564
+ getUsername() {
565
+ return this.publicLogger.getUsername();
566
+ }
567
+ /** 格式化日期为 yyyy-mm-dd */
568
+ formatDateStr(date) {
569
+ return formatDate(date);
570
+ }
571
+ /** 计算当日已有文件数,返回下一个序号 */
572
+ computeSkeletonNnn(logDir, datePrefix) {
573
+ try {
574
+ const entries = readdirSync(logDir);
575
+ let maxNnn = 0;
576
+ for (const entry of entries) {
577
+ if (entry.startsWith(datePrefix) && entry.endsWith('.md')) {
578
+ const nnnStr = entry.slice(datePrefix.length + 1, -3); // +1 跳过连字符
579
+ const nnn = parseInt(nnnStr, 10);
580
+ if (!isNaN(nnn) && nnn > maxNnn) {
581
+ maxNnn = nnn;
582
+ }
583
+ }
584
+ }
585
+ return maxNnn + 1;
586
+ }
587
+ catch {
588
+ return 1;
589
+ }
590
+ }
591
+ // ═══ 跨会话上下文恢复 ═══
592
+ /**
593
+ * 从 flow.json + status.md 恢复上下文
594
+ * 供 Feel 重启后准确恢复状态机位置,无需重新推断
595
+ * @param lang 语言标识(默认 'zh-CN')
596
+ * @returns 恢复上下文对象
597
+ */
598
+ recoverContext(lang = 'zh-CN') {
599
+ if (!this.data) {
600
+ return {
601
+ phase: null,
602
+ currentOp: null,
603
+ stageStatus: t('flow.recover.statusUninitialized', lang),
604
+ blockedBy: '',
605
+ pendingTasks: [],
606
+ };
607
+ }
608
+ const cur = this.data.pipeline.current;
609
+ // phase 从当前 stage 的 stage.phase 读取,而非全局 pipeline.phase
610
+ const phase = (cur.stage && this.data.stages[cur.stage])
611
+ ? this.data.stages[cur.stage].phase
612
+ : null;
613
+ const currentOp = (cur.stage && cur.op) ? `${cur.stage}.${cur.op}` : null;
614
+ // 从 status.md 读取当前阶段详细状态
615
+ let stageStatus = t('common.unknown', lang);
616
+ let blockedBy = '';
617
+ const pendingTasks = [];
618
+ if (cur.stage) {
619
+ const statusPath = this.findStatusPath(cur.stage);
620
+ if (statusPath) {
621
+ try {
622
+ const content = readFileSync(statusPath, 'utf-8');
623
+ // 提取执行模式
624
+ const execMatch = content.match(/\*\*执行模式\*\*[::]\s*(manual|auto)/);
625
+ const modeLabel = execMatch
626
+ ? (execMatch[1] === 'auto' ? t('flow.recover.statusAutoExec', lang) : t('flow.recover.statusManualExec', lang))
627
+ : '';
628
+ // 提取状态
629
+ const statusMatch = content.match(/\*\*状态\*\*[::]\s*(\S+)/);
630
+ stageStatus = statusMatch ? statusMatch[1] : t('common.unknown', lang);
631
+ if (modeLabel) {
632
+ stageStatus += ` (${modeLabel})`;
633
+ }
634
+ // 提取阻塞原因
635
+ const blockMatch = content.match(/\*\*阻塞原因\*\*[::]\s*(.+)/);
636
+ if (blockMatch) {
637
+ blockedBy = blockMatch[1].trim();
638
+ }
639
+ // 提取待完成/待续事项
640
+ const contentLower = content.toLowerCase();
641
+ // 查找待续事项或未完成的任务(`(?=##|$)` 支持其作为文件最后章节的边界场景)
642
+ const todoSection = content.match(/##\s*待续事项[\s\S]*?(?=##|$)/i)
643
+ || content.match(/##\s*待做[\s\S]*?(?=##|$)/i);
644
+ if (todoSection) {
645
+ const lines = todoSection[0].split(/\r?\n/);
646
+ for (const line of lines) {
647
+ const taskMatch = line.match(/[-*]\s*\[ \]\s*(.+)/);
648
+ if (taskMatch) {
649
+ pendingTasks.push(taskMatch[1].trim());
650
+ }
651
+ }
652
+ }
653
+ // 如果没有找到待续事项,尝试从所有阶段收集未完成的 op
654
+ if (pendingTasks.length === 0) {
655
+ for (const [, stage] of Object.entries(this.data.stages)) {
656
+ for (const [opKey, op] of Object.entries(stage.ops)) {
657
+ if (op.state === 'pending' || op.state === 'executing') {
658
+ pendingTasks.push(`${stage.name}.${opKey}: ${op.title}`);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ }
664
+ catch {
665
+ stageStatus = t('flow.recover.statusUnreadable', lang);
666
+ }
667
+ }
668
+ else {
669
+ stageStatus = t('flow.recover.statusFileMissing', lang);
670
+ }
671
+ }
672
+ else {
673
+ // 无当前阶段时,列出所有 pending/executing 的任务
674
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
675
+ for (const [opKey, op] of Object.entries(stage.ops)) {
676
+ if (op.state === 'pending' || op.state === 'executing') {
677
+ pendingTasks.push(`${stageId}.${opKey}: ${op.title}`);
678
+ }
679
+ }
680
+ }
681
+ stageStatus = t('flow.recover.statusNoCurrentStage', lang);
682
+ }
683
+ return { phase, currentOp, stageStatus, blockedBy, pendingTasks };
684
+ }
685
+ // ═══ 推进 ═══
686
+ /**
687
+ * 推进指定阶段到目标流水线阶段(新 API)
688
+ * @param stageName 阶段名(如 stage-01)
689
+ * @param phase 目标流水线阶段(PipelinePhase)
690
+ * @param triggeredBy 触发者标识(如 'cli' / 'Feel'),默认 'flow-manager'
691
+ * @returns 是否触发 done 归档(true = 本次推进将阶段变为 done 且之前非 done,需在 save 后执行归档 commit)
692
+ */
693
+ advanceStagePhase(stageName, phase, triggeredBy) {
694
+ if (!this.data) {
695
+ return false;
696
+ }
697
+ const actualTrigger = triggeredBy ?? 'flow-manager';
698
+ // 校验 stageName 存在
699
+ const stage = this.data.stages[stageName];
700
+ if (!stage) {
701
+ throw new Error(`阶段 '${stageName}' 不存在`);
702
+ }
703
+ // 校验 phase 为合法 PipelinePhase,非法时走模糊修正兜底
704
+ let targetPhase;
705
+ const phaseResult = PipelinePhaseSchema.safeParse(phase);
706
+ if (!phaseResult.success) {
707
+ const corrected = this.fuzzyCorrectPhase(phase);
708
+ if (corrected) {
709
+ console.warn(`[WARN] advanceStagePhase: Phase '${phase}' 自动修正为 '${corrected}'`);
710
+ targetPhase = corrected;
711
+ }
712
+ else {
713
+ throw new Error(`非法 phase '${phase}',模糊修正失败`);
714
+ }
715
+ }
716
+ else {
717
+ targetPhase = phaseResult.data;
718
+ }
719
+ // REV 闭环:blocking REV > 0 时拒绝推进到 done(--force 仅降级警告)
720
+ if (targetPhase === 'done') {
721
+ const stageReviews = this.data.reviews.filter((r) => r.op.startsWith(stageName + '.') || r.op === stageName);
722
+ const blockingOpen = stageReviews.filter((r) => r.blocking !== false && r.status === 'open');
723
+ if (blockingOpen.length > 0) {
724
+ const revList = blockingOpen
725
+ .map((r) => ` ${r.id}: ${r.title} (priority=${r.priority})`)
726
+ .join('\n');
727
+ throw new Error(`无法推进到 done:存在 ${blockingOpen.length} 个未解决的阻塞 REV:\n${revList}`);
728
+ }
729
+ }
730
+ // 保存旧 phase 用于日志
731
+ const fromPhase = stage.phase;
732
+ // 更新 stage phase
733
+ this.data.stages[stageName].phase = targetPhase;
734
+ // Checkpoint 自动快照:phase 推进成功后保存 flow.json 快照到 .openfeel/checkpoints/(失败不阻塞推进)
735
+ this.saveCheckpoint(stageName, targetPhase);
736
+ // 日志强制落档骨架:关键 phase 节点自动创建骨架文件
737
+ const SKELETON_PHASES = [
738
+ 'exec_running',
739
+ 'review_pending',
740
+ 'test_pending',
741
+ 'archiving',
742
+ ];
743
+ if (SKELETON_PHASES.includes(targetPhase)) {
744
+ this.createLogSkeleton(stageName, targetPhase);
745
+ }
746
+ // 同步更新 pipeline.current:从该 stage 的 ops 中找到第一个 pending/executing 的 op
747
+ const pendingOpEntry = Object.entries(stage.ops).find(([, op]) => op.state === 'pending' || op.state === 'executing');
748
+ this.data.pipeline.current = {
749
+ stage: stageName,
750
+ op: pendingOpEntry ? pendingOpEntry[0] : this.data.pipeline.current.op,
751
+ };
752
+ // 同步更新 pipeline.phase 为 'active'
753
+ this.data.pipeline.phase = 'active';
754
+ // 追加日志(使用实际触发者名,而非硬编码 'flow-manager')
755
+ this.appendLog({
756
+ time: '',
757
+ agent: actualTrigger,
758
+ action: 'advance_stage_phase',
759
+ detail: { stageName, from: fromPhase, to: targetPhase },
760
+ });
761
+ // REV-003: advance_stage_phase 改为 endStage 时批量聚合,取消逐条写入
762
+ // this.publicLogger.logPhaseChange({...});
763
+ // 同步 stage status(调用已有 mapPhaseToStageStatus)
764
+ const prevStatus = stage.status;
765
+ const newStatus = mapPhaseToStageStatus(targetPhase, prevStatus);
766
+ stage.status = newStatus;
767
+ // 自动记录阶段计时:首次状态变更时启动
768
+ if (!stage.stats || !stage.stats.start_time) {
769
+ this.startStage(stageName);
770
+ }
771
+ // 阶段完成时自动结束计时
772
+ if (targetPhase === 'done' && prevStatus !== 'done') {
773
+ this.endStage(stageName);
774
+ // 归档 git commit 移出本方法:必须在 flow.json save 之后执行(否则 commit 不含本次 phase 变更)。
775
+ // 返回 true 由命令层在 save 后调用 autoCommitOnDone。
776
+ return true;
777
+ }
778
+ return false;
779
+ }
780
+ /**
781
+ * 阶段归档自动 git 提交
782
+ * 目标 phase 为 done 时自动将工作区变更提交,保证归档点有版本记录。
783
+ * 非 git 仓库、git 不可用或无变更时静默跳过,不阻塞 done 推进。
784
+ * 必须在 flow.json save() 之后调用,确保 commit 包含本次 phase 变更。
785
+ * @param stageName 阶段名(用于 commit message)
786
+ */
787
+ autoCommitOnDone(stageName) {
788
+ const lang = getCliLang(process.cwd());
789
+ try {
790
+ const msg = `chore: 阶段归档 ${stageName}`;
791
+ // cwd 指向项目根,确保 git 在 flow.json 所在仓库执行而非进程工作目录
792
+ execSync(`git add -A && git commit -m "${msg}"`, { stdio: 'pipe', cwd: this.projectPath });
793
+ console.log(t('flow.advance.gitCommitOkTmpl', lang, { stage: stageName }));
794
+ }
795
+ catch {
796
+ // 不在 git 仓库 / git 不可用 / 无变更时静默跳过,不阻塞 done 推进
797
+ console.log(t('flow.advance.gitCommitSkipTmpl', lang, { stage: stageName }));
798
+ }
799
+ }
800
+ /**
801
+ * 新增流水线阶段
802
+ * @param stageId 阶段标识符(如 v4.3)
803
+ * @param initialPhase 初始流水线阶段,默认 plan_pending
804
+ */
805
+ addStage(stageId, initialPhase = 'plan_pending') {
806
+ if (!this.data) {
807
+ return;
808
+ }
809
+ if (this.data.stages[stageId]) {
810
+ throw new Error(`Stage '${stageId}' already exists`);
811
+ }
812
+ this.data.stages[stageId] = {
813
+ name: stageId,
814
+ phase: initialPhase,
815
+ status: 'planned',
816
+ deps: [],
817
+ ops: {},
818
+ };
819
+ this.data.pipeline.current = {
820
+ stage: stageId,
821
+ op: '',
822
+ };
823
+ this.appendLog({
824
+ time: '',
825
+ agent: 'flow-manager',
826
+ action: 'add_stage',
827
+ detail: { stageId, phase: initialPhase },
828
+ });
829
+ }
830
+ /**
831
+ * 推进流水线阶段(旧 API)
832
+ * @param opId 操作 ID(格式 "stage-xx.op-xxx")
833
+ * @param to 目标流水线阶段
834
+ * @param stageId 可选阶段 ID,传入时同步更新 flow.json.stages[stageId].status
835
+ * @param force 是否强制执行(非法 phase 时走模糊修正路径)
836
+ * @deprecated 请使用 advanceStagePhase(stageName, phase) 替代。内部保留 op 级逻辑后委托给 advanceStagePhase。
837
+ */
838
+ advancePhase(opId, to, stageId, force) {
839
+ console.warn('[DEPRECATED] advancePhase() 已弃用,请使用 advanceStagePhase(stageName, phase) 替代');
840
+ // ── 1. 校验目标 phase ──
841
+ let targetPhase;
842
+ const phaseResult = PipelinePhaseSchema.safeParse(to);
843
+ if (!phaseResult.success) {
844
+ if (force) {
845
+ const corrected = this.fuzzyCorrectPhase(to);
846
+ if (corrected) {
847
+ console.warn(`[WARN] Phase '${to}' 自动修正为 '${corrected}'`);
848
+ targetPhase = corrected;
849
+ }
850
+ else {
851
+ console.error(`错误: '${to}' 不是合法的 PipelinePhase 值,且无法自动修正`);
852
+ throw new Error(`非法 phase '${to}',模糊修正失败`);
853
+ }
854
+ }
855
+ else {
856
+ const validPhasesStr = PIPELINE_PHASES.join(', ');
857
+ console.error(`错误: '${to}' 不是合法的 PipelinePhase。合法值: [${validPhasesStr}]`);
858
+ return;
859
+ }
860
+ }
861
+ else {
862
+ targetPhase = phaseResult.data;
863
+ }
864
+ if (!this.data) {
865
+ return;
866
+ }
867
+ // ── 2. 解析 stageId ──
868
+ const resolvedStageId = stageId || (opId ? this.parseOpId(opId)?.stageId : this.data.pipeline.current.stage);
869
+ if (!resolvedStageId) {
870
+ console.warn('[DEPRECATED] advancePhase: 无法解析 stageId,跳过');
871
+ return;
872
+ }
873
+ // ── 3. Op 级工作(检查点更新、重试计数) ──
874
+ if (opId) {
875
+ const op = this.getOp(opId);
876
+ if (op) {
877
+ const parts = this.parseOpId(opId);
878
+ if (parts) {
879
+ const prevStage = this.data.pipeline.current.stage;
880
+ const prevOp = this.data.pipeline.current.op;
881
+ // 更新 pipeline current(用于 op 级追踪)
882
+ this.data.pipeline.current = { stage: parts.stageId, op: parts.opLocalId };
883
+ // REV-002: 切换到新操作时重置重试计数
884
+ if (prevStage !== parts.stageId || prevOp !== parts.opLocalId) {
885
+ this.data.pipeline.retry = 0;
886
+ }
887
+ // 根据目标 phase 更新对应的 checkpoint
888
+ const checkpointKey = this.getCheckpointFromPhase(targetPhase);
889
+ if (checkpointKey) {
890
+ if (checkpointKey === 'exec') {
891
+ if (targetPhase === 'exec_running') {
892
+ op.checkpoints.exec.self = 'running';
893
+ }
894
+ }
895
+ else {
896
+ if (targetPhase.endsWith('_passed')) {
897
+ op.checkpoints[checkpointKey] = 'passed';
898
+ }
899
+ else if (targetPhase.endsWith('_failed')) {
900
+ op.checkpoints[checkpointKey] = 'failed';
901
+ }
902
+ else {
903
+ op.checkpoints[checkpointKey] = 'pending';
904
+ }
905
+ }
906
+ }
907
+ }
908
+ }
909
+ }
910
+ // ── 4. 委托给 advanceStagePhase(处理 stage phase 更新、pipeline.phase、日志、stage status、统计) ──
911
+ this.advanceStagePhase(resolvedStageId, targetPhase);
912
+ }
913
+ // ── 阶段跳转检测 ──
914
+ /**
915
+ * 获取从指定 phase 可达的所有目标 phase(支持组合条件 key)
916
+ * 遍历 transitions 表,key 与 fromPhase 匹配(组合 key 如 'test_passed|review_passed' 任一条件匹配即可)即收集其目标。
917
+ * 无 '|' 的简单 key 保持单条件原行为(精确相等)。
918
+ * @param fromPhase 当前源 phase
919
+ * @returns 去重后的可达目标 phase 列表
920
+ */
921
+ getValidTargets(fromPhase) {
922
+ const transitions = this.pipelineConfig?.transitions ?? {};
923
+ const result = new Set();
924
+ for (const [key, targets] of Object.entries(transitions)) {
925
+ if (transitionKeyMatches(key, fromPhase)) {
926
+ for (const target of targets) {
927
+ result.add(target);
928
+ }
929
+ }
930
+ }
931
+ return [...result];
932
+ }
933
+ /**
934
+ * 检查当前 phase 到目标 phase 是否存在直接跳转路径
935
+ * 供 CLI --force 判断使用
936
+ * @param stageName 可选阶段名,提供时使用该阶段 phase 进行查找;否则使用 current.stage 的 phase
937
+ */
938
+ hasTransition(to, stageName) {
939
+ if (!this.data || !this.pipelineConfig) {
940
+ return false;
941
+ }
942
+ const phase = this.resolveCurrentPhase(stageName);
943
+ if (!phase)
944
+ return false;
945
+ return this.getValidTargets(phase).includes(to);
946
+ }
947
+ /**
948
+ * 获取当前阶段的所有可达下一阶段(基于 transitions 表,支持组合条件)
949
+ * 供 flow wizard 交互模式使用
950
+ * @param stageName 可选阶段名,提供时使用该阶段 phase 进行查找;否则使用 current.stage 的 phase
951
+ */
952
+ getAvailablePhases(stageName) {
953
+ if (!this.data) {
954
+ return [];
955
+ }
956
+ if (!this.pipelineConfig) {
957
+ return [];
958
+ }
959
+ const currentPhase = this.resolveCurrentPhase(stageName);
960
+ if (!currentPhase)
961
+ return [];
962
+ return this.getValidTargets(currentPhase);
963
+ }
964
+ /**
965
+ * 解析当前阶段 phase:优先使用传入的 stageName,其次 current.stage,最后 fallback
966
+ */
967
+ resolveCurrentPhase(stageName) {
968
+ if (!this.data)
969
+ return null;
970
+ const targetStageName = stageName || this.data.pipeline.current.stage;
971
+ if (!targetStageName)
972
+ return null;
973
+ const stage = this.data.stages[targetStageName];
974
+ return stage?.phase ?? null;
975
+ }
976
+ /**
977
+ * 获取流水线阶段的标签映射
978
+ * 从 pipelineConfig.phases 动态生成:已知阶段使用预定义标签,未知阶段自动生成回退标签
979
+ * 供 flow wizard 交互模式使用
980
+ * @param lang 语言标识('zh-CN' | 'en'),默认 'zh-CN'
981
+ */
982
+ getPhaseLabels(lang = 'zh-CN') {
983
+ // 内置标签映射(含所有标准阶段,双语)
984
+ const zhLabels = {
985
+ plan_pending: '计划待定',
986
+ plan_review: '计划审查中',
987
+ plan_passed: '计划已通过',
988
+ scheme_pending: '方案待定',
989
+ scheme_review: '方案审查中',
990
+ scheme_passed: '方案已通过',
991
+ exec_running: '执行中',
992
+ review_pending: '审查待定',
993
+ review_failed: '审查未通过',
994
+ review_passed: '审查已通过',
995
+ test_pending: '测试待定',
996
+ test_failed: '测试未通过',
997
+ test_passed: '测试已通过',
998
+ archiving: '归档中',
999
+ done: '已完成',
1000
+ };
1001
+ const enLabels = {
1002
+ plan_pending: 'Plan Pending',
1003
+ plan_review: 'Plan Reviewing',
1004
+ plan_passed: 'Plan Passed',
1005
+ scheme_pending: 'Scheme Pending',
1006
+ scheme_review: 'Scheme Reviewing',
1007
+ scheme_passed: 'Scheme Passed',
1008
+ exec_running: 'Executing',
1009
+ review_pending: 'Review Pending',
1010
+ review_failed: 'Review Failed',
1011
+ review_passed: 'Review Passed',
1012
+ test_pending: 'Test Pending',
1013
+ test_failed: 'Test Failed',
1014
+ test_passed: 'Test Passed',
1015
+ archiving: 'Archiving',
1016
+ done: 'Completed',
1017
+ };
1018
+ const builtinLabels = lang === 'en' ? enLabels : zhLabels;
1019
+ // 从 pipelineConfig 获取完整阶段列表
1020
+ const phases = this.pipelineConfig?.phases ?? [];
1021
+ // 动态生成标签:优先使用预定义标签,未知阶段生成回退标签
1022
+ const labels = {};
1023
+ for (const phase of phases) {
1024
+ if (builtinLabels[phase]) {
1025
+ labels[phase] = builtinLabels[phase];
1026
+ }
1027
+ else {
1028
+ // 自动生成回退标签:将下划线替换为空格作为可读名称
1029
+ labels[phase] = phase.replace(/_/g, ' ');
1030
+ }
1031
+ }
1032
+ return labels;
1033
+ }
1034
+ /**
1035
+ * verbose 模式摘要(返回结构化数据供命令层排版)
1036
+ * 包含:配置级联、最近状态变更、下游 Agent 就绪状态
1037
+ * @param maxChanges 最近状态变更条数(默认 5)
1038
+ */
1039
+ verboseSummary(maxChanges = 5) {
1040
+ const basic = this.getSummary();
1041
+ // ── 配置级联状态 ──
1042
+ const cascade = this.buildCascadeConfig();
1043
+ // ── 最近 N 条状态变更(从 status.md 的状态记录表提取) ──
1044
+ const recentChanges = this.extractRecentChanges(maxChanges);
1045
+ // ── 下游 Agent 就绪状态 ──
1046
+ const downstreamPhases = this.buildDownstreamPhases();
1047
+ return { basic, cascade, recentChanges, downstreamPhases };
1048
+ }
1049
+ /** 构建配置级联信息 */
1050
+ buildCascadeConfig() {
1051
+ const configDefaults = {};
1052
+ const statusOverrides = {};
1053
+ const effective = {};
1054
+ // 读取 config.yaml defaults
1055
+ const configPath = resolve(this.projectPath, '.openfeel', 'config.yaml');
1056
+ if (existsSync(configPath)) {
1057
+ try {
1058
+ const raw = readFileSync(configPath, 'utf-8');
1059
+ const config = parseYaml(raw);
1060
+ if (config && typeof config === 'object' && config.defaults) {
1061
+ const defaults = config.defaults;
1062
+ for (const [key, value] of Object.entries(defaults)) {
1063
+ configDefaults[key] = String(value);
1064
+ effective[key] = String(value);
1065
+ }
1066
+ }
1067
+ }
1068
+ catch {
1069
+ // 解析失败则用空值
1070
+ }
1071
+ }
1072
+ // 读取当前 stage 的 status.md(如果存在)
1073
+ if (this.data && this.data.pipeline.current.stage) {
1074
+ const stageId = this.data.pipeline.current.stage;
1075
+ const statusPath = this.findStatusPath(stageId);
1076
+ if (statusPath) {
1077
+ try {
1078
+ const content = readFileSync(statusPath, 'utf-8');
1079
+ // 匹配执行模式、自动推进
1080
+ const execMatch = content.match(/\*\*执行模式\*\*[::]\s*(manual|auto)/);
1081
+ const autoMatch = content.match(/\*\*自动推进\*\*[::]\s*(disabled|enabled)/);
1082
+ if (execMatch) {
1083
+ statusOverrides['execution_mode'] = execMatch[1];
1084
+ effective['execution_mode'] = execMatch[1];
1085
+ }
1086
+ if (autoMatch) {
1087
+ statusOverrides['auto_advance'] = autoMatch[1];
1088
+ effective['auto_advance'] = autoMatch[1];
1089
+ }
1090
+ }
1091
+ catch {
1092
+ // 读取失败则跳过
1093
+ }
1094
+ }
1095
+ }
1096
+ return { configDefaults, statusOverrides, effective };
1097
+ }
1098
+ /** 查找 status.md 的路径(先查 stages 目录,再回退 plan 目录) */
1099
+ findStatusPath(stageId) {
1100
+ const stagesDir = resolve(this.projectPath, '.openfeel', 'stages', stageId, 'status.md');
1101
+ if (existsSync(stagesDir)) {
1102
+ return stagesDir;
1103
+ }
1104
+ const planDir = resolve(this.projectPath, '.openfeel', 'plan', stageId, 'status.md');
1105
+ if (existsSync(planDir)) {
1106
+ return planDir;
1107
+ }
1108
+ return null;
1109
+ }
1110
+ /** 从 status.md 的状态记录表提取最近 N 条变更 */
1111
+ extractRecentChanges(maxChanges) {
1112
+ const results = [];
1113
+ if (!this.data || !this.data.pipeline.current.stage) {
1114
+ return results;
1115
+ }
1116
+ const statusPath = this.findStatusPath(this.data.pipeline.current.stage);
1117
+ if (!statusPath) {
1118
+ return results;
1119
+ }
1120
+ try {
1121
+ const content = readFileSync(statusPath, 'utf-8');
1122
+ // 找到 ## 状态记录 之后的表格
1123
+ const recordIdx = content.indexOf('## 状态记录');
1124
+ if (recordIdx === -1) {
1125
+ return results;
1126
+ }
1127
+ const afterRecord = content.substring(recordIdx);
1128
+ // 按行分割查找表格行(跳过表头和分隔线)
1129
+ const lines = afterRecord.split(/\r?\n/);
1130
+ let inTable = false;
1131
+ for (const line of lines) {
1132
+ const trimmed = line.trim();
1133
+ // 跳过表头
1134
+ if (trimmed.startsWith('| 时间') || trimmed.startsWith('|------')) {
1135
+ inTable = true;
1136
+ continue;
1137
+ }
1138
+ if (!inTable) {
1139
+ continue;
1140
+ }
1141
+ // 空行或非表格行结束
1142
+ if (!trimmed.startsWith('|')) {
1143
+ break;
1144
+ }
1145
+ // 解析表格行: | 时间 | Agent | 状态变化 | 说明 |
1146
+ const cells = trimmed.split('|').map((c) => c.trim()).filter(Boolean);
1147
+ if (cells.length >= 4) {
1148
+ results.push({
1149
+ time: cells[0],
1150
+ agent: cells[1],
1151
+ change: cells[2],
1152
+ description: cells[3],
1153
+ });
1154
+ }
1155
+ }
1156
+ }
1157
+ catch {
1158
+ // 读取失败
1159
+ }
1160
+ // 返回最近 N 条(倒序取最后)
1161
+ return results.slice(-maxChanges);
1162
+ }
1163
+ /** 构建下游阶段列表(当前 phase 的可达下一阶段 + Agent 映射) */
1164
+ buildDownstreamPhases() {
1165
+ const available = this.getAvailablePhases();
1166
+ const labels = this.getPhaseLabels();
1167
+ const result = [];
1168
+ for (const phase of available) {
1169
+ result.push({
1170
+ phase,
1171
+ label: labels[phase] ?? phase.replace(/_/g, ' '),
1172
+ responsibleAgent: this.mapPhaseToAgent(phase),
1173
+ });
1174
+ }
1175
+ return result;
1176
+ }
1177
+ /** 将 PipelinePhase 映射为负责 Agent 标识 */
1178
+ mapPhaseToAgent(phase) {
1179
+ const prefix = phase.split('_')[0];
1180
+ switch (prefix) {
1181
+ case 'plan':
1182
+ return 'planner';
1183
+ case 'scheme':
1184
+ return 'schemer';
1185
+ case 'exec':
1186
+ return 'executor';
1187
+ case 'review':
1188
+ return 'reviewer';
1189
+ case 'test':
1190
+ return 'feel-tester';
1191
+ case 'archiving':
1192
+ return 'archiver';
1193
+ case 'done':
1194
+ return 'none';
1195
+ default:
1196
+ return 'unknown';
1197
+ }
1198
+ }
1199
+ /** 从 PipelinePhase 提取对应的 checkpoint 字段名(从 pipeline.yaml 映射查找) */
1200
+ getCheckpointFromPhase(phase) {
1201
+ const prefix = phase.split('_')[0];
1202
+ return this.pipelineConfig?.checkpoint_mapping[prefix] ?? null;
1203
+ }
1204
+ /**
1205
+ * 记录一次操作执行结果
1206
+ * @returns 包含是否应重试、是否应重新规划的信息
1207
+ */
1208
+ recordAttempt(opId, result) {
1209
+ if (!this.data) {
1210
+ return { shouldRetry: false, shouldReplan: false };
1211
+ }
1212
+ const op = this.getOp(opId);
1213
+ if (!op) {
1214
+ return { shouldRetry: false, shouldReplan: false };
1215
+ }
1216
+ op.attempts += 1;
1217
+ if (result === 'pass') {
1218
+ op.state = 'done';
1219
+ this.data.pipeline.retry = 0;
1220
+ this.appendLog({
1221
+ time: '',
1222
+ agent: 'executor',
1223
+ action: 'attempt_pass',
1224
+ detail: { opId, attempts: op.attempts },
1225
+ });
1226
+ // 公共日志:记录执行通过(审计链)
1227
+ this.publicLogger.logPhaseChange({
1228
+ action: 'attempt_pass',
1229
+ opId,
1230
+ extra: { attempts: op.attempts },
1231
+ });
1232
+ return { shouldRetry: false, shouldReplan: false };
1233
+ }
1234
+ // result === 'fail'
1235
+ if (op.attempts < op.max_attempts) {
1236
+ op.state = 'pending'; // 回到 pending 等待重试
1237
+ this.data.pipeline.retry += 1;
1238
+ this.appendLog({
1239
+ time: '',
1240
+ agent: 'executor',
1241
+ action: 'attempt_fail_retry',
1242
+ detail: { opId, attempts: op.attempts, maxAttempts: op.max_attempts },
1243
+ });
1244
+ // 公共日志:记录执行失败-重试(审计链)
1245
+ this.publicLogger.logPhaseChange({
1246
+ action: 'attempt_fail_retry',
1247
+ opId,
1248
+ extra: { attempts: op.attempts, maxAttempts: op.max_attempts },
1249
+ });
1250
+ return { shouldRetry: true, shouldReplan: false };
1251
+ }
1252
+ // 重试耗尽
1253
+ op.state = 'failed';
1254
+ this.data.pipeline.retry += 1;
1255
+ this.appendLog({
1256
+ time: '',
1257
+ agent: 'executor',
1258
+ action: 'attempt_fail_exhausted',
1259
+ detail: { opId, attempts: op.attempts, maxAttempts: op.max_attempts },
1260
+ });
1261
+ // 公共日志:记录执行失败-耗尽(审计链)
1262
+ this.publicLogger.logPhaseChange({
1263
+ action: 'attempt_fail_exhausted',
1264
+ opId,
1265
+ extra: { attempts: op.attempts, maxAttempts: op.max_attempts },
1266
+ });
1267
+ return { shouldRetry: false, shouldReplan: true };
1268
+ }
1269
+ /** 添加或更新审查条目 */
1270
+ addReview(item) {
1271
+ if (!this.data) {
1272
+ return;
1273
+ }
1274
+ const idx = this.data.reviews.findIndex((r) => r.id === item.id);
1275
+ if (idx !== -1) {
1276
+ this.data.reviews[idx] = item;
1277
+ }
1278
+ else {
1279
+ this.data.reviews.push(item);
1280
+ }
1281
+ // 公共日志:记录审查事件(审计链)
1282
+ this.publicLogger.logReviewEvent({
1283
+ action: 'review_added',
1284
+ opId: item.op,
1285
+ extra: { reviewId: item.id, status: item.status, priority: item.priority, title: item.title },
1286
+ });
1287
+ }
1288
+ /** 将指定审查条目标记为 resolved */
1289
+ resolveReview(reviewId) {
1290
+ if (!this.data) {
1291
+ return false;
1292
+ }
1293
+ const review = this.data.reviews.find((r) => r.id === reviewId);
1294
+ if (!review) {
1295
+ return false;
1296
+ }
1297
+ review.status = 'resolved';
1298
+ return true;
1299
+ }
1300
+ /**
1301
+ * 添加自动修复审查条目
1302
+ * 当 Reviewer 认为问题可直接修复时调用,REV 条目状态直接设为 resolved,
1303
+ * pipeline.phase 跳过 review_failed→scheme_pending,直接推进到 exec_running。
1304
+ * @param item 审查条目(canAutoFix 自动设为 true)
1305
+ * @param opId 关联的操作 ID
1306
+ */
1307
+ addAutoFixReview(item, opId) {
1308
+ if (!this.data) {
1309
+ return;
1310
+ }
1311
+ // 校验 opId 格式:必须包含 '.',且 stage 必须存在于 stages 中
1312
+ if (!opId.includes('.')) {
1313
+ console.error(`错误:opId 格式不正确 "${opId}",应为 stage-xx.op-xxx`);
1314
+ return;
1315
+ }
1316
+ const stageId = opId.substring(0, opId.lastIndexOf('.'));
1317
+ const stage = this.data.stages[stageId];
1318
+ if (!stage) {
1319
+ console.error(`错误:opId "${opId}" 中的 stage "${stageId}" 不存在`);
1320
+ return;
1321
+ }
1322
+ // 前置条件:仅允许从 review_failed 状态调用,检查对应 stage 的 phase
1323
+ if (stage.phase !== 'review_failed') {
1324
+ console.warn(`[WARN] addAutoFixReview 仅允许从 review_failed 状态调用,当前 stage ${stageId} phase 为 ${stage.phase}`);
1325
+ return;
1326
+ }
1327
+ // 标记为自动修复
1328
+ item.canAutoFix = true;
1329
+ // 添加审查条目(状态直接为 resolved,跳过 pending→fixing 流程)
1330
+ item.status = 'resolved';
1331
+ this.addReview(item);
1332
+ // 推进流水线:跳过 review_failed→scheme_pending,直通 exec_running
1333
+ this.advancePhase(opId, 'exec_running');
1334
+ // 写入日志
1335
+ this.appendLog({
1336
+ time: '',
1337
+ agent: 'flow-manager',
1338
+ action: 'auto_fix_review',
1339
+ detail: { opId, reviewId: item.id, detail: item.autoFixDetail ?? '' },
1340
+ });
1341
+ // 公共日志:记录自动修复审查事件(审计链)
1342
+ // 注: advancePhase 和 addReview 调用已各自记录其事件,此处记录 auto_fix 专项事件
1343
+ this.publicLogger.logReviewEvent({
1344
+ action: 'auto_fix_review',
1345
+ opId,
1346
+ extra: { reviewId: item.id, autoFixDetail: item.autoFixDetail ?? '' },
1347
+ });
1348
+ }
1349
+ /** 追加操作日志(若 time 为空则自动生成) */
1350
+ appendLog(entry) {
1351
+ if (!this.data) {
1352
+ return;
1353
+ }
1354
+ const logEntry = {
1355
+ ...entry,
1356
+ time: entry.time || new Date().toISOString(),
1357
+ };
1358
+ this.data.log.push(logEntry);
1359
+ }
1360
+ // ═══ 校验 ═══
1361
+ /**
1362
+ * 校验 phase 流转是否合法(从 pipeline.yaml 配置驱动)
1363
+ * 使用 opId 对应的 stage.phase 作为当前阶段进行流转校验
1364
+ */
1365
+ canAdvance(opId, to) {
1366
+ if (!this.data) {
1367
+ return false;
1368
+ }
1369
+ // 检查 opId 指向的 op 是否存在
1370
+ const op = this.getOp(opId);
1371
+ if (!op) {
1372
+ return false;
1373
+ }
1374
+ // 若 pipelineConfig 未加载,无法校验
1375
+ if (!this.pipelineConfig) {
1376
+ return false;
1377
+ }
1378
+ // 从 opId 解析出 stage,使用 stage.phase 而非全局 pipeline.phase
1379
+ const parts = this.parseOpId(opId);
1380
+ if (!parts || !this.data.stages[parts.stageId]) {
1381
+ return false;
1382
+ }
1383
+ const currentPhase = this.data.stages[parts.stageId].phase;
1384
+ // 检查从当前 phase 到目标 phase 的流转是否合法(支持组合条件 key)
1385
+ const validTargets = this.getValidTargets(currentPhase);
1386
+ if (!validTargets.includes(to)) {
1387
+ return false;
1388
+ }
1389
+ return true;
1390
+ }
1391
+ /**
1392
+ * 校验 flow.json 格式合法性
1393
+ * 检查必填字段是否存在,对非法 phase 值进行模糊匹配自动修正。
1394
+ * 可自动修正的问题记录在 warnings 中而不影响 valid 判定。
1395
+ */
1396
+ validate() {
1397
+ const errors = [];
1398
+ const warnings = [];
1399
+ if (!this.data) {
1400
+ errors.push('flow.json 未加载或不存在');
1401
+ return { valid: false, errors, warnings };
1402
+ }
1403
+ // 检查 meta 必填字段
1404
+ if (!this.data.meta?.version) {
1405
+ errors.push('meta.version 缺失');
1406
+ }
1407
+ if (!this.data.meta?.project) {
1408
+ errors.push('meta.project 缺失');
1409
+ }
1410
+ // 检查 pipeline 必填字段
1411
+ if (!this.data.pipeline?.phase) {
1412
+ errors.push('pipeline.phase 缺失');
1413
+ }
1414
+ // pipeline.phase 校验:使用 MetaPhaseSchema(全局宏观状态:active/paused/done)
1415
+ if (this.data.pipeline?.phase) {
1416
+ const phaseResult = MetaPhaseSchema.safeParse(this.data.pipeline.phase);
1417
+ if (!phaseResult.success) {
1418
+ // 尝试简单修正:若非 MetaPhase 值,默认为 'active'
1419
+ const corrected = this.fuzzyCorrectMetaPhase(this.data.pipeline.phase);
1420
+ if (corrected) {
1421
+ warnings.push(`pipeline.phase '${this.data.pipeline.phase}' 自动修正为 '${corrected}'`);
1422
+ this.data.pipeline.phase = corrected;
1423
+ }
1424
+ else {
1425
+ errors.push(`pipeline.phase 值 "${this.data.pipeline.phase}" 不是合法的 MetaPhase 枚举值(active/paused/done),且无法自动修正`);
1426
+ }
1427
+ }
1428
+ }
1429
+ if (!this.data.pipeline?.current) {
1430
+ errors.push('pipeline.current 缺失');
1431
+ }
1432
+ // 检查 stages 是否为对象
1433
+ if (typeof this.data.stages !== 'object' || this.data.stages === null) {
1434
+ errors.push('stages 不是有效对象');
1435
+ }
1436
+ else {
1437
+ // 遍历每个 stage,校验其 phase 字段为合法 PipelinePhase
1438
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
1439
+ if (!stage.phase) {
1440
+ warnings.push(`stages.${stageId}.phase 缺失,设为默认 'plan_pending'`);
1441
+ stage.phase = 'plan_pending';
1442
+ }
1443
+ else {
1444
+ const stagePhaseResult = PipelinePhaseSchema.safeParse(stage.phase);
1445
+ if (!stagePhaseResult.success) {
1446
+ const corrected = this.fuzzyCorrectPhase(stage.phase);
1447
+ if (corrected) {
1448
+ warnings.push(`stages.${stageId}.phase '${stage.phase}' 自动修正为 '${corrected}'`);
1449
+ stage.phase = corrected;
1450
+ }
1451
+ else {
1452
+ errors.push(`stages.${stageId}.phase 值 "${stage.phase}" 不是合法的 PipelinePhase 枚举值,且无法自动修正`);
1453
+ }
1454
+ }
1455
+ }
1456
+ // 检查 phase 与 status 不一致(非阻塞警告)
1457
+ if (stage.phase && stage.status) {
1458
+ if (stage.status === 'done' && stage.phase !== 'done') {
1459
+ warnings.push(`${stageId}: status=done 但 phase=${stage.phase},不一致(非阻塞)`);
1460
+ }
1461
+ else if (stage.phase === 'done' && stage.status !== 'done') {
1462
+ warnings.push(`${stageId}: phase=done 但 status=${stage.status},不一致(非阻塞)`);
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+ // 检查 reviews 是否为数组
1468
+ if (!Array.isArray(this.data.reviews)) {
1469
+ errors.push('reviews 不是有效数组');
1470
+ }
1471
+ // 检查 log 是否为数组
1472
+ if (!Array.isArray(this.data.log)) {
1473
+ errors.push('log 不是有效数组');
1474
+ }
1475
+ return { valid: errors.length === 0, errors, warnings };
1476
+ }
1477
+ /**
1478
+ * 模糊匹配非法 phase 值到最近合法值
1479
+ * 优先级: 精确匹配 → phase_corrections 映射 → 前缀匹配 → 包含匹配
1480
+ */
1481
+ fuzzyCorrectPhase(input) {
1482
+ // 先尝试 phase_corrections 精确映射(来自 pipeline.yaml 配置)
1483
+ const phaseCorrections = this.pipelineConfig?.phase_corrections ?? {};
1484
+ if (phaseCorrections[input]) {
1485
+ return phaseCorrections[input];
1486
+ }
1487
+ // 标准化: 去除首尾空格,转小写,统一分隔符,去除首尾下划线
1488
+ const normalized = input.trim().toLowerCase().replace(/[\s_-]+/g, '_').replace(/^_|_$/g, '');
1489
+ // 直接匹配合法值
1490
+ if (PIPELINE_PHASES.includes(normalized)) {
1491
+ return normalized;
1492
+ }
1493
+ // 前缀匹配: 如 'plan' → 'plan_pending', 'review' → 'review_pending'
1494
+ const prefixMatches = PIPELINE_PHASES.filter((p) => p.startsWith(normalized + '_'));
1495
+ if (prefixMatches.length === 1) {
1496
+ return prefixMatches[0];
1497
+ }
1498
+ // 插入下划线后再试前缀匹配: 如 'planpending' → 'plan_pending'
1499
+ for (const phase of PIPELINE_PHASES) {
1500
+ const stripped = phase.replace(/_/g, '');
1501
+ if (stripped === normalized) {
1502
+ return phase;
1503
+ }
1504
+ }
1505
+ // 包含匹配(唯一命中时采用)
1506
+ const containsMatches = PIPELINE_PHASES.filter((p) => p.includes(normalized));
1507
+ if (containsMatches.length === 1) {
1508
+ return containsMatches[0];
1509
+ }
1510
+ // 后缀匹配: 查找 normalized 是否为某合法值的后缀
1511
+ for (const phase of PIPELINE_PHASES) {
1512
+ if (phase.endsWith('_' + normalized) || phase.endsWith(normalized)) {
1513
+ return phase;
1514
+ }
1515
+ }
1516
+ // 常见拼写修正(硬编码扩展)
1517
+ const extraCorrections = {
1518
+ 'planned': 'plan_pending',
1519
+ 'planning': 'plan_pending',
1520
+ 'plan_done': 'plan_passed',
1521
+ 'plans_passed': 'plan_passed',
1522
+ 'scheme_planned': 'scheme_pending',
1523
+ 'scheme_done': 'scheme_passed',
1524
+ 'exec': 'exec_running',
1525
+ 'exec_pending': 'scheme_passed',
1526
+ 'running': 'exec_running',
1527
+ 'review': 'review_pending',
1528
+ 'review_done': 'review_passed',
1529
+ 'reviews_passed': 'review_passed',
1530
+ 'testing': 'test_pending',
1531
+ 'test': 'test_pending',
1532
+ 'test_done': 'test_passed',
1533
+ 'archive': 'archiving',
1534
+ 'archived': 'done',
1535
+ 'finished': 'done',
1536
+ 'completed': 'done',
1537
+ 'complete': 'done',
1538
+ 'end': 'done',
1539
+ };
1540
+ if (extraCorrections[normalized]) {
1541
+ return extraCorrections[normalized];
1542
+ }
1543
+ return null;
1544
+ }
1545
+ /**
1546
+ * 模糊修正非法 MetaPhase 值
1547
+ * 将所有非 MetaPhase 的 pipeline.phase 默认为 'active'
1548
+ */
1549
+ fuzzyCorrectMetaPhase(input) {
1550
+ const normalized = input.trim().toLowerCase();
1551
+ if (META_PHASES.includes(normalized)) {
1552
+ return normalized;
1553
+ }
1554
+ // 任何 PipelinePhase 值或未知值皆默认映射为 'active'
1555
+ return 'active';
1556
+ }
1557
+ // ═══ 修复 ═══
1558
+ /**
1559
+ * 自动检测并修复 flow.json 中的常见问题
1560
+ * @param dryRun true 时仅检测不修复
1561
+ * @returns 修复结果(修改列表 + 是否从 .bak 恢复)
1562
+ */
1563
+ repair(dryRun = false) {
1564
+ const changes = [];
1565
+ let recovered = false;
1566
+ // 尝试加载 flow.json
1567
+ if (!existsSync(this.filePath)) {
1568
+ if (dryRun) {
1569
+ changes.push('检测到 flow.json 不存在(dry-run 模式下不自动创建)');
1570
+ return { fixed: false, changes, recovered: false };
1571
+ }
1572
+ FlowManager.initFlow(this.projectPath);
1573
+ changes.push('flow.json 不存在,已创建默认 flow.json');
1574
+ return { fixed: true, changes, recovered: false };
1575
+ }
1576
+ // 尝试解析 JSON,失败时从 .bak 恢复
1577
+ let raw;
1578
+ try {
1579
+ raw = readFileSync(this.filePath, 'utf-8');
1580
+ }
1581
+ catch {
1582
+ changes.push('无法读取 flow.json');
1583
+ return { fixed: false, changes, recovered: false };
1584
+ }
1585
+ let parsed;
1586
+ try {
1587
+ parsed = JSON.parse(raw);
1588
+ }
1589
+ catch {
1590
+ // JSON 解析失败,尝试从 .bak 恢复
1591
+ const bakPath = this.filePath + '.bak';
1592
+ if (existsSync(bakPath)) {
1593
+ try {
1594
+ const bakContent = readFileSync(bakPath, 'utf-8');
1595
+ parsed = JSON.parse(bakContent);
1596
+ recovered = true;
1597
+ changes.push('flow.json 解析失败,已从 .bak 恢复');
1598
+ }
1599
+ catch {
1600
+ // .bak 也损坏,无法恢复
1601
+ if (dryRun) {
1602
+ changes.push('flow.json 和 .bak 均已损坏(dry-run 模式下不自动重建)');
1603
+ return { fixed: false, changes, recovered: false };
1604
+ }
1605
+ // 删除损坏文件后重建(initFlow 仅在文件不存在时创建)
1606
+ try {
1607
+ unlinkSync(this.filePath);
1608
+ }
1609
+ catch {
1610
+ // 删除失败忽略
1611
+ }
1612
+ FlowManager.initFlow(this.projectPath);
1613
+ changes.push('flow.json 和 .bak 均已损坏,已重建默认 flow.json');
1614
+ return { fixed: true, changes, recovered: false };
1615
+ }
1616
+ }
1617
+ else {
1618
+ // 无 .bak,重建
1619
+ if (dryRun) {
1620
+ changes.push('flow.json 解析失败且无 .bak(dry-run 模式下不自动重建)');
1621
+ return { fixed: false, changes, recovered: false };
1622
+ }
1623
+ // 删除损坏文件后重建(initFlow 仅在文件不存在时创建)
1624
+ try {
1625
+ unlinkSync(this.filePath);
1626
+ }
1627
+ catch {
1628
+ // 删除失败忽略
1629
+ }
1630
+ FlowManager.initFlow(this.projectPath);
1631
+ changes.push('flow.json 解析失败且无 .bak,已重建默认 flow.json');
1632
+ return { fixed: true, changes, recovered: false };
1633
+ }
1634
+ }
1635
+ let modified = false;
1636
+ // 加载为 FlowData 进行修复
1637
+ const flowData = parsed;
1638
+ const defaults = defaultFlowData();
1639
+ // 修复缺失的 meta 字段
1640
+ if (!flowData.meta) {
1641
+ flowData.meta = defaults.meta;
1642
+ changes.push('已补全缺失的 meta');
1643
+ modified = true;
1644
+ }
1645
+ else {
1646
+ if (!flowData.meta.version) {
1647
+ flowData.meta.version = defaults.meta.version;
1648
+ changes.push('已补全缺失的 meta.version');
1649
+ modified = true;
1650
+ }
1651
+ if (!flowData.meta.project) {
1652
+ flowData.meta.project = defaults.meta.project;
1653
+ changes.push('已补全缺失的 meta.project');
1654
+ modified = true;
1655
+ }
1656
+ if (!flowData.meta.updated) {
1657
+ flowData.meta.updated = defaults.meta.updated;
1658
+ changes.push('已补全缺失的 meta.updated');
1659
+ modified = true;
1660
+ }
1661
+ }
1662
+ // 修复缺失的 pipeline 字段
1663
+ if (!flowData.pipeline) {
1664
+ flowData.pipeline = defaults.pipeline;
1665
+ changes.push('已补全缺失的 pipeline');
1666
+ modified = true;
1667
+ }
1668
+ else {
1669
+ if (!flowData.pipeline.current) {
1670
+ flowData.pipeline.current = defaults.pipeline.current;
1671
+ changes.push('已补全缺失的 pipeline.current');
1672
+ modified = true;
1673
+ }
1674
+ if (typeof flowData.pipeline.retry !== 'number') {
1675
+ flowData.pipeline.retry = defaults.pipeline.retry;
1676
+ changes.push('已修正 pipeline.retry 为默认值');
1677
+ modified = true;
1678
+ }
1679
+ // 修复非标准 pipeline.phase(MetaPhase:active/paused/done)
1680
+ if (flowData.pipeline.phase) {
1681
+ const phaseResult = MetaPhaseSchema.safeParse(flowData.pipeline.phase);
1682
+ if (!phaseResult.success) {
1683
+ const corrected = this.fuzzyCorrectMetaPhase(flowData.pipeline.phase);
1684
+ if (corrected) {
1685
+ changes.push(`pipeline.phase '${flowData.pipeline.phase}' 已修正为 '${corrected}'`);
1686
+ flowData.pipeline.phase = corrected;
1687
+ modified = true;
1688
+ }
1689
+ else {
1690
+ changes.push(`pipeline.phase '${flowData.pipeline.phase}' 无法修正,使用默认值 'active'`);
1691
+ flowData.pipeline.phase = defaults.pipeline.phase;
1692
+ modified = true;
1693
+ }
1694
+ }
1695
+ }
1696
+ else {
1697
+ flowData.pipeline.phase = defaults.pipeline.phase;
1698
+ changes.push('已补全缺失的 pipeline.phase');
1699
+ modified = true;
1700
+ }
1701
+ }
1702
+ // 修复 stages 类型
1703
+ if (!flowData.stages || typeof flowData.stages !== 'object' || Array.isArray(flowData.stages)) {
1704
+ flowData.stages = defaults.stages;
1705
+ changes.push('已补全缺失的 stages');
1706
+ modified = true;
1707
+ }
1708
+ else {
1709
+ // 修复各 stage 的 phase 字段(PipelinePhase)
1710
+ for (const [stageId, stage] of Object.entries(flowData.stages)) {
1711
+ const s = stage;
1712
+ if (s && typeof s === 'object' && s.phase) {
1713
+ const stagePhaseResult = PipelinePhaseSchema.safeParse(s.phase);
1714
+ if (!stagePhaseResult.success) {
1715
+ const corrected = this.fuzzyCorrectPhase(s.phase);
1716
+ if (corrected) {
1717
+ changes.push(`stages.${stageId}.phase '${s.phase}' 已修正为 '${corrected}'`);
1718
+ s.phase = corrected;
1719
+ modified = true;
1720
+ }
1721
+ else {
1722
+ changes.push(`stages.${stageId}.phase '${s.phase}' 无法修正,使用默认值 'plan_pending'`);
1723
+ s.phase = 'plan_pending';
1724
+ modified = true;
1725
+ }
1726
+ }
1727
+ }
1728
+ else if (s && typeof s === 'object' && !s.phase) {
1729
+ s.phase = 'plan_pending';
1730
+ changes.push(`已补全缺失的 stages.${stageId}.phase`);
1731
+ modified = true;
1732
+ }
1733
+ }
1734
+ }
1735
+ // 修复 reviews 类型
1736
+ if (!Array.isArray(flowData.reviews)) {
1737
+ flowData.reviews = defaults.reviews;
1738
+ changes.push('已补全缺失的 reviews');
1739
+ modified = true;
1740
+ }
1741
+ // 修复 log 类型
1742
+ if (!Array.isArray(flowData.log)) {
1743
+ flowData.log = defaults.log;
1744
+ changes.push('已补全缺失的 log');
1745
+ modified = true;
1746
+ }
1747
+ // 写入修复后的数据(recovered 场景下磁盘文件仍损坏,即使无字段修改也必须写回恢复内容)
1748
+ if ((modified || recovered) && !dryRun) {
1749
+ // 备份当前文件(recovered 场景下 .bak 已是有效恢复来源,避免被损坏文件覆盖)
1750
+ if (!recovered) {
1751
+ try {
1752
+ copyFileSync(this.filePath, this.filePath + '.bak');
1753
+ }
1754
+ catch {
1755
+ // 备份失败不阻塞
1756
+ }
1757
+ }
1758
+ // 使用安全写入
1759
+ const tmpPath = this.filePath + '.tmp';
1760
+ const serializable = JSON.parse(JSON.stringify(flowData));
1761
+ // 去除 op 中的 id 字段
1762
+ if (serializable.stages && typeof serializable.stages === 'object') {
1763
+ for (const stage of Object.values(serializable.stages)) {
1764
+ if (stage && typeof stage === 'object' && stage.ops) {
1765
+ for (const op of Object.values(stage.ops)) {
1766
+ if (op && typeof op === 'object') {
1767
+ delete op.id;
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ }
1773
+ const content = JSON.stringify(serializable, null, 2) + '\n';
1774
+ writeFileSync(tmpPath, content, 'utf-8');
1775
+ // 校验临时文件
1776
+ try {
1777
+ const tmpContent = readFileSync(tmpPath, 'utf-8');
1778
+ JSON.parse(tmpContent);
1779
+ }
1780
+ catch (parseErr) {
1781
+ try {
1782
+ writeFileSync(tmpPath, '', 'utf-8');
1783
+ }
1784
+ catch {
1785
+ // 清理失败不阻塞
1786
+ }
1787
+ changes.push(`修复后写入校验失败: ${parseErr.message}`);
1788
+ return { fixed: false, changes, recovered };
1789
+ }
1790
+ renameSync(tmpPath, this.filePath);
1791
+ }
1792
+ // changes 在无问题时保持空数组,CLI 层通过 changes.length === 0 判断"没问题"
1793
+ return { fixed: modified || recovered, changes, recovered };
1794
+ }
1795
+ /**
1796
+ * 自动修复指定阶段的 phase/status 不一致
1797
+ * - status=done 但 phase≠done → 同步 phase 为 done
1798
+ * - phase=done 但 status≠done → 同步 status 为 done
1799
+ * @param stageName 阶段名(如 stage-01)
1800
+ * @returns 修复结果(是否修复 + 详情)
1801
+ */
1802
+ autoRepairInconsistency(stageName) {
1803
+ if (!this.data) {
1804
+ return { fixed: false, detail: 'flow.json 未加载' };
1805
+ }
1806
+ const stage = this.data.stages[stageName];
1807
+ if (!stage) {
1808
+ return { fixed: false, detail: `阶段 '${stageName}' 不存在` };
1809
+ }
1810
+ // 修复: status=done 但 phase≠done 时,同步 phase 为 done
1811
+ if (stage.status === 'done' && stage.phase !== 'done') {
1812
+ const oldPhase = stage.phase;
1813
+ stage.phase = 'done';
1814
+ return { fixed: true, detail: `phase ${oldPhase} → done (与 status 同步)` };
1815
+ }
1816
+ // 修复: phase=done 但 status≠done 时,同步 status
1817
+ if (stage.phase === 'done' && stage.status !== 'done') {
1818
+ const oldStatus = stage.status;
1819
+ stage.status = 'done';
1820
+ return { fixed: true, detail: `status ${oldStatus} → done (与 phase 同步)` };
1821
+ }
1822
+ return { fixed: false, detail: '未检测到不一致' };
1823
+ }
1824
+ // ═══ 迁移 ═══
1825
+ /**
1826
+ * 检测是否为旧版格式(全局 PipelinePhase 而非 MetaPhase)
1827
+ * 旧版:pipeline.phase 为 PipelinePhase 值(如 "exec_running")
1828
+ * 新版:pipeline.phase 为 MetaPhase 值("active"/"paused"/"done")
1829
+ */
1830
+ needsMigration() {
1831
+ if (!this.data)
1832
+ return false;
1833
+ const phase = this.data.pipeline?.phase;
1834
+ if (!phase)
1835
+ return false;
1836
+ // 若 phase 是 PipelinePhase 值但非 MetaPhase 值 → 旧版
1837
+ return PIPELINE_PHASES.includes(phase)
1838
+ && !META_PHASES.includes(phase);
1839
+ }
1840
+ /**
1841
+ * 将旧版 flow.json(v4.0:全局 phase)迁移到新版格式(v4.1:阶段级 phase)
1842
+ *
1843
+ * 迁移逻辑:
1844
+ * 1. 旧 pipeline.phase 下沉到 current.stage 对应 stage → stages[stageId].phase = oldPhase
1845
+ * 2. 若 pipeline.current.stage 为 "-" 或空,下沉到第一个 stage
1846
+ * 3. stages 中已有 phase 的 stage 保持不变(不覆盖)
1847
+ * 4. stages 中无 phase 的 stage 默认设为 "done",但 current.stage 对应 stage 除外
1848
+ * 5. 全局 pipeline.phase 改为 "active"(若旧 phase 为 "done" 则改为 "done")
1849
+ *
1850
+ * @param dryRun 仅检测预览,不实际写入
1851
+ * @param noBackup 跳过 .bak 备份
1852
+ * @returns 迁移结果
1853
+ */
1854
+ migrate(dryRun = false, noBackup = false) {
1855
+ const changes = [];
1856
+ if (!this.data) {
1857
+ changes.push('flow.json 未加载,无法迁移');
1858
+ return { migrated: false, changes, failed: true };
1859
+ }
1860
+ // 已是新版格式
1861
+ if (!this.needsMigration()) {
1862
+ changes.push('已是新版格式,无需迁移');
1863
+ return { migrated: false, changes, failed: false };
1864
+ }
1865
+ const oldPhase = this.data.pipeline.phase;
1866
+ const currentStageId = this.data.pipeline.current?.stage || '-';
1867
+ const stageEntries = Object.entries(this.data.stages);
1868
+ changes.push(`检测到旧版格式: pipeline.phase="${oldPhase}"`);
1869
+ // ── 备份旧文件 ──
1870
+ if (!dryRun && !noBackup) {
1871
+ const bakPath = this.filePath + '.v4.0.bak';
1872
+ try {
1873
+ copyFileSync(this.filePath, bakPath);
1874
+ changes.push(`已备份旧文件: flow.json.v4.0.bak`);
1875
+ }
1876
+ catch (e) {
1877
+ changes.push(`备份失败: ${e.message}`);
1878
+ return { migrated: false, changes, failed: true };
1879
+ }
1880
+ }
1881
+ // dry-run 模式下使用数据副本,避免修改原始内存状态
1882
+ const targetData = dryRun ? JSON.parse(JSON.stringify(this.data)) : this.data;
1883
+ // ── 遍历 stages,为每个没有 phase 的 stage 分配 phase ──
1884
+ for (const [stageId, stage] of Object.entries(targetData.stages)) {
1885
+ // 已有 phase 的 stage 跳过,不覆盖
1886
+ if (stage.phase !== undefined) {
1887
+ changes.push(` ${stageId}: 已有 phase="${stage.phase}",跳过`);
1888
+ continue;
1889
+ }
1890
+ // 当前 stage → 下沉旧的 phase
1891
+ if (stageId === currentStageId && currentStageId !== '-') {
1892
+ stage.phase = oldPhase;
1893
+ changes.push(` ${stageId}: ← 下沉旧 phase="${oldPhase}"`);
1894
+ }
1895
+ else {
1896
+ stage.phase = 'done';
1897
+ changes.push(` ${stageId}: ← 设为 "done"`);
1898
+ }
1899
+ }
1900
+ // 若 current.stage 为 "-" 或空,下沉到第一个 stage
1901
+ if (currentStageId === '-' || !currentStageId) {
1902
+ const targetEntries = Object.entries(targetData.stages);
1903
+ const firstEntry = targetEntries[0];
1904
+ if (firstEntry) {
1905
+ const [firstStageId, firstStageData] = firstEntry;
1906
+ firstStageData.phase = oldPhase;
1907
+ changes.push(` ${firstStageId}: ← (current.stage 为空) 下沉旧 phase="${oldPhase}"`);
1908
+ }
1909
+ }
1910
+ // ── 更新全局 phase ──
1911
+ const newMetaPhase = oldPhase === 'done' ? 'done' : 'active';
1912
+ targetData.pipeline.phase = newMetaPhase;
1913
+ changes.push(`全局 pipeline.phase: "${oldPhase}" → "${newMetaPhase}"`);
1914
+ // ── 追加迁移日志 ──
1915
+ if (!dryRun) {
1916
+ this.appendLog({
1917
+ time: '',
1918
+ agent: 'flow-manager',
1919
+ action: 'migrate_v4.0_to_v4.1',
1920
+ detail: {
1921
+ oldPhase,
1922
+ newMetaPhase,
1923
+ currentStage: currentStageId,
1924
+ stagesUpdated: stageEntries.filter(([_, s]) => s.phase !== undefined).length,
1925
+ },
1926
+ });
1927
+ }
1928
+ return { migrated: true, changes, failed: false };
1929
+ }
1930
+ // ═══ 健康检查 ═══
1931
+ /**
1932
+ * 全面健康检查
1933
+ * 检查 flow.json、跨文件一致性、僵尸状态、config.yaml、pipeline.yaml、deps.yaml
1934
+ * @param quick true 时仅检查关键项(phase/current 合法性)
1935
+ */
1936
+ healthCheck(quick = false) {
1937
+ const items = [];
1938
+ // ── 1. flow.json 合法性 ──
1939
+ this.checkFlowJson(items);
1940
+ // ── 2. 跨文件一致性 ──
1941
+ if (!quick) {
1942
+ this.checkCrossFileConsistency(items);
1943
+ }
1944
+ // ── 3. 僵尸状态检测 ──
1945
+ if (!quick) {
1946
+ this.checkZombieStates(items);
1947
+ }
1948
+ // ── 4. config.yaml 有效性 ──
1949
+ if (!quick) {
1950
+ this.checkConfigYaml(items);
1951
+ }
1952
+ // ── 5. pipeline.yaml 合法性 ──
1953
+ if (!quick) {
1954
+ this.checkPipelineYaml(items);
1955
+ }
1956
+ // ── 6. deps.yaml 循环依赖检测 ──
1957
+ if (!quick) {
1958
+ this.checkDepsYaml(items);
1959
+ }
1960
+ const ok = items.every((i) => i.status !== 'fail');
1961
+ return { items, ok };
1962
+ }
1963
+ /** 1. 检查 flow.json 合法性 */
1964
+ checkFlowJson(items) {
1965
+ const fp = resolve(this.projectPath, '.openfeel', 'flow.json');
1966
+ if (!existsSync(fp)) {
1967
+ items.push({ section: 'flow.json', status: 'fail', message: 'flow.json 不存在' });
1968
+ return;
1969
+ }
1970
+ // JSON 可解析
1971
+ try {
1972
+ const raw = readFileSync(fp, 'utf-8');
1973
+ const parsed = JSON.parse(raw);
1974
+ if (!parsed || typeof parsed !== 'object') {
1975
+ items.push({ section: 'flow.json', status: 'fail', message: 'flow.json JSON 解析后不是有效对象' });
1976
+ return;
1977
+ }
1978
+ }
1979
+ catch (e) {
1980
+ items.push({ section: 'flow.json', status: 'fail', message: `flow.json 解析失败: ${e.message}` });
1981
+ return;
1982
+ }
1983
+ // 数据已加载时检查内部字段
1984
+ if (!this.data) {
1985
+ items.push({ section: 'flow.json', status: 'warn', message: 'flow.json 可解析但 FlowManager 未加载数据' });
1986
+ return;
1987
+ }
1988
+ // pipeline.phase 合法性(MetaPhase:active/paused/done)
1989
+ const metaPhase = this.data.pipeline?.phase;
1990
+ if (!metaPhase) {
1991
+ items.push({ section: 'flow.json', status: 'fail', message: 'pipeline.phase 缺失' });
1992
+ }
1993
+ else {
1994
+ const phaseResult = MetaPhaseSchema.safeParse(metaPhase);
1995
+ if (!phaseResult.success) {
1996
+ items.push({ section: 'flow.json', status: 'fail', message: `pipeline.phase="${metaPhase}" 不是合法 MetaPhase 枚举值(active/paused/done)` });
1997
+ }
1998
+ else {
1999
+ items.push({ section: 'flow.json', status: 'pass', message: `pipeline.phase=${metaPhase},合法` });
2000
+ }
2001
+ }
2002
+ // current 指向的 stage/op 是否存在
2003
+ const cur = this.data.pipeline?.current;
2004
+ if (cur?.stage && cur?.op) {
2005
+ const stage = this.data.stages[cur.stage];
2006
+ if (!stage) {
2007
+ items.push({ section: 'flow.json', status: 'fail', message: `current 指向不存在的 stage: ${cur.stage}` });
2008
+ }
2009
+ else if (!stage.ops[cur.op]) {
2010
+ items.push({ section: 'flow.json', status: 'fail', message: `current 指向不存在的 op: ${cur.stage}.${cur.op}` });
2011
+ }
2012
+ else {
2013
+ items.push({ section: 'flow.json', status: 'pass', message: `current=${cur.stage}.${cur.op},存在` });
2014
+ }
2015
+ }
2016
+ // 各 stage phase 合法性(PipelinePhase)
2017
+ if (this.data.stages) {
2018
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
2019
+ if (!stage.phase) {
2020
+ items.push({ section: 'flow.json', status: 'warn', message: `${stageId}.phase 缺失` });
2021
+ }
2022
+ else {
2023
+ const stagePhaseResult = PipelinePhaseSchema.safeParse(stage.phase);
2024
+ if (!stagePhaseResult.success) {
2025
+ items.push({ section: 'flow.json', status: 'fail', message: `${stageId}.phase="${stage.phase}" 不是合法 PipelinePhase 枚举值` });
2026
+ }
2027
+ else {
2028
+ items.push({ section: 'flow.json', status: 'pass', message: `${stageId}.phase=${stage.phase},合法` });
2029
+ }
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2034
+ /** 2. 检查 flow.json 与 plan/{stage}/status.md 跨文件一致性 */
2035
+ checkCrossFileConsistency(items) {
2036
+ if (!this.data) {
2037
+ return;
2038
+ }
2039
+ const planDir = resolve(this.projectPath, '.openfeel', 'plan');
2040
+ // 同时检查 .openfeel/stages/ 目录(代码实际使用路径)
2041
+ const stagesDir = resolve(this.projectPath, '.openfeel', 'stages');
2042
+ let total = 0;
2043
+ let consistent = 0;
2044
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
2045
+ // 尝试 .openfeel/stages/{stageId}/status.md(代码实际路径)
2046
+ let statusPath = resolve(stagesDir, stageId, 'status.md');
2047
+ if (!existsSync(statusPath)) {
2048
+ // 回退到 .openfeel/plan/{stageId}/status.md(文档声明路径)
2049
+ statusPath = resolve(planDir, stageId, 'status.md');
2050
+ if (!existsSync(statusPath)) {
2051
+ continue;
2052
+ }
2053
+ }
2054
+ total++;
2055
+ try {
2056
+ const content = readFileSync(statusPath, 'utf-8');
2057
+ // 查找状态行:**状态**:planned
2058
+ const statusMatch = content.match(/\*\*状态\*\*[::]\s*(\w+)/);
2059
+ if (statusMatch) {
2060
+ const fileStatus = statusMatch[1];
2061
+ if (fileStatus === stage.status) {
2062
+ consistent++;
2063
+ }
2064
+ else {
2065
+ items.push({ section: '跨文件一致性', status: 'warn', message: `${stageId}: flow.json 状态="${stage.status}",status.md 状态="${fileStatus}"` });
2066
+ }
2067
+ }
2068
+ }
2069
+ catch {
2070
+ items.push({ section: '跨文件一致性', status: 'warn', message: `${stageId}: 无法读取 status.md` });
2071
+ }
2072
+ }
2073
+ if (total > 0) {
2074
+ if (consistent === total) {
2075
+ items.push({ section: '跨文件一致性', status: 'pass', message: `一致 (${consistent}/${total} stages)` });
2076
+ }
2077
+ else {
2078
+ items.push({ section: '跨文件一致性', status: 'warn', message: `${consistent}/${total} stages 一致` });
2079
+ }
2080
+ }
2081
+ }
2082
+ /** 3. 僵尸状态检测 */
2083
+ checkZombieStates(items) {
2084
+ if (!this.data) {
2085
+ return;
2086
+ }
2087
+ // 僵尸阶段检测:各 stage 在 status.md 中声明的状态与 stage.status 不一致
2088
+ const planDir = resolve(this.projectPath, '.openfeel', 'plan');
2089
+ const stagesDir = resolve(this.projectPath, '.openfeel', 'stages');
2090
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
2091
+ // 所有 REV 已 closed 但 stage 仍为 review_failed
2092
+ if (stage.status === 'review_failed') {
2093
+ const stageReviews = this.data.reviews.filter((r) => r.op.startsWith(stageId));
2094
+ const allClosed = stageReviews.length > 0 && stageReviews.every((r) => r.status === 'closed');
2095
+ if (allClosed) {
2096
+ items.push({ section: '僵尸状态', status: 'warn', message: `${stageId}: 所有 REV 已 closed 但 stage 仍为 review_failed` });
2097
+ }
2098
+ }
2099
+ // 所有 BUG 已 closed 但 stage 仍为 bug_found
2100
+ // 注:当前 flow.json 中无独立 bugs 数据结构,且 Bug 文件按 users/{username}/bugs/{module}/ 组织
2101
+ // (非按 stage 组织),无法通过 stageId 前缀过滤。Bug 僵尸检测待 flow.json 增加 bugs 字段后完善。
2102
+ if (stage.status === 'bug_found') {
2103
+ // 僵尸 Bug 检测已延迟:需待 bugs 数据结构在 flow.json 中正式化
2104
+ // 届时可在此处遍历 this.data.bugs 检查是否有 open 状态的 bug
2105
+ }
2106
+ }
2107
+ // 确保至少有一条反馈
2108
+ const zombieEntries = items.filter((i) => i.section === '僵尸状态');
2109
+ if (zombieEntries.length === 0) {
2110
+ items.push({ section: '僵尸状态', status: 'pass', message: '未检测到僵尸状态' });
2111
+ }
2112
+ }
2113
+ /** 4. 检查 config.yaml 有效性 */
2114
+ checkConfigYaml(items) {
2115
+ const configPath = resolve(this.projectPath, '.openfeel', 'config.yaml');
2116
+ if (!existsSync(configPath)) {
2117
+ items.push({ section: 'config.yaml', status: 'warn', message: 'config.yaml 不存在(可选文件)' });
2118
+ return;
2119
+ }
2120
+ try {
2121
+ const raw = readFileSync(configPath, 'utf-8');
2122
+ const parsed = parseYaml(raw);
2123
+ // 不必检查具体字段,仅检查基本结构
2124
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2125
+ items.push({ section: 'config.yaml', status: 'fail', message: 'config.yaml 可解析但不是有效对象' });
2126
+ return;
2127
+ }
2128
+ items.push({ section: 'config.yaml', status: 'pass', message: '可解析,结构合法' });
2129
+ }
2130
+ catch (e) {
2131
+ items.push({ section: 'config.yaml', status: 'fail', message: `config.yaml 解析失败: ${e.message}` });
2132
+ }
2133
+ }
2134
+ /** 5. 检查 pipeline.yaml 合法性 */
2135
+ checkPipelineYaml(items) {
2136
+ const pipelinePath = resolve(this.projectPath, '.openfeel', 'pipeline.yaml');
2137
+ if (!existsSync(pipelinePath)) {
2138
+ // pipeline.yaml 非必须,不存在也视为通过
2139
+ return;
2140
+ }
2141
+ try {
2142
+ const raw = readFileSync(pipelinePath, 'utf-8');
2143
+ const parsed = parseYaml(raw);
2144
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2145
+ items.push({ section: 'pipeline.yaml', status: 'fail', message: 'pipeline.yaml 可解析但不是有效对象' });
2146
+ return;
2147
+ }
2148
+ // 通过 Zod Schema 校验
2149
+ try {
2150
+ PipelineConfigSchema.parse(parsed);
2151
+ items.push({ section: 'pipeline.yaml', status: 'pass', message: '可解析,通过 Schema 校验' });
2152
+ }
2153
+ catch {
2154
+ items.push({ section: 'pipeline.yaml', status: 'fail', message: 'pipeline.yaml 未通过 Schema 校验(可能缺少 phases/transitions/checkpoint_mapping 字段)' });
2155
+ }
2156
+ }
2157
+ catch (e) {
2158
+ items.push({ section: 'pipeline.yaml', status: 'fail', message: `pipeline.yaml 解析失败: ${e.message}` });
2159
+ }
2160
+ }
2161
+ /** 6. deps.yaml 循环依赖检测(拓扑排序法) */
2162
+ checkDepsYaml(items) {
2163
+ const depsPath = resolve(this.projectPath, '.openfeel', 'plan', 'deps.yaml');
2164
+ if (!existsSync(depsPath)) {
2165
+ return; // deps.yaml 非必须
2166
+ }
2167
+ try {
2168
+ const raw = readFileSync(depsPath, 'utf-8');
2169
+ const parsed = parseYaml(raw);
2170
+ if (!parsed || typeof parsed !== 'object') {
2171
+ items.push({ section: 'deps.yaml', status: 'fail', message: 'deps.yaml 可解析但不是有效对象' });
2172
+ return;
2173
+ }
2174
+ // 构建依赖图:{ stageId: string[] } 在 stages 键下
2175
+ const stages = parsed.stages;
2176
+ if (!stages || typeof stages !== 'object' || Array.isArray(stages)) {
2177
+ // deps.yaml 可能以 stages 键组织,也可能直接是键值对
2178
+ // 尝试直接使用 parsed 作为图
2179
+ const graph = this.buildDepsGraph(parsed);
2180
+ this.detectCycles(graph, items);
2181
+ return;
2182
+ }
2183
+ const graph = this.buildDepsGraph(stages);
2184
+ this.detectCycles(graph, items);
2185
+ }
2186
+ catch (e) {
2187
+ items.push({ section: 'deps.yaml', status: 'fail', message: `deps.yaml 解析失败: ${e.message}` });
2188
+ }
2189
+ }
2190
+ /** 从 YAML 解析后的对象中提取依赖图({ node: deps[] }) */
2191
+ buildDepsGraph(data) {
2192
+ const graph = {};
2193
+ for (const [key, value] of Object.entries(data)) {
2194
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
2195
+ const node = value;
2196
+ if (Array.isArray(node.deps)) {
2197
+ graph[key] = node.deps.map((d) => String(d));
2198
+ }
2199
+ else if (typeof node.depends_on === 'string') {
2200
+ graph[key] = [node.depends_on];
2201
+ }
2202
+ }
2203
+ }
2204
+ return graph;
2205
+ }
2206
+ /** DFS 检测有向图中的环 */
2207
+ detectCycles(graph, items) {
2208
+ const nodes = Object.keys(graph);
2209
+ if (nodes.length === 0) {
2210
+ items.push({ section: 'deps.yaml', status: 'pass', message: '无依赖声明,无环' });
2211
+ return;
2212
+ }
2213
+ const WHITE = 0; // 未访问
2214
+ const GRAY = 1; // 访问中(当前路径上)
2215
+ const BLACK = 2; // 已完成
2216
+ const color = new Map();
2217
+ for (const node of nodes) {
2218
+ color.set(node, WHITE);
2219
+ }
2220
+ const cycles = [];
2221
+ const dfs = (node, path) => {
2222
+ color.set(node, GRAY);
2223
+ // 不存在的节点引用视为合法(硬依赖但未在 stages 中声明)
2224
+ for (const dep of (graph[node] ?? [])) {
2225
+ if (!color.has(dep)) {
2226
+ continue; // 引用不存在的节点,跳过
2227
+ }
2228
+ const depColor = color.get(dep);
2229
+ if (depColor === GRAY) {
2230
+ // 找到环
2231
+ const cycleStart = path.indexOf(dep);
2232
+ const cycle = [...path.slice(cycleStart), dep].join(' → ');
2233
+ cycles.push(cycle);
2234
+ return true;
2235
+ }
2236
+ if (depColor === WHITE) {
2237
+ if (dfs(dep, [...path, dep])) {
2238
+ return true;
2239
+ }
2240
+ }
2241
+ }
2242
+ color.set(node, BLACK);
2243
+ return false;
2244
+ };
2245
+ for (const node of nodes) {
2246
+ if (color.get(node) === WHITE) {
2247
+ dfs(node, [node]);
2248
+ }
2249
+ }
2250
+ if (cycles.length > 0) {
2251
+ for (const cycle of cycles) {
2252
+ items.push({ section: 'deps.yaml', status: 'fail', message: `循环依赖检测: ${cycle}` });
2253
+ }
2254
+ }
2255
+ else {
2256
+ items.push({ section: 'deps.yaml', status: 'pass', message: `无循环依赖 (${nodes.length} 个节点)` });
2257
+ }
2258
+ }
2259
+ // ═══ 初始化 ═══
2260
+ /** 初始化 flow.json(创建默认模板) */
2261
+ static initFlow(projectPath) {
2262
+ const dirPath = resolve(projectPath, '.openfeel');
2263
+ // 确保 .openfeel/ 目录存在
2264
+ if (!existsSync(dirPath)) {
2265
+ mkdirSync(dirPath, { recursive: true });
2266
+ }
2267
+ const filePath = resolve(dirPath, 'flow.json');
2268
+ if (existsSync(filePath)) {
2269
+ return; // 已存在则不覆盖
2270
+ }
2271
+ const data = defaultFlowData();
2272
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
2273
+ }
2274
+ /**
2275
+ * 内嵌数据(仅供测试使用)
2276
+ * 直接设置 flow 数据而不从文件加载
2277
+ */
2278
+ setData(data) {
2279
+ this.data = data;
2280
+ }
2281
+ // ═══ 流水线配置加载 ═══
2282
+ /**
2283
+ * 从 .openfeel/pipeline.yaml 加载流水线配置
2284
+ * 若文件不存在或解析失败,回退到内置默认值
2285
+ */
2286
+ loadPipelineConfig() {
2287
+ const pipelinePath = resolve(this.projectPath, '.openfeel', 'pipeline.yaml');
2288
+ try {
2289
+ if (!existsSync(pipelinePath)) {
2290
+ this.pipelineConfig = this.getDefaultPipelineConfig();
2291
+ return;
2292
+ }
2293
+ const raw = readFileSync(pipelinePath, 'utf-8');
2294
+ const parsed = parseYaml(raw);
2295
+ this.pipelineConfig = PipelineConfigSchema.parse(parsed);
2296
+ }
2297
+ catch {
2298
+ // 解析失败时回退到内置默认值
2299
+ this.pipelineConfig = this.getDefaultPipelineConfig();
2300
+ }
2301
+ }
2302
+ /**
2303
+ * 获取内置默认流水线配置(等价于硬编码常量 + 3 个 Bug 修复)
2304
+ * 当 pipeline.yaml 不存在或解析失败时使用
2305
+ */
2306
+ getDefaultPipelineConfig() {
2307
+ return {
2308
+ phases: [
2309
+ 'plan_pending', 'plan_review', 'plan_passed',
2310
+ 'scheme_pending', 'scheme_review', 'scheme_passed',
2311
+ 'exec_running', 'review_pending', 'review_failed',
2312
+ 'review_passed', 'test_pending', 'test_failed',
2313
+ 'test_passed', 'archiving', 'done',
2314
+ ],
2315
+ transitions: {
2316
+ plan_pending: ['plan_review', 'plan_passed'],
2317
+ plan_review: ['plan_passed', 'plan_pending'],
2318
+ plan_passed: ['scheme_pending'],
2319
+ scheme_pending: ['scheme_review', 'scheme_passed'],
2320
+ scheme_review: ['scheme_passed', 'scheme_pending'],
2321
+ scheme_passed: ['exec_running'],
2322
+ exec_running: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
2323
+ review_pending: ['review_failed', 'review_passed'],
2324
+ review_failed: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
2325
+ review_passed: ['test_pending'],
2326
+ test_pending: ['test_failed', 'test_passed'],
2327
+ test_failed: ['test_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
2328
+ test_passed: ['archiving'],
2329
+ // 组合条件示例:审查通过 或 测试通过 任一即可推进到 archiving(多 Agent 并行场景,任一完成即推进)
2330
+ 'review_passed|test_passed': ['archiving'],
2331
+ archiving: ['done'],
2332
+ done: [],
2333
+ },
2334
+ checkpoint_mapping: {
2335
+ plan: 'plan',
2336
+ scheme: 'scheme',
2337
+ exec: 'exec',
2338
+ review: 'review',
2339
+ test: 'test',
2340
+ archive: 'archive',
2341
+ },
2342
+ phase_corrections: {
2343
+ completed: 'done',
2344
+ finished: 'done',
2345
+ archived: 'done',
2346
+ pending: 'plan_pending',
2347
+ },
2348
+ };
2349
+ }
2350
+ }
2351
+ // ── 辅助函数 ──
2352
+ /**
2353
+ * 将 PipelinePhase 映射为 stage 状态
2354
+ * @param phase 流水线阶段
2355
+ * @param currentStatus 当前 stage 状态(用于不做变更的 phase)
2356
+ * @param testEnabled 是否启用测试(false 时 review_passed 直接映射为 done)
2357
+ * @returns stage 状态字符串
2358
+ */
2359
+ export function mapPhaseToStageStatus(phase, currentStatus, testEnabled = true) {
2360
+ switch (phase) {
2361
+ case 'review_failed':
2362
+ return 'review_failed';
2363
+ case 'review_passed':
2364
+ return testEnabled ? 'review_passed' : 'done';
2365
+ case 'test_passed':
2366
+ // status=done 是 phase=done 的专属状态:test_passed 映射为中间状态 testing,
2367
+ // 避免 autoRepairInconsistency 在 flow advance 时把 phase 误修正为 done,截断 test_passed→archiving 路径
2368
+ return 'testing';
2369
+ case 'archiving':
2370
+ // 归档阶段映射为中间状态 archiving,避免被 autoRepairInconsistency 误修正为 done
2371
+ return 'archiving';
2372
+ case 'done':
2373
+ return 'done';
2374
+ default:
2375
+ // 其他阶段保持 stage 当前状态不变
2376
+ return currentStatus;
2377
+ }
2378
+ }
2379
+ //# sourceMappingURL=flow-manager.js.map