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
@@ -18,6 +18,7 @@ function makeSetup() {
return {
handler,
session,
permissionManager,
configStore,
forwarding,
warmParser,
@@ -55,4 +56,35 @@ describe("AgentPrepHandler", () => {
expect(result).toEqual({});
expect(session.getActiveSkillEntries()).toEqual([]);
});
it("filters denied skills out of structured systemPromptOptions.skills", async () => {
const { handler, session, permissionManager } = makeSetup();
vi.mocked(permissionManager.check).mockImplementation((intent) =>
makeCheckResult({
state:
intent.kind === "tool" &&
(intent.input as { name?: string })?.name === "dangerous"
? "deny"
: "allow",
}),
);
const options = {
skills: [
{ name: "librarian", description: "d", filePath: "/skills/librarian/SKILL.md", baseDir: "/skills/librarian" },
{ name: "dangerous", description: "d", filePath: "/skills/dangerous/SKILL.md", baseDir: "/skills/dangerous" },
{ name: "manual", description: "d", filePath: "/skills/manual/SKILL.md", baseDir: "/skills/manual", disableModelInvocation: true },
],
};
const result = await handler.handle(
{ systemPrompt: "You are an assistant.", systemPromptOptions: options },
makeCtx(),
);
expect(result).toEqual({});
expect(options.skills.map((skill) => skill.name)).toEqual(["librarian", "manual"]);
expect(session.getActiveSkillEntries().map((entry) => entry.name)).toEqual([
"librarian",
]);
});
});
@@ -7,6 +7,7 @@ import {
findSkillPathMatch,
parseAllSkillPromptSections,
resolveSkillPromptEntries,
resolveStructuredSkillEntries,
type SkillPermissionChecker,
} from "#src/skill-prompt-sanitizer";
import type { PermissionCheckResult } from "#src/types";
@@ -182,6 +183,92 @@ describe("resolveSkillPromptEntries", () => {
});
});
// ── resolveStructuredSkillEntries ───────────────────────────────────────────
describe("resolveStructuredSkillEntries", () => {
const makeSkill = (
name: string,
extra: Record<string, unknown> = {},
): Record<string, unknown> => ({
name,
description: `Description of ${name}`,
filePath: `/skills/${name}/SKILL.md`,
baseDir: `/skills/${name}`,
disableModelInvocation: false,
...extra,
});
test("filters denied skills out of the array and keeps them out of entries", () => {
const manager = makeManager("allow", { dangerous: "deny" });
const skills = [makeSkill("librarian"), makeSkill("dangerous")];
const result = resolveStructuredSkillEntries(
skills,
manager,
null,
normalizer,
);
expect(result.skills.map((skill) => skill.name)).toEqual(["librarian"]);
expect(result.entries.map((entry) => entry.name)).toEqual(["librarian"]);
expect(result.entries[0].state).toBe("allow");
expect(result.entries[0].location).toBe("/skills/librarian/SKILL.md");
expect(result.entries[0].normalizedBaseDir).toBe("/skills/librarian");
});
test("keeps disableModelInvocation skills in the array but out of entries", () => {
const manager = makeManager("allow");
const skills = [
makeSkill("visible"),
makeSkill("manual", { disableModelInvocation: true }),
];
const result = resolveStructuredSkillEntries(
skills,
manager,
null,
normalizer,
);
expect(result.skills.map((skill) => skill.name)).toEqual([
"visible",
"manual",
]);
expect(result.entries.map((entry) => entry.name)).toEqual(["visible"]);
});
test("passes through entries without a usable name", () => {
const manager = makeManager("deny");
const skills = [{ description: "no name" }];
const result = resolveStructuredSkillEntries(
skills,
manager,
null,
normalizer,
);
expect(result.skills).toHaveLength(1);
expect(manager.checkPermission).not.toHaveBeenCalled();
});
test("caches permission results per unique skill name", () => {
const manager = makeManager("allow");
const skills = [makeSkill("librarian"), makeSkill("librarian")];
resolveStructuredSkillEntries(skills, manager, null, normalizer);
expect(manager.checkPermission).toHaveBeenCalledTimes(1);
});
test("passes agentName to the permission check", () => {
const manager = makeManager("allow");
resolveStructuredSkillEntries(
[makeSkill("librarian")],
manager,
"my-agent",
normalizer,
);
expect(manager.checkPermission).toHaveBeenCalledWith(
"skill",
{ name: "librarian" },
"my-agent",
);
});
});
// ── findSkillPathMatch ──────────────────────────────────────────────────────
describe("findSkillPathMatch", () => {