mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-17 07:03:28 +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;
|
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[] {
|
function collectAutoPromptEntries(dir: string): string[] {
|
||||||
const entries: string[] = [];
|
const entries: string[] = [];
|
||||||
if (!existsSync(dir)) return entries;
|
if (!existsSync(dir)) return entries;
|
||||||
|
|
@ -1897,6 +1938,10 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
};
|
};
|
||||||
const userAgentsSkillsDir = join(homedir(), ".agents", "skills");
|
const userAgentsSkillsDir = join(homedir(), ".agents", "skills");
|
||||||
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);
|
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);
|
||||||
|
const companionWorkspaceSkillDirs = collectCompanionWorkspaceSkillDirs(
|
||||||
|
this.cwd,
|
||||||
|
this.agentDir,
|
||||||
|
);
|
||||||
|
|
||||||
const addResources = (
|
const addResources = (
|
||||||
resourceType: ResourceType,
|
resourceType: ResourceType,
|
||||||
|
|
@ -1923,6 +1968,9 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
"skills",
|
"skills",
|
||||||
[
|
[
|
||||||
...collectAutoSkillEntries(projectDirs.skills),
|
...collectAutoSkillEntries(projectDirs.skills),
|
||||||
|
...companionWorkspaceSkillDirs.flatMap((dir) =>
|
||||||
|
collectAutoSkillEntries(dir),
|
||||||
|
),
|
||||||
...projectAgentsSkillDirs.flatMap((dir) =>
|
...projectAgentsSkillDirs.flatMap((dir) =>
|
||||||
collectAutoSkillEntries(dir),
|
collectAutoSkillEntries(dir),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
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 chalk from "chalk";
|
||||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||||
import {
|
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(
|
function loadProjectContextFiles(
|
||||||
options: { cwd?: string; agentDir?: string } = {},
|
options: { cwd?: string; agentDir?: string } = {},
|
||||||
): Array<{ path: string; content: string }> {
|
): Array<{ path: string; content: string }> {
|
||||||
|
|
@ -129,11 +201,11 @@ function loadProjectContextFiles(
|
||||||
const contextFiles: Array<{ path: string; content: string }> = [];
|
const contextFiles: Array<{ path: string; content: string }> = [];
|
||||||
const seenPaths = new Set<string>();
|
const seenPaths = new Set<string>();
|
||||||
|
|
||||||
const globalContext = loadContextFileFromDir(resolvedAgentDir);
|
addContextFile(
|
||||||
if (globalContext) {
|
contextFiles,
|
||||||
contextFiles.push(globalContext);
|
seenPaths,
|
||||||
seenPaths.add(globalContext.path);
|
loadContextFileFromDir(resolvedAgentDir),
|
||||||
}
|
);
|
||||||
|
|
||||||
const ancestorContextFiles: Array<{ path: string; content: string }> = [];
|
const ancestorContextFiles: Array<{ path: string; content: string }> = [];
|
||||||
|
|
||||||
|
|
@ -156,17 +228,29 @@ function loadProjectContextFiles(
|
||||||
|
|
||||||
contextFiles.push(...ancestorContextFiles);
|
contextFiles.push(...ancestorContextFiles);
|
||||||
|
|
||||||
const globalSoul = loadNamedContextFileFromDir(resolvedAgentDir, "SOUL.md");
|
addContextFile(
|
||||||
if (globalSoul && !seenPaths.has(globalSoul.path)) {
|
contextFiles,
|
||||||
contextFiles.push(globalSoul);
|
seenPaths,
|
||||||
seenPaths.add(globalSoul.path);
|
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");
|
addContextFile(
|
||||||
if (projectSoul && !seenPaths.has(projectSoul.path)) {
|
contextFiles,
|
||||||
contextFiles.push(projectSoul);
|
seenPaths,
|
||||||
seenPaths.add(projectSoul.path);
|
loadNamedContextFileFromDir(resolvedCwd, "SOUL.md"),
|
||||||
}
|
);
|
||||||
|
|
||||||
return contextFiles;
|
return contextFiles;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,42 @@ function buildProjectContextSection(
|
||||||
({ path }) =>
|
({ path }) =>
|
||||||
path.replaceAll("\\", "/").endsWith("/SOUL.md") || path === "SOUL.md",
|
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";
|
let section = "\n\n# Project Context\n\n";
|
||||||
section += "Project-specific instructions and guidelines:\n";
|
section += "Project-specific instructions and guidelines:\n";
|
||||||
if (hasSoulFile) {
|
if (hasSoulFile) {
|
||||||
section +=
|
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";
|
"\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";
|
section += "\n";
|
||||||
for (const { path: filePath, content } of contextFiles) {
|
for (const { path: filePath, content } of contextFiles) {
|
||||||
section += `## ${filePath}\n\n${content}\n\n`;
|
section += `## ${filePath}\n\n${content}\n\n`;
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,38 @@ Content`,
|
||||||
result.skills.some((r) => r.path === middleSkill && r.enabled),
|
result.skills.some((r) => r.path === middleSkill && r.enabled),
|
||||||
).toBe(true);
|
).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", () => {
|
describe("ignore files", () => {
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,28 @@ Content`,
|
||||||
expect(agentsFiles.some((f) => f.path.endsWith("SOUL.md"))).toBe(true);
|
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 () => {
|
it("should discover SYSTEM.md from cwd/.pi", async () => {
|
||||||
const piDir = join(cwd, ".pi");
|
const piDir = join(cwd, ".pi");
|
||||||
mkdirSync(piDir, { recursive: true });
|
mkdirSync(piDir, { recursive: true });
|
||||||
|
|
|
||||||
|
|
@ -100,5 +100,35 @@ describe("buildSystemPrompt", () => {
|
||||||
);
|
);
|
||||||
expect(prompt).toContain("## /tmp/project/SOUL.md");
|
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