pi-patch-keeper 1.1.0 → 1.2.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.
Files changed (4) hide show
  1. package/README.en.md +40 -27
  2. package/README.md +36 -42
  3. package/index.ts +17 -10
  4. package/package.json +2 -2
package/README.en.md CHANGED
@@ -1,44 +1,57 @@
1
- # pi-patch-keeper — patch self-healing and status checks (pi extension)
1
+ # pi-patch-keeper — pi configuration guards (pi extension)
2
2
 
3
- Turns "patches to other people's packages must be re-applied after every upgrade" into startup self-healing with an explicit report.
3
+ > Requires pi >= 0.74 · Node >= 20 · MIT · no runtime dependencies · no PiDeck or third-party patching
4
4
 
5
- ## What it heals
5
+ Checks three classes of hazards in pi's own configuration at session start: what can be fixed is fixed, the rest is reported clearly.
6
6
 
7
- | Target | Action |
8
- | --- | --- |
9
- | pi-lens wordindex patch (symbol_search stuck on "building") | fixes in place at startup; stops when upstream ships the fix (warm_cache_adopted) |
10
- | pi-memory exit summary / section precedence | fixes in place, backup first |
11
- | pi-tactician section precedence | fixes in place, backup first |
12
- | PiDeck builtin extensions (5, dual copies) | restores from the snapshot copy when a release overwrites them |
13
- | PiDeck app.asar (3 patches) | detect only (file locked while PiDeck runs) |
14
- | force-typed extension order | auto-moves known force-typed packages to the end, backup first |
15
- | manual version vs injected version | warns once per mismatch pair |
7
+ ## Features
8
+
9
+ - **codemode single-source check**: disabling `builtin:codemode` and installing `pi-codemode-guard` must agree.
10
+ - **force-order guard**: force-type extensions (e.g. rtk / cache-optimizer) must sit last in `packages`, otherwise later section writes are silently dropped; wrong order is fixed automatically (with a backup).
11
+ - **Environment fingerprint drift**: notifies when `settings.json` packages / defaultTools / enabledModels / extensions / codemode change, since manuals or prior verification may be stale.
12
+ - **Injection alignment**: compares the injected `APPEND_SYSTEM.md` version with the on-disk version and, on mismatch, tells the model to trust disk.
13
+ - Commands `/patch-keeper` (status) and `/patch-keeper fix` (repair force order).
16
14
 
17
15
  ## Install
18
16
 
19
- pi install npm:pi-patch-keeper
20
- pi install ./pi-patch-keeper
17
+ ```bash
18
+ pi install npm:pi-patch-keeper
19
+ pi install ./pi-patch-keeper
20
+ ```
21
+
22
+ ## Configuration
21
23
 
22
- Reload or restart the session after installing.
24
+ **No config file**: the package only reads `settings.json` and the manual.
23
25
 
24
- ## Commands
26
+ | Item | Path | Description |
27
+ | --- | --- | --- |
28
+ | Fingerprint | `~/.pi/patch-keeper.json` | Settings snapshot from the previous start, used for drift detection (self-maintained) |
29
+ | Manual sentinel | `~/.pi/patch-keeper-manual.json` | Manual version / content signature, catches "content changed without a version bump" |
30
+ | Fix backup | `~/.pi/agent/settings.json.bak-force-order-*` | Backup created before repairing force order |
25
31
 
26
- | Command | Effect |
32
+ ### Commands
33
+
34
+ | Command | Purpose |
27
35
  | --- | --- |
28
- | /patch-keeper | status of every patch face |
29
- | /patch-keeper fix | heal what can be healed now |
36
+ | `/patch-keeper` | Show codemode / force / fingerprint / injection-alignment status |
37
+ | `/patch-keeper fix` | Repair force order in `packages` (with backup) |
38
+
39
+ ### Not included (local-only module)
40
+
41
+ PiDeck builtin patch restoration, read-only asar checks, pi-lens / pi-memory / pi-tactician source patches, local snapshot restore — all depend on the PiDeck install directory, third-party internals or local paths, so they are **not shipped** with this package. They live in a local module `patch-keeper-local` (`/patch-keeper-local`).
30
42
 
31
- ## Discipline
43
+ ## How it works
32
44
 
