@trim21/personal-pi-extensions 0.0.332 → 0.0.334

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.332",
3
+ "version": "0.0.334",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -0,0 +1,18 @@
1
+ ### aft_callgraph tool
2
+
3
+ 基于真实调用图回答代码关系问题(谁调用我、影响面、调用链),替代 grep + read 的链条式排查。
4
+
5
+ op 选择:
6
+
7
+ - `callers`:谁调用了目标符号。改名/改签名前先查调用点。含测试文件用 `includeTests: true`。
8
+ - `impact`:改动一个符号会波及谁(影响面分析)。与 callers 配合评估重构风险。
9
+ - `call_tree`:目标符号调用了什么(展开调用链)。
10
+ - `trace_to`:从某个入口函数如何执行到目标符号(调用链路径)。
11
+ - `trace_to_symbol`:两个符号之间的最短路径。需要 `toSymbol`;同名符号歧义时用 `toPath` 指定目标文件。
12
+ - `trace_data`:追踪某个值在参数/赋值间的流转。需要 `expression`(如参数名、变量名)。
13
+
14
+ 参数:`path`(包含目标符号的文件)+ `symbol` 必填;`depth` 限制遍历深度。
15
+
16
+ 标记含义:`~` = 仅按名字解析的边(可能指向同名符号);`[unresolved]` = 未解析到定义的调用点(外部库/stdlib 默认折叠为每父节点一条摘要,`includeUnresolved: true` 逐个列出)。
17
+
18
+ 符号未定义或调用图索引仍在构建时返回文本说明,不报错。
@@ -0,0 +1,12 @@
1
+ ### aft_import tool
2
+
3
+ 语言感知的 import 管理:add / remove。
4
+
5
+ 支持 TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, Vue。
6
+
7
+ - `add`:添加默认导入(`default_import`)、具名导入(`names`,支持按名 `as` 别名,如 `['useState']`、`['ERC20', 'IERC20 as IToken']`)、命名空间导入(`namespace`,如 `import * as ns from 'mod'`)。
8
+ - `remove`:移除整个 import(只给 `module`),或只移除其中某个具名导入(`remove_name`)。
9
+ - 语言相关修饰符用 `modifiers`(如 Java/C# `static`、Swift `@testable`);符号类导入用 `import_kind`(如 PHP `function`/`const`、Swift `struct`/`enum`);仅类型导入用 `type_only`(仅 TS)。
10
+ - `validate`:编辑后校验级别,默认 `syntax`,`full` 做完整校验。
11
+
12
+ import 排序整理交给 lint(如 import/order + --fix),本工具不负责排序。写操作走路径级审批。
@@ -6,6 +6,9 @@
6
6
  * 与 aft_refactor 相同:不支持 preview,写保护为路径级审批。
7
7
  */
8
8
 
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+
9
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
13
  import { Type } from "typebox";
11
14
 
@@ -14,6 +17,12 @@ import { guardWriteAccess } from "../lib/write-guard.js";
14
17
  import { callAftTool } from "./bridge.js";
15
18
  import { type AftToolContext, bridgeFor, buildPendantMarkdown, resolvePathArg } from "./tools.js";
16
19
 
20
+ /** 工具使用指南,以 markdown 形式维护,读起来像文档。 */
21
+ const IMPORT_PROMPT = readFileSync(
22
+ fileURLToPath(new URL("import.md", import.meta.url)),
23
+ "utf8",
24
+ ).trim();
25
+
17
26
  const IMPORT_OPS = ["add", "remove"] as const;
18
27
  const VALIDATE_LEVELS = ["syntax", "full"] as const;
19
28
 
@@ -82,6 +91,7 @@ export function registerImportTool(pi: ExtensionAPI, ctx: AftToolContext): void
82
91
  "import 排序整理交给 lint(如 import/order + --fix),本工具不负责。",
83
92
  ].join("\n"),
84
93
  promptSnippet: "Language-aware import add / remove",
