#!/usr/bin/env python3 """Best-effort session lifecycle bridge for Claude Code and Codex hooks.""" from __future__ import annotations import importlib.util import importlib.machinery import hashlib import json import os import pathlib import socket import sqlite3 import subprocess import sys import uuid HERE = pathlib.Path(__file__).resolve().parent SPEC = importlib.util.spec_from_loader( "coord_cli", importlib.machinery.SourceFileLoader("coord_cli", str(HERE / "coord")), ) cli = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None SPEC.loader.exec_module(cli) def annotate_tmux(runtime_id: str, conversation_id: str) -> None: pane = os.environ.get("TMUX_PANE") if not pane: return for key, value in ( ("@coord_instance", runtime_id), ("@coord_conversation", conversation_id), ("@coord_task", os.environ.get("FLEET_AI_TASK", "")), ): subprocess.run( ["tmux", "set-option", "-p", "-q", "-t", pane, key, value], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) def subagent_identity(conversation_id: str, agent_id: str) -> tuple[str, str]: """Derive the stable conversation/runtime pair shared by all hook processes.""" conversation = f"{conversation_id}:agent:{agent_id}" runtime = str(uuid.uuid5(uuid.NAMESPACE_URL, f"fleet-agent-coord:{conversation}")) return conversation, runtime def main() -> None: event = json.loads(sys.stdin.read()) conversation = event.get("session_id") or "" if not conversation: return agent_id = str(event.get("agent_id") or "") db = cli.store() name = event.get("hook_event_name") or "" requested = os.environ.get("COORD_INSTANCE_ID", "") if agent_id: conversation, requested = subagent_identity(conversation, agent_id) harness = ( "codex" if os.environ.get("CODEX_THREAD_ID") else "claude-code" ) if name == "SessionEnd": # Without a launcher-provided runtime UUID, conversation identity is # insufficient: a stale process could otherwise end a newer resume. if not requested: return try: db.end_instance(db.resolve_runtime(requested), reason="clean") except KeyError: pass return try: # A direct, unwrapped harness cannot communicate a newly generated UUID # to later hook processes. Reuse its one live conversation attachment; # managed launchers always provide a fresh exact runtime. if not requested: runtime_id = db.resolve_runtime(conversation) db.heartbeat( runtime_id, renew_lease_ttl_seconds=cli.storelib.DEFAULT_LEASE_TTL, ) else: runtime_id = db.register_instance( conversation, runtime_id=requested, harness=harness, host=socket.gethostname(), cwd=str(pathlib.Path(event.get("cwd") or os.getcwd()).resolve()), label=os.environ.get("FLEET_AI_TASK", ""), ) except (KeyError, sqlite3.IntegrityError): if not requested: runtime_id = db.register_instance( conversation, harness=harness, host=socket.gethostname(), cwd=str(pathlib.Path(event.get("cwd") or os.getcwd()).resolve()), label=os.environ.get("FLEET_AI_TASK", ""), ) else: runtime_id = db.resolve_runtime(requested) db.heartbeat( runtime_id, renew_lease_ttl_seconds=cli.storelib.DEFAULT_LEASE_TTL, ) annotate_tmux(runtime_id, conversation) peers = [item for item in db.live_instances() if item["runtime_id"] != runtime_id] leases = [item for item in db.active_leases() if item["runtime_id"] != runtime_id] pending = db.inbox(runtime_id, limit=5, mark_delivered=True) if not peers and not pending: return lines: list[str] = [] if peers: lines.append(f"Live peer agents: {len(peers)}") for peer in peers: lines.append( f" · {cli.runtime_handle(peer['runtime_id'])} " f"{peer['harness']} task={cli.peer_label(peer)}" ) if leases: lines.append("Active peer resource leases:") for lease in leases: lines.append( f" · {lease['path']} [{lease['mode']}] by " f"{cli.runtime_handle(lease['runtime_id'])} — {lease['purpose']}" ) if pending: lines.append("Pending durable peer messages:") for message in pending: sender = cli.runtime_handle(message.sender_runtime_id) if message.state == "delivered": digest = hashlib.sha256(message.body.encode("utf-8")).hexdigest()[:10] lines.append( f" · [{message.message_id}] {sender} pending again " f"({message.kind}, sha256:{digest}); `coord inbox --peek`" ) else: lines.append( f" · [{message.message_id}] {sender} " f"({message.kind}): {message.body}" ) lines.append( "Use an isolated managed clone for writable work; claim shared integration, " "push, activation, and deployment resources before those operations." ) print( json.dumps( { "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "\n".join(lines), } } ) ) if __name__ == "__main__": try: main() except Exception as exc: # lifecycle hooks must fail open print(f"coord-session: failing open: {exc}", file=sys.stderr) raise SystemExit(0)