33
- - Never returns systemPrompt; only notifies.
34
- - Any failure is reported as a hint, never thrown.
35
- - All paths derive from the home directory, so it survives machine, username and drive changes.
45
+ - On factory startup it compares fingerprints and self-checks force order; failures only notify, never throw or disturb the session.
46
+ - Injection alignment compares versions in `before_agent_start` and only writes its own notification and trailing message; it **never returns systemPrompt** (which would replace the whole prompt and drop other sections).
47
+ - All writes are atomic (temp file + rename).
36
48
 
37
- ## Limits
49
+ ## Limitations
38
50
 
39
- - asar patches cannot be applied while PiDeck is running; the extension only reports them.
40
- - Upstream patches are detected by marker strings; a substantially rewritten upstream file is reported as unknown instead of being patched.
51
+ - Covers pi's own configuration only, not third-party plugin behaviour.
52
+ - Force-order repair targets a built-in list of known force-type packages.
53
+ - Fingerprint drift reports what changed, not whether the new value is correct.
41
54
 
42
55
  ## License
43
56
 
44
- MIT.
57
+ MIT
package/README.md CHANGED
@@ -1,63 +1,57 @@
1
- # pi-patch-keeper —— 补丁自愈与状态检测(pi 扩展)
1
+ # pi-patch-keeper —— pi 配置守卫(pi 扩展)
2
2
 
3
- ## 安装
4
-
5
- npm 发布版:`pi install npm:pi-patch-keeper`
6
-
7
- 本地目录:`pi install <path/to/patch-keeper>`
8
-
9
- 安装后 `/reload` 或重启会话生效(扩展源码改动需重启)。
3
+ > 适用 pi ≥ 0.74 · Node ≥ 20 · MIT · 无运行时依赖 · 不含 PiDeck / 第三方补丁
10
4
 
11
- **是什么**:把「对别人的东西」的改动(**12 个面**:PiDeck builtin ×5 双份 · npm 包 ×4 · app.asar ×3)从「升级后手工重跑」变成**启动自愈 + 明确报告**——外置、模块化、抗上游升级。全量清单见 `~/Desktop/pi-env-snapshot/05-脚本补丁/PATCH-REGISTRY.md`(唯一权威源)。
5
+ 在每次会话启动时检查 pi 自身配置的三类隐患,能修的直接修,不能修的明确报出来。
12
6
 
13
- ## 来源与借鉴
7
+ ## 功能
14
8
 
15
- 原创,无上游实现。设计灵感来自本机长期维护 PiDeck / pi 补丁的实践:补丁会被升级覆盖,故需要启动时自检与自愈。
9
+ - **codemode 单点一致性**:禁 `builtin:codemode` 与安装 `pi-codemode-guard` 必须同时成立。
10
+ - **force 顺序守卫**:force 型扩展(如 rtk / cache-optimizer)必须排在 `packages` 末尾,否则其后的 section 写入会被静默丢弃;发现乱序自动修复(先备份)。
11
+ - **环境指纹漂移**:`settings.json` 的 packages / defaultTools / enabledModels / extensions / codemode 变化时提示「手册或上轮验证结论可能过期」。
12
+ - **注入对齐**:比对注入的 `APPEND_SYSTEM.md` 版本与磁盘版本,不一致时提示「以磁盘版为准」,并同步回灌模型。
13
+ - 命令 `/patch-keeper` 查看全部状态,`/patch-keeper fix` 修复 force 顺序。
16
14
 