94
+ promptGuidelines: [IMPORT_PROMPT],
85
95
  parameters: ImportParams,
86
96
  async execute(_id, params, _signal, _onUpdate, extCtx) {
87
97
  if (params.module === undefined || params.module.trim() === "") {
@@ -0,0 +1,11 @@
1
+ ### aft_outline tool
2
+
3
+ 在读取具体内容之前先了解文件或目录的结构(比整文件 read 省 token)。
4
+
5
+ - target 为文件路径时:返回该文件的符号大纲,每个符号带签名与行号(如 `function greet(name: string): void 5:12`);Markdown/HTML 返回标题层级。适合先定位符号名和位置,再决定是否用 aft_zoom 看实现。
6
+ - target 为目录路径时:默认返回扁平文件树(语言、顶层符号数、字节大小),一眼看清目录里有什么、规模多大;传 `files: false` 可切换为符号大纲(目录下每个文件的符号树,无签名,输出上限 30KB,超限截断)。
7
+ - `includeTests` 只在符号大纲模式(`files: false`)生效:为 true 时包含测试文件,默认排除。
8
+ - 目录递归上限 200 个文件;超出部分在结果中标记截断。
9
+ - 只接受单个 target,不支持数组;跨文件批量用多次调用。
10
+
11
+ 分工:看结构用本工具;看某个符号的完整源码用 aft_zoom;看符号间调用关系用 aft_callgraph;按语义/文本搜代码用 aft_search。
@@ -0,0 +1,9 @@
1
+ ### aft_refactor tool
2
+
3
+ workspace-wide 重构:跨文件移动符号、抽函数、内联,自动更新引用与 import。
4
+
5
+ - `move`:把顶层符号(非嵌套函数/类方法)移到另一个文件,全 workspace 重写 import 与引用;执行前自动创建 checkpoint。需要 `symbol` + `destination`;同名符号歧义时用 `scope` 消歧。注意:move / rename 整个文件请用 aft_move(OS 层操作,不更新引用)。
6
+ - `extract`:把行区间抽成新函数(TS/JS/TSX、Python)。需要 `name` + `start_line` + `end_line`(1 起,含端点)。
7
+ - `inline`:把调用点替换为函数体。需要 `symbol` + `call_site_line`(1 起)。
8
+
9
+ 重构是写操作:不支持 preview(无 diff 预览),写保护退化为路径级审批——workspace 内目标自动放行,外部路径需要确认。重构前先看影响面:对目标符号跑 aft_callgraph `impact`,确认波及范围符合预期再动手。
@@ -7,6 +7,9 @@
7
7
  * (workspace 内自动放行,外部路径经 write-guard 确认,无 diff 预览)。
8
8
  */
9
9
 
10
+ import { readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+
10
13
  import { coerceOptionalInt } from "@cortexkit/aft-bridge";
11
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
15
  import { Type } from "typebox";
@@ -16,6 +19,12 @@ import { guardWriteAccess } from "../lib/write-guard.js";
16
19
  import { callAftTool } from "./bridge.js";
17
20
  import { type AftToolContext, bridgeFor, buildPendantMarkdown, resolvePathArg } from "./tools.js";
18
21
 
22
+ /** 工具使用指南,以 markdown 形式维护,读起来像文档。 */
23
+ const REFACTOR_PROMPT = readFileSync(
24
+ fileURLToPath(new URL("refactor.md", import.meta.url)),
25
+ "utf8",
26
+ ).trim();
27
+
19
28
  const REFACTOR_OPS = ["move", "extract", "inline"] as const;
20
29
 
21
30
  function requireField(value: unknown, name: string, op: string): void {
@@ -68,6 +77,7 @@ export function registerRefactorTool(pi: ExtensionAPI, ctx: AftToolContext): voi
68
77
  "move / rename 整个文件用 aft_move(OS 层操作,不更新引用);移动代码符号用本工具 op=move。",
69
78
  ].join("\n"),
70
79
  promptSnippet: "Workspace-wide symbol move / function extraction / inlining",
80
+ promptGuidelines: [REFACTOR_PROMPT],
71
81
  parameters: RefactorParams,
72
82
  async execute(_id, params, _signal, _onUpdate, extCtx) {
73
83
  const startLine = coerceOptionalInt(
@@ -0,0 +1,11 @@
1
+ ### aft_search tool
2
+
3
+ 一个工具完成代码搜索,按意图自动路由到合适的引擎并按相关度排序。
4
+
5
+ - 概念类查询("ORM 如何构建并执行查询")用自然语言整句:语义通道理解意图,匹配 docstring 与注释。
6
+ - 精确的名字、字符串、正则保持简短(`^export`、`Cargo.lock`):走词法/正则匹配通道。
7
+ - `topK`:最大结果数,默认 10,最大 100。
8
+ - `includeTests: true`:包含测试文件,默认排除。
9
+ - `path`:仅当要搜索另一个 Git 项目时设置(绝对或 ~ 路径);默认搜索当前项目。
10
+
11
+ 需要语义索引可用(aft.jsonc 中 `semantic_search: true` 且 embedding 后端就绪);否则退化为词法/正则通道。启用语义索引时,首次调用会阻塞到索引构建完成,避免拿到部分结果。
package/src/aft/tools.ts CHANGED
@@ -5,8 +5,11 @@
5
5
  * 由 Rust 侧(tree-sitter 符号表 / trigram 索引 / 调用图)计算。
6
6
  */
7
7
 
8
+ import { readFileSync } from "node:fs";
9
+ import { stat } from "node:fs/promises";
8
10
  import { homedir } from "node:os";
9
11
  import { isAbsolute, join, relative, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
10
13
 
11
14
  import {
12
15
  type AftProjectTransport,
@@ -23,6 +26,21 @@ import { Type } from "typebox";
23
26
  import { type ToolPendant } from "../lib/pendant.js";
24
27
  import { callAftTool, SEMANTIC_INDEX_WAIT_TIMEOUT_MS } from "./bridge.js";
25
28
 
29
+ /** 工具使用指南,以 markdown 形式维护,读起来像文档。 */
30
+ const OUTLINE_PROMPT = readFileSync(
31
+ fileURLToPath(new URL("outline.md", import.meta.url)),
32
+ "utf8",
33
+ ).trim();
34
+ const ZOOM_PROMPT = readFileSync(fileURLToPath(new URL("zoom.md", import.meta.url)), "utf8").trim();
35
+ const CALLGRAPH_PROMPT = readFileSync(
36
+ fileURLToPath(new URL("callgraph.md", import.meta.url)),
37
+ "utf8",
38
+ ).trim();
39
+ const SEARCH_PROMPT = readFileSync(
40
+ fileURLToPath(new URL("search.md", import.meta.url)),
41
+ "utf8",
42
+ ).trim();
43
+
26
44
  /** 解析 `~` 前缀与相对路径(相对 session cwd)。URL 与绝对路径原样返回。 */
27
45
  export function resolvePathArg(cwd: string, input: string): string {
28
46
  if (input === "~" || input.startsWith("~/")) {
@@ -84,16 +102,18 @@ const OutlineParams = Type.Object(
84
102
  {
85
103
  target: Type.String({
86
104
  description:
87
- "要 outline 的对象:文件路径、目录路径或 URL(http:// 或 https://)。只接受单个 target;目录递归上限 200 个文件。",
105
+ "要 outline 的对象:文件路径或目录路径。只接受单个 target;目录递归上限 200 个文件。",
88
106
  }),
89
107
  files: Type.Optional(
90
108
  Type.Boolean({
91
109
  description:
92
- "目录模式:为 true 时 target 必须是目录,返回带语言/符号数/字节大小的扁平文件树,而非符号大纲。",
110
+ " true 时 target 必须是目录,返回带语言/符号数/字节大小的扁平文件树,而非符号大纲。默认:target 为目录时 true,为文件时 false。",
93
111
  }),
94
112
  ),
95
113
  includeTests: Type.Optional(
96
- Type.Boolean({ description: "目录大纲:包含测试文件。默认 false。" }),
114
+ Type.Boolean({
115
+ description: "目录符号大纲(files: false)模式:包含测试文件。默认 false。",
116
+ }),
97
117
  ),
98
118
  },
99
119
  { additionalProperties: false },
@@ -104,25 +124,28 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
104
124
  name: "aft_outline",
105
125
  label: "aft_outline",
106
126
  description: [
107
- "输出代码文件、目录、URL 的结构化大纲:函数/类/类型等符号及其行号范围;Markdown/HTML 返回标题层级。",
127
+ "输出代码文件、目录的结构化大纲:函数/类/类型等符号及其行号范围;Markdown/HTML 返回标题层级。",
108
128
  "用它在读取具体内容之前先了解文件结构(比整文件 read 省 token)。",
109
129
  "深入了解某个符号用 aft_zoom;看跨文件调用关系用 aft_callgraph。",
110
- "target 支持:文件路径(带签名的符号大纲)、目录路径(递归最多 200 文件)、URL。只接受单个 target。",
111
- "files: true 且 target 为目录时返回扁平文件树(语言、顶层符号数、字节大小)。",
130
+ "target 支持:文件路径(带签名的符号大纲)、目录路径(递归最多 200 文件)。只接受单个 target。",
131
+ "target 为目录时默认返回扁平文件树(语言、顶层符号数、字节大小);传 files: false 可改回符号大纲。",
112
132
  ].join("\n"),
113
- promptSnippet: "Output structural outline of a file/directory/URL",
133
+ promptSnippet: "Output structural outline of a file/directory",
134
+ promptGuidelines: [OUTLINE_PROMPT],
114
135
  parameters: OutlineParams,
115
136
  async execute(_id, params, _signal, _onUpdate, extCtx) {
116
137
  const target = coerceTargetParam(params.target);
117
138
  if (typeof target !== "string" || target.length === 0) {
118
- throw new Error("'target' must be a single path or URL (array targets are not supported)");
139
+ throw new Error("'target' must be a single path (array targets are not supported)");
140
+ }
141
+ const resolved = resolvePathArg(extCtx.cwd, target);
142
+ let filesMode = coerceBoolean(params.files);
143
+ if (params.files === undefined) {
144
+ const stats = await stat(resolved).catch(() => null);
145
+ filesMode = stats?.isDirectory() ?? false;
119
146
  }
120
- const filesMode = coerceBoolean(params.files);
121
147
  const rawArgs: Record<string, unknown> = {
122
- target:
123
- filesMode || target.startsWith("http://") || target.startsWith("https://")
124
- ? target
125
- : resolvePathArg(extCtx.cwd, target),
148
+ target: filesMode ? target : resolved,
126
149
  };
127
150
  if (filesMode) rawArgs.files = true;
128
151
  if (params.includeTests !== undefined) rawArgs.includeTests = params.includeTests;
@@ -139,23 +162,12 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
139
162
  });
140
163
  }
141
164
 
142
- const ZoomTarget = Type.Object({
143
- path: Type.String({ description: "文件路径(绝对或相对项目根)" }),
144
- symbol: Type.String({ description: "该文件中的符号名" }),
145
- });
146
-
147
165
  const ZoomParams = Type.Object(
148
166
  {
149
- path: Type.Optional(Type.String({ description: "文件路径(绝对或相对项目根)" })),
150
- url: Type.Optional(Type.String({ description: "要 zoom 的 HTML/Markdown 文档 URL" })),
167
+ path: Type.String({ description: "文件路径(绝对或相对项目根)" }),
151
168
  symbols: Type.Optional(
152
169
  Type.Union([Type.String(), Type.Array(Type.String())], {
153
- description: "符号名(代码)或标题文本(Markdown/HTML);字符串或数组(同文件批量查询)。",
154
- }),
155
- ),
156
- targets: Type.Optional(
157
- Type.Union([ZoomTarget, Type.Array(ZoomTarget)], {
158
- description: "跨文件批量:`{ path, symbol }` 或数组。与 path/url/symbols 互斥。",
170
+ description: "符号名(代码)或标题文本;字符串或数组(同文件批量查询)。",
159
171
  }),
160
172
  ),
161
173
  contextLines: Type.Optional(
@@ -180,51 +192,16 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
180
192
  "查看命名符号(函数/类/类型)的完整源码,或 Markdown/HTML 的标题段落内容。",
181
193
  "需要理解某个具体符号时用它(读整个文件用 read)。",
182
194
  "callgraph: true 时附带同文件内的调用关系标注。",
183
- "三种模式互斥,用且仅用一种:`{ path, symbols }`、`{ url, symbols }`、`{ targets }`。",
195
+ "同文件多符号用 `symbols` 数组。",
184
196
  ].join("\n"),
185
197
  promptSnippet: "Inspect the full source of a named symbol",
198
+ promptGuidelines: [ZOOM_PROMPT],
186
199
  parameters: ZoomParams,
187
200
  async execute(_id, params, _signal, _onUpdate, extCtx) {
188
- const isEmpty = (v: unknown): boolean =>
189
- v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
190
-
191
- const hasPath = !isEmpty(params.path);
192
- const hasUrl = !isEmpty(params.url);
193
- const hasSymbols = !isEmpty(params.symbols);
194
- const hasTargets = !isEmpty(params.targets);
195
-
196
- if (hasTargets && (hasPath || hasUrl || hasSymbols)) {
197
- throw new Error("'targets' 与 'path'/'url'/'symbols' 互斥,只能提供一种模式");
198
- }
199
- if (hasPath && hasUrl) {
200
- throw new Error("'path' 与 'url' 互斥,只能提供一种");
201
- }
202
- if (!hasTargets && !hasPath && !hasUrl) {
203
- throw new Error("Provide exactly one of 'path', 'url', or 'targets'");
204
- }
205
-
206
- const rawArgs: Record<string, unknown> = {};
207
- if (hasTargets) {
208
- const targetList = params.targets;
209
- if (!targetList) {
210
- throw new Error("'targets' must be a non-empty object or array");
211
- }
212
- const list = Array.isArray(targetList) ? targetList : [targetList];
213
- rawArgs.targets = list.map((t) => ({
214
- filePath: resolvePathArg(extCtx.cwd, t.path),
215
- symbol: t.symbol,
216
- }));
217
- } else if (hasUrl) {
218
- rawArgs.url = params.url;
219
- if (hasSymbols) rawArgs.symbols = params.symbols;
220
- } else {
221
- const filePath = params.path;
222
- if (!filePath) {
223
- throw new Error("'path' must be a non-empty string");
224
- }
225
- rawArgs.filePath = resolvePathArg(extCtx.cwd, filePath);
226
- if (hasSymbols) rawArgs.symbols = params.symbols;
227
- }
201
+ const rawArgs: Record<string, unknown> = {
202
+ filePath: resolvePathArg(extCtx.cwd, params.path),
203
+ ...(params.symbols && { symbols: params.symbols }),
204
+ };
228
205
 
229
206
  const contextLines = coerceOptionalInt(
230
207
  params.contextLines,
@@ -260,35 +237,15 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
260
237
  });
261
238
  }
262
239
 
263
- /** 构建 aft_zoom pendant 的 subtitle:`path="…" symbol="…"`,url 模式为 `url="…"`。 */
240
+ /** 构建 aft_zoom pendant 的 subtitle:`path="…" symbol="…"`。 */
264
241
  export function buildZoomSubtitle(
265
242
  cwd: string,
266
243
  params: Type.Static<typeof ZoomParams>,
267
244
  ): string | undefined {
268
- const isEmpty = (v: unknown): boolean =>
269
- v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
270
-
271
- const parts: string[] = [];
272
- const targets = params.targets;
273
- if (Array.isArray(targets)) {
274
- for (const t of targets) {
275
- parts.push(
276
- `path="${formatDisplayPath(cwd, resolvePathArg(cwd, t.path))}" symbol="${t.symbol}"`,
277
- );
278
- }
279
- } else if (targets) {
280
- parts.push(
281
- `path="${formatDisplayPath(cwd, resolvePathArg(cwd, targets.path))}" symbol="${targets.symbol}"`,
282
- );
283
- } else if (!isEmpty(params.url)) {
284
- parts.push(`url="${params.url}"`);
285
- } else if (params.path) {
286
- const symbols = params.symbols;
287
- const symbolStr = Array.isArray(symbols) ? symbols.join(", ") : symbols;
288
- const pathPart = `path="${formatDisplayPath(cwd, resolvePathArg(cwd, params.path))}"`;
289
- parts.push(symbolStr ? `${pathPart} symbol="${symbolStr}"` : pathPart);
290
- }
291
- return parts.length > 0 ? parts.join(" ") : undefined;
245
+ const symbols = params.symbols;
246
+ const symbolStr = Array.isArray(symbols) ? symbols.join(", ") : symbols;
247
+ const pathPart = `path="${formatDisplayPath(cwd, resolvePathArg(cwd, params.path))}"`;
248
+ return symbolStr ? `${pathPart} symbol="${symbolStr}"` : pathPart;
292
249
  }
293
250
 
294
251
  const CALLGRAPH_OPS = [
@@ -345,6 +302,7 @@ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): vo
345
302
  "标记:~ = 仅按名字解析的边(可能指向同名符号);[unresolved] = 未解析到定义的调用点。",
346
303
  ].join("\n"),
347
304
  promptSnippet: "Call graph and data-flow navigation",
305
+ promptGuidelines: [CALLGRAPH_PROMPT],
348
306
  parameters: CallgraphParams,
349
307
  async execute(_id, params, _signal, _onUpdate, extCtx) {
350
308
  const rawArgs: Record<string, unknown> = {
@@ -437,6 +395,7 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
437
395
  "启用语义索引时,首次调用会阻塞到索引构建完成,避免返回部分结果。",
438
396
  ].join("\n"),
439
397
  promptSnippet: "Search code by meaning or exact text",
398
+ promptGuidelines: [SEARCH_PROMPT],
440
399
  parameters: SearchParams,
441
400
  async execute(_id, params, _signal, _onUpdate, extCtx) {
442
401
  if (typeof params.query !== "string" || params.query.trim().length === 0) {
@@ -0,0 +1,11 @@
1
+ ### aft_zoom tool
2
+
3
+ 查看命名符号(函数/类/类型)的完整源码,或 Markdown/HTML 文档的标题段落内容。
4
+
5
+ - `path` 必填:文件路径(绝对或相对项目根)。
6
+ - `symbols`:要查看的符号名;字符串或数组(同文件多个符号一次查询)。代码文件按符号名精确解析;Markdown/HTML 按标题文本匹配(可含空格,用完整标题字符串)。
7
+ - `contextLines`:符号前后附加的上下文行数,默认 3。
8
+ - `callgraph: true`:附带同文件内 calls-out / called-by 调用关系标注,帮助理解符号如何被使用、依赖谁。
9
+ - 适合先 aft_outline 拿到符号名与行号,再 zoom 具体实现;不要用它读整个文件(用 read)。
10
+
11
+ 只支持本地文件路径,不支持 URL。
@@ -1,4 +1,4 @@
1
- # Bash tool
1
+ ### Bash tool
2
2
 
3
3
  Executes a given bash command and returns its output.
4
4
 
@@ -1,4 +1,4 @@
1
- # Edit tool
1
+ ### Edit tool
2
2
 
3
3
  Performs exact string replacements in files.
4
4
 
@@ -228,7 +228,7 @@ export function registerFileTools(
228
228
  "This tool reads files, not directories.",
229
229
  ].join("\n"),
230
230
  promptSnippet: "Read files from the local filesystem with line numbers",
231
- promptGuidelines: [`- -\n${READ_PROMPT}`],
231
+ promptGuidelines: [READ_PROMPT],
232
232
  parameters: Type.Object(
233
233
  {
234
234
  file_path: Type.String({ description: "The absolute path to the file to read" }),
@@ -331,7 +331,7 @@ export function registerFileTools(
331
331
  "This tool does not use regular expressions or fuzzy matching.",
332
332
  ].join("\n"),
333
333
  promptSnippet: "Make exact string replacements in files",
334
- promptGuidelines: [`- -\n${EDIT_PROMPT}`],
334
+ promptGuidelines: [EDIT_PROMPT],
335
335
  parameters: Type.Object(
336
336
  {
337
337
  file_path: Type.String({ description: "The absolute path to the file to modify" }),
@@ -502,7 +502,7 @@ export function registerFileTools(
502
502
  "If the file exists, you must use Read first. Prefer Edit for partial changes.",
503
503
  ].join("\n"),
504
504
  promptSnippet: "Create or overwrite files",
505
- promptGuidelines: [`- -\n${WRITE_PROMPT}`],
505
+ promptGuidelines: [WRITE_PROMPT],
506
506
  parameters: Type.Object(
507
507
  {
508
508
  file_path: Type.String({
@@ -1,4 +1,4 @@
1
- # Glob tool
1
+ ### Glob tool
2
2
 
3
3
  - Fast file pattern matching tool that works with any codebase size
4
4
  - Supports glob patterns like `**/*.js` or `src/**/*.ts`
@@ -111,7 +111,7 @@ export function registerGlobTool(pi: ExtensionAPI): void {
111
111
  "Returns matching file paths sorted by modification time (oldest first).",
112
112
  ].join("\n"),
113
113
  promptSnippet: "Find files by name patterns",
114
- promptGuidelines: [`- -\n${GLOB_PROMPT}`],
114
+ promptGuidelines: [GLOB_PROMPT],
115
115
  parameters: Type.Object(
116
116
  {
117
117
  pattern: Type.String({ description: "The glob pattern to match files against" }),
@@ -1,4 +1,4 @@
1
- # Grep tool
1
+ ### Grep tool
2
2
 
3
3
  A powerful search tool built on ripgrep.
4
4
 
@@ -241,7 +241,7 @@ export function registerGrepTool(pi: ExtensionAPI): void {
241
241
  'output_mode defaults to "files_with_matches"; use "content" for matching lines or "count" for match counts.',
242
242
  ].join("\n"),
243
243
  promptSnippet: "Search file contents with regular expressions",
244
- promptGuidelines: [`- -\n${GREP_PROMPT}`],
244
+ promptGuidelines: [GREP_PROMPT],
245
245
  parameters: grepParametersSchema,
246
246
  async execute(_id, params, signal, _onUpdate, ctx) {
247
247
  if (
@@ -1,4 +1,4 @@
1
- # Read tool
1
+ ### Read tool
2
2
 
3
3
  Reads a file from the local filesystem. You can access any file directly by using this tool.
4
4
 
@@ -177,7 +177,7 @@ export function registerSessionTools(pi: ExtensionAPI): void {
177
177
  'If you recommend an option, put it first and append "(Recommended)" to its label.',
178
178
  ].join("\n"),
179
179
  promptSnippet: "Ask the user questions during execution",
180
- promptGuidelines: [`- -\n${ASK_PROMPT}`],
180
+ promptGuidelines: [ASK_PROMPT],
181
181
  parameters: Type.Object(
182
182
  {
183
183
  questions: Type.Array(questionSchema, {
@@ -79,7 +79,7 @@ export function registerShellTools(
79
79
  pi.registerTool({
80
80
  name: "Bash",
81
81
  promptSnippet: "execute command",
82
- promptGuidelines: [`- -\n${BASH_PROMPT}`],
82
+ promptGuidelines: [BASH_PROMPT],
83
83
  label: "Bash",
84
84
  description: [
85
85
  "Executes a given bash command synchronously and returns its output.",
@@ -1,4 +1,4 @@
1
- # Write tool
1
+ ### Write tool
2
2
 
3
3
  Writes a file to the local filesystem.
4
4
 
@@ -4,9 +4,9 @@
4
4
  *
5
5
  * The subagent definition comes from `~/.pi/agent/agents/*.md` (markdown with
6
6
  * YAML frontmatter, see spawn-agent-agents.ts). The extension discovers the
7
- * available subagent types once at startup and appends them to the system
8
- * prompt on every agent start (same pattern as the bwrap extension), so the
9
- * model always knows which `agent` names it can pass to the tool. Execution
7
+ * available subagent types once at startup and injects the list via the tool's
8
+ * `promptGuidelines`, so the model always knows which `agent` names it can
9
+ * pass to the tool. Execution
10
10
  * is blocking: the tool awaits the subagent process until it exits and
11
11
  * returns its final output to the parent model. Progress is streamed through
12
12
  * `onUpdate`, the same channel the built-in bash tool uses for live output.
@@ -622,7 +622,7 @@ function parseJsonRecord(line: string): Record<string, unknown> | null {
622
622
  export function formatAgentListSection(agents: AgentConfig[]): string {
623
623
  const lines = agents.map((a) => `- \`${a.name}\`: ${a.description}`);
624
624
  return [
625
- "## Available subagents",
625
+ "### Available subagents",
626
626
  "",
627
627
  "You can delegate tasks to the following subagent types by calling the `spawn-agent` tool with their name in the `agent` parameter:",
628
628
  "",
@@ -653,15 +653,6 @@ export default function spawnAgent(pi: ExtensionAPI) {
653
653
  );
654
654
  const agentListSection = agents.length > 0 ? formatAgentListSection(agents) : null;
655
655
 
656
- if (agentListSection) {
657
- // Same pattern as the bwrap extension: append the list to the system
658
- // prompt on every agent start. The system prompt is rebuilt each turn
659
- // anyway, so a persistent per-session injection would add no value.
660
- pi.on("before_agent_start", (event) => {
661
- return { systemPrompt: `${event.systemPrompt}\n\n${agentListSection}` };
662
- });
663
- }
664
-
665
656
  pi.registerTool<typeof spawnAgentSchema, SubagentDetails>({
666
657
  name: "spawn-agent",
667
658
  label: "spawn-agent",
@@ -670,6 +661,7 @@ export default function spawnAgent(pi: ExtensionAPI) {
670
661
  "The `agent` parameter must be one of the available subagent types listed in the system prompt.",
671
662
  `Subagents run read-only (${DEFAULT_TOOLS.join(", ")}) unless the agent declares an explicit toolset.`,
672
663
  ].join(" "),
664
+ promptGuidelines: agentListSection ? [agentListSection] : undefined,
673
665
  parameters: spawnAgentSchema,
674
666
 
675
667
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -71,11 +71,11 @@ export function formatTools(
71
71
  return lines.length > 0 ? lines.join("\n") : "(none)";
72
72
  }
73
73
 
74
- /** 渲染工具特定 guideline bullets;为空时整个块(含标题)省略 */
74
+ /** 渲染工具特定 guideline 块;为空时整个块(含标题)省略。guide 本身是 md 文档,直接拼接。 */
75
75
  export function formatGuidelines(promptGuidelines: string[] | undefined): string {
76
- const bullets = (promptGuidelines ?? []).map((g) => g.trim()).filter((g) => g.length > 0);
77
- if (bullets.length === 0) return "";
78
- return `## Guidelines\n\n${bullets.map((g) => `- ${g}`).join("\n")}`;
76
+ const items = (promptGuidelines ?? []).map((g) => g.trim()).filter((g) => g.length > 0);
77
+ if (items.length === 0) return "";
78
+ return `## Guidelines\n\n${items.join("\n\n")}`;
79
79
  }
80
80
 
81
81
  /** 渲染 AGENTS.md 等上下文文件;为空时省略 */