Merge 59347115db into 4583f88626
This commit is contained in:
commit
9ddddfc83e
@ -118,6 +118,44 @@ adjust the limit with `agents.defaults.bootstrapMaxChars` (default: 20000).
|
||||
`moltbot setup` can recreate missing defaults without overwriting existing
|
||||
files.
|
||||
|
||||
## Extra workspace files
|
||||
|
||||
You can inject additional workspace files into the system prompt alongside the
|
||||
defaults using `extraWorkspaceFiles`:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
extraWorkspaceFiles: ["PANTHEON.md", "protocols/SHARED.md"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Paths are relative to the workspace directory. Files that do not exist are
|
||||
silently skipped (no "missing file" marker). Duplicates of default files are
|
||||
ignored.
|
||||
|
||||
Per-agent overrides are supported:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
extraWorkspaceFiles: ["PANTHEON.md"]
|
||||
},
|
||||
list: [
|
||||
{ id: "researcher", extraWorkspaceFiles: ["RESEARCH_PROTOCOL.md"] },
|
||||
{ id: "minimal", extraWorkspaceFiles: [] } // disable extras
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Extra files are included for subagent sessions alongside the default allowlist
|
||||
(AGENTS.md, TOOLS.md).
|
||||
|
||||
## What is NOT in the workspace
|
||||
|
||||
These live under `~/.clawdbot/` and should NOT be committed to the workspace repo:
|
||||
|
||||
@ -20,6 +20,7 @@ type ResolvedAgentConfig = {
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: AgentEntry["model"];
|
||||
extraWorkspaceFiles?: string[];
|
||||
memorySearch?: AgentEntry["memorySearch"];
|
||||
humanDelay?: AgentEntry["humanDelay"];
|
||||
heartbeat?: AgentEntry["heartbeat"];
|
||||
@ -103,6 +104,9 @@ export function resolveAgentConfig(
|
||||
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
||||
? entry.model
|
||||
: undefined,
|
||||
extraWorkspaceFiles: Array.isArray(entry.extraWorkspaceFiles)
|
||||
? entry.extraWorkspaceFiles
|
||||
: undefined,
|
||||
memorySearch: entry.memorySearch,
|
||||
humanDelay: entry.humanDelay,
|
||||
heartbeat: entry.heartbeat,
|
||||
|
||||
@ -3,12 +3,13 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveBootstrapContextForRun, resolveBootstrapFilesForRun } from "./bootstrap-files.js";
|
||||
import { makeTempWorkspace } from "../test-helpers/workspace.js";
|
||||
import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js";
|
||||
import {
|
||||
clearInternalHooks,
|
||||
registerInternalHook,
|
||||
type AgentBootstrapHookContext,
|
||||
} from "../hooks/internal-hooks.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
|
||||
describe("resolveBootstrapFilesForRun", () => {
|
||||
beforeEach(() => clearInternalHooks());
|
||||
@ -60,3 +61,72 @@ describe("resolveBootstrapContextForRun", () => {
|
||||
expect(extra?.content).toBe("extra");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extraWorkspaceFiles per-agent config", () => {
|
||||
beforeEach(() => clearInternalHooks());
|
||||
afterEach(() => clearInternalHooks());
|
||||
|
||||
it("uses per-agent extraWorkspaceFiles over defaults", async () => {
|
||||
const workspaceDir = await makeTempWorkspace("clawdbot-bootstrap-");
|
||||
await writeWorkspaceFile({ dir: workspaceDir, name: "AGENT_SPECIFIC.md", content: "agent" });
|
||||
await writeWorkspaceFile({ dir: workspaceDir, name: "DEFAULT.md", content: "default" });
|
||||
|
||||
const config: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: { extraWorkspaceFiles: ["DEFAULT.md"] },
|
||||
list: [{ id: "test-agent", extraWorkspaceFiles: ["AGENT_SPECIFIC.md"] }],
|
||||
},
|
||||
};
|
||||
|
||||
const files = await resolveBootstrapFilesForRun({
|
||||
workspaceDir,
|
||||
config,
|
||||
agentId: "test-agent",
|
||||
});
|
||||
|
||||
// Per-agent config should be used, not defaults
|
||||
expect(files.some((f) => f.name === "AGENT_SPECIFIC.md")).toBe(true);
|
||||
expect(files.some((f) => f.name === "DEFAULT.md")).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to defaults when agent has no extraWorkspaceFiles", async () => {
|
||||
const workspaceDir = await makeTempWorkspace("clawdbot-bootstrap-");
|
||||
await writeWorkspaceFile({ dir: workspaceDir, name: "DEFAULT.md", content: "default" });
|
||||
|
||||
const config: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: { extraWorkspaceFiles: ["DEFAULT.md"] },
|
||||
list: [{ id: "other-agent" }],
|
||||
},
|
||||
};
|
||||
|
||||
const files = await resolveBootstrapFilesForRun({
|
||||
workspaceDir,
|
||||
config,
|
||||
agentId: "other-agent",
|
||||
});
|
||||
|
||||
expect(files.some((f) => f.name === "DEFAULT.md")).toBe(true);
|
||||
});
|
||||
|
||||
it("allows per-agent to disable extras with empty array", async () => {
|
||||
const workspaceDir = await makeTempWorkspace("clawdbot-bootstrap-");
|
||||
await writeWorkspaceFile({ dir: workspaceDir, name: "DEFAULT.md", content: "default" });
|
||||
|
||||
const config: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: { extraWorkspaceFiles: ["DEFAULT.md"] },
|
||||
list: [{ id: "no-extras", extraWorkspaceFiles: [] }],
|
||||
},
|
||||
};
|
||||
|
||||
const files = await resolveBootstrapFilesForRun({
|
||||
workspaceDir,
|
||||
config,
|
||||
agentId: "no-extras",
|
||||
});
|
||||
|
||||
// Empty array should override defaults - no extra files
|
||||
expect(files.some((f) => f.name === "DEFAULT.md")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { MoltbotConfig } from "../config/config.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js";
|
||||
import {
|
||||
filterBootstrapFilesForSession,
|
||||
@ -24,8 +25,13 @@ export async function resolveBootstrapFilesForRun(params: {
|
||||
agentId?: string;
|
||||
}): Promise<WorkspaceBootstrapFile[]> {
|
||||
const sessionKey = params.sessionKey ?? params.sessionId;
|
||||
// Per-agent extraWorkspaceFiles takes precedence over global defaults
|
||||
const agentConfig =
|
||||
params.agentId && params.config ? resolveAgentConfig(params.config, params.agentId) : undefined;
|
||||
const extraFiles =
|
||||
agentConfig?.extraWorkspaceFiles ?? params.config?.agents?.defaults?.extraWorkspaceFiles;
|
||||
const bootstrapFiles = filterBootstrapFilesForSession(
|
||||
await loadWorkspaceBootstrapFiles(params.workspaceDir),
|
||||
await loadWorkspaceBootstrapFiles(params.workspaceDir, { extraFiles }),
|
||||
sessionKey,
|
||||
);
|
||||
return applyBootstrapHookOverrides({
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
DEFAULT_MEMORY_ALT_FILENAME,
|
||||
DEFAULT_MEMORY_FILENAME,
|
||||
DEFAULT_TOOLS_FILENAME,
|
||||
filterBootstrapFilesForSession,
|
||||
loadWorkspaceBootstrapFiles,
|
||||
} from "./workspace.js";
|
||||
import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js";
|
||||
@ -46,4 +49,72 @@ describe("loadWorkspaceBootstrapFiles", () => {
|
||||
|
||||
expect(memoryEntries).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("includes extraWorkspaceFiles when present", async () => {
|
||||
const tempDir = await makeTempWorkspace("clawdbot-workspace-");
|
||||
await writeWorkspaceFile({ dir: tempDir, name: "PANTHEON.md", content: "shared protocols" });
|
||||
|
||||
const files = await loadWorkspaceBootstrapFiles(tempDir, {
|
||||
extraFiles: ["PANTHEON.md"],
|
||||
});
|
||||
|
||||
const extra = files.find((f) => f.name === "PANTHEON.md");
|
||||
expect(extra).toBeDefined();
|
||||
expect(extra?.missing).toBe(false);
|
||||
expect(extra?.content).toBe("shared protocols");
|
||||
expect(extra?.isExtra).toBe(true);
|
||||
});
|
||||
|
||||
it("skips extraWorkspaceFiles when file does not exist", async () => {
|
||||
const tempDir = await makeTempWorkspace("clawdbot-workspace-");
|
||||
|
||||
const files = await loadWorkspaceBootstrapFiles(tempDir, {
|
||||
extraFiles: ["MISSING.md"],
|
||||
});
|
||||
|
||||
// Extra files that don't exist are silently skipped (not marked missing)
|
||||
const extra = files.find((f) => f.name === "MISSING.md");
|
||||
expect(extra).toBeUndefined();
|
||||
});
|
||||
|
||||
it("dedupes extraWorkspaceFiles against defaults", async () => {
|
||||
const tempDir = await makeTempWorkspace("clawdbot-workspace-");
|
||||
await writeWorkspaceFile({ dir: tempDir, name: "AGENTS.md", content: "default" });
|
||||
|
||||
const files = await loadWorkspaceBootstrapFiles(tempDir, {
|
||||
extraFiles: ["AGENTS.md"], // duplicate of default
|
||||
});
|
||||
|
||||
// Should only have one AGENTS.md entry (the default)
|
||||
const agentsEntries = files.filter((f) => f.name === DEFAULT_AGENTS_FILENAME);
|
||||
expect(agentsEntries).toHaveLength(1);
|
||||
expect(agentsEntries[0]?.isExtra).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterBootstrapFilesForSession", () => {
|
||||
it("returns all files for main session", () => {
|
||||
const files = [
|
||||
{ name: DEFAULT_AGENTS_FILENAME, path: "/a/AGENTS.md", missing: false },
|
||||
{ name: DEFAULT_TOOLS_FILENAME, path: "/a/TOOLS.md", missing: false },
|
||||
{ name: "IDENTITY.md", path: "/a/IDENTITY.md", missing: false },
|
||||
{ name: "PANTHEON.md", path: "/a/PANTHEON.md", missing: false, isExtra: true },
|
||||
];
|
||||
|
||||
const filtered = filterBootstrapFilesForSession(files, "agent:main:main");
|
||||
expect(filtered).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("filters to allowlist + extras for subagent session", () => {
|
||||
const files = [
|
||||
{ name: DEFAULT_AGENTS_FILENAME, path: "/a/AGENTS.md", missing: false },
|
||||
{ name: DEFAULT_TOOLS_FILENAME, path: "/a/TOOLS.md", missing: false },
|
||||
{ name: "IDENTITY.md", path: "/a/IDENTITY.md", missing: false },
|
||||
{ name: "PANTHEON.md", path: "/a/PANTHEON.md", missing: false, isExtra: true },
|
||||
];
|
||||
|
||||
const filtered = filterBootstrapFilesForSession(files, "agent:main:subagent:abc123");
|
||||
expect(filtered).toHaveLength(3); // AGENTS.md, TOOLS.md, PANTHEON.md (extra)
|
||||
expect(filtered.map((f) => f.name).sort()).toEqual(["AGENTS.md", "PANTHEON.md", "TOOLS.md"]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -68,10 +68,13 @@ export type WorkspaceBootstrapFileName =
|
||||
| typeof DEFAULT_MEMORY_ALT_FILENAME;
|
||||
|
||||
export type WorkspaceBootstrapFile = {
|
||||
name: WorkspaceBootstrapFileName;
|
||||
/** Filename (e.g., "AGENTS.md") or custom name for extra files */
|
||||
name: WorkspaceBootstrapFileName | string;
|
||||
path: string;
|
||||
content?: string;
|
||||
missing: boolean;
|
||||
/** True for user-configured extraWorkspaceFiles */
|
||||
isExtra?: boolean;
|
||||
};
|
||||
|
||||
async function writeFileIfMissing(filePath: string, content: string) {
|
||||
@ -221,12 +224,16 @@ async function resolveMemoryBootstrapEntries(
|
||||
return deduped;
|
||||
}
|
||||
|
||||
export async function loadWorkspaceBootstrapFiles(dir: string): Promise<WorkspaceBootstrapFile[]> {
|
||||
export async function loadWorkspaceBootstrapFiles(
|
||||
dir: string,
|
||||
options?: { extraFiles?: string[] },
|
||||
): Promise<WorkspaceBootstrapFile[]> {
|
||||
const resolvedDir = resolveUserPath(dir);
|
||||
|
||||
const entries: Array<{
|
||||
name: WorkspaceBootstrapFileName;
|
||||
name: WorkspaceBootstrapFileName | string;
|
||||
filePath: string;
|
||||
isExtra?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: DEFAULT_AGENTS_FILENAME,
|
||||
@ -260,6 +267,23 @@ export async function loadWorkspaceBootstrapFiles(dir: string): Promise<Workspac
|
||||
|
||||
entries.push(...(await resolveMemoryBootstrapEntries(resolvedDir)));
|
||||
|
||||
// Add extra workspace files from config
|
||||
if (options?.extraFiles) {
|
||||
const seenPaths = new Set(entries.map((e) => e.filePath.toLowerCase()));
|
||||
for (const extraPath of options.extraFiles) {
|
||||
const trimmed = extraPath.trim();
|
||||
if (!trimmed) continue;
|
||||
// Resolve path relative to workspace dir
|
||||
const filePath = path.isAbsolute(trimmed) ? trimmed : path.join(resolvedDir, trimmed);
|
||||
// Dedupe: skip if already in the default list
|
||||
if (seenPaths.has(filePath.toLowerCase())) continue;
|
||||
seenPaths.add(filePath.toLowerCase());
|
||||
// Use the basename as the display name
|
||||
const name = path.basename(trimmed);
|
||||
entries.push({ name, filePath, isExtra: true });
|
||||
}
|
||||
}
|
||||
|
||||
const result: WorkspaceBootstrapFile[] = [];
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
@ -269,8 +293,11 @@ export async function loadWorkspaceBootstrapFiles(dir: string): Promise<Workspac
|
||||
path: entry.filePath,
|
||||
content,
|
||||
missing: false,
|
||||
isExtra: entry.isExtra,
|
||||
});
|
||||
} catch {
|
||||
// For extra files, only include if they exist (don't mark as missing)
|
||||
if (entry.isExtra) continue;
|
||||
result.push({ name: entry.name, path: entry.filePath, missing: true });
|
||||
}
|
||||
}
|
||||
@ -284,5 +311,8 @@ export function filterBootstrapFilesForSession(
|
||||
sessionKey?: string,
|
||||
): WorkspaceBootstrapFile[] {
|
||||
if (!sessionKey || !isSubagentSessionKey(sessionKey)) return files;
|
||||
return files.filter((file) => SUBAGENT_BOOTSTRAP_ALLOWLIST.has(file.name));
|
||||
// For subagents: include allowlisted defaults + all extra workspace files
|
||||
return files.filter(
|
||||
(file) => SUBAGENT_BOOTSTRAP_ALLOWLIST.has(file.name) || file.isExtra === true,
|
||||
);
|
||||
}
|
||||
|
||||
@ -124,6 +124,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
|
||||
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
||||
"agents.list.*.identity.avatar": "Identity Avatar",
|
||||
"agents.list.*.extraWorkspaceFiles": "Extra Workspace Files",
|
||||
"gateway.remote.url": "Remote Gateway URL",
|
||||
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
||||
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
|
||||
@ -216,6 +217,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.workspace": "Workspace",
|
||||
"agents.defaults.repoRoot": "Repo Root",
|
||||
"agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars",
|
||||
"agents.defaults.extraWorkspaceFiles": "Extra Workspace Files",
|
||||
"agents.defaults.envelopeTimezone": "Envelope Timezone",
|
||||
"agents.defaults.envelopeTimestamp": "Envelope Timestamp",
|
||||
"agents.defaults.envelopeElapsed": "Envelope Elapsed",
|
||||
@ -488,6 +490,8 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).",
|
||||
"agents.defaults.bootstrapMaxChars":
|
||||
"Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).",
|
||||
"agents.defaults.extraWorkspaceFiles":
|
||||
"Additional workspace files to auto-inject into the system prompt alongside the defaults (AGENTS.md, SOUL.md, etc.). Paths are relative to the workspace directory (e.g., ['PANTHEON.md', 'protocols/SHARED.md']).",
|
||||
"agents.defaults.repoRoot":
|
||||
"Optional repository root shown in the system prompt runtime line (overrides auto-detect).",
|
||||
"agents.defaults.envelopeTimezone":
|
||||
@ -570,6 +574,8 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
|
||||
"agents.list.*.identity.avatar":
|
||||
"Agent avatar (workspace-relative path, http(s) URL, or data URI).",
|
||||
"agents.list.*.extraWorkspaceFiles":
|
||||
"Additional workspace files to auto-inject for this agent (overrides agents.defaults.extraWorkspaceFiles). Paths are relative to the workspace directory.",
|
||||
"agents.defaults.model.primary": "Primary model (provider/model).",
|
||||
"agents.defaults.model.fallbacks":
|
||||
"Ordered fallback models (provider/model). Used when the primary model fails.",
|
||||
|
||||
@ -106,6 +106,7 @@ export type AgentDefaultsConfig = {
|
||||
skipBootstrap?: boolean;
|
||||
/** Max chars for injected bootstrap files before truncation (default: 20000). */
|
||||
bootstrapMaxChars?: number;
|
||||
extraWorkspaceFiles?: string[];
|
||||
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
||||
userTimezone?: string;
|
||||
/** Time format in system prompt: auto (OS preference), 12-hour, or 24-hour. */
|
||||
|
||||
@ -24,6 +24,7 @@ export type AgentConfig = {
|
||||
workspace?: string;
|
||||
agentDir?: string;
|
||||
model?: AgentModelConfig;
|
||||
extraWorkspaceFiles?: string[];
|
||||
memorySearch?: MemorySearchConfig;
|
||||
/** Human-like delay between block replies for this agent. */
|
||||
humanDelay?: HumanDelayConfig;
|
||||
|
||||
@ -45,6 +45,7 @@ export const AgentDefaultsSchema = z
|
||||
repoRoot: z.string().optional(),
|
||||
skipBootstrap: z.boolean().optional(),
|
||||
bootstrapMaxChars: z.number().int().positive().optional(),
|
||||
extraWorkspaceFiles: z.array(z.string()).optional(),
|
||||
userTimezone: z.string().optional(),
|
||||
timeFormat: z.union([z.literal("auto"), z.literal("12"), z.literal("24")]).optional(),
|
||||
envelopeTimezone: z.string().optional(),
|
||||
|
||||
@ -422,6 +422,7 @@ export const AgentEntrySchema = z
|
||||
workspace: z.string().optional(),
|
||||
agentDir: z.string().optional(),
|
||||
model: AgentModelSchema.optional(),
|
||||
extraWorkspaceFiles: z.array(z.string()).optional(),
|
||||
memorySearch: MemorySearchSchema,
|
||||
humanDelay: HumanDelaySchema.optional(),
|
||||
heartbeat: HeartbeatSchema,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user