baxian 1.2.61 → 2.0.1
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 +7 -4
- package/README.zh-CN.md +7 -4
- package/dist/agent/bootstrap-poller.d.ts +1 -1
- package/dist/agent/bootstrap-poller.d.ts.map +1 -1
- package/dist/agent/bootstrap-poller.js.map +1 -1
- package/dist/agent/bootstrap.d.ts +1 -1
- package/dist/agent/bootstrap.d.ts.map +1 -1
- package/dist/agent/bootstrap.js +6 -2
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/branch.d.ts +1 -3
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +2 -2
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts +0 -1
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +15 -49
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +212 -90
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +2983 -1574
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts +3 -0
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +6 -0
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/pane-streamer-manager.d.ts +6 -6
- package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +2 -2
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +46 -36
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +20 -2
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +135 -17
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +9 -7
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +18 -9
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts +4 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +46 -45
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +8 -5
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +34 -30
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +8 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +70 -15
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.d.ts +20 -1
- package/dist/agent/review-transport.d.ts.map +1 -1
- package/dist/agent/review-transport.js +68 -20
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/agent/root-agent-runtime.d.ts +80 -0
- package/dist/agent/root-agent-runtime.d.ts.map +1 -0
- package/dist/agent/root-agent-runtime.js +635 -0
- package/dist/agent/root-agent-runtime.js.map +1 -0
- package/dist/agent/root-recovery-coordinator.d.ts +125 -0
- package/dist/agent/root-recovery-coordinator.d.ts.map +1 -0
- package/dist/agent/root-recovery-coordinator.js +1579 -0
- package/dist/agent/root-recovery-coordinator.js.map +1 -0
- package/dist/agent/runner.d.ts +1 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +103 -7
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts +1 -0
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +16 -10
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +3 -0
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +11 -5
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +42 -1
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +88 -17
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +71 -15
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/platform-guard.d.ts +10 -2
- package/dist/api/platform-guard.d.ts.map +1 -1
- package/dist/api/platform-guard.js +112 -10
- package/dist/api/platform-guard.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +57 -16
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +21 -36
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +6 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -7
- package/dist/cli.js.map +1 -1
- package/dist/config/hot-reload.d.ts +11 -2
- package/dist/config/hot-reload.d.ts.map +1 -1
- package/dist/config/hot-reload.js +43 -8
- package/dist/config/hot-reload.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +23 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/normalizer.d.ts.map +1 -1
- package/dist/config/normalizer.js +18 -0
- package/dist/config/normalizer.js.map +1 -1
- package/dist/config/validator.d.ts +3 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +247 -54
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +615 -1069
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +787 -487
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -79
- package/dist/index.js.map +1 -1
- package/dist/platform/command-renderer.d.ts +1 -0
- package/dist/platform/command-renderer.d.ts.map +1 -1
- package/dist/platform/command-renderer.js +16 -2
- package/dist/platform/command-renderer.js.map +1 -1
- package/dist/platform/comment-cursor.d.ts +9 -0
- package/dist/platform/comment-cursor.d.ts.map +1 -1
- package/dist/platform/comment-cursor.js +63 -5
- package/dist/platform/comment-cursor.js.map +1 -1
- package/dist/platform/driver-host.d.ts.map +1 -1
- package/dist/platform/driver-host.js +0 -2
- package/dist/platform/driver-host.js.map +1 -1
- package/dist/platform/driver-spec.d.ts.map +1 -1
- package/dist/platform/driver-spec.js +17 -6
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/feedback.d.ts.map +1 -1
- package/dist/platform/feedback.js +0 -9
- package/dist/platform/feedback.js.map +1 -1
- package/dist/platform/git-driver.d.ts +3 -0
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +45 -3
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +18 -5
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +283 -38
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugins/github/baxian-plugin.json +1 -1
- package/dist/platform/plugins/github/driver.json +14 -3
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +7 -6
- package/dist/platform/plugins/github/skills/baxian-cli-gh/agents/openai.yaml +2 -0
- package/dist/platform/pr-binding.d.ts.map +1 -1
- package/dist/platform/pr-binding.js +0 -2
- package/dist/platform/pr-binding.js.map +1 -1
- package/dist/platform/pr-conversation-cache.d.ts.map +1 -0
- package/dist/platform/pr-conversation-cache.js.map +1 -0
- package/dist/platform/row-schema.d.ts.map +1 -1
- package/dist/platform/row-schema.js +1 -0
- package/dist/platform/row-schema.js.map +1 -1
- package/dist/platform/startup.d.ts +29 -1
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +82 -4
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts +2 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +6 -4
- package/dist/platform/types.js.map +1 -1
- package/dist/platform/verdict-engine.d.ts.map +1 -1
- package/dist/platform/verdict-engine.js +0 -2
- package/dist/platform/verdict-engine.js.map +1 -1
- package/dist/shared/constants.d.ts +7 -1
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +5 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +98 -7
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +18 -0
- package/dist/shared/types.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +5 -15
- package/dist/skills/baxian-pr-recheck/SKILL.md +5 -44
- package/dist/skills/baxian-pr-review/SKILL.md +6 -43
- package/dist/skills/baxian-research/SKILL.md +1 -1
- package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
- package/dist/skills/baxian-server-review/SKILL.md +2 -2
- package/dist/skills/baxian-signals/SKILL.md +11 -7
- package/dist/skills/baxian-task-check/SKILL.md +3 -5
- package/dist/state/agent-store.js +0 -2
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/init.d.ts.map +1 -1
- package/dist/state/init.js +1 -0
- package/dist/state/init.js.map +1 -1
- package/dist/state/lock.d.ts +6 -0
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +8 -0
- package/dist/state/lock.js.map +1 -1
- package/dist/state/review-store.d.ts +21 -1
- package/dist/state/review-store.d.ts.map +1 -1
- package/dist/state/review-store.js +132 -0
- package/dist/state/review-store.js.map +1 -1
- package/dist/state/root-recovery-store.d.ts +107 -0
- package/dist/state/root-recovery-store.d.ts.map +1 -0
- package/dist/state/root-recovery-store.js +400 -0
- package/dist/state/root-recovery-store.js.map +1 -0
- package/dist/state/task-store.d.ts +6 -0
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +266 -21
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts +3 -3
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js.map +1 -1
- package/dist/terminal/stream-ws.d.ts.map +1 -1
- package/dist/terminal/stream-ws.js.map +1 -1
- package/dist/web/assets/index-BXfgaFNm.js +18 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/github/mapper.d.ts +0 -9
- package/dist/github/mapper.d.ts.map +0 -1
- package/dist/github/mapper.js +0 -25
- package/dist/github/mapper.js.map +0 -1
- package/dist/github/poller.d.ts +0 -73
- package/dist/github/poller.d.ts.map +0 -1
- package/dist/github/poller.js +0 -556
- package/dist/github/poller.js.map +0 -1
- package/dist/github/pr-conversation-cache.d.ts.map +0 -1
- package/dist/github/pr-conversation-cache.js.map +0 -1
- package/dist/github/pr-conversation.d.ts +0 -7
- package/dist/github/pr-conversation.d.ts.map +0 -1
- package/dist/github/pr-conversation.js +0 -175
- package/dist/github/pr-conversation.js.map +0 -1
- package/dist/github/resolver.d.ts +0 -8
- package/dist/github/resolver.d.ts.map +0 -1
- package/dist/github/resolver.js +0 -28
- package/dist/github/resolver.js.map +0 -1
- package/dist/state/post-approve-store.d.ts +0 -23
- package/dist/state/post-approve-store.d.ts.map +0 -1
- package/dist/state/post-approve-store.js +0 -82
- package/dist/state/post-approve-store.js.map +0 -1
- package/dist/web/assets/index-CUJtd0bC.js +0 -16
- /package/dist/{github → platform}/pr-conversation-cache.d.ts +0 -0
- /package/dist/{github → platform}/pr-conversation-cache.js +0 -0
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { isSpecStagePhase } from '../shared/index.js';
|
|
2
|
-
import { ReviewExchangeError } from '../agent/review-transport.js';
|
|
1
|
+
import { isSpecStagePhase, taskGenerationGuard } from '../shared/index.js';
|
|
2
|
+
import { ReviewExchangeError, } from '../agent/review-transport.js';
|
|
3
|
+
import { settlePermitOf } from '../agent/phase-signal-watcher.js';
|
|
4
|
+
import { computeBackoffMs } from '../timing/backoff.js';
|
|
5
|
+
import { reviewFindingsDigest, serverResponseFailureSignature, } from '../state/review-store.js';
|
|
3
6
|
const LEGACY_DIFF_LARGE_THRESHOLD = 2000;
|
|
4
7
|
const FILE_HEADER_RE = /^diff --git a\/(.+?) b\//;
|
|
8
|
+
const SERVER_HANDLER_ATTEMPTS = 3;
|
|
9
|
+
const SERVER_HANDLER_BACKOFF = { baseMs: 10, maxMs: 20, factor: 2, jitter: 0 };
|
|
10
|
+
const SERVER_SIGNAL_KINDS = new Set([
|
|
11
|
+
'code-done', 'code-reviewed', 'code-fixed', 'code-ready', 'spec-done', 'spec-reviewed', 'spec-fixed',
|
|
12
|
+
]);
|
|
5
13
|
function splitLegacyDiffByFile(diff) {
|
|
6
14
|
if (diff.trim() === '')
|
|
7
15
|
return [];
|
|
@@ -96,10 +104,14 @@ function aggregateBatchFindings(batches, round) {
|
|
|
96
104
|
function coverageGaps(findings, responseIds) {
|
|
97
105
|
const findingIds = new Set(findings.findings.map(f => f.id));
|
|
98
106
|
return {
|
|
99
|
-
missing: [...findingIds].filter(id => !responseIds.has(id)),
|
|
100
|
-
unknown: [...responseIds].filter(id => !findingIds.has(id)),
|
|
107
|
+
missing: [...findingIds].filter(id => !responseIds.has(id)).sort(),
|
|
108
|
+
unknown: [...responseIds].filter(id => !findingIds.has(id)).sort(),
|
|
101
109
|
};
|
|
102
110
|
}
|
|
111
|
+
function waitForHandlerRetry(attempt) {
|
|
112
|
+
const ms = computeBackoffMs(attempt, SERVER_HANDLER_BACKOFF);
|
|
113
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
103
115
|
async function emitIntervention(bus, task, data) {
|
|
104
116
|
try {
|
|
105
117
|
await bus.emit({
|
|
@@ -129,7 +141,9 @@ async function gate(bus, manager, event, expect) {
|
|
|
129
141
|
const token = event.data?.token;
|
|
130
142
|
const phaseOk = expect.phase === 'any'
|
|
131
143
|
|| (expect.phase === 'spec' ? task.phase !== 'code' : !isSpecStagePhase(task.phase));
|
|
132
|
-
if (
|
|
144
|
+
if (token && token !== task.signalToken)
|
|
145
|
+
return null;
|
|
146
|
+
if (task.status !== expect.status || !phaseOk || !token) {
|
|
133
147
|
await emitIntervention(bus, task, {
|
|
134
148
|
phase: `${event.type}-stale`,
|
|
135
149
|
taskStatus: task.status,
|
|
@@ -137,7 +151,7 @@ async function gate(bus, manager, event, expect) {
|
|
|
137
151
|
});
|
|
138
152
|
return null;
|
|
139
153
|
}
|
|
140
|
-
return { task };
|
|
154
|
+
return { task, expectedTask: taskGenerationGuard(task) };
|
|
141
155
|
}
|
|
142
156
|
export function registerServerEventHandlers(bus, manager) {
|
|
143
157
|
const configured = manager.getReviewStore();
|
|
@@ -166,25 +180,225 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
166
180
|
.catch(err => console.error(`[ServerHandler] max_rounds clear agentId(${task.id}) failed:`, err));
|
|
167
181
|
}
|
|
168
182
|
}
|
|
169
|
-
async function putVerdictRound(task, agentId, kind, data) {
|
|
183
|
+
async function putVerdictRound(task, agentId, kind, data, opts = {}) {
|
|
184
|
+
let lastError;
|
|
185
|
+
for (let attempt = 1; attempt <= SERVER_HANDLER_ATTEMPTS; attempt++) {
|
|
186
|
+
try {
|
|
187
|
+
const stored = opts.expectedTask
|
|
188
|
+
? await manager.putReviewRoundIfCurrent(task.id, data, opts.expectedTask)
|
|
189
|
+
: (await reviewStore.putRound(task.id, data.phase, data), true);
|
|
190
|
+
if (!stored)
|
|
191
|
+
return false;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
lastError = err;
|
|
196
|
+
try {
|
|
197
|
+
const stored = await reviewStore.getRound(task.id, data.phase, data.round);
|
|
198
|
+
if (stored && JSON.stringify(stored) === JSON.stringify(data))
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch (readBackError) {
|
|
202
|
+
lastError = readBackError;
|
|
203
|
+
}
|
|
204
|
+
if (attempt < SERVER_HANDLER_ATTEMPTS)
|
|
205
|
+
await waitForHandlerRetry(attempt);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return holdConsumedSignal(task, agentId, kind, 'verdict-store-failed', `server-${data.phase}-verdict-store-failed`, {
|
|
209
|
+
round: data.round,
|
|
210
|
+
error: lastError instanceof Error ? lastError.message : String(lastError),
|
|
211
|
+
}, opts.onSideEffect);
|
|
212
|
+
}
|
|
213
|
+
async function holdConsumedSignal(task, agentId, kind, reason, failurePhase, data = {}, onSideEffect) {
|
|
214
|
+
if (!SERVER_SIGNAL_KINDS.has(kind)) {
|
|
215
|
+
throw new Error(`unsupported consumed server signal kind: ${kind}`);
|
|
216
|
+
}
|
|
217
|
+
const phase = kind.startsWith('spec-') ? 'spec' : 'code';
|
|
218
|
+
const round = phase === 'spec' ? (task.specReviewRound ?? 0) : task.reviewRound;
|
|
219
|
+
const held = await manager.holdConsumedServerSignal(task, agentId, kind, { phase, round, reason, failurePhase });
|
|
220
|
+
if (!held)
|
|
221
|
+
return false;
|
|
222
|
+
onSideEffect?.();
|
|
223
|
+
await emitIntervention(bus, held, {
|
|
224
|
+
phase: failurePhase,
|
|
225
|
+
...data,
|
|
226
|
+
successorToken: held.signalToken,
|
|
227
|
+
});
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
async function readResponseWithRetry(task, dev) {
|
|
231
|
+
let result;
|
|
232
|
+
for (let attempt = 1; attempt <= SERVER_HANDLER_ATTEMPTS; attempt++) {
|
|
233
|
+
result = await transport().readResponseWithRaw(task, dev);
|
|
234
|
+
if (result.kind !== 'unknown' || attempt === SERVER_HANDLER_ATTEMPTS)
|
|
235
|
+
return result;
|
|
236
|
+
await waitForHandlerRetry(attempt);
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
async function runConsumedSignal(task, agentId, kind, handler) {
|
|
170
241
|
try {
|
|
171
|
-
await
|
|
172
|
-
return true;
|
|
242
|
+
await handler();
|
|
173
243
|
}
|
|
174
244
|
catch (err) {
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await manager.
|
|
245
|
+
await holdConsumedSignal(task, agentId, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-handler-failed`, { error: err instanceof Error ? err.message : String(err) });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function deleteFindingsOrHold(task, qa, kind, expectedTask) {
|
|
249
|
+
try {
|
|
250
|
+
return await manager.deleteReviewExchangeIfCurrent(task.id, qa, 'findings', expectedTask);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
await holdConsumedSignal(task, qa.id, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-findings-cleanup-failed`, { error: err instanceof Error ? err.message : String(err) });
|
|
181
254
|
return false;
|
|
182
255
|
}
|
|
183
256
|
}
|
|
184
|
-
async function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
257
|
+
async function deleteResponseOrHold(task, dev, kind, opts) {
|
|
258
|
+
try {
|
|
259
|
+
const deleted = await manager.deleteReviewExchangeIfCurrent(task.id, dev, 'response', opts.expectedTask ?? taskGenerationGuard(task));
|
|
260
|
+
if (deleted)
|
|
261
|
+
opts.onSideEffect?.();
|
|
262
|
+
return deleted;
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
return holdConsumedSignal(task, dev.id, kind, 'handler-failed', `server-${kind.startsWith('spec-') ? 'spec' : 'code'}-response-cleanup-failed`, { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function rejectServerResponse(task, dev, roundData, failure, onSideEffect) {
|
|
269
|
+
const phase = roundData.phase;
|
|
270
|
+
const signalKind = `${phase}-fixed`;
|
|
271
|
+
const sourceToken = task.signalToken;
|
|
272
|
+
if (!sourceToken)
|
|
273
|
+
return false;
|
|
274
|
+
const findingsDigest = reviewFindingsDigest(roundData.findings);
|
|
275
|
+
const missingFindingIds = [...(failure.missingFindingIds ?? [])].sort();
|
|
276
|
+
const unknownFindingIds = [...(failure.unknownFindingIds ?? [])].sort();
|
|
277
|
+
const schemaViolationCodes = [...(failure.schemaViolationCodes ?? [])].sort();
|
|
278
|
+
const failureSignature = serverResponseFailureSignature({
|
|
279
|
+
phase,
|
|
280
|
+
round: roundData.round,
|
|
281
|
+
findingsDigest,
|
|
282
|
+
reason: failure.reason,
|
|
283
|
+
missingFindingIds,
|
|
284
|
+
unknownFindingIds,
|
|
285
|
+
schemaViolationCodes,
|
|
286
|
+
});
|
|
287
|
+
const claimed = await manager.claimServerSignalRecovery(task, dev.id, signalKind, {
|
|
288
|
+
mode: 'classify-response',
|
|
289
|
+
phase,
|
|
290
|
+
round: roundData.round,
|
|
291
|
+
findingsDigest,
|
|
292
|
+
failureSignature,
|
|
293
|
+
reason: failure.reason,
|
|
294
|
+
failurePhase: failure.failurePhase,
|
|
295
|
+
missingFindingIds,
|
|
296
|
+
unknownFindingIds,
|
|
297
|
+
schemaViolationCodes,
|
|
298
|
+
...(failure.responseDigest ? { responseDigest: failure.responseDigest } : {}),
|
|
299
|
+
});
|
|
300
|
+
if (!claimed?.signalToken)
|
|
301
|
+
return false;
|
|
302
|
+
onSideEffect?.();
|
|
303
|
+
let recorded;
|
|
304
|
+
try {
|
|
305
|
+
recorded = await reviewStore.recordServerResponseFailure(task.id, phase, roundData.round, {
|
|
306
|
+
signalKind,
|
|
307
|
+
sourceToken,
|
|
308
|
+
successorToken: claimed.signalToken,
|
|
309
|
+
failureSignature,
|
|
310
|
+
reason: failure.reason,
|
|
311
|
+
missingFindingIds,
|
|
312
|
+
unknownFindingIds,
|
|
313
|
+
schemaViolationCodes,
|
|
314
|
+
createdAt: new Date().toISOString(),
|
|
315
|
+
...(failure.responseDigest ? { responseDigest: failure.responseDigest } : {}),
|
|
316
|
+
...(failure.rawResponse !== undefined ? { rawResponse: failure.rawResponse } : {}),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
321
|
+
const held = await manager.holdServerSignalRecovery(task.id, dev.id, claimed.signalToken, `server-${phase}-response-audit-store-failed`, `${message}. The response file was preserved and the old token is retired.`);
|
|
322
|
+
if (!held)
|
|
323
|
+
return false;
|
|
324
|
+
await emitIntervention(bus, held, {
|
|
325
|
+
phase: `server-${phase}-response-audit-store-failed`,
|
|
326
|
+
round: roundData.round,
|
|
327
|
+
error: message,
|
|
328
|
+
successorToken: claimed.signalToken,
|
|
329
|
+
});
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const nextMode = recorded.disposition === 'auto-correct' ? 'correct-response' : 'hold';
|
|
333
|
+
const finalized = await manager.setServerSignalRecoveryMode(task.id, claimed.signalToken, nextMode);
|
|
334
|
+
if (!finalized)
|
|
335
|
+
return false;
|
|
336
|
+
if (recorded.disposition !== 'auto-correct') {
|
|
337
|
+
const holdPhase = recorded.disposition === 'hold-repeated-signature'
|
|
338
|
+
? 'server-feedback-response-repeated'
|
|
339
|
+
: 'server-feedback-correction-limit';
|
|
340
|
+
const held = await manager.holdServerSignalRecovery(task.id, dev.id, claimed.signalToken, holdPhase, `The ${phase} response failed with ${failure.reason}; automatic correction stopped `
|
|
341
|
+
+ `(${recorded.disposition}). Rewrite it deliberately, then Restart or Cancel the task.`);
|
|
342
|
+
if (!held)
|
|
343
|
+
return false;
|
|
344
|
+
await emitIntervention(bus, held, {
|
|
345
|
+
phase: holdPhase,
|
|
346
|
+
reviewPhase: phase,
|
|
347
|
+
round: roundData.round,
|
|
348
|
+
reason: failure.reason,
|
|
349
|
+
disposition: recorded.disposition,
|
|
350
|
+
missingFindingIds,
|
|
351
|
+
unknownFindingIds,
|
|
352
|
+
findingsDigest,
|
|
353
|
+
successorToken: claimed.signalToken,
|
|
354
|
+
});
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const beforeCleanup = await manager.getTask(task.id);
|
|
358
|
+
const currentRecovery = beforeCleanup?.serverSignalRecovery;
|
|
359
|
+
const currentRound = phase === 'spec' ? beforeCleanup?.specReviewRound : beforeCleanup?.reviewRound;
|
|
360
|
+
if (!beforeCleanup
|
|
361
|
+
|| beforeCleanup.status !== task.status
|
|
362
|
+
|| beforeCleanup.phase !== task.phase
|
|
363
|
+
|| beforeCleanup.signalToken !== claimed.signalToken
|
|
364
|
+
|| currentRound !== roundData.round
|
|
365
|
+
|| currentRecovery?.mode !== 'correct-response'
|
|
366
|
+
|| currentRecovery.sourceToken !== sourceToken
|
|
367
|
+
|| currentRecovery.failureSignature !== failureSignature) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const deleted = await manager.deleteReviewExchangeIfCurrent(task.id, dev, 'response', taskGenerationGuard(beforeCleanup), currentRecovery);
|
|
372
|
+
if (!deleted)
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
377
|
+
const held = await manager.holdServerSignalRecovery(task.id, dev.id, claimed.signalToken, `server-${phase}-response-cleanup-failed`, `${message}. The rejected response is archived, but the live file could not be removed safely.`);
|
|
378
|
+
if (!held)
|
|
379
|
+
return false;
|
|
380
|
+
await emitIntervention(bus, held, {
|
|
381
|
+
phase: `server-${phase}-response-cleanup-failed`,
|
|
382
|
+
round: roundData.round,
|
|
383
|
+
error: message,
|
|
384
|
+
successorToken: claimed.signalToken,
|
|
385
|
+
});
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
await emitIntervention(bus, finalized, {
|
|
389
|
+
phase: failure.failurePhase,
|
|
390
|
+
round: roundData.round,
|
|
391
|
+
reason: failure.reason,
|
|
392
|
+
missingFindingIds,
|
|
393
|
+
unknownFindingIds,
|
|
394
|
+
schemaViolationCodes,
|
|
395
|
+
findingsDigest,
|
|
396
|
+
responseDigest: failure.responseDigest,
|
|
397
|
+
successorToken: claimed.signalToken,
|
|
398
|
+
...failure.details,
|
|
399
|
+
});
|
|
400
|
+
await manager.dispatchServerFeedbackCorrection(task.id, JSON.stringify(roundData.findings));
|
|
401
|
+
return false;
|
|
188
402
|
}
|
|
189
403
|
function resolveActiveQa(task) {
|
|
190
404
|
if (!task.qaAgentId)
|
|
@@ -192,28 +406,30 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
192
406
|
const recorded = manager.getAgentConfig(task.qaAgentId);
|
|
193
407
|
return recorded?.role === 'qa' ? recorded : undefined;
|
|
194
408
|
}
|
|
195
|
-
async function pauseForUnavailableQa(task, entryKind, phase) {
|
|
409
|
+
async function pauseForUnavailableQa(task, entryKind, phase, onSideEffect) {
|
|
196
410
|
if (!task.qaAgentId || resolveActiveQa(task))
|
|
197
411
|
return false;
|
|
198
|
-
await
|
|
199
|
-
if (task.agentId)
|
|
200
|
-
await rearmOrHold(task, task.agentId, entryKind);
|
|
412
|
+
await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', phase, { qaAgentId: task.qaAgentId }, onSideEffect);
|
|
201
413
|
return true;
|
|
202
414
|
}
|
|
203
|
-
async function autoApproveCode(task, entryKind = 'code-done') {
|
|
204
|
-
const afterDone = manager.
|
|
205
|
-
|
|
206
|
-
await manager.updateTask(task.id, { afterDone });
|
|
207
|
-
}
|
|
415
|
+
async function autoApproveCode(task, expectedTask, entryKind = 'code-done', onSideEffect) {
|
|
416
|
+
const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
|
|
417
|
+
onSideEffect?.();
|
|
208
418
|
if (afterDone === null) {
|
|
209
|
-
const ready = await manager.transitionTaskStatus(task.id, 'ready', {
|
|
210
|
-
|
|
211
|
-
|
|
419
|
+
const ready = await manager.transitionTaskStatus(task.id, 'ready', {
|
|
420
|
+
fromStatus: ['in_progress', 'fixing'],
|
|
421
|
+
...(expectedTask !== undefined ? { expectTask: expectedTask } : {}),
|
|
422
|
+
});
|
|
423
|
+
if (ready)
|
|
424
|
+
onSideEffect?.();
|
|
425
|
+
if (!ready) {
|
|
426
|
+
await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
|
|
427
|
+
}
|
|
212
428
|
return;
|
|
213
429
|
}
|
|
214
430
|
const dev = manager.getAgentConfig(task.agentId);
|
|
215
431
|
if (!dev) {
|
|
216
|
-
await
|
|
432
|
+
await holdConsumedSignal(task, task.agentId, entryKind, 'fix-no-dev-agent', 'server-code-auto-approve-no-dev-agent', {}, onSideEffect);
|
|
217
433
|
return;
|
|
218
434
|
}
|
|
219
435
|
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
@@ -222,73 +438,75 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
222
438
|
reviewHeadAnchorSha = await transport().readHeadSha(dev);
|
|
223
439
|
}
|
|
224
440
|
catch (err) {
|
|
225
|
-
await
|
|
226
|
-
phase: 'server-code-auto-approve-head-capture-failed',
|
|
227
|
-
error: err instanceof Error ? err.message : String(err),
|
|
228
|
-
});
|
|
229
|
-
await manager.setupPhaseSignal(task.id, task.agentId, entryKind, { skipSnapshot: true });
|
|
441
|
+
await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-head-capture-failed', { error: err instanceof Error ? err.message : String(err) }, onSideEffect);
|
|
230
442
|
return;
|
|
231
443
|
}
|
|
232
|
-
const approved = await manager.transitionTaskStatus(task.id, 'approved', {
|
|
444
|
+
const approved = await manager.transitionTaskStatus(task.id, 'approved', {
|
|
445
|
+
fromStatus: ['in_progress', 'fixing'],
|
|
446
|
+
...(expectedTask !== undefined ? { expectTask: expectedTask } : {}),
|
|
447
|
+
}, { reviewHeadAnchorSha });
|
|
448
|
+
if (approved)
|
|
449
|
+
onSideEffect?.();
|
|
233
450
|
if (!approved) {
|
|
234
|
-
await
|
|
451
|
+
await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
|
|
235
452
|
return;
|
|
236
453
|
}
|
|
237
|
-
await manager.dispatchServerAfterDone(task.id, afterDone);
|
|
454
|
+
const dispatched = await manager.dispatchServerAfterDone(task.id, afterDone);
|
|
455
|
+
if (!dispatched) {
|
|
456
|
+
await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-publish-dispatch-failed', {}, onSideEffect);
|
|
457
|
+
}
|
|
238
458
|
}
|
|
239
459
|
function specApprovalIsHuman(task) {
|
|
240
460
|
return task.researchAgentId !== undefined
|
|
241
461
|
|| manager.getProjectConfig(task.projectId)?.specApproval === 'human';
|
|
242
462
|
}
|
|
243
|
-
async function advanceApprovedSpec(task, failurePhase) {
|
|
463
|
+
async function advanceApprovedSpec(task, failurePhase, expectedTask) {
|
|
244
464
|
try {
|
|
245
465
|
const result = specApprovalIsHuman(task)
|
|
246
|
-
? await manager.parkTaskAtSpecReady(task.id
|
|
247
|
-
|
|
466
|
+
? await manager.parkTaskAtSpecReady(task.id, {
|
|
467
|
+
...(expectedTask !== undefined ? { expectedTask } : {}),
|
|
468
|
+
})
|
|
469
|
+
: await manager.transitionToCodePhase(task.id, expectedTask);
|
|
248
470
|
if (!result) {
|
|
249
|
-
await
|
|
471
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase);
|
|
250
472
|
}
|
|
251
473
|
}
|
|
252
474
|
catch (err) {
|
|
253
|
-
await
|
|
254
|
-
phase: failurePhase,
|
|
255
|
-
error: err instanceof Error ? err.message : String(err),
|
|
256
|
-
});
|
|
475
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase, { error: err instanceof Error ? err.message : String(err) });
|
|
257
476
|
}
|
|
258
477
|
}
|
|
259
|
-
async function autoApproveSpec(task) {
|
|
478
|
+
async function autoApproveSpec(task, expectedTask, onSideEffect) {
|
|
260
479
|
try {
|
|
261
|
-
const result = await manager.transitionToCodePhase(task.id);
|
|
480
|
+
const result = await manager.transitionToCodePhase(task.id, expectedTask);
|
|
481
|
+
if (result)
|
|
482
|
+
onSideEffect?.();
|
|
262
483
|
if (!result) {
|
|
263
|
-
await
|
|
484
|
+
await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-auto-approve-transition-failed', {}, onSideEffect);
|
|
264
485
|
return false;
|
|
265
486
|
}
|
|
266
487
|
return true;
|
|
267
488
|
}
|
|
268
489
|
catch (err) {
|
|
269
|
-
await
|
|
270
|
-
phase: 'server-spec-auto-approve-transition-failed',
|
|
271
|
-
error: err instanceof Error ? err.message : String(err),
|
|
272
|
-
});
|
|
490
|
+
await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-auto-approve-transition-failed', { error: err instanceof Error ? err.message : String(err) }, onSideEffect);
|
|
273
491
|
return false;
|
|
274
492
|
}
|
|
275
493
|
}
|
|
276
494
|
async function prepareAndDispatchCodeReview(task, opts) {
|
|
277
495
|
const dev = manager.getAgentConfig(task.agentId);
|
|
278
496
|
if (!dev) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
282
|
-
await rearmOrHold(task, task.agentId, kind);
|
|
283
|
-
}
|
|
284
|
-
return false;
|
|
497
|
+
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
498
|
+
return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-code-review-no-dev-agent', {}, opts.onSideEffect);
|
|
285
499
|
}
|
|
286
500
|
const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
287
|
-
const nextRound = task.reviewRound + 1;
|
|
501
|
+
const nextRound = opts.bumpRound === false ? task.reviewRound : task.reviewRound + 1;
|
|
288
502
|
if (nextRound > cap) {
|
|
289
|
-
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
503
|
+
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
504
|
+
fromStatus: ['in_progress', 'fixing'],
|
|
505
|
+
...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
|
|
506
|
+
});
|
|
290
507
|
if (!capResult)
|
|
291
508
|
return false;
|
|
509
|
+
opts.onSideEffect?.();
|
|
292
510
|
const paused = capResult.task;
|
|
293
511
|
if (paused.qaAgentId)
|
|
294
512
|
await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
|
|
@@ -303,49 +521,50 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
303
521
|
});
|
|
304
522
|
return false;
|
|
305
523
|
}
|
|
306
|
-
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
307
524
|
let content;
|
|
308
525
|
let reviewHeadAnchorSha;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (!
|
|
312
|
-
throw new
|
|
313
|
-
reviewHeadAnchorSha = content.headSha;
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
await emitIntervention(bus, task, {
|
|
317
|
-
phase: 'server-code-content-read-failed',
|
|
318
|
-
error: err instanceof Error ? err.message : String(err),
|
|
319
|
-
});
|
|
320
|
-
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
321
|
-
await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
const rearmEntry = async () => {
|
|
325
|
-
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
326
|
-
await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
|
|
327
|
-
};
|
|
328
|
-
try {
|
|
329
|
-
const violation = await manager.findLineageViolation(task.id, content.baseSha);
|
|
330
|
-
if (violation) {
|
|
331
|
-
await emitIntervention(bus, task, {
|
|
332
|
-
phase: 'server-code-lineage-violation',
|
|
333
|
-
offendingTaskId: violation.taskId,
|
|
334
|
-
offendingBranch: violation.branch,
|
|
335
|
-
offendingSha: violation.sha,
|
|
336
|
-
note: 'The task branch embeds another active task\'s commits — reviewing it would leak foreign work into this task. Have the dev rebase onto origin/HEAD and re-emit the signal.',
|
|
337
|
-
});
|
|
338
|
-
await rearmEntry();
|
|
339
|
-
return false;
|
|
526
|
+
if (opts.bumpRound === false) {
|
|
527
|
+
const stored = await reviewStore.getRound(task.id, 'code', nextRound);
|
|
528
|
+
if (!stored || stored.phase !== 'code' || !stored.headSha) {
|
|
529
|
+
throw new Error(`Task ${task.id} has no persisted code review round ${nextRound} to redispatch`);
|
|
340
530
|
}
|
|
531
|
+
content = {
|
|
532
|
+
content: stored.content,
|
|
533
|
+
...(stored.diffstat ? { diffstat: stored.diffstat } : {}),
|
|
534
|
+
...(stored.baseSha ? { baseSha: stored.baseSha } : {}),
|
|
535
|
+
headSha: stored.headSha,
|
|
536
|
+
...(stored.headTree ? { headTree: stored.headTree } : {}),
|
|
537
|
+
};
|
|
538
|
+
reviewHeadAnchorSha = stored.headSha;
|
|
341
539
|
}
|
|
342
|
-
|
|
343
|
-
await
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
540
|
+
else {
|
|
541
|
+
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
542
|
+
try {
|
|
543
|
+
content = await transport().readContent(task, dev, 'code');
|
|
544
|
+
if (!content.headSha)
|
|
545
|
+
throw new ReviewExchangeError('head-failed', 'review head missing from captured content');
|
|
546
|
+
reviewHeadAnchorSha = content.headSha;
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
550
|
+
return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-content-read-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const violation = await manager.findLineageViolation(task.id, content.baseSha);
|
|
554
|
+
if (violation) {
|
|
555
|
+
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
556
|
+
return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-lineage-violation', {
|
|
557
|
+
offendingTaskId: violation.taskId,
|
|
558
|
+
offendingBranch: violation.branch,
|
|
559
|
+
offendingSha: violation.sha,
|
|
560
|
+
note: 'The task branch embeds another active task\'s commits — reviewing it would leak foreign work into this task. Have the dev rebase onto origin/HEAD and re-emit the signal.',
|
|
561
|
+
}, opts.onSideEffect);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
566
|
+
return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-lineage-check-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
|
|
567
|
+
}
|
|
349
568
|
}
|
|
350
569
|
const round = {
|
|
351
570
|
round: nextRound,
|
|
@@ -357,21 +576,7 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
357
576
|
...(content.headTree ? { headTree: content.headTree } : {}),
|
|
358
577
|
startedAt: new Date().toISOString(),
|
|
359
578
|
};
|
|
360
|
-
const putEntryRound =
|
|
361
|
-
try {
|
|
362
|
-
await reviewStore.putRound(task.id, 'code', data);
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
catch (err) {
|
|
366
|
-
await emitIntervention(bus, task, {
|
|
367
|
-
phase: 'server-code-round-store-failed',
|
|
368
|
-
error: err instanceof Error ? err.message : String(err),
|
|
369
|
-
});
|
|
370
|
-
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
371
|
-
await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
};
|
|
579
|
+
const putEntryRound = (data) => putVerdictRound(task, task.agentId, task.status === 'fixing' ? 'code-fixed' : 'code-done', data, opts);
|
|
375
580
|
const resolveRecheckInterdiff = async () => {
|
|
376
581
|
if (!opts.recheck)
|
|
377
582
|
return undefined;
|
|
@@ -385,12 +590,13 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
385
590
|
return undefined;
|
|
386
591
|
}
|
|
387
592
|
};
|
|
388
|
-
if (!(await putEntryRound(round)))
|
|
593
|
+
if (opts.bumpRound !== false && !(await putEntryRound(round)))
|
|
389
594
|
return false;
|
|
390
595
|
const interdiff = await resolveRecheckInterdiff();
|
|
391
596
|
const dispatched = await manager.dispatchServerReviewToQa(task.id, {
|
|
392
597
|
phase: 'code',
|
|
393
598
|
recheck: opts.recheck,
|
|
599
|
+
...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
|
|
394
600
|
content: content.content,
|
|
395
601
|
reviewHeadAnchorSha,
|
|
396
602
|
...(content.baseSha ? { baseSha: content.baseSha } : {}),
|
|
@@ -399,8 +605,15 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
399
605
|
...(content.diffstat ? { diffstat: content.diffstat } : {}),
|
|
400
606
|
...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
|
|
401
607
|
...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
|
|
608
|
+
callerOwnsConsumedSignalFailure: true,
|
|
609
|
+
...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
|
|
610
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
611
|
+
...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
|
|
402
612
|
});
|
|
403
|
-
|
|
613
|
+
if (dispatched)
|
|
614
|
+
return true;
|
|
615
|
+
const kind = task.status === 'fixing' ? 'code-fixed' : 'code-done';
|
|
616
|
+
return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-code-review-dispatch-failed', {}, opts.onSideEffect);
|
|
404
617
|
}
|
|
405
618
|
async function dispatchLegacyBatch(taskId, batches, index, diffstat, opts) {
|
|
406
619
|
const text = batches[index].map(f => f.text).join('\n');
|
|
@@ -413,6 +626,9 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
413
626
|
batch: { index, total: batches.length },
|
|
414
627
|
...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
|
|
415
628
|
...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
|
|
629
|
+
callerOwnsConsumedSignalFailure: true,
|
|
630
|
+
...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
|
|
631
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
416
632
|
});
|
|
417
633
|
return dispatched !== null;
|
|
418
634
|
}
|
|
@@ -423,137 +639,135 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
423
639
|
const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'code', requireServerMode: true });
|
|
424
640
|
if (!gated)
|
|
425
641
|
return;
|
|
426
|
-
const { task } = gated;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
642
|
+
const { task, expectedTask } = gated;
|
|
643
|
+
await runConsumedSignal(task, task.agentId, 'code-done', async () => {
|
|
644
|
+
if (await pauseForUnavailableQa(task, 'code-done', 'server-code-qa-unavailable'))
|
|
645
|
+
return;
|
|
646
|
+
if (!task.qaAgentId) {
|
|
647
|
+
await autoApproveCode(task, expectedTask);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
await prepareAndDispatchCodeReview(task, { recheck: false, expectedTask, settlePermit: settlePermitOf(event) });
|
|
651
|
+
});
|
|
434
652
|
});
|
|
435
653
|
bus.on('server.code.review.submitted', async (event) => {
|
|
436
654
|
const gated = await gate(bus, manager, event, { status: 'review', phase: 'code', requireServerMode: true });
|
|
437
655
|
if (!gated)
|
|
438
656
|
return;
|
|
439
|
-
const { task } = gated;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
await rearmOrHold(task, task.qaAgentId, 'code-reviewed');
|
|
445
|
-
}
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
const round = Math.max(task.reviewRound, 1);
|
|
449
|
-
const roundData = await reviewStore.getRound(task.id, 'code', round);
|
|
450
|
-
if (!roundData) {
|
|
451
|
-
await emitIntervention(bus, task, { phase: 'server-code-review-round-missing', round });
|
|
452
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
await manager.refreshWorkdirCacheFor(qa.id);
|
|
456
|
-
let findings;
|
|
457
|
-
try {
|
|
458
|
-
findings = await transport().readFindings(task, qa);
|
|
459
|
-
}
|
|
460
|
-
catch (err) {
|
|
461
|
-
const reason = err instanceof ReviewExchangeError ? err.reason : 'unknown';
|
|
462
|
-
await emitIntervention(bus, task, {
|
|
463
|
-
phase: 'server-code-findings-invalid',
|
|
464
|
-
reason,
|
|
465
|
-
error: err instanceof Error ? err.message : String(err),
|
|
466
|
-
});
|
|
467
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
let current = roundData;
|
|
471
|
-
if (findings === null) {
|
|
472
|
-
const alreadyStored = task.batchTotal !== undefined
|
|
473
|
-
? (roundData.batchFindings?.length ?? 0) > (task.batchIndex ?? 0)
|
|
474
|
-
: roundData.findings !== undefined;
|
|
475
|
-
if (!alreadyStored) {
|
|
476
|
-
await emitIntervention(bus, task, { phase: 'server-code-findings-missing', round });
|
|
477
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
|
|
657
|
+
const { task, expectedTask } = gated;
|
|
658
|
+
await runConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', async () => {
|
|
659
|
+
const qa = manager.getAgentConfig(task.qaAgentId ?? '');
|
|
660
|
+
if (!qa) {
|
|
661
|
+
await holdConsumedSignal(task, task.qaAgentId ?? '', 'code-reviewed', 'handler-failed', 'server-code-review-no-qa-agent');
|
|
478
662
|
return;
|
|
479
663
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
round,
|
|
485
|
-
payloadRound: findings.round,
|
|
486
|
-
});
|
|
487
|
-
await transport().deleteFindings(qa);
|
|
488
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'code-reviewed', { skipSnapshot: true });
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
else if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
|
|
492
|
-
const batchFindings = [...(roundData.batchFindings ?? [])];
|
|
493
|
-
batchFindings[task.batchIndex] = findings;
|
|
494
|
-
current = { ...roundData, batchFindings };
|
|
495
|
-
if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current)))
|
|
664
|
+
const round = Math.max(task.reviewRound, 1);
|
|
665
|
+
const roundData = await reviewStore.getRound(task.id, 'code', round);
|
|
666
|
+
if (!roundData) {
|
|
667
|
+
await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-review-round-missing', { round });
|
|
496
668
|
return;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
669
|
+
}
|
|
670
|
+
await manager.refreshWorkdirCacheFor(qa.id);
|
|
671
|
+
let findings;
|
|
672
|
+
try {
|
|
673
|
+
findings = await transport().readFindings(task, qa);
|
|
674
|
+
}
|
|
675
|
+
catch (err) {
|
|
676
|
+
const reason = err instanceof ReviewExchangeError ? err.reason : 'unknown';
|
|
677
|
+
await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-findings-invalid', { reason, error: err instanceof Error ? err.message : String(err) });
|
|
502
678
|
return;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
679
|
+
}
|
|
680
|
+
let current = roundData;
|
|
681
|
+
if (findings === null) {
|
|
682
|
+
const alreadyStored = task.batchTotal !== undefined
|
|
683
|
+
? (roundData.batchFindings?.length ?? 0) > (task.batchIndex ?? 0)
|
|
684
|
+
: roundData.findings !== undefined;
|
|
685
|
+
if (!alreadyStored) {
|
|
686
|
+
await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-findings-missing', { round });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else if (findings.round !== round) {
|
|
691
|
+
await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-findings-round-mismatch', { round, payloadRound: findings.round });
|
|
515
692
|
return;
|
|
516
693
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
});
|
|
525
|
-
if (!stored)
|
|
694
|
+
else if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
|
|
695
|
+
const batchFindings = [...(roundData.batchFindings ?? [])];
|
|
696
|
+
batchFindings[task.batchIndex] = findings;
|
|
697
|
+
current = { ...roundData, batchFindings };
|
|
698
|
+
if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current, { expectedTask })))
|
|
699
|
+
return;
|
|
700
|
+
if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed', expectedTask)))
|
|
526
701
|
return;
|
|
527
702
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
703
|
+
else {
|
|
704
|
+
current = { ...roundData, findings, completedAt: new Date().toISOString() };
|
|
705
|
+
if (!(await putVerdictRound(task, qa.id, 'code-reviewed', current, { expectedTask })))
|
|
706
|
+
return;
|
|
707
|
+
if (!(await deleteFindingsOrHold(task, qa, 'code-reviewed', expectedTask)))
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const isRecheck = round > 1;
|
|
711
|
+
if (task.batchTotal !== undefined && task.batchIndex !== undefined) {
|
|
712
|
+
const nextIndex = task.batchIndex + 1;
|
|
713
|
+
if (nextIndex < task.batchTotal) {
|
|
714
|
+
const prev = isRecheck ? await reviewStore.getRound(task.id, 'code', round - 1) : null;
|
|
715
|
+
const dispatched = await dispatchLegacyBatch(task.id, rebuildLegacyBatches(current), nextIndex, current.diffstat, {
|
|
716
|
+
recheck: isRecheck,
|
|
717
|
+
...(prev?.findings ? { priorFindingsJson: JSON.stringify(prev.findings) } : {}),
|
|
718
|
+
...(prev?.response ? { priorResponseJson: JSON.stringify(prev.response) } : {}),
|
|
719
|
+
expectedTask,
|
|
720
|
+
settlePermit: settlePermitOf(event),
|
|
721
|
+
});
|
|
722
|
+
if (!dispatched) {
|
|
723
|
+
await holdConsumedSignal(task, qa.id, 'code-reviewed', 'handler-failed', 'server-code-review-continuation-dispatch-failed');
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const aggregated = current.findings
|
|
728
|
+
?? aggregateBatchFindings((current.batchFindings ?? []).filter(Boolean), round);
|
|
729
|
+
if (!current.findings) {
|
|
730
|
+
const stored = await putVerdictRound(task, qa.id, 'code-reviewed', {
|
|
731
|
+
...current,
|
|
732
|
+
findings: aggregated,
|
|
733
|
+
completedAt: new Date().toISOString(),
|
|
734
|
+
}, { expectedTask });
|
|
735
|
+
if (!stored)
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const cleared = await manager.updateTaskIfStatus(task.id, expectedTask.status, { batchIndex: undefined, batchTotal: undefined }, expectedTask);
|
|
739
|
+
if (!cleared)
|
|
740
|
+
return;
|
|
741
|
+
await routeCodeVerdict({ ...task, batchIndex: undefined, batchTotal: undefined }, aggregated, expectedTask);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
await routeCodeVerdict(task, current.findings, expectedTask);
|
|
745
|
+
});
|
|
533
746
|
});
|
|
534
|
-
async function routeCodeVerdict(task, findings) {
|
|
747
|
+
async function routeCodeVerdict(task, findings, expectedTask) {
|
|
535
748
|
if (findings.verdict === 'approve') {
|
|
536
|
-
const afterDone = manager.
|
|
537
|
-
if (task.afterDone === undefined) {
|
|
538
|
-
await manager.updateTask(task.id, { afterDone });
|
|
539
|
-
}
|
|
749
|
+
const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
|
|
540
750
|
if (afterDone === null) {
|
|
541
|
-
const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['review'] });
|
|
542
|
-
if (!ready)
|
|
543
|
-
await
|
|
751
|
+
const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['review'], expectTask: expectedTask });
|
|
752
|
+
if (!ready) {
|
|
753
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-ready-transition-failed');
|
|
754
|
+
}
|
|
544
755
|
return;
|
|
545
756
|
}
|
|
546
|
-
const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'] });
|
|
757
|
+
const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'], expectTask: expectedTask });
|
|
547
758
|
if (!approved) {
|
|
548
|
-
await
|
|
759
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-approved-transition-failed');
|
|
549
760
|
return;
|
|
550
761
|
}
|
|
551
|
-
await manager.dispatchServerAfterDone(task.id, afterDone);
|
|
762
|
+
const dispatched = await manager.dispatchServerAfterDone(task.id, afterDone);
|
|
763
|
+
if (!dispatched) {
|
|
764
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-publish-dispatch-failed');
|
|
765
|
+
}
|
|
552
766
|
return;
|
|
553
767
|
}
|
|
554
768
|
const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
555
769
|
if (task.reviewRound >= cap) {
|
|
556
|
-
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'] });
|
|
770
|
+
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'], expectTask: expectedTask });
|
|
557
771
|
if (!capResult)
|
|
558
772
|
return;
|
|
559
773
|
const paused = capResult.task;
|
|
@@ -571,173 +785,217 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
571
785
|
});
|
|
572
786
|
return;
|
|
573
787
|
}
|
|
574
|
-
await manager.dispatchServerFixToDev(task.id, JSON.stringify(findings));
|
|
788
|
+
const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(findings), { callerOwnsConsumedSignalFailure: true, expectedTask });
|
|
789
|
+
if (!dispatched) {
|
|
790
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-fix-dispatch-failed');
|
|
791
|
+
}
|
|
575
792
|
}
|
|
576
|
-
async function recheckCodeOrAutoApprove(task, priorFindingsJson, priorResponseJson) {
|
|
577
|
-
if (await pauseForUnavailableQa(task, 'code-fixed', 'server-code-qa-unavailable-after-fix')) {
|
|
793
|
+
async function recheckCodeOrAutoApprove(task, priorFindingsJson, priorResponseJson, opts = {}) {
|
|
794
|
+
if (await pauseForUnavailableQa(task, 'code-fixed', 'server-code-qa-unavailable-after-fix', opts.onSideEffect)) {
|
|
578
795
|
return false;
|
|
579
796
|
}
|
|
580
797
|
if (!task.qaAgentId) {
|
|
581
|
-
await autoApproveCode(task, 'code-fixed');
|
|
798
|
+
await autoApproveCode(task, opts.expectedTask, 'code-fixed', opts.onSideEffect);
|
|
582
799
|
return true;
|
|
583
800
|
}
|
|
584
|
-
return prepareAndDispatchCodeReview(task, {
|
|
801
|
+
return prepareAndDispatchCodeReview(task, {
|
|
802
|
+
recheck: true,
|
|
803
|
+
priorFindingsJson,
|
|
804
|
+
priorResponseJson,
|
|
805
|
+
...opts,
|
|
806
|
+
});
|
|
585
807
|
}
|
|
586
|
-
async function runCodeFixSubmission(task) {
|
|
808
|
+
async function runCodeFixSubmission(task, opts = {}) {
|
|
587
809
|
const dev = manager.getAgentConfig(task.agentId);
|
|
588
810
|
if (!dev) {
|
|
589
|
-
|
|
590
|
-
if (task.agentId) {
|
|
591
|
-
await rearmOrHold(task, task.agentId, 'code-fixed');
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
811
|
+
return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-no-dev-agent', 'server-code-fix-no-dev-agent', {}, opts.onSideEffect);
|
|
594
812
|
}
|
|
595
813
|
const round = Math.max(task.reviewRound, 1);
|
|
596
814
|
const roundData = await reviewStore.getRound(task.id, 'code', round);
|
|
597
815
|
if (!roundData?.findings) {
|
|
598
|
-
|
|
599
|
-
await manager.setupPhaseSignal(task.id, task.agentId, 'code-fixed', { skipSnapshot: true });
|
|
600
|
-
return false;
|
|
816
|
+
return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-findings-missing', 'server-code-fix-findings-missing', { round }, opts.onSideEffect);
|
|
601
817
|
}
|
|
602
818
|
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
603
|
-
let
|
|
819
|
+
let readResult;
|
|
604
820
|
try {
|
|
605
|
-
|
|
821
|
+
readResult = await readResponseWithRetry(task, dev);
|
|
606
822
|
}
|
|
607
823
|
catch (err) {
|
|
608
|
-
|
|
609
|
-
|
|
824
|
+
return holdConsumedSignal(task, dev.id, 'code-fixed', 'response-read-failed', 'server-code-response-read-failed', {
|
|
825
|
+
reason: err instanceof ReviewExchangeError ? err.reason : 'read-failed',
|
|
610
826
|
error: err instanceof Error ? err.message : String(err),
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
827
|
+
}, opts.onSideEffect);
|
|
828
|
+
}
|
|
829
|
+
if (readResult.kind === 'unknown') {
|
|
830
|
+
return holdConsumedSignal(task, dev.id, 'code-fixed', 'response-read-failed', 'server-code-response-read-failed', { reason: readResult.error.reason, error: readResult.error.message }, opts.onSideEffect);
|
|
831
|
+
}
|
|
832
|
+
if (readResult.kind === 'invalid') {
|
|
833
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
834
|
+
reason: 'response-invalid',
|
|
835
|
+
failurePhase: 'server-code-response-invalid',
|
|
836
|
+
rawResponse: readResult.raw,
|
|
837
|
+
responseDigest: readResult.responseDigest,
|
|
838
|
+
schemaViolationCodes: readResult.schemaViolationCodes,
|
|
839
|
+
details: { error: readResult.error.message },
|
|
840
|
+
}, opts.onSideEffect);
|
|
841
|
+
}
|
|
842
|
+
if (readResult.kind === 'absent') {
|
|
616
843
|
if (roundData.response === undefined) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
844
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
845
|
+
reason: 'response-missing',
|
|
846
|
+
failurePhase: 'server-code-response-missing',
|
|
847
|
+
}, opts.onSideEffect);
|
|
620
848
|
}
|
|
621
|
-
return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response));
|
|
849
|
+
return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response), opts);
|
|
622
850
|
}
|
|
851
|
+
const { response } = readResult;
|
|
623
852
|
if (response.round !== round) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
round,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
853
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
854
|
+
reason: 'round-mismatch',
|
|
855
|
+
failurePhase: 'server-code-response-round-mismatch',
|
|
856
|
+
rawResponse: readResult.raw,
|
|
857
|
+
responseDigest: readResult.responseDigest,
|
|
858
|
+
details: { payloadRound: response.round },
|
|
859
|
+
}, opts.onSideEffect);
|
|
860
|
+
}
|
|
861
|
+
if (response.token !== task.signalToken) {
|
|
862
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
863
|
+
reason: 'token-mismatch',
|
|
864
|
+
failurePhase: 'server-code-response-token-mismatch',
|
|
865
|
+
rawResponse: readResult.raw,
|
|
866
|
+
responseDigest: readResult.responseDigest,
|
|
867
|
+
}, opts.onSideEffect);
|
|
868
|
+
}
|
|
869
|
+
const findingsDigest = reviewFindingsDigest(roundData.findings);
|
|
870
|
+
if (response.findingsDigest !== findingsDigest) {
|
|
871
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
872
|
+
reason: 'findings-digest-mismatch',
|
|
873
|
+
failurePhase: 'server-code-response-findings-digest-mismatch',
|
|
874
|
+
rawResponse: readResult.raw,
|
|
875
|
+
responseDigest: readResult.responseDigest,
|
|
876
|
+
}, opts.onSideEffect);
|
|
632
877
|
}
|
|
633
878
|
const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
|
|
634
879
|
if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
880
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
881
|
+
reason: 'coverage-gap',
|
|
882
|
+
failurePhase: 'server-code-response-coverage-gap',
|
|
883
|
+
rawResponse: readResult.raw,
|
|
884
|
+
responseDigest: readResult.responseDigest,
|
|
638
885
|
missingFindingIds: gaps.missing,
|
|
639
886
|
unknownFindingIds: gaps.unknown,
|
|
640
|
-
});
|
|
641
|
-
await manager.setupPhaseSignal(task.id, dev.id, 'code-fixed', { skipSnapshot: true });
|
|
642
|
-
return false;
|
|
887
|
+
}, opts.onSideEffect);
|
|
643
888
|
}
|
|
644
|
-
if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response })))
|
|
889
|
+
if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response }, opts)))
|
|
645
890
|
return false;
|
|
646
|
-
await
|
|
647
|
-
|
|
891
|
+
if (!(await deleteResponseOrHold(task, dev, 'code-fixed', opts)))
|
|
892
|
+
return false;
|
|
893
|
+
return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(response), opts);
|
|
648
894
|
}
|
|
649
895
|
bus.on('server.code.fix.submitted', async (event) => {
|
|
650
896
|
const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'code', requireServerMode: true });
|
|
651
897
|
if (!gated)
|
|
652
898
|
return;
|
|
653
|
-
await
|
|
899
|
+
await runConsumedSignal(gated.task, gated.task.agentId, 'code-fixed', async () => {
|
|
900
|
+
await runCodeFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
|
|
901
|
+
});
|
|
654
902
|
});
|
|
655
903
|
bus.on('server.code.published', async (event) => {
|
|
656
904
|
const gated = await gate(bus, manager, event, { status: 'approved', phase: 'code', requireServerMode: true });
|
|
657
905
|
if (!gated)
|
|
658
906
|
return;
|
|
659
907
|
const { task } = gated;
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
908
|
+
await runConsumedSignal(task, task.agentId, 'code-ready', async () => {
|
|
909
|
+
const prNumber = typeof event.data?.prNumber === 'number' ? event.data.prNumber : task.prNumber;
|
|
910
|
+
if (prNumber === undefined && manager.resolveAfterDone(task) === 'pr') {
|
|
911
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-missing-pr-number');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const dev = manager.getAgentConfig(task.agentId);
|
|
915
|
+
if (!dev) {
|
|
916
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'fix-no-dev-agent', 'server-code-published-no-dev-agent');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
920
|
+
let publishedHead;
|
|
921
|
+
try {
|
|
922
|
+
publishedHead = await transport().readHeadSha(dev);
|
|
923
|
+
}
|
|
924
|
+
catch (err) {
|
|
925
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-capture-failed', { error: err instanceof Error ? err.message : String(err) });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (publishedHead !== task.reviewHeadAnchorSha) {
|
|
929
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-mismatch', {
|
|
930
|
+
publishedHead,
|
|
931
|
+
reviewedHead: task.reviewHeadAnchorSha ?? null,
|
|
672
932
|
});
|
|
673
|
-
await manager.setupPhaseSignal(task.id, task.agentId, 'code-ready', { skipSnapshot: true });
|
|
674
933
|
return;
|
|
675
934
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
const ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['approved'] });
|
|
716
|
-
if (!ready)
|
|
717
|
-
await emitIntervention(bus, task, { phase: 'server-code-published-transition-failed' });
|
|
935
|
+
let verifiedBaseBranch;
|
|
936
|
+
if (prNumber !== undefined) {
|
|
937
|
+
const verified = await manager.verifyPaneSignalPrNumber(task.id, prNumber);
|
|
938
|
+
if (!verified) {
|
|
939
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-pr-number-unverified', { prNumber });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (verified.headSha !== publishedHead) {
|
|
943
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-pr-head-mismatch', {
|
|
944
|
+
prNumber,
|
|
945
|
+
remoteHead: verified.headSha,
|
|
946
|
+
publishedHead,
|
|
947
|
+
reviewedHead: task.reviewHeadAnchorSha,
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
verifiedBaseBranch = verified.targetBranch;
|
|
952
|
+
}
|
|
953
|
+
let ready;
|
|
954
|
+
try {
|
|
955
|
+
ready = await manager.transitionTaskStatus(task.id, 'ready', { fromStatus: ['approved'], expectTask: gated.expectedTask }, {
|
|
956
|
+
...(prNumber !== undefined ? { prNumber } : {}),
|
|
957
|
+
...(prNumber !== undefined
|
|
958
|
+
&& task.baseBranch === undefined
|
|
959
|
+
&& verifiedBaseBranch !== undefined
|
|
960
|
+
? { baseBranch: verifiedBaseBranch }
|
|
961
|
+
: {}),
|
|
962
|
+
latestHeadSha: publishedHead,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-head-capture-failed', { error: err instanceof Error ? err.message : String(err) });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (!ready) {
|
|
970
|
+
await holdConsumedSignal(task, task.agentId, 'code-ready', 'handler-failed', 'server-code-published-transition-failed');
|
|
971
|
+
}
|
|
972
|
+
});
|
|
718
973
|
});
|
|
719
974
|
bus.on('server.spec.ready', async (event) => {
|
|
720
975
|
const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'spec', requireServerMode: false });
|
|
721
976
|
if (!gated)
|
|
722
977
|
return;
|
|
723
|
-
await
|
|
978
|
+
await runConsumedSignal(gated.task, gated.task.agentId, 'spec-done', async () => {
|
|
979
|
+
await dispatchSpecReview(gated.task, undefined, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
|
|
980
|
+
});
|
|
724
981
|
});
|
|
725
|
-
async function dispatchSpecReview(task, prior) {
|
|
982
|
+
async function dispatchSpecReview(task, prior, opts = {}) {
|
|
726
983
|
const dev = manager.getAgentConfig(task.agentId);
|
|
727
984
|
if (!dev) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
|
|
731
|
-
await rearmOrHold(task, task.agentId, kind);
|
|
732
|
-
}
|
|
733
|
-
return false;
|
|
985
|
+
const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
|
|
986
|
+
return holdConsumedSignal(task, task.agentId, kind, 'fix-no-dev-agent', 'server-spec-review-no-dev-agent', {}, opts.onSideEffect);
|
|
734
987
|
}
|
|
735
988
|
const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
736
|
-
const
|
|
989
|
+
const currentRound = task.specReviewRound ?? 0;
|
|
990
|
+
const nextRound = opts.bumpRound === false ? currentRound : currentRound + 1;
|
|
737
991
|
if (nextRound > cap) {
|
|
738
|
-
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
992
|
+
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
993
|
+
fromStatus: ['in_progress', 'fixing'],
|
|
994
|
+
...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
|
|
995
|
+
});
|
|
739
996
|
if (!capResult)
|
|
740
997
|
return false;
|
|
998
|
+
opts.onSideEffect?.();
|
|
741
999
|
const paused = capResult.task;
|
|
742
1000
|
if (paused.qaAgentId)
|
|
743
1001
|
await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
|
|
@@ -745,39 +1003,43 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
745
1003
|
await releaseAtCap(paused, paused.agentId, 'agentId');
|
|
746
1004
|
return false;
|
|
747
1005
|
}
|
|
748
|
-
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
749
1006
|
let content;
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
error: err instanceof Error ? err.message : String(err),
|
|
757
|
-
});
|
|
758
|
-
const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
|
|
759
|
-
await manager.setupPhaseSignal(task.id, task.agentId, kind, { skipSnapshot: true });
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
try {
|
|
763
|
-
if (!content.documents)
|
|
764
|
-
throw new Error('spec transport returned no documents');
|
|
765
|
-
await reviewStore.putRound(task.id, 'spec', {
|
|
766
|
-
round: nextRound,
|
|
767
|
-
phase: 'spec',
|
|
768
|
-
content: content.content,
|
|
769
|
-
documents: content.documents,
|
|
770
|
-
startedAt: new Date().toISOString(),
|
|
771
|
-
});
|
|
1007
|
+
if (opts.bumpRound === false) {
|
|
1008
|
+
const stored = await reviewStore.getRound(task.id, 'spec', nextRound);
|
|
1009
|
+
if (!stored || stored.phase !== 'spec') {
|
|
1010
|
+
throw new Error(`Task ${task.id} has no persisted spec review round ${nextRound} to redispatch`);
|
|
1011
|
+
}
|
|
1012
|
+
content = { content: stored.content, documents: stored.documents };
|
|
772
1013
|
}
|
|
773
|
-
|
|
774
|
-
await
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1014
|
+
else {
|
|
1015
|
+
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
1016
|
+
try {
|
|
1017
|
+
content = await transport().readContent(task, dev, 'spec');
|
|
1018
|
+
}
|
|
1019
|
+
catch (err) {
|
|
1020
|
+
const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
|
|
1021
|
+
return holdConsumedSignal(task, task.agentId, kind, 'handler-failed', 'server-spec-content-read-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
if (!content.documents)
|
|
1025
|
+
throw new Error('spec transport returned no documents');
|
|
1026
|
+
const roundData = {
|
|
1027
|
+
round: nextRound,
|
|
1028
|
+
phase: 'spec',
|
|
1029
|
+
content: content.content,
|
|
1030
|
+
documents: content.documents,
|
|
1031
|
+
startedAt: new Date().toISOString(),
|
|
1032
|
+
};
|
|
1033
|
+
const stored = opts.expectedTask
|
|
1034
|
+
? await manager.putReviewRoundIfCurrent(task.id, roundData, opts.expectedTask)
|
|
1035
|
+
: (await reviewStore.putRound(task.id, 'spec', roundData), true);
|
|
1036
|
+
if (!stored)
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
const kind = task.status === 'fixing' ? 'spec-fixed' : 'spec-done';
|
|
1041
|
+
return holdConsumedSignal(task, task.agentId, kind, 'verdict-store-failed', 'server-spec-round-store-failed', { error: err instanceof Error ? err.message : String(err) }, opts.onSideEffect);
|
|
1042
|
+
}
|
|
781
1043
|
}
|
|
782
1044
|
if (!resolveActiveQa(task)) {
|
|
783
1045
|
if (task.qaAgentId) {
|
|
@@ -787,175 +1049,208 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
787
1049
|
});
|
|
788
1050
|
}
|
|
789
1051
|
if (specApprovalIsHuman(task) || task.qaAgentId) {
|
|
790
|
-
const parked = await manager.parkTaskAtSpecReady(task.id, {
|
|
791
|
-
|
|
792
|
-
|
|
1052
|
+
const parked = await manager.parkTaskAtSpecReady(task.id, {
|
|
1053
|
+
specReviewRound: nextRound,
|
|
1054
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
1055
|
+
});
|
|
1056
|
+
if (parked)
|
|
1057
|
+
opts.onSideEffect?.();
|
|
1058
|
+
if (!parked) {
|
|
1059
|
+
await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-park-transition-failed', {}, opts.onSideEffect);
|
|
1060
|
+
}
|
|
793
1061
|
return !!parked;
|
|
794
1062
|
}
|
|
795
|
-
return autoApproveSpec(task);
|
|
1063
|
+
return autoApproveSpec(task, opts.expectedTask, opts.onSideEffect);
|
|
796
1064
|
}
|
|
797
1065
|
const dispatched = await manager.dispatchServerReviewToQa(task.id, {
|
|
798
1066
|
phase: 'spec',
|
|
1067
|
+
...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
|
|
799
1068
|
content: content.content,
|
|
800
1069
|
...(prior ? { priorFindingsJson: prior.findingsJson } : {}),
|
|
801
1070
|
...(prior?.responseJson ? { priorResponseJson: prior.responseJson } : {}),
|
|
1071
|
+
callerOwnsConsumedSignalFailure: true,
|
|
1072
|
+
...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
|
|
1073
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
1074
|
+
...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
|
|
802
1075
|
});
|
|
803
|
-
|
|
1076
|
+
if (dispatched)
|
|
1077
|
+
return true;
|
|
1078
|
+
return holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-review-dispatch-failed', {}, opts.onSideEffect);
|
|
804
1079
|
}
|
|
805
1080
|
bus.on('server.spec.review.submitted', async (event) => {
|
|
806
1081
|
const gated = await gate(bus, manager, event, { status: 'review', phase: 'spec', requireServerMode: false });
|
|
807
1082
|
if (!gated)
|
|
808
1083
|
return;
|
|
809
|
-
const { task } = gated;
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1084
|
+
const { task, expectedTask } = gated;
|
|
1085
|
+
await runConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', async () => {
|
|
1086
|
+
const qa = manager.getAgentConfig(task.qaAgentId ?? '');
|
|
1087
|
+
if (!qa) {
|
|
1088
|
+
await holdConsumedSignal(task, task.qaAgentId ?? '', 'spec-reviewed', 'handler-failed', 'server-spec-review-no-qa-agent');
|
|
1089
|
+
return;
|
|
815
1090
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (!roundData) {
|
|
821
|
-
await emitIntervention(bus, task, { phase: 'server-spec-review-round-missing', round });
|
|
822
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
await manager.refreshWorkdirCacheFor(qa.id);
|
|
826
|
-
let findings;
|
|
827
|
-
try {
|
|
828
|
-
findings = await transport().readFindings(task, qa);
|
|
829
|
-
}
|
|
830
|
-
catch (err) {
|
|
831
|
-
await emitIntervention(bus, task, {
|
|
832
|
-
phase: 'server-spec-findings-invalid',
|
|
833
|
-
error: err instanceof Error ? err.message : String(err),
|
|
834
|
-
});
|
|
835
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
let effective = findings;
|
|
839
|
-
if (effective === null) {
|
|
840
|
-
if (roundData.findings === undefined) {
|
|
841
|
-
await emitIntervention(bus, task, { phase: 'server-spec-findings-missing', round });
|
|
842
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
|
|
1091
|
+
const round = task.specReviewRound ?? 1;
|
|
1092
|
+
const roundData = await reviewStore.getRound(task.id, 'spec', round);
|
|
1093
|
+
if (!roundData) {
|
|
1094
|
+
await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-review-round-missing', { round });
|
|
843
1095
|
return;
|
|
844
1096
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
});
|
|
853
|
-
await transport().deleteFindings(qa);
|
|
854
|
-
await manager.setupPhaseSignal(task.id, qa.id, 'spec-reviewed', { skipSnapshot: true });
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
else {
|
|
858
|
-
const stored = await putVerdictRound(task, qa.id, 'spec-reviewed', {
|
|
859
|
-
...roundData,
|
|
860
|
-
findings: effective,
|
|
861
|
-
completedAt: new Date().toISOString(),
|
|
862
|
-
});
|
|
863
|
-
if (!stored)
|
|
1097
|
+
await manager.refreshWorkdirCacheFor(qa.id);
|
|
1098
|
+
let findings;
|
|
1099
|
+
try {
|
|
1100
|
+
findings = await transport().readFindings(task, qa);
|
|
1101
|
+
}
|
|
1102
|
+
catch (err) {
|
|
1103
|
+
await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-findings-invalid', { error: err instanceof Error ? err.message : String(err) });
|
|
864
1104
|
return;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (
|
|
1105
|
+
}
|
|
1106
|
+
let effective = findings;
|
|
1107
|
+
if (effective === null) {
|
|
1108
|
+
if (roundData.findings === undefined) {
|
|
1109
|
+
await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-findings-missing', { round });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
effective = roundData.findings;
|
|
1113
|
+
}
|
|
1114
|
+
else if (effective.round !== round) {
|
|
1115
|
+
await holdConsumedSignal(task, qa.id, 'spec-reviewed', 'handler-failed', 'server-spec-findings-round-mismatch', { round, payloadRound: effective.round });
|
|
875
1116
|
return;
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
await
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
const stored = await putVerdictRound(task, qa.id, 'spec-reviewed', {
|
|
1120
|
+
...roundData,
|
|
1121
|
+
findings: effective,
|
|
1122
|
+
completedAt: new Date().toISOString(),
|
|
1123
|
+
}, { expectedTask });
|
|
1124
|
+
if (!stored)
|
|
1125
|
+
return;
|
|
1126
|
+
if (!(await deleteFindingsOrHold(task, qa, 'spec-reviewed', expectedTask)))
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (effective.verdict === 'approve') {
|
|
1130
|
+
await advanceApprovedSpec(task, 'server-spec-approve-transition-failed', expectedTask);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
1134
|
+
if ((task.specReviewRound ?? 0) >= cap) {
|
|
1135
|
+
const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
1136
|
+
fromStatus: ['review'],
|
|
1137
|
+
expectTask: expectedTask,
|
|
1138
|
+
});
|
|
1139
|
+
if (!capResult)
|
|
1140
|
+
return;
|
|
1141
|
+
const paused = capResult.task;
|
|
1142
|
+
if (paused.qaAgentId)
|
|
1143
|
+
await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
|
|
1144
|
+
if (paused.agentId)
|
|
1145
|
+
await releaseAtCap(paused, paused.agentId, 'agentId');
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const dispatched = await manager.dispatchServerFixToDev(task.id, JSON.stringify(effective), { callerOwnsConsumedSignalFailure: true, expectedTask });
|
|
1149
|
+
if (!dispatched) {
|
|
1150
|
+
await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', 'server-spec-fix-dispatch-failed');
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
884
1153
|
});
|
|
885
|
-
async function runSpecFixSubmission(task) {
|
|
1154
|
+
async function runSpecFixSubmission(task, opts = {}) {
|
|
886
1155
|
const dev = manager.getAgentConfig(task.agentId);
|
|
887
1156
|
if (!dev) {
|
|
888
|
-
|
|
889
|
-
if (task.agentId) {
|
|
890
|
-
await rearmOrHold(task, task.agentId, 'spec-fixed');
|
|
891
|
-
}
|
|
892
|
-
return false;
|
|
1157
|
+
return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-no-dev-agent', 'server-spec-fix-no-dev-agent', {}, opts.onSideEffect);
|
|
893
1158
|
}
|
|
894
1159
|
const round = task.specReviewRound ?? 1;
|
|
895
1160
|
const roundData = await reviewStore.getRound(task.id, 'spec', round);
|
|
896
1161
|
if (!roundData?.findings) {
|
|
897
|
-
|
|
898
|
-
await manager.setupPhaseSignal(task.id, task.agentId, 'spec-fixed', { skipSnapshot: true });
|
|
899
|
-
return false;
|
|
1162
|
+
return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-findings-missing', 'server-spec-fix-findings-missing', { round }, opts.onSideEffect);
|
|
900
1163
|
}
|
|
901
1164
|
await manager.refreshWorkdirCacheFor(task.agentId);
|
|
902
|
-
let
|
|
1165
|
+
let readResult;
|
|
903
1166
|
try {
|
|
904
|
-
|
|
1167
|
+
readResult = await readResponseWithRetry(task, dev);
|
|
905
1168
|
}
|
|
906
1169
|
catch (err) {
|
|
907
|
-
|
|
908
|
-
|
|
1170
|
+
return holdConsumedSignal(task, dev.id, 'spec-fixed', 'response-read-failed', 'server-spec-response-read-failed', {
|
|
1171
|
+
reason: err instanceof ReviewExchangeError ? err.reason : 'read-failed',
|
|
909
1172
|
error: err instanceof Error ? err.message : String(err),
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1173
|
+
}, opts.onSideEffect);
|
|
1174
|
+
}
|
|
1175
|
+
if (readResult.kind === 'unknown') {
|
|
1176
|
+
return holdConsumedSignal(task, dev.id, 'spec-fixed', 'response-read-failed', 'server-spec-response-read-failed', { reason: readResult.error.reason, error: readResult.error.message }, opts.onSideEffect);
|
|
1177
|
+
}
|
|
1178
|
+
if (readResult.kind === 'invalid') {
|
|
1179
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
1180
|
+
reason: 'response-invalid',
|
|
1181
|
+
failurePhase: 'server-spec-response-invalid',
|
|
1182
|
+
rawResponse: readResult.raw,
|
|
1183
|
+
responseDigest: readResult.responseDigest,
|
|
1184
|
+
schemaViolationCodes: readResult.schemaViolationCodes,
|
|
1185
|
+
details: { error: readResult.error.message },
|
|
1186
|
+
}, opts.onSideEffect);
|
|
1187
|
+
}
|
|
1188
|
+
if (readResult.kind === 'absent') {
|
|
915
1189
|
if (roundData.response === undefined) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1190
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
1191
|
+
reason: 'response-missing',
|
|
1192
|
+
failurePhase: 'server-spec-response-missing',
|
|
1193
|
+
}, opts.onSideEffect);
|
|
919
1194
|
}
|
|
920
1195
|
return dispatchSpecReview(task, {
|
|
921
1196
|
findingsJson: JSON.stringify(roundData.findings),
|
|
922
1197
|
responseJson: JSON.stringify(roundData.response),
|
|
923
|
-
});
|
|
1198
|
+
}, opts);
|
|
924
1199
|
}
|
|
1200
|
+
const { response } = readResult;
|
|
925
1201
|
if (response.round !== round) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
round,
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1202
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
1203
|
+
reason: 'round-mismatch',
|
|
1204
|
+
failurePhase: 'server-spec-response-round-mismatch',
|
|
1205
|
+
rawResponse: readResult.raw,
|
|
1206
|
+
responseDigest: readResult.responseDigest,
|
|
1207
|
+
details: { payloadRound: response.round },
|
|
1208
|
+
}, opts.onSideEffect);
|
|
1209
|
+
}
|
|
1210
|
+
if (response.token !== task.signalToken) {
|
|
1211
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
1212
|
+
reason: 'token-mismatch',
|
|
1213
|
+
failurePhase: 'server-spec-response-token-mismatch',
|
|
1214
|
+
rawResponse: readResult.raw,
|
|
1215
|
+
responseDigest: readResult.responseDigest,
|
|
1216
|
+
}, opts.onSideEffect);
|
|
1217
|
+
}
|
|
1218
|
+
const findingsDigest = reviewFindingsDigest(roundData.findings);
|
|
1219
|
+
if (response.findingsDigest !== findingsDigest) {
|
|
1220
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
1221
|
+
reason: 'findings-digest-mismatch',
|
|
1222
|
+
failurePhase: 'server-spec-response-findings-digest-mismatch',
|
|
1223
|
+
rawResponse: readResult.raw,
|
|
1224
|
+
responseDigest: readResult.responseDigest,
|
|
1225
|
+
}, opts.onSideEffect);
|
|
934
1226
|
}
|
|
935
1227
|
const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
|
|
936
1228
|
if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1229
|
+
return rejectServerResponse(task, dev, roundData, {
|
|
1230
|
+
reason: 'coverage-gap',
|
|
1231
|
+
failurePhase: 'server-spec-response-coverage-gap',
|
|
1232
|
+
rawResponse: readResult.raw,
|
|
1233
|
+
responseDigest: readResult.responseDigest,
|
|
940
1234
|
missingFindingIds: gaps.missing,
|
|
941
1235
|
unknownFindingIds: gaps.unknown,
|
|
942
|
-
});
|
|
943
|
-
await manager.setupPhaseSignal(task.id, dev.id, 'spec-fixed', { skipSnapshot: true });
|
|
944
|
-
return false;
|
|
1236
|
+
}, opts.onSideEffect);
|
|
945
1237
|
}
|
|
946
|
-
if (!(await putVerdictRound(task, dev.id, 'spec-fixed', { ...roundData, response })))
|
|
1238
|
+
if (!(await putVerdictRound(task, dev.id, 'spec-fixed', { ...roundData, response }, opts)))
|
|
1239
|
+
return false;
|
|
1240
|
+
if (!(await deleteResponseOrHold(task, dev, 'spec-fixed', opts)))
|
|
947
1241
|
return false;
|
|
948
|
-
await transport().deleteResponse(dev);
|
|
949
1242
|
return dispatchSpecReview(task, {
|
|
950
1243
|
findingsJson: JSON.stringify(roundData.findings),
|
|
951
1244
|
responseJson: JSON.stringify(response),
|
|
952
|
-
});
|
|
1245
|
+
}, opts);
|
|
953
1246
|
}
|
|
954
1247
|
bus.on('server.spec.fix.submitted', async (event) => {
|
|
955
1248
|
const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'spec', requireServerMode: false });
|
|
956
1249
|
if (!gated)
|
|
957
1250
|
return;
|
|
958
|
-
await
|
|
1251
|
+
await runConsumedSignal(gated.task, gated.task.agentId, 'spec-fixed', async () => {
|
|
1252
|
+
await runSpecFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
|
|
1253
|
+
});
|
|
959
1254
|
});
|
|
960
1255
|
async function latestVerdictRound(taskId, phase, fromRound) {
|
|
961
1256
|
for (let r = fromRound; r >= 1; r--) {
|
|
@@ -968,26 +1263,31 @@ export function registerServerEventHandlers(bus, manager) {
|
|
|
968
1263
|
// 手工发起复用自然入口的完整协议:fixing 走 fix-submission(读/校验 dev 的 response 后派 recheck,
|
|
969
1264
|
// 缺 response 拒绝);其余状态按最近结论轮携带 prior 上下文,否则 QA 无从核对上一轮 findings
|
|
970
1265
|
manager.setServerReviewDriver({
|
|
971
|
-
dispatchCodeReview: async (task) => {
|
|
1266
|
+
dispatchCodeReview: async (task, opts = {}) => {
|
|
972
1267
|
if (task.status === 'fixing')
|
|
973
|
-
return runCodeFixSubmission(task);
|
|
1268
|
+
return runCodeFixSubmission(task, opts);
|
|
974
1269
|
const prior = await latestVerdictRound(task.id, 'code', task.reviewRound);
|
|
975
1270
|
return prepareAndDispatchCodeReview(task, {
|
|
976
1271
|
recheck: prior !== null,
|
|
1272
|
+
...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
|
|
1273
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
1274
|
+
...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
|
|
1275
|
+
...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
|
|
977
1276
|
...(prior?.findings ? { priorFindingsJson: JSON.stringify(prior.findings) } : {}),
|
|
978
1277
|
...(prior?.response ? { priorResponseJson: JSON.stringify(prior.response) } : {}),
|
|
979
1278
|
});
|
|
980
1279
|
},
|
|
981
|
-
dispatchSpecReview: async (task) => {
|
|
1280
|
+
dispatchSpecReview: async (task, opts = {}) => {
|
|
982
1281
|
if (task.status === 'fixing')
|
|
983
|
-
return runSpecFixSubmission(task);
|
|
1282
|
+
return runSpecFixSubmission(task, opts);
|
|
984
1283
|
const prior = await latestVerdictRound(task.id, 'spec', task.specReviewRound ?? 0);
|
|
985
|
-
|
|
1284
|
+
const priorContext = prior?.findings
|
|
986
1285
|
? {
|
|
987
1286
|
findingsJson: JSON.stringify(prior.findings),
|
|
988
1287
|
...(prior.response ? { responseJson: JSON.stringify(prior.response) } : {}),
|
|
989
1288
|
}
|
|
990
|
-
: undefined
|
|
1289
|
+
: undefined;
|
|
1290
|
+
return dispatchSpecReview(task, priorContext, opts);
|
|
991
1291
|
},
|
|
992
1292
|
});
|
|
993
1293
|
}
|