openfeel 1.1.5 → 1.1.7

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 (72) hide show
  1. package/README.en.md +25 -5
  2. package/README.md +22 -3
  3. package/README.zh-CN.md +26 -5
  4. package/dist/commands/config.d.ts.map +1 -1
  5. package/dist/commands/config.js +66 -4
  6. package/dist/commands/config.js.map +1 -1
  7. package/dist/commands/flow.d.ts +8 -0
  8. package/dist/commands/flow.d.ts.map +1 -1
  9. package/dist/commands/flow.js +169 -10
  10. package/dist/commands/flow.js.map +1 -1
  11. package/dist/commands/plan.d.ts.map +1 -1
  12. package/dist/commands/plan.js +59 -2
  13. package/dist/commands/plan.js.map +1 -1
  14. package/dist/commands/setup.d.ts.map +1 -1
  15. package/dist/commands/setup.js +24 -2
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/commands/stage.d.ts.map +1 -1
  18. package/dist/commands/stage.js +56 -2
  19. package/dist/commands/stage.js.map +1 -1
  20. package/dist/commands/update.d.ts.map +1 -1
  21. package/dist/commands/update.js +16 -4
  22. package/dist/commands/update.js.map +1 -1
  23. package/dist/core/backup.d.ts.map +1 -1
  24. package/dist/core/backup.js +18 -5
  25. package/dist/core/backup.js.map +1 -1
  26. package/dist/core/claude-settings.d.ts +31 -0
  27. package/dist/core/claude-settings.d.ts.map +1 -0
  28. package/dist/core/claude-settings.js +120 -0
  29. package/dist/core/claude-settings.js.map +1 -0
  30. package/dist/core/config.d.ts +17 -2
  31. package/dist/core/config.d.ts.map +1 -1
  32. package/dist/core/config.js +21 -6
  33. package/dist/core/config.js.map +1 -1
  34. package/dist/core/flow-manager.d.ts +71 -14
  35. package/dist/core/flow-manager.d.ts.map +1 -1
  36. package/dist/core/flow-manager.js +266 -51
  37. package/dist/core/flow-manager.js.map +1 -1
  38. package/dist/core/global-paths.d.ts +10 -0
  39. package/dist/core/global-paths.d.ts.map +1 -1
  40. package/dist/core/global-paths.js +21 -0
  41. package/dist/core/global-paths.js.map +1 -1
  42. package/dist/core/i18n-data/en.d.ts.map +1 -1
  43. package/dist/core/i18n-data/en.js +44 -9
  44. package/dist/core/i18n-data/en.js.map +1 -1
  45. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  46. package/dist/core/i18n-data/zh-CN.js +45 -9
  47. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  48. package/dist/core/pipeline-schema.d.ts +5 -0
  49. package/dist/core/pipeline-schema.d.ts.map +1 -1
  50. package/dist/core/pipeline-schema.js +5 -0
  51. package/dist/core/pipeline-schema.js.map +1 -1
  52. package/dist/core/plan/scheme.d.ts +22 -0
  53. package/dist/core/plan/scheme.d.ts.map +1 -1
  54. package/dist/core/plan/scheme.js +71 -2
  55. package/dist/core/plan/scheme.js.map +1 -1
  56. package/dist/core/plan/stage.d.ts +2 -0
  57. package/dist/core/plan/stage.d.ts.map +1 -1
  58. package/dist/core/plan/stage.js +8 -0
  59. package/dist/core/plan/stage.js.map +1 -1
  60. package/dist/core/setup.d.ts +2 -2
  61. package/dist/core/setup.d.ts.map +1 -1
  62. package/dist/core/setup.js +81 -53
  63. package/dist/core/setup.js.map +1 -1
  64. package/dist/core/template-loader.d.ts +15 -0
  65. package/dist/core/template-loader.d.ts.map +1 -1
  66. package/dist/core/template-loader.js +4218 -145
  67. package/dist/core/template-loader.js.map +1 -1
  68. package/dist/core/update.d.ts +8 -0
  69. package/dist/core/update.d.ts.map +1 -1
  70. package/dist/core/update.js +368 -141
  71. package/dist/core/update.js.map +1 -1
  72. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, readd
13
13
  import { resolve, dirname, basename } from 'node:path';
14
14
  import { execFileSync } from 'node:child_process';
15
15
  import { parse as parseYaml } from 'yaml';
16
- import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, } from './pipeline-schema.js';
16
+ import { PipelineConfigSchema, PipelinePhaseSchema, PIPELINE_PHASES, MetaPhaseSchema, META_PHASES, transitionKeyMatches, FastTransitionsSchema, } from './pipeline-schema.js';
17
17
  import { PublicLogger, formatDate } from './public-logger.js';
18
18
  import { t, getCliLang } from './i18n.js';
19
19
  import { findStageStatusPath, findStageDirConflict, parseStageId, normalizeStageId } from './plan/path.js';
