@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +15 -0
  2. package/package.json +7 -4
  3. package/scripts/rfl.mjs +308 -0
  4. package/skills/workflow-script-format/SKILL.md +1 -1
  5. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  6. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  7. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  8. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  9. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  10. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  11. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  12. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  13. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  14. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  15. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  16. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  17. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  18. package/src/execution/__tests__/execution-record.test.ts +199 -0
  19. package/src/execution/__tests__/finalize-record.test.ts +197 -15
  20. package/src/execution/__tests__/format.test.ts +131 -7
  21. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  22. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  23. package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
  24. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  25. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  26. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  27. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  28. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  29. package/src/execution/__tests__/list-component.test.ts +59 -5
  30. package/src/execution/__tests__/list-fields.test.ts +109 -0
  31. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  32. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  33. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  34. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  35. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  36. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  37. package/src/execution/__tests__/record-store.test.ts +442 -54
  38. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  39. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  40. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  41. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  42. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  43. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  44. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  45. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  46. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  47. package/src/execution/__tests__/session-pending.test.ts +61 -2
  48. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  49. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  50. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  51. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  52. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  53. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  54. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  55. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  56. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  57. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  59. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  60. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  61. package/src/execution/__tests__/tool-action.test.ts +12 -10
  62. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  63. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  64. package/src/execution/__tests__/worktree-manager.test.ts +300 -90
  65. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
  66. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  67. package/src/execution/__tests__/worktree-registry.test.ts +72 -34
  68. package/src/execution/agent-result-mapper.ts +4 -1
  69. package/src/execution/argv-mirror.ts +21 -2
  70. package/src/execution/channel-registry-access.ts +3 -1
  71. package/src/execution/execution-record.ts +126 -9
  72. package/src/execution/finalize-record.ts +99 -13
  73. package/src/execution/idle-gc.ts +47 -0
  74. package/src/execution/lifecycle-manager.ts +491 -0
  75. package/src/execution/lifecycle-predicates.ts +65 -0
  76. package/src/execution/manifest-store.ts +61 -16
  77. package/src/execution/model-resolver.ts +26 -5
  78. package/src/execution/notifier.ts +69 -12
  79. package/src/execution/pi-invocation.ts +21 -1
  80. package/src/execution/record-entry.ts +118 -0
  81. package/src/execution/record-store.ts +844 -108
  82. package/src/execution/session-pending.ts +121 -49
  83. package/src/execution/session-reconstructor.ts +224 -7
  84. package/src/execution/session-runner.ts +713 -316
  85. package/src/execution/sessions-index.ts +304 -0
  86. package/src/execution/stdin-writer.ts +93 -7
  87. package/src/execution/stream-sink.ts +20 -3
  88. package/src/execution/subagent-service.ts +917 -138
  89. package/src/execution/temp-prompt.ts +8 -3
  90. package/src/execution/turn-limiter.ts +14 -0
  91. package/src/execution/types.ts +218 -19
  92. package/src/execution/worktree-manager.ts +449 -59
  93. package/src/execution/worktree-registry.ts +97 -29
  94. package/src/index.ts +318 -20
  95. package/src/injectors/subagent-list-injector.ts +26 -8
  96. package/src/injectors/workflow-list-injector.ts +25 -8
  97. package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
  98. package/src/interface/__tests__/tool-render.test.ts +10 -8
  99. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
  100. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
  101. package/src/interface/bg-notify-render.ts +32 -8
  102. package/src/interface/command-actions.ts +26 -7
  103. package/src/interface/commands.ts +21 -22
  104. package/src/interface/format.ts +53 -20
  105. package/src/interface/gui-mappers.ts +6 -8
  106. package/src/interface/helpers.ts +170 -10
  107. package/src/interface/list-component.ts +53 -14
  108. package/src/interface/subagent-actions.ts +235 -17
  109. package/src/interface/subagent-tool.ts +81 -16
  110. package/src/interface/subagents.ts +2 -1
  111. package/src/interface/tool-render.ts +21 -27
  112. package/src/interface/tool-workflow-script.ts +29 -33
  113. package/src/interface/tool-workflow.ts +67 -100
  114. package/src/interface/views/WorkflowsView.ts +89 -32
  115. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  116. package/src/interface/views/detail-content.ts +1 -1
  117. package/src/interface/views/format.ts +3 -3
  118. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
  119. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  120. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  121. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  122. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  123. package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
  124. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  128. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  129. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
  130. package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
  131. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  132. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  133. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
  134. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  135. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  136. package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
  137. package/src/orchestration/agent-opts-resolver.ts +4 -1
  138. package/src/orchestration/args-validator.ts +2 -2
  139. package/src/orchestration/config-loader.ts +30 -1
  140. package/src/orchestration/error-recovery.ts +137 -30
  141. package/src/orchestration/execute-agent-call.ts +44 -10
  142. package/src/orchestration/jsonl-run-store.ts +397 -71
  143. package/src/orchestration/launcher.ts +7 -1
  144. package/src/orchestration/lifecycle.ts +145 -133
  145. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  146. package/src/orchestration/models/budget.ts +1 -1
  147. package/src/orchestration/models/run-runtime.ts +15 -17
  148. package/src/orchestration/models/run-spec.ts +2 -2
  149. package/src/orchestration/models/run-state.ts +3 -3
  150. package/src/orchestration/models/trace.ts +95 -15
  151. package/src/orchestration/models/types.ts +8 -9
  152. package/src/orchestration/models/workflow-run.ts +50 -71
  153. package/src/orchestration/models/workflow-script.ts +32 -1
  154. package/src/orchestration/skill-discovery.ts +30 -0
  155. package/src/orchestration/worker-handle.ts +1 -1
  156. package/src/orchestration/worker-host.ts +1 -1
  157. package/src/orchestration/worker-script-builder.ts +38 -11
  158. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  159. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  160. package/src/shared/__tests__/resource-discovery.test.ts +79 -0
  161. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  162. package/src/shared/agent-ref.ts +22 -1
  163. package/src/shared/resource-discovery.ts +162 -59
  164. package/src/shared/schema-jsonify.ts +56 -0
  165. package/workflows/review-fix-loop-utils.cjs +542 -32
  166. package/workflows/review-fix-loop.js +462 -109
@@ -44,6 +44,7 @@ parameters:
44
44
  maxFixAttempts: { type: integer, default: 2, minimum: 1 }
45
45
  convergeNewIssues: { type: integer, default: 1, minimum: 1 }
46
46
  convergeRounds: { type: integer, default: 2, minimum: 1 }
47
+ aggregatorModel: { type: string, description: cheaper model for the aggregate step (mechanical dedup/format work); route per global/project AGENTS.md, confirm with the owner before adding a new entry; defaults to the run model }
47
48
  reviewPrompt: { type: string }
48
49
  fixPrompt: { type: string }
49
50
  fallowScan: { type: boolean, default: false }
