openfeel 1.2.1 → 1.2.3

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 (87) hide show
  1. package/README.en.md +23 -1
  2. package/README.md +23 -1
  3. package/README.zh-CN.md +23 -1
  4. package/dist/cli/index.d.ts +17 -3
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +96 -8
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/project-option.d.ts +40 -0
  9. package/dist/cli/project-option.d.ts.map +1 -0
  10. package/dist/cli/project-option.js +140 -0
  11. package/dist/cli/project-option.js.map +1 -0
  12. package/dist/commands/archive.d.ts.map +1 -1
  13. package/dist/commands/archive.js +32 -2
  14. package/dist/commands/archive.js.map +1 -1
  15. package/dist/commands/config.d.ts.map +1 -1
  16. package/dist/commands/config.js +65 -7
  17. package/dist/commands/config.js.map +1 -1
  18. package/dist/commands/flow.d.ts.map +1 -1
  19. package/dist/commands/flow.js +394 -114
  20. package/dist/commands/flow.js.map +1 -1
  21. package/dist/commands/instructions.d.ts.map +1 -1
  22. package/dist/commands/instructions.js +13 -3
  23. package/dist/commands/instructions.js.map +1 -1
  24. package/dist/commands/knowledge.d.ts.map +1 -1
  25. package/dist/commands/knowledge.js +113 -17
  26. package/dist/commands/knowledge.js.map +1 -1
  27. package/dist/commands/plan.d.ts.map +1 -1
  28. package/dist/commands/plan.js +24 -6
  29. package/dist/commands/plan.js.map +1 -1
  30. package/dist/commands/project.d.ts +66 -0
  31. package/dist/commands/project.d.ts.map +1 -1
  32. package/dist/commands/project.js +122 -64
  33. package/dist/commands/project.js.map +1 -1
  34. package/dist/commands/req.d.ts.map +1 -1
  35. package/dist/commands/req.js +23 -11
  36. package/dist/commands/req.js.map +1 -1
  37. package/dist/commands/setup.d.ts.map +1 -1
  38. package/dist/commands/setup.js +11 -3
  39. package/dist/commands/setup.js.map +1 -1
  40. package/dist/commands/shared/errors.d.ts +55 -4
  41. package/dist/commands/shared/errors.d.ts.map +1 -1
  42. package/dist/commands/shared/errors.js +108 -10
  43. package/dist/commands/shared/errors.js.map +1 -1
  44. package/dist/commands/shared/json-output.d.ts +60 -0
  45. package/dist/commands/shared/json-output.d.ts.map +1 -0
  46. package/dist/commands/shared/json-output.js +152 -0
  47. package/dist/commands/shared/json-output.js.map +1 -0
  48. package/dist/commands/stage.d.ts.map +1 -1
  49. package/dist/commands/stage.js +62 -6
  50. package/dist/commands/stage.js.map +1 -1
  51. package/dist/commands/view.d.ts.map +1 -1
  52. package/dist/commands/view.js +27 -0
  53. package/dist/commands/view.js.map +1 -1
  54. package/dist/core/config.js +2 -2
  55. package/dist/core/errors.d.ts +100 -0
  56. package/dist/core/errors.d.ts.map +1 -0
  57. package/dist/core/errors.js +58 -0
  58. package/dist/core/errors.js.map +1 -0
  59. package/dist/core/flow-manager.d.ts +13 -0
  60. package/dist/core/flow-manager.d.ts.map +1 -1
  61. package/dist/core/flow-manager.js +22 -0
  62. package/dist/core/flow-manager.js.map +1 -1
  63. package/dist/core/fs/file-lock.d.ts +9 -0
  64. package/dist/core/fs/file-lock.d.ts.map +1 -1
  65. package/dist/core/fs/file-lock.js +27 -1
  66. package/dist/core/fs/file-lock.js.map +1 -1
  67. package/dist/core/i18n-data/en.d.ts.map +1 -1
  68. package/dist/core/i18n-data/en.js +42 -3
  69. package/dist/core/i18n-data/en.js.map +1 -1
  70. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  71. package/dist/core/i18n-data/zh-CN.js +42 -3
  72. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  73. package/dist/core/pipeline-schema.d.ts +7 -0
  74. package/dist/core/pipeline-schema.d.ts.map +1 -1
  75. package/dist/core/pipeline-schema.js +7 -0
  76. package/dist/core/pipeline-schema.js.map +1 -1
  77. package/dist/core/req/aggregate.d.ts +19 -1
  78. package/dist/core/req/aggregate.d.ts.map +1 -1
  79. package/dist/core/req/aggregate.js +18 -5
  80. package/dist/core/req/aggregate.js.map +1 -1
  81. package/dist/core/template-loader.d.ts.map +1 -1
  82. package/dist/core/template-loader.js +33 -9
  83. package/dist/core/template-loader.js.map +1 -1
  84. package/dist/core/update.d.ts.map +1 -1
  85. package/dist/core/update.js +29 -5
  86. package/dist/core/update.js.map +1 -1
  87. package/package.json +1 -1
@@ -1,15 +1,16 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, copyFileSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { FlowManager, isFlowConcurrentError, normalizeAgentName, detectFillState, MODE_DERIVED_TIER, PHASE_PATH_MAX_DEPTH } from '../core/flow-manager.js';
5
- import { PipelinePhaseSchema, PIPELINE_PHASES, REVIEW_TIERS, TIER_PRUNED_PHASES } from '../core/pipeline-schema.js';
4
+ import { FlowManager, isFlowConcurrentError, normalizeAgentName, detectFillState, MODE_DERIVED_TIER, listStageBlockingOpenReviews, PHASE_PATH_MAX_DEPTH } from '../core/flow-manager.js';
5
+ import { PipelinePhaseSchema, PIPELINE_PHASES, REVIEW_TIERS, TIER_PRUNED_PHASES, DEFAULT_EFFECTIVE_REVIEW_TIER } from '../core/pipeline-schema.js';
6
6
  import { validateStageId, suggestStageId, normalizeStageId, findStageStatusPath, isArchivedVersionOf } from '../core/plan/path.js';
7
7
  import { getConfigFieldLegalValues, setConfigValue } from '../core/config.js';
8
8
  import { parse as parseYaml } from 'yaml';
9
9
  import { MetricsStore, PHASE_AGENT_MAP } from '../core/metrics.js';
10
10
  import { runtimeLog } from '../core/runtime-log.js';
11
11
  import { addReviewEntry } from '../core/view/entry.js';
12
- import { handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
12
+ import { failCli, failInternal, handleAddStageError, handleConcurrentConflict } from './shared/errors.js';
13
+ import { withSchemaVersion, isJsonMode, emitJson } from './shared/json-output.js'; // JSON 契约助手(stage-95 op-001/op-002)
13
14
  import { t, getCliLang } from '../core/i18n.js';
14
15
  import { ARCHIVE_SCHEMA_VERSION } from '../core/flow-archive/archive-file.js';
15
16
  import { listArchivableCandidates, listBlockingOpenRevsForVersion, splitForArchive } from '../core/flow-archive/grouping.js';
@@ -142,6 +143,109 @@ export function registerFlowCommand(program) {
142
143
  }
143
144
  }
144
145
  });
