baxian 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +5 -7
  2. package/README.zh-CN.md +5 -7
  3. package/dist/agent/bootstrap.d.ts.map +1 -1
  4. package/dist/agent/bootstrap.js +2 -4
  5. package/dist/agent/bootstrap.js.map +1 -1
  6. package/dist/agent/branch.d.ts +0 -12
  7. package/dist/agent/branch.d.ts.map +1 -1
  8. package/dist/agent/branch.js +1 -71
  9. package/dist/agent/branch.js.map +1 -1
  10. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  11. package/dist/agent/dispatch-reconciler.js +9 -9
  12. package/dist/agent/dispatch-reconciler.js.map +1 -1
  13. package/dist/agent/manager.d.ts +65 -138
  14. package/dist/agent/manager.d.ts.map +1 -1
  15. package/dist/agent/manager.js +928 -2442
  16. package/dist/agent/manager.js.map +1 -1
  17. package/dist/agent/pane-streamer.d.ts +4 -1
  18. package/dist/agent/pane-streamer.d.ts.map +1 -1
  19. package/dist/agent/pane-streamer.js +25 -5
  20. package/dist/agent/pane-streamer.js.map +1 -1
  21. package/dist/agent/phase-signal-watcher.d.ts +2 -11
  22. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  23. package/dist/agent/phase-signal-watcher.js +38 -87
  24. package/dist/agent/phase-signal-watcher.js.map +1 -1
  25. package/dist/agent/phase-signal.d.ts +14 -40
  26. package/dist/agent/phase-signal.d.ts.map +1 -1
  27. package/dist/agent/phase-signal.js +47 -90
  28. package/dist/agent/phase-signal.js.map +1 -1
  29. package/dist/agent/preflight.d.ts +0 -1
  30. package/dist/agent/preflight.d.ts.map +1 -1
  31. package/dist/agent/preflight.js +8 -15
  32. package/dist/agent/preflight.js.map +1 -1
  33. package/dist/agent/prompt.d.ts +1 -30
  34. package/dist/agent/prompt.d.ts.map +1 -1
  35. package/dist/agent/prompt.js +21 -148
  36. package/dist/agent/prompt.js.map +1 -1
  37. package/dist/agent/repo-store.d.ts.map +1 -1
  38. package/dist/agent/repo-store.js +0 -4
  39. package/dist/agent/repo-store.js.map +1 -1
  40. package/dist/agent/root-agent-runtime.d.ts.map +1 -1
  41. package/dist/agent/root-agent-runtime.js +4 -2
  42. package/dist/agent/root-agent-runtime.js.map +1 -1
  43. package/dist/agent/root-recovery-coordinator.d.ts +0 -1
  44. package/dist/agent/root-recovery-coordinator.d.ts.map +1 -1
  45. package/dist/agent/root-recovery-coordinator.js +39 -61
  46. package/dist/agent/root-recovery-coordinator.js.map +1 -1
  47. package/dist/agent/vt-visible-text.d.ts +42 -0
  48. package/dist/agent/vt-visible-text.d.ts.map +1 -0
  49. package/dist/agent/vt-visible-text.js +482 -0
  50. package/dist/agent/vt-visible-text.js.map +1 -0
  51. package/dist/api/config.d.ts.map +1 -1
  52. package/dist/api/config.js +6 -2
  53. package/dist/api/config.js.map +1 -1
  54. package/dist/api/platform-guard.js +2 -2
  55. package/dist/api/platform-guard.js.map +1 -1
  56. package/dist/api/projects.d.ts.map +1 -1
  57. package/dist/api/projects.js +598 -318
  58. package/dist/api/projects.js.map +1 -1
  59. package/dist/api/tasks.d.ts.map +1 -1
  60. package/dist/api/tasks.js +15 -39
  61. package/dist/api/tasks.js.map +1 -1
  62. package/dist/config/loader.d.ts +5 -1
  63. package/dist/config/loader.d.ts.map +1 -1
  64. package/dist/config/loader.js +36 -24
  65. package/dist/config/loader.js.map +1 -1
  66. package/dist/config/validator.d.ts +7 -4
  67. package/dist/config/validator.d.ts.map +1 -1
  68. package/dist/config/validator.js +57 -73
  69. package/dist/config/validator.js.map +1 -1
  70. package/dist/event/handlers.d.ts.map +1 -1
  71. package/dist/event/handlers.js +359 -226
  72. package/dist/event/handlers.js.map +1 -1
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +0 -5
  75. package/dist/index.js.map +1 -1
  76. package/dist/platform/command-renderer.d.ts +6 -0
  77. package/dist/platform/command-renderer.d.ts.map +1 -1
  78. package/dist/platform/command-renderer.js +20 -0
  79. package/dist/platform/command-renderer.js.map +1 -1
  80. package/dist/platform/driver-host.d.ts.map +1 -1
  81. package/dist/platform/driver-host.js +3 -1
  82. package/dist/platform/driver-host.js.map +1 -1
  83. package/dist/platform/driver-spec.d.ts.map +1 -1
  84. package/dist/platform/driver-spec.js +32 -4
  85. package/dist/platform/driver-spec.js.map +1 -1
  86. package/dist/platform/git-driver.d.ts +2 -0
  87. package/dist/platform/git-driver.d.ts.map +1 -1
  88. package/dist/platform/git-driver.js +9 -3
  89. package/dist/platform/git-driver.js.map +1 -1
  90. package/dist/platform/platform-poller.d.ts +0 -1
  91. package/dist/platform/platform-poller.d.ts.map +1 -1
  92. package/dist/platform/platform-poller.js +0 -6
  93. package/dist/platform/platform-poller.js.map +1 -1
  94. package/dist/platform/plugins/github/driver.json +5 -0
  95. package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +20 -14
  96. package/dist/platform/startup.d.ts.map +1 -1
  97. package/dist/platform/startup.js +5 -5
  98. package/dist/platform/startup.js.map +1 -1
  99. package/dist/platform/types.d.ts +1 -0
  100. package/dist/platform/types.d.ts.map +1 -1
  101. package/dist/platform/types.js +3 -3
  102. package/dist/platform/types.js.map +1 -1
  103. package/dist/shared/constants.d.ts +1 -6
  104. package/dist/shared/constants.d.ts.map +1 -1
  105. package/dist/shared/constants.js +2 -29
  106. package/dist/shared/constants.js.map +1 -1
  107. package/dist/shared/types.d.ts +28 -124
  108. package/dist/shared/types.d.ts.map +1 -1
  109. package/dist/shared/types.js +7 -6
  110. package/dist/shared/types.js.map +1 -1
  111. package/dist/skills/baxian-pr-feedback/SKILL.md +5 -3
  112. package/dist/skills/baxian-pr-recheck/SKILL.md +4 -0
  113. package/dist/skills/baxian-pr-review/SKILL.md +4 -0
  114. package/dist/skills/baxian-signals/SKILL.md +15 -32
  115. package/dist/skills/baxian-task-check/SKILL.md +13 -14
  116. package/dist/state/snapshot.d.ts.map +1 -1
  117. package/dist/state/snapshot.js +5 -2
  118. package/dist/state/snapshot.js.map +1 -1
  119. package/dist/state/task-store.d.ts.map +1 -1
  120. package/dist/state/task-store.js +56 -140
  121. package/dist/state/task-store.js.map +1 -1
  122. package/dist/web/assets/index-BKiM0MSM.js +15 -0
  123. package/dist/web/assets/index-BS2adAfI.css +1 -0
  124. package/dist/web/index.html +2 -2
  125. package/package.json +1 -1
  126. package/dist/agent/review-transport.d.ts +0 -92
  127. package/dist/agent/review-transport.d.ts.map +0 -1
  128. package/dist/agent/review-transport.js +0 -512
  129. package/dist/agent/review-transport.js.map +0 -1
  130. package/dist/event/server-handlers.d.ts +0 -4
  131. package/dist/event/server-handlers.d.ts.map +0 -1
  132. package/dist/event/server-handlers.js +0 -1294
  133. package/dist/event/server-handlers.js.map +0 -1
  134. package/dist/skills/baxian-research/SKILL.md +0 -28
  135. package/dist/skills/baxian-server-feedback/SKILL.md +0 -40
  136. package/dist/skills/baxian-server-feedback/agents/openai.yaml +0 -2
  137. package/dist/skills/baxian-server-review/SKILL.md +0 -58
  138. package/dist/skills/baxian-server-review/agents/openai.yaml +0 -2
  139. package/dist/state/review-store.d.ts +0 -33
  140. package/dist/state/review-store.d.ts.map +0 -1
  141. package/dist/state/review-store.js +0 -272
  142. package/dist/state/review-store.js.map +0 -1
  143. package/dist/web/assets/index-BPv6vv3U.css +0 -1
  144. package/dist/web/assets/index-BXfgaFNm.js +0 -18