@@ -84,6 +85,7 @@ const {
84
85
  buildScopedRecheckPrompt,
85
86
  wrapUntrusted,
86
87
  buildFixPrompt,
88
+ buildR1ReviewPrompt,
87
89
  buildR2ReviewPrompt,
88
90
  buildAggregatorPrompt,
89
91
  resolveReviewReportPath,
@@ -98,6 +100,16 @@ const {
98
100
  parseResult,
99
101
  normalizeAggregatorResult,
100
102
  parseAggregatedMd,
103
+ resolveRunRoot,
104
+ computeOrigin,
105
+ recordDormant,
106
+ filterActiveIds,
107
+ filterDormantFromRecon,
108
+ landScores,
109
+ countMissingFields,
110
+ backfillFixRegression,
111
+ applyCleanRoundBackfill,
112
+ resolveAggregatorModel,
101
113
  resolveAgentDefs,
102
114
  recordAgentClean,
103
115
  recordAgentDirty,
@@ -114,7 +126,7 @@ const {
114
126
  for (const key of Object.keys($ARGS)) {
115
127
  if (VALID_ARG_KEYS.has(key)) continue;
116
128
  if (/^batch\d+$/.test(key)) continue;
117
- fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/skipCleanAgents/recheckAfterFix/fallowScan/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds)");
129
+ fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/skipCleanAgents/recheckAfterFix/fallowScan/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds/aggregatorModel)");
118
130
  }
119
131
 
120
132
  const targetType = $ARGS.targetType;
@@ -162,6 +174,11 @@ const maxFixAttempts = coerceInt($ARGS.maxFixAttempts, 2);
162
174
  const convergeNewIssues = coerceInt($ARGS.convergeNewIssues, 1);
163
175
  const convergeRounds = coerceInt($ARGS.convergeRounds, 2);
164
176
  const MODEL = $MODEL;
177
+ // rfl aggregator 降档(tier-1 6.4,T8):聚合是机械去重/格式化工作,可降档到便宜
178
+ // 模型。模型路由参考全局/项目 AGENTS.md(当前用户全局有条目:
179
+ // xiaomi-token-plan-cn/mimo-v2.5-pro,thinking 开非 max);无条目请先与主人确认
180
+ // 并写入 AGENTS.md。缺省回退主模型(行为与现状一致)。
181
+ const AGG_MODEL = resolveAggregatorModel($ARGS.aggregatorModel, MODEL);
165
182
 
166
183
  // base 锁定(RC-6,5.6):git-diff 场景 run 启动时锁定 base commit,全程用锁定 hash 构造
167
184
  // diff 指令,防止 run 期间 base ref 被更新导致各轮 diff 范围不一致。rev-parse 失败(非 git
@@ -214,22 +231,29 @@ const reviewerSchema = {
214
231
  },
215
232
  required: ["prev_id", "status"],
216
233
  },
217
- description: "R2+ reconciliation table (structured): MANDATORY for R2+ rounds, optional for R1",
234
+ description: "Reconciliation table (structured): return [] on R1 (no previous round); on R2+ every previous issue_id must have a status entry",
218
235
  },
219
236
  },
220
- required: ["report_file", "must_fix", "suggestion"],
237
+ // T9 前缀稳定化(tier-1 6.9):required 恒含 reconciliation——schema JSON 逐字嵌入
238
+ // appendSystemPrompt,R1↔R2+ 分叉会造成 system 段字节差异(消息级缓存前缀失效)。
239
+ // R1 的合规输出 = 空数组(prompt 动态段明示)。
240
+ required: ["report_file", "must_fix", "suggestion", "reconciliation"],
221
241
  };
222
242
 
