// D2 driver: one managed coder session on the scratch root, real modules from the land tree. // Usage: node d2-drive.mjs . Prints no credential value. import { readFileSync, existsSync, copyFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; const LAND = "/mnt/storage/scratch/sage-r64/land"; const D2 = "/mnt/storage/scratch/sage-r64/d2"; const [harness, mode] = process.argv.slice(2); const { Client } = await import(`${LAND}/packages/bus/src/client.mjs`); const { readSessions, SESSION_CREDENTIALS } = await import(`${LAND}/packages/seat/src/session.mjs`); const { bootConfig, loadSystem } = await import(`${LAND}/packages/cli/src/config.mjs`); const { startHost, startTimeOf } = await import(`${LAND}/packages/cli/src/host.mjs`); const { createLauncher } = await import(`${LAND}/packages/cli/src/launcher.mjs`); const { AGENT_MARKERS } = await import(`${LAND}/packages/cli/src/transport.mjs`); const env = { ...process.env, MOSAIC_CONFIG: join(D2, "config", "config.json"), MOSAIC_ROLES_DIR: join(LAND, "roles"), MOSAIC_CLAUDE_BIN: join(homedir(), ".local/bin/claude") }; for (const k of AGENT_MARKERS) delete env[k]; delete env.NODE_TEST_CONTEXT; for (const k of SESSION_CREDENTIALS.flat()) delete env[k]; // The host's session credential file holds one name, CLAUDE_CODE_OAUTH_TOKEN. Loaded, never printed. for (const line of readFileSync(join(homedir(), ".config/mosaic-dev/secrets/claude-session.env"), "utf8").split("\n")) { const m = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.*)$/); if (m) env[m[1]] = m[2].replace(/^(['"])(.*)\1$/, "$2"); } console.log("credential names loaded:", SESSION_CREDENTIALS.flat().filter((k) => env[k]).join(",")); const business = "mosaic-stack"; const logs = []; const log = (l) => { logs.push(l); console.log("log:", l); }; const system = loadSystem({ env }); const boot = bootConfig({ system, businessId: business, env }); const host = await startHost({ boot, business, log }); const launcher = createLauncher({ host, system, env, sessionDefaults: { pollInterval: 100 }, log }); let code = 0; try { await launcher.listen(); const bind = async (role, run) => { const b = await host.bindLaunch({ business, role, run, harness: "claude-code", pid: process.pid, startTime: startTimeOf(process.pid) }); const c = new Client({ path: host.path, cap: b.cap }); await c.call("role.claim"); return { b, c }; }; const chief = await bind("chief", `chief-d2-${harness}`); const cto = await bind("cto", `cto-d2-${harness}`); const launched = await launcher.fromSession(chief.b.cap, "coder"); console.log("launch:", JSON.stringify(launched)); if (!launched.ok) throw new Error("launch refused"); const run = launched.result.run; const runDir = readSessions(boot.dataRoot ?? system.dataRoot).find((s) => s.run === run)?.runDir ?? join(system.dataRoot, "launches", business, run); const manifest = join(runDir, "bundle", "manifest.json"); for (let i = 0; i < 100 && !existsSync(manifest); i++) await new Promise((r) => setTimeout(r, 100)); copyFileSync(manifest, join(D2, `manifest-${harness}.json`)); copyFileSync(join(runDir, "bundle", "prompt.md"), join(D2, `prompt-${harness}.md`)); console.log("runDir:", runDir); if (mode === "turn") { const sent = await cto.c.call("message.send", { to: "coder", class: "REQUEST", body: "D2 check for row 64. Reply in one line: list the skill names in your Skills section, and nothing else. Don't use any tool." }); const end = Date.now() + 240000; let reply; while (!reply && Date.now() < end) { reply = (await cto.c.call("message.receive")).find((m) => m.in_reply_to === sent.id); if (!reply) await new Promise((r) => setTimeout(r, 1000)); } console.log("reply:", reply ? JSON.stringify({ from_role: reply.from_role, from_run: reply.from_run, body: reply.body.slice(-600) }) : "NONE in 240 s"); if (!reply) code = 3; } else { await new Promise((r) => setTimeout(r, 3000)); } } catch (e) { console.log("error:", e.message); code = 2; } finally { await launcher.close(); await host.close(0); } process.exit(code);