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, reviewed: 3 };
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: "reviewed",
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
- /** 待处理:四种 kind 各一个徽章;「待入队」是灰的(它在排队,不需要人动手) */
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-evolve",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "技能进化:采集会话中的技能曝光/加载,异步复盘、生成改进提案并独立评审,附本机 Web 管理面板",
5
5
  "keywords": [
6
6
  "pi-package",