223
243
  const aggregatorSchema = {
224
244
  type: "object",
225
245
  properties: {
226
246
  report_file: { type: "string", description: "Absolute path to aggregated.md" },
227
- must_fix: { type: "number", description: "Total must-fix after dedup across all dimensions" },
247
+ must_fix: { type: "number", description: "Total must-fix after dedup across all dimensions, counting adjudication=evidence entries only" },
228
248
  suggestion: { type: "number", description: "Total suggestions after dedup across all dimensions" },
229
249
  must_fix_ids: {
230
250
  type: "array",
231
251
  // M1: 支持 [{id, severity}] 对象(severity: critical/major/minor——converged 终止的
232
252
  // 「无 critical」判定数据源)+ 旧格式 string[] 兼容。ajv 权威校验两者皆放行。
253
+ // rfl 数据链(tier-1 §7.2):对象分支增可选 files/evidence/guidance/note/adjudication——
254
+ // origin 归因(files)、修复指引(guidance)、裁决落盘(adjudication+note)的数据源。
255
+ // 降级条目(adjudication ∈ {downgraded, unverified})保留在数组中,由主循环
256
+ // filterActiveIds 过滤出修复队列(must_fix 计数由 aggregator 按非降级条目报)。
233
257
  items: {
234
258
  oneOf: [
235
259
  { type: "string" },
@@ -238,7 +262,14 @@ const aggregatorSchema = {
238
262
  required: ["id"],
239
263
  properties: {
240
264
  id: { type: "string" },
241
- severity: { type: "string" },
265
+ // A5: severity enum 约束生成侧(prompt 已列三值);normalize 侧另有
266
+ // 枚举回退兜底(畸形值 → major),双层防御。
267
+ severity: { type: "string", enum: ["critical", "major", "minor"] },
268
+ files: { type: "array", items: { type: "string" }, description: "File paths cited by the issue (regression attribution)" },
269
+ evidence: { type: "string", description: "Cited evidence (files/lines/test results) from the sub-review" },
270
+ guidance: { type: "string", description: "One-line fix direction for the fixer (code wins on conflict)" },
271
+ adjudication: { type: "string", enum: ["evidence", "unverified", "downgraded"], description: "Evidence verdict; downgraded/unverified entries stay in this array but are filtered out of the fix queue" },
272
+ note: { type: "string", description: "Adjudication reason (required when adjudication is unverified/downgraded)" },
242
273
  },
243
274
  },
244
275
  ],
@@ -250,6 +281,23 @@ const aggregatorSchema = {
250
281
  items: { type: "string" },
251
282
  description: "Caution entries for weak-evidence or high-risk claims (passed to fix stage)",
252
283
  },
284
+ // rfl scores(tier-1 §7.2,M2 打分 rubric 消费):可选——M1 只透传 schema 与
285
+ // normalize,打分 prompt 段属 M2(T7)。提前落 schema 避免二次 schema 破坏性变更。
286
+ scores: {
287
+ type: "array",
288
+ items: {
289
+ type: "object",
290
+ properties: {
291
+ round: { type: "number", description: "Round of the scored target (R2 aggregation scoring R1 fix -> round=1)" },
292
+ targetKind: { type: "string", description: "reviewer | fix" },
293
+ targetName: { type: "string" },
294
+ dimensions: { type: "object", description: "Dimension name -> 0-10 score" },
295
+ total: { type: ["number", "null"], description: "Weighted total; null = not computable (e.g. clean-round LLM dimensions missing)" },
296
+ note: { type: "string" },
297
+ },
298
+ },
299
+ description: "Optional per-round quality scores (may be omitted until the scoring rubric lands)",
300
+ },
253
301
  },
254
302
  required: ["report_file", "must_fix", "suggestion"],
255
303
  };
@@ -296,10 +344,15 @@ const path = require("path");
296
344
  const os = require("os");
297
345
 
298
346
  const RUN_ID = ($ARGS._runId && typeof $ARGS._runId === "string") ? $ARGS._runId : "run-" + Date.now();
299
- const RUN_ROOT = path.join(os.tmpdir(), "review-fix-loop", RUN_ID);
347
+ // rfl 仪表(tier-1 §6.8/§7.5):state 持久化迁出 $TMPDIR(系统清理即丢失)——
348
+ // ~/.review-fix-loop/<repo-slug>/<runId>/;$TMPDIR 仅作 home 不可写时的降级。
349
+ // 旧 $TMPDIR run 不迁移(易失数据不抢救),loadState 只从新位置读。
350
+ const { root: RUN_ROOT, degraded: RUN_ROOT_DEGRADED } = resolveRunRoot({ runId: RUN_ID, cwd: process.cwd() });
351
+ if (RUN_ROOT_DEGRADED) {
352
+ log("WARN: ~/.review-fix-loop not writable, falling back to $TMPDIR: " + RUN_ROOT);
353
+ }
300
354
  const STATE_FILE = RUN_ROOT + "/state.json";
301
355
 
302
- fs.mkdirSync(RUN_ROOT, { recursive: true });
303
356
  log("Run directory: " + RUN_ROOT);
304
357
 
305
358
  // ── Startup fail-fast: validate agent ref paths exist (ADR-0003 D6) ─
@@ -330,36 +383,124 @@ validateAgentPaths(startupDefs);
330
383
 
331
384
  // ── State management (persistent, atomic writes) ────────────────────
332
385
 
386
+ // 全新初始 state 模板:catch 分支(无残留)与「读到残留 state」分支共用(A1)。
387
+ // 易变累积字段(calls/fixResults/fixCount/issues/dormant...)全部归零——
388
+ // attempt-2 重放会重新 recordCall/push,复用旧 state 必然双记。
389
+ function freshState() {
390
+ return {
391
+ meta: {
392
+ runId: RUN_ID, workspace: $WORKSPACE || "", model: MODEL || "(default)",
393
+ targetType, target, batches: BATCHES, startedAt: new Date().toISOString(),
394
+ },
395
+ agentStatus: {},
396
+ fixCount: 0,
397
+ batches: [],
398
+ calls: [],
399
+ dormant: [],
400
+ fixResults: [],
401
+ issues: undefined,
402
+ knownRemaining: [],
403
+ convergeStreak: 0,
404
+ lastModifiedFiles: [],
405
+ };
406
+ }
407
+
333
408
  function loadState() {
334
409
  try {
335
- return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
410
+ const parsed = JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
411
+ // A1(rebuild 双记修复):读到既有 state = 同 _runId 上一 attempt 的残留——
412
+ // engine 因 script-error rebuild 后同 RUN_ROOT 重跑本脚本,原样返回旧 state 会让
413
+ // attempt-2 重放的 recordCall/fixResults/fixCount/issues 全部双记。state.json 在
414
+ // RUN_ROOT(按 runId 隔离)内,存在即属本 run 前次 attempt,无跨 run 误判。
415
+ // 只保留 previousAttempts 计数(观测 rebuild 次数),其余全部重置。
416
+ const prev = (parsed && parsed.meta && parsed.meta.previousAttempts) || 0;
417
+ log("INFO: found leftover state.json for this run (previous attempt residue after engine rebuild?) — resetting volatile accumulators (previousAttempts=" + (prev + 1) + ")");
418
+ const s = freshState();
419
+ s.meta.previousAttempts = prev + 1;
420
+ return s;
336
421
  } catch {
337
- return {
338
- meta: {
339
- runId: RUN_ID, workspace: $WORKSPACE || "", model: MODEL || "(default)",
340
- targetType, target, batches: BATCHES, startedAt: new Date().toISOString(),
341
- },
342
- agentStatus: {},
343
- fixCount: 0,
344
- batches: [],
345
- fixResults: [],
346
- issues: undefined,
347
- knownRemaining: [],
348
- convergeStreak: 0,
349
- lastModifiedFiles: [],
350
- };
422
+ return freshState();
351
423
  }
352
424
  }
353
425
 
426
+ // rfl 仪表:终止原因快照声明上移(saveState 引用;所有实际调用发生在
427
+ // 主循环初始化后,TDZ 不触发)。CLI list/stats 的终止原因数据源。
428
+ let terminated = "clean";
429
+
354
430
  function saveState(state) {
431
+ // rfl 仪表:terminated 随每次落盘快照(结构化终止路径设值后即 saveState)
432
+ if (state.meta) state.meta.terminated = terminated;
355
433
  const tmp = STATE_FILE + ".tmp";
356
434
  fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
357
435
  fs.renameSync(tmp, STATE_FILE);
358
436
  }
359
437
 
438
+ // ── rfl 仪表(tier-1 §7.3):calls[] 采集 + phaseTimings ───────────
439
+ // 每次 agent() 调用记录资源与耗时;数据源 = returnMeta 透传的 usage/durationMs/
440
+ // sessionId(引擎 T1 透传;旧引擎缺字段时 usage 键省略(undefined)、durationMs
441
+ // 落 null——phase 级耗时由 phaseTimings 覆盖)。
442
+ function recordCall(entry) {
443
+ if (!Array.isArray(state.calls)) state.calls = [];
444
+ state.calls.push(entry);
445
+ }
446
+ function normUsage(meta) {
447
+ const u = meta && typeof meta === "object" ? meta.usage : undefined;
448
+ if (!u || typeof u !== "object") return undefined;
449
+ return {
450
+ input: u.input ?? 0, output: u.output ?? 0,
451
+ cacheRead: u.cacheRead ?? 0, cacheWrite: u.cacheWrite ?? 0, cost: u.cost ?? 0,
452
+ };
453
+ }
454
+ // A10:引擎 T1 透传缺位(returnMeta 在但 usage 缺失 / durationMs 非数)只 WARN 一次
455
+ //(逐调用都打会淹没日志)——提示引擎版本或透传链路问题,calls[] usage 已降级。
456
+ let warnedTelemetryMissing = false;
457
+ function warnTelemetryMissingOnce() {
458
+ if (warnedTelemetryMissing) return;
459
+ warnedTelemetryMissing = true;
460
+ log("WARN: returnMeta usage/durationMs missing — engine telemetry passthrough (T1) not active? calls[] usage degraded");
461
+ }
462
+ // returnMeta 对象 → calls[] 条目(十字段,设计 §7.3)。
463
+ // A11(model 字段语义):model = 请求时参数。agent-ref 调用的实际模型由主线程按
464
+ // .md frontmatter 解析(resolveAgentOpts),此处可能记 "(default)" 而实际跑 frontmatter
465
+ // 模型——引擎侧透传实际 model 是后续工作,消费侧(§6.4 降档归因)需知此局限。
466
+ // A12(promptMode 语义):该字段 = review prompt 模式("full" | "scoped"),对非
467
+ // reviewer 角色(aggregator/fixer)无意义、显式传 null;仅缺省(undefined)回退
468
+ // "full"——空串/null 不再被 || 误转 "full"。
469
+ function buildCallRecord({ batch, round, role, name, model, prompt, promptMode, meta }) {
470
+ const promptStr = typeof prompt === "string" ? prompt : "";
471
+ const metaObj = meta && typeof meta === "object" ? meta : undefined;
472
+ // W1:失败调用(returnMeta = {value, error})天然无 usage/durationMs——排除
473
+ // error 分支,否则 agent 调用失败被误诊为「引擎透传未上线」并烧掉 once 名额。
474
+ // 三个 recordCall 调用点(review/aggregator/fixer)共用本函数,一处修复全覆盖。
475
+ if (metaObj && !metaObj.error && (normUsage(metaObj) === undefined || typeof metaObj.durationMs !== "number")) {
476
+ warnTelemetryMissingOnce();
477
+ }
478
+ return {
479
+ batch, round, role, name: name || role,
480
+ model: model || "(default)",
481
+ durationMs: (metaObj && typeof metaObj.durationMs === "number")
482
+ ? metaObj.durationMs
483
+ : null,
484
+ usage: normUsage(metaObj),
485
+ promptMode: promptMode === undefined ? "full" : promptMode,
486
+ promptBytes: Buffer.byteLength(promptStr, "utf8"),
487
+ sessionId: (metaObj && typeof metaObj.sessionId === "string") ? metaObj.sessionId : undefined,
488
+ };
489
+ }
490
+
360
491
  // clean 记录(recordAgentClean/recordAgentDirty 与跨批跳过判定 shouldSkipAgent
361
492
  // 在 review-fix-loop-utils.cjs,vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)
362
493
 
494
+ // A8(guidance/evidence 缺失观测):活跃条目缺 guidance/evidence 的单行 WARN——
495
+ // 数据链断点(aggregator 未提取 / 归一化丢失)的可观测信号;fixer 免侦查降级提示。
496
+ // 仅 N+M>0 时打,不逐条刷屏。
497
+ function warnMissingFields(mustFixIds) {
498
+ const miss = countMissingFields(mustFixIds);
499
+ if (miss.missingGuidance > 0 || miss.missingEvidence > 0) {
500
+ log("WARN: " + miss.missingGuidance + " active must-fix entries lack guidance (" + miss.missingEvidence + " lack evidence) — fixer self-investigation expected");
501
+ }
502
+ }
503
+
363
504
  // ── Agent defs(loadAgentMd/resolveAgentDefs 在 review-fix-loop-utils.cjs) ──
364
505
 
365
506
  // ── Build review calls ──────────────────────────────────────────────
@@ -379,6 +520,71 @@ function lastModifiedFiles() {
379
520
  return (r && Array.isArray(r.modifiedFiles)) ? r.modifiedFiles : [];
380
521
  }
381
522
 
523
+ // buildReviewCall 分支构造:fallow 内置工具型(无 .md,静态分析 prompt)。
524
+ function buildFallowReviewCall(base, def, header, roundDir) {
525
+ return {
526
+ ...base,
527
+ prompt: [
528
+ header,
529
+ "",
530
+ "Fallow static-analysis pre-scan (tool-based, NOT a git-diff review).",
531
+ "",
532
+ "Steps:",
533
+ "1. Check if fallow is installed: `which fallow`",
534
+ "2. If NOT installed: write the report with a one-line note, must_fix=0, suggestion=0.",
535
+ "3. If installed, run: `fallow audit --base " + lockedBase.base + " --format json --quiet`",
536
+ "4. Extract: complexity hotspots, dead code, unused exports, circular deps",
537
+ "5. Classify findings: critical/major count into must_fix; minor into suggestion.",
538
+ "",
539
+ "output 路径:" + roundDir + "/" + def.report + ".md",
540
+ "Write report to: " + roundDir + "/" + def.report + ".md",
541
+ ].join("\n"),
542
+ };
543
+ }
544
+
545
+ // buildReviewCall 分支构造:R2+ 三段式(5.2)——verify-first 对账 + known-remaining + 收敛 hunt。
546
+ function buildR2ReviewCall(base, def, header, round, max, roundDir, batchIndex) {
547
+ const prevRoundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1);
548
+ return {
549
+ ...base,
550
+ // T9:无 per-round spread——reviewerSchema 跨轮统一(缓存前缀稳定前提)
551
+ prompt: buildR2ReviewPrompt({
552
+ header, round, max, roundDir,
553
+ reportFile: def.report,
554
+ aggPath: prevRoundDir + "/aggregated.md",
555
+ fixResult: state.fixResults && state.fixResults.length
556
+ ? state.fixResults[state.fixResults.length - 1]
557
+ : null,
558
+ knownRemaining: (state.knownRemaining && Array.isArray(state.knownRemaining)) ? state.knownRemaining : [],
559
+ // rfl dormant 复活通道(tier-1 6.3 delta ③):降级条目注入 R2+ prompt(动态段内容)
560
+ dormant: (state.dormant && Array.isArray(state.dormant)) ? state.dormant : [],
561
+ reviewPrompt, reviewInstruction,
562
+ }),
563
+ agent: def.path,
564
+ };
565
+ }
566
+
567
+ // buildReviewCall 分支构造:recheck 限定(5.5)——clean agent 重派时只审 fix 改动
568
+ // 文件 + 自检关联点(scope = modifiedFiles ∪ affected_files,后者来自
569
+ // state.fixImpactFiles),不诱导全量重扫。
570
+ function buildScopedReviewCall(base, def, header, round, max, roundDir, batchIndex) {
571
+ return {
572
+ ...base,
573
+ prompt: buildScopedRecheckPrompt({
574
+ header, round, max, roundDir,
575
+ reportFile: def.report,
576
+ modifiedFiles: lastModifiedFiles(),
577
+ affectedFiles: (state.fixImpactFiles && Array.isArray(state.fixImpactFiles)) ? state.fixImpactFiles : [],
578
+ aggPath: RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1) + "/aggregated.md",
579
+ fixResult: state.fixResults && state.fixResults.length
580
+ ? state.fixResults[state.fixResults.length - 1]
581
+ : null,
582
+ reviewPrompt, reviewInstruction,
583
+ }),
584
+ agent: def.path,
585
+ };
586
+ }
587
+
382
588
  function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
