feat: 系统提示词改为单一入口统一管理

多个扩展各自整段改写系统提示词并按加载顺序接力,任一扩展定死提示词后
其余只能做字符串手术,且各上游的工具说明与组合包实际工具格局冲突。

- 新增 extensions/system-prompt.ts 作为唯一出口(加载列表最后),按
  config/prompt/(preamble.md、tools.jsonc、routing.jsonc、sections.jsonc)
  组合开场白、工具简介/规则与 <tool_routing> 分区;替代原封面与
  tool-routing 扩展,/dump-system-prompt 输出最终提示词。
- pi-chrome、pi-ssh、pi-hermes-memory 改为写命名分区,pi-rtk-optimizer
  改写 promptGuidelines,pi-permission-system 直接过滤结构化 skills,
  均不再返回 systemPrompt;pi-chrome 未授权时不再每轮冻结提示词。
- 新增 prompt-guard 代理加载第三方扩展(Hashline、FFF、LSP、
  context-mode):拦截直接改写提示词并按目录覆盖工具说明;context-mode
  去掉与组合包路由冲突的锚点段,保留记忆与恢复快照。
- 消除冲突:bash 简介不再建议 grep/find;Hashline edit 不再称可用 grep
  锚点、不再介绍已关闭的 replace_text;context-mode 读写/网页路由提示。
- 守门测试:工具目录覆盖率、禁用用法检查、context-mode 上游原文锁定;
  AGENTS.md 新增「系统提示词单一所有权」。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
云服务部-叶林立
2026-10-20 17:53:04 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent dc4fe92e27
commit ba164896d2
39 changed files with 1863 additions and 742 deletions
+1
View File
@@ -25,6 +25,7 @@
### 本地改动
- `devDependencies` 中的 `@earendil-works/pi-ai`、`@earendil-works/pi-coding-agent`、`@earendil-works/pi-tui`、`typebox`、`tsx`、`typescript`、`@types/better-sqlite3` 已从上游的 `^0.80.2` 等范围改为与根包一致的精确版本。上游的宽松范围与根包的 `0.84.2` 不兼容,会让 npm 在本目录嵌套安装第二套 Pi runtime(实测 229 MB),违反组合包"不得再安装一套 Pi runtime"的纪律;`pi-notify/package.json` 是同样的先例。运行时依赖(`better-sqlite3`、`strip-ansi`)与 `peerDependencies` 保持上游原样。
- `before_agent_start` 在结构化 `systemPromptOptions` 下把记忆上下文写入 `sections.memory_policy` 命名分区(而不是上游的 `appendSystemPrompt` 追加),分区顺序由组合包 `config/prompt/sections.jsonc` 统一控制;无结构化 options 的旧宿主分支保持原有的 `systemPrompt` 文本拼接行为。
### 工具与常驻
+5 -5
View File
@@ -306,10 +306,12 @@ export default function (pi: ExtensionAPI) {
if (!promptContext) return;
// pi 0.86+ hands handlers a normalized clone of these options and re-renders
// the prompt from it, so appending through `appendSystemPrompt` keeps the
// the prompt from it, so writing a named `memory_policy` section keeps the
// prompt a set of named sections; returning `systemPrompt` instead forces the
// rendered text and flattens the sections for every later handler and for
// section-aware providers (#251).
// section-aware providers (#251). The bundle's single system-prompt owner
// (extensions/system-prompt.ts) orders the section via
// config/prompt/sections.jsonc.
//
// Branch on that mechanism, not on the field: `systemPromptOptions` is on the
// event at the 0.80.6 floor too, but 0.80.x-0.85.x pass the raw long-lived
@@ -319,9 +321,7 @@ export default function (pi: ExtensionAPI) {
// 0.86+ runner normalizes `sections` onto the clone it passes.
const promptOptions = event.systemPromptOptions;
if (promptOptions && "sections" in promptOptions) {
promptOptions.appendSystemPrompt = [promptOptions.appendSystemPrompt, promptContext]
.filter(Boolean)
.join("\n\n");
(promptOptions.sections as Record<string, string>).memory_policy = promptContext;
return;
}
+21 -12
View File
@@ -2,9 +2,11 @@
* `before_agent_start` prompt injection (#251).
*
* pi 0.86+ passes a normalized *clone* of the prompt options and re-renders the
* prompt from it, so appending the memory context through `appendSystemPrompt`
* prompt from it, so writing the memory context into `sections.memory_policy`
* keeps the prompt a set of named sections for later handlers and section-aware
* providers; returning `systemPrompt` forces the rendered text instead.
* providers; returning `systemPrompt` forces the rendered text instead. The
* bundle's single system-prompt owner (extensions/system-prompt.ts) orders the
* section via `config/prompt/sections.jsonc`.
*
* 0.80.6-0.85.x put `systemPromptOptions` on the event too, but they pass the raw
* long-lived base options and only apply a returned `systemPrompt` — a mutation
@@ -123,31 +125,38 @@ after(async () => {
});
describe("before_agent_start prompt injection", () => {
it("appends through structured prompt options instead of forcing the prompt", async () => {
it("contributes a named section through structured prompt options instead of forcing the prompt", async () => {
await registerWith();
const event = { systemPrompt: "base prompt", systemPromptOptions: structuredOptions() };
const result = await emit("before_agent_start", event);
assert.equal(result, undefined, "structured events must not force a full prompt replacement");
assert.match(event.systemPromptOptions.appendSystemPrompt, /memory-policy/);
assert.match(event.systemPromptOptions.sections.memory_policy, /memory-policy/);
assert.equal(
event.systemPromptOptions.appendSystemPrompt,
"",
"the policy is a named section, not appended text",
);
});
it("appends after an earlier handler's appendSystemPrompt", async () => {
it("keeps earlier sections while adding memory_policy", async () => {
await registerWith();
const event = {
systemPrompt: "base prompt",
systemPromptOptions: structuredOptions({ appendSystemPrompt: "earlier handler text" }),
systemPromptOptions: structuredOptions({
appendSystemPrompt: "earlier handler text",
sections: { other_section: "earlier content" },
}),
};
await emit("before_agent_start", event);
const appended = event.systemPromptOptions.appendSystemPrompt;
assert.match(appended, /earlier handler text/);
assert.match(appended, /memory-policy/);
assert.ok(
appended.indexOf("earlier handler text") < appended.indexOf("memory-policy"),
"the policy goes after earlier contributions",
assert.equal(event.systemPromptOptions.sections.other_section, "earlier content");
assert.match(event.systemPromptOptions.sections.memory_policy, /memory-policy/);
assert.equal(
event.systemPromptOptions.appendSystemPrompt,
"earlier handler text",
);
});