17
- ## 机制(源码级)
18
-
19
- 1. **session_start 自检**(每次 pi 启动):
20
- - **pi-lens wordindex 补丁:能修的直接修**——本扩展是 user 扩展(加载 rank 2),先于 pi-lens 的 package rank 4;factory 内同步改 `dist/index.js`,pi-lens 随后 import 到的即打过补丁的内容(**当轮生效**);半打状态先还原再重打(幂等)。**上游发版后停手**:dist 出现 `warm_cache_adopted`(#4583 / PR #4586)即视为 A 已修复——跳过 A、只补 B(B 上游未修),`/patch-keeper` 显示「上游已修复 #4583」。
21
- - **pi-memory exit summary 空行规范化:能修的直接修**——改 npm pi-memory 的 `index.ts`(render 侧规范块级空行;幂等;写前留 `.bak-exitsummary-<stamp>` 备份)。
22
- - **PiDeck asar 三补丁:只检测不修**——PiDeck 运行中文件被锁、插件无权碰 Electron 主进程;检测到未打补丁时提示跑 `launch-pideck.cmd`(启动前自动重打)或快照 `05-脚本补丁/` 下脚本。
23
- 2. **before_agent_start 注入对齐检测**:比对「本轮注入的手册版本」vs「磁盘版」——不一致提示**一次**(同 pair 去重);方向不限(注入滞后 / 磁盘回滚都可能)。
24
- 3. **环境漂移指纹**:`~/.pi/patch-keeper.json` 记录 settings 关键字段(packages / defaultTools 等)——支撑漂移检测。
25
- 4. **force 顺序守卫(自动修复)**:packages 中条件 force 型(pi-rtk-optimizer / pi-cache-optimizer)须在末尾——启动与 /patch-keeper fix 时自动移到末尾(先备份 settings.json.bak-force-order-*),修复幂等;其后 section 写入不会被静默丢弃。
26
- 5. **force 型扩展扫描(疑似)**:扫描 packages 入口源码中的 return { systemPrompt } / forceSystemPrompt,报告疑似无条件 force 的包(会覆盖 system、使其后 section 静默丢失);位于中间者启动时提示。先写 section、只有旧宿主才 force 的归为「兼容兜底」,不计入疑似。
27
-
28
- ## 自我回灌(条件触发型)
15
+ ## 安装
29
16
 
30
- 与 caveman / ptc-meter / think-anchor 的「每轮回灌」不同:**常态零注入**,只在异常时出声一次——
17
+ ```bash
18
+ pi install npm:pi-patch-keeper
19
+ pi install ./pi-patch-keeper
20
+ ```
31
21
 
32
- - 每轮 before_agent_start 比对「本轮实际注入的手册版本」vs「磁盘版本」;
33
- - 不一致(注入滞后 / 磁盘回滚均可能)→ 一次 notify 提示「以磁盘为准、先读回全文」(同 pair 去重,不重复打扰);
34
- - notify 用户 + 回灌模型(nextTurn,四律②:用户侧收到后模型也须知道,否则「以磁盘为准」停在用户侧);检测失败静默。
22
+ ## 配置
35
23
 
36
- ## 纪律(回灌四律实践)
24
+ **无配置文件**:本包只读 `settings.json` 与手册,不引入自己的配置项。
37
25
 
38
- - **绝不返回 systemPrompt**——只 notify;任何失败静默(检测不该影响会话,绝不抛错)。
39
- - 路径全部从 home / `%LOCALAPPDATA%` 推导——换机、换用户名、复刻包到别机均可用。
26
+ | 项 | 路径 | 说明 |
27
+ | --- | --- | --- |
28
+ | 环境指纹 | `~/.pi/patch-keeper.json` | 上次启动时的配置快照,用于漂移对比(自动维护) |
29
+ | 手册哨兵 | `~/.pi/patch-keeper-manual.json` | 手册版本 / 内容签名,用于检测「改了内容没升版本」 |
30
+ | 自动修复备份 | `~/.pi/agent/settings.json.bak-force-order-*` | 修复 force 顺序前的备份 |
40
31
 
41
- ## 配置与命令
32
+ ### 命令
42
33
 
43
- | 项 | 值 |
34
+ | 命令 | 作用 |
44
35
  | --- | --- |
45
- | 指纹文件 | `~/.pi/patch-keeper.json`(随 settings 变化同步) |
46
- | 命令 | `/patch-keeper`(状态)· `/patch-keeper fix`(能修的立即修) |
36
+ | `/patch-keeper` | 输出 codemode / force / 指纹 / 注入对齐状态 |
37
+ | `/patch-keeper fix` | 修复 `packages` 的 force 顺序(带备份) |
47
38
 
48
- ## 生效与回滚
39
+ ### 不包含(本机专用模块)
49
40
 
50
- - 生效:装上即用(每次会话启动自检);代码改动 `/reload` 或重启。
51
- - 回滚:删本目录;已打补丁保持原样(回退用 `05-脚本补丁/` 下脚本各自的 rollback)。
41
+ PiDeck builtin 补丁双份恢复、asar 只读检测、pi-lens / pi-memory / pi-tactician 源码补丁、本机快照恢复 —— 这些依赖 PiDeck 安装目录、第三方包内部实现与本机路径,**不随本包发布**,由本机模块 `patch-keeper-local` 提供,命令 `/patch-keeper-local`。
52
42
 
53
- ## 测试
43
+ ## 机制
54
44
 
55
- 源码仓库内的隔离自测覆盖:force 顺序三例、命令注册、版本不一致告警一次的 pair 去重、一致静默、空 event 不抛、session_start 失败静默、自建扩展被覆盖后从快照恢复、上游已修标记(A 停打 / 仅补 B)。
45
+ - 启动时(factory)执行指纹对比与 force 自检;失败只提示,绝不抛错、不影响会话。
46
+ - 注入对齐在 `before_agent_start` 比较版本,只写自己的通知与尾部消息,**绝不返回 systemPrompt**(避免覆盖式替换丢掉其他 section)。
47
+ - 所有写盘走「临时文件 + rename」原子写。
56
48
 
57
- ## 环境依赖与降级
49
+ ## 限制
58
50
 
59
- 补丁目标(npm 包与 PiDeck builtin)不存在时对应项跳过并只报状态,不报错;本机快照目录(备份与恢复来源)缺失时,恢复类动作跳过,自愈仍然工作。适配任意用户名与安装路径。
51
+ - 只覆盖 pi 自身配置;不检查第三方插件行为。
52
+ - force 顺序修复只对已知的 force 型包生效(列表内置,随版本更新)。
53
+ - 指纹对比只报「变了什么」,不判断新值是否正确。
60
54
 
61
55
  ## License
62
56
 
63
- MIT.
57
+ MIT
package/index.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * - 任何失败都只 notify,不抛错、不影响会话(纪律:不合法就跳过 + 记日志,绝不崩溃)。
21
21
  */
22
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
23
- import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync } from "node:fs";
23
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync, renameSync } from "node:fs";
24
24
  import { createHash } from "node:crypto";
