refactor: expand bootstrap helpers and tests
This commit is contained in:
66
src/agents/bootstrap-files.test.ts
Normal file
66
src/agents/bootstrap-files.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
resolveBootstrapContextForRun,
|
||||||
|
resolveBootstrapFilesForRun,
|
||||||
|
} from "./bootstrap-files.js";
|
||||||
|
import {
|
||||||
|
clearInternalHooks,
|
||||||
|
registerInternalHook,
|
||||||
|
type AgentBootstrapHookContext,
|
||||||
|
} from "../hooks/internal-hooks.js";
|
||||||
|
|
||||||
|
describe("resolveBootstrapFilesForRun", () => {
|
||||||
|
beforeEach(() => clearInternalHooks());
|
||||||
|
afterEach(() => clearInternalHooks());
|
||||||
|
|
||||||
|
it("applies bootstrap hook overrides", async () => {
|
||||||
|
registerInternalHook("agent:bootstrap", (event) => {
|
||||||
|
const context = event.context as AgentBootstrapHookContext;
|
||||||
|
context.bootstrapFiles = [
|
||||||
|
...context.bootstrapFiles,
|
||||||
|
{
|
||||||
|
name: "EXTRA.md",
|
||||||
|
path: path.join(context.workspaceDir, "EXTRA.md"),
|
||||||
|
content: "extra",
|
||||||
|
missing: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-bootstrap-"));
|
||||||
|
const files = await resolveBootstrapFilesForRun({ workspaceDir });
|
||||||
|
|
||||||
|
expect(files.some((file) => file.name === "EXTRA.md")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveBootstrapContextForRun", () => {
|
||||||
|
beforeEach(() => clearInternalHooks());
|
||||||
|
afterEach(() => clearInternalHooks());
|
||||||
|
|
||||||
|
it("returns context files for hook-adjusted bootstrap files", async () => {
|
||||||
|
registerInternalHook("agent:bootstrap", (event) => {
|
||||||
|
const context = event.context as AgentBootstrapHookContext;
|
||||||
|
context.bootstrapFiles = [
|
||||||
|
...context.bootstrapFiles,
|
||||||
|
{
|
||||||
|
name: "EXTRA.md",
|
||||||
|
path: path.join(context.workspaceDir, "EXTRA.md"),
|
||||||
|
content: "extra",
|
||||||
|
missing: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-bootstrap-"));
|
||||||
|
const result = await resolveBootstrapContextForRun({ workspaceDir });
|
||||||
|
const extra = result.contextFiles.find((file) => file.path === "EXTRA.md");
|
||||||
|
|
||||||
|
expect(extra?.content).toBe("extra");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
loadWorkspaceBootstrapFiles,
|
loadWorkspaceBootstrapFiles,
|
||||||
type WorkspaceBootstrapFile,
|
type WorkspaceBootstrapFile,
|
||||||
} from "./workspace.js";
|
} from "./workspace.js";
|
||||||
|
import { buildBootstrapContextFiles, resolveBootstrapMaxChars } from "./pi-embedded-helpers.js";
|
||||||
|
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||||
|
|
||||||
export async function resolveBootstrapFilesForRun(params: {
|
export async function resolveBootstrapFilesForRun(params: {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
@@ -27,3 +29,22 @@ export async function resolveBootstrapFilesForRun(params: {
|
|||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveBootstrapContextForRun(params: {
|
||||||
|
workspaceDir: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
sessionKey?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
warn?: (message: string) => void;
|
||||||
|
}): Promise<{
|
||||||
|
bootstrapFiles: WorkspaceBootstrapFile[];
|
||||||
|
contextFiles: EmbeddedContextFile[];
|
||||||
|
}> {
|
||||||
|
const bootstrapFiles = await resolveBootstrapFilesForRun(params);
|
||||||
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles, {
|
||||||
|
maxChars: resolveBootstrapMaxChars(params.config),
|
||||||
|
warn: params.warn,
|
||||||
|
});
|
||||||
|
return { bootstrapFiles, contextFiles };
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { createSubsystemLogger } from "../logging.js";
|
|||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { resolveSessionAgentIds } from "./agent-scope.js";
|
import { resolveSessionAgentIds } from "./agent-scope.js";
|
||||||
import { resolveBootstrapFilesForRun } from "./bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "./bootstrap-files.js";
|
||||||
import { resolveCliBackendConfig } from "./cli-backends.js";
|
import { resolveCliBackendConfig } from "./cli-backends.js";
|
||||||
import {
|
import {
|
||||||
appendImagePathsToPrompt,
|
appendImagePathsToPrompt,
|
||||||
@@ -25,12 +25,7 @@ import {
|
|||||||
writeCliImages,
|
writeCliImages,
|
||||||
} from "./cli-runner/helpers.js";
|
} from "./cli-runner/helpers.js";
|
||||||
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
|
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
|
||||||
import {
|
import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js";
|
||||||
buildBootstrapContextFiles,
|
|
||||||
classifyFailoverReason,
|
|
||||||
isFailoverErrorMessage,
|
|
||||||
resolveBootstrapMaxChars,
|
|
||||||
} from "./pi-embedded-helpers.js";
|
|
||||||
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("agent/claude-cli");
|
const log = createSubsystemLogger("agent/claude-cli");
|
||||||
@@ -72,15 +67,12 @@ export async function runCliAgent(params: {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const hookAdjustedBootstrapFiles = await resolveBootstrapFilesForRun({
|
const sessionLabel = params.sessionKey ?? params.sessionId;
|
||||||
|
const { contextFiles } = await resolveBootstrapContextForRun({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
});
|
|
||||||
const sessionLabel = params.sessionKey ?? params.sessionId;
|
|
||||||
const contextFiles = buildBootstrapContextFiles(hookAdjustedBootstrapFiles, {
|
|
||||||
maxChars: resolveBootstrapMaxChars(params.config),
|
|
||||||
warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`),
|
warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`),
|
||||||
});
|
});
|
||||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
|||||||
import { resolveUserPath } from "../../utils.js";
|
import { resolveUserPath } from "../../utils.js";
|
||||||
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../agent-scope.js";
|
import { resolveSessionAgentIds } from "../agent-scope.js";
|
||||||
import { resolveBootstrapFilesForRun } from "../bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "../bootstrap-files.js";
|
||||||
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
||||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
||||||
import { getApiKeyForModel, resolveModelAuthMode } from "../model-auth.js";
|
import { getApiKeyForModel, resolveModelAuthMode } from "../model-auth.js";
|
||||||
import { ensureClawdbotModelsJson } from "../models-config.js";
|
import { ensureClawdbotModelsJson } from "../models-config.js";
|
||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
|
||||||
type EmbeddedContextFile,
|
|
||||||
ensureSessionHeader,
|
ensureSessionHeader,
|
||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
validateAnthropicTurns,
|
validateAnthropicTurns,
|
||||||
@@ -178,20 +176,14 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hookAdjustedBootstrapFiles = await resolveBootstrapFilesForRun({
|
const sessionLabel = params.sessionKey ?? params.sessionId;
|
||||||
|
const { contextFiles } = await resolveBootstrapContextForRun({
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
|
warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`),
|
||||||
});
|
});
|
||||||
const sessionLabel = params.sessionKey ?? params.sessionId;
|
|
||||||
const contextFiles: EmbeddedContextFile[] = buildBootstrapContextFiles(
|
|
||||||
hookAdjustedBootstrapFiles,
|
|
||||||
{
|
|
||||||
maxChars: resolveBootstrapMaxChars(params.config),
|
|
||||||
warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const runAbortController = new AbortController();
|
const runAbortController = new AbortController();
|
||||||
const toolsRaw = createClawdbotCodingTools({
|
const toolsRaw = createClawdbotCodingTools({
|
||||||
exec: {
|
exec: {
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
|||||||
import { resolveUserPath } from "../../../utils.js";
|
import { resolveUserPath } from "../../../utils.js";
|
||||||
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||||
import { resolveBootstrapFilesForRun } from "../../bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
import { resolveModelAuthMode } from "../../model-auth.js";
|
import { resolveModelAuthMode } from "../../model-auth.js";
|
||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
|
||||||
isCloudCodeAssistFormatError,
|
isCloudCodeAssistFormatError,
|
||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
validateAnthropicTurns,
|
validateAnthropicTurns,
|
||||||
@@ -120,17 +119,15 @@ export async function runEmbeddedAttempt(
|
|||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hookAdjustedBootstrapFiles = await resolveBootstrapFilesForRun({
|
|
||||||
workspaceDir: effectiveWorkspace,
|
|
||||||
config: params.config,
|
|
||||||
sessionKey: params.sessionKey,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
});
|
|
||||||
const sessionLabel = params.sessionKey ?? params.sessionId;
|
const sessionLabel = params.sessionKey ?? params.sessionId;
|
||||||
const contextFiles = buildBootstrapContextFiles(hookAdjustedBootstrapFiles, {
|
const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles } =
|
||||||
maxChars: resolveBootstrapMaxChars(params.config),
|
await resolveBootstrapContextForRun({
|
||||||
warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`),
|
workspaceDir: effectiveWorkspace,
|
||||||
});
|
config: params.config,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`),
|
||||||
|
});
|
||||||
|
|
||||||
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { resolveBootstrapMaxChars } from "../../agents/pi-embedded-helpers.js";
|
||||||
buildBootstrapContextFiles,
|
|
||||||
resolveBootstrapMaxChars,
|
|
||||||
} from "../../agents/pi-embedded-helpers.js";
|
|
||||||
import { createClawdbotCodingTools } from "../../agents/pi-tools.js";
|
import { createClawdbotCodingTools } from "../../agents/pi-tools.js";
|
||||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||||
@@ -9,7 +6,7 @@ import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|||||||
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
||||||
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
||||||
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
||||||
import { resolveBootstrapFilesForRun } from "../../agents/bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||||
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||||
import type { ReplyPayload } from "../types.js";
|
import type { ReplyPayload } from "../types.js";
|
||||||
@@ -52,15 +49,12 @@ async function resolveContextReport(
|
|||||||
|
|
||||||
const workspaceDir = params.workspaceDir;
|
const workspaceDir = params.workspaceDir;
|
||||||
const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg);
|
const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg);
|
||||||
const hookAdjustedBootstrapFiles = await resolveBootstrapFilesForRun({
|
const { bootstrapFiles, contextFiles: injectedFiles } = await resolveBootstrapContextForRun({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
sessionId: params.sessionEntry?.sessionId,
|
sessionId: params.sessionEntry?.sessionId,
|
||||||
});
|
});
|
||||||
const injectedFiles = buildBootstrapContextFiles(hookAdjustedBootstrapFiles, {
|
|
||||||
maxChars: bootstrapMaxChars,
|
|
||||||
});
|
|
||||||
const skillsSnapshot = (() => {
|
const skillsSnapshot = (() => {
|
||||||
try {
|
try {
|
||||||
return buildWorkspaceSkillSnapshot(workspaceDir, {
|
return buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||||
|
|||||||
45
src/hooks/bundled/soul-evil/handler.test.ts
Normal file
45
src/hooks/bundled/soul-evil/handler.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import handler from "./handler.js";
|
||||||
|
import { createHookEvent } from "../../hooks.js";
|
||||||
|
import type { AgentBootstrapHookContext } from "../../hooks.js";
|
||||||
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
|
|
||||||
|
describe("soul-evil hook", () => {
|
||||||
|
it("skips subagent sessions", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-soul-"));
|
||||||
|
await fs.writeFile(path.join(tempDir, "SOUL_EVIL.md"), "chaotic", "utf-8");
|
||||||
|
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
hooks: {
|
||||||
|
internal: {
|
||||||
|
entries: {
|
||||||
|
"soul-evil": { enabled: true, chance: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const context: AgentBootstrapHookContext = {
|
||||||
|
workspaceDir: tempDir,
|
||||||
|
bootstrapFiles: [
|
||||||
|
{
|
||||||
|
name: "SOUL.md",
|
||||||
|
path: path.join(tempDir, "SOUL.md"),
|
||||||
|
content: "friendly",
|
||||||
|
missing: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cfg,
|
||||||
|
sessionKey: "agent:main:subagent:abc",
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = createHookEvent("agent", "bootstrap", "agent:main:subagent:abc", context);
|
||||||
|
await handler(event);
|
||||||
|
|
||||||
|
expect(context.bootstrapFiles[0]?.content).toBe("friendly");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -128,4 +128,29 @@ describe("applySoulEvilOverride", () => {
|
|||||||
const soul = updated.find((file) => file.name === DEFAULT_SOUL_FILENAME);
|
const soul = updated.find((file) => file.name === DEFAULT_SOUL_FILENAME);
|
||||||
expect(soul?.content).toBe("friendly");
|
expect(soul?.content).toBe("friendly");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaves files untouched when SOUL.md is not in bootstrap files", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-soul-"));
|
||||||
|
const evilPath = path.join(tempDir, DEFAULT_SOUL_EVIL_FILENAME);
|
||||||
|
await fs.writeFile(evilPath, "chaotic", "utf-8");
|
||||||
|
|
||||||
|
const files: WorkspaceBootstrapFile[] = [
|
||||||
|
{
|
||||||
|
name: "AGENTS.md",
|
||||||
|
path: path.join(tempDir, "AGENTS.md"),
|
||||||
|
content: "agents",
|
||||||
|
missing: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const updated = await applySoulEvilOverride({
|
||||||
|
files,
|
||||||
|
workspaceDir: tempDir,
|
||||||
|
config: { chance: 1 },
|
||||||
|
userTimezone: "UTC",
|
||||||
|
random: () => 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated).toEqual(files);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user