baxian 1.2.61 → 2.0.1
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 -3
- 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 +15 -49
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +212 -90
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +2983 -1574
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts +3 -0
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +6 -0
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/pane-streamer-manager.d.ts +6 -6
- package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +2 -2
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +46 -36
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +20 -2
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +135 -17
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +9 -7
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +18 -9
- 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 -5
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +34 -30
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +8 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +70 -15
- 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/agent/root-agent-runtime.d.ts +80 -0
- package/dist/agent/root-agent-runtime.d.ts.map +1 -0
- package/dist/agent/root-agent-runtime.js +635 -0
- package/dist/agent/root-agent-runtime.js.map +1 -0
- package/dist/agent/root-recovery-coordinator.d.ts +125 -0
- package/dist/agent/root-recovery-coordinator.d.ts.map +1 -0
- package/dist/agent/root-recovery-coordinator.js +1579 -0
- package/dist/agent/root-recovery-coordinator.js.map +1 -0
- package/dist/agent/runner.d.ts +1 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +103 -7
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts +1 -0
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +16 -10
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +3 -0
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +11 -5
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +42 -1
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +88 -17
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +71 -15
- 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 +57 -16
- 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 +6 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -7
- package/dist/cli.js.map +1 -1
- package/dist/config/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.d.ts.map +1 -1
- package/dist/config/loader.js +23 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/normalizer.d.ts.map +1 -1
- package/dist/config/normalizer.js +18 -0
- package/dist/config/normalizer.js.map +1 -1
- package/dist/config/validator.d.ts +3 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +247 -54
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +615 -1069
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +787 -487
- 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 +99 -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 +283 -38
- 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/constants.d.ts +7 -1
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +5 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +98 -7
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +18 -0
- package/dist/shared/types.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +5 -15
- package/dist/skills/baxian-pr-recheck/SKILL.md +5 -44
- package/dist/skills/baxian-pr-review/SKILL.md +6 -43
- package/dist/skills/baxian-research/SKILL.md +1 -1
- package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
- package/dist/skills/baxian-server-review/SKILL.md +2 -2
- package/dist/skills/baxian-signals/SKILL.md +11 -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/init.d.ts.map +1 -1
- package/dist/state/init.js +1 -0
- package/dist/state/init.js.map +1 -1
- package/dist/state/lock.d.ts +6 -0
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +8 -0
- package/dist/state/lock.js.map +1 -1
- package/dist/state/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/root-recovery-store.d.ts +107 -0
- package/dist/state/root-recovery-store.d.ts.map +1 -0
- package/dist/state/root-recovery-store.js +400 -0
- package/dist/state/root-recovery-store.js.map +1 -0
- package/dist/state/task-store.d.ts +6 -0
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +266 -21
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts +3 -3
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js.map +1 -1
- package/dist/terminal/stream-ws.d.ts.map +1 -1
- package/dist/terminal/stream-ws.js.map +1 -1
- package/dist/web/assets/index-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,109 @@ 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
|
-
|
|
128
|
-
? { postApproveRedispatchCount: opts.redispatchCount }
|
|
129
|
-
: {}),
|
|
191
|
+
resumed = await manager.continueSession(live.id, live.agentId, 'post-approve', {
|
|
192
|
+
signalToken: key.token,
|
|
193
|
+
postApproveEpisode: key,
|
|
130
194
|
});
|
|
131
195
|
}
|
|
132
196
|
catch (err) {
|
|
133
197
|
dispatchErr = err;
|
|
134
|
-
console.error(`[EventHandler]
|
|
198
|
+
console.error(`[EventHandler] post-approve dispatch failed for task=${live.id}:`, err);
|
|
135
199
|
}
|
|
136
200
|
if (resumed) {
|
|
137
|
-
|
|
138
|
-
await manager.confirmPostApprovePromptDelivered(task.id, signalToken);
|
|
201
|
+
await manager.confirmPostApprovePromptDelivered(live.id, key);
|
|
139
202
|
return;
|
|
140
203
|
}
|
|
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
204
|
if (dispatchErr instanceof DispatchTerminalError) {
|
|
147
|
-
await manager.failTaskForDispatchError(
|
|
205
|
+
await manager.failTaskForDispatchError(live.id, 'post-approve', live.agentId, dispatchErr, {
|
|
206
|
+
expectedPostApproveEpisode: key,
|
|
207
|
+
});
|
|
148
208
|
return;
|
|
149
209
|
}
|
|
150
|
-
await manager.markAgentWaiting(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
210
|
+
await manager.markAgentWaiting(live.agentId, live.id, {
|
|
211
|
+
expectedPostApproveEpisode: key,
|
|
212
|
+
}).catch(err => {
|
|
213
|
+
console.error(`[EventHandler] post-approve rollback failed for task=${live.id}:`, err);
|
|
154
214
|
});
|
|
155
|
-
await emitIntervention(bus,
|
|
215
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
156
216
|
phase: 'post-approve-dispatch-failed',
|
|
157
|
-
reviewRound:
|
|
158
|
-
|
|
217
|
+
reviewRound: live.reviewRound,
|
|
218
|
+
generation: key.generation,
|
|
159
219
|
...(dispatchErr ? { error: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr) } : {}),
|
|
160
220
|
});
|
|
161
221
|
}
|
|
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
222
|
async function handlePrMergeReady(bus, manager, event) {
|
|
178
223
|
const taskId = event.taskId;
|
|
179
224
|
const taskNow = await manager.getTask(taskId);
|
|
180
|
-
if (!taskNow)
|
|
225
|
+
if (!taskNow || taskNow.reviewMode !== 'git')
|
|
181
226
|
return;
|
|
182
227
|
const eventPrNumber = event.data.prNumber;
|
|
183
228
|
const eventPrUrl = event.data.prUrl;
|
|
@@ -197,7 +242,12 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
197
242
|
|| !signalToken
|
|
198
243
|
|| completion?.token !== signalToken)
|
|
199
244
|
return;
|
|
200
|
-
const
|
|
245
|
+
const entryGitEpisode = postApproveEpisodeKey(taskNow);
|
|
246
|
+
if (entryGitEpisode?.token !== signalToken)
|
|
247
|
+
return;
|
|
248
|
+
const ok = await manager.markAgentWaiting(taskNow.agentId, taskNow.id, {
|
|
249
|
+
expectedPostApproveEpisode: entryGitEpisode,
|
|
250
|
+
});
|
|
201
251
|
if (!ok) {
|
|
202
252
|
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
203
253
|
phase: 'post-approve-dev-wait-gate-failed',
|
|
@@ -210,69 +260,91 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
210
260
|
]);
|
|
211
261
|
if (!freshTask
|
|
212
262
|
|| freshTask.status !== 'approved'
|
|
263
|
+
|| freshTask.reviewMode !== 'git'
|
|
213
264
|
|| freshTask.agentId !== taskNow.agentId
|
|
214
265
|
|| freshTask.postApproveRevoked !== undefined
|
|
215
|
-
|| entryCompletion?.token !== signalToken
|
|
216
|
-
|| freshTask.postApproveHeadSha === undefined
|
|
217
|
-
|| entryCompletion.approvedHeadSha !== freshTask.postApproveHeadSha) {
|
|
266
|
+
|| entryCompletion?.token !== signalToken) {
|
|
218
267
|
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
219
268
|
phase: 'post-approve-merge-skipped-stale-task',
|
|
220
269
|
});
|
|
221
270
|
return;
|
|
222
271
|
}
|
|
223
|
-
// 'git' merge-ready 接收即 server 权威双复核(spec §6/§10:provenance + ack 完成集,同一次全源扫描):
|
|
224
|
-
// agent 自查是尽责而非授权
|
|
225
272
|
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
|
-
|
|
273
|
+
const gitEpisodeKey = postApproveEpisodeKey(freshTask);
|
|
274
|
+
if (!gitEpisodeKey || gitEpisodeKey.token !== signalToken) {
|
|
275
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
276
|
+
phase: 'post-approve-merge-skipped-stale-task',
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await manager.markPostApproveSignalReceived(freshTask.id, gitEpisodeKey);
|
|
281
|
+
const scanFailedRetry = async (error) => {
|
|
282
|
+
await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: true }, { expectedGeneration: gitEpisodeKey.generation, expectedHeadSha: gitEpisodeKey.headSha });
|
|
283
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
284
|
+
phase: 'post-approve-merge-skipped-scan-failed',
|
|
285
|
+
error,
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
let verified;
|
|
289
|
+
try {
|
|
290
|
+
verified = await manager.platformVerifyAcceptedPass(freshTask.id);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
await scanFailedRetry(err instanceof Error ? err.message : String(err));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (verified.kind === 'retryable') {
|
|
297
|
+
await scanFailedRetry(verified.reason);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (verified.kind === 'provenance-invalid') {
|
|
301
|
+
const begun = await manager.beginGitReviewPass(freshTask.id, {
|
|
302
|
+
fromStatus: [freshTask.status],
|
|
303
|
+
headSha: gitEpisodeKey.headSha,
|
|
304
|
+
bumpRound: true,
|
|
305
|
+
qaPhase: 'recheck',
|
|
306
|
+
expectPhase: freshTask.phase,
|
|
307
|
+
expectSignalToken: freshTask.signalToken,
|
|
308
|
+
patch: { latestHeadSha: gitEpisodeKey.headSha },
|
|
309
|
+
});
|
|
310
|
+
if (begun?.task.reviewDispatch) {
|
|
311
|
+
try {
|
|
312
|
+
await manager.dispatchGitReviewLease(freshTask.id, {
|
|
313
|
+
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
314
|
+
});
|
|
249
315
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
catch (err) {
|
|
317
|
+
if (err instanceof DispatchTerminalError && begun.task.qaAgentId) {
|
|
318
|
+
await manager.failTaskForDispatchError(freshTask.id, 'recheck', begun.task.qaAgentId, err, { expectedReviewDispatch: begun.task.reviewDispatch });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
322
|
+
phase: 'post-approve-provenance-redispatch-failed',
|
|
323
|
+
reason: verified.reason,
|
|
324
|
+
error: err instanceof Error ? err.message : String(err),
|
|
325
|
+
});
|
|
260
326
|
}
|
|
261
327
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (verified.pendingCount > 0 && !freshCompletion.pendingRedispatch) {
|
|
331
|
+
if (await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: true }, { expectedGeneration: gitEpisodeKey.generation, expectedHeadSha: gitEpisodeKey.headSha })) {
|
|
332
|
+
freshCompletion = { ...freshCompletion, pendingRedispatch: true };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else if (verified.pendingCount === 0 && freshCompletion.pendingRedispatch) {
|
|
336
|
+
if (await manager.updatePostApproveCompletionIfToken(freshTask.id, signalToken, { pendingRedispatch: false }, { expectedGeneration: gitEpisodeKey.generation, expectedHeadSha: gitEpisodeKey.headSha })) {
|
|
337
|
+
freshCompletion = { ...freshCompletion, pendingRedispatch: false };
|
|
267
338
|
}
|
|
268
339
|
}
|
|
269
|
-
// A pending flag landing between the check and the transition re-enters here instead of losing the feedback.
|
|
270
340
|
for (;;) {
|
|
271
341
|
if (freshCompletion.pendingRedispatch) {
|
|
272
342
|
const nextCount = (freshCompletion.redispatchCount ?? 0) + 1;
|
|
273
343
|
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
274
344
|
const revoked = await manager.revokePostApproveCompletion(freshTask.id, 'redispatch-cap', {
|
|
275
345
|
expectedToken: freshCompletion.token,
|
|
346
|
+
expectedGeneration: gitEpisodeKey.generation,
|
|
347
|
+
expectedHeadSha: gitEpisodeKey.headSha,
|
|
276
348
|
});
|
|
277
349
|
if (!revoked) {
|
|
278
350
|
// The pass rotated after the entry re-validation; this merge-ready and its cap decision are stale.
|
|
@@ -288,10 +360,10 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
288
360
|
});
|
|
289
361
|
return;
|
|
290
362
|
}
|
|
291
|
-
await
|
|
363
|
+
await dispatchGitDevPostApproveCheck(bus, manager, freshTask, freshCompletion.approvedHeadSha, { redispatchCount: nextCount });
|
|
292
364
|
return;
|
|
293
365
|
}
|
|
294
|
-
const completed = await manager.completeApprovedPassToMergeReady(freshTask.id,
|
|
366
|
+
const completed = await manager.completeApprovedPassToMergeReady(freshTask.id, gitEpisodeKey);
|
|
295
367
|
if ('task' in completed)
|
|
296
368
|
return;
|
|
297
369
|
if (completed.refused === 'pending') {
|
|
@@ -310,156 +382,53 @@ async function handlePrMergeReady(bus, manager, event) {
|
|
|
310
382
|
async function handlePrFeedback(bus, manager, event) {
|
|
311
383
|
const taskId = event.taskId;
|
|
312
384
|
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)
|
|
385
|
+
if (!taskNow || taskNow.reviewMode !== 'git')
|
|
326
386
|
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 });
|
|
387
|
+
const revision = parseGitFeedbackRevision(event.data.revision);
|
|
388
|
+
if (!revision) {
|
|
389
|
+
await emitIntervention(bus, taskNow.projectId, taskNow.agentId, taskNow.id, {
|
|
390
|
+
phase: 'git-feedback-revision-invalid',
|
|
391
|
+
});
|
|
363
392
|
return;
|
|
364
393
|
}
|
|
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')
|
|
394
|
+
const revisionKey = ackRevisionKey(revision.sourceKey, revision.id, revision.bodyDigest);
|
|
395
|
+
if (taskNow.consumedFeedback?.[revisionKey] !== undefined)
|
|
376
396
|
return;
|
|
377
|
-
|
|
397
|
+
const revisionTime = revision.versionTime ?? Date.parse(event.timestamp);
|
|
398
|
+
const outcome = await manager.consumeGitFeedbackRevision(taskId, { key: revisionKey, versionTime: revisionTime }, { feedbackAt: event.timestamp });
|
|
399
|
+
if (outcome.kind !== 'pending' && outcome.kind !== 'returned')
|
|
378
400
|
return;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
});
|
|
401
|
+
const effected = outcome.task;
|
|
402
|
+
const completion = await manager.getPostApproveCompletion(effected.id);
|
|
403
|
+
const devState = await manager.getAgentState(effected.agentId);
|
|
404
|
+
if (outcome.kind === 'pending' && completion !== null && devState?.taskId === effected.id)
|
|
384
405
|
return;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
406
|
+
const nextCount = (effected.redispatchCount ?? completion?.redispatchCount ?? 0) + 1;
|
|
407
|
+
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
408
|
+
const key = postApproveEpisodeKey(effected);
|
|
409
|
+
await manager.revokePostApproveCompletion(effected.id, 'redispatch-cap', key
|
|
410
|
+
? {
|
|
411
|
+
expectedToken: key.token,
|
|
412
|
+
expectedGeneration: key.generation,
|
|
413
|
+
expectedHeadSha: key.headSha,
|
|
414
|
+
}
|
|
415
|
+
: {});
|
|
416
|
+
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
417
|
+
phase: 'post-approve-redispatch-cap-exceeded',
|
|
418
|
+
redispatchCount: effected.redispatchCount ?? 0,
|
|
419
|
+
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
391
420
|
});
|
|
392
421
|
return;
|
|
393
422
|
}
|
|
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 });
|
|
423
|
+
const approvedHead = effected.postApproveHeadSha ?? validHeadSha(effected.latestHeadSha);
|
|
424
|
+
if (!approvedHead) {
|
|
425
|
+
await emitIntervention(bus, effected.projectId, effected.agentId, effected.id, {
|
|
426
|
+
phase: 'post-approve-approved-head-unavailable',
|
|
427
|
+
});
|
|
458
428
|
return;
|
|
459
429
|
}
|
|
430
|
+
await dispatchGitDevPostApproveCheck(bus, manager, effected, approvedHead, { redispatchCount: nextCount });
|
|
460
431
|
}
|
|
461
|
-
// 恢复期消费 durable pendingRedispatch(spec §6 至少一次):merge-ready 退回或 path C 的 CAS
|
|
462
|
-
// 落盘后、派发前崩溃的残局在此补派;dev 仍绑定任务时不派(其在途 pass 的 merge-ready 循环接手)。
|
|
463
432
|
export async function recoverGitPostApprovePending(bus, manager) {
|
|
464
433
|
const tasks = await manager.listActiveGitTasks();
|
|
465
434
|
for (const task of tasks) {
|
|
@@ -467,7 +436,6 @@ export async function recoverGitPostApprovePending(bus, manager) {
|
|
|
467
436
|
await recoverOneGitPendingTask(bus, manager, task);
|
|
468
437
|
}
|
|
469
438
|
catch (err) {
|
|
470
|
-
// 单任务失败不得饿死其后的 durable pending(至少一次):报告并继续
|
|
471
439
|
console.warn(`[EventHandler] recoverGitPostApprovePending: task=${task.id} failed, continuing:`, err);
|
|
472
440
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
473
441
|
phase: 'post-approve-recovery-failed',
|
|
@@ -478,328 +446,159 @@ export async function recoverGitPostApprovePending(bus, manager) {
|
|
|
478
446
|
}
|
|
479
447
|
async function recoverOneGitPendingTask(bus, manager, task) {
|
|
480
448
|
{
|
|
481
|
-
|
|
449
|
+
const live = await manager.getTask(task.id);
|
|
450
|
+
if (!live || live.reviewMode !== 'git' || live.status !== 'approved' || live.pendingRedispatch !== true)
|
|
451
|
+
return;
|
|
452
|
+
if (live.postApproveRevoked)
|
|
453
|
+
return;
|
|
454
|
+
const updatedAt = Date.parse(live.updatedAt);
|
|
455
|
+
if (!Number.isFinite(updatedAt)) {
|
|
456
|
+
if (unparseableUpdatedAtWarned.get(live.id) !== live.updatedAt) {
|
|
457
|
+
unparseableUpdatedAtWarned.set(live.id, live.updatedAt);
|
|
458
|
+
console.warn(`[EventHandler] recoverGitPostApprovePending: task=${live.id} updatedAt unparseable, skipping recovery:`, live.updatedAt);
|
|
459
|
+
}
|
|
482
460
|
return;
|
|
483
|
-
|
|
461
|
+
}
|
|
462
|
+
unparseableUpdatedAtWarned.delete(live.id);
|
|
463
|
+
if (Date.now() - updatedAt < POST_APPROVE_RECOVERY_MIN_AGE_MS)
|
|
484
464
|
return;
|
|
485
|
-
if (
|
|
486
|
-
const devState = await manager.getAgentState(
|
|
487
|
-
if (devState?.taskId ===
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
465
|
+
if (live.postApprovePhase === 'delivered') {
|
|
466
|
+
const devState = await manager.getAgentState(live.agentId);
|
|
467
|
+
if (devState?.taskId === live.id) {
|
|
468
|
+
const rearmed = await manager.rearmPostApproveSignal(live.id);
|
|
469
|
+
if (!rearmed) {
|
|
470
|
+
const fresh = await manager.getTask(live.id);
|
|
471
|
+
if (fresh?.status === 'approved'
|
|
472
|
+
&& fresh.pendingRedispatch === true
|
|
473
|
+
&& fresh.postApprovePhase === 'delivered'
|
|
474
|
+
&& fresh.postApproveGeneration === live.postApproveGeneration
|
|
475
|
+
&& fresh.postApproveToken === live.postApproveToken) {
|
|
476
|
+
throw new Error(`post-approve signal re-arm refused for active task ${live.id}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
491
479
|
return;
|
|
492
480
|
}
|
|
493
481
|
}
|
|
494
|
-
const approvedHead = validHeadSha(
|
|
482
|
+
const approvedHead = validHeadSha(live.postApproveHeadSha ?? live.latestHeadSha);
|
|
495
483
|
if (!approvedHead) {
|
|
496
|
-
await emitIntervention(bus,
|
|
484
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
497
485
|
phase: 'post-approve-approved-head-unavailable',
|
|
498
486
|
});
|
|
499
487
|
return;
|
|
500
488
|
}
|
|
501
|
-
const nextCount = (
|
|
489
|
+
const nextCount = (live.redispatchCount ?? 0) + 1;
|
|
502
490
|
if (nextCount > POST_APPROVE_REDISPATCH_CAP) {
|
|
503
|
-
|
|
504
|
-
await
|
|
491
|
+
const key = postApproveEpisodeKey(live);
|
|
492
|
+
await manager.revokePostApproveCompletion(live.id, 'redispatch-cap', key
|
|
493
|
+
? {
|
|
494
|
+
expectedToken: key.token,
|
|
495
|
+
expectedGeneration: key.generation,
|
|
496
|
+
expectedHeadSha: key.headSha,
|
|
497
|
+
}
|
|
498
|
+
: {});
|
|
499
|
+
await emitIntervention(bus, live.projectId, live.agentId, live.id, {
|
|
505
500
|
phase: 'post-approve-redispatch-cap-exceeded',
|
|
506
|
-
redispatchCount:
|
|
501
|
+
redispatchCount: live.redispatchCount ?? 0,
|
|
507
502
|
cap: POST_APPROVE_REDISPATCH_CAP,
|
|
508
503
|
});
|
|
509
504
|
return;
|
|
510
505
|
}
|
|
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;
|
|
506
|
+
await dispatchGitDevPostApproveCheck(bus, manager, live, approvedHead, {
|
|
507
|
+
redispatchCount: nextCount,
|
|
508
|
+
expectedEpisode: postApproveEpisodeKey(live) ?? undefined,
|
|
509
|
+
});
|
|
522
510
|
}
|
|
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
511
|
}
|
|
530
512
|
async function handlePrCodePush(bus, manager, event) {
|
|
531
513
|
const taskId = event.taskId;
|
|
532
|
-
const agentId = event.agentId;
|
|
533
514
|
const eventPrNumber = event.data.prNumber;
|
|
534
515
|
const eventPrUrl = event.data.prUrl;
|
|
535
516
|
const eventKind = event.data.kind;
|
|
536
517
|
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)
|
|
518
|
+
const task = await manager.getTask(taskId);
|
|
519
|
+
if (!task || task.reviewMode !== 'git')
|
|
546
520
|
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
|
|
521
|
+
if (eventKind === 'push' && eventHeadSha
|
|
551
522
|
&& 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`);
|
|
523
|
+
&& task.reviewDispatch === undefined
|
|
524
|
+
&& task.latestHeadSha === eventHeadSha
|
|
525
|
+
&& task.reviewHeadAnchorSha === eventHeadSha) {
|
|
561
526
|
return;
|
|
562
527
|
}
|
|
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)
|
|
528
|
+
if (task.status === 'in_progress' && task.prNumber === undefined && eventPrNumber === undefined) {
|
|
529
|
+
console.warn(`[EventHandler] pr.updated: task ${taskId} in_progress but neither task nor event has prNumber; deferring catch-up`);
|
|
581
530
|
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
531
|
}
|
|
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
|
-
}
|
|
532
|
+
if (!PR_UPDATED_REVIEW_FROM_STATUSES.includes(task.status))
|
|
642
533
|
return;
|
|
534
|
+
let reviewHead = eventHeadSha ?? validHeadSha(task.latestHeadSha);
|
|
535
|
+
const prNumber = eventPrNumber ?? task.prNumber;
|
|
536
|
+
if (reviewHead === undefined && prNumber !== undefined) {
|
|
537
|
+
const verified = await manager.platformVerifyPrBinding(taskId, prNumber);
|
|
538
|
+
if (verified.ok)
|
|
539
|
+
reviewHead = validHeadSha(verified.headSha);
|
|
643
540
|
}
|
|
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,
|
|
541
|
+
if (reviewHead === undefined) {
|
|
542
|
+
await emitIntervention(bus, task.projectId, task.agentId, taskId, {
|
|
543
|
+
phase: 'git-review-head-unavailable',
|
|
651
544
|
});
|
|
652
545
|
return;
|
|
653
546
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
547
|
+
const bumpRound = task.status === 'in_progress'
|
|
548
|
+
|| task.status === 'approved'
|
|
549
|
+
|| task.status === 'merge-ready'
|
|
550
|
+
|| (task.status === 'review' && task.reviewRoundPending === true);
|
|
551
|
+
const begun = await manager.beginGitReviewPass(taskId, {
|
|
552
|
+
fromStatus: [task.status],
|
|
553
|
+
headSha: reviewHead,
|
|
554
|
+
bumpRound,
|
|
555
|
+
expectPhase: task.phase,
|
|
556
|
+
expectSignalToken: task.signalToken,
|
|
557
|
+
patch: {
|
|
558
|
+
...(eventPrNumber !== undefined ? { prNumber: eventPrNumber } : {}),
|
|
559
|
+
...(eventPrUrl !== undefined ? { prUrl: eventPrUrl } : {}),
|
|
560
|
+
latestHeadSha: reviewHead,
|
|
561
|
+
},
|
|
659
562
|
});
|
|
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
|
-
}
|
|
563
|
+
if (!begun?.task.reviewDispatch) {
|
|
564
|
+
const current = await manager.getTask(taskId);
|
|
565
|
+
if (current?.reviewDispatch?.phase === 'uncertain') {
|
|
566
|
+
const qaPhase = current.reviewDispatch.qaPhase;
|
|
567
|
+
await emitIntervention(bus, current.projectId, current.agentId, taskId, {
|
|
568
|
+
phase: 'git-review-dispatch-uncertain',
|
|
569
|
+
qaPhase,
|
|
570
|
+
headSha: reviewHead,
|
|
571
|
+
});
|
|
692
572
|
}
|
|
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
573
|
return;
|
|
703
574
|
}
|
|
704
|
-
|
|
705
|
-
let dispatchErr = null;
|
|
575
|
+
const qaPhase = begun.task.reviewDispatch.qaPhase;
|
|
706
576
|
try {
|
|
707
|
-
|
|
708
|
-
|
|
577
|
+
await manager.dispatchGitReviewLease(taskId, {
|
|
578
|
+
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
709
579
|
});
|
|
710
580
|
}
|
|
711
581
|
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,
|
|
582
|
+
console.error(`[EventHandler] pr.updated git review dispatch failed for task=${taskId}:`, err);
|
|
583
|
+
if (err instanceof DispatchTerminalError && err.reason === 'ack_unknown') {
|
|
584
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, taskId, {
|
|
585
|
+
phase: 'git-review-dispatch-uncertain',
|
|
738
586
|
qaPhase,
|
|
739
|
-
|
|
740
|
-
note,
|
|
587
|
+
error: err.message,
|
|
741
588
|
});
|
|
742
589
|
}
|
|
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
|
-
}
|
|
590
|
+
else if (err instanceof DispatchTerminalError && begun.task.qaAgentId) {
|
|
591
|
+
await manager.failTaskForDispatchError(taskId, qaPhase, begun.task.qaAgentId, err, {
|
|
592
|
+
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
593
|
+
});
|
|
770
594
|
}
|
|
771
595
|
else {
|
|
772
|
-
await
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
596
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, taskId, {
|
|
597
|
+
phase: 'git-review-dispatch-failed',
|
|
598
|
+
qaPhase,
|
|
599
|
+
error: err instanceof Error ? err.message : String(err),
|
|
776
600
|
});
|
|
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
601
|
}
|
|
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
602
|
}
|
|
804
603
|
}
|
|
805
604
|
async function redispatchReviewForStaleVerdict(manager, task) {
|
|
@@ -830,6 +629,7 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
830
629
|
await manager.dispatchReviewToQa(task.id, {
|
|
831
630
|
fromStatus: ['review'],
|
|
832
631
|
bumpRound: task.reviewRoundPending === true,
|
|
632
|
+
expectPhase: task.phase,
|
|
833
633
|
expectSignalToken: task.signalToken,
|
|
834
634
|
...(qaPhase !== undefined ? { qaPhase } : {}),
|
|
835
635
|
});
|
|
@@ -842,157 +642,80 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
842
642
|
return true;
|
|
843
643
|
}
|
|
844
644
|
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) {
|
|
645
|
+
if (reviewedHeadSha !== currentHeadSha) {
|
|
862
646
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
863
647
|
phase: 'stale-approval-head-mismatch',
|
|
864
648
|
reviewedHeadSha,
|
|
865
|
-
currentHeadSha
|
|
866
|
-
source:
|
|
867
|
-
...('fetchError' in anchor && anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
649
|
+
currentHeadSha,
|
|
650
|
+
source: 'payload-self',
|
|
868
651
|
});
|
|
869
652
|
return;
|
|
870
653
|
}
|
|
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,
|
|
654
|
+
if (task.failToken === undefined || task.signalToken === undefined)
|
|
655
|
+
return;
|
|
656
|
+
const provenance = {
|
|
657
|
+
sourceKey: gitVerdict.carrier.sourceKey,
|
|
658
|
+
id: gitVerdict.carrier.id,
|
|
659
|
+
bodyDigest: gitVerdict.carrier.bodyDigest,
|
|
660
|
+
token: gitVerdict.verdictToken,
|
|
661
|
+
failToken: task.failToken,
|
|
662
|
+
anchorSha: reviewedHeadSha,
|
|
663
|
+
};
|
|
664
|
+
const reviewedRound = Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0));
|
|
665
|
+
const transitioned = await manager.approveGitReviewPass(task.id, {
|
|
666
|
+
expectedSignalToken: task.signalToken,
|
|
667
|
+
headSha: reviewedHeadSha,
|
|
668
|
+
reviewRound: reviewedRound,
|
|
669
|
+
...(prPatch.prNumber !== undefined ? { prNumber: prPatch.prNumber } : {}),
|
|
670
|
+
...(prPatch.prUrl !== undefined ? { prUrl: prPatch.prUrl } : {}),
|
|
671
|
+
provenance,
|
|
897
672
|
});
|
|
898
|
-
if (!
|
|
673
|
+
if (!transitioned)
|
|
899
674
|
return;
|
|
900
|
-
const { task: transitioned } = result;
|
|
901
675
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
902
676
|
if (transitioned.qaAgentId) {
|
|
903
677
|
await manager.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
904
678
|
.catch(err => console.error(`[EventHandler] APPROVE releaseAgentForTask(QA=${transitioned.qaAgentId}) failed:`, err));
|
|
905
679
|
}
|
|
906
|
-
await
|
|
680
|
+
await dispatchGitDevPostApproveCheck(bus, manager, transitioned, reviewedHeadSha);
|
|
907
681
|
}
|
|
908
682
|
function gitHeadPatch(task, headSha) {
|
|
909
|
-
return
|
|
683
|
+
return headSha !== undefined && task.latestHeadSha !== headSha
|
|
910
684
|
? { latestHeadSha: headSha }
|
|
911
685
|
: {};
|
|
912
686
|
}
|
|
913
687
|
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) {
|
|
688
|
+
if (reviewedHeadSha !== currentHeadSha) {
|
|
927
689
|
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
928
690
|
phase: 'stale-request-changes-head-mismatch',
|
|
929
691
|
reviewedHeadSha,
|
|
930
|
-
currentHeadSha
|
|
931
|
-
source:
|
|
932
|
-
...('fetchError' in anchor && anchor.fetchError ? { fetchError: anchor.fetchError } : {}),
|
|
692
|
+
currentHeadSha,
|
|
693
|
+
source: 'payload-self',
|
|
933
694
|
});
|
|
934
695
|
return;
|
|
935
696
|
}
|
|
936
697
|
const reviewedRound = Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0));
|
|
937
698
|
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
699
|
if (nextRound > manager.getConfig().review.rounds) {
|
|
975
700
|
return handleMaxRounds(bus, manager, task, prPatch, reviewedRound);
|
|
976
701
|
}
|
|
977
702
|
const result = await manager.transitionTaskStatus(task.id, 'fixing', {
|
|
978
|
-
fromStatus: ['review'
|
|
979
|
-
|
|
980
|
-
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
703
|
+
fromStatus: ['review'],
|
|
704
|
+
expectSignalToken: task.signalToken,
|
|
981
705
|
}, {
|
|
982
706
|
...prPatch,
|
|
983
|
-
...gitHeadPatch(task,
|
|
707
|
+
...gitHeadPatch(task, currentHeadSha),
|
|
984
708
|
reviewRound: nextRound,
|
|
985
709
|
reviewRoundPending: undefined,
|
|
986
|
-
|
|
987
|
-
reviewDispatchPending: undefined,
|
|
710
|
+
reviewDispatch: undefined,
|
|
988
711
|
fixDispatchedAt: new Date().toISOString(),
|
|
989
712
|
});
|
|
990
713
|
if (!result)
|
|
991
714
|
return;
|
|
992
|
-
const { task: transitioned
|
|
715
|
+
const { task: transitioned } = result;
|
|
993
716
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
994
717
|
manager.stopPhaseSignalWatcher(transitioned.id);
|
|
995
|
-
if (
|
|
718
|
+
if (transitioned.qaAgentId) {
|
|
996
719
|
const qaReleased = await manager
|
|
997
720
|
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { allowAwaitingHuman: true })
|
|
998
721
|
.catch(err => {
|
|
@@ -1014,7 +737,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
1014
737
|
});
|
|
1015
738
|
return;
|
|
1016
739
|
}
|
|
1017
|
-
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
|
|
740
|
+
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed', { expectedStatus: 'fixing', expectedToken: transitioned.signalToken });
|
|
1018
741
|
if (!armed) {
|
|
1019
742
|
console.warn(`[EventHandler] REQUEST_CHANGES pr-fixed watcher failed to arm for task=${transitioned.id}; holding dev (not dispatching fix)`);
|
|
1020
743
|
await manager.markAwaitingHuman(transitioned.agentId, 'signal-arm-failed:pr-fixed', 'pr-fixed watcher failed to arm; the fix was not dispatched (its completion signal would have no consumer). Cancel the task or delete the agent to retry.', { expectedTaskId: transitioned.id });
|
|
@@ -1069,9 +792,14 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
1069
792
|
}
|
|
1070
793
|
async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
1071
794
|
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
1072
|
-
fromStatus: ['review'
|
|
1073
|
-
|
|
1074
|
-
}, {
|
|
795
|
+
fromStatus: ['review'],
|
|
796
|
+
expectSignalToken: task.signalToken,
|
|
797
|
+
}, {
|
|
798
|
+
...prPatch,
|
|
799
|
+
reviewRound: reviewedRound,
|
|
800
|
+
reviewRoundPending: undefined,
|
|
801
|
+
reviewDispatch: undefined,
|
|
802
|
+
});
|
|
1075
803
|
if (!result)
|
|
1076
804
|
return;
|
|
1077
805
|
const { task: transitioned } = result;
|
|
@@ -1108,18 +836,31 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1108
836
|
if (!event.taskId || !event.agentId)
|
|
1109
837
|
return;
|
|
1110
838
|
const taskAtEntry = await manager.getTask(event.taskId);
|
|
1111
|
-
if (taskAtEntry
|
|
839
|
+
if (!taskAtEntry)
|
|
1112
840
|
return;
|
|
1113
|
-
if (taskAtEntry
|
|
841
|
+
if (taskAtEntry.reviewMode === 'server') {
|
|
842
|
+
const prNumber = event.data.prNumber;
|
|
843
|
+
if (!Number.isInteger(prNumber) || prNumber < 1)
|
|
844
|
+
return;
|
|
845
|
+
const adopted = await manager.adoptServerPr(event.taskId, {
|
|
846
|
+
prNumber: prNumber,
|
|
847
|
+
...(typeof event.data.prUrl === 'string' ? { prUrl: event.data.prUrl } : {}),
|
|
848
|
+
...(validHeadSha(event.data.headSha) !== undefined ? { headSha: validHeadSha(event.data.headSha) } : {}),
|
|
849
|
+
...(typeof event.data.targetBranch === 'string' ? { targetBranch: event.data.targetBranch } : {}),
|
|
850
|
+
});
|
|
851
|
+
if (!adopted) {
|
|
852
|
+
await emitIntervention(bus, taskAtEntry.projectId, taskAtEntry.agentId, taskAtEntry.id, {
|
|
853
|
+
phase: 'server-pr-adoption-refused',
|
|
854
|
+
prNumber,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (isSpecStagePhase(taskAtEntry.phase)) {
|
|
1114
860
|
console.warn(`[EventHandler] pr.created ignored for task ${event.taskId}: task in spec phase`);
|
|
1115
861
|
return;
|
|
1116
862
|
}
|
|
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'
|
|
863
|
+
if (event.data.source === 'pane-signal'
|
|
1123
864
|
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
1124
865
|
&& taskAtEntry.status !== 'in_progress') {
|
|
1125
866
|
if (taskAtEntry.pendingPrSignalToken === undefined
|
|
@@ -1131,8 +872,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1131
872
|
? decodeSignalActorId(event.data.actorB64)
|
|
1132
873
|
: undefined;
|
|
1133
874
|
if (actorId === undefined) {
|
|
1134
|
-
// watcher 单发已被本事件消费:不重臂就永失 reconciliation 入口。skipSnapshot=true
|
|
1135
|
-
// 只等待后续正确的五段信号,避免滚动区里同一条三段残文立即重触发成环
|
|
1136
875
|
await manager.rearmGitReconciliationWatcher(taskAtEntry.id, { skipSnapshot: true });
|
|
1137
876
|
await emitIntervention(bus, taskAtEntry.projectId, event.agentId, taskAtEntry.id, {
|
|
1138
877
|
phase: 'git-pr-created-actor-missing',
|
|
@@ -1146,23 +885,20 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1146
885
|
pendingPrSignalToken: undefined,
|
|
1147
886
|
});
|
|
1148
887
|
}
|
|
1149
|
-
// fixing 态同键 watcher 已是 pr-fixed:拆掉会吞掉本轮完成信号,reconciliation 条目只存在于 review
|
|
1150
888
|
if (taskAtEntry.status === 'review') {
|
|
1151
889
|
manager.stopPhaseSignalWatcherAgent(taskAtEntry.id, event.agentId);
|
|
1152
890
|
}
|
|
1153
891
|
return;
|
|
1154
892
|
}
|
|
1155
|
-
if (
|
|
893
|
+
if (event.data.source !== 'pane-signal'
|
|
1156
894
|
&& taskAtEntry.prNumber !== undefined && taskAtEntry.prNumber === event.data.prNumber
|
|
1157
|
-
&& taskAtEntry.reviewDispatchPending !== true
|
|
1158
895
|
&& !['in_progress', 'fixing'].includes(taskAtEntry.status)) {
|
|
1159
|
-
// in_progress/fixing 或派发未完成(pending)的同号重放走完整收编重试(至少一次)
|
|
1160
896
|
return;
|
|
1161
897
|
}
|
|
1162
898
|
let paneVerifiedHeadSha;
|
|
1163
899
|
let reconciledBranch;
|
|
1164
|
-
let
|
|
1165
|
-
if (
|
|
900
|
+
let verifiedTargetBranch;
|
|
901
|
+
if (event.data.source === 'pane-signal' && event.data.prNumber !== undefined) {
|
|
1166
902
|
const prNumber = event.data.prNumber;
|
|
1167
903
|
try {
|
|
1168
904
|
let verify = await manager.platformVerifyPrBinding(event.taskId, prNumber);
|
|
@@ -1170,11 +906,11 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1170
906
|
const taskNow = await manager.getTask(event.taskId);
|
|
1171
907
|
const prBranch = verify.prBranch;
|
|
1172
908
|
if (taskNow) {
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
909
|
+
const ownBranch = BRANCH_PREFIX + event.taskId;
|
|
910
|
+
const foreignBxBranch = prBranch.startsWith(BRANCH_PREFIX) && prBranch !== ownBranch;
|
|
1175
911
|
const bound = await manager.findTaskByBranch(prBranch, taskNow.projectId);
|
|
1176
|
-
if (!
|
|
1177
|
-
&& (prBranch ===
|
|
912
|
+
if (!foreignBxBranch && isValidBranchName(prBranch)
|
|
913
|
+
&& (prBranch === ownBranch || bound?.id === taskNow.id)) {
|
|
1178
914
|
verify = await manager.platformVerifyPrBinding(event.taskId, prNumber, { branchOverride: prBranch });
|
|
1179
915
|
if (verify.ok)
|
|
1180
916
|
reconciledBranch = prBranch;
|
|
@@ -1196,13 +932,12 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1196
932
|
return;
|
|
1197
933
|
}
|
|
1198
934
|
paneVerifiedHeadSha = verify.headSha;
|
|
1199
|
-
|
|
935
|
+
verifiedTargetBranch = verify.targetBranch;
|
|
1200
936
|
}
|
|
1201
937
|
catch (err) {
|
|
1202
938
|
console.warn(`[EventHandler] pr.created: platformVerifyPrBinding failed for task ${event.taskId}:`, err);
|
|
1203
939
|
const taskNow = await manager.getTask(event.taskId);
|
|
1204
940
|
if (taskNow) {
|
|
1205
|
-
// skipSnapshot 缺省 true:滚动区里刚消费的信号残文在平台持续失败时会形成无退避热循环
|
|
1206
941
|
await reArmDevelopWatcher(manager, taskNow, event.agentId);
|
|
1207
942
|
await emitIntervention(bus, taskNow.projectId, event.agentId, event.taskId, {
|
|
1208
943
|
phase: 'pane-pr-created-verify-error',
|
|
@@ -1213,243 +948,127 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1213
948
|
return;
|
|
1214
949
|
}
|
|
1215
950
|
}
|
|
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.
|
|
951
|
+
const task = await manager.getTask(event.taskId);
|
|
952
|
+
if (!task || task.reviewMode !== 'git')
|
|
953
|
+
return;
|
|
1278
954
|
if (event.data.source !== 'pane-signal'
|
|
1279
|
-
&&
|
|
1280
|
-
&&
|
|
1281
|
-
&& taskBeforeTransition.prNumber !== event.data.prNumber) {
|
|
955
|
+
&& task.branch !== BRANCH_PREFIX + event.taskId
|
|
956
|
+
&& task.prNumber !== event.data.prNumber) {
|
|
1282
957
|
console.warn(`[EventHandler] pr.created: ignoring poller-sourced PR ${event.data.prNumber} on custom branch ` +
|
|
1283
958
|
`for task ${event.taskId} (untracked; custom-branch PRs adopt via the pane signal)`);
|
|
1284
959
|
return;
|
|
1285
960
|
}
|
|
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';
|
|
961
|
+
const adoption = {};
|
|
962
|
+
const target = event.data.source === 'pane-signal'
|
|
963
|
+
? verifiedTargetBranch
|
|
964
|
+
: (typeof event.data.targetBranch === 'string' ? event.data.targetBranch : undefined);
|
|
965
|
+
if (target !== undefined && task.baseBranch === undefined)
|
|
966
|
+
adoption.baseBranch = target;
|
|
967
|
+
if (event.data.source === 'pane-signal') {
|
|
968
|
+
const actorId = typeof event.data.actorB64 === 'string'
|
|
969
|
+
? decodeSignalActorId(event.data.actorB64)
|
|
970
|
+
: undefined;
|
|
971
|
+
if (actorId !== undefined) {
|
|
972
|
+
adoption.replyActorId = actorId;
|
|
973
|
+
adoption.replyActorStatus = 'verified';
|
|
974
|
+
adoption.pendingPrSignalToken = undefined;
|
|
1310
975
|
}
|
|
1311
976
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
977
|
+
else if (typeof event.data.prAuthorId === 'string' && task.replyActorStatus !== 'verified') {
|
|
978
|
+
adoption.replyActorId = event.data.prAuthorId;
|
|
979
|
+
adoption.replyActorStatus = 'provisional';
|
|
980
|
+
}
|
|
981
|
+
let reviewHead = validHeadSha(event.data.headSha) ?? paneVerifiedHeadSha;
|
|
982
|
+
if (reviewHead === undefined && typeof event.data.prNumber === 'number') {
|
|
983
|
+
const verified = await manager.platformVerifyPrBinding(event.taskId, event.data.prNumber);
|
|
984
|
+
if (verified.ok)
|
|
985
|
+
reviewHead = validHeadSha(verified.headSha);
|
|
986
|
+
}
|
|
987
|
+
if (reviewHead === undefined) {
|
|
988
|
+
await emitIntervention(bus, task.projectId, task.agentId, event.taskId, {
|
|
989
|
+
phase: 'git-review-head-unavailable',
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (task.status !== 'in_progress' && task.status !== 'fixing')
|
|
994
|
+
return;
|
|
995
|
+
const begun = await manager.beginGitReviewPass(event.taskId, {
|
|
996
|
+
fromStatus: [task.status],
|
|
997
|
+
headSha: reviewHead,
|
|
998
|
+
bumpRound: task.status === 'in_progress',
|
|
999
|
+
qaPhase: 'review',
|
|
1000
|
+
expectPhase: task.phase,
|
|
1001
|
+
expectSignalToken: task.signalToken,
|
|
1002
|
+
patch: {
|
|
1003
|
+
...(event.data.prNumber !== undefined ? { prNumber: event.data.prNumber } : {}),
|
|
1004
|
+
...(event.data.prUrl !== undefined ? { prUrl: event.data.prUrl } : {}),
|
|
1005
|
+
latestHeadSha: reviewHead,
|
|
1006
|
+
...(reconciledBranch ? { branch: reconciledBranch } : {}),
|
|
1007
|
+
...adoption,
|
|
1008
|
+
},
|
|
1322
1009
|
});
|
|
1323
|
-
if (!
|
|
1324
|
-
console.warn(`[EventHandler] pr.created: cannot
|
|
1010
|
+
if (!begun?.task.reviewDispatch) {
|
|
1011
|
+
console.warn(`[EventHandler] pr.created: cannot begin git review pass for task ${event.taskId}`);
|
|
1325
1012
|
if (event.data.source === 'pane-signal') {
|
|
1326
|
-
const
|
|
1327
|
-
if (
|
|
1328
|
-
await reArmDevelopWatcher(manager,
|
|
1329
|
-
await emitIntervention(bus,
|
|
1013
|
+
const current = await manager.getTask(event.taskId);
|
|
1014
|
+
if (current && ['in_progress', 'fixing'].includes(current.status)) {
|
|
1015
|
+
await reArmDevelopWatcher(manager, current, event.agentId);
|
|
1016
|
+
await emitIntervention(bus, current.projectId, event.agentId, event.taskId, {
|
|
1330
1017
|
phase: 'pane-pr-created-transition-failed',
|
|
1331
1018
|
claimedPrNumber: event.data.prNumber,
|
|
1332
|
-
taskBranch:
|
|
1019
|
+
taskBranch: current.branch ?? '',
|
|
1333
1020
|
});
|
|
1334
1021
|
}
|
|
1335
1022
|
}
|
|
1336
1023
|
return;
|
|
1337
1024
|
}
|
|
1338
|
-
const { task: transitioned, previousStatus } = result;
|
|
1339
1025
|
if (reconciledBranch) {
|
|
1340
|
-
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: `
|
|
1341
|
-
`${taskBeforeTransition?.branch} → ${reconciledBranch}`);
|
|
1026
|
+
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: ${task.branch} → ${reconciledBranch}`);
|
|
1342
1027
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1028
|
+
if (!begun.task.qaAgentId) {
|
|
1029
|
+
const parked = await manager.markAgentWaiting(event.agentId, begun.task.id);
|
|
1030
|
+
if (!parked) {
|
|
1031
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1348
1032
|
phase: 'dev-wait-gate-failed-no-qa',
|
|
1349
1033
|
});
|
|
1350
1034
|
}
|
|
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,
|
|
1035
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1036
|
+
phase: 'git-review-no-qa-partner',
|
|
1401
1037
|
});
|
|
1402
1038
|
return;
|
|
1403
1039
|
}
|
|
1404
|
-
let started = false;
|
|
1405
|
-
let dispatchErr = null;
|
|
1406
1040
|
try {
|
|
1407
|
-
|
|
1408
|
-
|
|
1041
|
+
await manager.dispatchGitReviewLease(begun.task.id, {
|
|
1042
|
+
expectedGeneration: begun.task.reviewDispatch.generation,
|
|
1409
1043
|
});
|
|
1410
1044
|
}
|
|
1411
1045
|
catch (err) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
await manager.failTaskForDispatchError(transitioned.id, 'review', qa.id, dispatchErr);
|
|
1046
|
+
console.error(`[EventHandler] pr.created git review dispatch failed for task=${begun.task.id}:`, err);
|
|
1047
|
+
if (err instanceof DispatchTerminalError && err.reason === 'ack_unknown') {
|
|
1048
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1049
|
+
phase: 'git-review-dispatch-uncertain',
|
|
1050
|
+
qaPhase: 'review',
|
|
1051
|
+
error: err.message,
|
|
1052
|
+
});
|
|
1420
1053
|
}
|
|
1421
|
-
else if (
|
|
1054
|
+
else if (err instanceof DispatchTerminalError) {
|
|
1055
|
+
await manager.failTaskForDispatchError(begun.task.id, 'review', begun.task.qaAgentId, err, {
|
|
1056
|
+
expectedReviewDispatch: begun.task.reviewDispatch,
|
|
1057
|
+
});
|
|
1422
1058
|
}
|
|
1423
1059
|
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,
|
|
1060
|
+
await emitIntervention(bus, begun.task.projectId, begun.task.agentId, begun.task.id, {
|
|
1061
|
+
phase: 'git-review-dispatch-failed',
|
|
1062
|
+
qaPhase: 'review',
|
|
1063
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1432
1064
|
});
|
|
1433
1065
|
}
|
|
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
1066
|
}
|
|
1446
1067
|
});
|
|
1447
1068
|
bus.on('pr.updated', async (event) => {
|
|
1448
|
-
if (!event.taskId
|
|
1069
|
+
if (!event.taskId)
|
|
1449
1070
|
return;
|
|
1450
1071
|
const taskAtEntry = await manager.getTask(event.taskId);
|
|
1451
|
-
if (taskAtEntry?.reviewMode === 'server')
|
|
1452
|
-
return;
|
|
1453
1072
|
const eventKind = event.data.kind;
|
|
1454
1073
|
if (taskAtEntry && isSpecStagePhase(taskAtEntry.phase)) {
|
|
1455
1074
|
console.warn(`[EventHandler] pr.updated (kind=${eventKind ?? 'push'}) ignored for task ${event.taskId}: task in spec phase`);
|
|
@@ -1457,7 +1076,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1457
1076
|
}
|
|
1458
1077
|
if (eventKind === 'pr-merge-ready')
|
|
1459
1078
|
return handlePrMergeReady(bus, manager, event);
|
|
1460
|
-
// closed-unmerged/reopened 只由 PlatformPoller 对 'git' 任务合成(spec §6 outbox 协议)
|
|
1461
1079
|
if (eventKind === 'closed-unmerged' || eventKind === 'reopened') {
|
|
1462
1080
|
const prNumber = event.data.prNumber;
|
|
1463
1081
|
if (typeof prNumber !== 'number')
|
|
@@ -1466,13 +1084,18 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1466
1084
|
await manager.recordClosedUnmergedAnchor(event.taskId, prNumber);
|
|
1467
1085
|
else
|
|
1468
1086
|
await manager.clearClosedUnmergedAnchor(event.taskId, prNumber);
|
|
1469
|
-
// 无条件投递:滞留的 outbox 条目借重复观察重试,而不是等到重启(spec §6 至少一次)
|
|
1470
1087
|
await manager.deliverTaskOutbox(event.taskId);
|
|
1471
1088
|
return;
|
|
1472
1089
|
}
|
|
1473
|
-
if (
|
|
1090
|
+
if (!event.agentId)
|
|
1091
|
+
return;
|
|
1092
|
+
if (taskAtEntry?.reviewMode === 'server')
|
|
1093
|
+
return;
|
|
1094
|
+
if (eventKind === 'comment' || eventKind === 'review-comment') {
|
|
1474
1095
|
return handlePrFeedback(bus, manager, event);
|
|
1475
|
-
|
|
1096
|
+
}
|
|
1097
|
+
if (eventKind === 'push' || eventKind === undefined)
|
|
1098
|
+
return handlePrCodePush(bus, manager, event);
|
|
1476
1099
|
});
|
|
1477
1100
|
bus.on('pr.merged', async (event) => {
|
|
1478
1101
|
if (!event.taskId)
|
|
@@ -1526,36 +1149,55 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1526
1149
|
bus.on('review.submitted', async (event) => {
|
|
1527
1150
|
if (!event.taskId)
|
|
1528
1151
|
return;
|
|
1529
|
-
const
|
|
1530
|
-
let task = await manager.getTask(event.taskId);
|
|
1152
|
+
const task = await manager.getTask(event.taskId);
|
|
1531
1153
|
if (!task)
|
|
1532
1154
|
return;
|
|
1533
1155
|
if (task.reviewMode === 'server')
|
|
1534
1156
|
return;
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
console.log(`[EventHandler] review.submitted pane verdict ignored for git task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1157
|
+
if (event.data.source === 'pane-signal') {
|
|
1158
|
+
console.log(`[EventHandler] review.submitted pane verdict ignored for task ${task.id} (comment-token protocol is the sole authority)`);
|
|
1538
1159
|
return;
|
|
1539
1160
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1161
|
+
const verdictTask = task;
|
|
1162
|
+
const rejectGitPayload = async (err) => {
|
|
1163
|
+
await emitIntervention(bus, verdictTask.projectId, verdictTask.agentId, verdictTask.id, {
|
|
1164
|
+
phase: 'git-verdict-payload-invalid',
|
|
1165
|
+
error: err.message,
|
|
1166
|
+
...(typeof event.data.source === 'string' ? { source: event.data.source } : {}),
|
|
1167
|
+
});
|
|
1168
|
+
};
|
|
1169
|
+
let gitPayload;
|
|
1170
|
+
try {
|
|
1171
|
+
gitPayload = parseGitVerdictPayload(event.data);
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
if (!(err instanceof GitVerdictPayloadError))
|
|
1175
|
+
throw err;
|
|
1176
|
+
await rejectGitPayload(err);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const action = gitPayload.action;
|
|
1180
|
+
if (task.signalToken === undefined || gitPayload.reviewPassToken !== task.signalToken) {
|
|
1181
|
+
if (task.status !== 'review') {
|
|
1182
|
+
await rejectGitPayload(new GitVerdictPayloadError('review pass token is not current'));
|
|
1183
|
+
return;
|
|
1558
1184
|
}
|
|
1185
|
+
if (!(await redispatchReviewForStaleVerdict(manager, task))) {
|
|
1186
|
+
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1187
|
+
phase: 'stale-verdict-wrong-pass',
|
|
1188
|
+
action,
|
|
1189
|
+
verdictPassToken: gitPayload.reviewPassToken,
|
|
1190
|
+
currentToken: task.signalToken,
|
|
1191
|
+
source: 'poller',
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const expectedVerdictToken = action === 'APPROVE' ? task.passToken : task.failToken;
|
|
1197
|
+
if (expectedVerdictToken === undefined || gitPayload.verdictToken !== expectedVerdictToken
|
|
1198
|
+
|| task.failToken === undefined) {
|
|
1199
|
+
await rejectGitPayload(new GitVerdictPayloadError('verdict token is not current'));
|
|
1200
|
+
return;
|
|
1559
1201
|
}
|
|
1560
1202
|
if (isSpecStagePhase(task.phase)) {
|
|
1561
1203
|
console.warn(`[EventHandler] review.submitted (action=${action}) ignored for task ${task.id}: task in spec phase`);
|
|
@@ -1578,72 +1220,18 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1578
1220
|
return;
|
|
1579
1221
|
}
|
|
1580
1222
|
}
|
|
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
1223
|
const prPatch = {
|
|
1598
|
-
...(
|
|
1599
|
-
...(
|
|
1224
|
+
...(gitPayload.prNumber !== task.prNumber ? { prNumber: gitPayload.prNumber } : {}),
|
|
1225
|
+
...(gitPayload.prUrl !== task.prUrl ? { prUrl: gitPayload.prUrl } : {}),
|
|
1600
1226
|
};
|
|
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
|
-
}
|
|
1227
|
+
const reviewedHeadSha = gitPayload.headSha;
|
|
1228
|
+
const currentHeadSha = gitPayload.currentHeadSha;
|
|
1229
|
+
if (task.status !== 'review') {
|
|
1230
|
+
await rejectGitPayload(new GitVerdictPayloadError(`task is ${task.status}, not review`));
|
|
1231
|
+
return;
|
|
1644
1232
|
}
|
|
1645
1233
|
if (action === 'APPROVE') {
|
|
1646
|
-
return handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch,
|
|
1234
|
+
return handleReviewApproval(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch, gitPayload);
|
|
1647
1235
|
}
|
|
1648
1236
|
if (action === 'REQUEST_CHANGES') {
|
|
1649
1237
|
return handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, currentHeadSha, prPatch);
|
|
@@ -1658,14 +1246,12 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1658
1246
|
const { projectId, agentId } = task;
|
|
1659
1247
|
const stayFixing = async (data) => {
|
|
1660
1248
|
await emitIntervention(bus, projectId, agentId, task.id, data);
|
|
1661
|
-
// 同代 fence:平台查询等待期间任务可能已被手动重派进 review——旧失败分支不得覆盖
|
|
1662
|
-
// successor 的 QA/reconciliation watcher
|
|
1663
1249
|
const fresh = await manager.getTask(task.id);
|
|
1664
1250
|
if (!fresh || fresh.status !== 'fixing' || fresh.signalToken !== task.signalToken)
|
|
1665
1251
|
return;
|
|
1666
1252
|
await manager.setupPhaseSignal(task.id, agentId, 'pr-fixed', {
|
|
1667
1253
|
skipSnapshot: true,
|
|
1668
|
-
|
|
1254
|
+
replaceScope: 'agent',
|
|
1669
1255
|
});
|
|
1670
1256
|
};
|
|
1671
1257
|
const token = typeof event.data.token === 'string' ? event.data.token : undefined;
|
|
@@ -1677,92 +1263,52 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1677
1263
|
});
|
|
1678
1264
|
return;
|
|
1679
1265
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1266
|
+
if (task.prNumber === undefined) {
|
|
1267
|
+
await stayFixing({ phase: 'fix-verify-no-anchor' });
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
let verify;
|
|
1271
|
+
try {
|
|
1272
|
+
verify = await manager.platformVerifyPrBinding(task.id, task.prNumber);
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
await stayFixing({
|
|
1276
|
+
phase: 'fix-verify-head-fetch-failed',
|
|
1277
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1278
|
+
});
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (!verify.ok) {
|
|
1282
|
+
// PR 在修复期间被关闭/转 draft/retarget:推进 recheck 只会造出无裁决入口的评审轮
|
|
1283
|
+
await stayFixing({ phase: 'fix-verify-binding-mismatch', reason: verify.reason });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const headSha = verify.headSha;
|
|
1287
|
+
if (!task.reviewHeadAnchorSha) {
|
|
1288
|
+
await stayFixing({ phase: 'fix-verify-no-anchor', headSha });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (headSha === task.reviewHeadAnchorSha) {
|
|
1292
|
+
let pendingCount;
|
|
1687
1293
|
try {
|
|
1688
|
-
|
|
1294
|
+
pendingCount = (await manager.platformPendingFeedback(task.id)).pending.size;
|
|
1689
1295
|
}
|
|
1690
1296
|
catch (err) {
|
|
1691
1297
|
await stayFixing({
|
|
1692
|
-
phase: 'fix-verify-
|
|
1298
|
+
phase: 'fix-verify-replies-fetch-failed',
|
|
1693
1299
|
error: err instanceof Error ? err.message : String(err),
|
|
1694
1300
|
});
|
|
1695
1301
|
return;
|
|
1696
1302
|
}
|
|
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) {
|
|
1303
|
+
if (pendingCount > 0) {
|
|
1709
1304
|
await stayFixing({
|
|
1710
|
-
phase: 'fix-
|
|
1711
|
-
|
|
1305
|
+
phase: 'fix-no-op-pending-feedback',
|
|
1306
|
+
pendingCount,
|
|
1307
|
+
note: 'Dev emitted pr-fixed on an unchanged head while feedback revisions still lack a valid ack. Inspect the PR replies.',
|
|
1712
1308
|
});
|
|
1713
1309
|
return;
|
|
1714
1310
|
}
|
|
1715
1311
|
}
|
|
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
1312
|
// head fetch / 回复检查是秒级网络等待,期间真 push / verdict 可能已换代 pass;旧信号不得再驱动派发
|
|
1767
1313
|
const preEmit = await manager.getTask(task.id);
|
|
1768
1314
|
if (!preEmit || preEmit.status !== 'fixing' || preEmit.signalToken !== token)
|