383
589
  const header = "Batch " + batchIndex + " Round " + round + "/" + max + " — " + BATCH_NAMES[batchIndex - 1];
384
590
  const prevBatchesHint = batchIndex > 1
@@ -398,80 +604,27 @@ function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
398
604
  // S4:agentRef = 路径(非 fallow 的 def 必有 path)——systemPrompt/model 由主线程
399
605
  // resolveAgentOpts 按 path 加载注入,脚本不再拼 md 内容。
400
606
  if (def.isFallow) {
401
- return {
402
- ...base,
403
- prompt: [
404
- header,
405
- "",
406
- "Fallow static-analysis pre-scan (tool-based, NOT a git-diff review).",
407
- "",
408
- "Steps:",
409
- "1. Check if fallow is installed: `which fallow`",
410
- "2. If NOT installed: write the report with a one-line note, must_fix=0, suggestion=0.",
411
- "3. If installed, run: `fallow audit --base " + lockedBase.base + " --format json --quiet`",
412
- "4. Extract: complexity hotspots, dead code, unused exports, circular deps",
413
- "5. Classify findings: critical/major count into must_fix; minor into suggestion.",
414
- "",
415
- "output 路径:" + roundDir + "/" + def.report + ".md",
416
- "Write report to: " + roundDir + "/" + def.report + ".md",
417
- ].join("\n"),
418
- };
607
+ return buildFallowReviewCall(base, def, header, roundDir);
419
608
  }
420
609
 
421
610
  // R2+ 三段式分支(5.2):round>1 且非 scoped → verify-first 对账 + known-remaining + 收敛 hunt。
422
611
  // scoped 分支(recheck 限定)在下方单独处理(含对账段)。
423
612
  if (round > 1 && !scoped) {
424
- const prevRoundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1);
425
- return {
426
- ...base,
427
- schema: { ...reviewerSchema, required: [...reviewerSchema.required, "reconciliation"] },
428
- prompt: buildR2ReviewPrompt({
429
- header, round, max, roundDir,
430
- reportFile: def.report,
431
- aggPath: prevRoundDir + "/aggregated.md",
432
- fixResult: state.fixResults && state.fixResults.length
433
- ? state.fixResults[state.fixResults.length - 1]
434
- : null,
435
- knownRemaining: (state.knownRemaining && Array.isArray(state.knownRemaining)) ? state.knownRemaining : [],
436
- }),
437
- agent: def.path,
438
- };
613
+ return buildR2ReviewCall(base, def, header, round, max, roundDir, batchIndex);
439
614
  }
440
615
 
441
- // recheck 限定分支(5.5):clean agent 重派时只审 fix 改动文件 + 自检关联点
442
- // (scope = modifiedFiles ∪ affected_files,后者来自 state.fixImpactFiles),不诱导全量重扫。
616
+ // recheck 限定分支(5.5
443
617
  if (scoped) {
444
- return {
445
- ...base,
446
- // m2: scoped 分支与 R2+ 分支一致——reconciliation 必填(recheck 也须对账前轮 fix)
447
- schema: { ...reviewerSchema, required: [...reviewerSchema.required, "reconciliation"] },
448
- prompt: buildScopedRecheckPrompt({
449
- header, round, max, roundDir,
450
- reportFile: def.report,
451
- modifiedFiles: lastModifiedFiles(),
452
- affectedFiles: (state.fixImpactFiles && Array.isArray(state.fixImpactFiles)) ? state.fixImpactFiles : [],
453
- aggPath: RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1) + "/aggregated.md",
454
- fixResult: state.fixResults && state.fixResults.length
455
- ? state.fixResults[state.fixResults.length - 1]
456
- : null,
457
- }),
458
- agent: def.path,
459
- };
618
+ return buildScopedReviewCall(base, def, header, round, max, roundDir, batchIndex);
460
619
  }
