mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 10:05:14 +00:00
Align pi sandbox context and bootstrap injection
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
fb782fa025
commit
59ad12335a
6 changed files with 261 additions and 15 deletions
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue