@andares/pi-toolkits 0.10.0 → 0.11.1
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/README.md +88 -2
- package/README.zh-CN.md +72 -2
- package/package.json +7 -3
- package/src/features/ask/index.ts +2 -1
- package/src/features/ask/prompt.ts +2 -1
- package/src/features/init/analyze.ts +162 -0
- package/src/features/init/apply.test.ts +264 -0
- package/src/features/init/apply.ts +693 -0
- package/src/features/init/audit.test.ts +292 -0
- package/src/features/init/audit.ts +586 -0
- package/src/features/init/coverage.test.ts +93 -0
- package/src/features/init/coverage.ts +163 -0
- package/src/features/init/detect.test.ts +237 -0
- package/src/features/init/detect.ts +808 -0
- package/src/features/init/dispatch.ts +135 -0
- package/src/features/init/index.test.ts +331 -0
- package/src/features/init/index.ts +569 -0
- package/src/features/init/plan.test.ts +261 -0
- package/src/features/init/plan.ts +867 -0
- package/src/features/init/render.ts +131 -0
- package/src/features/init/spec.ts +429 -0
- package/src/features/init/templates/agents-skeleton.md +44 -0
- package/src/features/init/templates/memory-readme.md +99 -0
- package/src/features/init/templates/signpost.md +15 -0
- package/src/features/init/templates/state-gitignore +6 -0
- package/src/features/init/templates.test.ts +58 -0
- package/src/features/init/templates.ts +79 -0
- package/src/features/init/test-helpers.ts +89 -0
- package/src/features/init/tool.ts +72 -0
- package/src/features/init/types.ts +284 -0
- package/src/features/init/ui.ts +160 -0
- package/src/features/permission/audit.test.ts +10 -10
- package/src/features/permission/constants.test.ts +28 -2
- package/src/features/permission/constants.ts +28 -3
- package/src/features/permission/index.test.ts +76 -33
- package/src/features/permission/policy.test.ts +48 -11
- package/src/features/permission/policy.ts +16 -1
- package/src/features/permission/units.test.ts +25 -9
- package/src/features/permission/units.ts +49 -24
- package/src/index.ts +3 -0
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ After installing, verify in any pi session:
|
|
|
56
56
|
- [🔎 review — third-party code review](#review)
|
|
57
57
|
- [🗜️ compact — dedicated compaction model](#compact)
|
|
58
58
|
- [🚀 autostart — session startup automation](#autostart)
|
|
59
|
+
- [🧠 init — agent memory framework](#init)
|
|
59
60
|
- [Development](#development)
|
|
60
61
|
- [Contributing](#contributing)
|
|
61
62
|
- [License](#license)
|
|
@@ -75,6 +76,7 @@ After installing, verify in any pi session:
|
|
|
75
76
|
| 🔎 **review** third-party code review | `/third-review` | summon the configured review model to check + fix the task just finished, focusing on "is this round's requirement implemented / is anything missing / is anything wrong" |
|
|
76
77
|
| 🗜️ **compact** dedicated compaction model | `/compact-model` | give `/compact` and auto-compact a cheap fast model to summarize with, leaving the main model free to focus on the conversation; zero overhead when unconfigured, falls back to pi's default on any failure |
|
|
77
78
|
| 🚀 **autostart** session startup automation | `.pi/autostart.toml` | declare slash commands to run automatically at session start, per project (first use case: mounting multi-repo directories with `/add-dir`); never runs in untrusted directories, unregistered commands are skipped instead of being sent to the model |
|
|
79
|
+
| 🧠 **init** agent memory framework | `/ask-init` | install or adopt a fixed memory layout (`AGENTS.md` + `specs/` + `.agents/memory` + `.agents/implementations`), migrate an existing one with a snapshot and a coverage table, and audit it later for drift, budgets and duplicated facts |
|
|
78
80
|
|
|
79
81
|
---
|
|
80
82
|
|
|
@@ -114,7 +116,7 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
|
|
|
114
116
|
|
|
115
117
|
Repeating the argument form with the modes already in force is a **no-op**: no second "ON" notice and no loss of the session grants collected so far. Changing any mode records the change and clears those grants, because they were made under the old policy.
|
|
116
118
|
|
|
117
|
-
**Defaults**: in-project read/write allowed, outside read allowed, outside write asks.
|
|
119
|
+
**Defaults**: in-project read/write allowed, outside read allowed, outside write asks — except writes under the OS temp root, which are scratch space and pass directly. An explicit `out write = deny` still denies them.
|
|
118
120
|
|
|
119
121
|
| Scope | reads | writes / exec |
|
|
120
122
|
| --- | --- | --- |
|
|
@@ -123,19 +125,22 @@ Repeating the argument form with the modes already in force is a **no-op**: no s
|
|
|
123
125
|
|
|
124
126
|
`exec` is folded into `write`: any command that changes state counts as a write.
|
|
125
127
|
|
|
128
|
+
**Temporary write zone**: a write outside the project but under the OS temp root (`os.tmpdir()`, `/var/tmp`, `/dev/shm`) is allowed without a dialog when `out write` is `ask` (the audit layer is `temp`). `/tmp` is shared scratch space, so this is a convenience rule, not an isolation boundary; deleting a temp root itself (`rm -rf /tmp`) still hits the destructive table.
|
|
129
|
+
|
|
126
130
|
**What is inspected**
|
|
127
131
|
|
|
128
132
|
- **bash** is decomposed into simple commands — `&&`, `||`, `;`, pipes, `$(…)`, backticks, `bash -c`, `eval`, the common indirection wrappers (`sudo`, `env`, `xargs`, `find -exec`, …), heredocs — and judged per command. `cd` folds into the commands after it; the strictest result wins, so `cd /tmp && rm x` cannot ride on the first segment.
|
|
129
133
|
- **path-bearing tools** (`read` / `write` / `edit` / `grep` / `find` / `ls`) are judged by their own path; every other tool is judged by the working directory.
|
|
130
134
|
- **unresolvable input** — a non-literal `cd` or `git -C`, an unknown shell variable in a path, an unterminated heredoc or substitution — is asked about regardless of mode (fail closed), never guessed.
|
|
131
135
|
- **`git -C <dir>`** is resolved like a local `cd`: relative path arguments (and the repository) are judged against `<dir>`, not the working directory.
|
|
136
|
+
- **device sinks and data arguments** — `/dev/null`, `/dev/stdout` and `/dev/stderr` are never boundary targets, whether written as a redirect (`cmd > /dev/null`) or an operand (`curl -o /dev/null`); the arguments of `echo` / `printf` are data, not paths.
|
|
132
137
|
|
|
133
138
|
**Answers**: `a` allow once · `s` allow this directory for the session · `d` deny · `f` deny this directory for the session. Session grants are kept in memory as `(read|write, directory)`, matched by containment, and cleared when the mode is turned off or the session ends. A session grant outranks the destructive table — once you have allowed an operation for the session, repeating it does not ask again. A command the parser cannot resolve is granted by its exact text instead, so repeating the same opaque command does not ask again.
|
|
134
139
|
|
|
135
140
|
**Fixed safety tables** (independent of the four modes):
|
|
136
141
|
|
|
137
142
|
- **credentials are denied in every mode** — `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.pi/agent/auth*.json`, `.env` files (templates excepted). Without this, `out read = allow` would hand out `~/.ssh/id_rsa`.
|
|
138
|
-
- **destructive commands always ask** — `rm` of `/`, the home directory,
|
|
143
|
+
- **destructive commands always ask** — `rm` of `/`, the home directory, the working directory, or a temp root (`/tmp`, `/var/tmp`, `/dev/shm`); `git reset --hard`; `git clean -f`; `git push --force`; package `publish`; `dd` to a raw device; `mkfs*`; shutdown/reboot; `chmod -R 777`; anything through `sudo`. Scope alone cannot express "destructive".
|
|
139
144
|
|
|
140
145
|
**Approval log** (write-only telemetry, on by default)
|
|
141
146
|
|
|
@@ -367,6 +372,87 @@ An **identical command line** declared by both sources runs once: a duplicate is
|
|
|
367
372
|
|
|
368
373
|
---
|
|
369
374
|
|
|
375
|
+
## 🧠 init — agent memory framework
|
|
376
|
+
|
|
377
|
+
<a name="init"></a>
|
|
378
|
+
|
|
379
|
+
`/ask-init` installs one opinionated memory shape into a project, or adopts the
|
|
380
|
+
documentation that is already there. The point is not the directory names — it is
|
|
381
|
+
the split between three things that decay when mixed:
|
|
382
|
+
|
|
383
|
+
| Family | Location | Answers | Lifetime |
|
|
384
|
+
| --- | --- | --- | --- |
|
|
385
|
+
| **plan** | `specs/` (`PLAN.md` + `<unit>.md`) | what to do, how it will be accepted | written before work, status updated at close |
|
|
386
|
+
| **memory** | `.agents/memory/` | what is known, decided, and why | long-lived; append and compact only |
|
|
387
|
+
| **evidence** | `.agents/implementations/` | what changed, when, with what proof | one record per change, never rewritten |
|
|
388
|
+
|
|
389
|
+
Only agent-facing material moves under the hidden `.agents/` directory, so the
|
|
390
|
+
repository root stays readable: `AGENTS.md`, `specs/`, `design/`, `docs/`,
|
|
391
|
+
`legal/` remain visible and reviewable.
|
|
392
|
+
|
|
393
|
+
**Commands**:
|
|
394
|
+
|
|
395
|
+
| Command | What it does |
|
|
396
|
+
| --- | --- |
|
|
397
|
+
| `/ask-init` | detect → show the plan → (confirm) → move/scaffold/snapshot → dispatch the prose work to the model |
|
|
398
|
+
| `/ask-init plan [--deep]` | print the plan and write nothing (`--dry-run` is the same) |
|
|
399
|
+
| `/ask-init check` | audit the installed framework (read-only; writes only a runtime report under `.agents/framework/reports/`) |
|
|
400
|
+
| `/ask-init roots` | sync the directories added with `pi-add-dir` into the framework state |
|
|
401
|
+
| `/ask-init --yes` | skip the confirmation panel |
|
|
402
|
+
| `/ask-init --lang zh\|en` | language for generated prose (default: detected from the project's own docs) |
|
|
403
|
+
|
|
404
|
+
**What the mechanical half guarantees**:
|
|
405
|
+
|
|
406
|
+
1. **Snapshot first** — every path the run may write (including every file whose
|
|
407
|
+
path references are rewritten) is copied to `.agents/framework/snapshots/<stamp>/`
|
|
408
|
+
with a sha256 manifest and the `git status` of that moment.
|
|
409
|
+
2. **Writes stay inside the repository** — added roots are never written from
|
|
410
|
+
extension code; a scope card there is an ordinary tool call and therefore goes
|
|
411
|
+
through the permission gate.
|
|
412
|
+
3. **Nothing is clobbered** — a create skips an existing path; a move refuses a
|
|
413
|
+
non-empty destination and turns the conflict into a manual task.
|
|
414
|
+
4. **Counts reconcile** — the reference rewrite uses the same bare-reference
|
|
415
|
+
predicate as the survey, so "N occurrences in M files" from the plan is what
|
|
416
|
+
the run reports (and the rewrite report lists every file).
|
|
417
|
+
5. **No silent loss** — a coverage table (`.agents/framework/reports/coverage-*.md`)
|
|
418
|
+
gets one row per migrated source section: found, declared with a destination, or
|
|
419
|
+
open with a reason. The audit fails while any row is open.
|
|
420
|
+
|
|
421
|
+
The prose half is a bounded work order with acceptance criteria per task: fill the
|
|
422
|
+
entry, merge the legacy plan, triage `todos/` into specs, deduplicate sections,
|
|
423
|
+
record budget debt, fill the coverage table. The model closes its own loop with the
|
|
424
|
+
`framework_audit` tool (activated for the run only).
|
|
425
|
+
|
|
426
|
+
**Migration of an existing project** moves `memory/`, `implementations/`,
|
|
427
|
+
`prompts/` under `.agents/` (one `git mv` each), moves `memory/PLAN.md` to
|
|
428
|
+
`specs/PLAN.md` byte-identically, and rewrites path references — with the counts
|
|
429
|
+
reported before and after. `todos/` is retired: requirement wording goes into the
|
|
430
|
+
unit spec verbatim. The default pass is conservative: install, fix mechanical
|
|
431
|
+
drift (tombstones, duplicated sections, appendix order), and record an over-budget
|
|
432
|
+
`AGENTS.md` as debt instead of rewriting prose; `--deep` opts into structural edits.
|
|
433
|
+
|
|
434
|
+
**Audit codes** (stable, so reports compare across runs): `E-PATH-01` bare legacy
|
|
435
|
+
paths, `E-STATE-01` memory gitignored, `W-STATE-01` memory not yet committed, `E-INDEX-01` index ↔ file
|
|
436
|
+
mismatch, `E-LINK-01` broken internal links, `W-BUDGET-01` `AGENTS.md` over budget,
|
|
437
|
+
`W-DUP-01` the same section in two files, `W-ORDER-01` sections appended after the
|
|
438
|
+
appendix, `W-COV-01` open coverage rows, `W-TODO-01` retired `todos/` still present,
|
|
439
|
+
`W-ROOT-02` zero-byte root files, `W-SCAFFOLD-01` unfilled `TODO(ask-init)` markers,
|
|
440
|
+
`I-INSTALL-01` framework not installed yet.
|
|
441
|
+
|
|
442
|
+
**Multi-repository workspaces**: memory lives in the main repository only.
|
|
443
|
+
`pi-add-dir` injects each added root's `AGENTS.md` verbatim into every request, so a
|
|
444
|
+
root entry must stay a **scope card** (≤30 lines: what lives there, its commands,
|
|
445
|
+
which main-repo memory file describes it) — never a second copy of the rules.
|
|
446
|
+
|
|
447
|
+
The framework contract shipped into a project is English (it is read by models and
|
|
448
|
+
published); text written *into* a project follows that project's language.
|
|
449
|
+
|
|
450
|
+
> **After the run**, `/reload` so the new `AGENTS.md` enters the prompt. The
|
|
451
|
+
> framework version lives in `.agents/framework/state.toml`; a project whose state
|
|
452
|
+
> version is newer than the installed tool is never migrated.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
370
456
|
## 🛠️ Development
|
|
371
457
|
|
|
372
458
|
<a name="development"></a>
|
package/README.zh-CN.md
CHANGED
|
@@ -56,6 +56,7 @@ pi -e ./src/index.ts
|
|
|
56
56
|
- [🔎 review — 第三方代码评审](#review)
|
|
57
57
|
- [🗜️ compact — 独立压缩模型](#compact)
|
|
58
58
|
- [🚀 autostart — 会话启动自动化](#autostart)
|
|
59
|
+
- [🧠 init — agent 记忆框架](#init)
|
|
59
60
|
- [开发](#development)
|
|
60
61
|
- [贡献](#contributing)
|
|
61
62
|
- [许可](#license)
|
|
@@ -75,6 +76,7 @@ pi -e ./src/index.ts
|
|
|
75
76
|
| 🔎 **review** 第三方代码评审 | `/third-review` | 召唤配置的评审模型,对刚完成的任务做查+修,重点检查「本轮需求实现 / 是否有遗漏 / 是否有错误」 |
|
|
76
77
|
| 🗜️ **compact** 独立压缩模型 | `/compact-model` | 给 `/compact` 与 auto-compact 配一个便宜快模型做总结,主模型专注对话;未配置零开销、任何异常回落 pi 默认 |
|
|
77
78
|
| 🚀 **autostart** 会话启动自动化 | `.pi/autostart.toml` | 项目内声明启动时自动执行的斜杠命令(首批用例:`/add-dir` 多仓库挂载);未信任不执行、未注册命令跳过不发给模型 |
|
|
79
|
+
| 🧠 **init** agent 记忆框架 | `/ask-init` | 安装或接管一套固定的记忆结构(`AGENTS.md` + `specs/` + `.agents/memory` + `.agents/implementations`),带快照与覆盖表迁移旧文档,之后可随时审计漂移、预算与重复事实 |
|
|
78
80
|
|
|
79
81
|
---
|
|
80
82
|
|
|
@@ -114,7 +116,7 @@ pi -e ./src/index.ts
|
|
|
114
116
|
|
|
115
117
|
参数形式重复执行、且四项模式与当前一致时是**空操作**:不会再出一条「ON」提示,也不会清掉已经攒下的会话授权。只有模式真的变了才会提示并清空授权——那些授权是按旧策略给的。
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
**默认值**:项目内读/写全放行,项目外读放行,项目外写询问——但 `os.tmpdir()`、`/var/tmp`、`/dev/shm` 下的临时写入直接放行。显式把 `out write` 设为 `deny` 时仍然拒绝。
|
|
118
120
|
|
|
119
121
|
| 作用域 | 读 | 写 / 执行 |
|
|
120
122
|
| --- | --- | --- |
|
|
@@ -123,19 +125,22 @@ pi -e ./src/index.ts
|
|
|
123
125
|
|
|
124
126
|
`exec` 折进 `write`:任何产生变更的执行都算写。
|
|
125
127
|
|
|
128
|
+
**临时写入区**:项目外、但落在操作系统临时根(`os.tmpdir()`、`/var/tmp`、`/dev/shm`)的写入,在 `out write = ask` 时直接放行(审计层记 `temp`)。`/tmp` 是共享草稿空间,这条是便利规则,不是隔离边界;**删除临时根本身**(`rm -rf /tmp`)仍然走破坏性表。
|
|
129
|
+
|
|
126
130
|
**检查方式**
|
|
127
131
|
|
|
128
132
|
- **bash** 先拆成简单命令 —— `&&`、`||`、`;`、管道、`$(…)`、反引号、`bash -c`、`eval`、常见间接包装(`sudo`、`env`、`xargs`、`find -exec`…)、heredoc —— 再逐条判定;`cd` 向前折叠,取最严格的结果,所以 `cd /tmp && rm x` 不会靠第一段蒙混过关
|
|
129
133
|
- **带路径的工具**(`read` / `write` / `edit` / `grep` / `find` / `ls`)按自己的路径判定;其它工具按工作目录判定
|
|
130
134
|
- **无法解析的输入** —— 非字面量 `cd` 或 `git -C`、路径里的未知 shell 变量、未闭合的 heredoc/命令替换 —— 一律询问,不看模式(fail closed),绝不猜测
|
|
131
135
|
- **`git -C <dir>`** 按局部 `cd` 处理:相对路径参数(和仓库本身)按 `<dir>` 判定,而不是工作目录
|
|
136
|
+
- **设备 sink 与数据参数** —— `/dev/null`、`/dev/stdout`、`/dev/stderr` 无论是重定向(`cmd > /dev/null`)还是普通操作数(`curl -o /dev/null`),都不算边界目标;`echo` / `printf` 的参数是数据,不按路径处理
|
|
132
137
|
|
|
133
138
|
**回答**:`a` 本次允许 · `s` 本会话允许该目录 · `d` 拒绝 · `f` 本会话拒绝该目录。会话授权在内存中按 `(读|写, 目录)` 保存、按包含关系匹配,关闭模式或会话结束时清空;**会话授权优先于破坏性表**——你按了 `s` 之后,重复同一操作不会再弹;解析不了完整路径的命令则按**完整命令文本**授权,重复同一条命令不会再问。
|
|
134
139
|
|
|
135
140
|
**固定安全表**(与四格模式无关):
|
|
136
141
|
|
|
137
142
|
- **凭据路径任何模式都拒绝** —— `~/.ssh`、`~/.gnupg`、`~/.aws`、`~/.pi/agent/auth*.json`、`.env` 文件(模板除外)。没有这一条,默认的「项目外读放行」就等于把 `~/.ssh/id_rsa` 交出去
|
|
138
|
-
- **破坏性命令永远询问** —— 对
|
|
143
|
+
- **破坏性命令永远询问** —— 对 `/`、家目录、工作目录或临时根(`/tmp`、`/var/tmp`、`/dev/shm`)的 `rm`;`git reset --hard`;`git clean -f`;`git push --force`;包 `publish`;`dd` 写裸设备;`mkfs*`;关机/重启;`chmod -R 777`;任何经 `sudo` 的命令。作用域本身表达不了「破坏性」
|
|
139
144
|
|
|
140
145
|
**审批日志**(只写遥测,默认开启)
|
|
141
146
|
|
|
@@ -367,6 +372,71 @@ run = [
|
|
|
367
372
|
|
|
368
373
|
---
|
|
369
374
|
|
|
375
|
+
## 🧠 init — agent 记忆框架
|
|
376
|
+
|
|
377
|
+
<a name="init"></a>
|
|
378
|
+
|
|
379
|
+
`/ask-init` 给项目安装一套有明确立场的记忆结构,或者接管项目里已经存在的文档。
|
|
380
|
+
重点不是目录名,而是把三样混在一起必然腐烂的东西分开:
|
|
381
|
+
|
|
382
|
+
| 家庭 | 位置 | 回答什么 | 生命周期 |
|
|
383
|
+
| --- | --- | --- | --- |
|
|
384
|
+
| **计划** | `specs/`(`PLAN.md` + `<unit>.md`) | 要做什么、怎么验收 | 开工前写,结项时更新状态 |
|
|
385
|
+
| **记忆** | `.agents/memory/` | 已知什么、定过什么、为什么 | 长期;只追加、只压缩 |
|
|
386
|
+
| **证据** | `.agents/implementations/` | 改了什么、什么时候、证据在哪 | 一次写定,不再改写 |
|
|
387
|
+
|
|
388
|
+
只有「纯给 agent 看」的东西进隐藏目录 `.agents/`,所以仓库根仍然清爽:
|
|
389
|
+
`AGENTS.md`、`specs/`、`design/`、`docs/`、`legal/` 都留在外面可读可审。
|
|
390
|
+
|
|
391
|
+
**命令**:
|
|
392
|
+
|
|
393
|
+
| 命令 | 作用 |
|
|
394
|
+
| --- | --- |
|
|
395
|
+
| `/ask-init` | 检测 → 出计划 → 确认 → 移动/脚手架/快照 → 把正文工作派发给模型 |
|
|
396
|
+
| `/ask-init plan [--deep]` | 只打印计划,不写任何盘(`--dry-run` 同义) |
|
|
397
|
+
| `/ask-init check` | 审计已装的框架(只读;只在 `.agents/framework/reports/` 留下运行期报告) |
|
|
398
|
+
| `/ask-init roots` | 把 `pi-add-dir` 添加的目录同步进框架状态 |
|
|
399
|
+
| `/ask-init --yes` | 跳过确认面板 |
|
|
400
|
+
| `/ask-init --lang zh\|en` | 生成正文的语言(默认按项目自身文档判定) |
|
|
401
|
+
|
|
402
|
+
**机械那一半保证什么**:
|
|
403
|
+
|
|
404
|
+
1. **先快照再动** —— 本次可能写的每一条路径(包括所有会被改写的引用文件)都复制到
|
|
405
|
+
`.agents/framework/snapshots/<stamp>/`,并带 sha256 清单与当时的 `git status`。
|
|
406
|
+
2. **只写仓内** —— 扩展代码绝不写 added root;在那里建 scope card 是普通工具调用,
|
|
407
|
+
因此会经过权限闸门。
|
|
408
|
+
3. **不覆盖** —— 创建时已存在的文件跳过;移动时目标非空则拒绝,并转成人工任务。
|
|
409
|
+
4. **计数对账** —— 引用改写与检测用同一个「裸路径」判定,计划里的「N 处 / M 个文件」
|
|
410
|
+
就是运行时报告的数字(并附逐文件清单)。
|
|
411
|
+
5. **不静默丢内容** —— 迁移的每个源章节在覆盖表里都有一行:命中、声明去向、或标记
|
|
412
|
+
OPEN 并写理由;只要还有 OPEN,审计就不算通过。
|
|
413
|
+
|
|
414
|
+
正文那一半是一份带验收标准的工作单:填空入口、合并旧计划、把 `todos/` 收敛进 specs、
|
|
415
|
+
去重章节、登记预算债务、填覆盖表。模型用只在本轮激活的 `framework_audit` 工具自检收口。
|
|
416
|
+
|
|
417
|
+
**迁移既有项目**会把 `memory/`、`implementations/`、`prompts/` 移到 `.agents/` 下
|
|
418
|
+
(逐个 `git mv`),把 `memory/PLAN.md` 原样搬到 `specs/PLAN.md`,再改写路径引用,
|
|
419
|
+
前后计数都会报出来。`todos/` 退役:需求原文原样进对应单元的 spec。默认是**保守 pass**:
|
|
420
|
+
装框架、修机械漂移(墓碑、重复章节、附录顺序),超预算的 `AGENTS.md` 只登记成债务;
|
|
421
|
+
`--deep` 才允许结构性改写。
|
|
422
|
+
|
|
423
|
+
**审计码**(稳定,可跨次对比):`E-PATH-01` 裸旧路径、`E-STATE-01` 记忆被 gitignore、`W-STATE-01` 记忆尚未提交、`E-INDEX-01` 索引与实际文件不一致、`E-LINK-01` 内部链接失效、
|
|
424
|
+
`W-BUDGET-01` `AGENTS.md` 超预算、`W-DUP-01` 同一章节出现在两个文件、
|
|
425
|
+
`W-ORDER-01` 附录之后又追加章节、`W-COV-01` 覆盖表仍有 OPEN、
|
|
426
|
+
`W-TODO-01` 仍存在退役的 `todos/`、`W-ROOT-02` 根下零字节文件、
|
|
427
|
+
`W-SCAFFOLD-01` 未填的 `TODO(ask-init)` 标记、`I-INSTALL-01` 尚未安装框架。
|
|
428
|
+
|
|
429
|
+
**多仓库工作区**:记忆只住主仓。`pi-add-dir` 会把每个 added root 的 `AGENTS.md`
|
|
430
|
+
全文注入每次请求,所以根里的入口只能是 **scope card**(≤30 行:这里放什么、它的命令、
|
|
431
|
+
去主仓哪个记忆文件读),绝不能是规则的第二份副本。
|
|
432
|
+
|
|
433
|
+
装进项目的框架契约用英文(给模型读、也随 npm 发布);写进项目的正文跟随项目语言。
|
|
434
|
+
|
|
435
|
+
> **跑完记得 `/reload`**,让新的 `AGENTS.md` 进入提示词。框架版本记在
|
|
436
|
+
> `.agents/framework/state.toml`;若项目的状态版本比当前工具新,则拒绝迁移。
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
370
440
|
## 🛠️ 开发
|
|
371
441
|
|
|
372
442
|
<a name="development"></a>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart,
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, a dedicated compaction model, and an agent memory framework (/ask-init)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"autostart",
|
|
31
31
|
"permission",
|
|
32
32
|
"approval",
|
|
33
|
-
"boundary"
|
|
33
|
+
"boundary",
|
|
34
|
+
"agent-memory",
|
|
35
|
+
"memory-framework",
|
|
36
|
+
"scaffold",
|
|
37
|
+
"init"
|
|
34
38
|
],
|
|
35
39
|
"peerDependencies": {
|
|
36
40
|
"@earendil-works/pi-coding-agent": ">=0.99.0",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ask feature — read-only consult mode.
|
|
3
3
|
*
|
|
4
|
-
* Design notes (see
|
|
4
|
+
* Design notes (see AGENTS.md §Feature: ask, plus the D1–D6 records in
|
|
5
|
+
* .agents/memory/ARCHITECTURE.md):
|
|
5
6
|
* - Project writes stay impossible: write/edit/powershell are removed from the
|
|
6
7
|
* active tool set via pi.setActiveTools(), and pi's runtime rejects calls to
|
|
7
8
|
* non-active tools ("Tool not found"). A gate blocks them again as defence in
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* be ~300 tokens per request restating what the sandbox already enforces.
|
|
17
17
|
*
|
|
18
18
|
* Do NOT use the removed `systemPromptAppend` field or wholesale systemPrompt
|
|
19
|
-
* replacement (see
|
|
19
|
+
* replacement (see `AGENTS.md` §Feature: ask and the D5 record in
|
|
20
|
+
* `.agents/memory/ARCHITECTURE.md`).
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
export interface AskBannerOptions {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init feature — pure document analysis shared by the planner and the auditor.
|
|
3
|
+
*
|
|
4
|
+
* These helpers answer questions about *documents*, not about the filesystem:
|
|
5
|
+
* duplicated sections, tombstones, appendix ordering, oversized sections and
|
|
6
|
+
* path references. Keeping them here means the plan's warnings and the audit's
|
|
7
|
+
* findings cannot drift apart.
|
|
8
|
+
*/
|
|
9
|
+
import type { DocInfo, Heading } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** Normalise a heading for comparison: drop numbering, markup, emoji, case. */
|
|
12
|
+
export function normalizeHeading(text: string): string {
|
|
13
|
+
return text
|
|
14
|
+
.replace(/^[§#\s]*\d+(?:[.\-–]\d+)*[.、:)]?\s*/, "")
|
|
15
|
+
.replace(/[`*_~[\]]/g, "")
|
|
16
|
+
.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, "")
|
|
17
|
+
.replace(/[\s\u3000]+/g, "")
|
|
18
|
+
.replace(/[::,,。.、()()]/g, "")
|
|
19
|
+
.toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** H2 headings (level 2 only) of a document. */
|
|
23
|
+
export function topSections(doc: DocInfo): Heading[] {
|
|
24
|
+
return doc.headings.filter((heading) => heading.level === 2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DuplicateGroup {
|
|
28
|
+
readonly normalized: string;
|
|
29
|
+
readonly occurrences: Array<{ path: string; heading: string; line: number }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Same H2 heading appearing in two or more documents. */
|
|
33
|
+
export function duplicateHeadings(docs: readonly DocInfo[]): DuplicateGroup[] {
|
|
34
|
+
const groups = new Map<string, DuplicateGroup["occurrences"]>();
|
|
35
|
+
for (const doc of docs) {
|
|
36
|
+
for (const heading of topSections(doc)) {
|
|
37
|
+
const key = normalizeHeading(heading.text);
|
|
38
|
+
if (key.length < 2) continue;
|
|
39
|
+
const list = groups.get(key) ?? [];
|
|
40
|
+
list.push({ path: doc.rel, heading: heading.text, line: heading.line });
|
|
41
|
+
groups.set(key, list);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const out: DuplicateGroup[] = [];
|
|
45
|
+
for (const [normalized, occurrences] of groups) {
|
|
46
|
+
if (occurrences.length < 2) continue;
|
|
47
|
+
// Same file twice is a different problem (repeated section), still useful.
|
|
48
|
+
out.push({ normalized, occurrences });
|
|
49
|
+
}
|
|
50
|
+
return out.sort((a, b) => b.occurrences.length - a.occurrences.length);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Section bodies as `[heading, body]`, body ending at the next same-or-higher heading. */
|
|
54
|
+
export function sectionBodies(doc: DocInfo): Array<{ heading: Heading; body: string }> {
|
|
55
|
+
const lines = doc.text.split("\n");
|
|
56
|
+
const out: Array<{ heading: Heading; body: string }> = [];
|
|
57
|
+
for (const heading of doc.headings) {
|
|
58
|
+
if (heading.level !== 2) continue;
|
|
59
|
+
const start = heading.line; // 0-based index of the line *after* the heading
|
|
60
|
+
let end = lines.length;
|
|
61
|
+
for (const other of doc.headings) {
|
|
62
|
+
if (other.line <= heading.line) continue;
|
|
63
|
+
if (other.level <= 2) {
|
|
64
|
+
end = other.line - 1;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
out.push({ heading, body: lines.slice(start, end).join("\n").trim() });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const POINTER_PHRASES =
|
|
74
|
+
/(已迁|迁至|移至|见\s*`|参见\s*`|moved to|see\s+`|已并入|merged into)/i;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A "tombstone": a section whose whole body is a one-line pointer to wherever
|
|
78
|
+
* the content went. After the move is registered in the index such sections are
|
|
79
|
+
* pure noise, so the plan asks for their removal.
|
|
80
|
+
*/
|
|
81
|
+
export function tombstones(doc: DocInfo): Heading[] {
|
|
82
|
+
const out: Heading[] = [];
|
|
83
|
+
for (const { heading, body } of sectionBodies(doc)) {
|
|
84
|
+
if (body.length === 0 || body.length > 220) continue;
|
|
85
|
+
if (!POINTER_PHRASES.test(body)) continue;
|
|
86
|
+
const meaningful = body
|
|
87
|
+
.split("\n")
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter((line) => line.length > 0 && !line.startsWith("|"));
|
|
90
|
+
if (meaningful.length <= 3) out.push(heading);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Headings that sit after the historical appendix started. Appendices are the
|
|
97
|
+
* last thing in a document by convention; dated sections appended later mean
|
|
98
|
+
* the file grew without a compacting pass.
|
|
99
|
+
*/
|
|
100
|
+
export function sectionsAfterAppendix(doc: DocInfo): Heading[] {
|
|
101
|
+
const sections = topSections(doc);
|
|
102
|
+
const firstAppendix = sections.findIndex((heading) =>
|
|
103
|
+
/^(附录|appendix)/i.test(heading.text.trim()),
|
|
104
|
+
);
|
|
105
|
+
if (firstAppendix < 0) return [];
|
|
106
|
+
return sections
|
|
107
|
+
.slice(firstAppendix + 1)
|
|
108
|
+
.filter((heading) => !/^(附录|appendix)/i.test(heading.text.trim()));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The N largest H2 sections, with their character counts (split candidates). */
|
|
112
|
+
export function largestSections(
|
|
113
|
+
doc: DocInfo,
|
|
114
|
+
limit = 3,
|
|
115
|
+
): Array<{ heading: Heading; chars: number }> {
|
|
116
|
+
return sectionBodies(doc)
|
|
117
|
+
.map(({ heading, body }) => ({ heading, chars: body.length }))
|
|
118
|
+
.sort((a, b) => b.chars - a.chars)
|
|
119
|
+
.slice(0, limit);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Paths mentioned in a document that the framework can verify: memory files,
|
|
124
|
+
* specs, implementation records. Returned without the surrounding backticks so
|
|
125
|
+
* callers can `existsSync` them directly.
|
|
126
|
+
*/
|
|
127
|
+
export function extractFrameworkPaths(text: string): string[] {
|
|
128
|
+
const out = new Set<string>();
|
|
129
|
+
const re =
|
|
130
|
+
/(?:\.agents\/memory\/(?:[\w.\-]+\.md)|specs\/[\w./-]+\.md|\.agents\/implementations\/[\w.\-]+\.md)/g;
|
|
131
|
+
for (const match of text.matchAll(re)) out.add(match[0]);
|
|
132
|
+
return [...out];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** First paragraph of a section, used as the coverage anchor. */
|
|
136
|
+
export function sectionAnchor(doc: DocInfo, heading: Heading): string {
|
|
137
|
+
const lines = doc.text.split("\n");
|
|
138
|
+
let index = heading.line; // first line after the heading
|
|
139
|
+
let collected = "";
|
|
140
|
+
let started = false;
|
|
141
|
+
for (; index < lines.length; index += 1) {
|
|
142
|
+
const line = lines[index].trim();
|
|
143
|
+
if (/^#{1,6}\s/.test(line)) break;
|
|
144
|
+
if (line.length === 0) {
|
|
145
|
+
if (started) break;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (/^([-*+]\s|\d+[.)]\s|\||>)/.test(line)) continue;
|
|
149
|
+
collected = collected.length === 0 ? line : `${collected} ${line}`;
|
|
150
|
+
started = true;
|
|
151
|
+
if (collected.length >= 80) break;
|
|
152
|
+
}
|
|
153
|
+
return collected.slice(0, 80);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Normalise prose for anchor matching (drop markdown emphasis and spacing). */
|
|
157
|
+
export function normalizeProse(text: string): string {
|
|
158
|
+
return text
|
|
159
|
+
.replace(/[`*_~[\]]/g, "")
|
|
160
|
+
.replace(/[\s\u3000]+/g, "")
|
|
161
|
+
.toLowerCase();
|
|
162
|
+
}
|