461
620
 
621
+ // T9:R1 函数化(三模板共享静态段,动态后置)
462
622
  return {
463
623
  ...base,
464
- prompt: [
465
- header,
466
- "",
467
- reviewInstruction + prevBatchesHint,
468
- "",
469
- "Review requirements:",
470
- reviewPrompt,
471
- "",
472
- "output 路径:" + roundDir + "/" + def.report + ".md",
473
- "Write report to: " + roundDir + "/" + def.report + ".md",
474
- ].join("\n"),
624
+ prompt: buildR1ReviewPrompt({
625
+ header, roundDir, reportFile: def.report, prevBatchesHint,
626
+ reviewPrompt, reviewInstruction,
627
+ }),
475
628
  agent: def.path,
476
629
  };
477
630
  }
@@ -488,7 +641,6 @@ const state = loadState();
488
641
  state.meta = state.meta || {};
489
642
  state.meta.baseHash = lockedBase.hash || "";
490
643
  let totalFixed = 0;
491
- let terminated = "clean";
492
644
  let finalMessage = "";
493
645
 
494
646
  for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
@@ -507,6 +659,11 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
507
659
  state.issues = undefined;
508
660
  state.convergeStreak = 0;
509
661
  state.knownRemaining = [];
662
+ // A2(dormant 跨批污染修复):aggregator id 空间每批从 MF-1 重新编号,批 1 降级的
663
+ // dormant MF-3 会与批 2 活跃 MF-3 冲突——filterDormantFromRecon 把它从 reconSeen
664
+ // 剥离后,批 2 的 fix-attempted 被误反转 fixed;且批 1 dormant 持续注入批 2+ prompt。
665
+ // dormant 必须与 issues 同点做批作用域重置。
666
+ state.dormant = [];
510
667
 
511
668
  // 跨批跳过:agent 在更早批 clean 且此后无 fix → 本批不派发
