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.
Files changed (86) hide show
  1. package/dist/agent/diff-split.d.ts +10 -0
  2. package/dist/agent/diff-split.d.ts.map +1 -0
  3. package/dist/agent/diff-split.js +83 -0
  4. package/dist/agent/diff-split.js.map +1 -0
  5. package/dist/agent/manager.d.ts +75 -12
  6. package/dist/agent/manager.d.ts.map +1 -1
  7. package/dist/agent/manager.js +1121 -320
  8. package/dist/agent/manager.js.map +1 -1
  9. package/dist/agent/phase-signal-watcher.d.ts +7 -1
  10. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  11. package/dist/agent/phase-signal-watcher.js +37 -11
  12. package/dist/agent/phase-signal-watcher.js.map +1 -1
  13. package/dist/agent/phase-signal.d.ts +29 -11
  14. package/dist/agent/phase-signal.d.ts.map +1 -1
  15. package/dist/agent/phase-signal.js +38 -8
  16. package/dist/agent/phase-signal.js.map +1 -1
  17. package/dist/agent/prompt.d.ts +15 -2
  18. package/dist/agent/prompt.d.ts.map +1 -1
  19. package/dist/agent/prompt.js +250 -52
  20. package/dist/agent/prompt.js.map +1 -1
  21. package/dist/agent/repo-store.d.ts +0 -1
  22. package/dist/agent/repo-store.d.ts.map +1 -1
  23. package/dist/agent/repo-store.js +0 -25
  24. package/dist/agent/repo-store.js.map +1 -1
  25. package/dist/agent/review-transport.d.ts +36 -0
  26. package/dist/agent/review-transport.d.ts.map +1 -0
  27. package/dist/agent/review-transport.js +246 -0
  28. package/dist/agent/review-transport.js.map +1 -0
  29. package/dist/agent/worktree.d.ts +2 -0
  30. package/dist/agent/worktree.d.ts.map +1 -1
  31. package/dist/agent/worktree.js +26 -0
  32. package/dist/agent/worktree.js.map +1 -1
  33. package/dist/api/agents.d.ts.map +1 -1
  34. package/dist/api/agents.js +4 -0
  35. package/dist/api/agents.js.map +1 -1
  36. package/dist/api/tasks.d.ts.map +1 -1
  37. package/dist/api/tasks.js +8 -0
  38. package/dist/api/tasks.js.map +1 -1
  39. package/dist/cli.d.ts.map +1 -1
  40. package/dist/cli.js +8 -1
  41. package/dist/cli.js.map +1 -1
  42. package/dist/config/loader.d.ts.map +1 -1
  43. package/dist/config/loader.js +3 -0
  44. package/dist/config/loader.js.map +1 -1
  45. package/dist/config/validator.js +23 -0
  46. package/dist/config/validator.js.map +1 -1
  47. package/dist/event/handlers.d.ts.map +1 -1
  48. package/dist/event/handlers.js +33 -451
  49. package/dist/event/handlers.js.map +1 -1
  50. package/dist/event/server-handlers.d.ts +4 -0
  51. package/dist/event/server-handlers.d.ts.map +1 -0
  52. package/dist/event/server-handlers.js +835 -0
  53. package/dist/event/server-handlers.js.map +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +5 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/shared/constants.d.ts +7 -1
  58. package/dist/shared/constants.d.ts.map +1 -1
  59. package/dist/shared/constants.js +26 -8
  60. package/dist/shared/constants.js.map +1 -1
  61. package/dist/shared/types.d.ts +64 -2
  62. package/dist/shared/types.d.ts.map +1 -1
  63. package/dist/skills/server-feedback/SKILL.md +34 -0
  64. package/dist/skills/server-recheck/SKILL.md +30 -0
  65. package/dist/skills/server-review/SKILL.md +43 -0
  66. package/dist/skills/server-spec-review/SKILL.md +31 -0
  67. package/dist/state/index.d.ts +1 -0
  68. package/dist/state/index.d.ts.map +1 -1
  69. package/dist/state/index.js +1 -0
  70. package/dist/state/index.js.map +1 -1
  71. package/dist/state/review-store.d.ts +13 -0
  72. package/dist/state/review-store.d.ts.map +1 -0
  73. package/dist/state/review-store.js +92 -0
  74. package/dist/state/review-store.js.map +1 -0
  75. package/dist/state/snapshot.js +1 -1
  76. package/dist/state/snapshot.js.map +1 -1
  77. package/dist/state/task-store.d.ts.map +1 -1
  78. package/dist/state/task-store.js +1 -0
  79. package/dist/state/task-store.js.map +1 -1
  80. package/dist/terminal/attach.d.ts.map +1 -1
  81. package/dist/terminal/attach.js +8 -2
  82. package/dist/terminal/attach.js.map +1 -1
  83. package/dist/web/assets/index-OtgjyQI1.js +4 -0
  84. package/dist/web/index.html +1 -1
  85. package/package.json +1 -1
  86. package/dist/web/assets/index-ByNjLidI.js +0 -4
@@ -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-created', 'pr-created'];
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 完全由 signal 协议(spec.ready / spec.review.submitted / spec.fix.submitted)
155
- // 驱动;spec doc PR push 也会触发 pr.created/updated,poller 不应越过 signal 协议派
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 由 signal 协议驱动;spec doc 的 push / spec-review 留下的 PR comment 都不应
334
- // 进入 code-review 流程(避免派 QA recheck → gh pr review --approve → 误派 post-approve)。
335
- // pr-merge-ready 是 dev 自己内部状态推进,不受 phase gate 限制。
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
- const project = manager.getProjectConfig(freshTask.projectId);
405
- if (project?.merge === 'auto') {
406
- try {
407
- await manager.mergePr(freshTask.id, { matchHeadSha: freshCompletion.approvedHeadSha });
408
- await manager.clearPostApproveCompletionIfMatches(freshTask.id, signalToken);
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-created/pr-created or pr-fixed) used the
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
- { fromStatus: ['in_progress', 'fixing', 'review', 'approved', 'merge-ready', 'max_rounds'] }, prPatch);
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)由 signal 协议(spec.review.submitted)驱动 verdict;GitHub
748
- // PR review 不应改 task.status。QA spec-review 槽位上若错走 gh pr review --approve
749
- // 这里早退避免把 status 推到 approved + 派 dev post-approve。terminal 状态已在前面兜底
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