pi-skill-evolve 0.0.0-stage → 0.1.0
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 +116 -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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/skill-evolve report --week`:一周一句结论的周报(设计稿 14.2 + 10.4)。
|
|
3
|
+
*
|
|
4
|
+
* 术语纪律:能观测的只有「技能文件是否被读取」,所以这份报告里只有**加载率**,
|
|
5
|
+
* 任何地方都不许写成「使用率」(读 ≠ 用,读了没照做也测不到)。
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { listSkillDirs } from "./hard-rules.ts";
|
|
10
|
+
import { createReferenceScan } from "./proc.ts";
|
|
11
|
+
import { readFrontmatter } from "./proposal.ts";
|
|
12
|
+
import { readQueue, type QueueItem } from "./queue.ts";
|
|
13
|
+
import { listRuns } from "./runs.ts";
|
|
14
|
+
import { ratio, readEvents, type TelemetryEvent, type TelemetrySkill } from "./telemetry.ts";
|
|
15
|
+
import { isoWeek } from "./metrics.ts";
|
|
16
|
+
|
|
17
|
+
/** 零加载候选的判据:近 4 周每周都有曝光、且一次都没被读过 */
|
|
18
|
+
export const CANDIDATE_WEEKS = 4;
|
|
19
|
+
|
|
20
|
+
export interface SkillRef {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 引用面清单:由 proc.ts 的 rg / 文件探测提供,单测注入假实现 */
|
|
26
|
+
export interface ReferenceScan {
|
|
27
|
+
cursorStub: boolean;
|
|
28
|
+
agentsIndex: boolean;
|
|
29
|
+
/** 其它技能 / 文档里提到该技能名的处数 */
|
|
30
|
+
mentions: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AppliedRun {
|
|
34
|
+
skill: string;
|
|
35
|
+
/** 采纳(或入队)时刻:进化收益的前后 7 天窗口以它为界 */
|
|
36
|
+
at: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WeeklyInput {
|
|
40
|
+
cwd: string;
|
|
41
|
+
now: number;
|
|
42
|
+
events: readonly TelemetryEvent[];
|
|
43
|
+
runs: readonly { runId: string; status: string; dispatchedAt: number | null }[];
|
|
44
|
+
queue: readonly QueueItem[];
|
|
45
|
+
skills: readonly SkillRef[];
|
|
46
|
+
applied: readonly AppliedRun[];
|
|
47
|
+
referenceScan?: (skill: string) => ReferenceScan;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SkillWeek {
|
|
51
|
+
advertised: number;
|
|
52
|
+
loaded: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function weekOf(ts: number): string {
|
|
56
|
+
return isoWeek(ts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function emptyStat(): SkillWeek {
|
|
60
|
+
return { advertised: 0, loaded: 0 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 周 → 技能 → 曝光/加载:零加载候选与趋势表都从这张表出 */
|
|
64
|
+
function perSkillWeeks(events: readonly TelemetryEvent[]): Map<string, Map<string, SkillWeek>> {
|
|
65
|
+
const table = new Map<string, Map<string, SkillWeek>>();
|
|
66
|
+
const bucket = (week: string, skill: string): SkillWeek => {
|
|
67
|
+
let inner = table.get(week);
|
|
68
|
+
if (!inner) {
|
|
69
|
+
inner = new Map<string, SkillWeek>();
|
|
70
|
+
table.set(week, inner);
|
|
71
|
+
}
|
|
72
|
+
let stat = inner.get(skill);
|
|
73
|
+
if (!stat) {
|
|
74
|
+
stat = emptyStat();
|
|
75
|
+
inner.set(skill, stat);
|
|
76
|
+
}
|
|
77
|
+
return stat;
|
|
78
|
+
};
|
|
79
|
+
for (const event of events) {
|
|
80
|
+
if (typeof event.ts !== "number") continue;
|
|
81
|
+
const week = weekOf(event.ts);
|
|
82
|
+
if (event.kind === "advertised") {
|
|
83
|
+
const skills = Array.isArray(event.skills) ? (event.skills as TelemetrySkill[]) : [];
|
|
84
|
+
for (const skill of skills) {
|
|
85
|
+
if (!skill || typeof skill.name !== "string" || skill.disabled === true) continue;
|
|
86
|
+
bucket(week, skill.name).advertised += 1;
|
|
87
|
+
}
|
|
88
|
+
} else if (event.kind === "loaded" && typeof event.skill === "string") {
|
|
89
|
+
bucket(week, event.skill).loaded += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return table;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function percent(part: number, total: number): string {
|
|
96
|
+
return `${(ratio(part, total) * 100).toFixed(1)}%`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 描述重叠度:中文按二元组、英文按词,取 Jaccard;用来区分「真没人用」和「两个技能抢同一件事」 */
|
|
100
|
+
export function descriptionOverlap(left: string, right: string): number {
|
|
101
|
+
const tokens = (text: string): Set<string> => {
|
|
102
|
+
const set = new Set<string>();
|
|
103
|
+
for (const word of text.toLowerCase().match(/[a-z0-9]{2,}/g) ?? []) set.add(word);
|
|
104
|
+
const cjk = (text.match(/[\u4e00-\u9fa5]+/g) ?? []).join("");
|
|
105
|
+
for (let index = 0; index + 1 < cjk.length; index += 1) set.add(cjk.slice(index, index + 2));
|
|
106
|
+
return set;
|
|
107
|
+
};
|
|
108
|
+
const a = tokens(left);
|
|
109
|
+
const b = tokens(right);
|
|
110
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
111
|
+
let shared = 0;
|
|
112
|
+
for (const token of a) if (b.has(token)) shared += 1;
|
|
113
|
+
return shared / (a.size + b.size - shared);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function topOverlap(skill: SkillRef, all: readonly SkillRef[]): { name: string; score: number } | null {
|
|
117
|
+
let best: { name: string; score: number } | null = null;
|
|
118
|
+
for (const other of all) {
|
|
119
|
+
if (other.name === skill.name) continue;
|
|
120
|
+
const score = descriptionOverlap(skill.description, other.description);
|
|
121
|
+
if (score > 0 && (best === null || score > best.score)) best = { name: other.name, score };
|
|
122
|
+
}
|
|
123
|
+
return best;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 进化收益:采纳后 7 天该技能加载率相对前 7 天的变化(单位 pp);任一窗口无曝光 = 样本不足 */
|
|
127
|
+
function gainOf(input: WeeklyInput, applied: AppliedRun): string {
|
|
128
|
+
const before = emptyStat();
|
|
129
|
+
const after = emptyStat();
|
|
130
|
+
const day = 86_400_000;
|
|
131
|
+
for (const event of input.events) {
|
|
132
|
+
if (typeof event.ts !== "number") continue;
|
|
133
|
+
const inBefore = event.ts >= applied.at - 7 * day && event.ts < applied.at;
|
|
134
|
+
const inAfter = event.ts >= applied.at && event.ts < applied.at + 7 * day;
|
|
135
|
+
if (!inBefore && !inAfter) continue;
|
|
136
|
+
const isAdvertised = event.kind === "advertised" && Array.isArray(event.skills)
|
|
137
|
+
&& (event.skills as TelemetrySkill[]).some((skill) => skill?.name === applied.skill && skill.disabled !== true);
|
|
138
|
+
const target = inBefore ? before : after;
|
|
139
|
+
if (isAdvertised) target.advertised += 1;
|
|
140
|
+
else if (event.kind === "loaded" && event.skill === applied.skill) target.loaded += 1;
|
|
141
|
+
}
|
|
142
|
+
if (before.advertised === 0 || after.advertised === 0) return "样本不足";
|
|
143
|
+
const delta = (ratio(after.loaded, after.advertised) - ratio(before.loaded, before.advertised)) * 100;
|
|
144
|
+
return `${delta >= 0 ? "+" : ""}${delta.toFixed(1)}pp`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildWeeklyReport(input: WeeklyInput): string {
|
|
148
|
+
const table = perSkillWeeks(input.events);
|
|
149
|
+
const currentWeek = weekOf(input.now);
|
|
150
|
+
const lines: string[] = [`技能进化周报:${currentWeek}(工作区:${input.cwd},只含 .pi/skills)`];
|
|
151
|
+
|
|
152
|
+
const weeks = [...table.keys()].sort().slice(-CANDIDATE_WEEKS);
|
|
153
|
+
for (const week of weeks) {
|
|
154
|
+
const stats = [...(table.get(week)?.values() ?? [])];
|
|
155
|
+
const advertised = stats.reduce((sum, stat) => sum + stat.advertised, 0);
|
|
156
|
+
const loaded = stats.reduce((sum, stat) => sum + stat.loaded, 0);
|
|
157
|
+
lines.push(` ${week} 曝光 ${advertised} · 加载 ${loaded} · 加载率 ${percent(loaded, advertised)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let advertised = 0;
|
|
161
|
+
let loaded = 0;
|
|
162
|
+
let failed = 0;
|
|
163
|
+
const attribution = new Map<string, number>();
|
|
164
|
+
const conflicts = { p0: 0, p1: 0, stock: 0, added: 0 };
|
|
165
|
+
for (const event of input.events) {
|
|
166
|
+
if (event.kind === "advertised") {
|
|
167
|
+
const skills = Array.isArray(event.skills) ? (event.skills as TelemetrySkill[]) : [];
|
|
168
|
+
advertised += skills.filter((skill) => skill?.disabled !== true).length;
|
|
169
|
+
} else if (event.kind === "loaded") loaded += 1;
|
|
170
|
+
else if (event.kind === "failed") failed += 1;
|
|
171
|
+
else if (event.kind === "attributed" && typeof event.category === "string") {
|
|
172
|
+
attribution.set(event.category, (attribution.get(event.category) ?? 0) + 1);
|
|
173
|
+
} else if (event.kind === "conflict") {
|
|
174
|
+
if (event.level === "P0") conflicts.p0 += 1;
|
|
175
|
+
else if (event.level === "P1") conflicts.p1 += 1;
|
|
176
|
+
if (event.stock === true) conflicts.stock += 1;
|
|
177
|
+
else conflicts.added += 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lines.push(`加载率 ${percent(loaded, advertised)}(加载 ${loaded} / 曝光 ${advertised})`);
|
|
182
|
+
// 分母是「加载 + 尝试失败」:只除加载数会算出 >100% 的假值
|
|
183
|
+
lines.push(`尝试失败率 ${percent(failed, loaded + failed)}(失败 ${failed} / 尝试 ${loaded + failed})`);
|
|
184
|
+
const attributionTotal = [...attribution.values()].reduce((sum, count) => sum + count, 0);
|
|
185
|
+
const distribution = ["A", "B", "C", "D", "E", "F", "G"]
|
|
186
|
+
.map((key) => `${key} ${attribution.get(key) ?? 0}`)
|
|
187
|
+
.join(" · ");
|
|
188
|
+
lines.push(`归因分布:${distribution}${attributionTotal === 0 ? "(本次无归因记录)" : ""}`);
|
|
189
|
+
lines.push(`冲突密度:存量 ${conflicts.stock} 条 / 新增 ${conflicts.added} 条(P0 ${conflicts.p0} · P1 ${conflicts.p1})`);
|
|
190
|
+
|
|
191
|
+
// stale = 卡死没出提案,既不是提案也不能算耗掉一次机会,排除在外(否则卡死越多采纳率越低)
|
|
192
|
+
const proposed = input.runs.filter(
|
|
193
|
+
(run) => run.status !== "dispatched" && run.status !== "abandoned" && run.status !== "stale",
|
|
194
|
+
).length;
|
|
195
|
+
const appliedRuns = input.runs.filter((run) => run.status === "applied").length;
|
|
196
|
+
const rolledBack = input.runs.filter((run) => run.status === "rolled-back").length;
|
|
197
|
+
lines.push(`采纳率 ${percent(appliedRuns, proposed)}(采纳 ${appliedRuns} / 提案 ${proposed})`);
|
|
198
|
+
lines.push(`回滚率 ${percent(rolledBack, appliedRuns + rolledBack)}(回滚 ${rolledBack} / 采纳 ${appliedRuns})`);
|
|
199
|
+
|
|
200
|
+
const gains = (input.applied ?? []).map((run) => `${run.skill} ${gainOf(input, run)}`);
|
|
201
|
+
lines.push(`进化收益(采纳后 7 天加载率 Δ):${gains.length > 0 ? gains.join("、") : "样本不足"}`);
|
|
202
|
+
|
|
203
|
+
lines.push(`零加载候选(近 ${CANDIDATE_WEEKS} 周 曝光>0 且 加载=0):`);
|
|
204
|
+
const candidates: string[] = [];
|
|
205
|
+
for (const skill of input.skills) {
|
|
206
|
+
const hit = weeks.length === CANDIDATE_WEEKS
|
|
207
|
+
&& weeks.every((week) => {
|
|
208
|
+
const stat = table.get(week)?.get(skill.name);
|
|
209
|
+
return stat !== undefined && stat.advertised > 0 && stat.loaded === 0;
|
|
210
|
+
});
|
|
211
|
+
if (!hit) continue;
|
|
212
|
+
const refs = input.referenceScan?.(skill.name);
|
|
213
|
+
const parts: string[] = [];
|
|
214
|
+
if (refs) {
|
|
215
|
+
parts.push(`.cursor stub ${refs.cursorStub ? "有" : "缺"}`);
|
|
216
|
+
parts.push(`AGENTS 索引 ${refs.agentsIndex ? "有" : "缺"}`);
|
|
217
|
+
parts.push(`其它引用 ${refs.mentions} 处`);
|
|
218
|
+
}
|
|
219
|
+
const overlap = topOverlap(skill, input.skills);
|
|
220
|
+
parts.push(overlap ? `描述重叠 ${overlap.name} ${overlap.score.toFixed(2)}` : "描述重叠 无");
|
|
221
|
+
candidates.push(` · ${skill.name} ${parts.join(" · ")}`);
|
|
222
|
+
}
|
|
223
|
+
if (weeks.length < CANDIDATE_WEEKS) {
|
|
224
|
+
lines.push(` 样本不足:当前只有 ${weeks.length} 周遥测,需要连续 ${CANDIDATE_WEEKS} 周`);
|
|
225
|
+
} else if (candidates.length === 0) {
|
|
226
|
+
lines.push(" 无");
|
|
227
|
+
} else {
|
|
228
|
+
lines.push(...candidates);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lines.push("注:加载率只反映「技能文件被读取」,读 ≠ 用;只读 description 就照做的情况记不到。");
|
|
232
|
+
lines.push("候选表只是提醒,不是判决;删技能要 /skill-evolve approve <runId> --prune");
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface WeeklyDeps {
|
|
237
|
+
cwd: string;
|
|
238
|
+
evolutionDir: string;
|
|
239
|
+
telemetryDir: string;
|
|
240
|
+
now?: () => number;
|
|
241
|
+
loadSkills?: () => { skills: { name: string; description: string }[] };
|
|
242
|
+
referenceScan?: (skill: string) => ReferenceScan;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 技能名 + description:零加载候选要靠描述重叠度判断「是不是兄弟技能在抢」 */
|
|
246
|
+
function readSkillRefs(cwd: string): SkillRef[] {
|
|
247
|
+
const dirs = listSkillDirs(path.join(cwd, ".pi", "skills"));
|
|
248
|
+
const refs: SkillRef[] = [];
|
|
249
|
+
for (const name of dirs) {
|
|
250
|
+
let text: string;
|
|
251
|
+
try {
|
|
252
|
+
text = fs.readFileSync(path.join(cwd, ".pi", "skills", name, "SKILL.md"), "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
refs.push({ name, description: readFrontmatter(text).description ?? "" });
|
|
257
|
+
}
|
|
258
|
+
return refs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 生产入口:读盘 + 组数据,报告本体在 buildWeeklyReport。weekText 每次调用都重读,避免拿到旧盘面 */
|
|
262
|
+
export function createWeeklyReport(deps: WeeklyDeps): { weekText(): string } {
|
|
263
|
+
const now = deps.now ?? (() => Date.now());
|
|
264
|
+
const loadSkills = deps.loadSkills ?? (() => ({ skills: readSkillRefs(deps.cwd) }));
|
|
265
|
+
const referenceScan = deps.referenceScan ?? createReferenceScan(deps.cwd);
|
|
266
|
+
const weekText = (): string => {
|
|
267
|
+
const time = now();
|
|
268
|
+
const queue = readQueue(deps.evolutionDir);
|
|
269
|
+
const applied = queue
|
|
270
|
+
.filter((item) => item.status === "applied")
|
|
271
|
+
.map((item) => ({ skill: item.skill, at: item.createdAt }));
|
|
272
|
+
const skills = loadSkills().skills.map((skill) => ({ name: skill.name, description: skill.description ?? "" }));
|
|
273
|
+
return buildWeeklyReport({
|
|
274
|
+
cwd: deps.cwd,
|
|
275
|
+
now: time,
|
|
276
|
+
events: readEvents(deps.telemetryDir, CANDIDATE_WEEKS * 7, time),
|
|
277
|
+
runs: listRuns(deps.evolutionDir, 50),
|
|
278
|
+
queue,
|
|
279
|
+
skills,
|
|
280
|
+
applied,
|
|
281
|
+
referenceScan,
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
return { weekText };
|
|
285
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# 技能进化 · 复盘任务(M3)
|
|
2
|
+
|
|
3
|
+
你是一个**后台复盘 agent**,不面向用户:没有人会读你的聊天回复,唯一的交付物是文件。
|
|
4
|
+
你的任务是回答一个问题:**上一轮会话里,被曝光的技能为什么没被加载?**
|
|
5
|
+
|
|
6
|
+
## 一、输入(全部已给在提示词末尾)
|
|
7
|
+
|
|
8
|
+
- `会话ID`:上一轮会话的 ID。
|
|
9
|
+
- `会话原文`:该会话 JSONL 的**绝对路径**。读它的唯一合法方式是本节「二」的抽查法。
|
|
10
|
+
- `索引`:`00-session/index.json`,由脚本预先生成,含每一行的 `line`(1 起算)、`type`、`role?`、`ts?`、`chars`、`excerpt`(≤80 字)。
|
|
11
|
+
- `run 目录`:`runs/<runId>/`,你**只能**往这里写文件。
|
|
12
|
+
- `覆盖轮数`:本次复盘覆盖的是「刚刚结束的一轮」还是「合并了 N 轮待复盘区间」。
|
|
13
|
+
写的是 N 轮时,起点 ts 到现在的**每一轮**都在范围内:结论必须覆盖整段,不能只看最后一轮;
|
|
14
|
+
同一个技能在多轮里重复曝光的,`evidence` 要把各轮的行号都列上。
|
|
15
|
+
|
|
16
|
+
如果会话原文一栏写着「不可用」(evidenceMode=telemetry-only):不要编造证据,`evidence` 一律留空数组,`confidence` 不得高于 `low`,并在 `notes` 里写明「本轮无会话原文,结论仅来自遥测」。
|
|
17
|
+
|
|
18
|
+
## 二、读会话:只许抽查,不许整文件读
|
|
19
|
+
|
|
20
|
+
会话原文平均接近 1MB。**禁止**直接整文件读它(禁止 `cat`、禁止不带 offset/limit 的 read、禁止把它塞进任何命令输出)。
|
|
21
|
+
|
|
22
|
+
正确做法:
|
|
23
|
+
|
|
24
|
+
1. 先读 `index.json`,用 `excerpt` 找候选行:`advertised` 相关轮次、用户请求、被调用的工具、疑似读了别的技能的位置。
|
|
25
|
+
2. 用行号附近的窗口读原文:`read <会话原文> offset=<目标行-10> limit=40`,或 `bash: sed -n '1000,1060p' <会话原文>`。
|
|
26
|
+
3. 需要全文定位关键词时用 `rg -n '<关键词>' <会话原文>`,**只取命中行号**,再回到第 2 步取上下文。
|
|
27
|
+
|
|
28
|
+
每次抽查都要能说出「我要在这几行验证哪条假设」。凑不出假设就说明证据不足,写 `confidence: low` 并说明缺口。
|
|
29
|
+
|
|
30
|
+
## 三、技能文件怎么读(可以读,但不许改)
|
|
31
|
+
|
|
32
|
+
- 想核对某技能 description 的措辞、或 frontmatter 是否损坏,可以读 `.pi/skills/<名>/SKILL.md`。
|
|
33
|
+
- **绝对禁止修改 `.pi/skills/` 下任何文件**,也禁止修改 `.pi/settings.json`、`.pi/evolution/config.json`、遥测文件、其他 run 目录。本任务只出结论,不做改动。
|
|
34
|
+
|
|
35
|
+
## 四、判断每条被曝光技能的结局
|
|
36
|
+
|
|
37
|
+
对上一轮 `advertised` 事件里的**每一个**技能(提示词里会给技能名清单;完整记录在 `telemetry/` 当日分片里,可读但不必读)判定:
|
|
38
|
+
|
|
39
|
+
- `loaded: true`:会话里确有读取该技能文件的动作(`read` 该 `SKILL.md`,或 `bash` 命令里出现 `skills/<名>/SKILL.md`)。给出该动作的行号。
|
|
40
|
+
- `loaded: false`:没有这条证据。此时必须给出 `whyNotLoaded`,取下面 A~G 之一。
|
|
41
|
+
|
|
42
|
+
### 归因代号(只许用这七个字母)
|
|
43
|
+
|
|
44
|
+
| 代号 | 含义 | 判定要点 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `A` | 技能被配置成不参与模型调用(`disableModelInvocation: true`) | 它压根没进系统提示词,不该算「未加载」。看技能 frontmatter / 设置里的禁用项 |
|
|
47
|
+
| `B` | frontmatter 坏了,技能没被正常发现 | 读该 `SKILL.md` 头部,看是否缺 `name`/`description`、YAML 是否破损 |
|
|
48
|
+
| `C` | 同名技能被别人抢先(collision) | 同一名字有多份定义,实际生效的不是这份 |
|
|
49
|
+
| `D` | 文件不在技能发现范围内 | 路径不在 `.pi/skills/`、或层级/命名不符合发现规则。只记录,不改 |
|
|
50
|
+
| `E` | 本轮没有能读文件的工具(无 `read`、无 `bash`) | 看系统提示词的 selectedTools / 本轮实际调用过的工具 |
|
|
51
|
+
| `F` | 兄弟技能被选中,它没有(描述重叠竞争) | 找到实际加载的同类技能,对比两边 description 的措辞差异 |
|
|
52
|
+
| `G` | 用户请求与 description 没有共同的路由词 | 摘出用户原话里的关键词,指出 description 缺哪些词 |
|
|
53
|
+
|
|
54
|
+
判不准就用 `confidence: low` 并在 `notes` 里写「需要人工判断」,**不要**为了凑字母硬套。
|
|
55
|
+
|
|
56
|
+
### 机械层已经判过的:你只做 F/G(重要)
|
|
57
|
+
|
|
58
|
+
提示词末尾有一节「已机械归因」:这些技能已经由脚本按机械判据定成 A~E(配置禁用 / frontmatter 诊断 / 同名冲突 /
|
|
59
|
+
不在发现范围 / 本轮没有读工具)。对这些技能:
|
|
60
|
+
|
|
61
|
+
- 直接沿用它的代号,**不要重判、不要改**;`evidence` 可以留空数组,`confidence` 写 `medium`
|
|
62
|
+
(机械判据不依赖会话证据)。
|
|
63
|
+
- 只有当你在会话里查到**能推翻**它的证据时才允许改,且必须在 `notes` 里写「改判理由 + 证据行号」,
|
|
64
|
+
`confidence` 不得高于 `medium`。
|
|
65
|
+
|
|
66
|
+
你真正要判的是**没出现在「已机械归因」清单里**的技能,而且只能给 `F`(兄弟技能被选中、描述重叠竞争)
|
|
67
|
+
或 `G`(description 缺路由词)。机械判据已经排除了「配置 / 损坏 / 冲突 / 范围 / 工具」五种非文案原因,
|
|
68
|
+
跳过它们直接建议「改写 description」是这条流程最常见的错判。
|
|
69
|
+
|
|
70
|
+
### 两个必须承认的盲区(写进 `notes`)
|
|
71
|
+
|
|
72
|
+
- 只读了技能 description 就照着做、从没打开 `SKILL.md`:本流程这类技能一律记 `loaded: false`,但其实它起作用了。
|
|
73
|
+
- 读了 `SKILL.md` 却没照做:本流程记 `loaded: true`,但其实没起作用。
|
|
74
|
+
- 所以「加载率低」不等于「技能没用」。你的结论只描述证据,不下价值判断。
|
|
75
|
+
|
|
76
|
+
## 五、输出:`01-findings.json`(唯一交付物)
|
|
77
|
+
|
|
78
|
+
写到 `<run 目录>/01-findings.json`,UTF-8,严格 JSON(不要注释、不要尾逗号):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"skills": [
|
|
83
|
+
{
|
|
84
|
+
"name": "技能名",
|
|
85
|
+
"advertised": true,
|
|
86
|
+
"loaded": false,
|
|
87
|
+
"whyNotLoaded": "G",
|
|
88
|
+
"confidence": "high",
|
|
89
|
+
"evidence": [{ "line": 1234 }]
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"notes": []
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
字段约束:
|
|
97
|
+
|
|
98
|
+
- `name`:与 `advertised` 里的技能名完全一致,不许改名或翻译。
|
|
99
|
+
- `advertised`:固定 `true`(这批就是曝光清单里的)。
|
|
100
|
+
- `loaded`:布尔。全部 `loaded: true` 的技能也照样列出,`whyNotLoaded` 省略该键。
|
|
101
|
+
- `whyNotLoaded`:仅 `loaded: false` 时出现,取值 `"A"`–`"G"`。
|
|
102
|
+
- `confidence`:`"high"` / `"medium"` / `"low"`,只有看到原文证据才配 `high`。
|
|
103
|
+
- `evidence`:数组,每项 `{ "line": 行号 }`,可再加 `{ "ref": "..." }` 说明该行是什么;行号必须是**会话 JSONL 的物理行号**,与 `index.json` 的 `line` 同一套编号。给不出行号就不要写 `evidence` 项,改为把 `confidence` 降到 `low`。
|
|
104
|
+
- **索引不可用时(prompt 里「索引」写「不可用」/ evidenceMode=telemetry-only):`evidence` 必须为空数组 `[]`,`confidence` 不得高于 `low`**,并在 `notes` 里写「本轮无会话原文与索引,结论仅来自遥测」。
|
|
105
|
+
- `notes`:字符串数组,写盲区提醒、证据缺口、需要人工确认的点。
|
|
106
|
+
|
|
107
|
+
## 六、提案:只对「有把握」的 finding 生成(可选)
|
|
108
|
+
|
|
109
|
+
满足**全部**条件的 finding 才生成提案:`loaded: false`、`whyNotLoaded` 是 `G`(或 `F` 且能指出兄弟技能的具体措辞差异)、
|
|
110
|
+
`confidence` 是 `high`。其余一律不生成 —— 宁可没有提案,也不拿猜测去改技能。
|
|
111
|
+
|
|
112
|
+
产物都写在 `<run 目录>` 下:
|
|
113
|
+
|
|
114
|
+
1. `02-proposal.json`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"type": "improve",
|
|
119
|
+
"skill": "技能名",
|
|
120
|
+
"rationale": "一句话:缺哪些路由词、证据在哪几行",
|
|
121
|
+
"evidence": [1234, 1250],
|
|
122
|
+
"diffFile": "02-proposal.diff"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- `type`:`improve`(改现有技能)/ `create`(新建)/ `merge`(合并两个重叠技能)/ `prune`(建议废弃)。
|
|
127
|
+
- 新建技能时把 `diffFile` 换成 `"candidateFile": "candidate/<名>/SKILL.md"`,并同时产出该文件。
|
|
128
|
+
- `evidence` 只许填**会话 JSONL 的物理行号**,与 `01-findings.json` 同一套编号。
|
|
129
|
+
|
|
130
|
+
2. `02-proposal.diff`(`improve`/`merge`/`prune` 必填):**unified diff**,不是整篇新正文。
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
--- a/.pi/skills/<名>/SKILL.md
|
|
134
|
+
+++ b/.pi/skills/<名>/SKILL.md
|
|
135
|
+
@@ -12,6 +12,8 @@
|
|
136
|
+
上下文行
|
|
137
|
+
-旧行
|
|
138
|
+
+新行
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- 文件头只能指向 `.pi/skills/...`;出现别的路径(例如 `b/rust_server/src/x.rs`)整份提案判 `bad_path` 作废。
|
|
142
|
+
- 增删合计 > 40 行会被标 `too_large` 转人工,所以提案要小而准:只改 description / 触发词那几行。
|
|
143
|
+
|
|
144
|
+
3. `candidate/<名>/SKILL.md`(`create` 必填):frontmatter 必须有 `name` 与 `description`;
|
|
145
|
+
`name` 与目录名一致、≤20 字、只含小写字母/数字/短横线(`^[a-z0-9-]+$`)。
|
|
146
|
+
|
|
147
|
+
## 七、三条禁令(违反即任务失败)
|
|
148
|
+
|
|
149
|
+
1. 只能写 `<run 目录>/` 下的文件。`.pi/skills/`、`.pi/settings.json`、`.pi/evolution/config.json`、遥测、别的 run 目录一律不许写。
|
|
150
|
+
2. 不许改 `.pi/skills/` 下任何文件(连「顺手修一下」也不行)—— 提案只是 diff,应用是人工后续动作。
|
|
151
|
+
3. 不许 `git commit` / `git add` / `git push`,也不许在 `run 目录` 外建文件、建临时目录。
|
|
152
|
+
|
|
153
|
+
## 八、收尾:把 run.json 标成 analyzed / proposed
|
|
154
|
+
|
|
155
|
+
`<run 目录>/run.json` 已存在,`status` 是 `"dispatched"`。复盘完成后:
|
|
156
|
+
|
|
157
|
+
1. 用 `read` 读它,用 `write` 整体写回(保持所有原字段不变);
|
|
158
|
+
2. 没生成提案时 `status` 写 `"analyzed"`;生成了提案时写 `"proposed"`,并追加
|
|
159
|
+
`"proposal": { "type", "skill", "status", "added", "removed", "file" }`
|
|
160
|
+
(`status` 填你自检的结论:`ok` / `too_large` / `bad_path` / `invalid_candidate`;`file` 是相对 run 目录的文件名);
|
|
161
|
+
3. 往 `stages` 数组追加一项:`{ "name": "analyze", "model": "<你实际用的模型名>", "usage": null, "ms": <你这次耗时毫秒> }`;
|
|
162
|
+
4. 往 `artifacts` 加 `"01-findings.json": "<绝对路径>"`,有提案时再加 `"02-proposal.json"` 与 `02-proposal.diff`(或 candidate 路径)。
|
|
163
|
+
|
|
164
|
+
如果中途发现证据不足以外的问题(比如索引缺行、会话原文读不到),不要伪造结论:`status` 改为 `"abandoned"`,加 `"abandonedReason": "<一句话原因>"`,然后在 `01-findings.json` 的 `notes` 里写明。
|
|
165
|
+
|
|
166
|
+
## 九、红线
|
|
167
|
+
|
|
168
|
+
- 不改任何技能、配置、遥测、别人的 run 目录。
|
|
169
|
+
- 不整文件读会话原文。
|
|
170
|
+
- 不虚构行号、不虚构技能名。
|
|
171
|
+
- 不写聊天总结充当交付物:没有 `01-findings.json` 就算没做完。
|
|
172
|
+
- 提案只在 `runs/<runId>/` 里,且只是 diff / candidate,不应用。
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端到端复演脚本(M6 验收):临时目录 + 假技能 + 假会话,走完 发现 → 提案 → 评审 → 人批 → 采纳 → 复验。
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* node --experimental-strip-types scripts/replay.ts # 假评审,省 token(默认)
|
|
6
|
+
* node --experimental-strip-types scripts/replay.ts --real # 真起 `pi -p` 做评审
|
|
7
|
+
* node --experimental-strip-types scripts/replay.ts --dir /tmp/xxx --keep
|
|
8
|
+
*
|
|
9
|
+
* 绝不写本仓:所有产物都在 `--dir` 指定的临时目录(默认 `mkdtemp`)。
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { REPLAY_BEFORE } from "../lib/replay-fixture.ts";
|
|
16
|
+
import { runReplay } from "../lib/replay.ts";
|
|
17
|
+
|
|
18
|
+
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
19
|
+
const REPO_ROOT = path.resolve(HERE, "..", "..", "..", "..");
|
|
20
|
+
|
|
21
|
+
interface Args {
|
|
22
|
+
real: boolean;
|
|
23
|
+
keep: boolean;
|
|
24
|
+
dir: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv: string[]): Args {
|
|
28
|
+
const args: Args = { real: false, keep: false, dir: null };
|
|
29
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
30
|
+
const item = argv[index] as string;
|
|
31
|
+
if (item === "--real") args.real = true;
|
|
32
|
+
else if (item === "--fake") args.real = false;
|
|
33
|
+
else if (item === "--keep") args.keep = true;
|
|
34
|
+
else if (item === "--dir") {
|
|
35
|
+
args.dir = argv[index + 1] ?? null;
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 造一个干净的临时工作区:技能已提交,闸0 的基线才是「干净」 */
|
|
43
|
+
function prepareWorkdir(dir: string): void {
|
|
44
|
+
const skillFile = path.join(dir, ".pi", "skills", "replay-demo", "SKILL.md");
|
|
45
|
+
fs.mkdirSync(path.dirname(skillFile), { recursive: true });
|
|
46
|
+
fs.writeFileSync(skillFile, REPLAY_BEFORE, "utf8");
|
|
47
|
+
const git = (args: string[]): void => {
|
|
48
|
+
const result = spawnSync("git", args, { cwd: dir, encoding: "utf8" });
|
|
49
|
+
if (result.status !== 0) throw new Error(`git ${args.join(" ")} 失败:${result.stderr || result.stdout}`);
|
|
50
|
+
};
|
|
51
|
+
git(["init", "-q"]);
|
|
52
|
+
git(["add", "-A"]);
|
|
53
|
+
git(["-c", "user.email=replay@example.com", "-c", "user.name=replay", "commit", "-qm", "复演初始状态"]);
|
|
54
|
+
|
|
55
|
+
// 硬规则用真仓那一份:复演要验的正是真实评审规则
|
|
56
|
+
const rulesSrc = path.join(REPO_ROOT, ".pi", "evolution", "rules", "hard-rules.json");
|
|
57
|
+
if (fs.existsSync(rulesSrc)) {
|
|
58
|
+
const rulesDst = path.join(dir, ".pi", "evolution", "rules", "hard-rules.json");
|
|
59
|
+
fs.mkdirSync(path.dirname(rulesDst), { recursive: true });
|
|
60
|
+
fs.copyFileSync(rulesSrc, rulesDst);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main(): Promise<void> {
|
|
65
|
+
const args = parseArgs(process.argv.slice(2));
|
|
66
|
+
const dir = args.dir ?? fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-replay-"));
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
prepareWorkdir(dir);
|
|
69
|
+
console.log(`[复演] 工作区:${dir}`);
|
|
70
|
+
console.log(`[复演] 评审:${args.real ? "真跑 pi -p" : "假评审(--real 可切真跑)"}`);
|
|
71
|
+
|
|
72
|
+
const outcome = await runReplay({
|
|
73
|
+
cwd: dir,
|
|
74
|
+
evolutionDir: path.join(dir, ".pi", "evolution"),
|
|
75
|
+
fakeReview: !args.real,
|
|
76
|
+
log: (msg) => console.log(msg),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log("");
|
|
80
|
+
for (const line of outcome.lines) console.log(line);
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(`[复演] run.json status 序列:${outcome.statuses.join(" → ")}`);
|
|
83
|
+
for (const failure of outcome.failures) console.log(`[复演] 失败:${failure}`);
|
|
84
|
+
console.log(`[复演] 结论:${outcome.ok ? "六段全通" : "存在失败段"}`);
|
|
85
|
+
if (!args.keep) {
|
|
86
|
+
// 保留在 /tmp 也没意义,失败时留证据
|
|
87
|
+
if (outcome.ok) fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
else console.log(`[复演] 失败现场保留:${dir}`);
|
|
89
|
+
}
|
|
90
|
+
process.exitCode = outcome.ok ? 0 : 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await main();
|