baxian 1.1.0 → 1.2.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/dist/agent/bootstrap.d.ts +1 -1
- package/dist/agent/bootstrap.d.ts.map +1 -1
- package/dist/agent/bootstrap.js +33 -0
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/manager.d.ts +16 -13
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +272 -465
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +1 -4
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +3 -22
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +1 -10
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +5 -8
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +49 -22
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +2 -3
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +35 -67
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +5 -4
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +53 -49
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.js +2 -2
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/agent/tmux-probe-poller.js +4 -4
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.js +9 -9
- package/dist/agent/tmux.js.map +1 -1
- package/dist/agent/worktree.d.ts +1 -0
- package/dist/agent/worktree.d.ts.map +1 -1
- package/dist/agent/worktree.js +14 -0
- package/dist/agent/worktree.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +5 -1
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.js +2 -2
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/tasks.js +2 -2
- 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.js +5 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +37 -3
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +18 -439
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +31 -24
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/github/poller.d.ts.map +1 -1
- package/dist/github/poller.js +13 -3
- package/dist/github/poller.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/shared/constants.d.ts +1 -1
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +1 -7
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/git-url.d.ts +14 -0
- package/dist/shared/git-url.d.ts.map +1 -0
- package/dist/shared/git-url.js +76 -0
- package/dist/shared/git-url.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/types.d.ts +2 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/skills/server-feedback/SKILL.md +4 -2
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +19 -3
- package/dist/terminal/attach.js.map +1 -1
- package/dist/web/assets/index-CC3XRKh1.js +4 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-DE_xpPQe.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) {
|
|
@@ -157,10 +157,8 @@ export function registerEventHandlers(bus, manager) {
|
|
|
157
157
|
// transition only needs prNumber to wire task→PR; prUrl is derivable from
|
|
158
158
|
// repo+number; headSha lands via the next pr.updated push event.
|
|
159
159
|
//
|
|
160
|
-
// spec phase
|
|
161
|
-
//
|
|
162
|
-
// QA review。否则 QA 在 spec-review 槽位上跑 pr-review 流程,再 gh pr review --approve
|
|
163
|
-
// → 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。
|
|
164
162
|
{
|
|
165
163
|
const taskNow = await manager.getTask(event.taskId);
|
|
166
164
|
if (taskNow?.phase === 'spec') {
|
|
@@ -334,16 +332,16 @@ export function registerEventHandlers(bus, manager) {
|
|
|
334
332
|
if (!event.taskId || !event.agentId)
|
|
335
333
|
return;
|
|
336
334
|
// Server tasks review via the exchange protocol — a poller-observed sync on
|
|
337
|
-
// the published PR must not drag them into legacy QA review
|
|
335
|
+
// the published PR must not drag them into legacy QA review.
|
|
338
336
|
// pr.merged stays open: external merges of a ready PR finish through it.
|
|
339
337
|
if (await isServerModeTask(manager, event.taskId))
|
|
340
338
|
return;
|
|
341
339
|
const eventPrNumber = event.data.prNumber;
|
|
342
340
|
const eventPrUrl = event.data.prUrl;
|
|
343
341
|
const eventKind = event.data.kind;
|
|
344
|
-
// spec phase 由
|
|
345
|
-
//
|
|
346
|
-
// 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 限制。
|
|
347
345
|
if (eventKind !== 'pr-merge-ready') {
|
|
348
346
|
const taskNow = await manager.getTask(event.taskId);
|
|
349
347
|
if (taskNow?.phase === 'spec') {
|
|
@@ -416,7 +414,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
416
414
|
// operator. merge:'auto' no longer merges here; it decides what the confirm
|
|
417
415
|
// endpoint executes (server merges on confirm vs human merges by hand).
|
|
418
416
|
// Persist the post-approve head: confirm's merge guards on it so a push
|
|
419
|
-
// landing inside the gate window can never be merged blind
|
|
417
|
+
// landing inside the gate window can never be merged blind.
|
|
420
418
|
const readied = await manager.transitionTaskStatus(freshTask.id, 'merge-ready', { fromStatus: ['approved'] }, { latestHeadSha: freshCompletion.approvedHeadSha });
|
|
421
419
|
if (readied) {
|
|
422
420
|
await manager.clearPostApproveCompletionIfMatches(freshTask.id, signalToken);
|
|
@@ -541,10 +539,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
541
539
|
}
|
|
542
540
|
// Any push once the task is already past initial dispatch (review/fixing/approved)
|
|
543
541
|
// is a re-look → 'recheck'. The recheck prompt is phrased neutrally ("re-check the
|
|
544
|
-
// new commits and any prior feedback"), so it
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
// framing for a push DURING a recheck (Codex review: don't downgrade mid-recheck pushes).
|
|
542
|
+
// new commits and any prior feedback"), so it never falsely claims "dev addressed
|
|
543
|
+
// your prior changes-requested"; keeping 'recheck' for previousStatus='review'
|
|
544
|
+
// preserves the recheck framing for a push DURING a recheck (don't downgrade it).
|
|
548
545
|
const qaPhase = previousStatus === 'fixing' || previousStatus === 'review'
|
|
549
546
|
|| previousStatus === 'approved' || previousStatus === 'merge-ready'
|
|
550
547
|
? 'recheck'
|
|
@@ -577,7 +574,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
577
574
|
// No armed watcher → a same-identity verdict would have no consumer.
|
|
578
575
|
console.warn(`[EventHandler] pr.updated verdict watcher failed to arm for task=${transitioned.id} (${qaPhase}); rolling back recheck dispatch`);
|
|
579
576
|
if (previousStatus === 'in_progress' || previousStatus === 'fixing') {
|
|
580
|
-
// Full rollback: the dev's prior-phase prompt (spec-
|
|
577
|
+
// Full rollback: the dev's prior-phase prompt (spec-done/pr-created or pr-fixed) used the
|
|
581
578
|
// pre-rotation token; restore status+token+anchor and re-arm so its already-emitted signal
|
|
582
579
|
// isn't stranded by the token rotation.
|
|
583
580
|
await manager.rollbackVerdictArmFailure(transitioned.id, {
|
|
@@ -680,7 +677,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
680
677
|
// max_rounds included so manual mark-complete (and an externally-merged
|
|
681
678
|
// max_rounds PR the poller detects) transitions to merged + runs cleanup.
|
|
682
679
|
// ready included for server-mode afterDone:'pr' tasks whose managed PR is
|
|
683
|
-
// merged directly on GitHub instead of via baxian's Confirm
|
|
680
|
+
// merged directly on GitHub instead of via baxian's Confirm.
|
|
684
681
|
{ fromStatus: ['in_progress', 'fixing', 'review', 'approved', 'merge-ready', 'ready', 'max_rounds'] }, prPatch);
|
|
685
682
|
if (!result)
|
|
686
683
|
return;
|
|
@@ -746,10 +743,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
746
743
|
}
|
|
747
744
|
}
|
|
748
745
|
}
|
|
749
|
-
// spec phase(非 terminal)由
|
|
750
|
-
// PR review 不应改 task.status
|
|
751
|
-
//
|
|
752
|
-
// 释放,此处只屏蔽 active spec 路径。
|
|
746
|
+
// spec phase(非 terminal)由 server 评审链(server.spec.review.submitted)驱动 verdict;
|
|
747
|
+
// GitHub PR review 不应改 task.status,早退避免误推 approved + 派 dev post-approve。
|
|
748
|
+
// terminal 状态已在前面兜底释放,此处只屏蔽 active spec 路径。
|
|
753
749
|
if (task.phase === 'spec') {
|
|
754
750
|
console.warn(`[EventHandler] review.submitted (action=${action}) ignored for task ${task.id}: task in spec phase`);
|
|
755
751
|
return;
|
|
@@ -1045,7 +1041,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1045
1041
|
// Set up the pr-fixed completion watcher BEFORE the prompt. rotateAndSetupPhaseSignal
|
|
1046
1042
|
// rotates the per-pass token atomically; continueSession then reads that token and
|
|
1047
1043
|
// embeds it in the fix prompt, so the dev's pr-fixed signal advances fixing→review
|
|
1048
|
-
// even when the round produced no push
|
|
1044
|
+
// even when the round produced no push.
|
|
1049
1045
|
const { armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
|
|
1050
1046
|
if (!armed) {
|
|
1051
1047
|
// pr-fixed watcher didn't arm → the fix completion signal (esp. the no-push case) would have
|
|
@@ -1085,225 +1081,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1085
1081
|
}
|
|
1086
1082
|
}
|
|
1087
1083
|
});
|
|
1088
|
-
bus.on('spec.ready', async (event) => {
|
|
1089
|
-
if (!event.taskId)
|
|
1090
|
-
return;
|
|
1091
|
-
const task = await manager.getTask(event.taskId);
|
|
1092
|
-
if (!task)
|
|
1093
|
-
return;
|
|
1094
|
-
// Freshness gate: 拒迟到 / scrollback 复活的 stale spec-created signal。
|
|
1095
|
-
// 期望 task 仍在 pre-spec 阶段 (phase undefined, status in_progress) 且 token 匹配。
|
|
1096
|
-
const eventToken = event.data?.token;
|
|
1097
|
-
const stale = task.phase !== undefined
|
|
1098
|
-
|| task.status !== 'in_progress'
|
|
1099
|
-
|| !eventToken
|
|
1100
|
-
|| eventToken !== task.signalToken;
|
|
1101
|
-
if (stale) {
|
|
1102
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1103
|
-
phase: 'spec-created-event-stale',
|
|
1104
|
-
taskPhase: task.phase ?? null,
|
|
1105
|
-
taskStatus: task.status,
|
|
1106
|
-
});
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
try {
|
|
1110
|
-
await manager.dispatchSpecReviewToQa(event.taskId);
|
|
1111
|
-
}
|
|
1112
|
-
catch (err) {
|
|
1113
|
-
console.error(`[EventHandler] spec.ready dispatchSpecReviewToQa(${event.taskId}) failed:`, err);
|
|
1114
|
-
await emitIntervention(bus, event.projectId, event.agentId ?? '', event.taskId, {
|
|
1115
|
-
phase: 'spec-created-dispatch-failed',
|
|
1116
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
bus.on('spec.review.submitted', async (event) => {
|
|
1121
|
-
if (!event.taskId)
|
|
1122
|
-
return;
|
|
1123
|
-
const task = await manager.getTask(event.taskId);
|
|
1124
|
-
if (!task)
|
|
1125
|
-
return;
|
|
1126
|
-
// Freshness gate: stale signal would inject old findings into a code-phase session.
|
|
1127
|
-
const eventToken = event.data?.token;
|
|
1128
|
-
const stale = task.phase !== 'spec'
|
|
1129
|
-
|| task.status !== 'review'
|
|
1130
|
-
|| !eventToken
|
|
1131
|
-
|| eventToken !== task.signalToken;
|
|
1132
|
-
if (stale) {
|
|
1133
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1134
|
-
phase: 'spec-review-event-stale',
|
|
1135
|
-
taskPhase: task.phase ?? null,
|
|
1136
|
-
taskStatus: task.status,
|
|
1137
|
-
});
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
const round = task.specReviewRound ?? 1;
|
|
1141
|
-
// Verdict comes from the signal kind directly — no findings.json read for
|
|
1142
|
-
// the verdict bit. findings.json is still read later (during spec-fix
|
|
1143
|
-
// dispatch) for the issue list, but its schema no longer carries a verdict.
|
|
1144
|
-
const signalKind = event.data?.kind;
|
|
1145
|
-
if (signalKind === 'spec-approved') {
|
|
1146
|
-
try {
|
|
1147
|
-
await manager.transitionToCodePhase(task.id);
|
|
1148
|
-
}
|
|
1149
|
-
catch (err) {
|
|
1150
|
-
console.error(`[EventHandler] spec.review approve transitionToCodePhase(${task.id}) failed:`, err);
|
|
1151
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1152
|
-
phase: 'spec-review-approve-transition-failed',
|
|
1153
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
if (signalKind !== 'spec-changes-requested') {
|
|
1159
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1160
|
-
phase: 'spec-review-unknown-verdict-signal',
|
|
1161
|
-
round,
|
|
1162
|
-
signalKind: signalKind ?? null,
|
|
1163
|
-
});
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
// changes-requested path: load findings.json for the issue list (verdict
|
|
1167
|
-
// already known from the signal kind above).
|
|
1168
|
-
const fileName = `round-${round}-findings.json`;
|
|
1169
|
-
let raw = null;
|
|
1170
|
-
try {
|
|
1171
|
-
raw = await manager.readSpecReviewFile(event.taskId, fileName);
|
|
1172
|
-
}
|
|
1173
|
-
catch (err) {
|
|
1174
|
-
console.error(`[EventHandler] spec.review.submitted readSpecReviewFile(${event.taskId}) failed:`, err);
|
|
1175
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1176
|
-
phase: 'spec-review-findings-read-failed',
|
|
1177
|
-
round,
|
|
1178
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1179
|
-
});
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
if (raw === null) {
|
|
1183
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1184
|
-
phase: 'spec-review-findings-missing',
|
|
1185
|
-
round,
|
|
1186
|
-
fileName,
|
|
1187
|
-
});
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
let parsed;
|
|
1191
|
-
try {
|
|
1192
|
-
parsed = JSON.parse(raw);
|
|
1193
|
-
}
|
|
1194
|
-
catch (err) {
|
|
1195
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1196
|
-
phase: 'spec-review-findings-invalid-json',
|
|
1197
|
-
round,
|
|
1198
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1199
|
-
});
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
// Round mismatch ⇒ QA wrote wrong-round content into current findings file.
|
|
1203
|
-
if (typeof parsed.round !== 'number' || parsed.round !== round) {
|
|
1204
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1205
|
-
phase: 'spec-review-findings-round-mismatch',
|
|
1206
|
-
round,
|
|
1207
|
-
parsedRound: parsed.round ?? null,
|
|
1208
|
-
});
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
const cap = manager.getConfig().review.rounds;
|
|
1212
|
-
if (round >= cap) {
|
|
1213
|
-
const result = await manager.transitionTaskStatus(event.taskId, 'max_rounds', { fromStatus: ['review'] });
|
|
1214
|
-
if (!result)
|
|
1215
|
-
return;
|
|
1216
|
-
const { task: transitioned } = result;
|
|
1217
|
-
manager.stopSpecSignalWatcher(transitioned.id);
|
|
1218
|
-
// release 失败时 binding 残留 → 后续 acquire 会拒;emit intervention 让 stale binding 可见。
|
|
1219
|
-
// spec max_rounds 同样暂停为 active 态:清掉已成功释放(解绑)的 agent 引用,否则该已释放
|
|
1220
|
-
// agent 之后因 tmux/recovery 失败会经 failTasksForAgent 把这个暂停 task 误标 failed。仅当释放
|
|
1221
|
-
// 成功(确已解绑)才清——若仍 held/bound 则保留引用,让其故障正常归因到本 task。
|
|
1222
|
-
const clearIds = {};
|
|
1223
|
-
if (transitioned.qaAgentId) {
|
|
1224
|
-
const qaReleased = await manager
|
|
1225
|
-
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle')
|
|
1226
|
-
.catch(() => false);
|
|
1227
|
-
if (qaReleased) {
|
|
1228
|
-
clearIds.qaAgentId = undefined;
|
|
1229
|
-
}
|
|
1230
|
-
else {
|
|
1231
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.qaAgentId, transitioned.id, {
|
|
1232
|
-
phase: 'spec-review-max-rounds-qa-release-failed',
|
|
1233
|
-
qaAgentId: transitioned.qaAgentId,
|
|
1234
|
-
});
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
if (transitioned.agentId) {
|
|
1238
|
-
const devReleased = await manager
|
|
1239
|
-
.releaseAgentForTask(transitioned.agentId, transitioned.id, 'idle')
|
|
1240
|
-
.catch(() => false);
|
|
1241
|
-
if (devReleased) {
|
|
1242
|
-
clearIds.agentId = undefined;
|
|
1243
|
-
}
|
|
1244
|
-
else {
|
|
1245
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1246
|
-
phase: 'spec-review-max-rounds-dev-release-failed',
|
|
1247
|
-
devAgentId: transitioned.agentId,
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
if ('qaAgentId' in clearIds || 'agentId' in clearIds) {
|
|
1252
|
-
await manager.updateTask(transitioned.id, clearIds)
|
|
1253
|
-
.catch(err => console.error(`[EventHandler] spec max_rounds clear agent ids(${transitioned.id}) failed:`, err));
|
|
1254
|
-
}
|
|
1255
|
-
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1256
|
-
phase: 'spec-review-max-rounds',
|
|
1257
|
-
round,
|
|
1258
|
-
cap,
|
|
1259
|
-
});
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
// changes-requested 必须有非空 findings — 提前 fail-loud。
|
|
1263
|
-
if (!Array.isArray(parsed.findings) || parsed.findings.length === 0) {
|
|
1264
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1265
|
-
phase: 'spec-review-findings-invalid-shape',
|
|
1266
|
-
round,
|
|
1267
|
-
findingsType: Array.isArray(parsed.findings) ? 'empty-array' : typeof parsed.findings,
|
|
1268
|
-
});
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
// 每条 finding 必须有唯一非空 id — coverage 校验依赖它。fail-closed。
|
|
1272
|
-
const findingsArr = parsed.findings;
|
|
1273
|
-
const idSet = new Set();
|
|
1274
|
-
for (const f of findingsArr) {
|
|
1275
|
-
const id = f?.id;
|
|
1276
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
1277
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1278
|
-
phase: 'spec-review-findings-missing-id',
|
|
1279
|
-
round,
|
|
1280
|
-
});
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
if (idSet.has(id)) {
|
|
1284
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1285
|
-
phase: 'spec-review-findings-duplicate-id',
|
|
1286
|
-
round,
|
|
1287
|
-
duplicateId: id,
|
|
1288
|
-
});
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
idSet.add(id);
|
|
1292
|
-
}
|
|
1293
|
-
try {
|
|
1294
|
-
await manager.dispatchSpecFixToDev(task.id, raw);
|
|
1295
|
-
}
|
|
1296
|
-
catch (err) {
|
|
1297
|
-
console.error(`[EventHandler] spec.review dispatchSpecFixToDev(${task.id}) failed:`, err);
|
|
1298
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1299
|
-
phase: 'spec-fix-dispatch-failed',
|
|
1300
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
});
|
|
1304
1084
|
// Dev emitted pr-fixed: it claims the fixing round is done. Verify on GitHub
|
|
1305
1085
|
// before advancing (option C) — a new commit OR a reply to the findings means
|
|
1306
|
-
// real work; neither means a no-op claim
|
|
1086
|
+
// real work; neither means a no-op claim. Every GitHub read
|
|
1307
1087
|
// fails closed: a verification we can't complete must NOT be read as "no-op".
|
|
1308
1088
|
bus.on('pr.fix.submitted', async (event) => {
|
|
1309
1089
|
if (!event.taskId || !event.agentId)
|
|
@@ -1407,206 +1187,5 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1407
1187
|
});
|
|
1408
1188
|
}
|
|
1409
1189
|
});
|
|
1410
|
-
bus.on('spec.fix.submitted', async (event) => {
|
|
1411
|
-
if (!event.taskId)
|
|
1412
|
-
return;
|
|
1413
|
-
const task = await manager.getTask(event.taskId);
|
|
1414
|
-
if (!task)
|
|
1415
|
-
return;
|
|
1416
|
-
// Freshness gate: stale signal may re-trigger dispatch after spec phase exit.
|
|
1417
|
-
const eventToken = event.data?.token;
|
|
1418
|
-
const stale = task.phase !== 'spec'
|
|
1419
|
-
|| task.status !== 'fixing'
|
|
1420
|
-
|| !eventToken
|
|
1421
|
-
|| eventToken !== task.signalToken;
|
|
1422
|
-
if (stale) {
|
|
1423
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1424
|
-
phase: 'spec-fix-event-stale',
|
|
1425
|
-
taskPhase: task.phase ?? null,
|
|
1426
|
-
taskStatus: task.status,
|
|
1427
|
-
});
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
const round = task.specReviewRound ?? 1;
|
|
1431
|
-
const fileName = `round-${round}-response.json`;
|
|
1432
|
-
let raw = null;
|
|
1433
|
-
try {
|
|
1434
|
-
raw = await manager.readSpecReviewFile(event.taskId, fileName);
|
|
1435
|
-
}
|
|
1436
|
-
catch (err) {
|
|
1437
|
-
console.error(`[EventHandler] spec.fix.submitted readSpecReviewFile(${event.taskId}) failed:`, err);
|
|
1438
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1439
|
-
phase: 'spec-fix-response-read-failed',
|
|
1440
|
-
round,
|
|
1441
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1442
|
-
});
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (raw === null) {
|
|
1446
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1447
|
-
phase: 'spec-fix-response-missing',
|
|
1448
|
-
round,
|
|
1449
|
-
fileName,
|
|
1450
|
-
});
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
let parsed;
|
|
1454
|
-
try {
|
|
1455
|
-
parsed = JSON.parse(raw);
|
|
1456
|
-
}
|
|
1457
|
-
catch (err) {
|
|
1458
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1459
|
-
phase: 'spec-fix-response-invalid-json',
|
|
1460
|
-
round,
|
|
1461
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1462
|
-
});
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
if (typeof parsed.round !== 'number' || parsed.round !== round) {
|
|
1466
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1467
|
-
phase: 'spec-fix-response-round-mismatch',
|
|
1468
|
-
round,
|
|
1469
|
-
parsedRound: parsed.round ?? null,
|
|
1470
|
-
});
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
if (!Array.isArray(parsed.responses)) {
|
|
1474
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1475
|
-
phase: 'spec-fix-response-invalid-shape',
|
|
1476
|
-
round,
|
|
1477
|
-
});
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
const responses = parsed.responses;
|
|
1481
|
-
if (responses.length === 0) {
|
|
1482
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1483
|
-
phase: 'spec-fix-response-empty',
|
|
1484
|
-
round,
|
|
1485
|
-
});
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
for (const r of responses) {
|
|
1489
|
-
if (r?.action !== 'fix' && r?.action !== 'reject') {
|
|
1490
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1491
|
-
phase: 'spec-fix-response-invalid-action',
|
|
1492
|
-
round,
|
|
1493
|
-
action: r?.action ?? null,
|
|
1494
|
-
});
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
// Coverage check: response must cover every finding.id; all branches fail-closed
|
|
1499
|
-
// (read/parse/round-mismatch all emit + return — fail-open would let all-reject sneak into code phase).
|
|
1500
|
-
let findingsRaw;
|
|
1501
|
-
try {
|
|
1502
|
-
findingsRaw = await manager.readSpecReviewFile(event.taskId, `round-${round}-findings.json`);
|
|
1503
|
-
}
|
|
1504
|
-
catch (err) {
|
|
1505
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1506
|
-
phase: 'spec-fix-coverage-findings-read-failed',
|
|
1507
|
-
round,
|
|
1508
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1509
|
-
});
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
if (findingsRaw === null) {
|
|
1513
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1514
|
-
phase: 'spec-fix-coverage-findings-missing',
|
|
1515
|
-
round,
|
|
1516
|
-
});
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
let findingsParsed;
|
|
1520
|
-
try {
|
|
1521
|
-
findingsParsed = JSON.parse(findingsRaw);
|
|
1522
|
-
}
|
|
1523
|
-
catch (err) {
|
|
1524
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1525
|
-
phase: 'spec-fix-coverage-findings-invalid-json',
|
|
1526
|
-
round,
|
|
1527
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1528
|
-
});
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
if (typeof findingsParsed.round !== 'number' || findingsParsed.round !== round) {
|
|
1532
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1533
|
-
phase: 'spec-fix-coverage-findings-round-mismatch',
|
|
1534
|
-
round,
|
|
1535
|
-
parsedRound: findingsParsed.round ?? null,
|
|
1536
|
-
});
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
// 独立 fail-closed schema 校验 — 上一阶段的校验对本 handler 不可信。
|
|
1540
|
-
if (!Array.isArray(findingsParsed.findings) || findingsParsed.findings.length === 0) {
|
|
1541
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1542
|
-
phase: 'spec-fix-coverage-findings-invalid-shape',
|
|
1543
|
-
round,
|
|
1544
|
-
findingsType: Array.isArray(findingsParsed.findings)
|
|
1545
|
-
? 'empty-array'
|
|
1546
|
-
: typeof findingsParsed.findings,
|
|
1547
|
-
});
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
const findingIdsList = [];
|
|
1551
|
-
for (const f of findingsParsed.findings) {
|
|
1552
|
-
const id = f?.id;
|
|
1553
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
1554
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1555
|
-
phase: 'spec-fix-coverage-findings-missing-id',
|
|
1556
|
-
round,
|
|
1557
|
-
});
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
if (findingIdsList.includes(id)) {
|
|
1561
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1562
|
-
phase: 'spec-fix-coverage-findings-duplicate-id',
|
|
1563
|
-
round,
|
|
1564
|
-
duplicateId: id,
|
|
1565
|
-
});
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
findingIdsList.push(id);
|
|
1569
|
-
}
|
|
1570
|
-
const findingIds = new Set(findingIdsList);
|
|
1571
|
-
const responseIds = new Set(responses
|
|
1572
|
-
.map(r => r?.findingId)
|
|
1573
|
-
.filter((id) => typeof id === 'string'));
|
|
1574
|
-
const missing = [...findingIds].filter(id => !responseIds.has(id));
|
|
1575
|
-
const unknown = [...responseIds].filter(id => !findingIds.has(id));
|
|
1576
|
-
if (missing.length > 0 || unknown.length > 0) {
|
|
1577
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1578
|
-
phase: 'spec-fix-response-coverage-mismatch',
|
|
1579
|
-
round,
|
|
1580
|
-
missingFindingIds: missing,
|
|
1581
|
-
unknownFindingIds: unknown,
|
|
1582
|
-
});
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
const hasAnyFix = responses.some(r => r.action === 'fix');
|
|
1586
|
-
if (!hasAnyFix) {
|
|
1587
|
-
// 全 reject → dev 直接进 code phase;不再 qa review。
|
|
1588
|
-
try {
|
|
1589
|
-
await manager.transitionToCodePhase(task.id);
|
|
1590
|
-
}
|
|
1591
|
-
catch (err) {
|
|
1592
|
-
console.error(`[EventHandler] spec.fix all-reject transitionToCodePhase(${task.id}) failed:`, err);
|
|
1593
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1594
|
-
phase: 'spec-fix-all-reject-transition-failed',
|
|
1595
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
try {
|
|
1601
|
-
await manager.dispatchSpecReviewToQa(task.id);
|
|
1602
|
-
}
|
|
1603
|
-
catch (err) {
|
|
1604
|
-
console.error(`[EventHandler] spec.fix dispatchSpecReviewToQa(${task.id}) failed:`, err);
|
|
1605
|
-
await emitIntervention(bus, task.projectId, task.agentId, task.id, {
|
|
1606
|
-
phase: 'spec-fix-redispatch-failed',
|
|
1607
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
});
|
|
1611
1190
|
}
|
|
1612
1191
|
//# sourceMappingURL=handlers.js.map
|