25
25
  import * as os from "node:os";
26
26
  import * as path from "node:path";
@@ -140,7 +140,7 @@ function healPiMemory(): Result {
140
140
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
141
141
  const bak = `${PI_MEMORY}.bak-exitsummary-${stamp}`;
142
142
  if (!existsSync(bak)) copyFileSync(PI_MEMORY, bak);
143
- writeFileSync(PI_MEMORY, next, "utf8");
143
+ writeAtomic(PI_MEMORY, next);
144
144
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
145
145
  return { ok: true, detail: "已自动修补(exit summary 空行规范化)—— 本轮即生效" };
146
146
  }
@@ -157,7 +157,7 @@ function healMemorySection(): Result {
157
157
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
158
158
  const bak = `${PI_MEMORY}.bak-memsec-${stamp}`;
159
159
  if (!existsSync(bak)) copyFileSync(PI_MEMORY, bak);
160
- writeFileSync(PI_MEMORY, next, "utf8");
160
+ writeAtomic(PI_MEMORY, next);
161
161
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
162
162
  return { ok: true, detail: "已自动修补(memory 段改 section)—— 重启后生效" };
163
163
  }
@@ -174,7 +174,7 @@ function healTactician(): Result {
174
174
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
175
175
  const bak = `${TACT}.bak-tactician-${stamp}`;
176
176
  if (!existsSync(bak)) copyFileSync(TACT, bak);
177
- writeFileSync(TACT, next, "utf8");
177
+ writeAtomic(TACT, next);
178
178
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
179
179
  return { ok: true, detail: "已自动修补(tactician 改 section)—— 重启后生效" };
180
180
  }
@@ -192,6 +192,13 @@ function safeRead(p: string): string | null {
192
192
  try { return readFileSync(p, "utf8"); } catch { return null; }
193
193
  }
194
194
 
195
+ /** 原子写:临时文件 + rename(崩溃/被杀不留半写文件)。 */
196
+ function writeAtomic(file: string, data: string | Buffer): void {
197
+ const tmp = file + ".tmp-" + process.pid;
198
+ writeFileSync(tmp, data);
199
+ renameSync(tmp, file);
200
+ }
201
+
195
202
  /** 统计 needle 在原串里的出现次数。 */
