baxian 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/diff-split.d.ts +10 -0
- package/dist/agent/diff-split.d.ts.map +1 -0
- package/dist/agent/diff-split.js +83 -0
- package/dist/agent/diff-split.js.map +1 -0
- package/dist/agent/manager.d.ts +75 -12
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +1121 -320
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +7 -1
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +37 -11
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +29 -11
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +38 -8
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/prompt.d.ts +15 -2
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +250 -52
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +0 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +0 -25
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.d.ts +36 -0
- package/dist/agent/review-transport.d.ts.map +1 -0
- package/dist/agent/review-transport.js +246 -0
- package/dist/agent/review-transport.js.map +1 -0
- package/dist/agent/worktree.d.ts +2 -0
- package/dist/agent/worktree.d.ts.map +1 -1
- package/dist/agent/worktree.js +26 -0
- package/dist/agent/worktree.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +4 -0
- package/dist/api/agents.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +8 -0
- package/dist/api/tasks.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -1
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.js +23 -0
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +33 -451
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.d.ts +4 -0
- package/dist/event/server-handlers.d.ts.map +1 -0
- package/dist/event/server-handlers.js +835 -0
- package/dist/event/server-handlers.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.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 +26 -8
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +64 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/skills/server-feedback/SKILL.md +34 -0
- package/dist/skills/server-recheck/SKILL.md +30 -0
- package/dist/skills/server-review/SKILL.md +43 -0
- package/dist/skills/server-spec-review/SKILL.md +31 -0
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +1 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/review-store.d.ts +13 -0
- package/dist/state/review-store.d.ts.map +1 -0
- package/dist/state/review-store.js +92 -0
- package/dist/state/review-store.js.map +1 -0
- package/dist/state/snapshot.js +1 -1
- package/dist/state/snapshot.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +1 -0
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +8 -2
- package/dist/terminal/attach.js.map +1 -1
- package/dist/web/assets/index-OtgjyQI1.js +4 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-ByNjLidI.js +0 -4
package/dist/event/handlers.js
CHANGED
|
@@ -8,7 +8,7 @@ function validHeadSha(value) {
|
|
|
8
8
|
// Re-establish the develop-phase watcher after a handler-side rejection so the same
|
|
9
9
|
// task can consume a corrected emit (same token) without a server restart.
|
|
10
10
|
async function reArmDevelopWatcher(manager, task, agentId) {
|
|
11
|
-
const kinds = task.phase === 'code' ? ['pr-created'] : ['spec-
|
|
11
|
+
const kinds = task.phase === 'code' ? ['pr-created'] : ['spec-done', 'pr-created'];
|
|
12
12
|
await manager.setupPhaseSignal(task.id, agentId, kinds);
|
|
13
13
|
}
|
|
14
14
|
async function emitIntervention(bus, projectId, agentId, taskId, data) {
|
|
@@ -127,6 +127,10 @@ async function dispatchDevPostApproveCheck(bus, manager, task, approvedHeadSha,
|
|
|
127
127
|
...(dispatchErr ? { error: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr) } : {}),
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
async function isServerModeTask(manager, taskId) {
|
|
131
|
+
const task = await manager.getTask(taskId);
|
|
132
|
+
return task?.reviewMode === 'server';
|
|
133
|
+
}
|
|
130
134
|
async function gateDevForPostApproveRedispatch(bus, manager, task) {
|
|
131
135
|
const ready = await manager
|
|
132
136
|
.releaseAgentForTask(task.agentId, task.id, 'waiting')
|
|
@@ -146,15 +150,15 @@ export function registerEventHandlers(bus, manager) {
|
|
|
146
150
|
bus.on('pr.created', async (event) => {
|
|
147
151
|
if (!event.taskId || !event.agentId)
|
|
148
152
|
return;
|
|
153
|
+
if (await isServerModeTask(manager, event.taskId))
|
|
154
|
+
return;
|
|
149
155
|
// pane-signal pr.created carries data.prNumber (extracted from the agent's
|
|
150
156
|
// [bx:pr-created:<num>:<token>] signal) but no prUrl / headSha. The
|
|
151
157
|
// transition only needs prNumber to wire task→PR; prUrl is derivable from
|
|
152
158
|
// repo+number; headSha lands via the next pr.updated push event.
|
|
153
159
|
//
|
|
154
|
-
// spec phase
|
|
155
|
-
//
|
|
156
|
-
// QA review。否则 QA 在 spec-review 槽位上跑 pr-review 流程,再 gh pr review --approve
|
|
157
|
-
// → review.submitted → post-approve dispatch,实际代码还没写。
|
|
160
|
+
// spec phase 由 server 评审链(server.spec.* handlers)驱动;poller 不应越过它派 QA review,
|
|
161
|
+
// 否则 QA 在 spec-review 槽位上跑 pr-review 流程,gh pr review --approve 会误触 post-approve。
|
|
158
162
|
{
|
|
159
163
|
const taskNow = await manager.getTask(event.taskId);
|
|
160
164
|
if (taskNow?.phase === 'spec') {
|
|
@@ -327,12 +331,17 @@ export function registerEventHandlers(bus, manager) {
|
|
|
327
331
|
bus.on('pr.updated', async (event) => {
|
|
328
332
|
if (!event.taskId || !event.agentId)
|
|
329
333
|
return;
|
|
334
|
+
// Server tasks review via the exchange protocol — a poller-observed sync on
|
|
335
|
+
// the published PR must not drag them into legacy QA review (PR #288).
|
|
336
|
+
// pr.merged stays open: external merges of a ready PR finish through it.
|
|
337
|
+
if (await isServerModeTask(manager, event.taskId))
|
|
338
|
+
return;
|
|
330
339
|
const eventPrNumber = event.data.prNumber;
|
|
331
340
|
const eventPrUrl = event.data.prUrl;
|
|
332
341
|
const eventKind = event.data.kind;
|
|
333
|
-
// spec phase 由
|
|
334
|
-
//
|
|
335
|
-
// pr-merge-ready 是 dev
|
|
342
|
+
// spec phase 由 server 评审链驱动;spec doc 的 push/comment 不应进入 code-review 流程
|
|
343
|
+
// (避免 QA recheck → gh pr review --approve → 误派 post-approve)。
|
|
344
|
+
// pr-merge-ready 是 dev 内部状态推进,不受 phase gate 限制。
|
|
336
345
|
if (eventKind !== 'pr-merge-ready') {
|
|
337
346
|
const taskNow = await manager.getTask(event.taskId);
|
|
338
347
|
if (taskNow?.phase === 'spec') {
|
|
@@ -401,25 +410,12 @@ export function registerEventHandlers(bus, manager) {
|
|
|
401
410
|
await dispatchDevPostApproveCheck(bus, manager, freshTask, freshCompletion.approvedHeadSha, { redispatchCount: nextCount });
|
|
402
411
|
return;
|
|
403
412
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
catch (err) {
|
|
411
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
412
|
-
console.error(`[EventHandler] post-approve auto-merge failed for task=${freshTask.id}:`, err);
|
|
413
|
-
await emitIntervention(bus, freshTask.projectId, freshTask.agentId, freshTask.id, {
|
|
414
|
-
phase: 'merge-failed',
|
|
415
|
-
error: message,
|
|
416
|
-
});
|
|
417
|
-
throw err;
|
|
418
|
-
}
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
// Manual merge: checks passed but baxian won't merge — surface merge-ready for the operator.
|
|
422
|
-
const readied = await manager.transitionTaskStatus(freshTask.id, 'merge-ready', { fromStatus: ['approved'] });
|
|
413
|
+
// Human gate (spec §10): checks passed — surface merge-ready and wait for the
|
|
414
|
+
// operator. merge:'auto' no longer merges here; it decides what the confirm
|
|
415
|
+
// endpoint executes (server merges on confirm vs human merges by hand).
|
|
416
|
+
// Persist the post-approve head: confirm's merge guards on it so a push
|
|
417
|
+
// landing inside the gate window can never be merged blind (PR #288).
|
|
418
|
+
const readied = await manager.transitionTaskStatus(freshTask.id, 'merge-ready', { fromStatus: ['approved'] }, { latestHeadSha: freshCompletion.approvedHeadSha });
|
|
423
419
|
if (readied) {
|
|
424
420
|
await manager.clearPostApproveCompletionIfMatches(freshTask.id, signalToken);
|
|
425
421
|
}
|
|
@@ -579,7 +575,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
579
575
|
// No armed watcher → a same-identity verdict would have no consumer.
|
|
580
576
|
console.warn(`[EventHandler] pr.updated verdict watcher failed to arm for task=${transitioned.id} (${qaPhase}); rolling back recheck dispatch`);
|
|
581
577
|
if (previousStatus === 'in_progress' || previousStatus === 'fixing') {
|
|
582
|
-
// Full rollback: the dev's prior-phase prompt (spec-
|
|
578
|
+
// Full rollback: the dev's prior-phase prompt (spec-done/pr-created or pr-fixed) used the
|
|
583
579
|
// pre-rotation token; restore status+token+anchor and re-arm so its already-emitted signal
|
|
584
580
|
// isn't stranded by the token rotation.
|
|
585
581
|
await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
@@ -681,7 +677,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
681
677
|
const result = await manager.transitionTaskStatus(event.taskId, 'merged',
|
|
682
678
|
// max_rounds included so manual mark-complete (and an externally-merged
|
|
683
679
|
// max_rounds PR the poller detects) transitions to merged + runs cleanup.
|
|
684
|
-
|
|
680
|
+
// ready included for server-mode afterDone:'pr' tasks whose managed PR is
|
|
681
|
+
// merged directly on GitHub instead of via baxian's Confirm (PR #288).
|
|
682
|
+
{ fromStatus: ['in_progress', 'fixing', 'review', 'approved', 'merge-ready', 'ready', 'max_rounds'] }, prPatch);
|
|
685
683
|
if (!result)
|
|
686
684
|
return;
|
|
687
685
|
const { task: transitioned } = result;
|
|
@@ -715,6 +713,8 @@ export function registerEventHandlers(bus, manager) {
|
|
|
715
713
|
bus.on('review.submitted', async (event) => {
|
|
716
714
|
if (!event.taskId)
|
|
717
715
|
return;
|
|
716
|
+
if (await isServerModeTask(manager, event.taskId))
|
|
717
|
+
return;
|
|
718
718
|
const action = event.data.action;
|
|
719
719
|
let task = await manager.getTask(event.taskId);
|
|
720
720
|
if (!task)
|
|
@@ -744,10 +744,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
746
|
}
|
|
747
|
-
// spec phase(非 terminal)由
|
|
748
|
-
// PR review 不应改 task.status
|
|
749
|
-
//
|
|
750
|
-
// 释放,此处只屏蔽 active spec 路径。
|
|
747
|
+
// spec phase(非 terminal)由 server 评审链(server.spec.review.submitted)驱动 verdict;
|
|
748
|
+
// GitHub PR review 不应改 task.status,早退避免误推 approved + 派 dev post-approve。
|
|
749
|
+
// terminal 状态已在前面兜底释放,此处只屏蔽 active spec 路径。
|
|
751
750
|
if (task.phase === 'spec') {
|
|
752
751
|
console.warn(`[EventHandler] review.submitted (action=${action}) ignored for task ${task.id}: task in spec phase`);
|
|
753
752
|
return;
|
|
@@ -1083,222 +1082,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1083
1082
|
}
|
|
1084
1083
|
}
|
|
1085
1084
|
});
|
|
1086
|
-
bus.on('spec.ready', async (event) => {
|
|
1087
|
-
if (!event.taskId)
|
|
1088
|
-
return;
|
|
1089
|
-
const task = await manager.getTask(event.taskId);
|
|
1090
|
-
if (!task)
|
|
1091
|
-
return;
|
|
1092
|
-
// Freshness gate: 拒迟到 / scrollback 复活的 stale spec-created signal。
|
|
1093
|
-
// 期望 task 仍在 pre-spec 阶段 (phase undefined, status in_progress) 且 token 匹配。
|
|
1094
|
-
const eventToken = event.data?.token;
|
|
1095
|
-
const stale = task.phase !== undefined
|
|
1096
|
-
|| task.status !== 'in_progress'
|
|
1097
|
-
|| !eventToken
|
|
1098
|
-
|| eventToken !== task.signalToken;
|
|
1099
|
-
if (stale) {
|
|
1100
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1101
|
-
phase: 'spec-created-event-stale',
|
|
1102
|
-
taskPhase: task.phase ?? null,
|
|
1103
|
-
taskStatus: task.status,
|
|
1104
|
-
});
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
try {
|
|
1108
|
-
await manager.dispatchSpecReviewToQa(event.taskId);
|
|
1109
|
-
}
|
|
1110
|
-
catch (err) {
|
|
1111
|
-
console.error(`[EventHandler] spec.ready dispatchSpecReviewToQa(${event.taskId}) failed:`, err);
|
|
1112
|
-
await emitIntervention(bus, event.projectId, event.agentId ?? '', event.taskId, {
|
|
1113
|
-
phase: 'spec-created-dispatch-failed',
|
|
1114
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
bus.on('spec.review.submitted', async (event) => {
|
|
1119
|
-
if (!event.taskId)
|
|
1120
|
-
return;
|
|
1121
|
-
const task = await manager.getTask(event.taskId);
|
|
1122
|
-
if (!task)
|
|
1123
|
-
return;
|
|
1124
|
-
// Freshness gate: stale signal would inject old findings into a code-phase session.
|
|
1125
|
-
const eventToken = event.data?.token;
|
|
1126
|
-
const stale = task.phase !== 'spec'
|
|
1127
|
-
|| task.status !== 'review'
|
|
1128
|
-
|| !eventToken
|
|
1129
|
-
|| eventToken !== task.signalToken;
|
|
1130
|
-
if (stale) {
|
|
1131
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1132
|
-
phase: 'spec-review-event-stale',
|
|
1133
|
-
taskPhase: task.phase ?? null,
|
|
1134
|
-
taskStatus: task.status,
|
|
1135
|
-
});
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
const round = task.specReviewRound ?? 1;
|
|
1139
|
-
// Verdict comes from the signal kind directly — no findings.json read for
|
|
1140
|
-
// the verdict bit. findings.json is still read later (during spec-fix
|
|
1141
|
-
// dispatch) for the issue list, but its schema no longer carries a verdict.
|
|
1142
|
-
const signalKind = event.data?.kind;
|
|
1143
|
-
if (signalKind === 'spec-approved') {
|
|
1144
|
-
try {
|
|
1145
|
-
await manager.transitionToCodePhase(task.id);
|
|
1146
|
-
}
|
|
1147
|
-
catch (err) {
|
|
1148
|
-
console.error(`[EventHandler] spec.review approve transitionToCodePhase(${task.id}) failed:`, err);
|
|
1149
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1150
|
-
phase: 'spec-review-approve-transition-failed',
|
|
1151
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (signalKind !== 'spec-changes-requested') {
|
|
1157
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1158
|
-
phase: 'spec-review-unknown-verdict-signal',
|
|
1159
|
-
round,
|
|
1160
|
-
signalKind: signalKind ?? null,
|
|
1161
|
-
});
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
// changes-requested path: load findings.json for the issue list (verdict
|
|
1165
|
-
// already known from the signal kind above).
|
|
1166
|
-
const fileName = `round-${round}-findings.json`;
|
|
1167
|
-
let raw = null;
|
|
1168
|
-
try {
|
|
1169
|
-
raw = await manager.readSpecReviewFile(event.taskId, fileName);
|
|
1170
|
-
}
|
|
1171
|
-
catch (err) {
|
|
1172
|
-
console.error(`[EventHandler] spec.review.submitted readSpecReviewFile(${event.taskId}) failed:`, err);
|
|
1173
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1174
|
-
phase: 'spec-review-findings-read-failed',
|
|
1175
|
-
round,
|
|
1176
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1177
|
-
});
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
if (raw === null) {
|
|
1181
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1182
|
-
phase: 'spec-review-findings-missing',
|
|
1183
|
-
round,
|
|
1184
|
-
fileName,
|
|
1185
|
-
});
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
let parsed;
|
|
1189
|
-
try {
|
|
1190
|
-
parsed = JSON.parse(raw);
|
|
1191
|
-
}
|
|
1192
|
-
catch (err) {
|
|
1193
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1194
|
-
phase: 'spec-review-findings-invalid-json',
|
|
1195
|
-
round,
|
|
1196
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1197
|
-
});
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
// Round mismatch ⇒ QA wrote wrong-round content into current findings file.
|
|
1201
|
-
if (typeof parsed.round !== 'number' || parsed.round !== round) {
|
|
1202
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1203
|
-
phase: 'spec-review-findings-round-mismatch',
|
|
1204
|
-
round,
|
|
1205
|
-
parsedRound: parsed.round ?? null,
|
|
1206
|
-
});
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
const cap = manager.getConfig().review.rounds;
|
|
1210
|
-
if (round >= cap) {
|
|
1211
|
-
const result = await manager.transitionTaskStatus(event.taskId, 'max_rounds', { fromStatus: ['review'] });
|
|
1212
|
-
if (!result)
|
|
1213
|
-
return;
|
|
1214
|
-
const { task: transitioned } = result;
|
|
1215
|
-
manager.stopSpecSignalWatcher(transitioned.id);
|
|
1216
|
-
// release 失败时 binding 残留 → 后续 acquire 会拒;emit intervention 让 stale binding 可见。
|
|
1217
|
-
// spec max_rounds 同样暂停为 active 态:清掉已成功释放(解绑)的 agent 引用,否则该已释放
|
|
1218
|
-
// agent 之后因 tmux/recovery 失败会经 failTasksForAgent 把这个暂停 task 误标 failed。仅当释放
|
|
1219
|
-
// 成功(确已解绑)才清——若仍 held/bound 则保留引用,让其故障正常归因到本 task。
|
|
1220
|
-
const clearIds = {};
|
|
1221
|
-
if (transitioned.qaAgentId) {
|
|
1222
|
-
const qaReleased = await manager
|
|
1223
|
-
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle')
|
|
1224
|
-
.catch(() => false);
|
|
1225
|
-
if (qaReleased) {
|
|
1226
|
-
clearIds.qaAgentId = undefined;
|
|
1227
|
-
}
|
|
1228
|
-
else {
|
|
1229
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.qaAgentId, transitioned.id, {
|
|
1230
|
-
phase: 'spec-review-max-rounds-qa-release-failed',
|
|
1231
|
-
qaAgentId: transitioned.qaAgentId,
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
if (transitioned.agentId) {
|
|
1236
|
-
const devReleased = await manager
|
|
1237
|
-
.releaseAgentForTask(transitioned.agentId, transitioned.id, 'idle')
|
|
1238
|
-
.catch(() => false);
|
|
1239
|
-
if (devReleased) {
|
|
1240
|
-
clearIds.agentId = undefined;
|
|
1241
|
-
}
|
|
1242
|
-
else {
|
|
1243
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1244
|
-
phase: 'spec-review-max-rounds-dev-release-failed',
|
|
1245
|
-
devAgentId: transitioned.agentId,
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
if ('qaAgentId' in clearIds || 'agentId' in clearIds) {
|
|
1250
|
-
await manager.updateTask(transitioned.id, clearIds)
|
|
1251
|
-
.catch(err => console.error(`[EventHandler] spec max_rounds clear agent ids(${transitioned.id}) failed:`, err));
|
|
1252
|
-
}
|
|
1253
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1254
|
-
phase: 'spec-review-max-rounds',
|
|
1255
|
-
round,
|
|
1256
|
-
cap,
|
|
1257
|
-
});
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
// changes-requested 必须有非空 findings — 提前 fail-loud。
|
|
1261
|
-
if (!Array.isArray(parsed.findings) || parsed.findings.length === 0) {
|
|
1262
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1263
|
-
phase: 'spec-review-findings-invalid-shape',
|
|
1264
|
-
round,
|
|
1265
|
-
findingsType: Array.isArray(parsed.findings) ? 'empty-array' : typeof parsed.findings,
|
|
1266
|
-
});
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
// 每条 finding 必须有唯一非空 id — coverage 校验依赖它。fail-closed。
|
|
1270
|
-
const findingsArr = parsed.findings;
|
|
1271
|
-
const idSet = new Set();
|
|
1272
|
-
for (const f of findingsArr) {
|
|
1273
|
-
const id = f?.id;
|
|
1274
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
1275
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1276
|
-
phase: 'spec-review-findings-missing-id',
|
|
1277
|
-
round,
|
|
1278
|
-
});
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
if (idSet.has(id)) {
|
|
1282
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1283
|
-
phase: 'spec-review-findings-duplicate-id',
|
|
1284
|
-
round,
|
|
1285
|
-
duplicateId: id,
|
|
1286
|
-
});
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
idSet.add(id);
|
|
1290
|
-
}
|
|
1291
|
-
try {
|
|
1292
|
-
await manager.dispatchSpecFixToDev(task.id, raw);
|
|
1293
|
-
}
|
|
1294
|
-
catch (err) {
|
|
1295
|
-
console.error(`[EventHandler] spec.review dispatchSpecFixToDev(${task.id}) failed:`, err);
|
|
1296
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1297
|
-
phase: 'spec-fix-dispatch-failed',
|
|
1298
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
1085
|
// Dev emitted pr-fixed: it claims the fixing round is done. Verify on GitHub
|
|
1303
1086
|
// before advancing (option C) — a new commit OR a reply to the findings means
|
|
1304
1087
|
// real work; neither means a no-op claim (the task-060 lie). Every GitHub read
|
|
@@ -1307,7 +1090,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1307
1090
|
if (!event.taskId || !event.agentId)
|
|
1308
1091
|
return;
|
|
1309
1092
|
const task = await manager.getTask(event.taskId);
|
|
1310
|
-
if (!task || task.status !== 'fixing' || task.phase === 'spec')
|
|
1093
|
+
if (!task || task.reviewMode === 'server' || task.status !== 'fixing' || task.phase === 'spec')
|
|
1311
1094
|
return;
|
|
1312
1095
|
const { projectId, agentId } = task;
|
|
1313
1096
|
// The watcher is one-shot and already consumed this pr-fixed. For paths that
|
|
@@ -1405,206 +1188,5 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1405
1188
|
});
|
|
1406
1189
|
}
|
|
1407
1190
|
});
|
|
1408
|
-
bus.on('spec.fix.submitted', async (event) => {
|
|
1409
|
-
if (!event.taskId)
|
|
1410
|
-
return;
|
|
1411
|
-
const task = await manager.getTask(event.taskId);
|
|
1412
|
-
if (!task)
|
|
1413
|
-
return;
|
|
1414
|
-
// Freshness gate: stale signal may re-trigger dispatch after spec phase exit.
|
|
1415
|
-
const eventToken = event.data?.token;
|
|
1416
|
-
const stale = task.phase !== 'spec'
|
|
1417
|
-
|| task.status !== 'fixing'
|
|
1418
|
-
|| !eventToken
|
|
1419
|
-
|| eventToken !== task.signalToken;
|
|
1420
|
-
if (stale) {
|
|
1421
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1422
|
-
phase: 'spec-fix-event-stale',
|
|
1423
|
-
taskPhase: task.phase ?? null,
|
|
1424
|
-
taskStatus: task.status,
|
|
1425
|
-
});
|
|
1426
|
-
return;
|
|
1427
|
-
}
|
|
1428
|
-
const round = task.specReviewRound ?? 1;
|
|
1429
|
-
const fileName = `round-${round}-response.json`;
|
|
1430
|
-
let raw = null;
|
|
1431
|
-
try {
|
|
1432
|
-
raw = await manager.readSpecReviewFile(event.taskId, fileName);
|
|
1433
|
-
}
|
|
1434
|
-
catch (err) {
|
|
1435
|
-
console.error(`[EventHandler] spec.fix.submitted readSpecReviewFile(${event.taskId}) failed:`, err);
|
|
1436
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1437
|
-
phase: 'spec-fix-response-read-failed',
|
|
1438
|
-
round,
|
|
1439
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1440
|
-
});
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
if (raw === null) {
|
|
1444
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1445
|
-
phase: 'spec-fix-response-missing',
|
|
1446
|
-
round,
|
|
1447
|
-
fileName,
|
|
1448
|
-
});
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
let parsed;
|
|
1452
|
-
try {
|
|
1453
|
-
parsed = JSON.parse(raw);
|
|
1454
|
-
}
|
|
1455
|
-
catch (err) {
|
|
1456
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1457
|
-
phase: 'spec-fix-response-invalid-json',
|
|
1458
|
-
round,
|
|
1459
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1460
|
-
});
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
if (typeof parsed.round !== 'number' || parsed.round !== round) {
|
|
1464
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1465
|
-
phase: 'spec-fix-response-round-mismatch',
|
|
1466
|
-
round,
|
|
1467
|
-
parsedRound: parsed.round ?? null,
|
|
1468
|
-
});
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
if (!Array.isArray(parsed.responses)) {
|
|
1472
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1473
|
-
phase: 'spec-fix-response-invalid-shape',
|
|
1474
|
-
round,
|
|
1475
|
-
});
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
const responses = parsed.responses;
|
|
1479
|
-
if (responses.length === 0) {
|
|
1480
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1481
|
-
phase: 'spec-fix-response-empty',
|
|
1482
|
-
round,
|
|
1483
|
-
});
|
|
1484
|
-
return;
|
|
1485
|
-
}
|
|
1486
|
-
for (const r of responses) {
|
|
1487
|
-
if (r?.action !== 'fix' && r?.action !== 'reject') {
|
|
1488
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1489
|
-
phase: 'spec-fix-response-invalid-action',
|
|
1490
|
-
round,
|
|
1491
|
-
action: r?.action ?? null,
|
|
1492
|
-
});
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
// Coverage check: response must cover every finding.id; all branches fail-closed
|
|
1497
|
-
// (read/parse/round-mismatch all emit + return — fail-open would let all-reject sneak into code phase).
|
|
1498
|
-
let findingsRaw;
|
|
1499
|
-
try {
|
|
1500
|
-
findingsRaw = await manager.readSpecReviewFile(event.taskId, `round-${round}-findings.json`);
|
|
1501
|
-
}
|
|
1502
|
-
catch (err) {
|
|
1503
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1504
|
-
phase: 'spec-fix-coverage-findings-read-failed',
|
|
1505
|
-
round,
|
|
1506
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1507
|
-
});
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
if (findingsRaw === null) {
|
|
1511
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1512
|
-
phase: 'spec-fix-coverage-findings-missing',
|
|
1513
|
-
round,
|
|
1514
|
-
});
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
let findingsParsed;
|
|
1518
|
-
try {
|
|
1519
|
-
findingsParsed = JSON.parse(findingsRaw);
|
|
1520
|
-
}
|
|
1521
|
-
catch (err) {
|
|
1522
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1523
|
-
phase: 'spec-fix-coverage-findings-invalid-json',
|
|
1524
|
-
round,
|
|
1525
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1526
|
-
});
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
if (typeof findingsParsed.round !== 'number' || findingsParsed.round !== round) {
|
|
1530
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1531
|
-
phase: 'spec-fix-coverage-findings-round-mismatch',
|
|
1532
|
-
round,
|
|
1533
|
-
parsedRound: findingsParsed.round ?? null,
|
|
1534
|
-
});
|
|
1535
|
-
return;
|
|
1536
|
-
}
|
|
1537
|
-
// 独立 fail-closed schema 校验 — 上一阶段的校验对本 handler 不可信。
|
|
1538
|
-
if (!Array.isArray(findingsParsed.findings) || findingsParsed.findings.length === 0) {
|
|
1539
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1540
|
-
phase: 'spec-fix-coverage-findings-invalid-shape',
|
|
1541
|
-
round,
|
|
1542
|
-
findingsType: Array.isArray(findingsParsed.findings)
|
|
1543
|
-
? 'empty-array'
|
|
1544
|
-
: typeof findingsParsed.findings,
|
|
1545
|
-
});
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
const findingIdsList = [];
|
|
1549
|
-
for (const f of findingsParsed.findings) {
|
|
1550
|
-
const id = f?.id;
|
|
1551
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
1552
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1553
|
-
phase: 'spec-fix-coverage-findings-missing-id',
|
|
1554
|
-
round,
|
|
1555
|
-
});
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1558
|
-
if (findingIdsList.includes(id)) {
|
|
1559
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1560
|
-
phase: 'spec-fix-coverage-findings-duplicate-id',
|
|
1561
|
-
round,
|
|
1562
|
-
duplicateId: id,
|
|
1563
|
-
});
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
findingIdsList.push(id);
|
|
1567
|
-
}
|
|
1568
|
-
const findingIds = new Set(findingIdsList);
|
|
1569
|
-
const responseIds = new Set(responses
|
|
1570
|
-
.map(r => r?.findingId)
|
|
1571
|
-
.filter((id) => typeof id === 'string'));
|
|
1572
|
-
const missing = [...findingIds].filter(id => !responseIds.has(id));
|
|
1573
|
-
const unknown = [...responseIds].filter(id => !findingIds.has(id));
|
|
1574
|
-
if (missing.length > 0 || unknown.length > 0) {
|
|
1575
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1576
|
-
phase: 'spec-fix-response-coverage-mismatch',
|
|
1577
|
-
round,
|
|
1578
|
-
missingFindingIds: missing,
|
|
1579
|
-
unknownFindingIds: unknown,
|
|
1580
|
-
});
|
|
1581
|
-
return;
|
|
1582
|
-
}
|
|
1583
|
-
const hasAnyFix = responses.some(r => r.action === 'fix');
|
|
1584
|
-
if (!hasAnyFix) {
|
|
1585
|
-
// 全 reject → dev 直接进 code phase;不再 qa review。
|
|
1586
|
-
try {
|
|
1587
|
-
await manager.transitionToCodePhase(task.id);
|
|
1588
|
-
}
|
|
1589
|
-
catch (err) {
|
|
1590
|
-
console.error(`[EventHandler] spec.fix all-reject transitionToCodePhase(${task.id}) failed:`, err);
|
|
1591
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1592
|
-
phase: 'spec-fix-all-reject-transition-failed',
|
|
1593
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
try {
|
|
1599
|
-
await manager.dispatchSpecReviewToQa(task.id);
|
|
1600
|
-
}
|
|
1601
|
-
catch (err) {
|
|
1602
|
-
console.error(`[EventHandler] spec.fix dispatchSpecReviewToQa(${task.id}) failed:`, err);
|
|
1603
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1604
|
-
phase: 'spec-fix-redispatch-failed',
|
|
1605
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
});
|
|
1609
1191
|
}
|
|
1610
1192
|
//# sourceMappingURL=handlers.js.map
|