diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index aae983040..aed9d88e6 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -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, diff --git a/src/agents/bootstrap-files.test.ts b/src/agents/bootstrap-files.test.ts index 43ef5e4a5..670700591 100644 --- a/src/agents/bootstrap-files.test.ts +++ b/src/agents/bootstrap-files.test.ts @@ -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); + }); +}); diff --git a/src/agents/bootstrap-files.ts b/src/agents/bootstrap-files.ts index a63846832..2a805ac7a 100644 --- a/src/agents/bootstrap-files.ts +++ b/src/agents/bootstrap-files.ts @@ -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,7 +25,11 @@ export async function resolveBootstrapFilesForRun(params: { agentId?: string; }): Promise { const sessionKey = params.sessionKey ?? params.sessionId; - const extraFiles = params.config?.agents?.defaults?.extraWorkspaceFiles; + // 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, { extraFiles }), sessionKey, diff --git a/src/config/schema.ts b/src/config/schema.ts index d474be44e..1ed58c7c3 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -124,6 +124,7 @@ const FIELD_LABELS: Record = { "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", @@ -570,6 +571,8 @@ const FIELD_HELP: Record = { "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.", diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index f083c1897..355621c89 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -24,6 +24,8 @@ export type AgentConfig = { workspace?: string; agentDir?: string; model?: AgentModelConfig; + /** Extra workspace files to inject alongside the default bootstrap files (paths relative to workspace). */ + extraWorkspaceFiles?: string[]; memorySearch?: MemorySearchConfig; /** Human-like delay between block replies for this agent. */ humanDelay?: HumanDelayConfig; diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 7a63e307d..24f86a44b 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -421,6 +421,8 @@ export const AgentEntrySchema = z workspace: z.string().optional(), agentDir: z.string().optional(), model: AgentModelSchema.optional(), + /** Extra workspace files to inject alongside the default bootstrap files (paths relative to workspace). */ + extraWorkspaceFiles: z.array(z.string()).optional(), memorySearch: MemorySearchSchema, humanDelay: HumanDelaySchema.optional(), heartbeat: HeartbeatSchema,