pi-firecode 1.0.0 → 1.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.
Files changed (93) hide show
  1. package/README.md +55 -41
  2. package/README.zh-CN.md +74 -0
  3. package/config.example.jsonc +21 -22
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11464 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/{master → dist/master}/prompts/master.zh.md +3 -3
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/watcher/prompts/watch.en.md +43 -0
  10. package/package.json +10 -27
  11. package/activity.ts +0 -92
  12. package/busy.ts +0 -188
  13. package/config.ts +0 -500
  14. package/deliver.ts +0 -119
  15. package/flame.ts +0 -149
  16. package/format.ts +0 -133
  17. package/header.ts +0 -218
  18. package/herdr-client.ts +0 -60
  19. package/index.ts +0 -68
  20. package/jsonc.ts +0 -34
  21. package/master/actions.ts +0 -293
  22. package/master/activity-list.ts +0 -281
  23. package/master/event-card.ts +0 -87
  24. package/master/event-format.ts +0 -83
  25. package/master/guard.ts +0 -46
  26. package/master/index.ts +0 -252
  27. package/master/list-view.ts +0 -129
  28. package/master/outbox.ts +0 -188
  29. package/master/prompt.ts +0 -26
  30. package/master/role.ts +0 -18
  31. package/master/run.ts +0 -315
  32. package/master/runtime.ts +0 -259
  33. package/master/spawn.ts +0 -234
  34. package/master/state.ts +0 -225
  35. package/master/worker-view.ts +0 -517
  36. package/provider/claude-sub.ts +0 -151
  37. package/provider/openai-native/index.ts +0 -8
  38. package/provider/openai-native/src/compact-client.ts +0 -350
  39. package/provider/openai-native/src/config.ts +0 -297
  40. package/provider/openai-native/src/extension.ts +0 -93
  41. package/provider/openai-native/src/native-compaction.ts +0 -151
  42. package/provider/openai-native/src/native-details.ts +0 -157
  43. package/provider/openai-native/src/native-replay.ts +0 -253
  44. package/provider/openai-native/src/native-runtime.ts +0 -144
  45. package/provider/openai-native/src/options.ts +0 -87
  46. package/provider/openai-native/src/request-pipeline.ts +0 -21
  47. package/provider/openai-native/src/responses-input.ts +0 -433
  48. package/review/advisor.ts +0 -110
  49. package/review/card.ts +0 -398
  50. package/review/checkpoint.ts +0 -378
  51. package/review/evidence.ts +0 -230
  52. package/review/index.ts +0 -992
  53. package/review/occupancy.ts +0 -25
  54. package/review/outcome.ts +0 -98
  55. package/review/prompt.ts +0 -209
  56. package/review/reviewer.ts +0 -353
  57. package/review/session.ts +0 -110
  58. package/review/state.ts +0 -877
  59. package/review/ui.ts +0 -89
  60. package/round-recorder.ts +0 -18
  61. package/session/herdr-display.ts +0 -87
  62. package/session/presets.ts +0 -300
  63. package/session/quota.ts +0 -123
  64. package/session/rename.ts +0 -46
  65. package/session/stats.ts +0 -313
  66. package/statusbar/index.ts +0 -256
  67. package/statusbar/render.ts +0 -113
  68. package/theme.ts +0 -45
  69. package/tools/actions.ts +0 -53
  70. package/tools/assistant-view.ts +0 -64
  71. package/tools/click-anchor.ts +0 -56
  72. package/tools/group-view.ts +0 -420
  73. package/tools/grouping.ts +0 -158
  74. package/tools/host.ts +0 -369
  75. package/tools/index.ts +0 -186
  76. package/tools/line.ts +0 -231
  77. package/tools/machine.ts +0 -78
  78. package/tools/parts.ts +0 -143
  79. package/tools/round.ts +0 -95
  80. package/tools/timing.ts +0 -22
  81. package/tools/turn-clock.ts +0 -41
  82. package/tools/turn-summary.ts +0 -109
  83. package/truncated-write.ts +0 -18
  84. package/watcher/card.ts +0 -91
  85. package/watcher/index.ts +0 -176
  86. package/watcher/observer.ts +0 -75
  87. package/watcher/transcript.ts +0 -56
  88. /package/{master → dist/master}/prompts/worker.zh.md +0 -0
  89. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  90. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
  91. /package/{review → dist/review}/prompts/review.en.md +0 -0
  92. /package/{review → dist/review}/prompts/review.zh.md +0 -0
  93. /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
