pi-skill-evolve 0.0.0-stage → 0.1.2
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/CHANGELOG.md +12 -0
- package/README.md +81 -2
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/after.md +10 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/after.md +9 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/after.md +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/fixture.json +6 -0
- package/extensions/skill-evolve/index.ts +300 -0
- package/extensions/skill-evolve/lib/apply-gates.ts +204 -0
- package/extensions/skill-evolve/lib/apply-targets.ts +90 -0
- package/extensions/skill-evolve/lib/apply.ts +141 -0
- package/extensions/skill-evolve/lib/attribution.ts +138 -0
- package/extensions/skill-evolve/lib/audit.ts +221 -0
- package/extensions/skill-evolve/lib/baseline.ts +176 -0
- package/extensions/skill-evolve/lib/child-env.ts +10 -0
- package/extensions/skill-evolve/lib/classify-action.ts +161 -0
- package/extensions/skill-evolve/lib/command.ts +100 -0
- package/extensions/skill-evolve/lib/config-command.ts +38 -0
- package/extensions/skill-evolve/lib/config.ts +194 -0
- package/extensions/skill-evolve/lib/detect.ts +62 -0
- package/extensions/skill-evolve/lib/diff-apply.ts +90 -0
- package/extensions/skill-evolve/lib/diff-gen.ts +78 -0
- package/extensions/skill-evolve/lib/dispatch.ts +300 -0
- package/extensions/skill-evolve/lib/evolution-apply.ts +265 -0
- package/extensions/skill-evolve/lib/evolution-model.ts +72 -0
- package/extensions/skill-evolve/lib/evolution-observe.ts +82 -0
- package/extensions/skill-evolve/lib/evolution-views.ts +89 -0
- package/extensions/skill-evolve/lib/evolution.ts +289 -0
- package/extensions/skill-evolve/lib/flags.ts +44 -0
- package/extensions/skill-evolve/lib/gate0.ts +78 -0
- package/extensions/skill-evolve/lib/hard-rules.ts +167 -0
- package/extensions/skill-evolve/lib/lock.ts +215 -0
- package/extensions/skill-evolve/lib/logfile.ts +52 -0
- package/extensions/skill-evolve/lib/mechanical-report.ts +95 -0
- package/extensions/skill-evolve/lib/mechanical.ts +136 -0
- package/extensions/skill-evolve/lib/metrics.ts +172 -0
- package/extensions/skill-evolve/lib/model-config.ts +95 -0
- package/extensions/skill-evolve/lib/notify.ts +132 -0
- package/extensions/skill-evolve/lib/panel.ts +174 -0
- package/extensions/skill-evolve/lib/pending.ts +34 -0
- package/extensions/skill-evolve/lib/proc.ts +65 -0
- package/extensions/skill-evolve/lib/prompt.ts +80 -0
- package/extensions/skill-evolve/lib/proposal.ts +227 -0
- package/extensions/skill-evolve/lib/prune-plan.ts +62 -0
- package/extensions/skill-evolve/lib/queue.ts +136 -0
- package/extensions/skill-evolve/lib/replay-fixture.ts +80 -0
- package/extensions/skill-evolve/lib/replay.ts +246 -0
- package/extensions/skill-evolve/lib/report.ts +181 -0
- package/extensions/skill-evolve/lib/review-post.ts +101 -0
- package/extensions/skill-evolve/lib/review-prompt.ts +82 -0
- package/extensions/skill-evolve/lib/review-record.ts +63 -0
- package/extensions/skill-evolve/lib/review-run.ts +131 -0
- package/extensions/skill-evolve/lib/review-spawn.ts +83 -0
- package/extensions/skill-evolve/lib/review-supervisor.ts +267 -0
- package/extensions/skill-evolve/lib/round.ts +116 -0
- package/extensions/skill-evolve/lib/run-watch.ts +34 -0
- package/extensions/skill-evolve/lib/runs.ts +208 -0
- package/extensions/skill-evolve/lib/selftest.ts +201 -0
- package/extensions/skill-evolve/lib/session-index.ts +145 -0
- package/extensions/skill-evolve/lib/shutdown-dispatch.ts +177 -0
- package/extensions/skill-evolve/lib/single-flight.ts +111 -0
- package/extensions/skill-evolve/lib/spawn-plan.ts +103 -0
- package/extensions/skill-evolve/lib/stale.ts +217 -0
- package/extensions/skill-evolve/lib/telemetry.ts +205 -0
- package/extensions/skill-evolve/lib/ui-gate.ts +34 -0
- package/extensions/skill-evolve/lib/web/actions.ts +299 -0
- package/extensions/skill-evolve/lib/web/activity.ts +57 -0
- package/extensions/skill-evolve/lib/web/applied.ts +64 -0
- package/extensions/skill-evolve/lib/web/approvals.ts +58 -0
- package/extensions/skill-evolve/lib/web/audit-log.ts +115 -0
- package/extensions/skill-evolve/lib/web/autostart.ts +177 -0
- package/extensions/skill-evolve/lib/web/data.ts +298 -0
- package/extensions/skill-evolve/lib/web/diff-lines.ts +141 -0
- package/extensions/skill-evolve/lib/web/filter.ts +203 -0
- package/extensions/skill-evolve/lib/web/http.ts +68 -0
- package/extensions/skill-evolve/lib/web/json-read.ts +33 -0
- package/extensions/skill-evolve/lib/web/panel-pid.ts +99 -0
- package/extensions/skill-evolve/lib/web/render-controls.ts +156 -0
- package/extensions/skill-evolve/lib/web/render-rows.ts +252 -0
- package/extensions/skill-evolve/lib/web/render-run.ts +188 -0
- package/extensions/skill-evolve/lib/web/render-skills.ts +164 -0
- package/extensions/skill-evolve/lib/web/render.ts +136 -0
- package/extensions/skill-evolve/lib/web/reports.ts +160 -0
- package/extensions/skill-evolve/lib/web/run-detail.ts +234 -0
- package/extensions/skill-evolve/lib/web/run-path.ts +21 -0
- package/extensions/skill-evolve/lib/web/server.ts +268 -0
- package/extensions/skill-evolve/lib/web/shell.ts +87 -0
- package/extensions/skill-evolve/lib/web/skills.ts +202 -0
- package/extensions/skill-evolve/lib/web/style.ts +107 -0
- package/extensions/skill-evolve/lib/web/text.ts +52 -0
- package/extensions/skill-evolve/lib/web/trash.ts +36 -0
- package/extensions/skill-evolve/lib/weekly.ts +285 -0
- package/extensions/skill-evolve/prompts//345/244/215/347/233/230.md +172 -0
- package/extensions/skill-evolve/scripts/replay.ts +93 -0
- package/extensions/skill-evolve/test/apply.test.ts +278 -0
- package/extensions/skill-evolve/test/approve.test.ts +248 -0
- package/extensions/skill-evolve/test/attribution.test.ts +133 -0
- package/extensions/skill-evolve/test/audit.test.ts +151 -0
- package/extensions/skill-evolve/test/baseline.test.ts +132 -0
- package/extensions/skill-evolve/test/classify-action.test.ts +133 -0
- package/extensions/skill-evolve/test/command.test.ts +267 -0
- package/extensions/skill-evolve/test/config-command.test.ts +73 -0
- package/extensions/skill-evolve/test/config.test.ts +154 -0
- package/extensions/skill-evolve/test/detect.test.ts +71 -0
- package/extensions/skill-evolve/test/diff-apply.test.ts +46 -0
- package/extensions/skill-evolve/test/dispatch-busy.test.ts +223 -0
- package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +88 -0
- package/extensions/skill-evolve/test/dispatch.test.ts +306 -0
- package/extensions/skill-evolve/test/evolution.test.ts +378 -0
- package/extensions/skill-evolve/test/fake-review-spawn.ts +60 -0
- package/extensions/skill-evolve/test/hard-rules.test.ts +123 -0
- package/extensions/skill-evolve/test/lock.test.ts +138 -0
- package/extensions/skill-evolve/test/logfile.test.ts +85 -0
- package/extensions/skill-evolve/test/mechanical-wiring.test.ts +217 -0
- package/extensions/skill-evolve/test/metrics.test.ts +129 -0
- package/extensions/skill-evolve/test/model-config.test.ts +84 -0
- package/extensions/skill-evolve/test/notify.test.ts +133 -0
- package/extensions/skill-evolve/test/observe.test.ts +100 -0
- package/extensions/skill-evolve/test/panel-pid.test.ts +84 -0
- package/extensions/skill-evolve/test/panel.test.ts +115 -0
- package/extensions/skill-evolve/test/proposal.test.ts +159 -0
- package/extensions/skill-evolve/test/queue.test.ts +110 -0
- package/extensions/skill-evolve/test/reject.test.ts +134 -0
- package/extensions/skill-evolve/test/replay.test.ts +111 -0
- package/extensions/skill-evolve/test/report.test.ts +66 -0
- package/extensions/skill-evolve/test/review-post.test.ts +92 -0
- package/extensions/skill-evolve/test/review-prompt.test.ts +105 -0
- package/extensions/skill-evolve/test/review-spawn.test.ts +81 -0
- package/extensions/skill-evolve/test/review-supervisor.test.ts +243 -0
- package/extensions/skill-evolve/test/selftest.test.ts +113 -0
- package/extensions/skill-evolve/test/session-index.test.ts +128 -0
- package/extensions/skill-evolve/test/single-flight.test.ts +122 -0
- package/extensions/skill-evolve/test/stale.test.ts +272 -0
- package/extensions/skill-evolve/test/telemetry.test.ts +145 -0
- package/extensions/skill-evolve/test/ui-gate.test.ts +77 -0
- package/extensions/skill-evolve/test/web-actions.test.ts +262 -0
- package/extensions/skill-evolve/test/web-applied.test.ts +100 -0
- package/extensions/skill-evolve/test/web-audit.test.ts +87 -0
- package/extensions/skill-evolve/test/web-autostart.test.ts +268 -0
- package/extensions/skill-evolve/test/web-data.test.ts +420 -0
- package/extensions/skill-evolve/test/web-diff-lines.test.ts +155 -0
- package/extensions/skill-evolve/test/web-filter.test.ts +212 -0
- package/extensions/skill-evolve/test/web-panel-pid.test.ts +97 -0
- package/extensions/skill-evolve/test/web-render-run.test.ts +132 -0
- package/extensions/skill-evolve/test/web-render-skills.test.ts +134 -0
- package/extensions/skill-evolve/test/web-render.test.ts +300 -0
- package/extensions/skill-evolve/test/web-run-detail.test.ts +256 -0
- package/extensions/skill-evolve/test/web-server-views.test.ts +113 -0
- package/extensions/skill-evolve/test/web-server.test.ts +465 -0
- package/extensions/skill-evolve/test/web-shell.test.ts +67 -0
- package/extensions/skill-evolve/test/web-skills.test.ts +239 -0
- package/extensions/skill-evolve/test/weekly.test.ts +123 -0
- package/package.json +34 -4
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run 记录(`runs/<runId>/run.json`)的读写:
|
|
3
|
+
* 状态机 dispatched → analyzed / proposed → reviewed / rejected / review_failed(或任一步 abandoned)。
|
|
4
|
+
*
|
|
5
|
+
* 拆成独立文件是为了让 dispatch.ts(派生流程)与 evolution.ts(状态/报告)共用同一套
|
|
6
|
+
* 序列化口径,同时把 dispatch.ts 压在 300 行以内。
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { readProposalSummary, type ProposalSummary } from "./proposal.ts";
|
|
11
|
+
import type { SupervisorStatus } from "./review-record.ts";
|
|
12
|
+
import type { ActionLevel } from "./classify-action.ts";
|
|
13
|
+
import type { MutationQueue } from "./telemetry.ts";
|
|
14
|
+
|
|
15
|
+
export const RUN_STATUSES = [
|
|
16
|
+
"dispatched",
|
|
17
|
+
"analyzed",
|
|
18
|
+
"proposed",
|
|
19
|
+
"reviewed",
|
|
20
|
+
"rejected",
|
|
21
|
+
"review_failed",
|
|
22
|
+
"abandoned",
|
|
23
|
+
// M5 采纳结果(闸门逐闸推进,见 apply.ts)
|
|
24
|
+
"applied",
|
|
25
|
+
"rolled-back",
|
|
26
|
+
"unauthorized_write",
|
|
27
|
+
"failed",
|
|
28
|
+
// 卡死(子进程已死 / 超时):判定见 lib/stale.ts,不自动重跑
|
|
29
|
+
"stale",
|
|
30
|
+
] as const;
|
|
31
|
+
export type RunStatus = (typeof RUN_STATUSES)[number];
|
|
32
|
+
|
|
33
|
+
export interface RunStage {
|
|
34
|
+
name: string;
|
|
35
|
+
model: string | null;
|
|
36
|
+
usage: unknown;
|
|
37
|
+
ms: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RunRecord {
|
|
41
|
+
runId: string;
|
|
42
|
+
status: RunStatus;
|
|
43
|
+
/** 谁触发的:自动结算 / 手动命令 / 退出前补发 —— 报告里能区分别把“手动兜底”当自动行为 */
|
|
44
|
+
trigger: "auto" | "manual" | "shutdown";
|
|
45
|
+
sessionId: string;
|
|
46
|
+
sessionHash: string;
|
|
47
|
+
sessionFile: string | null;
|
|
48
|
+
/** 会话原文不可读时降级为 telemetry-only,报告里要写明 */
|
|
49
|
+
evidenceMode: "session+telemetry" | "telemetry-only";
|
|
50
|
+
dispatchedAt: number;
|
|
51
|
+
dispatchedAtIso: string;
|
|
52
|
+
pid: number | null;
|
|
53
|
+
/** 评审督导 pid(全局单飞闸的第二个占用位):派发时写入,督导退出/失败时清成 null */
|
|
54
|
+
supervisorPid?: number | null;
|
|
55
|
+
cwd: string;
|
|
56
|
+
skills: string[];
|
|
57
|
+
/** 去抖合并掉的轮次范围(缺省 = 只覆盖刚结束的 1 轮) */
|
|
58
|
+
coverage?: { rounds: number; since: number };
|
|
59
|
+
stages: RunStage[];
|
|
60
|
+
artifacts: Record<string, string>;
|
|
61
|
+
/** M3:复盘子进程产出提案后回写({type, skill, status, added, removed, file}) */
|
|
62
|
+
proposal?: ProposalSummary;
|
|
63
|
+
/** M4:评审督导回写(只给建议动作,本版一律不写 .pi/skills) */
|
|
64
|
+
review?: RunReview;
|
|
65
|
+
abandonedReason?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RunReview {
|
|
69
|
+
status: SupervisorStatus;
|
|
70
|
+
verdict: string | null;
|
|
71
|
+
action: ActionLevel | null;
|
|
72
|
+
reviewerModel: string;
|
|
73
|
+
proposerModel: string;
|
|
74
|
+
sameModel: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RunSummary {
|
|
78
|
+
runId: string;
|
|
79
|
+
status: string;
|
|
80
|
+
dispatchedAt: number | null;
|
|
81
|
+
sessionId: string;
|
|
82
|
+
/** 子进程 pid:面板靠它判「还活着吗」(见 lib/stale.ts) */
|
|
83
|
+
pid: number | null;
|
|
84
|
+
/** 评审督导 pid:面板据此把阶段标成「评审中」 */
|
|
85
|
+
supervisorPid: number | null;
|
|
86
|
+
/** 本轮产出的提案摘要(M3):只用于展示,不应用 */
|
|
87
|
+
proposal?: ProposalSummary;
|
|
88
|
+
/** 本轮评审结论(M4):只用于展示,不应用 */
|
|
89
|
+
review?: RunReview;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeRunRecord(runDir: string, record: RunRecord, queue: MutationQueue): Promise<void> {
|
|
93
|
+
const file = path.join(runDir, "run.json");
|
|
94
|
+
await queue(file, async () => {
|
|
95
|
+
await fs.promises.mkdir(runDir, { recursive: true });
|
|
96
|
+
await fs.promises.writeFile(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 同步写 run.json:只给退出前补发用(主进程马上退出,不能等异步队列) */
|
|
101
|
+
export function writeRunRecordSync(runDir: string, record: RunRecord): void {
|
|
102
|
+
const file = path.join(runDir, "run.json");
|
|
103
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 同步打补丁:只改给定字段,其余原样保留(督导 pid 的写入 / 清除走这条)。
|
|
109
|
+
* 读不到 run.json 就返回 false,不凭空造记录。
|
|
110
|
+
*/
|
|
111
|
+
export function patchRunRecordSync(runDir: string, patch: Record<string, unknown>): boolean {
|
|
112
|
+
const file = path.join(runDir, "run.json");
|
|
113
|
+
try {
|
|
114
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
115
|
+
Object.assign(raw, patch);
|
|
116
|
+
writeRunRecordSync(runDir, raw as unknown as RunRecord);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 记容器的失败态:写不进去只记日志,绝不让「收尸」这一步反过来把主流程搞挂 */
|
|
124
|
+
export async function markRunAbandoned(
|
|
125
|
+
runDir: string,
|
|
126
|
+
reason: string,
|
|
127
|
+
queue: MutationQueue,
|
|
128
|
+
log: (msg: string) => void,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
const record = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8")) as RunRecord;
|
|
132
|
+
record.status = "abandoned";
|
|
133
|
+
record.abandonedReason = reason;
|
|
134
|
+
await writeRunRecord(runDir, record, queue);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
log(`[skill-evolve] 标记 abandoned 失败:${String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 采纳后回写状态:只改 status 与可选补丁字段,其余字段原样保留 */
|
|
141
|
+
export async function updateRunStatus(
|
|
142
|
+
evolutionDir: string,
|
|
143
|
+
runId: string,
|
|
144
|
+
status: RunStatus,
|
|
145
|
+
queue: MutationQueue,
|
|
146
|
+
patch: Record<string, unknown> = {},
|
|
147
|
+
): Promise<boolean> {
|
|
148
|
+
const runDir = path.join(evolutionDir, "runs", runId);
|
|
149
|
+
const file = path.join(runDir, "run.json");
|
|
150
|
+
let found = false;
|
|
151
|
+
try {
|
|
152
|
+
await queue(file, async () => {
|
|
153
|
+
let raw: Record<string, unknown>;
|
|
154
|
+
try {
|
|
155
|
+
raw = JSON.parse(await fs.promises.readFile(file, "utf8")) as Record<string, unknown>;
|
|
156
|
+
} catch {
|
|
157
|
+
return; // run.json 不在:不凭空造一条假记录
|
|
158
|
+
}
|
|
159
|
+
found = true;
|
|
160
|
+
raw.status = status;
|
|
161
|
+
Object.assign(raw, patch);
|
|
162
|
+
await fs.promises.writeFile(file, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 按派生时间倒序列出最近若干个 run(run.json 坏掉的目录直接跳过) */
|
|
171
|
+
export function listRuns(evolutionDir: string, limit = 10): RunSummary[] {
|
|
172
|
+
const dir = path.join(evolutionDir, "runs");
|
|
173
|
+
let names: string[];
|
|
174
|
+
try {
|
|
175
|
+
names = fs.readdirSync(dir);
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
const runs: RunSummary[] = [];
|
|
180
|
+
for (const name of names) {
|
|
181
|
+
const runDir = path.join(dir, name);
|
|
182
|
+
try {
|
|
183
|
+
const raw = JSON.parse(fs.readFileSync(path.join(runDir, "run.json"), "utf8")) as Partial<RunRecord>;
|
|
184
|
+
// 子进程可能只写了 02-proposal.json 而没回写 run.json,所以两处都要看
|
|
185
|
+
const proposal = raw.proposal ?? readProposalSummary(runDir);
|
|
186
|
+
// 同理:结论写盘了但宿主进程已退出、没人把状态推到 analyzed —— 读的时候补上。
|
|
187
|
+
// 不补的话,一个已经成功的复盘会被卡死判定误抦成「复盘失败」(面板最忌讳的事)。
|
|
188
|
+
const hasFindings = fs.existsSync(path.join(runDir, "01-findings.json"));
|
|
189
|
+
const status =
|
|
190
|
+
hasFindings && (raw.status === "dispatched" || raw.status === "stale")
|
|
191
|
+
? "analyzed"
|
|
192
|
+
: (raw.status ?? "unknown");
|
|
193
|
+
runs.push({
|
|
194
|
+
runId: raw.runId ?? name,
|
|
195
|
+
status,
|
|
196
|
+
dispatchedAt: typeof raw.dispatchedAt === "number" ? raw.dispatchedAt : null,
|
|
197
|
+
sessionId: raw.sessionId ?? "",
|
|
198
|
+
pid: typeof raw.pid === "number" ? raw.pid : null,
|
|
199
|
+
supervisorPid: typeof raw.supervisorPid === "number" ? raw.supervisorPid : null,
|
|
200
|
+
...(proposal ? { proposal } : {}),
|
|
201
|
+
...(raw.review ? { review: raw.review } : {}),
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return runs.sort((a, b) => (b.dispatchedAt ?? 0) - (a.dispatchedAt ?? 0)).slice(0, limit);
|
|
208
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 必红自检(设计稿 12.4):拿三个**已知该被拦住**的坏样本去考评审器。
|
|
3
|
+
*
|
|
4
|
+
* 这是整条流水线唯一的「评审器自己可信吗」证据:如果评审器把「可以用 find」判成 pass,
|
|
5
|
+
* 后面所有真提案的 pass 都没有意义,所以这里任何一条 expected 没命中就拿 `passed: false`
|
|
6
|
+
* 并在报告里打红字,派发也随之停摆。
|
|
7
|
+
*
|
|
8
|
+
* 注意:这里**故意不跑机械 P0 复验** —— 那几个坏样本里有两条本身就违反硬规则,
|
|
9
|
+
* 用规则引擎拒掉只能证明规则还活着,证明不了评审器会判断内容。
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { loadHardRules, type CompiledRules } from "./hard-rules.ts";
|
|
15
|
+
import { buildReviewPrompt } from "./review-prompt.ts";
|
|
16
|
+
import { reviewWithRetry, type ReviewSpawn } from "./review-run.ts";
|
|
17
|
+
|
|
18
|
+
export type SelftestExpectation = "pass" | "reject" | "revise";
|
|
19
|
+
|
|
20
|
+
export interface SelftestFixture {
|
|
21
|
+
name: string;
|
|
22
|
+
skill: string;
|
|
23
|
+
expected: SelftestExpectation;
|
|
24
|
+
why: string;
|
|
25
|
+
dir: string;
|
|
26
|
+
diff: string;
|
|
27
|
+
after: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SelftestCase {
|
|
31
|
+
name: string;
|
|
32
|
+
expected: SelftestExpectation;
|
|
33
|
+
actual: string;
|
|
34
|
+
ok: boolean;
|
|
35
|
+
detail?: string;
|
|
36
|
+
attempts: number;
|
|
37
|
+
ms: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SelftestReport {
|
|
41
|
+
at: number;
|
|
42
|
+
atIso: string;
|
|
43
|
+
passed: boolean;
|
|
44
|
+
results: SelftestCase[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const SELFTEST_FILE = "selftest.json";
|
|
48
|
+
/** 报告与命令里共用的红字提示 */
|
|
49
|
+
export const UNTRUSTED_NOTE = "评审器不可信,流水线停用:先修评审 prompt / 换更大的评审模型,再跑 /skill-evolve selftest";
|
|
50
|
+
|
|
51
|
+
const EXPECTATIONS: readonly SelftestExpectation[] = ["pass", "reject", "revise"];
|
|
52
|
+
|
|
53
|
+
export function selftestFile(evolutionDir: string): string {
|
|
54
|
+
return path.join(evolutionDir, SELFTEST_FILE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readText(file: string): string | undefined {
|
|
58
|
+
try {
|
|
59
|
+
return fs.readFileSync(file, "utf8");
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 读三个 fixture;缺文件 / 坏 JSON 的目录直接跳过(不猜、不补) */
|
|
66
|
+
export function loadFixtures(dir: string, log: (msg: string) => void = () => {}): SelftestFixture[] {
|
|
67
|
+
let entries: string[];
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(dir).sort();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
log(`[skill-evolve] 自检样本目录读不到:${String(error)}`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const fixtures: SelftestFixture[] = [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fixtureDir = path.join(dir, entry);
|
|
77
|
+
const meta = readText(path.join(fixtureDir, "fixture.json"));
|
|
78
|
+
if (!meta) continue;
|
|
79
|
+
let parsed: Record<string, unknown>;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(meta) as Record<string, unknown>;
|
|
82
|
+
} catch {
|
|
83
|
+
log(`[skill-evolve] 自检样本坏 JSON,跳过:${entry}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const name = typeof parsed.name === "string" ? parsed.name : entry;
|
|
87
|
+
const skill = typeof parsed.skill === "string" ? parsed.skill : name;
|
|
88
|
+
const expected = parsed.expected as SelftestExpectation | undefined;
|
|
89
|
+
if (!expected || !EXPECTATIONS.includes(expected)) {
|
|
90
|
+
log(`[skill-evolve] 自检样本 expected 非法,跳过:${entry}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const diff = readText(path.join(fixtureDir, "change.diff"));
|
|
94
|
+
const after = readText(path.join(fixtureDir, "after.md"));
|
|
95
|
+
if (diff === undefined || after === undefined) {
|
|
96
|
+
log(`[skill-evolve] 自检样本缺 change.diff / after.md,跳过:${entry}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
fixtures.push({
|
|
100
|
+
name,
|
|
101
|
+
skill,
|
|
102
|
+
expected,
|
|
103
|
+
why: typeof parsed.why === "string" ? parsed.why : "",
|
|
104
|
+
dir: fixtureDir,
|
|
105
|
+
diff,
|
|
106
|
+
after,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return fixtures;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function readSelftestReport(evolutionDir: string): SelftestReport | undefined {
|
|
113
|
+
const text = readText(selftestFile(evolutionDir));
|
|
114
|
+
if (!text) return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const raw = JSON.parse(text) as Record<string, unknown>;
|
|
117
|
+
return typeof raw.passed === "boolean" ? (raw as unknown as SelftestReport) : undefined;
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface SelftestOptions {
|
|
124
|
+
evolutionDir: string;
|
|
125
|
+
cwd: string;
|
|
126
|
+
fixturesDir?: string;
|
|
127
|
+
rules?: CompiledRules;
|
|
128
|
+
model?: string;
|
|
129
|
+
env?: NodeJS.ProcessEnv;
|
|
130
|
+
spawnFn?: ReviewSpawn;
|
|
131
|
+
now?: () => number;
|
|
132
|
+
sleep?: (ms: number) => Promise<void>;
|
|
133
|
+
timeoutMs?: number;
|
|
134
|
+
log?: (msg: string) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 依次跑完三个坏样本;返回报告并写 `selftest.json`(写失败只记日志) */
|
|
138
|
+
export async function runSelftest(options: SelftestOptions): Promise<SelftestReport> {
|
|
139
|
+
const log = options.log ?? (() => {});
|
|
140
|
+
const now = options.now ?? Date.now;
|
|
141
|
+
const fixturesDir = options.fixturesDir ?? fileURLToPath(new URL("../fixtures/selftest", import.meta.url));
|
|
142
|
+
const rules = options.rules ?? loadHardRules(path.join(options.evolutionDir, "rules", "hard-rules.json"));
|
|
143
|
+
const fixtures = loadFixtures(fixturesDir, log);
|
|
144
|
+
const results: SelftestCase[] = [];
|
|
145
|
+
for (const fixture of fixtures) {
|
|
146
|
+
const prompt = buildReviewPrompt({
|
|
147
|
+
skillName: fixture.skill,
|
|
148
|
+
skillText: fixture.after,
|
|
149
|
+
diffApplied: true,
|
|
150
|
+
diff: fixture.diff,
|
|
151
|
+
hardRules: rules.compiled,
|
|
152
|
+
});
|
|
153
|
+
const outcome = await reviewWithRetry({
|
|
154
|
+
prompt,
|
|
155
|
+
cwd: options.cwd,
|
|
156
|
+
...(options.model ? { model: options.model } : {}),
|
|
157
|
+
...(options.env ? { env: options.env } : {}),
|
|
158
|
+
...(options.spawnFn ? { spawnFn: options.spawnFn } : {}),
|
|
159
|
+
...(options.sleep ? { sleep: options.sleep } : {}),
|
|
160
|
+
...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
|
|
161
|
+
now,
|
|
162
|
+
log,
|
|
163
|
+
});
|
|
164
|
+
const actual = outcome.result.verdict ?? outcome.result.status;
|
|
165
|
+
const ok = outcome.result.status === "ok" && outcome.result.verdict === fixture.expected;
|
|
166
|
+
results.push({
|
|
167
|
+
name: fixture.name,
|
|
168
|
+
expected: fixture.expected,
|
|
169
|
+
actual,
|
|
170
|
+
ok,
|
|
171
|
+
...(outcome.result.detail ? { detail: outcome.result.detail } : {}),
|
|
172
|
+
attempts: outcome.attempts,
|
|
173
|
+
ms: outcome.ms,
|
|
174
|
+
});
|
|
175
|
+
log(
|
|
176
|
+
`[skill-evolve] 自检 ${fixture.name}:期望 ${fixture.expected},实际 ${actual}${ok ? "" : " ← 不合格"}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const at = now();
|
|
180
|
+
const report: SelftestReport = { at, atIso: new Date(at).toISOString(), passed: results.length > 0 && results.every((item) => item.ok), results };
|
|
181
|
+
try {
|
|
182
|
+
fs.mkdirSync(options.evolutionDir, { recursive: true });
|
|
183
|
+
fs.writeFileSync(selftestFile(options.evolutionDir), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
184
|
+
} catch (error) {
|
|
185
|
+
log(`[skill-evolve] 自检报告写失败:${String(error)}`);
|
|
186
|
+
}
|
|
187
|
+
log(`[skill-evolve] 自检${report.passed ? "通过" : "未通过"}:${results.filter((item) => item.ok).length}/${results.length}`);
|
|
188
|
+
return report;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 命令侧文案:任一不合格就把红字顶到第一行 */
|
|
192
|
+
export function renderSelftestReport(report: SelftestReport | undefined): string {
|
|
193
|
+
if (!report) return "自检:还没跑过(/skill-evolve selftest)";
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
if (!report.passed) lines.push(`【红】${UNTRUSTED_NOTE}`, "");
|
|
196
|
+
lines.push(`自检(${report.atIso}):${report.passed ? "通过" : "未通过"}`);
|
|
197
|
+
for (const item of report.results) {
|
|
198
|
+
lines.push(` ${item.ok ? "✓" : "✗"} ${item.name} 期望 ${item.expected} 实际 ${item.actual}${item.detail ? `(${item.detail})` : ""}`);
|
|
199
|
+
}
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话索引:把 `getSessionFile()` 的 JSONL 变成「行号 → 类型/角色/时间/摘要」。
|
|
3
|
+
*
|
|
4
|
+
* 铁律(设计稿 5.4):**只产索引,绝不把会话原文复制进 run 目录**。
|
|
5
|
+
* 平均会话近 1MB,正文进 prompt 会立刻撑爆上下文;复盘子进程拿索引抽查,按行号回读原文。
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { MutationQueue } from "./telemetry.ts";
|
|
10
|
+
|
|
11
|
+
export interface SessionIndexLine {
|
|
12
|
+
/** 1 起算的物理行号,复盘证据就写这个 */
|
|
13
|
+
line: number;
|
|
14
|
+
type: string;
|
|
15
|
+
role?: string;
|
|
16
|
+
ts?: string;
|
|
17
|
+
/** 该行原始字符数,用来判断这行值不值得读 */
|
|
18
|
+
chars: number;
|
|
19
|
+
/** 摘要,最多 80 字 */
|
|
20
|
+
excerpt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SessionIndex {
|
|
24
|
+
lines: SessionIndexLine[];
|
|
25
|
+
stats: { lines: number; bytes: number; hasCompaction: boolean };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const EXCERPT_MAX = 80;
|
|
29
|
+
|
|
30
|
+
function flatten(text: string): string {
|
|
31
|
+
return text.replace(/\s+/g, " ").trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 截到 80 字以内(超出时用省略号占最后一位) */
|
|
35
|
+
export function excerptOf(text: string): string {
|
|
36
|
+
const flat = flatten(text);
|
|
37
|
+
return flat.length > EXCERPT_MAX ? `${flat.slice(0, EXCERPT_MAX - 1)}…` : flat;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function textOfContent(content: unknown): string {
|
|
41
|
+
if (typeof content === "string") return content;
|
|
42
|
+
if (!Array.isArray(content)) return "";
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
for (const part of content) {
|
|
45
|
+
if (!part || typeof part !== "object") continue;
|
|
46
|
+
const piece = part as Record<string, unknown>;
|
|
47
|
+
if (piece.type === "text" && typeof piece.text === "string") parts.push(piece.text);
|
|
48
|
+
else if (piece.type === "toolCall") parts.push(`[调用 ${String(piece.name ?? "工具")}]`);
|
|
49
|
+
}
|
|
50
|
+
return parts.join(" ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 一条 entry 里值得进摘要的文字 */
|
|
54
|
+
function excerptOfEntry(entry: Record<string, unknown>, type: string): string {
|
|
55
|
+
if (type === "session") return String(entry.cwd ?? "");
|
|
56
|
+
if (type === "compaction" || type === "branch_summary") return String(entry.summary ?? "");
|
|
57
|
+
if (type === "custom" || type === "custom_message") return String(entry.customType ?? "");
|
|
58
|
+
const message = entry.message;
|
|
59
|
+
if (!message || typeof message !== "object") return "";
|
|
60
|
+
const fields = message as Record<string, unknown>;
|
|
61
|
+
const text = textOfContent(fields.content);
|
|
62
|
+
const tool = typeof fields.toolName === "string" ? fields.toolName : "";
|
|
63
|
+
return tool ? `[${tool}] ${text}` : text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildSessionIndex(text: string): SessionIndex {
|
|
67
|
+
const rawLines = text.split("\n");
|
|
68
|
+
const lines: SessionIndexLine[] = [];
|
|
69
|
+
let total = 0;
|
|
70
|
+
let hasCompaction = false;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < rawLines.length; i += 1) {
|
|
73
|
+
const raw = rawLines[i];
|
|
74
|
+
if (raw.trim().length === 0) continue;
|
|
75
|
+
total += 1;
|
|
76
|
+
let entry: Record<string, unknown>;
|
|
77
|
+
try {
|
|
78
|
+
const parsed: unknown = JSON.parse(raw);
|
|
79
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
80
|
+
entry = parsed as Record<string, unknown>;
|
|
81
|
+
} catch {
|
|
82
|
+
// 坏行只计数不进索引:截断的会话尾巴很常见,不能因此整份索引失败
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const type = typeof entry.type === "string" ? entry.type : "unknown";
|
|
86
|
+
if (type === "compaction") hasCompaction = true;
|
|
87
|
+
const message = entry.message && typeof entry.message === "object" ? (entry.message as Record<string, unknown>) : null;
|
|
88
|
+
const role = message && typeof message.role === "string" ? message.role : undefined;
|
|
89
|
+
const ts = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
|
|
90
|
+
const line: SessionIndexLine = {
|
|
91
|
+
line: i + 1,
|
|
92
|
+
type,
|
|
93
|
+
chars: raw.length,
|
|
94
|
+
// 摘要一律过一遍 excerptOf:这是「≤80 字」的唯一出口,绝不让某类 entry 绕过它
|
|
95
|
+
excerpt: excerptOf(excerptOfEntry(entry, type)),
|
|
96
|
+
};
|
|
97
|
+
if (role) line.role = role;
|
|
98
|
+
if (ts) line.ts = ts;
|
|
99
|
+
lines.push(line);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { lines, stats: { lines: total, bytes: Buffer.byteLength(text, "utf8"), hasCompaction } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 同步版索引生成:只给退出前补发用(主进程马上退出,不能 await) */
|
|
106
|
+
export function writeSessionIndexSync(options: {
|
|
107
|
+
sessionFile: string;
|
|
108
|
+
outDir: string;
|
|
109
|
+
log?: (msg: string) => void;
|
|
110
|
+
}): SessionIndex | null {
|
|
111
|
+
const log = options.log ?? (() => {});
|
|
112
|
+
try {
|
|
113
|
+
const text = fs.readFileSync(options.sessionFile, "utf8");
|
|
114
|
+
const index = buildSessionIndex(text);
|
|
115
|
+
fs.mkdirSync(options.outDir, { recursive: true });
|
|
116
|
+
fs.writeFileSync(path.join(options.outDir, "index.json"), `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
117
|
+
return index;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
log(`[skill-evolve] 会话索引生成失败(${options.sessionFile}):${String(error)}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 写 `runs/<runId>/00-session/index.json`;会话不可读时返回 null,由调用方降级为 telemetry-only */
|
|
125
|
+
export async function writeSessionIndex(options: {
|
|
126
|
+
sessionFile: string;
|
|
127
|
+
outDir: string;
|
|
128
|
+
queue: MutationQueue;
|
|
129
|
+
log?: (msg: string) => void;
|
|
130
|
+
}): Promise<SessionIndex | null> {
|
|
131
|
+
const log = options.log ?? (() => {});
|
|
132
|
+
try {
|
|
133
|
+
const text = await fs.promises.readFile(options.sessionFile, "utf8");
|
|
134
|
+
const index = buildSessionIndex(text);
|
|
135
|
+
const outFile = path.join(options.outDir, "index.json");
|
|
136
|
+
await options.queue(outFile, async () => {
|
|
137
|
+
await fs.promises.mkdir(options.outDir, { recursive: true });
|
|
138
|
+
await fs.promises.writeFile(outFile, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
139
|
+
});
|
|
140
|
+
return index;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
log(`[skill-evolve] 会话索引生成失败(${options.sessionFile}):${String(error)}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|