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.
- package/README.md +5 -7
- package/README.zh-CN.md +5 -7
- package/dist/agent/bootstrap.d.ts.map +1 -1
- package/dist/agent/bootstrap.js +2 -4
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/branch.d.ts +0 -12
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +1 -71
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +9 -9
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +65 -138
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +928 -2442
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +4 -1
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +25 -5
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +2 -11
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +38 -87
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +14 -40
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +47 -90
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts +0 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +8 -15
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +1 -30
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +21 -148
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +0 -4
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/root-agent-runtime.d.ts.map +1 -1
- package/dist/agent/root-agent-runtime.js +4 -2
- package/dist/agent/root-agent-runtime.js.map +1 -1
- package/dist/agent/root-recovery-coordinator.d.ts +0 -1
- package/dist/agent/root-recovery-coordinator.d.ts.map +1 -1
- package/dist/agent/root-recovery-coordinator.js +39 -61
- package/dist/agent/root-recovery-coordinator.js.map +1 -1
- package/dist/agent/vt-visible-text.d.ts +42 -0
- package/dist/agent/vt-visible-text.d.ts.map +1 -0
- package/dist/agent/vt-visible-text.js +482 -0
- package/dist/agent/vt-visible-text.js.map +1 -0
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +6 -2
- package/dist/api/config.js.map +1 -1
- package/dist/api/platform-guard.js +2 -2
- package/dist/api/platform-guard.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +598 -318
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +15 -39
- package/dist/api/tasks.js.map +1 -1
- package/dist/config/loader.d.ts +5 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +36 -24
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.d.ts +7 -4
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +57 -73
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +359 -226
- package/dist/event/handlers.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +1 -1
- package/dist/platform/command-renderer.d.ts +6 -0
- package/dist/platform/command-renderer.d.ts.map +1 -1
- package/dist/platform/command-renderer.js +20 -0
- package/dist/platform/command-renderer.js.map +1 -1
- package/dist/platform/driver-host.d.ts.map +1 -1
- package/dist/platform/driver-host.js +3 -1
- package/dist/platform/driver-host.js.map +1 -1
- package/dist/platform/driver-spec.d.ts.map +1 -1
- package/dist/platform/driver-spec.js +32 -4
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/git-driver.d.ts +2 -0
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +9 -3
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +0 -1
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +0 -6
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugins/github/driver.json +5 -0
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +20 -14
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +5 -5
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts +1 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +3 -3
- package/dist/platform/types.js.map +1 -1
- package/dist/shared/constants.d.ts +1 -6
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +2 -29
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +28 -124
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +7 -6
- package/dist/shared/types.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +5 -3
- package/dist/skills/baxian-pr-recheck/SKILL.md +4 -0
- package/dist/skills/baxian-pr-review/SKILL.md +4 -0
- package/dist/skills/baxian-signals/SKILL.md +15 -32
- package/dist/skills/baxian-task-check/SKILL.md +13 -14
- package/dist/state/snapshot.d.ts.map +1 -1
- package/dist/state/snapshot.js +5 -2
- package/dist/state/snapshot.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +56 -140
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-BKiM0MSM.js +15 -0
- package/dist/web/assets/index-BS2adAfI.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/agent/review-transport.d.ts +0 -92
- package/dist/agent/review-transport.d.ts.map +0 -1
- package/dist/agent/review-transport.js +0 -512
- package/dist/agent/review-transport.js.map +0 -1
- package/dist/event/server-handlers.d.ts +0 -4
- package/dist/event/server-handlers.d.ts.map +0 -1
- package/dist/event/server-handlers.js +0 -1294
- package/dist/event/server-handlers.js.map +0 -1
- package/dist/skills/baxian-research/SKILL.md +0 -28
- package/dist/skills/baxian-server-feedback/SKILL.md +0 -40
- package/dist/skills/baxian-server-feedback/agents/openai.yaml +0 -2
- package/dist/skills/baxian-server-review/SKILL.md +0 -58
- package/dist/skills/baxian-server-review/agents/openai.yaml +0 -2
- package/dist/state/review-store.d.ts +0 -33
- package/dist/state/review-store.d.ts.map +0 -1
- package/dist/state/review-store.js +0 -272
- package/dist/state/review-store.js.map +0 -1
- package/dist/web/assets/index-BPv6vv3U.css +0 -1
- package/dist/web/assets/index-BXfgaFNm.js +0 -18
package/dist/event/handlers.js
CHANGED
|
@@ -3,15 +3,21 @@ import { BODY_DIGEST_SOURCE } from '../platform/body-digest.js';
|
|
|
3
3
|
import { ackRevisionKey } from '../platform/markers.js';
|
|
4
4
|
import { LINE_SAFE_ID_RE } from '../platform/row-schema.js';
|
|
5
5
|
import { SHA_HEX_SOURCE, SOURCE_KEY_PATTERN } from '../platform/types.js';
|
|
6
|
-
import { BRANCH_PREFIX,
|
|
6
|
+
import { BRANCH_PREFIX, effectiveTaskReviewRound, isRecord, isValidBranchName, taskGenerationGuard, taskReviewRound, } from '../shared/index.js';
|
|
7
7
|
import { DispatchTerminalError, isRecoverableQaDispatchHold, } from '../agent/manager.js';
|
|
8
|
-
import { ReplNotReadyError } from '../agent/tmux.js';
|
|
9
|
-
import { DirtyWorkdirError } from '../agent/branch.js';
|
|
10
8
|
const HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
|
|
11
9
|
const BODY_DIGEST_RE = new RegExp(`^${BODY_DIGEST_SOURCE}$`);
|
|
12
10
|
const PR_UPDATED_REVIEW_FROM_STATUSES = [
|
|
13
|
-
'in_progress', 'fixing', 'review', 'approved', 'merge-ready',
|
|
11
|
+
'in_progress', 'fixing', 'review', 'spec-ready', 'approved', 'merge-ready',
|
|
14
12
|
];
|
|
13
|
+
const PREMATURE_MERGE_BLOCKABLE_STATUSES = [
|
|
14
|
+
'pending', 'in_progress', 'review', 'fixing', 'spec-ready', 'approved', 'max_rounds',
|
|
15
|
+
];
|
|
16
|
+
function requireTaskQaAgentId(task, operation) {
|
|
17
|
+
if (!task.qaAgentId)
|
|
18
|
+
throw new Error(`${operation}: task ${task.id} has no QA participant`);
|
|
19
|
+
return task.qaAgentId;
|
|
20
|
+
}
|
|
15
21
|
function validHeadSha(value) {
|
|
16
22
|
return typeof value === 'string' && HEAD_SHA_RE.test(value) ? value : undefined;
|
|
17
23
|
}
|
|
@@ -96,11 +102,9 @@ function parseGitFeedbackRevision(value) {
|
|
|
96
102
|
};
|
|
97
103
|
}
|
|
98
104
|
async function reArmDevelopWatcher(manager, task, agentId, opts = {}) {
|
|
99
|
-
const kinds = task.phase === '
|
|
100
|
-
? ['
|
|
101
|
-
:
|
|
102
|
-
? ['pr-created']
|
|
103
|
-
: ['spec-done', 'pr-created'];
|
|
105
|
+
const kinds = task.phase === 'code'
|
|
106
|
+
? ['pr-created']
|
|
107
|
+
: ['spec-done', 'pr-created'];
|
|
104
108
|
await manager.setupPhaseSignal(task.id, agentId, kinds, { skipSnapshot: opts.skipSnapshot ?? true });
|
|
105
109
|
}
|
|
106
110
|
async function emitIntervention(bus, projectId, agentId, taskId, data) {
|
|
@@ -120,6 +124,47 @@ async function emitIntervention(bus, projectId, agentId, taskId, data) {
|
|
|
120
124
|
console.warn(`[EventHandler] human.intervention emit failed (phase=${data.phase}):`, emitErr);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
127
|
+
async function cleanupPrematureMergeParticipants(bus, manager, task) {
|
|
128
|
+
const participantIds = [...new Set([task.agentId, task.qaAgentId]
|
|
129
|
+
.filter((id) => typeof id === 'string' && id !== ''))];
|
|
130
|
+
const errors = [];
|
|
131
|
+
try {
|
|
132
|
+
await manager.cancelTask(task.id);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
136
|
+
}
|
|
137
|
+
const remainingParticipants = new Set();
|
|
138
|
+
for (const participantId of participantIds) {
|
|
139
|
+
try {
|
|
140
|
+
if ((await manager.getAgentState(participantId))?.taskId === task.id) {
|
|
141
|
+
remainingParticipants.add(participantId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
remainingParticipants.add(participantId);
|
|
146
|
+
errors.push(`${participantId}: binding probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
if (await manager.getAgentLockOwner(participantId) === task.id) {
|
|
150
|
+
remainingParticipants.add(participantId);
|
|
151
|
+
errors.push(`${participantId}: exclusive lock still owned by task ${task.id}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
remainingParticipants.add(participantId);
|
|
156
|
+
errors.push(`${participantId}: lock probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (errors.length === 0 && remainingParticipants.size === 0)
|
|
160
|
+
return;
|
|
161
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
162
|
+
phase: 'premature-merge-participant-cleanup-failed',
|
|
163
|
+
participants: participantIds,
|
|
164
|
+
remainingParticipants: [...remainingParticipants],
|
|
165
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
123
168
|
const POST_APPROVE_REDISPATCH_CAP = 10;
|
|
124
169
|
const POST_APPROVE_RECOVERY_MIN_AGE_MS = 60_000;
|
|
125
170
|
// 损坏值不可自愈,每周期 sweep 会重踩同一分支:按 task+值去重,值变化或修复后允许重报
|
|
@@ -144,7 +189,7 @@ async function dispatchGitDevPostApproveCheck(bus, manager, snapshot, approvedHe
|
|
|
144
189
|
return;
|
|
145
190
|
}
|
|
146
191
|
const live = await manager.getTask(snapshot.id);
|
|
147
|
-
if (!live || live.status !== 'approved'
|
|
192
|
+
if (!live || live.status !== 'approved')
|
|
148
193
|
return;
|
|
149
194
|
let key = postApproveEpisodeKey(live);
|
|
150
195
|
if (!key || key.headSha !== approvedHeadSha || live.postApprovePhase === undefined) {
|
|
@@ -222,7 +267,7 @@ async function dispatchGitDevPostApproveCheck(bus, manager, snapshot, approvedHe
|
|
|
222
267
|
async function handlePrMergeReady(bus, manager, event) {
|
|
223
268
|
const taskId = event.taskId;
|
|
224
269
|
const taskNow = await manager.getTask(taskId);
|
|
225
|
-
if (!taskNow
|
|
270
|
+
if (!taskNow)
|
|
226
271
|
return;
|
|
227
272
|
const eventPrNumber = event.data.prNumber;
|
|
228
273
|
const eventPrUrl = event.data.prUrl;
|
|
@@ -260,7 +305,6 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
260
305
|
]);
|
|
261
306
|
if (!freshTask
|
|
262
307
|
|| freshTask.status !== 'approved'
|
|
263
|
-
|| freshTask.reviewMode !== 'git'
|
|
264
308
|
|| freshTask.agentId !== taskNow.agentId
|
|
265
309
|
|| freshTask.postApproveRevoked !== undefined
|
|
266
310
|
|| entryCompletion?.token !== signalToken) {
|
|
@@ -308,14 +352,15 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
308
352
|
patch: { latestHeadSha: gitEpisodeKey.headSha },
|
|
309
353
|
});
|
|
310
354
|
if (begun?.task.reviewDispatch) {
|
|
355
|
+
const qaAgentId = requireTaskQaAgentId(begun.task, 'handlePostApproveMergeReady');
|
|
311
356
|
try {
|
|
312
357
|
await manager.dispatchGitReviewLease(freshTask.id, {
|
|
313
358
|
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
314
359
|
});
|
|
315
360
|
}
|
|
316
361
|
catch (err) {
|
|
317
|
-
if (err instanceof DispatchTerminalError
|
|
318
|
-
await manager.failTaskForDispatchError(freshTask.id, 'recheck',
|
|
362
|
+
if (err instanceof DispatchTerminalError) {
|
|
363
|
+
await manager.failTaskForDispatchError(freshTask.id, 'recheck', qaAgentId, err, { expectedReviewDispatch: begun.task.reviewDispatch });
|
|
319
364
|
return;
|
|
320
365
|
}
|
|
321
366
|
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
@@ -382,7 +427,7 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
382
427
|
async function handlePrFeedback(bus, manager, event) {
|
|
383
428
|
const taskId = event.taskId;
|
|
384
429
|
const taskNow = await manager.getTask(taskId);
|
|
385
|
-
if (!taskNow
|
|
430
|
+
if (!taskNow)
|
|
386
431
|
return;
|
|
387
432
|
const revision = parseGitFeedbackRevision(event.data.revision);
|
|
388
433
|
if (!revision) {
|
|
@@ -447,7 +492,7 @@ export async function recoverGitPostApprovePending(bus, manager) {
|
|
|
447
492
|
async function recoverOneGitPendingTask(bus, manager, task) {
|
|
448
493
|
{
|
|
449
494
|
const live = await manager.getTask(task.id);
|
|
450
|
-
if (!live || live.
|
|
495
|
+
if (!live || live.status !== 'approved' || live.pendingRedispatch !== true)
|
|
451
496
|
return;
|
|
452
497
|
if (live.postApproveRevoked)
|
|
453
498
|
return;
|
|
@@ -516,8 +561,19 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
516
561
|
const eventKind = event.data.kind;
|
|
517
562
|
const eventHeadSha = validHeadSha(event.data.headSha);
|
|
518
563
|
const task = await manager.getTask(taskId);
|
|
519
|
-
if (!task
|
|
564
|
+
if (!task)
|
|
565
|
+
return;
|
|
566
|
+
if (task.status === 'in_progress'
|
|
567
|
+
&& (task.phase === undefined || task.deliveryConfirmation?.phase !== task.phase)) {
|
|
568
|
+
if (eventPrNumber !== undefined && eventHeadSha !== undefined) {
|
|
569
|
+
await manager.recordGitPrObservation(taskId, {
|
|
570
|
+
prNumber: eventPrNumber,
|
|
571
|
+
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
572
|
+
headSha: eventHeadSha,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
520
575
|
return;
|
|
576
|
+
}
|
|
521
577
|
if (eventKind === 'push' && eventHeadSha
|
|
522
578
|
&& event.data.source !== 'pr-fixed'
|
|
523
579
|
&& task.reviewDispatch === undefined
|
|
@@ -529,6 +585,16 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
529
585
|
console.warn(`[EventHandler] pr.updated: task ${taskId} in_progress but neither task nor event has prNumber; deferring catch-up`);
|
|
530
586
|
return;
|
|
531
587
|
}
|
|
588
|
+
if (task.status === 'max_rounds') {
|
|
589
|
+
if (eventPrNumber !== undefined && eventHeadSha !== undefined) {
|
|
590
|
+
await manager.recordGitPrObservation(taskId, {
|
|
591
|
+
prNumber: eventPrNumber,
|
|
592
|
+
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
593
|
+
headSha: eventHeadSha,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
532
598
|
if (!PR_UPDATED_REVIEW_FROM_STATUSES.includes(task.status))
|
|
533
599
|
return;
|
|
534
600
|
let reviewHead = eventHeadSha ?? validHeadSha(task.latestHeadSha);
|
|
@@ -545,6 +611,7 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
545
611
|
return;
|
|
546
612
|
}
|
|
547
613
|
const bumpRound = task.status === 'in_progress'
|
|
614
|
+
|| task.status === 'spec-ready'
|
|
548
615
|
|| task.status === 'approved'
|
|
549
616
|
|| task.status === 'merge-ready'
|
|
550
617
|
|| (task.status === 'review' && task.reviewRoundPending === true);
|
|
@@ -573,6 +640,7 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
573
640
|
return;
|
|
574
641
|
}
|
|
575
642
|
const qaPhase = begun.task.reviewDispatch.qaPhase;
|
|
643
|
+
const qaAgentId = requireTaskQaAgentId(begun.task, 'handlePrUpdated');
|
|
576
644
|
try {
|
|
577
645
|
await manager.dispatchGitReviewLease(taskId, {
|
|
578
646
|
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
@@ -587,8 +655,8 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
587
655
|
error: err.message,
|
|
588
656
|
});
|
|
589
657
|
}
|
|
590
|
-
else if (err instanceof DispatchTerminalError
|
|
591
|
-
await manager.failTaskForDispatchError(taskId, qaPhase,
|
|
658
|
+
else if (err instanceof DispatchTerminalError) {
|
|
659
|
+
await manager.failTaskForDispatchError(taskId, qaPhase, qaAgentId, err, {
|
|
592
660
|
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
593
661
|
});
|
|
594
662
|
}
|
|
@@ -604,28 +672,27 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
604
672
|
async function redispatchReviewForStaleVerdict(manager, task) {
|
|
605
673
|
if (task.status !== 'review')
|
|
606
674
|
return false;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
catch (err) {
|
|
615
|
-
// 无状态证据时 fail closed:不自动重派,让调用方落 stale-verdict 兜底介入
|
|
616
|
-
console.warn(`[EventHandler] stale-verdict QA state read failed for ${task.qaAgentId}; not redispatching:`, err);
|
|
617
|
-
return false;
|
|
618
|
-
}
|
|
619
|
-
if (qaState?.taskId === task.id && !isRecoverableQaDispatchHold(qaState))
|
|
620
|
-
return false;
|
|
675
|
+
const qaAgentId = requireTaskQaAgentId(task, 'redispatchReviewForStaleVerdict');
|
|
676
|
+
// 当前 pass 的 QA 会话还在跑时,迟到的旧 verdict 只是噪音,不能重派打断;
|
|
677
|
+
// 派发期 hold(checkout 未备好)意味着从未开工,等同无会话,可安全重派
|
|
678
|
+
let qaState;
|
|
679
|
+
try {
|
|
680
|
+
qaState = await manager.getAgentState(qaAgentId);
|
|
621
681
|
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
// 无状态证据时 fail closed:不自动重派,让调用方落 stale-verdict 兜底介入
|
|
684
|
+
console.warn(`[EventHandler] stale-verdict QA state read failed for ${qaAgentId}; not redispatching:`, err);
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
if (qaState?.taskId === task.id && !isRecoverableQaDispatchHold(qaState))
|
|
688
|
+
return false;
|
|
622
689
|
try {
|
|
623
690
|
// fromStatus/expectSignalToken 在 dispatch 锁内复核:本地快照到这里的窗口里任务可能已离开 review
|
|
624
691
|
// 或被并发 dispatcher 换代 pass,不能靠上面的预检
|
|
625
692
|
const entry = manager.getPendingDispatchRetry(task.id);
|
|
626
693
|
const qaPhase = entry?.kind === 'qa-recheck' && entry.signalToken === task.signalToken && entry.qaPhase !== undefined
|
|
627
694
|
? entry.qaPhase
|
|
628
|
-
: (task
|
|
695
|
+
: (taskReviewRound(task) === 0 ? 'review' : undefined);
|
|
629
696
|
await manager.dispatchReviewToQa(task.id, {
|
|
630
697
|
fromStatus: ['review'],
|
|
631
698
|
bumpRound: task.reviewRoundPending === true,
|
|
@@ -653,6 +720,7 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
653
720
|
}
|
|
654
721
|
if (task.failToken === undefined || task.signalToken === undefined)
|
|
655
722
|
return;
|
|
723
|
+
const expectedSignalToken = task.signalToken;
|
|
656
724
|
const provenance = {
|
|
657
725
|
sourceKey: gitVerdict.carrier.sourceKey,
|
|
658
726
|
id: gitVerdict.carrier.id,
|
|
@@ -661,9 +729,67 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
661
729
|
failToken: task.failToken,
|
|
662
730
|
anchorSha: reviewedHeadSha,
|
|
663
731
|
};
|
|
664
|
-
const reviewedRound =
|
|
732
|
+
const reviewedRound = effectiveTaskReviewRound(task);
|
|
733
|
+
if (task.phase === 'spec') {
|
|
734
|
+
const humanApproval = manager.getProjectConfig(task.projectId)?.specApproval === 'human';
|
|
735
|
+
if (!humanApproval) {
|
|
736
|
+
let verified;
|
|
737
|
+
try {
|
|
738
|
+
verified = await manager.platformVerifyPrBinding(task.id, gitVerdict.prNumber);
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
742
|
+
phase: 'git-spec-auto-approve-head-verify-failed',
|
|
743
|
+
error: err instanceof Error ? err.message : String(err),
|
|
744
|
+
});
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
if (!verified.ok) {
|
|
748
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
749
|
+
phase: 'git-spec-auto-approve-binding-invalid',
|
|
750
|
+
reason: verified.reason,
|
|
751
|
+
});
|
|
752
|
+
throw new Error(`automatic spec approval binding is invalid: ${verified.reason}`);
|
|
753
|
+
}
|
|
754
|
+
if (verified.headSha.toLowerCase() !== reviewedHeadSha.toLowerCase()) {
|
|
755
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
756
|
+
phase: 'git-spec-auto-approve-head-mismatch',
|
|
757
|
+
reviewedHeadSha,
|
|
758
|
+
currentHeadSha: verified.headSha,
|
|
759
|
+
source: 'platform-live-read',
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const expectedTask = taskGenerationGuard(task);
|
|
765
|
+
const approval = {
|
|
766
|
+
specReviewRound: reviewedRound,
|
|
767
|
+
expectedSignalToken,
|
|
768
|
+
...(prPatch.prNumber !== undefined ? { prNumber: prPatch.prNumber } : {}),
|
|
769
|
+
...(prPatch.prUrl !== undefined ? { prUrl: prPatch.prUrl } : {}),
|
|
770
|
+
headSha: reviewedHeadSha,
|
|
771
|
+
passProvenance: provenance,
|
|
772
|
+
};
|
|
773
|
+
const transitioned = humanApproval
|
|
774
|
+
? await manager.parkTaskAtSpecReady(task.id, {
|
|
775
|
+
specReviewRound: reviewedRound,
|
|
776
|
+
expectedTask,
|
|
777
|
+
expectedSignalToken,
|
|
778
|
+
...(prPatch.prNumber !== undefined ? { prNumber: prPatch.prNumber } : {}),
|
|
779
|
+
...(prPatch.prUrl !== undefined ? { prUrl: prPatch.prUrl } : {}),
|
|
780
|
+
headSha: reviewedHeadSha,
|
|
781
|
+
passProvenance: provenance,
|
|
782
|
+
})
|
|
783
|
+
: await manager.transitionToCodePhase(task.id, expectedTask, approval);
|
|
784
|
+
if (!transitioned) {
|
|
785
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
786
|
+
phase: 'git-spec-approve-transition-failed',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
665
791
|
const transitioned = await manager.approveGitReviewPass(task.id, {
|
|
666
|
-
expectedSignalToken
|
|
792
|
+
expectedSignalToken,
|
|
667
793
|
headSha: reviewedHeadSha,
|
|
668
794
|
reviewRound: reviewedRound,
|
|
669
795
|
...(prPatch.prNumber !== undefined ? { prNumber: prPatch.prNumber } : {}),
|
|
@@ -673,10 +799,9 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
673
799
|
if (!transitioned)
|
|
674
800
|
return;
|
|
675
801
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
802
|
+
const qaAgentId = requireTaskQaAgentId(transitioned, 'handleReviewApproval');
|
|
803
|
+
await manager.releaseAgentForTask(qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
804
|
+
.catch(err => console.error(`[EventHandler] APPROVE releaseAgentForTask(QA=${qaAgentId}) failed:`, err));
|
|
680
805
|
await dispatchGitDevPostApproveCheck(bus, manager, transitioned, reviewedHeadSha);
|
|
681
806
|
}
|
|
682
807
|
function gitHeadPatch(task, headSha) {
|
|
@@ -694,9 +819,10 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
694
819
|
});
|
|
695
820
|
return;
|
|
696
821
|
}
|
|
697
|
-
const reviewedRound =
|
|
822
|
+
const reviewedRound = effectiveTaskReviewRound(task);
|
|
698
823
|
const nextRound = reviewedRound + 1;
|
|
699
|
-
|
|
824
|
+
const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
825
|
+
if (nextRound > cap) {
|
|
700
826
|
return handleMaxRounds(bus, manager, task, prPatch, reviewedRound);
|
|
701
827
|
}
|
|
702
828
|
const result = await manager.transitionTaskStatus(task.id, 'fixing', {
|
|
@@ -705,7 +831,9 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
705
831
|
}, {
|
|
706
832
|
...prPatch,
|
|
707
833
|
...gitHeadPatch(task, currentHeadSha),
|
|
708
|
-
|
|
834
|
+
...(task.phase === 'spec'
|
|
835
|
+
? { specReviewRound: nextRound }
|
|
836
|
+
: { reviewRound: nextRound }),
|
|
709
837
|
reviewRoundPending: undefined,
|
|
710
838
|
reviewDispatch: undefined,
|
|
711
839
|
fixDispatchedAt: new Date().toISOString(),
|
|
@@ -715,80 +843,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
715
843
|
const { task: transitioned } = result;
|
|
716
844
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
717
845
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
718
|
-
|
|
719
|
-
const qaReleased = await manager
|
|
720
|
-
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
721
|
-
.catch(err => {
|
|
722
|
-
console.error(`[EventHandler] REQUEST_CHANGES releaseAgentForTask(QA=${transitioned.qaAgentId}) failed:`, err);
|
|
723
|
-
return false;
|
|
724
|
-
});
|
|
725
|
-
if (!qaReleased) {
|
|
726
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
727
|
-
phase: 'qa-release-failed-but-dev-dispatched',
|
|
728
|
-
qaAgentId: transitioned.qaAgentId,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const acquired = await manager.acquireAgentForTask(transitioned.agentId, transitioned.id, 'fix');
|
|
733
|
-
if (!acquired) {
|
|
734
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
735
|
-
phase: 'dev-acquire-failed-fix',
|
|
736
|
-
devAgentId: transitioned.agentId,
|
|
737
|
-
});
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed', { expectedStatus: 'fixing', expectedToken: transitioned.signalToken });
|
|
741
|
-
if (!armed) {
|
|
742
|
-
console.warn(`[EventHandler] REQUEST_CHANGES pr-fixed watcher failed to arm for task=${transitioned.id}; holding dev (not dispatching fix)`);
|
|
743
|
-
await manager.markAwaitingHuman(transitioned.agentId, 'signal-arm-failed:pr-fixed', 'pr-fixed watcher failed to arm; the fix was not dispatched (its completion signal would have no consumer). Cancel the task or delete the agent to retry.', { expectedTaskId: transitioned.id });
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
let resumed = false;
|
|
747
|
-
let dispatchErr = null;
|
|
748
|
-
try {
|
|
749
|
-
resumed = await manager.continueSession(transitioned.id, transitioned.agentId, 'fix', {
|
|
750
|
-
signalToken: fixToken,
|
|
751
|
-
guardBeforeInject: async () => {
|
|
752
|
-
const freshTask = await manager.getTask(transitioned.id);
|
|
753
|
-
return freshTask?.status === 'fixing' && freshTask.signalToken === fixToken;
|
|
754
|
-
},
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
catch (err) {
|
|
758
|
-
dispatchErr = err;
|
|
759
|
-
console.error(`[EventHandler] REQUEST_CHANGES continueSession(dev=${transitioned.agentId}, fix) failed:`, err);
|
|
760
|
-
}
|
|
761
|
-
if (!resumed) {
|
|
762
|
-
console.warn(`[EventHandler] REQUEST_CHANGES dev=${transitioned.agentId} not resumable for task=${transitioned.id}; ` +
|
|
763
|
-
`task remains in 'fixing' but no dev session is attached`);
|
|
764
|
-
if (dispatchErr instanceof DispatchTerminalError) {
|
|
765
|
-
await manager.failTaskForDispatchError(transitioned.id, 'fix', transitioned.agentId, dispatchErr);
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
const parked = await manager.markAgentWaiting(transitioned.agentId, transitioned.id)
|
|
769
|
-
.catch(err => {
|
|
770
|
-
console.error(`[EventHandler] REQUEST_CHANGES markAgentWaiting(dev=${transitioned.agentId}) rollback failed:`, err);
|
|
771
|
-
return false;
|
|
772
|
-
});
|
|
773
|
-
if (parked && (dispatchErr instanceof ReplNotReadyError || dispatchErr instanceof DirtyWorkdirError)) {
|
|
774
|
-
// dev 回合在途(忙碌/未提交改动)是常态而非故障:登记待补派,回合结束后由对账 re-continue。
|
|
775
|
-
// 停驻失败说明绑定/锁已不受控,pending 无法证明还有可承接的 session,必须走人工告警
|
|
776
|
-
manager.registerPendingDispatchRetry(transitioned.id, {
|
|
777
|
-
kind: 'dev-fix',
|
|
778
|
-
agentId: transitioned.agentId,
|
|
779
|
-
signalToken: fixToken,
|
|
780
|
-
});
|
|
781
|
-
console.warn(`[EventHandler] REQUEST_CHANGES fix deferred for task=${transitioned.id}: dev pane busy or workdir dirty ` +
|
|
782
|
-
`(${dispatchErr.name}); reconciler will re-continue when idle`);
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
786
|
-
phase: 'fix-resume-failed',
|
|
787
|
-
reviewRound: transitioned.reviewRound,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
846
|
+
await manager.dispatchGitFixToDev(transitioned.id);
|
|
792
847
|
}
|
|
793
848
|
async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
794
849
|
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
@@ -796,7 +851,9 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
796
851
|
expectSignalToken: task.signalToken,
|
|
797
852
|
}, {
|
|
798
853
|
...prPatch,
|
|
799
|
-
|
|
854
|
+
...(task.phase === 'spec'
|
|
855
|
+
? { specReviewRound: reviewedRound }
|
|
856
|
+
: { reviewRound: reviewedRound }),
|
|
800
857
|
reviewRoundPending: undefined,
|
|
801
858
|
reviewDispatch: undefined,
|
|
802
859
|
});
|
|
@@ -805,16 +862,15 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
805
862
|
const { task: transitioned } = result;
|
|
806
863
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
807
864
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
865
|
+
const qaAgentId = requireTaskQaAgentId(transitioned, 'handleMaxRounds');
|
|
866
|
+
const qaState = await manager.getAgentState(qaAgentId);
|
|
867
|
+
if (qaState?.taskId === transitioned.id) {
|
|
868
|
+
await manager
|
|
869
|
+
.releaseAgentForTask(qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
870
|
+
.catch(err => {
|
|
871
|
+
console.error(`[EventHandler] max_rounds releaseAgentForTask(QA=${qaAgentId}) failed:`, err);
|
|
872
|
+
return false;
|
|
873
|
+
});
|
|
818
874
|
}
|
|
819
875
|
try {
|
|
820
876
|
await bus.emit({
|
|
@@ -824,7 +880,10 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
824
880
|
projectId: transitioned.projectId,
|
|
825
881
|
agentId: transitioned.agentId,
|
|
826
882
|
taskId: transitioned.id,
|
|
827
|
-
data: {
|
|
883
|
+
data: {
|
|
884
|
+
phase: transitioned.phase,
|
|
885
|
+
reviewRound: taskReviewRound(transitioned),
|
|
886
|
+
},
|
|
828
887
|
});
|
|
829
888
|
}
|
|
830
889
|
catch (emitErr) {
|
|
@@ -832,37 +891,17 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
832
891
|
}
|
|
833
892
|
}
|
|
834
893
|
export function registerEventHandlers(bus, manager) {
|
|
835
|
-
|
|
894
|
+
const handleGitPrDelivery = async (event, deliveryPhase) => {
|
|
836
895
|
if (!event.taskId || !event.agentId)
|
|
837
896
|
return;
|
|
838
897
|
const taskAtEntry = await manager.getTask(event.taskId);
|
|
839
898
|
if (!taskAtEntry)
|
|
840
899
|
return;
|
|
841
|
-
if (taskAtEntry.reviewMode === 'server') {
|
|
842
|
-
const prNumber = event.data.prNumber;
|
|
843
|
-
if (!Number.isInteger(prNumber) || prNumber < 1)
|
|
844
|
-
return;
|
|
845
|
-
const adopted = await manager.adoptServerPr(event.taskId, {
|
|
846
|
-
prNumber: prNumber,
|
|
847
|
-
...(typeof event.data.prUrl === 'string' ? { prUrl: event.data.prUrl } : {}),
|
|
848
|
-
...(validHeadSha(event.data.headSha) !== undefined ? { headSha: validHeadSha(event.data.headSha) } : {}),
|
|
849
|
-
...(typeof event.data.targetBranch === 'string' ? { targetBranch: event.data.targetBranch } : {}),
|
|
850
|
-
});
|
|
851
|
-
if (!adopted) {
|
|
852
|
-
await emitIntervention(bus, taskAtEntry.projectId, taskAtEntry.agentId, taskAtEntry.id, {
|
|
853
|
-
phase: 'server-pr-adoption-refused',
|
|
854
|
-
prNumber,
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (isSpecStagePhase(taskAtEntry.phase)) {
|
|
860
|
-
console.warn(`[EventHandler] pr.created ignored for task ${event.taskId}: task in spec phase`);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
900
|
if (event.data.source === 'pane-signal'
|
|
864
901
|
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
865
902
|
&& taskAtEntry.status !== 'in_progress') {
|
|
903
|
+
if (taskAtEntry.phase !== deliveryPhase)
|
|
904
|
+
return;
|
|
866
905
|
if (taskAtEntry.pendingPrSignalToken === undefined
|
|
867
906
|
|| event.data.token !== taskAtEntry.pendingPrSignalToken) {
|
|
868
907
|
return;
|
|
@@ -948,8 +987,8 @@ export function registerEventHandlers(bus, manager) {
|
|
|
948
987
|
return;
|
|
949
988
|
}
|
|
950
989
|
}
|
|
951
|
-
|
|
952
|
-
if (!task
|
|
990
|
+
let task = await manager.getTask(event.taskId);
|
|
991
|
+
if (!task)
|
|
953
992
|
return;
|
|
954
993
|
if (event.data.source !== 'pane-signal'
|
|
955
994
|
&& task.branch !== BRANCH_PREFIX + event.taskId
|
|
@@ -958,26 +997,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
958
997
|
`for task ${event.taskId} (untracked; custom-branch PRs adopt via the pane signal)`);
|
|
959
998
|
return;
|
|
960
999
|
}
|
|
961
|
-
const adoption = {};
|
|
962
1000
|
const target = event.data.source === 'pane-signal'
|
|
963
1001
|
? verifiedTargetBranch
|
|
964
1002
|
: (typeof event.data.targetBranch === 'string' ? event.data.targetBranch : undefined);
|
|
965
|
-
if (target !== undefined && task.baseBranch === undefined)
|
|
966
|
-
adoption.baseBranch = target;
|
|
967
|
-
if (event.data.source === 'pane-signal') {
|
|
968
|
-
const actorId = typeof event.data.actorB64 === 'string'
|
|
969
|
-
? decodeSignalActorId(event.data.actorB64)
|
|
970
|
-
: undefined;
|
|
971
|
-
if (actorId !== undefined) {
|
|
972
|
-
adoption.replyActorId = actorId;
|
|
973
|
-
adoption.replyActorStatus = 'verified';
|
|
974
|
-
adoption.pendingPrSignalToken = undefined;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
else if (typeof event.data.prAuthorId === 'string' && task.replyActorStatus !== 'verified') {
|
|
978
|
-
adoption.replyActorId = event.data.prAuthorId;
|
|
979
|
-
adoption.replyActorStatus = 'provisional';
|
|
980
|
-
}
|
|
981
1003
|
let reviewHead = validHeadSha(event.data.headSha) ?? paneVerifiedHeadSha;
|
|
982
1004
|
if (reviewHead === undefined && typeof event.data.prNumber === 'number') {
|
|
983
1005
|
const verified = await manager.platformVerifyPrBinding(event.taskId, event.data.prNumber);
|
|
@@ -990,6 +1012,54 @@ export function registerEventHandlers(bus, manager) {
|
|
|
990
1012
|
});
|
|
991
1013
|
return;
|
|
992
1014
|
}
|
|
1015
|
+
if (deliveryPhase === undefined) {
|
|
1016
|
+
if (typeof event.data.prNumber !== 'number')
|
|
1017
|
+
return;
|
|
1018
|
+
await manager.recordGitPrObservation(event.taskId, {
|
|
1019
|
+
prNumber: event.data.prNumber,
|
|
1020
|
+
...(typeof event.data.prUrl === 'string' ? { prUrl: event.data.prUrl } : {}),
|
|
1021
|
+
headSha: reviewHead,
|
|
1022
|
+
...(target !== undefined ? { targetBranch: target } : {}),
|
|
1023
|
+
...(typeof event.data.prAuthorId === 'string' ? { prAuthorId: event.data.prAuthorId } : {}),
|
|
1024
|
+
});
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const actorId = typeof event.data.actorB64 === 'string'
|
|
1028
|
+
? decodeSignalActorId(event.data.actorB64)
|
|
1029
|
+
: undefined;
|
|
1030
|
+
if (actorId === undefined || typeof event.data.prNumber !== 'number') {
|
|
1031
|
+
await reArmDevelopWatcher(manager, task, event.agentId);
|
|
1032
|
+
await emitIntervention(bus, task.projectId, event.agentId, event.taskId, {
|
|
1033
|
+
phase: 'git-pr-delivery-actor-missing',
|
|
1034
|
+
deliveryPhase,
|
|
1035
|
+
claimedPrNumber: event.data.prNumber,
|
|
1036
|
+
});
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const confirmed = await manager.confirmGitDelivery(event.taskId, {
|
|
1040
|
+
phase: deliveryPhase,
|
|
1041
|
+
source: 'signal',
|
|
1042
|
+
prNumber: event.data.prNumber,
|
|
1043
|
+
...(typeof event.data.prUrl === 'string' ? { prUrl: event.data.prUrl } : {}),
|
|
1044
|
+
headSha: reviewHead,
|
|
1045
|
+
...(target !== undefined ? { targetBranch: target } : {}),
|
|
1046
|
+
...(reconciledBranch !== undefined ? { branch: reconciledBranch } : {}),
|
|
1047
|
+
actorId,
|
|
1048
|
+
expectedSignalToken: typeof event.data.token === 'string' ? event.data.token : undefined,
|
|
1049
|
+
});
|
|
1050
|
+
if (!confirmed) {
|
|
1051
|
+
const current = await manager.getTask(event.taskId);
|
|
1052
|
+
if (current?.status === 'in_progress') {
|
|
1053
|
+
await reArmDevelopWatcher(manager, current, event.agentId);
|
|
1054
|
+
await emitIntervention(bus, current.projectId, event.agentId, event.taskId, {
|
|
1055
|
+
phase: 'pane-pr-delivery-transition-failed',
|
|
1056
|
+
deliveryPhase,
|
|
1057
|
+
claimedPrNumber: event.data.prNumber,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
task = confirmed;
|
|
993
1063
|
if (task.status !== 'in_progress' && task.status !== 'fixing')
|
|
994
1064
|
return;
|
|
995
1065
|
const begun = await manager.beginGitReviewPass(event.taskId, {
|
|
@@ -1000,11 +1070,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1000
1070
|
expectPhase: task.phase,
|
|
1001
1071
|
expectSignalToken: task.signalToken,
|
|
1002
1072
|
patch: {
|
|
1003
|
-
...(event.data.prNumber !== undefined ? { prNumber: event.data.prNumber } : {}),
|
|
1004
|
-
...(event.data.prUrl !== undefined ? { prUrl: event.data.prUrl } : {}),
|
|
1005
1073
|
latestHeadSha: reviewHead,
|
|
1006
|
-
...(reconciledBranch ? { branch: reconciledBranch } : {}),
|
|
1007
|
-
...adoption,
|
|
1008
1074
|
},
|
|
1009
1075
|
});
|
|
1010
1076
|
if (!begun?.task.reviewDispatch) {
|
|
@@ -1025,18 +1091,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1025
1091
|
if (reconciledBranch) {
|
|
1026
1092
|
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: ${task.branch} → ${reconciledBranch}`);
|
|
1027
1093
|
}
|
|
1028
|
-
|
|
1029
|
-
const parked = await manager.markAgentWaiting(event.agentId, begun.task.id);
|
|
1030
|
-
if (!parked) {
|
|
1031
|
-
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1032
|
-
phase: 'dev-wait-gate-failed-no-qa',
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1036
|
-
phase: 'git-review-no-qa-partner',
|
|
1037
|
-
});
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1094
|
+
const qaAgentId = requireTaskQaAgentId(begun.task, 'pr.created');
|
|
1040
1095
|
try {
|
|
1041
1096
|
await manager.dispatchGitReviewLease(begun.task.id, {
|
|
1042
1097
|
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
@@ -1052,7 +1107,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1052
1107
|
});
|
|
1053
1108
|
}
|
|
1054
1109
|
else if (err instanceof DispatchTerminalError) {
|
|
1055
|
-
await manager.failTaskForDispatchError(begun.task.id, 'review',
|
|
1110
|
+
await manager.failTaskForDispatchError(begun.task.id, 'review', qaAgentId, err, {
|
|
1056
1111
|
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
1057
1112
|
});
|
|
1058
1113
|
}
|
|
@@ -1064,16 +1119,20 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1064
1119
|
});
|
|
1065
1120
|
}
|
|
1066
1121
|
}
|
|
1122
|
+
};
|
|
1123
|
+
bus.on('pr.created', async (event) => {
|
|
1124
|
+
await handleGitPrDelivery(event, event.data.source === 'pane-signal' ? 'code' : undefined);
|
|
1125
|
+
});
|
|
1126
|
+
bus.on('spec.ready', async (event) => {
|
|
1127
|
+
const task = event.taskId ? await manager.getTask(event.taskId) : null;
|
|
1128
|
+
if (!task)
|
|
1129
|
+
return;
|
|
1130
|
+
await handleGitPrDelivery(event, 'spec');
|
|
1067
1131
|
});
|
|
1068
1132
|
bus.on('pr.updated', async (event) => {
|
|
1069
1133
|
if (!event.taskId)
|
|
1070
1134
|
return;
|
|
1071
|
-
const taskAtEntry = await manager.getTask(event.taskId);
|
|
1072
1135
|
const eventKind = event.data.kind;
|
|
1073
|
-
if (taskAtEntry && isSpecStagePhase(taskAtEntry.phase)) {
|
|
1074
|
-
console.warn(`[EventHandler] pr.updated (kind=${eventKind ?? 'push'}) ignored for task ${event.taskId}: task in spec phase`);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
1136
|
if (eventKind === 'pr-merge-ready')
|
|
1078
1137
|
return handlePrMergeReady(bus, manager, event);
|
|
1079
1138
|
if (eventKind === 'closed-unmerged' || eventKind === 'reopened') {
|
|
@@ -1089,8 +1148,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1089
1148
|
}
|
|
1090
1149
|
if (!event.agentId)
|
|
1091
1150
|
return;
|
|
1092
|
-
if (taskAtEntry?.reviewMode === 'server')
|
|
1093
|
-
return;
|
|
1094
1151
|
if (eventKind === 'comment' || eventKind === 'review-comment') {
|
|
1095
1152
|
return handlePrFeedback(bus, manager, event);
|
|
1096
1153
|
}
|
|
@@ -1100,43 +1157,125 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1100
1157
|
bus.on('pr.merged', async (event) => {
|
|
1101
1158
|
if (!event.taskId)
|
|
1102
1159
|
return;
|
|
1160
|
+
const taskId = event.taskId;
|
|
1103
1161
|
const eventPrNumber = event.data.prNumber;
|
|
1104
1162
|
const eventPrUrl = event.data.prUrl;
|
|
1105
|
-
|
|
1106
|
-
// this task never tracked (e.g. a reused custom branch name). bx/<taskId> is unique to this task,
|
|
1107
|
-
// so its merge is definitively ours even if pr.created was missed and prNumber was never recorded;
|
|
1108
|
-
// for any other branch require the tracked PR number to match, else a foreign/stale merge on a
|
|
1109
|
-
// reused custom branch name could prematurely finish an active task.
|
|
1110
|
-
const taskBefore = await manager.getTask(event.taskId);
|
|
1163
|
+
const taskBefore = await manager.getTask(taskId);
|
|
1111
1164
|
if (!taskBefore)
|
|
1112
1165
|
return;
|
|
1113
|
-
|
|
1166
|
+
const mergedOwnBxBranch = event.data.branch === BRANCH_PREFIX + taskId;
|
|
1167
|
+
const sameBoundPr = eventPrNumber !== undefined
|
|
1168
|
+
&& (taskBefore.prNumber === eventPrNumber || (taskBefore.prNumber === undefined && mergedOwnBxBranch));
|
|
1169
|
+
if (eventPrNumber !== undefined && !sameBoundPr) {
|
|
1170
|
+
await emitIntervention(bus, taskBefore.projectId, taskBefore.agentId, taskBefore.id, {
|
|
1171
|
+
phase: 'pr-merged-status-race',
|
|
1172
|
+
status: taskBefore.status,
|
|
1173
|
+
reason: taskBefore.prNumber === undefined ? 'pr-binding-unconfirmed' : 'pr-binding-changed',
|
|
1174
|
+
boundPrNumber: taskBefore.prNumber,
|
|
1175
|
+
prNumber: eventPrNumber,
|
|
1176
|
+
});
|
|
1114
1177
|
return;
|
|
1115
|
-
|
|
1116
|
-
if (
|
|
1178
|
+
}
|
|
1179
|
+
if ((taskBefore.status === 'merged' || taskBefore.status === 'done') && sameBoundPr)
|
|
1117
1180
|
return;
|
|
1181
|
+
let mergeExpectedPrNumber = taskBefore.prNumber;
|
|
1182
|
+
let mergeBindingConfirmed = sameBoundPr;
|
|
1118
1183
|
const prPatch = {
|
|
1119
1184
|
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1120
1185
|
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
1121
1186
|
};
|
|
1122
|
-
|
|
1123
|
-
|
|
1187
|
+
if (taskBefore.status !== 'merge-ready') {
|
|
1188
|
+
if (!sameBoundPr || !PREMATURE_MERGE_BLOCKABLE_STATUSES.includes(taskBefore.status)) {
|
|
1189
|
+
await emitIntervention(bus, taskBefore.projectId, taskBefore.agentId, taskBefore.id, {
|
|
1190
|
+
phase: 'pr-merged-outside-merge-ready',
|
|
1191
|
+
status: taskBefore.status,
|
|
1192
|
+
taskPhase: taskBefore.phase,
|
|
1193
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1194
|
+
});
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const blocked = await manager.transitionTaskStatus(taskId, 'failed', {
|
|
1198
|
+
fromStatus: PREMATURE_MERGE_BLOCKABLE_STATUSES,
|
|
1199
|
+
expectPrNumber: taskBefore.prNumber,
|
|
1200
|
+
}, prPatch);
|
|
1201
|
+
if (blocked) {
|
|
1202
|
+
manager.stopPhaseSignalWatcher(taskId);
|
|
1203
|
+
await emitIntervention(bus, blocked.task.projectId, blocked.task.agentId, blocked.task.id, {
|
|
1204
|
+
phase: 'pr-merged-outside-merge-ready',
|
|
1205
|
+
status: blocked.previousStatus,
|
|
1206
|
+
taskPhase: blocked.task.phase,
|
|
1207
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1208
|
+
});
|
|
1209
|
+
await cleanupPrematureMergeParticipants(bus, manager, blocked.task);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
const current = await manager.getTask(taskId);
|
|
1213
|
+
if (!current)
|
|
1214
|
+
return;
|
|
1215
|
+
const currentSameBoundPr = eventPrNumber !== undefined
|
|
1216
|
+
&& (current.prNumber === eventPrNumber || (current.prNumber === undefined && mergedOwnBxBranch));
|
|
1217
|
+
if ((current.status === 'merged' || current.status === 'done') && currentSameBoundPr)
|
|
1218
|
+
return;
|
|
1219
|
+
if (!currentSameBoundPr) {
|
|
1220
|
+
await emitIntervention(bus, current.projectId, current.agentId, current.id, {
|
|
1221
|
+
phase: 'pr-merged-status-race',
|
|
1222
|
+
status: current.status,
|
|
1223
|
+
reason: 'pr-binding-changed',
|
|
1224
|
+
boundPrNumber: current.prNumber,
|
|
1225
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1226
|
+
});
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (current.status !== 'merge-ready') {
|
|
1230
|
+
await emitIntervention(bus, current.projectId, current.agentId, current.id, {
|
|
1231
|
+
phase: 'pr-merged-status-race',
|
|
1232
|
+
status: current.status,
|
|
1233
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1234
|
+
});
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
mergeExpectedPrNumber = current.prNumber;
|
|
1238
|
+
mergeBindingConfirmed = true;
|
|
1239
|
+
}
|
|
1240
|
+
if (!mergeBindingConfirmed) {
|
|
1241
|
+
await emitIntervention(bus, taskBefore.projectId, taskBefore.agentId, taskBefore.id, {
|
|
1242
|
+
phase: 'pr-merged-status-race',
|
|
1243
|
+
status: taskBefore.status,
|
|
1244
|
+
reason: 'pr-binding-unconfirmed',
|
|
1245
|
+
boundPrNumber: taskBefore.prNumber,
|
|
1246
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const result = await manager.transitionTaskStatus(taskId, 'merged', { fromStatus: ['merge-ready'], expectPrNumber: mergeExpectedPrNumber }, prPatch);
|
|
1251
|
+
if (!result) {
|
|
1252
|
+
const current = await manager.getTask(event.taskId);
|
|
1253
|
+
if (current && (current.status === 'merged' || current.status === 'done')
|
|
1254
|
+
&& eventPrNumber !== undefined && current.prNumber === eventPrNumber)
|
|
1255
|
+
return;
|
|
1256
|
+
if (current) {
|
|
1257
|
+
await emitIntervention(bus, current.projectId, current.agentId, current.id, {
|
|
1258
|
+
phase: 'pr-merged-status-race',
|
|
1259
|
+
status: current.status,
|
|
1260
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1124
1263
|
return;
|
|
1264
|
+
}
|
|
1125
1265
|
const { task: transitioned } = result;
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1266
|
+
const qaAgentId = requireTaskQaAgentId(transitioned, 'pr.merged');
|
|
1267
|
+
if (transitioned.prNumber && transitioned.branch) {
|
|
1268
|
+
await manager.dispatchPostMergeCleanup(qaAgentId, {
|
|
1269
|
+
taskId: transitioned.id,
|
|
1270
|
+
branch: transitioned.branch,
|
|
1271
|
+
}).catch(err => console.warn(`[EventHandler] pr.merged dispatchPostMergeCleanup(QA=${qaAgentId}) failed:`, err));
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
try {
|
|
1275
|
+
await manager.releaseAgentForTask(qaAgentId, transitioned.id, 'idle');
|
|
1132
1276
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
await manager.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle');
|
|
1136
|
-
}
|
|
1137
|
-
catch (err) {
|
|
1138
|
-
console.error(`[EventHandler] pr.merged releaseAgentForTask(QA=${transitioned.qaAgentId}) failed:`, err);
|
|
1139
|
-
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
console.error(`[EventHandler] pr.merged releaseAgentForTask(QA=${qaAgentId}) failed:`, err);
|
|
1140
1279
|
}
|
|
1141
1280
|
}
|
|
1142
1281
|
try {
|
|
@@ -1152,8 +1291,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1152
1291
|
const task = await manager.getTask(event.taskId);
|
|
1153
1292
|
if (!task)
|
|
1154
1293
|
return;
|
|
1155
|
-
if (task.reviewMode === 'server')
|
|
1156
|
-
return;
|
|
1157
1294
|
if (event.data.source === 'pane-signal') {
|
|
1158
1295
|
console.log(`[EventHandler] review.submitted pane verdict ignored for task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1159
1296
|
return;
|
|
@@ -1199,10 +1336,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1199
1336
|
await rejectGitPayload(new GitVerdictPayloadError('verdict token is not current'));
|
|
1200
1337
|
return;
|
|
1201
1338
|
}
|
|
1202
|
-
if (isSpecStagePhase(task.phase)) {
|
|
1203
|
-
console.warn(`[EventHandler] review.submitted (action=${action}) ignored for task ${task.id}: task in spec phase`);
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
1339
|
const verdictSubmittedAt = typeof event.data.submittedAt === 'string' ? event.data.submittedAt : undefined;
|
|
1207
1340
|
if (verdictSubmittedAt && task.reviewDispatchedAt) {
|
|
1208
1341
|
const submittedMs = Date.parse(verdictSubmittedAt);
|
|
@@ -1241,7 +1374,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1241
1374
|
if (!event.taskId || !event.agentId)
|
|
1242
1375
|
return;
|
|
1243
1376
|
const task = await manager.getTask(event.taskId);
|
|
1244
|
-
if (!task || task.
|
|
1377
|
+
if (!task || task.status !== 'fixing')
|
|
1245
1378
|
return;
|
|
1246
1379
|
const { projectId, agentId } = task;
|
|
1247
1380
|
const stayFixing = async (data) => {
|