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