pi-firecode 0.8.1 → 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.
- package/README.md +58 -24
- package/README.zh-CN.md +74 -0
- package/config.example.jsonc +47 -57
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11464 -0
- package/dist/master/prompts/master.en.md +22 -0
- package/dist/master/prompts/master.zh.md +22 -0
- package/dist/master/prompts/worker.en.md +1 -0
- package/dist/master/prompts/worker.zh.md +1 -0
- package/{review → dist/review}/prompts/review.en.md +7 -6
- package/{review → dist/review}/prompts/review.zh.md +7 -6
- package/dist/watcher/prompts/watch.en.md +43 -0
- package/{watcher → dist/watcher}/prompts/watch.zh.md +1 -1
- package/package.json +10 -27
- package/config.ts +0 -474
- package/deliver.ts +0 -32
- package/flame-frames.ts +0 -460
- package/format.ts +0 -80
- package/header.ts +0 -92
- package/herdr-client.ts +0 -60
- package/index.ts +0 -62
- package/jsonc.ts +0 -34
- package/master/event-card.ts +0 -106
- package/master/event-format.ts +0 -52
- package/master/index.ts +0 -1200
- package/master/prompt.ts +0 -26
- package/master/prompts/master.zh.md +0 -17
- package/master/prompts/worker.zh.md +0 -1
- package/master/role.ts +0 -13
- package/master/spawn.ts +0 -193
- package/master/state.ts +0 -201
- package/provider/claude-sub.ts +0 -129
- package/provider/openai-native/index.ts +0 -8
- package/provider/openai-native/src/compact-client.ts +0 -362
- package/provider/openai-native/src/config.ts +0 -297
- package/provider/openai-native/src/extension.ts +0 -93
- package/provider/openai-native/src/native-compaction.ts +0 -151
- package/provider/openai-native/src/native-details.ts +0 -157
- package/provider/openai-native/src/native-replay.ts +0 -253
- package/provider/openai-native/src/native-runtime.ts +0 -144
- package/provider/openai-native/src/options.ts +0 -85
- package/provider/openai-native/src/request-pipeline.ts +0 -21
- package/provider/openai-native/src/responses-input.ts +0 -433
- package/review/advisor.ts +0 -112
- package/review/card.ts +0 -385
- package/review/checkpoint.ts +0 -384
- package/review/evidence.ts +0 -232
- package/review/index.ts +0 -1246
- package/review/outcome.ts +0 -74
- package/review/progress.ts +0 -324
- package/review/prompt.ts +0 -210
- package/review/reviewer.ts +0 -356
- package/review/session.ts +0 -129
- package/review/state.ts +0 -905
- package/review/ui.ts +0 -528
- package/session/bark.ts +0 -157
- package/session/herdr-display.ts +0 -87
- package/session/presets.ts +0 -257
- package/session/rename.ts +0 -46
- package/session/stats.ts +0 -310
- package/session/working-flame.ts +0 -116
- package/statusbar/index.ts +0 -118
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/render.ts +0 -181
- package/statusbar/tps.ts +0 -87
- package/theme.ts +0 -84
- package/tools/grouping.ts +0 -233
- package/tools/index.ts +0 -183
- package/tools/line.ts +0 -194
- package/tools/parts.ts +0 -143
- package/tools/timing.ts +0 -22
- package/watcher/card.ts +0 -85
- package/watcher/index.ts +0 -204
- package/watcher/observer.ts +0 -75
- package/watcher/transcript.ts +0 -64
- /package/{review → dist/review}/prompts/advisor.en.md +0 -0
- /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
package/review/reviewer.ts
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
/** 审查者:进程内 memory 会话 + PASS/FAIL 输出契约解析。 */
|
|
2
|
-
import type { Language, ThinkingLevelValue } from "../config.js";
|
|
3
|
-
import type { PromptLayers } from "./prompt.js";
|
|
4
|
-
import type { ReviewerResult, ReviewerStatus } from "./state.js";
|
|
5
|
-
import type { ReviewSessionRunner } from "./session.js";
|
|
6
|
-
|
|
7
|
-
export interface ReviewModelConfig {
|
|
8
|
-
model: string;
|
|
9
|
-
thinking: ThinkingLevelValue;
|
|
10
|
-
tools: string[];
|
|
11
|
-
timeoutMs: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface RunReviewerOptions {
|
|
15
|
-
index: number;
|
|
16
|
-
config: ReviewModelConfig;
|
|
17
|
-
prompt: PromptLayers;
|
|
18
|
-
cwd: string;
|
|
19
|
-
language: Language;
|
|
20
|
-
signal?: AbortSignal;
|
|
21
|
-
runSession: ReviewSessionRunner;
|
|
22
|
-
/** 结构化会话事件:驱动活动条的实时进度。 */
|
|
23
|
-
onEvent?: (event: Record<string, unknown>) => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type ParseOutcome = {
|
|
27
|
-
status: Exclude<ReviewerStatus, "running">;
|
|
28
|
-
summary: string;
|
|
29
|
-
details: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** 运行一个独立审查会话并解析输出。会话故障记为 error,不拖垮整轮。 */
|
|
33
|
-
export async function runReviewer(options: RunReviewerOptions): Promise<ReviewerResult> {
|
|
34
|
-
const result = await options.runSession({
|
|
35
|
-
role: "reviewer",
|
|
36
|
-
model: options.config.model,
|
|
37
|
-
thinking: options.config.thinking,
|
|
38
|
-
tools: options.config.tools,
|
|
39
|
-
prompt: options.prompt,
|
|
40
|
-
cwd: options.cwd,
|
|
41
|
-
timeoutMs: options.config.timeoutMs,
|
|
42
|
-
signal: options.signal,
|
|
43
|
-
onEvent: options.onEvent,
|
|
44
|
-
});
|
|
45
|
-
const parsed =
|
|
46
|
-
result.kind === "output"
|
|
47
|
-
? parseReviewOutput(result.text, options.language)
|
|
48
|
-
: processFailure(result, options.language);
|
|
49
|
-
return {
|
|
50
|
-
index: options.index,
|
|
51
|
-
model: options.config.model,
|
|
52
|
-
thinking: options.config.thinking,
|
|
53
|
-
status: parsed.status,
|
|
54
|
-
summary: parsed.summary,
|
|
55
|
-
details: parsed.details,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 解析审查者文本输出:首行严格 PASS/FAIL + 证据锚点闸门。 */
|
|
60
|
-
export function parseReviewOutput(text: string, language: Language): ParseOutcome {
|
|
61
|
-
const trimmed = stripApplyInstruction(text);
|
|
62
|
-
if (!trimmed) return invalidFormat("(empty)", language);
|
|
63
|
-
const [firstLine = "", ...rest] = trimmed.split(/\r?\n/);
|
|
64
|
-
const verdict = verdictOf(firstLine);
|
|
65
|
-
if (verdict === "PASS") {
|
|
66
|
-
const body = rest.join("\n").trim();
|
|
67
|
-
const issue = passIssue(body);
|
|
68
|
-
if (issue) return contractViolation(issue, language);
|
|
69
|
-
const summary = firstSummary(body);
|
|
70
|
-
// passIssue 只保证证据行前有行,那行可能是建议区标题;没有真摘要就是契约违规,
|
|
71
|
-
// 不能让 undefined 流进多模型汇总把循环撞死。
|
|
72
|
-
if (!summary) return contractViolation("PASS 缺少摘要行(证据行前必须有一行极简摘要)", language);
|
|
73
|
-
return { status: "passed", summary, details: body };
|
|
74
|
-
}
|
|
75
|
-
if (verdict === "FAIL") {
|
|
76
|
-
const body = rest.join("\n").trim();
|
|
77
|
-
const issue = failIssue(body, language);
|
|
78
|
-
if (issue) return contractViolation(issue, language);
|
|
79
|
-
return { status: "failed", summary: firstIssue(body, language), details: body };
|
|
80
|
-
}
|
|
81
|
-
return invalidFormat(firstLine.trim() || "(empty)", language);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function processFailure(
|
|
85
|
-
result:
|
|
86
|
-
| { kind: "timeout" }
|
|
87
|
-
| { kind: "aborted" }
|
|
88
|
-
| { kind: "error"; message: string }
|
|
89
|
-
| { kind: "empty" },
|
|
90
|
-
language: Language,
|
|
91
|
-
): ParseOutcome {
|
|
92
|
-
if (result.kind === "aborted")
|
|
93
|
-
return { status: "error", summary: "", details: "" };
|
|
94
|
-
if (result.kind === "timeout")
|
|
95
|
-
return { status: "error", summary: "", details: systemError(language, "timeout") };
|
|
96
|
-
if (result.kind === "error")
|
|
97
|
-
return {
|
|
98
|
-
status: "error",
|
|
99
|
-
summary: "",
|
|
100
|
-
details: `${systemError(language, "start")}${result.message}`,
|
|
101
|
-
};
|
|
102
|
-
return { status: "error", summary: "", details: systemError(language, "empty") };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 首行判定失败(既不是 PASS 也不是 FAIL)。 */
|
|
106
|
-
function invalidFormat(actual: string, language: Language): ParseOutcome {
|
|
107
|
-
const prefix =
|
|
108
|
-
language === "en"
|
|
109
|
-
? `first line must be PASS or FAIL; actual: `
|
|
110
|
-
: `第一行必须是 PASS 或 FAIL;实际是:`;
|
|
111
|
-
return contractViolation(prefix + tail(actual), language);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** 输出契约违例:该票作废记为基础设施错误,不拖垮整轮。 */
|
|
115
|
-
function contractViolation(issue: string, language: Language): ParseOutcome {
|
|
116
|
-
const prefix =
|
|
117
|
-
language === "en" ? "review output format invalid: " : "审查输出格式无效:";
|
|
118
|
-
return { status: "error", summary: "", details: prefix + issue };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function systemError(language: Language, kind: "timeout" | "start" | "empty") {
|
|
122
|
-
if (language === "en") {
|
|
123
|
-
if (kind === "timeout") return "review session timed out before returning valid output. ";
|
|
124
|
-
if (kind === "start") return "review session failed. ";
|
|
125
|
-
return "review output is empty: no check result. ";
|
|
126
|
-
}
|
|
127
|
-
if (kind === "timeout") return "审查会话超时,未在时限内返回有效输出。";
|
|
128
|
-
if (kind === "start") return "审查会话失败。";
|
|
129
|
-
return "审查输出为空:无审查结论。";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function tail(text: string) {
|
|
133
|
-
return text.length > 500 ? `${text.slice(0, 500)}…` : text;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---- 契约解析(纯函数)----
|
|
137
|
-
|
|
138
|
-
export function verdictOf(line: string): "PASS" | "FAIL" | undefined {
|
|
139
|
-
const normalized = line
|
|
140
|
-
.trim()
|
|
141
|
-
.replace(/^\*{1,2}(.+?)\*{1,2}$/u, "$1")
|
|
142
|
-
.replace(/^__([^_]+)__$/u, "$1")
|
|
143
|
-
// 与 advisor 同模式:模型把判定词写成 `PASS` 时剔掉包裹的反引号,避免整票误判为格式错误。
|
|
144
|
-
.replace(/^`+(.+?)`+$/u, "$1")
|
|
145
|
-
.trim()
|
|
146
|
-
.toUpperCase();
|
|
147
|
-
return normalized === "PASS" || normalized === "FAIL" ? normalized : undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const EVIDENCE_LINE = /^(?:[-*]\s*)?(?:证据:|Evidence:)/u;
|
|
151
|
-
const FILE_SEGMENT = /(?:文件|files)\s*=\s*([^;;]*)/iu;
|
|
152
|
-
const COMMAND_SEGMENT = /(?:命令|commands)\s*=\s*([^;;]*)/iu;
|
|
153
|
-
const FILE_ANCHOR = /[\w@./-]*\w\.[a-zA-Z]\w{0,5}\b/u;
|
|
154
|
-
const FINDING_ISSUE = /^[-*+]\s*(?:\*\*)?(?:问题|Issue)(?:\*\*)?\s*[::]\s*(.*)$/u;
|
|
155
|
-
// 不能用 \b 收尾:中文不是\w,「发现 1」里「现」与空格之间不构成词边界。
|
|
156
|
-
const FINDING_HEADING = /^#{1,6}\s*(?:发现|Finding)/u;
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 发现必填字段,事实源是 `prompts/review.{zh,en}.md` 的输出契约。
|
|
160
|
-
* 只校验字段存在且非空,不校验取值(如严重程度写“高危”不应被判非法)。
|
|
161
|
-
*/
|
|
162
|
-
// 字段标签容忍可选粗体包裹与新旧两套措辞:滚动开放清单里可能混有旧格式发现的复述。
|
|
163
|
-
const FINDING_FIELDS = [
|
|
164
|
-
{
|
|
165
|
-
key: "severity",
|
|
166
|
-
zh: "严重程度",
|
|
167
|
-
en: "Severity",
|
|
168
|
-
// 提示词规定阻塞发现只有高/中;低严重度必须进建议区,不得驱动修复循环。
|
|
169
|
-
pattern: /^[-*+]\s*(?:\*\*)?(?:严重程度|Severity)(?:\*\*)?\s*[::]\s*(?:高|中|High|Medium)\s*$/iu,
|
|
170
|
-
},
|
|
171
|
-
{ key: "issue", zh: "问题", en: "Issue", pattern: /^[-*+]\s*(?:\*\*)?(?:问题|Issue)(?:\*\*)?\s*[::]\s*(\S.*)$/u },
|
|
172
|
-
{
|
|
173
|
-
key: "evidence",
|
|
174
|
-
zh: "证据",
|
|
175
|
-
en: "Evidence",
|
|
176
|
-
pattern: /^[-*+]\s*(?:\*\*)?(?:证据|Evidence)(?:\*\*)?\s*[::]\s*(\S.*)$/u,
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
key: "contract",
|
|
180
|
-
zh: "违反的约定与期望行为",
|
|
181
|
-
en: "Violated agreement & expected behavior",
|
|
182
|
-
pattern:
|
|
183
|
-
/^[-*+]\s*(?:\*\*)?(?:违反的(?:约定与期望(?:行为)?|契约(?:或期望行为)?)|Violated agreement(?: & expected behavior)?|Contract(?: or expected behavior)? violated)(?:\*\*)?\s*[::]\s*(\S.*)$/u,
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
key: "commands",
|
|
187
|
-
zh: "验证命令",
|
|
188
|
-
en: "Verification command",
|
|
189
|
-
pattern:
|
|
190
|
-
/^[-*+]\s*(?:\*\*)?(?:(?:需要运行的)?验证命令|Verification commands?(?: to run)?)(?:\*\*)?\s*[::]\s*(\S.*)$/u,
|
|
191
|
-
},
|
|
192
|
-
] as const;
|
|
193
|
-
const SUGGESTIONS_HEADING = /^##\s+(?:建议(非阻塞)|Suggestions \(non-blocking\))\s*$/iu;
|
|
194
|
-
|
|
195
|
-
/** PASS 证据锚点闸门:摘要行在前,首个证据行必须同时含文件段(带扩展名)与命令段。 */
|
|
196
|
-
export function passIssue(body: string): string | undefined {
|
|
197
|
-
const lines = body
|
|
198
|
-
.split(/\r?\n/)
|
|
199
|
-
.map((line) => line.trim())
|
|
200
|
-
.filter(Boolean);
|
|
201
|
-
const evidenceIndex = lines.findIndex((line) => EVIDENCE_LINE.test(line));
|
|
202
|
-
if (evidenceIndex === -1) return "PASS 缺少证据锚点行(证据:文件=…;命令=…)";
|
|
203
|
-
if (evidenceIndex === 0) return "PASS 缺少摘要行(证据行前必须有一行极简摘要)";
|
|
204
|
-
const line = lines[evidenceIndex];
|
|
205
|
-
if (!hasFileSegment(line)) return "PASS 证据行缺少文件段(文件=至少一个带扩展名的路径)";
|
|
206
|
-
if (!hasCommandSegment(line)) return "PASS 证据行缺少命令段(命令=实际运行的命令)";
|
|
207
|
-
return undefined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* FAIL 发现闸门:至少一条带「问题」的发现,且不能全落在「建议(非阻塞)」区。
|
|
212
|
-
* 空 FAIL 或一段散文都不能驱动执行模型改代码——格式非法的票一律作废为基础设施错误。
|
|
213
|
-
*/
|
|
214
|
-
export function failIssue(body: string, language: Language): string | undefined {
|
|
215
|
-
const noFinding =
|
|
216
|
-
language === "en"
|
|
217
|
-
? "FAIL has no blocking finding: a `## Finding` section is required"
|
|
218
|
-
: "FAIL 缺少阻塞发现:需要一个「## 发现」小节";
|
|
219
|
-
if (!body) return noFinding;
|
|
220
|
-
const blocking = (body.split(SUGGESTIONS_HEADING_SPLIT)[0] ?? "")
|
|
221
|
-
.split(/\r?\n/)
|
|
222
|
-
.map((line) => line.trim());
|
|
223
|
-
const starts = blocking
|
|
224
|
-
.map((line, index) => (FINDING_HEADING.test(line) ? index : -1))
|
|
225
|
-
.filter((index) => index >= 0);
|
|
226
|
-
if (starts.length === 0) return noFinding;
|
|
227
|
-
// 每条发现都必须满足完整契约;同票混入非法发现整票作废。
|
|
228
|
-
// 契约完整才能驱动自动修复:半成品票据无法核实,也无法验收。
|
|
229
|
-
for (const [order, start] of starts.entries()) {
|
|
230
|
-
const end = starts[order + 1] ?? blocking.length;
|
|
231
|
-
const section = blocking.slice(start, end);
|
|
232
|
-
const missing = FINDING_FIELDS.filter(
|
|
233
|
-
(field) => !sectionHasField(section, field),
|
|
234
|
-
);
|
|
235
|
-
if (missing.length > 0)
|
|
236
|
-
return missingFieldsMessage(order + 1, missing, language);
|
|
237
|
-
}
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function sectionHasField(section: readonly string[], field: (typeof FINDING_FIELDS)[number]): boolean {
|
|
242
|
-
for (let i = 0; i < section.length; i += 1) {
|
|
243
|
-
const line = section[i] ?? "";
|
|
244
|
-
if (field.pattern.test(line)) return true;
|
|
245
|
-
if (field.key === "severity") continue;
|
|
246
|
-
const tagSource = field.pattern.source.replace(/\s*\(\\S\.\*\)\$/u, "\\s*");
|
|
247
|
-
const tagPattern = new RegExp(tagSource, "iu");
|
|
248
|
-
if (tagPattern.test(line)) {
|
|
249
|
-
const inline = line.replace(tagPattern, "").trim();
|
|
250
|
-
if (inline) return true;
|
|
251
|
-
for (let j = i + 1; j < section.length; j += 1) {
|
|
252
|
-
const nextLine = (section[j] ?? "").trim();
|
|
253
|
-
if (/^[-*+]\s*(?:\*\*)?(?:严重程度|Severity|问题|Issue|证据|Evidence|违反|Violated|Contract|验证|Verification)/iu.test(nextLine) || /^#{1,6}\s+/u.test(nextLine)) {
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
if (nextLine) return true;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function missingFieldsMessage(
|
|
264
|
-
index: number,
|
|
265
|
-
missing: readonly (typeof FINDING_FIELDS)[number][],
|
|
266
|
-
language: Language,
|
|
267
|
-
) {
|
|
268
|
-
const names = missing
|
|
269
|
-
.map((field) => (language === "en" ? field.en : field.zh))
|
|
270
|
-
.join(language === "en" ? ", " : "、");
|
|
271
|
-
return language === "en"
|
|
272
|
-
? `FAIL finding ${index} is missing required fields: ${names}`
|
|
273
|
-
: `FAIL 第 ${index} 条发现缺少必填字段:${names}`;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const SUGGESTIONS_HEADING_SPLIT =
|
|
277
|
-
/^##\s+(?:建议(非阻塞)|Suggestions \(non-blocking\))\s*$/imu;
|
|
278
|
-
|
|
279
|
-
function hasFileSegment(line: string) {
|
|
280
|
-
const segment = FILE_SEGMENT.exec(line)?.[1];
|
|
281
|
-
return Boolean(segment && FILE_ANCHOR.test(segment));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function hasCommandSegment(line: string) {
|
|
285
|
-
return Boolean(COMMAND_SEGMENT.exec(line)?.[1]?.trim());
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** 汇总摘要:剥离证据锚点行与建议区后的首行。 */
|
|
289
|
-
function firstSummary(body: string) {
|
|
290
|
-
return body
|
|
291
|
-
.split(/\r?\n/)
|
|
292
|
-
.map((line) => line.trim())
|
|
293
|
-
.filter((line) => line && !EVIDENCE_LINE.test(line) && !SUGGESTIONS_HEADING.test(line))[0];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** 发现一句话问题(FAIL 卡片回顾用):取第一条「- 问题:」行(支持同行及换行)。 */
|
|
297
|
-
function firstIssue(body: string, language: Language) {
|
|
298
|
-
const lines = body.split(/\r?\n/).map((line) => line.trim());
|
|
299
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
300
|
-
const line = lines[i] ?? "";
|
|
301
|
-
const match = FINDING_ISSUE.exec(line);
|
|
302
|
-
if (match) {
|
|
303
|
-
if (match[1]?.trim()) return clean(match[1]);
|
|
304
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
305
|
-
const nextLine = lines[j] ?? "";
|
|
306
|
-
if (/^[-*+]\s+/u.test(nextLine) || /^#{1,6}\s+/u.test(nextLine)) break;
|
|
307
|
-
if (nextLine.trim()) return clean(nextLine);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return language === "en" ? "Review failed with findings." : "审查未通过,存在发现。";
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function clean(text: string) {
|
|
315
|
-
return text.replace(/`([^`]+)`/gu, "$1").trim();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** 去掉模型可能夹带的"应用反馈"指令尾巴(防执行模型把修复指令回传给审查者)。 */
|
|
319
|
-
function stripApplyInstruction(text: string) {
|
|
320
|
-
return removeWhitespaceInsensitive(
|
|
321
|
-
removeWhitespaceInsensitive(text, APPLY_INSTRUCTION_ZH),
|
|
322
|
-
APPLY_INSTRUCTION_EN,
|
|
323
|
-
)
|
|
324
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
325
|
-
.trim();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const APPLY_INSTRUCTION_ZH =
|
|
329
|
-
"将审查反馈视为待核实假设,而非事实;先基于当前文件、测试/检查输出和会话约束核实。反馈属实时,逐条修复全部属实发现,修根因而非表象,同一根因的其他出现点一并修复,修完端到端验证问题已彻底解决再结束,避免无关重构、抽象、依赖或风格改动;反馈不成立时,不应用该反馈,并说明依据(文件、命令输出或约束)。";
|
|
330
|
-
const APPLY_INSTRUCTION_EN =
|
|
331
|
-
"Treat the review feedback as hypotheses to verify, not facts; verify against current files, test/check output and session constraints. When feedback is valid, fix every valid finding, fixing root causes not symptoms, fixing other occurrences of the same root cause too, and verify end-to-end that issues are truly resolved before finishing; avoid unrelated refactors, abstractions, dependency or style changes. When feedback is not valid, do not apply it and explain why (files, command output, or constraints).";
|
|
332
|
-
|
|
333
|
-
function removeWhitespaceInsensitive(text: string, needle: string) {
|
|
334
|
-
let result = "";
|
|
335
|
-
let index = 0;
|
|
336
|
-
while (index < text.length) {
|
|
337
|
-
const matchEnd = whitespaceInsensitiveMatchEnd(text, needle, index);
|
|
338
|
-
if (matchEnd === undefined) {
|
|
339
|
-
result += text[index];
|
|
340
|
-
index += 1;
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
index = matchEnd;
|
|
344
|
-
}
|
|
345
|
-
return result;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function whitespaceInsensitiveMatchEnd(text: string, needle: string, start: number) {
|
|
349
|
-
let textIndex = start;
|
|
350
|
-
for (const char of needle) {
|
|
351
|
-
while (textIndex < text.length && /\s/.test(text[textIndex])) textIndex += 1;
|
|
352
|
-
if (text[textIndex] !== char) return undefined;
|
|
353
|
-
textIndex += 1;
|
|
354
|
-
}
|
|
355
|
-
return textIndex;
|
|
356
|
-
}
|
package/review/session.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { ThinkingLevelValue } from "../config.js";
|
|
4
|
-
import type { InProcessSessionPool } from "../master/spawn.js";
|
|
5
|
-
import type { PromptLayers } from "./prompt.js";
|
|
6
|
-
|
|
7
|
-
export type ReviewSessionResult =
|
|
8
|
-
| { kind: "output"; text: string }
|
|
9
|
-
| { kind: "empty" }
|
|
10
|
-
| { kind: "timeout" }
|
|
11
|
-
| { kind: "aborted" }
|
|
12
|
-
| { kind: "error"; message: string };
|
|
13
|
-
|
|
14
|
-
export interface ReviewSessionOptions {
|
|
15
|
-
pool: InProcessSessionPool;
|
|
16
|
-
resolveModel(id: string): Promise<Model<any>>;
|
|
17
|
-
role: "reviewer" | "advisor";
|
|
18
|
-
model: string;
|
|
19
|
-
thinking: ThinkingLevelValue;
|
|
20
|
-
tools: string[];
|
|
21
|
-
prompt: PromptLayers;
|
|
22
|
-
cwd: string;
|
|
23
|
-
timeoutMs: number;
|
|
24
|
-
signal?: AbortSignal;
|
|
25
|
-
onEvent?: (event: AgentSessionEvent) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type ReviewSessionRunner = (
|
|
29
|
-
options: Omit<ReviewSessionOptions, "pool" | "resolveModel">,
|
|
30
|
-
) => Promise<ReviewSessionResult>;
|
|
31
|
-
|
|
32
|
-
export function createReviewSessionRunner(
|
|
33
|
-
pool: InProcessSessionPool,
|
|
34
|
-
resolveModel: ReviewSessionOptions["resolveModel"],
|
|
35
|
-
): ReviewSessionRunner {
|
|
36
|
-
return (options) => runReviewSession({ ...options, pool, resolveModel });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function runReviewSession(options: ReviewSessionOptions): Promise<ReviewSessionResult> {
|
|
40
|
-
if (options.signal?.aborted) return { kind: "aborted" };
|
|
41
|
-
let spawned: Awaited<ReturnType<InProcessSessionPool["spawn"]>>;
|
|
42
|
-
try {
|
|
43
|
-
spawned = await options.pool.spawn({
|
|
44
|
-
cwd: options.cwd,
|
|
45
|
-
role: options.role,
|
|
46
|
-
model: await options.resolveModel(options.model),
|
|
47
|
-
thinking: options.thinking,
|
|
48
|
-
tools: [...new Set(options.tools)].filter((tool) => tool !== "write" && tool !== "edit"),
|
|
49
|
-
systemPrompt: { mode: "replace", text: clean(options.prompt.system) },
|
|
50
|
-
contextFiles: false,
|
|
51
|
-
persistence: { type: "memory" },
|
|
52
|
-
isolated: true,
|
|
53
|
-
});
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return options.signal?.aborted
|
|
56
|
-
? { kind: "aborted" }
|
|
57
|
-
: { kind: "error", message: errorText(error) };
|
|
58
|
-
}
|
|
59
|
-
if (options.signal?.aborted) {
|
|
60
|
-
spawned.dispose();
|
|
61
|
-
return { kind: "aborted" };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let finalText: string | undefined;
|
|
65
|
-
let finalError: string | undefined;
|
|
66
|
-
const unsubscribe = spawned.session.subscribe((event) => {
|
|
67
|
-
const assistant = assistantMessage(event);
|
|
68
|
-
if (assistant) {
|
|
69
|
-
finalText = messageText(assistant.content);
|
|
70
|
-
finalError = assistant.stopReason === "error"
|
|
71
|
-
? assistant.errorMessage || "model error"
|
|
72
|
-
: undefined;
|
|
73
|
-
}
|
|
74
|
-
options.onEvent?.(event);
|
|
75
|
-
});
|
|
76
|
-
let interrupted: "aborted" | "timeout" | undefined;
|
|
77
|
-
let wake!: () => void;
|
|
78
|
-
const interruption = new Promise<void>((resolve) => { wake = resolve; });
|
|
79
|
-
const onAbort = () => { interrupted = "aborted"; wake(); };
|
|
80
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
81
|
-
const timeout = setTimeout(() => { interrupted = "timeout"; wake(); }, options.timeoutMs);
|
|
82
|
-
try {
|
|
83
|
-
const run = spawned.prompt(clean(options.prompt.user)).catch((error) => {
|
|
84
|
-
finalError = errorText(error);
|
|
85
|
-
});
|
|
86
|
-
await Promise.race([run, interruption]);
|
|
87
|
-
if (interrupted) {
|
|
88
|
-
await spawned.session.abort();
|
|
89
|
-
return { kind: interrupted };
|
|
90
|
-
}
|
|
91
|
-
if (finalError) return { kind: "error", message: finalError };
|
|
92
|
-
return finalText?.trim() ? { kind: "output", text: finalText } : { kind: "empty" };
|
|
93
|
-
} finally {
|
|
94
|
-
clearTimeout(timeout);
|
|
95
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
96
|
-
unsubscribe();
|
|
97
|
-
spawned.dispose();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function assistantMessage(event: AgentSessionEvent): {
|
|
102
|
-
role?: string;
|
|
103
|
-
content?: unknown;
|
|
104
|
-
stopReason?: string;
|
|
105
|
-
errorMessage?: string;
|
|
106
|
-
} | undefined {
|
|
107
|
-
if (event.type === "message_end") return event.message.role === "assistant" ? event.message : undefined;
|
|
108
|
-
if (event.type !== "agent_end") return undefined;
|
|
109
|
-
return [...event.messages].reverse().find((message) => message.role === "assistant");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function messageText(content: unknown): string {
|
|
113
|
-
if (typeof content === "string") return content;
|
|
114
|
-
if (!Array.isArray(content)) return "";
|
|
115
|
-
return content
|
|
116
|
-
.filter((part): part is { type: "text"; text: string } =>
|
|
117
|
-
typeof part === "object" && part !== null && (part as { type?: unknown }).type === "text"
|
|
118
|
-
&& typeof (part as { text?: unknown }).text === "string")
|
|
119
|
-
.map((part) => part.text)
|
|
120
|
-
.join("\n");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function clean(text: string): string {
|
|
124
|
-
return text.replaceAll("\0", "");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function errorText(error: unknown): string {
|
|
128
|
-
return error instanceof Error ? error.message : String(error);
|
|
129
|
-
}
|