feat(pi-hermes-memory): 导入上游 v0.9.10 源码快照

从官方 Git 仓库导入 chandra447/pi-hermes-memory 在 tag v0.9.10
(df624056a2e8c68bc89ba0a81e1c120856ec9665,2026-10-02)的源码快照。
本次仅导入源码,尚未接入组合包:package.json、加载顺序、依赖与权限策略
均未改动。

- 原样导入 src/、tests/、docs/、scripts/、README、CHANGELOG、PLAN、
  AGENTS 与 LICENSE;该快照共 156 个上游文件。
- 剔除嵌套 .git、上游 .github 工作流、.ralph/、.claude/、node_modules、
  覆盖率与构建产物。
- 新增中文 UPSTREAM.md,记录来源,以及启用前必须完成的待决事项:
  在 pi-compaction 之前注册加载顺序、better-sqlite3 版本与根 allowScripts
  对齐、strip-ansi 解析确认。
- 说明选择 tag 而非 main 的原因:导入时上游 main 已领先至
  24fcd8ad26fc27badb8d7dbaf8034645de07b893(2026-10-11),选 tag 是为
  对应一个已发布、可复现的版本。

尚未接线。启用前还需反转 tests/extension-load.test.ts 中"不得加载
Hermes"的断言,并同步 pi-tool-search 分组、权限策略与文档。

