Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
88fe1e8
Bridge seam: route FO-event egress through the per-host adapter patte…
gcko Jun 30, 2026
baa53a7
Merge updated bridge-seam-inbox-events (main 0.23.0, contract v2) int…
gcko Jun 30, 2026
1f83c08
Add harness-neutral bridge egress producers
gcko Jul 1, 2026
6792187
Normalize Pi bridge egress lifecycle events
gcko Jul 1, 2026
4932c89
Document Bridge reply loop contract
gcko Jul 1, 2026
dc2e884
Clarify unknown broadcast target routing
gcko Jul 1, 2026
302157e
Address Copilot review on bridge egress
gcko Jul 1, 2026
58c65f1
Keep Codex plugin-dir out of launch argv
gcko Jul 2, 2026
0682d23
fix(codex): install dev plugin hooks for bridge egress
gcko Jul 2, 2026
8a29481
feat(bridge): wake Codex FO from inbox intents
gcko Jul 2, 2026
e46903f
feat(bridge): emit FO permission alerts
gcko Jul 2, 2026
79c38c1
fix(bridge): make permission alerts non-blocking
gcko Jul 2, 2026
1b16bbe
fix(bridge): wake codex from durable delivery state
gcko Jul 2, 2026
eb1fba3
fix(bridge): harden wake durability and correct egress docs
gcko Jul 2, 2026
0f1cd3e
feat(bridge): packaged inbox drain + Claude durable wake
gcko Jul 3, 2026
0d96a40
fix(review): address DRC-3798 self-review findings
gcko Jul 3, 2026
1ea0966
fix(bridge): anchor egress at nearest git root, not raw cwd
gcko Jul 3, 2026
641920e
feat(bridge): loud ack correlator guard + drain auto-acting ack
gcko Jul 3, 2026
c8eca69
feat(bridge): add fo-initiate writer + verb for FO-authored gates
gcko Jul 3, 2026
a9d35b4
fix(bridge): correct present-gate emit doc and prune resolved gates
gcko Jul 3, 2026
ec5a17a
Merge updated bridge-seam-inbox-events (PR 435 + main) into DRC-3798 …
gcko Jul 8, 2026
49073da
feat(bridge): bound fo-replies.jsonl with a cursor-floor retention wi…
gcko Jul 8, 2026
ba6444b
test(bridge): cover truncateReplies floor+window retention rule
gcko Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"automation"
],
"skills": "./skills/",
"hooks": "./hooks/codex-hooks.json",
"requires-contract": ">=3,<4",
"interface": {
"displayName": "Spacedock",
Expand Down
111 changes: 110 additions & 1 deletion .pi/extensions/spacedock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Spacedock pi extension — parent-session skill discovery.
// Spacedock pi extension — parent-session skill discovery and Bridge egress.
//
// Once `spacedock install --host pi` (or the dev `pi install ./local/path`)
// registers the Spacedock package in ~/.pi/agent/settings.json `packages`, the
Expand All @@ -12,10 +12,12 @@
// the package-root scan reading `package.json` `pi.skills` — no cwd dependency.

import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
import * as path from "node:path";

export default function registerSpacedockExtension(pi: {
on(event: "resources_discover", handler: (event: { type: "resources_discover"; cwd: string; reason: string }) => { skillPaths?: string[] } | void): void;
on(event: string, handler: (event: Record<string, unknown>, ctx?: Record<string, unknown>) => void | Promise<void>): void;
}): void {
pi.on("resources_discover", () => {
const extDir = path.dirname(fileURLToPath(import.meta.url));
Expand All @@ -24,4 +26,111 @@ export default function registerSpacedockExtension(pi: {
const skillsDir = path.join(repoRoot, "skills");
return { skillPaths: [skillsDir] };
});

const lifecycleEvents = [
"session_start",
"session_shutdown",
"agent_start",
"agent_end",
"turn_start",
"turn_end",
"tool_execution_start",
"tool_execution_end",
"tool_call",
"tool_result",
];
for (const eventName of lifecycleEvents) {
pi.on(eventName, (event, ctx) => {
void emitBridgeEgress(eventName, event, ctx).catch(() => {});
});
}
}

async function emitBridgeEgress(eventName: string, event: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<void> {
const cwd = stringValue(event.cwd) || contextCwd(ctx) || process.cwd();
const sessionFile = contextSessionFile(ctx);
const payload = {
event: eventName,
cwd,
session_file: sessionFile,
session_id: sessionIdFromSessionFile(sessionFile),
agent_id: "",
agent_type: "",
detail: eventDetail(eventName, event),
};

await invokeSpacedockEgress(payload, cwd);
}

function invokeSpacedockEgress(payload: Record<string, unknown>, cwd: string): Promise<void> {
const bin = process.env.SPACEDOCK_BIN || "spacedock";
return new Promise((resolve) => {
const child = spawn(bin, ["bridge", "egress", "emit", "--host", "pi"], {
cwd,
stdio: ["pipe", "ignore", "ignore"],
});
child.on("error", () => resolve());
child.on("close", () => resolve());
child.stdin.on("error", () => resolve());
try {
child.stdin.end(JSON.stringify(payload));
} catch {
resolve();
}
});
}

function eventDetail(eventName: string, event: Record<string, unknown>): Record<string, unknown> {
const detail: Record<string, unknown> = { source: "pi" };
const tool = stringValue(event.toolName);
if (tool) detail.tool = tool;
if (eventName === "session_start" || eventName === "session_shutdown") {
const reason = stringValue(event.reason);
if (reason) detail.reason = reason;
}
const toolCallId = stringValue(event.toolCallId);
if (toolCallId) detail.tool_call_id = toolCallId;
return detail;
}

function contextCwd(ctx?: Record<string, unknown>): string {
const opts = getSystemPromptOptions(ctx);
if (!opts) return "";
return stringValue(opts.cwd);
}

function contextSessionFile(ctx?: Record<string, unknown>): string {
const manager = objectValue(ctx?.sessionManager);
const getSessionFile = manager?.getSessionFile;
if (typeof getSessionFile !== "function") return "";
try {
return stringValue(getSessionFile.call(manager));
} catch {
return "";
}
}

function getSystemPromptOptions(ctx?: Record<string, unknown>): Record<string, unknown> | undefined {
const fn = ctx?.getSystemPromptOptions;
if (typeof fn !== "function") return undefined;
try {
return objectValue(fn.call(ctx));
} catch {
return undefined;
}
}

function sessionIdFromSessionFile(sessionFile: string): string {
if (!sessionFile) return "";
const base = path.basename(sessionFile);
const ext = path.extname(base);
return ext ? base.slice(0, -ext.length) : base;
}

function objectValue(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" ? value as Record<string, unknown> : undefined;
}

function stringValue(value: unknown): string {
return typeof value === "string" ? value : "";
}
Loading