Align pi sandbox context and bootstrap injection

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Harivansh Rathi 2026-03-08 17:41:57 -07:00
parent fb782fa025
commit 59ad12335a
6 changed files with 261 additions and 15 deletions

View file

@ -368,6 +368,47 @@ function collectAncestorAgentsSkillDirs(startDir: string): string[] {
return skillDirs;
}
function collectCompanionWorkspaceSkillDirs(
startDir: string,
agentDir: string,
): string[] {
const skillDirs: string[] = [];
const seen = new Set<string>();
const configDir = dirname(resolve(agentDir));
const defaultWorkspaceDir = join(configDir, "workspace");
const addDir = (dir: string): void => {
const skillDir = join(resolve(dir), ".agents", "skills");
if (seen.has(skillDir)) {
return;
}
skillDirs.push(skillDir);
seen.add(skillDir);
};
if (existsSync(defaultWorkspaceDir)) {
addDir(defaultWorkspaceDir);
}
let dir = resolve(startDir);
while (true) {
if (dirname(dir) === configDir) {
const dirName = basename(dir);
if (dirName === "workspace" || dirName.startsWith("workspace-")) {
addDir(dir);
}
}
const parent = dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
return skillDirs;
}
function collectAutoPromptEntries(dir: string): string[] {
const entries: string[] = [];
if (!existsSync(dir)) return entries;
@ -1897,6 +1938,10 @@ export class DefaultPackageManager implements PackageManager {
};
const userAgentsSkillsDir = join(homedir(), ".agents", "skills");
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);
const companionWorkspaceSkillDirs = collectCompanionWorkspaceSkillDirs(
this.cwd,
this.agentDir,
);
const addResources = (
resourceType: ResourceType,
@ -1923,6 +1968,9 @@ export class DefaultPackageManager implements PackageManager {
"skills",
[
...collectAutoSkillEntries(projectDirs.skills),
...companionWorkspaceSkillDirs.flatMap((dir) =>
collectAutoSkillEntries(dir),
),
...projectAgentsSkillDirs.flatMap((dir) =>
collectAutoSkillEntries(dir),
),

View file

@ -1,6 +1,6 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve, sep } from "node:path";
import { basename, dirname, join, resolve, sep } from "node:path";
import chalk from "chalk";
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
import {
@ -120,6 +120,78 @@ function loadNamedContextFileFromDir(
}
}
const companionContextFilenames = [
"IDENTITY.md",
"SOUL.md",
"USER.md",
"TOOLS.md",
"HEARTBEAT.md",
"MEMORY.md",
"BOOTSTRAP.md",
];
function loadNamedContextFilesFromDir(
dir: string,
filenames: string[],
): Array<{ path: string; content: string }> {
const files: Array<{ path: string; content: string }> = [];
for (const filename of filenames) {
const file = loadNamedContextFileFromDir(dir, filename);
if (file) {
files.push(file);
}
}
return files;
}
function addContextFile(
contextFiles: Array<{ path: string; content: string }>,
seenPaths: Set<string>,
file: { path: string; content: string } | null,
): void {
if (!file || seenPaths.has(file.path)) {
return;
}
contextFiles.push(file);
seenPaths.add(file.path);
}
function collectCompanionContextDirs(cwd: string, agentDir: string): string[] {
const contextDirs: string[] = [];
const seenDirs = new Set<string>();
const configDir = dirname(resolve(agentDir));
const defaultWorkspaceDir = join(configDir, "workspace");
const addDir = (dir: string): void => {
const resolvedDir = resolve(dir);
if (!existsSync(resolvedDir) || seenDirs.has(resolvedDir)) {
return;
}
contextDirs.push(resolvedDir);
seenDirs.add(resolvedDir);
};
addDir(defaultWorkspaceDir);
let currentDir = resolve(cwd);
while (true) {
if (dirname(currentDir) === configDir) {
const dirName = basename(currentDir);
if (dirName === "workspace" || dirName.startsWith("workspace-")) {
addDir(currentDir);
}
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
}
return contextDirs;
}
function loadProjectContextFiles(
options: { cwd?: string; agentDir?: string } = {},
): Array<{ path: string; content: string }> {
@ -129,11 +201,11 @@ function loadProjectContextFiles(
const contextFiles: Array<{ path: string; content: string }> = [];
const seenPaths = new Set<string>();
const globalContext = loadContextFileFromDir(resolvedAgentDir);
if (globalContext) {
contextFiles.push(globalContext);
seenPaths.add(globalContext.path);
}
addContextFile(
contextFiles,
seenPaths,
loadContextFileFromDir(resolvedAgentDir),
);
const ancestorContextFiles: Array<{ path: string; content: string }> = [];
@ -156,17 +228,29 @@ function loadProjectContextFiles(
contextFiles.push(...ancestorContextFiles);
const globalSoul = loadNamedContextFileFromDir(resolvedAgentDir, "SOUL.md");
if (globalSoul && !seenPaths.has(globalSoul.path)) {
contextFiles.push(globalSoul);
seenPaths.add(globalSoul.path);
addContextFile(
contextFiles,
seenPaths,
loadNamedContextFileFromDir(resolvedAgentDir, "SOUL.md"),
);
for (const companionDir of collectCompanionContextDirs(
resolvedCwd,
resolvedAgentDir,
)) {
for (const file of loadNamedContextFilesFromDir(
companionDir,
companionContextFilenames,
)) {
addContextFile(contextFiles, seenPaths, file);
}
}
const projectSoul = loadNamedContextFileFromDir(resolvedCwd, "SOUL.md");
if (projectSoul && !seenPaths.has(projectSoul.path)) {
contextFiles.push(projectSoul);
seenPaths.add(projectSoul.path);
}
addContextFile(
contextFiles,
seenPaths,
loadNamedContextFileFromDir(resolvedCwd, "SOUL.md"),
);
return contextFiles;
}

View file

@ -49,12 +49,42 @@ function buildProjectContextSection(
({ path }) =>
path.replaceAll("\\", "/").endsWith("/SOUL.md") || path === "SOUL.md",
);
const hasContextFile = (filename: string) =>
contextFiles.some(
({ path }) =>
path.replaceAll("\\", "/").endsWith(`/${filename}`) ||
path === filename,
);
let section = "\n\n# Project Context\n\n";
section += "Project-specific instructions and guidelines:\n";
if (hasSoulFile) {
section +=
"\nIf SOUL.md is present, embody its persona and tone. Avoid generic assistant filler and follow its guidance unless higher-priority instructions override it.\n";
}
if (hasContextFile("IDENTITY.md")) {
section +=
"\nIf IDENTITY.md is present, treat it as the agent's self-description and stay consistent with it.\n";
}
if (hasContextFile("USER.md")) {
section +=
"\nIf USER.md is present, use it as durable context about the user and avoid re-asking for facts already captured there.\n";
}
if (hasContextFile("MEMORY.md")) {
section +=
"\nIf MEMORY.md is present, use it as long-term memory and keep it aligned with durable user or project context when the task calls for it.\n";
}
if (hasContextFile("TOOLS.md")) {
section +=
"\nIf TOOLS.md is present, treat it as the source of truth for the current sandbox filesystem, app locations, and environment-specific workflow details.\n";
}
if (hasContextFile("HEARTBEAT.md")) {
section +=
"\nIf HEARTBEAT.md is present, honor it as recurring operational guidance for the agent.\n";
}
if (hasContextFile("BOOTSTRAP.md")) {
section +=
"\nIf BOOTSTRAP.md is present, treat it as an actionable onboarding task list and execute it before drifting into unrelated work.\n";
}
section += "\n";
for (const { path: filePath, content } of contextFiles) {
section += `## ${filePath}\n\n${content}\n\n`;

View file

@ -322,6 +322,38 @@ Content`,
result.skills.some((r) => r.path === middleSkill && r.enabled),
).toBe(true);
});
it("should include default workspace skills when cwd is outside the workspace root", async () => {
const workspaceSkill = join(
tempDir,
"workspace",
".agents",
"skills",
"build-app",
"SKILL.md",
);
mkdirSync(join(tempDir, "workspace", ".agents", "skills", "build-app"), {
recursive: true,
});
writeFileSync(
workspaceSkill,
"---\nname: build-app\ndescription: Build apps\n---\n",
);
const appCwd = join(tempDir, "apps", "portfolio");
mkdirSync(appCwd, { recursive: true });
const pm = new DefaultPackageManager({
cwd: appCwd,
agentDir,
settingsManager,
});
const result = await pm.resolve();
expect(
result.skills.some((r) => r.path === workspaceSkill && r.enabled),
).toBe(true);
});
});
describe("ignore files", () => {

View file

@ -320,6 +320,28 @@ Content`,
expect(agentsFiles.some((f) => f.path.endsWith("SOUL.md"))).toBe(true);
});
it("should discover companion context files from the default workspace", async () => {
const workspaceDir = join(tempDir, "workspace");
const appDir = join(tempDir, "apps", "todo-app");
mkdirSync(workspaceDir, { recursive: true });
mkdirSync(appDir, { recursive: true });
writeFileSync(join(workspaceDir, "IDENTITY.md"), "# Identity\n\nPi");
writeFileSync(join(workspaceDir, "TOOLS.md"), "# Tools\n\nUse ~/.pi");
writeFileSync(join(workspaceDir, "BOOTSTRAP.md"), "# Bootstrap\n\nDo it");
const loader = new DefaultResourceLoader({ cwd: appDir, agentDir });
await loader.reload();
const { agentsFiles } = loader.getAgentsFiles();
expect(agentsFiles.some((f) => f.path.endsWith("IDENTITY.md"))).toBe(
true,
);
expect(agentsFiles.some((f) => f.path.endsWith("TOOLS.md"))).toBe(true);
expect(agentsFiles.some((f) => f.path.endsWith("BOOTSTRAP.md"))).toBe(
true,
);
});
it("should discover SYSTEM.md from cwd/.pi", async () => {
const piDir = join(cwd, ".pi");
mkdirSync(piDir, { recursive: true });

View file

@ -100,5 +100,35 @@ describe("buildSystemPrompt", () => {
);
expect(prompt).toContain("## /tmp/project/SOUL.md");
});
test("adds companion context guidance for identity, tools, and bootstrap files", () => {
const prompt = buildSystemPrompt({
contextFiles: [
{
path: "/home/node/.pi/workspace/IDENTITY.md",
content: "# Identity\n\nPi",
},
{
path: "/home/node/.pi/workspace/TOOLS.md",
content: "# Tools\n\nUse ~/.pi/apps",
},
{
path: "/home/node/.pi/workspace/BOOTSTRAP.md",
content: "# Bootstrap\n\nDo the setup",
},
],
skills: [],
});
expect(prompt).toContain(
"If IDENTITY.md is present, treat it as the agent's self-description",
);
expect(prompt).toContain(
"If TOOLS.md is present, treat it as the source of truth for the current sandbox filesystem",
);
expect(prompt).toContain(
"If BOOTSTRAP.md is present, treat it as an actionable onboarding task list",
);
});
});
});