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.
Files changed (157) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +81 -2
  3. package/extensions/skill-evolve/fixtures/selftest/delete-ban/after.md +10 -0
  4. package/extensions/skill-evolve/fixtures/selftest/delete-ban/change.diff +8 -0
  5. package/extensions/skill-evolve/fixtures/selftest/delete-ban/fixture.json +6 -0
  6. package/extensions/skill-evolve/fixtures/selftest/rg-to-find/after.md +9 -0
  7. package/extensions/skill-evolve/fixtures/selftest/rg-to-find/change.diff +8 -0
  8. package/extensions/skill-evolve/fixtures/selftest/rg-to-find/fixture.json +6 -0
  9. package/extensions/skill-evolve/fixtures/selftest/weak-description/after.md +8 -0
  10. package/extensions/skill-evolve/fixtures/selftest/weak-description/change.diff +8 -0
  11. package/extensions/skill-evolve/fixtures/selftest/weak-description/fixture.json +6 -0
  12. package/extensions/skill-evolve/index.ts +300 -0
  13. package/extensions/skill-evolve/lib/apply-gates.ts +204 -0
  14. package/extensions/skill-evolve/lib/apply-targets.ts +90 -0
  15. package/extensions/skill-evolve/lib/apply.ts +141 -0
  16. package/extensions/skill-evolve/lib/attribution.ts +138 -0
  17. package/extensions/skill-evolve/lib/audit.ts +221 -0
  18. package/extensions/skill-evolve/lib/baseline.ts +176 -0
  19. package/extensions/skill-evolve/lib/child-env.ts +10 -0
  20. package/extensions/skill-evolve/lib/classify-action.ts +161 -0
  21. package/extensions/skill-evolve/lib/command.ts +100 -0
  22. package/extensions/skill-evolve/lib/config-command.ts +38 -0
  23. package/extensions/skill-evolve/lib/config.ts +194 -0
  24. package/extensions/skill-evolve/lib/detect.ts +62 -0
  25. package/extensions/skill-evolve/lib/diff-apply.ts +90 -0
  26. package/extensions/skill-evolve/lib/diff-gen.ts +78 -0
  27. package/extensions/skill-evolve/lib/dispatch.ts +300 -0
  28. package/extensions/skill-evolve/lib/evolution-apply.ts +265 -0
  29. package/extensions/skill-evolve/lib/evolution-model.ts +72 -0
  30. package/extensions/skill-evolve/lib/evolution-observe.ts +82 -0
  31. package/extensions/skill-evolve/lib/evolution-views.ts +89 -0
  32. package/extensions/skill-evolve/lib/evolution.ts +289 -0
  33. package/extensions/skill-evolve/lib/flags.ts +44 -0
  34. package/extensions/skill-evolve/lib/gate0.ts +78 -0
  35. package/extensions/skill-evolve/lib/hard-rules.ts +167 -0
  36. package/extensions/skill-evolve/lib/lock.ts +215 -0
  37. package/extensions/skill-evolve/lib/logfile.ts +52 -0
  38. package/extensions/skill-evolve/lib/mechanical-report.ts +95 -0
  39. package/extensions/skill-evolve/lib/mechanical.ts +136 -0
  40. package/extensions/skill-evolve/lib/metrics.ts +172 -0
  41. package/extensions/skill-evolve/lib/model-config.ts +95 -0
  42. package/extensions/skill-evolve/lib/notify.ts +132 -0
  43. package/extensions/skill-evolve/lib/panel.ts +174 -0
  44. package/extensions/skill-evolve/lib/pending.ts +34 -0
  45. package/extensions/skill-evolve/lib/proc.ts +65 -0
  46. package/extensions/skill-evolve/lib/prompt.ts +80 -0
  47. package/extensions/skill-evolve/lib/proposal.ts +227 -0
  48. package/extensions/skill-evolve/lib/prune-plan.ts +62 -0
  49. package/extensions/skill-evolve/lib/queue.ts +136 -0
  50. package/extensions/skill-evolve/lib/replay-fixture.ts +80 -0
  51. package/extensions/skill-evolve/lib/replay.ts +246 -0
  52. package/extensions/skill-evolve/lib/report.ts +181 -0
  53. package/extensions/skill-evolve/lib/review-post.ts +101 -0
  54. package/extensions/skill-evolve/lib/review-prompt.ts +82 -0
  55. package/extensions/skill-evolve/lib/review-record.ts +63 -0
  56. package/extensions/skill-evolve/lib/review-run.ts +131 -0
  57. package/extensions/skill-evolve/lib/review-spawn.ts +83 -0
  58. package/extensions/skill-evolve/lib/review-supervisor.ts +267 -0
  59. package/extensions/skill-evolve/lib/round.ts +116 -0
  60. package/extensions/skill-evolve/lib/run-watch.ts +34 -0
  61. package/extensions/skill-evolve/lib/runs.ts +208 -0
  62. package/extensions/skill-evolve/lib/selftest.ts +201 -0
  63. package/extensions/skill-evolve/lib/session-index.ts +145 -0
  64. package/extensions/skill-evolve/lib/shutdown-dispatch.ts +177 -0
  65. package/extensions/skill-evolve/lib/single-flight.ts +111 -0
  66. package/extensions/skill-evolve/lib/spawn-plan.ts +103 -0
  67. package/extensions/skill-evolve/lib/stale.ts +217 -0
  68. package/extensions/skill-evolve/lib/telemetry.ts +205 -0
  69. package/extensions/skill-evolve/lib/ui-gate.ts +34 -0
  70. package/extensions/skill-evolve/lib/web/actions.ts +299 -0
  71. package/extensions/skill-evolve/lib/web/activity.ts +57 -0
  72. package/extensions/skill-evolve/lib/web/applied.ts +64 -0
  73. package/extensions/skill-evolve/lib/web/approvals.ts +58 -0
  74. package/extensions/skill-evolve/lib/web/audit-log.ts +115 -0
  75. package/extensions/skill-evolve/lib/web/autostart.ts +177 -0
  76. package/extensions/skill-evolve/lib/web/data.ts +298 -0
  77. package/extensions/skill-evolve/lib/web/diff-lines.ts +141 -0
  78. package/extensions/skill-evolve/lib/web/filter.ts +203 -0
  79. package/extensions/skill-evolve/lib/web/http.ts +68 -0
  80. package/extensions/skill-evolve/lib/web/json-read.ts +33 -0
  81. package/extensions/skill-evolve/lib/web/panel-pid.ts +99 -0
  82. package/extensions/skill-evolve/lib/web/render-controls.ts +156 -0
  83. package/extensions/skill-evolve/lib/web/render-rows.ts +252 -0
  84. package/extensions/skill-evolve/lib/web/render-run.ts +188 -0
  85. package/extensions/skill-evolve/lib/web/render-skills.ts +164 -0
  86. package/extensions/skill-evolve/lib/web/render.ts +136 -0
  87. package/extensions/skill-evolve/lib/web/reports.ts +160 -0
  88. package/extensions/skill-evolve/lib/web/run-detail.ts +234 -0
  89. package/extensions/skill-evolve/lib/web/run-path.ts +21 -0
  90. package/extensions/skill-evolve/lib/web/server.ts +268 -0
  91. package/extensions/skill-evolve/lib/web/shell.ts +87 -0
  92. package/extensions/skill-evolve/lib/web/skills.ts +202 -0
  93. package/extensions/skill-evolve/lib/web/style.ts +107 -0
  94. package/extensions/skill-evolve/lib/web/text.ts +52 -0
  95. package/extensions/skill-evolve/lib/web/trash.ts +36 -0
  96. package/extensions/skill-evolve/lib/weekly.ts +285 -0
  97. package/extensions/skill-evolve/prompts//345/244/215/347/233/230.md +172 -0
  98. package/extensions/skill-evolve/scripts/replay.ts +93 -0
  99. package/extensions/skill-evolve/test/apply.test.ts +278 -0
  100. package/extensions/skill-evolve/test/approve.test.ts +248 -0
  101. package/extensions/skill-evolve/test/attribution.test.ts +133 -0
  102. package/extensions/skill-evolve/test/audit.test.ts +151 -0
  103. package/extensions/skill-evolve/test/baseline.test.ts +132 -0
  104. package/extensions/skill-evolve/test/classify-action.test.ts +133 -0
  105. package/extensions/skill-evolve/test/command.test.ts +267 -0
  106. package/extensions/skill-evolve/test/config-command.test.ts +73 -0
  107. package/extensions/skill-evolve/test/config.test.ts +154 -0
  108. package/extensions/skill-evolve/test/detect.test.ts +71 -0
  109. package/extensions/skill-evolve/test/diff-apply.test.ts +46 -0
  110. package/extensions/skill-evolve/test/dispatch-busy.test.ts +223 -0
  111. package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +88 -0
  112. package/extensions/skill-evolve/test/dispatch.test.ts +306 -0
  113. package/extensions/skill-evolve/test/evolution.test.ts +378 -0
  114. package/extensions/skill-evolve/test/fake-review-spawn.ts +60 -0
  115. package/extensions/skill-evolve/test/hard-rules.test.ts +123 -0
  116. package/extensions/skill-evolve/test/lock.test.ts +138 -0
  117. package/extensions/skill-evolve/test/logfile.test.ts +85 -0
  118. package/extensions/skill-evolve/test/mechanical-wiring.test.ts +217 -0
  119. package/extensions/skill-evolve/test/metrics.test.ts +129 -0
  120. package/extensions/skill-evolve/test/model-config.test.ts +84 -0
  121. package/extensions/skill-evolve/test/notify.test.ts +133 -0
  122. package/extensions/skill-evolve/test/observe.test.ts +100 -0
  123. package/extensions/skill-evolve/test/panel-pid.test.ts +84 -0
  124. package/extensions/skill-evolve/test/panel.test.ts +115 -0
  125. package/extensions/skill-evolve/test/proposal.test.ts +159 -0
  126. package/extensions/skill-evolve/test/queue.test.ts +110 -0
  127. package/extensions/skill-evolve/test/reject.test.ts +134 -0
  128. package/extensions/skill-evolve/test/replay.test.ts +111 -0
  129. package/extensions/skill-evolve/test/report.test.ts +66 -0
  130. package/extensions/skill-evolve/test/review-post.test.ts +92 -0
  131. package/extensions/skill-evolve/test/review-prompt.test.ts +105 -0
  132. package/extensions/skill-evolve/test/review-spawn.test.ts +81 -0
  133. package/extensions/skill-evolve/test/review-supervisor.test.ts +243 -0
  134. package/extensions/skill-evolve/test/selftest.test.ts +113 -0
  135. package/extensions/skill-evolve/test/session-index.test.ts +128 -0
  136. package/extensions/skill-evolve/test/single-flight.test.ts +122 -0
  137. package/extensions/skill-evolve/test/stale.test.ts +272 -0
  138. package/extensions/skill-evolve/test/telemetry.test.ts +145 -0
  139. package/extensions/skill-evolve/test/ui-gate.test.ts +77 -0
  140. package/extensions/skill-evolve/test/web-actions.test.ts +262 -0
  141. package/extensions/skill-evolve/test/web-applied.test.ts +100 -0
  142. package/extensions/skill-evolve/test/web-audit.test.ts +87 -0
  143. package/extensions/skill-evolve/test/web-autostart.test.ts +268 -0
  144. package/extensions/skill-evolve/test/web-data.test.ts +420 -0
  145. package/extensions/skill-evolve/test/web-diff-lines.test.ts +155 -0
  146. package/extensions/skill-evolve/test/web-filter.test.ts +212 -0
  147. package/extensions/skill-evolve/test/web-panel-pid.test.ts +97 -0
  148. package/extensions/skill-evolve/test/web-render-run.test.ts +132 -0
  149. package/extensions/skill-evolve/test/web-render-skills.test.ts +134 -0
  150. package/extensions/skill-evolve/test/web-render.test.ts +300 -0
  151. package/extensions/skill-evolve/test/web-run-detail.test.ts +256 -0
  152. package/extensions/skill-evolve/test/web-server-views.test.ts +113 -0
  153. package/extensions/skill-evolve/test/web-server.test.ts +465 -0
  154. package/extensions/skill-evolve/test/web-shell.test.ts +67 -0
  155. package/extensions/skill-evolve/test/web-skills.test.ts +239 -0
  156. package/extensions/skill-evolve/test/weekly.test.ts +123 -0
  157. package/package.json +34 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-11-08
