pi-skill-evolve 0.1.2 → 0.1.4
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/extensions/skill-evolve/index.ts +2 -2
- package/extensions/skill-evolve/lib/command.ts +7 -5
- package/extensions/skill-evolve/lib/config.ts +45 -14
- package/extensions/skill-evolve/lib/dispatch.ts +26 -33
- package/extensions/skill-evolve/lib/drain-pending.ts +65 -0
- package/extensions/skill-evolve/lib/evolution-apply.ts +30 -1
- package/extensions/skill-evolve/lib/evolution.ts +7 -5
- package/extensions/skill-evolve/lib/flags.ts +32 -18
- package/extensions/skill-evolve/lib/queue.ts +2 -0
- package/extensions/skill-evolve/lib/replay.ts +2 -0
- package/extensions/skill-evolve/lib/report.ts +4 -5
- package/extensions/skill-evolve/lib/review-spawn.ts +11 -0
- package/extensions/skill-evolve/lib/shutdown-dispatch.ts +4 -0
- package/extensions/skill-evolve/lib/web/actions.ts +3 -3
- package/extensions/skill-evolve/lib/web/activity.ts +19 -2
- package/extensions/skill-evolve/lib/web/render-controls.ts +21 -9
- package/extensions/skill-evolve/test/approve.test.ts +63 -0
- package/extensions/skill-evolve/test/command.test.ts +33 -10
- package/extensions/skill-evolve/test/config.test.ts +75 -23
- package/extensions/skill-evolve/test/dispatch-busy.test.ts +3 -0
- package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +28 -3
- package/extensions/skill-evolve/test/evolution.test.ts +57 -14
- package/extensions/skill-evolve/test/reject.test.ts +2 -0
- package/extensions/skill-evolve/test/review-spawn.test.ts +25 -2
- package/extensions/skill-evolve/test/stale.test.ts +1 -0
- package/extensions/skill-evolve/test/web-actions.test.ts +42 -3
- package/extensions/skill-evolve/test/web-data.test.ts +8 -3
- package/extensions/skill-evolve/test/web-render.test.ts +31 -8
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ test("督导用 node + --experimental-strip-types 起,参数里带 runDir", ()
|
|
|
17
17
|
runDir: "/repo/.pi/evolution/runs/run-1",
|
|
18
18
|
cwd: "/repo",
|
|
19
19
|
env: {},
|
|
20
|
+
autoReview: true,
|
|
20
21
|
spawnFn: (command, args, options) => {
|
|
21
22
|
seen.calls.push({ command, args, options });
|
|
22
23
|
return {
|
|
@@ -44,8 +45,8 @@ test("inherit 不追加 --model,显式模型追加在 runDir 之后", () => {
|
|
|
44
45
|
explicitArgs.push(args);
|
|
45
46
|
return { pid: 1 };
|
|
46
47
|
};
|
|
47
|
-
spawnReviewSupervisor({ runDir: "/r", cwd: "/repo", env: {}, reviewModel: "inherit", spawnFn });
|
|
48
|
-
spawnReviewSupervisor({ runDir: "/r", cwd: "/repo", env: {}, reviewModel: "deepseek/deepseek-v3.2", spawnFn });
|
|
48
|
+
spawnReviewSupervisor({ runDir: "/r", cwd: "/repo", env: {}, autoReview: true, reviewModel: "inherit", spawnFn });
|
|
49
|
+
spawnReviewSupervisor({ runDir: "/r", cwd: "/repo", env: {}, autoReview: true, reviewModel: "deepseek/deepseek-v3.2", spawnFn });
|
|
49
50
|
assert.deepEqual(inheritArgs[0], ["--experimental-strip-types", SUPERVISOR_SCRIPT, "/r"]);
|
|
50
51
|
assert.deepEqual(explicitArgs[1], ["--experimental-strip-types", SUPERVISOR_SCRIPT, "/r", "deepseek/deepseek-v3.2"]);
|
|
51
52
|
});
|
|
@@ -56,6 +57,7 @@ test("env 里去掉 SKILL_EVOLVE_CHILD,其他变量照旧带走", () => {
|
|
|
56
57
|
runDir: "/r",
|
|
57
58
|
cwd: "/repo",
|
|
58
59
|
env: { SKILL_EVOLVE_CHILD: "1", PATH: "/usr/bin" },
|
|
60
|
+
autoReview: true,
|
|
59
61
|
spawnFn: (_command, _args, options) => {
|
|
60
62
|
env = options.env as NodeJS.ProcessEnv;
|
|
61
63
|
return { pid: 1 };
|
|
@@ -65,12 +67,33 @@ test("env 里去掉 SKILL_EVOLVE_CHILD,其他变量照旧带走", () => {
|
|
|
65
67
|
assert.equal(env.PATH, "/usr/bin");
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
test("自动评审关着:一个进程都不起,只留一句「停在 proposed」日志", () => {
|
|
71
|
+
const logs: string[] = [];
|
|
72
|
+
let spawned = 0;
|
|
73
|
+
const pid = spawnReviewSupervisor({
|
|
74
|
+
runDir: "/repo/.pi/evolution/runs/run-7",
|
|
75
|
+
cwd: "/repo",
|
|
76
|
+
env: {},
|
|
77
|
+
autoReview: false,
|
|
78
|
+
log: (msg) => logs.push(msg),
|
|
79
|
+
spawnFn: () => {
|
|
80
|
+
spawned += 1;
|
|
81
|
+
return { pid: 1 };
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
assert.equal(pid, null);
|
|
85
|
+
assert.equal(spawned, 0, "评审关着就不许起子进程(每份子进程都是 token)");
|
|
86
|
+
assert.equal(logs.length, 1);
|
|
87
|
+
assert.match(logs[0]!, /自动评审已关,run run-7 停在 proposed 待人工 \/skill-evolve approve/);
|
|
88
|
+
});
|
|
89
|
+
|
|
68
90
|
test("起不来只记日志返回 null,不抛", () => {
|
|
69
91
|
const logs: string[] = [];
|
|
70
92
|
const pid = spawnReviewSupervisor({
|
|
71
93
|
runDir: "/r",
|
|
72
94
|
cwd: "/repo",
|
|
73
95
|
env: {},
|
|
96
|
+
autoReview: true,
|
|
74
97
|
log: (msg) => logs.push(msg),
|
|
75
98
|
spawnFn: () => {
|
|
76
99
|
throw new Error("EMFILE");
|
|
@@ -13,13 +13,26 @@ import { moveRunToTrash } from "../lib/web/trash.ts";
|
|
|
13
13
|
|
|
14
14
|
const FORM = "application/x-www-form-urlencoded";
|
|
15
15
|
|
|
16
|
-
function workspace(
|
|
16
|
+
function workspace(
|
|
17
|
+
options: {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
telemetry?: boolean;
|
|
20
|
+
autoReview?: boolean;
|
|
21
|
+
autoApply?: boolean;
|
|
22
|
+
runs?: { runId: string; status: string }[];
|
|
23
|
+
} = {},
|
|
24
|
+
): string {
|
|
17
25
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-webaction-"));
|
|
18
26
|
const evolutionDir = path.join(cwd, ".pi", "evolution");
|
|
19
27
|
fs.mkdirSync(path.join(evolutionDir, "runs"), { recursive: true });
|
|
20
28
|
fs.writeFileSync(
|
|
21
29
|
path.join(evolutionDir, "config.json"),
|
|
22
|
-
JSON.stringify({
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
enabled: options.enabled === true,
|
|
32
|
+
telemetry: options.telemetry === true,
|
|
33
|
+
autoReview: options.autoReview === true,
|
|
34
|
+
autoApply: options.autoApply === true,
|
|
35
|
+
}),
|
|
23
36
|
"utf8",
|
|
24
37
|
);
|
|
25
38
|
for (const run of options.runs ?? []) {
|
|
@@ -103,6 +116,32 @@ test("POST /toggle?field=telemetry:只动采集,不动复盘", async () => {
|
|
|
103
116
|
}
|
|
104
117
|
});
|
|
105
118
|
|
|
119
|
+
test("POST /toggle?field=autoReview:只动评审,复盘 / 采集 / 采纳一个都不动", async () => {
|
|
120
|
+
const evolutionDir = workspace({ enabled: true, telemetry: true, autoReview: true });
|
|
121
|
+
const toggle = spyToggle("自动评审已关闭");
|
|
122
|
+
const panel = await open(evolutionDir, { onToggle: toggle.fn });
|
|
123
|
+
try {
|
|
124
|
+
assert.equal((await post(panel, "/toggle?field=autoReview", { csrf: panel.csrf })).status, 303);
|
|
125
|
+
assert.deepEqual(toggle.calls, [["autoReview", false]], "当前评审是开的 → 传 false,且 field 只能是 autoReview");
|
|
126
|
+
assert.equal(readAudit(evolutionDir).at(-1)?.field, "autoReview");
|
|
127
|
+
} finally {
|
|
128
|
+
await panel.close();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("POST /toggle?field=autoApply:关着的采纳能被单独打开,且不动其它三个", async () => {
|
|
133
|
+
const evolutionDir = workspace({ enabled: true, telemetry: true, autoReview: true, autoApply: false });
|
|
134
|
+
const toggle = spyToggle("自动采纳已开启");
|
|
135
|
+
const panel = await open(evolutionDir, { onToggle: toggle.fn });
|
|
136
|
+
try {
|
|
137
|
+
assert.equal((await post(panel, "/toggle?field=autoApply", { csrf: panel.csrf })).status, 303);
|
|
138
|
+
assert.deepEqual(toggle.calls, [["autoApply", true]]);
|
|
139
|
+
assert.equal(readAudit(evolutionDir).at(-1)?.field, "autoApply");
|
|
140
|
+
} finally {
|
|
141
|
+
await panel.close();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
106
145
|
test("POST /toggle 的 field 越界 / 缺失:400,不调回调,写一条带原值的 error 审计", async () => {
|
|
107
146
|
const evolutionDir = workspace({ enabled: true, telemetry: true });
|
|
108
147
|
const toggle = spyToggle("不该被调用");
|
|
@@ -110,7 +149,7 @@ test("POST /toggle 的 field 越界 / 缺失:400,不调回调,写一条带
|
|
|
110
149
|
try {
|
|
111
150
|
const wrong = await post(panel, "/toggle?field=propose", { csrf: panel.csrf });
|
|
112
151
|
assert.equal(wrong.status, 400);
|
|
113
|
-
assert.match(await wrong.text(), /field 只认 telemetry \/ enabled/);
|
|
152
|
+
assert.match(await wrong.text(), /field 只认 telemetry \/ enabled \/ autoReview \/ autoApply/);
|
|
114
153
|
const missing = await post(panel, "/toggle", { csrf: panel.csrf });
|
|
115
154
|
assert.equal(missing.status, 400, "缺 field 不能猜一个默认值");
|
|
116
155
|
assert.deepEqual(toggle.calls, [], "越界不放行到回调");
|
|
@@ -411,10 +411,15 @@ test("approvals:缺 proposal / review 文件时给 null,不抛也不编造
|
|
|
411
411
|
assert.equal(row.createdAt, NOW);
|
|
412
412
|
});
|
|
413
413
|
|
|
414
|
-
test("collectControls
|
|
414
|
+
test("collectControls:四个开关各读各的字段(采集关、复盘开这个中间态要能显示出来)", () => {
|
|
415
415
|
const evolutionDir = workspace();
|
|
416
|
-
fs.writeFileSync(
|
|
416
|
+
fs.writeFileSync(
|
|
417
|
+
path.join(evolutionDir, "config.json"),
|
|
418
|
+
JSON.stringify({ enabled: true, telemetry: false, autoReview: false, autoApply: true }),
|
|
419
|
+
"utf8",
|
|
420
|
+
);
|
|
417
421
|
const controls = collectControls(evolutionDir);
|
|
418
|
-
assert.deepEqual([controls.telemetry, controls.enabled], [false, true]);
|
|
422
|
+
assert.deepEqual([controls.telemetry, controls.enabled, controls.autoReview, controls.autoApply], [false, true, false, true]);
|
|
423
|
+
assert.equal(controls.pendingAuto, 0, "队列空时没有待人工的 auto 条");
|
|
419
424
|
assert.equal(controls.lastActivityAt, null, "没有任何遥测 / run 时最后活动为 null,不编造时间");
|
|
420
425
|
});
|
|
@@ -262,37 +262,60 @@ test("待审批页不做 5 秒自刷新:刷新会打断正在点的按钮", ()
|
|
|
262
262
|
assert.equal(renderPage(emptyState(), "approvals").includes('http-equiv="refresh"'), false);
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
-
test("
|
|
265
|
+
test("控制条:四个独立开关,各自 post 到 /toggle?field=", () => {
|
|
266
266
|
const html = renderPage(emptyState(), "pending", {
|
|
267
|
-
controls: { telemetry: true, enabled: false, lastActivityAt: null },
|
|
267
|
+
controls: { telemetry: true, enabled: false, autoReview: false, autoApply: false, pendingAuto: 0, lastActivityAt: null },
|
|
268
268
|
csrf: "tok",
|
|
269
269
|
canWrite: true,
|
|
270
270
|
});
|
|
271
271
|
assert.match(html, /采集遥测:开/);
|
|
272
272
|
assert.match(html, /自动复盘:关/);
|
|
273
|
-
|
|
274
|
-
assert.
|
|
275
|
-
|
|
273
|
+
assert.match(html, /自动评审:关/);
|
|
274
|
+
assert.match(html, /自动采纳:关/);
|
|
275
|
+
// 四个按钮各自打自己的字段:一个按钮同时翻两个,就回到了「捆在一起」的老问题
|
|
276
|
+
for (const field of ["telemetry", "enabled", "autoReview", "autoApply"]) {
|
|
277
|
+
assert.equal(html.includes(`action="/toggle?field=${field}"`), true, field);
|
|
278
|
+
}
|
|
276
279
|
assert.match(html, /只写本地日志,不花 token/);
|
|
277
280
|
assert.match(html, /会派生子进程,花 token/);
|
|
278
281
|
assert.match(html, /关闭采集遥测/, "按钮文案是动作(当前开 → 关闭),不是状态的复读");
|
|
279
282
|
assert.match(html, /开启自动复盘/);
|
|
283
|
+
assert.match(html, /再起一个督导子进程,再花一份 token/);
|
|
284
|
+
assert.match(html, /改 \.pi\/skills\/ 与 \.cursor\/ 文件,可回滚/);
|
|
280
285
|
assert.equal(html.includes("复盘在跑但采集关着"), false, "两个状态正常时不带警告");
|
|
281
286
|
// 只读(没 csrf)时两个开关只显示状态,不渲染表单
|
|
282
|
-
const readonly = renderPage(emptyState(), "pending", {
|
|
287
|
+
const readonly = renderPage(emptyState(), "pending", {
|
|
288
|
+
controls: { telemetry: false, enabled: false, autoReview: true, autoApply: true, pendingAuto: 2, lastActivityAt: null },
|
|
289
|
+
});
|
|
283
290
|
assert.match(readonly, /采集遥测:关/);
|
|
284
291
|
assert.equal(readonly.includes("/toggle?field="), false);
|
|
285
292
|
});
|
|
286
293
|
|
|
294
|
+
test("控制条:自动采纳关着且队列有 auto 条时写「N 条待人工」,关了预警就消失", () => {
|
|
295
|
+
const backlog = renderPage(emptyState(), "pending", {
|
|
296
|
+
controls: { telemetry: true, enabled: true, autoReview: true, autoApply: false, pendingAuto: 3, lastActivityAt: null },
|
|
297
|
+
csrf: "tok",
|
|
298
|
+
canWrite: true,
|
|
299
|
+
});
|
|
300
|
+
assert.match(backlog, /⏸ 3 条 auto 提案待人工(自动采纳关着)/);
|
|
301
|
+
// 采纳打开后那些条会被自动消化,提醒必须跟着消失(否则永远挂着一条假待办)
|
|
302
|
+
const cleared = renderPage(emptyState(), "pending", {
|
|
303
|
+
controls: { telemetry: true, enabled: true, autoReview: true, autoApply: true, pendingAuto: 3, lastActivityAt: null },
|
|
304
|
+
csrf: "tok",
|
|
305
|
+
canWrite: true,
|
|
306
|
+
});
|
|
307
|
+
assert.equal(cleared.includes("条 auto 提案待人工"), false);
|
|
308
|
+
});
|
|
309
|
+
|
|
287
310
|
test("控制条:只开复盘不采集时给一行警告,两个都开后警告消失", () => {
|
|
288
311
|
const warn = renderPage(emptyState(), "pending", {
|
|
289
|
-
controls: { telemetry: false, enabled: true, lastActivityAt: null },
|
|
312
|
+
controls: { telemetry: false, enabled: true, autoReview: true, autoApply: false, pendingAuto: 0, lastActivityAt: null },
|
|
290
313
|
csrf: "tok",
|
|
291
314
|
canWrite: true,
|
|
292
315
|
});
|
|
293
316
|
assert.match(warn, /<p class="warn">⚠ 复盘在跑但采集关着:归因只能靠原始会话,加载率不可得<\/p>/);
|
|
294
317
|
const ok = renderPage(emptyState(), "pending", {
|
|
295
|
-
controls: { telemetry: true, enabled: true, lastActivityAt: null },
|
|
318
|
+
controls: { telemetry: true, enabled: true, autoReview: true, autoApply: false, pendingAuto: 0, lastActivityAt: null },
|
|
296
319
|
csrf: "tok",
|
|
297
320
|
canWrite: true,
|
|
298
321
|
});
|