@@ -52,8 +52,8 @@ export class StageDirConflictError extends Error {
52
52
  this.other = other;
53
53
  }
54
54
  }
55
- /** `openfeel config effective` 的三个受管键(与 DEFAULT_CONFIG 一致) */
56
- const EFFECTIVE_CONFIG_KEYS = ['execution_mode', 'auto_advance', 'merge_mode'];
55
+ /** `openfeel config effective` 的四个受管键(与 DEFAULT_CONFIG 一致) */
56
+ const EFFECTIVE_CONFIG_KEYS = ['execution_mode', 'auto_advance', 'merge_mode', 'workflow_mode'];
57
57
  // ── 默认值 ──
58
58
  /** 创建默认的 Checkpoints */
59
59
  function defaultCheckpoints() {
@@ -163,6 +163,23 @@ export function findOrphanOps(projectPath) {
163
163
  }
164
164
  /** findPhasePath BFS 深度上限(B5-1;超出按拒绝处理) */
165
165
  export const PHASE_PATH_MAX_DEPTH = 8;
166
+ /**
167
+ * fast 模式内置快线转移表(裁定 C):8 条流转键 + `done` 终止项(共 9 条目)。
168
+ * 单一来源:仅 getDefaultPipelineConfig('fast') 消费;禁止在命令层/模板层复制(R-74-1)。
169
+ * `plan_review` 在 fast 下必经(plan_pending → plan_review → plan_passed;通过 → plan_passed,问题退回 → plan_pending)。
170
+ * review_failed 双出口 = 失败回环(重审 或 回执行修复,对应 full 表 scheme_pending 位);Q3 收紧项留审查裁定。
171
+ */
172
+ const FAST_TRANSITIONS = {
173
+ plan_pending: ['plan_review'],
174
+ plan_review: ['plan_passed', 'plan_pending'],
175
+ plan_passed: ['exec_running'],
176
+ exec_running: ['review_pending'],
177
+ review_pending: ['review_failed', 'review_passed'],
178
+ review_failed: ['review_pending', 'exec_running'],
179
+ review_passed: ['archiving'],
180
+ archiving: ['done'],
181
+ done: [],
182
+ };
166
183
  // ── 核心类 ──
167
184
  export class FlowManager {
168
185
  projectPath;
@@ -172,6 +189,8 @@ export class FlowManager {
172
189
  filePath;
173
190
  /** 从 pipeline.yaml 加载的流水线配置(含后备默认值) */
174
191
  pipelineConfig = null;
192
+ /** 从 pipeline.yaml 加载的可选快线表(fast_transitions);null = 缺失/非法/空对象 → 回退内置快线表 [REV-74-009 修订] */
193
+ fastTransitions = null;
175
194
  /** 公共日志写入器(审计链) */
176
195
  publicLogger;
177
196
  constructor(projectPath) {
@@ -1477,10 +1496,11 @@ export class FlowManager {
1477
1496
  * 遍历 transitions 表,key 与 fromPhase 匹配(组合 key 如 'test_passed|review_passed' 任一条件匹配即可)即收集其目标。
1478
1497
  * 无 '|' 的简单 key 保持单条件原行为(精确相等)。
1479
1498
  * @param fromPhase 当前源 phase
1499
+ * @param mode 工作流模式(**必传**;由调用点经 resolveStageWorkflowMode 从 stage 派生——禁止默认 mode)
1480
1500
  * @returns 去重后的可达目标 phase 列表
1481
1501
  */
1482
- getValidTargets(fromPhase) {
1483
- const transitions = this.pipelineConfig?.transitions ?? {};
1502
+ getValidTargets(fromPhase, mode) {
1503
+ const transitions = this.resolveTransitionsForMode(mode);
1484
1504
  const result = new Set();
1485
1505
  for (const [key, targets] of Object.entries(transitions)) {
1486
1506
  if (transitionKeyMatches(key, fromPhase)) {
@@ -1503,7 +1523,8 @@ export class FlowManager {
1503
1523
  const phase = this.resolveCurrentPhase(stageName);
1504
1524
  if (!phase)
1505
1525
  return false;
1506
- return this.getValidTargets(phase).includes(to);
1526
+ const mode = this.resolveStageWorkflowMode(stageName).value;
1527
+ return this.getValidTargets(phase, mode).includes(to);
1507
1528
  }
1508
1529
  /**
1509
1530
  * 获取当前阶段的所有可达下一阶段(基于 transitions 表,支持组合条件)
@@ -1520,7 +1541,8 @@ export class FlowManager {
1520
1541
  const currentPhase = this.resolveCurrentPhase(stageName);
1521
1542
  if (!currentPhase)
1522
1543
  return [];
1523
- return this.getValidTargets(currentPhase);
1544
+ const mode = this.resolveStageWorkflowMode(stageName).value;
1545
+ return this.getValidTargets(currentPhase, mode);
1524
1546
  }
1525
1547
  /**
1526
1548
  * 在运行时 transitions 上求 stageName 当前 phase → to 的**唯一**可达路径(B5-1)。
@@ -1542,6 +1564,8 @@ export class FlowManager {
1542
1564
  if (start === to) {
1543
1565
  return { path: [], reason: 'already-at-target' };
1544
1566
  }
1567
+ // 模式在 BFS 外解析一次(stage 派生;同一 T 内 mode 恒定——单 stage 只读一张表)
1568
+ const mode = this.resolveStageWorkflowMode(key).value;
1545
1569
  // BFS 逐层扩展;visitedDepth 记录各 phase 首次到达的最小深度(剪枝,避免重复/环)
1546
1570
  const visitedDepth = new Map();
1547
1571
  visitedDepth.set(start, 0);
@@ -1554,7 +1578,7 @@ export class FlowManager {
1554
1578
  if (node.path.length >= PHASE_PATH_MAX_DEPTH) {
1555
1579
  continue;
1556
1580
  }
1557
- for (const target of this.getValidTargets(node.phase)) {
1581
+ for (const target of this.getValidTargets(node.phase, mode)) {
1558
1582
  const path = [...node.path, target];
1559
1583
  if (target === to) {
1560
1584
  hits.push(path);
@@ -1591,21 +1615,34 @@ export class FlowManager {
1591
1615
  const phases = this.pipelineConfig?.phases ?? this.getDefaultPipelineConfig().phases;
1592
1616
  return [...phases];
1593
1617
  }
1618
+ /**
1619
+ * 按模式解析运行时转移表(唯一取表法则;校验/推进/路径 与 展示 同经本函数 ⇒ 单一信源)。
1620
+ * full → 运行时 pipelineConfig.transitions(pipeline.yaml 覆盖线);fast → fast_transitions ?? 内置快线表。
1621
+ * 说明:pipelineConfig 构造期恒已加载(constructor 无条件调用),`?? 内置` 保留既有访问器回退语义。
1622
+ */
1623
+ resolveTransitionsForMode(mode) {
1624
+ if (mode === 'fast') {
1625
+ return this.fastTransitions ?? this.getDefaultPipelineConfig('fast').transitions;
1626
+ }
1627
+ return this.pipelineConfig?.transitions ?? this.getDefaultPipelineConfig().transitions;
1628
+ }
1594
1629
  /**
1595
1630
  * 获取运行时生效的 phase 转移表(key 可含 '|' 组合条件)。
1596
1631
  * 缺省/解析失败时回退内置默认配置。返回浅拷贝。
1632
+ * **无参 = full**(零回归既有消费点);fast 供 stage-75 `flow phases` 模式分节消费。
1597
1633
  */
1598
- getPipelineTransitions() {
1599
- const transitions = this.pipelineConfig?.transitions ?? this.getDefaultPipelineConfig().transitions;
1634
+ getPipelineTransitions(mode = 'full') {
1635
+ const transitions = this.resolveTransitionsForMode(mode);
1600
1636
  return Object.fromEntries(Object.entries(transitions).map(([k, v]) => [k, [...v]]));
1601
1637
  }
1602
1638
  /**
1603
1639
  * 获取内置默认 phase 转移表(不受项目 pipeline.yaml 影响)。
1604
1640
  * 供 `flow phases` 做「运行时 vs 默认」差异报告(T19)。
1641
+ * **无参 = full**(零回归既有消费点);fast 供 stage-75 `flow phases` 模式分节消费。
1605
1642
  * @returns 默认 transitions 的深拷贝
1606
1643
  */
1607
- getDefaultTransitions() {
1608
- return Object.fromEntries(Object.entries(this.getDefaultPipelineConfig().transitions).map(([k, v]) => [k, [...v]]));
1644
+ getDefaultTransitions(mode = 'full') {
1645
+ return Object.fromEntries(Object.entries(this.getDefaultPipelineConfig(mode).transitions).map(([k, v]) => [k, [...v]]));
1609
1646
  }
1610
1647
  /**
1611
1648
  * 解析当前阶段 phase:优先使用传入的 stageName,其次 current.stage,最后 fallback
@@ -1698,10 +1735,13 @@ export class FlowManager {
1698
1735
  }
1699
1736
  /**
1700
1737
  * 构建配置级联信息(优先级:profileDefaults < configDefaults < statusOverrides)
1701
- * 全局画像仅作最低优先级兜底(P2),且只取 preferences.auto_advance 单值,
1702
- * 不对 preferences 做整体替换(与 config.yaml defaults 键集重叠的仅 auto_advance)。
1738
+ * 全局画像仅作最低优先级兜底(P2),且只取 preferences.auto_advance 单值,**不扩展** workflow_mode。
1739
+ * @param stageId 可选目标阶段(stage 形参唯一入口);缺省 = pipeline.current.stage
1740
+ * (**当前阶段域**,无参行为逐字不变)。任意阶段域由 resolveStageWorkflowMode(stageId) 显式传入。
1741
+ * 静态约束(REV-74-001/002):无参调用**仅允许**出现在既有两出口 resolveEffectiveConfig()
1742
+ * 与 verboseSummary();新增消费点一律显式传 stage。
1703
1743
  */
1704
- buildCascadeConfig() {
1744
+ buildCascadeConfig(stageId) {
1705
1745
  const profileDefaults = {};
1706
1746
  const configDefaults = {};
1707
1747
  const statusOverrides = {};
@@ -1752,22 +1792,26 @@ export class FlowManager {
1752
1792
  // 解析失败则用空值
1753
1793
  }
1754
1794
  }
1755
- // 最高优先级:当前 stage 的 status.md(如果存在)
1756
- if (this.data && this.data.pipeline.current.stage) {
1757
- const stageId = this.data.pipeline.current.stage;
1758
- const statusPath = this.findStatusPath(stageId);
1795
+ // 最高优先级:目标 stage 的 status.md(缺省 = pipeline.current.stage;stage 形参路径读该阶段)
1796
+ const targetStage = stageId ?? this.data?.pipeline.current.stage;
1797
+ if (targetStage) {
1798
+ const statusPath = this.findStatusPath(targetStage);
1759
1799
  if (statusPath) {
1760
1800
  try {
1761
1801
  const content = readFileSync(statusPath, 'utf-8');
1762
- // 匹配执行模式、自动推进
1802
+ // 匹配执行模式、自动推进、工作流模式
1763
1803
  const execMatch = content.match(/\*\*执行模式\*\*[::]\s*(manual|auto)/);
1764
1804
  const autoMatch = content.match(/\*\*自动推进\*\*[::]\s*(disabled|enabled)/);
1805
+ const modeMatch = content.match(/\*\*工作流模式\*\*[::]\s*(full|fast)/);
1765
1806
  if (execMatch) {
1766
1807
  statusOverrides['execution_mode'] = execMatch[1];
1767
1808
  }
1768
1809
  if (autoMatch) {
1769
1810
  statusOverrides['auto_advance'] = autoMatch[1];
1770
1811
  }
1812
+ if (modeMatch) {
1813
+ statusOverrides['workflow_mode'] = modeMatch[1];
1814
+ }
1771
1815
  }
1772
1816
  catch {
1773
1817
  // 读取失败则跳过
@@ -1779,7 +1823,7 @@ export class FlowManager {
1779
1823
  return { profileDefaults, configDefaults, statusOverrides, effective };
1780
1824
  }
1781
1825
  /**
1782
- * 解析三个受管配置键的「有效值 + 生效来源」(`openfeel config effective`,P9)。
1826
+ * 解析四个受管配置键的「有效值 + 生效来源」(`openfeel config effective`,P9)。
1783
1827
  * 复用 buildCascadeConfig(单一权威,避免第二套解析),优先级:
1784
1828
  * status.md > config.yaml > profile.yaml > builtin。
1785
1829
  * @returns key → { value, source }
@@ -1804,6 +1848,30 @@ export class FlowManager {
1804
1848
  }
1805
1849
  return result;
1806
1850
  }
1851
+ /**
1852
+ * 解析指定阶段的工作流模式(裁定 E:「stage → mode」表选择解析入口;stage-75 `flow mode` 契约)。
1853
+ * 带 stage 形参独立解析(REV-74-001):该阶段 status.md(findStatusPath(stageId))
1854
+ * > config.yaml defaults > profile.yaml(**有意不承载**,实际不产出)> builtin(DEFAULT_CONFIG.workflow_mode = fast)。
1855
+ * 语义边界:`config effective` / `flow status --verbose` = **当前阶段域**(无参);
1856
+ * 本方法显式 stageId = **任意阶段域**(flow mode --stage / 表选择)。
1857
+ * @param stageId 目标阶段 ID(缺省 = pipeline.current.stage;无该阶段 status.md → 自然回落下一层)
1858
+ * @returns { value, source },source ∈ status.md | config.yaml | profile.yaml | builtin
1859
+ */
1860
+ resolveStageWorkflowMode(stageId) {
1861
+ const cascade = this.buildCascadeConfig(stageId);
1862
+ const key = 'workflow_mode';
1863
+ if (cascade.statusOverrides[key] !== undefined) {
1864
+ return { value: cascade.statusOverrides[key], source: 'status.md' };
1865
+ }
1866
+ if (cascade.configDefaults[key] !== undefined) {
1867
+ return { value: cascade.configDefaults[key], source: 'config.yaml' };
1868
+ }
1869
+ if (cascade.profileDefaults[key] !== undefined) {
1870
+ // profile 层有意不承载 workflow_mode(画像 = 个人偏好层,REV-74-005);此分支为对称性保留,实际不产出
1871
+ return { value: cascade.profileDefaults[key], source: 'profile.yaml' };
1872
+ }
1873
+ return { value: DEFAULT_CONFIG.workflow_mode, source: 'builtin' };
1874
+ }
1807
1875
  /** 查找 status.md 的路径(三级回退:plan/{series}/ 精确 → plan 递归 → stages 兜底) */
1808
1876
  findStatusPath(stageId) {
1809
1877
  return findStageStatusPath(this.projectPath, stageId);
@@ -1861,7 +1929,7 @@ export class FlowManager {
1861
1929
  // 返回最近 N 条(倒序取最后)
1862
1930
  return results.slice(-maxChanges);
1863
1931
  }
1864
- /** 构建下游阶段列表(当前 phase 的可达下一阶段 + Agent 映射) */
1932
+ /** 构建下游阶段列表(当前阶段的可达下一阶段 + Agent 映射;模式经 getAvailablePhases() 无参 → current.stage 派生) */
1865
1933
  buildDownstreamPhases() {
1866
1934
  const available = this.getAvailablePhases();
1867
1935
  const labels = this.getPhaseLabels();
@@ -2187,8 +2255,10 @@ export class FlowManager {
2187
2255
  return false;
2188
2256
  }
2189
2257
  const currentPhase = this.data.stages[parts.stageId].phase;
2258
+ // stage 派生 mode:opId 解析出的 stage(src 内无生产调用方,仅测试;仍为 mode 透传必改点)
2259
+ const mode = this.resolveStageWorkflowMode(parts.stageId).value;
2190
2260
  // 检查从当前 phase 到目标 phase 的流转是否合法(支持组合条件 key)
2191
- const validTargets = this.getValidTargets(currentPhase);
2261
+ const validTargets = this.getValidTargets(currentPhase, mode);
2192
2262
  if (!validTargets.includes(to)) {
2193
2263
  return false;
2194
2264
  }
@@ -2817,6 +2887,10 @@ export class FlowManager {
2817
2887
  if (!quick) {
2818
2888
  this.checkEmptyTemplates(items);
2819
2889
  }
2890
+ // ── 10. 工作流模式冗余字段一致性(stage-75 op-003;仅 warn,不 fail、不自动修复)──
2891
+ if (!quick) {
2892
+ this.checkWorkflowModeRedundancy(items);
2893
+ }
2820
2894
  const ok = items.every((i) => i.status !== 'fail');
2821
2895
  return { items, ok };
2822
2896
  }
@@ -2902,23 +2976,84 @@ export class FlowManager {
2902
2976
  return true;
2903
2977
  });
2904
2978
  }
2979
+ /**
2980
+ * status.md 字段「插入」写(原子写 + 按阶段目录加锁)。
2981
+ * 与 `writeStatusField`(替换语义)并列的**独立**实现,禁双重语义:
2982
+ * 字段行已存在 → `exists`(不写盘);锚点链「自动推进」后 → 回退「执行模式」后 → 皆缺 `no-anchor`(不写盘 / 无 `.bak`)。
2983
+ * 备份 `.openfeel/tmp/status.{stageId}.bak` 仅在**确认插入**时生成。
2984
+ * 不依赖 `this.data`(flow.json 未加载的存量项目也可仅凭 `this.projectPath` 定位锁/备份)。
2985
+ * @param statusPath status.md 绝对路径
2986
+ * @param stageId 备份文件名中的阶段 ID(单阶段 = 用户输入原值;批量 = flow.json 注册键)
2987
+ * @param key 字段名(如「工作流模式」)
2988
+ * @param value 目标值
2989
+ * @returns `inserted` 已插入 | `exists` 字段已存在(竞态防御,调用方可回退替换)| `no-anchor` 锚点缺失
2990
+ */
2991
+ insertStatusField(statusPath, stageId, key, value) {
2992
+ const stageDir = basename(dirname(statusPath));
2993
+ const lockPath = projectLockPath(this.projectPath, `status-${stageDir}`);
2994
+ return withFileLock(lockPath, () => {
2995
+ const content = readFileSync(statusPath, 'utf-8');
2996
+ // 锁内复查:字段行已存在 → 不写盘(探针与写之间的竞态防御)
2997
+ const fieldRegex = new RegExp(`^-\\s*(?:\\*\\*)?${escapeRegex(key)}(?:\\*\\*)?[::]`, 'm');
2998
+ if (fieldRegex.test(content)) {
2999
+ return 'exists';
3000
+ }
3001
+ // 锚点链:「自动推进」行后优先;缺失则回退「执行模式」行后;皆缺 = no-anchor
3002
+ const anchor = content.match(/^-\s*(?:\*\*)?自动推进(?:\*\*)?[::]/m) ??
3003
+ content.match(/^-\s*(?:\*\*)?执行模式(?:\*\*)?[::]/m);
3004
+ if (!anchor || anchor.index === undefined) {
3005
+ return 'no-anchor';
3006
+ }
3007
+ const newLine = `- **${key}**:${value}`;
3008
+ const lineEnd = content.indexOf('\n', anchor.index + anchor[0].length);
3009
+ let updated;
3010
+ if (lineEnd === -1) {
3011
+ // 错误路径:锚点行位于文件末行(无尾换行)→ 换行后追加
3012
+ updated = `${content}\n${newLine}`;
3013
+ }
3014
+ else {
3015
+ // 保留锚点行原行尾风格(CRLF 感知),其余字节逐字保留
3016
+ const isCrlf = content[lineEnd - 1] === '\r';
3017
+ const eol = isCrlf ? '\r\n' : '\n';
3018
+ updated = `${content.slice(0, lineEnd + 1)}${newLine}${eol}${content.slice(lineEnd + 1)}`;
3019
+ }
3020
+ // 备份仅在确认插入时生成
3021
+ const tmpDir = resolve(this.projectPath, '.openfeel', 'tmp');
3022
+ if (!existsSync(tmpDir)) {
3023
+ mkdirSync(tmpDir, { recursive: true });
3024
+ }
3025
+ copyFileSync(statusPath, resolve(tmpDir, `status.${stageId}.bak`));
3026
+ atomicWriteFileSync(statusPath, updated);
3027
+ return 'inserted';
3028
+ });
3029
+ }
2905
3030
  /**
2906
3031
  * 将受管配置键批量同步到所有已注册阶段的 status.md 对应字段(`config set --sync-stages`)。
2907
- * 仅 auto_advance→「自动推进」、execution_mode→「执行模式」有阶段级字段;
2908
- * 其它键无阶段字段 → 全部 skipped-no-field(D-config 裁定,跳过并报告)。
2909
- * 复用 writeStatusField 的定向写 + 按阶段锁;同值 → noop(零写盘)。
2910
- * 只读 flow.json 的阶段枚举,不写 flow.json。
3032
+ * 仅 auto_advance→「自动推进」、execution_mode→「执行模式」、workflow_mode→「工作流模式」
3033
+ * 三个映射键有阶段级字段;其它键无阶段字段 → 全部 skipped-no-field(D-config 裁定,跳过并报告)。
3034
+ * `workflow_mode` 字段缺失时复用 `insertStatusField`(插入语义,单一实现),锚点缺失 → skipped-no-anchor。
3035
+ * 复用 writeStatusField / insertStatusField 的定向写 + 按阶段锁;同值 → noop(零写盘)。
3036
+ * **冗余写入(裁定 G)**:`workflow_mode` 实际写入(updated / inserted)时同步 `stages[id].workflow_mode`
3037
+ * 并 save 一次;noop / skipped 全量运行 → flow.json 零写盘。
2911
3038
  * @param configKey 已归一的受管配置键(如 auto_advance)
2912
3039
  * @param value 目标值
2913
- * @returns 每阶段一条结果(updated/noop/skipped-no-field/skipped-no-status)
3040
+ * @param options 可选作用域:`onlyStages` 限定阶段 ID 列表(已归一化;缺省 = 全量现状)
3041
+ * @returns 每阶段一条结果(updated/inserted/noop/skipped-no-field/skipped-no-status/skipped-no-anchor)
2914
3042
  */
2915
- syncConfigFieldToStages(configKey, value) {
3043
+ syncConfigFieldToStages(configKey, value, options) {
2916
3044
  const out = [];
2917
3045
  if (!this.data) {
2918
3046
  return out;
2919
3047
  }
2920
3048
  const field = STAGE_FIELD_BY_CONFIG_KEY[configKey];
3049
+ const targets = options?.onlyStages;
3050
+ // 冗余写入标记:仅实际写入(updated / inserted)时置位,循环后 save 一次(D6)
3051
+ let flowJsonDirty = false;
2921
3052
  for (const stageId of Object.keys(this.data.stages)) {
3053
+ // --stage 作用域限定:归一化比较兼容存量短名键(D5;未注册 id 自然落 skipped-no-status)
3054
+ if (targets && !targets.includes(normalizeStageId(stageId) ?? stageId)) {
3055
+ continue;
3056
+ }
2922
3057
  // 错误路径:该 config 键无阶段级字段 → 跳过并报告
2923
3058
  if (!field) {
2924
3059
  out.push({ stage: stageId, result: 'skipped-no-field' });
@@ -2931,8 +3066,31 @@ export class FlowManager {
2931
3066
  continue;
2932
3067
  }
2933
3068
  const current = readStatusFieldValue(readFileSync(statusPath, 'utf-8'), field);
2934
- // 错误路径:字段行缺失 → 跳过(不新建字段)
3069
+ // 错误路径:字段行缺失 →
3070
+ // workflow_mode 走「插入」语义(复用 op-001 单一实现);其余键保持 skipped-no-field(零回归)
2935
3071
  if (current === null) {
3072
+ if (configKey === 'workflow_mode') {
3073
+ const r = this.insertStatusField(statusPath, stageId, field, value);
3074
+ if (r === 'inserted') {
3075
+ this.appendLog({ time: '', agent: 'cli', action: 'config_sync_stage', detail: { stage: stageId, field, value } });
3076
+ out.push({ stage: stageId, result: 'inserted' });
3077
+ this.data.stages[stageId].workflow_mode = value;
3078
+ flowJsonDirty = true;
3079
+ continue;
3080
+ }
3081
+ if (r === 'exists') {
3082
+ // 竞态防御:锁内发现字段已就位 → 回退替换语义(不产生双行)
3083
+ this.writeStatusField(statusPath, field, value);
3084
+ this.appendLog({ time: '', agent: 'cli', action: 'config_sync_stage', detail: { stage: stageId, field, value } });
3085
+ out.push({ stage: stageId, result: 'updated' });
3086
+ this.data.stages[stageId].workflow_mode = value;
3087
+ flowJsonDirty = true;
3088
+ continue;
3089
+ }
3090
+ // 锚点链均缺失 → 跳过类(不写盘、不审计)
3091
+ out.push({ stage: stageId, result: 'skipped-no-anchor' });
3092
+ continue;
3093
+ }
2936
3094
  out.push({ stage: stageId, result: 'skipped-no-field' });
2937
3095
  continue;
2938
3096
  }
@@ -2944,6 +3102,15 @@ export class FlowManager {
2944
3102
  this.writeStatusField(statusPath, field, value);
2945
3103
  this.appendLog({ time: '', agent: 'cli', action: 'config_sync_stage', detail: { stage: stageId, field, value } });
2946
3104
  out.push({ stage: stageId, result: 'updated' });
3105
+ // 冗余写入点③:仅实际写入时同步(noop / skipped 不写)
3106
+ if (configKey === 'workflow_mode') {
3107
+ this.data.stages[stageId].workflow_mode = value;
3108
+ flowJsonDirty = true;
3109
+ }
3110
+ }
3111
+ // 仅在实际发生写入时保存(幂等:全 noop → flow.json 零写盘)
3112
+ if (flowJsonDirty) {
3113
+ this.save();
2947
3114
  }
2948
3115
  return out;
2949
3116
  }
@@ -2977,6 +3144,37 @@ export class FlowManager {
2977
3144
  }
2978
3145
  items.push({ section: '悬空依赖', status: 'pass', message: '未检测到悬空依赖' });
2979
3146
  }
3147
+ /**
3148
+ * 10. 工作流模式冗余字段一致性:flow.json `stages[].workflow_mode` 与级联解析值不一致 → warn(stage-75 op-003)。
3149
+ * 冗余字段不参与级联(裁定 G),读取权威恒为 status.md/级联;字段缺失(旧 flow.json)零报错;
3150
+ * 仅告警不 fail、不自动修复。无不一致时不产生条目(保持输出精简)——与 checkOrphanOps 同风格。
3151
+ * @param items 健康检查结果收集数组
3152
+ */
3153
+ checkWorkflowModeRedundancy(items) {
3154
+ if (!this.data) {
3155
+ return;
3156
+ }
3157
+ const mismatched = [];
3158
+ for (const [stageId, stage] of Object.entries(this.data.stages)) {
3159
+ const redundant = stage.workflow_mode;
3160
+ // 错误路径:旧 flow.json 无该字段 → 直接跳过(零报错)
3161
+ if (redundant === undefined) {
3162
+ continue;
3163
+ }
3164
+ const resolved = this.resolveStageWorkflowMode(stageId).value;
3165
+ if (redundant !== resolved) {
3166
+ mismatched.push(`${stageId}(冗余 ${redundant} ≠ 解析 ${resolved})`);
3167
+ }
3168
+ }
3169
+ if (mismatched.length === 0) {
3170
+ return;
3171
+ }
3172
+ items.push({
3173
+ section: '工作流模式',
3174
+ status: 'warn',
3175
+ message: `检出 ${mismatched.length} 个阶段的 flow.json 冗余 workflow_mode 与权威解析值不一致(冗余不参与级联,可执行 openfeel config set workflow_mode <v> --sync-stages 修复):${mismatched.slice(0, 5).join('; ')}${mismatched.length > 5 ? ' …' : ''}`,
3176
+ });
3177
+ }
2980
3178
  /**
2981
3179
  * 8. 孤儿 op 检测:flow.json 的 op 键与 ops/ 目录文件是否一致(N1-3)
2982
3180
  * 仅作数据卫生告警(warn),不阻断;退出码不受影响(仅 fail 决定)。
@@ -3361,6 +3559,19 @@ export class FlowManager {
3361
3559
  const raw = readFileSync(pipelinePath, 'utf-8');
3362
3560
  const parsed = parseYaml(raw);
3363
3561
  this.pipelineConfig = PipelineConfigSchema.parse(parsed);
3562
+ // 可选快线表:独立容错解析(非法类型 → 忽略该键 + 告警,不 fail 整体配置;缺失/空对象 → 内置快线表)
3563
+ const fastRaw = parsed?.['fast_transitions'];
3564
+ if (fastRaw !== undefined) {
3565
+ const fastParsed = FastTransitionsSchema.safeParse(fastRaw);
3566
+ if (fastParsed.success) {
3567
+ // [REV-74-009 修订] 空对象 {} 等同缺失(空表使 fast 全链不可推进,无合法用途)→ 回落内置快线表
3568
+ this.fastTransitions = Object.keys(fastParsed.data).length > 0 ? fastParsed.data : null;
3569
+ }
3570
+ else {
3571
+ this.fastTransitions = null;
3572
+ console.warn(t('flow.manager.fastTransitionsInvalid', getCliLang(this.projectPath)));
3573
+ }
3574
+ }
3364
3575
  }
3365
3576
  catch {
3366
3577
  // 解析失败时回退到内置默认值
@@ -3369,9 +3580,11 @@ export class FlowManager {
3369
3580
  }
3370
3581
  /**
3371
3582
  * 获取内置默认流水线配置(等价于硬编码常量 + 3 个 Bug 修复)
3372
- * 当 pipeline.yaml 不存在或解析失败时使用
3583
+ * 当 pipeline.yaml 不存在或解析失败时使用。
3584
+ * @param mode 工作流模式;**无参 = full**(内置 full 表逐字等于历史现状,零回归既有消费点);
3585
+ * 'fast' 返回快线表(FAST_TRANSITIONS 常量引用)。phases / checkpoint_mapping / phase_corrections 两模式共用。
3373
3586
  */
3374
- getDefaultPipelineConfig() {
3587
+ getDefaultPipelineConfig(mode = 'full') {
3375
3588
  return {
3376
3589
  phases: [
3377
3590
  'plan_pending', 'plan_review', 'plan_passed',
@@ -3380,25 +3593,27 @@ export class FlowManager {
3380
3593
  'review_passed', 'test_pending', 'test_failed',
3381
3594
  'test_passed', 'archiving', 'done',
3382
3595
  ],
3383
- transitions: {
3384
- plan_pending: ['plan_review', 'plan_passed'],
3385
- plan_review: ['plan_passed', 'plan_pending'],
3386
- plan_passed: ['scheme_pending'],
3387
- scheme_pending: ['scheme_review', 'scheme_passed'],
3388
- scheme_review: ['scheme_passed', 'scheme_pending'],
3389
- scheme_passed: ['exec_running'],
3390
- exec_running: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3391
- review_pending: ['review_failed', 'review_passed'],
3392
- review_failed: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3393
- review_passed: ['test_pending'],
3394
- test_pending: ['test_failed', 'test_passed'],
3395
- test_failed: ['test_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3396
- test_passed: ['archiving'],
3397
- // 组合条件示例:审查通过 或 测试通过 任一即可推进到 archiving(多 Agent 并行场景,任一完成即推进)
3398
- 'review_passed|test_passed': ['archiving'],
3399
- archiving: ['done'],
3400
- done: [],
3401
- },
3596
+ transitions: mode === 'fast'
3597
+ ? FAST_TRANSITIONS
3598
+ : {
3599
+ plan_pending: ['plan_review', 'plan_passed'],
3600
+ plan_review: ['plan_passed', 'plan_pending'],
3601
+ plan_passed: ['scheme_pending'],
3602
+ scheme_pending: ['scheme_review', 'scheme_passed'],
3603
+ scheme_review: ['scheme_passed', 'scheme_pending'],
3604
+ scheme_passed: ['exec_running'],
3605
+ exec_running: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3606
+ review_pending: ['review_failed', 'review_passed'],
3607
+ review_failed: ['review_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3608
+ review_passed: ['test_pending'],
3609
+ test_pending: ['test_failed', 'test_passed'],
3610
+ test_failed: ['test_pending', 'scheme_pending'], // BUG 修复:增加 scheme_pending
3611
+ test_passed: ['archiving'],
3612
+ // 组合条件示例:审查通过 或 测试通过 任一即可推进到 archiving(多 Agent 并行场景,任一完成即推进)
3613
+ 'review_passed|test_passed': ['archiving'],
3614
+ archiving: ['done'],
3615
+ done: [],
3616
+ },
3402
3617
  checkpoint_mapping: {
3403
3618
  plan: 'plan',
3404
3619
  scheme: 'scheme',