146
+ // flow next — 当前阶段调度决策(只读;P0-1)
147
+ flow
148
+ .command('next')
149
+ .description('显示当前阶段的调度决策(负责 Agent / 合法下一相位 / 建议命令 / 守卫 / 阻塞项;只读)')
150
+ .option('--json', 'Output as JSON (machine-readable, single JSON document)')
151
+ .action(() => {
152
+ const lang = getCliLang(process.cwd());
153
+ const mgr = createManager();
154
+ if (!mgr.isLoaded()) {
155
+ const message = t('common.errorNoInit', lang);
156
+ failCli({ code: 'NO_INIT', message, detail: null, stderrLines: [message] });
157
+ return;
158
+ }
159
+ const data = mgr.getData();
160
+ const rawStage = data.pipeline.current.stage || '';
161
+ // 双键回退(与 core resolveCurrentPhase / advanceStagePhase 同范式):归一化键优先,其次原始键
162
+ const normalized = rawStage ? (normalizeStageId(rawStage) ?? rawStage) : '';
163
+ const activeKey = rawStage
164
+ ? (data.stages[normalized] ? normalized : (data.stages[rawStage] ? rawStage : null))
165
+ : null;
166
+ let phase = null;
167
+ let phaseLabel = null;
168
+ let responsibleAgent = 'feel'; // 无活跃阶段显式约定
169
+ let workflowMode = null;
170
+ let reviewTier = null;
171
+ let effectiveReviewTier = null;
172
+ let legalNextPhases = [];
173
+ let suggestedCommand = null;
174
+ const guards = [];
175
+ let blockingReviews = [];
176
+ if (activeKey) {
177
+ const currentPhase = data.stages[activeKey].phase;
178
+ phase = currentPhase;
179
+ phaseLabel = mgr.getPhaseLabels(lang)[currentPhase] ?? currentPhase;
180
+ responsibleAgent = mgr.getResponsibleAgent(currentPhase);
181
+ workflowMode = mgr.resolveStageWorkflowMode(activeKey).value;
182
+ reviewTier = MODE_DERIVED_TIER[workflowMode];
183
+ effectiveReviewTier = DEFAULT_EFFECTIVE_REVIEW_TIER;
184
+ legalNextPhases = mgr.getAvailablePhases(activeKey);
185
+ suggestedCommand = legalNextPhases.length > 0
186
+ ? `openfeel flow advance --stage ${activeKey} --to ${legalNextPhases[0]}`
187
+ : null;
188
+ blockingReviews = listStageBlockingOpenReviews(data.reviews, activeKey).map((r) => ({
189
+ id: r.id, title: r.title, priority: r.priority, op: r.op,
190
+ }));
191
+ // 守卫顺序契约固定(§12.1):auto-advance → blocking → 无目标 → 档位差
192
+ if (mgr.resolveEffectiveConfig()['auto_advance']?.value === 'disabled') {
193
+ guards.push(t('flow.next.guardAutoAdvanceDisabled', lang));
194
+ }
195
+ if (blockingReviews.length > 0) {
196
+ guards.push(t('flow.next.guardBlockingReviewsTmpl', lang, { n: String(blockingReviews.length) }));
197
+ }
198
+ if (legalNextPhases.length === 0) {
199
+ guards.push(t('flow.next.guardNoLegalTargets', lang));
200
+ }
201
+ if (reviewTier !== effectiveReviewTier) {
202
+ guards.push(t('flow.next.guardTierDiffTmpl', lang, { display: reviewTier, effective: effectiveReviewTier }));
203
+ }
204
+ }
205
+ else {
206
+ guards.push(t('flow.next.guardNoActiveStage', lang));
207
+ }
208
+ const doc = withSchemaVersion({
209
+ stage: activeKey ?? (rawStage || null),
210
+ phase,
211
+ phaseLabel,
212
+ responsibleAgent,
213
+ workflowMode,
214
+ reviewTier,
215
+ effectiveReviewTier,
216
+ legalNextPhases,
217
+ suggestedCommand,
218
+ guards,
219
+ blockingReviews,
220
+ });
221
+ // JSON 模式单点(stage-95 不变量 2):命令层只读 isJsonMode()
222
+ if (isJsonMode()) {
223
+ emitJson(doc);
224
+ return;
225
+ }
226
+ // 人类输出(逐行标签;空值统一 t('common.none', lang))
227
+ console.log(t('flow.next.title', lang));
228
+ console.log(` ${t('flow.next.stageLabel', lang)}: ${doc.stage ?? t('common.none', lang)}`);
229
+ console.log(` ${t('flow.next.phaseLabel', lang)}: ${doc.phase ?? t('common.none', lang)}${doc.phaseLabel ? ` (${doc.phaseLabel})` : ''}`);
230
+ console.log(` ${t('flow.next.responsibleAgentLabel', lang)}: ${doc.responsibleAgent}`);
231
+ console.log(` ${t('flow.next.workflowModeLabel', lang)}: ${doc.workflowMode ?? t('common.none', lang)}`);
232
+ console.log(` ${t('flow.next.displayTierLabel', lang)}: ${doc.reviewTier ?? t('common.none', lang)}`);
233
+ console.log(` ${t('flow.next.effectiveTierLabel', lang)}: ${doc.effectiveReviewTier ?? t('common.none', lang)}`);
234
+ console.log(` ${t('flow.next.legalNextPhasesLabel', lang)}: ${doc.legalNextPhases.length > 0 ? `[${doc.legalNextPhases.join(', ')}]` : t('common.none', lang)}`);
235
+ console.log(` ${t('flow.next.suggestedCommandLabel', lang)}: ${doc.suggestedCommand ?? t('common.none', lang)}`);
236
+ if (doc.guards.length > 0) {
237
+ console.log(` ${t('flow.next.guardsLabel', lang)}:`);
238
+ for (const g of doc.guards) {
239
+ console.log(` - ${g}`);
240
+ }
241
+ }
242
+ if (doc.blockingReviews.length > 0) {
243
+ console.log(` ${t('flow.next.blockingReviewsLabel', lang)}:`);
244
+ for (const r of doc.blockingReviews) {
245
+ console.log(` - ${t('flow.next.blockingReviewItemTmpl', lang, { id: r.id, title: r.title, priority: r.priority, op: r.op })}`);
246
+ }
247
+ }
248
+ });
145
249
  // flow overview — 全状态可视化(openfeel flow overview 的后端实现)
146
250
  flow
147
251
  .command('overview')
