mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
- 组合包拥有、可随时重建的配置、日志与缓存统一放在 <agentDir>/my-pi/:permission-system/(policy.jsonc、hard-guards.jsonc、 logs/)、rtk-optimizer/config.json、tool-search/(config.json、 catalog-v1.json)与 system-prompt-cover.md。hashline.json、lsp.json 由外部扩展读取固定路径,是唯一例外。 - 新增 config/bundle-manifest.json 与 scripts/rebuild-bundle-config.mjs: 先校验全部来源与目标,在临时目录构建完整树,再整体删除旧 my-pi/ (符号链接只删链接)并原子替换,不备份。install.sh / update.sh 每次 运行都调用它;扩展自身不再播种或迁移配置。 - update.sh 删除 pi update 失败后的 git clean 恢复逻辑:Pi 1.3.0 会在 下一次 pi update 时依据中断标记自行恢复。同时删除 Tool Search settings.json 快照迁移与封面部署脚本。 - Tool Search 改为读取 my-pi/tool-search/config.json(源为 config/tool-search.json,须与 BUNDLE_TOOL_SEARCH_DEFAULTS 一致), 不再读写 settings.json 的 toolSearch 段,移除 bundleDefaultsVersion。 - 默认权限策略开启 debugLog。 - 旧位置的残留文件不迁移、不清理;AGENTS.md / README 同步新约束。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
readCoverFile,
|
|
resolveCoverPath,
|
|
spliceCover,
|
|
} from "../extensions/system-prompt-cover-config.ts";
|
|
|
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const bundledCoverPath = join(repositoryRoot, "config", "system-prompt-cover.md");
|
|
|
|
const DEFAULT_PREAMBLE =
|
|
"You are an expert coding assistant operating inside pi, a coding agent harness. " +
|
|
"You help users by reading files, executing commands, editing code, and writing new files.";
|
|
|
|
/** A rendered prompt shaped like Pi's: preamble, then tag-wrapped top-level sections. */
|
|
function renderedPrompt(preamble = DEFAULT_PREAMBLE) {
|
|
return [
|
|
preamble,
|
|
"<tools>",
|
|
"- read: reads files",
|
|
"</tools>",
|
|
"<rules>",
|
|
"- Be concise",
|
|
"</rules>",
|
|
"<skills>",
|
|
"<available_skills>",
|
|
" <skill>",
|
|
" <name>s</name>",
|
|
" </skill>",
|
|
"</available_skills>",
|
|
"</skills>",
|
|
"<project_context>",
|
|
'<project_instructions path="/work/AGENTS.md">',
|
|
"Nested content with a </project_context>-looking line is not a tag.",
|
|
"</project_instructions>",
|
|
"</project_context>",
|
|
"<cwd>",
|
|
"/work",
|
|
"</cwd>",
|
|
].join("\n");
|
|
}
|
|
|
|
function tagsOf(text: string): string[] {
|
|
return [...text.matchAll(/^<([a-z][a-z0-9_-]*)>$/gm)].map((match) => match[1]);
|
|
}
|
|
|
|
async function makeTempDir() {
|
|
return mkdtemp(join(tmpdir(), "my-pi-cover-"));
|
|
}
|
|
|
|
test("bundled cover holds Pi's default preamble text", async () => {
|
|
const content = await readFile(bundledCoverPath, "utf8");
|
|
assert.equal(content.replace(/\s+$/, ""), DEFAULT_PREAMBLE);
|
|
});
|
|
|
|
test("spliceCover replaces only the preamble and preserves the remaining bytes", () => {
|
|
const rendered = renderedPrompt();
|
|
const spliced = spliceCover(rendered, "MY CUSTOM COVER");
|
|
|
|
assert.ok(spliced.startsWith("MY CUSTOM COVER\n"));
|
|
|
|
// Everything after the preamble's terminating newline is untouched.
|
|
const originalTail = rendered.slice(rendered.indexOf("\n<tools>"));
|
|
assert.ok(spliced.endsWith(originalTail));
|
|
|
|
// Section order — including nested tags — is identical to the unmodified prompt.
|
|
assert.deepEqual(tagsOf(spliced), tagsOf(rendered));
|
|
assert.deepEqual(tagsOf(spliced), [
|
|
"tools",
|
|
"rules",
|
|
"skills",
|
|
"available_skills",
|
|
"project_context",
|
|
"cwd",
|
|
]);
|
|
});
|
|
|
|
test("spliceCover keeps section order rather than appending recovered sections", () => {
|
|
const rendered = renderedPrompt("ORIGINAL COVER");
|
|
const spliced = spliceCover(rendered, "NEW COVER");
|
|
const order = tagsOf(spliced).join(",");
|
|
|
|
assert.equal(order, "tools,rules,skills,available_skills,project_context,cwd");
|
|
assert.ok(
|
|
order.indexOf("tools") < order.indexOf("cwd"),
|
|
"tools must still precede cwd; a re-appended section list would invert this",
|
|
);
|
|
assert.ok(
|
|
order.indexOf("project_context") < order.indexOf("cwd"),
|
|
"project_context must keep its original position",
|
|
);
|
|
});
|
|
|
|
test("spliceCover tolerates a trailing newline from the editor and a blank cover", () => {
|
|
const rendered = renderedPrompt();
|
|
|
|
assert.equal(spliceCover(rendered, "COVER\n\n"), spliceCover(rendered, "COVER"));
|
|
// A whitespace-only cover leaves an empty preamble but still keeps every section.
|
|
const blank = spliceCover(rendered, " \n");
|
|
assert.equal(blank, rendered.slice(rendered.indexOf("\n<tools>")));
|
|
assert.deepEqual(tagsOf(blank), tagsOf(rendered));
|
|
});
|
|
|
|
test("spliceCover degenerates to the cover when the prompt has no sections", () => {
|
|
assert.equal(spliceCover("just a preamble", "NEW"), "NEW");
|
|
assert.equal(spliceCover("", "NEW"), "NEW");
|
|
});
|
|
|
|
test("spliceCover keeps an attribute-bearing nested tag inside its parent section", () => {
|
|
// Pi nests <project_instructions path="..."> inside the bare <project_context> tag, so the
|
|
// boundary must be the first *bare* tag line and the nested markup must survive verbatim.
|
|
const rendered = [
|
|
"ORIGINAL COVER",
|
|
"<tools>",
|
|
"- read: reads files",
|
|
"</tools>",
|
|
"<project_context>",
|
|
'<project_instructions path="/work/AGENTS.md">',
|
|
"nested instructions",
|
|
"</project_instructions>",
|
|
"</project_context>",
|
|
"<cwd>",
|
|
"/work",
|
|
"</cwd>",
|
|
].join("\n");
|
|
|
|
const spliced = spliceCover(rendered, "NEW");
|
|
assert.equal(tagsOf(spliced).join(","), "tools,project_context,cwd");
|
|
assert.ok(spliced.startsWith("NEW\n<tools>"));
|
|
assert.ok(
|
|
spliced.includes('<project_instructions path="/work/AGENTS.md">\nnested instructions\n</project_instructions>'),
|
|
"nested project instructions must be preserved byte-for-byte",
|
|
);
|
|
});
|
|
|
|
test("readCoverFile returns undefined when the file is absent or blank", async () => {
|
|
const dir = await makeTempDir();
|
|
try {
|
|
const coverPath = resolveCoverPath(dir);
|
|
assert.equal(coverPath, join(dir, "my-pi", "system-prompt-cover.md"));
|
|
assert.equal(readCoverFile(coverPath), undefined);
|
|
|
|
await mkdir(dirname(coverPath), { recursive: true });
|
|
await writeFile(coverPath, " \n\n", "utf8");
|
|
assert.equal(readCoverFile(coverPath), undefined);
|
|
|
|
await writeFile(coverPath, " REAL COVER \n", "utf8");
|
|
assert.equal(readCoverFile(coverPath), " REAL COVER");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|