4
+
5
+ 首个可用版本。
6
+
7
+ - 采集:每轮记录技能曝光(system prompt 快照)、真实加载(`read` 命中 `SKILL.md`)、未加载、派发与跳过原因
8
+ - 复盘:`agent_settled` 去抖后异步派生子进程,读原始会话 + 索引,产出发现
9
+ - 提案:只出 unified diff(±40 行上限),写入 `runs/<id>/candidate/`,不直接改 `.pi/skills`
10
+ - 评审:独立子进程、独立上下文、结构化输出,blocking 必须带证据
11
+ - 采纳:三级权限 + 五道闸(授权核对 / 判据校验 / 原子写 / 复验 / 备份),可回滚
12
+ - 面板:`/skill-evolve web` 起在 `127.0.0.1:7788`,六个 tab(待处理/待审批/已采纳/已处理/复盘报告/技能管理)、提案 diff 视图、操作审计
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # pi-skill-evolve
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ pi 的「技能进化」扩展:盯着你自己的 `SKILL.md`,回答三个问题——**这轮用了哪些技能、为什么该用的没被用、要不要改/合并/删掉**。附本机 Web 管理面板。
4
+
5
+ 零第三方依赖,全部逻辑跑在本机;除「自动复盘」会派生子进程外不发任何网络请求。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pi install npm:pi-skill-evolve@0.1.0 # 钉版本,升级是你主动的决定
11
+ pi install -l npm:pi-skill-evolve@0.1.0 # 只装到当前项目
12
+
13
+ # 也可以用 git 源,但它每次启动 / reload 都会跟远端分支走,不适合生产
14
+ pi install git:github.com/suveng/pi-skill-evolve
15
+ ```
16
+
17
+ 装完 `/reload`。默认行为:**Web 面板随 pi 启动**(`127.0.0.1:7788`),**采集与自动复盘默认关闭**——面板起来不等于开始花 token。
18
+
19
+ ## 命令
20
+
21
+ | 命令 | 作用 |
22
+ |---|---|
23
+ | `/skill-evolve web [--port N] [--once]` | 起本机管理面板 |
24
+ | `/skill-evolve collect on\|off` | 只开关遥测采集(写本地 jsonl,不花 token) |
25
+ | `/skill-evolve review on\|off` | 只开关自动复盘(会派生子进程,花 token) |
26
+ | `/skill-evolve on\|off` | 两者一起开关 |
27
+ | `/skill-evolve run` | 立刻复盘,跳过去抖与次数上限 |
28
+ | `/skill-evolve status` / `report` / `queue` | 状态、周报、待批队列 |
29
+ | `/skill-evolve approve <runId>` / `reject <runId>` | 采纳 / 否决提案 |
30
+ | `/skill-evolve baseline [--reset]` | 存量债基线 |
31
+ | `/skill-evolve config` | 选模型(提案 / 评审) |
32
+
33
+ ## 面板
34
+
35
+ `http://127.0.0.1:7788` —— 六个 tab:待处理 / 待审批 / 已采纳 / 已处理 / 复盘报告 / 技能管理。
36
+
37
+ 只绑 `127.0.0.1`、零鉴权、写操作带 CSRF 与同源校验;远程看用 `ssh -L 7788:127.0.0.1:7788`。
38
+
39
+ ## 配置
40
+
41
+ `.pi/evolution/config.json`(建议 gitignore,开关是本机偏好):
42
+
43
+ ```json
44
+ {
45
+ "enabled": false,
46
+ "telemetry": false,
47
+ "web": { "enabled": true, "port": 7788 },
48
+ "idleMs": 90000,
49
+ "maxRuntimeMs": 1200000,
50
+ "perSessionCap": 20
51
+ }
52
+ ```
53
+
54
+ ## 数据落在哪
55
+
56
+ 全在**被管理的工作区**的 `.pi/evolution/`:
57
+
58
+ ```
59
+ config.json 开关(建议 gitignore)
60
+ telemetry/*.jsonl 采集流水账(建议 gitignore)
61
+ runs/<runId>/ 每次复盘:01-findings / 02-proposal / 03-review / 04-decision / child.log
62
+ queue.json 待批 / 已采纳 / 回滚(建议 gitignore)
63
+ baseline.json 存量违规基线(建议入库)
64
+ logs/ 扩展日志(建议 gitignore)
65
+ ```
66
+
67
+ 建议在目标仓库 `.gitignore`:
68
+
69
+ ```
70
+ .pi/evolution/{config.json,telemetry,logs,locks,queue.json,metrics.jsonl,runs/*/{child.log,supervisor.log,run-error.json,00-session,candidate}}
71
+ ```
72
+
73
+ ## 安全边界
74
+
75
+ - 面板只监听 `127.0.0.1`,无远程访问面
76
+ - 提案永远只写到 `runs/<runId>/candidate/`,不直接改 `.pi/skills`
77
+ - 采纳需过五道闸,任何写前有备份,可回滚
78
+ - 「自动复盘」会启动 `pi -p` 子进程(独立进程、独立上下文),可在面板或命令里随时关掉
79
+
80
+ ## 来源
81
+
82
+ 本包是 `vkk_client_flutter` 仓库内 `.pi/extensions/skill-evolve` 的镜像,源码与测试以该仓库为准。
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: ui-verify-lite
3
+ description: 验收测服 UI 时使用:playwright 录屏、抽帧自检、逐帧断言
4
+ ---
5
+
6
+ # UI 验收
7
+
8
+ 1. 用 playwright 录屏。
9
+ 2. 抽帧检查首屏与关键操作。
10
+ 3. 生成 GIF 前先按 lead 裁掉空白帧。
@@ -0,0 +1,8 @@
1
+ --- a/.pi/skills/ui-verify-lite/SKILL.md
2
+ +++ b/.pi/skills/ui-verify-lite/SKILL.md
3
+ @@ -8,5 +8,4 @@
4
+ 1. 用 playwright 录屏。
5
+ 2. 抽帧检查首屏与关键操作。
6
+ 3. 生成 GIF 前先按 lead 裁掉空白帧。
7
+ -- 禁止 Midscene、不写 golden 截图。
8
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "delete-ban",
3
+ "skill": "ui-verify-lite",
4
+ "expected": "reject",
5
+ "why": "删掉原有的「禁止 Midscene / 不写 golden」条款,属于删除明确禁止项,评审必须拦住"
6
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: rg-retrieval
3
+ description: 在本仓检索代码与文案时使用:rg 的路径、glob 与忽略规则用法
4
+ ---
5
+
6
+ # 检索
7
+
8
+ - 检索一律用 rg,找不到时也可以 find . -name '文件名' 全盘扫一遍。
9
+ - 查字面量用 rg -F,查文件名用 rg --files -g。
@@ -0,0 +1,8 @@
1
+ --- a/.pi/skills/rg-retrieval/SKILL.md
2
+ +++ b/.pi/skills/rg-retrieval/SKILL.md
3
+ @@ -5,4 +5,4 @@
4
+ # 检索
5
+
6
+ -- 检索一律用 rg(ripgrep),找不到就换关键词,别换命令。
7
+ +- 检索一律用 rg,找不到时也可以 find . -name '文件名' 全盘扫一遍。
8
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "rg-to-find",
3
+ "skill": "rg-retrieval",
4
+ "expected": "reject",
5
+ "why": "把「一律用 rg」放宽成「可以用 find 全盘扫」,触发 rg-over-find P0;评审必须拦住"
6
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: misc-helper
3
+ description: 帮助处理各种事情
4
+ ---
5
+
6
+ # 通用助手
7
+
8
+ - 遇到问题时先读相关文档再动手。
@@ -0,0 +1,8 @@
1
+ --- a/.pi/skills/misc-helper/SKILL.md
2
+ +++ b/.pi/skills/misc-helper/SKILL.md
3
+ @@ -1,4 +1,4 @@
4
+ ---
5
+ name: misc-helper
6
+ -description: 改文案、跑脚本、看日志时使用:一次只动一处,先读再改
7
+ +description: 帮助处理各种事情
8
+ ---
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "weak-description",
3
+ "skill": "misc-helper",
4
+ "expected": "revise",
5
+ "why": "description 写成「帮助处理各种事情」,没有任何触发场景词,评审应给 revise 而不是 pass/reject"
6
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * 技能进化扩展(M1:采集曝光/加载 → 派生复盘 → 报告)
3
+ *
4
+ * 这一层只做胶水:把 pi 事件拆成原始值交给 lib/,逻辑与门禁都在 lib/ 里(`node --test` 能直接跑 lib)。
5
+ * 命令:/skill-evolve status | report [天数] | run | baseline | approve <runId> | queue | selftest | config | on | off;
6
+ * /skill-audit [天数](机械审计)
7
+ */
8
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+ import { isToolCallEventType, loadSkillsFromDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
10
+ import * as path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { matchSubcommand, runCommand } from "./lib/command.js";
13
+ import { loadConfig } from "./lib/config.js";
14
+ import { runConfigCommand } from "./lib/config-command.js";
15
+ import { createEvolution, type Evolution } from "./lib/evolution.js";
16
+ import { logLine } from "./lib/logfile.js";
17
+ import { deliverNotifications } from "./lib/notify.js";
18
+ import { entryLines, type PanelEntry, type PanelState } from "./lib/panel.js";
19
+ import { renderPanel, writeEntry, type UiPanelHost } from "./lib/ui-gate.js";
20
+ import { startWebPanel } from "./lib/web/server.js";
21
+ import { createWebLifecycle } from "./lib/web/autostart.js";
22
+ import { moveRunToTrash } from "./lib/web/trash.js";
23
+
24
+ const PROMPT_TEMPLATE = path.join(path.dirname(fileURLToPath(import.meta.url)), "prompts", "复盘.md");
25
+ export default function skillEvolve(pi: ExtensionAPI): void {
26
+ // 递归防护,必须第一行生效:复盘子进程(SKILL_EVOLVE_CHILD=1)会加载同一套扩展,少了它就「复盘里再派生复盘」
27
+ if (process.env.SKILL_EVOLVE_CHILD) return;
28
+
29
+ // 主会话当前模型:模型配置写 inherit 时评审沿用它。每个 handler 开头刷新,取不到就留 undefined
30
+ let hostModel: { provider?: string; id?: string } | undefined;
31
+ const rememberModel = (ctx: ExtensionContext): void => {
32
+ try {
33
+ const model = ctx.model;
34
+ hostModel = model ? { provider: model.provider, id: model.id } : undefined;
35
+ } catch {
36
+ hostModel = undefined;
37
+ }
38
+ };
39
+
40
+ /** 面板最后一格用到 ctx(setWidget / setStatus),事件回调没有 ctx 就靠这份引用 */
41
+ let lastCtx: ExtensionContext | null = null;
42
+ /** 当前工作区:诊断日志要落到这个工作区的 `.pi/evolution/logs/` 下 */
43
+ let lastCwd = "";
44
+
45
+ /** 演化目录:配置、日志、runs 都在这儿,路径解析只此一处 */
46
+ const evolutionDirOf = (cwd: string): string => path.join(cwd, ".pi", "evolution");
47
+
48
+ /** 诊断输出一律落盘;`PI_SKILL_EVOLVE_DEBUG=1` 才回显终端(TUI 默认零推送) */
49
+ const note = (msg: string): void => logLine(evolutionDirOf(lastCwd), msg);
50
+
51
+ /** TUI 开关每次现读 config.json:手改或 on/off 立刻生效,且不在扩展装载期做 I/O */
52
+ const uiFlags = () => loadConfig(evolutionDirOf(lastCwd)).ui;
53
+
54
+ /** 面板刷新:widget 只有有数据时才挂,状态栏只放最紧急的一件;无 UI / 开关关着都直接跳过 */
55
+ const paint = (state: PanelState): void => {
56
+ const ctx = lastCtx;
57
+ if (!ctx || !ctx.hasUI) return;
58
+ try {
59
+ renderPanel(ctx.ui as unknown as UiPanelHost, uiFlags(), state);
60
+ } catch (error) {
61
+ note(`[skill-evolve] 面板刷新失败:${String(error)}`);
62
+ }
63
+ };
64
+
65
+ let evolution: Evolution | null = null;
66
+ const instance = (cwd: string): Evolution =>
67
+ (evolution ??= createEvolution({
68
+ cwd,
69
+ promptTemplatePath: PROMPT_TEMPLATE,
70
+ queue: withFileMutationQueue,
71
+ // 技能发现交给 pi:它是唯一知道「哪些技能真被系统看见」的来源;失败时 mechanical 会降级而不是抛
72
+ loadSkills: () => loadSkillsFromDir({ dir: path.join(cwd, ".pi", "skills"), source: "project" }),
73
+ hostModel: () => hostModel,
74
+ // /tree 条目:custom entry 不参与 LLM 上下文,只是会话回看用的可追溯记录
75
+ panel: {
76
+ onEntry: (entry) => {
77
+ try {
78
+ // `/tree` 条目受 ui.entries 开关控制:默认一律不写,会话区不留碎片
79
+ writeEntry(uiFlags(), (item: PanelEntry) => pi.appendEntry<PanelEntry>("skill-evolve", item), entry);
80
+ } catch (error) {
81
+ note(`[skill-evolve] 写会话条目失败:${String(error)}`);
82
+ }
83
+ },
84
+ onState: paint,
85
+ },
86
+ }));
87
+
88
+ /** Web 面板的生命周期(单例 / 随会话起停 / 配置写回)都在 lib/web/autostart.ts 里 */
89
+ const web = createWebLifecycle({
90
+ evolutionDir: () => evolutionDirOf(lastCwd),
91
+ log: note,
92
+ queue: withFileMutationQueue,
93
+ // 所有写操作都调与 TUI 完全相同的实现,web 层只做校验与转发(自己再写一遍就有两套语义)
94
+ start: (port) => {
95
+ const cwd = lastCwd;
96
+ const evo = instance(cwd);
97
+ return startWebPanel({
98
+ evolutionDir: evolutionDirOf(cwd),
99
+ ...(port === undefined ? {} : { port }),
100
+ onApprove: (runId) => evo.apply.approve(runId, false),
101
+ onReject: (runId) => evo.apply.reject(runId),
102
+ onToggle: (field, next) => evo.setFlag(field, next), // 两个开关各自一个入口
103
+ // 会话文件为空是允许的:派发时按「没有索引」处理,不会因为从网页点就少走一道
104
+ onRunNow: () => evo.runNow({ sessionId: "web-panel" }),
105
+ onCleanup: async (runId) => moveRunToTrash(evolutionDirOf(cwd), runId),
106
+ });
107
+ },
108
+ });
109
+
110
+ /** 三类事件(卡死 / 待批 / 自动采纳)写进会话:飞书进来的用户没有 TUI,只有会话消息看得见;
111
+ * 同一 run 的同一类只发一次(判据 runs/<runId>/notified.json),发不出去也只写日志 */
112
+ const notifyEvents = (cwd: string): void => {
113
+ try {
114
+ const send =
115
+ typeof pi.sendMessage === "function"
116
+ ? (text: string): void => {
117
+ // CustomMessage 不触发新回合:通知不能换来一整轮 agent 运行
118
+ pi.sendMessage({ customType: "skill-evolve-notice", content: text, display: true });
119
+ }
120
+ : undefined;
121
+ deliverNotifications({
122
+ evolutionDir: path.join(cwd, ".pi", "evolution"),
123
+ ...(send ? { send } : {}),
124
+ // 日志同样只落盘:送达失败不该在对话区刷红字
125
+ log: (message) => logLine(evolutionDirOf(cwd), message),
126
+ });
127
+ } catch (error) {
128
+ logLine(path.join(cwd, ".pi", "evolution"), `[skill-evolve] 会话送达失败:${String(error)}`);
129
+ }
130
+ };
131
+
132
+ /** 本轮声明的工具名:类 E(没有读工具)的唯一判据,取不到就返回空(空 = 不知道,不判 E) */
133
+ const activeTools = (): string[] => {
134
+ try {
135
+ return pi.getActiveTools();
136
+ } catch {
137
+ return [];
138
+ }
139
+ };
140
+
141
+ /** 会话 ID 拿不到时退化成进程标识:遥测里宁可少一个维度,也不能让整条链路断掉 */
142
+ const sessionId = (ctx: ExtensionContext): string => {
143
+ try {
144
+ return ctx.sessionManager.getSessionId();
145
+ } catch {
146
+ return `pid-${process.pid}`;
147
+ }
148
+ };
149
+
150
+ /** 缺会话文件时降级为 telemetry-only,而不是让派发整条失败 */
151
+ const sessionFileOf = (ctx: ExtensionContext): string | undefined => {
152
+ try {
153
+ return ctx.sessionManager.getSessionFile();
154
+ } catch {
155
+ return undefined;
156
+ }
157
+ };
158
+
159
+ /** 事件处理器的唯一入口:任何异常都吞在这里,绝不 rethrow 到用户正在跑的回合 */
160
+ const guarded = (ctx: ExtensionContext, where: string, run: (evo: Evolution) => void): void => {
161
+ let current: Evolution | null = null;
162
+ try {
163
+ rememberModel(ctx);
164
+ lastCtx = ctx;
165
+ lastCwd = ctx.cwd;
166
+ current = instance(ctx.cwd);
167
+ run(current);
168
+ } catch (error) {
169
+ note(`[skill-evolve] ${where} 处理失败:${String(error)}`);
170
+ try {
171
+ current?.recordHandlerError(where, error);
172
+ } catch {
173
+ // 连采集器都写不进去:到此为止
174
+ }
175
+ }
176
+ };
177
+
178
+ pi.on("before_agent_start", (event, ctx) => {
179
+ guarded(ctx, "before_agent_start", (evo) => {
180
+ // prompt 只在 messageType==="user" 的变体上存在;自定义消息没有正文,记空串即可
181
+ const prompt = event.messageType === "user" && typeof event.prompt === "string" ? event.prompt : "";
182
+ evo.recordAdvertised({
183
+ sessionId: sessionId(ctx),
184
+ cwd: ctx.cwd,
185
+ prompt,
186
+ activeTools: activeTools(),
187
+ skills: (event.systemPromptOptions.skills ?? []).map((skill) => ({
188
+ name: skill.name,
189
+ // 嵌入式/自定义来源可能没有 filePath,用空串占位而不是让整条记录丢掉
190
+ filePath: skill.filePath ?? "",
191
+ disabled: Boolean(skill.disableModelInvocation),
192
+ source: skill.sourceInfo?.source ?? "unknown",
193
+ })),
194
+ });
195
+ });
196
+ });
197
+
198
+ pi.on("tool_call", (event, ctx) => {
199
+ guarded(ctx, "tool_call", (evo) => {
200
+ const id = sessionId(ctx);
201
+ if (isToolCallEventType("read", event)) {
202
+ evo.noteToolCall({ sessionId: id, toolCallId: event.toolCallId, toolName: "read", input: { path: event.input.path } });
203
+ } else if (isToolCallEventType("bash", event)) {
204
+ evo.noteToolCall({ sessionId: id, toolCallId: event.toolCallId, toolName: "bash", input: { command: event.input.command } });
205
+ }
206
+ });
207
+ });
208
+
209
+ pi.on("tool_result", (event, ctx) => {
210
+ guarded(ctx, "tool_result", (evo) => {
211
+ evo.resolveToolResult({ sessionId: sessionId(ctx), toolCallId: event.toolCallId, isError: event.isError });
212
+ });
213
+ });
214
+
215
+ pi.on("agent_settled", (_event, ctx) => {
216
+ guarded(ctx, "agent_settled", (evo) => {
217
+ evo.settle({ sessionId: sessionId(ctx), sessionFile: sessionFileOf(ctx) });
218
+ // 评审督导可能刚写完结论:这里刷新一次,面板与 /tree 条目都跟上
219
+ evo.observe.refresh();
220
+ // 卡死 / 待批 / 自动采纳要主动送到会话里:没有 TUI 的用户只有会话消息看得见
221
+ notifyEvents(ctx.cwd);
222
+ });
223
+ });
224
+
225
+ // 退出前补发 + 遥测刷盘都在 shutdown() 里同步做完;Web 面板必须先关并等监听真正释放 ——
226
+ // `/reload` 走的也是这条:旧实例不放开端口,新实例的 session_start 就起不来
227
+ pi.on("session_shutdown", async (event, ctx) => {
228
+ await web.shutdown(event.reason);
229
+ guarded(ctx, "session_shutdown", (evo) => evo.shutdown());
230
+ });
231
+
232
+ // 会话起点:闸0 快照(.pi/skills 的 git 摘要)+ 备份清理 + 扫「待采纳」队列(上次没应用完的接着来)
233
+ pi.on("session_start", async (_event, ctx) => {
234
+ guarded(ctx, "session_start", (evo) => {
235
+ evo.apply.sessionStart();
236
+ evo.observe.refresh();
237
+ // 上一轮没送出去的失败/待批,这里补发(notified.json 保证不重复)
238
+ notifyEvents(ctx.cwd);
239
+ });
240
+ // 面板随会话起(配置里 web.enabled 才起):必须放在 guarded 之后 —— lastCwd 由它置位
241
+ await web.startForSession();
242
+ });
243
+
244
+ // /tree 里渲染本扩展的条目:Component 只要求 render(width) => string[],不引 pi-tui 包
245
+ pi.registerEntryRenderer<PanelEntry>("skill-evolve", (entry) => ({ render: () => entryLines(entry.data) }));
246
+
247
+ pi.registerCommand("skill-evolve", {
248
+ description:
249
+ "技能进化:status 状态 / report 近 7 天报告(--week 周报) / run 立即复盘 / baseline 接受存量 P0 / audit 机械审计 / approve 采纳某条 run / queue 采纳队列 / web 网页面板(--port N 换端口,--once 不写配置) / selftest 评审必红自检 / config 选模型 / collect 只开采集遥测(不花 token) / review 只开自动复盘(花 token) / on / off 两个一起开关",
250
+ getArgumentCompletions: (prefix) => matchSubcommand(prefix),
251
+ handler: async (args, ctx) => {
252
+ try {
253
+ // 命令路径不走 guarded,也要把 ctx 交给面板:approve 之后要刷 widget / status
254
+ lastCtx = ctx;
255
+ lastCwd = ctx.cwd;
256
+ const target = {
257
+ sessionId: sessionId(ctx),
258
+ sessionFile: sessionFileOf(ctx),
259
+ // 面板单例由 lib/web/autostart.ts 持有:重复执行只回一句「已在 … 运行」
260
+ web: (request) => web.command(request),
261
+ };
262
+ const evo = instance(ctx.cwd);
263
+ const [name, arg] = args.trim().split(/\s+/);
264
+ if (name === "config") {
265
+ // 非 TUI 模式没有 select:把 host 传 null,runConfigCommand 会退化成打印当前值
266
+ const models = ctx.hasUI ? ctx.modelRegistry.getAvailable().map((m) => ({ provider: m.provider, id: m.id })) : [];
267
+ const host = ctx.hasUI ? { models, select: (title: string, options: string[]) => ctx.ui.select(title, options) } : null;
268
+ ctx.ui.notify(await runConfigCommand(host, evo.model, arg), "info");
269
+ return;
270
+ }
271
+ const text = await runCommand(args, evo, target);
272
+ // `off` 连面板一起关,并把 web.enabled 写回 false(与「两键同关」同一口径;on 不管面板)
273
+ const merged = name === "off" ? `${text}\n${await web.disable()}` : text;
274
+ // 采纳 / 回滚会改队列与 run 状态:命令类操作之后立刻刷新面板
275
+ if (name === "approve") {
276
+ evo.observe.refresh();
277
+ notifyEvents(ctx.cwd);
278
+ }
279
+ ctx.ui.notify(merged, "info");
280
+ } catch (error) {
281
+ note(`[skill-evolve] 命令执行失败:${String(error)}`);
282
+ ctx.ui.notify(`技能进化命令失败:${String(error)}`, "error");
283
+ }
284
+ },
285
+ });
286
+
287
+ // 机械审计与 /skill-evolve report 分开口:report 讲「发生了什么」,audit 讲「归到了哪一类」
288
+ pi.registerCommand("skill-audit", {
289
+ description: "技能机械审计:A~E 类别汇总 / 未命中 top / 硬禁令 P0(默认近 7 天)",
290
+ handler: async (args, ctx) => {
291
+ try {
292
+ const days = Number.parseInt(args.trim(), 10);
293
+ ctx.ui.notify(await instance(ctx.cwd).auditText(Number.isFinite(days) && days > 0 ? days : 7), "info");
294
+ } catch (error) {
295
+ note(`[skill-evolve] 审计命令失败:${String(error)}`);
296
+ ctx.ui.notify(`技能审计失败:${String(error)}`, "error");
297
+ }
298
+ },
299
+ });
300
+ }