baxian 2.0.0 → 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 -13
- 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 +19 -14
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +90 -134
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +1154 -2265
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts +3 -0
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +6 -0
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/pane-streamer-manager.d.ts +6 -6
- package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +6 -3
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +70 -40
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +7 -2
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +90 -32
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +19 -38
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +54 -88
- 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 -35
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +22 -162
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +4 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +33 -11
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/root-agent-runtime.d.ts +80 -0
- package/dist/agent/root-agent-runtime.d.ts.map +1 -0
- package/dist/agent/root-agent-runtime.js +637 -0
- package/dist/agent/root-agent-runtime.js.map +1 -0
- package/dist/agent/root-recovery-coordinator.d.ts +124 -0
- package/dist/agent/root-recovery-coordinator.d.ts.map +1 -0
- package/dist/agent/root-recovery-coordinator.js +1557 -0
- package/dist/agent/root-recovery-coordinator.js.map +1 -0
- package/dist/agent/runner.d.ts +1 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +103 -7
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts +1 -0
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +16 -10
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +3 -0
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +11 -5
- package/dist/agent/tmux.js.map +1 -1
- package/dist/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/agents.d.ts.map +1 -1
- package/dist/api/agents.js +42 -1
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +83 -9
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +68 -13
- package/dist/api/hosts.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 +621 -325
- 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/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -7
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts +5 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +57 -24
- package/dist/config/loader.js.map +1 -1
- package/dist/config/normalizer.d.ts.map +1 -1
- package/dist/config/normalizer.js +18 -0
- package/dist/config/normalizer.js.map +1 -1
- package/dist/config/validator.d.ts +7 -4
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +251 -96
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +359 -229
- package/dist/event/handlers.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -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 +1 -11
- 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 +8 -7
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +6 -28
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +52 -125
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +24 -5
- package/dist/shared/types.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +6 -6
- package/dist/skills/baxian-pr-recheck/SKILL.md +6 -0
- package/dist/skills/baxian-pr-review/SKILL.md +6 -0
- package/dist/skills/baxian-signals/SKILL.md +17 -28
- package/dist/skills/baxian-task-check/SKILL.md +13 -14
- package/dist/state/init.d.ts.map +1 -1
- package/dist/state/init.js +1 -0
- package/dist/state/init.js.map +1 -1
- package/dist/state/lock.d.ts +6 -0
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +8 -0
- package/dist/state/lock.js.map +1 -1
- package/dist/state/root-recovery-store.d.ts +107 -0
- package/dist/state/root-recovery-store.d.ts.map +1 -0
- package/dist/state/root-recovery-store.js +400 -0
- package/dist/state/root-recovery-store.js.map +1 -0
- package/dist/state/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 +57 -137
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts +3 -3
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js.map +1 -1
- package/dist/terminal/stream-ws.d.ts.map +1 -1
- package/dist/terminal/stream-ws.js.map +1 -1
- package/dist/web/assets/index-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 -1221
- 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) {
|
|
@@ -191,9 +236,6 @@ async function dispatchGitDevPostApproveCheck(bus, manager, snapshot, approvedHe
|
|
|
191
236
|
resumed = await manager.continueSession(live.id, live.agentId, 'post-approve', {
|
|
192
237
|
signalToken: key.token,
|
|
193
238
|
postApproveEpisode: key,
|
|
194
|
-
...(opts.redispatchCount !== undefined
|
|
195
|
-
? { postApproveRedispatchCount: opts.redispatchCount }
|
|
196
|
-
: {}),
|
|
197
239
|
});
|
|
198
240
|
}
|
|
199
241
|
catch (err) {
|
|
@@ -225,7 +267,7 @@ async function dispatchGitDevPostApproveCheck(bus, manager, snapshot, approvedHe
|
|
|
225
267
|
async function handlePrMergeReady(bus, manager, event) {
|
|
226
268
|
const taskId = event.taskId;
|
|
227
269
|
const taskNow = await manager.getTask(taskId);
|
|
228
|
-
if (!taskNow
|
|
270
|
+
if (!taskNow)
|
|
229
271
|
return;
|
|
230
272
|
const eventPrNumber = event.data.prNumber;
|
|
231
273
|
const eventPrUrl = event.data.prUrl;
|
|
@@ -263,7 +305,6 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
263
305
|
]);
|
|
264
306
|
if (!freshTask
|
|
265
307
|
|| freshTask.status !== 'approved'
|
|
266
|
-
|| freshTask.reviewMode !== 'git'
|
|
267
308
|
|| freshTask.agentId !== taskNow.agentId
|
|
268
309
|
|| freshTask.postApproveRevoked !== undefined
|
|
269
310
|
|| entryCompletion?.token !== signalToken) {
|
|
@@ -311,14 +352,15 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
311
352
|
patch: { latestHeadSha: gitEpisodeKey.headSha },
|
|
312
353
|
});
|
|
313
354
|
if (begun?.task.reviewDispatch) {
|
|
355
|
+
const qaAgentId = requireTaskQaAgentId(begun.task, 'handlePostApproveMergeReady');
|
|
314
356
|
try {
|
|
315
357
|
await manager.dispatchGitReviewLease(freshTask.id, {
|
|
316
358
|
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
317
359
|
});
|
|
318
360
|
}
|
|
319
361
|
catch (err) {
|
|
320
|
-
if (err instanceof DispatchTerminalError
|
|
321
|
-
await manager.failTaskForDispatchError(freshTask.id, 'recheck',
|
|
362
|
+
if (err instanceof DispatchTerminalError) {
|
|
363
|
+
await manager.failTaskForDispatchError(freshTask.id, 'recheck', qaAgentId, err, { expectedReviewDispatch: begun.task.reviewDispatch });
|
|
322
364
|
return;
|
|
323
365
|
}
|
|
324
366
|
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
@@ -385,7 +427,7 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
385
427
|
async function handlePrFeedback(bus, manager, event) {
|
|
386
428
|
const taskId = event.taskId;
|
|
387
429
|
const taskNow = await manager.getTask(taskId);
|
|
388
|
-
if (!taskNow
|
|
430
|
+
if (!taskNow)
|
|
389
431
|
return;
|
|
390
432
|
const revision = parseGitFeedbackRevision(event.data.revision);
|
|
391
433
|
if (!revision) {
|
|
@@ -450,7 +492,7 @@ export async function recoverGitPostApprovePending(bus, manager) {
|
|
|
450
492
|
async function recoverOneGitPendingTask(bus, manager, task) {
|
|
451
493
|
{
|
|
452
494
|
const live = await manager.getTask(task.id);
|
|
453
|
-
if (!live || live.
|
|
495
|
+
if (!live || live.status !== 'approved' || live.pendingRedispatch !== true)
|
|
454
496
|
return;
|
|
455
497
|
if (live.postApproveRevoked)
|
|
456
498
|
return;
|
|
@@ -519,8 +561,19 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
519
561
|
const eventKind = event.data.kind;
|
|
520
562
|
const eventHeadSha = validHeadSha(event.data.headSha);
|
|
521
563
|
const task = await manager.getTask(taskId);
|
|
522
|
-
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
|
+
}
|
|
523
575
|
return;
|
|
576
|
+
}
|
|
524
577
|
if (eventKind === 'push' && eventHeadSha
|
|
525
578
|
&& event.data.source !== 'pr-fixed'
|
|
526
579
|
&& task.reviewDispatch === undefined
|
|
@@ -532,6 +585,16 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
532
585
|
console.warn(`[EventHandler] pr.updated: task ${taskId} in_progress but neither task nor event has prNumber; deferring catch-up`);
|
|
533
586
|
return;
|
|
534
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
|
+
}
|
|
535
598
|
if (!PR_UPDATED_REVIEW_FROM_STATUSES.includes(task.status))
|
|
536
599
|
return;
|
|
537
600
|
let reviewHead = eventHeadSha ?? validHeadSha(task.latestHeadSha);
|
|
@@ -548,6 +611,7 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
548
611
|
return;
|
|
549
612
|
}
|
|
550
613
|
const bumpRound = task.status === 'in_progress'
|
|
614
|
+
|| task.status === 'spec-ready'
|
|
551
615
|
|| task.status === 'approved'
|
|
552
616
|
|| task.status === 'merge-ready'
|
|
553
617
|
|| (task.status === 'review' && task.reviewRoundPending === true);
|
|
@@ -576,6 +640,7 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
576
640
|
return;
|
|
577
641
|
}
|
|
578
642
|
const qaPhase = begun.task.reviewDispatch.qaPhase;
|
|
643
|
+
const qaAgentId = requireTaskQaAgentId(begun.task, 'handlePrUpdated');
|
|
579
644
|
try {
|
|
580
645
|
await manager.dispatchGitReviewLease(taskId, {
|
|
581
646
|
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
@@ -590,8 +655,8 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
590
655
|
error: err.message,
|
|
591
656
|
});
|
|
592
657
|
}
|
|
593
|
-
else if (err instanceof DispatchTerminalError
|
|
594
|
-
await manager.failTaskForDispatchError(taskId, qaPhase,
|
|
658
|
+
else if (err instanceof DispatchTerminalError) {
|
|
659
|
+
await manager.failTaskForDispatchError(taskId, qaPhase, qaAgentId, err, {
|
|
595
660
|
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
596
661
|
});
|
|
597
662
|
}
|
|
@@ -607,28 +672,27 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
607
672
|
async function redispatchReviewForStaleVerdict(manager, task) {
|
|
608
673
|
if (task.status !== 'review')
|
|
609
674
|
return false;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
catch (err) {
|
|
618
|
-
// 无状态证据时 fail closed:不自动重派,让调用方落 stale-verdict 兜底介入
|
|
619
|
-
console.warn(`[EventHandler] stale-verdict QA state read failed for ${task.qaAgentId}; not redispatching:`, err);
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
if (qaState?.taskId === task.id && !isRecoverableQaDispatchHold(qaState))
|
|
623
|
-
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);
|
|
624
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;
|
|
625
689
|
try {
|
|
626
690
|
// fromStatus/expectSignalToken 在 dispatch 锁内复核:本地快照到这里的窗口里任务可能已离开 review
|
|
627
691
|
// 或被并发 dispatcher 换代 pass,不能靠上面的预检
|
|
628
692
|
const entry = manager.getPendingDispatchRetry(task.id);
|
|
629
693
|
const qaPhase = entry?.kind === 'qa-recheck' && entry.signalToken === task.signalToken && entry.qaPhase !== undefined
|
|
630
694
|
? entry.qaPhase
|
|
631
|
-
: (task
|
|
695
|
+
: (taskReviewRound(task) === 0 ? 'review' : undefined);
|
|
632
696
|
await manager.dispatchReviewToQa(task.id, {
|
|
633
697
|
fromStatus: ['review'],
|
|
634
698
|
bumpRound: task.reviewRoundPending === true,
|
|
@@ -656,6 +720,7 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
656
720
|
}
|
|
657
721
|
if (task.failToken === undefined || task.signalToken === undefined)
|
|
658
722
|
return;
|
|
723
|
+
const expectedSignalToken = task.signalToken;
|
|
659
724
|
const provenance = {
|
|
660
725
|
sourceKey: gitVerdict.carrier.sourceKey,
|
|
661
726
|
id: gitVerdict.carrier.id,
|
|
@@ -664,9 +729,67 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
664
729
|
failToken: task.failToken,
|
|
665
730
|
anchorSha: reviewedHeadSha,
|
|
666
731
|
};
|
|
667
|
-
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
|
+
}
|
|
668
791
|
const transitioned = await manager.approveGitReviewPass(task.id, {
|
|
669
|
-
expectedSignalToken
|
|
792
|
+
expectedSignalToken,
|
|
670
793
|
headSha: reviewedHeadSha,
|
|
671
794
|
reviewRound: reviewedRound,
|
|
672
795
|
...(prPatch.prNumber !== undefined ? { prNumber: prPatch.prNumber } : {}),
|
|
@@ -676,10 +799,9 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
676
799
|
if (!transitioned)
|
|
677
800
|
return;
|
|
678
801
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
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));
|
|
683
805
|
await dispatchGitDevPostApproveCheck(bus, manager, transitioned, reviewedHeadSha);
|
|
684
806
|
}
|
|
685
807
|
function gitHeadPatch(task, headSha) {
|
|
@@ -697,9 +819,10 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
697
819
|
});
|
|
698
820
|
return;
|
|
699
821
|
}
|
|
700
|
-
const reviewedRound =
|
|
822
|
+
const reviewedRound = effectiveTaskReviewRound(task);
|
|
701
823
|
const nextRound = reviewedRound + 1;
|
|
702
|
-
|
|
824
|
+
const cap = manager.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
825
|
+
if (nextRound > cap) {
|
|
703
826
|
return handleMaxRounds(bus, manager, task, prPatch, reviewedRound);
|
|
704
827
|
}
|
|
705
828
|
const result = await manager.transitionTaskStatus(task.id, 'fixing', {
|
|
@@ -708,7 +831,9 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
708
831
|
}, {
|
|
709
832
|
...prPatch,
|
|
710
833
|
...gitHeadPatch(task, currentHeadSha),
|
|
711
|
-
|
|
834
|
+
...(task.phase === 'spec'
|
|
835
|
+
? { specReviewRound: nextRound }
|
|
836
|
+
: { reviewRound: nextRound }),
|
|
712
837
|
reviewRoundPending: undefined,
|
|
713
838
|
reviewDispatch: undefined,
|
|
714
839
|
fixDispatchedAt: new Date().toISOString(),
|
|
@@ -718,80 +843,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
718
843
|
const { task: transitioned } = result;
|
|
719
844
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
720
845
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
721
|
-
|
|
722
|
-
const qaReleased = await manager
|
|
723
|
-
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
724
|
-
.catch(err => {
|
|
725
|
-
console.error(`[EventHandler] REQUEST_CHANGES releaseAgentForTask(QA=${transitioned.qaAgentId}) failed:`, err);
|
|
726
|
-
return false;
|
|
727
|
-
});
|
|
728
|
-
if (!qaReleased) {
|
|
729
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
730
|
-
phase: 'qa-release-failed-but-dev-dispatched',
|
|
731
|
-
qaAgentId: transitioned.qaAgentId,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
const acquired = await manager.acquireAgentForTask(transitioned.agentId, transitioned.id, 'fix');
|
|
736
|
-
if (!acquired) {
|
|
737
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
738
|
-
phase: 'dev-acquire-failed-fix',
|
|
739
|
-
devAgentId: transitioned.agentId,
|
|
740
|
-
});
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed', { expectedStatus: 'fixing', expectedToken: transitioned.signalToken });
|
|
744
|
-
if (!armed) {
|
|
745
|
-
console.warn(`[EventHandler] REQUEST_CHANGES pr-fixed watcher failed to arm for task=${transitioned.id}; holding dev (not dispatching fix)`);
|
|
746
|
-
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 });
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
let resumed = false;
|
|
750
|
-
let dispatchErr = null;
|
|
751
|
-
try {
|
|
752
|
-
resumed = await manager.continueSession(transitioned.id, transitioned.agentId, 'fix', {
|
|
753
|
-
signalToken: fixToken,
|
|
754
|
-
guardBeforeInject: async () => {
|
|
755
|
-
const freshTask = await manager.getTask(transitioned.id);
|
|
756
|
-
return freshTask?.status === 'fixing' && freshTask.signalToken === fixToken;
|
|
757
|
-
},
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
catch (err) {
|
|
761
|
-
dispatchErr = err;
|
|
762
|
-
console.error(`[EventHandler] REQUEST_CHANGES continueSession(dev=${transitioned.agentId}, fix) failed:`, err);
|
|
763
|
-
}
|
|
764
|
-
if (!resumed) {
|
|
765
|
-
console.warn(`[EventHandler] REQUEST_CHANGES dev=${transitioned.agentId} not resumable for task=${transitioned.id}; ` +
|
|
766
|
-
`task remains in 'fixing' but no dev session is attached`);
|
|
767
|
-
if (dispatchErr instanceof DispatchTerminalError) {
|
|
768
|
-
await manager.failTaskForDispatchError(transitioned.id, 'fix', transitioned.agentId, dispatchErr);
|
|
769
|
-
}
|
|
770
|
-
else {
|
|
771
|
-
const parked = await manager.markAgentWaiting(transitioned.agentId, transitioned.id)
|
|
772
|
-
.catch(err => {
|
|
773
|
-
console.error(`[EventHandler] REQUEST_CHANGES markAgentWaiting(dev=${transitioned.agentId}) rollback failed:`, err);
|
|
774
|
-
return false;
|
|
775
|
-
});
|
|
776
|
-
if (parked && (dispatchErr instanceof ReplNotReadyError || dispatchErr instanceof DirtyWorkdirError)) {
|
|
777
|
-
// dev 回合在途(忙碌/未提交改动)是常态而非故障:登记待补派,回合结束后由对账 re-continue。
|
|
778
|
-
// 停驻失败说明绑定/锁已不受控,pending 无法证明还有可承接的 session,必须走人工告警
|
|
779
|
-
manager.registerPendingDispatchRetry(transitioned.id, {
|
|
780
|
-
kind: 'dev-fix',
|
|
781
|
-
agentId: transitioned.agentId,
|
|
782
|
-
signalToken: fixToken,
|
|
783
|
-
});
|
|
784
|
-
console.warn(`[EventHandler] REQUEST_CHANGES fix deferred for task=${transitioned.id}: dev pane busy or workdir dirty ` +
|
|
785
|
-
`(${dispatchErr.name}); reconciler will re-continue when idle`);
|
|
786
|
-
}
|
|
787
|
-
else {
|
|
788
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
789
|
-
phase: 'fix-resume-failed',
|
|
790
|
-
reviewRound: transitioned.reviewRound,
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
846
|
+
await manager.dispatchGitFixToDev(transitioned.id);
|
|
795
847
|
}
|
|
796
848
|
async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
797
849
|
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
@@ -799,7 +851,9 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
799
851
|
expectSignalToken: task.signalToken,
|
|
800
852
|
}, {
|
|
801
853
|
...prPatch,
|
|
802
|
-
|
|
854
|
+
...(task.phase === 'spec'
|
|
855
|
+
? { specReviewRound: reviewedRound }
|
|
856
|
+
: { reviewRound: reviewedRound }),
|
|
803
857
|
reviewRoundPending: undefined,
|
|
804
858
|
reviewDispatch: undefined,
|
|
805
859
|
});
|
|
@@ -808,16 +862,15 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
808
862
|
const { task: transitioned } = result;
|
|
809
863
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
810
864
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
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
|
+
});
|
|
821
874
|
}
|
|
822
875
|
try {
|
|
823
876
|
await bus.emit({
|
|
@@ -827,7 +880,10 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
827
880
|
projectId: transitioned.projectId,
|
|
828
881
|
agentId: transitioned.agentId,
|
|
829
882
|
taskId: transitioned.id,
|
|
830
|
-
data: {
|
|
883
|
+
data: {
|
|
884
|
+
phase: transitioned.phase,
|
|
885
|
+
reviewRound: taskReviewRound(transitioned),
|
|
886
|
+
},
|
|
831
887
|
});
|
|
832
888
|
}
|
|
833
889
|
catch (emitErr) {
|
|
@@ -835,37 +891,17 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
835
891
|
}
|
|
836
892
|
}
|
|
837
893
|
export function registerEventHandlers(bus, manager) {
|
|
838
|
-
|
|
894
|
+
const handleGitPrDelivery = async (event, deliveryPhase) => {
|
|
839
895
|
if (!event.taskId || !event.agentId)
|
|
840
896
|
return;
|
|
841
897
|
const taskAtEntry = await manager.getTask(event.taskId);
|
|
842
898
|
if (!taskAtEntry)
|
|
843
899
|
return;
|
|
844
|
-
if (taskAtEntry.reviewMode === 'server') {
|
|
845
|
-
const prNumber = event.data.prNumber;
|
|
846
|
-
if (!Number.isInteger(prNumber) || prNumber < 1)
|
|
847
|
-
return;
|
|
848
|
-
const adopted = await manager.adoptServerPr(event.taskId, {
|
|
849
|
-
prNumber: prNumber,
|
|
850
|
-
...(typeof event.data.prUrl === 'string' ? { prUrl: event.data.prUrl } : {}),
|
|
851
|
-
...(validHeadSha(event.data.headSha) !== undefined ? { headSha: validHeadSha(event.data.headSha) } : {}),
|
|
852
|
-
...(typeof event.data.targetBranch === 'string' ? { targetBranch: event.data.targetBranch } : {}),
|
|
853
|
-
});
|
|
854
|
-
if (!adopted) {
|
|
855
|
-
await emitIntervention(bus, taskAtEntry.projectId, taskAtEntry.agentId, taskAtEntry.id, {
|
|
856
|
-
phase: 'server-pr-adoption-refused',
|
|
857
|
-
prNumber,
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
if (isSpecStagePhase(taskAtEntry.phase)) {
|
|
863
|
-
console.warn(`[EventHandler] pr.created ignored for task ${event.taskId}: task in spec phase`);
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
900
|
if (event.data.source === 'pane-signal'
|
|
867
901
|
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
868
902
|
&& taskAtEntry.status !== 'in_progress') {
|
|
903
|
+
if (taskAtEntry.phase !== deliveryPhase)
|
|
904
|
+
return;
|
|
869
905
|
if (taskAtEntry.pendingPrSignalToken === undefined
|
|
870
906
|
|| event.data.token !== taskAtEntry.pendingPrSignalToken) {
|
|
871
907
|
return;
|
|
@@ -951,8 +987,8 @@ export function registerEventHandlers(bus, manager) {
|
|
|
951
987
|
return;
|
|
952
988
|
}
|
|
953
989
|
}
|
|
954
|
-
|
|
955
|
-
if (!task
|
|
990
|
+
let task = await manager.getTask(event.taskId);
|
|
991
|
+
if (!task)
|
|
956
992
|
return;
|
|
957
993
|
if (event.data.source !== 'pane-signal'
|
|
958
994
|
&& task.branch !== BRANCH_PREFIX + event.taskId
|
|
@@ -961,26 +997,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
961
997
|
`for task ${event.taskId} (untracked; custom-branch PRs adopt via the pane signal)`);
|
|
962
998
|
return;
|
|
963
999
|
}
|
|
964
|
-
const adoption = {};
|
|
965
1000
|
const target = event.data.source === 'pane-signal'
|
|
966
1001
|
? verifiedTargetBranch
|
|
967
1002
|
: (typeof event.data.targetBranch === 'string' ? event.data.targetBranch : undefined);
|
|
968
|
-
if (target !== undefined && task.baseBranch === undefined)
|
|
969
|
-
adoption.baseBranch = target;
|
|
970
|
-
if (event.data.source === 'pane-signal') {
|
|
971
|
-
const actorId = typeof event.data.actorB64 === 'string'
|
|
972
|
-
? decodeSignalActorId(event.data.actorB64)
|
|
973
|
-
: undefined;
|
|
974
|
-
if (actorId !== undefined) {
|
|
975
|
-
adoption.replyActorId = actorId;
|
|
976
|
-
adoption.replyActorStatus = 'verified';
|
|
977
|
-
adoption.pendingPrSignalToken = undefined;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
else if (typeof event.data.prAuthorId === 'string' && task.replyActorStatus !== 'verified') {
|
|
981
|
-
adoption.replyActorId = event.data.prAuthorId;
|
|
982
|
-
adoption.replyActorStatus = 'provisional';
|
|
983
|
-
}
|
|
984
1003
|
let reviewHead = validHeadSha(event.data.headSha) ?? paneVerifiedHeadSha;
|
|
985
1004
|
if (reviewHead === undefined && typeof event.data.prNumber === 'number') {
|
|
986
1005
|
const verified = await manager.platformVerifyPrBinding(event.taskId, event.data.prNumber);
|
|
@@ -993,6 +1012,54 @@ export function registerEventHandlers(bus, manager) {
|
|
|
993
1012
|
});
|
|
994
1013
|
return;
|
|
995
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;
|
|
996
1063
|
if (task.status !== 'in_progress' && task.status !== 'fixing')
|
|
997
1064
|
return;
|
|
998
1065
|
const begun = await manager.beginGitReviewPass(event.taskId, {
|
|
@@ -1003,11 +1070,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1003
1070
|
expectPhase: task.phase,
|
|
1004
1071
|
expectSignalToken: task.signalToken,
|
|
1005
1072
|
patch: {
|
|
1006
|
-
...(event.data.prNumber !== undefined ? { prNumber: event.data.prNumber } : {}),
|
|
1007
|
-
...(event.data.prUrl !== undefined ? { prUrl: event.data.prUrl } : {}),
|
|
1008
1073
|
latestHeadSha: reviewHead,
|
|
1009
|
-
...(reconciledBranch ? { branch: reconciledBranch } : {}),
|
|
1010
|
-
...adoption,
|
|
1011
1074
|
},
|
|
1012
1075
|
});
|
|
1013
1076
|
if (!begun?.task.reviewDispatch) {
|
|
@@ -1028,18 +1091,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1028
1091
|
if (reconciledBranch) {
|
|
1029
1092
|
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: ${task.branch} → ${reconciledBranch}`);
|
|
1030
1093
|
}
|
|
1031
|
-
|
|
1032
|
-
const parked = await manager.markAgentWaiting(event.agentId, begun.task.id);
|
|
1033
|
-
if (!parked) {
|
|
1034
|
-
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1035
|
-
phase: 'dev-wait-gate-failed-no-qa',
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1039
|
-
phase: 'git-review-no-qa-partner',
|
|
1040
|
-
});
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1094
|
+
const qaAgentId = requireTaskQaAgentId(begun.task, 'pr.created');
|
|
1043
1095
|
try {
|
|
1044
1096
|
await manager.dispatchGitReviewLease(begun.task.id, {
|
|
1045
1097
|
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
@@ -1055,7 +1107,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1055
1107
|
});
|
|
1056
1108
|
}
|
|
1057
1109
|
else if (err instanceof DispatchTerminalError) {
|
|
1058
|
-
await manager.failTaskForDispatchError(begun.task.id, 'review',
|
|
1110
|
+
await manager.failTaskForDispatchError(begun.task.id, 'review', qaAgentId, err, {
|
|
1059
1111
|
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
1060
1112
|
});
|
|
1061
1113
|
}
|
|
@@ -1067,16 +1119,20 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1067
1119
|
});
|
|
1068
1120
|
}
|
|
1069
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');
|
|
1070
1131
|
});
|
|
1071
1132
|
bus.on('pr.updated', async (event) => {
|
|
1072
1133
|
if (!event.taskId)
|
|
1073
1134
|
return;
|
|
1074
|
-
const taskAtEntry = await manager.getTask(event.taskId);
|
|
1075
1135
|
const eventKind = event.data.kind;
|
|
1076
|
-
if (taskAtEntry && isSpecStagePhase(taskAtEntry.phase)) {
|
|
1077
|
-
console.warn(`[EventHandler] pr.updated (kind=${eventKind ?? 'push'}) ignored for task ${event.taskId}: task in spec phase`);
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
1136
|
if (eventKind === 'pr-merge-ready')
|
|
1081
1137
|
return handlePrMergeReady(bus, manager, event);
|
|
1082
1138
|
if (eventKind === 'closed-unmerged' || eventKind === 'reopened') {
|
|
@@ -1092,8 +1148,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1092
1148
|
}
|
|
1093
1149
|
if (!event.agentId)
|
|
1094
1150
|
return;
|
|
1095
|
-
if (taskAtEntry?.reviewMode === 'server')
|
|
1096
|
-
return;
|
|
1097
1151
|
if (eventKind === 'comment' || eventKind === 'review-comment') {
|
|
1098
1152
|
return handlePrFeedback(bus, manager, event);
|
|
1099
1153
|
}
|
|
@@ -1103,43 +1157,125 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1103
1157
|
bus.on('pr.merged', async (event) => {
|
|
1104
1158
|
if (!event.taskId)
|
|
1105
1159
|
return;
|
|
1160
|
+
const taskId = event.taskId;
|
|
1106
1161
|
const eventPrNumber = event.data.prNumber;
|
|
1107
1162
|
const eventPrUrl = event.data.prUrl;
|
|
1108
|
-
|
|
1109
|
-
// this task never tracked (e.g. a reused custom branch name). bx/<taskId> is unique to this task,
|
|
1110
|
-
// so its merge is definitively ours even if pr.created was missed and prNumber was never recorded;
|
|
1111
|
-
// for any other branch require the tracked PR number to match, else a foreign/stale merge on a
|
|
1112
|
-
// reused custom branch name could prematurely finish an active task.
|
|
1113
|
-
const taskBefore = await manager.getTask(event.taskId);
|
|
1163
|
+
const taskBefore = await manager.getTask(taskId);
|
|
1114
1164
|
if (!taskBefore)
|
|
1115
1165
|
return;
|
|
1116
|
-
|
|
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
|
+
});
|
|
1117
1177
|
return;
|
|
1118
|
-
|
|
1119
|
-
if (
|
|
1178
|
+
}
|
|
1179
|
+
if ((taskBefore.status === 'merged' || taskBefore.status === 'done') && sameBoundPr)
|
|
1120
1180
|
return;
|
|
1181
|
+
let mergeExpectedPrNumber = taskBefore.prNumber;
|
|
1182
|
+
let mergeBindingConfirmed = sameBoundPr;
|
|
1121
1183
|
const prPatch = {
|
|
1122
1184
|
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
1123
1185
|
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
1124
1186
|
};
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
+
}
|
|
1127
1263
|
return;
|
|
1264
|
+
}
|
|
1128
1265
|
const { task: transitioned } = result;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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');
|
|
1135
1276
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
await manager.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle');
|
|
1139
|
-
}
|
|
1140
|
-
catch (err) {
|
|
1141
|
-
console.error(`[EventHandler] pr.merged releaseAgentForTask(QA=${transitioned.qaAgentId}) failed:`, err);
|
|
1142
|
-
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
console.error(`[EventHandler] pr.merged releaseAgentForTask(QA=${qaAgentId}) failed:`, err);
|
|
1143
1279
|
}
|
|
1144
1280
|
}
|
|
1145
1281
|
try {
|
|
@@ -1155,8 +1291,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1155
1291
|
const task = await manager.getTask(event.taskId);
|
|
1156
1292
|
if (!task)
|
|
1157
1293
|
return;
|
|
1158
|
-
if (task.reviewMode === 'server')
|
|
1159
|
-
return;
|
|
1160
1294
|
if (event.data.source === 'pane-signal') {
|
|
1161
1295
|
console.log(`[EventHandler] review.submitted pane verdict ignored for task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1162
1296
|
return;
|
|
@@ -1202,10 +1336,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1202
1336
|
await rejectGitPayload(new GitVerdictPayloadError('verdict token is not current'));
|
|
1203
1337
|
return;
|
|
1204
1338
|
}
|
|
1205
|
-
if (isSpecStagePhase(task.phase)) {
|
|
1206
|
-
console.warn(`[EventHandler] review.submitted (action=${action}) ignored for task ${task.id}: task in spec phase`);
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
1339
|
const verdictSubmittedAt = typeof event.data.submittedAt === 'string' ? event.data.submittedAt : undefined;
|
|
1210
1340
|
if (verdictSubmittedAt && task.reviewDispatchedAt) {
|
|
1211
1341
|
const submittedMs = Date.parse(verdictSubmittedAt);
|
|
@@ -1244,7 +1374,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1244
1374
|
if (!event.taskId || !event.agentId)
|
|
1245
1375
|
return;
|
|
1246
1376
|
const task = await manager.getTask(event.taskId);
|
|
1247
|
-
if (!task || task.
|
|
1377
|
+
if (!task || task.status !== 'fixing')
|
|
1248
1378
|
return;
|
|
1249
1379
|
const { projectId, agentId } = task;
|
|
1250
1380
|
const stayFixing = async (data) => {
|