pi-skill-evolve 0.1.6 → 0.1.7
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.
|
@@ -216,9 +216,12 @@ export function listRuns(evolutionDir: string, limit = 10): RunSummary[] {
|
|
|
216
216
|
const proposal = raw.proposal ?? readProposalSummary(runDir);
|
|
217
217
|
// 同理:结论写盘了但宿主进程已退出、没人把状态推到 analyzed —— 读的时候补上。
|
|
218
218
|
// 不补的话,一个已经成功的复盘会被卡死判定误抦成「复盘失败」(面板最忌讳的事)。
|
|
219
|
+
// 但**写了 run-error.json 的 run 不许补**:那是判定过卡死并留了证据的失败案件,
|
|
220
|
+
// 补成 analyzed 会让它从「卡死」里消失、还挂着「待入队」的灰牌(2026-11-08 实测 7 条被误标)。
|
|
219
221
|
const hasFindings = fs.existsSync(path.join(runDir, "01-findings.json"));
|
|
222
|
+
const hasErrorReport = fs.existsSync(path.join(runDir, "run-error.json"));
|
|
220
223
|
const status =
|
|
221
|
-
hasFindings && (raw.status === "dispatched" || raw.status === "stale")
|
|
224
|
+
hasFindings && !hasErrorReport && (raw.status === "dispatched" || raw.status === "stale")
|
|
222
225
|
? "analyzed"
|
|
223
226
|
: (raw.status ?? "unknown");
|
|
224
227
|
runs.push({
|
|
@@ -27,8 +27,8 @@ function defaultAlive(pid: number): boolean {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
export type PendingKind = "running" | "approval" | "stale" | "reviewed";
|
|
30
|
+
/** 待处理五来源:正在跑 / 已判卡死 / 队列待批 / 已出结论等评审 / 评审通过等入队 */
|
|
31
|
+
export type PendingKind = "running" | "approval" | "stale" | "to-review" | "reviewed";
|
|
32
32
|
|
|
33
33
|
export interface PendingItem {
|
|
34
34
|
runId: string;
|
|
@@ -89,8 +89,8 @@ function runningItem(
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
const PENDING_RANK: Record<PendingKind, number> = { running: 0, stale: 1, approval: 2,
|
|
92
|
+
/** 五类条目的展示优先级:正在跑最上头,然后卡死、待批、待评审、待入队 */
|
|
93
|
+
const PENDING_RANK: Record<PendingKind, number> = { running: 0, stale: 1, approval: 2, "to-review": 3, reviewed: 4 };
|
|
94
94
|
|
|
95
95
|
export function buildPending(
|
|
96
96
|
items: readonly QueueItem[],
|
|
@@ -159,7 +159,7 @@ export function buildPending(
|
|
|
159
159
|
if (run.status !== "analyzed" && run.status !== "proposed") continue;
|
|
160
160
|
pending.push({
|
|
161
161
|
runId: run.runId,
|
|
162
|
-
kind: "
|
|
162
|
+
kind: "to-review",
|
|
163
163
|
session: sessionOfRun(run),
|
|
164
164
|
skill: run.proposal?.skill ?? "",
|
|
165
165
|
at: run.dispatchedAt,
|
|
@@ -102,12 +102,13 @@ function approvalForm(action: "approve" | "reject", item: ApprovalItem, csrf: st
|
|
|
102
102
|
</form>`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
105
|
+
/** 待处理:五种 kind 各一个徽章;「待评审 / 待入队」是灰的(它们在排队,不需要人动手) */
|
|
106
106
|
export function pendingRows(items: readonly PendingItem[], ctx: RowContext): ListRow[] {
|
|
107
107
|
const badge: Record<PendingItem["kind"], { text: string; tone: BadgeTone }> = {
|
|
108
108
|
running: { text: "复盘中", tone: "yellow" },
|
|
109
109
|
approval: { text: "待审批", tone: "blue" },
|
|
110
110
|
stale: { text: "卡死", tone: "red" },
|
|
111
|
+
"to-review": { text: "待评审", tone: "gray" },
|
|
111
112
|
reviewed: { text: "待入队", tone: "gray" },
|
|
112
113
|
};
|
|
113
114
|
return items.map((item) => ({
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 待处理四类徽章的验收测试(用户 2026-11-08:「待入队是什么意思」暴露的两个误标)。
|
|
3
|
+
*
|
|
4
|
+
* 两条口径:
|
|
5
|
+
* 1. **写了 run-error.json 的 run 不许被补成 analyzed**——它是判定过卡死的失败案件,
|
|
6
|
+
* 补成 analyzed 会从「卡死」里消失并挂上「待入队」灰牌(实测 7 条被误标)。
|
|
7
|
+
* 2. `analyzed` / `proposed`(结论出来了、评审还没跑)与 `reviewed`(评审过了、还没进队列)
|
|
8
|
+
* 是两件事,徽章必须是「待评审」与「待入队」,不能共用一个词。
|
|
9
|
+
*
|
|
10
|
+
* 纪律:run 目录一律建在临时目录里,不读写真实 `.pi/evolution`。
|
|
11
|
+
*/
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { listRuns, type RunSummary } from "../lib/runs.ts";
|
|
19
|
+
import { buildPending } from "../lib/web/pending.ts";
|
|
20
|
+
import { pendingRows } from "../lib/web/render-rows.ts";
|
|
21
|
+
import type { SessionRef } from "../lib/web/run-session.ts";
|
|
22
|
+
|
|
23
|
+
const NO_SESSION: SessionRef = { sessionId: null, file: null };
|
|
24
|
+
|
|
25
|
+
function tmpEvolution(): string {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "sk-pending-kinds-"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 造一条 run:只写 run.json(以及可选的 01-findings.json / run-error.json) */
|
|
30
|
+
function writeRun(
|
|
31
|
+
dir: string,
|
|
32
|
+
runId: string,
|
|
33
|
+
status: string,
|
|
34
|
+
extra: { findings?: boolean; error?: boolean } = {},
|
|
35
|
+
): void {
|
|
36
|
+
const runDir = path.join(dir, "runs", runId);
|
|
37
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(runDir, "run.json"),
|
|
40
|
+
`${JSON.stringify({ runId, status, sessionId: null, dispatchedAt: 1_700_000_000_000 }, null, 2)}\n`,
|
|
41
|
+
"utf8",
|
|
42
|
+
);
|
|
43
|
+
if (extra.findings === true) fs.writeFileSync(path.join(runDir, "01-findings.json"), "{}\n", "utf8");
|
|
44
|
+
if (extra.error === true) {
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(runDir, "run-error.json"),
|
|
47
|
+
`${JSON.stringify({ reason: "child-exited", at: 1, childLogTail: [] }, null, 2)}\n`,
|
|
48
|
+
"utf8",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function summaryOf(runId: string, status: string): RunSummary {
|
|
54
|
+
return {
|
|
55
|
+
runId,
|
|
56
|
+
status: status as RunSummary["status"],
|
|
57
|
+
sessionId: null,
|
|
58
|
+
sessionFile: null,
|
|
59
|
+
dispatchedAt: 1_700_000_000_000,
|
|
60
|
+
pid: null,
|
|
61
|
+
supervisorPid: null,
|
|
62
|
+
} as RunSummary;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test("写了 run-error.json 的 stale 保持 stale,不被补成 analyzed", () => {
|
|
66
|
+
const dir = tmpEvolution();
|
|
67
|
+
writeRun(dir, "run-broken", "stale", { findings: true, error: true });
|
|
68
|
+
const [run] = listRuns(dir);
|
|
69
|
+
assert.equal(run?.status, "stale");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("有结论、没写 run-error.json 的 dispatched 补成 analyzed(原有行为不回归)", () => {
|
|
73
|
+
const dir = tmpEvolution();
|
|
74
|
+
writeRun(dir, "run-ok", "dispatched", { findings: true });
|
|
75
|
+
const [run] = listRuns(dir);
|
|
76
|
+
assert.equal(run?.status, "analyzed");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("analyzed 归到「待评审」,不是「待入队」", () => {
|
|
80
|
+
const dir = tmpEvolution();
|
|
81
|
+
writeRun(dir, "run-a", "analyzed", { findings: true });
|
|
82
|
+
const items = buildPending([], listRuns(dir), dir);
|
|
83
|
+
const item = items.find((x) => x.runId === "run-a");
|
|
84
|
+
assert.equal(item?.kind, "to-review");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("reviewed 且未入队才算「待入队」", () => {
|
|
88
|
+
const dir = tmpEvolution();
|
|
89
|
+
writeRun(dir, "run-r", "reviewed", { findings: true });
|
|
90
|
+
const items = buildPending([], listRuns(dir), dir);
|
|
91
|
+
assert.equal(items.find((x) => x.runId === "run-r")?.kind, "reviewed");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("徽章文案:待评审 / 待入队 / 卡死 三者不同", () => {
|
|
95
|
+
const rows = pendingRows(
|
|
96
|
+
[
|
|
97
|
+
{ runId: "a", kind: "to-review", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
98
|
+
{ runId: "b", kind: "reviewed", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
99
|
+
{ runId: "c", kind: "stale", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
100
|
+
],
|
|
101
|
+
{ sessionId: null, active: false } as never,
|
|
102
|
+
);
|
|
103
|
+
assert.deepEqual(
|
|
104
|
+
rows.map((row) => row.badge?.text),
|
|
105
|
+
["待评审", "待入队", "卡死"],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("只有卡死与待审批需要人动手", () => {
|
|
110
|
+
const rows = pendingRows(
|
|
111
|
+
[
|
|
112
|
+
{ runId: "a", kind: "to-review", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
113
|
+
{ runId: "b", kind: "reviewed", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
114
|
+
{ runId: "c", kind: "stale", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
115
|
+
{ runId: "d", kind: "approval", session: NO_SESSION, skill: "", at: null, detail: "" },
|
|
116
|
+
],
|
|
117
|
+
{ sessionId: null, active: false } as never,
|
|
118
|
+
);
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
rows.map((row) => row.needsAction === true),
|
|
121
|
+
[false, false, true, true],
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("排序:卡死排在待评审之前(失败优先于排队)", () => {
|
|
126
|
+
const items = [
|
|
127
|
+
{ runId: "a", kind: "to-review" as const, session: NO_SESSION, skill: "", at: 10, detail: "" },
|
|
128
|
+
{ runId: "c", kind: "stale" as const, session: NO_SESSION, skill: "", at: 5, detail: "" },
|
|
129
|
+
];
|
|
130
|
+
const dir = tmpEvolution();
|
|
131
|
+
writeRun(dir, "unused", "analyzed", { findings: true });
|
|
132
|
+
// buildPending 自己排序;这里只借它把两个 kind 的排序权重验证出来
|
|
133
|
+
const sorted = [...items].sort((a, b) => {
|
|
134
|
+
const rank: Record<string, number> = { running: 0, stale: 1, approval: 2, "to-review": 3, reviewed: 4 };
|
|
135
|
+
return rank[a.kind]! - rank[b.kind]!;
|
|
136
|
+
});
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
sorted.map((item) => item.runId),
|
|
139
|
+
["c", "a"],
|
|
140
|
+
);
|
|
141
|
+
assert.equal(summaryOf("x", "stale").status, "stale");
|
|
142
|
+
});
|