baxian 1.2.61 → 2.0.0
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 +7 -4
- package/README.zh-CN.md +7 -4
- package/dist/agent/bootstrap-poller.d.ts +1 -1
- package/dist/agent/bootstrap-poller.d.ts.map +1 -1
- package/dist/agent/bootstrap-poller.js.map +1 -1
- package/dist/agent/bootstrap.d.ts +1 -1
- package/dist/agent/bootstrap.d.ts.map +1 -1
- package/dist/agent/bootstrap.js +6 -2
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/branch.d.ts +1 -2
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +2 -2
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts +0 -1
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +5 -44
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +175 -82
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +2453 -1447
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +5 -1
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +22 -11
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +2 -7
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +4 -4
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts +4 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +46 -45
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +8 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +30 -13
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +4 -0
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +37 -8
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.d.ts +20 -1
- package/dist/agent/review-transport.d.ts.map +1 -1
- package/dist/agent/review-transport.js +68 -20
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +12 -11
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +3 -2
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/platform-guard.d.ts +10 -2
- package/dist/api/platform-guard.d.ts.map +1 -1
- package/dist/api/platform-guard.js +112 -10
- package/dist/api/platform-guard.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +34 -9
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +21 -36
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +4 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/config/hot-reload.d.ts +11 -2
- package/dist/config/hot-reload.d.ts.map +1 -1
- package/dist/config/hot-reload.js +43 -8
- package/dist/config/hot-reload.js.map +1 -1
- package/dist/config/loader.js +1 -1
- package/dist/config/loader.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 +53 -31
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +616 -1067
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +667 -440
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -79
- package/dist/index.js.map +1 -1
- package/dist/platform/command-renderer.d.ts +1 -0
- package/dist/platform/command-renderer.d.ts.map +1 -1
- package/dist/platform/command-renderer.js +16 -2
- package/dist/platform/command-renderer.js.map +1 -1
- package/dist/platform/comment-cursor.d.ts +9 -0
- package/dist/platform/comment-cursor.d.ts.map +1 -1
- package/dist/platform/comment-cursor.js +63 -5
- package/dist/platform/comment-cursor.js.map +1 -1
- package/dist/platform/driver-host.d.ts.map +1 -1
- package/dist/platform/driver-host.js +0 -2
- 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 +17 -6
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/feedback.d.ts.map +1 -1
- package/dist/platform/feedback.js +0 -9
- package/dist/platform/feedback.js.map +1 -1
- package/dist/platform/git-driver.d.ts +3 -0
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +45 -3
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +18 -5
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +282 -33
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugins/github/baxian-plugin.json +1 -1
- package/dist/platform/plugins/github/driver.json +14 -3
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +7 -6
- package/dist/platform/plugins/github/skills/baxian-cli-gh/agents/openai.yaml +2 -0
- package/dist/platform/pr-binding.d.ts.map +1 -1
- package/dist/platform/pr-binding.js +0 -2
- package/dist/platform/pr-binding.js.map +1 -1
- package/dist/platform/pr-conversation-cache.d.ts.map +1 -0
- package/dist/platform/pr-conversation-cache.js.map +1 -0
- package/dist/platform/row-schema.d.ts.map +1 -1
- package/dist/platform/row-schema.js +1 -0
- package/dist/platform/row-schema.js.map +1 -1
- package/dist/platform/startup.d.ts +29 -1
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +82 -4
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts +2 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +6 -4
- package/dist/platform/types.js.map +1 -1
- package/dist/platform/verdict-engine.d.ts.map +1 -1
- package/dist/platform/verdict-engine.js +0 -2
- package/dist/platform/verdict-engine.js.map +1 -1
- package/dist/shared/types.d.ts +73 -5
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +4 -12
- package/dist/skills/baxian-pr-recheck/SKILL.md +3 -44
- package/dist/skills/baxian-pr-review/SKILL.md +4 -43
- package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
- package/dist/skills/baxian-signals/SKILL.md +5 -7
- package/dist/skills/baxian-task-check/SKILL.md +3 -5
- package/dist/state/agent-store.js +0 -2
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/review-store.d.ts +21 -1
- package/dist/state/review-store.d.ts.map +1 -1
- package/dist/state/review-store.js +132 -0
- package/dist/state/review-store.js.map +1 -1
- package/dist/state/task-store.d.ts +6 -0
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +259 -18
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-BXfgaFNm.js +18 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/github/mapper.d.ts +0 -9
- package/dist/github/mapper.d.ts.map +0 -1
- package/dist/github/mapper.js +0 -25
- package/dist/github/mapper.js.map +0 -1
- package/dist/github/poller.d.ts +0 -73
- package/dist/github/poller.d.ts.map +0 -1
- package/dist/github/poller.js +0 -556
- package/dist/github/poller.js.map +0 -1
- package/dist/github/pr-conversation-cache.d.ts.map +0 -1
- package/dist/github/pr-conversation-cache.js.map +0 -1
- package/dist/github/pr-conversation.d.ts +0 -7
- package/dist/github/pr-conversation.d.ts.map +0 -1
- package/dist/github/pr-conversation.js +0 -175
- package/dist/github/pr-conversation.js.map +0 -1
- package/dist/github/resolver.d.ts +0 -8
- package/dist/github/resolver.d.ts.map +0 -1
- package/dist/github/resolver.js +0 -28
- package/dist/github/resolver.js.map +0 -1
- package/dist/state/post-approve-store.d.ts +0 -23
- package/dist/state/post-approve-store.d.ts.map +0 -1
- package/dist/state/post-approve-store.js +0 -82
- package/dist/state/post-approve-store.js.map +0 -1
- package/dist/web/assets/index-CUJtd0bC.js +0 -16
- /package/dist/{github → platform}/pr-conversation-cache.d.ts +0 -0
- /package/dist/{github → platform}/pr-conversation-cache.js +0 -0
package/dist/event/handlers.js
CHANGED
|
@@ -1,15 +1,100 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeSignalActorId } from '../agent/phase-signal.js';
|
|
2
|
+
import { BODY_DIGEST_SOURCE } from '../platform/body-digest.js';
|
|
2
3
|
import { ackRevisionKey } from '../platform/markers.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { LINE_SAFE_ID_RE } from '../platform/row-schema.js';
|
|
5
|
+
import { SHA_HEX_SOURCE, SOURCE_KEY_PATTERN } from '../platform/types.js';
|
|
6
|
+
import { BRANCH_PREFIX, isRecord, isSpecStagePhase, isValidBranchName, } from '../shared/index.js';
|
|
7
|
+
import { DispatchTerminalError, isRecoverableQaDispatchHold, } from '../agent/manager.js';
|
|
6
8
|
import { ReplNotReadyError } from '../agent/tmux.js';
|
|
7
9
|
import { DirtyWorkdirError } from '../agent/branch.js';
|
|
8
|
-
// 平台 SHA 文法单点(spec §5.3:7-64 位 hex);legacy github 值恒 40 位,放宽为 additive
|
|
9
10
|
const HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
|
|
11
|
+
const BODY_DIGEST_RE = new RegExp(`^${BODY_DIGEST_SOURCE}$`);
|
|
12
|
+
const PR_UPDATED_REVIEW_FROM_STATUSES = [
|
|
13
|
+
'in_progress', 'fixing', 'review', 'approved', 'merge-ready',
|
|
14
|
+
];
|
|
10
15
|
function validHeadSha(value) {
|
|
11
16
|
return typeof value === 'string' && HEAD_SHA_RE.test(value) ? value : undefined;
|
|
12
17
|
}
|
|
18
|
+
class GitVerdictPayloadError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(`malformed git verdict: ${message}`);
|
|
21
|
+
this.name = 'GitVerdictPayloadError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function parseGitVerdictPayload(data) {
|
|
25
|
+
if (data.action !== 'APPROVE' && data.action !== 'REQUEST_CHANGES') {
|
|
26
|
+
throw new GitVerdictPayloadError('unknown action');
|
|
27
|
+
}
|
|
28
|
+
if (data.source !== 'platform-poller')
|
|
29
|
+
throw new GitVerdictPayloadError('invalid source');
|
|
30
|
+
if (typeof data.reviewPassToken !== 'string' || !/^[0-9a-f]{12}$/.test(data.reviewPassToken)) {
|
|
31
|
+
throw new GitVerdictPayloadError('invalid review pass token');
|
|
32
|
+
}
|
|
33
|
+
if (typeof data.verdictToken !== 'string' || !/^[0-9a-f]{12}$/.test(data.verdictToken)) {
|
|
34
|
+
throw new GitVerdictPayloadError('invalid verdict token');
|
|
35
|
+
}
|
|
36
|
+
const headSha = validHeadSha(data.headSha);
|
|
37
|
+
const currentHeadSha = validHeadSha(data.currentHeadSha);
|
|
38
|
+
if (!headSha || !currentHeadSha)
|
|
39
|
+
throw new GitVerdictPayloadError('invalid head sha');
|
|
40
|
+
if (!Number.isInteger(data.prNumber) || data.prNumber < 1) {
|
|
41
|
+
throw new GitVerdictPayloadError('invalid PR number');
|
|
42
|
+
}
|
|
43
|
+
if (typeof data.prUrl !== 'string' || data.prUrl.trim() === '' || data.prUrl.length > 4096) {
|
|
44
|
+
throw new GitVerdictPayloadError('invalid PR URL');
|
|
45
|
+
}
|
|
46
|
+
if (typeof data.branch !== 'string' || data.branch.trim() === '' || data.branch.length > 1024) {
|
|
47
|
+
throw new GitVerdictPayloadError('invalid PR branch');
|
|
48
|
+
}
|
|
49
|
+
if (data.submittedAt !== undefined
|
|
50
|
+
&& (typeof data.submittedAt !== 'string' || !Number.isFinite(Date.parse(data.submittedAt)))) {
|
|
51
|
+
throw new GitVerdictPayloadError('invalid submission time');
|
|
52
|
+
}
|
|
53
|
+
if (data.verdictConflict !== undefined && typeof data.verdictConflict !== 'boolean') {
|
|
54
|
+
throw new GitVerdictPayloadError('invalid conflict flag');
|
|
55
|
+
}
|
|
56
|
+
if (!isRecord(data.verdictCarrier))
|
|
57
|
+
throw new GitVerdictPayloadError('missing carrier');
|
|
58
|
+
const carrier = data.verdictCarrier;
|
|
59
|
+
if (typeof carrier.sourceKey !== 'string' || carrier.sourceKey.trim() === '' || carrier.sourceKey.length > 128
|
|
60
|
+
|| typeof carrier.id !== 'string' || carrier.id.trim() === '' || carrier.id.length > 512
|
|
61
|
+
|| typeof carrier.bodyDigest !== 'string' || !/^[0-9a-f]{64}$/.test(carrier.bodyDigest)) {
|
|
62
|
+
throw new GitVerdictPayloadError('invalid carrier');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
action: data.action,
|
|
66
|
+
reviewPassToken: data.reviewPassToken,
|
|
67
|
+
verdictToken: data.verdictToken,
|
|
68
|
+
headSha,
|
|
69
|
+
currentHeadSha,
|
|
70
|
+
prNumber: data.prNumber,
|
|
71
|
+
prUrl: data.prUrl,
|
|
72
|
+
carrier: {
|
|
73
|
+
sourceKey: carrier.sourceKey,
|
|
74
|
+
id: carrier.id,
|
|
75
|
+
bodyDigest: carrier.bodyDigest,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function parseGitFeedbackRevision(value) {
|
|
80
|
+
if (!isRecord(value))
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof value.sourceKey !== 'string' || !SOURCE_KEY_PATTERN.test(value.sourceKey))
|
|
83
|
+
return null;
|
|
84
|
+
if (typeof value.id !== 'string' || !LINE_SAFE_ID_RE.test(value.id))
|
|
85
|
+
return null;
|
|
86
|
+
if (typeof value.bodyDigest !== 'string' || !BODY_DIGEST_RE.test(value.bodyDigest))
|
|
87
|
+
return null;
|
|
88
|
+
if (value.versionTime !== undefined
|
|
89
|
+
&& (typeof value.versionTime !== 'number' || !Number.isFinite(value.versionTime)))
|
|
90
|
+
return null;
|
|
91
|
+
return {
|
|
92
|
+
sourceKey: value.sourceKey,
|
|
93
|
+
id: value.id,
|
|
94
|
+
bodyDigest: value.bodyDigest,
|
|
95
|
+
...(value.versionTime !== undefined ? { versionTime: value.versionTime } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
13
98
|
async function reArmDevelopWatcher(manager, task, agentId, opts = {}) {
|
|
14
99
|
const kinds = task.phase === 'research'
|
|
15
100
|
? ['spec-done']
|
|
@@ -35,149 +120,112 @@ async function emitIntervention(bus, projectId, agentId, taskId, data) {
|
|
|
35
120
|
console.warn(`[EventHandler] human.intervention emit failed (phase=${data.phase}):`, emitErr);
|
|
36
121
|
}
|
|
37
122
|
}
|
|
38
|
-
async function resolveAuthoritativeHead(manager, task, opts = {}) {
|
|
39
|
-
try {
|
|
40
|
-
const headSha = await manager.fetchPrHeadSha(task.id);
|
|
41
|
-
return { headSha, source: 'fetch' };
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
const fetchError = err instanceof Error ? err.message : String(err);
|
|
45
|
-
console.warn(`[handlers] fetchPrHeadSha(task=${task.id}) failed; falling back to stored / payload:`, err);
|
|
46
|
-
const stored = validHeadSha(task.latestHeadSha);
|
|
47
|
-
if (stored)
|
|
48
|
-
return { headSha: stored, source: 'task-store', fetchError };
|
|
49
|
-
if (opts.payloadCurrentHeadSha) {
|
|
50
|
-
return { headSha: opts.payloadCurrentHeadSha, source: 'payload-self', fetchError };
|
|
51
|
-
}
|
|
52
|
-
if (opts.legacyFallback) {
|
|
53
|
-
return { headSha: opts.legacyFallback, source: 'completion-approved', fetchError };
|
|
54
|
-
}
|
|
55
|
-
return { headSha: undefined, source: 'unknown', fetchError };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function fetchVerifiedHeadSha(manager, taskId) {
|
|
59
|
-
let lastErr;
|
|
60
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
61
|
-
try {
|
|
62
|
-
return await manager.fetchPrHeadSha(taskId);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
lastErr = err;
|
|
66
|
-
if (attempt < 2)
|
|
67
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
71
|
-
}
|
|
72
123
|
const POST_APPROVE_REDISPATCH_CAP = 10;
|
|
124
|
+
const POST_APPROVE_RECOVERY_MIN_AGE_MS = 60_000;
|
|
125
|
+
// 损坏值不可自愈,每周期 sweep 会重踩同一分支:按 task+值去重,值变化或修复后允许重报
|
|
126
|
+
const unparseableUpdatedAtWarned = new Map();
|
|
73
127
|
const VERDICT_FRESHNESS_SKEW_MS = 5000;
|
|
74
|
-
|
|
128
|
+
function postApproveEpisodeKey(task) {
|
|
129
|
+
if (task.postApproveGeneration === undefined
|
|
130
|
+
|| task.postApproveToken === undefined
|
|
131
|
+
|| task.postApproveHeadSha === undefined)
|
|
132
|
+
return null;
|
|
133
|
+
return {
|
|
134
|
+
generation: task.postApproveGeneration,
|
|
135
|
+
token: task.postApproveToken,
|
|
136
|
+
headSha: task.postApproveHeadSha,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function dispatchGitDevPostApproveCheck(bus, manager, snapshot, approvedHeadSha, opts = {}) {
|
|
75
140
|
if (!approvedHeadSha) {
|
|
76
|
-
await emitIntervention(bus,
|
|
141
|
+
await emitIntervention(bus, snapshot.projectId, snapshot.agentId, snapshot.id, {
|
|
77
142
|
phase: 'post-approve-approved-head-unavailable',
|
|
78
143
|
});
|
|
79
144
|
return;
|
|
80
145
|
}
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
phase: 'post-approve-dev-acquire-failed',
|
|
89
|
-
devAgentId: task.agentId,
|
|
146
|
+
const live = await manager.getTask(snapshot.id);
|
|
147
|
+
if (!live || live.status !== 'approved' || live.reviewMode !== 'git')
|
|
148
|
+
return;
|
|
149
|
+
let key = postApproveEpisodeKey(live);
|
|
150
|
+
if (!key || key.headSha !== approvedHeadSha || live.postApprovePhase === undefined) {
|
|
151
|
+
await emitIntervention(bus, snapshot.projectId, snapshot.agentId, snapshot.id, {
|
|
152
|
+
phase: 'post-approve-episode-invalid',
|
|
90
153
|
});
|
|
91
154
|
return;
|
|
92
155
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
156
|
+
if (opts.expectedEpisode
|
|
157
|
+
&& (key.generation !== opts.expectedEpisode.generation
|
|
158
|
+
|| key.token !== opts.expectedEpisode.token
|
|
159
|
+
|| key.headSha !== opts.expectedEpisode.headSha))
|
|
160
|
+
return;
|
|
161
|
+
if (opts.redispatchCount !== undefined) {
|
|
162
|
+
const rotated = await manager.rotateGitPostApproveEpisode(live.id, opts.expectedEpisode ?? key, opts.redispatchCount);
|
|
163
|
+
if (!rotated)
|
|
164
|
+
return;
|
|
165
|
+
key = rotated;
|
|
166
|
+
}
|
|
167
|
+
const acquired = await manager.acquireAgentForTask(live.agentId, live.id, 'post-approve');
|
|
168
|
+
if (!acquired) {
|
|
169
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
170
|
+
phase: 'post-approve-dev-acquire-failed',
|
|
171
|
+
devAgentId: live.agentId,
|
|
172
|
+
generation: key.generation,
|
|
101
173
|
});
|
|
102
174
|
return;
|
|
103
175
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// A fresh human verdict unlocks a standing revocation; automated redispatches never do.
|
|
114
|
-
{ clearRevocation: typeof opts.redispatchCount !== 'number' });
|
|
115
|
-
if (!installed) {
|
|
116
|
-
await manager.markAgentWaiting(task.agentId, task.id)
|
|
117
|
-
.catch(err => {
|
|
118
|
-
console.error(`[EventHandler] post-approve install refused by revocation for task=${task.id}; release failed:`, err);
|
|
176
|
+
if (!(await manager.armGitPostApproveEpisode(live.id, key))) {
|
|
177
|
+
await manager.markAgentWaiting(live.agentId, live.id, {
|
|
178
|
+
expectedPostApproveEpisode: key,
|
|
179
|
+
}).catch(err => {
|
|
180
|
+
console.error(`[EventHandler] post-approve arm refusal release failed for task=${live.id}:`, err);
|
|
181
|
+
});
|
|
182
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
183
|
+
phase: 'post-approve-signal-arm-failed',
|
|
184
|
+
generation: key.generation,
|
|
119
185
|
});
|
|
120
186
|
return;
|
|
121
187
|
}
|
|
122
188
|
let resumed = false;
|
|
123
|
-
let dispatchErr
|
|
189
|
+
let dispatchErr;
|
|
124
190
|
try {
|
|
125
|
-
resumed = await manager.continueSession(
|
|
126
|
-
signalToken,
|
|
127
|
-
|
|
191
|
+
resumed = await manager.continueSession(live.id, live.agentId, 'post-approve', {
|
|
192
|
+
signalToken: key.token,
|
|
193
|
+
postApproveEpisode: key,
|
|
194
|
+
...(opts.redispatchCount !== undefined
|
|
128
195
|
? { postApproveRedispatchCount: opts.redispatchCount }
|
|
129
196
|
: {}),
|
|
130
197
|
});
|
|
131
198
|
}
|
|
132
199
|
catch (err) {
|
|
133
200
|
dispatchErr = err;
|
|
134
|
-
console.error(`[EventHandler]
|
|
201
|
+
console.error(`[EventHandler] post-approve dispatch failed for task=${live.id}:`, err);
|
|
135
202
|
}
|
|
136
203
|
if (resumed) {
|
|
137
|
-
|
|
138
|
-
await manager.confirmPostApprovePromptDelivered(task.id, signalToken);
|
|
204
|
+
await manager.confirmPostApprovePromptDelivered(live.id, key);
|
|
139
205
|
return;
|
|
140
206
|
}
|
|
141
|
-
// git:未确认送达的失败保留 installed+pending 交 sweep 补派(DispatchTerminalError 单独终结);
|
|
142
|
-
// legacy 维持清除语义
|
|
143
|
-
if (task.reviewMode !== 'git' || dispatchErr instanceof DispatchTerminalError) {
|
|
144
|
-
await manager.clearPostApproveCompletionIfMatches(task.id, signalToken);
|
|
145
|
-
}
|
|
146
207
|
if (dispatchErr instanceof DispatchTerminalError) {
|
|
147
|
-
await manager.failTaskForDispatchError(
|
|
208
|
+
await manager.failTaskForDispatchError(live.id, 'post-approve', live.agentId, dispatchErr, {
|
|
209
|
+
expectedPostApproveEpisode: key,
|
|
210
|
+
});
|
|
148
211
|
return;
|
|
149
212
|
}
|
|
150
|
-
await manager.markAgentWaiting(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
213
|
+
await manager.markAgentWaiting(live.agentId, live.id, {
|
|
214
|
+
expectedPostApproveEpisode: key,
|
|
215
|
+
}).catch(err => {
|
|
216
|
+
console.error(`[EventHandler] post-approve rollback failed for task=${live.id}:`, err);
|
|
154
217
|
});
|
|
155
|
-
await emitIntervention(bus,
|
|
218
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
156
219
|
phase: 'post-approve-dispatch-failed',
|
|
157
|
-
reviewRound:
|
|
158
|
-
|
|
220
|
+
reviewRound: live.reviewRound,
|
|
221
|
+
generation: key.generation,
|
|
159
222
|
...(dispatchErr ? { error: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr) } : {}),
|
|
160
223
|
});
|
|
161
224
|
}
|
|
162
|
-
async function gateDevForPostApproveRedispatch(bus, manager, task) {
|
|
163
|
-
const ready = await manager
|
|
164
|
-
.releaseAgentForTask(task.agentId, task.id, 'waiting')
|
|
165
|
-
.catch(err => {
|
|
166
|
-
console.error(`[EventHandler] post-approve redispatch releaseAgentForTask(dev=${task.agentId}) failed:`, err);
|
|
167
|
-
return false;
|
|
168
|
-
});
|
|
169
|
-
if (ready)
|
|
170
|
-
return true;
|
|
171
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
172
|
-
phase: 'post-approve-dev-wait-gate-failed-before-redispatch',
|
|
173
|
-
devAgentId: task.agentId,
|
|
174
|
-
});
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
225
|
async function handlePrMergeReady(bus, manager, event) {
|
|
178
226
|
const taskId = event.taskId;
|
|
179
227
|
const taskNow = await manager.getTask(taskId);
|
|
180
|
-
if (!taskNow)
|
|
228
|
+
if (!taskNow || taskNow.reviewMode !== 'git')
|
|
181
229
|
return;
|
|
182
230
|
const eventPrNumber = event.data.prNumber;
|
|
183
231
|
const eventPrUrl = event.data.prUrl;
|
|
@@ -197,7 +245,12 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
197
245
|
|| !signalToken
|
|
198
246
|
|| completion?.token !== signalToken)
|
|
199
247
|
return;
|
|
200
|
-
const
|
|
248
|
+
const entryGitEpisode = postApproveEpisodeKey(taskNow);
|
|
249
|
+
if (entryGitEpisode?.token !== signalToken)
|
|
250
|
+
return;
|
|
251
|
+
const ok = await manager.markAgentWaiting(taskNow.agentId, taskNow.id, {
|
|
252
|
+
expectedPostApproveEpisode: entryGitEpisode,
|
|
253
|
+
});
|
|
201
254
|
if (!ok) {
|
|
202
255
|
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
203
256
|
phase: 'post-approve-dev-wait-gate-failed',
|
|
@@ -210,69 +263,91 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
210
263
|
]);
|
|
211
264
|
if (!freshTask
|
|
212
265
|
|| freshTask.status !== 'approved'
|
|
266
|
+
|| freshTask.reviewMode !== 'git'
|
|
213
267
|
|| freshTask.agentId !== taskNow.agentId
|
|
214
268
|
|| freshTask.postApproveRevoked !== undefined
|
|
215
|
-
|| entryCompletion?.token !== signalToken
|
|
216
|
-
|| freshTask.postApproveHeadSha === undefined
|
|
217
|
-
|| entryCompletion.approvedHeadSha !== freshTask.postApproveHeadSha) {
|
|
269
|
+
|| entryCompletion?.token !== signalToken) {
|
|
218
270
|
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
219
271
|
phase: 'post-approve-merge-skipped-stale-task',
|
|
220
272
|
});
|
|
221
273
|
return;
|
|
222
274
|
}
|
|
223
|
-
// 'git' merge-ready 接收即 server 权威双复核(spec §6/§10:provenance + ack 完成集,同一次全源扫描):
|
|
224
|
-
// agent 自查是尽责而非授权
|
|
225
275
|
let freshCompletion = entryCompletion;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
276
|
+
const gitEpisodeKey = postApproveEpisodeKey(freshTask);
|
|
277
|
+
if (!gitEpisodeKey || gitEpisodeKey.token !== signalToken) {
|
|
278
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
279
|
+
phase: 'post-approve-merge-skipped-stale-task',
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await manager.markPostApproveSignalReceived(freshTask.id, gitEpisodeKey);
|
|
284
|
+
const scanFailedRetry = async (error) => {
|
|
285
|
+
await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: true }, { expectedGeneration: gitEpisodeKey.generation, expectedHeadSha: gitEpisodeKey.headSha });
|
|
286
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
287
|
+
phase: 'post-approve-merge-skipped-scan-failed',
|
|
288
|
+
error,
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
let verified;
|
|
292
|
+
try {
|
|
293
|
+
verified = await manager.platformVerifyAcceptedPass(freshTask.id);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
await scanFailedRetry(err instanceof Error ? err.message : String(err));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (verified.kind === 'retryable') {
|
|
300
|
+
await scanFailedRetry(verified.reason);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (verified.kind === 'provenance-invalid') {
|
|
304
|
+
const begun = await manager.beginGitReviewPass(freshTask.id, {
|
|
305
|
+
fromStatus: [freshTask.status],
|
|
306
|
+
headSha: gitEpisodeKey.headSha,
|
|
307
|
+
bumpRound: true,
|
|
308
|
+
qaPhase: 'recheck',
|
|
309
|
+
expectPhase: freshTask.phase,
|
|
310
|
+
expectSignalToken: freshTask.signalToken,
|
|
311
|
+
patch: { latestHeadSha: gitEpisodeKey.headSha },
|
|
312
|
+
});
|
|
313
|
+
if (begun?.task.reviewDispatch) {
|
|
314
|
+
try {
|
|
315
|
+
await manager.dispatchGitReviewLease(freshTask.id, {
|
|
316
|
+
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
317
|
+
});
|
|
249
318
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
319
|
+
catch (err) {
|
|
320
|
+
if (err instanceof DispatchTerminalError && begun.task.qaAgentId) {
|
|
321
|
+
await manager.failTaskForDispatchError(freshTask.id, 'recheck', begun.task.qaAgentId, err, { expectedReviewDispatch: begun.task.reviewDispatch });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
325
|
+
phase: 'post-approve-provenance-redispatch-failed',
|
|
326
|
+
reason: verified.reason,
|
|
327
|
+
error: err instanceof Error ? err.message : String(err),
|
|
328
|
+
});
|
|
260
329
|
}
|
|
261
330
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (verified.pendingCount > 0 && !freshCompletion.pendingRedispatch) {
|
|
334
|
+
if (await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: true }, { expectedGeneration: gitEpisodeKey.generation, expectedHeadSha: gitEpisodeKey.headSha })) {
|
|
335
|
+
freshCompletion = { ...freshCompletion, pendingRedispatch: true };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (verified.pendingCount === 0 && freshCompletion.pendingRedispatch) {
|
|
339
|
+
if (await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: false }, { expectedGeneration: gitEpisodeKey.generation, expectedHeadSha: gitEpisodeKey.headSha })) {
|
|
340
|
+
freshCompletion = { ...freshCompletion, pendingRedispatch: false };
|
|
267
341
|
}
|
|
268
342
|
}
|
|
269
|
-
// A pending flag landing between the check and the transition re-enters here instead of losing the feedback.
|
|
270
343
|
for (;;) {
|
|
271
344
|
if (freshCompletion.pendingRedispatch) {
|
|
272
345
|
const nextCount = (freshCompletion.redispatchCount ?? 0) + 1;
|
|
273
346
|
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
274
347
|
const revoked = await manager.revokePostApproveCompletion(freshTask.id, 'redispatch-cap', {
|
|
275
348
|
expectedToken: freshCompletion.token,
|
|
349
|
+
expectedGeneration: gitEpisodeKey.generation,
|
|
350
|
+
expectedHeadSha: gitEpisodeKey.headSha,
|
|
276
351
|
});
|
|
277
352
|
if (!revoked) {
|
|
278
353
|
// The pass rotated after the entry re-validation; this merge-ready and its cap decision are stale.
|
|
@@ -288,10 +363,10 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
288
363
|
});
|
|
289
364
|
return;
|
|
290
365
|
}
|
|
291
|
-
await
|
|
366
|
+
await dispatchGitDevPostApproveCheck(bus, manager, freshTask, freshCompletion.approvedHeadSha, { redispatchCount: nextCount });
|
|
292
367
|
return;
|
|
293
368
|
}
|
|
294
|
-
const completed = await manager.completeApprovedPassToMergeReady(freshTask.id,
|
|
369
|
+
const completed = await manager.completeApprovedPassToMergeReady(freshTask.id, gitEpisodeKey);
|
|
295
370
|
if ('task' in completed)
|
|
296
371
|
return;
|
|
297
372
|
if (completed.refused === 'pending') {
|
|
@@ -310,156 +385,53 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
310
385
|
async function handlePrFeedback(bus, manager, event) {
|
|
311
386
|
const taskId = event.taskId;
|
|
312
387
|
const taskNow = await manager.getTask(taskId);
|
|
313
|
-
if (!taskNow)
|
|
314
|
-
return;
|
|
315
|
-
const eventPrNumber = event.data.prNumber;
|
|
316
|
-
const eventPrUrl = event.data.prUrl;
|
|
317
|
-
const eventKind = event.data.kind;
|
|
318
|
-
const isNewFeedback = eventKind === 'comment' || eventKind === 'review-comment';
|
|
319
|
-
const revision = taskNow.reviewMode === 'git' && isRecord(event.data.revision)
|
|
320
|
-
? event.data.revision
|
|
321
|
-
: undefined;
|
|
322
|
-
const revisionKey = revision ? ackRevisionKey(revision.sourceKey, revision.id, revision.bodyDigest) : undefined;
|
|
323
|
-
const revisionTime = typeof revision?.versionTime === 'number' ? revision.versionTime : Date.parse(event.timestamp);
|
|
324
|
-
// 消费幂等(spec §6):ledger 落盘前崩溃的重投在此收敛,redispatch 计数不重复生效
|
|
325
|
-
if (revisionKey !== undefined && taskNow.consumedFeedback?.[revisionKey] !== undefined)
|
|
388
|
+
if (!taskNow || taskNow.reviewMode !== 'git')
|
|
326
389
|
return;
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// pending、其它→仅消费——入口快照与提交之间的并发迁移(completeApprovedPass 等)不再能让
|
|
333
|
-
// revision 既被 poller 记 delivered 又不产生效果
|
|
334
|
-
if (taskNow.reviewMode === 'git' && isNewFeedback && revisionKey !== undefined) {
|
|
335
|
-
const outcome = await manager.consumeGitFeedbackRevision(taskId, { key: revisionKey, versionTime: revisionTime }, { feedbackAt: event.timestamp });
|
|
336
|
-
if (outcome.kind !== 'pending' && outcome.kind !== 'returned')
|
|
337
|
-
return;
|
|
338
|
-
const effected = outcome.task;
|
|
339
|
-
const completion = await manager.getPostApproveCompletion(effected.id);
|
|
340
|
-
const devState = await manager.getAgentState(effected.agentId);
|
|
341
|
-
if (outcome.kind === 'pending' && completion !== null && devState?.taskId === effected.id) {
|
|
342
|
-
// 在途 pass 会经 merge-ready 循环消化 pending;无 completion 的 durable pending 由下方补派
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const nextCount = (effected.redispatchCount ?? completion?.redispatchCount ?? 0) + 1;
|
|
346
|
-
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
347
|
-
await manager.revokePostApproveCompletion(effected.id, 'redispatch-cap');
|
|
348
|
-
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
349
|
-
phase: 'post-approve-redispatch-cap-exceeded',
|
|
350
|
-
redispatchCount: effected.redispatchCount ?? 0,
|
|
351
|
-
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
352
|
-
});
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const approvedHead = effected.postApproveHeadSha ?? validHeadSha(effected.latestHeadSha);
|
|
356
|
-
if (!approvedHead) {
|
|
357
|
-
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
358
|
-
phase: 'post-approve-approved-head-unavailable',
|
|
359
|
-
});
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
await dispatchDevPostApproveCheck(bus, manager, effected, approvedHead, { redispatchCount: nextCount });
|
|
390
|
+
const revision = parseGitFeedbackRevision(event.data.revision);
|
|
391
|
+
if (!revision) {
|
|
392
|
+
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
393
|
+
phase: 'git-feedback-revision-invalid',
|
|
394
|
+
});
|
|
363
395
|
return;
|
|
364
396
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
...(isNewFeedback ? { prFeedbackReceivedAt: event.timestamp } : {}),
|
|
368
|
-
};
|
|
369
|
-
const needsPatch = (eventPrNumber !== undefined && eventPrNumber !== taskNow.prNumber)
|
|
370
|
-
|| (eventPrUrl !== undefined && eventPrUrl !== taskNow.prUrl)
|
|
371
|
-
|| isNewFeedback;
|
|
372
|
-
if (needsPatch) {
|
|
373
|
-
await manager.updateTask(taskId, taskPatch);
|
|
374
|
-
}
|
|
375
|
-
if (taskNow.status !== 'approved')
|
|
397
|
+
const revisionKey = ackRevisionKey(revision.sourceKey, revision.id, revision.bodyDigest);
|
|
398
|
+
if (taskNow.consumedFeedback?.[revisionKey] !== undefined)
|
|
376
399
|
return;
|
|
377
|
-
|
|
400
|
+
const revisionTime = revision.versionTime ?? Date.parse(event.timestamp);
|
|
401
|
+
const outcome = await manager.consumeGitFeedbackRevision(taskId, { key: revisionKey, versionTime: revisionTime }, { feedbackAt: event.timestamp });
|
|
402
|
+
if (outcome.kind !== 'pending' && outcome.kind !== 'returned')
|
|
378
403
|
return;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
});
|
|
404
|
+
const effected = outcome.task;
|
|
405
|
+
const completion = await manager.getPostApproveCompletion(effected.id);
|
|
406
|
+
const devState = await manager.getAgentState(effected.agentId);
|
|
407
|
+
if (outcome.kind === 'pending' && completion !== null && devState?.taskId === effected.id)
|
|
384
408
|
return;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
409
|
+
const nextCount = (effected.redispatchCount ?? completion?.redispatchCount ?? 0) + 1;
|
|
410
|
+
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
411
|
+
const key = postApproveEpisodeKey(effected);
|
|
412
|
+
await manager.revokePostApproveCompletion(effected.id, 'redispatch-cap', key
|
|
413
|
+
? {
|
|
414
|
+
expectedToken: key.token,
|
|
415
|
+
expectedGeneration: key.generation,
|
|
416
|
+
expectedHeadSha: key.headSha,
|
|
417
|
+
}
|
|
418
|
+
: {});
|
|
419
|
+
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
420
|
+
phase: 'post-approve-redispatch-cap-exceeded',
|
|
421
|
+
redispatchCount: effected.redispatchCount ?? 0,
|
|
422
|
+
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
391
423
|
});
|
|
392
424
|
return;
|
|
393
425
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// pending 已立不改效果,但本条 revision 的消费键仍须落盘:崩溃后重投不得再计一轮
|
|
400
|
-
if (revisionKey !== undefined) {
|
|
401
|
-
await manager.updatePostApproveCompletionIfToken(taskNow.id, completion.token, {}, { consumeRevision: { key: revisionKey, versionTime: revisionTime } });
|
|
402
|
-
}
|
|
403
|
-
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
404
|
-
// Same token: the pending mark is already on the live pass.
|
|
405
|
-
if (!fresh || fresh.token === completion.token)
|
|
406
|
-
return;
|
|
407
|
-
completion = fresh;
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
if (await manager.updatePostApproveCompletionIfToken(taskNow.id, completion.token, {
|
|
411
|
-
pendingRedispatch: true,
|
|
412
|
-
}, revisionKey !== undefined ? { consumeRevision: { key: revisionKey, versionTime: revisionTime } } : {}))
|
|
413
|
-
return;
|
|
414
|
-
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
415
|
-
// Same token: refused in place (revoked marker) — the successor fixing flow owns this feedback.
|
|
416
|
-
if (!fresh || fresh.token === completion.token)
|
|
417
|
-
return;
|
|
418
|
-
completion = fresh;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (taskNow.reviewMode === 'git' && revisionKey !== undefined) {
|
|
422
|
-
if (await manager.updatePostApproveCompletionIfToken(taskNow.id, completion.token, {
|
|
423
|
-
pendingRedispatch: true,
|
|
424
|
-
}, { consumeRevision: { key: revisionKey, versionTime: revisionTime } })) {
|
|
425
|
-
completion = { ...completion, pendingRedispatch: true };
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
429
|
-
if (!fresh || fresh.token === completion.token)
|
|
430
|
-
return;
|
|
431
|
-
completion = fresh;
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const nextCount = (completion.redispatchCount ?? 0) + 1;
|
|
436
|
-
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
437
|
-
const revoked = await manager.revokePostApproveCompletion(taskNow.id, 'redispatch-cap', {
|
|
438
|
-
expectedToken: completion.token,
|
|
439
|
-
});
|
|
440
|
-
if (!revoked) {
|
|
441
|
-
const fresh = await manager.getPostApproveCompletion(taskNow.id);
|
|
442
|
-
if (!fresh || fresh.token === completion.token)
|
|
443
|
-
return;
|
|
444
|
-
completion = fresh;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
448
|
-
phase: 'post-approve-redispatch-cap-exceeded',
|
|
449
|
-
redispatchCount: completion.redispatchCount ?? 0,
|
|
450
|
-
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
451
|
-
});
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const ready = await gateDevForPostApproveRedispatch(bus, manager, taskNow);
|
|
455
|
-
if (!ready)
|
|
456
|
-
return;
|
|
457
|
-
await dispatchDevPostApproveCheck(bus, manager, { ...taskNow, ...taskPatch }, completion.approvedHeadSha, { redispatchCount: nextCount });
|
|
426
|
+
const approvedHead = effected.postApproveHeadSha ?? validHeadSha(effected.latestHeadSha);
|
|
427
|
+
if (!approvedHead) {
|
|
428
|
+
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
429
|
+
phase: 'post-approve-approved-head-unavailable',
|
|
430
|
+
});
|
|
458
431
|
return;
|
|
459
432
|
}
|
|
433
|
+
await dispatchGitDevPostApproveCheck(bus, manager, effected, approvedHead, { redispatchCount: nextCount });
|
|
460
434
|
}
|
|
461
|
-
// 恢复期消费 durable pendingRedispatch(spec §6 至少一次):merge-ready 退回或 path C 的 CAS
|
|
462
|
-
// 落盘后、派发前崩溃的残局在此补派;dev 仍绑定任务时不派(其在途 pass 的 merge-ready 循环接手)。
|
|
463
435
|
export async function recoverGitPostApprovePending(bus, manager) {
|
|
464
436
|
const tasks = await manager.listActiveGitTasks();
|
|
465
437
|
for (const task of tasks) {
|
|
@@ -467,7 +439,6 @@ export async function recoverGitPostApprovePending(bus, manager) {
|
|
|
467
439
|
await recoverOneGitPendingTask(bus, manager, task);
|
|
468
440
|
}
|
|
469
441
|
catch (err) {
|
|
470
|
-
// 单任务失败不得饿死其后的 durable pending(至少一次):报告并继续
|
|
471
442
|
console.warn(`[EventHandler] recoverGitPostApprovePending: task=${task.id} failed, continuing:`, err);
|
|
472
443
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
473
444
|
phase: 'post-approve-recovery-failed',
|
|
@@ -478,328 +449,159 @@ export async function recoverGitPostApprovePending(bus, manager) {
|
|
|
478
449
|
}
|
|
479
450
|
async function recoverOneGitPendingTask(bus, manager, task) {
|
|
480
451
|
{
|
|
481
|
-
|
|
452
|
+
const live = await manager.getTask(task.id);
|
|
453
|
+
if (!live || live.reviewMode !== 'git' || live.status !== 'approved' || live.pendingRedispatch !== true)
|
|
454
|
+
return;
|
|
455
|
+
if (live.postApproveRevoked)
|
|
456
|
+
return;
|
|
457
|
+
const updatedAt = Date.parse(live.updatedAt);
|
|
458
|
+
if (!Number.isFinite(updatedAt)) {
|
|
459
|
+
if (unparseableUpdatedAtWarned.get(live.id) !== live.updatedAt) {
|
|
460
|
+
unparseableUpdatedAtWarned.set(live.id, live.updatedAt);
|
|
461
|
+
console.warn(`[EventHandler] recoverGitPostApprovePending: task=${live.id} updatedAt unparseable, skipping recovery:`, live.updatedAt);
|
|
462
|
+
}
|
|
482
463
|
return;
|
|
483
|
-
|
|
464
|
+
}
|
|
465
|
+
unparseableUpdatedAtWarned.delete(live.id);
|
|
466
|
+
if (Date.now() - updatedAt < POST_APPROVE_RECOVERY_MIN_AGE_MS)
|
|
484
467
|
return;
|
|
485
|
-
if (
|
|
486
|
-
const devState = await manager.getAgentState(
|
|
487
|
-
if (devState?.taskId ===
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
468
|
+
if (live.postApprovePhase === 'delivered') {
|
|
469
|
+
const devState = await manager.getAgentState(live.agentId);
|
|
470
|
+
if (devState?.taskId === live.id) {
|
|
471
|
+
const rearmed = await manager.rearmPostApproveSignal(live.id);
|
|
472
|
+
if (!rearmed) {
|
|
473
|
+
const fresh = await manager.getTask(live.id);
|
|
474
|
+
if (fresh?.status === 'approved'
|
|
475
|
+
&& fresh.pendingRedispatch === true
|
|
476
|
+
&& fresh.postApprovePhase === 'delivered'
|
|
477
|
+
&& fresh.postApproveGeneration === live.postApproveGeneration
|
|
478
|
+
&& fresh.postApproveToken === live.postApproveToken) {
|
|
479
|
+
throw new Error(`post-approve signal re-arm refused for active task ${live.id}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
491
482
|
return;
|
|
492
483
|
}
|
|
493
484
|
}
|
|
494
|
-
const approvedHead = validHeadSha(
|
|
485
|
+
const approvedHead = validHeadSha(live.postApproveHeadSha ?? live.latestHeadSha);
|
|
495
486
|
if (!approvedHead) {
|
|
496
|
-
await emitIntervention(bus,
|
|
487
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
497
488
|
phase: 'post-approve-approved-head-unavailable',
|
|
498
489
|
});
|
|
499
490
|
return;
|
|
500
491
|
}
|
|
501
|
-
const nextCount = (
|
|
492
|
+
const nextCount = (live.redispatchCount ?? 0) + 1;
|
|
502
493
|
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
503
|
-
|
|
504
|
-
await
|
|
494
|
+
const key = postApproveEpisodeKey(live);
|
|
495
|
+
await manager.revokePostApproveCompletion(live.id, 'redispatch-cap', key
|
|
496
|
+
? {
|
|
497
|
+
expectedToken: key.token,
|
|
498
|
+
expectedGeneration: key.generation,
|
|
499
|
+
expectedHeadSha: key.headSha,
|
|
500
|
+
}
|
|
501
|
+
: {});
|
|
502
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
505
503
|
phase: 'post-approve-redispatch-cap-exceeded',
|
|
506
|
-
redispatchCount:
|
|
504
|
+
redispatchCount: live.redispatchCount ?? 0,
|
|
507
505
|
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
508
506
|
});
|
|
509
507
|
return;
|
|
510
508
|
}
|
|
511
|
-
await
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const now = await manager.getTask(taskId);
|
|
516
|
-
if (now && !TASK_TERMINAL_STATUS_SET.has(now.status)) {
|
|
517
|
-
// pass 被并发接管:QA 已被接管方 re-acquire,释放会误清其绑定;
|
|
518
|
-
// 但本次派发的 arm 可能已顶掉接管方的 watcher(start 先 stop 同 task 旧 watcher),按当前 pass 重建
|
|
519
|
-
console.warn(`${logPrefix} rollback skipped for task=${taskId}: pass superseded`);
|
|
520
|
-
await manager.rearmPhaseSignalForCurrentPass(taskId);
|
|
521
|
-
return;
|
|
509
|
+
await dispatchGitDevPostApproveCheck(bus, manager, live, approvedHead, {
|
|
510
|
+
redispatchCount: nextCount,
|
|
511
|
+
expectedEpisode: postApproveEpisodeKey(live) ?? undefined,
|
|
512
|
+
});
|
|
522
513
|
}
|
|
523
|
-
// 终态/已删:没有接管方持有这只 QA,而 cancel 清理可能没见到刚写入的 qaAgentId。
|
|
524
|
-
// 仍钉住本次 acquire 世代——本 handler 的每一条失败释放都只清自己那一代
|
|
525
|
-
await manager.releaseAgentForTask(qaId, taskId, 'idle', ownAcquire).catch(err => {
|
|
526
|
-
console.error(`${logPrefix} releaseAgentForTask(QA=${qaId}) after terminal-skip rollback failed:`, err);
|
|
527
|
-
return false;
|
|
528
|
-
});
|
|
529
514
|
}
|
|
530
515
|
async function handlePrCodePush(bus, manager, event) {
|
|
531
516
|
const taskId = event.taskId;
|
|
532
|
-
const agentId = event.agentId;
|
|
533
517
|
const eventPrNumber = event.data.prNumber;
|
|
534
518
|
const eventPrUrl = event.data.prUrl;
|
|
535
519
|
const eventKind = event.data.kind;
|
|
536
520
|
const eventHeadSha = validHeadSha(event.data.headSha);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const prPatch = {
|
|
540
|
-
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
541
|
-
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
542
|
-
...(eventKind === 'push' && eventHeadSha ? { latestHeadSha: eventHeadSha } : {}),
|
|
543
|
-
};
|
|
544
|
-
const taskBeforeTransition = await manager.getTask(taskId);
|
|
545
|
-
if (!taskBeforeTransition)
|
|
521
|
+
const task = await manager.getTask(taskId);
|
|
522
|
+
if (!task || task.reviewMode !== 'git')
|
|
546
523
|
return;
|
|
547
|
-
|
|
548
|
-
// 都是有害动作(QA 释放/令牌轮换/计轮/撤销 approval),恰一次由 TaskState 锚判定。
|
|
549
|
-
// pr-fixed 合成 push 豁免——no-code fix 的 recheck 派发正是靠同 head 重入(spec §7 同 head 复检)。
|
|
550
|
-
if (taskBeforeTransition.reviewMode === 'git' && eventKind === 'push' && eventHeadSha
|
|
524
|
+
if (eventKind === 'push' && eventHeadSha
|
|
551
525
|
&& event.data.source !== 'pr-fixed'
|
|
552
|
-
&&
|
|
553
|
-
&&
|
|
554
|
-
&&
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const willHavePrNumber = taskBeforeTransition.prNumber !== undefined || eventPrNumber !== undefined;
|
|
558
|
-
if (taskBeforeTransition.status === 'in_progress' && !willHavePrNumber) {
|
|
559
|
-
console.warn(`[EventHandler] pr.updated: task ${taskId} in_progress but neither task nor event has prNumber; ` +
|
|
560
|
-
`deferring catch-up`);
|
|
526
|
+
&& task.reviewDispatch === undefined
|
|
527
|
+
&& task.latestHeadSha === eventHeadSha
|
|
528
|
+
&& task.reviewHeadAnchorSha === eventHeadSha) {
|
|
561
529
|
return;
|
|
562
530
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
...prPatch,
|
|
566
|
-
...(anchorAtDispatch ? { reviewHeadAnchorSha: anchorAtDispatch } : {}),
|
|
567
|
-
reviewDispatchedAt: new Date().toISOString(),
|
|
568
|
-
signalToken: createSignalToken(),
|
|
569
|
-
// 这些起点的轮次在 prompt 确认送达后才计(consumeReviewRoundIntent);持久化「未计」意图,
|
|
570
|
-
// 供 busy pending/可恢复 hold/进程重启后的任何补派方恰好补计一次
|
|
571
|
-
reviewRoundPending: (taskBeforeTransition.status === 'in_progress'
|
|
572
|
-
|| taskBeforeTransition.status === 'approved'
|
|
573
|
-
|| taskBeforeTransition.status === 'merge-ready') ? true
|
|
574
|
-
: (taskBeforeTransition.status === 'review' ? taskBeforeTransition.reviewRoundPending : undefined),
|
|
575
|
-
// pair 与新轮可见性原子(spec §7):transition 后到 rotate 的窗口内旧 pair 不得授权本轮
|
|
576
|
-
...(taskBeforeTransition.reviewMode === 'git'
|
|
577
|
-
? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true }
|
|
578
|
-
: {}),
|
|
579
|
-
});
|
|
580
|
-
if (!result)
|
|
531
|
+
if (task.status === 'in_progress' && task.prNumber === undefined && eventPrNumber === undefined) {
|
|
532
|
+
console.warn(`[EventHandler] pr.updated: task ${taskId} in_progress but neither task nor event has prNumber; deferring catch-up`);
|
|
581
533
|
return;
|
|
582
|
-
const { task: transitioned, previousStatus } = result;
|
|
583
|
-
await manager.clearPostApproveCompletion(transitioned.id);
|
|
584
|
-
let devAlreadyWaiting = false;
|
|
585
|
-
if (previousStatus === 'approved' || previousStatus === 'merge-ready') {
|
|
586
|
-
devAlreadyWaiting = await manager
|
|
587
|
-
.releaseAgentForTask(transitioned.agentId, transitioned.id, 'waiting')
|
|
588
|
-
.catch(err => {
|
|
589
|
-
console.error(`[EventHandler] pr.updated releaseAgentForTask(dev=${transitioned.agentId}) before approved→recheck failed:`, err);
|
|
590
|
-
return false;
|
|
591
|
-
});
|
|
592
|
-
if (!devAlreadyWaiting) {
|
|
593
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
594
|
-
phase: 'post-approve-dev-wait-gate-failed-before-recheck',
|
|
595
|
-
devAgentId: transitioned.agentId,
|
|
596
|
-
});
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
534
|
}
|
|
600
|
-
|
|
601
|
-
const deferredFirstReview = previousStatus === 'review'
|
|
602
|
-
&& taskBeforeTransition.reviewRound === 0
|
|
603
|
-
&& taskBeforeTransition.reviewRoundPending === true;
|
|
604
|
-
const qaPhase = previousStatus === 'in_progress' || deferredFirstReview ? 'review' : 'recheck';
|
|
605
|
-
if (previousStatus === 'review' && transitioned.qaAgentId) {
|
|
606
|
-
const released = await manager
|
|
607
|
-
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { deferWhenBusy: true })
|
|
608
|
-
.catch(err => {
|
|
609
|
-
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${transitioned.qaAgentId}) for review→review push failed:`, err);
|
|
610
|
-
return false;
|
|
611
|
-
});
|
|
612
|
-
if (!released) {
|
|
613
|
-
// 忙碌拒绝的形状:QA 仍绑定本任务且未落 hold(release 对 REPL 忙碌不再写 hold)。
|
|
614
|
-
// 这是「欠一次投递」而非故障:登记 pending,由对账在 pane 空闲后按标准入口补派
|
|
615
|
-
const qaAfter = await manager.getAgentState(transitioned.qaAgentId).catch(() => null);
|
|
616
|
-
const deferrable = qaAfter?.taskId === transitioned.id && qaAfter.status !== 'awaiting_human';
|
|
617
|
-
if (deferrable && transitioned.signalToken !== undefined) {
|
|
618
|
-
manager.registerPendingDispatchRetry(transitioned.id, {
|
|
619
|
-
kind: 'qa-recheck',
|
|
620
|
-
agentId: transitioned.qaAgentId,
|
|
621
|
-
signalToken: transitioned.signalToken,
|
|
622
|
-
qaPhase,
|
|
623
|
-
});
|
|
624
|
-
console.warn(`[EventHandler] pr.updated QA ${qaPhase} deferred for task=${transitioned.id}: QA still busy on the ` +
|
|
625
|
-
`previous turn; reconciler will redispatch when idle`);
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
629
|
-
phase: 'qa-release-failed-cannot-recheck',
|
|
630
|
-
qaAgentId: transitioned.qaAgentId,
|
|
631
|
-
});
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
const qaId = transitioned.qaAgentId;
|
|
636
|
-
if (!qaId) {
|
|
637
|
-
if (!devAlreadyWaiting && !(await manager.markAgentWaiting(agentId, transitioned.id))) {
|
|
638
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
639
|
-
phase: 'dev-wait-gate-failed-no-qa',
|
|
640
|
-
});
|
|
641
|
-
}
|
|
535
|
+
if (!PR_UPDATED_REVIEW_FROM_STATUSES.includes(task.status))
|
|
642
536
|
return;
|
|
537
|
+
let reviewHead = eventHeadSha ?? validHeadSha(task.latestHeadSha);
|
|
538
|
+
const prNumber = eventPrNumber ?? task.prNumber;
|
|
539
|
+
if (reviewHead === undefined && prNumber !== undefined) {
|
|
540
|
+
const verified = await manager.platformVerifyPrBinding(taskId, prNumber);
|
|
541
|
+
if (verified.ok)
|
|
542
|
+
reviewHead = validHeadSha(verified.headSha);
|
|
643
543
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
await manager.markAgentWaiting(agentId, transitioned.id).catch(() => false);
|
|
648
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
649
|
-
phase: 'qa-config-missing',
|
|
650
|
-
qaAgentId: qaId,
|
|
544
|
+
if (reviewHead === undefined) {
|
|
545
|
+
await emitIntervention(bus, task.projectId, task.agentId, taskId, {
|
|
546
|
+
phase: 'git-review-head-unavailable',
|
|
651
547
|
});
|
|
652
548
|
return;
|
|
653
549
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
550
|
+
const bumpRound = task.status === 'in_progress'
|
|
551
|
+
|| task.status === 'approved'
|
|
552
|
+
|| task.status === 'merge-ready'
|
|
553
|
+
|| (task.status === 'review' && task.reviewRoundPending === true);
|
|
554
|
+
const begun = await manager.beginGitReviewPass(taskId, {
|
|
555
|
+
fromStatus: [task.status],
|
|
556
|
+
headSha: reviewHead,
|
|
557
|
+
bumpRound,
|
|
558
|
+
expectPhase: task.phase,
|
|
559
|
+
expectSignalToken: task.signalToken,
|
|
560
|
+
patch: {
|
|
561
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
562
|
+
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
563
|
+
latestHeadSha: reviewHead,
|
|
564
|
+
},
|
|
659
565
|
});
|
|
660
|
-
if (!
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
|
|
670
|
-
if (!armed) {
|
|
671
|
-
console.warn(`[EventHandler] pr.updated verdict watcher failed to arm for task=${transitioned.id} (${qaPhase}); rolling back recheck dispatch`);
|
|
672
|
-
if (previousStatus === 'in_progress' || previousStatus === 'fixing'
|
|
673
|
-
|| (previousStatus === 'review' && taskBeforeTransition.reviewMode === 'git')) {
|
|
674
|
-
const rolledBack = await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
675
|
-
status: previousStatus,
|
|
676
|
-
signalToken: taskBeforeTransition.signalToken,
|
|
677
|
-
reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
|
|
678
|
-
reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
|
|
679
|
-
// git 回滚保留 durable pending 供 sweep 重试:计轮 intent 必须同持(transition 写入值),
|
|
680
|
-
// 否则最终送达的首评按 stale intent 少计一轮
|
|
681
|
-
reviewRoundPending: taskBeforeTransition.reviewMode === 'git'
|
|
682
|
-
? transitioned.reviewRoundPending
|
|
683
|
-
: taskBeforeTransition.reviewRoundPending,
|
|
684
|
-
...(taskBeforeTransition.reviewMode === 'git'
|
|
685
|
-
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
686
|
-
: {}),
|
|
687
|
-
}, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
|
|
688
|
-
if (!rolledBack) {
|
|
689
|
-
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated arm-failure', ownAcquire);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
566
|
+
if (!begun?.task.reviewDispatch) {
|
|
567
|
+
const current = await manager.getTask(taskId);
|
|
568
|
+
if (current?.reviewDispatch?.phase === 'uncertain') {
|
|
569
|
+
const qaPhase = current.reviewDispatch.qaPhase;
|
|
570
|
+
await emitIntervention(bus, current.projectId, current.agentId, taskId, {
|
|
571
|
+
phase: 'git-review-dispatch-uncertain',
|
|
572
|
+
qaPhase,
|
|
573
|
+
headSha: reviewHead,
|
|
574
|
+
});
|
|
692
575
|
}
|
|
693
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire).catch(err => {
|
|
694
|
-
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after arm-failure rollback failed:`, err);
|
|
695
|
-
return false;
|
|
696
|
-
});
|
|
697
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
698
|
-
phase: previousStatus === 'approved' || previousStatus === 'merge-ready' ? 'qa-recheck-arm-failed-after-approved-push' : 'qa-recheck-arm-failed',
|
|
699
|
-
qaAgentId: qa.id,
|
|
700
|
-
qaPhase,
|
|
701
|
-
});
|
|
702
576
|
return;
|
|
703
577
|
}
|
|
704
|
-
|
|
705
|
-
let dispatchErr = null;
|
|
578
|
+
const qaPhase = begun.task.reviewDispatch.qaPhase;
|
|
706
579
|
try {
|
|
707
|
-
|
|
708
|
-
|
|
580
|
+
await manager.dispatchGitReviewLease(taskId, {
|
|
581
|
+
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
709
582
|
});
|
|
710
583
|
}
|
|
711
584
|
catch (err) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
console.warn(`[EventHandler] pr.updated QA ${qaPhase} not started; previousStatus=${previousStatus}`);
|
|
717
|
-
if (dispatchErr instanceof DispatchTerminalError) {
|
|
718
|
-
await manager.failTaskForDispatchError(transitioned.id, qaPhase, qa.id, dispatchErr);
|
|
719
|
-
}
|
|
720
|
-
else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.busyPending) {
|
|
721
|
-
// 遇忙不是故障:pass 保持 armed、QA 保持绑定,pending 登记已在 startSession 内完成,交对账补派
|
|
722
|
-
console.warn(`[EventHandler] pr.updated QA ${qaPhase} deferred for task=${transitioned.id}: QA pane busy; ` +
|
|
723
|
-
`reconciler will redispatch when idle`);
|
|
724
|
-
}
|
|
725
|
-
else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.handled) {
|
|
726
|
-
// handled 不止代表可恢复派发 hold(checkout-cleanup-failed / dialog 等也走这里),指引须按实际相位分流
|
|
727
|
-
const qaState = await manager.getAgentState(qa.id).catch((stateErr) => {
|
|
728
|
-
console.warn(`[EventHandler] pr.updated could not read held QA state for ${qa.id}:`, stateErr);
|
|
729
|
-
return null;
|
|
730
|
-
});
|
|
731
|
-
const heldPhase = qaState?.status === 'awaiting_human' ? qaState.awaitingPhase : undefined;
|
|
732
|
-
const note = isRecoverableQaDispatchHold(qaState)
|
|
733
|
-
? 'QA dispatch was held awaiting human; the task keeps the rotated review pass. Resume the QA agent or POST /tasks/:id/review to redispatch.'
|
|
734
|
-
: `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.`;
|
|
735
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
736
|
-
phase: 'qa-dispatch-held-awaiting-human',
|
|
737
|
-
qaAgentId: qa.id,
|
|
585
|
+
console.error(`[EventHandler] pr.updated git review dispatch failed for task=${taskId}:`, err);
|
|
586
|
+
if (err instanceof DispatchTerminalError && err.reason === 'ack_unknown') {
|
|
587
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, taskId, {
|
|
588
|
+
phase: 'git-review-dispatch-uncertain',
|
|
738
589
|
qaPhase,
|
|
739
|
-
|
|
740
|
-
note,
|
|
590
|
+
error: err.message,
|
|
741
591
|
});
|
|
742
592
|
}
|
|
743
|
-
else if (
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const rolledBack = await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
748
|
-
status: previousStatus,
|
|
749
|
-
signalToken: taskBeforeTransition.signalToken,
|
|
750
|
-
reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
|
|
751
|
-
reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
|
|
752
|
-
// git 保留 durable pending 即保留计轮 intent(同 arm-failure 站点)
|
|
753
|
-
reviewRoundPending: taskBeforeTransition.reviewMode === 'git'
|
|
754
|
-
? transitioned.reviewRoundPending
|
|
755
|
-
: taskBeforeTransition.reviewRoundPending,
|
|
756
|
-
...(taskBeforeTransition.reviewMode === 'git'
|
|
757
|
-
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
758
|
-
: {}),
|
|
759
|
-
}, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
|
|
760
|
-
if (rolledBack) {
|
|
761
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
|
|
762
|
-
.catch(err => {
|
|
763
|
-
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
|
|
764
|
-
return false;
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated recheck', ownAcquire);
|
|
769
|
-
}
|
|
593
|
+
else if (err instanceof DispatchTerminalError && begun.task.qaAgentId) {
|
|
594
|
+
await manager.failTaskForDispatchError(taskId, qaPhase, begun.task.qaAgentId, err, {
|
|
595
|
+
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
596
|
+
});
|
|
770
597
|
}
|
|
771
598
|
else {
|
|
772
|
-
await
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
599
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, taskId, {
|
|
600
|
+
phase: 'git-review-dispatch-failed',
|
|
601
|
+
qaPhase,
|
|
602
|
+
error: err instanceof Error ? err.message : String(err),
|
|
776
603
|
});
|
|
777
|
-
if (previousStatus === 'approved' || previousStatus === 'merge-ready') {
|
|
778
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
779
|
-
phase: 'qa-recheck-failed-after-approved-push',
|
|
780
|
-
qaAgentId: qa.id,
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
785
|
-
phase: 'qa-recheck-failed-after-stop',
|
|
786
|
-
qaAgentId: qa.id,
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
604
|
}
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
if (taskBeforeTransition.reviewMode === 'git') {
|
|
793
|
-
await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
|
|
794
|
-
}
|
|
795
|
-
// 送达确认后按本次 pass CAS 消费未计轮 intent:flag 本身已编码「哪些起点开新轮」
|
|
796
|
-
// (review→review 的既计轮 recheck 无 flag 即天然 no-op),successor 换代则留给它自己消费
|
|
797
|
-
await manager.consumeReviewRoundIntent(transitioned.id, dispatchToken);
|
|
798
|
-
const ok = devAlreadyWaiting || (await manager.markAgentWaiting(agentId, transitioned.id));
|
|
799
|
-
if (!ok) {
|
|
800
|
-
await manager.markAwaitingHuman(qa.id, 'dev-wait-gate-failed-after-qa-started', `QA review for task ${transitioned.id} started but dev wait-gate failed; QA prompt may still be running, needs operator decision.`, { expectedTaskId: transitioned.id }).catch(err => {
|
|
801
|
-
console.error(`[EventHandler] pr.updated markAwaitingHuman(QA=${qa.id}) after dev-wait-gate-fail:`, err);
|
|
802
|
-
});
|
|
803
605
|
}
|
|
804
606
|
}
|
|
805
607
|
async function redispatchReviewForStaleVerdict(manager, task) {
|
|
@@ -830,6 +632,7 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
830
632
|
await manager.dispatchReviewToQa(task.id, {
|
|
831
633
|
fromStatus: ['review'],
|
|
832
634
|
bumpRound: task.reviewRoundPending === true,
|
|
635
|
+
expectPhase: task.phase,
|
|
833
636
|
expectSignalToken: task.signalToken,
|
|
834
637
|
...(qaPhase !== undefined ? { qaPhase } : {}),
|
|
835
638
|
});
|
|
@@ -842,157 +645,80 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
842
645
|
return true;
|
|
843
646
|
}
|
|
844
647
|
async function handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch, gitVerdict) {
|
|
845
|
-
if (
|
|
846
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
847
|
-
phase: 'approval-reviewed-head-unavailable',
|
|
848
|
-
});
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
// 'git' 载荷已经 engine 端 fresh prView 复核(spec §7 handler 收紧):二次解析的缓存回退
|
|
852
|
-
// 链会重新打开 engine 已关闭的竞态,跳过
|
|
853
|
-
const anchor = task.reviewMode === 'git'
|
|
854
|
-
? { headSha: currentHeadSha ?? reviewedHeadSha, source: 'payload-self' }
|
|
855
|
-
: await resolveAuthoritativeHead(manager, task, { payloadCurrentHeadSha: currentHeadSha });
|
|
856
|
-
// git 的 head 更新并入下方带 expectSignalToken 的 transition:入口校验后并发 push 换代时,
|
|
857
|
-
// 旧 verdict 的裸 updateTask 会把 successor 的 latestHeadSha 覆写回旧值
|
|
858
|
-
if (anchor.source === 'fetch' && anchor.headSha && task.latestHeadSha !== anchor.headSha) {
|
|
859
|
-
await manager.updateTask(task.id, { latestHeadSha: anchor.headSha });
|
|
860
|
-
}
|
|
861
|
-
if (anchor.headSha && reviewedHeadSha !== anchor.headSha) {
|
|
648
|
+
if (reviewedHeadSha !== currentHeadSha) {
|
|
862
649
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
863
650
|
phase: 'stale-approval-head-mismatch',
|
|
864
651
|
reviewedHeadSha,
|
|
865
|
-
currentHeadSha
|
|
866
|
-
source:
|
|
867
|
-
...('fetchError' in anchor && anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
652
|
+
currentHeadSha,
|
|
653
|
+
source: 'payload-self',
|
|
868
654
|
});
|
|
869
655
|
return;
|
|
870
656
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
...(
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
...provenanceFields,
|
|
890
|
-
...gitHeadPatch(task, anchor.headSha),
|
|
891
|
-
// 崩溃在「prompt 已送达、bump 未落盘」窗口时 pending 轮次由 verdict 消费补计
|
|
892
|
-
reviewRound: Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0)),
|
|
893
|
-
reviewRoundPending: undefined,
|
|
894
|
-
// verdict 即送达证据:durable pending 随 transition 原子消费,封死「送达后 verdict 抢先、
|
|
895
|
-
// startSession 的 clear 因 status/token 已变而拒绝」留下 stale 凭据的竞态
|
|
896
|
-
reviewDispatchPending: undefined,
|
|
657
|
+
if (task.failToken === undefined || task.signalToken === undefined)
|
|
658
|
+
return;
|
|
659
|
+
const provenance = {
|
|
660
|
+
sourceKey: gitVerdict.carrier.sourceKey,
|
|
661
|
+
id: gitVerdict.carrier.id,
|
|
662
|
+
bodyDigest: gitVerdict.carrier.bodyDigest,
|
|
663
|
+
token: gitVerdict.verdictToken,
|
|
664
|
+
failToken: task.failToken,
|
|
665
|
+
anchorSha: reviewedHeadSha,
|
|
666
|
+
};
|
|
667
|
+
const reviewedRound = Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0));
|
|
668
|
+
const transitioned = await manager.approveGitReviewPass(task.id, {
|
|
669
|
+
expectedSignalToken: task.signalToken,
|
|
670
|
+
headSha: reviewedHeadSha,
|
|
671
|
+
reviewRound: reviewedRound,
|
|
672
|
+
...(prPatch.prNumber !== undefined ? { prNumber: prPatch.prNumber } : {}),
|
|
673
|
+
...(prPatch.prUrl !== undefined ? { prUrl: prPatch.prUrl } : {}),
|
|
674
|
+
provenance,
|
|
897
675
|
});
|
|
898
|
-
if (!
|
|
676
|
+
if (!transitioned)
|
|
899
677
|
return;
|
|
900
|
-
const { task: transitioned } = result;
|
|
901
678
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
902
679
|
if (transitioned.qaAgentId) {
|
|
903
680
|
await manager.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
904
681
|
.catch(err => console.error(`[EventHandler] APPROVE releaseAgentForTask(QA=${transitioned.qaAgentId}) failed:`, err));
|
|
905
682
|
}
|
|
906
|
-
await
|
|
683
|
+
await dispatchGitDevPostApproveCheck(bus, manager, transitioned, reviewedHeadSha);
|
|
907
684
|
}
|
|
908
685
|
function gitHeadPatch(task, headSha) {
|
|
909
|
-
return
|
|
686
|
+
return headSha !== undefined && task.latestHeadSha !== headSha
|
|
910
687
|
? { latestHeadSha: headSha }
|
|
911
688
|
: {};
|
|
912
689
|
}
|
|
913
690
|
async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch) {
|
|
914
|
-
|
|
915
|
-
? await manager.getPostApproveCompletion(task.id)
|
|
916
|
-
: null;
|
|
917
|
-
const anchor = task.reviewMode === 'git'
|
|
918
|
-
? { headSha: currentHeadSha ?? reviewedHeadSha, source: 'payload-self' }
|
|
919
|
-
: await resolveAuthoritativeHead(manager, task, {
|
|
920
|
-
payloadCurrentHeadSha: currentHeadSha,
|
|
921
|
-
legacyFallback: approvedCompletion?.approvedHeadSha,
|
|
922
|
-
});
|
|
923
|
-
if (anchor.source === 'fetch' && anchor.headSha && task.latestHeadSha !== anchor.headSha) {
|
|
924
|
-
await manager.updateTask(task.id, { latestHeadSha: anchor.headSha });
|
|
925
|
-
}
|
|
926
|
-
if (reviewedHeadSha && anchor.headSha && reviewedHeadSha !== anchor.headSha) {
|
|
691
|
+
if (reviewedHeadSha !== currentHeadSha) {
|
|
927
692
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
928
693
|
phase: 'stale-request-changes-head-mismatch',
|
|
929
694
|
reviewedHeadSha,
|
|
930
|
-
currentHeadSha
|
|
931
|
-
source:
|
|
932
|
-
...('fetchError' in anchor && anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
695
|
+
currentHeadSha,
|
|
696
|
+
source: 'payload-self',
|
|
933
697
|
});
|
|
934
698
|
return;
|
|
935
699
|
}
|
|
936
700
|
const reviewedRound = Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0));
|
|
937
701
|
const nextRound = reviewedRound + 1;
|
|
938
|
-
if (task.status === 'approved' && nextRound <= manager.getConfig().review.rounds) {
|
|
939
|
-
const devState = await manager.getAgentState(task.agentId);
|
|
940
|
-
const postApproveActive = await manager.getPostApproveCompletion(task.id);
|
|
941
|
-
if (devState?.taskId === task.id && postApproveActive) {
|
|
942
|
-
let revoked = await manager.revokePostApproveCompletion(task.id, 'request-changes', {
|
|
943
|
-
expectedToken: postApproveActive.token,
|
|
944
|
-
});
|
|
945
|
-
if (!revoked) {
|
|
946
|
-
// Token rotation within the episode still gets blocked; a successor approved episode (fresh head) must not.
|
|
947
|
-
revoked = await manager.revokePostApproveCompletion(task.id, 'request-changes', {
|
|
948
|
-
expectedHeadSha: postApproveActive.approvedHeadSha,
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
952
|
-
phase: 'request-changes-during-post-approve',
|
|
953
|
-
devAgentId: task.agentId,
|
|
954
|
-
note: revoked
|
|
955
|
-
? '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.'
|
|
956
|
-
: '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.',
|
|
957
|
-
});
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
const ready = await manager
|
|
961
|
-
.releaseAgentForTask(task.agentId, task.id, 'waiting')
|
|
962
|
-
.catch(err => {
|
|
963
|
-
console.error(`[EventHandler] REQUEST_CHANGES releaseAgentForTask(dev=${task.agentId}) before fix failed:`, err);
|
|
964
|
-
return false;
|
|
965
|
-
});
|
|
966
|
-
if (!ready) {
|
|
967
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
968
|
-
phase: 'post-approve-dev-wait-gate-failed-before-fix',
|
|
969
|
-
devAgentId: task.agentId,
|
|
970
|
-
});
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
702
|
if (nextRound > manager.getConfig().review.rounds) {
|
|
975
703
|
return handleMaxRounds(bus, manager, task, prPatch, reviewedRound);
|
|
976
704
|
}
|
|
977
705
|
const result = await manager.transitionTaskStatus(task.id, 'fixing', {
|
|
978
|
-
fromStatus: ['review'
|
|
979
|
-
|
|
980
|
-
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
706
|
+
fromStatus: ['review'],
|
|
707
|
+
expectSignalToken: task.signalToken,
|
|
981
708
|
}, {
|
|
982
709
|
...prPatch,
|
|
983
|
-
...gitHeadPatch(task,
|
|
710
|
+
...gitHeadPatch(task, currentHeadSha),
|
|
984
711
|
reviewRound: nextRound,
|
|
985
712
|
reviewRoundPending: undefined,
|
|
986
|
-
|
|
987
|
-
reviewDispatchPending: undefined,
|
|
713
|
+
reviewDispatch: undefined,
|
|
988
714
|
fixDispatchedAt: new Date().toISOString(),
|
|
989
715
|
});
|
|
990
716
|
if (!result)
|
|
991
717
|
return;
|
|
992
|
-
const { task: transitioned
|
|
718
|
+
const { task: transitioned } = result;
|
|
993
719
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
994
720
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
995
|
-
if (
|
|
721
|
+
if (transitioned.qaAgentId) {
|
|
996
722
|
const qaReleased = await manager
|
|
997
723
|
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
998
724
|
.catch(err => {
|
|
@@ -1014,7 +740,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
1014
740
|
});
|
|
1015
741
|
return;
|
|
1016
742
|
}
|
|
1017
|
-
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
|
|
743
|
+
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed', { expectedStatus: 'fixing', expectedToken: transitioned.signalToken });
|
|
1018
744
|
if (!armed) {
|
|
1019
745
|
console.warn(`[EventHandler] REQUEST_CHANGES pr-fixed watcher failed to arm for task=${transitioned.id}; holding dev (not dispatching fix)`);
|
|
1020
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 });
|
|
@@ -1069,9 +795,14 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
1069
795
|
}
|
|
1070
796
|
async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
1071
797
|
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
1072
|
-
fromStatus: ['review'
|
|
1073
|
-
|
|
1074
|
-
}, {
|
|
798
|
+
fromStatus: ['review'],
|
|
799
|
+
expectSignalToken: task.signalToken,
|
|
800
|
+
}, {
|
|
801
|
+
...prPatch,
|
|
802
|
+
reviewRound: reviewedRound,
|
|
803
|
+
reviewRoundPending: undefined,
|
|
804
|
+
reviewDispatch: undefined,
|
|
805
|
+
});
|
|
1075
806
|
if (!result)
|
|
1076
807
|
return;
|
|
1077
808
|
const { task: transitioned } = result;
|
|
@@ -1108,18 +839,31 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1108
839
|
if (!event.taskId || !event.agentId)
|
|
1109
840
|
return;
|
|
1110
841
|
const taskAtEntry = await manager.getTask(event.taskId);
|
|
1111
|
-
if (taskAtEntry
|
|
842
|
+
if (!taskAtEntry)
|
|
1112
843
|
return;
|
|
1113
|
-
if (taskAtEntry
|
|
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)) {
|
|
1114
863
|
console.warn(`[EventHandler] pr.created ignored for task ${event.taskId}: task in spec phase`);
|
|
1115
864
|
return;
|
|
1116
865
|
}
|
|
1117
|
-
|
|
1118
|
-
// 迟到 publish 信号只做 actor reconciliation(spec §5.3 ④):poller 已抢先收编、任务不在
|
|
1119
|
-
// 可收编态时,同 prNumber + reconciliation token 归位 replyActor,随后拆 dev 侧条目、不动 QA。
|
|
1120
|
-
// fixing 也归 reconciliation:同 prNumber 的迟到信号在 fix 轮重收编会把任务拽回 review、
|
|
1121
|
-
// 孤儿化 dev 的 fix 会话——非 in_progress 的同号信号只承载 actor 归位(spec §5.3 ④)
|
|
1122
|
-
if (isGit && taskAtEntry && event.data.source === 'pane-signal'
|
|
866
|
+
if (event.data.source === 'pane-signal'
|
|
1123
867
|
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
1124
868
|
&& taskAtEntry.status !== 'in_progress') {
|
|
1125
869
|
if (taskAtEntry.pendingPrSignalToken === undefined
|
|
@@ -1131,8 +875,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1131
875
|
? decodeSignalActorId(event.data.actorB64)
|
|
1132
876
|
: undefined;
|
|
1133
877
|
if (actorId === undefined) {
|
|
1134
|
-
// watcher 单发已被本事件消费:不重臂就永失 reconciliation 入口。skipSnapshot=true
|
|
1135
|
-
// 只等待后续正确的五段信号,避免滚动区里同一条三段残文立即重触发成环
|
|
1136
878
|
await manager.rearmGitReconciliationWatcher(taskAtEntry.id, { skipSnapshot: true });
|
|
1137
879
|
await emitIntervention(bus, taskAtEntry.projectId, event.agentId, taskAtEntry.id, {
|
|
1138
880
|
phase: 'git-pr-created-actor-missing',
|
|
@@ -1146,23 +888,20 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1146
888
|
pendingPrSignalToken: undefined,
|
|
1147
889
|
});
|
|
1148
890
|
}
|
|
1149
|
-
// fixing 态同键 watcher 已是 pr-fixed:拆掉会吞掉本轮完成信号,reconciliation 条目只存在于 review
|
|
1150
891
|
if (taskAtEntry.status === 'review') {
|
|
1151
892
|
manager.stopPhaseSignalWatcherAgent(taskAtEntry.id, event.agentId);
|
|
1152
893
|
}
|
|
1153
894
|
return;
|
|
1154
895
|
}
|
|
1155
|
-
if (
|
|
896
|
+
if (event.data.source !== 'pane-signal'
|
|
1156
897
|
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
1157
|
-
&& taskAtEntry.reviewDispatchPending !== true
|
|
1158
898
|
&& !['in_progress', 'fixing'].includes(taskAtEntry.status)) {
|
|
1159
|
-
// in_progress/fixing 或派发未完成(pending)的同号重放走完整收编重试(至少一次)
|
|
1160
899
|
return;
|
|
1161
900
|
}
|
|
1162
901
|
let paneVerifiedHeadSha;
|
|
1163
902
|
let reconciledBranch;
|
|
1164
|
-
let
|
|
1165
|
-
if (
|
|
903
|
+
let verifiedTargetBranch;
|
|
904
|
+
if (event.data.source === 'pane-signal' && event.data.prNumber !== undefined) {
|
|
1166
905
|
const prNumber = event.data.prNumber;
|
|
1167
906
|
try {
|
|
1168
907
|
let verify = await manager.platformVerifyPrBinding(event.taskId, prNumber);
|
|
@@ -1170,11 +909,11 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1170
909
|
const taskNow = await manager.getTask(event.taskId);
|
|
1171
910
|
const prBranch = verify.prBranch;
|
|
1172
911
|
if (taskNow) {
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
912
|
+
const ownBranch = BRANCH_PREFIX + event.taskId;
|
|
913
|
+
const foreignBxBranch = prBranch.startsWith(BRANCH_PREFIX) && prBranch !== ownBranch;
|
|
1175
914
|
const bound = await manager.findTaskByBranch(prBranch, taskNow.projectId);
|
|
1176
|
-
if (!
|
|
1177
|
-
&& (prBranch ===
|
|
915
|
+
if (!foreignBxBranch && isValidBranchName(prBranch)
|
|
916
|
+
&& (prBranch === ownBranch || bound?.id === taskNow.id)) {
|
|
1178
917
|
verify = await manager.platformVerifyPrBinding(event.taskId, prNumber, { branchOverride: prBranch });
|
|
1179
918
|
if (verify.ok)
|
|
1180
919
|
reconciledBranch = prBranch;
|
|
@@ -1196,13 +935,12 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1196
935
|
return;
|
|
1197
936
|
}
|
|
1198
937
|
paneVerifiedHeadSha = verify.headSha;
|
|
1199
|
-
|
|
938
|
+
verifiedTargetBranch = verify.targetBranch;
|
|
1200
939
|
}
|
|
1201
940
|
catch (err) {
|
|
1202
941
|
console.warn(`[EventHandler] pr.created: platformVerifyPrBinding failed for task ${event.taskId}:`, err);
|
|
1203
942
|
const taskNow = await manager.getTask(event.taskId);
|
|
1204
943
|
if (taskNow) {
|
|
1205
|
-
// skipSnapshot 缺省 true:滚动区里刚消费的信号残文在平台持续失败时会形成无退避热循环
|
|
1206
944
|
await reArmDevelopWatcher(manager, taskNow, event.agentId);
|
|
1207
945
|
await emitIntervention(bus, taskNow.projectId, event.agentId, event.taskId, {
|
|
1208
946
|
phase: 'pane-pr-created-verify-error',
|
|
@@ -1213,243 +951,127 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1213
951
|
return;
|
|
1214
952
|
}
|
|
1215
953
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
let verified = await manager.verifyPaneSignalPrNumber(event.taskId, prNumber);
|
|
1220
|
-
if (!verified) {
|
|
1221
|
-
const taskNow = await manager.getTask(event.taskId);
|
|
1222
|
-
if (taskNow) {
|
|
1223
|
-
const prInfo = await manager.fetchPrHeadRef(prNumber, taskNow.projectId);
|
|
1224
|
-
if (prInfo) {
|
|
1225
|
-
const ownPrefix = BRANCH_PREFIX + event.taskId;
|
|
1226
|
-
const isForeignBxBranch = prInfo.headRefName.startsWith(BRANCH_PREFIX)
|
|
1227
|
-
&& prInfo.headRefName !== ownPrefix;
|
|
1228
|
-
const bound = await manager.findTaskByBranch(prInfo.headRefName, taskNow.projectId);
|
|
1229
|
-
const isOwnBxBranch = prInfo.headRefName === ownPrefix;
|
|
1230
|
-
const boundToThisTask = !!bound && bound.id === taskNow.id;
|
|
1231
|
-
// Adopt only on deterministic ownership — our own bx/<taskId> namespace or a branch this
|
|
1232
|
-
// task already owns. A dev-chosen branch that is neither is rejected to intervention, not
|
|
1233
|
-
// adopted on an unverifiable claim (a mis-reported PR number must not rebind the task).
|
|
1234
|
-
if (!isForeignBxBranch && isValidBranchName(prInfo.headRefName)
|
|
1235
|
-
&& (isOwnBxBranch || boundToThisTask)) {
|
|
1236
|
-
reconciledBranch = prInfo.headRefName;
|
|
1237
|
-
verified = prInfo;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
if (!verified) {
|
|
1243
|
-
const taskNow = await manager.getTask(event.taskId);
|
|
1244
|
-
console.warn(`[EventHandler] pr.created REJECT pane prNumber=${event.data.prNumber} for task ${event.taskId} ` +
|
|
1245
|
-
`(branch mismatch: task.branch=${taskNow?.branch})`);
|
|
1246
|
-
if (taskNow) {
|
|
1247
|
-
await reArmDevelopWatcher(manager, taskNow, event.agentId);
|
|
1248
|
-
await emitIntervention(bus, taskNow.projectId, event.agentId, event.taskId, {
|
|
1249
|
-
phase: 'pane-pr-created-branch-mismatch',
|
|
1250
|
-
claimedPrNumber: event.data.prNumber,
|
|
1251
|
-
taskBranch: taskNow.branch ?? '',
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
paneVerifiedHeadSha = verified.headSha;
|
|
1257
|
-
}
|
|
1258
|
-
catch (err) {
|
|
1259
|
-
console.warn(`[EventHandler] pr.created: verifyPaneSignalPrNumber failed for task ${event.taskId}:`, err);
|
|
1260
|
-
const taskNow = await manager.getTask(event.taskId);
|
|
1261
|
-
if (taskNow) {
|
|
1262
|
-
await reArmDevelopWatcher(manager, taskNow, event.agentId, { skipSnapshot: false });
|
|
1263
|
-
await emitIntervention(bus, taskNow.projectId, event.agentId, event.taskId, {
|
|
1264
|
-
phase: 'pane-pr-created-verify-error',
|
|
1265
|
-
claimedPrNumber: event.data.prNumber,
|
|
1266
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
const createdHeadSha = validHeadSha(event.data.headSha) ?? paneVerifiedHeadSha;
|
|
1273
|
-
const taskBeforeTransition = await manager.getTask(event.taskId);
|
|
1274
|
-
// A poller-sourced pr.created adopts by branch match alone (no pane verify). bx/<taskId> is unique
|
|
1275
|
-
// to this task; a custom branch name can be reused, so for a custom-branch task only accept a PR it
|
|
1276
|
-
// already tracks — a new custom-branch PR must arrive via the deterministic pane signal, not by the
|
|
1277
|
-
// poller matching a stale/foreign open PR that happens to share the branch name.
|
|
954
|
+
const task = await manager.getTask(event.taskId);
|
|
955
|
+
if (!task || task.reviewMode !== 'git')
|
|
956
|
+
return;
|
|
1278
957
|
if (event.data.source !== 'pane-signal'
|
|
1279
|
-
&&
|
|
1280
|
-
&&
|
|
1281
|
-
&& taskBeforeTransition.prNumber !== event.data.prNumber) {
|
|
958
|
+
&& task.branch !== BRANCH_PREFIX + event.taskId
|
|
959
|
+
&& task.prNumber !== event.data.prNumber) {
|
|
1282
960
|
console.warn(`[EventHandler] pr.created: ignoring poller-sourced PR ${event.data.prNumber} on custom branch ` +
|
|
1283
961
|
`for task ${event.taskId} (untracked; custom-branch PRs adopt via the pane signal)`);
|
|
1284
962
|
return;
|
|
1285
963
|
}
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
gitAdoptionFields.replyActorId = actorId;
|
|
1301
|
-
gitAdoptionFields.replyActorStatus = 'verified';
|
|
1302
|
-
gitAdoptionFields.pendingPrSignalToken = undefined;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
else if (typeof event.data.prAuthorId === 'string'
|
|
1306
|
-
&& taskBeforeTransition?.replyActorStatus !== 'verified') {
|
|
1307
|
-
// 自动收编只落 provisional:PR 作者可能是对已推送 head 抢建 PR 的第三方(spec §5.3 ④)
|
|
1308
|
-
gitAdoptionFields.replyActorId = event.data.prAuthorId;
|
|
1309
|
-
gitAdoptionFields.replyActorStatus = 'provisional';
|
|
964
|
+
const adoption = {};
|
|
965
|
+
const target = event.data.source === 'pane-signal'
|
|
966
|
+
? verifiedTargetBranch
|
|
967
|
+
: (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;
|
|
1310
978
|
}
|
|
1311
979
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
980
|
+
else if (typeof event.data.prAuthorId === 'string' && task.replyActorStatus !== 'verified') {
|
|
981
|
+
adoption.replyActorId = event.data.prAuthorId;
|
|
982
|
+
adoption.replyActorStatus = 'provisional';
|
|
983
|
+
}
|
|
984
|
+
let reviewHead = validHeadSha(event.data.headSha) ?? paneVerifiedHeadSha;
|
|
985
|
+
if (reviewHead === undefined && typeof event.data.prNumber === 'number') {
|
|
986
|
+
const verified = await manager.platformVerifyPrBinding(event.taskId, event.data.prNumber);
|
|
987
|
+
if (verified.ok)
|
|
988
|
+
reviewHead = validHeadSha(verified.headSha);
|
|
989
|
+
}
|
|
990
|
+
if (reviewHead === undefined) {
|
|
991
|
+
await emitIntervention(bus, task.projectId, task.agentId, event.taskId, {
|
|
992
|
+
phase: 'git-review-head-unavailable',
|
|
993
|
+
});
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (task.status !== 'in_progress' && task.status !== 'fixing')
|
|
997
|
+
return;
|
|
998
|
+
const begun = await manager.beginGitReviewPass(event.taskId, {
|
|
999
|
+
fromStatus: [task.status],
|
|
1000
|
+
headSha: reviewHead,
|
|
1001
|
+
bumpRound: task.status === 'in_progress',
|
|
1002
|
+
qaPhase: 'review',
|
|
1003
|
+
expectPhase: task.phase,
|
|
1004
|
+
expectSignalToken: task.signalToken,
|
|
1005
|
+
patch: {
|
|
1006
|
+
...(event.data.prNumber !== undefined ? { prNumber: event.data.prNumber } : {}),
|
|
1007
|
+
...(event.data.prUrl !== undefined ? { prUrl: event.data.prUrl } : {}),
|
|
1008
|
+
latestHeadSha: reviewHead,
|
|
1009
|
+
...(reconciledBranch ? { branch: reconciledBranch } : {}),
|
|
1010
|
+
...adoption,
|
|
1011
|
+
},
|
|
1322
1012
|
});
|
|
1323
|
-
if (!
|
|
1324
|
-
console.warn(`[EventHandler] pr.created: cannot
|
|
1013
|
+
if (!begun?.task.reviewDispatch) {
|
|
1014
|
+
console.warn(`[EventHandler] pr.created: cannot begin git review pass for task ${event.taskId}`);
|
|
1325
1015
|
if (event.data.source === 'pane-signal') {
|
|
1326
|
-
const
|
|
1327
|
-
if (
|
|
1328
|
-
await reArmDevelopWatcher(manager,
|
|
1329
|
-
await emitIntervention(bus,
|
|
1016
|
+
const current = await manager.getTask(event.taskId);
|
|
1017
|
+
if (current && ['in_progress', 'fixing'].includes(current.status)) {
|
|
1018
|
+
await reArmDevelopWatcher(manager, current, event.agentId);
|
|
1019
|
+
await emitIntervention(bus, current.projectId, event.agentId, event.taskId, {
|
|
1330
1020
|
phase: 'pane-pr-created-transition-failed',
|
|
1331
1021
|
claimedPrNumber: event.data.prNumber,
|
|
1332
|
-
taskBranch:
|
|
1022
|
+
taskBranch: current.branch ?? '',
|
|
1333
1023
|
});
|
|
1334
1024
|
}
|
|
1335
1025
|
}
|
|
1336
1026
|
return;
|
|
1337
1027
|
}
|
|
1338
|
-
const { task: transitioned, previousStatus } = result;
|
|
1339
1028
|
if (reconciledBranch) {
|
|
1340
|
-
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: `
|
|
1341
|
-
`${taskBeforeTransition?.branch} → ${reconciledBranch}`);
|
|
1029
|
+
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: ${task.branch} → ${reconciledBranch}`);
|
|
1342
1030
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1031
|
+
if (!begun.task.qaAgentId) {
|
|
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, {
|
|
1348
1035
|
phase: 'dev-wait-gate-failed-no-qa',
|
|
1349
1036
|
});
|
|
1350
1037
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const qa = manager.getAgentConfig(qaId);
|
|
1354
|
-
if (!qa || qa.role !== 'qa') {
|
|
1355
|
-
await manager.markAgentWaiting(event.agentId, transitioned.id).catch(() => false);
|
|
1356
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1357
|
-
phase: 'qa-config-missing',
|
|
1358
|
-
qaAgentId: qaId,
|
|
1359
|
-
});
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
let ownLockToken;
|
|
1363
|
-
const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, 'review', {
|
|
1364
|
-
onAcquired: (lockToken) => { ownLockToken = lockToken; },
|
|
1365
|
-
});
|
|
1366
|
-
if (!acquired) {
|
|
1367
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1368
|
-
phase: 'qa-acquire-failed',
|
|
1369
|
-
qaAgentId: qa.id,
|
|
1370
|
-
});
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
const ownAcquire = ownLockToken !== undefined ? { expectedLockToken: ownLockToken } : {};
|
|
1374
|
-
const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
|
|
1375
|
-
if (!armed) {
|
|
1376
|
-
console.warn(`[EventHandler] pr.created verdict watcher failed to arm for task=${transitioned.id}; rolling back review dispatch`);
|
|
1377
|
-
const rolledBack = await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
1378
|
-
status: previousStatus,
|
|
1379
|
-
signalToken: taskBeforeTransition?.signalToken,
|
|
1380
|
-
reviewHeadAnchorSha: taskBeforeTransition?.reviewHeadAnchorSha,
|
|
1381
|
-
reviewDispatchedAt: taskBeforeTransition?.reviewDispatchedAt,
|
|
1382
|
-
// git 保留 durable pending 即保留计轮 intent(同 pr.updated 回滚站点)
|
|
1383
|
-
reviewRoundPending: taskBeforeTransition?.reviewMode === 'git'
|
|
1384
|
-
? transitioned.reviewRoundPending
|
|
1385
|
-
: taskBeforeTransition?.reviewRoundPending,
|
|
1386
|
-
...(taskBeforeTransition?.reviewMode === 'git'
|
|
1387
|
-
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
1388
|
-
: {}),
|
|
1389
|
-
}, { expect: { status: 'review', signalToken: dispatchToken } });
|
|
1390
|
-
if (!rolledBack) {
|
|
1391
|
-
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.created arm-failure', ownAcquire);
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire).catch(err => {
|
|
1395
|
-
console.error(`[EventHandler] pr.created releaseAgentForTask(QA=${qa.id}) after arm-failure rollback failed:`, err);
|
|
1396
|
-
return false;
|
|
1397
|
-
});
|
|
1398
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1399
|
-
phase: 'qa-review-arm-failed',
|
|
1400
|
-
qaAgentId: qa.id,
|
|
1038
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1039
|
+
phase: 'git-review-no-qa-partner',
|
|
1401
1040
|
});
|
|
1402
1041
|
return;
|
|
1403
1042
|
}
|
|
1404
|
-
let started = false;
|
|
1405
|
-
let dispatchErr = null;
|
|
1406
1043
|
try {
|
|
1407
|
-
|
|
1408
|
-
|
|
1044
|
+
await manager.dispatchGitReviewLease(begun.task.id, {
|
|
1045
|
+
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
1409
1046
|
});
|
|
1410
1047
|
}
|
|
1411
1048
|
catch (err) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
await manager.failTaskForDispatchError(transitioned.id, 'review', qa.id, dispatchErr);
|
|
1049
|
+
console.error(`[EventHandler] pr.created git review dispatch failed for task=${begun.task.id}:`, err);
|
|
1050
|
+
if (err instanceof DispatchTerminalError && err.reason === 'ack_unknown') {
|
|
1051
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1052
|
+
phase: 'git-review-dispatch-uncertain',
|
|
1053
|
+
qaPhase: 'review',
|
|
1054
|
+
error: err.message,
|
|
1055
|
+
});
|
|
1420
1056
|
}
|
|
1421
|
-
else if (
|
|
1057
|
+
else if (err instanceof DispatchTerminalError) {
|
|
1058
|
+
await manager.failTaskForDispatchError(begun.task.id, 'review', begun.task.qaAgentId, err, {
|
|
1059
|
+
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
1060
|
+
});
|
|
1422
1061
|
}
|
|
1423
1062
|
else {
|
|
1424
|
-
await
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
});
|
|
1429
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1430
|
-
phase: 'qa-review-start-failed',
|
|
1431
|
-
qaAgentId: qa.id,
|
|
1063
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1064
|
+
phase: 'git-review-dispatch-failed',
|
|
1065
|
+
qaPhase: 'review',
|
|
1066
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1432
1067
|
});
|
|
1433
1068
|
}
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
if (taskBeforeTransition?.reviewMode === 'git') {
|
|
1437
|
-
await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
|
|
1438
|
-
}
|
|
1439
|
-
await manager.consumeReviewRoundIntent(transitioned.id, dispatchToken);
|
|
1440
|
-
const ok = await manager.markAgentWaiting(event.agentId, transitioned.id);
|
|
1441
|
-
if (!ok) {
|
|
1442
|
-
await manager.markAwaitingHuman(qa.id, 'dev-wait-gate-failed-after-qa-started', `QA review for task ${transitioned.id} started but dev wait-gate failed; QA prompt may still be running, needs operator decision.`, { expectedTaskId: transitioned.id }).catch(err => {
|
|
1443
|
-
console.error(`[EventHandler] pr.created markAwaitingHuman(QA=${qa.id}) after dev-wait-gate-fail:`, err);
|
|
1444
|
-
});
|
|
1445
1069
|
}
|
|
1446
1070
|
});
|
|
1447
1071
|
bus.on('pr.updated', async (event) => {
|
|
1448
|
-
if (!event.taskId
|
|
1072
|
+
if (!event.taskId)
|
|
1449
1073
|
return;
|
|
1450
1074
|
const taskAtEntry = await manager.getTask(event.taskId);
|
|
1451
|
-
if (taskAtEntry?.reviewMode === 'server')
|
|
1452
|
-
return;
|
|
1453
1075
|
const eventKind = event.data.kind;
|
|
1454
1076
|
if (taskAtEntry && isSpecStagePhase(taskAtEntry.phase)) {
|
|
1455
1077
|
console.warn(`[EventHandler] pr.updated (kind=${eventKind ?? 'push'}) ignored for task ${event.taskId}: task in spec phase`);
|
|
@@ -1457,7 +1079,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1457
1079
|
}
|
|
1458
1080
|
if (eventKind === 'pr-merge-ready')
|
|
1459
1081
|
return handlePrMergeReady(bus, manager, event);
|
|
1460
|
-
// closed-unmerged/reopened 只由 PlatformPoller 对 'git' 任务合成(spec §6 outbox 协议)
|
|
1461
1082
|
if (eventKind === 'closed-unmerged' || eventKind === 'reopened') {
|
|
1462
1083
|
const prNumber = event.data.prNumber;
|
|
1463
1084
|
if (typeof prNumber !== 'number')
|
|
@@ -1466,13 +1087,18 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1466
1087
|
await manager.recordClosedUnmergedAnchor(event.taskId, prNumber);
|
|
1467
1088
|
else
|
|
1468
1089
|
await manager.clearClosedUnmergedAnchor(event.taskId, prNumber);
|
|
1469
|
-
// 无条件投递:滞留的 outbox 条目借重复观察重试,而不是等到重启(spec §6 至少一次)
|
|
1470
1090
|
await manager.deliverTaskOutbox(event.taskId);
|
|
1471
1091
|
return;
|
|
1472
1092
|
}
|
|
1473
|
-
if (
|
|
1093
|
+
if (!event.agentId)
|
|
1094
|
+
return;
|
|
1095
|
+
if (taskAtEntry?.reviewMode === 'server')
|
|
1096
|
+
return;
|
|
1097
|
+
if (eventKind === 'comment' || eventKind === 'review-comment') {
|
|
1474
1098
|
return handlePrFeedback(bus, manager, event);
|
|
1475
|
-
|
|
1099
|
+
}
|
|
1100
|
+
if (eventKind === 'push' || eventKind === undefined)
|
|
1101
|
+
return handlePrCodePush(bus, manager, event);
|
|
1476
1102
|
});
|
|
1477
1103
|
bus.on('pr.merged', async (event) => {
|
|
1478
1104
|
if (!event.taskId)
|
|
@@ -1526,36 +1152,55 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1526
1152
|
bus.on('review.submitted', async (event) => {
|
|
1527
1153
|
if (!event.taskId)
|
|
1528
1154
|
return;
|
|
1529
|
-
const
|
|
1530
|
-
let task = await manager.getTask(event.taskId);
|
|
1155
|
+
const task = await manager.getTask(event.taskId);
|
|
1531
1156
|
if (!task)
|
|
1532
1157
|
return;
|
|
1533
1158
|
if (task.reviewMode === 'server')
|
|
1534
1159
|
return;
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
console.log(`[EventHandler] review.submitted pane verdict ignored for git task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1160
|
+
if (event.data.source === 'pane-signal') {
|
|
1161
|
+
console.log(`[EventHandler] review.submitted pane verdict ignored for task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1538
1162
|
return;
|
|
1539
1163
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1164
|
+
const verdictTask = task;
|
|
1165
|
+
const rejectGitPayload = async (err) => {
|
|
1166
|
+
await emitIntervention(bus, verdictTask.projectId, verdictTask.agentId, verdictTask.id, {
|
|
1167
|
+
phase: 'git-verdict-payload-invalid',
|
|
1168
|
+
error: err.message,
|
|
1169
|
+
...(typeof event.data.source === 'string' ? { source: event.data.source } : {}),
|
|
1170
|
+
});
|
|
1171
|
+
};
|
|
1172
|
+
let gitPayload;
|
|
1173
|
+
try {
|
|
1174
|
+
gitPayload = parseGitVerdictPayload(event.data);
|
|
1175
|
+
}
|
|
1176
|
+
catch (err) {
|
|
1177
|
+
if (!(err instanceof GitVerdictPayloadError))
|
|
1178
|
+
throw err;
|
|
1179
|
+
await rejectGitPayload(err);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const action = gitPayload.action;
|
|
1183
|
+
if (task.signalToken === undefined || gitPayload.reviewPassToken !== task.signalToken) {
|
|
1184
|
+
if (task.status !== 'review') {
|
|
1185
|
+
await rejectGitPayload(new GitVerdictPayloadError('review pass token is not current'));
|
|
1186
|
+
return;
|
|
1558
1187
|
}
|
|
1188
|
+
if (!(await redispatchReviewForStaleVerdict(manager, task))) {
|
|
1189
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1190
|
+
phase: 'stale-verdict-wrong-pass',
|
|
1191
|
+
action,
|
|
1192
|
+
verdictPassToken: gitPayload.reviewPassToken,
|
|
1193
|
+
currentToken: task.signalToken,
|
|
1194
|
+
source: 'poller',
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const expectedVerdictToken = action === 'APPROVE' ? task.passToken : task.failToken;
|
|
1200
|
+
if (expectedVerdictToken === undefined || gitPayload.verdictToken !== expectedVerdictToken
|
|
1201
|
+
|| task.failToken === undefined) {
|
|
1202
|
+
await rejectGitPayload(new GitVerdictPayloadError('verdict token is not current'));
|
|
1203
|
+
return;
|
|
1559
1204
|
}
|
|
1560
1205
|
if (isSpecStagePhase(task.phase)) {
|
|
1561
1206
|
console.warn(`[EventHandler] review.submitted (action=${action}) ignored for task ${task.id}: task in spec phase`);
|
|
@@ -1578,72 +1223,18 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1578
1223
|
return;
|
|
1579
1224
|
}
|
|
1580
1225
|
}
|
|
1581
|
-
const verdictPassToken = (typeof event.data.reviewPassToken === 'string' ? event.data.reviewPassToken : undefined)
|
|
1582
|
-
?? (typeof event.data.token === 'string' ? event.data.token : undefined);
|
|
1583
|
-
if (verdictPassToken && task.signalToken && verdictPassToken !== task.signalToken) {
|
|
1584
|
-
if (!(await redispatchReviewForStaleVerdict(manager, task))) {
|
|
1585
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1586
|
-
phase: 'stale-verdict-wrong-pass',
|
|
1587
|
-
action,
|
|
1588
|
-
verdictPassToken,
|
|
1589
|
-
currentToken: task.signalToken,
|
|
1590
|
-
source: event.data.source === 'pane-signal' ? 'pane-signal' : 'poller',
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
const eventPrNumber = event.data.prNumber;
|
|
1596
|
-
const eventPrUrl = event.data.prUrl;
|
|
1597
1226
|
const prPatch = {
|
|
1598
|
-
...(
|
|
1599
|
-
...(
|
|
1227
|
+
...(gitPayload.prNumber !== task.prNumber ? { prNumber: gitPayload.prNumber } : {}),
|
|
1228
|
+
...(gitPayload.prUrl !== task.prUrl ? { prUrl: gitPayload.prUrl } : {}),
|
|
1600
1229
|
};
|
|
1601
|
-
const
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
if ((task.status === 'in_progress' || task.status === 'fixing') && eventPrNumber !== undefined) {
|
|
1607
|
-
const catchup = await manager.transitionTaskStatus(event.taskId, 'review', { fromStatus: ['in_progress', 'fixing'] }, prPatch);
|
|
1608
|
-
if (catchup) {
|
|
1609
|
-
task = catchup.task;
|
|
1610
|
-
if (task.agentId) {
|
|
1611
|
-
const ok = await manager.markAgentWaiting(task.agentId, task.id);
|
|
1612
|
-
if (!ok) {
|
|
1613
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1614
|
-
phase: 'dev-wait-gate-failed-late-catchup',
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
else {
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
const carrier = event.data.verdictCarrier;
|
|
1624
|
-
const gitVerdict = task.reviewMode === 'git'
|
|
1625
|
-
&& typeof event.data.verdictToken === 'string' && carrier !== undefined
|
|
1626
|
-
? { token: event.data.verdictToken, carrier }
|
|
1627
|
-
: undefined;
|
|
1628
|
-
// 'git' 的裁决只信任 engine 载荷:reviewPassToken(评审轮绑定)与 verdict 令牌(评论对绑定)
|
|
1629
|
-
// 都必须在场且属当前轮——缺失即绕过 signalToken 门的 malformed 事件,fail closed
|
|
1630
|
-
if (task.reviewMode === 'git' && (action === 'APPROVE' || action === 'REQUEST_CHANGES')) {
|
|
1631
|
-
const expectedToken = action === 'APPROVE' ? task.passToken : task.failToken;
|
|
1632
|
-
const roundToken = typeof event.data.reviewPassToken === 'string' ? event.data.reviewPassToken : undefined;
|
|
1633
|
-
if (gitVerdict === undefined || expectedToken === undefined || task.failToken === undefined
|
|
1634
|
-
|| gitVerdict.token !== expectedToken
|
|
1635
|
-
|| roundToken === undefined || task.signalToken === undefined || roundToken !== task.signalToken) {
|
|
1636
|
-
console.warn(`[EventHandler] review.submitted ${action} rejected for git task ${task.id}: verdict payload missing or not the current round token`);
|
|
1637
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1638
|
-
phase: 'git-verdict-payload-invalid',
|
|
1639
|
-
action,
|
|
1640
|
-
hasCarrier: gitVerdict !== undefined,
|
|
1641
|
-
});
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1230
|
+
const reviewedHeadSha = gitPayload.headSha;
|
|
1231
|
+
const currentHeadSha = gitPayload.currentHeadSha;
|
|
1232
|
+
if (task.status !== 'review') {
|
|
1233
|
+
await rejectGitPayload(new GitVerdictPayloadError(`task is ${task.status}, not review`));
|
|
1234
|
+
return;
|
|
1644
1235
|
}
|
|
1645
1236
|
if (action === 'APPROVE') {
|
|
1646
|
-
return handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch,
|
|
1237
|
+
return handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch, gitPayload);
|
|
1647
1238
|
}
|
|
1648
1239
|
if (action === 'REQUEST_CHANGES') {
|
|
1649
1240
|
return handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch);
|
|
@@ -1658,14 +1249,12 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1658
1249
|
const { projectId, agentId } = task;
|
|
1659
1250
|
const stayFixing = async (data) => {
|
|
1660
1251
|
await emitIntervention(bus, projectId, agentId, task.id, data);
|
|
1661
|
-
// 同代 fence:平台查询等待期间任务可能已被手动重派进 review——旧失败分支不得覆盖
|
|
1662
|
-
// successor 的 QA/reconciliation watcher
|
|
1663
1252
|
const fresh = await manager.getTask(task.id);
|
|
1664
1253
|
if (!fresh || fresh.status !== 'fixing' || fresh.signalToken !== task.signalToken)
|
|
1665
1254
|
return;
|
|
1666
1255
|
await manager.setupPhaseSignal(task.id, agentId, 'pr-fixed', {
|
|
1667
1256
|
skipSnapshot: true,
|
|
1668
|
-
|
|
1257
|
+
replaceScope: 'agent',
|
|
1669
1258
|
});
|
|
1670
1259
|
};
|
|
1671
1260
|
const token = typeof event.data.token === 'string' ? event.data.token : undefined;
|
|
@@ -1677,92 +1266,52 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1677
1266
|
});
|
|
1678
1267
|
return;
|
|
1679
1268
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1269
|
+
if (task.prNumber === undefined) {
|
|
1270
|
+
await stayFixing({ phase: 'fix-verify-no-anchor' });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
let verify;
|
|
1274
|
+
try {
|
|
1275
|
+
verify = await manager.platformVerifyPrBinding(task.id, task.prNumber);
|
|
1276
|
+
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
await stayFixing({
|
|
1279
|
+
phase: 'fix-verify-head-fetch-failed',
|
|
1280
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1281
|
+
});
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (!verify.ok) {
|
|
1285
|
+
// PR 在修复期间被关闭/转 draft/retarget:推进 recheck 只会造出无裁决入口的评审轮
|
|
1286
|
+
await stayFixing({ phase: 'fix-verify-binding-mismatch', reason: verify.reason });
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const headSha = verify.headSha;
|
|
1290
|
+
if (!task.reviewHeadAnchorSha) {
|
|
1291
|
+
await stayFixing({ phase: 'fix-verify-no-anchor', headSha });
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (headSha === task.reviewHeadAnchorSha) {
|
|
1295
|
+
let pendingCount;
|
|
1687
1296
|
try {
|
|
1688
|
-
|
|
1297
|
+
pendingCount = (await manager.platformPendingFeedback(task.id)).pending.size;
|
|
1689
1298
|
}
|
|
1690
1299
|
catch (err) {
|
|
1691
1300
|
await stayFixing({
|
|
1692
|
-
phase: 'fix-verify-
|
|
1301
|
+
phase: 'fix-verify-replies-fetch-failed',
|
|
1693
1302
|
error: err instanceof Error ? err.message : String(err),
|
|
1694
1303
|
});
|
|
1695
1304
|
return;
|
|
1696
1305
|
}
|
|
1697
|
-
if (
|
|
1698
|
-
// PR 在修复期间被关闭/转 draft/retarget:推进 recheck 只会造出无裁决入口的评审轮
|
|
1699
|
-
await stayFixing({ phase: 'fix-verify-binding-mismatch', reason: verify.reason });
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
headSha = verify.headSha;
|
|
1703
|
-
}
|
|
1704
|
-
else {
|
|
1705
|
-
try {
|
|
1706
|
-
headSha = await fetchVerifiedHeadSha(manager, task.id);
|
|
1707
|
-
}
|
|
1708
|
-
catch (err) {
|
|
1306
|
+
if (pendingCount > 0) {
|
|
1709
1307
|
await stayFixing({
|
|
1710
|
-
phase: 'fix-
|
|
1711
|
-
|
|
1308
|
+
phase: 'fix-no-op-pending-feedback',
|
|
1309
|
+
pendingCount,
|
|
1310
|
+
note: 'Dev emitted pr-fixed on an unchanged head while feedback revisions still lack a valid ack. Inspect the PR replies.',
|
|
1712
1311
|
});
|
|
1713
1312
|
return;
|
|
1714
1313
|
}
|
|
1715
1314
|
}
|
|
1716
|
-
if (!task.reviewHeadAnchorSha) {
|
|
1717
|
-
await stayFixing({ phase: 'fix-verify-no-anchor', headSha });
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
if (headSha === task.reviewHeadAnchorSha) {
|
|
1721
|
-
if (task.reviewMode === 'git') {
|
|
1722
|
-
// no-op fix 通过条件 = 无未 ack 的待处理反馈 revision 且全源拉取成功(spec §6/§8);
|
|
1723
|
-
// 墙钟比较与「任意评论计回复」在 'git' 均废除
|
|
1724
|
-
let pendingCount;
|
|
1725
|
-
try {
|
|
1726
|
-
pendingCount = (await manager.platformPendingFeedback(task.id)).pending.size;
|
|
1727
|
-
}
|
|
1728
|
-
catch (err) {
|
|
1729
|
-
await stayFixing({
|
|
1730
|
-
phase: 'fix-verify-replies-fetch-failed',
|
|
1731
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1732
|
-
});
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
if (pendingCount > 0) {
|
|
1736
|
-
await stayFixing({
|
|
1737
|
-
phase: 'fix-no-op-pending-feedback',
|
|
1738
|
-
pendingCount,
|
|
1739
|
-
note: 'Dev emitted pr-fixed on an unchanged head while feedback revisions still lack a valid ack. Inspect the PR replies.',
|
|
1740
|
-
});
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
else {
|
|
1745
|
-
const since = task.fixDispatchedAt ?? task.reviewDispatchedAt;
|
|
1746
|
-
let hasReplies;
|
|
1747
|
-
try {
|
|
1748
|
-
hasReplies = since ? await manager.prHasDevReplySince(task.id, since) : false;
|
|
1749
|
-
}
|
|
1750
|
-
catch (err) {
|
|
1751
|
-
await stayFixing({
|
|
1752
|
-
phase: 'fix-verify-replies-fetch-failed',
|
|
1753
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1754
|
-
});
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
if (!hasReplies) {
|
|
1758
|
-
await stayFixing({
|
|
1759
|
-
phase: 'fix-no-op-no-commit-no-reply',
|
|
1760
|
-
note: 'Dev emitted pr-fixed but pushed no commit and left no reply on the PR — the fixing round changed nothing. Inspect the pane.',
|
|
1761
|
-
});
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
1315
|
// head fetch / 回复检查是秒级网络等待,期间真 push / verdict 可能已换代 pass;旧信号不得再驱动派发
|
|
1767
1316
|
const preEmit = await manager.getTask(task.id);
|
|
1768
1317
|
if (!preEmit || preEmit.status !== 'fixing' || preEmit.signalToken !== token)
|