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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/skill-audit`:把遥测里的归因记录、历史 missed、硬禁令现扫结果汇总成一份人读文本。
|
|
3
|
+
*
|
|
4
|
+
* 两个原则:
|
|
5
|
+
* 1. 只报事实 —— attributed 事件是 M2 之后写的;M2 之前的历史 missed 没有类别,
|
|
6
|
+
* 要么用当前快照现算(并标注「现算」),要么老实列进「未归因」,绝不把两者混成一份数字;
|
|
7
|
+
* 2. 加载率必须带注解 —— 「读了 description 就照做」「读了文件没照做」都不在「读文件」这条证据链里,
|
|
8
|
+
* 单看加载率会把最有用的技能判成最没用的。
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
CATEGORY_ORDER,
|
|
12
|
+
DISPOSITIONS,
|
|
13
|
+
type AttributionCategory,
|
|
14
|
+
type ExposedSkill,
|
|
15
|
+
} from "./attribution.ts";
|
|
16
|
+
import { splitHits } from "./baseline.ts";
|
|
17
|
+
import type { HardRuleHit } from "./hard-rules.ts";
|
|
18
|
+
import { summarize, type MissedStat, type TelemetryEvent, type TelemetrySkill, type TelemetrySummary } from "./telemetry.ts";
|
|
19
|
+
|
|
20
|
+
export interface AuditCategoryRow {
|
|
21
|
+
category: AttributionCategory;
|
|
22
|
+
label: string;
|
|
23
|
+
count: number;
|
|
24
|
+
skills: { skill: string; count: number }[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuditModel {
|
|
28
|
+
days: number;
|
|
29
|
+
summary: TelemetrySummary;
|
|
30
|
+
rows: AuditCategoryRow[];
|
|
31
|
+
/** 一个归因记录都没有的历史 missed(M2 之前采集的数据),不硬编类别 */
|
|
32
|
+
unattributed: MissedStat[];
|
|
33
|
+
conflicts: HardRuleHit[];
|
|
34
|
+
/** 存量债指纹:命中的算存量(设计稿 10.4 已接受),其余算新增 */
|
|
35
|
+
knownFingerprints: ReadonlySet<string>;
|
|
36
|
+
invalidRules: string[];
|
|
37
|
+
/** 归因条数来源统计:遥测里已有的 / 用当前快照现算的 */
|
|
38
|
+
fromTelemetry: number;
|
|
39
|
+
fromLive: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const AUDIT_LOAD_RATE_NOTE = "加载率低 ≠ 技能没用:只读 description 就照做、读了文件却没照做,这两类都不在「读文件」这条证据链里。";
|
|
43
|
+
|
|
44
|
+
/** 存量只列前几条:它们不是失败信号,列多了反而淹没真正要看的新增 */
|
|
45
|
+
const STOCK_PREVIEW = 5;
|
|
46
|
+
const SKILLS_PER_CATEGORY = 6;
|
|
47
|
+
const LABEL_WIDTH = 15;
|
|
48
|
+
const CATEGORY_WIDTH = 8;
|
|
49
|
+
|
|
50
|
+
/** 中文按 2 列宽计算:不然 padEnd 出来的表格在终端里必错位 */
|
|
51
|
+
function displayWidth(text: string): number {
|
|
52
|
+
let width = 0;
|
|
53
|
+
for (const char of text) width += /[\u3000-\u9fff\uff00-\uffef]/.test(char) ? 2 : 1;
|
|
54
|
+
return width;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function padTo(text: string, width: number): string {
|
|
58
|
+
return text + " ".repeat(Math.max(0, width - displayWidth(text)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function asCategory(value: unknown): AttributionCategory | null {
|
|
62
|
+
return typeof value === "string" && (CATEGORY_ORDER as readonly string[]).includes(value)
|
|
63
|
+
? (value as AttributionCategory)
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function countMissed(events: readonly TelemetryEvent[]): Map<string, number> {
|
|
68
|
+
const counts = new Map<string, number>();
|
|
69
|
+
for (const event of events) {
|
|
70
|
+
if (event.kind !== "missed") continue;
|
|
71
|
+
const skill = typeof event.skill === "string" && event.skill.length > 0 ? event.skill : "(未知)";
|
|
72
|
+
counts.set(skill, (counts.get(skill) ?? 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
return counts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 最近一次 advertised 里的技能快照:给现算归因提供 filePath / disabled */
|
|
78
|
+
export function exposedFromEvents(events: readonly TelemetryEvent[]): ExposedSkill[] {
|
|
79
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
80
|
+
const event = events[index];
|
|
81
|
+
if (event.kind !== "advertised" || !Array.isArray(event.skills)) continue;
|
|
82
|
+
return (event.skills as TelemetrySkill[])
|
|
83
|
+
.filter((skill) => typeof skill?.name === "string")
|
|
84
|
+
.map((skill) => ({ name: skill.name, filePath: skill.filePath ?? "", disabled: skill.disabled === true }));
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 最近一次 advertised 记下的 activeTools;没有记录就返回空(空 = 不知道,不能拿来判 E) */
|
|
90
|
+
export function activeToolsFromEvents(events: readonly TelemetryEvent[]): string[] {
|
|
91
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
92
|
+
const event = events[index];
|
|
93
|
+
if (event.kind !== "advertised") continue;
|
|
94
|
+
if (!Array.isArray(event.activeTools)) return [];
|
|
95
|
+
return (event.activeTools as unknown[]).filter((tool): tool is string => typeof tool === "string");
|
|
96
|
+
}
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildAuditModel(input: {
|
|
101
|
+
days: number;
|
|
102
|
+
events: readonly TelemetryEvent[];
|
|
103
|
+
/** 给历史 missed 现算类别的回调;不给就把它们全列进「未归因」 */
|
|
104
|
+
classify?: (skill: string) => AttributionCategory;
|
|
105
|
+
conflicts: readonly HardRuleHit[];
|
|
106
|
+
knownFingerprints?: ReadonlySet<string>;
|
|
107
|
+
invalidRules: readonly string[];
|
|
108
|
+
}): AuditModel {
|
|
109
|
+
const counted = new Map<AttributionCategory, Map<string, number>>();
|
|
110
|
+
const add = (category: AttributionCategory, skill: string, amount: number): void => {
|
|
111
|
+
const bucket = counted.get(category) ?? new Map<string, number>();
|
|
112
|
+
bucket.set(skill, (bucket.get(skill) ?? 0) + amount);
|
|
113
|
+
counted.set(category, bucket);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const covered = new Set<string>();
|
|
117
|
+
let fromTelemetry = 0;
|
|
118
|
+
for (const event of input.events) {
|
|
119
|
+
if (event.kind !== "attributed") continue;
|
|
120
|
+
const skill = typeof event.skill === "string" ? event.skill : "";
|
|
121
|
+
const category = asCategory(event.category);
|
|
122
|
+
if (skill.length === 0 || category === null) continue;
|
|
123
|
+
covered.add(skill);
|
|
124
|
+
add(category, skill, 1);
|
|
125
|
+
fromTelemetry += 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let fromLive = 0;
|
|
129
|
+
const unattributed: MissedStat[] = [];
|
|
130
|
+
const misses = countMissed(input.events);
|
|
131
|
+
for (const [skill, count] of misses) {
|
|
132
|
+
if (covered.has(skill)) continue;
|
|
133
|
+
if (input.classify !== undefined) {
|
|
134
|
+
add(input.classify(skill), skill, count);
|
|
135
|
+
fromLive += count;
|
|
136
|
+
} else {
|
|
137
|
+
unattributed.push({ skill, count });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
unattributed.sort((a, b) => b.count - a.count || a.skill.localeCompare(b.skill));
|
|
141
|
+
|
|
142
|
+
const rows: AuditCategoryRow[] = CATEGORY_ORDER.map((category) => {
|
|
143
|
+
const bucket = counted.get(category) ?? new Map<string, number>();
|
|
144
|
+
const skills = [...bucket.entries()]
|
|
145
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
146
|
+
.slice(0, SKILLS_PER_CATEGORY)
|
|
147
|
+
.map(([skill, count]) => ({ skill, count }));
|
|
148
|
+
const count = [...bucket.values()].reduce((sum, value) => sum + value, 0);
|
|
149
|
+
return { category, label: DISPOSITIONS[category].label, count, skills };
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
days: input.days,
|
|
154
|
+
summary: summarize(input.events),
|
|
155
|
+
rows,
|
|
156
|
+
unattributed,
|
|
157
|
+
conflicts: [...input.conflicts],
|
|
158
|
+
knownFingerprints: input.knownFingerprints ?? new Set<string>(),
|
|
159
|
+
invalidRules: [...input.invalidRules],
|
|
160
|
+
fromTelemetry,
|
|
161
|
+
fromLive,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 冲突段两段式:存量(已接受)只列前几条,新增才是本次要处理的失败信号 */
|
|
166
|
+
function renderConflicts(model: AuditModel): string[] {
|
|
167
|
+
if (model.conflicts.length === 0) return [" P0 命中 0 条(存量 0 / 新增 0)"];
|
|
168
|
+
const { stock, added } = splitHits(model.conflicts, model.knownFingerprints);
|
|
169
|
+
const line = (tag: string, hit: HardRuleHit): string =>
|
|
170
|
+
` ${tag} ${hit.ruleId} ${hit.skill}:${hit.line} ${hit.level} 「${hit.quote}」`;
|
|
171
|
+
const stockShown = stock.slice(0, STOCK_PREVIEW);
|
|
172
|
+
return [
|
|
173
|
+
` P0 命中 ${model.conflicts.length} 条:存量(baseline)${stock.length} 条 · 新增 ${added.length} 条`,
|
|
174
|
+
...(added.length === 0 ? [] : [...added.map((hit) => line("新增", hit))]),
|
|
175
|
+
...(stock.length === 0 ? [] : [" 存量(已接受的存量债,只列前 5 条):"]),
|
|
176
|
+
...stockShown.map((hit) => line("存量", hit)),
|
|
177
|
+
...(stock.length > stockShown.length ? [` 存量其余 ${stock.length - stockShown.length} 条略`] : []),
|
|
178
|
+
...(added.length === 0 ? [] : [" 新增命中:修掉它,或确认无害后跑 /skill-evolve baseline 接受"]),
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function auditText(model: AuditModel): string {
|
|
183
|
+
const { summary } = model;
|
|
184
|
+
const percent = `${(summary.loadRate * 100).toFixed(1)}%`;
|
|
185
|
+
const rate = summary.advertised > 0 ? percent : "无曝光数据";
|
|
186
|
+
const total = model.rows.reduce((sum, row) => sum + row.count, 0);
|
|
187
|
+
|
|
188
|
+
const categoryLines = model.rows.flatMap((row) => {
|
|
189
|
+
const head = ` ${padTo(row.category, CATEGORY_WIDTH)} ${padTo(row.label, LABEL_WIDTH)} ${String(row.count).padStart(4)}`;
|
|
190
|
+
const detail = row.skills.map((item) => ` - ${item.skill} × ${item.count}`);
|
|
191
|
+
return [head, ...detail];
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const categoryHeader = total === 0
|
|
195
|
+
? `按类别(近 ${model.days} 天没有可归因的未命中):`
|
|
196
|
+
: "按类别(顺序即设计稿第八章决策树,命中即停):";
|
|
197
|
+
|
|
198
|
+
const conflictLines = renderConflicts(model);
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
`技能进化 · 机械审计(近 ${model.days} 天)`,
|
|
202
|
+
` 曝光:${summary.rounds} 轮 / ${summary.advertised} 次 · 加载 ${summary.loaded} 次(读取失败 ${summary.failed} 次)· 加载率 ${rate}`,
|
|
203
|
+
` 归因来源:遥测 attributed ${model.fromTelemetry} 条 + 当前快照现算 ${model.fromLive} 条`,
|
|
204
|
+
categoryHeader,
|
|
205
|
+
...categoryLines,
|
|
206
|
+
...(model.unattributed.length === 0
|
|
207
|
+
? []
|
|
208
|
+
: [
|
|
209
|
+
`未归因(M2 之前的历史 missed,没有类别记录):${model.unattributed.length} 个技能`,
|
|
210
|
+
...model.unattributed.slice(0, 10).map((item, index) => ` ${index + 1}. ${item.skill} × ${item.count}`),
|
|
211
|
+
]),
|
|
212
|
+
...(summary.missedTop.length === 0
|
|
213
|
+
? []
|
|
214
|
+
: ["未命中 top:", ...summary.missedTop.map((item, index) => ` ${index + 1}. ${item.skill} × ${item.count}`)]),
|
|
215
|
+
`机械冲突(现扫 .pi/skills,规则 .pi/evolution/rules/hard-rules.json):`,
|
|
216
|
+
...conflictLines,
|
|
217
|
+
...(model.invalidRules.length === 0 ? [] : ["规则不可用:", ...model.invalidRules.map((item) => ` - ${item}`)]),
|
|
218
|
+
"",
|
|
219
|
+
`注意:${AUDIT_LOAD_RATE_NOTE}`,
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存量债基线(设计稿 10.4):机械扫描命中的 P0 里,「已经认账」的那部分不算失败信号。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有它:`.pi/skills` 里本来就存在历史违规(例如 deploy-test 里的 grep 写法)。
|
|
5
|
+
* 新插件如果天天把它们报成红,等于宣布自己不可用 —— 所以命中分两段:存量(baseline)/ 新增。
|
|
6
|
+
* 只有「新增」才是本次要处理的问题。
|
|
7
|
+
*
|
|
8
|
+
* 指纹口径:ruleId + skill + sha256(quote 前 120 字),**不含行号**。
|
|
9
|
+
* 行号会随正文增删上下漂移,带上行号就会把同一个老问题反复报成「新增」。
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import type { HardRuleHit } from "./hard-rules.ts";
|
|
15
|
+
import { logLine } from "./logfile.ts";
|
|
16
|
+
import type { MutationQueue } from "./telemetry.ts";
|
|
17
|
+
|
|
18
|
+
/** 与 hard-rules.ts 的 QUOTE_MAX 同口径:quote 只留前 120 字,指纹也只算这 120 字 */
|
|
19
|
+
export const FINGERPRINT_QUOTE_MAX = 120;
|
|
20
|
+
export const BASELINE_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
export interface BaselineViolation {
|
|
23
|
+
ruleId: string;
|
|
24
|
+
level: string;
|
|
25
|
+
skill: string;
|
|
26
|
+
/** 行号只作参考,不进指纹:行号漂移不算新增 */
|
|
27
|
+
line: number;
|
|
28
|
+
quote: string;
|
|
29
|
+
fingerprint: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Baseline {
|
|
33
|
+
version: number;
|
|
34
|
+
generatedAt: number;
|
|
35
|
+
/** true = 首次审计自动生成(把当时全部命中都当存量);false = 人工 /skill-evolve baseline 接受 */
|
|
36
|
+
bootstrap: boolean;
|
|
37
|
+
violations: BaselineViolation[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type HitLike = Pick<HardRuleHit, "ruleId" | "skill" | "quote">;
|
|
41
|
+
|
|
42
|
+
export function fingerprintOf(hit: HitLike): string {
|
|
43
|
+
const quote = hit.quote.trim().slice(0, FINGERPRINT_QUOTE_MAX);
|
|
44
|
+
return createHash("sha256").update(`${hit.ruleId}\u0000${hit.skill}\u0000${quote}`).digest("hex").slice(0, 16);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toViolation(hit: HardRuleHit): BaselineViolation {
|
|
48
|
+
return {
|
|
49
|
+
ruleId: hit.ruleId,
|
|
50
|
+
level: hit.level,
|
|
51
|
+
skill: hit.skill,
|
|
52
|
+
line: hit.line,
|
|
53
|
+
quote: hit.quote,
|
|
54
|
+
fingerprint: fingerprintOf(hit),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** bootstrap=true 供首次审计自动生成;false 供人工「接受当前全部命中」 */
|
|
59
|
+
export function buildBaseline(hits: readonly HardRuleHit[], now: number, bootstrap: boolean): Baseline {
|
|
60
|
+
return {
|
|
61
|
+
version: BASELINE_VERSION,
|
|
62
|
+
generatedAt: now,
|
|
63
|
+
bootstrap,
|
|
64
|
+
violations: hits.map(toViolation),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function baselinePath(evolutionDir: string): string {
|
|
69
|
+
return path.join(evolutionDir, "baseline.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 读基线:文件不存在是正常状态(返回 null),内容坏了也只当不存在,绝不抛 */
|
|
73
|
+
export function readBaseline(file: string, log?: (msg: string) => void): Baseline | null {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as Baseline;
|
|
76
|
+
if (!Array.isArray(parsed.violations)) return null;
|
|
77
|
+
return parsed;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
80
|
+
log?.(`[skill-evolve] 存量基线读取失败,按不存在处理:${String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function saveBaseline(file: string, baseline: Baseline, queue: MutationQueue): Promise<void> {
|
|
87
|
+
await queue(path.resolve(file), async () => {
|
|
88
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
89
|
+
await fs.promises.writeFile(file, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function fingerprintsOf(baseline: Baseline | null): Set<string> {
|
|
94
|
+
return new Set((baseline?.violations ?? []).map((item) => item.fingerprint));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 按指纹切两段:known 里的算存量,其余算新增(同一条命中不会同时落两边) */
|
|
98
|
+
export function splitHits(
|
|
99
|
+
hits: readonly HardRuleHit[],
|
|
100
|
+
known: ReadonlySet<string>,
|
|
101
|
+
): { stock: HardRuleHit[]; added: HardRuleHit[] } {
|
|
102
|
+
const stock: HardRuleHit[] = [];
|
|
103
|
+
const added: HardRuleHit[] = [];
|
|
104
|
+
for (const hit of hits) {
|
|
105
|
+
if (known.has(fingerprintOf(hit))) stock.push(hit);
|
|
106
|
+
else added.push(hit);
|
|
107
|
+
}
|
|
108
|
+
return { stock, added };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface BaselineGateDeps {
|
|
112
|
+
file: string;
|
|
113
|
+
/** 当前命中:懒到第一次要用时才算,避免 factory 里读盘 */
|
|
114
|
+
hits: () => readonly HardRuleHit[];
|
|
115
|
+
now: () => number;
|
|
116
|
+
queue: MutationQueue;
|
|
117
|
+
log?: (msg: string) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface BaselineGate {
|
|
121
|
+
/** 同步取:缺文件时按当前命中 bootstrap 并 fire-and-forget 写盘(settle 是同步的,不能 await) */
|
|
122
|
+
baseline(): Baseline;
|
|
123
|
+
isStock(hit: HardRuleHit): boolean;
|
|
124
|
+
/** `/skill-evolve baseline`:把当前全部命中写进基线,返回给人看的一句话 */
|
|
125
|
+
acceptAll(): Promise<string>;
|
|
126
|
+
/** `/skill-evolve baseline --reset`:清空清单(不是删文件 —— 删了下次审计又会自动 bootstrap 回存量) */
|
|
127
|
+
reset(): Promise<string>;
|
|
128
|
+
/** 等最后一次写盘落地(命令里要确认文件真的写了) */
|
|
129
|
+
settled(): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createBaselineGate(deps: BaselineGateDeps): BaselineGate {
|
|
133
|
+
// 默认只落盘:基线文件就在 `.pi/evolution/` 下,日志走同一个目录的 logs/extension.log
|
|
134
|
+
const log = deps.log ?? ((msg: string) => logLine(path.dirname(deps.file), msg));
|
|
135
|
+
let cached: Baseline | null = null;
|
|
136
|
+
let pending: Promise<void> = Promise.resolve();
|
|
137
|
+
|
|
138
|
+
const persist = (baseline: Baseline): void => {
|
|
139
|
+
cached = baseline;
|
|
140
|
+
pending = saveBaseline(deps.file, baseline, deps.queue).catch((error: unknown) => {
|
|
141
|
+
log(`[skill-evolve] 存量基线写盘失败:${String(error)}`);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const baseline = (): Baseline => {
|
|
146
|
+
if (cached) return cached;
|
|
147
|
+
const onDisk = readBaseline(deps.file, log);
|
|
148
|
+
if (onDisk) {
|
|
149
|
+
cached = onDisk;
|
|
150
|
+
return cached;
|
|
151
|
+
}
|
|
152
|
+
// 首次审计:把此刻扫到的全部命中当存量接受,否则第一次运行就是一屏红
|
|
153
|
+
const created = buildBaseline(deps.hits(), deps.now(), true);
|
|
154
|
+
log(`[skill-evolve] 首次审计自动生成存量基线:${created.violations.length} 条(${deps.file})`);
|
|
155
|
+
persist(created);
|
|
156
|
+
return created;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
baseline,
|
|
161
|
+
isStock: (hit) => fingerprintsOf(baseline()).has(fingerprintOf(hit)),
|
|
162
|
+
acceptAll: async () => {
|
|
163
|
+
const accepted = buildBaseline(deps.hits(), deps.now(), false);
|
|
164
|
+
persist(accepted);
|
|
165
|
+
await pending;
|
|
166
|
+
return `已接受 ${accepted.violations.length} 条存量 P0:${deps.file}`;
|
|
167
|
+
},
|
|
168
|
+
reset: async () => {
|
|
169
|
+
const empty = buildBaseline([], deps.now(), false);
|
|
170
|
+
persist(empty);
|
|
171
|
+
await pending;
|
|
172
|
+
return `已清空存量基线:${deps.file}(下次审计把所有命中都算新增)`;
|
|
173
|
+
},
|
|
174
|
+
settled: () => pending,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 子进程门禁(设计稿 4.2 门禁⓪):任一环境变量在场就说明当前不是用户主会话。
|
|
3
|
+
*
|
|
4
|
+
* 单独成文件的原因:evolution.ts 要守 300 行;这里只是一个常量加一个判据,无副作用。
|
|
5
|
+
*/
|
|
6
|
+
const CHILD_ENV_KEYS = ["SKILL_EVOLVE_CHILD", "PI_SUBAGENT_CHILD", "PI_AGENT_RUNNER", "PI_ASYNC_NATIVE_RUNNER"];
|
|
7
|
+
|
|
8
|
+
export function isChildProcess(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
9
|
+
return CHILD_ENV_KEYS.some((key) => Boolean(env[key]));
|
|
10
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 13.1 三级权限的代码判定:只看 diff 真改了什么,不看 LLM 写的 action 字段(它只作参考,冲突以本文件为准)。
|
|
3
|
+
*
|
|
4
|
+
* forbidden = 立即拒绝(删技能、改 frontmatter 语义字段、动插件自身、路径越界);
|
|
5
|
+
* auto = settle 时自动应用(只动 description 文案、新建 .cursor 路由 stub、AGENTS.md 索引表增删一行);
|
|
6
|
+
* approval = 其余全部(正文条款、新增技能、合并),必须 `/skill-evolve approve <runId>`。
|
|
7
|
+
*
|
|
8
|
+
* 为什么按「行」判而不是按意图判:模型可以声称自己只改了 description,代码只认它真改的行。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type ActionLevel = "auto" | "approval" | "forbidden";
|
|
12
|
+
|
|
13
|
+
export interface ClassifyInput {
|
|
14
|
+
diffText: string;
|
|
15
|
+
/** improve / create / merge / prune */
|
|
16
|
+
proposalType: string;
|
|
17
|
+
skill: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ClassifyResult {
|
|
21
|
+
level: ActionLevel;
|
|
22
|
+
/** 判定依据(人话,进 06-verify.json 与告警) */
|
|
23
|
+
reasons: string[];
|
|
24
|
+
/** diff 里出现的所有目标路径(相对仓库根) */
|
|
25
|
+
targets: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DiffSection {
|
|
29
|
+
path: string;
|
|
30
|
+
created: boolean;
|
|
31
|
+
deleted: boolean;
|
|
32
|
+
lines: { sign: "+" | "-" | " "; text: string }[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 允许被采纳路径的白名单:路径本身就是授权边界,白名单外一律 forbidden */
|
|
36
|
+
const ALLOWED_PATH = /^(?:\.pi\/skills\/[^/]+\/SKILL\.md|\.cursor\/skills\/[^/]+\/SKILL\.md|(?:\.pi\/)?AGENTS\.md)$/;
|
|
37
|
+
const SKILL_FILE = /^(?:\.pi|\.cursor)\/skills\/[^/]+\/SKILL\.md$/;
|
|
38
|
+
const CURSOR_STUB = /^\.cursor\/skills\/[^/]+\/SKILL\.md$/;
|
|
39
|
+
const AGENTS_FILE = /(?:^|\/)AGENTS\.md$/;
|
|
40
|
+
const PROTECTED_PATH = ".pi/extensions/skill-evolve";
|
|
41
|
+
/** 换掉这三个字段就是换技能语义(名字 / 工具面 / 是否进 prompt),不许自动采纳 */
|
|
42
|
+
const FORBIDDEN_FIELD = /^(name|allowed-tools|disable-model-invocation):/;
|
|
43
|
+
const DESCRIPTION_START = /^description:/;
|
|
44
|
+
/** AGENTS.md 的 Skill 索引表行:`- \`<名>\` —— <路径>` */
|
|
45
|
+
const INDEX_ROW = /^\s*[-*]\s+`[^`]+`\s/;
|
|
46
|
+
|
|
47
|
+
function normalizePath(raw: string): string | null {
|
|
48
|
+
const head = raw.split("\t")[0].trim();
|
|
49
|
+
if (head === "/dev/null" || head.length === 0) return null;
|
|
50
|
+
return head.replace(/^[ab]\//, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 按 `+++` 切开文件段:多文件 diff(技能文件 + .cursor stub + AGENTS.md)必须逐段判 */
|
|
54
|
+
export function parseSections(diffText: string): DiffSection[] {
|
|
55
|
+
const sections: DiffSection[] = [];
|
|
56
|
+
let current: DiffSection | null = null;
|
|
57
|
+
let oldPath: string | null = null;
|
|
58
|
+
let oldIsNull = false;
|
|
59
|
+
for (const raw of diffText.split(/\r?\n/)) {
|
|
60
|
+
if (raw.startsWith("--- ")) {
|
|
61
|
+
oldPath = normalizePath(raw.slice(4));
|
|
62
|
+
oldIsNull = oldPath === null;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (raw.startsWith("+++ ")) {
|
|
66
|
+
const newPath = normalizePath(raw.slice(4));
|
|
67
|
+
current = {
|
|
68
|
+
path: newPath ?? oldPath ?? "",
|
|
69
|
+
created: oldIsNull,
|
|
70
|
+
deleted: newPath === null,
|
|
71
|
+
lines: [],
|
|
72
|
+
};
|
|
73
|
+
sections.push(current);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!current || raw.length === 0) continue;
|
|
77
|
+
if (raw.startsWith("@@") || raw.startsWith("\\ ")) continue;
|
|
78
|
+
if (!raw.startsWith("+") && !raw.startsWith("-") && !raw.startsWith(" ")) continue;
|
|
79
|
+
current.lines.push({ sign: raw[0] as "+" | "-" | " ", text: raw.slice(1) });
|
|
80
|
+
}
|
|
81
|
+
return sections;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 标出每个改动行是否属于 description(含 YAML 续行:缩进 ≥2 且上一行在 description 里)。
|
|
86
|
+
* 只有这类行的改动才够格走 auto —— 它改的是「什么时候该用我」的措辞,不是行为约束。
|
|
87
|
+
*/
|
|
88
|
+
function descriptionFlags(section: DiffSection): boolean[] {
|
|
89
|
+
const flags: boolean[] = [];
|
|
90
|
+
let inDescription = false;
|
|
91
|
+
for (const line of section.lines) {
|
|
92
|
+
if (DESCRIPTION_START.test(line.text)) inDescription = true;
|
|
93
|
+
else if (line.text.trim().length === 0) {
|
|
94
|
+
// 空行不打断:folded / literal 块里允许空行
|
|
95
|
+
} else if (!/^\s{2,}\S/.test(line.text) || !inDescription) inDescription = false;
|
|
96
|
+
flags.push(inDescription);
|
|
97
|
+
}
|
|
98
|
+
return flags;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function classifyChange(input: ClassifyInput): ClassifyResult {
|
|
102
|
+
const sections = parseSections(input.diffText);
|
|
103
|
+
const targets = sections.map((section) => section.path).filter((item) => item.length > 0);
|
|
104
|
+
const deny = (why: string): ClassifyResult => ({ level: "forbidden", reasons: [why], targets });
|
|
105
|
+
|
|
106
|
+
if (sections.length === 0) return { level: "approval", reasons: ["diff 里没有可判定的文件段"], targets };
|
|
107
|
+
if (input.proposalType === "prune") return deny("prune(删技能)必须人批:插件只出待执行清单,不真删");
|
|
108
|
+
|
|
109
|
+
for (const section of sections) {
|
|
110
|
+
if (section.deleted) return deny(`删除文件:${section.path || "(未知路径)"}`);
|
|
111
|
+
if (section.path.includes(PROTECTED_PATH)) return deny(`改动插件自身:${section.path}`);
|
|
112
|
+
if (!ALLOWED_PATH.test(section.path)) return deny(`路径越界:${section.path || "(未知路径)"}`);
|
|
113
|
+
for (const line of section.lines) {
|
|
114
|
+
// 只看增删行:上下文行里出现 `name:` 不是「改动字段」,否则任何挨着 frontmatter 的提案都会被误判 forbidden
|
|
115
|
+
if (line.sign === " " || line.text.trim().length === 0) continue;
|
|
116
|
+
if (FORBIDDEN_FIELD.test(line.text.trim())) return deny(`改动 frontmatter 字段:${line.text.trim()}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const changes = sections.flatMap((section) => section.lines.filter((line) => line.sign !== " "));
|
|
121
|
+
if (changes.length === 0) return { level: "approval", reasons: ["没有任何增删行"], targets };
|
|
122
|
+
|
|
123
|
+
// 逐段判「这一段自身是否够格 auto」:多文件提案必须每一段都够格,才能整条自动采纳
|
|
124
|
+
const sectionAuto = (section: DiffSection): boolean => {
|
|
125
|
+
// 新建 .cursor 路由 stub:内容天然只是指回 SSOT,没有行为约束
|
|
126
|
+
if (CURSOR_STUB.test(section.path)) return section.created;
|
|
127
|
+
// AGENTS.md:只认索引表行,且全文件合计只能一行(见下面的计数)
|
|
128
|
+
if (AGENTS_FILE.test(section.path)) {
|
|
129
|
+
return section.lines.every((line) => line.sign === " " || line.text.trim().length === 0 || INDEX_ROW.test(line.text));
|
|
130
|
+
}
|
|
131
|
+
if (!SKILL_FILE.test(section.path)) return false;
|
|
132
|
+
const flags = descriptionFlags(section);
|
|
133
|
+
return section.lines.every((line, index) => line.sign === " " || line.text.trim().length === 0 || flags[index]);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!sections.every(sectionAuto)) {
|
|
137
|
+
// 说清是哪一段不够格,方便人一眼看出为什么没自动采纳
|
|
138
|
+
const offender = sections.find((section) => !sectionAuto(section));
|
|
139
|
+
return {
|
|
140
|
+
level: "approval",
|
|
141
|
+
reasons: [`改动落在正文条款 / 新增技能 / 合并(${offender?.path ?? "未知路径"})`],
|
|
142
|
+
targets,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// AGENTS.md 索引表只增/删一行:多行就是结构性改动
|
|
147
|
+
const agentsChanges = sections
|
|
148
|
+
.filter((section) => AGENTS_FILE.test(section.path))
|
|
149
|
+
.flatMap((section) => section.lines.filter((line) => line.sign !== " " && line.text.trim().length > 0));
|
|
150
|
+
if (agentsChanges.length > 1) return { level: "approval", reasons: ["AGENTS.md 改动超过一行索引"], targets };
|
|
151
|
+
if (agentsChanges.length === 1) return { level: "auto", reasons: ["AGENTS.md 索引表增删一行"], targets };
|
|
152
|
+
|
|
153
|
+
const cursorOnly = sections.every((section) => CURSOR_STUB.test(section.path));
|
|
154
|
+
if (cursorOnly) return { level: "auto", reasons: ["新建 .cursor 路由 stub"], targets };
|
|
155
|
+
return { level: "auto", reasons: ["只改 description(含续行)与 .cursor stub"], targets };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 只看级别的旧调用面(评审督导写 03-review.json 的 action 用) */
|
|
159
|
+
export function classifyAction(diffText: string, proposalType: string): ActionLevel {
|
|
160
|
+
return classifyChange({ diffText, proposalType, skill: "" }).level;
|
|
161
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/skill-evolve` 的子命令路由与补全:只产出展示文本,方便单测。
|
|
3
|
+
*/
|
|
4
|
+
import { runConfigCommand } from "./config-command.ts";
|
|
5
|
+
import type { ConfigFlag } from "./config.ts";
|
|
6
|
+
import type { Evolution } from "./evolution.ts";
|
|
7
|
+
import { COMMAND_FLAG, FLAG_EFFECT, FLAG_LABEL } from "./flags.ts";
|
|
8
|
+
import type { WebLaunchRequest } from "./web/autostart.ts";
|
|
9
|
+
|
|
10
|
+
export interface SubcommandItem {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
description: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SUBCOMMANDS: readonly SubcommandItem[] = [
|
|
17
|
+
{ value: "status", label: "status", description: "开关、去抖、锁、最近 run" },
|
|
18
|
+
{ value: "report", label: "report", description: "近 7 天曝光/加载/未命中;--week 出周报(含零加载候选)" },
|
|
19
|
+
{ value: "run", label: "run", description: "立即派发一次复盘(跳过去抖与上限,仍守全局锁)" },
|
|
20
|
+
{ value: "baseline", label: "baseline", description: "把当前 P0 命中全部接受为存量(--reset 清空清单)" },
|
|
21
|
+
{ value: "selftest", label: "selftest", description: "跑三个坏样本:评审器必须 reject/reject/revise,否则流水线停用" },
|
|
22
|
+
{ value: "approve", label: "approve", description: "采纳某条 run(走五道闸);--prune 只生成删技能清单,不真删" },
|
|
23
|
+
{ value: "queue", label: "queue", description: "列采纳队列:pending / applied / rolled-back / failed / superseded" },
|
|
24
|
+
{ value: "web", label: "web", description: "启动本机 Web 面板(127.0.0.1:7788,--port N 换端口,--once 不写进配置)" },
|
|
25
|
+
{ value: "config", label: "config", description: "选提案/评审模型(inherit = 跟随主会话)" },
|
|
26
|
+
{ value: "collect", label: "collect", description: "只开/关遥测采集(on|off;只写本地日志,不花 token)" },
|
|
27
|
+
{ value: "review", label: "review", description: "只开/关自动复盘(on|off;会派生子进程,花 token)" },
|
|
28
|
+
{ value: "on", label: "on", description: "采集与派生一起打开" },
|
|
29
|
+
{ value: "off", label: "off", description: "采集与派生一起关闭" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** 补全:前缀命中就只给命中项;一个都没命中时给全量,避免用户看到空列表 */
|
|
33
|
+
export function matchSubcommand(prefix: string): SubcommandItem[] {
|
|
34
|
+
const head = prefix.trim();
|
|
35
|
+
const hit = SUBCOMMANDS.filter((item) => item.value.startsWith(head));
|
|
36
|
+
return hit.length > 0 ? [...hit] : [...SUBCOMMANDS];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 命令需要知道「当前会话」才能手动派发,所以会话标识由调用方(index.ts)传入 */
|
|
40
|
+
export interface CommandTarget {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
sessionFile?: string;
|
|
43
|
+
/**
|
|
44
|
+
* `/skill-evolve web` 的启动器:面板必须由扩展持有单例并在 session_shutdown 关掉,
|
|
45
|
+
* 所以这里只注入一个回调,参数解析与文案留在本文件(可单测)。
|
|
46
|
+
*/
|
|
47
|
+
web?: (request: WebLaunchRequest) => Promise<string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `web [--port N] [N] [--once]`:`--once` 表示只本次会话起、不写回配置 */
|
|
51
|
+
export function parseWebArgs(args: readonly string[]): WebLaunchRequest {
|
|
52
|
+
const once = args.includes("--once");
|
|
53
|
+
// `--port 7799` 与 `web 7799` 两种写法都认;`--port` / `--once` 本身不能当端口解析
|
|
54
|
+
const port = parseWebPort(args.find((item) => item !== "--port" && item !== "--once"));
|
|
55
|
+
return port === undefined ? { once } : { once, port };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `web [--port N]` / `web [N]`:只认 1~65535 的整数,其余返回 undefined 用默认端口 */
|
|
59
|
+
export function parseWebPort(arg: string | undefined): number | undefined {
|
|
60
|
+
if (!arg) return undefined;
|
|
61
|
+
const value = Number.parseInt(arg, 10);
|
|
62
|
+
return Number.isInteger(value) && value > 0 && value < 65536 ? value : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runCommand(args: string, evo: Evolution, target: CommandTarget): Promise<string> {
|
|
66
|
+
const parts = args.trim().split(/\s+/).filter((part) => part.length > 0);
|
|
67
|
+
const [name, arg] = parts;
|
|
68
|
+
const command = name && name.length > 0 ? name : "status";
|
|
69
|
+
if (command === "report") {
|
|
70
|
+
// `--week` 是 M6 的周报:加载率 / 归因分布 / 冲突密度 / 采纳率 / 回滚率 / 进化收益 + 零加载候选
|
|
71
|
+
if (arg === "--week") return evo.observe.weekReportText();
|
|
72
|
+
const days = Number.parseInt(arg ?? "", 10);
|
|
73
|
+
return evo.reportText(Number.isFinite(days) && days > 0 ? days : 7);
|
|
74
|
+
}
|
|
75
|
+
if (command === "run") return evo.runNow(target);
|
|
76
|
+
if (command === "baseline") return evo.acceptBaseline(arg === "--reset" ? "reset" : "accept");
|
|
77
|
+
if (command === "selftest") return evo.model.selftestText();
|
|
78
|
+
// 非 TUI 兜底:真正能交互选模型的路径在 index.ts(那里才拿得到 ctx.ui.select)
|
|
79
|
+
if (command === "config") return runConfigCommand(null, evo.model, arg);
|
|
80
|
+
if (command === "on" || command === "off") return evo.setEnabled(command === "on");
|
|
81
|
+
// `collect on|off` 只动 telemetry、`review on|off` 只动 enabled:缺参数或写错就给用法,一概不改配置
|
|
82
|
+
if (command === "collect" || command === "review") {
|
|
83
|
+
const field: ConfigFlag = COMMAND_FLAG[command];
|
|
84
|
+
const value = arg === "on" ? true : arg === "off" ? false : null;
|
|
85
|
+
if (value === null) {
|
|
86
|
+
return `用法:/skill-evolve ${command} on|off —— 只改「${FLAG_LABEL[field]}」(${FLAG_EFFECT[field]}),配置未改动`;
|
|
87
|
+
}
|
|
88
|
+
return evo.setFlag(field, value);
|
|
89
|
+
}
|
|
90
|
+
if (command === "queue") return evo.apply.queueText();
|
|
91
|
+
if (command === "web") {
|
|
92
|
+
if (!target.web) return "用法:/skill-evolve web [--port N] [--once]\n当前环境没有注入面板启动器(只在 pi 会话里可用)";
|
|
93
|
+
return target.web(parseWebArgs(parts.slice(1)));
|
|
94
|
+
}
|
|
95
|
+
// approve 可能带第二个参数(`--prune`),所以这里直接用整串剩余部分
|
|
96
|
+
if (command === "approve") return evo.apply.approve(arg ?? "", parts.includes("--prune"));
|
|
97
|
+
if (command === "status") return evo.statusText();
|
|
98
|
+
const usage = SUBCOMMANDS.map((item) => item.value).join("|");
|
|
99
|
+
return `未知子命令:${command}\n用法:/skill-evolve [${usage}]`;
|
|
100
|
+
}
|