验证:快照 156 个上游文件全部导入,目录内仅多出 UPSTREAM.md 一份;
已确认与上游 main 内容存在差异,证明取的是 tag 而非 main。
This commit is contained in:
云服务部-叶林立
2026-10-13 23:19:35 -07:00
parent 879fc5d37f
commit 312bb2ba5a
157 changed files with 52287 additions and 0 deletions
@@ -0,0 +1,678 @@
/**
* Unit tests for SkillStore — scoped CRUD, migration, and Pi-native file layout.
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import * as assert from "node:assert/strict";
import { describe, it, before, after, beforeEach, afterEach } from "node:test";
import { SkillStore } from "../../src/store/skill-store.js";
let ROOT_DIR = "";
let GLOBAL_SKILLS_DIR = "";
let PROJECT_SKILLS_DIR = "";
let LEGACY_SKILLS_DIR = "";
let PI_GLOBAL_SKILLS_DIR = "";
let MIGRATION_SENTINEL = "";
async function makeStore(withProject = true): Promise<SkillStore> {
return new SkillStore({
globalSkillsDir: GLOBAL_SKILLS_DIR,
piGlobalSkillsDir: PI_GLOBAL_SKILLS_DIR,
projectSkillsDir: withProject ? PROJECT_SKILLS_DIR : null,
projectName: withProject ? "demo-project" : null,
legacySkillsDir: LEGACY_SKILLS_DIR,
migrationSentinelPath: MIGRATION_SENTINEL,
});
}
async function cleanSlate(): Promise<void> {
try {
await fs.rm(ROOT_DIR, { recursive: true, force: true });
} catch {
// ignore
}
await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
await fs.mkdir(PROJECT_SKILLS_DIR, { recursive: true });
await fs.mkdir(LEGACY_SKILLS_DIR, { recursive: true });
await fs.mkdir(PI_GLOBAL_SKILLS_DIR, { recursive: true });
}
async function readFile(filePath: string): Promise<string> {
return fs.readFile(filePath, "utf-8");
}
describe("SkillStore", { concurrency: 1 }, () => {
before(async () => {
ROOT_DIR = await fs.mkdtemp(path.join(os.tmpdir(), "pi-skill-test-"));
GLOBAL_SKILLS_DIR = path.join(ROOT_DIR, "global-skills");
PROJECT_SKILLS_DIR = path.join(ROOT_DIR, "project-skills");
LEGACY_SKILLS_DIR = path.join(ROOT_DIR, "legacy-skills");
PI_GLOBAL_SKILLS_DIR = path.join(ROOT_DIR, "pi-global-skills");
MIGRATION_SENTINEL = path.join(ROOT_DIR, ".skill-migration");
});
after(async () => {
try {
await fs.rm(ROOT_DIR, { recursive: true, force: true });
} catch {
// ignore
}
});
beforeEach(async () => {
await cleanSlate();
});
afterEach(async () => {
await cleanSlate();
});
describe("create()", () => {
it("writes global skills to <slug>/SKILL.md", async () => {
const store = await makeStore();
const result = await store.create(
"Debug TypeScript Errors",
"Step-by-step approach to debugging TS errors",
"## Procedure\n1. Read the error\n2. Check types",
);
assert.ok(result.success, `create failed: ${result.error}`);
assert.strictEqual(result.skillId, "global:debug-typescript-errors");
const filePath = path.join(GLOBAL_SKILLS_DIR, "debug-typescript-errors", "SKILL.md");
const raw = await readFile(filePath);
assert.ok(raw.includes('name: "debug-typescript-errors"'));
assert.ok(raw.includes('display_name: "Debug TypeScript Errors"'));
assert.ok(raw.includes('description: "Step-by-step approach to debugging TS errors"'));
assert.ok(raw.includes("version: 1"));
assert.ok(raw.includes("## Procedure"));
});
it("writes explicit project skills to projects-memory/<project>/skills/<slug>/SKILL.md", async () => {
const store = await makeStore();
const result = await store.create(
"Release App",
"Release this repository",
"## Procedure\n1. Run pnpm build\n2. Run pnpm deploy",
"project",
);
assert.ok(result.success, `create failed: ${result.error}`);
assert.strictEqual(result.skillId, "project:demo-project:release-app");
const filePath = path.join(PROJECT_SKILLS_DIR, "release-app", "SKILL.md");
const raw = await readFile(filePath);
assert.ok(raw.includes('name: "release-app"'));
assert.ok(raw.includes('display_name: "Release App"'));
assert.ok(raw.includes("Run pnpm deploy"));
});
it("classifies transferable procedures as global by default", async () => {
const store = await makeStore();
const result = await store.create(
"debug-fetch-errors",
"Reusable workflow for tracing fetch failures",
"## Procedure\n1. Reproduce the request\n2. Check the network trace\n3. Compare status codes",
);
assert.ok(result.success, `create failed: ${result.error}`);
assert.strictEqual(result.scope, "global");
assert.strictEqual(result.skillId, "global:debug-fetch-errors");
});
it("classifies repo-specific procedures as project by default", async () => {
const store = await makeStore();
const result = await store.create(
"release-demo-project",
"How to release this repo",
"## Procedure\n1. In this repo, run pnpm build\n2. Check package.json scripts\n3. Deploy from src/server",
);
assert.ok(result.success, `create failed: ${result.error}`);
assert.strictEqual(result.scope, "project");
assert.strictEqual(result.skillId, "project:demo-project:release-demo-project");
});
it("returns error for empty name", async () => {
const store = await makeStore();
const result = await store.create("", "desc", "body");
assert.ok(!result.success);
assert.ok(result.error!.includes("name is required"));
});
it("returns error for duplicate slug in same scope", async () => {
const store = await makeStore();
await store.create("my-skill", "desc", "body");
const result = await store.create("my skill", "new desc", "new body");
assert.ok(!result.success);
assert.ok(result.error!.includes("already exists"));
assert.strictEqual(result.conflictType, "duplicate");
assert.deepStrictEqual(result.similarSkillIds, ["global:my-skill"]);
assert.strictEqual(result.suggestedAction, "patch");
});
it("blocks creating a similar global skill and suggests patching", async () => {
const store = await makeStore();
await store.create(
"debug-typescript-errors",
"Step-by-step workflow for debugging TypeScript compiler errors, type mismatches, and incorrect type assumptions",
"## Procedure\n1. Reproduce\n2. Inspect inferred types",
);
const result = await store.create(
"debug-typescript-errors-fast",
"Step-by-step workflow for debugging TypeScript compiler errors, type mismatches, and incorrect type assumptions",
"## Procedure\n1. Reproduce\n2. Narrow the failing type",
);
assert.ok(!result.success);
assert.strictEqual(result.conflictType, "similar");
assert.strictEqual(result.suggestedAction, "patch");
assert.ok(result.similarSkillIds?.includes("global:debug-typescript-errors"));
assert.ok(result.error?.includes("similar global skill already exists"));
const index = await store.loadIndex("global");
assert.strictEqual(index.length, 1);
});
it("blocks near-name global collisions even when descriptions diverge", async () => {
const store = await makeStore();
await store.create(
"debug-typescript-errors",
"Step-by-step workflow for debugging TypeScript compiler errors",
"## Procedure\n1. Reproduce\n2. Inspect types",
);
const result = await store.create(
"debug-typescript-errors-runtime",
"Incident-response runbook for on-call paging, service alerts, and runtime triage escalation",
"## Procedure\n1. Acknowledge page\n2. Escalate and contain",
);
assert.ok(!result.success);
assert.strictEqual(result.conflictType, "name-collision");
assert.strictEqual(result.suggestedAction, "rename");
assert.ok(result.similarSkillIds?.includes("global:debug-typescript-errors"));
assert.ok(result.error?.includes("near-name global skill already exists"));
const index = await store.loadIndex("global");
assert.strictEqual(index.length, 1);
});
it("allows creating distinct global skills", async () => {
const store = await makeStore();
await store.create(
"debug-typescript-errors",
"Step-by-step workflow for debugging TypeScript compiler errors",
"## Procedure\n1. Reproduce\n2. Inspect types",
);
const result = await store.create(
"optimize-postgres-indexes",
"How to profile slow PostgreSQL queries and add safe indexes",
"## Procedure\n1. Capture EXPLAIN ANALYZE\n2. Evaluate index options",
);
assert.ok(result.success, `create failed unexpectedly: ${result.error}`);
assert.strictEqual(result.skillId, "global:optimize-postgres-indexes");
const index = await store.loadIndex("global");
assert.strictEqual(index.length, 2);
});
it("does not allow project scope without an active project", async () => {
const store = await makeStore(false);
const result = await store.create("repo-only", "desc", "body", "project");
assert.ok(!result.success);
assert.ok(result.error!.includes("active project"));
});
});
describe("loadIndex()", () => {
it("returns both global and project skills", async () => {
const store = await makeStore();
await store.create("skill-a", "First skill", "body a");
await store.create("skill-b", "Second skill", "body b", "project");
const index = await store.loadIndex();
assert.strictEqual(index.length, 2);
assert.ok(index.some((skill) => skill.skillId === "global:skill-a"));
assert.ok(index.some((skill) => skill.skillId === "project:demo-project:skill-b"));
});
it("includes existing user-managed global Pi skills", async () => {
const store = await makeStore();
const customDir = path.join(GLOBAL_SKILLS_DIR, "manual-skill");
await fs.mkdir(customDir, { recursive: true });
await fs.writeFile(path.join(customDir, "SKILL.md"), [
"---",
"name: manual-skill",
"description: A manually created Pi skill",
"---",
"# Manual Skill",
].join("\n"), "utf-8");
const index = await store.loadIndex();
assert.strictEqual(index.length, 1);
assert.strictEqual(index[0].skillId, "global:manual-skill");
assert.strictEqual(index[0].scope, "global");
});
it("returns empty array when no skills exist", async () => {
const store = await makeStore();
const index = await store.loadIndex();
assert.deepStrictEqual(index, []);
});
it("sorts skills by updated date descending, then created date descending", async () => {
const store = await makeStore();
const olderDir = path.join(GLOBAL_SKILLS_DIR, "older-skill");
const newerDir = path.join(GLOBAL_SKILLS_DIR, "newer-skill");
await fs.mkdir(olderDir, { recursive: true });
await fs.mkdir(newerDir, { recursive: true });
await fs.writeFile(path.join(olderDir, "SKILL.md"), [
"---",
'name: "older-skill"',
'description: "Older skill"',
"version: 2",
'created: "2026-05-18"',
'updated: "2026-05-20"',
"---",
"## Procedure",
"1. Old",
].join("\n"), "utf-8");
await fs.writeFile(path.join(newerDir, "SKILL.md"), [
"---",
'name: "newer-skill"',
'description: "Newer skill"',
"version: 1",
'created: "2026-05-19"',
'updated: "2026-05-21"',
"---",
"## Procedure",
"1. New",
].join("\n"), "utf-8");
const index = await store.loadIndex("global");
assert.strictEqual(index[0]?.skillId, "global:newer-skill");
assert.strictEqual(index[1]?.skillId, "global:older-skill");
assert.ok(index[0]!.updated >= index[1]!.updated);
});
});
describe("loadSkill()", () => {
it("returns full document with scope and skill id", async () => {
const store = await makeStore();
const created = await store.create("My Skill", "A test skill", "## Procedure\n1. Do it");
const doc = await store.loadSkill(created.skillId!);
assert.ok(doc);
assert.strictEqual(doc!.skillId, "global:my-skill");
assert.strictEqual(doc!.scope, "global");
assert.strictEqual(doc!.name, "my-skill");
assert.strictEqual(doc!.displayName, "My Skill");
assert.strictEqual(doc!.description, "A test skill");
assert.strictEqual(doc!.version, 1);
assert.ok(doc!.body.includes("## Procedure"));
});
it("returns null for missing skill id", async () => {
const store = await makeStore();
const doc = await store.loadSkill("global:missing");
assert.strictEqual(doc, null);
});
});
describe("patch()", () => {
it("replaces an existing section by skill id", async () => {
const store = await makeStore();
const created = await store.create("test", "desc", "## Procedure\n1. Old way\n\n## Pitfalls\nWatch out");
const result = await store.patch(created.skillId!, "Procedure", "1. New way\n2. Better way");
assert.ok(result.success, `patch failed: ${result.error}`);
const doc = await store.loadSkill(created.skillId!);
assert.ok(doc!.body.includes("1. New way"));
assert.ok(!doc!.body.includes("1. Old way"));
assert.ok(doc!.body.includes("## Pitfalls"));
});
it("appends a missing section", async () => {
const store = await makeStore();
const created = await store.create("test", "desc", "## Procedure\n1. Do it");
const result = await store.patch(created.skillId!, "Verification", "Run the tests");
assert.ok(result.success, `patch failed: ${result.error}`);
const doc = await store.loadSkill(created.skillId!);
assert.ok(doc!.body.includes("## Verification"));
assert.ok(doc!.body.includes("Run the tests"));
assert.strictEqual(doc!.version, 2);
});
it("coerces JSON string arrays into ordered Procedure steps", async () => {
const store = await makeStore();
const created = await store.create("test", "desc", "## Procedure\n1. Old\n\n## Pitfalls\n- Keep me");
const result = await store.patch(
created.skillId!,
"Procedure",
JSON.stringify(["First step", "Second step"]),
);
assert.ok(result.success, `patch failed: ${result.error}`);
const doc = await store.loadSkill(created.skillId!);
assert.match(doc!.body, /## Procedure\n1\. First step\n2\. Second step/);
assert.match(doc!.body, /## Pitfalls\n- Keep me/);
assert.doesNotMatch(doc!.body, /\[\"First step\"/);
});
it("rejects JSON object patch payloads", async () => {
const store = await makeStore();
const created = await store.create("test", "desc", "## Procedure\n1. Keep");
const result = await store.patch(created.skillId!, "Procedure", '{"steps":["a"]}');
assert.equal(result.success, false);
assert.match(result.error ?? "", /JSON object/i);
const doc = await store.loadSkill(created.skillId!);
assert.match(doc!.body, /1\. Keep/);
assert.equal(doc!.version, 1);
});
it("rejects empty patch content", async () => {
const store = await makeStore();
const created = await store.create("test", "desc", "## Procedure\n1. Keep");
const result = await store.patch(created.skillId!, "Procedure", " ");
assert.equal(result.success, false);
assert.match(result.error ?? "", /required for patch/i);
assert.equal((await store.loadSkill(created.skillId!))!.version, 1);
});
it("rejects patch content that injects section headers", async () => {
const store = await makeStore();
const created = await store.create("test", "desc", "## Procedure\n1. Keep\n\n## Pitfalls\n- Keep");
const result = await store.patch(
created.skillId!,
"Procedure",
"1. New\n\n## Pitfalls\n- Injected duplicate",
);
assert.equal(result.success, false);
assert.match(result.error ?? "", /section headers/i);
const doc = await store.loadSkill(created.skillId!);
assert.equal(doc!.version, 1);
assert.equal(doc!.body.match(/## Pitfalls/g)?.length, 1);
});
});
describe("edit()", () => {
it("replaces description and body", async () => {
const store = await makeStore();
const created = await store.create("test", "old desc", "## Old Body");
const result = await store.edit(created.skillId!, "new desc", "## New Body");
assert.ok(result.success, `edit failed: ${result.error}`);
const doc = await store.loadSkill(created.skillId!);
assert.strictEqual(doc!.description, "new desc");
assert.ok(doc!.body.includes("## New Body"));
assert.ok(!doc!.body.includes("## Old Body"));
assert.strictEqual(doc!.version, 2);
});
});
describe("move()", () => {
it("moves a global skill into the active project scope", async () => {
const store = await makeStore();
const created = await store.create("move-me", "Reusable process", "## Procedure\n1. Do it", "global");
const result = await store.move(created.skillId!, "project");
assert.ok(result.success, `move failed: ${result.error}`);
assert.strictEqual(result.skillId, "project:demo-project:move-me");
await assert.rejects(fs.access(path.join(GLOBAL_SKILLS_DIR, "move-me", "SKILL.md")));
await fs.access(path.join(PROJECT_SKILLS_DIR, "move-me", "SKILL.md"));
});
it("moves a project skill into global scope", async () => {
const store = await makeStore();
const created = await store.create("repo-runbook", "Project process", "## Procedure\n1. Run it", "project");
const result = await store.move(created.skillId!, "global");
assert.ok(result.success, `move failed: ${result.error}`);
assert.strictEqual(result.skillId, "global:repo-runbook");
await assert.rejects(fs.access(path.join(PROJECT_SKILLS_DIR, "repo-runbook", "SKILL.md")));
await fs.access(path.join(GLOBAL_SKILLS_DIR, "repo-runbook", "SKILL.md"));
});
it("blocks move when destination scope already has the same slug", async () => {
const store = await makeStore();
const globalSkill = await store.create("same-name", "global skill", "body", "global");
const projectSkill = await store.create("same-name", "project skill", "body", "project");
const result = await store.move(globalSkill.skillId!, "project");
assert.ok(!result.success);
assert.strictEqual(result.conflictType, "scope-conflict");
assert.ok(result.error?.includes("already exists"));
const globalDoc = await store.loadSkill(globalSkill.skillId!);
const projectDoc = await store.loadSkill(projectSkill.skillId!);
assert.ok(globalDoc);
assert.ok(projectDoc);
});
it("returns an error when moving to project scope without an active project", async () => {
const store = await makeStore(false);
const created = await store.create("global-skill", "desc", "body", "global");
const result = await store.move(created.skillId!, "project");
assert.ok(!result.success);
assert.ok(result.error?.includes("active project"));
});
});
describe("delete()", () => {
it("removes the skill file from disk", async () => {
const store = await makeStore();
const created = await store.create("to-delete", "desc", "body");
const result = await store.delete(created.skillId!);
assert.ok(result.success, `delete failed: ${result.error}`);
const index = await store.loadIndex();
assert.strictEqual(index.length, 0);
});
it("keeps duplicate slugs across scopes safe because ids differ", async () => {
const store = await makeStore();
const globalSkill = await store.create("same-name", "global", "body");
const projectSkill = await store.create("same-name", "project", "body", "project");
assert.ok(globalSkill.success);
assert.ok(projectSkill.success);
assert.notStrictEqual(globalSkill.skillId, projectSkill.skillId);
const index = await store.loadIndex();
assert.strictEqual(index.length, 2);
});
});
describe("migration", () => {
it("migrates legacy memory/skills/*.md files into global Pi skills", async () => {
const legacyFile = path.join(LEGACY_SKILLS_DIR, "legacy-skill.md");
await fs.writeFile(legacyFile, [
"---",
"name: Legacy Skill",
"description: Legacy migrated skill",
"version: 2",
"created: 2026-01-01",
"updated: 2026-01-02",
"---",
"## Procedure",
"1. Do the legacy thing",
].join("\n"), "utf-8");
const store = await makeStore();
const result = await store.migrateLegacySkills();
assert.strictEqual(result.migrated, 1);
const migratedPath = path.join(GLOBAL_SKILLS_DIR, "legacy-skill", "SKILL.md");
const raw = await readFile(migratedPath);
assert.ok(raw.includes('name: "legacy-skill"'));
assert.ok(raw.includes('display_name: "Legacy Skill"'));
assert.ok(raw.includes('description: "Legacy migrated skill"'));
assert.ok(raw.includes("1. Do the legacy thing"));
});
it("does not rerun after the sentinel is created", async () => {
const legacyFile = path.join(LEGACY_SKILLS_DIR, "legacy-skill.md");
await fs.writeFile(legacyFile, [
"---",
"name: legacy-skill",
"description: Legacy migrated skill",
"---",
"body",
].join("\n"), "utf-8");
const store = await makeStore();
const first = await store.migrateLegacySkills();
const second = await store.migrateLegacySkills();
assert.strictEqual(first.migrated, 1);
assert.strictEqual(second.migrated, 0);
});
it("does not overwrite an existing global skill unexpectedly", async () => {
const existingDir = path.join(GLOBAL_SKILLS_DIR, "legacy-skill");
await fs.mkdir(existingDir, { recursive: true });
await fs.writeFile(path.join(existingDir, "SKILL.md"), [
"---",
"name: legacy-skill",
"description: Existing global skill",
"---",
"# Existing",
].join("\n"), "utf-8");
await fs.writeFile(path.join(LEGACY_SKILLS_DIR, "legacy-skill.md"), [
"---",
"name: legacy-skill",
"description: Legacy version",
"---",
"# Legacy",
].join("\n"), "utf-8");
const store = await makeStore();
const result = await store.migrateLegacySkills();
assert.strictEqual(result.migrated, 0);
assert.strictEqual(result.skipped, 1);
const raw = await readFile(path.join(existingDir, "SKILL.md"));
assert.ok(raw.includes("Existing global skill"));
assert.ok(!raw.includes("Legacy version"));
});
it("migrates flat markdown files under global skills root into SKILL.md folders", async () => {
await fs.writeFile(path.join(GLOBAL_SKILLS_DIR, "flat-legacy.md"), [
"---",
"name: flat-legacy",
"description: Flat legacy skill",
"---",
"# Flat Body",
].join("\n"), "utf-8");
const store = await makeStore();
const result = await store.migrateLegacySkills();
assert.strictEqual(result.migrated, 1);
await assert.rejects(fs.access(path.join(GLOBAL_SKILLS_DIR, "flat-legacy.md")));
const migrated = await readFile(path.join(GLOBAL_SKILLS_DIR, "flat-legacy", "SKILL.md"));
assert.ok(migrated.includes('description: "Flat legacy skill"'));
});
it("does not write the sentinel when warnings occur, so migration can retry", async () => {
await fs.mkdir(path.join(LEGACY_SKILLS_DIR, "broken.md"), { recursive: true });
const legacyFile = path.join(LEGACY_SKILLS_DIR, "legacy-skill.md");
await fs.writeFile(legacyFile, [
"---",
"name: legacy-skill",
"description: Legacy migrated skill",
"---",
"body",
].join("\n"), "utf-8");
const store = await makeStore();
const first = await store.migrateLegacySkills();
assert.ok(first.warnings.length >= 1);
await assert.rejects(fs.access(MIGRATION_SENTINEL));
await fs.rm(path.join(LEGACY_SKILLS_DIR, "broken.md"), { recursive: true, force: true });
const second = await store.migrateLegacySkills();
assert.strictEqual(second.migrated, 0);
await fs.access(MIGRATION_SENTINEL);
});
});
describe("shadowing guard against Pi's own global skills root", () => {
it("writes global skills to the extension directory, never Pi's root", async () => {
const result = await (await makeStore()).create("deploy-service", "Deploy the service", "# Deploy", "global");
assert.strictEqual(result.success, true);
assert.ok((await readFile(path.join(GLOBAL_SKILLS_DIR, "deploy-service", "SKILL.md"))).includes("# Deploy"));
// Skills the user installed themselves stay untouched by ours (#126).
await assert.rejects(fs.access(path.join(PI_GLOBAL_SKILLS_DIR, "deploy-service")));
});
it("refuses a global name Pi already loads instead of writing a shadowed copy", async () => {
const theirs = path.join(PI_GLOBAL_SKILLS_DIR, "swarm-decompose");
await fs.mkdir(theirs, { recursive: true });
await fs.writeFile(path.join(theirs, "SKILL.md"), "---\nname: \"swarm-decompose\"\n---\n# Theirs", "utf-8");
const result = await (await makeStore()).create("swarm-decompose", "Decompose a swarm", "# Ours", "global");
// Pi loads its own root first, so writing here would succeed on disk and
// change nothing about the agent's behaviour — the #125 write-loss bug.
assert.strictEqual(result.success, false);
assert.strictEqual(result.conflictType, "name-collision");
assert.ok(result.error?.includes(path.join(theirs, "SKILL.md")));
await assert.rejects(fs.access(path.join(GLOBAL_SKILLS_DIR, "swarm-decompose")));
assert.ok((await readFile(path.join(theirs, "SKILL.md"))).includes("# Theirs"));
});
it("still allows a project skill to reuse a name taken in Pi's global root", async () => {
const theirs = path.join(PI_GLOBAL_SKILLS_DIR, "run-tests");
await fs.mkdir(theirs, { recursive: true });
await fs.writeFile(path.join(theirs, "SKILL.md"), "---\nname: \"run-tests\"\n---\n# Theirs", "utf-8");
const result = await (await makeStore()).create("run-tests", "Run this repo's tests", "# Ours", "project");
// Project skills are a different scope; only the global root collides.
assert.strictEqual(result.success, true);
assert.ok((await readFile(path.join(PROJECT_SKILLS_DIR, "run-tests", "SKILL.md"))).includes("# Ours"));
});
});
describe("dynamic project context", () => {
it("can retarget project skill creation to a new project directory", async () => {
const store = await makeStore();
const altProjectDir = path.join(ROOT_DIR, "another-project-skills");
store.setProjectContext("another-project", altProjectDir);
const result = await store.create(
"Deploy Another Project",
"Project-specific deploy flow",
"## Procedure\n1. Run npm run deploy",
"project",
);
assert.ok(result.success, `create failed: ${result.error}`);
assert.strictEqual(result.skillId, "project:another-project:deploy-another-project");
await fs.access(path.join(altProjectDir, "deploy-another-project", "SKILL.md"));
});
});
});