@andares/pi-toolkits 0.9.8 → 0.11.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/README.md +117 -5
- package/README.zh-CN.md +102 -5
- package/package.json +8 -3
- 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 +159 -0
- package/src/features/permission/audit.ts +145 -0
- package/src/features/permission/index.test.ts +138 -3
- package/src/features/permission/index.ts +108 -12
- package/src/features/permission/policy.ts +39 -3
- package/src/index.ts +9 -2
- package/src/lib/approval-log.test.ts +289 -0
- package/src/lib/approval-log.ts +724 -0
- package/src/lib/gates.audit.test.ts +343 -0
- package/src/lib/gates.ts +291 -10
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)
|
|
@@ -69,12 +70,13 @@ After installing, verify in any pi session:
|
|
|
69
70
|
| Feature | Entry point | One-line description |
|
|
70
71
|
| --- | --- | --- |
|
|
71
72
|
| 🛡️ **ask** read-only consult mode | `/ask` | enter consult mode: `write`/`edit`/`powershell` hard-disabled, `bash` runs in a read-only sandbox, `grep`/`find`/`ls` activated, and `scratchpad` allowed to write throwaway notes into the session's temp directory |
|
|
72
|
-
| 🔐 **permission** session-scoped boundary checks | `/ask-perm` | four in/out × read/write modes, off by default and never persisted; outside writes ask, credentials are denied, destructive commands always ask; composes with `/ask
|
|
73
|
+
| 🔐 **permission** session-scoped boundary checks | `/ask-perm` | four in/out × read/write modes, off by default and never persisted; outside writes ask, credentials are denied, destructive commands always ask; composes with `/ask`; a capped JSONL approval log feeds later rule mining |
|
|
73
74
|
| ⭐ **favorites** model favorites | `ctrl+A` / `ctrl+J` inside `/model` | favorite the models you use most (marked in bold bright yellow), `ctrl+p` cycles only among favorites |
|
|
74
75
|
| 📥 **stash** prompt stash | `ctrl+alt+y` | a stash slot for one prompt: press to swap, press twice to stash and clear the input box |
|
|
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
|
|
|
@@ -104,7 +106,7 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
|
|
|
104
106
|
|
|
105
107
|
<a name="permission"></a>
|
|
106
108
|
|
|
107
|
-
`/ask-perm` turns on boundary checks for the **current session only**. Every piece of state — the on/off flag, the four modes, the session grants — lives in the extension's memory
|
|
109
|
+
`/ask-perm` turns on boundary checks for the **current session only**. Every piece of state — the on/off flag, the four modes, the session grants — lives in the extension's memory and is never persisted: the feature is off by default and leaves no status behind when disabled. Toggling it on can never silently follow you into the next session. Separately, decisions worth keeping are appended to a capped local **approval log** for later rule mining (see below); it is write-only telemetry and is never consulted while deciding.
|
|
108
110
|
|
|
109
111
|
**Enable**
|
|
110
112
|
|
|
@@ -137,9 +139,36 @@ Repeating the argument form with the modes already in force is a **no-op**: no s
|
|
|
137
139
|
- **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
140
|
- **destructive commands always ask** — `rm` of `/`, the home directory, or the working directory; `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
141
|
|
|
142
|
+
**Approval log** (write-only telemetry, on by default)
|
|
143
|
+
|
|
144
|
+
Every decision worth keeping becomes one JSON object per line (JSONL) in `~/.pi/agent/approval-log/audit-NNNNNN.jsonl`. The policy never reads the log: deleting it or making the directory read-only changes no verdict.
|
|
145
|
+
|
|
146
|
+
- **Scope**: blocks from `/ask` mode are approval decisions too, so they are recorded as well (with the gate name and the model-facing reason).
|
|
147
|
+
- **What is recorded**: interactive asks (the exact dialog lines, the bash decomposition, the chosen answer, the grant and its latency), automatic denials and blocks, session-grant allows, `/ask-perm` mode changes, and one `session` summary with call/ask/block counters. Plain mode-allowed `read`/`grep` calls are skipped by default so the ring buffer is not buried in noise; `PI_APPROVAL_LOG=all` also logs plain mode-allowed calls, `PI_APPROVAL_LOG=off` disables the log.
|
|
148
|
+
- **Rotation**: 1000 records **or** 8 MiB per file, at most 10 files; creating an 11th deletes the lowest-numbered one. Typical retention is 15–30 MB (hard ceiling 80 MB) and roughly the last 10,000 records.
|
|
149
|
+
- **Fidelity**: the four modes, session roots, tool call, every command unit (including `opaque` units and parser notes), each target's canonical path / effect / in-or-out scope, the deciding rule layer, the dialog lines, the answer and the resulting grant. `write`/`edit` file **contents are never logged** (only path and byte count); bearer tokens, `token=`/`password=`, URL credentials and known key prefixes are redacted.
|
|
150
|
+
- **Failure and privacy**: directory `0700`, files `0600`, local only, safe to delete at any time. A logging failure is reported once and swallowed — the verdict path never fails because the log is unavailable. `PI_APPROVAL_LOG_DIR` relocates the directory.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# focus on the prompts that became session grants
|
|
154
|
+
jq -r 'select(.event=="ask" and .answer.choice=="allow-session")
|
|
155
|
+
| [.ts,.tool,.verdict.target,.input.command] | @tsv' \
|
|
156
|
+
~/.pi/agent/approval-log/*.jsonl
|
|
157
|
+
|
|
158
|
+
# target roots × answers — the shortest path to a "safe temp dir" rule
|
|
159
|
+
jq -r 'select(.event=="ask") | [.verdict.grantRoot,.answer.choice] | @tsv' \
|
|
160
|
+
~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
|
|
161
|
+
|
|
162
|
+
# parser gaps (opaque units) worth teaching units.ts
|
|
163
|
+
jq -r 'select(.event=="ask") | .units[] | select(.opaque) | [.command,.note] | @tsv' \
|
|
164
|
+
~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
See the repository's `pnpm approval-report` for a read-only summary (answer mix, target roots, rule candidates, opaque gaps, playwright patterns).
|
|
168
|
+
|
|
140
169
|
**Composition with `/ask`**: the two features are independent gates on one bus. Each answers only its own question — ask: "does this mutate?", permission: "does this cross the boundary?" — and either can block. Ask mode still denies credential reads, and permission still applies while the read-only sandbox is active. Neither feature needs to know the other exists. The one exception is ask's `scratchpad`: that tool is self-jailed (relative names only, one temp directory), so ask marks it while active and permission skips the boundary dialog that would otherwise fire for an "outside write" on every note.
|
|
141
170
|
|
|
142
|
-
**Enable it for every session** (the state itself is never persisted — this is the opt-in):
|
|
171
|
+
**Enable it for every session** (the state itself is never persisted — this is the opt-in; the audit log is independent and on by default):
|
|
143
172
|
|
|
144
173
|
```toml
|
|
145
174
|
# <project>/.pi/autostart.toml, or <agent-dir>/autostart.toml for all projects
|
|
@@ -147,7 +176,7 @@ Repeating the argument form with the modes already in force is a **no-op**: no s
|
|
|
147
176
|
run = ["/ask-perm allow allow allow ask"]
|
|
148
177
|
```
|
|
149
178
|
|
|
150
|
-
> **Implementation notes**: a new session resets the state on `session_start` (a repeated emission for the same session keeps it); the footer shows `perm i:A/A o:A/ask` while on (`A` allow, `a` ask, `D` deny). Subagent sessions start with their own state and do not inherit the parent's mode — when child sessions must be gated too, enable it through the project's autostart file. POSIX hosts (Linux / macOS / WSL) are the tested target; native Windows Git-Bash path spellings are approximated, not verified. bash decomposition uses `shell-quote`; unknown variables expand to a sentinel that makes the unit ask instead of silently becoming an empty path. Added directories are read from pi-add-dir's session entry (`add-dir:state`). Heuristic, not a sandbox: project-internal destructive commands are covered by the fixed table, not by a sandbox. One known blind spot: a *read* command's bare filename operand (`cat notes.txt`) is not collected as a target, so with `out read = ask` only absolute-path-shaped read operands are questioned — reads never write, so the four modes' write side is unaffected.
|
|
179
|
+
> **Implementation notes**: a new session resets the state on `session_start` (a repeated emission for the same session keeps it); the footer shows `perm i:A/A o:A/ask` while on (`A` allow, `a` ask, `D` deny). Subagent sessions start with their own state and do not inherit the parent's mode — when child sessions must be gated too, enable it through the project's autostart file. The approval log is appended by the gate bus (asks/blocks/session summaries) and by the permission gate (mode changes and session-grant allows); the writer is lazy, keeps no state across processes, and never participates in a decision. POSIX hosts (Linux / macOS / WSL) are the tested target; native Windows Git-Bash path spellings are approximated, not verified. bash decomposition uses `shell-quote`; unknown variables expand to a sentinel that makes the unit ask instead of silently becoming an empty path. Added directories are read from pi-add-dir's session entry (`add-dir:state`). Heuristic, not a sandbox: project-internal destructive commands are covered by the fixed table, not by a sandbox. One known blind spot: a *read* command's bare filename operand (`cat notes.txt`) is not collected as a target, so with `out read = ask` only absolute-path-shaped read operands are questioned — reads never write, so the four modes' write side is unaffected.
|
|
151
180
|
|
|
152
181
|
---
|
|
153
182
|
|
|
@@ -340,6 +369,87 @@ An **identical command line** declared by both sources runs once: a duplicate is
|
|
|
340
369
|
|
|
341
370
|
---
|
|
342
371
|
|
|
372
|
+
## 🧠 init — agent memory framework
|
|
373
|
+
|
|
374
|
+
<a name="init"></a>
|
|
375
|
+
|
|
376
|
+
`/ask-init` installs one opinionated memory shape into a project, or adopts the
|
|
377
|
+
documentation that is already there. The point is not the directory names — it is
|
|
378
|
+
the split between three things that decay when mixed:
|
|
379
|
+
|
|
380
|
+
| Family | Location | Answers | Lifetime |
|
|
381
|
+
| --- | --- | --- | --- |
|
|
382
|
+
| **plan** | `specs/` (`PLAN.md` + `<unit>.md`) | what to do, how it will be accepted | written before work, status updated at close |
|
|
383
|
+
| **memory** | `.agents/memory/` | what is known, decided, and why | long-lived; append and compact only |
|
|
384
|
+
| **evidence** | `.agents/implementations/` | what changed, when, with what proof | one record per change, never rewritten |
|
|
385
|
+
|
|
386
|
+
Only agent-facing material moves under the hidden `.agents/` directory, so the
|
|
387
|
+
repository root stays readable: `AGENTS.md`, `specs/`, `design/`, `docs/`,
|
|
388
|
+
`legal/` remain visible and reviewable.
|
|
389
|
+
|
|
390
|
+
**Commands**:
|
|
391
|
+
|
|
392
|
+
| Command | What it does |
|
|
393
|
+
| --- | --- |
|
|
394
|
+
| `/ask-init` | detect → show the plan → (confirm) → move/scaffold/snapshot → dispatch the prose work to the model |
|
|
395
|
+
| `/ask-init plan [--deep]` | print the plan and write nothing (`--dry-run` is the same) |
|
|
396
|
+
| `/ask-init check` | audit the installed framework (read-only; writes only a runtime report under `.agents/framework/reports/`) |
|
|
397
|
+
| `/ask-init roots` | sync the directories added with `pi-add-dir` into the framework state |
|
|
398
|
+
| `/ask-init --yes` | skip the confirmation panel |
|
|
399
|
+
| `/ask-init --lang zh\|en` | language for generated prose (default: detected from the project's own docs) |
|
|
400
|
+
|
|
401
|
+
**What the mechanical half guarantees**:
|
|
402
|
+
|
|
403
|
+
1. **Snapshot first** — every path the run may write (including every file whose
|
|
404
|
+
path references are rewritten) is copied to `.agents/framework/snapshots/<stamp>/`
|
|
405
|
+
with a sha256 manifest and the `git status` of that moment.
|
|
406
|
+
2. **Writes stay inside the repository** — added roots are never written from
|
|
407
|
+
extension code; a scope card there is an ordinary tool call and therefore goes
|
|
408
|
+
through the permission gate.
|
|
409
|
+
3. **Nothing is clobbered** — a create skips an existing path; a move refuses a
|
|
410
|
+
non-empty destination and turns the conflict into a manual task.
|
|
411
|
+
4. **Counts reconcile** — the reference rewrite uses the same bare-reference
|
|
412
|
+
predicate as the survey, so "N occurrences in M files" from the plan is what
|
|
413
|
+
the run reports (and the rewrite report lists every file).
|
|
414
|
+
5. **No silent loss** — a coverage table (`.agents/framework/reports/coverage-*.md`)
|
|
415
|
+
gets one row per migrated source section: found, declared with a destination, or
|
|
416
|
+
open with a reason. The audit fails while any row is open.
|
|
417
|
+
|
|
418
|
+
The prose half is a bounded work order with acceptance criteria per task: fill the
|
|
419
|
+
entry, merge the legacy plan, triage `todos/` into specs, deduplicate sections,
|
|
420
|
+
record budget debt, fill the coverage table. The model closes its own loop with the
|
|
421
|
+
`framework_audit` tool (activated for the run only).
|
|
422
|
+
|
|
423
|
+
**Migration of an existing project** moves `memory/`, `implementations/`,
|
|
424
|
+
`prompts/` under `.agents/` (one `git mv` each), moves `memory/PLAN.md` to
|
|
425
|
+
`specs/PLAN.md` byte-identically, and rewrites path references — with the counts
|
|
426
|
+
reported before and after. `todos/` is retired: requirement wording goes into the
|
|
427
|
+
unit spec verbatim. The default pass is conservative: install, fix mechanical
|
|
428
|
+
drift (tombstones, duplicated sections, appendix order), and record an over-budget
|
|
429
|
+
`AGENTS.md` as debt instead of rewriting prose; `--deep` opts into structural edits.
|
|
430
|
+
|
|
431
|
+
**Audit codes** (stable, so reports compare across runs): `E-PATH-01` bare legacy
|
|
432
|
+
paths, `E-STATE-01` memory gitignored, `W-STATE-01` memory not yet committed, `E-INDEX-01` index ↔ file
|
|
433
|
+
mismatch, `E-LINK-01` broken internal links, `W-BUDGET-01` `AGENTS.md` over budget,
|
|
434
|
+
`W-DUP-01` the same section in two files, `W-ORDER-01` sections appended after the
|
|
435
|
+
appendix, `W-COV-01` open coverage rows, `W-TODO-01` retired `todos/` still present,
|
|
436
|
+
`W-ROOT-02` zero-byte root files, `W-SCAFFOLD-01` unfilled `TODO(ask-init)` markers,
|
|
437
|
+
`I-INSTALL-01` framework not installed yet.
|
|
438
|
+
|
|
439
|
+
**Multi-repository workspaces**: memory lives in the main repository only.
|
|
440
|
+
`pi-add-dir` injects each added root's `AGENTS.md` verbatim into every request, so a
|
|
441
|
+
root entry must stay a **scope card** (≤30 lines: what lives there, its commands,
|
|
442
|
+
which main-repo memory file describes it) — never a second copy of the rules.
|
|
443
|
+
|
|
444
|
+
The framework contract shipped into a project is English (it is read by models and
|
|
445
|
+
published); text written *into* a project follows that project's language.
|
|
446
|
+
|
|
447
|
+
> **After the run**, `/reload` so the new `AGENTS.md` enters the prompt. The
|
|
448
|
+
> framework version lives in `.agents/framework/state.toml`; a project whose state
|
|
449
|
+
> version is newer than the installed tool is never migrated.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
343
453
|
## 🛠️ Development
|
|
344
454
|
|
|
345
455
|
<a name="development"></a>
|
|
@@ -353,6 +463,7 @@ pnpm test # vitest (all feature unit tests)
|
|
|
353
463
|
|
|
354
464
|
pnpm check:keybindings # audit this plugin's keys against the host's own dispatch table
|
|
355
465
|
pnpm check:compat # mandatory 4-line window + backward probe (4 more lines) → public floor
|
|
466
|
+
pnpm approval-report # summarize the local approval log (answer mix, targets, rule candidates)
|
|
356
467
|
```
|
|
357
468
|
|
|
358
469
|
### Project structure
|
|
@@ -363,6 +474,7 @@ src/
|
|
|
363
474
|
├── lib/ # shared infrastructure (tool-set snapshot/restore,
|
|
364
475
|
│ # gate bus composing tool_call policies, jailed-tool
|
|
365
476
|
│ # registry the boundary gate consults,
|
|
477
|
+
│ # rolling JSONL approval log,
|
|
366
478
|
│ # model-key resolution shared by review & compact)
|
|
367
479
|
└── features/ # feature modules — one directory per feature
|
|
368
480
|
├── ask/ # ask mode: command + state machine, bash sandbox,
|
|
@@ -377,7 +489,7 @@ src/
|
|
|
377
489
|
│ # persisted store, tests
|
|
378
490
|
├── permission/ # session-scoped boundary permission: modes, panel,
|
|
379
491
|
│ # bash units, policy, session grants, prompt,
|
|
380
|
-
│ # tests
|
|
492
|
+
│ # audit payload, tests
|
|
381
493
|
├── review/ # third-party review: input/command triggers,
|
|
382
494
|
│ # review-model config store, prompt, tests
|
|
383
495
|
└── stash/ # prompt stash: hotkey state machine + tests
|
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)
|
|
@@ -69,12 +70,13 @@ pi -e ./src/index.ts
|
|
|
69
70
|
| 功能 | 入口 | 一句话说明 |
|
|
70
71
|
| --- | --- | --- |
|
|
71
72
|
| 🛡️ **ask** 只读咨询模式 | `/ask` | 进入咨询模式:`write`/`edit`/`powershell` 硬禁用,`bash` 只读沙箱,临时启用 `grep`/`find`/`ls`,并开放 `scratchpad` 把草稿写进会话临时目录 |
|
|
72
|
-
| 🔐 **permission** 会话级边界权限 | `/ask-perm` | 项目内/外 × 读/写四格模式,默认关闭且从不落盘;项目外写询问、凭据路径硬拒、破坏性命令必问;与 `/ask`
|
|
73
|
+
| 🔐 **permission** 会话级边界权限 | `/ask-perm` | 项目内/外 × 读/写四格模式,默认关闭且从不落盘;项目外写询问、凭据路径硬拒、破坏性命令必问;与 `/ask` 并行组合;另有滚动 JSONL 审批日志供事后挖规则 |
|
|
73
74
|
| ⭐ **favorites** 模型收藏 | `/model` 内 `ctrl+A` / `ctrl+J` | 收藏常用模型(加粗亮黄标记),`ctrl+p` 仅在收藏间循环 |
|
|
74
75
|
| 📥 **stash** 提示词暂存 | `ctrl+alt+y` | 一段提示词的暂存槽,按键交换 / 连按两次暂存并清空输入框 |
|
|
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
|
|
|
@@ -104,7 +106,7 @@ pi -e ./src/index.ts
|
|
|
104
106
|
|
|
105
107
|
<a name="permission"></a>
|
|
106
108
|
|
|
107
|
-
`/ask-perm`
|
|
109
|
+
`/ask-perm` 只在**当前会话**开启边界检查。开关状态、四项模式、会话授权全部只存在扩展内存里:权限**状态**不落盘、默认关闭、关闭后不留任何状态。开启一次不会悄悄跟到下一个会话。另外,值得留档的判定会追加到本地**审批日志**(见下),供事后挖规则用;它是只写的遥测数据,判定时永远不会被读取。
|
|
108
110
|
|
|
109
111
|
**开启**
|
|
110
112
|
|
|
@@ -137,9 +139,36 @@ pi -e ./src/index.ts
|
|
|
137
139
|
- **凭据路径任何模式都拒绝** —— `~/.ssh`、`~/.gnupg`、`~/.aws`、`~/.pi/agent/auth*.json`、`.env` 文件(模板除外)。没有这一条,默认的「项目外读放行」就等于把 `~/.ssh/id_rsa` 交出去
|
|
138
140
|
- **破坏性命令永远询问** —— 对 `/`、家目录或工作目录的 `rm`;`git reset --hard`;`git clean -f`;`git push --force`;包 `publish`;`dd` 写裸设备;`mkfs*`;关机/重启;`chmod -R 777`;任何经 `sudo` 的命令。作用域本身表达不了「破坏性」
|
|
139
141
|
|
|
142
|
+
**审批日志**(只写遥测,默认开启)
|
|
143
|
+
|
|
144
|
+
每个值得留档的判定都会写成一行 JSON(JSONL),落在 `~/.pi/agent/approval-log/audit-NNNNNN.jsonl`。策略层从不读它:删掉日志或把目录改成只读,不会改变任何判定结果。
|
|
145
|
+
|
|
146
|
+
- **范围**:`/ask` 模式的拦截同样算审批决定,也会记录(带闸门名与模型可见理由)。
|
|
147
|
+
- **记录什么**:交互弹窗(用户看到的原始行、bash 拆解结果、答复、授权与决策耗时)、自动拒绝/拦截、会话授权放行、`/ask-perm` 模式切换,以及每个会话一条 `session` 摘要(调用/弹窗/拦截计数)。普通自动放行的 `read`/`grep` 默认不记,避免把环形缓冲淹在噪声里;`PI_APPROVAL_LOG=all` 连普通自动放行也记,`PI_APPROVAL_LOG=off` 关闭日志。
|
|
148
|
+
- **滚动**:每文件 1000 条**或** 8 MiB,最多 10 个文件;创建第 11 个时删掉序号最小的那个。典型体积 15–30 MB(硬上限 80 MB),约保留最近 1 万条判定。
|
|
149
|
+
- **保真度**:四格模式、会话根目录、工具调用、每个命令单元(含 `opaque` 单元与解析器备注)、每个目标规范化后的路径/读或写/项目内或外、命中哪条规则层、弹窗原文、答复与产生的授权。`write`/`edit` 的文件**正文永不记录**(只记路径与字节数);bearer token、`token=`/`password=`、URL 内嵌凭据与已知密钥前缀会脱敏。
|
|
150
|
+
- **失败与隐私**:目录 `0700`、文件 `0600`,只在本机,可随时删除。日志故障只警告一次并被吞掉——判定链路不会因为日志不可用而失败。`PI_APPROVAL_LOG_DIR` 可改目录。
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# 只看"本会话允许"的弹窗
|
|
154
|
+
jq -r 'select(.event=="ask" and .answer.choice=="allow-session")
|
|
155
|
+
| [.ts,.tool,.verdict.target,.input.command] | @tsv' \
|
|
156
|
+
~/.pi/agent/approval-log/*.jsonl
|
|
157
|
+
|
|
158
|
+
# 目标根目录 × 答复 —— 找「临时目录直接放行」规则的最短路径
|
|
159
|
+
jq -r 'select(.event=="ask") | [.verdict.grantRoot,.answer.choice] | @tsv' \
|
|
160
|
+
~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
|
|
161
|
+
|
|
162
|
+
# 解析不了的单元:定位 units.ts 的缺口
|
|
163
|
+
jq -r 'select(.event=="ask") | .units[] | select(.opaque) | [.command,.note] | @tsv' \
|
|
164
|
+
~/.pi/agent/approval-log/*.jsonl | sort | uniq -c | sort -rn
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
仓库内可用 `pnpm approval-report` 生成只读汇总(答复分布、目标根目录、候选规则、opaque 缺口、playwright 模式)。
|
|
168
|
+
|
|
140
169
|
**与 `/ask` 的组合**:两者是同一总线上的独立闸门,各自只回答自己的问题 —— ask 回答「会不会产生变更」,permission 回答「有没有越过边界」,任一拦截都生效。所以 ask 模式下凭据仍然读不到,permission 在只读沙箱下也照常工作;两个功能互不需要知道对方存在。
|
|
141
170
|
|
|
142
|
-
**每个会话自动开启**(
|
|
171
|
+
**每个会话自动开启**(状态本身不落盘,这就是持久化的替身;审批日志独立于它、默认开启):
|
|
143
172
|
|
|
144
173
|
```toml
|
|
145
174
|
# <项目>/.pi/autostart.toml;想对所有项目生效则用 <agent-dir>/autostart.toml
|
|
@@ -147,7 +176,7 @@ pi -e ./src/index.ts
|
|
|
147
176
|
run = ["/ask-perm allow allow allow ask"]
|
|
148
177
|
```
|
|
149
178
|
|
|
150
|
-
> **实现说明**:新会话在 `session_start` 重置状态(同一会话的重复 emission 保持状态);开启时 footer 显示 `perm i:A/A o:A/ask`(`A` allow、`a` ask、`D` deny)。子代理会话有自己的状态、不继承父会话的模式;需要子会话也受控时,通过项目 autostart
|
|
179
|
+
> **实现说明**:新会话在 `session_start` 重置状态(同一会话的重复 emission 保持状态);开启时 footer 显示 `perm i:A/A o:A/ask`(`A` allow、`a` ask、`D` deny)。子代理会话有自己的状态、不继承父会话的模式;需要子会话也受控时,通过项目 autostart 再开启一次。审批日志由闸门总线(弹窗/拦截/会话摘要)与 permission 闸门(模式切换、会话授权放行)追加;写入器懒初始化、不跨进程保存状态,也从不参与判定。实测目标是 POSIX 宿主(Linux/macOS/WSL);原生 Windows Git-Bash 的路径拼写只做了近似,未验证。bash 拆解使用 `shell-quote`;未知变量会展开成哨兵而不是空字符串,使该命令转为询问而不是被悄悄当成空路径。add-dir 目录从 pi-add-dir 的会话条目 `add-dir:state` 读取。启发式,不是沙箱:项目内的破坏性操作由固定表兜底,而不是靠沙箱。已知盲区一处:**读命令的裸文件名参数**(`cat notes.txt`)不被收集为目标,所以把 `out read` 调成 `ask` 时只有绝对路径形状的读目标会被问——读本身不会写,四格模式的写侧不受影响。
|
|
151
180
|
|
|
152
181
|
---
|
|
153
182
|
|
|
@@ -340,6 +369,71 @@ run = [
|
|
|
340
369
|
|
|
341
370
|
---
|
|
342
371
|
|
|
372
|
+
## 🧠 init — agent 记忆框架
|
|
373
|
+
|
|
374
|
+
<a name="init"></a>
|
|
375
|
+
|
|
376
|
+
`/ask-init` 给项目安装一套有明确立场的记忆结构,或者接管项目里已经存在的文档。
|
|
377
|
+
重点不是目录名,而是把三样混在一起必然腐烂的东西分开:
|
|
378
|
+
|
|
379
|
+
| 家庭 | 位置 | 回答什么 | 生命周期 |
|
|
380
|
+
| --- | --- | --- | --- |
|
|
381
|
+
| **计划** | `specs/`(`PLAN.md` + `<unit>.md`) | 要做什么、怎么验收 | 开工前写,结项时更新状态 |
|
|
382
|
+
| **记忆** | `.agents/memory/` | 已知什么、定过什么、为什么 | 长期;只追加、只压缩 |
|
|
383
|
+
| **证据** | `.agents/implementations/` | 改了什么、什么时候、证据在哪 | 一次写定,不再改写 |
|
|
384
|
+
|
|
385
|
+
只有「纯给 agent 看」的东西进隐藏目录 `.agents/`,所以仓库根仍然清爽:
|
|
386
|
+
`AGENTS.md`、`specs/`、`design/`、`docs/`、`legal/` 都留在外面可读可审。
|
|
387
|
+
|
|
388
|
+
**命令**:
|
|
389
|
+
|
|
390
|
+
| 命令 | 作用 |
|
|
391
|
+
| --- | --- |
|
|
392
|
+
| `/ask-init` | 检测 → 出计划 → 确认 → 移动/脚手架/快照 → 把正文工作派发给模型 |
|
|
393
|
+
| `/ask-init plan [--deep]` | 只打印计划,不写任何盘(`--dry-run` 同义) |
|
|
394
|
+
| `/ask-init check` | 审计已装的框架(只读;只在 `.agents/framework/reports/` 留下运行期报告) |
|
|
395
|
+
| `/ask-init roots` | 把 `pi-add-dir` 添加的目录同步进框架状态 |
|
|
396
|
+
| `/ask-init --yes` | 跳过确认面板 |
|
|
397
|
+
| `/ask-init --lang zh\|en` | 生成正文的语言(默认按项目自身文档判定) |
|
|
398
|
+
|
|
399
|
+
**机械那一半保证什么**:
|
|
400
|
+
|
|
401
|
+
1. **先快照再动** —— 本次可能写的每一条路径(包括所有会被改写的引用文件)都复制到
|
|
402
|
+
`.agents/framework/snapshots/<stamp>/`,并带 sha256 清单与当时的 `git status`。
|
|
403
|
+
2. **只写仓内** —— 扩展代码绝不写 added root;在那里建 scope card 是普通工具调用,
|
|
404
|
+
因此会经过权限闸门。
|
|
405
|
+
3. **不覆盖** —— 创建时已存在的文件跳过;移动时目标非空则拒绝,并转成人工任务。
|
|
406
|
+
4. **计数对账** —— 引用改写与检测用同一个「裸路径」判定,计划里的「N 处 / M 个文件」
|
|
407
|
+
就是运行时报告的数字(并附逐文件清单)。
|
|
408
|
+
5. **不静默丢内容** —— 迁移的每个源章节在覆盖表里都有一行:命中、声明去向、或标记
|
|
409
|
+
OPEN 并写理由;只要还有 OPEN,审计就不算通过。
|
|
410
|
+
|
|
411
|
+
正文那一半是一份带验收标准的工作单:填空入口、合并旧计划、把 `todos/` 收敛进 specs、
|
|
412
|
+
去重章节、登记预算债务、填覆盖表。模型用只在本轮激活的 `framework_audit` 工具自检收口。
|
|
413
|
+
|
|
414
|
+
**迁移既有项目**会把 `memory/`、`implementations/`、`prompts/` 移到 `.agents/` 下
|
|
415
|
+
(逐个 `git mv`),把 `memory/PLAN.md` 原样搬到 `specs/PLAN.md`,再改写路径引用,
|
|
416
|
+
前后计数都会报出来。`todos/` 退役:需求原文原样进对应单元的 spec。默认是**保守 pass**:
|
|
417
|
+
装框架、修机械漂移(墓碑、重复章节、附录顺序),超预算的 `AGENTS.md` 只登记成债务;
|
|
418
|
+
`--deep` 才允许结构性改写。
|
|
419
|
+
|
|
420
|
+
**审计码**(稳定,可跨次对比):`E-PATH-01` 裸旧路径、`E-STATE-01` 记忆被 gitignore、`W-STATE-01` 记忆尚未提交、`E-INDEX-01` 索引与实际文件不一致、`E-LINK-01` 内部链接失效、
|
|
421
|
+
`W-BUDGET-01` `AGENTS.md` 超预算、`W-DUP-01` 同一章节出现在两个文件、
|
|
422
|
+
`W-ORDER-01` 附录之后又追加章节、`W-COV-01` 覆盖表仍有 OPEN、
|
|
423
|
+
`W-TODO-01` 仍存在退役的 `todos/`、`W-ROOT-02` 根下零字节文件、
|
|
424
|
+
`W-SCAFFOLD-01` 未填的 `TODO(ask-init)` 标记、`I-INSTALL-01` 尚未安装框架。
|
|
425
|
+
|
|
426
|
+
**多仓库工作区**:记忆只住主仓。`pi-add-dir` 会把每个 added root 的 `AGENTS.md`
|
|
427
|
+
全文注入每次请求,所以根里的入口只能是 **scope card**(≤30 行:这里放什么、它的命令、
|
|
428
|
+
去主仓哪个记忆文件读),绝不能是规则的第二份副本。
|
|
429
|
+
|
|
430
|
+
装进项目的框架契约用英文(给模型读、也随 npm 发布);写进项目的正文跟随项目语言。
|
|
431
|
+
|
|
432
|
+
> **跑完记得 `/reload`**,让新的 `AGENTS.md` 进入提示词。框架版本记在
|
|
433
|
+
> `.agents/framework/state.toml`;若项目的状态版本比当前工具新,则拒绝迁移。
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
343
437
|
## 🛠️ 开发
|
|
344
438
|
|
|
345
439
|
<a name="development"></a>
|
|
@@ -353,6 +447,7 @@ pnpm test # vitest(全部功能单元测试)
|
|
|
353
447
|
|
|
354
448
|
pnpm check:keybindings # 用宿主自己的分发表审计本插件的按键
|
|
355
449
|
pnpm check:compat # 校验声明的 pi 下限:强制 4 条线窗口 + 向后 4 条线探针 → public floor
|
|
450
|
+
pnpm approval-report # 汇总本地审批日志(答复分布、目标目录、候选规则)
|
|
356
451
|
```
|
|
357
452
|
|
|
358
453
|
### 项目结构
|
|
@@ -363,6 +458,7 @@ src/
|
|
|
363
458
|
├── lib/ # 共享基础设施(工具集快照/恢复、
|
|
364
459
|
│ # 组合 tool_call 策略的闸门总线、
|
|
365
460
|
│ # 边界闸门会查询的自封闭工具登记表、
|
|
461
|
+
│ # 滚动 JSONL 审批日志、
|
|
366
462
|
│ # review 与 compact 共用的模型 key 解析)
|
|
367
463
|
└── features/ # 功能模块 —— 一个功能一个目录
|
|
368
464
|
├── ask/ # ask 模式:命令 + 状态机、bash 沙箱、
|
|
@@ -375,7 +471,8 @@ src/
|
|
|
375
471
|
├── favorites/ # 模型收藏:选择器与循环 patch、
|
|
376
472
|
│ # 持久化存储、测试
|
|
377
473
|
├── permission/ # 会话级边界权限:模式、面板、bash 拆解、
|
|
378
|
-
│ #
|
|
474
|
+
│ # 策略、会话授权、确认对话框、
|
|
475
|
+
│ # 审计 payload、测试
|
|
379
476
|
├── review/ # 第三方评审:输入/指令触发、
|
|
380
477
|
│ # 评审模型配置存储、提示词、测试
|
|
381
478
|
└── stash/ # 提示词暂存:热键状态机 + 测试
|
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.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, 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",
|
|
@@ -61,6 +65,7 @@
|
|
|
61
65
|
"test": "vitest run",
|
|
62
66
|
"check:keybindings": "node ./scripts/check-keybindings.mjs",
|
|
63
67
|
"check:compat": "node ./scripts/check-compat.mjs",
|
|
68
|
+
"approval-report": "node ./scripts/approval-report.mjs",
|
|
64
69
|
"release:github": "node ./scripts/release-github.mjs",
|
|
65
70
|
"release": "node ./scripts/publish.mjs",
|
|
66
71
|
"tag-current": "node ./scripts/tag-current.mjs"
|
|
@@ -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
|
+
}
|