baxian 1.2.57 → 1.2.59
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/dist/agent/branch.d.ts +6 -1
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +45 -9
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/manager.d.ts +182 -5
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +2235 -147
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +8 -1
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +66 -15
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +3 -1
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +27 -4
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/tmux.d.ts +8 -1
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +61 -1
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +16 -4
- package/dist/api/config.js.map +1 -1
- package/dist/api/platform-guard.d.ts +7 -0
- package/dist/api/platform-guard.d.ts.map +1 -0
- package/dist/api/platform-guard.js +32 -0
- package/dist/api/platform-guard.js.map +1 -0
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +34 -6
- package/dist/api/projects.js.map +1 -1
- package/dist/config/normalizer.d.ts.map +1 -1
- package/dist/config/normalizer.js +1 -0
- package/dist/config/normalizer.js.map +1 -1
- package/dist/config/validator.d.ts +3 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +104 -7
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts +1 -0
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +661 -89
- package/dist/event/handlers.js.map +1 -1
- package/dist/github/mapper.d.ts +1 -6
- package/dist/github/mapper.d.ts.map +1 -1
- package/dist/github/mapper.js.map +1 -1
- package/dist/github/poller.d.ts +3 -3
- package/dist/github/poller.d.ts.map +1 -1
- package/dist/github/poller.js +2 -11
- package/dist/github/poller.js.map +1 -1
- package/dist/github/resolver.js +1 -1
- package/dist/github/resolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/dist/platform/body-digest.d.ts +4 -0
- package/dist/platform/body-digest.d.ts.map +1 -0
- package/dist/platform/body-digest.js +13 -0
- package/dist/platform/body-digest.js.map +1 -0
- package/dist/platform/command-renderer.d.ts +22 -0
- package/dist/platform/command-renderer.d.ts.map +1 -0
- package/dist/platform/command-renderer.js +82 -0
- package/dist/platform/command-renderer.js.map +1 -0
- package/dist/platform/comment-cursor.d.ts +36 -0
- package/dist/platform/comment-cursor.d.ts.map +1 -0
- package/dist/platform/comment-cursor.js +235 -0
- package/dist/platform/comment-cursor.js.map +1 -0
- package/dist/platform/driver-host.d.ts +7 -0
- package/dist/platform/driver-host.d.ts.map +1 -0
- package/dist/platform/driver-host.js +18 -0
- package/dist/platform/driver-host.js.map +1 -0
- package/dist/platform/driver-spec.d.ts +7 -0
- package/dist/platform/driver-spec.d.ts.map +1 -0
- package/dist/platform/driver-spec.js +475 -0
- package/dist/platform/driver-spec.js.map +1 -0
- package/dist/platform/feedback.d.ts +31 -0
- package/dist/platform/feedback.d.ts.map +1 -0
- package/dist/platform/feedback.js +85 -0
- package/dist/platform/feedback.js.map +1 -0
- package/dist/platform/field-mapper.d.ts +7 -0
- package/dist/platform/field-mapper.d.ts.map +1 -0
- package/dist/platform/field-mapper.js +117 -0
- package/dist/platform/field-mapper.js.map +1 -0
- package/dist/platform/git-driver.d.ts +55 -0
- package/dist/platform/git-driver.d.ts.map +1 -0
- package/dist/platform/git-driver.js +171 -0
- package/dist/platform/git-driver.js.map +1 -0
- package/dist/platform/manifest.d.ts +12 -0
- package/dist/platform/manifest.d.ts.map +1 -0
- package/dist/platform/manifest.js +57 -0
- package/dist/platform/manifest.js.map +1 -0
- package/dist/platform/markers.d.ts +46 -0
- package/dist/platform/markers.d.ts.map +1 -0
- package/dist/platform/markers.js +121 -0
- package/dist/platform/markers.js.map +1 -0
- package/dist/platform/platform-poller.d.ts +68 -0
- package/dist/platform/platform-poller.d.ts.map +1 -0
- package/dist/platform/platform-poller.js +605 -0
- package/dist/platform/platform-poller.js.map +1 -0
- package/dist/platform/plugin-registry.d.ts +31 -0
- package/dist/platform/plugin-registry.d.ts.map +1 -0
- package/dist/platform/plugin-registry.js +238 -0
- package/dist/platform/plugin-registry.js.map +1 -0
- package/dist/platform/plugin-roots.d.ts +3 -0
- package/dist/platform/plugin-roots.d.ts.map +1 -0
- package/dist/platform/plugin-roots.js +11 -0
- package/dist/platform/plugin-roots.js.map +1 -0
- package/dist/platform/plugins/github/baxian-plugin.json +8 -0
- package/dist/platform/plugins/github/driver.json +134 -0
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +99 -0
- package/dist/platform/poller-health.d.ts +3 -0
- package/dist/platform/poller-health.d.ts.map +1 -0
- package/dist/platform/poller-health.js +12 -0
- package/dist/platform/poller-health.js.map +1 -0
- package/dist/platform/pr-binding.d.ts +22 -0
- package/dist/platform/pr-binding.d.ts.map +1 -0
- package/dist/platform/pr-binding.js +28 -0
- package/dist/platform/pr-binding.js.map +1 -0
- package/dist/platform/response-parser.d.ts +6 -0
- package/dist/platform/response-parser.d.ts.map +1 -0
- package/dist/platform/response-parser.js +92 -0
- package/dist/platform/response-parser.js.map +1 -0
- package/dist/platform/row-schema.d.ts +12 -0
- package/dist/platform/row-schema.d.ts.map +1 -0
- package/dist/platform/row-schema.js +127 -0
- package/dist/platform/row-schema.js.map +1 -0
- package/dist/platform/startup.d.ts +16 -0
- package/dist/platform/startup.d.ts.map +1 -0
- package/dist/platform/startup.js +52 -0
- package/dist/platform/startup.js.map +1 -0
- package/dist/platform/types.d.ts +62 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +32 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/platform/verdict-engine.d.ts +61 -0
- package/dist/platform/verdict-engine.d.ts.map +1 -0
- package/dist/platform/verdict-engine.js +134 -0
- package/dist/platform/verdict-engine.js.map +1 -0
- package/dist/shared/constants.d.ts +2 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +3 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/git-url.d.ts +8 -0
- package/dist/shared/git-url.d.ts.map +1 -1
- package/dist/shared/git-url.js +38 -0
- package/dist/shared/git-url.js.map +1 -1
- package/dist/shared/types.d.ts +52 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/state/agent-store.js +2 -0
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +91 -4
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-DskVWe7e.js +13 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-3Ypk8Gfo.js +0 -13
package/dist/event/handlers.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { createSignalToken } from '../agent/phase-signal.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { createSignalToken, decodeSignalActorId } from '../agent/phase-signal.js';
|
|
2
|
+
import { ackRevisionKey } from '../platform/markers.js';
|
|
3
|
+
import { SHA_HEX_SOURCE } from '../platform/types.js';
|
|
4
|
+
import { BRANCH_PREFIX, TASK_TERMINAL_STATUS_SET, isRecord, isSpecStagePhase, isValidBranchName, } from '../shared/index.js';
|
|
5
|
+
import { DispatchTerminalError, EnsureSessionError, isRecoverableQaDispatchHold } from '../agent/manager.js';
|
|
6
|
+
// 平台 SHA 文法单点(spec §5.3:7-64 位 hex);legacy github 值恒 40 位,放宽为 additive
|
|
7
|
+
const HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
|
|
5
8
|
function validHeadSha(value) {
|
|
6
9
|
return typeof value === 'string' && HEAD_SHA_RE.test(value) ? value : undefined;
|
|
7
10
|
}
|
|
@@ -75,18 +78,45 @@ async function dispatchDevPostApproveCheck(bus, manager, task, approvedHeadSha,
|
|
|
75
78
|
}
|
|
76
79
|
const acquired = await manager.acquireAgentForTask(task.agentId, task.id, 'post-approve');
|
|
77
80
|
if (!acquired) {
|
|
81
|
+
if (task.reviewMode === 'git') {
|
|
82
|
+
// durable retry 锚:acquire 失败发生在 completion 安装前,无 pending 则 sweep 永远看不到
|
|
83
|
+
await manager.updateTask(task.id, { pendingRedispatch: true }).catch(() => undefined);
|
|
84
|
+
}
|
|
78
85
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
79
86
|
phase: 'post-approve-dev-acquire-failed',
|
|
80
87
|
devAgentId: task.agentId,
|
|
81
88
|
});
|
|
82
89
|
return;
|
|
83
90
|
}
|
|
91
|
+
// Persisted after acquire and only while still approved: a stale write would forge rebuild evidence.
|
|
92
|
+
const headPersisted = await manager.updateTaskIfStatus(task.id, 'approved', {
|
|
93
|
+
postApproveHeadSha: approvedHeadSha,
|
|
94
|
+
}, { signalToken: task.signalToken, reviewRound: task.reviewRound });
|
|
95
|
+
if (!headPersisted) {
|
|
96
|
+
await manager.markAgentWaiting(task.agentId, task.id)
|
|
97
|
+
.catch(err => {
|
|
98
|
+
console.error(`[EventHandler] post-approve head persist refused for task=${task.id}; release failed:`, err);
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
84
102
|
const signalToken = createSignalToken();
|
|
85
|
-
await manager.setPostApproveCompletion(task.id, {
|
|
103
|
+
const installed = await manager.setPostApproveCompletion(task.id, {
|
|
86
104
|
token: signalToken,
|
|
87
105
|
approvedHeadSha,
|
|
88
106
|
...(typeof opts.redispatchCount === 'number' ? { redispatchCount: opts.redispatchCount } : {}),
|
|
89
|
-
|
|
107
|
+
// 'git' 至少一次投递(spec §6):pending 随安装保持,continueSession 确认后才清——
|
|
108
|
+
// 安装与注入之间的崩溃由恢复扫描按 pending 补派
|
|
109
|
+
...(task.reviewMode === 'git' ? { pendingRedispatch: true } : {}),
|
|
110
|
+
},
|
|
111
|
+
// A fresh human verdict unlocks a standing revocation; automated redispatches never do.
|
|
112
|
+
{ clearRevocation: typeof opts.redispatchCount !== 'number' });
|
|
113
|
+
if (!installed) {
|
|
114
|
+
await manager.markAgentWaiting(task.agentId, task.id)
|
|
115
|
+
.catch(err => {
|
|
116
|
+
console.error(`[EventHandler] post-approve install refused by revocation for task=${task.id}; release failed:`, err);
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
90
120
|
let resumed = false;
|
|
91
121
|
let dispatchErr = null;
|
|
92
122
|
try {
|
|
@@ -101,9 +131,16 @@ async function dispatchDevPostApproveCheck(bus, manager, task, approvedHeadSha,
|
|
|
101
131
|
dispatchErr = err;
|
|
102
132
|
console.error(`[EventHandler] APPROVE continueSession(dev=${task.agentId}, post-approve) failed:`, err);
|
|
103
133
|
}
|
|
104
|
-
if (resumed)
|
|
134
|
+
if (resumed) {
|
|
135
|
+
if (task.reviewMode === 'git')
|
|
136
|
+
await manager.confirmPostApprovePromptDelivered(task.id, signalToken);
|
|
105
137
|
return;
|
|
106
|
-
|
|
138
|
+
}
|
|
139
|
+
// git:未确认送达的失败保留 installed+pending 交 sweep 补派(DispatchTerminalError 单独终结);
|
|
140
|
+
// legacy 维持清除语义
|
|
141
|
+
if (task.reviewMode !== 'git' || dispatchErr instanceof DispatchTerminalError) {
|
|
142
|
+
await manager.clearPostApproveCompletionIfMatches(task.id, signalToken);
|
|
143
|
+
}
|
|
107
144
|
if (dispatchErr instanceof DispatchTerminalError) {
|
|
108
145
|
await manager.failTaskForDispatchError(task.id, 'post-approve', task.agentId, dispatchErr);
|
|
109
146
|
return;
|
|
@@ -165,36 +202,107 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
165
202
|
});
|
|
166
203
|
return;
|
|
167
204
|
}
|
|
168
|
-
const [freshTask,
|
|
205
|
+
const [freshTask, entryCompletion] = await Promise.all([
|
|
169
206
|
manager.getTask(taskNow.id),
|
|
170
207
|
manager.getPostApproveCompletion(taskNow.id),
|
|
171
208
|
]);
|
|
172
209
|
if (!freshTask
|
|
173
210
|
|| freshTask.status !== 'approved'
|
|
174
211
|
|| freshTask.agentId !== taskNow.agentId
|
|
175
|
-
||
|
|
212
|
+
|| freshTask.postApproveRevoked !== undefined
|
|
213
|
+
|| entryCompletion?.token !== signalToken
|
|
214
|
+
|| freshTask.postApproveHeadSha === undefined
|
|
215
|
+
|| entryCompletion.approvedHeadSha !== freshTask.postApproveHeadSha) {
|
|
176
216
|
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
177
217
|
phase: 'post-approve-merge-skipped-stale-task',
|
|
178
218
|
});
|
|
179
219
|
return;
|
|
180
220
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
// 'git' merge-ready 接收即 server 权威双复核(spec §6/§10:provenance + ack 完成集,同一次全源扫描):
|
|
222
|
+
// agent 自查是尽责而非授权
|
|
223
|
+
let freshCompletion = entryCompletion;
|
|
224
|
+
if (freshTask.reviewMode === 'git') {
|
|
225
|
+
// 信号已消费(token 已验证):durable 记录 signaled——瞬态复核失败时 dev 不会重发信号,
|
|
226
|
+
// 恢复/sweep 凭此相位重派而不是等一个不会再来的信号
|
|
227
|
+
await manager.markPostApproveSignalReceived(freshTask.id, signalToken);
|
|
228
|
+
const scanFailedRetry = async (error) => {
|
|
229
|
+
await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: true });
|
|
185
230
|
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
186
|
-
phase: 'post-approve-
|
|
187
|
-
|
|
188
|
-
|
|
231
|
+
phase: 'post-approve-merge-skipped-scan-failed',
|
|
232
|
+
error,
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
let verified;
|
|
236
|
+
try {
|
|
237
|
+
verified = await manager.platformVerifyAcceptedPass(freshTask.id);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
await scanFailedRetry(err instanceof Error ? err.message : String(err));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!verified.ok) {
|
|
244
|
+
if (verified.reason === 'source scan incomplete') {
|
|
245
|
+
await scanFailedRetry(verified.reason);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// pass 已被撤销/编辑(确定性失效):拒绝迁移滞留 approved,交人工重派评审(spec §6 merge 条)
|
|
249
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
250
|
+
phase: 'post-approve-merge-skipped-provenance',
|
|
251
|
+
reason: verified.reason,
|
|
189
252
|
});
|
|
190
253
|
return;
|
|
191
254
|
}
|
|
192
|
-
|
|
193
|
-
|
|
255
|
+
if (verified.pendingCount > 0 && !freshCompletion.pendingRedispatch) {
|
|
256
|
+
if (await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: true })) {
|
|
257
|
+
freshCompletion = { ...freshCompletion, pendingRedispatch: true };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (verified.pendingCount === 0 && freshCompletion.pendingRedispatch) {
|
|
261
|
+
// 权威扫描为空即清 stale pending:dev 已在本轮内补齐 ack,不再白派一轮
|
|
262
|
+
if (await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: false })) {
|
|
263
|
+
freshCompletion = { ...freshCompletion, pendingRedispatch: false };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
194
266
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
267
|
+
// A pending flag landing between the check and the transition re-enters here instead of losing the feedback.
|
|
268
|
+
for (;;) {
|
|
269
|
+
if (freshCompletion.pendingRedispatch) {
|
|
270
|
+
const nextCount = (freshCompletion.redispatchCount ?? 0) + 1;
|
|
271
|
+
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
272
|
+
const revoked = await manager.revokePostApproveCompletion(freshTask.id, 'redispatch-cap', {
|
|
273
|
+
expectedToken: freshCompletion.token,
|
|
274
|
+
});
|
|
275
|
+
if (!revoked) {
|
|
276
|
+
// The pass rotated after the entry re-validation; this merge-ready and its cap decision are stale.
|
|
277
|
+
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
278
|
+
phase: 'post-approve-merge-skipped-stale-task',
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
283
|
+
phase: 'post-approve-redispatch-cap-exceeded',
|
|
284
|
+
redispatchCount: freshCompletion.redispatchCount ?? 0,
|
|
285
|
+
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
await dispatchDevPostApproveCheck(bus, manager, freshTask, freshCompletion.approvedHeadSha, { redispatchCount: nextCount });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const completed = await manager.completeApprovedPassToMergeReady(freshTask.id, signalToken);
|
|
293
|
+
if ('task' in completed)
|
|
294
|
+
return;
|
|
295
|
+
if (completed.refused === 'pending') {
|
|
296
|
+
const latest = await manager.getPostApproveCompletion(freshTask.id);
|
|
297
|
+
if (latest?.token === signalToken) {
|
|
298
|
+
freshCompletion = latest;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
303
|
+
phase: 'post-approve-merge-skipped-stale-task',
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
198
306
|
}
|
|
199
307
|
}
|
|
200
308
|
async function handlePrFeedback(bus, manager, event) {
|
|
@@ -206,10 +314,52 @@ async function handlePrFeedback(bus, manager, event) {
|
|
|
206
314
|
const eventPrUrl = event.data.prUrl;
|
|
207
315
|
const eventKind = event.data.kind;
|
|
208
316
|
const isNewFeedback = eventKind === 'comment' || eventKind === 'review-comment';
|
|
317
|
+
const revision = taskNow.reviewMode === 'git' && isRecord(event.data.revision)
|
|
318
|
+
? event.data.revision
|
|
319
|
+
: undefined;
|
|
320
|
+
const revisionKey = revision ? ackRevisionKey(revision.sourceKey, revision.id, revision.bodyDigest) : undefined;
|
|
321
|
+
const revisionTime = typeof revision?.versionTime === 'number' ? revision.versionTime : Date.parse(event.timestamp);
|
|
322
|
+
// 消费幂等(spec §6):ledger 落盘前崩溃的重投在此收敛,redispatch 计数不重复生效
|
|
323
|
+
if (revisionKey !== undefined && taskNow.consumedFeedback?.[revisionKey] !== undefined)
|
|
324
|
+
return;
|
|
209
325
|
const prPatch = {
|
|
210
326
|
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
211
327
|
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
212
328
|
};
|
|
329
|
+
// 消费与效果由 manager 的单一锁内状态机决定(spec §6):merge-ready→退回+pending、approved→
|
|
330
|
+
// pending、其它→仅消费——入口快照与提交之间的并发迁移(completeApprovedPass 等)不再能让
|
|
331
|
+
// revision 既被 poller 记 delivered 又不产生效果
|
|
332
|
+
if (taskNow.reviewMode === 'git' && isNewFeedback && revisionKey !== undefined) {
|
|
333
|
+
const outcome = await manager.consumeGitFeedbackRevision(taskId, { key: revisionKey, versionTime: revisionTime }, { feedbackAt: event.timestamp });
|
|
334
|
+
if (outcome.kind !== 'pending' && outcome.kind !== 'returned')
|
|
335
|
+
return;
|
|
336
|
+
const effected = outcome.task;
|
|
337
|
+
const completion = await manager.getPostApproveCompletion(effected.id);
|
|
338
|
+
const devState = await manager.getAgentState(effected.agentId);
|
|
339
|
+
if (outcome.kind === 'pending' && completion !== null && devState?.taskId === effected.id) {
|
|
340
|
+
// 在途 pass 会经 merge-ready 循环消化 pending;无 completion 的 durable pending 由下方补派
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const nextCount = (effected.redispatchCount ?? completion?.redispatchCount ?? 0) + 1;
|
|
344
|
+
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
345
|
+
await manager.revokePostApproveCompletion(effected.id, 'redispatch-cap');
|
|
346
|
+
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
347
|
+
phase: 'post-approve-redispatch-cap-exceeded',
|
|
348
|
+
redispatchCount: effected.redispatchCount ?? 0,
|
|
349
|
+
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const approvedHead = effected.postApproveHeadSha ?? validHeadSha(effected.latestHeadSha);
|
|
354
|
+
if (!approvedHead) {
|
|
355
|
+
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
356
|
+
phase: 'post-approve-approved-head-unavailable',
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
await dispatchDevPostApproveCheck(bus, manager, effected, approvedHead, { redispatchCount: nextCount });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
213
363
|
const taskPatch = {
|
|
214
364
|
...prPatch,
|
|
215
365
|
...(isNewFeedback ? { prFeedbackReceivedAt: event.timestamp } : {}),
|
|
@@ -224,40 +374,140 @@ async function handlePrFeedback(bus, manager, event) {
|
|
|
224
374
|
return;
|
|
225
375
|
if (!isNewFeedback)
|
|
226
376
|
return;
|
|
227
|
-
|
|
377
|
+
let completion = await manager.getPostApproveCompletion(taskNow.id);
|
|
228
378
|
if (!completion) {
|
|
229
379
|
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
230
380
|
phase: 'post-approve-approved-head-unavailable',
|
|
231
381
|
});
|
|
232
382
|
return;
|
|
233
383
|
}
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
384
|
+
// Rotated successors are installed under a head CAS, so only the entry snapshot needs this proof.
|
|
385
|
+
if (taskNow.postApproveHeadSha === undefined
|
|
386
|
+
|| completion.approvedHeadSha !== taskNow.postApproveHeadSha) {
|
|
387
|
+
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
388
|
+
phase: 'post-approve-completion-episode-mismatch',
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Every CAS miss below means the completion rotated mid-flight; re-decide against the live pass so the feedback is never dropped.
|
|
393
|
+
for (;;) {
|
|
394
|
+
const devState = await manager.getAgentState(taskNow.agentId);
|
|
395
|
+
if (devState?.taskId === taskNow.id) {
|
|
396
|
+
if (completion.pendingRedispatch) {
|
|
397
|
+
// pending 已立不改效果,但本条 revision 的消费键仍须落盘:崩溃后重投不得再计一轮
|
|
398
|
+
if (revisionKey !== undefined) {
|
|
399
|
+
await manager.updatePostApproveCompletionIfToken(taskNow.id, completion.token, {}, { consumeRevision: { key: revisionKey, versionTime: revisionTime } });
|
|
400
|
+
}
|
|
401
|
+
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
402
|
+
// Same token: the pending mark is already on the live pass.
|
|
403
|
+
if (!fresh || fresh.token === completion.token)
|
|
404
|
+
return;
|
|
405
|
+
completion = fresh;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (await manager.updatePostApproveCompletionIfToken(taskNow.id, completion.token, {
|
|
409
|
+
pendingRedispatch: true,
|
|
410
|
+
}, revisionKey !== undefined ? { consumeRevision: { key: revisionKey, versionTime: revisionTime } } : {}))
|
|
411
|
+
return;
|
|
412
|
+
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
413
|
+
// Same token: refused in place (revoked marker) — the successor fixing flow owns this feedback.
|
|
414
|
+
if (!fresh || fresh.token === completion.token)
|
|
415
|
+
return;
|
|
416
|
+
completion = fresh;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (taskNow.reviewMode === 'git' && revisionKey !== undefined) {
|
|
420
|
+
if (await manager.updatePostApproveCompletionIfToken(taskNow.id, completion.token, {
|
|
242
421
|
pendingRedispatch: true,
|
|
422
|
+
}, { consumeRevision: { key: revisionKey, versionTime: revisionTime } })) {
|
|
423
|
+
completion = { ...completion, pendingRedispatch: true };
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
427
|
+
if (!fresh || fresh.token === completion.token)
|
|
428
|
+
return;
|
|
429
|
+
completion = fresh;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const nextCount = (completion.redispatchCount ?? 0) + 1;
|
|
434
|
+
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
435
|
+
const revoked = await manager.revokePostApproveCompletion(taskNow.id, 'redispatch-cap', {
|
|
436
|
+
expectedToken: completion.token,
|
|
243
437
|
});
|
|
438
|
+
if (!revoked) {
|
|
439
|
+
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
440
|
+
if (!fresh || fresh.token === completion.token)
|
|
441
|
+
return;
|
|
442
|
+
completion = fresh;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
446
|
+
phase: 'post-approve-redispatch-cap-exceeded',
|
|
447
|
+
redispatchCount: completion.redispatchCount ?? 0,
|
|
448
|
+
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
244
451
|
}
|
|
452
|
+
const ready = await gateDevForPostApproveRedispatch(bus, manager, taskNow);
|
|
453
|
+
if (!ready)
|
|
454
|
+
return;
|
|
455
|
+
await dispatchDevPostApproveCheck(bus, manager, { ...taskNow, ...taskPatch }, completion.approvedHeadSha, { redispatchCount: nextCount });
|
|
245
456
|
return;
|
|
246
457
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
458
|
+
}
|
|
459
|
+
// 恢复期消费 durable pendingRedispatch(spec §6 至少一次):merge-ready 退回或 path C 的 CAS
|
|
460
|
+
// 落盘后、派发前崩溃的残局在此补派;dev 仍绑定任务时不派(其在途 pass 的 merge-ready 循环接手)。
|
|
461
|
+
export async function recoverGitPostApprovePending(bus, manager) {
|
|
462
|
+
const tasks = await manager.listActiveGitTasks();
|
|
463
|
+
for (const task of tasks) {
|
|
464
|
+
try {
|
|
465
|
+
await recoverOneGitPendingTask(bus, manager, task);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
// 单任务失败不得饿死其后的 durable pending(至少一次):报告并继续
|
|
469
|
+
console.warn(`[EventHandler] recoverGitPostApprovePending: task=${task.id} failed, continuing:`, err);
|
|
470
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
471
|
+
phase: 'post-approve-recovery-failed',
|
|
472
|
+
error: err instanceof Error ? err.message : String(err),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function recoverOneGitPendingTask(bus, manager, task) {
|
|
478
|
+
{
|
|
479
|
+
if (task.status !== 'approved' || task.pendingRedispatch !== true)
|
|
480
|
+
return;
|
|
481
|
+
if (task.postApproveRevoked)
|
|
482
|
+
return;
|
|
483
|
+
if (task.postApprovePhase === 'delivered') {
|
|
484
|
+
const devState = await manager.getAgentState(task.agentId);
|
|
485
|
+
if (devState?.taskId === task.id) {
|
|
486
|
+
// prompt 已送达且尚未回信号(dev 在跑):补派会向运行中的会话注入第二个 prompt 并轮换
|
|
487
|
+
// token——只重臂 watcher 等信号,pending 由 merge-ready 循环消化
|
|
488
|
+
await manager.rearmPostApproveSignal(task.id).catch(() => false);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const approvedHead = validHeadSha(task.postApproveHeadSha ?? task.latestHeadSha);
|
|
493
|
+
if (!approvedHead) {
|
|
494
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
495
|
+
phase: 'post-approve-approved-head-unavailable',
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const nextCount = (task.redispatchCount ?? 0) + 1;
|
|
500
|
+
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
501
|
+
await manager.revokePostApproveCompletion(task.id, 'redispatch-cap');
|
|
502
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
503
|
+
phase: 'post-approve-redispatch-cap-exceeded',
|
|
504
|
+
redispatchCount: task.redispatchCount ?? 0,
|
|
505
|
+
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
await dispatchDevPostApproveCheck(bus, manager, task, approvedHead, { redispatchCount: nextCount });
|
|
256
510
|
}
|
|
257
|
-
const ready = await gateDevForPostApproveRedispatch(bus, manager, taskNow);
|
|
258
|
-
if (!ready)
|
|
259
|
-
return;
|
|
260
|
-
await dispatchDevPostApproveCheck(bus, manager, { ...taskNow, ...taskPatch }, completion.approvedHeadSha, { redispatchCount: nextCount });
|
|
261
511
|
}
|
|
262
512
|
async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix) {
|
|
263
513
|
const now = await manager.getTask(taskId);
|
|
@@ -291,6 +541,16 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
291
541
|
const taskBeforeTransition = await manager.getTask(taskId);
|
|
292
542
|
if (!taskBeforeTransition)
|
|
293
543
|
return;
|
|
544
|
+
// 'git' push 重投收敛(spec §6 生命周期幂等):同 head 的评审轮已派发过,重放在任何后继态
|
|
545
|
+
// 都是有害动作(QA 释放/令牌轮换/计轮/撤销 approval),恰一次由 TaskState 锚判定。
|
|
546
|
+
// pr-fixed 合成 push 豁免——no-code fix 的 recheck 派发正是靠同 head 重入(spec §7 同 head 复检)。
|
|
547
|
+
if (taskBeforeTransition.reviewMode === 'git' && eventKind === 'push' && eventHeadSha
|
|
548
|
+
&& event.data.source !== 'pr-fixed'
|
|
549
|
+
&& taskBeforeTransition.reviewDispatchPending !== true
|
|
550
|
+
&& taskBeforeTransition.latestHeadSha === eventHeadSha
|
|
551
|
+
&& taskBeforeTransition.reviewHeadAnchorSha === eventHeadSha) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
294
554
|
const willHavePrNumber = taskBeforeTransition.prNumber !== undefined || eventPrNumber !== undefined;
|
|
295
555
|
if (taskBeforeTransition.status === 'in_progress' && !willHavePrNumber) {
|
|
296
556
|
console.warn(`[EventHandler] pr.updated: task ${taskId} in_progress but neither task nor event has prNumber; ` +
|
|
@@ -303,6 +563,10 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
303
563
|
...(anchorAtDispatch ? { reviewHeadAnchorSha: anchorAtDispatch } : {}),
|
|
304
564
|
reviewDispatchedAt: new Date().toISOString(),
|
|
305
565
|
signalToken: createSignalToken(),
|
|
566
|
+
// pair 与新轮可见性原子(spec §7):transition 后到 rotate 的窗口内旧 pair 不得授权本轮
|
|
567
|
+
...(taskBeforeTransition.reviewMode === 'git'
|
|
568
|
+
? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true }
|
|
569
|
+
: {}),
|
|
306
570
|
});
|
|
307
571
|
if (!result)
|
|
308
572
|
return;
|
|
@@ -375,12 +639,16 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
375
639
|
const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
|
|
376
640
|
if (!armed) {
|
|
377
641
|
console.warn(`[EventHandler] pr.updated verdict watcher failed to arm for task=${transitioned.id} (${qaPhase}); rolling back recheck dispatch`);
|
|
378
|
-
if (previousStatus === 'in_progress' || previousStatus === 'fixing'
|
|
642
|
+
if (previousStatus === 'in_progress' || previousStatus === 'fixing'
|
|
643
|
+
|| (previousStatus === 'review' && taskBeforeTransition.reviewMode === 'git')) {
|
|
379
644
|
const rolledBack = await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
380
645
|
status: previousStatus,
|
|
381
646
|
signalToken: taskBeforeTransition.signalToken,
|
|
382
647
|
reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
|
|
383
648
|
reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
|
|
649
|
+
...(taskBeforeTransition.reviewMode === 'git'
|
|
650
|
+
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
651
|
+
: {}),
|
|
384
652
|
}, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
|
|
385
653
|
if (!rolledBack) {
|
|
386
654
|
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated arm-failure');
|
|
@@ -413,8 +681,25 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
413
681
|
await manager.failTaskForDispatchError(transitioned.id, qaPhase, qa.id, dispatchErr);
|
|
414
682
|
}
|
|
415
683
|
else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.handled) {
|
|
684
|
+
// handled 不止代表可恢复派发 hold(checkout-cleanup-failed / dialog 等也走这里),指引须按实际相位分流
|
|
685
|
+
const qaState = await manager.getAgentState(qa.id).catch((stateErr) => {
|
|
686
|
+
console.warn(`[EventHandler] pr.updated could not read held QA state for ${qa.id}:`, stateErr);
|
|
687
|
+
return null;
|
|
688
|
+
});
|
|
689
|
+
const heldPhase = qaState?.status === 'awaiting_human' ? qaState.awaitingPhase : undefined;
|
|
690
|
+
const note = isRecoverableQaDispatchHold(qaState)
|
|
691
|
+
? 'QA dispatch was held awaiting human; the task keeps the rotated review pass. Resume the QA agent or POST /tasks/:id/review to redispatch.'
|
|
692
|
+
: `QA dispatch was held awaiting human (${heldPhase ?? 'hold not persisted'}); this hold is not auto-redispatchable — inspect the QA agent's awaiting reason, resolve it, then redispatch via POST /tasks/:id/review.`;
|
|
693
|
+
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
694
|
+
phase: 'qa-dispatch-held-awaiting-human',
|
|
695
|
+
qaAgentId: qa.id,
|
|
696
|
+
qaPhase,
|
|
697
|
+
...(heldPhase !== undefined ? { awaitingPhase: heldPhase } : {}),
|
|
698
|
+
note,
|
|
699
|
+
});
|
|
416
700
|
}
|
|
417
|
-
else if (previousStatus === 'in_progress' || previousStatus === 'fixing'
|
|
701
|
+
else if (previousStatus === 'in_progress' || previousStatus === 'fixing'
|
|
702
|
+
|| (previousStatus === 'review' && taskBeforeTransition.reviewMode === 'git')) {
|
|
418
703
|
// 仅回滚 status 会留下 transition 轮换后的 token,dev 在途 pass 的完成信号将永久失配;
|
|
419
704
|
// startSession 窗口内 pass 可能已被并发接管,此时回滚与 QA 释放都要让位于接管方
|
|
420
705
|
const rolledBack = await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
@@ -422,6 +707,9 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
422
707
|
signalToken: taskBeforeTransition.signalToken,
|
|
423
708
|
reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
|
|
424
709
|
reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
|
|
710
|
+
...(taskBeforeTransition.reviewMode === 'git'
|
|
711
|
+
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
712
|
+
: {}),
|
|
425
713
|
}, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
|
|
426
714
|
if (rolledBack) {
|
|
427
715
|
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
|
|
@@ -455,6 +743,9 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
455
743
|
}
|
|
456
744
|
return;
|
|
457
745
|
}
|
|
746
|
+
if (taskBeforeTransition.reviewMode === 'git') {
|
|
747
|
+
await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
|
|
748
|
+
}
|
|
458
749
|
if (previousStatus === 'in_progress' || previousStatus === 'approved' || previousStatus === 'merge-ready') {
|
|
459
750
|
await manager.bumpReviewRoundIfStillAt(transitioned.id, expectedRound);
|
|
460
751
|
}
|
|
@@ -469,9 +760,18 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
469
760
|
if (task.status !== 'review')
|
|
470
761
|
return false;
|
|
471
762
|
if (task.qaAgentId) {
|
|
472
|
-
// 当前 pass 的 QA 会话还在跑时,迟到的旧 verdict
|
|
473
|
-
|
|
474
|
-
|
|
763
|
+
// 当前 pass 的 QA 会话还在跑时,迟到的旧 verdict 只是噪音,不能重派打断;
|
|
764
|
+
// 派发期 hold(checkout 未备好)意味着从未开工,等同无会话,可安全重派
|
|
765
|
+
let qaState;
|
|
766
|
+
try {
|
|
767
|
+
qaState = await manager.getAgentState(task.qaAgentId);
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
// 无状态证据时 fail closed:不自动重派,让调用方落 stale-verdict 兜底介入
|
|
771
|
+
console.warn(`[EventHandler] stale-verdict QA state read failed for ${task.qaAgentId}; not redispatching:`, err);
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
if (qaState?.taskId === task.id && !isRecoverableQaDispatchHold(qaState))
|
|
475
775
|
return false;
|
|
476
776
|
}
|
|
477
777
|
try {
|
|
@@ -490,16 +790,20 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
490
790
|
console.warn(`[EventHandler] stale verdict for task ${task.id} arrived with no active QA session; redispatched review`);
|
|
491
791
|
return true;
|
|
492
792
|
}
|
|
493
|
-
async function handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch) {
|
|
793
|
+
async function handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch, gitVerdict) {
|
|
494
794
|
if (!reviewedHeadSha) {
|
|
495
795
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
496
796
|
phase: 'approval-reviewed-head-unavailable',
|
|
497
797
|
});
|
|
498
798
|
return;
|
|
499
799
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
800
|
+
// 'git' 载荷已经 engine 端 fresh prView 复核(spec §7 handler 收紧):二次解析的缓存回退
|
|
801
|
+
// 链会重新打开 engine 已关闭的竞态,跳过
|
|
802
|
+
const anchor = task.reviewMode === 'git'
|
|
803
|
+
? { headSha: currentHeadSha ?? reviewedHeadSha, source: 'payload-self' }
|
|
804
|
+
: await resolveAuthoritativeHead(manager, task, { payloadCurrentHeadSha: currentHeadSha });
|
|
805
|
+
// git 的 head 更新并入下方带 expectSignalToken 的 transition:入口校验后并发 push 换代时,
|
|
806
|
+
// 旧 verdict 的裸 updateTask 会把 successor 的 latestHeadSha 覆写回旧值
|
|
503
807
|
if (anchor.source === 'fetch' && anchor.headSha && task.latestHeadSha !== anchor.headSha) {
|
|
504
808
|
await manager.updateTask(task.id, { latestHeadSha: anchor.headSha });
|
|
505
809
|
}
|
|
@@ -509,11 +813,29 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
509
813
|
reviewedHeadSha,
|
|
510
814
|
currentHeadSha: anchor.headSha,
|
|
511
815
|
source: anchor.source,
|
|
512
|
-
...(anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
816
|
+
...('fetchError' in anchor && anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
513
817
|
});
|
|
514
818
|
return;
|
|
515
819
|
}
|
|
516
|
-
const
|
|
820
|
+
const provenanceFields = task.reviewMode === 'git' && gitVerdict !== undefined && task.failToken !== undefined
|
|
821
|
+
? {
|
|
822
|
+
passProvenance: {
|
|
823
|
+
sourceKey: gitVerdict.carrier.sourceKey,
|
|
824
|
+
id: gitVerdict.carrier.id,
|
|
825
|
+
bodyDigest: gitVerdict.carrier.bodyDigest,
|
|
826
|
+
token: gitVerdict.token,
|
|
827
|
+
failToken: task.failToken,
|
|
828
|
+
anchorSha: reviewedHeadSha,
|
|
829
|
+
},
|
|
830
|
+
}
|
|
831
|
+
: {};
|
|
832
|
+
const result = await manager.transitionTaskStatus(task.id, 'approved', {
|
|
833
|
+
fromStatus: ['review'],
|
|
834
|
+
// 校验后提交前的并发换代(push/重派轮换 token+pair)使旧 APPROVE 失效(spec §7)
|
|
835
|
+
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
836
|
+
}, task.reviewRound === 0
|
|
837
|
+
? { ...prPatch, ...provenanceFields, ...gitHeadPatch(task, anchor.headSha), reviewRound: 1 }
|
|
838
|
+
: { ...prPatch, ...provenanceFields, ...gitHeadPatch(task, anchor.headSha) });
|
|
517
839
|
if (!result)
|
|
518
840
|
return;
|
|
519
841
|
const { task: transitioned } = result;
|
|
@@ -524,14 +846,21 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
524
846
|
}
|
|
525
847
|
await dispatchDevPostApproveCheck(bus, manager, transitioned, reviewedHeadSha);
|
|
526
848
|
}
|
|
849
|
+
function gitHeadPatch(task, headSha) {
|
|
850
|
+
return task.reviewMode === 'git' && headSha !== undefined && task.latestHeadSha !== headSha
|
|
851
|
+
? { latestHeadSha: headSha }
|
|
852
|
+
: {};
|
|
853
|
+
}
|
|
527
854
|
async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch) {
|
|
528
855
|
const approvedCompletion = task.status === 'approved'
|
|
529
856
|
? await manager.getPostApproveCompletion(task.id)
|
|
530
857
|
: null;
|
|
531
|
-
const anchor =
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
858
|
+
const anchor = task.reviewMode === 'git'
|
|
859
|
+
? { headSha: currentHeadSha ?? reviewedHeadSha, source: 'payload-self' }
|
|
860
|
+
: await resolveAuthoritativeHead(manager, task, {
|
|
861
|
+
payloadCurrentHeadSha: currentHeadSha,
|
|
862
|
+
legacyFallback: approvedCompletion?.approvedHeadSha,
|
|
863
|
+
});
|
|
535
864
|
if (anchor.source === 'fetch' && anchor.headSha && task.latestHeadSha !== anchor.headSha) {
|
|
536
865
|
await manager.updateTask(task.id, { latestHeadSha: anchor.headSha });
|
|
537
866
|
}
|
|
@@ -541,7 +870,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
541
870
|
reviewedHeadSha,
|
|
542
871
|
currentHeadSha: anchor.headSha,
|
|
543
872
|
source: anchor.source,
|
|
544
|
-
...(anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
873
|
+
...('fetchError' in anchor && anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
545
874
|
});
|
|
546
875
|
return;
|
|
547
876
|
}
|
|
@@ -551,11 +880,21 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
551
880
|
const devState = await manager.getAgentState(task.agentId);
|
|
552
881
|
const postApproveActive = await manager.getPostApproveCompletion(task.id);
|
|
553
882
|
if (devState?.taskId === task.id && postApproveActive) {
|
|
554
|
-
await manager.
|
|
883
|
+
let revoked = await manager.revokePostApproveCompletion(task.id, 'request-changes', {
|
|
884
|
+
expectedToken: postApproveActive.token,
|
|
885
|
+
});
|
|
886
|
+
if (!revoked) {
|
|
887
|
+
// Token rotation within the episode still gets blocked; a successor approved episode (fresh head) must not.
|
|
888
|
+
revoked = await manager.revokePostApproveCompletion(task.id, 'request-changes', {
|
|
889
|
+
expectedHeadSha: postApproveActive.approvedHeadSha,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
555
892
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
556
893
|
phase: 'request-changes-during-post-approve',
|
|
557
894
|
devAgentId: task.agentId,
|
|
558
|
-
note:
|
|
895
|
+
note: revoked
|
|
896
|
+
? 'Dev is still running post-approve check; fix dispatch deferred to avoid prompt collision. PostApproveCompletion cleared to block auto-merge. Operator: wait for post-approve signal to complete, then re-trigger REQUEST_CHANGES manually or cancel the task.'
|
|
897
|
+
: 'Dev is still running post-approve check and the task moved past the reviewed approved episode before the block could apply; the verdict belongs to a superseded pass. Re-trigger REQUEST_CHANGES if it still applies, or cancel the task.',
|
|
559
898
|
});
|
|
560
899
|
return;
|
|
561
900
|
}
|
|
@@ -576,7 +915,11 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
576
915
|
if (nextRound > manager.getConfig().review.rounds) {
|
|
577
916
|
return handleMaxRounds(bus, manager, task, prPatch, reviewedRound);
|
|
578
917
|
}
|
|
579
|
-
const result = await manager.transitionTaskStatus(task.id, 'fixing', {
|
|
918
|
+
const result = await manager.transitionTaskStatus(task.id, 'fixing', {
|
|
919
|
+
fromStatus: ['review', 'approved', 'merge-ready'],
|
|
920
|
+
// fail 方向同样按入口代际 CAS(spec §7):await 窗口内 push/重派换代后旧 RC 不得推进新轮
|
|
921
|
+
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
922
|
+
}, { ...prPatch, ...gitHeadPatch(task, anchor.headSha), reviewRound: nextRound, fixDispatchedAt: new Date().toISOString() });
|
|
580
923
|
if (!result)
|
|
581
924
|
return;
|
|
582
925
|
const { task: transitioned, previousStatus } = result;
|
|
@@ -639,7 +982,10 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
639
982
|
}
|
|
640
983
|
}
|
|
641
984
|
async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
642
|
-
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
985
|
+
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
986
|
+
fromStatus: ['review', 'approved', 'merge-ready'],
|
|
987
|
+
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
988
|
+
}, { ...prPatch, reviewRound: reviewedRound });
|
|
643
989
|
if (!result)
|
|
644
990
|
return;
|
|
645
991
|
const { task: transitioned } = result;
|
|
@@ -682,9 +1028,106 @@ export function registerEventHandlers(bus, manager) {
|
|
|
682
1028
|
console.warn(`[EventHandler] pr.created ignored for task ${event.taskId}: task in spec phase`);
|
|
683
1029
|
return;
|
|
684
1030
|
}
|
|
1031
|
+
const isGit = taskAtEntry?.reviewMode === 'git';
|
|
1032
|
+
// 迟到 publish 信号只做 actor reconciliation(spec §5.3 ④):poller 已抢先收编、任务不在
|
|
1033
|
+
// 可收编态时,同 prNumber + reconciliation token 归位 replyActor,随后拆 dev 侧条目、不动 QA。
|
|
1034
|
+
// fixing 也归 reconciliation:同 prNumber 的迟到信号在 fix 轮重收编会把任务拽回 review、
|
|
1035
|
+
// 孤儿化 dev 的 fix 会话——非 in_progress 的同号信号只承载 actor 归位(spec §5.3 ④)
|
|
1036
|
+
if (isGit && taskAtEntry && event.data.source === 'pane-signal'
|
|
1037
|
+
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
1038
|
+
&& taskAtEntry.status !== 'in_progress') {
|
|
1039
|
+
if (taskAtEntry.pendingPrSignalToken === undefined
|
|
1040
|
+
|| event.data.token !== taskAtEntry.pendingPrSignalToken) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (taskAtEntry.replyActorStatus !== 'verified') {
|
|
1044
|
+
const actorId = typeof event.data.actorB64 === 'string'
|
|
1045
|
+
? decodeSignalActorId(event.data.actorB64)
|
|
1046
|
+
: undefined;
|
|
1047
|
+
if (actorId === undefined) {
|
|
1048
|
+
// watcher 单发已被本事件消费:不重臂就永失 reconciliation 入口。skipSnapshot=true
|
|
1049
|
+
// 只等待后续正确的五段信号,避免滚动区里同一条三段残文立即重触发成环
|
|
1050
|
+
await manager.rearmGitReconciliationWatcher(taskAtEntry.id, { skipSnapshot: true });
|
|
1051
|
+
await emitIntervention(bus, taskAtEntry.projectId, event.agentId, taskAtEntry.id, {
|
|
1052
|
+
phase: 'git-pr-created-actor-missing',
|
|
1053
|
+
claimedPrNumber: event.data.prNumber,
|
|
1054
|
+
});
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
await manager.updateTask(taskAtEntry.id, {
|
|
1058
|
+
replyActorId: actorId,
|
|
1059
|
+
replyActorStatus: 'verified',
|
|
1060
|
+
pendingPrSignalToken: undefined,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
// fixing 态同键 watcher 已是 pr-fixed:拆掉会吞掉本轮完成信号,reconciliation 条目只存在于 review
|
|
1064
|
+
if (taskAtEntry.status === 'review') {
|
|
1065
|
+
manager.stopPhaseSignalWatcherAgent(taskAtEntry.id, event.agentId);
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (isGit && taskAtEntry && event.data.source !== 'pane-signal'
|
|
1070
|
+
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
1071
|
+
&& taskAtEntry.reviewDispatchPending !== true
|
|
1072
|
+
&& !['in_progress', 'fixing'].includes(taskAtEntry.status)) {
|
|
1073
|
+
// in_progress/fixing 或派发未完成(pending)的同号重放走完整收编重试(至少一次)
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
685
1076
|
let paneVerifiedHeadSha;
|
|
686
1077
|
let reconciledBranch;
|
|
687
|
-
|
|
1078
|
+
let gitVerifiedTargetBranch;
|
|
1079
|
+
if (isGit && event.data.source === 'pane-signal' && event.data.prNumber !== undefined) {
|
|
1080
|
+
const prNumber = event.data.prNumber;
|
|
1081
|
+
try {
|
|
1082
|
+
let verify = await manager.platformVerifyPrBinding(event.taskId, prNumber);
|
|
1083
|
+
if (!verify.ok && verify.reason === 'branch' && verify.prBranch !== undefined) {
|
|
1084
|
+
const taskNow = await manager.getTask(event.taskId);
|
|
1085
|
+
const prBranch = verify.prBranch;
|
|
1086
|
+
if (taskNow) {
|
|
1087
|
+
const ownPrefix = BRANCH_PREFIX + event.taskId;
|
|
1088
|
+
const isForeignBxBranch = prBranch.startsWith(BRANCH_PREFIX) && prBranch !== ownPrefix;
|
|
1089
|
+
const bound = await manager.findTaskByBranch(prBranch, taskNow.projectId);
|
|
1090
|
+
if (!isForeignBxBranch && isValidBranchName(prBranch)
|
|
1091
|
+
&& (prBranch === ownPrefix || (!!bound && bound.id === taskNow.id))) {
|
|
1092
|
+
verify = await manager.platformVerifyPrBinding(event.taskId, prNumber, { branchOverride: prBranch });
|
|
1093
|
+
if (verify.ok)
|
|
1094
|
+
reconciledBranch = prBranch;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (!verify.ok) {
|
|
1099
|
+
const taskNow = await manager.getTask(event.taskId);
|
|
1100
|
+
console.warn(`[EventHandler] pr.created REJECT pane prNumber=${prNumber} for git task ${event.taskId} (${verify.reason})`);
|
|
1101
|
+
if (taskNow) {
|
|
1102
|
+
await reArmDevelopWatcher(manager, taskNow, event.agentId);
|
|
1103
|
+
await emitIntervention(bus, taskNow.projectId, event.agentId, event.taskId, {
|
|
1104
|
+
phase: 'pane-pr-created-branch-mismatch',
|
|
1105
|
+
claimedPrNumber: prNumber,
|
|
1106
|
+
taskBranch: taskNow.branch ?? '',
|
|
1107
|
+
reason: verify.reason,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
paneVerifiedHeadSha = verify.headSha;
|
|
1113
|
+
gitVerifiedTargetBranch = verify.targetBranch;
|
|
1114
|
+
}
|
|
1115
|
+
catch (err) {
|
|
1116
|
+
console.warn(`[EventHandler] pr.created: platformVerifyPrBinding failed for task ${event.taskId}:`, err);
|
|
1117
|
+
const taskNow = await manager.getTask(event.taskId);
|
|
1118
|
+
if (taskNow) {
|
|
1119
|
+
// skipSnapshot 缺省 true:滚动区里刚消费的信号残文在平台持续失败时会形成无退避热循环
|
|
1120
|
+
await reArmDevelopWatcher(manager, taskNow, event.agentId);
|
|
1121
|
+
await emitIntervention(bus, taskNow.projectId, event.agentId, event.taskId, {
|
|
1122
|
+
phase: 'pane-pr-created-verify-error',
|
|
1123
|
+
claimedPrNumber: prNumber,
|
|
1124
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
else if (event.data.source === 'pane-signal' && event.data.prNumber !== undefined) {
|
|
688
1131
|
try {
|
|
689
1132
|
const prNumber = event.data.prNumber;
|
|
690
1133
|
let verified = await manager.verifyPaneSignalPrNumber(event.taskId, prNumber);
|
|
@@ -754,13 +1197,41 @@ export function registerEventHandlers(bus, manager) {
|
|
|
754
1197
|
`for task ${event.taskId} (untracked; custom-branch PRs adopt via the pane signal)`);
|
|
755
1198
|
return;
|
|
756
1199
|
}
|
|
1200
|
+
const gitAdoptionFields = {};
|
|
1201
|
+
if (isGit) {
|
|
1202
|
+
const target = event.data.source === 'pane-signal'
|
|
1203
|
+
? gitVerifiedTargetBranch
|
|
1204
|
+
: (typeof event.data.targetBranch === 'string' ? event.data.targetBranch : undefined);
|
|
1205
|
+
// baseBranch 快照一经写入不可变(spec §6):默认分支后续改名不影响既有任务
|
|
1206
|
+
if (target !== undefined && taskBeforeTransition?.baseBranch === undefined) {
|
|
1207
|
+
gitAdoptionFields.baseBranch = target;
|
|
1208
|
+
}
|
|
1209
|
+
if (event.data.source === 'pane-signal') {
|
|
1210
|
+
const actorId = typeof event.data.actorB64 === 'string'
|
|
1211
|
+
? decodeSignalActorId(event.data.actorB64)
|
|
1212
|
+
: undefined;
|
|
1213
|
+
if (actorId !== undefined) {
|
|
1214
|
+
gitAdoptionFields.replyActorId = actorId;
|
|
1215
|
+
gitAdoptionFields.replyActorStatus = 'verified';
|
|
1216
|
+
gitAdoptionFields.pendingPrSignalToken = undefined;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
else if (typeof event.data.prAuthorId === 'string'
|
|
1220
|
+
&& taskBeforeTransition?.replyActorStatus !== 'verified') {
|
|
1221
|
+
// 自动收编只落 provisional:PR 作者可能是对已推送 head 抢建 PR 的第三方(spec §5.3 ④)
|
|
1222
|
+
gitAdoptionFields.replyActorId = event.data.prAuthorId;
|
|
1223
|
+
gitAdoptionFields.replyActorStatus = 'provisional';
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
757
1226
|
const result = await manager.transitionTaskStatus(event.taskId, 'review', { fromStatus: ['in_progress', 'fixing'] }, {
|
|
758
1227
|
...(event.data.prNumber !== undefined ? { prNumber: event.data.prNumber } : {}),
|
|
759
1228
|
...(event.data.prUrl !== undefined ? { prUrl: event.data.prUrl } : {}),
|
|
760
1229
|
...(createdHeadSha ? { latestHeadSha: createdHeadSha, reviewHeadAnchorSha: createdHeadSha } : {}),
|
|
761
1230
|
...(reconciledBranch ? { branch: reconciledBranch } : {}),
|
|
1231
|
+
...gitAdoptionFields,
|
|
762
1232
|
reviewDispatchedAt: new Date().toISOString(),
|
|
763
1233
|
signalToken: createSignalToken(),
|
|
1234
|
+
...(isGit ? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true } : {}),
|
|
764
1235
|
});
|
|
765
1236
|
if (!result) {
|
|
766
1237
|
console.warn(`[EventHandler] pr.created: cannot transition task ${event.taskId} (terminal or invalid from-state)`);
|
|
@@ -818,6 +1289,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
818
1289
|
signalToken: taskBeforeTransition?.signalToken,
|
|
819
1290
|
reviewHeadAnchorSha: taskBeforeTransition?.reviewHeadAnchorSha,
|
|
820
1291
|
reviewDispatchedAt: taskBeforeTransition?.reviewDispatchedAt,
|
|
1292
|
+
...(taskBeforeTransition?.reviewMode === 'git'
|
|
1293
|
+
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
1294
|
+
: {}),
|
|
821
1295
|
}, { expect: { status: 'review', signalToken: dispatchToken } });
|
|
822
1296
|
if (!rolledBack) {
|
|
823
1297
|
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.created arm-failure');
|
|
@@ -886,6 +1360,19 @@ export function registerEventHandlers(bus, manager) {
|
|
|
886
1360
|
}
|
|
887
1361
|
if (eventKind === 'pr-merge-ready')
|
|
888
1362
|
return handlePrMergeReady(bus, manager, event);
|
|
1363
|
+
// closed-unmerged/reopened 只由 PlatformPoller 对 'git' 任务合成(spec §6 outbox 协议)
|
|
1364
|
+
if (eventKind === 'closed-unmerged' || eventKind === 'reopened') {
|
|
1365
|
+
const prNumber = event.data.prNumber;
|
|
1366
|
+
if (typeof prNumber !== 'number')
|
|
1367
|
+
return;
|
|
1368
|
+
if (eventKind === 'closed-unmerged')
|
|
1369
|
+
await manager.recordClosedUnmergedAnchor(event.taskId, prNumber);
|
|
1370
|
+
else
|
|
1371
|
+
await manager.clearClosedUnmergedAnchor(event.taskId, prNumber);
|
|
1372
|
+
// 无条件投递:滞留的 outbox 条目借重复观察重试,而不是等到重启(spec §6 至少一次)
|
|
1373
|
+
await manager.deliverTaskOutbox(event.taskId);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
889
1376
|
if (eventKind !== 'push' && eventKind !== undefined)
|
|
890
1377
|
return handlePrFeedback(bus, manager, event);
|
|
891
1378
|
return handlePrCodePush(bus, manager, event);
|
|
@@ -948,6 +1435,11 @@ export function registerEventHandlers(bus, manager) {
|
|
|
948
1435
|
return;
|
|
949
1436
|
if (task.reviewMode === 'server')
|
|
950
1437
|
return;
|
|
1438
|
+
// 'git' 唯一裁决授权是评论配对令牌(spec §7):pane verdict 恒拒绝,命中只记日志
|
|
1439
|
+
if (task.reviewMode === 'git' && event.data.source === 'pane-signal') {
|
|
1440
|
+
console.log(`[EventHandler] review.submitted pane verdict ignored for git task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
951
1443
|
{
|
|
952
1444
|
const terminalQaId = task.qaAgentId;
|
|
953
1445
|
if (terminalQaId && TASK_TERMINAL_STATUS_SET.has(task.status)) {
|
|
@@ -1031,8 +1523,30 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1031
1523
|
return;
|
|
1032
1524
|
}
|
|
1033
1525
|
}
|
|
1526
|
+
const carrier = event.data.verdictCarrier;
|
|
1527
|
+
const gitVerdict = task.reviewMode === 'git'
|
|
1528
|
+
&& typeof event.data.verdictToken === 'string' && carrier !== undefined
|
|
1529
|
+
? { token: event.data.verdictToken, carrier }
|
|
1530
|
+
: undefined;
|
|
1531
|
+
// 'git' 的裁决只信任 engine 载荷:reviewPassToken(评审轮绑定)与 verdict 令牌(评论对绑定)
|
|
1532
|
+
// 都必须在场且属当前轮——缺失即绕过 signalToken 门的 malformed 事件,fail closed
|
|
1533
|
+
if (task.reviewMode === 'git' && (action === 'APPROVE' || action === 'REQUEST_CHANGES')) {
|
|
1534
|
+
const expectedToken = action === 'APPROVE' ? task.passToken : task.failToken;
|
|
1535
|
+
const roundToken = typeof event.data.reviewPassToken === 'string' ? event.data.reviewPassToken : undefined;
|
|
1536
|
+
if (gitVerdict === undefined || expectedToken === undefined || task.failToken === undefined
|
|
1537
|
+
|| gitVerdict.token !== expectedToken
|
|
1538
|
+
|| roundToken === undefined || task.signalToken === undefined || roundToken !== task.signalToken) {
|
|
1539
|
+
console.warn(`[EventHandler] review.submitted ${action} rejected for git task ${task.id}: verdict payload missing or not the current round token`);
|
|
1540
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1541
|
+
phase: 'git-verdict-payload-invalid',
|
|
1542
|
+
action,
|
|
1543
|
+
hasCarrier: gitVerdict !== undefined,
|
|
1544
|
+
});
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1034
1548
|
if (action === 'APPROVE') {
|
|
1035
|
-
return handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch);
|
|
1549
|
+
return handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch, gitVerdict);
|
|
1036
1550
|
}
|
|
1037
1551
|
if (action === 'REQUEST_CHANGES') {
|
|
1038
1552
|
return handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch);
|
|
@@ -1047,7 +1561,15 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1047
1561
|
const { projectId, agentId } = task;
|
|
1048
1562
|
const stayFixing = async (data) => {
|
|
1049
1563
|
await emitIntervention(bus, projectId, agentId, task.id, data);
|
|
1050
|
-
|
|
1564
|
+
// 同代 fence:平台查询等待期间任务可能已被手动重派进 review——旧失败分支不得覆盖
|
|
1565
|
+
// successor 的 QA/reconciliation watcher
|
|
1566
|
+
const fresh = await manager.getTask(task.id);
|
|
1567
|
+
if (!fresh || fresh.status !== 'fixing' || fresh.signalToken !== task.signalToken)
|
|
1568
|
+
return;
|
|
1569
|
+
await manager.setupPhaseSignal(task.id, agentId, 'pr-fixed', {
|
|
1570
|
+
skipSnapshot: true,
|
|
1571
|
+
...(fresh.reviewMode === 'git' ? { replaceScope: 'agent' } : {}),
|
|
1572
|
+
});
|
|
1051
1573
|
};
|
|
1052
1574
|
const token = typeof event.data.token === 'string' ? event.data.token : undefined;
|
|
1053
1575
|
if (!token || !task.signalToken || token !== task.signalToken) {
|
|
@@ -1059,41 +1581,91 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1059
1581
|
return;
|
|
1060
1582
|
}
|
|
1061
1583
|
let headSha;
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1069
|
-
});
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
if (!task.reviewHeadAnchorSha) {
|
|
1073
|
-
await stayFixing({ phase: 'fix-verify-no-anchor', headSha });
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
if (headSha === task.reviewHeadAnchorSha) {
|
|
1077
|
-
const since = task.fixDispatchedAt ?? task.reviewDispatchedAt;
|
|
1078
|
-
let hasReplies;
|
|
1584
|
+
if (task.reviewMode === 'git') {
|
|
1585
|
+
if (task.prNumber === undefined) {
|
|
1586
|
+
await stayFixing({ phase: 'fix-verify-no-anchor' });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
let verify;
|
|
1079
1590
|
try {
|
|
1080
|
-
|
|
1591
|
+
verify = await manager.platformVerifyPrBinding(task.id, task.prNumber);
|
|
1081
1592
|
}
|
|
1082
1593
|
catch (err) {
|
|
1083
1594
|
await stayFixing({
|
|
1084
|
-
phase: 'fix-verify-
|
|
1595
|
+
phase: 'fix-verify-head-fetch-failed',
|
|
1085
1596
|
error: err instanceof Error ? err.message : String(err),
|
|
1086
1597
|
});
|
|
1087
1598
|
return;
|
|
1088
1599
|
}
|
|
1089
|
-
if (!
|
|
1600
|
+
if (!verify.ok) {
|
|
1601
|
+
// PR 在修复期间被关闭/转 draft/retarget:推进 recheck 只会造出无裁决入口的评审轮
|
|
1602
|
+
await stayFixing({ phase: 'fix-verify-binding-mismatch', reason: verify.reason });
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
headSha = verify.headSha;
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
try {
|
|
1609
|
+
headSha = await fetchVerifiedHeadSha(manager, task.id);
|
|
1610
|
+
}
|
|
1611
|
+
catch (err) {
|
|
1090
1612
|
await stayFixing({
|
|
1091
|
-
phase: 'fix-
|
|
1092
|
-
|
|
1613
|
+
phase: 'fix-verify-head-fetch-failed',
|
|
1614
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1093
1615
|
});
|
|
1094
1616
|
return;
|
|
1095
1617
|
}
|
|
1096
1618
|
}
|
|
1619
|
+
if (!task.reviewHeadAnchorSha) {
|
|
1620
|
+
await stayFixing({ phase: 'fix-verify-no-anchor', headSha });
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (headSha === task.reviewHeadAnchorSha) {
|
|
1624
|
+
if (task.reviewMode === 'git') {
|
|
1625
|
+
// no-op fix 通过条件 = 无未 ack 的待处理反馈 revision 且全源拉取成功(spec §6/§8);
|
|
1626
|
+
// 墙钟比较与「任意评论计回复」在 'git' 均废除
|
|
1627
|
+
let pendingCount;
|
|
1628
|
+
try {
|
|
1629
|
+
pendingCount = (await manager.platformPendingFeedback(task.id)).pending.size;
|
|
1630
|
+
}
|
|
1631
|
+
catch (err) {
|
|
1632
|
+
await stayFixing({
|
|
1633
|
+
phase: 'fix-verify-replies-fetch-failed',
|
|
1634
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1635
|
+
});
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
if (pendingCount > 0) {
|
|
1639
|
+
await stayFixing({
|
|
1640
|
+
phase: 'fix-no-op-pending-feedback',
|
|
1641
|
+
pendingCount,
|
|
1642
|
+
note: 'Dev emitted pr-fixed on an unchanged head while feedback revisions still lack a valid ack. Inspect the PR replies.',
|
|
1643
|
+
});
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
const since = task.fixDispatchedAt ?? task.reviewDispatchedAt;
|
|
1649
|
+
let hasReplies;
|
|
1650
|
+
try {
|
|
1651
|
+
hasReplies = since ? await manager.prHasDevReplySince(task.id, since) : false;
|
|
1652
|
+
}
|
|
1653
|
+
catch (err) {
|
|
1654
|
+
await stayFixing({
|
|
1655
|
+
phase: 'fix-verify-replies-fetch-failed',
|
|
1656
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1657
|
+
});
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (!hasReplies) {
|
|
1661
|
+
await stayFixing({
|
|
1662
|
+
phase: 'fix-no-op-no-commit-no-reply',
|
|
1663
|
+
note: 'Dev emitted pr-fixed but pushed no commit and left no reply on the PR — the fixing round changed nothing. Inspect the pane.',
|
|
1664
|
+
});
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1097
1669
|
// head fetch / 回复检查是秒级网络等待,期间真 push / verdict 可能已换代 pass;旧信号不得再驱动派发
|
|
1098
1670
|
const preEmit = await manager.getTask(task.id);
|
|
1099
1671
|
if (!preEmit || preEmit.status !== 'fixing' || preEmit.signalToken !== token)
|