@@ -1,1294 +0,0 @@
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';
6
- const LEGACY_DIFF_LARGE_THRESHOLD = 2000;
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
- ]);
13
- function splitLegacyDiffByFile(diff) {
14
- if (diff.trim() === '')
15
- return [];
16
- const out = [];
17
- let current = null;
18
- const flush = () => {
19
- if (!current)
20
- return;
21
- out.push({
22
- path: current.path,
23
- text: current.lines.join('\n'),
24
- lines: current.lines.length,
25
- });
26
- current = null;
27
- };
28
- for (const line of diff.split('\n')) {
29
- const m = FILE_HEADER_RE.exec(line);
30
- if (m) {
31
- flush();
32
- current = { path: m[1], lines: [line] };
33
- }
34
- else if (current) {
35
- current.lines.push(line);
36
- }
37
- }
38
- flush();
39
- return out;
40
- }
41
- function legacyTopDir(path) {
42
- const idx = path.lastIndexOf('/');
43
- return idx === -1 ? '.' : path.slice(0, idx);
44
- }
45
- function buildLegacyBatches(files, maxLines) {
46
- const groups = [];
47
- const indexByDir = new Map();
48
- for (const file of files) {
49
- const dir = legacyTopDir(file.path);
50
- const existing = indexByDir.get(dir);
51
- if (existing === undefined) {
52
- indexByDir.set(dir, groups.length);
53
- groups.push({ files: [file], lines: file.lines });
54
- }
55
- else {
56
- groups[existing].files.push(file);
57
- groups[existing].lines += file.lines;
58
- }
59
- }
60
- const batches = [];
61
- let pending = [];
62
- let pendingLines = 0;
63
- const flushPending = () => {
64
- if (pending.length === 0)
65
- return;
66
- batches.push(pending);
67
- pending = [];
68
- pendingLines = 0;
69
- };
70
- for (const group of groups) {
71
- if (group.lines > maxLines) {
72
- flushPending();
73
- for (const file of group.files)
74
- batches.push([file]);
75
- continue;
76
- }
77
- if (pendingLines + group.lines > maxLines)
78
- flushPending();
79
- pending.push(...group.files);
80
- pendingLines += group.lines;
81
- }
82
- flushPending();
83
- return batches;
84
- }
85
- function aggregateBatchFindings(batches, round) {
86
- const findings = [];
87
- const used = new Set();
88
- for (const [i, batch] of batches.entries()) {
89
- for (const f of batch.findings) {
90
- let id = /^b\d+-/.test(f.id) ? f.id : `b${i}-${f.id}`;
91
- if (used.has(id)) {
92
- let candidate = `b${i}-r${round}-${f.id}`;
93
- for (let n = 2; used.has(candidate); n++)
94
- candidate = `b${i}-r${round}-${f.id}-${n}`;
95
- id = candidate;
96
- }
97
- used.add(id);
98
- findings.push(id === f.id ? f : { ...f, id });
99
- }
100
- }
101
- const verdict = batches.some(b => b.verdict === 'request-changes') ? 'request-changes' : 'approve';
102
- return { round, verdict, findings };
103
- }
104
- function coverageGaps(findings, responseIds) {
105
- const findingIds = new Set(findings.findings.map(f => f.id));
106
- return {
107
- missing: [...findingIds].filter(id => !responseIds.has(id)).sort(),
108
- unknown: [...responseIds].filter(id => !findingIds.has(id)).sort(),
109
- };
110
- }
111
- function waitForHandlerRetry(attempt) {
112
- const ms = computeBackoffMs(attempt, SERVER_HANDLER_BACKOFF);
113
- return new Promise(resolve => setTimeout(resolve, ms));
114
- }
115
- async function emitIntervention(bus, task, data) {
116
- try {
117
- await bus.emit({
118
- id: '',
119
- type: 'human.intervention',
120
- timestamp: new Date().toISOString(),
121
- projectId: task.projectId,
122
- agentId: task.agentId,
123
- taskId: task.id,
124
- data,
125
- });
126
- }
127
- catch (err) {
128
- console.warn('[ServerEventHandler] intervention emit failed:', err);
129
- }
130
- }
131
- async function gate(bus, manager, event, expect) {
132
- if (!event.taskId)
133
- return null;
134
- const task = await manager.getTask(event.taskId);
135
- if (!task)
136
- return null;
137
- if (expect.requireServerMode && task.reviewMode !== 'server') {
138
- console.warn(`[ServerEventHandler] ${event.type} ignored: task ${task.id} not in server mode`);
139
- return null;
140
- }
141
- const token = event.data?.token;
142
- const phaseOk = expect.phase === 'any'
143
- || (expect.phase === 'spec' ? task.phase !== 'code' : !isSpecStagePhase(task.phase));
144
- if (token && token !== task.signalToken)
145
- return null;
146
- if (task.status !== expect.status || !phaseOk || !token) {
147
- await emitIntervention(bus, task, {
148
- phase: `${event.type}-stale`,
149
- taskStatus: task.status,
150
- taskPhase: task.phase ?? null,
151
- });
152
- return null;
153
- }
154
- return { task, expectedTask: taskGenerationGuard(task) };
155
- }
156
- export function registerServerEventHandlers(bus, manager) {
157
- const configured = manager.getReviewStore();
158
- if (!configured) {
159
- console.warn('[ServerEventHandler] no ReviewStore configured; server review mode disabled');
160
- return;
161
- }
162
- const reviewStore = configured;
163
- const transport = () => manager.getReviewTransport();
164
- async function releaseAtCap(task, agentId, field, opts = {}) {
165
- const released = await manager.releaseAgentForTask(agentId, task.id, 'idle', opts)
166
- .catch(() => false);
167
- if (!released) {
168
- const state = await manager.getAgentState(agentId);
169
- if (state && state.taskId === task.id) {
170
- await emitIntervention(bus, task, {
171
- phase: 'server-max-rounds-release-failed',
172
- agentId,
173
- field,
174
- });
175
- return;
176
- }
177
- }
178
- if (field === 'agentId') {
179
- await manager.updateTask(task.id, { agentId: '' })
180
- .catch(err => console.error(`[ServerHandler] max_rounds clear agentId(${task.id}) failed:`, err));
181
- }
182
- }
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) {
241
- try {
242
- await handler();
243
- }
244
- catch (err) {
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) });
254
- return false;
255
- }
256
- }
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;
402
- }
403
- function resolveActiveQa(task) {
404
- if (!task.qaAgentId)
405
- return undefined;
406
- const recorded = manager.getAgentConfig(task.qaAgentId);
407
- return recorded?.role === 'qa' ? recorded : undefined;
408
- }
409
- async function pauseForUnavailableQa(task, entryKind, phase, onSideEffect) {
410
- if (!task.qaAgentId || resolveActiveQa(task))
411
- return false;
412
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', phase, { qaAgentId: task.qaAgentId }, onSideEffect);
413
- return true;
414
- }
415
- async function autoApproveCode(task, expectedTask, entryKind = 'code-done', onSideEffect) {
416
- const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
417
- onSideEffect?.();
418
- if (afterDone === null) {
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
- }
428
- return;
429
- }
430
- const dev = manager.getAgentConfig(task.agentId);
431
- if (!dev) {
432
- await holdConsumedSignal(task, task.agentId, entryKind, 'fix-no-dev-agent', 'server-code-auto-approve-no-dev-agent', {}, onSideEffect);
433
- return;
434
- }
435
- await manager.refreshWorkdirCacheFor(task.agentId);
436
- let reviewHeadAnchorSha;
437
- try {
438
- reviewHeadAnchorSha = await transport().readHeadSha(dev);
439
- }
440
- catch (err) {
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);
442
- return;
443
- }
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?.();
450
- if (!approved) {
451
- await holdConsumedSignal(task, task.agentId, entryKind, 'handler-failed', 'server-code-auto-approve-transition-failed', {}, onSideEffect);
452
- return;
453
- }
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
- }
458
- }
459
- function specApprovalIsHuman(task) {
460
- return task.researchAgentId !== undefined
461
- || manager.getProjectConfig(task.projectId)?.specApproval === 'human';
462
- }
463
- async function advanceApprovedSpec(task, failurePhase, expectedTask) {
464
- try {
465
- const result = specApprovalIsHuman(task)
466
- ? await manager.parkTaskAtSpecReady(task.id, {
467
- ...(expectedTask !== undefined ? { expectedTask } : {}),
468
- })
469
- : await manager.transitionToCodePhase(task.id, expectedTask);
470
- if (!result) {
471
- await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase);
472
- }
473
- }
474
- catch (err) {
475
- await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'spec-reviewed', 'handler-failed', failurePhase, { error: err instanceof Error ? err.message : String(err) });
476
- }
477
- }
478
- async function autoApproveSpec(task, expectedTask, onSideEffect) {
479
- try {
480
- const result = await manager.transitionToCodePhase(task.id, expectedTask);
481
- if (result)
482
- onSideEffect?.();
483
- if (!result) {
484
- await holdConsumedSignal(task, task.agentId, task.status === 'fixing' ? 'spec-fixed' : 'spec-done', 'handler-failed', 'server-spec-auto-approve-transition-failed', {}, onSideEffect);
485
- return false;
486
- }
487
- return true;
488
- }
489
- catch (err) {
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);
491
- return false;
492
- }
493
- }
494
- async function prepareAndDispatchCodeReview(task, opts) {
495
- const dev = manager.getAgentConfig(task.agentId);
496
- if (!dev) {
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);
499
- }
500
- const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
501
- const nextRound = opts.bumpRound === false ? task.reviewRound : task.reviewRound + 1;
502
- if (nextRound > cap) {
503
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
504
- fromStatus: ['in_progress', 'fixing'],
505
- ...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
506
- });
507
- if (!capResult)
508
- return false;
509
- opts.onSideEffect?.();
510
- const paused = capResult.task;
511
- if (paused.qaAgentId)
512
- await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
513
- await bus.emit({
514
- id: '',
515
- type: 'review.max_rounds',
516
- timestamp: new Date().toISOString(),
517
- projectId: paused.projectId,
518
- agentId: paused.agentId,
519
- taskId: paused.id,
520
- data: { round: nextRound, cap },
521
- });
522
- return false;
523
- }
524
- let content;
525
- let reviewHeadAnchorSha;
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`);
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;
539
- }
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
- }
568
- }
569
- const round = {
570
- round: nextRound,
571
- phase: 'code',
572
- content: content.content,
573
- headSha: reviewHeadAnchorSha,
574
- ...(content.diffstat ? { diffstat: content.diffstat } : {}),
575
- ...(content.baseSha ? { baseSha: content.baseSha } : {}),
576
- ...(content.headTree ? { headTree: content.headTree } : {}),
577
- startedAt: new Date().toISOString(),
578
- };
579
- const putEntryRound = (data) => putVerdictRound(task, task.agentId, task.status === 'fixing' ? 'code-fixed' : 'code-done', data, opts);
580
- const resolveRecheckInterdiff = async () => {
581
- if (!opts.recheck)
582
- return undefined;
583
- try {
584
- const result = await manager.computeCodeInterdiff(task.id, nextRound);
585
- return result.ok && result.diff.trim() !== '' ? result.diff : undefined;
586
- }
587
- catch (err) {
588
- console.warn(`[ServerEventHandler] recheck interdiff unavailable for task ${task.id} round ${nextRound}; ` +
589
- `dispatching full diff only: ${err instanceof Error ? err.message : String(err)}`);
590
- return undefined;
591
- }
592
- };
593
- if (opts.bumpRound !== false && !(await putEntryRound(round)))
594
- return false;
595
- const interdiff = await resolveRecheckInterdiff();
596
- const dispatched = await manager.dispatchServerReviewToQa(task.id, {
597
- phase: 'code',
598
- recheck: opts.recheck,
599
- ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
600
- content: content.content,
601
- reviewHeadAnchorSha,
602
- ...(content.baseSha ? { baseSha: content.baseSha } : {}),
603
- ...(content.headTree ? { headTree: content.headTree } : {}),
604
- ...(interdiff ? { interdiff } : {}),
605
- ...(content.diffstat ? { diffstat: content.diffstat } : {}),
606
- ...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
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 } : {}),
612
- });
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);
617
- }
618
- async function dispatchLegacyBatch(taskId, batches, index, diffstat, opts) {
619
- const text = batches[index].map(f => f.text).join('\n');
620
- const dispatched = await manager.dispatchServerReviewToQa(taskId, {
621
- phase: 'code',
622
- recheck: opts.recheck,
623
- continuation: true,
624
- content: text,
625
- ...(diffstat ? { diffstat } : {}),
626
- batch: { index, total: batches.length },
627
- ...(opts.priorFindingsJson ? { priorFindingsJson: opts.priorFindingsJson } : {}),
628
- ...(opts.priorResponseJson ? { priorResponseJson: opts.priorResponseJson } : {}),
629
- callerOwnsConsumedSignalFailure: true,
630
- ...(opts.settlePermit !== undefined ? { settlePermit: opts.settlePermit } : {}),
631
- ...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
632
- });
633
- return dispatched !== null;
634
- }
635
- function rebuildLegacyBatches(roundData) {
636
- return buildLegacyBatches(splitLegacyDiffByFile(roundData.content), LEGACY_DIFF_LARGE_THRESHOLD);
637
- }
638
- bus.on('server.code.ready', async (event) => {
639
- const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'code', requireServerMode: true });
640
- if (!gated)
641
- return;
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
- });
652
- });
653
- bus.on('server.code.review.submitted', async (event) => {
654
- const gated = await gate(bus, manager, event, { status: 'review', phase: 'code', requireServerMode: true });
655
- if (!gated)
656
- return;
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');
662
- return;
663
- }
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 });
668
- return;
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) });
678
- return;
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 });
692
- return;
693
- }
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)))
701
- return;
702
- }
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
- });
746
- });
747
- async function routeCodeVerdict(task, findings, expectedTask) {
748
- if (findings.verdict === 'approve') {
749
- const afterDone = await manager.commitServerAfterDone(task.id, expectedTask);
750
- if (afterDone === null) {
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
- }
755
- return;
756
- }
757
- const approved = await manager.transitionTaskStatus(task.id, 'approved', { fromStatus: ['review'], expectTask: expectedTask });
758
- if (!approved) {
759
- await holdConsumedSignal(task, task.qaAgentId ?? task.agentId, 'code-reviewed', 'handler-failed', 'server-code-approved-transition-failed');
760
- return;
761
- }
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
- }
766
- return;
767
- }
768
- const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
769
- if (task.reviewRound >= cap) {
770
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', { fromStatus: ['review'], expectTask: expectedTask });
771
- if (!capResult)
772
- return;
773
- const paused = capResult.task;
774
- if (paused.qaAgentId) {
775
- await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId', { allowAwaitingHuman: true });
776
- }
777
- await bus.emit({
778
- id: '',
779
- type: 'review.max_rounds',
780
- timestamp: new Date().toISOString(),
781
- projectId: paused.projectId,
782
- agentId: paused.agentId,
783
- taskId: paused.id,
784
- data: { round: paused.reviewRound, cap },
785
- });
786
- return;
787
- }
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
- }
792
- }
793
- async function recheckCodeOrAutoApprove(task, priorFindingsJson, priorResponseJson, opts = {}) {
794
- if (await pauseForUnavailableQa(task, 'code-fixed', 'server-code-qa-unavailable-after-fix', opts.onSideEffect)) {
795
- return false;
796
- }
797
- if (!task.qaAgentId) {
798
- await autoApproveCode(task, opts.expectedTask, 'code-fixed', opts.onSideEffect);
799
- return true;
800
- }
801
- return prepareAndDispatchCodeReview(task, {
802
- recheck: true,
803
- priorFindingsJson,
804
- priorResponseJson,
805
- ...opts,
806
- });
807
- }
808
- async function runCodeFixSubmission(task, opts = {}) {
809
- const dev = manager.getAgentConfig(task.agentId);
810
- if (!dev) {
811
- return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-no-dev-agent', 'server-code-fix-no-dev-agent', {}, opts.onSideEffect);
812
- }
813
- const round = Math.max(task.reviewRound, 1);
814
- const roundData = await reviewStore.getRound(task.id, 'code', round);
815
- if (!roundData?.findings) {
816
- return holdConsumedSignal(task, task.agentId, 'code-fixed', 'fix-findings-missing', 'server-code-fix-findings-missing', { round }, opts.onSideEffect);
817
- }
818
- await manager.refreshWorkdirCacheFor(task.agentId);
819
- let readResult;
820
- try {
821
- readResult = await readResponseWithRetry(task, dev);
822
- }
823
- catch (err) {
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',
826
- error: err instanceof Error ? err.message : String(err),
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') {
843
- if (roundData.response === undefined) {
844
- return rejectServerResponse(task, dev, roundData, {
845
- reason: 'response-missing',
846
- failurePhase: 'server-code-response-missing',
847
- }, opts.onSideEffect);
848
- }
849
- return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(roundData.response), opts);
850
- }
851
- const { response } = readResult;
852
- if (response.round !== round) {
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);
877
- }
878
- const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
879
- if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
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,
885
- missingFindingIds: gaps.missing,
886
- unknownFindingIds: gaps.unknown,
887
- }, opts.onSideEffect);
888
- }
889
- if (!(await putVerdictRound(task, dev.id, 'code-fixed', { ...roundData, response }, opts)))
890
- return false;
891
- if (!(await deleteResponseOrHold(task, dev, 'code-fixed', opts)))
892
- return false;
893
- return recheckCodeOrAutoApprove(task, JSON.stringify(roundData.findings), JSON.stringify(response), opts);
894
- }
895
- bus.on('server.code.fix.submitted', async (event) => {
896
- const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'code', requireServerMode: true });
897
- if (!gated)
898
- return;
899
- await runConsumedSignal(gated.task, gated.task.agentId, 'code-fixed', async () => {
900
- await runCodeFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
901
- });
902
- });
903
- bus.on('server.code.published', async (event) => {
904
- const gated = await gate(bus, manager, event, { status: 'approved', phase: 'code', requireServerMode: true });
905
- if (!gated)
906
- return;
907
- const { task } = gated;
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,
932
- });
933
- return;
934
- }
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
- });
973
- });
974
- bus.on('server.spec.ready', async (event) => {
975
- const gated = await gate(bus, manager, event, { status: 'in_progress', phase: 'spec', requireServerMode: false });
976
- if (!gated)
977
- return;
978
- await runConsumedSignal(gated.task, gated.task.agentId, 'spec-done', async () => {
979
- await dispatchSpecReview(gated.task, undefined, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
980
- });
981
- });
982
- async function dispatchSpecReview(task, prior, opts = {}) {
983
- const dev = manager.getAgentConfig(task.agentId);
984
- if (!dev) {
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);
987
- }
988
- const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
989
- const currentRound = task.specReviewRound ?? 0;
990
- const nextRound = opts.bumpRound === false ? currentRound : currentRound + 1;
991
- if (nextRound > cap) {
992
- const capResult = await manager.transitionTaskStatus(task.id, 'max_rounds', {
993
- fromStatus: ['in_progress', 'fixing'],
994
- ...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
995
- });
996
- if (!capResult)
997
- return false;
998
- opts.onSideEffect?.();
999
- const paused = capResult.task;
1000
- if (paused.qaAgentId)
1001
- await releaseAtCap(paused, paused.qaAgentId, 'qaAgentId');
1002
- if (paused.agentId)
1003
- await releaseAtCap(paused, paused.agentId, 'agentId');
1004
- return false;
1005
- }
1006
- let content;
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 };
1013
- }
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
- }
1043
- }
1044
- if (!resolveActiveQa(task)) {
1045
- if (task.qaAgentId) {
1046
- await emitIntervention(bus, task, {
1047
- phase: 'server-spec-qa-unavailable',
1048
- qaAgentId: task.qaAgentId,
1049
- });
1050
- }
1051
- if (specApprovalIsHuman(task) || task.qaAgentId) {
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
- }
1061
- return !!parked;
1062
- }
1063
- return autoApproveSpec(task, opts.expectedTask, opts.onSideEffect);
1064
- }
1065
- const dispatched = await manager.dispatchServerReviewToQa(task.id, {
1066
- phase: 'spec',
1067
- ...(opts.bumpRound !== undefined ? { bumpRound: opts.bumpRound } : {}),
1068
- content: content.content,
1069
- ...(prior ? { priorFindingsJson: prior.findingsJson } : {}),
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 } : {}),
1075
- });
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);
1079
- }
1080
- bus.on('server.spec.review.submitted', async (event) => {
1081
- const gated = await gate(bus, manager, event, { status: 'review', phase: 'spec', requireServerMode: false });
1082
- if (!gated)
1083
- return;
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;
1090
- }
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 });
1095
- return;
1096
- }
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) });
1104
- return;
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 });
1116
- return;
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
- });
1153
- });
1154
- async function runSpecFixSubmission(task, opts = {}) {
1155
- const dev = manager.getAgentConfig(task.agentId);
1156
- if (!dev) {
1157
- return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-no-dev-agent', 'server-spec-fix-no-dev-agent', {}, opts.onSideEffect);
1158
- }
1159
- const round = task.specReviewRound ?? 1;
1160
- const roundData = await reviewStore.getRound(task.id, 'spec', round);
1161
- if (!roundData?.findings) {
1162
- return holdConsumedSignal(task, task.agentId, 'spec-fixed', 'fix-findings-missing', 'server-spec-fix-findings-missing', { round }, opts.onSideEffect);
1163
- }
1164
- await manager.refreshWorkdirCacheFor(task.agentId);
1165
- let readResult;
1166
- try {
1167
- readResult = await readResponseWithRetry(task, dev);
1168
- }
1169
- catch (err) {
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',
1172
- error: err instanceof Error ? err.message : String(err),
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') {
1189
- if (roundData.response === undefined) {
1190
- return rejectServerResponse(task, dev, roundData, {
1191
- reason: 'response-missing',
1192
- failurePhase: 'server-spec-response-missing',
1193
- }, opts.onSideEffect);
1194
- }
1195
- return dispatchSpecReview(task, {
1196
- findingsJson: JSON.stringify(roundData.findings),
1197
- responseJson: JSON.stringify(roundData.response),
1198
- }, opts);
1199
- }
1200
- const { response } = readResult;
1201
- if (response.round !== round) {
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);
1226
- }
1227
- const gaps = coverageGaps(roundData.findings, new Set(response.responses.map(r => r.findingId)));
1228
- if (gaps.missing.length > 0 || gaps.unknown.length > 0) {
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,
1234
- missingFindingIds: gaps.missing,
1235
- unknownFindingIds: gaps.unknown,
1236
- }, opts.onSideEffect);
1237
- }
1238
- if (!(await putVerdictRound(task, dev.id, 'spec-fixed', { ...roundData, response }, opts)))
1239
- return false;
1240
- if (!(await deleteResponseOrHold(task, dev, 'spec-fixed', opts)))
1241
- return false;
1242
- return dispatchSpecReview(task, {
1243
- findingsJson: JSON.stringify(roundData.findings),
1244
- responseJson: JSON.stringify(response),
1245
- }, opts);
1246
- }
1247
- bus.on('server.spec.fix.submitted', async (event) => {
1248
- const gated = await gate(bus, manager, event, { status: 'fixing', phase: 'spec', requireServerMode: false });
1249
- if (!gated)
1250
- return;
1251
- await runConsumedSignal(gated.task, gated.task.agentId, 'spec-fixed', async () => {
1252
- await runSpecFixSubmission(gated.task, { expectedTask: gated.expectedTask, settlePermit: settlePermitOf(event) });
1253
- });
1254
- });
1255
- async function latestVerdictRound(taskId, phase, fromRound) {
1256
- for (let r = fromRound; r >= 1; r--) {
1257
- const data = await reviewStore.getRound(taskId, phase, r);
1258
- if (data?.findings)
1259
- return data;
1260
- }
1261
- return null;
1262
- }
1263
- // 手工发起复用自然入口的完整协议:fixing 走 fix-submission(读/校验 dev 的 response 后派 recheck,
1264
- // 缺 response 拒绝);其余状态按最近结论轮携带 prior 上下文,否则 QA 无从核对上一轮 findings
1265
- manager.setServerReviewDriver({
1266
- dispatchCodeReview: async (task, opts = {}) => {
1267
- if (task.status === 'fixing')
1268
- return runCodeFixSubmission(task, opts);
1269
- const prior = await latestVerdictRound(task.id, 'code', task.reviewRound);
1270
- return prepareAndDispatchCodeReview(task, {
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 } : {}),
1276
- ...(prior?.findings ? { priorFindingsJson: JSON.stringify(prior.findings) } : {}),
1277
- ...(prior?.response ? { priorResponseJson: JSON.stringify(prior.response) } : {}),
1278
- });
1279
- },
1280
- dispatchSpecReview: async (task, opts = {}) => {
1281
- if (task.status === 'fixing')
1282
- return runSpecFixSubmission(task, opts);
1283
- const prior = await latestVerdictRound(task.id, 'spec', task.specReviewRound ?? 0);
1284
- const priorContext = prior?.findings
1285
- ? {
1286
- findingsJson: JSON.stringify(prior.findings),
1287
- ...(prior.response ? { responseJson: JSON.stringify(prior.response) } : {}),
1288
- }
1289
- : undefined;
1290
- return dispatchSpecReview(task, priorContext, opts);
1291
- },
1292
- });
1293
- }
1294
- //# sourceMappingURL=server-handlers.js.map