package/review/state.ts DELETED
@@ -1,877 +0,0 @@
1
- /**
2
- * /fire-review 的状态机:唯一事实源,纯 reducer,零 IO 零副作用。
3
- *
4
- * 循环状态只存在这一个 reducer 里:模块级不持有可变循环状态,
5
- * 持久化与 UI 都是本状态在 checkpoint / 结果卡上的投影。
6
- *
7
- * 相:idle → queued → reviewing → needs_fix → awaiting_fix → reviewing,终态 settled;
8
- * 质量裁决终态(通过 / 顾问叫停 / maxRounds 用尽)先经 summarizing:投递一个总结回合
9
- * 让执行模型用人话收尾(修了什么/为何过不了),回合结束才落 settled;
10
- * 事故终态(取消 / 超时 / 基础设施错误)直接 settled,不烧总结回合。
11
- * 不变量:同一时刻至多一个活动轮;round 单调递增;history 只追加不改写。
12
- */
13
- import type { ModelAtom } from "../config.js";
14
-
15
- export type Phase =
16
- | "idle"
17
- | "queued"
18
- | "reviewing"
19
- | "needs_fix"
20
- | "awaiting_fix"
21
- | "summarizing"
22
- | "settled";
23
-
24
- export type ReviewerStatus = "running" | "passed" | "failed" | "error";
25
- export type RoundResult = "passed" | "failed" | "error" | "stopped" | "cancelled" | "timed_out";
26
- export type AdvisorVerdict = "continue" | "stop" | "narrow";
27
- export type StopReason = "advisor" | "max_rounds" | "user" | "shutdown" | "timeout";
28
-
29
- /** 单个审查者的输出(output 契约解析结果,纯数据)。 */
30
- export interface ReviewerResult extends ModelAtom {
31
- index: number;
32
- status: Exclude<ReviewerStatus, "running">;
33
- /** 短摘要:PASS 一行收敛摘要 / FAIL 发现一句话。 */
34
- summary: string;
35
- /** 全文:PASS 摘要+证据锚点 / FAIL 发现列表。归档用。 */
36
- details: string;
37
- }
38
-
39
- /** 审查中某个审查者的进行状态;settled 后携带完整结果。 */
40
- export interface ActiveReviewer extends ModelAtom {
41
- index: number;
42
- status: ReviewerStatus;
43
- result: ReviewerResult | null;
44
- }
45
-
46
- /** 当前轮的审查者集合。 */
47
- export interface ActiveCheck {
48
- round: number;
49
- reviewers: ActiveReviewer[];
50
- settledCount: number;
51
- }
52
-
53
- export interface AdvisorResult {
54
- verdict: AdvisorVerdict;
55
- advice: string;
56
- }
57
-
58
- /** 一轮已收口的记录;只追加不改写。 */
59
- export interface ReviewRound {
60
- round: number;
61
- result: RoundResult;
62
- /** 全文归档:PASS 摘要 / FAIL 发现列表 / advisor 建议等。 */
63
- details: string;
64
- reviewers: ReviewerResult[];
65
- advisor?: AdvisorResult;
66
- /** 取消 / 超时 / 停止的终止原因(展示层解析文案)。 */
67
- reason?: StopReason;
68
- elapsedMs: number;
69
- }
70
-
71
- /** 已判 FAIL 但尚未收口的轮:等顾问仲裁或直接投递反馈。 */
72
- export interface PendingRound {
73
- round: number;
74
- reviewers: ReviewerResult[];
75
- details: string;
76
- }
77
-
78
- export type RepairStatus = "pending" | "awaiting_start" | "running" | "completed";
79
-
80
- /** FAIL 后的修复回合。必须持久化,reload 才不会跳过尚未启动的反馈。 */
81
- export interface RepairState {
82
- details: string;
83
- advisor: AdvisorResult | null;
84
- status: RepairStatus;
85
- }
86
-
87
- export type SummaryKind = "passed" | "max_rounds" | "advisor_stop";
88
- export type SummaryStatus = "pending" | "awaiting_start" | "running";
89
-
90
- /** 质量裁决终态后的总结回合生命周期;持久化后 reload 才能重投未启动的总结。 */
91
- export interface SummaryState {
92
- kind: SummaryKind;
93
- status: SummaryStatus;
94
- }
95
-
96
- export interface ReviewState {
97
- runId: string;
98
- phase: Phase;
99
- /** 当前轮号(1 起);queued 时 0。 */
100
- round: number;
101
- focus: string;
102
- /** 已收口轮,只追加。 */
103
- history: ReviewRound[];
104
- /** reviewing 轮的活动检查;其余相为 null。 */
105
- active: ActiveCheck | null;
106
- /** FAIL 轮等待顾问收口(needs_fix)。 */
107
- pending: PendingRound | null;
108
- /** awaiting_fix 的反馈与修复回合生命周期。 */
109
- repair: RepairState | null;
110
- /** summarizing 的总结回合生命周期;其余相为 null。 */
111
- summary: SummaryState | null;
112
- /** 连续未通过轮数(顾问仲裁阈值)。 */
113
- consecutiveFailures: number;
114
- startedAt: number;
115
- /** 当前轮起点(本轮用时)。 */
116
- roundStartedAt: number;
117
- updatedAt: number;
118
- }
119
-
120
- /** reducer 需要的最小配置(限制语义 + 审查者模型清单,纯数据)。 */
121
- export interface ReviewLimits {
122
- maxRounds: number;
123
- advisorAfterFailures: number;
124
- advisorModel: string;
125
- /** 本轮审查者(model/thinking),beginRound 时填入 active。 */
126
- reviewers: ModelAtom[];
127
- language?: "zh" | "en";
128
- }
129
-
130
- export type ReviewEvent =
131
- | { type: "START"; focus: string; busy: boolean }
132
- | { type: "RECOVER" }
133
- | { type: "REVIEWER_SETTLED"; index: number; result: ReviewerResult }
134
- | { type: "ADVISOR_SETTLED"; result: AdvisorResult }
135
- | { type: "ADVISOR_SKIPPED" }
136
- | { type: "INFRASTRUCTURE_ERROR"; details: string }
137
- | { type: "ADVANCE" }
138
- | { type: "FEEDBACK_DISPATCHED" }
139
- | { type: "REPAIR_STARTED" }
140
- | { type: "REPAIR_COMPLETED" }
141
- | { type: "SUMMARY_DISPATCHED" }
142
- | { type: "SUMMARY_STARTED" }
143
- | { type: "SUMMARY_SETTLED" }
144
- | { type: "CANCEL"; reason: "user" | "shutdown" }
145
- | { type: "TIMEOUT" };
146
-
147
- /** 结果卡:executor 渲染成消息;reducer 只决定发哪张、带什么数据。 */
148
- export type CardData =
149
- | { kind: "start"; round: number; focus: string; models: string[] }
150
- | { kind: "pass"; round: number; summary: string; details: string; elapsedMs: number; totalElapsedMs?: number }
151
- | {
152
- kind: "fail";
153
- round: number;
154
- details: string;
155
- advisor: AdvisorResult | null;
156
- elapsedMs?: number;
157
- totalElapsedMs?: number;
158
- }
159
- | { kind: "stop"; reason: "advisor"; round: number; details: string; advisor: AdvisorResult; advisorModel: string; elapsedMs?: number; totalElapsedMs?: number }
160
- | { kind: "stop"; reason: Exclude<StopReason, "advisor">; round: number; details: string; elapsedMs?: number; totalElapsedMs?: number }
161
- | { kind: "cancel"; round: number; reason: StopReason }
162
- | { kind: "timeout"; round: number; reason: StopReason }
163
- | { kind: "error"; message: string; elapsedMs?: number; totalElapsedMs?: number }
164
- // advisor 卡的 elapsedMs 是咨询时长(进入 needs_fix 到裁决落定),不是轮时长。
165
- | { kind: "advisor"; advisor: AdvisorResult; advisorModel: string; elapsedMs?: number };
166
-
167
- export type ReviewEffect =
168
- | { kind: "advance" }
169
- | { kind: "send_card"; card: CardData };
170
-
171
- export interface ReduceResult {
172
- state: ReviewState;
173
- effects: ReviewEffect[];
174
- }
175
-
176
- export function initialState(runId: string): ReviewState {
177
- return {
178
- runId,
179
- phase: "idle",
180
- round: 0,
181
- focus: "",
182
- history: [],
183
- active: null,
184
- pending: null,
185
- repair: null,
186
- summary: null,
187
- consecutiveFailures: 0,
188
- startedAt: 0,
189
- roundStartedAt: 0,
190
- updatedAt: 0,
191
- };
192
- }
193
-
194
- export function reduce(
195
- state: ReviewState,
196
- event: ReviewEvent,
197
- limits: ReviewLimits,
198
- now: number,
199
- ): ReduceResult {
200
- switch (event.type) {
201
- case "START":
202
- return onStart(state, event, limits, now);
203
- case "RECOVER":
204
- return onRecover(state);
205
- case "REVIEWER_SETTLED":
206
- return onReviewerSettled(state, event, limits, now);
207
- case "ADVISOR_SETTLED":
208
- return onAdvisorSettled(state, event, limits, now);
209
- case "ADVISOR_SKIPPED":
210
- return onAdvisorSkipped(state, now);
211
- case "INFRASTRUCTURE_ERROR":
212
- return onInfrastructureError(state, event.details, now);
213
- case "ADVANCE":
214
- return onAdvance(state, limits, now);
215
- case "FEEDBACK_DISPATCHED":
216
- return updateRepairStatus(state, "pending", "awaiting_start", now);
217
- case "REPAIR_STARTED":
218
- return updateRepairStatus(state, "awaiting_start", "running", now);
219
- case "REPAIR_COMPLETED":
220
- return updateRepairStatus(state, "running", "completed", now);
221
- case "SUMMARY_DISPATCHED":
222
- return updateSummaryStatus(state, "pending", "awaiting_start", now);
223
- case "SUMMARY_STARTED":
224
- return updateSummaryStatus(state, "awaiting_start", "running", now);
225
- case "SUMMARY_SETTLED":
226
- return onSummarySettled(state, now);
227
- case "CANCEL":
228
- return onCancel(state, event.reason, now);
229
- case "TIMEOUT":
230
- return onTimeout(state, now);
231
- }
232
- }
233
-
234
- function onStart(
235
- state: ReviewState,
236
- event: Extract<ReviewEvent, { type: "START" }>,
237
- limits: ReviewLimits,
238
- now: number,
239
- ): ReduceResult {
240
- if (state.phase === "reviewing" || state.phase === "needs_fix" || state.phase === "awaiting_fix")
241
- return { state, effects: [] };
242
- const focus = event.focus.trim();
243
- // 排队相不发卡:输入框边框与审查活动行已各有一份排队提示,记录里只留开始/结果卡。
244
- if (event.busy)
245
- return {
246
- state: {
247
- ...initialState(state.runId),
248
- phase: "queued",
249
- focus,
250
- startedAt: now,
251
- updatedAt: now,
252
- },
253
- effects: [{ kind: "advance" }],
254
- };
255
- return {
256
- // startedAt 在此落地:排队路径在 queued 写,直接开审路径必须同样记录总耗时起点。
257
- state: beginRound({ ...initialState(state.runId), startedAt: now }, focus, 1, limits, now),
258
- effects: [
259
- {
260
- kind: "send_card",
261
- card: {
262
- kind: "start",
263
- round: 1,
264
- focus,
265
- models: limits.reviewers.map((item) => item.model),
266
- },
267
- },
268
- { kind: "advance" },
269
- ],
270
- };
271
- }
272
-
273
- function onRecover(state: ReviewState): ReduceResult {
274
- if (state.phase === "idle" || state.phase === "settled")
275
- return { state, effects: [] };
276
- // reload 会中断尚未确认完成的修复/总结回合;重新投递同一份持久化内容,不能跳过。
277
- const repair =
278
- state.phase === "awaiting_fix" && state.repair && state.repair.status !== "completed"
279
- ? { ...state.repair, status: "pending" as const }
280
- : state.repair;
281
- const summary =
282
- state.phase === "summarizing" && state.summary
283
- ? { ...state.summary, status: "pending" as const }
284
- : state.summary;
285
- // session_start 只恢复持久状态;宿主在所有 session_start handler 完成后
286
- // 另发 resources_discover,执行器到那个正式边界才请求推进。
287
- return { state: { ...state, repair, summary }, effects: [] };
288
- }
289
-
290
- function updateRepairStatus(
291
- state: ReviewState,
292
- expected: RepairStatus,
293
- status: RepairStatus,
294
- now: number,
295
- ): ReduceResult {
296
- if (state.phase !== "awaiting_fix" || state.repair?.status !== expected)
297
- return { state, effects: [] };
298
- return {
299
- state: { ...state, repair: { ...state.repair, status }, updatedAt: now },
300
- effects: [],
301
- };
302
- }
303
-
304
- function updateSummaryStatus(
305
- state: ReviewState,
306
- expected: SummaryStatus,
307
- status: SummaryStatus,
308
- now: number,
309
- ): ReduceResult {
310
- if (state.phase !== "summarizing" || state.summary?.status !== expected)
311
- return { state, effects: [] };
312
- return {
313
- state: { ...state, summary: { ...state.summary, status }, updatedAt: now },
314
- effects: [],
315
- };
316
- }
317
-
318
- /** 总结回合结束(或投递失败放弃):无论何种结局都落 settled,总结是尽力而非必须。 */
319
- function onSummarySettled(state: ReviewState, now: number): ReduceResult {
320
- if (state.phase !== "summarizing") return { state, effects: [] };
321
- return {
322
- state: { ...state, phase: "settled", summary: null, updatedAt: now },
323
- effects: [],
324
- };
325
- }
326
-
327
- /** 进入总结相的统一出口:携带意图,由 advance 在 idle 边界投递总结提示。 */
328
- function summarizing(kind: SummaryKind): Pick<ReviewState, "phase" | "summary"> {
329
- return { phase: "summarizing", summary: { kind, status: "pending" } };
330
- }
331
-
332
- function onAdvance(
333
- state: ReviewState,
334
- limits: ReviewLimits,
335
- now: number,
336
- ): ReduceResult {
337
- if (state.phase === "queued")
338
- return {
339
- state: beginRound(state, state.focus, 1, limits, now),
340
- effects: [
341
- {
342
- kind: "send_card",
343
- card: {
344
- kind: "start",
345
- round: 1,
346
- focus: state.focus,
347
- models: limits.reviewers.map((item) => item.model),
348
- },
349
- },
350
- { kind: "advance" },
351
- ],
352
- };
353
- if (state.phase !== "awaiting_fix" || state.repair?.status !== "completed")
354
- return { state, effects: [] };
355
- if (state.round >= limits.maxRounds)
356
- return {
357
- state: { ...state, ...summarizing("max_rounds"), repair: null, updatedAt: now },
358
- effects: [
359
- {
360
- kind: "send_card",
361
- card: { kind: "stop", reason: "max_rounds", round: state.round, details: "" },
362
- },
363
- { kind: "advance" },
364
- ],
365
- };
366
- // 开始卡只发第 1 轮:后续轮的边界由结果卡的轮号承担,重复开始卡只制造噪声。
367
- return {
368
- state: beginRound(state, state.focus, state.round + 1, limits, now),
369
- effects: [{ kind: "advance" }],
370
- };
371
- }
372
-
373
- function onReviewerSettled(
374
- state: ReviewState,
375
- event: Extract<ReviewEvent, { type: "REVIEWER_SETTLED" }>,
376
- limits: ReviewLimits,
377
- now: number,
378
- ): ReduceResult {
379
- if (state.phase !== "reviewing" || !state.active) return { state, effects: [] };
380
- const active = state.active;
381
- const settled: ReviewerResult[] = [];
382
- const reviewers = active.reviewers.map((item) => {
383
- if (item.index !== event.index) return item;
384
- settled.push(event.result);
385
- return { ...item, status: event.result.status, result: event.result };
386
- });
387
- const settledCount = active.settledCount + 1;
388
- const nextActive: ActiveCheck = { ...active, reviewers, settledCount };
389
- if (settledCount < reviewers.length)
390
- return {
391
- state: { ...state, active: nextActive, updatedAt: now },
392
- effects: [],
393
- };
394
- const allSettled = nextActive.reviewers.flatMap((item) =>
395
- item.result ? [item.result] : [],
396
- );
397
- return settleRound(state, nextActive, allSettled, limits, now);
398
- }
399
-
400
- function settleRound(
401
- state: ReviewState,
402
- active: ActiveCheck,
403
- settled: ReviewerResult[],
404
- limits: ReviewLimits,
405
- now: number,
406
- ): ReduceResult {
407
- const { result, displayDetails, feedbackDetails, archiveDetails, summary } = aggregate(
408
- settled,
409
- limits.language ?? "zh",
410
- );
411
- const base = { ...state, active: null, updatedAt: now };
412
- const totalElapsedMs = Math.max(0, now - state.startedAt);
413
- const passSummary = result === "passed"
414
- ? appendClosedFindings(summary, state.history, active.round, limits.language ?? "zh")
415
- : summary;
416
- if (result === "passed" || result === "error") {
417
- const round = roundRecord(active.round, result, archiveDetails, settled, undefined, state.roundStartedAt, now);
418
- const history = [...state.history, round];
419
- // 通过是质量裁决终态 → 总结回合;基础设施错误是事故 → 直接收尾。
420
- if (result === "passed")
421
- return {
422
- state: { ...base, ...summarizing("passed"), history },
423
- effects: [
424
- {
425
- kind: "send_card",
426
- card: {
427
- kind: "pass",
428
- round: active.round,
429
- summary: passSummary,
430
- details: archiveDetails,
431
- elapsedMs: round.elapsedMs,
432
- totalElapsedMs,
433
- },
434
- },
435
- { kind: "advance" },
436
- ],
437
- };
438
- return {
439
- state: { ...base, phase: "settled", history },
440
- effects: [
441
- {
442
- kind: "send_card",
443
- card: { kind: "error", message: displayDetails, elapsedMs: round.elapsedMs, totalElapsedMs },
444
- },
445
- ],
446
- };
447
- }
448
- const consecutiveFailures = state.consecutiveFailures + 1;
449
- if (active.round >= limits.maxRounds) {
450
- const round = roundRecord(active.round, "failed", archiveDetails, settled, undefined, state.roundStartedAt, now);
451
- return {
452
- state: {
453
- ...base,
454
- ...summarizing("max_rounds"),
455
- consecutiveFailures,
456
- history: [...state.history, round],
457
- },
458
- effects: [
459
- {
460
- kind: "send_card",
461
- card: {
462
- kind: "stop",
463
- reason: "max_rounds",
464
- round: active.round,
465
- details: displayDetails,
466
- elapsedMs: round.elapsedMs,
467
- },
468
- },
469
- { kind: "advance" },
470
- ],
471
- };
472
- }
473
- // 每轮未通过都发一张可见的失败卡:用户要能看到本轮结论,
474
- // 而不是只收到一条投给执行模型的隐藏反馈。
475
- const failCard: ReviewEffect = {
476
- kind: "send_card",
477
- card: {
478
- kind: "fail",
479
- round: active.round,
480
- details: displayDetails,
481
- advisor: null,
482
- elapsedMs: Math.max(0, now - state.roundStartedAt),
483
- totalElapsedMs,
484
- },
485
- };
486
- const pending: PendingRound = { round: active.round, reviewers: settled, details: feedbackDetails };
487
- if (consecutiveFailures >= limits.advisorAfterFailures)
488
- return {
489
- state: { ...base, phase: "needs_fix", pending, consecutiveFailures },
490
- // 顾问可能裁定 stop,反馈永远不会投递:此时不能提前宣布「已交回修复」;
491
- // 卡里也不写「顾问介入中」——持久记录会过时,实况由审查活动行与输入框边框承担。
492
- effects: [failCard, { kind: "advance" }],
493
- };
494
- return {
495
- state: {
496
- ...base,
497
- phase: "awaiting_fix",
498
- pending: null,
499
- repair: { details: feedbackDetails, advisor: null, status: "pending" },
500
- consecutiveFailures,
501
- history: [
502
- ...state.history,
503
- roundRecord(active.round, "failed", archiveDetails, settled, undefined, state.roundStartedAt, now),
504
- ],
505
- },
506
- effects: [failCard, { kind: "advance" }],
507
- };
508
- }
509
-
510
- function onAdvisorSettled(
511
- state: ReviewState,
512
- event: Extract<ReviewEvent, { type: "ADVISOR_SETTLED" }>,
513
- limits: ReviewLimits,
514
- now: number,
515
- ): ReduceResult {
516
- if (state.phase !== "needs_fix" || !state.pending) return { state, effects: [] };
517
- const pending = state.pending;
518
- const advisor = event.result;
519
- if (advisor.verdict === "stop") {
520
- const round = roundRecord(pending.round, "stopped", advisor.advice, pending.reviewers, advisor, state.roundStartedAt, now);
521
- return {
522
- state: { ...state, ...summarizing("advisor_stop"), pending: null, history: [...state.history, round], updatedAt: now },
523
- effects: [
524
- {
525
- kind: "send_card",
526
- card: {
527
- kind: "stop",
528
- reason: "advisor",
529
- round: pending.round,
530
- // findings 已在咨询前的失败卡显示;终止卡只承载顾问裁决,避免重复整张报告。
531
- details: advisor.advice,
532
- advisor,
533
- advisorModel: limits.advisorModel,
534
- // 与顾问建议卡同一语义:咨询时长,不是轮时长(轮时长已在咨询前的失败卡显示)。
535
- elapsedMs: Math.max(0, now - state.updatedAt),
536
- totalElapsedMs: Math.max(0, now - state.startedAt),
537
- },
538
- },
539
- { kind: "advance" },
540
- ],
541
- };
542
- }
543
- const round = roundRecord(pending.round, "failed", pending.details, pending.reviewers, advisor, state.roundStartedAt, now);
544
- return {
545
- state: {
546
- ...state,
547
- phase: "awaiting_fix",
548
- pending: null,
549
- repair: { details: pending.details, advisor, status: "pending" },
550
- history: [...state.history, round],
551
- updatedAt: now,
552
- },
553
- // 失败卡已在咨询前发出;咨询完成只补 pi-flow 的中性顾问建议卡。
554
- effects: [
555
- {
556
- kind: "send_card",
557
- // 咨询时长:needs_fix 相内只有顾问事件会迁移,updatedAt 即进入咨询的时刻。
558
- card: {
559
- kind: "advisor",
560
- advisor,
561
- advisorModel: limits.advisorModel,
562
- elapsedMs: Math.max(0, now - state.updatedAt),
563
- },
564
- },
565
- { kind: "advance" },
566
- ],
567
- };
568
- }
569
-
570
- function onAdvisorSkipped(state: ReviewState, now: number): ReduceResult {
571
- if (state.phase !== "needs_fix" || !state.pending) return { state, effects: [] };
572
- const pending = state.pending;
573
- const round = roundRecord(
574
- pending.round,
575
- "failed",
576
- pending.details,
577
- pending.reviewers,
578
- undefined,
579
- state.roundStartedAt,
580
- now,
581
- );
582
- return {
583
- state: {
584
- ...state,
585
- phase: "awaiting_fix",
586
- pending: null,
587
- repair: { details: pending.details, advisor: null, status: "pending" },
588
- history: [...state.history, round],
589
- updatedAt: now,
590
- },
591
- effects: [{ kind: "advance" }],
592
- };
593
- }
594
-
595
- function onCancel(
596
- state: ReviewState,
597
- reason: "user" | "shutdown",
598
- now: number,
599
- ): ReduceResult {
600
- if (state.phase === "idle" || state.phase === "settled")
601
- return { state, effects: [] };
602
- // 总结相被取消/退出:质量裁决与结果卡已落地,静默收尾,不追加轮记录不发卡。
603
- if (state.phase === "summarizing") return onSummarySettled(state, now);
604
- if (state.phase === "queued")
605
- return {
606
- state: { ...state, phase: "settled", updatedAt: now },
607
- effects: [{ kind: "send_card", card: { kind: "cancel", round: 0, reason } }],
608
- };
609
- const round = resolveRoundRecord(state, "cancelled", reason, now);
610
- return {
611
- state: {
612
- ...state,
613
- phase: "settled",
614
- active: null,
615
- pending: null,
616
- repair: null,
617
- history: round ? [...state.history, round] : state.history,
618
- updatedAt: now,
619
- },
620
- effects: [{ kind: "send_card", card: { kind: "cancel", round: state.round, reason } }],
621
- };
622
- }
623
-
624
- function onInfrastructureError(
625
- state: ReviewState,
626
- details: string,
627
- now: number,
628
- ): ReduceResult {
629
- if (state.phase === "idle" || state.phase === "settled") return { state, effects: [] };
630
- if (state.phase === "summarizing") return onSummarySettled(state, now);
631
- const message = details.trim() || "review infrastructure unavailable";
632
- const reviewers = state.active?.reviewers.flatMap((item) => item.result ? [item.result] : [])
633
- ?? state.pending?.reviewers
634
- ?? [];
635
- const round = state.round > 0
636
- ? roundRecord(state.round, "error", message, reviewers, undefined, state.roundStartedAt, now)
637
- : undefined;
638
- return {
639
- state: {
640
- ...state,
641
- phase: "settled",
642
- active: null,
643
- pending: null,
644
- repair: null,
645
- history: round ? [...state.history, round] : state.history,
646
- updatedAt: now,
647
- },
648
- effects: [{ kind: "send_card", card: { kind: "error", message } }],
649
- };
650
- }
651
-
652
- function onTimeout(state: ReviewState, now: number): ReduceResult {
653
- if (state.phase === "idle" || state.phase === "settled")
654
- return { state, effects: [] };
655
- // 看门狗在总结相到点:裁决已落地,静默收尾不误报超时。
656
- if (state.phase === "summarizing") return onSummarySettled(state, now);
657
- if (state.phase === "queued")
658
- return {
659
- state: { ...state, phase: "settled", updatedAt: now },
660
- effects: [{ kind: "send_card", card: { kind: "timeout", round: 0, reason: "timeout" } }],
661
- };
662
- const round = resolveRoundRecord(state, "timed_out", "timeout", now);
663
- return {
664
- state: {
665
- ...state,
666
- phase: "settled",
667
- active: null,
668
- pending: null,
669
- repair: null,
670
- history: round ? [...state.history, round] : state.history,
671
- updatedAt: now,
672
- },
673
- effects: [
674
- { kind: "send_card", card: { kind: "timeout", round: state.round, reason: "timeout" } },
675
- ],
676
- };
677
- }
678
-
679
- function beginRound(
680
- state: ReviewState,
681
- focus: string,
682
- round: number,
683
- limits: ReviewLimits,
684
- now: number,
685
- ): ReviewState {
686
- const reviewers: ActiveReviewer[] = limits.reviewers.map((item, index) => ({
687
- index,
688
- model: item.model,
689
- thinking: item.thinking,
690
- status: "running",
691
- result: null,
692
- }));
693
- return {
694
- ...state,
695
- phase: "reviewing",
696
- round,
697
- focus,
698
- active: { round, reviewers, settledCount: 0 },
699
- pending: null,
700
- repair: null,
701
- summary: null,
702
- roundStartedAt: now,
703
- updatedAt: now,
704
- };
705
- }
706
-
707
- /** 展示保留每个模型分节;修复反馈只携带 FAIL 票,归档保留全部原文。 */
708
- function aggregate(
709
- reviewers: ReviewerResult[],
710
- language: "zh" | "en",
711
- ): {
712
- result: "passed" | "failed" | "error";
713
- displayDetails: string;
714
- feedbackDetails: string;
715
- archiveDetails: string;
716
- summary: string;
717
- } {
718
- const verdicts = reviewers.filter((item) => item.status === "passed" || item.status === "failed");
719
- const absent = reviewers.filter((item) => item.status === "error");
720
- const archiveDetails = aggregateDetails(reviewers, language);
721
- // 有裁决就成轮:缺席者(会话故障或输出契约违例)只在结论里点名,不阻止形成质量结论;
722
- // 全员缺席才是基础设施不可用。否则一个供应商额度耗尽就会让整条审查通道停摆。
723
- if (verdicts.length === 0)
724
- return { result: "error", displayDetails: archiveDetails, feedbackDetails: "", archiveDetails, summary: "" };
725
- const failed = verdicts.filter((item) => item.status === "failed");
726
- if (failed.length > 0)
727
- return {
728
- result: "failed",
729
- displayDetails: archiveDetails,
730
- feedbackDetails: aggregateDetails(failed, language),
731
- archiveDetails,
732
- summary: "",
733
- };
734
- return {
735
- result: "passed",
736
- displayDetails: archiveDetails,
737
- feedbackDetails: "",
738
- archiveDetails,
739
- summary: aggregatePassSummary(verdicts, absent, language),
740
- };
741
- }
742
-
743
- function aggregateDetails(reviewers: ReviewerResult[], language: "zh" | "en"): string {
744
- return reviewers
745
- .map((item) => `${modelLabel(item, language)}\n${item.details.trim()}`)
746
- .join("\n\n");
747
- }
748
-
749
- function aggregatePassSummary(passed: ReviewerResult[], absent: ReviewerResult[], language: "zh" | "en") {
750
- const en = language === "en";
751
- const fallback = en ? "Review passed." : "审查通过。";
752
- const lines = passed.length === 1
753
- ? [passBody(passed[0]?.summary ?? "") || fallback]
754
- : passed.map((item) => `• ${shortModel(item.model)}${en ? ": " : ":"}${passBody(item.summary) || fallback}`);
755
- if (absent.length > 0)
756
- lines.push(
757
- (en ? "No verdict from " : "未形成裁决:")
758
- + absent.map((item) => `${shortModel(item.model)}${en ? ` (${firstLine(item.details)})` : `(${firstLine(item.details)})`}`).join(en ? ", " : "、"),
759
- );
760
- const suggestions = [...new Set(passed.flatMap((item) => splitSuggestions(item.details).suggestions))];
761
- if (suggestions.length === 0) return lines.join("\n");
762
- return [
763
- ...lines,
764
- "",
765
- en ? "## Suggestions (non-blocking)" : "## 建议(非阻塞)",
766
- ...suggestions.map((item) => `- ${item}`),
767
- ].join("\n");
768
- }
769
-
770
- function firstLine(text: string) {
771
- return text.trim().split(/\r?\n/u, 1)[0]?.split(/[::]/u, 1)[0]?.trim() ?? "";
772
- }
773
-
774
- function splitSuggestions(summary: string) {
775
- const lines = summary.split(/\r?\n/u);
776
- const index = lines.findIndex((line) => /^##\s*(?:建议(非阻塞)|Suggestions \(non-blocking\))/iu.test(line.trim()));
777
- if (index < 0) return { body: summary, suggestions: [] as string[] };
778
- return {
779
- body: lines.slice(0, index).join("\n"),
780
- suggestions: lines.slice(index + 1).map((line) => line.replace(/^[-*]\s*/u, "").trim()).filter(Boolean),
781
- };
782
- }
783
-
784
- function passBody(summary: string) {
785
- return summary.replace(/^(?:PASS\s*)/iu, "").trim();
786
- }
787
-
788
- function shortModel(model: string) {
789
- return model.split("/").at(-1) || model;
790
- }
791
-
792
- function appendClosedFindings(
793
- summary: string,
794
- history: ReviewRound[],
795
- beforeRound: number,
796
- language: "zh" | "en",
797
- ) {
798
- const seen = new Set<string>();
799
- const findings: { round: number; issue: string }[] = [];
800
- for (const round of history) {
801
- if (round.round >= beforeRound || round.result !== "failed") continue;
802
- for (const line of round.details.split(/\r?\n/u)) {
803
- const match = /^[-*+]\s*(?:\*\*)?(?:问题|Issue)(?:\*\*)?\s*[::]\s*(.+)$/iu.exec(line.trim());
804
- const issue = match?.[1]?.replace(/`([^`]+)`/gu, "$1").trim();
805
- if (!issue) continue;
806
- const key = issue.replace(/\s+/gu, "");
807
- if (seen.has(key)) continue;
808
- seen.add(key);
809
- findings.push({ round: round.round, issue });
810
- }
811
- }
812
- if (findings.length === 0) return summary;
813
- const shown = findings.slice(0, 6);
814
- const recap = [
815
- language === "en" ? "**Issues closed in this check**:" : "**本次收口的问题**:",
816
- ...shown.map((finding) =>
817
- `• ${finding.issue}${language === "en" ? ` (Round ${finding.round})` : `(第 ${finding.round} 轮)`}`,
818
- ),
819
- ];
820
- if (findings.length > shown.length)
821
- recap.push(language === "en"
822
- ? `…and ${findings.length - shown.length} more; see the review report`
823
- : `…另有 ${findings.length - shown.length} 项,详见审查报告`);
824
- return `${summary}\n\n${recap.join("\n")}`;
825
- }
826
-
827
- function modelLabel(item: ReviewerResult, language: "zh" | "en") {
828
- const model = item.model.split("/").at(-1) || item.model;
829
- return `${language === "en" ? "Model" : "模型"} ${item.index + 1} · ${model}`;
830
- }
831
-
832
- function resolveRoundRecord(
833
- state: ReviewState,
834
- result: RoundResult,
835
- reason: StopReason,
836
- now: number,
837
- ): ReviewRound | undefined {
838
- if (state.phase === "reviewing" && state.active) {
839
- const settled = state.active.reviewers.flatMap((item) =>
840
- item.result ? [item.result] : [],
841
- );
842
- return roundRecord(state.active.round, result, "", settled, undefined, state.roundStartedAt, now, reason);
843
- }
844
- if (state.phase === "needs_fix" && state.pending)
845
- return roundRecord(
846
- state.pending.round,
847
- result,
848
- "",
849
- state.pending.reviewers,
850
- undefined,
851
- state.roundStartedAt,
852
- now,
853
- reason,
854
- );
855
- return undefined;
856
- }
857
-
858
- function roundRecord(
859
- round: number,
860
- result: RoundResult,
861
- details: string,
862
- reviewers: ReviewerResult[],
863
- advisor: AdvisorResult | undefined,
864
- roundStartedAt: number,
865
- now: number,
866
- reason?: StopReason,
867
- ): ReviewRound {
868
- return {
869
- round,
870
- result,
871
- details,
872
- reviewers,
873
- ...(advisor ? { advisor } : {}),
874
- ...(reason ? { reason } : {}),
875
- elapsedMs: roundStartedAt ? Math.max(0, now - roundStartedAt) : 0,
876
- };
877
- }