import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { copyToClipboard } from "@earendil-works/pi-coding-agent"; import type { AutocompleteItem } from "@earendil-works/pi-tui"; import * as fs from "node:fs"; import * as path from "node:path"; import { AdminManager, DEFAULT_ADMIN_PORT } from "./admin-manager.ts"; import { registerBoard, renderCard } from "./board.ts"; import type { XferController } from "./controller.ts"; import { BrokerManager } from "./broker-manager.ts"; import { doctorSeverity, formatDoctorReport, runDoctor } from "./doctor.ts"; import { collectGarbage } from "./gc.ts"; import { MESH_CONFIG_PATH } from "./constants.ts"; import type { MeshNode } from "./mesh.ts"; import { ensureRunnerPool, listMeshNodes } from "./mesh-engine.ts"; import { loadMeshConfig } from "./mesh-config.ts"; import { XFER_DIR } from "./constants.ts"; import { endpointForName, listPeers, peerDescription } from "./utils.ts"; /** Options for `registerXferCommand`. */ export interface XferCommandOptions { /** Broker lifecycle manager; defaults to a real BrokerManager (tests inject a stub). */ brokerManager?: BrokerManager; /** Directory holding broker.log; defaults to the broker manager's xferDir (XFER_DIR). */ brokerXferDir?: string; /** Directory scanned by `/xfer gc` for zombie peer sockets; defaults to XFER_DIR. */ xferDir?: string; /** Board directory; defaults to ~/.pi/xfer/board. */ boardDir?: string; /** Admin daemon lifecycle manager; defaults to a real AdminManager (tests inject a stub). */ adminManager?: AdminManager; /** Mesh engine config read by `/xfer doctor`; defaults to /mesh.config.json via MESH_CONFIG_PATH. */ meshConfigPath?: string; /** Mesh transport node; when absent the `/xfer mesh` command group is unavailable. */ mesh?: MeshNode; } /** Listener summary shared by `/xfer list` and `/xfer status`. */ function listenerSection(controller: XferController): string { const identity = controller.state.identity; let text = "\n\n๐Ÿ“ก Listener:"; text += `\n unix socket: ${identity ? `${identity.endpoint} โ€” name "${identity.name}"` : "(not initialised)"}`; return text; } /** Best-effort open of a URL in the system browser (fire-and-forget). */ function openUrl(url: string): void { const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open"; import("node:child_process").then(({ spawn }) => { spawn(opener, [url], { detached: true, stdio: "ignore" }).unref(); }).catch(() => { /* browser open is best-effort */ }); } /** How many trailing broker.log lines `/xfer broker logs` shows. */ const LOG_TAIL_LINES = 50; /** Tail of /broker.log โ€” last ~50 non-empty lines, or an explanatory note. */ function brokerLogTail(xferDir: string, lines = LOG_TAIL_LINES): string { const logPath = path.join(xferDir, "broker.log"); try { const tail = fs .readFileSync(logPath, "utf-8") .split("\n") .filter((line) => line.trim() !== "") .slice(-lines); return tail.length > 0 ? tail.join("\n") : `(empty) ${logPath}`; } catch { return `broker.log unavailable at ${logPath}`; } } /** Register the `/xfer` slash command. */ export function registerXferCommand(pi: ExtensionAPI, controller: XferController, options: XferCommandOptions = {}): void { const brokerManager = options.brokerManager ?? new BrokerManager(); const adminManager = options.adminManager ?? new AdminManager(); const brokerXferDir = options.brokerXferDir ?? XFER_DIR; const xferDir = options.xferDir ?? XFER_DIR; const meshConfigPath = options.meshConfigPath ?? MESH_CONFIG_PATH; const board = registerBoard(pi, { boardDir: options.boardDir, getAuthor: () => controller.state.identity?.name ?? "unknown" }); pi.registerCommand("xfer", { description: "Xfer: one-way handoff to another Pi.\n" + " /xfer โ€” generate doc and send\n" + " /xfer list โ€” list peers\n" + " /xfer name [] โ€” show or set name\n" + " /xfer desc [] โ€” set or clear this node's description (no text clears; synced to mesh when online)\n" + " /xfer board โ€” async collaboration board\n" + " /xfer gc โ€” reap zombie peer sockets (dead pid / no listener)\n" + " /xfer broker โ€” broker daemon lifecycle\n" + " /xfer admin [stop] โ€” local web console (idempotent start + open URL)\n" + " /xfer doctor โ€” self-check the install (sockets / peers / broker / admin / mesh)", getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => { // `/xfer board ` completes board subcommands, then card ids. if (prefix.startsWith("board") && (prefix.length === 5 || prefix.startsWith("board "))) { return board.completions(prefix.slice(5)); } // `/xfer broker ` completes the broker subcommand group only. if (prefix.startsWith("broker ")) { const subPrefix = prefix.slice("broker ".length).replace(/^\s+/, ""); const items: AutocompleteItem[] = [ { value: "broker start", label: "start", description: "Start the broker daemon (--port N to pin, 0 = ephemeral; already-running is a no-op)" }, { value: "broker status", label: "status", description: "Show broker status (port/pid)" }, { value: "broker stop", label: "stop", description: "Stop the broker daemon" }, { value: "broker logs", label: "logs", description: "Show the last broker.log lines" }, ].filter(i => i.label.startsWith(subPrefix)); return items.length > 0 ? items : null; } // `/xfer admin ` completes the admin subcommand group only. if (prefix.startsWith("admin ")) { const subPrefix = prefix.slice("admin ".length).replace(/^\s+/, ""); const items: AutocompleteItem[] = [ { value: "admin stop", label: "stop", description: "Stop the admin web console daemon" }, ].filter(i => i.label.startsWith(subPrefix)); return items.length > 0 ? items : null; } const peers = listPeers(controller.state.identity?.name ?? "", xferDir); // `/xfer mesh ` completes the mesh subcommand group. if (prefix.startsWith("mesh ") || prefix === "mesh") { if (prefix === "mesh") { return [{ value: "mesh", label: "mesh", description: "Mesh transport: status / up / down / list / " }]; } const subPrefix = prefix.slice("mesh ".length).replace(/^\s+/, ""); const items: AutocompleteItem[] = [ { value: "mesh up", label: "up", description: "Bring this instance online as its xfer name (needs mesh.config.json)" }, { value: "mesh down", label: "down", description: "Destroy this node and go offline" }, { value: "mesh list", label: "list", description: "List mesh nodes (live engine query)" }, { value: "mesh setup", label: "setup", description: "Verify mesh.config.json works: parse config, ensure the pool's runner-config, query the engine" }, ].filter(i => i.label.startsWith(subPrefix)); return items.length > 0 ? items : null; } const all: AutocompleteItem[] = [ { value: "list", label: "list", description: "List available peers" }, { value: "name", label: "name", description: "Show or set this agent's name" }, { value: "desc", label: "desc", description: "Set or clear this node's description (what requests it handles; synced to mesh when online)" }, { value: "broker", label: "broker", description: "Broker daemon: start / status / stop / logs" }, { value: "admin", label: "admin", description: "Local web console: open (idempotent start) / stop" }, { value: "board", label: "board", description: "Async collaboration board: list / new / read / write / del / clean / open" }, { value: "gc", label: "gc", description: "Reap zombie peer sockets (dead pid / no listener)" }, { value: "doctor", label: "doctor", description: "Self-check the install: socket dir / peers / broker / admin / mesh" }, { value: "status", label: "status", description: "Show listener status" }, { value: "mesh", label: "mesh", description: "Mesh transport: status / up / down / list / " }, ...peers.map(peer => ({ value: peer.xferName, label: peer.xferName, description: peerDescription(peer), })), ]; const filtered = all.filter(i => i.value.startsWith(prefix)); return filtered.length > 0 ? filtered : null; }, handler: async (args, ctx) => { const state = controller.state; const parts = (args ?? "").trim().split(/\s+/); const cmd = parts[0]; // โ”€โ”€ help โ”€โ”€ if (!cmd || cmd === "help") { ctx.ui.notify( "๐Ÿ“ก /xfer โ€” generate handoff doc\n" + " /xfer list โ€” list peers\n" + " /xfer status โ€” listener status\n" + " /xfer name [] โ€” show or set name\n" + " /xfer desc [] โ€” set or clear this node's description (no text clears; synced to mesh when online)\n" + " /xfer board list|new|read|write|del|clean|open โ€” async collaboration board\n" + " /xfer broker start|status|stop|logs โ€” broker daemon lifecycle\n" + " /xfer admin [stop] โ€” local web console (start is idempotent, opens the URL)\n" + " /xfer mesh [ ] โ€” mesh transport\n" + " /xfer gc โ€” reap zombie peer sockets\n" + " /xfer doctor โ€” self-check the install and print fix hints\n" + "\n" + "๐Ÿ’ก One-way, no wait. Declare the kind โ€” a task comes back, a notification stays silent.", "info", ); return; } // โ”€โ”€ list โ”€โ”€ if (cmd === "list") { const peers = listPeers(state.identity?.name ?? "", xferDir); let text = peers.length ? `๐Ÿ“ก Peers:\n\n${peers.map(peer => ` ${peer.xferName}\n ${peerDescription(peer)}`).join("\n\n")}` : "๐Ÿ“ก No peers found"; text += listenerSection(controller); ctx.ui.notify(text, "info"); return; } // โ”€โ”€ name (show or set) โ”€โ”€ if (cmd === "name") { const newName = parts[1]; if (!newName) { // show current xfer name + session name, and copy xfer name to clipboard if (!state.identity) return; const currentSessionName = state.sessionName(); try { await copyToClipboard(state.identity.name); ctx.ui.notify( `๐Ÿ“ก Xfer name: ${state.identity.name}\n` + ` Session name: ${currentSessionName ?? "(unnamed)"} (copied xfer name to clipboard)`, "info", ); } catch { ctx.ui.notify( `๐Ÿ“ก Xfer name: ${state.identity.name}\n` + ` Session name: ${currentSessionName ?? "(unnamed)"}`, "info", ); } return; } await controller.rename(ctx, newName); return; } // โ”€โ”€ desc (set or clear this node's description) โ”€โ”€ // Stored locally in this node's .json so it works without mesh; // synced to the mesh actor best-effort when online (and on /xfer mesh up). if (cmd === "desc") { const text = parts.slice(1).join(" ").trim(); state.description = text || undefined; state.writeMetadata(true); const name = state.identity?.name ?? "this node"; const mesh = options.mesh; if (mesh?.status().online) { try { const stored = await mesh.setDescription(text); ctx.ui.notify( stored ? `๐Ÿชช Node description set ("${name}", synced to mesh): ${stored}` : `๐Ÿชช Node description cleared ("${name}", synced to mesh)`, "info", ); } catch (err) { ctx.ui.notify( `โš ๏ธ Saved locally, but mesh sync failed: ${err instanceof Error ? err.message : String(err)}`, "error", ); } } else { ctx.ui.notify( text ? `๐Ÿชช Node description set ("${name}", local only): ${text}` : `๐Ÿชช Node description cleared ("${name}", local only)`, "info", ); } return; } // โ”€โ”€ /xfer broker start|status|stop|logs (broker daemon) โ”€โ”€ if (cmd === "broker") { const sub = parts[1]; if (sub !== "start" && sub !== "status" && sub !== "stop" && sub !== "logs") { ctx.ui.notify("Usage: /xfer broker โ€” start accepts --port N (0 = ephemeral port)", "error"); return; } if (sub === "start") { // Optional `--port N` pins the daemon port for this start (0 = // ephemeral). A live broker.pid keeps it a no-op ("already running") // regardless of the port โ€” the pid is the source of truth, not the // port (broker-main.ts pre-bind pid check). let requested: number | undefined; const portIdx = parts.indexOf("--port"); if (portIdx >= 0) { requested = Number.parseInt(parts[portIdx + 1] ?? "", 10); if (!Number.isInteger(requested) || requested < 0 || requested > 65535) { ctx.ui.notify("โŒ --port needs an integer 0..65535 (0 = ephemeral)", "error"); return; } } try { const result = await brokerManager.start(requested); ctx.ui.notify(result === "already running" ? "๐ŸŸก Broker already running" : `๐ŸŸข ${result}`, "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } if (sub === "status") { // status() never rejects (chrome-devtools convention: read the output, not the exit code). ctx.ui.notify(await brokerManager.status(), "info"); return; } if (sub === "stop") { try { const result = await brokerManager.stop(); ctx.ui.notify(result === "broker not running" ? "โญ• Broker not running" : `๐Ÿ›‘ ${result}`, "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } // logs โ€” tail /broker.log without touching the daemon. ctx.ui.notify(brokerLogTail(brokerXferDir), "info"); return; } // โ”€โ”€ /xfer admin [stop] โ€” local web console daemon โ”€โ”€ if (cmd === "admin") { const sub = parts[1]; if (sub !== undefined && sub !== "stop" && sub !== "open") { ctx.ui.notify("Usage: /xfer admin [stop] โ€” bare command starts (idempotently) and opens the console", "error"); return; } if (sub === "stop") { try { const result = await adminManager.stop(); ctx.ui.notify(result === "admin not running" ? "โญ• Admin not running" : `๐Ÿ›‘ ${result}`, "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } try { const result = await adminManager.start(); const info = await adminManager.probe(); const url = `http://localhost:${info?.port ?? DEFAULT_ADMIN_PORT}`; openUrl(url); ctx.ui.notify( result === "already running" ? `๐ŸŸก Admin already running โ€” ${url}` : `๐ŸŸข ${result}\n๐ŸŒ ${url}`, "info", ); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } // โ”€โ”€ /xfer mesh โ€” opt-in cross-machine transport โ”€โ”€ if (cmd === "mesh") { const mesh = options.mesh; if (!mesh) { ctx.ui.notify("โŒ Mesh transport is not available in this session", "error"); return; } const sub = parts[1]; if (!sub || sub === "help") { ctx.ui.notify( "๐ŸŒ /xfer mesh โ€” mesh status\n" + " /xfer mesh up โ€” bring this instance online (as its xfer name)\n" + " /xfer mesh down โ€” destroy this node and go offline\n" + " /xfer mesh list โ€” list mesh nodes (live engine query)\n" + " /xfer mesh setup โ€” verify mesh.config.json: config parse, pool runner-config, engine reachability\n" + " /xfer mesh โ€” send a one-way handoff via mesh", "info", ); return; } if (sub === "up") { const name = state.identity?.name; if (!name) { ctx.ui.notify("โŒ Xfer is not initialised", "error"); return; } try { await mesh.up(name); if (state.description) { try { await mesh.setDescription(state.description); } catch { /* best effort */ } } ctx.ui.notify(`๐ŸŸข Mesh online as "${name}" (engine: ${MESH_CONFIG_PATH})`, "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } if (sub === "down") { try { await mesh.down(); ctx.ui.notify("๐Ÿ›‘ Mesh offline", "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } if (sub === "list") { try { const nodes = await mesh.list(); const status = mesh.status(); const lines = nodes.map((n) => ` ${n.name}${status.online && status.name === n.name ? " (this node)" : ""}`); ctx.ui.notify(nodes.length ? `๐ŸŒ Mesh nodes:\n${lines.join("\n")}` : "๐ŸŒ No mesh nodes online", "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; if (sub === "setup") { try { const config = loadMeshConfig(MESH_CONFIG_PATH); const lines = [`โœ“ config OK (${MESH_CONFIG_PATH}) โ€” engine ${config.endpoint}`]; if (config.pool) { const { existed } = await ensureRunnerPool(config); lines.push(existed ? `โœ“ runner-config "${config.pool}" already exists` : `โœ“ created runner-config "${config.pool}"`); } else { lines.push('โ„น no "pool" set โ€” this host uses the engine default pool'); } const nodes = await listMeshNodes(config); lines.push(`โœ“ engine reachable โ€” ${nodes.length} node(s)`); lines.push('Next: /xfer mesh up'); ctx.ui.notify(lines.join("\n"), "info"); } catch (err) { ctx.ui.notify(`โŒ ${err instanceof Error ? err.message : String(err)}`, "error"); } return; } } const target = sub; const requirement = parts.slice(2).join(" ").trim(); if (!requirement) { ctx.ui.notify(`Usage: /xfer mesh ${target} `, "error"); return; } if (!mesh.status().online) { ctx.ui.notify("โŒ Mesh is offline โ€” run /xfer mesh up first", "error"); return; } if (!state.identity) { ctx.ui.notify("โŒ Xfer is not initialised", "error"); return; } pi.sendUserMessage( `## Handoff Request (mesh, one-way)\n\n` + `**Target**: ${target} (mesh node โ€” use the \`xfer_mesh_to\` tool, not \`xfer_to\`)\n` + `**From**: ${state.identity.name}\n` + `**Request**: ${requirement}\n\n` + `Based on chat context, write a markdown handoff doc ` + `and call \`xfer_mesh_to\` to send it to ${target} over the mesh.\n\n` + `Handoff doc must include:\n` + `- Context summary\n` + `- Problem to solve\n` + `- Specific requirements\n` + `- Relevant files/code references\n` + `- **Suggested skills**: Skills from the agent's repertoire that would help complete the task.\n` + `- **Kind**: pass \`kind\` to \`xfer_mesh_to\` โ€” 'notification' when the target is only being told something, 'task' when it must produce output. A task returns a conclusion; a notification stays silent.\n` + `- **Return address**: from=\`${state.identity.name}\` โ€” the reply target, for a task.\n` + `- Notes\n\n` + `Note: mesh xfer is one-way, no reply wait. The doc is sent inline (no /tmp path on the other machine).`, { deliverAs: "followUp", triggerTurn: true }, ); return; } // โ”€โ”€ /xfer board โ€” async collaboration board โ”€โ”€ if (cmd === "board") { const sub = parts[1]; const author = state.identity?.name ?? "unknown"; const rest = parts.slice(2); if (!sub || sub === "help") { ctx.ui.notify( "๐Ÿ“‹ /xfer board list โ€” list cards\n" + " /xfer board new [title...] โ€” create a card (title optional โ€” the agent names it)\n" + " /xfer board read โ€” inject a card into the conversation\n" + " /xfer board write โ€” the agent composes an entry from your intent\n" + " /xfer board del โ€” delete a single card\n" + " /xfer board clean โ€” delete all cards\n" + " /xfer board open โ€” open the board directory in the file manager", "info", ); return; } if (sub === "list") { const cards = board.board.list(); ctx.ui.notify( cards.length ? `๐Ÿ“‹ Board (${cards.length} card(s)):\n\n` + cards.map(c => ` ${c.id} โ€” ${c.title}\n by ${c.createdBy} ยท ${c.createdAt}`).join("\n") : "๐Ÿ“‹ Board is empty", "info", ); return; } if (sub === "new") { const title = rest.join(" ").trim(); if (title) { const card = board.board.create(title, author); ctx.ui.notify(`๐Ÿ†• ${card.id} โ€” ${card.title}`, "info"); return; } // Two-phase: the agent names the card and writes the first entry. board.board.setPending({ cardId: null, isNew: true }); pi.sendUserMessage( `## Board Card Request\n\n` + `Based on the current conversation, open a new card on the collaboration board: ` + `give it a concise title and write the first entry capturing the topic.\n\n` + `Reply with ONLY a fenced code block tagged \`board-entry\`, nothing else:\n\n` + "```board-entry\n" + "title: \n" + "type: note | finding | question | answer\n" + "---\n" + "\n" + "```", { deliverAs: "followUp", triggerTurn: true }, ); return; } if (sub === "read") { const id = rest[0]; const card = id ? board.board.read(id) : null; if (!card) { ctx.ui.notify(`โŒ Card not found: ${id ?? "(no id given)"} โ€” see /xfer board list`, "error"); return; } pi.sendUserMessage( `## Board Card (read-only injection)\n\n\`${card.id}\` โ€” ${card.title}\n\n` + renderCard(card) + "\nThe human injected this card for your awareness. Do not take action yet โ€” wait for their instruction.", { deliverAs: "followUp", triggerTurn: true }, ); return; } if (sub === "write") { const id = rest[0]; const intent = rest.slice(1).join(" ").trim(); const card = id ? board.board.read(id) : null; if (!card) { ctx.ui.notify(`โŒ Card not found: ${id ?? "(no id given)"} โ€” see /xfer board list`, "error"); return; } if (!intent) { ctx.ui.notify("Usage: /xfer board write โ€” describe what to record", "error"); return; } board.board.setPending({ cardId: card.id, isNew: false }); pi.sendUserMessage( `## Board Entry Request\n\n` + `Card \`${card.id}\` โ€” ${card.title}\n\n` + `Human intent: ${intent}\n\n` + `Based on the conversation and this intent, compose one entry for the card.\n\n` + `Reply with ONLY a fenced code block tagged \`board-entry\`, nothing else:\n\n` + "```board-entry\n" + "type: note | finding | question | answer\n" + "---\n" + "\n" + "```", { deliverAs: "followUp", triggerTurn: true }, ); return; } if (sub === "del") { const id = rest[0]; const card = id ? board.board.read(id) : null; if (!card) { ctx.ui.notify(`โŒ Card not found: ${id ?? "(no id given)"} โ€” see /xfer board list`, "error"); return; } const ok = await ctx.ui.confirm("Delete card", `Delete ${card.id} โ€” ${card.title}?`); if (!ok) return; board.board.del(card.id); ctx.ui.notify(`๐Ÿ—‘๏ธ Deleted ${card.id}`, "info"); return; } if (sub === "clean") { const cards = board.board.list(); if (cards.length === 0) { ctx.ui.notify("๐Ÿ“‹ Board is already empty", "info"); return; } const ok = await ctx.ui.confirm("Clean board", `Delete all ${cards.length} card(s)? This cannot be undone.`); if (!ok) return; const removed = board.board.clean(); ctx.ui.notify(`๐Ÿงน Removed ${removed.length} card(s)`, "info"); return; } if (sub === "open") { const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open"; import("node:child_process").then(({ spawn }) => { spawn(opener, [board.board.dir], { detached: true, stdio: "ignore" }).unref(); }); ctx.ui.notify(`๐Ÿ“‚ ${board.board.dir}`, "info"); return; } ctx.ui.notify(`Usage: /xfer board `, "error"); return; } // โ”€โ”€ /xfer gc (reap zombie peer sockets) โ”€โ”€ if (cmd === "gc") { const report = await collectGarbage(xferDir); if (report.zombies.length === 0) { ctx.ui.notify(`๐Ÿงน No zombie sockets โ€” ${report.alive.length} live peer(s)`, "info"); return; } const lines = report.zombies.map((z) => { const pid = z.pid !== undefined ? `pid ${z.pid}` : "no metadata"; return ` ${z.name} โ€” ${z.reason} (${pid})`; }); ctx.ui.notify( `๐Ÿงน Removed ${report.zombies.length} zombie(s):\n${lines.join("\n")}\n` + `kept ${report.alive.length} live peer(s)`, "info", ); return; } // โ”€โ”€ /xfer doctor (read-only self-check) โ”€โ”€ if (cmd === "doctor") { const report = await runDoctor({ xferDir, myName: state.identity?.name ?? "", brokerManager, adminManager, meshConfigPath, }); ctx.ui.notify(formatDoctorReport(report), doctorSeverity(report)); return; } // โ”€โ”€ /xfer status (listener summary) โ”€โ”€ if (cmd === "status") { ctx.ui.notify(listenerSection(controller).trimStart(), "info"); return; } // โ”€โ”€ /xfer โ”€โ”€ const target = cmd; const requirement = parts.slice(1).join(" "); if (!requirement) { ctx.ui.notify(`Usage: /xfer ${target} `, "error"); return; } if (!state.identity) { ctx.ui.notify("โŒ Xfer is not initialised", "error"); return; } const targetEndpoint = endpointForName(target, xferDir); if (!fs.existsSync(targetEndpoint)) { ctx.ui.notify(`โŒ Peer "${target}" not found โ€” use /xfer list`, "error"); return; } pi.sendUserMessage( `## Handoff Request (one-way)\n\n` + `**Target**: ${target}\n` + `**From**: ${state.identity.name}\n` + `**Request**: ${requirement}\n\n` + `Based on chat context, write a markdown handoff doc ` + `and call \`xfer_to\` to send it to ${target}.\n\n` + `Handoff doc must include:\n` + `- Context summary\n` + `- Problem to solve\n` + `- Specific requirements\n` + `- Relevant files/code references\n` + `- **Suggested skills**: Skills from the agent's repertoire that would help complete the task.\n` + `- **Kind**: pass \`kind\` to \`xfer_to\` โ€” 'notification' when the target is only being told something, 'task' when it must produce output. A task returns a conclusion; a notification stays silent.\n` + `- **Return address**: from=\`${state.identity.name}\` โ€” the reply target, for a task.\n` + `- Notes\n\n` + `Note: xfer is one-way, no reply wait. Returns handoff_id upon delivery.`, { deliverAs: "followUp", triggerTurn: true }, ); }, }); }