512
669
  if (batchIndex > 1) {
@@ -543,7 +700,20 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
543
700
 
544
701
  log("Review: " + active.map((d) => d.name).join(", ") + " (" + active.length + " agent(s) in parallel)...");
545
702
  const calls = active.map((def) => buildReviewCall(def, round, maxRounds, batchIndex, roundDir, scopedClean.has(def.name)));
703
+ const phaseTimings = { review: null, aggregate: null, fix: null };
704
+ const reviewT0 = Date.now();
546
705
  const allRaw = await parallel(calls.map(runReviewAgent));
706
+ phaseTimings.review = [reviewT0, Date.now()];
707
+ // rfl 仪表:本轮全部 reviewer 调用落 calls[](allRaw 与 calls 一一对应,parallel 语义)
708
+ for (let i = 0; i < allRaw.length; i++) {
709
+ recordCall(buildCallRecord({
710
+ batch: batchIndex, round, role: "reviewer",
711
+ name: active[i].name, model: calls[i].model,
712
+ prompt: calls[i].prompt,
713
+ promptMode: scopedClean.has(active[i].name) ? "scoped" : "full",
714
+ meta: allRaw[i],
715
+ }));
716
+ }
547
717
 
548
718
  // per-agent 结果区分:parallel 结果与 calls 一一对应
549
719
  const reviewResults = [];
@@ -557,6 +727,12 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
557
727
  // 审查 agent 调用失败(含 AgentRegistry not found / 超时)。不裸 throw——与
558
728
  // aggregator-failure/stuck/fix-failure 路径一致:saveState + terminated 结构化终止,
559
729
  // 保证 state.json 有记录、调用方拿到结构化结果而非裸异常(MF-3)。
730
+ // W8:失败轮的当前轮条目也落 batchRounds(该轮 phase 时长不因结构化终止
731
+ // 从时间线消失);F3:聚合未发生/失败,mustFix/suggestion 是未知而非 0——
732
+ // 落 0 会被时间线误读为 clean 轮(rfl.mjs 消费侧 `mustFix ?? "-"` 对 null
733
+ // 显示 "-",suggestion 无消费点,null 安全);agents 为已收集的部分结果,
734
+ // aggregate/fix 相位 null 是如实采集(未到达)。
735
+ batchRounds.push({ round, mustFix: null, suggestion: null, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
560
736
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
561
737
  saveState(state);
562
738
  terminated = "review-failure";
@@ -577,7 +753,14 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
577
753
  log("WARN: failed to write report_content to " + reportPath + " — " + e.message);
578
754
  }
579
755
  }
580
- reviewResults.push(parsed);
756
+ // W6:report_content 已落盘(上方 resolveReviewReportPath 写盘 + report_file
757
+ // 回填),aggregator prompt 指示其 read report_file——整份正文再随 reviewResults
758
+ // JSON 内嵌即同一内容双份付费(实测 ~48% 重复)。push 前剥离;下游
759
+ // (buildAggregatorPrompt 路径清单 / must_fix 计数 / all-clean 判定)只消费
760
+ // report_file 与计数字段,无 report_content 消费。
761
+ const parsedWithoutContent = { ...parsed };
762
+ delete parsedWithoutContent.report_content;
763
+ reviewResults.push(parsedWithoutContent);
581
764
  for (const r of parsed.reconciliation) {
582
765
  if (r.status === "escalate") reconEscalate.add(r.prev_id);
583
766
  else if (r.status !== "fixed") reconSeen.add(r.prev_id);
@@ -594,6 +777,9 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
594
777
  } else {
595
778
  // tools 受限的 agent(如 tools: read)会过滤掉 structured-output → schema 失效,
596
779
  // 结果缺 must_fix。结构化终止(MF-3),raw 完整 dump 便于定位。
780
+ // W8:同 review-failure——当前轮条目落 batchRounds(phase 时长保留在时间线);
781
+ // F3:聚合未发生,mustFix/suggestion 未知非 0(null,消费侧 ?? "-" 兜底)。
782
+ batchRounds.push({ round, mustFix: null, suggestion: null, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
597
783
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
598
784
  saveState(state);
599
785
  terminated = "review-failure";
@@ -607,25 +793,47 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
607
793
 
608
794
  if (reviewResults.every((r) => r.must_fix === 0)) {
609
795
  log("Batch " + batchIndex + " round " + round + ": all agents clean.");
610
- batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [] });
796
+ // rfl clean 轮黑洞修复(tier-1 6.6 v5,T7):all-clean 现状在聚合/reconcile
797
+ // break——末轮 fix 的对账与回归回填永不发生,eval 数据在最 canonical 的成功
798
+ // 路径上失真。break 前用本轮已解析的 reconciliation 数据做确定性回填(不调
799
+ // LLM):fix-attempted 未再现 → fixed + 上轮 fix 的 regression 维度回填。
800
+ if (round > 1) {
801
+ applyCleanRoundBackfill(state, {
802
+ reconSeen, reconEscalate, round, stuckThreshold, batch: batchIndex,
803
+ });
804
+ log("Clean-round backfill applied (reconcile + regression backfill for the previous fix).");
805
+ }
806
+ batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
611
807
  saveState(state);
612
808
  batchClean = true;
613
809
  break;
614
810
  }
615
811
 
616
812
  // ── Aggregate(内置 prompt,不依赖任何 agent.md) ─────────
813
+ // rfl T7/T8:prevFixResult 作打分材料(R2+ 聚合给上轮 fix 打 LLM 三维度分);
814
+ // model 用 AGG_MODEL(可降档参数,缺省 = 主模型,行为与现状一致)。
815
+ const aggT0 = Date.now();
816
+ const aggPrompt = buildAggregatorPrompt({
817
+ header: "Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
818
+ round, max: maxRounds, roundDir,
819
+ reviewResults,
820
+ prevFixResult: round > 1 && state.fixResults && state.fixResults.length
821
+ ? state.fixResults[state.fixResults.length - 1]
822
+ : null,
823
+ });
617
824
  const aggRaw = await agent({
618
- prompt: buildAggregatorPrompt({
619
- header: "Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
620
- round, max: maxRounds, roundDir,
621
- reviewResults,
622
- }),
623
- model: MODEL,
825
+ prompt: aggPrompt,
826
+ model: AGG_MODEL,
624
827
  schema: aggregatorSchema,
625
828
  description: "aggregate",
626
829
  timeoutMs: 3_600_000, // 1h
627
830
  returnMeta: true,
628
831
  });
832
+ phaseTimings.aggregate = [aggT0, Date.now()];
833
+ recordCall(buildCallRecord({
834
+ batch: batchIndex, round, role: "aggregator", name: "aggregate",
835
+ model: AGG_MODEL, prompt: aggPrompt, promptMode: null, meta: aggRaw,
836
+ }));
629
837
 
630
838
  // returnMeta 下 aggRaw = {value, error};失败时 value 为空串、error 在 finalMessage 透出(MF-1)
631
839
  const aggValue = aggRaw?.value ?? aggRaw;
@@ -646,6 +854,10 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
646
854
 
647
855
  if (!agg || typeof agg.must_fix !== "number") {
648
856
  log("Aggregator failed and fallback failed, stopping.");
857
+ // W8:aggregator-failure 轮的当前轮条目落 batchRounds——review/aggregate 相位
858
+ // 时长已实测(fix 相位 null 如实采集);F3:聚合失败,mustFix/suggestion 未知
859
+ // 非 0(null,消费侧 ?? "-" 兜底,不误读为 clean 轮)。
860
+ batchRounds.push({ round, mustFix: null, suggestion: null, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
649
861
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
650
862
  saveState(state);
651
863
  terminated = "aggregator-failure";
@@ -660,12 +872,31 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
660
872
  const suggestion = agg.suggestion ?? 0;
661
873
  log("Aggregated: " + mustFix + " must-fix + " + suggestion + " suggestion(s).");
662
874
 
875
+ // rfl 打分落盘(tier-1 6.6,T7):aggregator 顺手输出的弱信号 scores(reviewer
876
+ // 分每轮 / fix LLM 三维度 R2+)。A6:逐条形状校验(landScores 纯函数)——畸形
877
+ // (targetKind 缺失/round 非数/dimensions 非 object)不再静默落盘,计数进 WARN。
878
+ // 打分失败不影响循环(权威层是状态机客观回填,见 reconcileIssues + backfillFixRegression)。
879
+ {
880
+ const landed0 = landScores(state.scores, Array.isArray(agg.scores) ? agg.scores : [], batchIndex);
881
+ state.scores = landed0.scores;
882
+ if (landed0.landed === 0) {
883
+ log("WARN: aggregator scores unusable this round (landed=0, malformed=" + landed0.malformed + ") — quality scoring degraded");
884
+ } else if (landed0.malformed > 0) {
885
+ log("WARN: aggregator scores partially malformed (landed=" + landed0.landed + ", malformed=" + landed0.malformed + ") — malformed entries dropped");
886
+ }
887
+ }
888
+
663
889
  // 5.1:R1 的 aggregator must_fix_ids 初始化 state.issues(数字 ID 起点)
890
+ // rfl 数据链(tier-1 6.1/6.3):降级条目(adjudication downgraded/unverified)
891
+ // 不建 issue——消费侧过滤(filterActiveIds),修复队列/ES3 校验只含活跃条目;
892
+ // guidance/evidence 随条目落 issues(fixer 免侦查 + 裁决可追踪)。
664
893
  if (round === 1 && agg.must_fix_ids && agg.must_fix_ids.length > 0) {
665
894
  if (!state.issues) state.issues = {};
895
+ const activeIds = new Set(filterActiveIds(agg.must_fix_ids));
666
896
  for (const entry of agg.must_fix_ids) {
667
897
  const id = typeof entry === "string" ? entry : entry && entry.id;
668
898
  if (!id || state.issues[id]) continue;
899
+ if (!activeIds.has(id)) continue;
669
900
  state.issues[id] = {
670
901
  // severity 结构化(5.7):aggregator 标注 critical/major/minor,converged 终止的
671
902
  // 「无 critical」判定依赖它;旧格式(string)默认 major(must-fix 语义)。
@@ -673,8 +904,23 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
673
904
  severity: typeof entry === "string" ? "major" : (entry.severity || "major"),
674
905
  status: "open",
675
906
  history: [{ round: 1, status: "open" }], fixAttempts: 0,
907
+ ...(entry && typeof entry.guidance === "string" && entry.guidance ? { guidance: entry.guidance } : {}),
908
+ ...(entry && typeof entry.evidence === "string" && entry.evidence ? { evidence: entry.evidence } : {}),
676
909
  };
677
910
  }
911
+ // A8:R1 落盘后观测 guidance/evidence 缺失(数据链断点信号)
912
+ warnMissingFields(agg.must_fix_ids);
913
+ }
914
+
915
+ // rfl dormant(tier-1 6.3):adjudication 降级条目落盘(含裁决理由),R2+ prompt
916
+ // 注入复活通道——「降级即消失」的修复。每轮聚合后统一记录(同 id 幂等)。
917
+ if (agg.must_fix_ids && agg.must_fix_ids.length > 0) {
918
+ const before = (state.dormant || []).length;
919
+ state.dormant = recordDormant(state.dormant, agg.must_fix_ids, round,
920
+ new Set(Object.keys(state.issues || {})));
921
+ if (state.dormant.length > before) {
922
+ log("Dormant recorded: " + (state.dormant.length - before) + " adjudication-downgraded issue(s) (total " + state.dormant.length + ")");
923
+ }
678
924
  }
679
925
 
680
926
  // ── Stuck detection ─────────────────────────────────────
@@ -701,11 +947,20 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
701
947
  // 再次被报告同样转 regressed——否则 fixed 停留 + 收敛终止组合会在默认配置下
702
948
  // R3 即以 converged 提前终止而 must-fix 仍活跃。
703
949
  // newFindings 统计与 needs-redesign/fixAttempts 追踪对 R2+ 新发现生效。
950
+ // rfl 数据链(tier-1 6.1/6.3):新发现带 origin(computeOrigin:files 与上轮 fix
951
+ // 触碰文件相交 = regression,否则 new;无 files 不可归因 WARN);重新上报的
952
+ // dormant 条目 → revived=true 回修复队列;本轮降级条目不建 issue(同 R1 过滤)。
704
953
  if (round > 1 && state.issues && agg.must_fix_ids && agg.must_fix_ids.length > 0) {
705
954
  let added = 0;
955
+ const activeIds = new Set(filterActiveIds(agg.must_fix_ids));
706
956
  for (const entry of agg.must_fix_ids) {
707
957
  const id = typeof entry === "string" ? entry : entry && entry.id;
708
958
  if (!id) continue;
959
+ // exec-review 修复(major-2a):本轮降级条目(不在 activeIds)对新建与已追踪
960
+ // 转换都不生效——已追踪条目被聚合降级重报 = 聚合撤回本轮判定,不翻 regressed
961
+ //(对账通道 reconcileIssues 才是 fix-attempted → regressed 的权威转换点),
962
+ // 也不落 dormant(在 issues 活跃追踪,recordDormant 的 excludeIds 排除)。
963
+ if (!activeIds.has(id)) continue;
709
964
  if (state.issues[id]) {
710
965
  if (reconCount === 0 && (state.issues[id].status === "fix-attempted" || state.issues[id].status === "fixed")) {
711
966
  state.issues[id].status = "regressed";
@@ -717,25 +972,65 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
717
972
  }
718
973
  continue;
719
974
  }
975
+ // 复活置位(6.3 delta ③ 的闭环另一半):dormant 条目以活跃身份重新上报 →
976
+ // 回修复队列(下方正常建 issue),后续轮 prompt 不再注入它。
977
+ const dormantHit = (state.dormant || []).find((d) => d.id === id && d.revived !== true);
978
+ if (dormantHit) {
979
+ dormantHit.revived = true;
980
+ log("Dormant revived: " + id + " re-reported in round " + round);
981
+ }
982
+ const origin = computeOrigin(entry, {
983
+ lastModifiedFiles: state.lastModifiedFiles || [],
984
+ fixImpactFiles: state.fixImpactFiles || [],
985
+ });
986
+ if (!origin) log("WARN: issue " + id + " carries no files — origin not attributable");
720
987
  state.issues[id] = {
721
988
  firstSeen: round,
722
989
  severity: typeof entry === "string" ? "major" : (entry.severity || "major"),
723
990
  status: "open", openStreak: 1,
724
991
  history: [{ round, status: "open" }], fixAttempts: 0,
992
+ ...(origin ? { origin } : {}),
993
+ ...(typeof entry === "object" && typeof entry.guidance === "string" && entry.guidance ? { guidance: entry.guidance } : {}),
994
+ ...(typeof entry === "object" && typeof entry.evidence === "string" && entry.evidence ? { evidence: entry.evidence } : {}),
725
995
  };
726
996
  added++;
727
997
  }
998
+ // A8:R2+ merge 落盘后观测 guidance/evidence 缺失(与 R1 init 同一观测口径)
999
+ warnMissingFields(agg.must_fix_ids);
728
1000
  if (added > 0) log("New findings tracked: " + added + " new or re-reported issue(s) in round " + round);
729
1001
  }
730
1002
 
731
1003
  let stuck = { stuck: false };
732
1004
  if (round > 1 && (reconCount > 0 || hasFixAttempted)) {
733
- const rec = reconcileIssues(state.issues || {}, { seenIds: reconSeen, escalateIds: reconEscalate, round, stuckThreshold });
1005
+ // 对账通道的 dormant 分区(exec-review major-1 修复):reviewer dormant id
1006
+ // 声明 not-fixed 不经此通道建 issue(复活唯一入口 = 聚合活跃重报)
1007
+ const reconFiltered = filterDormantFromRecon(reconSeen, reconEscalate, state.dormant);
1008
+ const rec = reconcileIssues(state.issues || {}, { seenIds: reconFiltered.seen, escalateIds: reconFiltered.escalate, round, stuckThreshold });
734
1009
  state.issues = rec.issues;
735
1010
  state.knownRemaining = rec.knownRemaining;
736
1011
  stuck = { stuck: rec.stuck, stuckIds: rec.stuckIds };
737
1012
  log("Reconcile: " + Object.keys(rec.issues).length + " tracked issue(s), known-remaining: " + rec.knownRemaining.length);
1013
+ // rfl regression 维度确定性回填(tier-1 6.6,T7):reconcile 后 regressed 数已
1014
+ // 定,为上轮 fix 的 score entry 填 regression(无 entry 时创建确定性 entry)
1015
+ if (state.fixResults && state.fixResults.length > 0) {
1016
+ state.scores = backfillFixRegression({
1017
+ scores: state.scores, fixResult: state.fixResults[state.fixResults.length - 1],
1018
+ issues: state.issues || {}, round, batch: batchIndex, cleanRound: false,
1019
+ });
1020
+ }
738
1021
  } else {
1022
+ // A9(regression 回填边缘缺口):else 分支 = reconCount===0 且无 fix-attempted
1023
+ //(aggregator numeric-only fallback 等无对账数据场景)——上轮 fix 的 regressed
1024
+ // 数本轮不可判定,此前该场景的 regression entry 永缺。以 unverifiable 模式补
1025
+ // 确定性 entry(regression=null,不诚实造 10 分)。unverifiable 为终态(W3):
1026
+ // 回填只匹配最近一次 fix 的 entry、永不重访旧轮,该轮 regression 维度永久
1027
+ // 缺失(CLI 显示 n/a),同轮后续回填经终态 guard 不覆盖。
1028
+ if (round > 1 && state.fixResults && state.fixResults.length > 0) {
1029
+ state.scores = backfillFixRegression({
1030
+ scores: state.scores, fixResult: state.fixResults[state.fixResults.length - 1],
1031
+ issues: state.issues || {}, round, batch: batchIndex, mode: "unverifiable",
1032
+ });
1033
+ }
739
1034
  const s = updateStuckState(prevMustFix, stuckCount, mustFix, stuckThreshold);
740
1035
  stuckCount = s.stuckCount;
741
1036
  prevMustFix = s.prevMustFix;
@@ -744,7 +1039,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
744
1039
  if (stuck.stuck) {
745
1040
  const stuckIds = (stuck.stuckIds || []).join(", ");
746
1041
  log("Stuck: issue(s) not converging for " + stuckThreshold + " rounds: " + stuckIds + ". Stopping.");
747
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
1042
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
748
1043
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
749
1044
  saveState(state);
750
1045
  terminated = "stuck";
@@ -766,7 +1061,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
766
1061
  return r.issue_id + " [" + (hist || "no history") + "]";
767
1062
  }).join("; ");
768
1063
  log("Needs redesign: " + ids + " not converging after " + maxFixAttempts + " fix attempts. Stopping.");
769
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
1064
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
770
1065
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
771
1066
  saveState(state);
772
1067
  terminated = "needs-redesign";
@@ -802,7 +1097,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
802
1097
  .filter(([, i]) => i.status !== "fixed" && i.status !== "deferred")
803
1098
  .map(([id]) => id);
804
1099
  log("Converged: new findings <= " + convergeNewIssues + " for " + convergeRounds + " rounds. Batch " + batchIndex + " done, proceeding to next batch.");
805
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
1100
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
806
1101
  saveState(state);
807
1102
  terminated = "converged";
808
1103
  finalMessage = "Batch " + batchIndex + " round " + round + ": 新发现率收敛(连续 " + convergeRounds
@@ -814,6 +1109,41 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
814
1109
  }
815
1110
  }
816
1111
 
1112
+ // A4(全降级轮不驱动 fix,设计 §6.3 省轮次的兑现):reviewer 原始计数有 must-fix
1113
+ // 但 aggregator 裁决后全部降级(mustFix===0 且活跃条目为 0)时,all-clean break
1114
+ //(reviewer 原始计数口径,见上方 reviewResults.every 判定)拦不住本路径——不守卫
1115
+ // 会空转派发 fixer(fixCount++ 且无问题可修)。suggestions-only 轮不受影响
1116
+ //(reviewer 全 0 已在 all-clean break 提前终止)。
1117
+ if (mustFix === 0
1118
+ && reviewResults.some((r) => r.must_fix > 0)
1119
+ && (agg.must_fix_ids ? filterActiveIds(agg.must_fix_ids).length : 0) === 0) {
1120
+ log("All reviewer must-fix entries adjudicated down this round — no active fix queue, skipping fix stage.");
1121
+ // F4:此处不需要 backfillFixRegression 调用(原死防御已删)。第 3 轮探针实证
1122
+ // 其恒为逐字节 no-op:能到达 A4 的 R2+ 轮(round>1 且 fixResults 非空),上方
1123
+ // stuck 检测的 if/else 两分支已在相同门控与相同匹配键(round-1/batch)下分别
1124
+ // 回填过——reconcile 路径(reconCount>0 或有 fix-attempted)mode=normal、无对账
1125
+ // 数据路径 mode=unverifiable——regression 键必已存在(number 或 null),W3 终态
1126
+ // guard(键存在即不再处理)使任何后续同键调用直接短路。round=1 时 A4 的调用
1127
+ // 门控本就不满足。保留调用会误导维护者以为此处承担回填职责。
1128
+ // W5:A4 场景 reviewer 原始 must_fix>0 走了 recordAgentDirty,但裁决后全部降级
1129
+ // = 无真实问题(语义等价 clean)——对 active 中本轮 must_fix>0 的 reviewer 补记
1130
+ // recordAgentClean(快照语义对齐常规调用点:lastCleanBatch + 当时 fixCount),
1131
+ // 让「裁决降级=噪声」也推进跨批 skip(否则同 agent 后续批每批全价重扫)。
1132
+ // 本轮无 fix(fixCount 不变),批后快照比较成立。
1133
+ // F2:agg.must_fix_ids 缺失(aggregator JSON 无效走 parseAggregatedMd numeric
1134
+ // fallback 的 agg 无此键)时无条目级裁决证据——「must_fix=0」只是 md 里的一行
1135
+ // 数字,不能证明 reviewer 的 must-fix 是被裁决降级的噪声。此时弱证据不授予
1136
+ // 跨批 clean-skip(否则「跳一轮」被 shouldSkipAgent 放大为「跳到底」且该 agent
1137
+ // 永不再重扫),留待后续批全价复核。
1138
+ for (const a of agentRoundResults) {
1139
+ if (agg.must_fix_ids && a.must_fix > 0) recordAgentClean(state, a.name, batchIndex);
1140
+ }
1141
+ batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
1142
+ saveState(state);
1143
+ batchClean = true;
1144
+ break;
1145
+ }
1146
+
817
1147
  // ── Fix ─────────────────────────────────────────────────
818
1148
  phase("Fix");
819
1149
  let reportContent;
@@ -838,14 +1168,30 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
838
1168
  "- Commit with message: `fix: review batch " + batchIndex + " round " + round + " — " + mustFix + " must-fix`"
839
1169
  : "- Do NOT commit. Leave the fixes in the working tree (autoCommit=false).";
840
1170
 
1171
+ // A3(guidance 链最后一跳):活跃条目中带非空 guidance 的清单——构造确定性通道
1172
+ // 传给 fixer(reportContent 正文之外,per-issue 直达)。W2:先 filterActiveIds 过滤
1173
+ //(与修复队列/ES3 校验同口径)——降级条目(adjudication downgraded/unverified)的
1174
+ // guidance 不再以 "MUST-FIX GUIDANCE" 标题混给 fixer(口径不一致会诱导修复已裁决
1175
+ // 噪声)。normalize 后条目均为对象;string 旧格式(经 fallback 等路径)无 guidance
1176
+ // 自然跳过;must_fix_ids 为 undefined(fallback 路径)时 activeGuidanceIds 为空集
1177
+ // → fixGuidance 仍为空清单。
1178
+ const activeGuidanceIds = new Set(filterActiveIds(agg.must_fix_ids || []));
1179
+ const fixGuidance = (agg.must_fix_ids || [])
1180
+ .filter((e) => e && typeof e === "object" && typeof e.guidance === "string" && e.guidance.trim()
1181
+ && activeGuidanceIds.has(e.id))
1182
+ .map((e) => ({ id: e.id, guidance: e.guidance }));
1183
+
1184
+ const fixT0 = Date.now();
1185
+ const fixPromptBuilt = buildFixPrompt({
1186
+ header: "Fix round " + round + " (batch " + batchIndex + ")",
1187
+ reportContent,
1188
+ fixPrompt,
1189
+ commitInstr,
1190
+ caution: agg.fixes_caution && agg.fixes_caution.length ? agg.fixes_caution : [],
1191
+ guidance: fixGuidance,
1192
+ });
841
1193
  const fxRaw = await agent({
842
- prompt: buildFixPrompt({
843
- header: "Fix round " + round + " (batch " + batchIndex + ")",
844
- reportContent,
845
- fixPrompt,
846
- commitInstr,
847
- caution: agg.fixes_caution && agg.fixes_caution.length ? agg.fixes_caution : [],
848
- }),
1194
+ prompt: fixPromptBuilt,
849
1195
  schema: fixSchema,
850
1196
  // S4:fixAgent = agentRef 路径(主线程按路径加载 + frontmatter model 传播);
851
1197
  // 未传保持现状(通用 subagent + 内联 fixPrompt)。
@@ -856,6 +1202,11 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
856
1202
  returnMeta: true,
857
1203
  ...(FIX_DEF && FIX_DEF.path ? { agent: FIX_DEF.path } : {}),
858
1204
  });
1205
+ phaseTimings.fix = [fixT0, Date.now()];
1206
+ recordCall(buildCallRecord({
1207
+ batch: batchIndex, round, role: "fixer",
1208
+ name: (FIX_DEF && FIX_DEF.name) || "fix", model: MODEL, prompt: fixPromptBuilt, promptMode: null, meta: fxRaw,
1209
+ }));
859
1210
 
860
1211
  // returnMeta 下 fxRaw = {value, error}:先查 error(失败分支可达,MF-1),再对 value 做 parseResult
861
1212
  if (fxRaw && typeof fxRaw === "object" && fxRaw.error) {
@@ -863,7 +1214,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
863
1214
  // 与 review 路径(raw.error → review-failure)对齐:结构化终止而非静默当成功——
864
1215
  // 否则 fixed_count 缺失被 `?? mustFix` 回退,totalFixed 虚增且 must_fix 不降白跑轮次(MF-1)。
865
1216
  log("Fix agent failed, stopping.");
866
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
1217
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
867
1218
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
868
1219
  saveState(state);
869
1220
  terminated = "fix-failure";
@@ -875,7 +1226,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
875
1226
  const fixResult = fx ? normalizeFixResult(fx) : null;
876
1227
  if (!fixResult) {
877
1228
  log("Fix agent failed, stopping.");
878
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
1229
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
879
1230
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
880
1231
  saveState(state);
881
1232
  terminated = "fix-failure";
@@ -889,7 +1240,9 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
889
1240
  // 判 violation)。任一违规 → fix-failure(结构化终止)。trackedIssues 传入
890
1241
  // state.issues——deferred severity 与追踪表交叉核对(MF-4):must-fix 被标 minor
891
1242
  // 塞进 deferred 的逃逸路径在追踪表面前失效(追踪 severity 为准)。
892
- const es3Violations = validateFixResult(fixResult, agg.must_fix_ids, state.issues);
1243
+ // rfl(tier-1 6.3):mustFixIds filterActiveIds 结果——降级条目不占修复队列
1244
+ //(must_fix 计数由 aggregator 按非降级条目报,两侧口径一致)。
1245
+ const es3Violations = validateFixResult(fixResult, filterActiveIds(agg.must_fix_ids), state.issues);
893
1246
  if (es3Violations.length > 0) {
894
1247
  // m7: violation 分两类——deferred 非 minor / must-fix 漏修(must-fix-not-fixed),
895
1248
  // finalMessage 文案区分:统一文案会把漏修误报成 defer 违规,误导修复方向
@@ -899,7 +1252,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
899
1252
  : "deferred 含非 minor 条目(must-fix 不得 defer)— " + v.issue_id + "(" + v.severity + ")"
900
1253
  );
901
1254
  log("ES3 violation: " + JSON.stringify(es3Violations));
902
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [] });
1255
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
903
1256
  state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
904
1257
  saveState(state);
905
1258
  terminated = "fix-failure";
@@ -984,7 +1337,7 @@ for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
984
1337
  modifiedFiles = out ? out.split("\n") : [];
985
1338
  } catch { /* empty */ }
986
1339
  }
987
- batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles });
1340
+ batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles, phaseTimings });
988
1341
  // M4: 批内即时字段——下轮 scoped 分支(recheckAfterFix=true)从
989
1342
  // state.lastModifiedFiles 读本批 fix 的真实改动文件(git 实测兜底)
990
1343
  state.lastModifiedFiles = modifiedFiles;