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,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可观测层(设计稿 14 章)的胶水:面板刷新 + 状态变迁转 `/tree` 条目 + 周报与指标落盘。
|
|
3
|
+
*
|
|
4
|
+
* 与采集/门禁隔开的原因:这里只读盘、算数、出文本,既不写技能文件也不派发进程,
|
|
5
|
+
* 单测与 evolution.test.ts 都能直接喂假目录。
|
|
6
|
+
*/
|
|
7
|
+
import { appendMetrics, buildMetrics, type MetricsRun } from "./metrics.ts";
|
|
8
|
+
import { collectPanelState, statusEntries, type PanelEntry, type PanelState } from "./panel.ts";
|
|
9
|
+
import { listRuns } from "./runs.ts";
|
|
10
|
+
import { scanStaleRuns } from "./stale.ts";
|
|
11
|
+
import { readEvents, type MutationQueue } from "./telemetry.ts";
|
|
12
|
+
import { createWeeklyReport } from "./weekly.ts";
|
|
13
|
+
|
|
14
|
+
/** 面板渲染交给 index.ts(widget / status / appendEntry 都是 pi 的能力);这里只往外抛数据 */
|
|
15
|
+
export interface PanelDeps {
|
|
16
|
+
onEntry?: (entry: PanelEntry) => void;
|
|
17
|
+
onState?: (state: PanelState) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ObserverDeps {
|
|
21
|
+
cwd: string;
|
|
22
|
+
evolutionDir: string;
|
|
23
|
+
telemetryDir: string;
|
|
24
|
+
queue: MutationQueue;
|
|
25
|
+
now: () => number;
|
|
26
|
+
/** 单 run 上限(来自配置):pid 还活着但跑超过它也算卡死 */
|
|
27
|
+
maxRuntimeMs?: number;
|
|
28
|
+
panel?: PanelDeps;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Observer {
|
|
32
|
+
/** settle / session_start / approve 之后调:顺带把新出现的状态变迁发成条目 */
|
|
33
|
+
refresh(): PanelState;
|
|
34
|
+
/** 派发成功时立刻记一条(此时 run.json 刚写,下一轮 refresh 才能看到) */
|
|
35
|
+
noteDispatched(runId: string): void;
|
|
36
|
+
/** `/skill-evolve report --week` */
|
|
37
|
+
weekReportText(): Promise<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const METRICS_WINDOW_DAYS = 28;
|
|
41
|
+
|
|
42
|
+
export function createObserver(deps: ObserverDeps): Observer {
|
|
43
|
+
const panelDeps = deps.panel ?? {};
|
|
44
|
+
// 进程内记忆「上次见到的状态」:一条状态只发一次条目,不因反复刷新重复刷屏
|
|
45
|
+
const seen = new Map<string, string>();
|
|
46
|
+
|
|
47
|
+
const refresh: Observer["refresh"] = () => {
|
|
48
|
+
const now = deps.now();
|
|
49
|
+
// 先扫盘再算数:子进程已死的 run 在这里落成 stale,否则面板会永远显示「复盘 N」
|
|
50
|
+
const limit = deps.maxRuntimeMs === undefined ? {} : { maxRuntimeMs: deps.maxRuntimeMs };
|
|
51
|
+
scanStaleRuns(deps.evolutionDir, now, limit);
|
|
52
|
+
const state = collectPanelState({ evolutionDir: deps.evolutionDir, now, ...limit });
|
|
53
|
+
for (const entry of statusEntries(listRuns(deps.evolutionDir, 20), seen, now)) panelDeps.onEntry?.(entry);
|
|
54
|
+
panelDeps.onState?.(state);
|
|
55
|
+
return state;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const noteDispatched: Observer["noteDispatched"] = (runId) => {
|
|
59
|
+
seen.set(runId, "dispatched");
|
|
60
|
+
panelDeps.onEntry?.({ kind: "dispatched", runId, at: deps.now() });
|
|
61
|
+
refresh();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** 指标落盘:按周聚合后逐行 append,同 week 已有就跳过(appendMetrics 内部去重) */
|
|
65
|
+
const weekReportText: Observer["weekReportText"] = async () => {
|
|
66
|
+
const now = deps.now();
|
|
67
|
+
const events = readEvents(deps.telemetryDir, METRICS_WINDOW_DAYS, now);
|
|
68
|
+
const runs: MetricsRun[] = listRuns(deps.evolutionDir, 50).map((run) => ({
|
|
69
|
+
runId: run.runId,
|
|
70
|
+
status: run.status,
|
|
71
|
+
dispatchedAt: run.dispatchedAt,
|
|
72
|
+
}));
|
|
73
|
+
const report = createWeeklyReport({ cwd: deps.cwd, evolutionDir: deps.evolutionDir, telemetryDir: deps.telemetryDir, now: deps.now });
|
|
74
|
+
const text = report.weekText();
|
|
75
|
+
for (const row of buildMetrics(events, now, runs)) {
|
|
76
|
+
await appendMetrics(deps.evolutionDir, row, deps.queue);
|
|
77
|
+
}
|
|
78
|
+
return text;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return { refresh, noteDispatched, weekReportText };
|
|
82
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/skill-evolve status|report` 与 `/skill-audit` 的读盘 + 渲染(从 evolution.ts 拆出)。
|
|
3
|
+
*
|
|
4
|
+
* 拆出来的原因很实际:evolution.ts 要守 300 行闸门,而这一组只做「读状态 → 出文本」,
|
|
5
|
+
* 与采集/门禁/派发没有任何耦合,放一起反而让门禁逻辑难读。
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import { auditText as renderAuditText, buildAuditModel, activeToolsFromEvents, exposedFromEvents } from "./audit.ts";
|
|
9
|
+
import { fingerprintsOf, type BaselineGate } from "./baseline.ts";
|
|
10
|
+
import { configFile, type EvolutionConfig } from "./config.ts";
|
|
11
|
+
import type { ModelTools } from "./evolution-model.ts";
|
|
12
|
+
import { lockDir, readOwner } from "./lock.ts";
|
|
13
|
+
import type { Mechanical } from "./mechanical.ts";
|
|
14
|
+
import type { MechanicalReporter } from "./mechanical-report.ts";
|
|
15
|
+
import { renderReport, renderStatus } from "./report.ts";
|
|
16
|
+
import { listRuns } from "./runs.ts";
|
|
17
|
+
import { listStaleRuns, scanStaleRuns } from "./stale.ts";
|
|
18
|
+
import { readEvents, summarize } from "./telemetry.ts";
|
|
19
|
+
import { isAlive, readWebPid } from "./web/panel-pid.ts";
|
|
20
|
+
|
|
21
|
+
export interface ViewsDeps {
|
|
22
|
+
evolutionDir: string;
|
|
23
|
+
telemetryDir: string;
|
|
24
|
+
config: EvolutionConfig;
|
|
25
|
+
now: () => number;
|
|
26
|
+
mechanical: Mechanical;
|
|
27
|
+
mechanicalReporter: MechanicalReporter;
|
|
28
|
+
baselineGate: BaselineGate;
|
|
29
|
+
modelTools: ModelTools;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Views {
|
|
33
|
+
statusText(): Promise<string>;
|
|
34
|
+
reportText(days?: number): string;
|
|
35
|
+
auditText(days?: number): Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createViews(deps: ViewsDeps): Views {
|
|
39
|
+
const statusText = async (): Promise<string> => {
|
|
40
|
+
const dir = lockDir(deps.evolutionDir);
|
|
41
|
+
const held = fs.existsSync(dir);
|
|
42
|
+
const owner = held ? await readOwner(dir) : null;
|
|
43
|
+
// 先扫一遍再报:子进程死掉的 run 要在这里被标成 stale,否则 status 会永远漏报失败
|
|
44
|
+
scanStaleRuns(deps.evolutionDir, deps.now(), { maxRuntimeMs: deps.config.maxRuntimeMs });
|
|
45
|
+
// Web 面板记录只在 pid 还活着时显示:残记录(kill -9 之后)不该出现在状态里
|
|
46
|
+
const web = readWebPid(deps.evolutionDir);
|
|
47
|
+
return renderStatus({
|
|
48
|
+
config: deps.config,
|
|
49
|
+
configPath: configFile(deps.evolutionDir),
|
|
50
|
+
telemetryDir: deps.telemetryDir,
|
|
51
|
+
lockPid: held ? (owner?.pid ?? null) : undefined,
|
|
52
|
+
webPanel: web && isAlive(web.pid) ? { url: web.url, pid: web.pid } : null,
|
|
53
|
+
runs: listRuns(deps.evolutionDir, 5),
|
|
54
|
+
staleRuns: listStaleRuns(deps.evolutionDir, 5),
|
|
55
|
+
selftest: deps.modelTools.lastSelftest(),
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const reportText = (days = 7): string =>
|
|
60
|
+
renderReport({
|
|
61
|
+
days,
|
|
62
|
+
summary: summarize(readEvents(deps.telemetryDir, days, deps.now())),
|
|
63
|
+
runs: listRuns(deps.evolutionDir, 10),
|
|
64
|
+
selftest: deps.modelTools.lastSelftest(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** 机械审计:类别汇总来自遥测 attributed;历史 missed 用当前快照现算(可能是旧数据,标注清楚) */
|
|
68
|
+
const auditText = async (days = 7): Promise<string> => {
|
|
69
|
+
const events = readEvents(deps.telemetryDir, days, deps.now());
|
|
70
|
+
deps.mechanicalReporter.noteFailure();
|
|
71
|
+
const { hits, invalid } = deps.mechanical.conflicts();
|
|
72
|
+
// 先取(缺文件时在这里 bootstrap),再等写盘落地:顺序反了文件可能还在写就返回了
|
|
73
|
+
const knownFingerprints = fingerprintsOf(deps.baselineGate.baseline());
|
|
74
|
+
await deps.baselineGate.settled();
|
|
75
|
+
const exposed = exposedFromEvents(events);
|
|
76
|
+
const activeTools = activeToolsFromEvents(events);
|
|
77
|
+
const model = buildAuditModel({
|
|
78
|
+
days,
|
|
79
|
+
events,
|
|
80
|
+
conflicts: hits,
|
|
81
|
+
knownFingerprints,
|
|
82
|
+
invalidRules: invalid,
|
|
83
|
+
classify: (skill) => deps.mechanical.attribute({ missed: [skill], exposed, activeTools })[0].category,
|
|
84
|
+
});
|
|
85
|
+
return renderAuditText(model);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return { statusText, reportText, auditText };
|
|
89
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主会话里的采集与门禁:曝光 / 加载 / 未命中 / 去抖派生 + 命令文本。
|
|
3
|
+
*
|
|
4
|
+
* 这一层刻意不 import pi 的任何东西:事件字段由 index.ts 拆成原始值再传进来,
|
|
5
|
+
* 这样门禁逻辑能被 node --test 直接跑,不依赖 `@earendil-works/pi-coding-agent` 的可解析性。
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { configFile, loadConfig, saveConfigSwitch, saveFlag, type ConfigFlag, type EvolutionConfig } from "./config.ts";
|
|
9
|
+
import type { Attribution } from "./attribution.ts";
|
|
10
|
+
import { baselinePath, createBaselineGate } from "./baseline.ts";
|
|
11
|
+
import { isChildProcess } from "./child-env.ts";
|
|
12
|
+
import type { ApplyDeps } from "./apply.ts";
|
|
13
|
+
import { createDispatcher } from "./dispatch.ts";
|
|
14
|
+
import { createApplyTools, type ApplyTools } from "./evolution-apply.ts";
|
|
15
|
+
import { createModelTools, type ModelTools } from "./evolution-model.ts";
|
|
16
|
+
import { createObserver, type Observer, type PanelDeps } from "./evolution-observe.ts";
|
|
17
|
+
import { createViews } from "./evolution-views.ts";
|
|
18
|
+
import { flagStateText, FLAG_LABEL } from "./flags.ts";
|
|
19
|
+
import type { GitRunner } from "./gate0.ts";
|
|
20
|
+
import { logLine } from "./logfile.ts";
|
|
21
|
+
import { createMechanicalReporter } from "./mechanical-report.ts";
|
|
22
|
+
import { createMechanical, type RawDiagnostic, type SnapshotSkill } from "./mechanical.ts";
|
|
23
|
+
import { createCommitRunner, createGitRunner, createMdCheckRunner } from "./proc.ts";
|
|
24
|
+
import { renderManualDispatch } from "./report.ts";
|
|
25
|
+
import type { SupervisorSpawn } from "./review-spawn.ts";
|
|
26
|
+
import type { QueueItem } from "./queue.ts";
|
|
27
|
+
import { createRoundTracker } from "./round.ts";
|
|
28
|
+
import type { SpawnLike } from "./spawn-plan.ts";
|
|
29
|
+
import { createTelemetry, type MutationQueue, type TelemetrySkill } from "./telemetry.ts";
|
|
30
|
+
|
|
31
|
+
/** 子进程门禁在这里再导出一次:历史调用方与测试都从 evolution 取它 */
|
|
32
|
+
export { isChildProcess };
|
|
33
|
+
|
|
34
|
+
export interface EvolutionDeps {
|
|
35
|
+
cwd: string;
|
|
36
|
+
promptTemplatePath: string;
|
|
37
|
+
queue: MutationQueue;
|
|
38
|
+
log?: (msg: string) => void;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
env?: NodeJS.ProcessEnv;
|
|
41
|
+
spawnFn?: SpawnLike;
|
|
42
|
+
/** 生产注入:() => loadSkillsFromDir({ dir: join(cwd, ".pi/skills"), source: "project" }) */
|
|
43
|
+
loadSkills?: () => { skills: SnapshotSkill[]; diagnostics: RawDiagnostic[] };
|
|
44
|
+
/** 默认 .pi/evolution/rules/hard-rules.json */
|
|
45
|
+
rulesFile?: string;
|
|
46
|
+
/** 主会话当前模型:模型配置写 inherit 时用它解析评审模型 */
|
|
47
|
+
hostModel?: () => { provider?: string; id?: string } | undefined;
|
|
48
|
+
/** 评审督导的 spawn(默认真起 node 进程;单测注入假实现,避免留 900s 的后台进程) */
|
|
49
|
+
supervisorSpawnFn?: SupervisorSpawn;
|
|
50
|
+
/** 闸0/闸3 的 git 摘要(单测注入假实现:真实 git 与本扩展无关) */
|
|
51
|
+
git?: GitRunner;
|
|
52
|
+
/** 闸3 的 md-check 子进程(单测注入假实现) */
|
|
53
|
+
runMdCheck?: (absPath: string) => { code: number; output: string };
|
|
54
|
+
/** auto 级成功后的单独 commit(单测注入假实现;不注入则用真 git) */
|
|
55
|
+
commit?: ApplyDeps["commit"];
|
|
56
|
+
/** 面板回调:widget / status / appendEntry 是 pi 的能力,由 index.ts 提供 */
|
|
57
|
+
panel?: PanelDeps;
|
|
58
|
+
/** pid 存活判定(全局单飞闸用;单测注入,测试里不起真子进程) */
|
|
59
|
+
alive?: (pid: number) => boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Evolution {
|
|
63
|
+
recordAdvertised(input: {
|
|
64
|
+
sessionId: string;
|
|
65
|
+
cwd: string;
|
|
66
|
+
prompt: string;
|
|
67
|
+
skills: TelemetrySkill[];
|
|
68
|
+
/** 本轮声明的工具名(pi.getActiveTools()):类 E 判定的唯一依据 */
|
|
69
|
+
activeTools?: string[];
|
|
70
|
+
}): void;
|
|
71
|
+
noteToolCall(input: { sessionId: string; toolCallId: string; toolName: string; input: Record<string, unknown> }): void;
|
|
72
|
+
resolveToolResult(input: { sessionId: string; toolCallId: string; isError: boolean }): void;
|
|
73
|
+
settle(input: { sessionId: string; sessionFile?: string }): void;
|
|
74
|
+
recordHandlerError(where: string, error: unknown): void;
|
|
75
|
+
/** 手动兜底:立即派发一次(跳过去抖与上限,仍守全局锁) */
|
|
76
|
+
runNow(input: { sessionId: string; sessionFile?: string }): Promise<string>;
|
|
77
|
+
/** 退出前把还没派发的轮次补发一次;同步完成,不抛 */
|
|
78
|
+
shutdown(): void;
|
|
79
|
+
flush(): Promise<void>;
|
|
80
|
+
statusText(): Promise<string>;
|
|
81
|
+
reportText(days?: number): string;
|
|
82
|
+
/** 机械审计:类别汇总 + 硬禁令两段(存量 / 新增)+ 加载率注解 */
|
|
83
|
+
auditText(days?: number): Promise<string>;
|
|
84
|
+
/** `/skill-evolve baseline`:把当前命中全部接受为存量;reset 清空清单 */
|
|
85
|
+
acceptBaseline(mode: "accept" | "reset"): Promise<string>;
|
|
86
|
+
setEnabled(enabled: boolean): Promise<string>;
|
|
87
|
+
/** `/skill-evolve collect|review` 与 Web 控制条:只切一个开关(另一个保持原值) */
|
|
88
|
+
setFlag(field: ConfigFlag, value: boolean): Promise<string>;
|
|
89
|
+
/** 模型配置 + 必红自检(`/skill-evolve config|selftest`) */
|
|
90
|
+
model: ModelTools;
|
|
91
|
+
/** 分级采纳:闸0 快照 / 队列扫描 / 自动应用 / approve 与 queue 命令 */
|
|
92
|
+
apply: ApplyTools;
|
|
93
|
+
/** M6:面板刷新(状态变迁转 /tree 条目)与周报 + 指标落盘 */
|
|
94
|
+
observe: Observer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createEvolution(deps: EvolutionDeps): Evolution {
|
|
98
|
+
const evolutionDir = path.join(deps.cwd, ".pi", "evolution");
|
|
99
|
+
// 默认只落盘(`.pi/evolution/logs/extension.log`):TUI 默认零推送,PI_SKILL_EVOLVE_DEBUG=1 才回显
|
|
100
|
+
const log = deps.log ?? ((msg: string) => logLine(evolutionDir, msg));
|
|
101
|
+
const env = deps.env ?? process.env;
|
|
102
|
+
const now = deps.now ?? (() => Date.now());
|
|
103
|
+
const telemetryDir = path.join(evolutionDir, "telemetry");
|
|
104
|
+
// 配置对象全生命周期只此一份:setEnabled 用 Object.assign 原地改,派发器看到的是同一份
|
|
105
|
+
const config: EvolutionConfig = loadConfig(evolutionDir, log);
|
|
106
|
+
// 传 getter 而不是快照:on/off 当场改 config 后,采集与写入立刻跟着变
|
|
107
|
+
const telemetry = createTelemetry({ dir: telemetryDir, queue: deps.queue, enabled: () => config.telemetry, log, now });
|
|
108
|
+
// 机械层:发现结果与硬禁令扫描都按进程缓存,第一次用到才干活(factory 里不读盘)
|
|
109
|
+
const mechanical = createMechanical({
|
|
110
|
+
cwd: deps.cwd,
|
|
111
|
+
rulesFile: deps.rulesFile ?? path.join(evolutionDir, "rules", "hard-rules.json"),
|
|
112
|
+
loadSkills: deps.loadSkills ?? (() => {
|
|
113
|
+
throw new Error("未注入 loadSkillsFromDir,机械归因降级");
|
|
114
|
+
}),
|
|
115
|
+
log,
|
|
116
|
+
});
|
|
117
|
+
const modelTools = createModelTools({
|
|
118
|
+
evolutionDir,
|
|
119
|
+
cwd: deps.cwd,
|
|
120
|
+
queue: deps.queue,
|
|
121
|
+
log,
|
|
122
|
+
now,
|
|
123
|
+
env,
|
|
124
|
+
...(deps.hostModel ? { hostModel: deps.hostModel } : {}),
|
|
125
|
+
});
|
|
126
|
+
const dispatcher = createDispatcher({
|
|
127
|
+
evolutionDir,
|
|
128
|
+
cwd: deps.cwd,
|
|
129
|
+
config,
|
|
130
|
+
telemetry,
|
|
131
|
+
queue: deps.queue,
|
|
132
|
+
promptTemplatePath: deps.promptTemplatePath,
|
|
133
|
+
log,
|
|
134
|
+
now,
|
|
135
|
+
env,
|
|
136
|
+
spawnFn: deps.spawnFn,
|
|
137
|
+
reviewModel: modelTools.reviewModel,
|
|
138
|
+
selftestFailed: modelTools.selftestFailed,
|
|
139
|
+
...(deps.alive ? { alive: deps.alive } : {}),
|
|
140
|
+
...(deps.supervisorSpawnFn ? { supervisorSpawnFn: deps.supervisorSpawnFn } : {}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const applyTools = createApplyTools({
|
|
144
|
+
evolutionDir,
|
|
145
|
+
cwd: deps.cwd,
|
|
146
|
+
queue: deps.queue,
|
|
147
|
+
log,
|
|
148
|
+
now,
|
|
149
|
+
git: deps.git ?? createGitRunner(),
|
|
150
|
+
// 闸3 的技能诊断直接复用 mechanical 的发现结果,避免第二次 readdir 出不一致的结论
|
|
151
|
+
loadDiagnostics: () => (deps.loadSkills ? deps.loadSkills().diagnostics : []),
|
|
152
|
+
runMdCheck: deps.runMdCheck ?? createMdCheckRunner(deps.cwd),
|
|
153
|
+
...(deps.commit ? { commit: deps.commit } : { commit: (item: QueueItem) => createCommitRunner(deps.cwd)(item) }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const observer = createObserver({
|
|
157
|
+
cwd: deps.cwd,
|
|
158
|
+
evolutionDir,
|
|
159
|
+
telemetryDir,
|
|
160
|
+
queue: deps.queue,
|
|
161
|
+
now,
|
|
162
|
+
maxRuntimeMs: config.maxRuntimeMs,
|
|
163
|
+
...(deps.panel ? { panel: deps.panel } : {}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/** 最近一轮的技能集 / 会话文件 / 归因:服务手动 `/skill-evolve run`,它可能发生在 settle 之后 */
|
|
167
|
+
const lastRound = new Map<string, { skills: string[]; sessionFile?: string; activeTools: string[]; attributions: Attribution[] }>();
|
|
168
|
+
// 存量债基线(设计稿 10.4):懒加载,首次审计时把当时全部命中当存量
|
|
169
|
+
const baselineGate = createBaselineGate({ file: baselinePath(evolutionDir), hits: () => mechanical.conflicts().hits, now, queue: deps.queue, log });
|
|
170
|
+
const mechanicalReporter = createMechanicalReporter({ mechanical, append: (event) => telemetry.append(event), log, stockOf: baselineGate.isStock });
|
|
171
|
+
// 一轮内的曝光/加载状态机(内存态):本层只关心结算结果与派发时机
|
|
172
|
+
const rounds = createRoundTracker({ cwd: deps.cwd, append: (event) => telemetry.append(event) });
|
|
173
|
+
|
|
174
|
+
const recordAdvertised: Evolution["recordAdvertised"] = (input) => {
|
|
175
|
+
const activeTools = input.activeTools ?? [];
|
|
176
|
+
rounds.recordAdvertised({ ...input, activeTools });
|
|
177
|
+
lastRound.set(input.sessionId, {
|
|
178
|
+
skills: input.skills.filter((skill) => !skill.disabled).map((skill) => skill.name),
|
|
179
|
+
sessionFile: lastRound.get(input.sessionId)?.sessionFile,
|
|
180
|
+
activeTools,
|
|
181
|
+
attributions: [],
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const noteToolCall: Evolution["noteToolCall"] = (input) => rounds.noteToolCall(input);
|
|
186
|
+
const resolveToolResult: Evolution["resolveToolResult"] = (input) => rounds.resolveToolResult(input);
|
|
187
|
+
|
|
188
|
+
const settle: Evolution["settle"] = (input) => {
|
|
189
|
+
// 评审督导是独立进程、可能刚写完 03-review.json:每轮都扫一次队列,auto 条自动落盘
|
|
190
|
+
if (!isChildProcess(env)) applyTools.settle();
|
|
191
|
+
const result = rounds.settle(input.sessionId);
|
|
192
|
+
if (!result) return;
|
|
193
|
+
|
|
194
|
+
// 机械归因(A~E)+ 硬禁令扫描:只记事实,判不动的标 semantic 交给复盘 agent 做 F/G
|
|
195
|
+
const attributions = mechanicalReporter.report({
|
|
196
|
+
sessionId: input.sessionId,
|
|
197
|
+
exposed: result.exposed,
|
|
198
|
+
missed: result.missed,
|
|
199
|
+
activeTools: result.activeTools,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (isChildProcess(env)) return; // 门禁⓪
|
|
203
|
+
lastRound.set(input.sessionId, {
|
|
204
|
+
skills: result.exposed.map((skill) => skill.name),
|
|
205
|
+
sessionFile: input.sessionFile,
|
|
206
|
+
activeTools: result.activeTools,
|
|
207
|
+
attributions,
|
|
208
|
+
});
|
|
209
|
+
// 门禁②③(全局锁 / 总开关)与去抖在派发器里;这里必须同步返回,不 await
|
|
210
|
+
void dispatcher
|
|
211
|
+
.dispatch({
|
|
212
|
+
sessionId: input.sessionId,
|
|
213
|
+
skills: result.exposed.map((skill) => skill.name),
|
|
214
|
+
sessionFile: input.sessionFile,
|
|
215
|
+
attributions,
|
|
216
|
+
})
|
|
217
|
+
.then((outcome) => {
|
|
218
|
+
if (outcome.status === "skipped") log(`[skill-evolve] 本轮未派生:${outcome.why}`);
|
|
219
|
+
else if (outcome.status === "dispatched") observer.noteDispatched(outcome.run.runId);
|
|
220
|
+
})
|
|
221
|
+
.catch((error: unknown) => recordHandlerError("dispatch", error));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const recordHandlerError: Evolution["recordHandlerError"] = (where, error) => {
|
|
225
|
+
telemetry.append({ kind: "handler_error", where, message: String(error) });
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const runNow: Evolution["runNow"] = async (input) => {
|
|
229
|
+
const remembered = lastRound.get(input.sessionId);
|
|
230
|
+
const outcome = await dispatcher.dispatch(
|
|
231
|
+
{
|
|
232
|
+
sessionId: input.sessionId,
|
|
233
|
+
skills: remembered?.skills ?? [],
|
|
234
|
+
sessionFile: input.sessionFile ?? remembered?.sessionFile,
|
|
235
|
+
attributions: remembered?.attributions,
|
|
236
|
+
},
|
|
237
|
+
{ bypassThrottle: true, trigger: "manual" },
|
|
238
|
+
);
|
|
239
|
+
return renderManualDispatch(outcome);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const shutdown: Evolution["shutdown"] = () => {
|
|
243
|
+
try {
|
|
244
|
+
// 同步补发:主进程马上退出,await 很可能到此为止就没了
|
|
245
|
+
const dispatched = dispatcher.flushPending();
|
|
246
|
+
if (dispatched > 0) log(`[skill-evolve] 退出前补发 ${dispatched} 次复盘`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
log(`[skill-evolve] 退出补发失败:${String(error)}`);
|
|
249
|
+
}
|
|
250
|
+
void telemetry.flush().catch((error: unknown) => log(`[skill-evolve] 遥测刷盘失败:${String(error)}`));
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const views = createViews({ evolutionDir, telemetryDir, config, now, mechanical, mechanicalReporter, baselineGate, modelTools });
|
|
254
|
+
|
|
255
|
+
const acceptBaseline = (mode: "accept" | "reset"): Promise<string> => (mode === "reset" ? baselineGate.reset() : baselineGate.acceptAll());
|
|
256
|
+
|
|
257
|
+
const setEnabled = async (on: boolean): Promise<string> => {
|
|
258
|
+
Object.assign(config, await saveConfigSwitch(evolutionDir, on, deps.queue));
|
|
259
|
+
return on
|
|
260
|
+
? `技能进化已打开(采集 + 自动复盘):${configFile(evolutionDir)}(当前会话立即生效)`
|
|
261
|
+
: `技能进化已全部关闭(采集也停):${configFile(evolutionDir)}(当前会话立即生效)`;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/** 只切一个开关:另一个保持原值(`collect` = telemetry、`review` = enabled) */
|
|
265
|
+
const setFlag = async (field: ConfigFlag, value: boolean): Promise<string> => {
|
|
266
|
+
Object.assign(config, await saveFlag(evolutionDir, field, value, deps.queue));
|
|
267
|
+
return `${FLAG_LABEL[field]}已${value ? "开启" : "关闭"}:${configFile(evolutionDir)}(当前会话立即生效)\n${flagStateText(config.enabled, config.telemetry)}`;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
recordAdvertised,
|
|
272
|
+
noteToolCall,
|
|
273
|
+
resolveToolResult,
|
|
274
|
+
settle,
|
|
275
|
+
recordHandlerError,
|
|
276
|
+
runNow,
|
|
277
|
+
shutdown,
|
|
278
|
+
flush: () => telemetry.flush(),
|
|
279
|
+
statusText: views.statusText,
|
|
280
|
+
reportText: views.reportText,
|
|
281
|
+
auditText: views.auditText,
|
|
282
|
+
acceptBaseline,
|
|
283
|
+
setEnabled,
|
|
284
|
+
setFlag,
|
|
285
|
+
model: modelTools,
|
|
286
|
+
apply: applyTools,
|
|
287
|
+
observe: observer,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「采集遥测」与「自动复盘」两个开关的口径:字段名、标签、后果说明、状态文案只此一份。
|
|
3
|
+
*
|
|
4
|
+
* 两个字段在配置里本来就是独立的(`telemetry` 只写本地 jsonl,不花 token;`enabled` 派生子进程复盘,花 token),
|
|
5
|
+
* 命令文案、Web 控制条、HTTP 的 `field` 校验都从这里取 —— 三处各写一份字符串,
|
|
6
|
+
* 迟早出现「命令里叫遥测、网页上叫采集、日志里又叫 telemetry」这种对不上的情况。
|
|
7
|
+
*/
|
|
8
|
+
import type { ConfigFlag } from "./config.ts";
|
|
9
|
+
|
|
10
|
+
/** 字段顺序即界面顺序:先采集(不花钱的),再复盘(花钱的) */
|
|
11
|
+
export const FLAG_FIELDS: readonly ConfigFlag[] = ["telemetry", "enabled"];
|
|
12
|
+
|
|
13
|
+
export const FLAG_LABEL: Record<ConfigFlag, string> = { telemetry: "采集遥测", enabled: "自动复盘" };
|
|
14
|
+
|
|
15
|
+
/** 后果说明:两个开关长得一样,不写清各自花什么,人只会以为都是「开关插件」 */
|
|
16
|
+
export const FLAG_EFFECT: Record<ConfigFlag, string> = {
|
|
17
|
+
telemetry: "只写本地日志,不花 token",
|
|
18
|
+
enabled: "会派生子进程,花 token",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** 命令名 → 字段:`collect` 管采集,`review` 管复盘(命令名与字段名故意不同,别在别处再映射一遍) */
|
|
22
|
+
export const COMMAND_FLAG: Record<"collect" | "review", ConfigFlag> = {
|
|
23
|
+
collect: "telemetry",
|
|
24
|
+
review: "enabled",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** 参数 → 字段:不认的值一律 null(调用方回用法提示或 400,不猜一个默认值) */
|
|
28
|
+
export function parseFlag(value: string | null | undefined): ConfigFlag | null {
|
|
29
|
+
return value === "telemetry" || value === "enabled" ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 一行两段的状态文案:`status` 与命令回执共用,免得同一句话在两处写法不同 */
|
|
33
|
+
export function flagStateText(enabled: boolean, telemetry: boolean): string {
|
|
34
|
+
const collect = telemetry ? "采集:开(遥测写入 .pi/evolution/telemetry/)" : "采集:关(不写遥测)";
|
|
35
|
+
const review = enabled ? "复盘:开(会派生 agent)" : "复盘:关(不会派生 agent)";
|
|
36
|
+
return `${collect}|${review}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 只开复盘却不采集时唯一的不一致组合:归因靠遥测,缺了就只能退到原始会话,加载率算不出来 */
|
|
40
|
+
export const TELEMETRY_OFF_WARNING = "复盘在跑但采集关着:归因只能靠原始会话,加载率不可得";
|
|
41
|
+
|
|
42
|
+
export function telemetryOffWhileEnabled(enabled: boolean, telemetry: boolean): boolean {
|
|
43
|
+
return enabled && !telemetry;
|
|
44
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 闸0 · 授权核对(设计稿 13.2):会话开始时记一份 `.pi/skills` 的 git 摘要,采纳前重算。
|
|
3
|
+
*
|
|
4
|
+
* 判据只有一条:这段时间里 `.pi/skills` 的 git status / diff **一个字节都不能变**。
|
|
5
|
+
* 变了就说明有人(或另一个会话)也在改同一片文件 —— 这时写盘会把别人的改动一起提交,
|
|
6
|
+
* 所以本插件**不写盘**,只告警并留证据(03/06 里都有),绝不自动还原别人的改动:
|
|
7
|
+
* 并发编辑时「自动还原」= 直接把别人的活儿删掉。
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import type { MutationQueue } from "./telemetry.ts";
|
|
12
|
+
|
|
13
|
+
/** 同步跑 git 的注入点:测试给假实现,生产给 spawnSync 包装(绝不在测试里真起进程) */
|
|
14
|
+
export type GitRunner = (args: string[], cwd: string) => { code: number; out: string };
|
|
15
|
+
|
|
16
|
+
export interface Gate0Snapshot {
|
|
17
|
+
at: number;
|
|
18
|
+
status: string;
|
|
19
|
+
diff: string;
|
|
20
|
+
/** git 自身失败的说明(非空即视为无法核对) */
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const GATE0_FILE = "gate0.json";
|
|
25
|
+
|
|
26
|
+
export function gate0Path(evolutionDir: string): string {
|
|
27
|
+
return path.join(evolutionDir, "locks", GATE0_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function captureGate0(git: GitRunner, cwd: string, now: number): Gate0Snapshot {
|
|
31
|
+
const status = git(["status", "--porcelain", "--", ".pi/skills"], cwd);
|
|
32
|
+
const diff = git(["diff", "--", ".pi/skills"], cwd);
|
|
33
|
+
const failed: string[] = [];
|
|
34
|
+
if (status.code !== 0) failed.push(`status 退出码 ${status.code}`);
|
|
35
|
+
if (diff.code !== 0) failed.push(`diff 退出码 ${diff.code}`);
|
|
36
|
+
return {
|
|
37
|
+
at: now,
|
|
38
|
+
status: status.out,
|
|
39
|
+
diff: diff.out,
|
|
40
|
+
...(failed.length > 0 ? { error: failed.join(";") } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 与采纳前的现场比对。返回 null = 未漂移;返回字符串 = 拒绝采纳的原因(人话)。
|
|
46
|
+
* 没有快照(扩展不是从 session_start 起来的)时按「无法核对」处理 —— 宁可拒采,不赌。
|
|
47
|
+
*/
|
|
48
|
+
export function gate0Drift(before: Gate0Snapshot | null, after: Gate0Snapshot): string {
|
|
49
|
+
if (!before) return "闸0:没有会话起点的 .pi/skills 快照,无法核对授权面";
|
|
50
|
+
if (after.error) return `闸0:git 不可用(${after.error})`;
|
|
51
|
+
if (before.status !== after.status || before.diff !== after.diff) {
|
|
52
|
+
return "闸0:.pi/skills 在会话开始后被改动过(并发编辑),本次拒绝写盘并告警";
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function saveGate0(evolutionDir: string, snapshot: Gate0Snapshot, queue: MutationQueue): Promise<void> {
|
|
58
|
+
const file = gate0Path(evolutionDir);
|
|
59
|
+
await queue(file, async () => {
|
|
60
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
61
|
+
await fs.promises.writeFile(file, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readGate0(evolutionDir: string): Gate0Snapshot | null {
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(fs.readFileSync(gate0Path(evolutionDir), "utf8")) as Partial<Gate0Snapshot>;
|
|
68
|
+
if (typeof raw.status !== "string" || typeof raw.diff !== "string") return null;
|
|
69
|
+
return {
|
|
70
|
+
at: typeof raw.at === "number" ? raw.at : 0,
|
|
71
|
+
status: raw.status,
|
|
72
|
+
diff: raw.diff,
|
|
73
|
+
...(typeof raw.error === "string" ? { error: raw.error } : {}),
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|