196
203
  function count(hay: string, needle: string): number {
197
204
  let n = 0, i = 0;
@@ -230,7 +237,7 @@ function healPiLens(): Result {
230
237
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
231
238
  const bak = `${PI_LENS}.bak-wordindex-${stamp}`;
232
239
  if (!existsSync(bak)) copyFileSync(PI_LENS, bak);
233
- writeFileSync(PI_LENS, next, "utf8");
240
+ writeAtomic(PI_LENS, next);
234
241
  } catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
235
242
  return { ok: true, detail: state === "upstream" ? `上游已修复 #4583 —— 跳过 A,仅补 B${a1 ? "(A 残留,建议 npm 重装清除)" : ""}` : `已自动修补(原状态 ${state})—— 本轮即生效` };
236
243
  }
@@ -265,7 +272,7 @@ function healMemoryFormat(): Result {
265
272
  const txt = safeRead(p);
266
273
  if (txt === null) continue;
267
274
  const norm = normMd(txt);
268
- if (norm !== txt) { try { writeFileSync(p, norm, "utf8"); fixed += 1; } catch { /* 静默 */ } }
275
+ if (norm !== txt) { try { writeAtomic(p, norm); fixed += 1; } catch { /* 静默 */ } }
269
276
  }
270
277
  };
271
278
  walk(dir);
@@ -320,7 +327,7 @@ function fingerprintDrift(): string[] {
320
327
  let prev: Record<string, string[]> | null = null;
321
328
  const prevRaw = safeRead(FINGERPRINT);
322
329
  if (prevRaw) { try { prev = JSON.parse(prevRaw) as Record<string, string[]>; } catch { prev = null; } }
323
- try { writeFileSync(FINGERPRINT, JSON.stringify(cur, null, 1), "utf8"); } catch { /* 静默 */ }
330
+ try { writeAtomic(FINGERPRINT, JSON.stringify(cur, null, 1)); } catch { /* 静默 */ }
324
331
  if (prev === null) return [];
325
332
  const out: string[] = [];
326
333
  for (const k of ["packages", "defaultTools", "enabledModels", "extensions", "codemode"] as const) {
@@ -371,7 +378,7 @@ function manualDrift(): string | null {
371
378
  let prev: { ver?: string; sig?: string } | null = null;
372
379
  const raw = safeRead(MANUAL_STATE);
373
380
  if (raw) { try { prev = JSON.parse(raw); } catch { prev = null; } }
374
- try { writeFileSync(MANUAL_STATE, JSON.stringify({ ver, sig })); } catch { /* ignore */ }
381
+ try { writeAtomic(MANUAL_STATE, JSON.stringify({ ver, sig })); } catch { /* ignore */ }
375
382
  if (prev && prev.ver === ver && prev.sig !== sig) return `手册内容已变但版本号未升(${ver})—— bump-manual 或撤销未登记改动`;
376
383
  return null;
377
384
  }
@@ -411,7 +418,7 @@ function fixForceOrder(): string | null {
411
418
  try {
412
419
  copyFileSync(SETTINGS, `${SETTINGS}.bak-force-order-${Date.now()}`);
413
420
  s.packages = next;
414
- writeFileSync(SETTINGS, JSON.stringify(s, null, 2), "utf8");
421
+ writeAtomic(SETTINGS, JSON.stringify(s, null, 2));
415
422
  } catch (e) { return `force 顺序自动修复失败:${(e as Error).message}`; }
416
423
  return `force 顺序已自动修复:${FORCE_TAIL.filter((n) => next.includes(n)).join(" / ")} 移到 packages 末尾`;
417
424
  }
@@ -472,7 +479,7 @@ function healSelfExtension(): Result {
472
479
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
473
480
  const bak = `${SELF_EXT}.bak-overwrite-${stamp}`;
474
481
  if (!existsSync(bak)) copyFileSync(SELF_EXT, bak);
475
- writeFileSync(SELF_EXT, snap, "utf8");
482
+ writeAtomic(SELF_EXT, snap);
476
483
  } catch (e) { return { ok: false, detail: `恢复失败:${(e as Error).message}` }; }
477
484
  return { ok: true, detail: "疑似被发布精简版覆盖 —— 已从快照恢复(重启后生效)" };
478
485
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-patch-keeper",
3
- "version": "1.1.0",
4
- "description": "Startup self-healing for pi/pi-lens/pi-memory/PiDeck patches, plus force-typed extension order guard and status report.",
3
+ "version": "1.2.0",
4
+ "description": "pi config guards: codemode single-source check, force-order guard (auto-fix), settings fingerprint drift, injected-manual alignment. (PiDeck / third-party source patches are a local-only module, not bundled.)",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [