@zhushanwen/subagent-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/dist/chunk-3VOERJPJ.js +22 -0
- package/dist/chunk-A4IVZWVX.js +31 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
- package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
- package/dist/execution/engine/engines/zcode/constants.js +22 -0
- package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
- package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
- package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
- package/dist/execution/engine/engines/zcode/reader.js +239 -0
- package/dist/execution/engine/paths.cjs +55 -0
- package/dist/execution/engine/paths.d.cts +8 -0
- package/dist/execution/engine/paths.d.ts +8 -0
- package/dist/execution/engine/paths.js +26 -0
- package/dist/execution/relay-env.cjs +62 -0
- package/dist/execution/relay-env.d.cts +33 -0
- package/dist/execution/relay-env.d.ts +33 -0
- package/dist/execution/relay-env.js +20 -0
- package/dist/index.cjs +1852 -0
- package/dist/index.d.cts +1184 -0
- package/dist/index.d.ts +1184 -0
- package/dist/index.js +1810 -0
- package/dist/types-BxyAidGf.d.cts +875 -0
- package/dist/types-BxyAidGf.d.ts +875 -0
- package/package.json +99 -0
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
- package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
- package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
- package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
- package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
- package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
- package/src/__tests__/m2-append-content-probe.test.ts +73 -0
- package/src/__tests__/manifest-store.test.ts +368 -0
- package/src/__tests__/record-store-cache.test.ts +335 -0
- package/src/__tests__/record-store-index.test.ts +560 -0
- package/src/__tests__/record-store-last-line.test.ts +129 -0
- package/src/__tests__/review-fix-loop-script.test.ts +288 -0
- package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
- package/src/__tests__/robustness-low-batch1.test.ts +286 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
- package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
- package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
- package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
- package/src/__tests__/session-runner.test.ts +69 -0
- package/src/__tests__/smoke.test.ts +9 -0
- package/src/__tests__/stream-sink.test.ts +203 -0
- package/src/core/__tests__/host-services.test.ts +144 -0
- package/src/core/__tests__/logger.test.ts +78 -0
- package/src/core/__tests__/notify-ports.test.ts +104 -0
- package/src/core/host-services.ts +97 -0
- package/src/core/logger.ts +47 -0
- package/src/core/notify-ports.ts +126 -0
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
- package/src/execution/__tests__/config.test.ts +302 -0
- package/src/execution/__tests__/delivery-methods.test.ts +424 -0
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
- package/src/execution/__tests__/execute-nesting.test.ts +323 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
- package/src/execution/__tests__/execution-record.test.ts +1394 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/finalize-record.test.ts +367 -0
- package/src/execution/__tests__/finalized-marker.test.ts +104 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
- package/src/execution/__tests__/model-resolver.test.ts +465 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/output-collector.test.ts +358 -0
- package/src/execution/__tests__/path-encoding.test.ts +104 -0
- package/src/execution/__tests__/pi-invocation.test.ts +134 -0
- package/src/execution/__tests__/record-store.test.ts +1057 -0
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
- package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +293 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
- package/src/execution/__tests__/spawn-args.test.ts +445 -0
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/stdin-writer.test.ts +462 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +788 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +615 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +633 -0
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +199 -0
- package/src/execution/agent-registry.ts +199 -0
- package/src/execution/agent-result-mapper.ts +90 -0
- package/src/execution/alive-store.ts +119 -0
- package/src/execution/argv-mirror.ts +113 -0
- package/src/execution/best-effort.ts +38 -0
- package/src/execution/channel-registry-access.ts +144 -0
- package/src/execution/concurrency-pool.ts +116 -0
- package/src/execution/config.ts +165 -0
- package/src/execution/dialog-queue.ts +330 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +66 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +158 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +133 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +309 -0
- package/src/execution/execute-options-mapper.ts +115 -0
- package/src/execution/execution-record.ts +984 -0
- package/src/execution/finalize-record.ts +254 -0
- package/src/execution/finalized-marker.ts +70 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +56 -0
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +513 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +256 -0
- package/src/execution/model-config-service.ts +250 -0
- package/src/execution/model-resolver.ts +248 -0
- package/src/execution/notifier.ts +372 -0
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +228 -0
- package/src/execution/path-encoding.ts +60 -0
- package/src/execution/pi-invocation.ts +120 -0
- package/src/execution/record-entry.ts +132 -0
- package/src/execution/record-store.ts +1265 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +120 -0
- package/src/execution/session-pending.ts +170 -0
- package/src/execution/session-reconstructor.ts +678 -0
- package/src/execution/session-runner.ts +1789 -0
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/spawn-event-adapter.ts +363 -0
- package/src/execution/stdin-writer.ts +198 -0
- package/src/execution/stream-sink.ts +126 -0
- package/src/execution/subagent-service.ts +2268 -0
- package/src/execution/subprocess-agent-runner.ts +317 -0
- package/src/execution/temp-prompt.ts +62 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +102 -0
- package/src/execution/types.ts +1023 -0
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +243 -0
- package/src/execution/ui-request-observability.ts +81 -0
- package/src/execution/ui-request-queue.ts +184 -0
- package/src/execution/worktree-manager.ts +688 -0
- package/src/execution/worktree-registry.ts +279 -0
- package/src/index.ts +87 -0
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +503 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
- package/src/orchestration/__tests__/script-lint.test.ts +831 -0
- package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/agent-opts-resolver.ts +174 -0
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +316 -0
- package/src/orchestration/error-recovery.ts +1018 -0
- package/src/orchestration/execute-agent-call.ts +256 -0
- package/src/orchestration/launcher.ts +455 -0
- package/src/orchestration/lifecycle.ts +399 -0
- package/src/orchestration/models/__tests__/budget.test.ts +307 -0
- package/src/orchestration/models/agent-call.ts +83 -0
- package/src/orchestration/models/budget.ts +118 -0
- package/src/orchestration/models/ports.ts +164 -0
- package/src/orchestration/models/run-runtime.ts +104 -0
- package/src/orchestration/models/run-spec.ts +83 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +183 -0
- package/src/orchestration/models/types.ts +301 -0
- package/src/orchestration/models/workflow-run.ts +254 -0
- package/src/orchestration/models/workflow-script-registry.ts +35 -0
- package/src/orchestration/models/workflow-script.ts +117 -0
- package/src/orchestration/script-lint.ts +766 -0
- package/src/orchestration/skill-discovery.ts +135 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +98 -0
- package/src/orchestration/worker-script-builder.ts +421 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +133 -0
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
- package/src/shared/__tests__/resource-discovery.test.ts +749 -0
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/agent-ref.ts +57 -0
- package/src/shared/meta-parser.ts +261 -0
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/resource-discovery.ts +835 -0
- package/src/shared/resource-meta.ts +65 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +58 -0
- package/src/shared/timer-delay.ts +54 -0
- package/src/shared/xml-injection.ts +35 -0
- package/workflows/README.md +81 -0
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +137 -0
- package/workflows/map-reduce.js +180 -0
- package/workflows/parallel.js +154 -0
- package/workflows/review-fix-loop-utils.cjs +1542 -0
- package/workflows/review-fix-loop.js +1605 -0
- package/workflows/scatter-gather.js +184 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
// review-fix-loop.js — 通用多批审查-修复循环(内置 workflow)
|
|
2
|
+
//
|
|
3
|
+
// 模式:多批(batch)串行,批内循环(round):并行 review → aggregate → fix → 重审。
|
|
4
|
+
// 批次用于表达前置依赖(fallow 静态分析等前置检查必须先完成,后续审查才有意义)。
|
|
5
|
+
// 修复范围 = 全部等级(must-fix + suggestion/minor);批内某 agent 已无任何等级问题
|
|
6
|
+
// (must-fix 与 suggestion 全 0)则后续轮跳过,优化 token 效率。终止/收敛判定仍以
|
|
7
|
+
// must-fix 为主驱动,但任何「成功类」终止(clean/converged/A4 全降级)都要求 suggestion 也为 0。
|
|
8
|
+
// stuck 检测只看 must-fix(suggestion 主观新冒不谈 stuck,由 maxRounds 硬顶兑底)。
|
|
9
|
+
//
|
|
10
|
+
// 台账守门(假 clean 防护):state.issues 是「问题是否全部解决」的唯一权威——四个
|
|
11
|
+
// 成功收工点(全员 clean 早退 / all-clean / converged / A4 全降级)统一前置
|
|
12
|
+
// hasOpenResidue:本轮观测(reviewer 报数 / 聚合活跃条目)为 0 不蕴涵台账已清。
|
|
13
|
+
// 残留时重派追账(台账未结条目注入 R2+ prompt),reviewer 的 reconciliation 申报
|
|
14
|
+
// fixed(带 evidence)→ reconcileIssues 清账;not-fixed → openStreak 增长走 stuck;
|
|
15
|
+
// 无视 → maxRounds 兜底——任何路径不再产出「clean 终态 + 台账 open 残留」的自相矛盾。
|
|
16
|
+
//
|
|
17
|
+
// 身份对齐(编号+标题三级):aggregator 编号跨轮不稳定(紧凑化重排 / 同题换号),
|
|
18
|
+
// 表格号不直接当台账主键——merge 时 resolveIssueIdentity 三级对齐(L1 编号+标题
|
|
19
|
+
// 双命中沿用 / L2 标题归一唯一命中沿用旧键 / L3 避让分配),改键条目记入
|
|
20
|
+
// state.idMap(本轮表格号→台账键),fix 写入与下轮对账经 translateId 翻译
|
|
21
|
+
// (ES3 集合比较双侧保持表格号空间不翻译)。title 随条目落 issues/dormant。
|
|
22
|
+
//
|
|
23
|
+
// 用法:
|
|
24
|
+
// workflow run review-fix-loop --args targetType=git-diff target=main \
|
|
25
|
+
// batch1="/path/reviewer-a.md,/path/reviewer-b.md" autoCommit=true
|
|
26
|
+
// workflow run review-fix-loop --args targetType=file target=/path/to/doc.md \
|
|
27
|
+
// batch1="/path/doc-reviewer.md" autoCommit=false
|
|
28
|
+
//
|
|
29
|
+
// S4 路径统一:batch1..batchN/fixAgent 值 = agentRef(.md 绝对路径,<available_subagents> 的
|
|
30
|
+
// <location>);fallowScan=true 独立参数前置插入静态分析批次(不占 batchN)。
|
|
31
|
+
// ⚠️ 唯一带写操作的内置 workflow:fix 阶段会修改文件(autoCommit=true 时 commit)。
|
|
32
|
+
// ⚠️ lintScript 约束(本脚本已遵守):含 parallel() 入口,禁止 bare IIFE;
|
|
33
|
+
// agent() 调用顺序确定(批次按配置顺序稳定排序,callId 重放安全)。
|
|
34
|
+
//
|
|
35
|
+
// ⚠️ 与 main 的 4.0.0 版分叉(merge 时 add/add 冲突,刻意决策记录):
|
|
36
|
+
// 本版(feat-recursive-optimize)保留——纯函数拆到 review-fix-loop-utils.cjs(vitest 覆盖)、
|
|
37
|
+
// 支持 fallow-scan 前置批次、无默认批次(batch1..batchN/agents 必传)、recheckAfterFix 默认 false。
|
|
38
|
+
// main 4.0.0 版自包含 677 行、缺批次参数时默认单批 ["reviewer"]、recheckAfterFix 默认 false。
|
|
39
|
+
// merge 时保留本版(功能更全),详见 .changeset/tidy-waves-description-phase-lint.md。
|
|
40
|
+
|
|
41
|
+
/* @pi-meta
|
|
42
|
+
name: review-fix-loop
|
|
43
|
+
description: >-
|
|
44
|
+
多批串行审查-修复循环:批内并行 review 聚合全部等级问题(must-fix + suggestion)后迭代修复直到 clean,终止判定以 must-fix 驱动且要求 suggestion 同样归零(唯一带写操作与 commit 副作用的内置 workflow,autoCommit 默认 false)
|
|
45
|
+
when: 用户要 review 并迭代修复至 clean
|
|
46
|
+
notFor: 单纯审查不改代码
|
|
47
|
+
phases: [Review, Fix]
|
|
48
|
+
parameters:
|
|
49
|
+
type: object
|
|
50
|
+
properties:
|
|
51
|
+
targetType: { type: string, enum: [git-diff, file, dir, text] }
|
|
52
|
+
target: { type: string, minLength: 1, pattern: '\S' }
|
|
53
|
+
autoCommit: { type: boolean, default: false }
|
|
54
|
+
maxRounds: { type: integer, default: 10, minimum: 1 }
|
|
55
|
+
stuckThreshold: { type: integer, default: 3, minimum: 1 }
|
|
56
|
+
skipCleanAgents: { type: boolean, default: true }
|
|
57
|
+
recheckAfterFix: { type: boolean, default: false }
|
|
58
|
+
fixAgent: { type: string }
|
|
59
|
+
maxFixAttempts: { type: integer, default: 2, minimum: 1 }
|
|
60
|
+
convergeNewIssues: { type: integer, default: 1, minimum: 1 }
|
|
61
|
+
convergeRounds: { type: integer, default: 2, minimum: 1 }
|
|
62
|
+
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 }
|
|
63
|
+
reviewPrompt: { type: string }
|
|
64
|
+
fixPrompt: { type: string }
|
|
65
|
+
fallowScan: { type: boolean, default: false }
|
|
66
|
+
agents: { type: string }
|
|
67
|
+
batchNames: { type: string }
|
|
68
|
+
patternProperties:
|
|
69
|
+
"^batch\\d+$":
|
|
70
|
+
type: string
|
|
71
|
+
description: 任意 batchN 编号,至少一个;值为 agent .md 绝对路径(逗号分隔多 agent)
|
|
72
|
+
required: [targetType, target]
|
|
73
|
+
usage: |
|
|
74
|
+
## 使用说明
|
|
75
|
+
- batch1..batchN 与 agents 互斥(至少传一个 batchN);值 = agentRef(.md 绝对路径,<available_subagents> 的 <location>)
|
|
76
|
+
- fixAgent:fix 阶段执行者(agentRef),缺省用通用 subagent + 内联 fixPrompt
|
|
77
|
+
- fallowScan=true 仅 targetType=git-diff 合法(前置静态分析批次,不占 batchN)
|
|
78
|
+
- 示例:workflow run review-fix-loop --args targetType=git-diff target=main batch1="/path/fallow-agent.md,/path/reviewer.md" autoCommit=true
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
// ── 参数解析 + 白名单校验(fail-fast) ────────────────────────────
|
|
82
|
+
|
|
83
|
+
function fail(msg) {
|
|
84
|
+
throw new Error("review-fix-loop: " + msg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── 可测纯函数模块 ────────────────────────────────────────────────
|
|
88
|
+
// 参数校验(白名单,类型校验由 m3 args-validator schema 接管)/批次解析/聚合结果解析/审查指令构建
|
|
89
|
+
// 的纯函数在 review-fix-loop-utils.cjs,
|
|
90
|
+
// vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts。
|
|
91
|
+
// worker 运行时经 workerData.scriptPath 定位自身目录——内置 workflow 在 npm 包内,
|
|
92
|
+
// process.cwd() 是用户项目目录,不能作为锚点;scriptPath 注入是 worker 契约的显式前提
|
|
93
|
+
// (D1 加固),缺席即 fail-fast,不回退 cwd——消除从用户目录误加载/被植入同名
|
|
94
|
+
// review-fix-loop-utils.cjs 的代码加载面。
|
|
95
|
+
// 校验先于 $ARGS 白名单执行;fail 为函数声明(提升可用),沿用同一失败输出协议。
|
|
96
|
+
if (typeof workerData === "undefined" || !workerData || typeof workerData.scriptPath !== "string") {
|
|
97
|
+
fail("core_module_load_failed: workerData.scriptPath 缺失,无法定位 review-fix-loop-utils.cjs。" +
|
|
98
|
+
"恢复动作:worker 宿主(WorkerHost)启动 worker 时必须经 workerData 注入 scriptPath" +
|
|
99
|
+
"(指向本脚本真实路径,注入点见 src/orchestration/worker-host.ts 的 workerData 组装处)," +
|
|
100
|
+
"禁止依赖 process.cwd() 巧合。");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const {
|
|
104
|
+
TARGET_TYPES,
|
|
105
|
+
VALID_ARG_KEYS,
|
|
106
|
+
parseBatches,
|
|
107
|
+
resolveBatchNames,
|
|
108
|
+
buildReviewInstruction,
|
|
109
|
+
lockReviewBase,
|
|
110
|
+
buildScopedRecheckPrompt,
|
|
111
|
+
wrapUntrusted,
|
|
112
|
+
buildFixPrompt,
|
|
113
|
+
buildR1ReviewPrompt,
|
|
114
|
+
buildR2ReviewPrompt,
|
|
115
|
+
buildAggregatorPrompt,
|
|
116
|
+
resolveReviewReportPath,
|
|
117
|
+
normalizeFixResult,
|
|
118
|
+
validateFixResult,
|
|
119
|
+
findIssueKey,
|
|
120
|
+
hasOpenResidue,
|
|
121
|
+
resolveIssueIdentity,
|
|
122
|
+
translateId,
|
|
123
|
+
translateReconSets,
|
|
124
|
+
reconcileIssues,
|
|
125
|
+
normalizeReviewResult,
|
|
126
|
+
computeKnownRemaining,
|
|
127
|
+
checkConvergence,
|
|
128
|
+
findNeedsRedesign,
|
|
129
|
+
parseResult,
|
|
130
|
+
normalizeAggregatorResult,
|
|
131
|
+
parseAggregatedMd,
|
|
132
|
+
resolveRunRoot,
|
|
133
|
+
computeOrigin,
|
|
134
|
+
recordDormant,
|
|
135
|
+
filterActiveIds,
|
|
136
|
+
filterDormantFromRecon,
|
|
137
|
+
landScores,
|
|
138
|
+
countMissingFields,
|
|
139
|
+
backfillFixRegression,
|
|
140
|
+
applyCleanRoundBackfill,
|
|
141
|
+
resolveAggregatorModel,
|
|
142
|
+
resolveAgentDefs,
|
|
143
|
+
recordAgentClean,
|
|
144
|
+
recordAgentDirty,
|
|
145
|
+
shouldSkipAgent,
|
|
146
|
+
updateStuckState,
|
|
147
|
+
resolveBatchTerminated,
|
|
148
|
+
} = require(require("path").dirname(workerData.scriptPath) + "/review-fix-loop-utils.cjs");
|
|
149
|
+
|
|
150
|
+
// 白名单校验:未知参数名(防 batchX 拼错如 batchl)→ 报错
|
|
151
|
+
for (const key of Object.keys($ARGS)) {
|
|
152
|
+
if (VALID_ARG_KEYS.has(key)) continue;
|
|
153
|
+
if (/^batch\d+$/.test(key)) continue;
|
|
154
|
+
fail("未知参数: " + key + "(合法参数: targetType/target/batch1..batchN/agents/batchNames/reviewPrompt/fixPrompt/autoCommit/maxRounds/stuckThreshold/skipCleanAgents/recheckAfterFix/fallowScan/fixAgent/maxFixAttempts/convergeNewIssues/convergeRounds/aggregatorModel)");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const targetType = $ARGS.targetType;
|
|
158
|
+
if (!TARGET_TYPES.includes(targetType)) {
|
|
159
|
+
fail("targetType 必填且必须是枚举之一: " + TARGET_TYPES.join("/") + "(实际: " + JSON.stringify(targetType) + ")");
|
|
160
|
+
}
|
|
161
|
+
const target = typeof $ARGS.target === "string" ? $ARGS.target.trim() : "";
|
|
162
|
+
if (!target) fail("target 必填(git-diff 时传 base ref 如 main;file 传路径;dir 传目录;text 传描述)");
|
|
163
|
+
|
|
164
|
+
const reviewPrompt = typeof $ARGS.reviewPrompt === "string" && $ARGS.reviewPrompt.trim()
|
|
165
|
+
? $ARGS.reviewPrompt.trim()
|
|
166
|
+
: "审查变更/目标是否存在:逻辑错误、边界条件、类型不安全、遗漏、回归风险、代码规范问题。发现问题分三级:critical(严重,必须修)/ major(重要,应当修)/ minor(轻微,建议修)。critical+major 计入 must_fix。";
|
|
167
|
+
const fixPrompt = typeof $ARGS.fixPrompt === "string" && $ARGS.fixPrompt.trim()
|
|
168
|
+
? $ARGS.fixPrompt.trim()
|
|
169
|
+
: "修复全部 must-fix 问题(critical/major)。最小正确修复,不做重构、不做风格改动。";
|
|
170
|
+
// 字符串强制转换(m2 exec-review MAJOR-1):LLM 可能以字符串传布尔/整数(旧 normalizeBool
|
|
171
|
+
// 正是为此防御——"false" ?? false 是 truthy 会误触发 commit)。m3 args-validator
|
|
172
|
+
// (chokepoint + coerceTypes)上线后此处为双保险,直接执行路径仍受保护。
|
|
173
|
+
const coerceBool = (v, fallback) =>
|
|
174
|
+
typeof v === "boolean" ? v : v === "true" ? true : v === "false" ? false : fallback;
|
|
175
|
+
const coerceInt = (v, fallback) =>
|
|
176
|
+
typeof v === "number" && Number.isInteger(v)
|
|
177
|
+
? v
|
|
178
|
+
: typeof v === "string" && /^\d+$/.test(v.trim())
|
|
179
|
+
? parseInt(v, 10)
|
|
180
|
+
: fallback;
|
|
181
|
+
|
|
182
|
+
const autoCommit = coerceBool($ARGS.autoCommit, false);
|
|
183
|
+
const maxRounds = coerceInt($ARGS.maxRounds, 10);
|
|
184
|
+
const stuckThreshold = coerceInt($ARGS.stuckThreshold, 3);
|
|
185
|
+
const skipCleanAgents = coerceBool($ARGS.skipCleanAgents, true);
|
|
186
|
+
// 默认 recheckAfterFix=false:clean agent 下轮跳过(与 skipCleanAgents=true 字面语义一致),
|
|
187
|
+
// RC-5(fix 后全批全量重审放大 token)在默认场景消失。传 true 启用可选强回归模式:fix 后重派
|
|
188
|
+
// 全批,clean agent 走限定 prompt(buildScopedRecheckPrompt,只审 modifiedFiles,5.5)。
|
|
189
|
+
const recheckAfterFix = coerceBool($ARGS.recheckAfterFix, false);
|
|
190
|
+
// fixAgent(5.3):值语义同 batchN 的 agent 项(内置名 / agent.md 路径),解析复用
|
|
191
|
+
// resolveAgentDefs 白名单与加载逻辑。传入时 fix 阶段用 agent({agent: ...}) 派发(代码场景
|
|
192
|
+
// 的 verify 命令写在该 agent.md 内);未传保持现状(通用 subagent + 内联 prompt)。
|
|
193
|
+
const FIX_AGENT_RAW = typeof $ARGS.fixAgent === "string" && $ARGS.fixAgent.trim()
|
|
194
|
+
? $ARGS.fixAgent.trim() : undefined;
|
|
195
|
+
const FIX_DEF = FIX_AGENT_RAW ? resolveAgentDefs([FIX_AGENT_RAW])[0] : null;
|
|
196
|
+
// 5.7 收敛终止参数:maxFixAttempts(needs-redesign 阈值,RC-7)/ convergeNewIssues +
|
|
197
|
+
// convergeRounds(新发现率收敛阈值)
|
|
198
|
+
const maxFixAttempts = coerceInt($ARGS.maxFixAttempts, 2);
|
|
199
|
+
const convergeNewIssues = coerceInt($ARGS.convergeNewIssues, 1);
|
|
200
|
+
const convergeRounds = coerceInt($ARGS.convergeRounds, 2);
|
|
201
|
+
const MODEL = $MODEL;
|
|
202
|
+
// rfl aggregator 降档(tier-1 6.4,T8):聚合是机械去重/格式化工作,可降档到便宜
|
|
203
|
+
// 模型。模型路由参考全局/项目 AGENTS.md(当前用户全局有条目:
|
|
204
|
+
// xiaomi-token-plan-cn/mimo-v2.5-pro,thinking 开非 max);无条目请先与主人确认
|
|
205
|
+
// 并写入 AGENTS.md。缺省回退主模型(行为与现状一致)。
|
|
206
|
+
const AGG_MODEL = resolveAggregatorModel($ARGS.aggregatorModel, MODEL);
|
|
207
|
+
|
|
208
|
+
// base 锁定(RC-6,5.6):git-diff 场景 run 启动时锁定 base commit,全程用锁定 hash 构造
|
|
209
|
+
// diff 指令,防止 run 期间 base ref 被更新导致各轮 diff 范围不一致。rev-parse 失败(非 git
|
|
210
|
+
// 目录 / ref 不存在)降级用原 ref 并 warn,锁定结果随 state.meta.baseHash 记录。
|
|
211
|
+
const lockedBase = lockReviewBase(targetType, target);
|
|
212
|
+
if (targetType === "git-diff") {
|
|
213
|
+
if (lockedBase.hash) {
|
|
214
|
+
log("Locked review base: " + target + " -> " + lockedBase.hash);
|
|
215
|
+
} else {
|
|
216
|
+
log("WARN: git rev-parse " + target + " failed, falling back to ref for diff base: " + target);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const reviewInstruction = buildReviewInstruction(targetType, lockedBase.base);
|
|
220
|
+
|
|
221
|
+
// 批次解析:batch1..batchN(缺号报错)/ agents 简写;无默认批次——缺批次参数时
|
|
222
|
+
// parseBatches 直接 fail-fast(与头注释「batch1..batchN/agents 必传」一致)
|
|
223
|
+
const rawBatches = parseBatches($ARGS, fail);
|
|
224
|
+
|
|
225
|
+
// S4:fallowScan 独立参数(不由 batchN 触发——batchN 值域 = agentRef 路径)。
|
|
226
|
+
// fallow 作为内置首批前置插入(fallow-scan 保留字在 resolveAgentDefs 内解析)。
|
|
227
|
+
const fallowScan = coerceBool($ARGS.fallowScan, false);
|
|
228
|
+
if (fallowScan && targetType !== "git-diff") {
|
|
229
|
+
fail("fallowScan 只支持 targetType=git-diff(它审查 git 变更的静态分析),实际 targetType=" + targetType);
|
|
230
|
+
}
|
|
231
|
+
const BATCHES = fallowScan ? [["fallow-scan"], ...rawBatches] : rawBatches;
|
|
232
|
+
|
|
233
|
+
// batchNames(数量校验)
|
|
234
|
+
const rawBatchNames = typeof $ARGS.batchNames === "string" && $ARGS.batchNames.trim()
|
|
235
|
+
? $ARGS.batchNames.split(",").map((s) => s.trim()).filter(Boolean)
|
|
236
|
+
: [];
|
|
237
|
+
const BATCH_NAMES = resolveBatchNames(rawBatchNames, BATCHES, fail);
|
|
238
|
+
|
|
239
|
+
// ── Schemas ─────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const reviewerSchema = {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
report_file: { type: "string", description: "Absolute path to the written review report (.md)" },
|
|
245
|
+
report_content: { type: "string", description: "Full markdown report body (for schema-only agents without write tool; workflow writes it to <roundDir>/<report>.md)" },
|
|
246
|
+
must_fix: { type: "number", description: "Number of must-fix (critical+major) issues found" },
|
|
247
|
+
suggestion: { type: "number", description: "Number of suggestion-level (minor) issues found" },
|
|
248
|
+
reconciliation: {
|
|
249
|
+
type: "array",
|
|
250
|
+
items: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
prev_id: { type: "string", description: "Issue id from the previous round (reconciliation continuation)" },
|
|
254
|
+
status: { type: "string", description: "fixed / not-fixed / regressed / escalate — only fixed counts as resolved; fix result claiming fixed is NOT evidence; escalate = a DEFERRED issue whose context was changed by this round's fix (workflow re-opens it for fixing)" },
|
|
255
|
+
evidence: { type: "string", description: "What was read/confirmed (file + what changed)" },
|
|
256
|
+
},
|
|
257
|
+
required: ["prev_id", "status"],
|
|
258
|
+
},
|
|
259
|
+
description: "Reconciliation table (structured): return [] on R1 (no previous round); on R2+ every previous issue_id must have a status entry",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
// T9 前缀稳定化(tier-1 6.9):required 恒含 reconciliation——schema JSON 逐字嵌入
|
|
263
|
+
// appendSystemPrompt,R1↔R2+ 分叉会造成 system 段字节差异(消息级缓存前缀失效)。
|
|
264
|
+
// R1 的合规输出 = 空数组(prompt 动态段明示)。
|
|
265
|
+
required: ["report_file", "must_fix", "suggestion", "reconciliation"],
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const aggregatorSchema = {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
report_file: { type: "string", description: "Absolute path to aggregated.md" },
|
|
272
|
+
must_fix: { type: "number", description: "Total must-fix after dedup across all dimensions, counting adjudication=evidence entries only" },
|
|
273
|
+
suggestion: { type: "number", description: "Total suggestions after dedup across all dimensions" },
|
|
274
|
+
must_fix_ids: {
|
|
275
|
+
type: "array",
|
|
276
|
+
// M1: 支持 [{id, severity}] 对象(severity: critical/major/minor——converged 终止的
|
|
277
|
+
// 「无 critical」判定数据源)+ 旧格式 string[] 兼容。ajv 权威校验两者皆放行。
|
|
278
|
+
// rfl 数据链(tier-1 §7.2):对象分支增可选 files/evidence/guidance/note/adjudication——
|
|
279
|
+
// origin 归因(files)、修复指引(guidance)、裁决落盘(adjudication+note)的数据源。
|
|
280
|
+
// 降级条目(adjudication ∈ {downgraded, unverified})保留在数组中,由主循环
|
|
281
|
+
// filterActiveIds 过滤出修复队列(must_fix 计数由 aggregator 按非降级条目报)。
|
|
282
|
+
items: {
|
|
283
|
+
oneOf: [
|
|
284
|
+
{ type: "string" },
|
|
285
|
+
{
|
|
286
|
+
type: "object",
|
|
287
|
+
required: ["id"],
|
|
288
|
+
properties: {
|
|
289
|
+
id: { type: "string" },
|
|
290
|
+
// title(跨轮身份锚点):buildAggregatorPrompt 已强制要求,schema 侧
|
|
291
|
+
// 补齐保证生成/校验不丢字段——缺失时 L1/L2 身份匹配静默退化为纯编号
|
|
292
|
+
//(编号撞车防护失效)。normalizeMustFixEntry 透传 title。
|
|
293
|
+
title: { type: "string", description: "One-line issue title (cross-round identity anchor)" },
|
|
294
|
+
// A5: severity 加 enum 约束生成侧(prompt 已列三值);normalize 侧另有
|
|
295
|
+
// 枚举回退兜底(畸形值 → major),双层防御。
|
|
296
|
+
severity: { type: "string", enum: ["critical", "major", "minor"] },
|
|
297
|
+
files: { type: "array", items: { type: "string" }, description: "File paths cited by the issue (regression attribution)" },
|
|
298
|
+
evidence: { type: "string", description: "Cited evidence (files/lines/test results) from the sub-review" },
|
|
299
|
+
guidance: { type: "string", description: "One-line fix direction for the fixer (code wins on conflict)" },
|
|
300
|
+
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" },
|
|
301
|
+
note: { type: "string", description: "Adjudication reason (required when adjudication is unverified/downgraded)" },
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
description: "Issue ids of the deduplicated must-fix list (MF-1..N), matching the first column of the markdown table",
|
|
307
|
+
},
|
|
308
|
+
fixes_caution: {
|
|
309
|
+
type: "array",
|
|
310
|
+
items: { type: "string" },
|
|
311
|
+
description: "Caution entries for weak-evidence or high-risk claims (passed to fix stage)",
|
|
312
|
+
},
|
|
313
|
+
// rfl scores(tier-1 §7.2,M2 打分 rubric 消费):可选——M1 只透传 schema 与
|
|
314
|
+
// normalize,打分 prompt 段属 M2(T7)。提前落 schema 避免二次 schema 破坏性变更。
|
|
315
|
+
scores: {
|
|
316
|
+
type: "array",
|
|
317
|
+
items: {
|
|
318
|
+
type: "object",
|
|
319
|
+
properties: {
|
|
320
|
+
round: { type: "number", description: "Round of the scored target (R2 aggregation scoring R1 fix -> round=1)" },
|
|
321
|
+
targetKind: { type: "string", description: "reviewer | fix" },
|
|
322
|
+
targetName: { type: "string" },
|
|
323
|
+
dimensions: { type: "object", description: "Dimension name -> 0-10 score" },
|
|
324
|
+
total: { type: ["number", "null"], description: "Weighted total; null = not computable (e.g. clean-round LLM dimensions missing)" },
|
|
325
|
+
note: { type: "string" },
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
description: "Optional per-round quality scores (may be omitted until the scoring rubric lands)",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
required: ["report_file", "must_fix", "suggestion"],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// fix schema(5.3):object[](issue_id/description/self_check/affected_files)+ deferred。
|
|
335
|
+
// 兼容性:normalizeFixResult 对旧格式(fixes string[]、无 deferred)兜底解析。
|
|
336
|
+
const fixSchema = {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
fixed_count: { type: "number", description: "Number of issues fixed" },
|
|
340
|
+
fixes: {
|
|
341
|
+
type: "array",
|
|
342
|
+
items: {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
issue_id: { type: "string", description: "Issue identifier from the aggregated report" },
|
|
346
|
+
description: { type: "string", description: "One-line description of the fix" },
|
|
347
|
+
self_check: { type: "string", description: "grep command + hit count + sync action proving the fix is complete" },
|
|
348
|
+
affected_files: { type: "array", items: { type: "string" }, description: "Files touched by this fix + files checked/synced as reference points" },
|
|
349
|
+
},
|
|
350
|
+
// m3: issue_id 是 ES3 交叉校验(must-fix 必须全进 fixes[])的匹配键,必填
|
|
351
|
+
required: ["issue_id"],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
deferred: {
|
|
355
|
+
type: "array",
|
|
356
|
+
items: {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: {
|
|
359
|
+
issue_id: { type: "string" },
|
|
360
|
+
severity: { type: "string", description: "Must be minor — critical/major must not be deferred" },
|
|
361
|
+
reason: { type: "string", description: "Concrete cost description: which files/mechanisms involved, why high cost, suggested follow-up" },
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
required: ["fixed_count"],
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// ── Per-run isolation: runId-scoped directories ─────────────────────
|
|
370
|
+
|
|
371
|
+
const fs = require("fs");
|
|
372
|
+
const path = require("path");
|
|
373
|
+
const os = require("os");
|
|
374
|
+
|
|
375
|
+
const RUN_ID = ($ARGS._runId && typeof $ARGS._runId === "string") ? $ARGS._runId : "run-" + Date.now();
|
|
376
|
+
// rfl 仪表(tier-1 §6.8/§7.5):state 持久化迁出 $TMPDIR(系统清理即丢失)——
|
|
377
|
+
// ~/.review-fix-loop/<repo-slug>/<runId>/;$TMPDIR 仅作 home 不可写时的降级。
|
|
378
|
+
// 旧 $TMPDIR run 不迁移(易失数据不抢救),loadState 只从新位置读。
|
|
379
|
+
const { root: RUN_ROOT, degraded: RUN_ROOT_DEGRADED } = resolveRunRoot({ runId: RUN_ID, cwd: process.cwd() });
|
|
380
|
+
if (RUN_ROOT_DEGRADED) {
|
|
381
|
+
log("WARN: ~/.review-fix-loop not writable, falling back to $TMPDIR: " + RUN_ROOT);
|
|
382
|
+
}
|
|
383
|
+
const STATE_FILE = RUN_ROOT + "/state.json";
|
|
384
|
+
|
|
385
|
+
log("Run directory: " + RUN_ROOT);
|
|
386
|
+
|
|
387
|
+
// ── Startup fail-fast: validate agent ref paths exist (ADR-0003 D6) ─
|
|
388
|
+
// 启动期校验所有 batchN/fixAgent 路径存在,不存在立即报错(带 location 恢复指引),
|
|
389
|
+
// 避免跑到 round 中段 agent-call 时 loadByPath 失败才暴露。FALLOW_DEF(isFallow,
|
|
390
|
+
// 无 path)跳过——它是内置工具标记非文件路径。
|
|
391
|
+
function validateAgentPaths(defs) {
|
|
392
|
+
for (const def of defs) {
|
|
393
|
+
if (def.isFallow || !def.path) continue;
|
|
394
|
+
// MF-2:与 normalizeRef(src/shared/agent-ref.ts)对齐——~/ 前缀展开为 homedir 后再 statSync。
|
|
395
|
+
// resolveAgentDefs 接受 ~/ 前缀、normalizeRef 运行时也展开,此处不展开会「先接受后拒绝」误报 ENOENT。
|
|
396
|
+
const expanded = def.path.startsWith("~/")
|
|
397
|
+
? path.join(os.homedir(), def.path.slice(2))
|
|
398
|
+
: def.path;
|
|
399
|
+
try {
|
|
400
|
+
fs.statSync(expanded);
|
|
401
|
+
} catch {
|
|
402
|
+
fail("Agent file not found: " + def.path + ". Check <available_subagents> <location> for valid agent refs (absolute .md path).");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const startupDefs = [];
|
|
407
|
+
for (const batch of BATCHES) {
|
|
408
|
+
startupDefs.push(...resolveAgentDefs(batch));
|
|
409
|
+
}
|
|
410
|
+
if (FIX_DEF) startupDefs.push(FIX_DEF);
|
|
411
|
+
validateAgentPaths(startupDefs);
|
|
412
|
+
|
|
413
|
+
// ── State management (persistent, atomic writes) ────────────────────
|
|
414
|
+
|
|
415
|
+
// 全新初始 state 模板:catch 分支(无残留)与「读到残留 state」分支共用(A1)。
|
|
416
|
+
// 易变累积字段(calls/fixResults/fixCount/issues/dormant...)全部归零——
|
|
417
|
+
// attempt-2 重放会重新 recordCall/push,复用旧 state 必然双记。
|
|
418
|
+
function freshState() {
|
|
419
|
+
return {
|
|
420
|
+
meta: {
|
|
421
|
+
runId: RUN_ID, workspace: $WORKSPACE || "", model: MODEL || "(default)",
|
|
422
|
+
targetType, target, batches: BATCHES, startedAt: new Date().toISOString(),
|
|
423
|
+
},
|
|
424
|
+
agentStatus: {},
|
|
425
|
+
fixCount: 0,
|
|
426
|
+
batches: [],
|
|
427
|
+
calls: [],
|
|
428
|
+
dormant: [],
|
|
429
|
+
fixResults: [],
|
|
430
|
+
issues: undefined,
|
|
431
|
+
knownRemaining: [],
|
|
432
|
+
convergeStreak: 0,
|
|
433
|
+
lastModifiedFiles: [],
|
|
434
|
+
idMap: null,
|
|
435
|
+
lastAggPath: "",
|
|
436
|
+
lastAggRound: 0,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function loadState() {
|
|
441
|
+
try {
|
|
442
|
+
const parsed = JSON.parse(fs.readFileSync(STATE_FILE, "utf-8"));
|
|
443
|
+
// A1(rebuild 双记修复):读到既有 state = 同 _runId 上一 attempt 的残留——
|
|
444
|
+
// engine 因 script-error rebuild 后同 RUN_ROOT 重跑本脚本,原样返回旧 state 会让
|
|
445
|
+
// attempt-2 重放的 recordCall/fixResults/fixCount/issues 全部双记。state.json 在
|
|
446
|
+
// RUN_ROOT(按 runId 隔离)内,存在即属本 run 前次 attempt,无跨 run 误判。
|
|
447
|
+
// 只保留 previousAttempts 计数(观测 rebuild 次数),其余全部重置。
|
|
448
|
+
const prev = (parsed && parsed.meta && parsed.meta.previousAttempts) || 0;
|
|
449
|
+
log("INFO: found leftover state.json for this run (previous attempt residue after engine rebuild?) — resetting volatile accumulators (previousAttempts=" + (prev + 1) + ")");
|
|
450
|
+
const s = freshState();
|
|
451
|
+
s.meta.previousAttempts = prev + 1;
|
|
452
|
+
return s;
|
|
453
|
+
} catch {
|
|
454
|
+
return freshState();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// rfl 仪表:终止原因快照声明上移(saveState 引用;所有实际调用发生在
|
|
459
|
+
// 主循环初始化后,TDZ 不触发)。CLI list/stats 的终止原因数据源。
|
|
460
|
+
let terminated = "clean";
|
|
461
|
+
|
|
462
|
+
function saveState(state) {
|
|
463
|
+
// rfl 仪表:terminated 随每次落盘快照(结构化终止路径设值后即 saveState)
|
|
464
|
+
if (state.meta) state.meta.terminated = terminated;
|
|
465
|
+
const tmp = STATE_FILE + ".tmp";
|
|
466
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
467
|
+
fs.renameSync(tmp, STATE_FILE);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── rfl 仪表(tier-1 §7.3):calls[] 采集 + phaseTimings ───────────
|
|
471
|
+
// 每次 agent() 调用记录资源与耗时;数据源 = returnMeta 透传的 usage/durationMs/
|
|
472
|
+
// sessionId(引擎 T1 透传;旧引擎缺字段时 usage 键省略(undefined)、durationMs
|
|
473
|
+
// 落 null——phase 级耗时由 phaseTimings 覆盖)。
|
|
474
|
+
function recordCall(entry) {
|
|
475
|
+
if (!Array.isArray(state.calls)) state.calls = [];
|
|
476
|
+
state.calls.push(entry);
|
|
477
|
+
}
|
|
478
|
+
function normUsage(meta) {
|
|
479
|
+
const u = meta && typeof meta === "object" ? meta.usage : undefined;
|
|
480
|
+
if (!u || typeof u !== "object") return undefined;
|
|
481
|
+
return {
|
|
482
|
+
input: u.input ?? 0, output: u.output ?? 0,
|
|
483
|
+
cacheRead: u.cacheRead ?? 0, cacheWrite: u.cacheWrite ?? 0, cost: u.cost ?? 0,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// A10:引擎 T1 透传缺位(returnMeta 在但 usage 缺失 / durationMs 非数)只 WARN 一次
|
|
487
|
+
//(逐调用都打会淹没日志)——提示引擎版本或透传链路问题,calls[] usage 已降级。
|
|
488
|
+
let warnedTelemetryMissing = false;
|
|
489
|
+
function warnTelemetryMissingOnce() {
|
|
490
|
+
if (warnedTelemetryMissing) return;
|
|
491
|
+
warnedTelemetryMissing = true;
|
|
492
|
+
log("WARN: returnMeta usage/durationMs missing — engine telemetry passthrough (T1) not active? calls[] usage degraded");
|
|
493
|
+
}
|
|
494
|
+
// returnMeta 对象 → calls[] 条目(十字段,设计 §7.3)。
|
|
495
|
+
// A11(model 字段语义):model = 请求时参数。agent-ref 调用的实际模型由主线程按
|
|
496
|
+
// .md frontmatter 解析(resolveAgentOpts),此处可能记 "(default)" 而实际跑 frontmatter
|
|
497
|
+
// 模型——引擎侧透传实际 model 是后续工作,消费侧(§6.4 降档归因)需知此局限。
|
|
498
|
+
// A12(promptMode 语义):该字段 = review prompt 模式("full" | "scoped"),对非
|
|
499
|
+
// reviewer 角色(aggregator/fixer)无意义、显式传 null;仅缺省(undefined)回退
|
|
500
|
+
// "full"——空串/null 不再被 || 误转 "full"。
|
|
501
|
+
function buildCallRecord({ batch, round, role, name, model, prompt, promptMode, meta }) {
|
|
502
|
+
const promptStr = typeof prompt === "string" ? prompt : "";
|
|
503
|
+
const metaObj = meta && typeof meta === "object" ? meta : undefined;
|
|
504
|
+
// W1:失败调用(returnMeta = {value, error})天然无 usage/durationMs——排除
|
|
505
|
+
// error 分支,否则 agent 调用失败被误诊为「引擎透传未上线」并烧掉 once 名额。
|
|
506
|
+
// 三个 recordCall 调用点(review/aggregator/fixer)共用本函数,一处修复全覆盖。
|
|
507
|
+
if (metaObj && !metaObj.error && (normUsage(metaObj) === undefined || typeof metaObj.durationMs !== "number")) {
|
|
508
|
+
warnTelemetryMissingOnce();
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
batch, round, role, name: name || role,
|
|
512
|
+
model: model || "(default)",
|
|
513
|
+
durationMs: (metaObj && typeof metaObj.durationMs === "number")
|
|
514
|
+
? metaObj.durationMs
|
|
515
|
+
: null,
|
|
516
|
+
usage: normUsage(metaObj),
|
|
517
|
+
promptMode: promptMode === undefined ? "full" : promptMode,
|
|
518
|
+
promptBytes: Buffer.byteLength(promptStr, "utf8"),
|
|
519
|
+
sessionId: (metaObj && typeof metaObj.sessionId === "string") ? metaObj.sessionId : undefined,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// clean 记录(recordAgentClean/recordAgentDirty 与跨批跳过判定 shouldSkipAgent
|
|
524
|
+
// 在 review-fix-loop-utils.cjs,vitest 单测见 src/__tests__/review-fix-loop-utils.test.ts)
|
|
525
|
+
|
|
526
|
+
// A8(guidance/evidence 缺失观测):活跃条目缺 guidance/evidence 的单行 WARN——
|
|
527
|
+
// 数据链断点(aggregator 未提取 / 归一化丢失)的可观测信号;fixer 免侦查降级提示。
|
|
528
|
+
// 仅 N+M>0 时打,不逐条刷屏。
|
|
529
|
+
function warnMissingFields(mustFixIds) {
|
|
530
|
+
const miss = countMissingFields(mustFixIds);
|
|
531
|
+
if (miss.missingGuidance > 0 || miss.missingEvidence > 0) {
|
|
532
|
+
log("WARN: " + miss.missingGuidance + " active must-fix entries lack guidance (" + miss.missingEvidence + " lack evidence) — fixer self-investigation expected");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ── Agent defs(loadAgentMd/resolveAgentDefs 在 review-fix-loop-utils.cjs) ──
|
|
537
|
+
|
|
538
|
+
// ── Build review calls ──────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
// 上一轮 fix 的 modifiedFiles(git diff 实测,fix 阶段写入 batchRounds)。
|
|
541
|
+
// recheck 限定 prompt(scoped)的 scope 来源(初版 = modifiedFiles)。
|
|
542
|
+
function lastModifiedFiles() {
|
|
543
|
+
// M4: 批内轮次循环中 state.batches 尚未 push(仅在终止/批结束 push)——scoped 分支
|
|
544
|
+
// (recheckAfterFix=true 的下轮 review)执行时读不到本批数据。fix 阶段把
|
|
545
|
+
// modifiedFiles 同步写入 state.lastModifiedFiles(即时字段,批内立即可用),
|
|
546
|
+
// 此处优先读它;fallback 旧路径(跨批场景)。
|
|
547
|
+
if (state.lastModifiedFiles && Array.isArray(state.lastModifiedFiles)) {
|
|
548
|
+
return state.lastModifiedFiles;
|
|
549
|
+
}
|
|
550
|
+
const b = state.batches[state.batches.length - 1];
|
|
551
|
+
const r = b && b.rounds && b.rounds[b.rounds.length - 1];
|
|
552
|
+
return (r && Array.isArray(r.modifiedFiles)) ? r.modifiedFiles : [];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// buildReviewCall 分支构造:fallow 内置工具型(无 .md,静态分析 prompt)。
|
|
556
|
+
function buildFallowReviewCall(base, def, header, roundDir) {
|
|
557
|
+
return {
|
|
558
|
+
...base,
|
|
559
|
+
prompt: [
|
|
560
|
+
header,
|
|
561
|
+
"",
|
|
562
|
+
"Fallow static-analysis pre-scan (tool-based, NOT a git-diff review).",
|
|
563
|
+
"",
|
|
564
|
+
"Steps:",
|
|
565
|
+
"1. Check if fallow is installed: `which fallow`",
|
|
566
|
+
"2. If NOT installed: write the report with a one-line note, must_fix=0, suggestion=0.",
|
|
567
|
+
"3. If installed, run: `fallow audit --base " + lockedBase.base + " --format json --quiet`",
|
|
568
|
+
"4. Extract: complexity hotspots, dead code, unused exports, circular deps",
|
|
569
|
+
"5. Classify findings: critical/major count into must_fix; minor into suggestion.",
|
|
570
|
+
"",
|
|
571
|
+
"output 路径:" + roundDir + "/" + def.report + ".md",
|
|
572
|
+
"Write report to: " + roundDir + "/" + def.report + ".md",
|
|
573
|
+
].join("\n"),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// buildReviewCall 分支构造:R2+ 三段式(5.2)——verify-first 对账 + known-remaining + 收敛 hunt。
|
|
578
|
+
function buildR2ReviewCall(base, def, header, round, max, roundDir, batchIndex) {
|
|
579
|
+
const prevRoundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1);
|
|
580
|
+
// 对账段引用路径:优先最近一次实际产出的聚合报告(state.lastAggPath)——
|
|
581
|
+
// all-clean 轮不聚合(round-1 目录无 aggregated.md),残留 continue 后需回溯到
|
|
582
|
+
// 最近存在的报告,否则 reviewer 收到必败的 read 指令。
|
|
583
|
+
const aggRef = (state.lastAggPath && round > 1) ? state.lastAggPath : prevRoundDir + "/aggregated.md";
|
|
584
|
+
// 台账未结条目(假 clean 防护的信息通路):open/regressed 条目显式注入 R2+ prompt,
|
|
585
|
+
// 不依赖上轮 aggregated.md——漏报条目不在报告里,reviewer 无从对账,查台账守门
|
|
586
|
+
// 就成了零调用的空转。条目带 title(B 链)/severity/evidence。
|
|
587
|
+
const openLedgerIssues = Object.entries(state.issues || {})
|
|
588
|
+
.filter(([, i]) => i.status === "open" || i.status === "regressed")
|
|
589
|
+
.map(([id, i]) => ({ id, severity: i.severity, title: i.title, evidence: i.evidence }));
|
|
590
|
+
return {
|
|
591
|
+
...base,
|
|
592
|
+
// T9:无 per-round spread——reviewerSchema 跨轮统一(缓存前缀稳定前提)
|
|
593
|
+
prompt: buildR2ReviewPrompt({
|
|
594
|
+
header, round, max, roundDir,
|
|
595
|
+
reportFile: def.report,
|
|
596
|
+
aggPath: aggRef,
|
|
597
|
+
aggRound: aggRef === state.lastAggPath && state.lastAggRound ? state.lastAggRound : round - 1,
|
|
598
|
+
fixResult: state.fixResults && state.fixResults.length
|
|
599
|
+
? state.fixResults[state.fixResults.length - 1]
|
|
600
|
+
: null,
|
|
601
|
+
fixRound: state.fixResults && state.fixResults.length
|
|
602
|
+
? state.fixResults[state.fixResults.length - 1].round
|
|
603
|
+
: undefined,
|
|
604
|
+
knownRemaining: (state.knownRemaining && Array.isArray(state.knownRemaining)) ? state.knownRemaining : [],
|
|
605
|
+
// rfl dormant 复活通道(tier-1 6.3 delta ③):降级条目注入 R2+ prompt(动态段内容)
|
|
606
|
+
dormant: (state.dormant && Array.isArray(state.dormant)) ? state.dormant : [],
|
|
607
|
+
openIssues: openLedgerIssues,
|
|
608
|
+
reviewPrompt, reviewInstruction,
|
|
609
|
+
}),
|
|
610
|
+
agent: def.path,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// buildReviewCall 分支构造:recheck 限定(5.5)——clean agent 重派时只审 fix 改动
|
|
615
|
+
// 文件 + 自检关联点(scope = modifiedFiles ∪ affected_files,后者来自
|
|
616
|
+
// state.fixImpactFiles),不诱导全量重扫。
|
|
617
|
+
function buildScopedReviewCall(base, def, header, round, max, roundDir, batchIndex) {
|
|
618
|
+
return {
|
|
619
|
+
...base,
|
|
620
|
+
prompt: buildScopedRecheckPrompt({
|
|
621
|
+
header, round, max, roundDir,
|
|
622
|
+
reportFile: def.report,
|
|
623
|
+
modifiedFiles: lastModifiedFiles(),
|
|
624
|
+
affectedFiles: (state.fixImpactFiles && Array.isArray(state.fixImpactFiles)) ? state.fixImpactFiles : [],
|
|
625
|
+
aggPath: RUN_ROOT + "/batch-" + batchIndex + "/round-" + (round - 1) + "/aggregated.md",
|
|
626
|
+
aggRound: round - 1,
|
|
627
|
+
fixResult: state.fixResults && state.fixResults.length
|
|
628
|
+
? state.fixResults[state.fixResults.length - 1]
|
|
629
|
+
: null,
|
|
630
|
+
fixRound: state.fixResults && state.fixResults.length
|
|
631
|
+
? state.fixResults[state.fixResults.length - 1].round
|
|
632
|
+
: undefined,
|
|
633
|
+
reviewPrompt, reviewInstruction,
|
|
634
|
+
}),
|
|
635
|
+
agent: def.path,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function buildReviewCall(def, round, max, batchIndex, roundDir, scoped) {
|
|
640
|
+
const header = "Batch " + batchIndex + " Round " + round + "/" + max + " — " + BATCH_NAMES[batchIndex - 1];
|
|
641
|
+
const prevBatchesHint = batchIndex > 1
|
|
642
|
+
? "\nPrior batch reports (optional context): " + RUN_ROOT + "/batch-*/ (use read)"
|
|
643
|
+
: "";
|
|
644
|
+
const base = {
|
|
645
|
+
model: MODEL,
|
|
646
|
+
schema: reviewerSchema,
|
|
647
|
+
description: def.name,
|
|
648
|
+
timeoutMs: 3_600_000, // 1h(只读审查 + retry 退避余量)
|
|
649
|
+
// returnMeta: true — 失败时 resolve
|
|
650
|
+
// {value, error},raw.error 可检测(结构化终止可达);成功时
|
|
651
|
+
// value = parsedOutput ?? content,parseResult 作用于 raw.value(MF-1)。
|
|
652
|
+
returnMeta: true,
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// S4:agentRef = 路径(非 fallow 的 def 必有 path)——systemPrompt/model 由主线程
|
|
656
|
+
// resolveAgentOpts 按 path 加载注入,脚本不再拼 md 内容。
|
|
657
|
+
if (def.isFallow) {
|
|
658
|
+
return buildFallowReviewCall(base, def, header, roundDir);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// R2+ 三段式分支(5.2):round>1 且非 scoped → verify-first 对账 + known-remaining + 收敛 hunt。
|
|
662
|
+
// scoped 分支(recheck 限定)在下方单独处理(含对账段)。
|
|
663
|
+
if (round > 1 && !scoped) {
|
|
664
|
+
return buildR2ReviewCall(base, def, header, round, max, roundDir, batchIndex);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// recheck 限定分支(5.5)
|
|
668
|
+
if (scoped) {
|
|
669
|
+
return buildScopedReviewCall(base, def, header, round, max, roundDir, batchIndex);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// T9:R1 函数化(三模板共享静态段,动态后置)
|
|
673
|
+
return {
|
|
674
|
+
...base,
|
|
675
|
+
prompt: buildR1ReviewPrompt({
|
|
676
|
+
header, roundDir, reportFile: def.report, prevBatchesHint,
|
|
677
|
+
reviewPrompt, reviewInstruction,
|
|
678
|
+
}),
|
|
679
|
+
agent: def.path,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// S4:agentRef = 路径,主线程按路径加载(systemPrompt 注入);无名字查找,无需前缀兜底
|
|
684
|
+
async function runReviewAgent(call) {
|
|
685
|
+
return agent(call);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Main loop: batches (serial) × rounds (per-batch) ────────────────
|
|
689
|
+
|
|
690
|
+
const state = loadState();
|
|
691
|
+
// 5.6 锁定结果落 state.meta.baseHash(commit 1 声称与实现一致化,run 后可从 state.json 追溯审查基线)
|
|
692
|
+
state.meta = state.meta || {};
|
|
693
|
+
state.meta.baseHash = lockedBase.hash || "";
|
|
694
|
+
let totalFixed = 0;
|
|
695
|
+
let finalMessage = "";
|
|
696
|
+
|
|
697
|
+
for (let batchIndex = 1; batchIndex <= BATCHES.length; batchIndex++) {
|
|
698
|
+
const defs = resolveAgentDefs(BATCHES[batchIndex - 1]);
|
|
699
|
+
const cleanNames = new Set();
|
|
700
|
+
let round = 0;
|
|
701
|
+
let prevMustFix = -1;
|
|
702
|
+
let stuckCount = 0;
|
|
703
|
+
let batchClean = false;
|
|
704
|
+
let roundHasFix = false; // recheckAfterFix 用:上轮是否有 fix
|
|
705
|
+
const batchRounds = [];
|
|
706
|
+
|
|
707
|
+
// MF-1 批级 review 状态隔离:每批开始重置 issue 追踪 / 收敛计数 / known-remaining,
|
|
708
|
+
// 防止跨批 newFindings 语义污染(firstSeen 用批内 round 号写入、convergeStreak 跨批
|
|
709
|
+
// 累加会让前批收敛状态泄漏到后批,复合导致 converged 错误跳过后续批次)。
|
|
710
|
+
state.issues = undefined;
|
|
711
|
+
state.convergeStreak = 0;
|
|
712
|
+
state.knownRemaining = [];
|
|
713
|
+
// A2(dormant 跨批污染修复):aggregator id 空间每批从 MF-1 重新编号,批 1 降级的
|
|
714
|
+
// dormant MF-3 会与批 2 活跃 MF-3 冲突——filterDormantFromRecon 把它从 reconSeen
|
|
715
|
+
// 剥离后,批 2 的 fix-attempted 被误反转 fixed;且批 1 dormant 持续注入批 2+ prompt。
|
|
716
|
+
// dormant 必须与 issues 同点做批作用域重置。
|
|
717
|
+
state.dormant = [];
|
|
718
|
+
// 身份翻译层(编号+标题三级对齐):idMap = 最近一次聚合的 {表格号→台账键},
|
|
719
|
+
// lastAggPath = 最近一次聚合报告路径(all-clean 轮 continue 后 round-1 无聚合,
|
|
720
|
+
// 对账段引用需回溯到最近存在的报告)。批作用域重置防跨批残留。
|
|
721
|
+
state.idMap = null;
|
|
722
|
+
state.lastAggPath = "";
|
|
723
|
+
state.lastAggRound = 0;
|
|
724
|
+
|
|
725
|
+
// 跨批跳过:agent 在更早批 clean 且此后无 fix → 本批不派发
|
|
726
|
+
if (batchIndex > 1) {
|
|
727
|
+
for (const def of defs) {
|
|
728
|
+
const s = state.agentStatus[def.name];
|
|
729
|
+
if (shouldSkipAgent(s, state.fixCount, batchIndex)) {
|
|
730
|
+
cleanNames.add(def.name);
|
|
731
|
+
log("Cross-batch skip: " + def.name + " (clean in batch " + s.lastCleanBatch + ", no fix since)");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
while (round < maxRounds) {
|
|
737
|
+
round++;
|
|
738
|
+
log("--- Batch " + batchIndex + "/" + BATCHES.length + " (" + BATCH_NAMES[batchIndex - 1] + ") Round " + round + "/" + maxRounds + " ---");
|
|
739
|
+
|
|
740
|
+
phase("Review");
|
|
741
|
+
const roundDir = RUN_ROOT + "/batch-" + batchIndex + "/round-" + round;
|
|
742
|
+
fs.mkdirSync(roundDir, { recursive: true });
|
|
743
|
+
|
|
744
|
+
let active = defs.filter((def) => !(skipCleanAgents && cleanNames.has(def.name)));
|
|
745
|
+
let scopedClean = new Set(); // recheckAfterFix 重派时:上一轮 clean 的 agent 本轮走限定 prompt
|
|
746
|
+
if (recheckAfterFix && round > 1 && roundHasFix) {
|
|
747
|
+
scopedClean = new Set(cleanNames); // 重派前快照上一轮 clean 集合
|
|
748
|
+
active = defs; // fix 后重派全批(强回归模式)
|
|
749
|
+
cleanNames.clear();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (active.length === 0) {
|
|
753
|
+
// 台账守门(第四收工点):全员 clean/skip 但台账有 open/regressed 残留时不得
|
|
754
|
+
// 收工——全员已进 cleanNames、无人可派,注入的台账清单会没有消费方。清空批内
|
|
755
|
+
// clean 名单强制全体重派追账(跨批 skip 状态 state.agentStatus 不动,重派后再
|
|
756
|
+
// clean 会幂等刷新)。reviewer 对注入条目申报 fixed → 清账正常收工;申报
|
|
757
|
+
// not-fixed → openStreak 增长走 stuck 诚实终止;无视 → maxRounds 兜底。
|
|
758
|
+
if (hasOpenResidue(state.issues)) {
|
|
759
|
+
log("All agents clean/skipped but ledger has unresolved issue(s) — re-dispatching all agents to reconcile.");
|
|
760
|
+
cleanNames.clear();
|
|
761
|
+
scopedClean.clear();
|
|
762
|
+
active = defs;
|
|
763
|
+
} else {
|
|
764
|
+
log("All agents clean/skipped — batch " + batchIndex + " done.");
|
|
765
|
+
batchClean = true;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
log("Review: " + active.map((d) => d.name).join(", ") + " (" + active.length + " agent(s) in parallel)...");
|
|
771
|
+
const calls = active.map((def) => buildReviewCall(def, round, maxRounds, batchIndex, roundDir, scopedClean.has(def.name)));
|
|
772
|
+
const phaseTimings = { review: null, aggregate: null, fix: null };
|
|
773
|
+
const reviewT0 = Date.now();
|
|
774
|
+
const allRaw = await parallel(calls.map(runReviewAgent));
|
|
775
|
+
phaseTimings.review = [reviewT0, Date.now()];
|
|
776
|
+
// rfl 仪表:本轮全部 reviewer 调用落 calls[](allRaw 与 calls 一一对应,parallel 语义)
|
|
777
|
+
for (let i = 0; i < allRaw.length; i++) {
|
|
778
|
+
recordCall(buildCallRecord({
|
|
779
|
+
batch: batchIndex, round, role: "reviewer",
|
|
780
|
+
name: active[i].name, model: calls[i].model,
|
|
781
|
+
prompt: calls[i].prompt,
|
|
782
|
+
promptMode: scopedClean.has(active[i].name) ? "scoped" : "full",
|
|
783
|
+
meta: allRaw[i],
|
|
784
|
+
}));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// per-agent 结果区分:parallel 结果与 calls 一一对应
|
|
788
|
+
const reviewResults = [];
|
|
789
|
+
const agentRoundResults = [];
|
|
790
|
+
const reconSeen = new Set(); // R2+ reconciliation 声明的上轮 ID(status !== fixed)——stuck ID 驱动数据源(5.1)
|
|
791
|
+
const reconEscalate = new Set(); // 5.1-5 escalate 声明:deferred 条目上下文改变 → 重新 open
|
|
792
|
+
const reconFixed = new Set(); // reconciliation 声明 fixed 且 evidence 非空(verify-first 申报)——open/regressed 条目的清账通道(不再丢弃)
|
|
793
|
+
const reconAll = new Set(); // M2: 所有 status 条目(含 fixed)的 prev_id 去重——reconcile 门控数据源
|
|
794
|
+
for (let i = 0; i < allRaw.length; i++) {
|
|
795
|
+
const raw = allRaw[i];
|
|
796
|
+
if (raw && typeof raw === "object" && raw.error) {
|
|
797
|
+
// 审查 agent 调用失败(含 AgentRegistry not found / 超时)。不裸 throw——与
|
|
798
|
+
// aggregator-failure/stuck/fix-failure 路径一致:saveState + terminated 结构化终止,
|
|
799
|
+
// 保证 state.json 有记录、调用方拿到结构化结果而非裸异常(MF-3)。
|
|
800
|
+
// W8:失败轮的当前轮条目也落 batchRounds(该轮 phase 时长不因结构化终止
|
|
801
|
+
// 从时间线消失);F3:聚合未发生/失败,mustFix/suggestion 是未知而非 0——
|
|
802
|
+
// 落 0 会被时间线误读为 clean 轮(rfl.mjs 消费侧 `mustFix ?? "-"` 对 null
|
|
803
|
+
// 显示 "-",suggestion 无消费点,null 安全);agents 为已收集的部分结果,
|
|
804
|
+
// aggregate/fix 相位 null 是如实采集(未到达)。
|
|
805
|
+
batchRounds.push({ round, mustFix: null, suggestion: null, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
806
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
807
|
+
saveState(state);
|
|
808
|
+
terminated = "review-failure";
|
|
809
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 审查 agent 调用失败 " + active[i].name + " — " + raw.error;
|
|
810
|
+
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
const parsed = normalizeReviewResult(raw.value);
|
|
814
|
+
if (parsed) {
|
|
815
|
+
// 5.8 通用落盘:schema-only agent(report_content 无 report_file,如 doc-reviewer)→
|
|
816
|
+
// workflow 写盘到 <roundDir>/<def.report>.md 并填入 report_file(aggregator 读取路径不变)。
|
|
817
|
+
const reportPath = resolveReviewReportPath(parsed, roundDir, active[i].report);
|
|
818
|
+
if (reportPath && !(parsed.report_file && parsed.report_file.trim())) {
|
|
819
|
+
try {
|
|
820
|
+
fs.writeFileSync(reportPath, parsed.report_content || "", "utf-8");
|
|
821
|
+
parsed.report_file = reportPath;
|
|
822
|
+
} catch (e) {
|
|
823
|
+
log("WARN: failed to write report_content to " + reportPath + " — " + e.message);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// W6:report_content 已落盘(上方 resolveReviewReportPath 写盘 + report_file
|
|
827
|
+
// 回填),aggregator prompt 指示其 read report_file——整份正文再随 reviewResults
|
|
828
|
+
// JSON 内嵌即同一内容双份付费(实测 ~48% 重复)。push 前剥离;下游
|
|
829
|
+
// (buildAggregatorPrompt 路径清单 / must_fix 计数 / all-clean 判定)只消费
|
|
830
|
+
// report_file 与计数字段,无 report_content 消费。
|
|
831
|
+
const parsedWithoutContent = { ...parsed };
|
|
832
|
+
delete parsedWithoutContent.report_content;
|
|
833
|
+
reviewResults.push(parsedWithoutContent);
|
|
834
|
+
for (const r of parsed.reconciliation) {
|
|
835
|
+
if (r.status === "escalate") reconEscalate.add(r.prev_id);
|
|
836
|
+
else if (r.status !== "fixed") reconSeen.add(r.prev_id);
|
|
837
|
+
else if (typeof r.evidence === "string" && r.evidence.trim()) reconFixed.add(r.prev_id); // 空 evidence 的口头断言不采信(reviewer 吹牛防护,与 fix-attempted 的 verify 语义对齐)
|
|
838
|
+
else log("reconciliation fixed-claim without evidence ignored: " + r.prev_id); // 保守丢弃可见化(S-1)
|
|
839
|
+
reconAll.add(r.prev_id); // M2: 含 fixed——全 fixed 时 reconSeen 空但 reconcile 仍需执行
|
|
840
|
+
}
|
|
841
|
+
const def = active[i];
|
|
842
|
+
// 修复范围全等级后,agent clean = must-fix 与 suggestion 全 0(skipCleanAgents 授予变严:
|
|
843
|
+
// 只剩 suggestion 的 agent 继续参与轮次直到修完,避免「must-fix 清零即跳」漏修 suggestion)
|
|
844
|
+
const agentAllClean = parsed.must_fix === 0 && (parsed.suggestion ?? 0) === 0;
|
|
845
|
+
if (agentAllClean) {
|
|
846
|
+
recordAgentClean(state, def.name, batchIndex);
|
|
847
|
+
cleanNames.add(def.name);
|
|
848
|
+
} else {
|
|
849
|
+
recordAgentDirty(state, def.name, parsed.must_fix, batchIndex);
|
|
850
|
+
}
|
|
851
|
+
agentRoundResults.push({ name: def.name, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0, clean: agentAllClean });
|
|
852
|
+
} else {
|
|
853
|
+
// tools 受限的 agent(如 tools: read)会过滤掉 structured-output → schema 失效,
|
|
854
|
+
// 结果缺 must_fix。结构化终止(MF-3),raw 完整 dump 便于定位。
|
|
855
|
+
// W8:同 review-failure——当前轮条目落 batchRounds(phase 时长保留在时间线);
|
|
856
|
+
// F3:聚合未发生,mustFix/suggestion 未知非 0(null,消费侧 ?? "-" 兜底)。
|
|
857
|
+
batchRounds.push({ round, mustFix: null, suggestion: null, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
858
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
859
|
+
saveState(state);
|
|
860
|
+
terminated = "review-failure";
|
|
861
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 审查 agent 结果无效(缺 must_fix) " + active[i].name + " raw=" + JSON.stringify(raw.value).slice(0, 400);
|
|
862
|
+
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (terminated === "review-failure") break; // 已结构化终止,退出 round 循环(MF-3)
|
|
868
|
+
|
|
869
|
+
// 全等级终止:任何等级(含 suggestion)未清零都不算 clean——否则建议级问题会在
|
|
870
|
+
// must-fix 清零的出口被静默漏修(与「must-fix 只是终止条件、不是修复范围」的语义对齐)
|
|
871
|
+
if (reviewResults.every((r) => r.must_fix === 0 && (r.suggestion ?? 0) === 0)) {
|
|
872
|
+
log("Batch " + batchIndex + " round " + round + ": all agents clean.");
|
|
873
|
+
// rfl clean 轮黑洞修复(tier-1 6.6 v5,T7):all-clean 现状在聚合/reconcile 前
|
|
874
|
+
// break——末轮 fix 的对账与回归回填永不发生,eval 数据在最 canonical 的成功
|
|
875
|
+
// 路径上失真。break 前用本轮已解析的 reconciliation 数据做确定性回填(不调
|
|
876
|
+
// LLM):fix-attempted 未再现 → fixed + open/regressed 申报 fixed(带 evidence)
|
|
877
|
+
// → fixed + 上轮 fix 的 regression 维度回填。stuck 一并消费(不再丢弃)。
|
|
878
|
+
let cleanStuck = { stuck: false, stuckIds: [] };
|
|
879
|
+
if (round > 1) {
|
|
880
|
+
const backfill = applyCleanRoundBackfill(state, {
|
|
881
|
+
reconSeen, reconEscalate, reconFixed, round, stuckThreshold, batch: batchIndex,
|
|
882
|
+
});
|
|
883
|
+
cleanStuck = { stuck: backfill.stuck, stuckIds: backfill.stuckIds };
|
|
884
|
+
log("Clean-round backfill applied (reconcile + regression backfill for the previous fix).");
|
|
885
|
+
}
|
|
886
|
+
if (cleanStuck.stuck) {
|
|
887
|
+
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
888
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
889
|
+
saveState(state);
|
|
890
|
+
terminated = "stuck";
|
|
891
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 全员 clean 但台账问题 " + cleanStuck.stuckIds.join(", ")
|
|
892
|
+
+ " 连续 " + stuckThreshold + " 轮未收敛(clean 轮对账判定)。残留: "
|
|
893
|
+
+ (state.knownRemaining && state.knownRemaining.length ? state.knownRemaining.join("; ") : "无 deferred");
|
|
894
|
+
batchIndex = BATCHES.length + 1;
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
// 台账守门(backfill 清账后再查——先消费本轮 fixed 申报,避免已清零的残留
|
|
898
|
+
// 触发空转一轮):仍有 open/regressed 残留时不收工——本轮全员报 0 只是观测,
|
|
899
|
+
// 不蕴涵台账已清。清空批内 clean 名单重派追账(注入段给 reviewer 台账清单),
|
|
900
|
+
// 下轮申报 fixed → 清账收工 / not-fixed → openStreak 增长 / 无视 → maxRounds 兜底。
|
|
901
|
+
if (hasOpenResidue(state.issues)) {
|
|
902
|
+
const residueIds = Object.entries(state.issues)
|
|
903
|
+
.filter(([, i]) => i.status === "open" || i.status === "regressed")
|
|
904
|
+
.map(([id]) => id).join(", ");
|
|
905
|
+
log("All agents reported clean but ledger has unresolved issue(s): " + residueIds + " — continuing to reconcile.");
|
|
906
|
+
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
907
|
+
saveState(state);
|
|
908
|
+
cleanNames.clear();
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
912
|
+
saveState(state);
|
|
913
|
+
batchClean = true;
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── Aggregate(内置 prompt,不依赖任何 agent.md) ─────────
|
|
918
|
+
// rfl T7/T8:prevFixResult 作打分材料(R2+ 聚合给上轮 fix 打 LLM 三维度分);
|
|
919
|
+
// model 用 AGG_MODEL(可降档参数,缺省 = 主模型,行为与现状一致)。
|
|
920
|
+
const aggT0 = Date.now();
|
|
921
|
+
const aggPrompt = buildAggregatorPrompt({
|
|
922
|
+
header: "Batch " + batchIndex + "/" + BATCHES.length + " Round " + round + "/" + maxRounds + " — AGGREGATE REVIEWS",
|
|
923
|
+
round, max: maxRounds, roundDir,
|
|
924
|
+
reviewResults,
|
|
925
|
+
prevFixResult: round > 1 && state.fixResults && state.fixResults.length
|
|
926
|
+
? state.fixResults[state.fixResults.length - 1]
|
|
927
|
+
: null,
|
|
928
|
+
// S-5:上一轮标题注入——「keep the title close to the previous wording」此前无
|
|
929
|
+
// 材料可依。取台账(issues + dormant)条目的 id: title 清单。
|
|
930
|
+
// R3 S-1:清单截尾(最近 50 条)——长循环台账单调膨胀只造成 prompt token 膨胀。
|
|
931
|
+
prevTitles: round > 1
|
|
932
|
+
? [
|
|
933
|
+
...Object.entries(state.issues || {}).map(([id, i]) => (i && typeof i.title === "string" && i.title ? id + ": " + i.title : null)),
|
|
934
|
+
...((state.dormant || []) || []).map((d) => (d && typeof d.title === "string" && d.title ? d.id + ": " + d.title : null)),
|
|
935
|
+
].filter(Boolean).slice(-50)
|
|
936
|
+
: [],
|
|
937
|
+
});
|
|
938
|
+
let aggRaw = await agent({
|
|
939
|
+
prompt: aggPrompt,
|
|
940
|
+
model: AGG_MODEL,
|
|
941
|
+
schema: aggregatorSchema,
|
|
942
|
+
description: "aggregate",
|
|
943
|
+
timeoutMs: 3_600_000, // 1h
|
|
944
|
+
returnMeta: true,
|
|
945
|
+
});
|
|
946
|
+
phaseTimings.aggregate = [aggT0, Date.now()];
|
|
947
|
+
recordCall(buildCallRecord({
|
|
948
|
+
batch: batchIndex, round, role: "aggregator", name: "aggregate",
|
|
949
|
+
model: AGG_MODEL, prompt: aggPrompt, promptMode: null, meta: aggRaw,
|
|
950
|
+
}));
|
|
951
|
+
|
|
952
|
+
// returnMeta 下 aggRaw = {value, error};失败时 value 为空串、error 在 finalMessage 透出(MF-1)
|
|
953
|
+
let aggValue = aggRaw?.value ?? aggRaw;
|
|
954
|
+
let agg = normalizeAggregatorResult(aggValue);
|
|
955
|
+
|
|
956
|
+
if (!agg || typeof agg.must_fix !== "number") {
|
|
957
|
+
// 重试先于 md 兜底(C 修复):聚合失败最常见形态 = md 报告已写好 + JSON 尾部
|
|
958
|
+
// 格式漂移——此时 md 兜底「成功」但降级为 numeric-only(丢 must_fix_ids 整条
|
|
959
|
+
// 数据链:merge 跳过 / fixed 重报转 regressed 链断 / dormant 不落 / W5 门控
|
|
960
|
+
// 退化),代价高于一次重试调用。先原 prompt 重试争取完整恢复。
|
|
961
|
+
log("Aggregator JSON invalid (len=" + (typeof aggValue === "string" ? aggValue.length : 0) + ") — retrying once with the same prompt.");
|
|
962
|
+
aggRaw = await agent({
|
|
963
|
+
prompt: aggPrompt,
|
|
964
|
+
model: AGG_MODEL,
|
|
965
|
+
schema: aggregatorSchema,
|
|
966
|
+
description: "aggregate",
|
|
967
|
+
timeoutMs: 1_200_000, // 20min:确定性失败(context overflow 等)不该在 md 兜底前再烧 1h(S-4)
|
|
968
|
+
returnMeta: true,
|
|
969
|
+
});
|
|
970
|
+
recordCall(buildCallRecord({
|
|
971
|
+
batch: batchIndex, round, role: "aggregator", name: "aggregate",
|
|
972
|
+
model: AGG_MODEL, prompt: aggPrompt, promptMode: null, meta: aggRaw,
|
|
973
|
+
}));
|
|
974
|
+
aggValue = aggRaw?.value ?? aggRaw;
|
|
975
|
+
agg = normalizeAggregatorResult(aggValue);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (!agg || typeof agg.must_fix !== "number") {
|
|
979
|
+
const rawPreview = (aggValue === undefined ? "undefined" : typeof aggValue === "string" ? aggValue : JSON.stringify(aggValue)).slice(0, 200);
|
|
980
|
+
log("Aggregator retry failed (len=" + (typeof aggValue === "string" ? aggValue.length : 0) + "): " + rawPreview);
|
|
981
|
+
const fallbackPath = (agg && agg.report_file) || (roundDir + "/aggregated.md");
|
|
982
|
+
try {
|
|
983
|
+
const content = fs.readFileSync(fallbackPath, "utf-8");
|
|
984
|
+
const parsed = parseAggregatedMd(content);
|
|
985
|
+
if (parsed && typeof parsed.must_fix === "number") {
|
|
986
|
+
agg = { report_file: fallbackPath, must_fix: parsed.must_fix, suggestion: parsed.suggestion ?? 0 };
|
|
987
|
+
log("Fallback parsed from " + fallbackPath + ": must_fix=" + agg.must_fix);
|
|
988
|
+
}
|
|
989
|
+
} catch { /* fallback read failed */ }
|
|
990
|
+
|
|
991
|
+
if (!agg || typeof agg.must_fix !== "number") {
|
|
992
|
+
log("Aggregator failed and fallback failed, stopping.");
|
|
993
|
+
// W8:aggregator-failure 轮的当前轮条目落 batchRounds——review/aggregate 相位
|
|
994
|
+
// 时长已实测(fix 相位 null 如实采集);F3:聚合失败,mustFix/suggestion 未知
|
|
995
|
+
// 非 0(null,消费侧 ?? "-" 兜底,不误读为 clean 轮)。
|
|
996
|
+
batchRounds.push({ round, mustFix: null, suggestion: null, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
997
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
998
|
+
saveState(state);
|
|
999
|
+
terminated = "aggregator-failure";
|
|
1000
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": aggregator 失败且 fallback 解析失败"
|
|
1001
|
+
+ (aggRaw && typeof aggRaw === "object" && aggRaw.error ? " — " + aggRaw.error : "");
|
|
1002
|
+
batchIndex = BATCHES.length + 1; // 终止外层循环
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const mustFix = agg.must_fix;
|
|
1008
|
+
const suggestion = agg.suggestion ?? 0;
|
|
1009
|
+
log("Aggregated: " + mustFix + " must-fix + " + suggestion + " suggestion(s).");
|
|
1010
|
+
// 最近一次聚合报告路径(含 numeric-only fallback):all-clean 轮不聚合(round-1
|
|
1011
|
+
// 目录无 aggregated.md),残留 continue 后下轮对账段需回溯到最近存在的报告。
|
|
1012
|
+
state.lastAggPath = (typeof agg.report_file === "string" && agg.report_file.trim()) || (roundDir + "/aggregated.md");
|
|
1013
|
+
state.lastAggRound = round; // S-3:对账段标注报告轮号(与 fixResult 轮号可能不同轮)
|
|
1014
|
+
|
|
1015
|
+
// rfl 打分落盘(tier-1 6.6,T7):aggregator 顺手输出的弱信号 scores(reviewer
|
|
1016
|
+
// 分每轮 / fix LLM 三维度 R2+)。A6:逐条形状校验(landScores 纯函数)——畸形
|
|
1017
|
+
// (targetKind 缺失/round 非数/dimensions 非 object)不再静默落盘,计数进 WARN。
|
|
1018
|
+
// 打分失败不影响循环(权威层是状态机客观回填,见 reconcileIssues + backfillFixRegression)。
|
|
1019
|
+
{
|
|
1020
|
+
const landed0 = landScores(state.scores, Array.isArray(agg.scores) ? agg.scores : [], batchIndex);
|
|
1021
|
+
state.scores = landed0.scores;
|
|
1022
|
+
if (landed0.landed === 0) {
|
|
1023
|
+
log("WARN: aggregator scores unusable this round (landed=0, malformed=" + landed0.malformed + ") — quality scoring degraded");
|
|
1024
|
+
} else if (landed0.malformed > 0) {
|
|
1025
|
+
log("WARN: aggregator scores partially malformed (landed=" + landed0.landed + ", malformed=" + landed0.malformed + ") — malformed entries dropped");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// 5.1:R1 的 aggregator must_fix_ids 初始化 state.issues(数字 ID 起点)
|
|
1030
|
+
// rfl 数据链(tier-1 6.1/6.3):降级条目(adjudication downgraded/unverified)
|
|
1031
|
+
// 不建 issue——消费侧过滤(filterActiveIds),修复队列/ES3 校验只含活跃条目;
|
|
1032
|
+
// guidance/evidence 随条目落 issues(fixer 免侦查 + 裁决可追踪)。
|
|
1033
|
+
// 身份对齐(编号+标题三级):R1 时 issues/dormant 均空(批开始重置),解析恒走
|
|
1034
|
+
// L3 直接沿用表格号——统一入口调用保持一致性(fallow 前置批等未来变化下防御)。
|
|
1035
|
+
// title 随条目落储(跨轮身份锚点 + 台账注入段展示原料)。
|
|
1036
|
+
if (round === 1 && agg.must_fix_ids && agg.must_fix_ids.length > 0) {
|
|
1037
|
+
if (!state.issues) state.issues = {};
|
|
1038
|
+
const activeIds = new Set(filterActiveIds(agg.must_fix_ids));
|
|
1039
|
+
const r1IdMap = {};
|
|
1040
|
+
for (const entry of agg.must_fix_ids) {
|
|
1041
|
+
const id = typeof entry === "string" ? entry : entry && entry.id;
|
|
1042
|
+
if (!id || state.issues[id]) continue;
|
|
1043
|
+
if (!activeIds.has(id)) continue;
|
|
1044
|
+
const resolved = resolveIssueIdentity(
|
|
1045
|
+
typeof entry === "string" ? { id } : entry,
|
|
1046
|
+
{ issues: state.issues, dormant: state.dormant || [] },
|
|
1047
|
+
);
|
|
1048
|
+
if (resolved.mapped) r1IdMap[id] = resolved.key;
|
|
1049
|
+
state.issues[resolved.key] = {
|
|
1050
|
+
// severity 结构化(5.7):aggregator 标注 critical/major/minor,converged 终止的
|
|
1051
|
+
// 「无 critical」判定依赖它;旧格式(string)默认 major(must-fix 语义)。
|
|
1052
|
+
firstSeen: 1,
|
|
1053
|
+
severity: typeof entry === "string" ? "major" : (entry.severity || "major"),
|
|
1054
|
+
status: "open",
|
|
1055
|
+
history: [{ round: 1, status: "open" }], fixAttempts: 0,
|
|
1056
|
+
...(entry && typeof entry === "object" && typeof entry.title === "string" && entry.title ? { title: entry.title } : {}),
|
|
1057
|
+
...(entry && typeof entry.guidance === "string" && entry.guidance ? { guidance: entry.guidance } : {}),
|
|
1058
|
+
...(entry && typeof entry.evidence === "string" && entry.evidence ? { evidence: entry.evidence } : {}),
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
state.idMap = { round: 1, map: r1IdMap };
|
|
1062
|
+
// A8:R1 落盘后观测 guidance/evidence 缺失(数据链断点信号)
|
|
1063
|
+
warnMissingFields(agg.must_fix_ids);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// dormant exclude 基准:issues 键(R1 恒等即表格号);R2+ merge 后并入翻译命中的活跃表格号
|
|
1067
|
+
const dormantExcludeIds = new Set(Object.keys(state.issues || {}));
|
|
1068
|
+
// rfl dormant(tier-1 6.3):adjudication 降级条目落盘(含裁决理由),R2+ prompt
|
|
1069
|
+
// 注入复活通道——「降级即消失」的修复。每轮聚合后统一记录(同 id 幂等)。
|
|
1070
|
+
// 执行点在 merge 三级对齐之后(R2+ 路径):excludeIds 需含「翻译后仍在台账活跃的
|
|
1071
|
+
// 表格号」——L2/L3 改键后,活跃条目的表格号 ≠ 台账键,仅用 issues 键做 exclude
|
|
1072
|
+
// 会把活跃追踪中的条目误落 dormant(exec-review 修复场景的改键形态复发)。
|
|
1073
|
+
function recordDormantThisRound() {
|
|
1074
|
+
if (!(agg.must_fix_ids && agg.must_fix_ids.length > 0)) return;
|
|
1075
|
+
const before = (state.dormant || []).length;
|
|
1076
|
+
state.dormant = recordDormant(state.dormant, agg.must_fix_ids, round, dormantExcludeIds);
|
|
1077
|
+
if (state.dormant.length > before) {
|
|
1078
|
+
log("Dormant recorded: " + (state.dormant.length - before) + " adjudication-downgraded issue(s) (total " + state.dormant.length + ")");
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ── Stuck detection ─────────────────────────────────────
|
|
1083
|
+
// 5.1 ID 驱动:R2+ 用 reconciliation 声明的 seenIds + reconcileIssues(同一 ID 连续 N 轮
|
|
1084
|
+
// open/regressed);无 reconciliation 数据(R1 或 reviewer 未填)降级计数式 updateStuckState
|
|
1085
|
+
// (现状语义,MF-2 注释保留)。needs-redesign(fixAttempts>=2)在 wave 5 接入。
|
|
1086
|
+
// M2: reconCount = 所有 status 条目的 prev_id 去重计数(含 fixed)——全 fixed(最常见
|
|
1087
|
+
// 收敛路径)时 reconSeen 为空,但 reconciliation 有数据仍须调 reconcileIssues:
|
|
1088
|
+
// fix-attempted → fixed 的唯一转换点(否则 fix-attempted 永不转 fixed)
|
|
1089
|
+
const reconCount = reconAll.size;
|
|
1090
|
+
// F1: 无对账数据(doc-reviewer-only 批 reconciliation 恒空)时,fix-attempted 条目
|
|
1091
|
+
// 存在仍须执行 reconcile——空 seenIds 语义 = 未被重新报告 = 已修复(5.1「未出现→fixed」)
|
|
1092
|
+
const hasFixAttempted = state.issues
|
|
1093
|
+
? Object.values(state.issues).some((i) => i.status === "fix-attempted")
|
|
1094
|
+
: false;
|
|
1095
|
+
|
|
1096
|
+
// 5.1-2 R2+ 新发现 ID 契约(M2 移出 reconcile 分支,独立执行;F1 扩展为
|
|
1097
|
+
// 「重新报告 = 未修复」转换):aggregator 的 must_fix_ids 中
|
|
1098
|
+
// a) 三级身份对齐(L1 编号+标题双命中沿用 / L2 标题归一唯一命中沿用旧键 /
|
|
1099
|
+
// L3 避让分配)解析出台账键——不在 issues → 创建为新条目(firstSeen=round)
|
|
1100
|
+
// b) 已存在且(fix-attempted 或 fixed)且本轮无对账数据(reconCount===0,
|
|
1101
|
+
// doc-reviewer 场景)→ 重新报告 = 修复失败:转 regressed + fixAttempts+1 + openStreak+1
|
|
1102
|
+
// (RC-7 needs-redesign 出口在无对账配置下可达;reconciliation 场景由
|
|
1103
|
+
// reconcileIssues 处理,避免双计)。fixed 重报分支(MF-2):已确认修复的问题
|
|
1104
|
+
// 再次被报告同样转 regressed——否则 fixed 停留 + 收敛终止组合会在默认配置下
|
|
1105
|
+
// R3 即以 converged 提前终止而 must-fix 仍活跃。
|
|
1106
|
+
// newFindings 统计与 needs-redesign/fixAttempts 追踪对 R2+ 新发现生效。
|
|
1107
|
+
// rfl 数据链(tier-1 6.1/6.3):新发现带 origin(computeOrigin:files 与上轮 fix
|
|
1108
|
+
// 触碰文件相交 = regression,否则 new;无 files 不可归因 WARN);重新上报的
|
|
1109
|
+
// dormant 条目 → revived=true 回修复队列;本轮降级条目不建 issue(同 R1 过滤)。
|
|
1110
|
+
// 身份对齐(编号+标题):LLM 编号跨轮不稳定(紧凑化重排会把新问题排到旧号上、
|
|
1111
|
+
// 同题重报会换号),表格号不再直接当台账主键——resolveIssueIdentity 三级对齐后
|
|
1112
|
+
// 改键条目记入 state.idMap(本轮表格号→台账键),fix 写入与下轮对账经 translateId
|
|
1113
|
+
// 翻译(ES3 的 mustFixIds↔fixes 集合比较保持表格号空间,不翻译)。
|
|
1114
|
+
// 对账翻译用「上一轮」idMap 快照(MF-1):reviewer 的 prev_id 抄自上一轮
|
|
1115
|
+
// aggregated.md,须用产出该报告那轮的表格号空间翻译。必须在下方 merge 块
|
|
1116
|
+
// (会以本轮 roundIdMap 覆盖 state.idMap)之前快照;clean 轮 merge 不执行,
|
|
1117
|
+
// 快照自然携带最近一次聚合的 map。
|
|
1118
|
+
const prevIdMap = (state.idMap && state.idMap.map) || {};
|
|
1119
|
+
if (round > 1 && state.issues && agg.must_fix_ids && agg.must_fix_ids.length > 0) {
|
|
1120
|
+
let added = 0;
|
|
1121
|
+
const activeIds = new Set(filterActiveIds(agg.must_fix_ids));
|
|
1122
|
+
const roundIdMap = {};
|
|
1123
|
+
for (const entry of agg.must_fix_ids) {
|
|
1124
|
+
const id = typeof entry === "string" ? entry : entry && entry.id;
|
|
1125
|
+
if (!id) continue;
|
|
1126
|
+
// exec-review 修复(major-2a):本轮降级条目(不在 activeIds)对新建与已追踪
|
|
1127
|
+
// 转换都不生效——已追踪条目被聚合降级重报 = 聚合撤回本轮判定,不翻 regressed
|
|
1128
|
+
//(对账通道 reconcileIssues 才是 fix-attempted → regressed 的权威转换点),
|
|
1129
|
+
// 也不落 dormant(在 issues 活跃追踪,recordDormant 的 excludeIds 排除)。
|
|
1130
|
+
if (!activeIds.has(id)) continue;
|
|
1131
|
+
// 三级身份对齐:resolved.key = 台账键(mapped=true 时 ≠ 表格号,记入 idMap)
|
|
1132
|
+
const resolved = resolveIssueIdentity(
|
|
1133
|
+
typeof entry === "string" ? { id } : entry,
|
|
1134
|
+
{ issues: state.issues, dormant: state.dormant || [] },
|
|
1135
|
+
);
|
|
1136
|
+
if (resolved.mapped) roundIdMap[id] = resolved.key;
|
|
1137
|
+
const tracked = state.issues[resolved.key];
|
|
1138
|
+
if (tracked) {
|
|
1139
|
+
dormantExcludeIds.add(id); // 翻译后仍在台账活跃的表格号 → dormant exclude
|
|
1140
|
+
if (reconCount === 0 && (tracked.status === "fix-attempted" || tracked.status === "fixed")) {
|
|
1141
|
+
tracked.status = "regressed";
|
|
1142
|
+
tracked.fixAttempts = (tracked.fixAttempts || 0) + 1;
|
|
1143
|
+
tracked.openStreak = (tracked.openStreak || 0) + 1;
|
|
1144
|
+
tracked.severity = typeof entry === "string" ? "major" : (entry.severity || "major");
|
|
1145
|
+
tracked.history.push({ round, status: "regressed" });
|
|
1146
|
+
added++;
|
|
1147
|
+
}
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
// 复活置位(6.3 delta ③ 的闭环另一半):dormant 条目以活跃身份重新上报 →
|
|
1151
|
+
// 回修复队列(下方正常建 issue),后续轮 prompt 不再注入它。判定不需要独立
|
|
1152
|
+
// 的标题匹配——resolveIssueIdentity 的 L2 已按标题归一查过 dormant,标题命中
|
|
1153
|
+
// 时 resolved.key 即 dormant 条目的 id(编号或标题命中殊途同归)。
|
|
1154
|
+
const dormantHit = (state.dormant || []).find((d) => d.id === resolved.key && d.revived !== true);
|
|
1155
|
+
if (dormantHit) {
|
|
1156
|
+
dormantHit.revived = true;
|
|
1157
|
+
log("Dormant revived: " + resolved.key + " re-reported in round " + round);
|
|
1158
|
+
}
|
|
1159
|
+
const origin = computeOrigin(entry, {
|
|
1160
|
+
lastModifiedFiles: state.lastModifiedFiles || [],
|
|
1161
|
+
fixImpactFiles: state.fixImpactFiles || [],
|
|
1162
|
+
});
|
|
1163
|
+
if (!origin) log("WARN: issue " + resolved.key + " carries no files — origin not attributable");
|
|
1164
|
+
state.issues[resolved.key] = {
|
|
1165
|
+
firstSeen: round,
|
|
1166
|
+
severity: typeof entry === "string" ? "major" : (entry.severity || "major"),
|
|
1167
|
+
status: "open", openStreak: 1,
|
|
1168
|
+
history: [{ round, status: "open" }], fixAttempts: 0,
|
|
1169
|
+
...(typeof entry === "object" && typeof entry.title === "string" && entry.title ? { title: entry.title } : {}),
|
|
1170
|
+
...(origin ? { origin } : {}),
|
|
1171
|
+
...(typeof entry === "object" && typeof entry.guidance === "string" && entry.guidance ? { guidance: entry.guidance } : {}),
|
|
1172
|
+
...(typeof entry === "object" && typeof entry.evidence === "string" && entry.evidence ? { evidence: entry.evidence } : {}),
|
|
1173
|
+
};
|
|
1174
|
+
added++;
|
|
1175
|
+
}
|
|
1176
|
+
state.idMap = { round, map: roundIdMap };
|
|
1177
|
+
// A8:R2+ merge 落盘后观测 guidance/evidence 缺失(与 R1 init 同一观测口径)
|
|
1178
|
+
warnMissingFields(agg.must_fix_ids);
|
|
1179
|
+
if (added > 0) log("New findings tracked: " + added + " new or re-reported issue(s) in round " + round);
|
|
1180
|
+
}
|
|
1181
|
+
// dormant 落盘(merge 后:excludeIds 已并入翻译命中的活跃表格号)
|
|
1182
|
+
recordDormantThisRound();
|
|
1183
|
+
|
|
1184
|
+
let stuck = { stuck: false };
|
|
1185
|
+
if (round > 1 && (reconCount > 0 || hasFixAttempted || reconFixed.size > 0)) {
|
|
1186
|
+
// 对账 id 翻译(表格号→台账键,台账键优先——注入段清单的 id 即台账键):
|
|
1187
|
+
// L2/L3 改键后 reviewer 从 aggregated.md 抄的表格号需经 prevIdMap(上一轮
|
|
1188
|
+
// idMap 快照,MF-1)翻译才能
|
|
1189
|
+
// 命中台账。冲突互斥(not-fixed/escalate 优先于 fixed,保守方向):多 reviewer
|
|
1190
|
+
// 并行对同一 prev_id 申报矛盾时,采信「未修好」侧——误转 fixed 会销账真问题,
|
|
1191
|
+
// 误留 open 只多跑一轮,可由下轮对账纠正。
|
|
1192
|
+
{
|
|
1193
|
+
translateReconSets(reconSeen, reconEscalate, reconFixed, prevIdMap, state.issues);
|
|
1194
|
+
}
|
|
1195
|
+
// 对账通道的 dormant 分区(exec-review major-1 修复):reviewer 对 dormant id
|
|
1196
|
+
// 声明 not-fixed 不经此通道建 issue(复活唯一入口 = 聚合活跃重报)
|
|
1197
|
+
const reconFiltered = filterDormantFromRecon(reconSeen, reconEscalate, state.dormant);
|
|
1198
|
+
const rec = reconcileIssues(state.issues || {}, { seenIds: reconFiltered.seen, escalateIds: reconFiltered.escalate, fixedIds: reconFixed, round, stuckThreshold });
|
|
1199
|
+
state.issues = rec.issues;
|
|
1200
|
+
state.knownRemaining = rec.knownRemaining;
|
|
1201
|
+
stuck = { stuck: rec.stuck, stuckIds: rec.stuckIds };
|
|
1202
|
+
log("Reconcile: " + Object.keys(rec.issues).length + " tracked issue(s), known-remaining: " + rec.knownRemaining.length);
|
|
1203
|
+
// rfl regression 维度确定性回填(tier-1 6.6,T7):reconcile 后 regressed 数已
|
|
1204
|
+
// 定,为上轮 fix 的 score entry 填 regression(无 entry 时创建确定性 entry)
|
|
1205
|
+
if (state.fixResults && state.fixResults.length > 0) {
|
|
1206
|
+
state.scores = backfillFixRegression({
|
|
1207
|
+
scores: state.scores, fixResult: state.fixResults[state.fixResults.length - 1],
|
|
1208
|
+
issues: state.issues || {}, round, batch: batchIndex, cleanRound: false,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
// A9(regression 回填边缘缺口):else 分支 = reconCount===0 且无 fix-attempted
|
|
1213
|
+
//(aggregator numeric-only fallback 等无对账数据场景)——上轮 fix 的 regressed
|
|
1214
|
+
// 数本轮不可判定,此前该场景的 regression entry 永缺。以 unverifiable 模式补
|
|
1215
|
+
// 确定性 entry(regression=null,不诚实造 10 分)。unverifiable 为终态(W3):
|
|
1216
|
+
// 回填只匹配最近一次 fix 的 entry、永不重访旧轮,该轮 regression 维度永久
|
|
1217
|
+
// 缺失(CLI 显示 n/a),同轮后续回填经终态 guard 不覆盖。
|
|
1218
|
+
if (round > 1 && state.fixResults && state.fixResults.length > 0) {
|
|
1219
|
+
state.scores = backfillFixRegression({
|
|
1220
|
+
scores: state.scores, fixResult: state.fixResults[state.fixResults.length - 1],
|
|
1221
|
+
issues: state.issues || {}, round, batch: batchIndex, mode: "unverifiable",
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
const s = updateStuckState(prevMustFix, stuckCount, mustFix, stuckThreshold);
|
|
1225
|
+
stuckCount = s.stuckCount;
|
|
1226
|
+
prevMustFix = s.prevMustFix;
|
|
1227
|
+
stuck = { stuck: s.stuck };
|
|
1228
|
+
}
|
|
1229
|
+
if (stuck.stuck) {
|
|
1230
|
+
const stuckIds = (stuck.stuckIds || []).join(", ");
|
|
1231
|
+
log("Stuck: issue(s) not converging for " + stuckThreshold + " rounds: " + stuckIds + ". Stopping.");
|
|
1232
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1233
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
1234
|
+
saveState(state);
|
|
1235
|
+
terminated = "stuck";
|
|
1236
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 问题 " + stuckIds + " 连续 " + stuckThreshold + " 轮未收敛。残留: "
|
|
1237
|
+
+ (state.knownRemaining && state.knownRemaining.length ? state.knownRemaining.join("; ") : "无 deferred");
|
|
1238
|
+
batchIndex = BATCHES.length + 1;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// 5.7 needs-redesign(RC-7):fixAttempts >= maxFixAttempts 且 regressed → 终止。
|
|
1243
|
+
// 顺序在 stuck 之后:stuck(一直在)信息更宏观,needs-redesign(修不好)更具体,先 stuck 后 redesign。
|
|
1244
|
+
if (round > 1 && state.issues) {
|
|
1245
|
+
const redesign = findNeedsRedesign(state.issues, maxFixAttempts);
|
|
1246
|
+
if (redesign.length > 0) {
|
|
1247
|
+
const ids = redesign.map((r) => r.issue_id).join(", ");
|
|
1248
|
+
// 5.7 message 三要素:ID + 修复历史摘要 + 残留清单(history 全文随 state.json 落盘)
|
|
1249
|
+
const historySummary = redesign.map((r) => {
|
|
1250
|
+
const hist = (r.history || []).map((h) => "R" + h.round + ":" + h.status).join(" -> ");
|
|
1251
|
+
return r.issue_id + " [" + (hist || "no history") + "]";
|
|
1252
|
+
}).join("; ");
|
|
1253
|
+
log("Needs redesign: " + ids + " not converging after " + maxFixAttempts + " fix attempts. Stopping.");
|
|
1254
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1255
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
1256
|
+
saveState(state);
|
|
1257
|
+
terminated = "needs-redesign";
|
|
1258
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 问题 " + historySummary + " 经 " + maxFixAttempts
|
|
1259
|
+
+ " 次修复仍未收敛,属于需要重新设计而非继续补丁的结构性问题,请人工介入。残留: "
|
|
1260
|
+
+ (state.knownRemaining && state.knownRemaining.length ? state.knownRemaining.join("; ") : "无 deferred");
|
|
1261
|
+
batchIndex = BATCHES.length + 1;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// 5.7 新发现率收敛:连续 convergeRounds 轮新发现 <= convergeNewIssues → converged
|
|
1266
|
+
const newIssues = Object.values(state.issues).filter((i) => i.firstSeen === round);
|
|
1267
|
+
const newFindings = newIssues.length;
|
|
1268
|
+
const newFindingsCritical = newIssues.filter((i) => i.severity === "critical").length;
|
|
1269
|
+
const conv = checkConvergence({
|
|
1270
|
+
prevStreak: state.convergeStreak || 0, newFindings, newFindingsCritical,
|
|
1271
|
+
convergeNewIssues, convergeRounds,
|
|
1272
|
+
});
|
|
1273
|
+
state.convergeStreak = conv.streak;
|
|
1274
|
+
// MF-2/S-21 收敛门槛:新发现率收敛 ≠ 问题已解决——必须同时满足「无 open/regressed
|
|
1275
|
+
// 活跃条目」才允许 converged 终止。fixed 条目复发(reconcile/merge 已转 regressed)
|
|
1276
|
+
// 后活跃条目存在 → 不收敛,继续修复循环(默认配置下 R3 复发不再提前终止)。
|
|
1277
|
+
// issues 无追踪(aggregator 缺 must_fix_ids)时回退 mustFix===0 数字级判定,
|
|
1278
|
+
// 避免 must_fix>0 照常收敛掩盖未处理问题(S-21)。
|
|
1279
|
+
const trackedCount = Object.keys(state.issues || {}).length;
|
|
1280
|
+
const activeIssues = Object.values(state.issues || {})
|
|
1281
|
+
.filter((i) => i.status === "open" || i.status === "regressed");
|
|
1282
|
+
const noActiveIssues = trackedCount === 0 ? mustFix === 0 : activeIssues.length === 0;
|
|
1283
|
+
if (conv.converged && noActiveIssues && suggestion === 0) {
|
|
1284
|
+
// MF-2 ④:converged 消息列出 open issue ID(对齐 max-rounds 的 remainingIds 逻辑)。
|
|
1285
|
+
// 门槛保证正常路径此处为空;状态漂移时调用方仍能看到残留而非「无 deferred」误报。
|
|
1286
|
+
const remainingIds = Object.entries(state.issues || {})
|
|
1287
|
+
.filter(([, i]) => i.status !== "fixed" && i.status !== "deferred")
|
|
1288
|
+
.map(([id]) => id);
|
|
1289
|
+
log("Converged: new findings <= " + convergeNewIssues + " for " + convergeRounds + " rounds. Batch " + batchIndex + " done, proceeding to next batch.");
|
|
1290
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1291
|
+
saveState(state);
|
|
1292
|
+
terminated = "converged";
|
|
1293
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": 新发现率收敛(连续 " + convergeRounds
|
|
1294
|
+
+ " 轮新问题 ≤" + convergeNewIssues + ")。残留: "
|
|
1295
|
+
+ (remainingIds.length ? remainingIds.join(", ") : "无")
|
|
1296
|
+
+ (state.knownRemaining && state.knownRemaining.length ? ";deferred: " + state.knownRemaining.join("; ") : "");
|
|
1297
|
+
batchClean = true; // MF-1: 与 clean 一致,让外层 for 推进下一批(不跳过后续批次)
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// A4(全降级轮不驱动 fix,设计 §6.3 省轮次的兑现):reviewer 原始计数有 must-fix
|
|
1303
|
+
// 但 aggregator 裁决后全部降级(mustFix===0 且活跃条目为 0)且 suggestion 也为 0 时,
|
|
1304
|
+
// all-clean break(reviewer 原始计数口径,见上方 every 判定)拦不住本路径——不守卫
|
|
1305
|
+
// 会空转派发 fixer(fixCount++ 且无问题可修)。suggestion>0 时不得在此 break
|
|
1306
|
+
//(修复范围全等级,建议级问题仍需走 fix 修复),fall through 到下方 fix 阶段。
|
|
1307
|
+
if (mustFix === 0
|
|
1308
|
+
&& suggestion === 0
|
|
1309
|
+
&& reviewResults.some((r) => r.must_fix > 0)
|
|
1310
|
+
&& (agg.must_fix_ids ? filterActiveIds(agg.must_fix_ids).length : 0) === 0) {
|
|
1311
|
+
// 台账守门(第三收工点):本轮聚合活跃条目为 0 不蕴涵台账已清——上轮 open
|
|
1312
|
+
// 条目本轮聚合漏报时残留仍在,此时收工 = 假 clean。有残留则跳过 fix(修复
|
|
1313
|
+
// 队列为空,派 fixer 只会对着全降级报告乱动)继续轮追账,且不授予 W5 跨批
|
|
1314
|
+
// clean 补记(本轮没真正干净,clean 补记会喂大下轮全员 skip → 注入段无消费方)。
|
|
1315
|
+
if (hasOpenResidue(state.issues)) {
|
|
1316
|
+
const residueIds = Object.entries(state.issues || {})
|
|
1317
|
+
.filter(([, i]) => i.status === "open" || i.status === "regressed")
|
|
1318
|
+
.map(([id]) => id).join(", ");
|
|
1319
|
+
log("All downgraded this round but ledger has unresolved issue(s): " + residueIds + " — skipping fix stage, continuing to reconcile.");
|
|
1320
|
+
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1321
|
+
saveState(state);
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
log("All reviewer must-fix entries adjudicated down this round — no active fix queue, skipping fix stage.");
|
|
1325
|
+
// F4:此处不需要 backfillFixRegression 调用(原死防御已删)。第 3 轮探针实证
|
|
1326
|
+
// 其恒为逐字节 no-op:能到达 A4 的 R2+ 轮(round>1 且 fixResults 非空),上方
|
|
1327
|
+
// stuck 检测的 if/else 两分支已在相同门控与相同匹配键(round-1/batch)下分别
|
|
1328
|
+
// 回填过——reconcile 路径(reconCount>0 或有 fix-attempted)mode=normal、无对账
|
|
1329
|
+
// 数据路径 mode=unverifiable——regression 键必已存在(number 或 null),W3 终态
|
|
1330
|
+
// guard(键存在即不再处理)使任何后续同键调用直接短路。round=1 时 A4 的调用
|
|
1331
|
+
// 门控本就不满足。保留调用会误导维护者以为此处承担回填职责。
|
|
1332
|
+
// W5:A4 场景 reviewer 原始 must_fix>0 走了 recordAgentDirty,但裁决后全部降级
|
|
1333
|
+
// = 无真实问题(语义等价 clean)——对 active 中本轮 must_fix>0 的 reviewer 补记
|
|
1334
|
+
// recordAgentClean(快照语义对齐常规调用点:lastCleanBatch + 当时 fixCount),
|
|
1335
|
+
// 让「裁决降级=噪声」也推进跨批 skip(否则同 agent 后续批每批全价重扫)。
|
|
1336
|
+
// 本轮无 fix(fixCount 不变),批后快照比较成立。
|
|
1337
|
+
// F2:agg.must_fix_ids 缺失(aggregator JSON 无效走 parseAggregatedMd numeric
|
|
1338
|
+
// fallback 的 agg 无此键)时无条目级裁决证据——「must_fix=0」只是 md 里的一行
|
|
1339
|
+
// 数字,不能证明 reviewer 的 must-fix 是被裁决降级的噪声。此时弱证据不授予
|
|
1340
|
+
// 跨批 clean-skip(否则「跳一轮」被 shouldSkipAgent 放大为「跳到底」且该 agent
|
|
1341
|
+
// 永不再重扫),留待后续批全价复核。
|
|
1342
|
+
for (const a of agentRoundResults) {
|
|
1343
|
+
if (agg.must_fix_ids && a.must_fix > 0) recordAgentClean(state, a.name, batchIndex);
|
|
1344
|
+
}
|
|
1345
|
+
batchRounds.push({ round, mustFix: 0, suggestion: reviewResults.reduce((a, r) => a + (r.suggestion ?? 0), 0), agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1346
|
+
saveState(state);
|
|
1347
|
+
batchClean = true;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// ── Fix ─────────────────────────────────────────────────
|
|
1352
|
+
phase("Fix");
|
|
1353
|
+
let reportContent;
|
|
1354
|
+
try {
|
|
1355
|
+
reportContent = fs.readFileSync(agg.report_file, "utf-8");
|
|
1356
|
+
} catch {
|
|
1357
|
+
reportContent = "(could not read aggregated report)";
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
let prevHead = "";
|
|
1361
|
+
try {
|
|
1362
|
+
prevHead = require("child_process").execSync(
|
|
1363
|
+
"git rev-parse HEAD", { encoding: "utf-8", timeout: 10_000 }
|
|
1364
|
+
).trim();
|
|
1365
|
+
} catch {
|
|
1366
|
+
// 非 git 项目(如纯文档目录):prevHead 为空,跳过 modifiedFiles 统计
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const commitInstr = autoCommit
|
|
1370
|
+
? "- After all fixes, stage ONLY the files you modified: `git add <file1> <file2> ...` (explicit paths).\n" +
|
|
1371
|
+
"- NEVER use `git add -A` or `git add .` — the workspace may contain unrelated untracked files.\n" +
|
|
1372
|
+
"- Commit with message: `fix: review batch " + batchIndex + " round " + round + " — " + mustFix + " must-fix + " + suggestion + " suggestion`"
|
|
1373
|
+
: "- Do NOT commit. Leave the fixes in the working tree (autoCommit=false).";
|
|
1374
|
+
|
|
1375
|
+
// A3(guidance 链最后一跳):活跃条目中带非空 guidance 的清单——构造确定性通道
|
|
1376
|
+
// 传给 fixer(reportContent 正文之外,per-issue 直达)。W2:先 filterActiveIds 过滤
|
|
1377
|
+
//(与修复队列/ES3 校验同口径)——降级条目(adjudication downgraded/unverified)的
|
|
1378
|
+
// guidance 不再以 "MUST-FIX GUIDANCE" 标题混给 fixer(口径不一致会诱导修复已裁决
|
|
1379
|
+
// 噪声)。normalize 后条目均为对象;string 旧格式(经 fallback 等路径)无 guidance
|
|
1380
|
+
// 自然跳过;must_fix_ids 为 undefined(fallback 路径)时 activeGuidanceIds 为空集
|
|
1381
|
+
// → fixGuidance 仍为空清单。
|
|
1382
|
+
const activeGuidanceIds = new Set(filterActiveIds(agg.must_fix_ids || []));
|
|
1383
|
+
const fixGuidance = (agg.must_fix_ids || [])
|
|
1384
|
+
.filter((e) => e && typeof e === "object" && typeof e.guidance === "string" && e.guidance.trim()
|
|
1385
|
+
&& activeGuidanceIds.has(e.id))
|
|
1386
|
+
.map((e) => ({ id: e.id, guidance: e.guidance }));
|
|
1387
|
+
|
|
1388
|
+
const fixT0 = Date.now();
|
|
1389
|
+
const fixPromptBuilt = buildFixPrompt({
|
|
1390
|
+
header: "Fix round " + round + " (batch " + batchIndex + ")",
|
|
1391
|
+
reportContent,
|
|
1392
|
+
fixPrompt,
|
|
1393
|
+
commitInstr,
|
|
1394
|
+
caution: agg.fixes_caution && agg.fixes_caution.length ? agg.fixes_caution : [],
|
|
1395
|
+
guidance: fixGuidance,
|
|
1396
|
+
});
|
|
1397
|
+
const fxRaw = await agent({
|
|
1398
|
+
prompt: fixPromptBuilt,
|
|
1399
|
+
schema: fixSchema,
|
|
1400
|
+
// S4:fixAgent = agentRef 路径(主线程按路径加载 + frontmatter model 传播);
|
|
1401
|
+
// 未传保持现状(通用 subagent + 内联 fixPrompt)。
|
|
1402
|
+
model: MODEL,
|
|
1403
|
+
description: (FIX_DEF && FIX_DEF.name) || "fix",
|
|
1404
|
+
// fix 不设 timeoutMs = 不限时(execute-options-mapper: undefined/<=0 → 不设超时)。
|
|
1405
|
+
// 带写操作(改项目代码)可能很久(大重构/多文件),不应被墙钟超时打断。
|
|
1406
|
+
returnMeta: true,
|
|
1407
|
+
...(FIX_DEF && FIX_DEF.path ? { agent: FIX_DEF.path } : {}),
|
|
1408
|
+
});
|
|
1409
|
+
phaseTimings.fix = [fixT0, Date.now()];
|
|
1410
|
+
recordCall(buildCallRecord({
|
|
1411
|
+
batch: batchIndex, round, role: "fixer",
|
|
1412
|
+
name: (FIX_DEF && FIX_DEF.name) || "fix", model: MODEL, prompt: fixPromptBuilt, promptMode: null, meta: fxRaw,
|
|
1413
|
+
}));
|
|
1414
|
+
|
|
1415
|
+
// returnMeta 下 fxRaw = {value, error}:先查 error(失败分支可达,MF-1),再对 value 做 parseResult
|
|
1416
|
+
if (fxRaw && typeof fxRaw === "object" && fxRaw.error) {
|
|
1417
|
+
// fix agent 调用失败(AgentRegistry not found / 超时等)。
|
|
1418
|
+
// 与 review 路径(raw.error → review-failure)对齐:结构化终止而非静默当成功——
|
|
1419
|
+
// 否则 fixed_count 缺失被 `?? mustFix` 回退,totalFixed 虚增且 must_fix 不降白跑轮次(MF-1)。
|
|
1420
|
+
log("Fix agent failed, stopping.");
|
|
1421
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1422
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
1423
|
+
saveState(state);
|
|
1424
|
+
terminated = "fix-failure";
|
|
1425
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": fix agent 调用失败 — " + fxRaw.error;
|
|
1426
|
+
batchIndex = BATCHES.length + 1;
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
const fx = parseResult(fxRaw.value);
|
|
1430
|
+
const fixResult = fx ? normalizeFixResult(fx) : null;
|
|
1431
|
+
if (!fixResult) {
|
|
1432
|
+
log("Fix agent failed, stopping.");
|
|
1433
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1434
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
1435
|
+
saveState(state);
|
|
1436
|
+
terminated = "fix-failure";
|
|
1437
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": fix agent 结果无效";
|
|
1438
|
+
batchIndex = BATCHES.length + 1;
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// ES3 硬校验(5.3 红线,恢复 mustFixIds 交叉校验——wave 3 后 agg.must_fix_ids
|
|
1443
|
+
// 已是标准字段):(1) deferred 只允许 minor;(2) must-fix 必须全进 fixes[](漏修
|
|
1444
|
+
// 判 violation)。任一违规 → fix-failure(结构化终止)。trackedIssues 传入
|
|
1445
|
+
// state.issues——deferred severity 与追踪表交叉核对(MF-4):must-fix 被标 minor
|
|
1446
|
+
// 塞进 deferred 的逃逸路径在追踪表面前失效(追踪 severity 为准)。
|
|
1447
|
+
// rfl(tier-1 6.3):mustFixIds 传 filterActiveIds 结果——降级条目不占修复队列
|
|
1448
|
+
//(must_fix 计数由 aggregator 按非降级条目报,两侧口径一致)。
|
|
1449
|
+
// idMap(本轮表格号→台账键)只翻译 deferred 交叉核对的查表输入;mustFixIds 与
|
|
1450
|
+
// fixes[].issue_id 的集合比较双侧保持表格号空间(同源 aggregated.md,翻译即误杀)。
|
|
1451
|
+
const fixIdMap = (state.idMap && state.idMap.round === round && state.idMap.map) || {};
|
|
1452
|
+
const es3Violations = validateFixResult(fixResult, filterActiveIds(agg.must_fix_ids), state.issues, fixIdMap);
|
|
1453
|
+
if (es3Violations.length > 0) {
|
|
1454
|
+
// m7: violation 分两类——deferred 非 minor / must-fix 漏修(must-fix-not-fixed),
|
|
1455
|
+
// finalMessage 文案区分:统一文案会把漏修误报成 defer 违规,误导修复方向
|
|
1456
|
+
const parts = es3Violations.map((v) =>
|
|
1457
|
+
v.severity === "must-fix-not-fixed"
|
|
1458
|
+
? "must-fix 未在 fixes[] 中修复(漏修)— " + v.issue_id
|
|
1459
|
+
: "deferred 含非 minor 条目(must-fix 不得 defer)— " + v.issue_id + "(" + v.severity + ")"
|
|
1460
|
+
);
|
|
1461
|
+
log("ES3 violation: " + JSON.stringify(es3Violations));
|
|
1462
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles: [], phaseTimings });
|
|
1463
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
1464
|
+
saveState(state);
|
|
1465
|
+
terminated = "fix-failure";
|
|
1466
|
+
finalMessage = "Batch " + batchIndex + " round " + round + ": " + parts.join("; ");
|
|
1467
|
+
batchIndex = BATCHES.length + 1;
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// ES2 软校验(5.3 证据标准):defer 理由过短/无实质 → warning 日志(不终止)
|
|
1472
|
+
for (const d of fixResult.deferred) {
|
|
1473
|
+
const reason = typeof d.reason === "string" ? d.reason : "";
|
|
1474
|
+
if (reason.trim().length < 20) {
|
|
1475
|
+
log("WARN: deferred reason too short / no concrete cost description: " + JSON.stringify(d));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// affected_files 并入 state.fixImpactFiles(5.3/5.5):recheck scope = modifiedFiles ∪ fixImpactFiles
|
|
1480
|
+
const impactFiles = [];
|
|
1481
|
+
for (const f of fixResult.fixes) {
|
|
1482
|
+
if (Array.isArray(f.affected_files)) {
|
|
1483
|
+
for (const af of f.affected_files) {
|
|
1484
|
+
if (typeof af === "string" && af.trim() && !impactFiles.includes(af.trim())) impactFiles.push(af.trim());
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
state.fixImpactFiles = impactFiles;
|
|
1489
|
+
|
|
1490
|
+
// m4: 先初始化容器——aggregator JSON 无效走 parseAggregatedMd 回退时 agg 无
|
|
1491
|
+
// must_fix_ids → R1 初始化跳过 → state.issues undefined;此处初始化保证 deferred
|
|
1492
|
+
// 写入与 knownRemaining 同步链路生效(否则 knownRemaining 恒空,deferred 跨轮继承整链失效)
|
|
1493
|
+
if (!state.issues) state.issues = {};
|
|
1494
|
+
// 5.1:fix 结果标记 fix-attempted(ID 对账驱动)+ fixResults 落库(R2+ prompt 输入)
|
|
1495
|
+
// 归一化查表(findIssueKey,与 ES3 同键空间):fix agent ID 漂移("mf-1"/
|
|
1496
|
+
// "MF-1 (fixed)")不再丢匹配——精确键查表时 issue 停留 open,reconcile 无
|
|
1497
|
+
// fix-attempted 可转 fixed/regressed,needs-redesign 出口对该类 ID 静默失效。
|
|
1498
|
+
// 查表前先经 idMap 翻译(fixer 申报表格号,L2/L3 改键后需翻译到台账键;翻译
|
|
1499
|
+
// miss 回退原值走归一化兜底 = 现状行为)。
|
|
1500
|
+
for (const f of fixResult.fixes) {
|
|
1501
|
+
if (f && typeof f.issue_id === "string") {
|
|
1502
|
+
const trackedKey = findIssueKey(state.issues, translateId(fixIdMap, f.issue_id, state.issues));
|
|
1503
|
+
if (trackedKey) {
|
|
1504
|
+
state.issues[trackedKey].status = "fix-attempted";
|
|
1505
|
+
state.issues[trackedKey].history.push({ round, status: "fix-attempted" });
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// 5.3-4 deferred 写入 state.issues(known-remaining 跨轮继承链路):deferred 条目
|
|
1510
|
+
// 以 status=deferred 入 issues,据此生成 knownRemaining 传给 R2+ prompt。
|
|
1511
|
+
// ID 已存在(曾被修复/降级,含大小写/尾注漂移)→ 更新状态 + reason;
|
|
1512
|
+
// 不存在(S-x minor)→ 新建。漂移 ID 归一化匹配防止幽灵条目(原条目仍 open 阻塞收敛)。
|
|
1513
|
+
for (const d of fixResult.deferred) {
|
|
1514
|
+
if (!d || typeof d.issue_id !== "string" || !d.issue_id) continue;
|
|
1515
|
+
const reason = typeof d.reason === "string" ? d.reason : "";
|
|
1516
|
+
const trackedKey = findIssueKey(state.issues, translateId(fixIdMap, d.issue_id, state.issues));
|
|
1517
|
+
if (trackedKey) {
|
|
1518
|
+
state.issues[trackedKey].status = "deferred";
|
|
1519
|
+
state.issues[trackedKey].deferredReason = reason;
|
|
1520
|
+
state.issues[trackedKey].history.push({ round, status: "deferred" });
|
|
1521
|
+
} else {
|
|
1522
|
+
state.issues[d.issue_id] = {
|
|
1523
|
+
firstSeen: round, severity: "minor", status: "deferred",
|
|
1524
|
+
deferredReason: reason,
|
|
1525
|
+
history: [{ round, status: "deferred" }], fixAttempts: 0,
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
// known-remaining 同步更新:deferred 在本轮 fix 后即生效,R2+ prompt 立即消费
|
|
1530
|
+
// (不依赖下轮 reconcile 才生成——否则滞后一轮,reviewer 本轮看不到 deferred 清单)
|
|
1531
|
+
state.knownRemaining = computeKnownRemaining(state.issues);
|
|
1532
|
+
if (!state.fixResults) state.fixResults = [];
|
|
1533
|
+
state.fixResults.push({ ...fixResult, round }); // round 供对账段标注轮号(S-3)
|
|
1534
|
+
|
|
1535
|
+
const fixedCount = fixResult.fixed_count ?? mustFix;
|
|
1536
|
+
totalFixed += fixedCount;
|
|
1537
|
+
state.fixCount++;
|
|
1538
|
+
roundHasFix = true;
|
|
1539
|
+
|
|
1540
|
+
let modifiedFiles = [];
|
|
1541
|
+
if (prevHead) {
|
|
1542
|
+
try {
|
|
1543
|
+
const out = require("child_process").execSync(
|
|
1544
|
+
"git diff --name-only " + prevHead, { encoding: "utf-8", timeout: 10_000 }
|
|
1545
|
+
).trim();
|
|
1546
|
+
modifiedFiles = out ? out.split("\n") : [];
|
|
1547
|
+
} catch { /* empty */ }
|
|
1548
|
+
}
|
|
1549
|
+
batchRounds.push({ round, mustFix, suggestion, agents: agentRoundResults, modifiedFiles, phaseTimings });
|
|
1550
|
+
// M4: 批内即时字段——下轮 scoped 分支(recheckAfterFix=true)从
|
|
1551
|
+
// state.lastModifiedFiles 读本批 fix 的真实改动文件(git 实测兜底)
|
|
1552
|
+
state.lastModifiedFiles = modifiedFiles;
|
|
1553
|
+
saveState(state);
|
|
1554
|
+
|
|
1555
|
+
log("Fixed " + fixedCount + " issue(s). Total: " + totalFixed + ". Modified " + modifiedFiles.length + " file(s). Continuing...");
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
if (batchIndex > BATCHES.length) break; // 已终止
|
|
1559
|
+
|
|
1560
|
+
state.batches.push({ index: batchIndex, name: BATCH_NAMES[batchIndex - 1], rounds: batchRounds });
|
|
1561
|
+
|
|
1562
|
+
terminated = resolveBatchTerminated(batchClean, terminated);
|
|
1563
|
+
if (terminated === "max-rounds") {
|
|
1564
|
+
// 该批达到 maxRounds 仍残留 must-fix → fail-fast,不进入后续批。
|
|
1565
|
+
// 5.9 残留清单:未 fixed 的 issues(status != fixed/deferred)+ known-remaining。
|
|
1566
|
+
const remainingIds = state.issues
|
|
1567
|
+
? Object.entries(state.issues)
|
|
1568
|
+
.filter(([, i]) => i.status !== "fixed" && i.status !== "deferred")
|
|
1569
|
+
.map(([id]) => id)
|
|
1570
|
+
: [];
|
|
1571
|
+
finalMessage = "Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") 达到 maxRounds=" + maxRounds
|
|
1572
|
+
+ " 仍有 must-fix,终止整个 workflow。残留: "
|
|
1573
|
+
+ (remainingIds.length ? remainingIds.join(", ") : "(issues 未追踪)")
|
|
1574
|
+
+ (state.knownRemaining && state.knownRemaining.length ? ";deferred: " + state.knownRemaining.join("; ") : "");
|
|
1575
|
+
log(finalMessage);
|
|
1576
|
+
saveState(state);
|
|
1577
|
+
batchIndex = BATCHES.length + 1;
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
saveState(state);
|
|
1581
|
+
log("=== Batch " + batchIndex + " (" + BATCH_NAMES[batchIndex - 1] + ") CLEAN ===");
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
log("\n=== Loop Complete ===");
|
|
1585
|
+
saveState(state);
|
|
1586
|
+
|
|
1587
|
+
return {
|
|
1588
|
+
batches: BATCHES.length,
|
|
1589
|
+
totalFixed,
|
|
1590
|
+
terminated,
|
|
1591
|
+
targetType,
|
|
1592
|
+
target,
|
|
1593
|
+
runDir: RUN_ROOT,
|
|
1594
|
+
// 5.9 terminated 透出:非 clean 时 message 含终止原因 + 残留 ID 清单 + deferred 理由
|
|
1595
|
+
// (stuck/needs-redesign/converged/max-rounds/*-failure 均由 finalMessage 承载)。
|
|
1596
|
+
// 渲染层特判(launcher 对 terminated 非 clean 的视觉区分)留 TODO:当前 tool 结果
|
|
1597
|
+
// 已含完整 message,主 agent 可直接感知差异。
|
|
1598
|
+
// 5.9 视觉区分:非 clean 终止加 [UNRESOLVED] 前缀(tool 结果即主 agent 可见层,
|
|
1599
|
+
// launcher 透传 message——无需跨模块渲染特判,W5C3 决策更新)
|
|
1600
|
+
message: terminated === "clean"
|
|
1601
|
+
? "All batches clean. " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
|
|
1602
|
+
: terminated === "converged"
|
|
1603
|
+
? finalMessage + " " + totalFixed + " issue(s) fixed total. State: " + STATE_FILE
|
|
1604
|
+
: "[UNRESOLVED] " + finalMessage + ". State: " + STATE_FILE,
|
|
1605
|
+
};
|