baxian 2.0.3 → 2.0.4
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 +2 -2
- package/README.zh-CN.md +3 -3
- package/dist/agent/bootstrap.js +4 -4
- package/dist/agent/dispatch-reconciler.d.ts +2 -0
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +48 -4
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +41 -4
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +707 -77
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/tmux-probe-poller.js +4 -4
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +2 -7
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.js +7 -7
- package/dist/api/hosts.js +6 -6
- package/dist/api/platform-guard.js +2 -2
- package/dist/api/projects.js +14 -14
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +94 -57
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/config/loader.js +5 -5
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.js +29 -29
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +3 -0
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/ws.d.ts.map +1 -1
- package/dist/event/ws.js +3 -3
- package/dist/event/ws.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/platform/feedback.d.ts +5 -1
- package/dist/platform/feedback.d.ts.map +1 -1
- package/dist/platform/feedback.js +7 -1
- package/dist/platform/feedback.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +5 -1
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +8 -6
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/pr-conversation-cache.d.ts +12 -7
- package/dist/platform/pr-conversation-cache.d.ts.map +1 -1
- package/dist/platform/pr-conversation-cache.js +60 -23
- package/dist/platform/pr-conversation-cache.js.map +1 -1
- package/dist/platform/review-timeline.d.ts +17 -2
- package/dist/platform/review-timeline.d.ts.map +1 -1
- package/dist/platform/review-timeline.js +93 -84
- package/dist/platform/review-timeline.js.map +1 -1
- package/dist/shared/types.d.ts +31 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +14 -0
- package/dist/shared/types.js.map +1 -1
- package/dist/state/snapshot.d.ts +0 -1
- package/dist/state/snapshot.d.ts.map +1 -1
- package/dist/state/snapshot.js +0 -13
- package/dist/state/snapshot.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +96 -8
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-CzVzThuT.js +15 -0
- package/dist/web/assets/{index-yqxawAPJ.css → index-Rmp4wT_5.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CiAnjs1_.js +0 -15
package/dist/agent/manager.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
4
|
import { join, normalize } from 'node:path';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { createSignalToken, PASSIVE_VERDICT_WATCH } from './phase-signal.js';
|
|
6
7
|
import { tmuxInstallHint } from './preflight.js';
|
|
7
|
-
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, effectiveTaskReviewRound, isSpecStagePhase, isTaskOpen, repoIdentityKey, taskGenerationGuard, taskReviewRound, taskMatchesGeneration, } from '../shared/index.js';
|
|
8
|
+
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, effectiveTaskReviewRound, isSpecStagePhase, isTaskOpen, repoIdentityKey, taskAttentionGeneration, taskGenerationGuard, taskReviewRound, taskMatchesGeneration, } from '../shared/index.js';
|
|
8
9
|
import { buildProjectDriver, makeDriverExec } from '../platform/driver-host.js';
|
|
9
10
|
import { DriverOpError, buildDriverRunContext, GitDriver, safeDriverErrorText } from '../platform/git-driver.js';
|
|
11
|
+
import { versionTimeOf } from '../platform/row-schema.js';
|
|
10
12
|
import { checkOpenPrBinding, checkPrBinding } from '../platform/pr-binding.js';
|
|
11
13
|
import { collectPendingFeedback, scanCommentSourcesOnce } from '../platform/feedback.js';
|
|
12
|
-
import {
|
|
14
|
+
import { prReviewCacheRevision } from '../platform/pr-conversation-cache.js';
|
|
15
|
+
import { deadTokens, recheckPassProvenance } from '../platform/verdict-engine.js';
|
|
13
16
|
import { platformBindingMismatch, taskNeedsPlatformBindingAudit, } from '../platform/startup.js';
|
|
14
17
|
import { AGENT_STORE_NOOP } from '../state/agent-store.js';
|
|
15
18
|
import { SkillRegistry } from '../skill/registry.js';
|
|
@@ -27,6 +30,7 @@ import { resolveProjectTool } from '../config/validator.js';
|
|
|
27
30
|
import { SHA_HEX_SOURCE } from '../platform/types.js';
|
|
28
31
|
import { bodyDigest } from '../platform/body-digest.js';
|
|
29
32
|
import { validateCommentBody } from '../platform/command-renderer.js';
|
|
33
|
+
import { buildReviewTokenLine, extractReviewTokens, rowTokens } from '../platform/markers.js';
|
|
30
34
|
const BENIGN_DISPATCH_CONFLICT_CODES = new Set(['dispatch-superseded', 'dispatch-in-flight']);
|
|
31
35
|
export function isBenignDispatchConflict(err) {
|
|
32
36
|
return err instanceof ApiError
|
|
@@ -39,6 +43,16 @@ function requireTaskQaAgentId(task, operation) {
|
|
|
39
43
|
throw new Error(`${operation}: task ${task.id} has no QA participant`);
|
|
40
44
|
return task.qaAgentId;
|
|
41
45
|
}
|
|
46
|
+
function samePassProvenance(left, right) {
|
|
47
|
+
return left !== undefined
|
|
48
|
+
&& right !== undefined
|
|
49
|
+
&& left.sourceKey === right.sourceKey
|
|
50
|
+
&& left.id === right.id
|
|
51
|
+
&& left.bodyDigest === right.bodyDigest
|
|
52
|
+
&& left.token === right.token
|
|
53
|
+
&& left.failToken === right.failToken
|
|
54
|
+
&& left.anchorSha === right.anchorSha;
|
|
55
|
+
}
|
|
42
56
|
export class EnsureSessionError extends Error {
|
|
43
57
|
partial;
|
|
44
58
|
constructor(partial, message) {
|
|
@@ -226,6 +240,7 @@ export class AgentManager {
|
|
|
226
240
|
paneStreamerManager;
|
|
227
241
|
pluginRegistry;
|
|
228
242
|
platformDefaultBranchOf;
|
|
243
|
+
prConversationCache;
|
|
229
244
|
platformDriverExec;
|
|
230
245
|
phaseSignalWatcher;
|
|
231
246
|
errorRecordStore;
|
|
@@ -234,6 +249,7 @@ export class AgentManager {
|
|
|
234
249
|
dispatchSettleTimeoutMs;
|
|
235
250
|
dispatchAckResendIntervalMs = 3_000;
|
|
236
251
|
taskMutationQueue = Promise.resolve();
|
|
252
|
+
taskMutationScope = new AsyncLocalStorage();
|
|
237
253
|
agentOperationQueues = new Map();
|
|
238
254
|
agentIndex;
|
|
239
255
|
platformRunner;
|
|
@@ -254,6 +270,8 @@ export class AgentManager {
|
|
|
254
270
|
cancelInterruptGuardWaitMs = DEFAULT_DISPATCH_ACK_TIMEOUT_MS + 5_000;
|
|
255
271
|
markCompleteInFlight = new Set();
|
|
256
272
|
specVerdictInFlight = new Set();
|
|
273
|
+
codeVerdictInFlight = new Set();
|
|
274
|
+
retryTaskInFlight = new Set();
|
|
257
275
|
cancelCleanupInFlight = new Map();
|
|
258
276
|
deletionInFlight = new Set();
|
|
259
277
|
divergedAgents = new Set();
|
|
@@ -273,6 +291,7 @@ export class AgentManager {
|
|
|
273
291
|
this.paneStreamerManager = deps.paneStreamerManager;
|
|
274
292
|
this.pluginRegistry = deps.pluginRegistry;
|
|
275
293
|
this.platformDefaultBranchOf = deps.platformDefaultBranchOf;
|
|
294
|
+
this.prConversationCache = deps.prConversationCache;
|
|
276
295
|
this.errorRecordStore = deps.errorRecordStore;
|
|
277
296
|
this.phaseSignalWatcher = deps.phaseSignalWatcher
|
|
278
297
|
?? (deps.paneStreamerManager
|
|
@@ -299,10 +318,26 @@ export class AgentManager {
|
|
|
299
318
|
};
|
|
300
319
|
}
|
|
301
320
|
withTaskLock(fn) {
|
|
302
|
-
const
|
|
321
|
+
const inherited = this.taskMutationScope.getStore();
|
|
322
|
+
if (inherited?.active)
|
|
323
|
+
return fn();
|
|
324
|
+
const next = this.taskMutationQueue.then(() => {
|
|
325
|
+
const scope = { active: true };
|
|
326
|
+
return this.taskMutationScope.run(scope, async () => {
|
|
327
|
+
try {
|
|
328
|
+
return await fn();
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
scope.active = false;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
303
335
|
this.taskMutationQueue = next.catch(() => undefined);
|
|
304
336
|
return next;
|
|
305
337
|
}
|
|
338
|
+
outsideTaskMutationScope(fn) {
|
|
339
|
+
return this.taskMutationScope.exit(fn);
|
|
340
|
+
}
|
|
306
341
|
withAgentOperationLease(agentId, fn) {
|
|
307
342
|
const previous = this.agentOperationQueues.get(agentId) ?? Promise.resolve();
|
|
308
343
|
const next = previous.then(fn);
|
|
@@ -365,7 +400,7 @@ export class AgentManager {
|
|
|
365
400
|
return true;
|
|
366
401
|
}
|
|
367
402
|
catch (err) {
|
|
368
|
-
console.warn(`[AgentManager] safeEmit ${event.type} failed
|
|
403
|
+
console.warn(`[AgentManager] safeEmit ${event.type} failed; persistence or handler delivery may be incomplete:`, err);
|
|
369
404
|
return false;
|
|
370
405
|
}
|
|
371
406
|
}
|
|
@@ -911,7 +946,7 @@ export class AgentManager {
|
|
|
911
946
|
}
|
|
912
947
|
}
|
|
913
948
|
await this.markDialogPending(agentId, creationToken);
|
|
914
|
-
void this.slowPollDialogPending(agentId, creationToken).catch((pollErr) => {
|
|
949
|
+
void this.outsideTaskMutationScope(() => this.slowPollDialogPending(agentId, creationToken)).catch((pollErr) => {
|
|
915
950
|
console.warn(`[bootstrap] slowPoll for ${agentId} crashed:`, pollErr);
|
|
916
951
|
});
|
|
917
952
|
return;
|
|
@@ -1184,10 +1219,10 @@ export class AgentManager {
|
|
|
1184
1219
|
err.partial.handled = true;
|
|
1185
1220
|
const snapshotPaneId = state.paneId;
|
|
1186
1221
|
const snapshotTaskId = state.taskId;
|
|
1187
|
-
void this.slowPollDialogPending(agentId, state.creationToken, {
|
|
1222
|
+
void this.outsideTaskMutationScope(() => this.slowPollDialogPending(agentId, state.creationToken, {
|
|
1188
1223
|
...(snapshotPaneId !== undefined ? { expectedPaneId: snapshotPaneId } : {}),
|
|
1189
1224
|
expectedTaskId: snapshotTaskId,
|
|
1190
|
-
}).catch((pollErr) => {
|
|
1225
|
+
})).catch((pollErr) => {
|
|
1191
1226
|
console.warn(`[runtime] slowPoll for ${agentId} crashed:`, pollErr);
|
|
1192
1227
|
});
|
|
1193
1228
|
return true;
|
|
@@ -1255,6 +1290,7 @@ export class AgentManager {
|
|
|
1255
1290
|
return;
|
|
1256
1291
|
const now = new Date().toISOString();
|
|
1257
1292
|
let projectIdForEmit = '';
|
|
1293
|
+
let taskIdForEmit;
|
|
1258
1294
|
let wrote = false;
|
|
1259
1295
|
const isBootstrapPath = creationToken !== undefined;
|
|
1260
1296
|
if (isBootstrapPath && !(await this.runGreetingHandshake(agentId, cfg, pane))) {
|
|
@@ -1270,6 +1306,7 @@ export class AgentManager {
|
|
|
1270
1306
|
return AGENT_STORE_NOOP;
|
|
1271
1307
|
}
|
|
1272
1308
|
projectIdForEmit = fresh.projectId;
|
|
1309
|
+
taskIdForEmit = fresh.taskId;
|
|
1273
1310
|
wrote = true;
|
|
1274
1311
|
if (isBootstrapPath) {
|
|
1275
1312
|
return {
|
|
@@ -1283,11 +1320,15 @@ export class AgentManager {
|
|
|
1283
1320
|
updatedAt: now,
|
|
1284
1321
|
};
|
|
1285
1322
|
}
|
|
1323
|
+
const awaitingReason = fresh.taskId
|
|
1324
|
+
? `Runtime dialog resolved; agent REPL ready. Open task ${fresh.taskId}: cancel it if it is still active, ` +
|
|
1325
|
+
'otherwise Resume can release the binding; Delete the agent if the binding cannot be recovered.'
|
|
1326
|
+
: 'Runtime dialog resolved; agent REPL ready. Click Resume to release the binding and let baxian pick the next task.';
|
|
1286
1327
|
return {
|
|
1287
1328
|
...fresh,
|
|
1288
1329
|
paneId,
|
|
1289
1330
|
awaitingPhase: 'agent_dialog_resolved_runtime',
|
|
1290
|
-
awaitingReason
|
|
1331
|
+
awaitingReason,
|
|
1291
1332
|
updatedAt: now,
|
|
1292
1333
|
};
|
|
1293
1334
|
});
|
|
@@ -1304,9 +1345,13 @@ export class AgentManager {
|
|
|
1304
1345
|
});
|
|
1305
1346
|
}
|
|
1306
1347
|
else {
|
|
1307
|
-
|
|
1348
|
+
const note = taskIdForEmit
|
|
1349
|
+
? `Runtime dialog resolved; agent REPL ready. Open task ${taskIdForEmit}: cancel it if it is still active, ` +
|
|
1350
|
+
'otherwise Resume can release the binding; Delete the agent if the binding cannot be recovered.'
|
|
1351
|
+
: 'Runtime dialog resolved; agent REPL ready. Click Resume to continue.';
|
|
1352
|
+
await this.emitIntervention(projectIdForEmit, agentId, taskIdForEmit, {
|
|
1308
1353
|
phase: 'agent_dialog_resolved_runtime',
|
|
1309
|
-
note
|
|
1354
|
+
note,
|
|
1310
1355
|
});
|
|
1311
1356
|
}
|
|
1312
1357
|
return;
|
|
@@ -1317,7 +1362,7 @@ export class AgentManager {
|
|
|
1317
1362
|
return;
|
|
1318
1363
|
const controller = new AbortController();
|
|
1319
1364
|
this.runtimeMenuWatchers.set(agentId, controller);
|
|
1320
|
-
void this.runtimeMenuWatchLoop(agentId, controller.signal)
|
|
1365
|
+
void this.outsideTaskMutationScope(() => this.runtimeMenuWatchLoop(agentId, controller.signal))
|
|
1321
1366
|
.catch((err) => {
|
|
1322
1367
|
console.warn(`[runtimeMenuWatch] ${agentId} loop crashed:`, err);
|
|
1323
1368
|
})
|
|
@@ -1378,6 +1423,10 @@ export class AgentManager {
|
|
|
1378
1423
|
}
|
|
1379
1424
|
else if (!onMenu && pendingMenu) {
|
|
1380
1425
|
pendingMenu = false;
|
|
1426
|
+
await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
|
|
1427
|
+
phase: 'agent_runtime_menu_resolved',
|
|
1428
|
+
previousPhase: 'agent_runtime_menu_pending',
|
|
1429
|
+
});
|
|
1381
1430
|
}
|
|
1382
1431
|
}
|
|
1383
1432
|
}
|
|
@@ -2272,7 +2321,7 @@ export class AgentManager {
|
|
|
2272
2321
|
if (this.needInputRetryTimer)
|
|
2273
2322
|
return;
|
|
2274
2323
|
this.needInputRetryTimer = setInterval(() => {
|
|
2275
|
-
void this.needInputRetryPass();
|
|
2324
|
+
void this.outsideTaskMutationScope(() => this.needInputRetryPass());
|
|
2276
2325
|
}, this.needInputRetryIntervalMs);
|
|
2277
2326
|
this.needInputRetryTimer.unref?.();
|
|
2278
2327
|
}
|
|
@@ -2464,13 +2513,13 @@ export class AgentManager {
|
|
|
2464
2513
|
if (wm !== null && bumped && mode === 'restore') {
|
|
2465
2514
|
const delta = this.collectPredecessorNeedInputDelta(args.agentId, args.taskId, wm.epoch);
|
|
2466
2515
|
if (delta) {
|
|
2467
|
-
void this.commitNeedInputWatermark({
|
|
2516
|
+
void this.outsideTaskMutationScope(() => this.commitNeedInputWatermark({
|
|
2468
2517
|
agentId: args.agentId,
|
|
2469
2518
|
taskId: args.taskId,
|
|
2470
2519
|
epoch: wm.epoch,
|
|
2471
2520
|
askSeq: Math.max(delta.askSeq, wm.askSeq),
|
|
2472
2521
|
answeredSeq: Math.max(delta.answeredSeq, wm.answeredSeq),
|
|
2473
|
-
});
|
|
2522
|
+
}));
|
|
2474
2523
|
}
|
|
2475
2524
|
}
|
|
2476
2525
|
try {
|
|
@@ -3762,8 +3811,8 @@ export class AgentManager {
|
|
|
3762
3811
|
const project = this.getProjectConfig(cfg.projectId);
|
|
3763
3812
|
if (!project)
|
|
3764
3813
|
throw new Error(`Unknown project: ${cfg.projectId}`);
|
|
3765
|
-
const
|
|
3766
|
-
return { targets:
|
|
3814
|
+
const team = this.findAgentTeam(agentId);
|
|
3815
|
+
return { targets: team?.map(agent => agent.id) ?? [agentId] };
|
|
3767
3816
|
}
|
|
3768
3817
|
async cleanupRemovedAgentRuntime(targets) {
|
|
3769
3818
|
const failures = [];
|
|
@@ -3862,13 +3911,13 @@ export class AgentManager {
|
|
|
3862
3911
|
const now = new Date().toISOString();
|
|
3863
3912
|
if (cfg.role !== 'dev')
|
|
3864
3913
|
throw new Error(`Agent ${cfg.id} is not a dev agent`);
|
|
3865
|
-
const
|
|
3914
|
+
const team = this.findAgentTeam(cfg.id);
|
|
3866
3915
|
const devAgentId = cfg.id;
|
|
3867
3916
|
const qaAgentId = input.preferredAgentId === ''
|
|
3868
3917
|
? '_unassigned_preview_qa'
|
|
3869
|
-
:
|
|
3918
|
+
: team?.find(agent => agent.role === 'qa')?.id;
|
|
3870
3919
|
if (!qaAgentId)
|
|
3871
|
-
throw new Error(`Agent ${cfg.id} has no qa agent in its
|
|
3920
|
+
throw new Error(`Agent ${cfg.id} has no qa agent in its Agent Team`);
|
|
3872
3921
|
const fakeTask = {
|
|
3873
3922
|
id: 'task-9999999999',
|
|
3874
3923
|
projectId,
|
|
@@ -3910,11 +3959,11 @@ export class AgentManager {
|
|
|
3910
3959
|
return undefined;
|
|
3911
3960
|
return buildProjectDriver(project, this.pluginRegistry, this.platformDriverExec);
|
|
3912
3961
|
}
|
|
3913
|
-
|
|
3962
|
+
findAgentTeam(anchorAgentId) {
|
|
3914
3963
|
for (const project of this.config.project) {
|
|
3915
|
-
for (const
|
|
3916
|
-
if (
|
|
3917
|
-
return
|
|
3964
|
+
for (const team of project.agent) {
|
|
3965
|
+
if (team.some(agent => agent.id === anchorAgentId))
|
|
3966
|
+
return team;
|
|
3918
3967
|
}
|
|
3919
3968
|
}
|
|
3920
3969
|
return undefined;
|
|
@@ -3925,8 +3974,8 @@ export class AgentManager {
|
|
|
3925
3974
|
return undefined;
|
|
3926
3975
|
let longest;
|
|
3927
3976
|
let longestBytes = -1;
|
|
3928
|
-
for (const
|
|
3929
|
-
for (const a of
|
|
3977
|
+
for (const team of project.agent) {
|
|
3978
|
+
for (const a of team) {
|
|
3930
3979
|
if (a.role !== 'dev' || !a.workdir)
|
|
3931
3980
|
continue;
|
|
3932
3981
|
const bytes = Buffer.byteLength(a.workdir, 'utf8');
|
|
@@ -3941,6 +3990,99 @@ export class AgentManager {
|
|
|
3941
3990
|
async getTask(taskId) {
|
|
3942
3991
|
return this.taskStore.get(taskId);
|
|
3943
3992
|
}
|
|
3993
|
+
async recordTaskAttention(event) {
|
|
3994
|
+
if (!event.taskId)
|
|
3995
|
+
return;
|
|
3996
|
+
await this.withTaskLock(async () => {
|
|
3997
|
+
const task = await this.taskStore.get(event.taskId);
|
|
3998
|
+
if (!task)
|
|
3999
|
+
return;
|
|
4000
|
+
const reasonValue = typeof event.data.phase === 'string'
|
|
4001
|
+
? event.data.phase
|
|
4002
|
+
: typeof event.data.reason === 'string'
|
|
4003
|
+
? event.data.reason
|
|
4004
|
+
: 'human-intervention';
|
|
4005
|
+
const reason = reasonValue.trim() || 'human-intervention';
|
|
4006
|
+
const previousPhase = typeof event.data.previousPhase === 'string'
|
|
4007
|
+
? event.data.previousPhase.trim()
|
|
4008
|
+
: '';
|
|
4009
|
+
const clearedReason = reason === 'mr-reopened'
|
|
4010
|
+
? 'mr-closed-unmerged'
|
|
4011
|
+
: reason === 'resumed'
|
|
4012
|
+
|| reason === 'git-review-dispatch-hold-cleared'
|
|
4013
|
+
|| reason === 'agent_runtime_menu_resolved'
|
|
4014
|
+
? previousPhase
|
|
4015
|
+
: '';
|
|
4016
|
+
if (clearedReason) {
|
|
4017
|
+
if (task.attention?.reason !== clearedReason
|
|
4018
|
+
|| Date.parse(event.timestamp) < Date.parse(task.attention.occurredAt))
|
|
4019
|
+
return;
|
|
4020
|
+
const next = { ...task, updatedAt: new Date().toISOString() };
|
|
4021
|
+
delete next.attention;
|
|
4022
|
+
await this.taskStore.set(next);
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
if (reason === 'resumed'
|
|
4026
|
+
|| reason === 'git-review-dispatch-hold-cleared'
|
|
4027
|
+
|| reason === 'agent_runtime_menu_resolved')
|
|
4028
|
+
return;
|
|
4029
|
+
const runbookValue = [
|
|
4030
|
+
event.data.note,
|
|
4031
|
+
event.data.recommendation,
|
|
4032
|
+
event.data.message,
|
|
4033
|
+
event.data.error,
|
|
4034
|
+
...(typeof event.data.phase === 'string' ? [event.data.reason] : []),
|
|
4035
|
+
]
|
|
4036
|
+
.find(value => typeof value === 'string' && value.trim() !== '');
|
|
4037
|
+
const runbook = typeof runbookValue === 'string'
|
|
4038
|
+
? runbookValue.trim()
|
|
4039
|
+
: `Task ${task.id} requires human intervention for ${reason}. Inspect the task and its linked agent before choosing an action.`;
|
|
4040
|
+
const occurredAt = Number.isFinite(Date.parse(event.timestamp))
|
|
4041
|
+
? event.timestamp
|
|
4042
|
+
: new Date().toISOString();
|
|
4043
|
+
const generation = taskAttentionGeneration(task);
|
|
4044
|
+
let recommendedActions;
|
|
4045
|
+
if (task.status === 'max_rounds') {
|
|
4046
|
+
recommendedActions = ['verdict', 'cancel'];
|
|
4047
|
+
}
|
|
4048
|
+
else if (TERMINAL_STATUSES.includes(task.status)) {
|
|
4049
|
+
recommendedActions = task.remoteCleanup !== undefined
|
|
4050
|
+
&& task.remoteCleanup.stage !== 'manual'
|
|
4051
|
+
? ['cancel']
|
|
4052
|
+
: ['retry'];
|
|
4053
|
+
}
|
|
4054
|
+
else if (task.status === 'review') {
|
|
4055
|
+
recommendedActions = isSpecStagePhase(task.phase)
|
|
4056
|
+
? ['advance', 'cancel']
|
|
4057
|
+
: ['advance', 'verdict', 'cancel'];
|
|
4058
|
+
}
|
|
4059
|
+
else if (task.status === 'spec-ready' || task.status === 'merge-ready') {
|
|
4060
|
+
recommendedActions = ['verdict', 'cancel'];
|
|
4061
|
+
}
|
|
4062
|
+
else {
|
|
4063
|
+
recommendedActions = ['advance', 'cancel'];
|
|
4064
|
+
}
|
|
4065
|
+
if (task.attention
|
|
4066
|
+
&& Date.parse(task.attention.occurredAt) > Date.parse(occurredAt))
|
|
4067
|
+
return;
|
|
4068
|
+
if (task.attention
|
|
4069
|
+
&& task.attention.reason === reason
|
|
4070
|
+
&& task.attention.runbook === runbook
|
|
4071
|
+
&& Date.parse(task.attention.occurredAt) >= Date.parse(occurredAt))
|
|
4072
|
+
return;
|
|
4073
|
+
await this.taskStore.set({
|
|
4074
|
+
...task,
|
|
4075
|
+
attention: {
|
|
4076
|
+
reason,
|
|
4077
|
+
runbook,
|
|
4078
|
+
occurredAt,
|
|
4079
|
+
recommendedActions,
|
|
4080
|
+
generation,
|
|
4081
|
+
},
|
|
4082
|
+
updatedAt: new Date().toISOString(),
|
|
4083
|
+
});
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
3944
4086
|
async getAgentState(agentId) {
|
|
3945
4087
|
return this.agentStore.get(agentId);
|
|
3946
4088
|
}
|
|
@@ -4027,15 +4169,30 @@ export class AgentManager {
|
|
|
4027
4169
|
delete next.reviewDispatch;
|
|
4028
4170
|
delete next.reviewRoundPending;
|
|
4029
4171
|
}
|
|
4030
|
-
if (nextStatus !== task.status
|
|
4031
|
-
|
|
4032
|
-
if (outbox.length > 0)
|
|
4033
|
-
next.outbox = outbox;
|
|
4034
|
-
else
|
|
4035
|
-
delete next.outbox;
|
|
4036
|
-
}
|
|
4172
|
+
if (nextStatus !== task.status)
|
|
4173
|
+
this.stripGitVerdictOutbox(next);
|
|
4037
4174
|
return next;
|
|
4038
4175
|
}
|
|
4176
|
+
stripGitVerdictOutbox(task) {
|
|
4177
|
+
if (!task.outbox)
|
|
4178
|
+
return;
|
|
4179
|
+
const outbox = task.outbox.filter(entry => entry.type !== 'git.spec-verdict' && entry.type !== 'git.code-verdict');
|
|
4180
|
+
if (outbox.length > 0)
|
|
4181
|
+
task.outbox = outbox;
|
|
4182
|
+
else
|
|
4183
|
+
delete task.outbox;
|
|
4184
|
+
}
|
|
4185
|
+
stripCodeVerdictOutboxForAnchor(task, anchorSha) {
|
|
4186
|
+
if (!task.outbox)
|
|
4187
|
+
return;
|
|
4188
|
+
const normalizedAnchor = anchorSha.toLowerCase();
|
|
4189
|
+
const outbox = task.outbox.filter(entry => entry.type !== 'git.code-verdict'
|
|
4190
|
+
|| entry.data.anchorSha.toLowerCase() === normalizedAnchor);
|
|
4191
|
+
if (outbox.length > 0)
|
|
4192
|
+
task.outbox = outbox;
|
|
4193
|
+
else
|
|
4194
|
+
delete task.outbox;
|
|
4195
|
+
}
|
|
4039
4196
|
async confirmPostApprovePromptDelivered(taskId, expected) {
|
|
4040
4197
|
await this.withTaskLock(async () => {
|
|
4041
4198
|
const task = await this.taskStore.get(taskId);
|
|
@@ -4229,6 +4386,9 @@ export class AgentManager {
|
|
|
4229
4386
|
specVerdictOutbox(task) {
|
|
4230
4387
|
return task.outbox?.find((entry) => entry.type === 'git.spec-verdict');
|
|
4231
4388
|
}
|
|
4389
|
+
codeVerdictOutbox(task) {
|
|
4390
|
+
return task.outbox?.find((entry) => entry.type === 'git.code-verdict');
|
|
4391
|
+
}
|
|
4232
4392
|
async mutateTaskOutboxEntry(taskId, key, mutate) {
|
|
4233
4393
|
return this.withTaskLock(async () => {
|
|
4234
4394
|
const task = await this.taskStore.get(taskId);
|
|
@@ -4258,6 +4418,10 @@ export class AgentManager {
|
|
|
4258
4418
|
await this.deliverSpecVerdictOutbox(taskId, entry.key);
|
|
4259
4419
|
continue;
|
|
4260
4420
|
}
|
|
4421
|
+
if (entry.type === 'git.code-verdict') {
|
|
4422
|
+
await this.deliverCodeVerdictOutbox(taskId, entry.key);
|
|
4423
|
+
continue;
|
|
4424
|
+
}
|
|
4261
4425
|
await this.eventBus.emit({
|
|
4262
4426
|
id: `outbox:${entry.key}`,
|
|
4263
4427
|
type: 'human.intervention',
|
|
@@ -4379,6 +4543,10 @@ export class AgentManager {
|
|
|
4379
4543
|
return null;
|
|
4380
4544
|
if (Object.hasOwn(opts, 'expectPhase') && current.phase !== opts.expectPhase)
|
|
4381
4545
|
return null;
|
|
4546
|
+
if (current.status === 'review'
|
|
4547
|
+
&& current.reviewHeadAnchorSha?.toLowerCase() === opts.headSha.toLowerCase()
|
|
4548
|
+
&& this.codeVerdictOutbox(current) !== undefined)
|
|
4549
|
+
return null;
|
|
4382
4550
|
if (opts.patch?.branch && opts.patch.branch !== current.branch) {
|
|
4383
4551
|
const existing = await this.findTaskByBranch(opts.patch.branch, current.projectId);
|
|
4384
4552
|
if (existing && existing.id !== taskId)
|
|
@@ -4399,6 +4567,7 @@ export class AgentManager {
|
|
|
4399
4567
|
? 'review'
|
|
4400
4568
|
: 'recheck');
|
|
4401
4569
|
const next = this.stripPostApproveEpisodeFields(current);
|
|
4570
|
+
this.stripGitVerdictOutbox(next);
|
|
4402
4571
|
delete next.passProvenance;
|
|
4403
4572
|
Object.assign(next, opts.patch ?? {}, {
|
|
4404
4573
|
status: 'review',
|
|
@@ -4468,6 +4637,7 @@ export class AgentManager {
|
|
|
4468
4637
|
return null;
|
|
4469
4638
|
const now = new Date().toISOString();
|
|
4470
4639
|
const next = this.stripPostApproveEpisodeFields(current);
|
|
4640
|
+
this.stripGitVerdictOutbox(next);
|
|
4471
4641
|
delete next.reviewDispatch;
|
|
4472
4642
|
delete next.reviewRoundPending;
|
|
4473
4643
|
const postApproveGeneration = createSignalToken();
|
|
@@ -4567,6 +4737,7 @@ export class AgentManager {
|
|
|
4567
4737
|
},
|
|
4568
4738
|
updatedAt: now,
|
|
4569
4739
|
};
|
|
4740
|
+
this.stripCodeVerdictOutboxForAnchor(next, verifiedHeadSha);
|
|
4570
4741
|
await this.taskStore.set(next);
|
|
4571
4742
|
if (qaHold !== undefined) {
|
|
4572
4743
|
const cleared = await this.clearAwaitingHuman(qaId, {
|
|
@@ -4612,6 +4783,9 @@ export class AgentManager {
|
|
|
4612
4783
|
}
|
|
4613
4784
|
next.reviewDispatchedAt = now;
|
|
4614
4785
|
delete next.reviewDispatch;
|
|
4786
|
+
if (next.attention?.reason === 'git-review-dispatch-recovery-uncertain') {
|
|
4787
|
+
delete next.attention;
|
|
4788
|
+
}
|
|
4615
4789
|
await this.taskStore.set(next);
|
|
4616
4790
|
return true;
|
|
4617
4791
|
});
|
|
@@ -5100,20 +5274,28 @@ export class AgentManager {
|
|
|
5100
5274
|
return next;
|
|
5101
5275
|
});
|
|
5102
5276
|
}
|
|
5103
|
-
async noteReviewConversationRevision(taskId) {
|
|
5277
|
+
async noteReviewConversationRevision(taskId, conversation) {
|
|
5104
5278
|
await this.withTaskLock(async () => {
|
|
5105
5279
|
const task = await this.taskStore.get(taskId);
|
|
5106
5280
|
if (!task || TERMINAL_STATUSES.includes(task.status))
|
|
5107
5281
|
return;
|
|
5108
5282
|
const now = new Date().toISOString();
|
|
5109
|
-
|
|
5283
|
+
const next = { ...task, reviewConversationUpdatedAt: now, updatedAt: now };
|
|
5284
|
+
const repoKey = next.platformBinding?.repoKey;
|
|
5285
|
+
if (conversation !== undefined
|
|
5286
|
+
&& this.prConversationCache !== undefined
|
|
5287
|
+
&& repoKey !== undefined
|
|
5288
|
+
&& next.prNumber === conversation.prNumber) {
|
|
5289
|
+
this.prConversationCache.put(next.id, prReviewCacheRevision(next, repoKey), conversation.payload, next.reviewConversationUpdatedAt ?? '');
|
|
5290
|
+
}
|
|
5291
|
+
await this.taskStore.set(next);
|
|
5110
5292
|
});
|
|
5111
5293
|
}
|
|
5112
5294
|
assertPlatformBinding(task) {
|
|
5113
5295
|
const mismatch = platformBindingMismatch(this.config, task);
|
|
5114
5296
|
if (mismatch === undefined)
|
|
5115
5297
|
return;
|
|
5116
|
-
void this.emitPlatformBindingIntervention(task, mismatch);
|
|
5298
|
+
void this.outsideTaskMutationScope(() => this.emitPlatformBindingIntervention(task, mismatch));
|
|
5117
5299
|
if (mismatch.reason === 'missing-binding-snapshot') {
|
|
5118
5300
|
throw new Error(`platform binding missing for task ${task.id}; refusing platform operations`);
|
|
5119
5301
|
}
|
|
@@ -5650,8 +5832,12 @@ export class AgentManager {
|
|
|
5650
5832
|
return null;
|
|
5651
5833
|
if (task.prNumber !== undefined && task.prNumber !== observation.prNumber)
|
|
5652
5834
|
return null;
|
|
5835
|
+
const repairsDeliveryConfirmation = confirmation?.source === 'human'
|
|
5836
|
+
&& (task.status === 'review' || task.status === 'fixing')
|
|
5837
|
+
&& Object.hasOwn(confirmation, 'expectedSignalToken');
|
|
5653
5838
|
if (confirmation !== undefined) {
|
|
5654
5839
|
if (task.status !== 'in_progress'
|
|
5840
|
+
&& !repairsDeliveryConfirmation
|
|
5655
5841
|
&& (task.phase !== confirmation.phase
|
|
5656
5842
|
|| task.deliveryConfirmation?.phase !== confirmation.phase))
|
|
5657
5843
|
return null;
|
|
@@ -5678,7 +5864,7 @@ export class AgentManager {
|
|
|
5678
5864
|
...(confirmation !== undefined
|
|
5679
5865
|
? {
|
|
5680
5866
|
phase: confirmation.phase,
|
|
5681
|
-
deliveryConfirmation: task.status === 'in_progress'
|
|
5867
|
+
deliveryConfirmation: task.status === 'in_progress' || repairsDeliveryConfirmation
|
|
5682
5868
|
? { phase: confirmation.phase, source: confirmation.source, at: now }
|
|
5683
5869
|
: task.deliveryConfirmation,
|
|
5684
5870
|
...(confirmation.branch !== undefined ? { branch: confirmation.branch } : {}),
|
|
@@ -5930,13 +6116,13 @@ export class AgentManager {
|
|
|
5930
6116
|
}
|
|
5931
6117
|
const genAtEntry = this.deletionGenerationOf(input.preferredAgentId);
|
|
5932
6118
|
const target = await this.pickAgent(projectId, input.preferredAgentId);
|
|
5933
|
-
const
|
|
5934
|
-
const dev =
|
|
6119
|
+
const team = this.findAgentTeam(input.preferredAgentId);
|
|
6120
|
+
const dev = team?.find(agent => agent.role === 'dev');
|
|
5935
6121
|
if (!dev)
|
|
5936
|
-
throw new ApiError(409, `Agent ${input.preferredAgentId} has no dev agent in its
|
|
5937
|
-
const qa =
|
|
6122
|
+
throw new ApiError(409, `Agent ${input.preferredAgentId} has no dev agent in its Agent Team`);
|
|
6123
|
+
const qa = team?.find(agent => agent.role === 'qa');
|
|
5938
6124
|
if (!qa)
|
|
5939
|
-
throw new ApiError(409, `Agent ${input.preferredAgentId} has no qa agent in its
|
|
6125
|
+
throw new ApiError(409, `Agent ${input.preferredAgentId} has no qa agent in its Agent Team`);
|
|
5940
6126
|
const participantGens = new Map([dev.id, qa.id]
|
|
5941
6127
|
.map(id => [id, this.deletionGenerationOf(id)]));
|
|
5942
6128
|
const participantsFresh = () => this.deletionGenerationOf(input.preferredAgentId) === genAtEntry
|
|
@@ -5951,7 +6137,7 @@ export class AgentManager {
|
|
|
5951
6137
|
};
|
|
5952
6138
|
const persistQueuedOrReject = (reason) => {
|
|
5953
6139
|
if (!participantsFresh()) {
|
|
5954
|
-
throw new ApiError(409, `Agent ${input.preferredAgentId} or
|
|
6140
|
+
throw new ApiError(409, `Agent ${input.preferredAgentId} or an Agent Team member is being deleted or recreated during task creation; please retry`);
|
|
5955
6141
|
}
|
|
5956
6142
|
return this.persistQueuedTask(queued, reason, input.preferredAgentId);
|
|
5957
6143
|
};
|
|
@@ -6061,6 +6247,9 @@ export class AgentManager {
|
|
|
6061
6247
|
}
|
|
6062
6248
|
async createAndStartTask(projectId, input, opts = {}) {
|
|
6063
6249
|
const task = await this.createTask(projectId, input);
|
|
6250
|
+
return this.startCreatedTask(task, opts);
|
|
6251
|
+
}
|
|
6252
|
+
async startCreatedTask(task, opts = {}) {
|
|
6064
6253
|
if (task.status === 'in_progress' && task.agentId) {
|
|
6065
6254
|
const signalToken = createSignalToken();
|
|
6066
6255
|
await this.updateTask(task.id, this.dispatchTokenFields(signalToken));
|
|
@@ -6238,7 +6427,7 @@ export class AgentManager {
|
|
|
6238
6427
|
await tmux.sendKeysLiteral(pane, command);
|
|
6239
6428
|
await tmux.sendEnter(pane);
|
|
6240
6429
|
guardHandedOff = true;
|
|
6241
|
-
void this.waitForReplPromptReady(tmux, pane, cfg.runtime, this.compactIdleWaitMs)
|
|
6430
|
+
void this.outsideTaskMutationScope(() => this.waitForReplPromptReady(tmux, pane, cfg.runtime, this.compactIdleWaitMs))
|
|
6242
6431
|
.catch(err => {
|
|
6243
6432
|
console.warn(`[AgentManager] sendSlashCommand(${agentId}, ${command}) post idle wait failed:`, err);
|
|
6244
6433
|
})
|
|
@@ -6281,6 +6470,16 @@ export class AgentManager {
|
|
|
6281
6470
|
clearPendingDispatchRetry(taskId) {
|
|
6282
6471
|
this.pendingDispatchRetryByTask.delete(taskId);
|
|
6283
6472
|
}
|
|
6473
|
+
resetPendingDispatchRetryBudget(taskId) {
|
|
6474
|
+
const entry = this.pendingDispatchRetryByTask.get(taskId);
|
|
6475
|
+
if (!entry)
|
|
6476
|
+
return;
|
|
6477
|
+
this.pendingDispatchRetryByTask.set(taskId, {
|
|
6478
|
+
...entry,
|
|
6479
|
+
since: Date.now(),
|
|
6480
|
+
budgetAlerted: undefined,
|
|
6481
|
+
});
|
|
6482
|
+
}
|
|
6284
6483
|
clearPendingDispatchRetryIfMatches(taskId, expected) {
|
|
6285
6484
|
const entry = this.pendingDispatchRetryByTask.get(taskId);
|
|
6286
6485
|
if (entry?.agentId === expected.agentId && entry.signalToken === expected.signalToken) {
|
|
@@ -6450,20 +6649,20 @@ export class AgentManager {
|
|
|
6450
6649
|
}
|
|
6451
6650
|
const now = new Date().toISOString();
|
|
6452
6651
|
const initiallyUnassigned = fresh.preferredAgentId === '';
|
|
6453
|
-
const
|
|
6652
|
+
const team = initiallyUnassigned ? this.findAgentTeam(agentId) : undefined;
|
|
6454
6653
|
const devId = initiallyUnassigned
|
|
6455
|
-
?
|
|
6654
|
+
? team?.find(agent => agent.role === 'dev')?.id
|
|
6456
6655
|
: fresh.devAgentId;
|
|
6457
6656
|
if (!devId) {
|
|
6458
6657
|
await this.lockManager.releaseIfOwner(agentId, taskId, lockToken);
|
|
6459
|
-
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no dev agent in its
|
|
6658
|
+
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no dev agent in its Agent Team` };
|
|
6460
6659
|
}
|
|
6461
6660
|
const qaId = initiallyUnassigned
|
|
6462
|
-
?
|
|
6661
|
+
? team?.find(agent => agent.role === 'qa')?.id
|
|
6463
6662
|
: fresh.qaAgentId;
|
|
6464
6663
|
if (!qaId) {
|
|
6465
6664
|
await this.lockManager.releaseIfOwner(agentId, taskId, lockToken);
|
|
6466
|
-
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no qa agent in its
|
|
6665
|
+
return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no qa agent in its Agent Team` };
|
|
6467
6666
|
}
|
|
6468
6667
|
const participantGens = new Map([devId, qaId]
|
|
6469
6668
|
.map(pid => [pid, this.deletionGenerationOf(pid)]));
|
|
@@ -7780,10 +7979,10 @@ export class AgentManager {
|
|
|
7780
7979
|
if (await this.rollbackUndeliveredBootstrap(state))
|
|
7781
7980
|
continue;
|
|
7782
7981
|
await this.markDialogPending(state.id, state.creationToken);
|
|
7783
|
-
void this.slowPollDialogPending(state.id, state.creationToken, {
|
|
7982
|
+
void this.outsideTaskMutationScope(() => this.slowPollDialogPending(state.id, state.creationToken, {
|
|
7784
7983
|
...(state.paneId !== undefined ? { expectedPaneId: state.paneId } : {}),
|
|
7785
7984
|
expectedTaskId: state.taskId,
|
|
7786
|
-
}).catch((pollErr) => {
|
|
7985
|
+
})).catch((pollErr) => {
|
|
7787
7986
|
console.warn(`[recover] slowPoll for ${state.id} crashed:`, pollErr);
|
|
7788
7987
|
});
|
|
7789
7988
|
continue;
|
|
@@ -8369,6 +8568,169 @@ export class AgentManager {
|
|
|
8369
8568
|
return dispatched;
|
|
8370
8569
|
}
|
|
8371
8570
|
}
|
|
8571
|
+
async advanceTask(taskId, opts = {}) {
|
|
8572
|
+
let task = await this.taskStore.get(taskId);
|
|
8573
|
+
if (!task)
|
|
8574
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
8575
|
+
const executor = opts.executor ?? (task.status === 'review' ? 'qa' : 'dev');
|
|
8576
|
+
let updated;
|
|
8577
|
+
if (task.status === 'pending') {
|
|
8578
|
+
if (executor !== 'dev')
|
|
8579
|
+
throw new ApiError(409, 'Pending tasks can only advance to Dev');
|
|
8580
|
+
const result = await this.dispatchPendingTask(taskId, opts.agentId);
|
|
8581
|
+
if (result.errorCode !== undefined)
|
|
8582
|
+
throw new ApiError(result.errorCode, result.error ?? 'Dispatch failed');
|
|
8583
|
+
if (!result.task)
|
|
8584
|
+
throw new ApiError(404, `Task ${taskId} disappeared during dispatch`);
|
|
8585
|
+
updated = result.task;
|
|
8586
|
+
}
|
|
8587
|
+
else if (executor === 'qa') {
|
|
8588
|
+
if (task.status !== 'in_progress' && task.status !== 'fixing' && task.status !== 'review') {
|
|
8589
|
+
throw new ApiError(409, `Task ${taskId} cannot advance to QA from ${task.status}`);
|
|
8590
|
+
}
|
|
8591
|
+
updated = await this.dispatchReviewToQa(taskId, {
|
|
8592
|
+
fromStatus: [task.status],
|
|
8593
|
+
confirmUncertainNotDelivered: true,
|
|
8594
|
+
...(opts.stage !== undefined ? { stage: opts.stage } : {}),
|
|
8595
|
+
...(opts.actorId !== undefined ? { actorId: opts.actorId } : {}),
|
|
8596
|
+
...(opts.prNumber !== undefined ? { prNumber: opts.prNumber } : {}),
|
|
8597
|
+
});
|
|
8598
|
+
}
|
|
8599
|
+
else if (task.status === 'review') {
|
|
8600
|
+
throw new ApiError(409, 'Review cannot advance to Dev without a request-changes verdict');
|
|
8601
|
+
}
|
|
8602
|
+
else if (task.status === 'in_progress' || task.status === 'fixing') {
|
|
8603
|
+
if (!task.devAgentId)
|
|
8604
|
+
throw new ApiError(409, `Task ${taskId} has no Dev participant`);
|
|
8605
|
+
const replayed = await this.redispatchTaskPromptAfterReplRestart(task.devAgentId, taskId);
|
|
8606
|
+
if (!replayed)
|
|
8607
|
+
throw new ApiError(409, `Task ${taskId} changed or its Dev prompt could not be replayed`);
|
|
8608
|
+
updated = (await this.taskStore.get(taskId)) ?? task;
|
|
8609
|
+
}
|
|
8610
|
+
else if (task.status === 'approved') {
|
|
8611
|
+
if (!task.devAgentId)
|
|
8612
|
+
throw new ApiError(409, `Task ${taskId} has no Dev participant`);
|
|
8613
|
+
if (task.postApproveRevoked) {
|
|
8614
|
+
if (opts.confirmRevoked !== true) {
|
|
8615
|
+
throw new ApiError(409, `Task ${taskId} post-approve replay is revoked; confirmRevoked=true is required`);
|
|
8616
|
+
}
|
|
8617
|
+
task = await this.restoreRevokedPostApprove(task);
|
|
8618
|
+
}
|
|
8619
|
+
const replayed = await this.redispatchTaskPromptAfterReplRestart(task.devAgentId, taskId);
|
|
8620
|
+
if (!replayed)
|
|
8621
|
+
throw new ApiError(409, `Task ${taskId} changed or its post-approve prompt could not be replayed`);
|
|
8622
|
+
updated = (await this.taskStore.get(taskId)) ?? task;
|
|
8623
|
+
}
|
|
8624
|
+
else {
|
|
8625
|
+
throw new ApiError(409, `Task ${taskId} cannot be advanced from ${task.status}`);
|
|
8626
|
+
}
|
|
8627
|
+
await this.auditHumanTaskOperation(updated, 'advance', executor, opts.note);
|
|
8628
|
+
return updated;
|
|
8629
|
+
}
|
|
8630
|
+
async restoreRevokedPostApprove(task) {
|
|
8631
|
+
if (!task.postApproveRevoked || task.prNumber === undefined) {
|
|
8632
|
+
throw new ApiError(409, `Task ${task.id} has no revoked post-approve episode`);
|
|
8633
|
+
}
|
|
8634
|
+
const [binding, accepted] = await Promise.all([
|
|
8635
|
+
this.platformVerifyPrBinding(task.id, task.prNumber),
|
|
8636
|
+
this.platformVerifyAcceptedPass(task.id),
|
|
8637
|
+
]);
|
|
8638
|
+
if (!binding.ok) {
|
|
8639
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: PR binding ${binding.reason}`);
|
|
8640
|
+
}
|
|
8641
|
+
if (accepted.kind !== 'valid') {
|
|
8642
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: ${accepted.reason}`);
|
|
8643
|
+
}
|
|
8644
|
+
if (accepted.pendingCount > 0) {
|
|
8645
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: the accepted pass has pending feedback`);
|
|
8646
|
+
}
|
|
8647
|
+
const reviewedPass = task.passProvenance;
|
|
8648
|
+
if (!reviewedPass || binding.headSha.toLowerCase() !== reviewedPass.anchorSha.toLowerCase()) {
|
|
8649
|
+
throw new ApiError(409, `Cannot restore task ${task.id}: the current PR head is not the accepted head`);
|
|
8650
|
+
}
|
|
8651
|
+
return this.withTaskLock(async () => {
|
|
8652
|
+
const fresh = await this.taskStore.get(task.id);
|
|
8653
|
+
if (!fresh || fresh.status !== 'approved'
|
|
8654
|
+
|| fresh.postApproveRevoked?.generation !== task.postApproveRevoked.generation
|
|
8655
|
+
|| fresh.prNumber !== task.prNumber
|
|
8656
|
+
|| !samePassProvenance(fresh.passProvenance, reviewedPass)) {
|
|
8657
|
+
throw new ApiError(409, `Task ${task.id} changed while restoring post-approve replay`);
|
|
8658
|
+
}
|
|
8659
|
+
const generation = createSignalToken();
|
|
8660
|
+
const token = createSignalTokenExcluding(generation);
|
|
8661
|
+
const next = {
|
|
8662
|
+
...this.stripPostApproveEpisodeFields(fresh),
|
|
8663
|
+
postApproveGeneration: generation,
|
|
8664
|
+
postApproveHeadSha: binding.headSha,
|
|
8665
|
+
postApproveToken: token,
|
|
8666
|
+
postApprovePhase: 'installed',
|
|
8667
|
+
pendingRedispatch: true,
|
|
8668
|
+
redispatchCount: 0,
|
|
8669
|
+
updatedAt: new Date().toISOString(),
|
|
8670
|
+
};
|
|
8671
|
+
await this.taskStore.set(next);
|
|
8672
|
+
return next;
|
|
8673
|
+
});
|
|
8674
|
+
}
|
|
8675
|
+
async submitTaskVerdict(taskId, action, comments, note) {
|
|
8676
|
+
const task = await this.taskStore.get(taskId);
|
|
8677
|
+
if (!task)
|
|
8678
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
8679
|
+
let updated;
|
|
8680
|
+
if (task.status === 'spec-ready'
|
|
8681
|
+
|| (task.status === 'max_rounds' && isSpecStagePhase(task.phase))) {
|
|
8682
|
+
if (action !== 'approve' && action !== 'request-changes') {
|
|
8683
|
+
throw new ApiError(409, `Task ${taskId} requires a spec approve or request-changes verdict`);
|
|
8684
|
+
}
|
|
8685
|
+
updated = await this.submitSpecVerdict(taskId, action, comments);
|
|
8686
|
+
}
|
|
8687
|
+
else if (task.status === 'review' && !isSpecStagePhase(task.phase)) {
|
|
8688
|
+
if (action !== 'pass' && action !== 'request-changes') {
|
|
8689
|
+
throw new ApiError(409, `Task ${taskId} requires a code pass or request-changes verdict`);
|
|
8690
|
+
}
|
|
8691
|
+
updated = await this.submitCodeVerdict(taskId, action, comments);
|
|
8692
|
+
}
|
|
8693
|
+
else if (task.status === 'max_rounds') {
|
|
8694
|
+
if (action === 'continue')
|
|
8695
|
+
updated = await this.continueDevRound(taskId);
|
|
8696
|
+
else if (action === 'complete')
|
|
8697
|
+
updated = await this.markTaskComplete(taskId);
|
|
8698
|
+
else
|
|
8699
|
+
throw new ApiError(409, `Task ${taskId} requires continue or complete at code max_rounds`);
|
|
8700
|
+
}
|
|
8701
|
+
else if (task.status === 'merge-ready') {
|
|
8702
|
+
if (action !== 'confirm-merge') {
|
|
8703
|
+
throw new ApiError(409, `Task ${taskId} requires confirm-merge`);
|
|
8704
|
+
}
|
|
8705
|
+
updated = await this.markTaskComplete(taskId);
|
|
8706
|
+
}
|
|
8707
|
+
else {
|
|
8708
|
+
throw new ApiError(409, `Task ${taskId} cannot accept a verdict from ${task.status}`);
|
|
8709
|
+
}
|
|
8710
|
+
await this.auditHumanTaskOperation(updated, 'verdict', action, note);
|
|
8711
|
+
return updated;
|
|
8712
|
+
}
|
|
8713
|
+
async auditHumanTaskOperation(task, operation, action, note, details = {}) {
|
|
8714
|
+
const emitted = await this.safeEmit({
|
|
8715
|
+
id: '',
|
|
8716
|
+
type: 'task.updated',
|
|
8717
|
+
timestamp: new Date().toISOString(),
|
|
8718
|
+
projectId: task.projectId,
|
|
8719
|
+
...(task.agentId ? { agentId: task.agentId } : {}),
|
|
8720
|
+
taskId: task.id,
|
|
8721
|
+
data: {
|
|
8722
|
+
...details,
|
|
8723
|
+
operation,
|
|
8724
|
+
action,
|
|
8725
|
+
actor: 'human',
|
|
8726
|
+
provenance: 'human',
|
|
8727
|
+
note: note?.trim() || `${operation}:${action}`,
|
|
8728
|
+
},
|
|
8729
|
+
});
|
|
8730
|
+
if (!emitted) {
|
|
8731
|
+
throw new ApiError(500, `Task ${task.id} ${operation} completed, but its audit event could not be persisted; verify the task state before retrying`);
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8372
8734
|
async dispatchGitFixToDev(taskId) {
|
|
8373
8735
|
const task = await this.taskStore.get(taskId);
|
|
8374
8736
|
if (!task || task.status !== 'fixing')
|
|
@@ -8461,7 +8823,7 @@ export class AgentManager {
|
|
|
8461
8823
|
const devState = await this.agentStore.get(devAgentId);
|
|
8462
8824
|
if (devState?.taskId !== taskId || !devState.workdir) {
|
|
8463
8825
|
throw new ApiError(409, `Dev ${devAgentId} no longer holds task ${taskId}'s Workdir (cannot continue); ` +
|
|
8464
|
-
`
|
|
8826
|
+
`submit a complete verdict to merge the PR as-is, or cancel the task`);
|
|
8465
8827
|
}
|
|
8466
8828
|
const prevReviewRound = task.reviewRound;
|
|
8467
8829
|
const transitioned = await this.transitionTaskStatus(taskId, 'fixing', { fromStatus: ['max_rounds'] }, { reviewRound: prevReviewRound + 1, fixDispatchedAt: new Date().toISOString() });
|
|
@@ -8553,26 +8915,53 @@ export class AgentManager {
|
|
|
8553
8915
|
const t = await this.taskStore.get(taskId);
|
|
8554
8916
|
if (!t)
|
|
8555
8917
|
throw new ApiError(404, 'Task not found');
|
|
8556
|
-
|
|
8557
|
-
|| (t.status === 'max_rounds' && isSpecStagePhase(t.phase));
|
|
8558
|
-
if (!retryable) {
|
|
8918
|
+
if (!TERMINAL_STATUSES.includes(t.status)) {
|
|
8559
8919
|
throw new ApiError(409, `Task ${taskId} cannot be retried in status "${t.status}"; cancel it first or wait for completion`);
|
|
8560
8920
|
}
|
|
8921
|
+
if (t.replacementTaskId !== undefined) {
|
|
8922
|
+
throw new ApiError(409, `Task ${taskId} was already retried as ${t.replacementTaskId}`);
|
|
8923
|
+
}
|
|
8924
|
+
if (this.retryTaskInFlight.has(taskId)) {
|
|
8925
|
+
throw new ApiError(409, `Task ${taskId} retry is already in progress`);
|
|
8926
|
+
}
|
|
8927
|
+
this.retryTaskInFlight.add(taskId);
|
|
8561
8928
|
return t;
|
|
8562
8929
|
});
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8930
|
+
try {
|
|
8931
|
+
const input = {
|
|
8932
|
+
title: old.title,
|
|
8933
|
+
description: old.description,
|
|
8934
|
+
preferredAgentId: this.resolveRetryPreferredAgentId(old),
|
|
8935
|
+
};
|
|
8936
|
+
if (old.images?.length) {
|
|
8937
|
+
input.images = await this.readStagedImages(old.id, old.images);
|
|
8938
|
+
}
|
|
8939
|
+
await this.validateTaskDispatch(old.projectId, input);
|
|
8940
|
+
const replacement = await this.createTask(old.projectId, input);
|
|
8941
|
+
await this.withTaskLock(async () => {
|
|
8942
|
+
const source = await this.taskStore.get(taskId);
|
|
8943
|
+
if (!source)
|
|
8944
|
+
throw new ApiError(404, `Task ${taskId} disappeared while recording its retry`);
|
|
8945
|
+
if (source.replacementTaskId !== undefined && source.replacementTaskId !== replacement.id) {
|
|
8946
|
+
throw new ApiError(409, `Task ${taskId} was already retried as ${source.replacementTaskId}`);
|
|
8947
|
+
}
|
|
8948
|
+
const next = {
|
|
8949
|
+
...source,
|
|
8950
|
+
replacementTaskId: replacement.id,
|
|
8951
|
+
updatedAt: new Date().toISOString(),
|
|
8952
|
+
};
|
|
8953
|
+
if (old.attention !== undefined
|
|
8954
|
+
&& source.attention?.reason === old.attention.reason
|
|
8955
|
+
&& source.attention.occurredAt === old.attention.occurredAt) {
|
|
8956
|
+
delete next.attention;
|
|
8957
|
+
}
|
|
8958
|
+
await this.taskStore.set(next);
|
|
8959
|
+
});
|
|
8960
|
+
return this.startCreatedTask(replacement);
|
|
8570
8961
|
}
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
await this.cancelTask(old.id);
|
|
8962
|
+
finally {
|
|
8963
|
+
this.retryTaskInFlight.delete(taskId);
|
|
8574
8964
|
}
|
|
8575
|
-
return this.createAndStartTask(old.projectId, input);
|
|
8576
8965
|
}
|
|
8577
8966
|
resolveRetryPreferredAgentId(task) {
|
|
8578
8967
|
const configured = this.getAgentConfig(task.preferredAgentId);
|
|
@@ -8582,7 +8971,7 @@ export class AgentManager {
|
|
|
8582
8971
|
const anchors = new Set([task.qaAgentId, task.devAgentId, task.agentId]
|
|
8583
8972
|
.filter((agentId) => typeof agentId === 'string' && agentId !== ''));
|
|
8584
8973
|
for (const anchor of anchors) {
|
|
8585
|
-
const dev = this.
|
|
8974
|
+
const dev = this.findAgentTeam(anchor)?.find(agent => TASK_OWNER_ROLES.has(agent.role));
|
|
8586
8975
|
const current = dev ? this.getAgentConfig(dev.id) : undefined;
|
|
8587
8976
|
if (current?.projectId === task.projectId)
|
|
8588
8977
|
return current.id;
|
|
@@ -8624,13 +9013,13 @@ export class AgentManager {
|
|
|
8624
9013
|
}
|
|
8625
9014
|
if (!TASK_OWNER_ROLES.has(cfg.role))
|
|
8626
9015
|
throw new ApiError(400, 'Agent is not a dev agent');
|
|
8627
|
-
const
|
|
8628
|
-
const dev =
|
|
9016
|
+
const team = this.findAgentTeam(cfg.id);
|
|
9017
|
+
const dev = team?.find(agent => agent.role === 'dev');
|
|
8629
9018
|
if (!dev)
|
|
8630
|
-
throw new ApiError(409, `Agent ${cfg.id} has no dev agent in its
|
|
8631
|
-
const qa =
|
|
9019
|
+
throw new ApiError(409, `Agent ${cfg.id} has no dev agent in its Agent Team`);
|
|
9020
|
+
const qa = team?.find(agent => agent.role === 'qa');
|
|
8632
9021
|
if (!qa)
|
|
8633
|
-
throw new ApiError(409, `Agent ${cfg.id} has no qa agent in its
|
|
9022
|
+
throw new ApiError(409, `Agent ${cfg.id} has no qa agent in its Agent Team`);
|
|
8634
9023
|
task.preferredAgentId = patch.preferredAgentId;
|
|
8635
9024
|
task.devAgentId = dev.id;
|
|
8636
9025
|
task.qaAgentId = qa.id;
|
|
@@ -8668,22 +9057,22 @@ export class AgentManager {
|
|
|
8668
9057
|
throw new ApiError(409, `Task ${taskId} is not at max_rounds (status=${task.status})`);
|
|
8669
9058
|
}
|
|
8670
9059
|
if (isSpecStagePhase(task.phase)) {
|
|
8671
|
-
throw new ApiError(409, `
|
|
9060
|
+
throw new ApiError(409, `Complete verdict is only supported for code-phase tasks`);
|
|
8672
9061
|
}
|
|
8673
9062
|
if (!task.prNumber || !task.branch) {
|
|
8674
|
-
throw new ApiError(400, `Task ${taskId} has no PR/branch; cannot
|
|
9063
|
+
throw new ApiError(400, `Task ${taskId} has no PR/branch; cannot complete`);
|
|
8675
9064
|
}
|
|
8676
9065
|
for (const agentId of [task.agentId, task.qaAgentId]) {
|
|
8677
9066
|
if (!agentId)
|
|
8678
9067
|
continue;
|
|
8679
9068
|
const state = await this.agentStore.get(agentId);
|
|
8680
9069
|
if (state?.status === 'awaiting_human' && state.taskId === taskId) {
|
|
8681
|
-
throw new ApiError(409, `Agent ${agentId} is awaiting human intervention on this task; resume/restart/delete it before
|
|
9070
|
+
throw new ApiError(409, `Agent ${agentId} is awaiting human intervention on this task; resume/restart/delete it before completing`);
|
|
8682
9071
|
}
|
|
8683
9072
|
}
|
|
8684
9073
|
const claimed = await this.transitionTaskStatus(taskId, 'merge-ready', { fromStatus: ['max_rounds'] });
|
|
8685
9074
|
if (!claimed) {
|
|
8686
|
-
throw new ApiError(409, `Task ${taskId} changed status during
|
|
9075
|
+
throw new ApiError(409, `Task ${taskId} changed status during complete verdict; aborted`);
|
|
8687
9076
|
}
|
|
8688
9077
|
try {
|
|
8689
9078
|
await this.mergePr(taskId, { humanOverride: true });
|
|
@@ -9193,6 +9582,247 @@ export class AgentManager {
|
|
|
9193
9582
|
}
|
|
9194
9583
|
return parked;
|
|
9195
9584
|
}
|
|
9585
|
+
async submitCodeVerdict(taskId, verdict, comments) {
|
|
9586
|
+
const trimmed = comments?.trim() ?? '';
|
|
9587
|
+
if (verdict === 'request-changes' && !trimmed) {
|
|
9588
|
+
throw new ApiError(400, 'comments is required for request-changes');
|
|
9589
|
+
}
|
|
9590
|
+
if (extractReviewTokens(trimmed).length > 0) {
|
|
9591
|
+
throw new ApiError(400, 'comments must not contain baxian review markers');
|
|
9592
|
+
}
|
|
9593
|
+
const task = await this.withTaskLock(async () => {
|
|
9594
|
+
const fresh = await this.taskStore.get(taskId);
|
|
9595
|
+
if (!fresh)
|
|
9596
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
9597
|
+
const pending = this.codeVerdictOutbox(fresh);
|
|
9598
|
+
const kind = verdict === 'pass' ? 'pass' : 'fail';
|
|
9599
|
+
if (pending !== undefined
|
|
9600
|
+
&& (pending.data.kind !== kind || pending.data.comments !== trimmed)) {
|
|
9601
|
+
throw new ApiError(409, `Task ${taskId} has a different code verdict already in progress`);
|
|
9602
|
+
}
|
|
9603
|
+
if (pending === undefined && (fresh.status !== 'review' || isSpecStagePhase(fresh.phase))) {
|
|
9604
|
+
throw new ApiError(409, `Task ${taskId} is not awaiting a code review verdict`);
|
|
9605
|
+
}
|
|
9606
|
+
if (this.codeVerdictInFlight.has(taskId)) {
|
|
9607
|
+
throw new ApiError(409, `Task ${taskId} code verdict is already being processed`);
|
|
9608
|
+
}
|
|
9609
|
+
this.codeVerdictInFlight.add(taskId);
|
|
9610
|
+
return fresh;
|
|
9611
|
+
});
|
|
9612
|
+
try {
|
|
9613
|
+
return await this.executeCodeVerdict(task, verdict, trimmed);
|
|
9614
|
+
}
|
|
9615
|
+
finally {
|
|
9616
|
+
this.codeVerdictInFlight.delete(taskId);
|
|
9617
|
+
}
|
|
9618
|
+
}
|
|
9619
|
+
codeVerdictCommentBody(entry) {
|
|
9620
|
+
const lines = [
|
|
9621
|
+
entry.data.kind === 'pass'
|
|
9622
|
+
? 'Human code verdict: pass'
|
|
9623
|
+
: 'Human code verdict: request changes',
|
|
9624
|
+
];
|
|
9625
|
+
if (entry.data.comments)
|
|
9626
|
+
lines.push('', entry.data.comments);
|
|
9627
|
+
lines.push('', buildReviewTokenLine({
|
|
9628
|
+
kind: entry.data.kind,
|
|
9629
|
+
anchorSha: entry.data.anchorSha,
|
|
9630
|
+
token: entry.data.token,
|
|
9631
|
+
}));
|
|
9632
|
+
const body = lines.join('\n');
|
|
9633
|
+
try {
|
|
9634
|
+
validateCommentBody(body);
|
|
9635
|
+
}
|
|
9636
|
+
catch (err) {
|
|
9637
|
+
throw new ApiError(400, `Invalid code verdict comment: ${err instanceof Error ? err.message : String(err)}`);
|
|
9638
|
+
}
|
|
9639
|
+
return body;
|
|
9640
|
+
}
|
|
9641
|
+
async scanCodeVerdictMarker(driver, entry, oppositeToken) {
|
|
9642
|
+
const scans = await this.platformScanComments(driver, entry.data.prNumber);
|
|
9643
|
+
if (scans.some(scan => !scan.ok))
|
|
9644
|
+
return 'unknown';
|
|
9645
|
+
const dead = deadTokens(scans);
|
|
9646
|
+
if (dead.has(entry.data.token))
|
|
9647
|
+
return 'dead';
|
|
9648
|
+
let present = false;
|
|
9649
|
+
let opposite = false;
|
|
9650
|
+
for (const scan of scans) {
|
|
9651
|
+
for (const row of scan.rows) {
|
|
9652
|
+
if (row.system === true || versionTimeOf(row) === undefined)
|
|
9653
|
+
continue;
|
|
9654
|
+
for (const marker of rowTokens(row)) {
|
|
9655
|
+
if (dead.has(marker.token))
|
|
9656
|
+
continue;
|
|
9657
|
+
if (marker.anchorSha.toLowerCase() !== entry.data.anchorSha.toLowerCase())
|
|
9658
|
+
continue;
|
|
9659
|
+
if (marker.kind === entry.data.kind && marker.token === entry.data.token)
|
|
9660
|
+
present = true;
|
|
9661
|
+
if (marker.kind !== entry.data.kind && marker.token === oppositeToken)
|
|
9662
|
+
opposite = true;
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
if (opposite)
|
|
9667
|
+
return 'opposite';
|
|
9668
|
+
return present ? 'present' : 'absent';
|
|
9669
|
+
}
|
|
9670
|
+
async reconcileCodeVerdictComment(driver, entry) {
|
|
9671
|
+
const expectedDigest = bodyDigest(this.codeVerdictCommentBody(entry));
|
|
9672
|
+
const scans = await this.platformScanComments(driver, entry.data.prNumber);
|
|
9673
|
+
if (scans.some(scan => !scan.ok))
|
|
9674
|
+
return 'unknown';
|
|
9675
|
+
if (deadTokens(scans).has(entry.data.token))
|
|
9676
|
+
return 'dead';
|
|
9677
|
+
if (scans.some(scan => scan.rows.some(row => row.system !== true
|
|
9678
|
+
&& versionTimeOf(row) !== undefined
|
|
9679
|
+
&& row.bodyDigest === expectedDigest)))
|
|
9680
|
+
return 'present';
|
|
9681
|
+
return 'absent';
|
|
9682
|
+
}
|
|
9683
|
+
async verifyCodeVerdictHead(taskId, prNumber, anchorSha) {
|
|
9684
|
+
try {
|
|
9685
|
+
const binding = await this.platformVerifyPrBinding(taskId, prNumber);
|
|
9686
|
+
if (!binding.ok)
|
|
9687
|
+
return { ok: false, reason: `the PR binding is ${binding.reason}` };
|
|
9688
|
+
if (binding.headSha.toLowerCase() !== anchorSha.toLowerCase()) {
|
|
9689
|
+
return { ok: false, reason: 'the PR head changed' };
|
|
9690
|
+
}
|
|
9691
|
+
return { ok: true };
|
|
9692
|
+
}
|
|
9693
|
+
catch (err) {
|
|
9694
|
+
throw new ApiError(503, `Cannot verify code verdict head for task ${taskId}: ${safeDriverErrorText(err)}`);
|
|
9695
|
+
}
|
|
9696
|
+
}
|
|
9697
|
+
async deliverCodeVerdictOutbox(taskId, key) {
|
|
9698
|
+
let task = await this.taskStore.get(taskId);
|
|
9699
|
+
let entry = task ? this.codeVerdictOutbox(task) : undefined;
|
|
9700
|
+
if (!task || !entry || entry.key !== key)
|
|
9701
|
+
return;
|
|
9702
|
+
const driver = this.platformDriverFor(task.projectId);
|
|
9703
|
+
if (!driver)
|
|
9704
|
+
throw new ApiError(503, `Git driver unavailable for task ${task.id}`);
|
|
9705
|
+
const binding = await this.verifyCodeVerdictHead(task.id, entry.data.prNumber, entry.data.anchorSha);
|
|
9706
|
+
if (!binding.ok) {
|
|
9707
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9708
|
+
throw new ApiError(409, `Cannot write code verdict for task ${task.id}: ${binding.reason}`);
|
|
9709
|
+
}
|
|
9710
|
+
if (entry.data.writeAttemptedAt !== undefined) {
|
|
9711
|
+
const reconciled = await this.reconcileCodeVerdictComment(driver, entry);
|
|
9712
|
+
if (reconciled === 'dead') {
|
|
9713
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9714
|
+
throw new ApiError(409, `Code verdict token for task ${task.id} was dismissed; advance QA to create a new review pass`);
|
|
9715
|
+
}
|
|
9716
|
+
if (reconciled === 'present') {
|
|
9717
|
+
return;
|
|
9718
|
+
}
|
|
9719
|
+
if (reconciled === 'unknown'
|
|
9720
|
+
|| Date.now() - Date.parse(entry.data.writeAttemptedAt) < driver.visibilityLagMs) {
|
|
9721
|
+
throw new ApiError(503, `Code verdict comment outcome is still being reconciled for task ${task.id}`);
|
|
9722
|
+
}
|
|
9723
|
+
}
|
|
9724
|
+
const expectedAttempt = entry.data.writeAttemptedAt;
|
|
9725
|
+
const claimed = await this.mutateTaskOutboxEntry(task.id, entry.key, current => current.type === 'git.code-verdict' && current.data.writeAttemptedAt === expectedAttempt
|
|
9726
|
+
? { ...current, data: { ...current.data, writeAttemptedAt: new Date().toISOString() } }
|
|
9727
|
+
: null);
|
|
9728
|
+
const claimedEntry = claimed ? this.codeVerdictOutbox(claimed) : undefined;
|
|
9729
|
+
if (!claimed || claimedEntry?.key !== entry.key) {
|
|
9730
|
+
throw new ApiError(503, `Code verdict comment write is already claimed for task ${task.id}`);
|
|
9731
|
+
}
|
|
9732
|
+
task = claimed;
|
|
9733
|
+
entry = claimedEntry;
|
|
9734
|
+
let writeFailure;
|
|
9735
|
+
try {
|
|
9736
|
+
await driver.runOp('comment', {
|
|
9737
|
+
prNumber: entry.data.prNumber,
|
|
9738
|
+
body: this.codeVerdictCommentBody(entry),
|
|
9739
|
+
});
|
|
9740
|
+
}
|
|
9741
|
+
catch (error) {
|
|
9742
|
+
writeFailure = { error };
|
|
9743
|
+
}
|
|
9744
|
+
const reconciled = await this.reconcileCodeVerdictComment(driver, entry);
|
|
9745
|
+
if (reconciled === 'dead') {
|
|
9746
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9747
|
+
throw new ApiError(409, `Code verdict token for task ${task.id} was dismissed; advance QA to create a new review pass`);
|
|
9748
|
+
}
|
|
9749
|
+
if (writeFailure !== undefined && reconciled !== 'present') {
|
|
9750
|
+
const { error } = writeFailure;
|
|
9751
|
+
const uncertain = !(error instanceof DriverOpError)
|
|
9752
|
+
|| error.info.exitCode === 255
|
|
9753
|
+
|| isTransientNetworkFailure(error.info.stderrTail ?? '')
|
|
9754
|
+
|| isTransientNetworkFailure(error.message);
|
|
9755
|
+
if (reconciled === 'unknown' || uncertain) {
|
|
9756
|
+
throw new ApiError(503, `Code verdict comment outcome is unknown for task ${task.id}; the persisted outbox will reconcile it`);
|
|
9757
|
+
}
|
|
9758
|
+
await this.mutateTaskOutboxEntry(task.id, key, current => current.type === 'git.code-verdict' ? undefined : null);
|
|
9759
|
+
throw new ApiError(502, `Code verdict comment was rejected for task ${task.id}: ${safeDriverErrorText(error)}`);
|
|
9760
|
+
}
|
|
9761
|
+
if (reconciled !== 'present') {
|
|
9762
|
+
throw new ApiError(503, `Code verdict comment is not yet visible for task ${task.id}; the persisted outbox will reconcile it`);
|
|
9763
|
+
}
|
|
9764
|
+
}
|
|
9765
|
+
async executeCodeVerdict(task, verdict, comments) {
|
|
9766
|
+
if (task.status !== 'review' || isSpecStagePhase(task.phase)
|
|
9767
|
+
|| task.prNumber === undefined || task.reviewHeadAnchorSha === undefined
|
|
9768
|
+
|| task.passToken === undefined || task.failToken === undefined) {
|
|
9769
|
+
throw new ApiError(409, `Task ${task.id} has no complete code review generation`);
|
|
9770
|
+
}
|
|
9771
|
+
const kind = verdict === 'pass' ? 'pass' : 'fail';
|
|
9772
|
+
const token = kind === 'pass' ? task.passToken : task.failToken;
|
|
9773
|
+
const existingEntry = this.codeVerdictOutbox(task);
|
|
9774
|
+
const entry = existingEntry ?? {
|
|
9775
|
+
key: token,
|
|
9776
|
+
type: 'git.code-verdict',
|
|
9777
|
+
data: {
|
|
9778
|
+
prNumber: task.prNumber,
|
|
9779
|
+
kind,
|
|
9780
|
+
anchorSha: task.reviewHeadAnchorSha,
|
|
9781
|
+
token,
|
|
9782
|
+
comments,
|
|
9783
|
+
},
|
|
9784
|
+
};
|
|
9785
|
+
this.codeVerdictCommentBody(entry);
|
|
9786
|
+
const driver = this.platformDriverFor(task.projectId);
|
|
9787
|
+
if (!driver)
|
|
9788
|
+
throw new ApiError(503, `Git driver unavailable for task ${task.id}`);
|
|
9789
|
+
const binding = await this.verifyCodeVerdictHead(task.id, task.prNumber, task.reviewHeadAnchorSha);
|
|
9790
|
+
if (!binding.ok) {
|
|
9791
|
+
throw new ApiError(409, `Cannot submit code verdict for task ${task.id}: ${binding.reason}`);
|
|
9792
|
+
}
|
|
9793
|
+
if (existingEntry === undefined) {
|
|
9794
|
+
const marker = await this.scanCodeVerdictMarker(driver, entry, kind === 'pass' ? task.failToken : task.passToken);
|
|
9795
|
+
if (marker === 'present')
|
|
9796
|
+
return task;
|
|
9797
|
+
if (marker === 'opposite') {
|
|
9798
|
+
throw new ApiError(409, `Task ${task.id} already has the opposite verdict on the current review head`);
|
|
9799
|
+
}
|
|
9800
|
+
if (marker === 'dead') {
|
|
9801
|
+
throw new ApiError(409, `Code verdict token for task ${task.id} was dismissed; advance QA to create a new review pass`);
|
|
9802
|
+
}
|
|
9803
|
+
if (marker === 'unknown') {
|
|
9804
|
+
throw new ApiError(503, `Cannot safely check existing code verdict comments for task ${task.id}`);
|
|
9805
|
+
}
|
|
9806
|
+
const created = await this.withTaskLock(async () => {
|
|
9807
|
+
const fresh = await this.taskStore.get(task.id);
|
|
9808
|
+
if (!fresh || !taskMatchesGeneration(fresh, taskGenerationGuard(task))
|
|
9809
|
+
|| this.codeVerdictOutbox(fresh) !== undefined)
|
|
9810
|
+
return null;
|
|
9811
|
+
const next = {
|
|
9812
|
+
...fresh,
|
|
9813
|
+
outbox: [...(fresh.outbox ?? []), entry],
|
|
9814
|
+
updatedAt: new Date().toISOString(),
|
|
9815
|
+
};
|
|
9816
|
+
await this.taskStore.set(next);
|
|
9817
|
+
return next;
|
|
9818
|
+
});
|
|
9819
|
+
if (!created)
|
|
9820
|
+
throw new ApiError(409, `Task ${task.id} changed while recording the code verdict`);
|
|
9821
|
+
task = created;
|
|
9822
|
+
}
|
|
9823
|
+
await this.deliverCodeVerdictOutbox(task.id, entry.key);
|
|
9824
|
+
return (await this.taskStore.get(task.id)) ?? task;
|
|
9825
|
+
}
|
|
9196
9826
|
async submitSpecVerdict(taskId, verdict, comments) {
|
|
9197
9827
|
const trimmed = comments?.trim();
|
|
9198
9828
|
if (verdict === 'request-changes' && !trimmed) {
|
|
@@ -9849,8 +10479,8 @@ export class AgentManager {
|
|
|
9849
10479
|
function buildAgentIndex(config) {
|
|
9850
10480
|
const index = new Map();
|
|
9851
10481
|
for (const project of config.project) {
|
|
9852
|
-
for (const
|
|
9853
|
-
for (const agent of
|
|
10482
|
+
for (const team of project.agent) {
|
|
10483
|
+
for (const agent of team) {
|
|
9854
10484
|
index.set(agent.id, { ...agent, projectId: project.id });
|
|
9855
10485
|
}
|
|
9856
10486
|
}
|