refactor: restructure to plugin/ + services/ layout and add per-turn bootstrap injection
- Migrate src/ → plugin/ (plugin/core/, plugin/web/, plugin/commands/)
and src/mcp/ → services/ per OpenClaw plugin dev spec
- Add Gemini CLI backend (plugin/core/gemini/sdk-adapter.ts) with GEMINI.md
system-prompt injection
- Inject bootstrap as stateless system prompt on every turn instead of
first turn only: Claude via --system-prompt, Gemini via workspace/GEMINI.md;
eliminates isFirstTurn branch, keeps skills in sync with OpenClaw snapshots
- Fix session-map-store defensive parsing (sessions ?? []) to handle bare {}
reset files without crashing on .find()
- Add docs/TEST_FLOW.md with E2E test scenarios and expected outcomes
- Add docs/claude/BRIDGE_MODEL_FINDINGS.md with contractor-probe results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
117
plugin/web/bootstrap.ts
Normal file
117
plugin/web/bootstrap.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export type BootstrapInput = {
|
||||
agentId: string;
|
||||
openclawSessionKey: string;
|
||||
workspace: string;
|
||||
/** Skills XML block extracted from the OpenClaw system prompt, if any */
|
||||
skillsBlock?: string;
|
||||
/** Subset of OpenClaw context files present in the workspace (for persona/identity) */
|
||||
workspaceContextFiles?: string[];
|
||||
};
|
||||
|
||||
/** Read a workspace file. Returns the content, or null if missing/unreadable. */
|
||||
function readWorkspaceFile(workspace: string, filename: string): string | null {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(workspace, filename), "utf8").trim();
|
||||
return content || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the one-time bootstrap message injected at the start of a new session.
|
||||
* Workspace context files (SOUL.md, IDENTITY.md, MEMORY.md, USER.md) are read
|
||||
* here and embedded inline — the agent must not need to fetch them separately.
|
||||
* Must NOT be re-injected on every turn.
|
||||
*/
|
||||
export function buildBootstrap(input: BootstrapInput): string {
|
||||
const contextFiles = input.workspaceContextFiles ?? [];
|
||||
|
||||
const lines = [
|
||||
`You are operating as a contractor agent inside OpenClaw.`,
|
||||
``,
|
||||
`## Context`,
|
||||
`- Agent ID: ${input.agentId}`,
|
||||
`- Session key: ${input.openclawSessionKey}`,
|
||||
`- Workspace: ${input.workspace}`,
|
||||
``,
|
||||
`## Role`,
|
||||
`You receive tasks from OpenClaw users and complete them using your tools.`,
|
||||
`You do not need to manage your own session context — OpenClaw handles session routing.`,
|
||||
`Your responses go directly back to the user through OpenClaw.`,
|
||||
``,
|
||||
`## Guidelines`,
|
||||
`- Work in the specified workspace directory.`,
|
||||
`- Be concise and action-oriented. Use tools to accomplish tasks rather than describing what you would do.`,
|
||||
`- Each message you receive contains the latest user request. Previous context is in your session memory.`,
|
||||
`- If a task is unclear, ask one focused clarifying question.`,
|
||||
];
|
||||
|
||||
// ── Persona (SOUL.md) ─────────────────────────────────────────────────────
|
||||
// Injected directly so the agent embodies the persona immediately without
|
||||
// needing to read files first.
|
||||
if (contextFiles.includes("SOUL.md")) {
|
||||
const soul = readWorkspaceFile(input.workspace, "SOUL.md");
|
||||
if (soul) {
|
||||
lines.push(``, `## Soul`, soul);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Identity (IDENTITY.md) ────────────────────────────────────────────────
|
||||
if (contextFiles.includes("IDENTITY.md")) {
|
||||
const identity = readWorkspaceFile(input.workspace, "IDENTITY.md");
|
||||
if (identity) {
|
||||
lines.push(``, `## Identity`, identity);
|
||||
// If the file still looks like the default template, encourage the agent
|
||||
// to fill it in.
|
||||
if (identity.includes("Fill this in during your first conversation")) {
|
||||
lines.push(
|
||||
``,
|
||||
`_IDENTITY.md is still a template. Pick a name, creature type, and vibe for yourself`,
|
||||
`and update the file at ${input.workspace}/IDENTITY.md._`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── User profile (USER.md) ────────────────────────────────────────────────
|
||||
if (contextFiles.includes("USER.md")) {
|
||||
const user = readWorkspaceFile(input.workspace, "USER.md");
|
||||
if (user) {
|
||||
lines.push(``, `## User Profile`, user);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Memory index (MEMORY.md) ──────────────────────────────────────────────
|
||||
if (contextFiles.includes("MEMORY.md") || contextFiles.some(f => f.startsWith("memory/"))) {
|
||||
const memoryIndex = readWorkspaceFile(input.workspace, "MEMORY.md");
|
||||
lines.push(``, `## Memory`);
|
||||
if (memoryIndex) {
|
||||
lines.push(memoryIndex);
|
||||
}
|
||||
lines.push(
|
||||
``,
|
||||
`Memory files live in ${input.workspace}/memory/. Use the Read tool to fetch individual`,
|
||||
`memories and write new ones to ${input.workspace}/memory/<topic>.md.`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Skills ────────────────────────────────────────────────────────────────
|
||||
if (input.skillsBlock) {
|
||||
lines.push(
|
||||
``,
|
||||
`## Skills`,
|
||||
`The following skills are available. When a task matches a skill's description:`,
|
||||
`1. Read the skill's SKILL.md using the Read tool (the <location> field is the absolute path).`,
|
||||
`2. Follow the instructions in SKILL.md. Replace \`{baseDir}\` with the directory containing SKILL.md.`,
|
||||
`3. Run scripts using the Bash tool, NOT the \`exec\` tool (you have Bash, not exec).`,
|
||||
``,
|
||||
input.skillsBlock,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user