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:
h z
2026-04-11 21:21:32 +01:00
parent eee62efbf1
commit 07a0f06e2e
30 changed files with 1239 additions and 172 deletions

View File

@@ -0,0 +1,33 @@
import type { ContractorAgentMetadata } from "../types/contractor.js";
const CONTRACTOR_MODEL = "contractor-agent/contractor-claude-bridge";
type AgentConfig = {
id: string;
workspace?: string;
model?: string;
};
/**
* Determine whether an agent is a contractor-backed Claude agent and return its metadata.
* Contractor agents are identified by their model string — we do NOT use a custom
* runtime.type because OpenClaw's schema only allows "embedded" or "acp".
* Returns null if the agent is not contractor-backed.
*/
export function resolveContractorAgentMetadata(
agentConfig: AgentConfig,
permissionMode: string,
): ContractorAgentMetadata | null {
if (agentConfig.model !== CONTRACTOR_MODEL) return null;
const workspace = agentConfig.workspace;
if (!workspace) return null;
return {
agentId: agentConfig.id,
contractor: "claude",
bridgeModel: "contractor-claude-bridge",
workspace,
permissionMode,
};
}

View File

@@ -0,0 +1,15 @@
import path from "node:path";
import fs from "node:fs";
export function getContractorStateDir(workspace: string): string {
return path.join(workspace, ".openclaw", "contractor-agent");
}
export function getSessionMapPath(workspace: string): string {
return path.join(getContractorStateDir(workspace), "session-map.json");
}
export function ensureContractorStateDir(workspace: string): void {
const dir = getContractorStateDir(workspace);
fs.mkdirSync(dir, { recursive: true });
}

View File

@@ -0,0 +1,69 @@
import fs from "node:fs";
import { getSessionMapPath, ensureContractorStateDir } from "./runtime-state.js";
import type { SessionMapEntry, SessionMapFile } from "../types/session-map.js";
function readFile(workspace: string): SessionMapFile {
const p = getSessionMapPath(workspace);
if (!fs.existsSync(p)) {
return { version: 1, sessions: [] };
}
try {
const parsed = JSON.parse(fs.readFileSync(p, "utf8")) as Partial<SessionMapFile>;
return { version: parsed.version ?? 1, sessions: parsed.sessions ?? [] };
} catch {
return { version: 1, sessions: [] };
}
}
function writeFile(workspace: string, data: SessionMapFile): void {
ensureContractorStateDir(workspace);
fs.writeFileSync(getSessionMapPath(workspace), JSON.stringify(data, null, 2), "utf8");
}
export function getSession(workspace: string, openclawSessionKey: string): SessionMapEntry | null {
const data = readFile(workspace);
return data.sessions.find((s) => s.openclawSessionKey === openclawSessionKey) ?? null;
}
export function putSession(workspace: string, entry: SessionMapEntry): void {
const data = readFile(workspace);
const idx = data.sessions.findIndex((s) => s.openclawSessionKey === entry.openclawSessionKey);
if (idx >= 0) {
data.sessions[idx] = entry;
} else {
data.sessions.push(entry);
}
writeFile(workspace, data);
}
export function updateActivity(workspace: string, openclawSessionKey: string): void {
const data = readFile(workspace);
const entry = data.sessions.find((s) => s.openclawSessionKey === openclawSessionKey);
if (entry) {
entry.lastActivityAt = new Date().toISOString();
writeFile(workspace, data);
}
}
export function markOrphaned(workspace: string, openclawSessionKey: string): void {
const data = readFile(workspace);
const entry = data.sessions.find((s) => s.openclawSessionKey === openclawSessionKey);
if (entry) {
entry.state = "orphaned";
writeFile(workspace, data);
}
}
export function removeSession(workspace: string, openclawSessionKey: string): void {
const data = readFile(workspace);
data.sessions = data.sessions.filter((s) => s.openclawSessionKey !== openclawSessionKey);
writeFile(workspace, data);
}
export function initEmptySessionMap(workspace: string): void {
ensureContractorStateDir(workspace);
const p = getSessionMapPath(workspace);
if (!fs.existsSync(p)) {
writeFile(workspace, { version: 1, sessions: [] });
}
}