@@ -360,8 +464,13 @@ export function registerFlowCommand(program) {
360
464
  if (options.mode !== undefined) {
361
465
  const allowed = getConfigFieldLegalValues('workflow_mode') ?? [];
362
466
  if (!allowed.includes(options.mode)) {
363
- console.error(t('stage.set.invalidValueTmpl', lang, { field: '工作流模式', value: options.mode, allowed: allowed.join(', ') }));
364
- process.exit(1);
467
+ const message = t('stage.set.invalidValueTmpl', lang, { field: '工作流模式', value: options.mode, allowed: allowed.join(', ') });
468
+ failCli({
469
+ code: 'INVALID_ARGUMENT',
470
+ message,
471
+ detail: { field: 'workflowMode', value: options.mode, allowed },
472
+ stderrLines: [message],
473
+ });
365
474
  return;
366
475
  }
367
476
  explicitMode = options.mode;
@@ -371,17 +480,15 @@ export function registerFlowCommand(program) {
371
480
  if (options.stage !== undefined) {
372
481
  const v = validateStageId(options.stage);
373
482
  if (!v.ok) {
374
- console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
375
- console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
376
- process.exit(1);
483
+ const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
484
+ failCli({ code: 'INVALID_ARGUMENT', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
377
485
  return;
378
486
  }
379
487
  stageArg = normalizeStageId(options.stage) ?? options.stage;
380
488
  const known = new Set(Object.keys(mgr.isLoaded() ? mgr.getData().stages : {}).map((k) => normalizeStageId(k) ?? k));
381
489
  if (!known.has(stageArg) && !findStageStatusPath(projectPath, stageArg)) {
382
- console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
383
- console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
384
- process.exit(1);
490
+ const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
491
+ failCli({ code: 'STAGE_NOT_FOUND', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
385
492
  return;
386
493
  }
387
494
  }
@@ -401,7 +508,9 @@ export function registerFlowCommand(program) {
401
508
  // advanceAccepted = 内置 15 phase(flow advance 的推进白名单),与 phases(运行时存在视图,可含自定义)显式区分
402
509
  // 既有三字段(phases/transitions/advanceAccepted)+ transitionsDiff 逐字保留;schemaVersion 与 workflowMode 追加于末位(B1-6 / stage-75 op-002)
403
510
  // reviewTier 再追加于末位(stage-92 op-001;既有 6 键逐字保留、顺序不变)
404
- console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff, workflowMode: derivedMode, reviewTier: MODE_DERIVED_TIER[derivedMode] }, null, 2));
511
+ // stage-96 op-002:末位再追加 displayReviewTier(与 reviewTier 同值显式名)与 effectiveReviewTier(推进缺省档位,恒 full)
512
+ const displayTier = MODE_DERIVED_TIER[derivedMode];
513
+ console.log(JSON.stringify({ schemaVersion: 1, phases, transitions, advanceAccepted: [...PIPELINE_PHASES], transitionsDiff, workflowMode: derivedMode, reviewTier: displayTier, displayReviewTier: displayTier, effectiveReviewTier: DEFAULT_EFFECTIVE_REVIEW_TIER }, null, 2));
405
514
  return;
406
515
  }
407
516
  console.log(t('flow.phases.title', lang));
@@ -456,18 +565,16 @@ export function registerFlowCommand(program) {
456
565
  if (options.stage !== undefined) {
457
566
  const v = validateStageId(options.stage);
458
567
  if (!v.ok) {
459
- console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
460
- console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
461
- process.exit(1);
568
+ const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
569
+ failCli({ code: 'INVALID_ARGUMENT', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
462
570
  return;
463
571
  }
464
572
  key = normalizeStageId(options.stage) ?? options.stage;
465
573
  // 存在性:已注册(flow.json)或有 status.md;二者皆无 → exit 1(防 typo 静默落 config 层)
466
574
  const known = new Set(Object.keys(mgr.isLoaded() ? mgr.getData().stages : {}).map((k) => normalizeStageId(k) ?? k));
467
575
  if (!known.has(key) && !findStageStatusPath(projectPath, key)) {
468
- console.error(t('common.stageIdInvalidTmpl', lang, { input: options.stage }));
469
- console.error(t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, options.stage) }));
470
- process.exit(1);
576
+ const stderrLines = stageIdErrorLines(lang, projectPath, options.stage);
577
+ failCli({ code: 'STAGE_NOT_FOUND', message: stderrLines[0], detail: { stage: options.stage }, stderrLines });
471
578
  return;
472
579
  }
473
580
  }
@@ -599,9 +706,10 @@ export function registerFlowCommand(program) {
599
706
  console.log(t('flow.stage.remove.okTmpl', lang, { stage: stageArg }));
600
707
  }
601
708
  catch (err) {
602
- // 并发写冲突:统一单点(i18n 文案 + 退出码 2);其余走通用错误
709
+ // 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
603
710
  if (isFlowConcurrentError(err)) {
604
711
  handleConcurrentConflict(err, lang);
712
+ return;
605
713
  }
606
714
  const msg = err instanceof Error ? err.message : String(err);
607
715
  console.error(t('common.errorTmpl', lang, { msg }));
@@ -658,8 +766,10 @@ export function registerFlowCommand(program) {
658
766
  mgr.setStageDeps(stageId, deps);
659
767
  }
660
768
  catch (err) {
769
+ // 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
661
770
  if (isFlowConcurrentError(err)) {
662
771
  handleConcurrentConflict(err, lang);
772
+ return;
663
773
  }
664
774
  const msg = err instanceof Error ? err.message : String(err);
665
775
  console.error(t('common.errorTmpl', lang, { msg }));
@@ -710,9 +820,10 @@ export function registerFlowCommand(program) {
710
820
  }));
711
821
  }
712
822
  catch (err) {
713
- // 并发写冲突:统一单点(i18n 文案 + 退出码 2);其余走通用错误
823
+ // 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
714
824
  if (isFlowConcurrentError(err)) {
715
825
  handleConcurrentConflict(err, lang);
826
+ return;
716
827
  }
717
828
  const msg = err instanceof Error ? err.message : String(err);
718
829
  console.error(t('common.errorTmpl', lang, { msg }));
@@ -728,32 +839,49 @@ export function registerFlowCommand(program) {
728
839
  .option('--stage <id>', '阶段 ID(如 stage-03),必须指定')
729
840
  .option('--force', '强制执行(跳过非法 phase 校验和阶段跳跃检查,但不可绕过 REV 阻塞检查)')
730
841
  .option('--dry-run', '仅验证不执行修改(预览输出)。与 --force 组合时跳过校验但仍不执行修改')
731
- .option('--review-tier <tier>', '审查档位(full / lean / skip;缺省=按模式派生;仅显式声明时裁剪审查关,不可绕过 REV 阻塞检查)')
842
+ .option('--review-tier <tier>', '审查档位(full / lean / skip;缺省=full(零裁剪);仅显式声明时裁剪审查关,不可绕过 REV 阻塞检查)')
843
+ .option('--json', '以 JSON 输出错误封装(仅失败时;成功输出保持人类文本)')
732
844
  .option('--quiet', '静默非错误输出(成功确认与 Git 警告均不打印)')
733
845
  .action((options) => {
734
846
  const lang = getCliLang(process.cwd());
847
+ // JSON 模式(--json 为错误封装开关)下,失败前的人类进展行改走 stderr,保证 stdout 仅一个 envelope;
848
+ // 成功路径的人类输出保持 console.log(advance 成功不 JSON 化,契约 §7/§8)。
849
+ const info = (line) => {
850
+ if (isJsonMode()) {
851
+ console.error(line);
852
+ return;
853
+ }
854
+ console.log(line);
855
+ };
735
856
  // 自定义 --stage 必选校验(提供中文错误提示)
736
857
  if (!options.stage) {
737
- console.error(t('flow.advance.errorNoStage', lang));
738
- process.exit(1);
858
+ const message = t('flow.advance.errorNoStage', lang);
859
+ failCli({ code: 'INVALID_ARGUMENT', message, detail: { required: '--stage' }, stderrLines: [message] });
860
+ return;
739
861
  }
740
862
  // 审查档位值域校验(stage-92 op-001;fail-fast 且**先于任何取值 / 路径解析 / autoRepair 写盘**)
741
863
  // 复用 stage.set.invalidValueTmpl(与 flow phases --mode 同款,零新增键);非法 → exit 1 零写盘
742
864
  let explicitTier;
743
865
  if (options.reviewTier !== undefined) {
744
866
  if (!REVIEW_TIERS.includes(options.reviewTier)) {
745
- console.error(t('stage.set.invalidValueTmpl', lang, { field: '审查档位', value: options.reviewTier, allowed: REVIEW_TIERS.join(', ') }));
746
- process.exit(1);
867
+ const message = t('stage.set.invalidValueTmpl', lang, { field: '审查档位', value: options.reviewTier, allowed: REVIEW_TIERS.join(', ') });
868
+ failCli({
869
+ code: 'INVALID_ARGUMENT',
870
+ message,
871
+ detail: { field: 'reviewTier', value: options.reviewTier, allowed: REVIEW_TIERS },
872
+ stderrLines: [message],
873
+ });
747
874
  return;
748
875
  }
749
876
  explicitTier = options.reviewTier;
750
877
  }
751
- // 生效档位:显式声明 ?? full(**不传 = 零裁剪**;派生档仅用于 `flow phases` 展示与提示,不改变推进语义)
752
- const effTier = explicitTier ?? 'full';
878
+ // 生效档位:显式声明 ?? DEFAULT_EFFECTIVE_REVIEW_TIER(**不传 = 零裁剪**;派生档仅用于 `flow phases` 展示与提示,不改变推进语义)
879
+ const effTier = explicitTier ?? DEFAULT_EFFECTIVE_REVIEW_TIER;
753
880
  const mgr = createManager();
754
881
  if (!mgr.isLoaded()) {
755
- console.error(t('common.errorNoInit', lang));
756
- process.exit(1);
882
+ const message = t('common.errorNoInit', lang);
883
+ failCli({ code: 'NO_INIT', message, detail: null, stderrLines: [message] });
884
+ return;
757
885
  }
758
886
  // REV-005:命令层归一化(短名 → 全名),与 plan.ts / flow.ts 其它 stage 解析点同范式
759
887
  const stageArg = normalizeStageId(options.stage) ?? options.stage;
@@ -762,7 +890,8 @@ export function registerFlowCommand(program) {
762
890
  // B1 修复:dry-run 走预览模式(不写内存/不写盘),仅在非 dry-run 时 save()
763
891
  const repairResult = mgr.autoRepairInconsistency(stageArg, { dryRun: options.dryRun });
764
892
  if (repairResult.fixed) {
765
- console.log(options.dryRun
893
+ // JSON 模式下改走 stderr(失败时 stdout 仅保留一个 envelope)
894
+ info(options.dryRun
766
895
  ? t('flow.advance.autoRepairPreview', lang) + `: ${repairResult.detail}`
767
896
  : t('flow.advance.autoRepaired', lang) + `: ${repairResult.detail}`);
768
897
  if (!options.dryRun) {
@@ -776,23 +905,35 @@ export function registerFlowCommand(program) {
776
905
  for (const w of warnings) {
777
906
  console.warn(`[WARN] ${w}`);
778
907
  }
779
- console.log(t('flow.advance.warnAutoCorrect', lang));
908
+ // JSON 模式下改走 stderr(失败时 stdout 仅保留一个 envelope)
909
+ info(t('flow.advance.warnAutoCorrect', lang));
780
910
  }
781
911
  if (!valid) {
782
- console.error(t('flow.advance.errorInvalidFormat', lang));
783
- for (const err of errors) {
784
- console.error(` - ${err}`);
785
- }
786
- process.exit(1);
912
+ const message = t('flow.advance.errorInvalidFormat', lang);
913
+ failCli({
914
+ code: 'INVALID_ARGUMENT',
915
+ message,
916
+ detail: { errors },
917
+ stderrLines: [message, ...errors.map((e) => ' - ' + e)],
918
+ });
919
+ return;
787
920
  }
788
921
  // 非法 phase 校验(P1):拒绝不在枚举中的目标 phase;--force 可跳过此项,但不可绕过 REV 阻塞检查
789
922
  if (!options.force) {
790
923
  const phaseResult = PipelinePhaseSchema.safeParse(options.to);
791
924
  if (!phaseResult.success) {
792
- console.error(t('flow.advance.errorInvalidPhaseTmpl', lang, { phase: options.to }));
793
- console.error(t('flow.advance.labelValidPhases', lang) + `: [${PIPELINE_PHASES.join(', ')}]`);
794
- console.error(t('flow.advance.hintUseForceFuzzy', lang));
795
- process.exit(1);
925
+ const message = t('flow.advance.errorInvalidPhaseTmpl', lang, { phase: options.to });
926
+ failCli({
927
+ code: 'PHASE_TRANSITION_ILLEGAL',
928
+ message,
929
+ detail: { stage: stageArg, to: options.to, allowed: [...PIPELINE_PHASES] },
930
+ stderrLines: [
931
+ message,
932
+ t('flow.advance.labelValidPhases', lang) + `: [${PIPELINE_PHASES.join(', ')}]`,
933
+ t('flow.advance.hintUseForceFuzzy', lang),
934
+ ],
935
+ });
936
+ return;
796
937
  }
797
938
  }
798
939
  // 阶段路径解析(B5-1):非 --force 时求「当前 phase → to」的唯一可达路径
@@ -806,36 +947,47 @@ export function registerFlowCommand(program) {
806
947
  const data = mgr.getData();
807
948
  const currentPhase = data?.stages[stageArg]?.phase ?? t('common.unknown', lang);
808
949
  const availableTargets = mgr.getAvailablePhases(stageArg, effTier);
950
+ const stderrLines = [];
809
951
  if (pathResult.reason === 'ambiguous') {
810
- console.error(t('flow.advance.ambiguousTmpl', lang));
952
+ stderrLines.push(t('flow.advance.ambiguousTmpl', lang));
811
953
  }
812
954
  else if (pathResult.reason === 'depth-exceeded') {
813
- console.error(t('flow.advance.depthExceededTmpl', lang, { max: String(PHASE_PATH_MAX_DEPTH) }));
955
+ stderrLines.push(t('flow.advance.depthExceededTmpl', lang, { max: String(PHASE_PATH_MAX_DEPTH) }));
814
956
  }
815
957
  else {
816
958
  // general(无流水线)→ 显式错误引导(防御路径:常规写路径不产生 general 阶段态;REV-90-005 / D-90-3)
817
959
  if (mgr.resolveStageWorkflowMode(stageArg).value === 'general') {
818
- console.error(t('flow.advance.generalNoPipeline', lang, { stage: stageArg }));
819
- process.exit(1);
960
+ const message = t('flow.advance.generalNoPipeline', lang, { stage: stageArg });
961
+ failCli({
962
+ code: 'PHASE_PATH_NOT_FOUND',
963
+ message,
964
+ detail: { stage: stageArg, from: currentPhase, to: options.to, reason: pathResult.reason, targets: availableTargets },
965
+ stderrLines: [message],
966
+ });
820
967
  return;
821
968
  }
822
- console.error(t('flow.advance.noPathTmpl', lang, {
969
+ stderrLines.push(t('flow.advance.noPathTmpl', lang, {
823
970
  from: currentPhase,
824
971
  to: options.to,
825
972
  targets: availableTargets.length > 0 ? `[${availableTargets.join(', ')}]` : t('common.none', lang),
826
973
  }));
827
974
  }
828
975
  // 保留既有跳转诊断(可达目标)——与现状一致
829
- console.error(t('flow.advance.errorPhaseJumpTmpl', lang, { stage: stageArg, to: options.to }));
830
- console.error(t('flow.advance.currentPhaseTmpl', lang, { phase: currentPhase }));
976
+ stderrLines.push(t('flow.advance.errorPhaseJumpTmpl', lang, { stage: stageArg, to: options.to }));
977
+ stderrLines.push(t('flow.advance.currentPhaseTmpl', lang, { phase: currentPhase }));
831
978
  if (availableTargets.length > 0) {
832
- console.error(t('flow.advance.availableTargets', lang) + `: [${availableTargets.join(', ')}]`);
979
+ stderrLines.push(t('flow.advance.availableTargets', lang) + `: [${availableTargets.join(', ')}]`);
833
980
  }
834
981
  else {
835
- console.error(t('flow.advance.noAvailableTargets', lang));
982
+ stderrLines.push(t('flow.advance.noAvailableTargets', lang));
836
983
  }
837
- console.error(t('flow.advance.hintUseForce', lang));
838
- process.exit(1);
984
+ stderrLines.push(t('flow.advance.hintUseForce', lang));
985
+ failCli({
986
+ code: 'PHASE_PATH_NOT_FOUND',
987
+ message: stderrLines[0],
988
+ detail: { stage: stageArg, from: currentPhase, to: options.to, reason: pathResult.reason, targets: availableTargets },
989
+ stderrLines,
990
+ });
839
991
  return;
840
992
  }
841
993
  if (pathResult.reason === 'already-at-target') {
@@ -870,9 +1022,13 @@ export function registerFlowCommand(program) {
870
1022
  if (options.force) {
871
1023
  console.warn(t('flow.advance.forceRevRefused', lang));
872
1024
  }
873
- console.error(t('flow.advance.blockingRevRefused', lang));
874
- console.error(t('flow.advance.blockingRevHint', lang));
875
- process.exit(1);
1025
+ const message = t('flow.advance.blockingRevRefused', lang);
1026
+ failCli({
1027
+ code: 'BLOCKING_REVIEWS',
1028
+ message,
1029
+ detail: { stage: stageArg, to: 'done', reviews: blockingOpen.map((r) => r.id) },
1030
+ stderrLines: [message, t('flow.advance.blockingRevHint', lang)],
1031
+ });
876
1032
  return;
877
1033
  }
878
1034
  }
@@ -919,14 +1075,21 @@ export function registerFlowCommand(program) {
919
1075
  archived = mgr.advanceStagePhase(stageArg, next, 'cli', explicitTier ? { reviewTier: explicitTier } : undefined);
920
1076
  }
921
1077
  catch (err) {
1078
+ const stderrLines = [];
922
1079
  if (totalSteps > 1) {
923
1080
  const remaining = phasePath.slice(i).join(t('flow.advance.pathArrow', lang));
924
1081
  const doneTo = i > 0 ? phasePath[i - 1] : t('common.none', lang);
925
- console.error(t('flow.advance.partialFailTmpl', lang, { done: doneTo, failed: next, remaining }));
1082
+ stderrLines.push(t('flow.advance.partialFailTmpl', lang, { done: doneTo, failed: next, remaining }));
926
1083
  }
927
1084
  const msg = err instanceof Error ? err.message : String(err);
928
- console.error(t('common.errorTmpl', lang, { msg }));
929
- process.exit(1);
1085
+ const message = t('common.errorTmpl', lang, { msg });
1086
+ stderrLines.push(message);
1087
+ // 多步循环中的意外异常归内部错误(D-95-11:原 exit 1 → exit 3,登记为分类补全)
1088
+ failInternal(err, {
1089
+ message,
1090
+ stderrLines,
1091
+ detail: { stage: stageArg, failed: next, remaining: phasePath.slice(i) },
1092
+ });
930
1093
  return;
931
1094
  }
932
1095
  archivedAny = archivedAny || archived;
@@ -965,11 +1128,16 @@ export function registerFlowCommand(program) {
965
1128
  const blockingOpen = assertNoBlockingOpenRev(mgr, stageArg, lang);
966
1129
  if (blockingOpen.length > 0) {
967
1130
  const remainingPath = phasePath.slice(i + 1);
968
- console.error(t('flow.advance.blockedByRevTmpl', lang, {
1131
+ const message = t('flow.advance.blockedByRevTmpl', lang, {
969
1132
  revs: blockingOpen.map((r) => r.id).join(', '),
970
1133
  remaining: remainingPath.length > 0 ? remainingPath.join(t('flow.advance.pathArrow', lang)) : t('common.none', lang),
971
- }));
972
- process.exit(1);
1134
+ });
1135
+ failCli({
1136
+ code: 'BLOCKING_REVIEWS',
1137
+ message,
1138
+ detail: { stage: stageArg, to: 'done', reviews: blockingOpen.map((r) => r.id) },
1139
+ stderrLines: [message],
1140
+ });
973
1141
  return;
974
1142
  }
975
1143
  }
@@ -1102,14 +1270,14 @@ export function registerFlowCommand(program) {
1102
1270
  }
1103
1271
  }
1104
1272
  if (options.json) {
1105
- // 复用 op-001 口径:顶层对象 + schemaVersion:1
1273
+ // 复用 op-001 口径:顶层对象 + schemaVersion:1;warning 恒在(非 empty 为 null)
1106
1274
  const ops = items.map((it) => ({
1107
1275
  stage: it.stage,
1108
1276
  opId: it.opId,
1109
1277
  title: it.title,
1110
1278
  state: it.state,
1111
1279
  fill: it.fill,
1112
- ...(it.fill === 'empty' ? { warning: t('flow.ops.emptyWarningTmpl', lang, { opId: it.opId }) } : {}),
1280
+ warning: it.fill === 'empty' ? t('flow.ops.emptyWarningTmpl', lang, { opId: it.opId }) : null,
1113
1281
  }));
1114
1282
  console.log(JSON.stringify({ schemaVersion: 1, ops }, null, 2));
1115
1283
  return;
@@ -1138,14 +1306,29 @@ export function registerFlowCommand(program) {
1138
1306
  .command('log')
1139
1307
  .description('显示最近操作日志')
1140
1308
  .option('--last <n>', '显示最近 n 条(默认 10)', '10')
1309
+ .option('--json', t('help.flow.log.json', getCliLang(process.cwd())))
1141
1310
  .action((options) => {
1142
1311
  const lang = getCliLang(process.cwd());
1143
1312
  const mgr = createManager();
1313
+ const n = Math.max(1, parseInt(options.last, 10) || 10);
1314
+ // JSON 分支须为首行纯 JSON 单文档;空日志与未初始化同形(entries: [])
1315
+ if (options.json) {
1316
+ const entries = mgr.isLoaded() && mgr.getData() ? mgr.getData().log.slice(-n) : [];
1317
+ emitJson(withSchemaVersion({
1318
+ count: entries.length,
1319
+ entries: entries.map((e) => ({
1320
+ time: e.time,
1321
+ agent: normalizeAgentName(e.agent),
1322
+ action: e.action,
1323
+ detail: e.detail,
1324
+ })),
1325
+ }));
1326
+ return;
1327
+ }
1144
1328
  if (!mgr.isLoaded()) {
1145
1329
  console.log(t('flow.log.noInit', lang));
1146
1330
  return;
1147
1331
  }
1148
- const n = Math.max(1, parseInt(options.last, 10) || 10);
1149
1332
  const data = mgr.getData();
1150
1333
  if (!data || data.log.length === 0) {
1151
1334
  console.log(t('flow.log.noLogs', lang));
@@ -1513,10 +1696,20 @@ export function registerFlowCommand(program) {
1513
1696
  .command('list')
1514
1697
  .description('列出所有(或指定阶段的)Checkpoint 快照')
1515
1698
  .argument('[stage]', '阶段 ID(可选),如 v5.3-stage-01')
1516
- .action((stage) => {
1699
+ .option('--json', t('help.flow.checkpoint.list.json', getCliLang(process.cwd())))
1700
+ .action((stage, options) => {
1517
1701
  const lang = getCliLang(process.cwd());
1518
1702
  const mgr = createManager();
1519
1703
  const snapshots = mgr.listCheckpoints(stage);
1704
+ // JSON 分支:未初始化与空集合同形(checkpoints: [])
1705
+ if (options.json) {
1706
+ emitJson(withSchemaVersion({
1707
+ stage: stage ?? null,
1708
+ checkpoints: snapshots,
1709
+ count: snapshots.length,
1710
+ }));
1711
+ return;
1712
+ }
1520
1713
  if (snapshots.length === 0) {
1521
1714
  console.log(stage
1522
1715
  ? t('flow.checkpoint.noSnapshotsStageTmpl', lang, { stage })
@@ -1600,18 +1793,19 @@ export function registerFlowCommand(program) {
1600
1793
  const projectPath = process.cwd();
1601
1794
  const lang = getCliLang(projectPath);
1602
1795
  const mgr = createManager();
1603
- // --json 失败出口统一:纯 JSON 单文档 + exit 1(不混人类文案、不走 stderr)
1604
- const failJson = (payload) => {
1605
- console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
1606
- process.exitCode = 1;
1796
+ // --json 失败出口统一(legacy 形态):纯 JSON 单文档、不混人类文案;
1797
+ // `errorKind` 恒为末尾键,`error` 恒 string、既有键序零改动(REV-95-002 / D-95-09)
1798
+ const failJson = (payload, errorKind = 'refusal', exitCode = 1) => {
1799
+ console.log(JSON.stringify({ schemaVersion: 1, ...payload, errorKind }, null, 2));
1800
+ process.exitCode = exitCode;
1607
1801
  };
1608
1802
  if (!mgr.isLoaded()) {
1609
1803
  if (options.json) {
1610
1804
  failJson({ error: 'no-init' });
1611
1805
  return;
1612
1806
  }
1613
- console.error(t('common.errorNoInit', lang));
1614
- process.exitCode = 1;
1807
+ const message = t('common.errorNoInit', lang);
1808
+ failCli({ code: 'NO_INIT', message, detail: null, stderrLines: [message] });
1615
1809
  return;
1616
1810
  }
1617
1811
  // 版本归一(REV-79-004):去 v 再加 v;非法 → exit 1 零写盘
@@ -1621,8 +1815,8 @@ export function registerFlowCommand(program) {
1621
1815
  failJson({ error: 'invalid-version', input: versionInput });
1622
1816
  return;
1623
1817
  }
1624
- console.error(t('flow.archive.invalidVersionTmpl', lang, { input: versionInput }));
1625
- process.exitCode = 1;
1818
+ const message = t('flow.archive.invalidVersionTmpl', lang, { input: versionInput });
1819
+ failCli({ code: 'INVALID_ARGUMENT', message, detail: { input: versionInput }, stderrLines: [message] });
1626
1820
  return;
1627
1821
  }
1628
1822
  const data = mgr.getData();
@@ -1633,8 +1827,8 @@ export function registerFlowCommand(program) {
1633
1827
  failJson({ error: 'version-not-active', version });
1634
1828
  return;
1635
1829
  }
1636
- console.error(t('flow.archive.versionNotActiveTmpl', lang, { version }));
1637
- process.exitCode = 1;
1830
+ const message = t('flow.archive.versionNotActiveTmpl', lang, { version });
1831
+ failCli({ code: 'INVALID_ARGUMENT', message, detail: { version }, stderrLines: [message] });
1638
1832
  return;
1639
1833
  }
1640
1834
  // 判据门:全 done(candidate.allDone 空集守卫)+ 无 blocking open REV(全仓唯一谓词,REV-79-002)
@@ -1642,23 +1836,38 @@ export function registerFlowCommand(program) {
1642
1836
  const blockingOpen = listBlockingOpenRevsForVersion(data, version);
1643
1837
  if (!candidate.allDone || blockingOpen.length > 0) {
1644
1838
  if (options.json) {
1645
- failJson({ error: 'gate-rejected', version, notDone, blockingOpen: blockingOpen.map((r) => r.id) });
1839
+ // 判据明细(D-97-03-04):legacy 五键序保留,三新键插在 blockingOpen 后、errorKind 前
1840
+ failJson({
1841
+ error: 'gate-rejected',
1842
+ version,
1843
+ notDone,
1844
+ blockingOpen: blockingOpen.map((r) => r.id),
1845
+ eligible: false,
1846
+ unfinishedStages: notDone.map((id) => ({ id, phase: data.stages[id]?.phase ?? null })),
1847
+ blockingReviews: blockingOpen.map((r) => ({ id: r.id, title: r.title, priority: r.priority })),
1848
+ });
1646
1849
  return;
1647
1850
  }
1851
+ const stderrLines = [];
1648
1852
  if (notDone.length > 0) {
1649
- console.error(t('flow.archive.gateNotDoneTitleTmpl', lang, { version, n: String(notDone.length) }));
1853
+ stderrLines.push(t('flow.archive.gateNotDoneTitleTmpl', lang, { version, n: String(notDone.length) }));
1650
1854
  for (const id of notDone) {
1651
- console.error(t('flow.archive.gateItemTmpl', lang, { item: id + ' [' + (data.stages[id]?.phase ?? '') + ']' }));
1855
+ stderrLines.push(t('flow.archive.gateItemTmpl', lang, { item: id + ' [' + (data.stages[id]?.phase ?? '') + ']' }));
1652
1856
  }
1653
1857
  }
1654
1858
  if (blockingOpen.length > 0) {
1655
- console.error(t('flow.archive.gateBlockingRevTitleTmpl', lang, { version, n: String(blockingOpen.length) }));
1859
+ stderrLines.push(t('flow.archive.gateBlockingRevTitleTmpl', lang, { version, n: String(blockingOpen.length) }));
1656
1860
  for (const r of blockingOpen) {
1657
- console.error(t('flow.archive.gateItemTmpl', lang, { item: r.id + ': ' + r.title + ' (priority=' + r.priority + ')' }));
1861
+ stderrLines.push(t('flow.archive.gateItemTmpl', lang, { item: r.id + ': ' + r.title + ' (priority=' + r.priority + ')' }));
1658
1862
  }
1659
1863
  }
1660
- console.error(t('flow.archive.gateRejected', lang));
1661
- process.exitCode = 1;
1864
+ stderrLines.push(t('flow.archive.gateRejected', lang));
1865
+ failCli({
1866
+ code: 'ARCHIVE_GATE_REJECTED',
1867
+ message: stderrLines[stderrLines.length - 1],
1868
+ detail: { version, notDone, blockingOpen: blockingOpen.map((r) => r.id) },
1869
+ stderrLines,
1870
+ });
1662
1871
  return;
1663
1872
  }
1664
1873
  // 切分(纯函数;禁命令层第二套解析/切分)
@@ -1678,7 +1887,11 @@ export function registerFlowCommand(program) {
1678
1887
  };
1679
1888
  const preview = applyArchivePipelineDisposition(previewData, version);
1680
1889
  if (options.json) {
1681
- console.log(JSON.stringify({ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log, dryRun: true }, null, 2));
1890
+ // 成功 9 键(D-97-03-03):dry-run 只读 counts,零写盘
1891
+ console.log(JSON.stringify({
1892
+ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log,
1893
+ dryRun: true, eligible: true, unfinishedStages: [], blockingReviews: [],
1894
+ }, null, 2));
1682
1895
  return;
1683
1896
  }
1684
1897
  console.log(t('flow.archive.previewTmpl', lang, { version, stages: String(counts.stages), reviews: String(counts.reviews), log: String(counts.log) }));
@@ -1694,13 +1907,14 @@ export function registerFlowCommand(program) {
1694
1907
  writeArchiveFile(projectPath, file);
1695
1908
  }
1696
1909
  catch (err) {
1697
- // 写盘失败(REV-79-013):内存未改动(移除/索引/审计均在其后),无需回滚;exit 1 零 flow.json 变更
1910
+ // 写盘失败(REV-79-013):内存未改动(移除/索引/审计均在其后),无需回滚;exit 3 零 flow.json 变更
1911
+ // 分类补全(D-95-09):写盘失败用户无法通过修正输入解决 → internal / exit 3
1698
1912
  if (options.json) {
1699
- failJson({ error: 'archive-write-failed', version });
1913
+ failJson({ error: 'archive-write-failed', version }, 'internal', 3);
1700
1914
  return;
1701
1915
  }
1702
- console.error(t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) }));
1703
- process.exitCode = 1;
1916
+ const message = t('common.errorTmpl', lang, { msg: err instanceof Error ? err.message : String(err) });
1917
+ failCli({ code: 'ARCHIVE_WRITE_FAILED', message, detail: { version }, stderrLines: [message] });
1704
1918
  return;
1705
1919
  }
1706
1920
  // ② 内存移除(stages / reviews / log 三段)
@@ -1727,7 +1941,11 @@ export function registerFlowCommand(program) {
1727
1941
  return;
1728
1942
  }
1729
1943
  if (options.json) {
1730
- console.log(JSON.stringify({ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log, dryRun: false }, null, 2));
1944
+ // 成功 9 键(D-97-03-03):保持在 mgr.save() 成功之后输出(顺序不变)
1945
+ console.log(JSON.stringify({
1946
+ schemaVersion: 1, version, stages: counts.stages, reviews: counts.reviews, log: counts.log,
1947
+ dryRun: false, eligible: true, unfinishedStages: [], blockingReviews: [],
1948
+ }, null, 2));
1731
1949
  return;
1732
1950
  }
1733
1951
  if (added) {
@@ -1794,30 +2012,33 @@ export function registerFlowCommand(program) {
1794
2012
  const version = normalizeArchiveVersion(versionInput);
1795
2013
  if (!version) {
1796
2014
  if (json) {
1797
- console.log(JSON.stringify({ schemaVersion: 1, error: 'invalid-version', input: versionInput, available }, null, 2));
2015
+ // legacy 形态:error 恒 string、既有键序不变,errorKind 恒末尾
2016
+ console.log(JSON.stringify({ schemaVersion: 1, error: 'invalid-version', input: versionInput, available, errorKind: 'refusal' }, null, 2));
1798
2017
  process.exitCode = 1;
1799
2018
  return;
1800
2019
  }
1801
- console.error(t('flow.archive.invalidVersionTmpl', lang, { input: versionInput }));
1802
- process.exitCode = 1;
2020
+ const message = t('flow.archive.invalidVersionTmpl', lang, { input: versionInput });
2021
+ failCli({ code: 'INVALID_ARGUMENT', message, detail: { input: versionInput }, stderrLines: [message] });
1803
2022
  return;
1804
2023
  }
1805
2024
  const file = readArchiveFile(projectPath, version);
1806
2025
  if (!file) {
1807
2026
  if (json) {
1808
- console.log(JSON.stringify({ schemaVersion: 1, error: 'not-found', version, available }, null, 2));
2027
+ // legacy 形态:error 恒 string、既有键序不变,errorKind 恒末尾
2028
+ console.log(JSON.stringify({ schemaVersion: 1, error: 'not-found', version, available, errorKind: 'refusal' }, null, 2));
1809
2029
  process.exitCode = 1;
1810
2030
  return;
1811
2031
  }
1812
- console.error(t('flow.archive.show.notFoundTmpl', lang, {
2032
+ const message = t('flow.archive.show.notFoundTmpl', lang, {
1813
2033
  version, versions: available.length > 0 ? available.join(', ') : t('common.none', lang),
1814
- }));
1815
- process.exitCode = 1;
2034
+ });
2035
+ failCli({ code: 'INVALID_ARGUMENT', message, detail: { version }, stderrLines: [message] });
1816
2036
  return;
1817
2037
  }
1818
2038
  // --json = 全量载荷(ArchiveFile 原文自带 schemaVersion:1,含 log / reviews)——显式请求才取全量(REV-79-003)
2039
+ // G-1(stage-95 op-001):命令层 withSchemaVersion 包装,保证 schemaVersion 恒为首键;既有键相对顺序不变
1819
2040
  if (json) {
1820
- console.log(JSON.stringify(file, null, 2));
2041
+ console.log(JSON.stringify(withSchemaVersion(file), null, 2));
1821
2042
  return;
1822
2043
  }
1823
2044
  console.log(t('flow.archive.show.titleTmpl', lang, { version, at: typeof file.archived_at === 'string' ? file.archived_at : '' }));
@@ -1863,10 +2084,9 @@ export function registerFlowCommand(program) {
1863
2084
  reconciled = mgr.reconcileStatusMd({ dryRun: options.dryRun });
1864
2085
  }
1865
2086
  catch (err) {
1866
- // 写盘失败 → 明确错误 + 非 0 退出
2087
+ // 写盘失败 → 内部错误(D-95-11:原 exit 1 → exit 3,登记为分类补全);非 JSON 文案逐字不变
1867
2088
  const msg = err instanceof Error ? err.message : String(err);
1868
- console.error(t('common.errorTmpl', lang, { msg }));
1869
- process.exit(1);
2089
+ failInternal(err, { stderrLines: [t('common.errorTmpl', lang, { msg })] });
1870
2090
  return;
1871
2091
  }
1872
2092
  }
@@ -1879,11 +2099,8 @@ export function registerFlowCommand(program) {
1879
2099
  ...archiveItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
1880
2100
  ...reqItems.map((i) => ({ name: i.section, status: i.status, detail: i.message })),
1881
2101
  ];
1882
- const payload = { schemaVersion: 1, ok: report.ok, items };
1883
- // 与 op-001 的 health --json 合并字段(--fix 时追加 reconciled)
1884
- if (reconciled) {
1885
- payload.reconciled = reconciled;
1886
- }
2102
+ // D-95-02:reconciled 恒在(非 --fix 时 null),位置固定于 items 之后
2103
+ const payload = { schemaVersion: 1, ok: report.ok, items, reconciled: reconciled ?? null };
1887
2104
  console.log(JSON.stringify(payload, null, 2));
1888
2105
  if (!report.ok) {
1889
2106
  process.exit(1);
@@ -1947,14 +2164,42 @@ export function registerFlowCommand(program) {
1947
2164
  flow
1948
2165
  .command('recover')
1949
2166
  .description('跨会话上下文恢复:输出流水线状态、阻塞原因和待处理任务')
1950
- .action(() => {
2167
+ .option('--json', t('help.flow.recover.json', getCliLang(process.cwd())))
2168
+ .action((options) => {
1951
2169
  const lang = getCliLang(process.cwd());
1952
2170
  const mgr = createManager();
1953
2171
  if (!mgr.isLoaded()) {
2172
+ // JSON 分支:未初始化 exit 0 + initialized:false + 空值(镜像人类 exit 0)
2173
+ if (options.json) {
2174
+ emitJson(withSchemaVersion({
2175
+ initialized: false,
2176
+ phase: null,
2177
+ currentOp: null,
2178
+ stageStatus: null,
2179
+ blockedBy: null,
2180
+ pendingTasks: [],
2181
+ stageStats: {},
2182
+ }));
2183
+ return;
2184
+ }
1954
2185
  console.log(t('common.noInit', lang));
1955
2186
  return;
1956
2187
  }
1957
2188
  const recovery = mgr.recoverContext(lang);
2189
+ const stageStats = mgr.getAllStageStats();
2190
+ // JSON 分支(键序:initialized,phase,currentOp,stageStatus,blockedBy,pendingTasks,stageStats)
2191
+ if (options.json) {
2192
+ emitJson(withSchemaVersion({
2193
+ initialized: true,
2194
+ phase: recovery.phase,
2195
+ currentOp: recovery.currentOp,
2196
+ stageStatus: recovery.stageStatus,
2197
+ blockedBy: recovery.blockedBy === '' ? null : recovery.blockedBy,
2198
+ pendingTasks: recovery.pendingTasks,
2199
+ stageStats,
2200
+ }));
2201
+ return;
2202
+ }
1958
2203
  console.log('');
1959
2204
  console.log(t('flow.recover.title', lang));
1960
2205
  console.log('');
@@ -2171,9 +2416,10 @@ export function registerFlowCommand(program) {
2171
2416
  }
2172
2417
  }
2173
2418
  catch (err) {
2174
- // 并发写冲突:统一单点(i18n 文案 + 退出码 2)
2419
+ // 并发写冲突:统一单点(i18n 文案 + 退出码 2);JSON 模式置码返回 → 显式 return 防落入通用分支
2175
2420
  if (isFlowConcurrentError(err)) {
2176
2421
  handleConcurrentConflict(err, lang);
2422
+ return;
2177
2423
  }
2178
2424
  // 失败:统一错误模板 + 非 0 退出(T39)
2179
2425
  const msg = err instanceof Error ? err.message : String(err);
@@ -2254,6 +2500,18 @@ function findPhaseEnteredAtMs(mgr, stageArg, fromPhase, logLenBefore) {
2254
2500
  function createManager() {
2255
2501
  return new FlowManager(process.cwd());
2256
2502
  }
2503
+ /**
2504
+ * `--stage` 非法 / 不存在错误的 stderr 两行(逐字保持既有文案与顺序)。
2505
+ * @param lang 语言标识
2506
+ * @param projectPath 项目根
2507
+ * @param input 用户输入的阶段 ID
2508
+ */
2509
+ function stageIdErrorLines(lang, projectPath, input) {
2510
+ return [
2511
+ t('common.stageIdInvalidTmpl', lang, { input }),
2512
+ t('common.stageIdSuggestTmpl', lang, { suggest: suggestStageId(projectPath, input) }),
2513
+ ];
2514
+ }
2257
2515
  /**
2258
2516
  * 断言不存在阻塞中的 open REV(REV 闭环兜底,B5-3 / REV-52-001)。
2259
2517
  * 命中 → 打印列出并返回命中的 REV 列表(调用方决定退出码与文案);无命中 → 空数组。
@@ -2267,8 +2525,8 @@ function assertNoBlockingOpenRev(mgr, stage, lang) {
2267
2525
  return [];
2268
2526
  }
2269
2527
  // REV-007:补 `.` 分隔符(与 core flow-manager.ts 的 stageId + '.' 范式一致),避免 stageId 前缀重叠误拦
2270
- const stageReviews = mgr.getReviewItems().filter((r) => r.op === stage || r.op.startsWith(stage + '.'));
2271
- const blockingOpen = stageReviews.filter((r) => r.blocking !== false && r.status === 'open');
2528
+ // 单点委托:阶段域 + blocking !== false + status === 'open' 判定归核心 listStageBlockingOpenReviews(禁第二份)
2529
+ const blockingOpen = listStageBlockingOpenReviews(mgr.getReviewItems(), stage);
2272
2530
  if (blockingOpen.length > 0) {
2273
2531
  console.warn(t('flow.advance.revBlockedTitleTmpl', lang, { n: String(blockingOpen.length) }));
2274
2532
  for (const rev of blockingOpen) {
@@ -2313,9 +2571,21 @@ function collectOpStates(stage) {
2313
2571
  }
2314
2572
  return result;
2315
2573
  }
2574
+ /**
2575
+ * 由 ops (opId→state) 派生汇总:total = 键数;byState 仅含观测到的状态(无零值键)。
2576
+ * 键序 = 状态首次出现序;空 ops → { total: 0, byState: {} }。
2577
+ */
2578
+ function summarizeOpStates(ops) {
2579
+ const byState = {};
2580
+ for (const state of Object.values(ops)) {
2581
+ byState[state] = (byState[state] ?? 0) + 1;
2582
+ }
2583
+ return { total: Object.keys(ops).length, byState };
2584
+ }
2316
2585
  /**
2317
2586
  * 构建 `flow status --json` 的结构化输出(B1-1)。
2318
2587
  * 顶层对象 + schemaVersion:1;stages 为数组,ops 为 opId→state 映射;counts 为阶段计数。
2588
+ * stage-96 op-002:stages[] 末位追加 5 个调度属性(既有 5 键键序/类型逐字不变)。
2319
2589
  */
2320
2590
  function buildStatusJson(mgr) {
2321
2591
  const data = mgr.getData();
@@ -2327,13 +2597,23 @@ function buildStatusJson(mgr) {
2327
2597
  counts: { total: 0, done: 0, active: 0 },
2328
2598
  };
2329
2599
  }
2330
- const stages = Object.entries(data.stages).map(([id, stage]) => ({
2331
- id,
2332
- phase: stage.phase,
2333
- status: stage.status,
2334
- deps: stage.deps ?? [],
2335
- ops: collectOpStates(stage),
2336
- }));
2600
+ const stages = Object.entries(data.stages).map(([id, stage]) => {
2601
+ const ops = collectOpStates(stage);
2602
+ // 模式单点解析:逐阶段经 resolveStageWorkflowMode(同一 manager 快照;D-96-12 不缓存)
2603
+ const workflowMode = mgr.resolveStageWorkflowMode(id).value;
2604
+ return {
2605
+ id,
2606
+ phase: stage.phase,
2607
+ status: stage.status,
2608
+ deps: stage.deps ?? [],
2609
+ ops,
2610
+ workflowMode,
2611
+ requirementId: stage.requirement_id ?? null,
2612
+ reviewTier: MODE_DERIVED_TIER[workflowMode],
2613
+ effectiveReviewTier: DEFAULT_EFFECTIVE_REVIEW_TIER,
2614
+ opsSummary: summarizeOpStates(ops),
2615
+ };
2616
+ });
2337
2617
  const total = stages.length;
2338
2618
  const done = stages.filter((s) => s.phase === 'done').length;
2339
2619
  return {