@automagik/genie 4.260330.22 → 4.260330.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "genie",
13
- "version": "4.260330.22",
13
+ "version": "4.260330.24",
14
14
  "source": "./plugins/genie",
15
15
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
16
16
  }
package/dist/genie.js CHANGED
@@ -1362,19 +1362,18 @@ Define your agent's mission here. What is their primary goal? What do they own?
1362
1362
  `,failedCount++,deps.log({timestamp:now.toISOString(),level:"warn",event:"orphan_run_failed",run_id:run.id,worker_id:run.worker_id,dead_heartbeats:threshold})}if(failedCount>0)deps.log({timestamp:now.toISOString(),level:"info",event:"orphan_reconciliation_completed",failed_count:failedCount});return failedCount}async function collectMachineSnapshot(deps){let sql=await deps.getConnection(),now=deps.now(),snapshotId=deps.generateId(),workers=await deps.listWorkers(),activeWorkers=workers.filter((w)=>!["done","error","suspended"].includes(w.state)).length,teams=new Set(workers.filter((w)=>w.team).map((w)=>w.team)),tmuxSessions=await deps.countTmuxSessions(),cpuPercent=null,memoryMb=null;try{let mem=process.memoryUsage();memoryMb=Math.round(mem.rss/1024/1024)}catch{}try{let cpus=(await import("os")).cpus();if(cpus.length>0){let total=cpus.reduce((acc,cpu)=>{let t=Object.values(cpu.times).reduce((a,b2)=>a+b2,0);return acc+t-cpu.times.idle},0),totalAll=cpus.reduce((acc,cpu)=>acc+Object.values(cpu.times).reduce((a,b2)=>a+b2,0),0);cpuPercent=totalAll>0?Math.round(total/totalAll*100):null}}catch{}await sql`
1363
1363
  INSERT INTO machine_snapshots (id, active_workers, active_teams, tmux_sessions, cpu_percent, memory_mb, created_at)
1364
1364
  VALUES (${snapshotId}, ${activeWorkers}, ${teams.size}, ${tmuxSessions}, ${cpuPercent}, ${memoryMb}, ${now})
1365
- `,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id),repoPath=worker.repoPath??process.cwd();if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"},repoPath);else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"},repoPath),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"},repoPath)}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"},prev.repoPath??process.cwd()),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,eventRouterHandle=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;eventRouterHandle?.stop().catch(()=>{}),eventRouterHandle=null,Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{}),Promise.resolve().then(() => (init_db(),exports_db)).then(({getLockfilePath:getLockfilePath2})=>{try{__require("fs").unlinkSync(getLockfilePath2())}catch{}}).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}};async function setupListenNotify(d,onTrigger){try{let sql=await d.getConnection();return await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await onTrigger()}),d.log({timestamp:d.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"}),sql}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"listen_failed",error:message}),null}}function startOrphanTimer(d,cfg){return setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(d,cfg)}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},cfg.orphanCheckIntervalMs)}async function startEventRouterSafe(d){try{let handle=await startEventRouter();return d.log({timestamp:d.now().toISOString(),level:"info",event:"event_router_started"}),handle}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"event_router_start_failed",error:message}),null}}async function initSessionCapture(d,cfg){try{let captureSql=await d.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));d.log({timestamp:d.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"});let timer2=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},cfg.heartbeatIntervalMs);return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),timer2}return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),null}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message}),null}}async function runHeartbeat(d){if(!running2)return;try{await collectHeartbeats(d),await collectMachineSnapshot(d),await emitWorkerEvents(d);try{let retSql=await d.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}}let done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}listenConnection=await setupListenNotify(deps,processTriggers),heartbeatTimer=setInterval(()=>runHeartbeat(deps),config.heartbeatIntervalMs),orphanTimer=startOrphanTimer(deps,config),inboxWatcherHandle=startInboxWatcherIfEnabled(deps),eventRouterHandle=await startEventRouterSafe(deps),captureFallbackTimer=await initSessionCapture(deps,config),await processTriggers();while(running2){if(await new Promise((resolve5)=>{pollResolve=resolve5,pollTimeout=setTimeout(resolve5,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_event_router();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_serve={};__export(exports_serve,{registerServeCommands:()=>registerServeCommands,isTuiSessionReady:()=>isTuiSessionReady,isServeRunning:()=>isServeRunning,ensureTuiSession:()=>ensureTuiSession,autoStartServe:()=>autoStartServe});import{execSync as execSync6,spawn as spawn3}from"child_process";import{existsSync as existsSync24,mkdirSync as mkdirSync8,readFileSync as readFileSync11,unlinkSync as unlinkSync5,writeFileSync as writeFileSync8}from"fs";import{homedir as homedir20}from"os";import{join as join31}from"path";function genieHome(){return process.env.GENIE_HOME??join31(homedir20(),".genie")}function servePidPath(){return join31(genieHome(),"serve.pid")}function genieTmuxConf(){return[join31(genieHome(),"tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function tuiTmuxConf(){return[join31(genieHome(),"tui-tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function readServePid(){let path2=servePidPath();if(!existsSync24(path2))return null;let raw=readFileSync11(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function writeServePid(pid){mkdirSync8(genieHome(),{recursive:!0}),writeFileSync8(servePidPath(),String(pid),"utf-8")}function removeServePid(){let path2=servePidPath();if(existsSync24(path2))try{unlinkSync5(path2)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function tmuxCmd(socket,conf,subcmd){return`tmux -L ${socket} -f ${conf} ${subcmd}`}function genieTmux(subcmd){return tmuxCmd(GENIE_SOCKET,genieTmuxConf(),subcmd)}function tuiTmux(subcmd){return tmuxCmd(TUI_SOCKET,tuiTmuxConf(),subcmd)}function isTmuxServerRunning(socket,conf){try{return execSync6(tmuxCmd(socket,conf,"list-sessions"),{stdio:"ignore"}),!0}catch{return!1}}function applyTuiStyle(){let cmds=[`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,`set-option -t ${TUI_SESSION} mouse off`,`set-option -t ${TUI_SESSION} status off`,`set-option -t ${TUI_SESSION} pane-border-status off`];for(let cmd of cmds)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function setupTuiKeybindings(){let bindings=[`bind-key -T ${KEY_TABLE} Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0" \\; switch-client -T ${KEY_TABLE}`,`bind-key -T ${KEY_TABLE} C-b if-shell "[ $(tmux -L ${TUI_SOCKET} display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}" \\; switch-client -T ${KEY_TABLE}`,`bind-key -T ${KEY_TABLE} C-q detach-client`,`bind-key -T ${KEY_TABLE} 'C-\\\\' detach-client`,`set-hook -t ${TUI_SESSION} client-session-changed "switch-client -T ${KEY_TABLE}"`];for(let cmd of bindings)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function startTuiTmuxServer(){try{execSync6(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"});let panes2=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
1366
- `);return{leftPane:panes2[0],rightPane:panes2[1]||panes2[0]}}catch{}let cols=120;execSync6(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${40} -e GENIE_TUI_PANE=left`),{stdio:"ignore"}),execSync6(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols-NAV_WIDTH-1}`),{stdio:"ignore"});let panes=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
1365
+ `,deps.log({timestamp:now.toISOString(),level:"debug",event:"machine_snapshot",active_workers:activeWorkers,active_teams:teams.size,tmux_sessions:tmuxSessions,cpu_percent:cpuPercent,memory_mb:memoryMb})}async function emitWorkerEvents(deps){let workers=await deps.listWorkers(),now=deps.now().toISOString(),currentIds=new Set;for(let worker of workers){currentIds.add(worker.id);let prev=previousWorkerStates.get(worker.id),repoPath=worker.repoPath??process.cwd();if(!prev)await deps.publishEvent(`genie.agent.${worker.id}.spawned`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} spawned`,data:{state:worker.state},source:"registry"},repoPath);else if(prev.state!==worker.state){if(await deps.publishEvent(`genie.agent.${worker.id}.state`,{timestamp:now,kind:"state",agent:worker.id,team:worker.team,text:`Agent ${worker.id} state: ${prev.state} \u2192 ${worker.state}`,data:{previousState:prev.state,state:worker.state},source:"registry"},repoPath),worker.state==="done"&&worker.wishSlug&&worker.groupNumber!=null)await deps.publishEvent(`genie.wish.${worker.wishSlug}.group.${worker.groupNumber}.done`,{timestamp:now,kind:"system",agent:worker.id,team:worker.team,text:`Wish ${worker.wishSlug} group ${worker.groupNumber} completed by ${worker.id}`,data:{wishSlug:worker.wishSlug,groupNumber:worker.groupNumber},source:"registry"},repoPath)}previousWorkerStates.set(worker.id,{...worker})}for(let[id,prev]of previousWorkerStates)if(!currentIds.has(id))await deps.publishEvent(`genie.agent.${id}.killed`,{timestamp:now,kind:"state",agent:id,team:prev.team,text:`Agent ${id} killed`,data:{lastState:prev.state},source:"registry"},prev.repoPath??process.cwd()),previousWorkerStates.delete(id)}function _resetWorkerStatesForTesting(){previousWorkerStates.clear()}function startInboxWatcherIfEnabled(deps){let pollMs=getInboxPollIntervalMs();if(pollMs===0)return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_disabled"}),null;return deps.log({timestamp:deps.now().toISOString(),level:"info",event:"inbox_watcher_started",poll_interval_ms:pollMs}),startInboxWatcher()}function startDaemon(configOverrides,depsOverrides){let config=resolveConfig(configOverrides),deps={...createDefaultDeps(),...depsOverrides},daemonId=deps.generateId(),running2=!0,pollTimeout=null,pollResolve=null,listenConnection=null,heartbeatTimer=null,orphanTimer=null,inboxWatcherHandle=null,captureFallbackTimer=null,eventRouterHandle=null,stop=()=>{if(running2=!1,pollTimeout)clearTimeout(pollTimeout),pollTimeout=null;if(pollResolve)pollResolve(),pollResolve=null;if(heartbeatTimer)clearInterval(heartbeatTimer),heartbeatTimer=null;if(orphanTimer)clearInterval(orphanTimer),orphanTimer=null;if(inboxWatcherHandle)stopInboxWatcher(inboxWatcherHandle),inboxWatcherHandle=null;if(listenConnection)listenConnection.end().catch(()=>{}),listenConnection=null;if(captureFallbackTimer)clearInterval(captureFallbackTimer),captureFallbackTimer=null;eventRouterHandle?.stop().catch(()=>{}),eventRouterHandle=null,Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)).then((m)=>m.stopFilewatch()).catch(()=>{}),Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill)).then((m)=>m.stopBackfill()).catch(()=>{}),Promise.resolve().then(() => (init_db(),exports_db)).then(({getLockfilePath:getLockfilePath2})=>{try{__require("fs").unlinkSync(getLockfilePath2())}catch{}}).catch(()=>{})},processTriggers=async()=>{try{let claimed=await claimDueTriggers(deps,config,daemonId);if(claimed.length===0)return;if(claimed.length>config.jitterThreshold){let jitterMs=deps.jitter(config.maxJitterMs);deps.log({timestamp:deps.now().toISOString(),level:"info",event:"jitter_applied",count:claimed.length,jitter_ms:jitterMs}),await deps.sleep(jitterMs)}for(let trigger of claimed){if(!running2)break;await fireTrigger(deps,trigger,daemonId)}}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"process_cycle_error",error:message})}};async function setupListenNotify(d,onTrigger){try{let sql=await d.getConnection();return await sql.listen("genie_trigger_due",async()=>{if(!running2)return;await onTrigger()}),d.log({timestamp:d.now().toISOString(),level:"info",event:"listen_started",channel:"genie_trigger_due"}),sql}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"listen_failed",error:message}),null}}function startOrphanTimer(d,cfg){return setInterval(async()=>{if(!running2)return;try{await reconcileOrphans(d,cfg)}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"orphan_reconciliation_error",error:message})}},cfg.orphanCheckIntervalMs)}async function startEventRouterSafe(d){try{let handle=await startEventRouter();return d.log({timestamp:d.now().toISOString(),level:"info",event:"event_router_started"}),handle}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"event_router_start_failed",error:message}),null}}async function initSessionCapture(d,cfg){try{let captureSql=await d.getConnection(),{startFilewatch:startFilewatch2}=await Promise.resolve().then(() => (init_session_filewatch(),exports_session_filewatch)),{startBackfill:startBackfill2}=await Promise.resolve().then(() => (init_session_backfill(),exports_session_backfill));if(!await startFilewatch2(captureSql)){let{ingestFileFull:ingestFileFull2,discoverAllJsonlFiles:discoverAllJsonlFiles2,buildWorkerMap:buildWorkerMap2}=await Promise.resolve().then(() => (init_session_capture(),exports_session_capture));d.log({timestamp:d.now().toISOString(),level:"warn",event:"filewatch_failed_fallback_polling"});let timer2=setInterval(async()=>{if(!running2)return;try{let files=await discoverAllJsonlFiles2(),workerMap=await buildWorkerMap2(captureSql);for(let f of files)await ingestFileFull2(captureSql,f.sessionId,f.jsonlPath,f.projectPath,0,{parentSessionId:f.parentSessionId,isSubagent:f.isSubagent,workerMap})}catch{}},cfg.heartbeatIntervalMs);return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),timer2}return startBackfill2(captureSql).catch((err)=>{let msg=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"backfill_error",error:msg})}),null}catch(err){let message=err instanceof Error?err.message:String(err);return d.log({timestamp:d.now().toISOString(),level:"warn",event:"session_capture_init_failed",error:message}),null}}async function runHeartbeat(d){if(!running2)return;try{await collectHeartbeats(d),await collectMachineSnapshot(d),await emitWorkerEvents(d);try{let retSql=await d.getConnection();await retSql`DELETE FROM heartbeats WHERE created_at < now() - interval '7 days'`,await retSql`DELETE FROM machine_snapshots WHERE created_at < now() - interval '30 days'`,await retSql`DELETE FROM audit_events WHERE entity_type LIKE 'otel_%' AND created_at < now() - interval '30 days'`}catch{}}catch(err){let message=err instanceof Error?err.message:String(err);d.log({timestamp:d.now().toISOString(),level:"error",event:"heartbeat_error",error:message})}}let done=(async()=>{deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_started",daemon_id:daemonId,max_concurrent:config.maxConcurrent,poll_interval_ms:config.pollIntervalMs});try{await recoverOnStartup(deps,daemonId,config)}catch(err){let message=err instanceof Error?err.message:String(err);deps.log({timestamp:deps.now().toISOString(),level:"error",event:"recovery_error",error:message})}listenConnection=await setupListenNotify(deps,processTriggers),heartbeatTimer=setInterval(()=>runHeartbeat(deps),config.heartbeatIntervalMs),orphanTimer=startOrphanTimer(deps,config),inboxWatcherHandle=startInboxWatcherIfEnabled(deps),eventRouterHandle=await startEventRouterSafe(deps),captureFallbackTimer=await initSessionCapture(deps,config),await processTriggers();while(running2){if(await new Promise((resolve5)=>{pollResolve=resolve5,pollTimeout=setTimeout(resolve5,config.pollIntervalMs)}),pollResolve=null,!running2)break;await processTriggers()}deps.log({timestamp:deps.now().toISOString(),level:"info",event:"daemon_stopped",daemon_id:daemonId})})();return{stop,done,daemonId}}var RESUME_COOLDOWN_MS=60000,DEFAULT_MAX_RESUME_ATTEMPTS=3,previousWorkerStates;var init_scheduler_daemon=__esm(()=>{init_cron();init_event_router();init_inbox_watcher();init_run_spec();previousWorkerStates=new Map});var exports_serve={};__export(exports_serve,{registerServeCommands:()=>registerServeCommands,isTuiSessionReady:()=>isTuiSessionReady,isServeRunning:()=>isServeRunning,ensureTuiSession:()=>ensureTuiSession,autoStartServe:()=>autoStartServe});import{execSync as execSync6,spawn as spawn3}from"child_process";import{existsSync as existsSync24,mkdirSync as mkdirSync8,readFileSync as readFileSync11,unlinkSync as unlinkSync5,writeFileSync as writeFileSync8}from"fs";import{homedir as homedir20}from"os";import{join as join31}from"path";function genieHome(){return process.env.GENIE_HOME??join31(homedir20(),".genie")}function servePidPath(){return join31(genieHome(),"serve.pid")}function genieTmuxConf(){return[join31(genieHome(),"tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function readServePid(){let path2=servePidPath();if(!existsSync24(path2))return null;let raw=readFileSync11(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function writeServePid(pid){mkdirSync8(genieHome(),{recursive:!0}),writeFileSync8(servePidPath(),String(pid),"utf-8")}function removeServePid(){let path2=servePidPath();if(existsSync24(path2))try{unlinkSync5(path2)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function genieTmux(subcmd){return`tmux -L ${GENIE_SOCKET} -f ${genieTmuxConf()} ${subcmd}`}function tuiTmuxConf(){return[join31(genieHome(),"tui-tmux.conf")].find((p)=>existsSync24(p))??"/dev/null"}function tuiTmux(subcmd){return`tmux -L genie-tui -f ${tuiTmuxConf()} ${subcmd}`}function isGenieTmuxRunning(){try{return execSync6(genieTmux("list-sessions"),{stdio:"ignore"}),!0}catch{return!1}}function applyTuiStyle(){let cmds=[`set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,`set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,`set-option -t ${TUI_SESSION} mouse on`,`set-option -t ${TUI_SESSION} status off`,`set-option -t ${TUI_SESSION} pane-border-status off`];for(let cmd of cmds)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function setupTuiKeybindings(){let bindings=[`bind-key -T root Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0"`,`bind-key -T root C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}"`,`bind-key -T root C-t send-keys -t ${TUI_SESSION}:0.1 C-b c`,"bind-key -T root C-q detach-client"];for(let cmd of bindings)try{execSync6(tuiTmux(cmd),{stdio:"ignore"})}catch{}}function startTuiTmuxServer(){try{execSync6(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"});let panes2=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
1366
+ `);return{leftPane:panes2[0],rightPane:panes2[1]||panes2[0]}}catch{}let cols=120;execSync6(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${40}`),{stdio:"ignore"}),execSync6(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols-NAV_WIDTH-1}`),{stdio:"ignore"});let panes=execSync6(tuiTmux(`list-panes -t ${TUI_SESSION}:0 -F '#{pane_id}'`),{encoding:"utf-8"}).trim().split(`
1367
1367
  `);applyTuiStyle(),setupTuiKeybindings();try{execSync6(tuiTmux(`select-pane -t ${panes[0]}`),{stdio:"ignore"})}catch{}return{leftPane:panes[0],rightPane:panes[1]||panes[0]}}function sendTuiLaunchScript(leftPane,rightPane,workspaceRoot){let home=genieHome(),bunPath=process.execPath||"bun",genieBin=process.argv[1]||"genie",scriptPath=join31(home,"tui-launch.sh"),envVars=["GENIE_TUI_PANE=left",`GENIE_TUI_RIGHT=${rightPane}`];if(workspaceRoot)envVars.push(`GENIE_TUI_WORKSPACE=${workspaceRoot}`);let content=`#!/bin/sh
1368
1368
  export ${envVars.join(`
1369
1369
  export `)}
1370
1370
  exec ${bunPath} ${genieBin}
1371
- `;writeFileSync8(scriptPath,content,{mode:493});try{execSync6(tuiTmux(`send-keys -t '${leftPane}' '${scriptPath}' Enter`),{stdio:"ignore"})}catch{}}function killTmuxServer(socket,conf){try{execSync6(tmuxCmd(socket,conf,"kill-server"),{stdio:"ignore"})}catch{}}function listAgentSessions(){try{return execSync6(genieTmux("list-sessions -F '#{session_name}'"),{encoding:"utf-8"}).trim().split(`
1372
- `).filter(Boolean)}catch{return[]}}function isServeRunning(){let pid=readServePid();return pid!==null&&isProcessAlive(pid)}async function autoStartServe(){if(isServeRunning())return;let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",{spawn:spawnChild}=await import("child_process");spawnChild(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}}).unref();let deadline=Date.now()+15000;while(Date.now()<deadline)if(await new Promise((resolve5)=>setTimeout(resolve5,500)),isServeRunning()&&isTuiSessionReady())return;if(!isServeRunning())throw Error("genie serve failed to start within 15s. Run `genie serve` manually.")}function isTuiSessionReady(){try{return execSync6(tmuxCmd(TUI_SOCKET,tuiTmuxConf(),`has-session -t ${TUI_SESSION}`),{stdio:"ignore"}),!0}catch{return!1}}function ensureTuiSession(workspaceRoot){if(isTuiSessionReady())return;let{leftPane,rightPane}=startTuiTmuxServer();sendTuiLaunchScript(leftPane,rightPane,workspaceRoot)}async function startAgentSync(){try{let{findWorkspace:findWorkspace2}=(init_workspace(),__toCommonJS(exports_workspace)),ws=findWorkspace2();if(!ws)return null;let{syncAgentDirectory:syncAgentDirectory2,watchAgentDirectory:watchAgentDirectory2}=await Promise.resolve().then(() => (init_agent_sync(),exports_agent_sync)),syncResult=await syncAgentDirectory2(ws.root);if(syncResult.registered.length+syncResult.updated.length>0)console.log(` Agent sync: ${syncResult.registered.length} registered, ${syncResult.updated.length} updated`);let watcher2=watchAgentDirectory2(ws.root,{onSync:(name,action)=>{console.log(` [agent-watcher] ${name}: ${action}`)}});if(watcher2)console.log(" Agent watcher started (watching agents/ directory)");return watcher2}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(` Agent sync failed: ${msg}`),null}}async function startForeground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();process.env.GENIE_IS_DAEMON="1",writeServePid(process.pid),console.log(`genie serve starting (PID ${process.pid})`),console.log(" Starting pgserve...");try{let{ensurePgserve:ensurePgserve2}=await Promise.resolve().then(() => (init_db(),exports_db)),port=await ensurePgserve2();console.log(` pgserve ready on port ${port}`)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(` pgserve failed: ${msg}`)}let sessions=listAgentSessions();if(sessions.length>0)console.log(` Agent server (-L ${GENIE_SOCKET}): ${sessions.length} sessions`);else console.log(` Agent server (-L ${GENIE_SOCKET}): no sessions yet (created on first spawn)`);handles.agentWatcher=await startAgentSync(),console.log(` Starting tmux -L ${TUI_SOCKET} server...`);let{leftPane,rightPane}=startTuiTmuxServer(),ws=(()=>{try{let{findWorkspace:findWorkspace2}=(init_workspace(),__toCommonJS(exports_workspace));return findWorkspace2()}catch{return null}})();sendTuiLaunchScript(leftPane,rightPane,ws?.root),console.log(" TUI server ready (session: genie-tui)"),console.log(" Starting scheduler daemon...");try{let{startDaemon:startDaemon2}=await Promise.resolve().then(() => (init_scheduler_daemon(),exports_scheduler_daemon));handles.schedulerHandle=startDaemon2(),console.log(" Scheduler started (includes event-router + inbox-watcher)")}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(` Scheduler failed: ${msg}`)}console.log(`
1371
+ `;writeFileSync8(scriptPath,content,{mode:493});try{execSync6(tuiTmux(`send-keys -t '${leftPane}' '${scriptPath}' Enter`),{stdio:"ignore"})}catch{}}function killTuiSession(){try{execSync6(tuiTmux("kill-server"),{stdio:"ignore"})}catch{}}function listAgentSessions(){try{return execSync6(genieTmux("list-sessions -F '#{session_name}'"),{encoding:"utf-8"}).trim().split(`
1372
+ `).filter(Boolean)}catch{return[]}}function isServeRunning(){let pid=readServePid();return pid!==null&&isProcessAlive(pid)}async function autoStartServe(){if(isServeRunning())return;let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",{spawn:spawnChild}=await import("child_process");spawnChild(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}}).unref();let deadline=Date.now()+15000;while(Date.now()<deadline)if(await new Promise((resolve5)=>setTimeout(resolve5,500)),isServeRunning()&&isTuiSessionReady())return;if(!isServeRunning())throw Error("genie serve failed to start within 15s. Run `genie serve` manually.")}function isTuiSessionReady(){try{return execSync6(tuiTmux(`has-session -t ${TUI_SESSION}`),{stdio:"ignore"}),!0}catch{return!1}}function ensureTuiSession(workspaceRoot){if(isTuiSessionReady())return;let{leftPane,rightPane}=startTuiTmuxServer();sendTuiLaunchScript(leftPane,rightPane,workspaceRoot)}async function startAgentSync(){try{let{findWorkspace:findWorkspace2}=(init_workspace(),__toCommonJS(exports_workspace)),ws=findWorkspace2();if(!ws)return null;let{syncAgentDirectory:syncAgentDirectory2,watchAgentDirectory:watchAgentDirectory2}=await Promise.resolve().then(() => (init_agent_sync(),exports_agent_sync)),syncResult=await syncAgentDirectory2(ws.root);if(syncResult.registered.length+syncResult.updated.length>0)console.log(` Agent sync: ${syncResult.registered.length} registered, ${syncResult.updated.length} updated`);let watcher2=watchAgentDirectory2(ws.root,{onSync:(name,action)=>{console.log(` [agent-watcher] ${name}: ${action}`)}});if(watcher2)console.log(" Agent watcher started (watching agents/ directory)");return watcher2}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(` Agent sync failed: ${msg}`),null}}async function startForeground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();process.env.GENIE_IS_DAEMON="1",writeServePid(process.pid),console.log(`genie serve starting (PID ${process.pid})`),console.log(" Starting pgserve...");try{let{ensurePgserve:ensurePgserve2}=await Promise.resolve().then(() => (init_db(),exports_db)),port=await ensurePgserve2();console.log(` pgserve ready on port ${port}`)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(` pgserve failed: ${msg}`)}let sessions=listAgentSessions();if(sessions.length>0)console.log(` Agent server (-L ${GENIE_SOCKET}): ${sessions.length} sessions`);else console.log(` Agent server (-L ${GENIE_SOCKET}): no sessions yet (created on first spawn)`);handles.agentWatcher=await startAgentSync(),console.log(" Setting up TUI session...");let{leftPane,rightPane}=startTuiTmuxServer(),ws=(()=>{try{let{findWorkspace:findWorkspace2}=(init_workspace(),__toCommonJS(exports_workspace));return findWorkspace2()}catch{return null}})();sendTuiLaunchScript(leftPane,rightPane,ws?.root),console.log(" TUI server ready (session: genie-tui)"),console.log(" Starting scheduler daemon...");try{let{startDaemon:startDaemon2}=await Promise.resolve().then(() => (init_scheduler_daemon(),exports_scheduler_daemon));handles.schedulerHandle=startDaemon2(),console.log(" Scheduler started (includes event-router + inbox-watcher)")}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(` Scheduler failed: ${msg}`)}console.log(`
1373
1373
  genie serve is running. Press Ctrl+C to stop.`);let shutdown2=()=>{console.log(`
1374
- Shutting down genie serve...`),handles.agentWatcher?.close(),handles.schedulerHandle?.stop(),killTmuxServer(TUI_SOCKET,tuiTmuxConf()),removeServePid(),console.log("genie serve stopped.")};if(process.on("SIGTERM",()=>{shutdown2(),process.exit(143)}),process.on("SIGINT",()=>{shutdown2(),process.exit(130)}),handles.schedulerHandle)await handles.schedulerHandle.done;else await new Promise(()=>{});removeServePid()}async function startBackground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",child=spawn3(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}});if(child.unref(),child.pid)if(await new Promise((resolve5)=>setTimeout(resolve5,1000)),isProcessAlive(child.pid))console.log(`genie serve started (PID ${child.pid})`);else console.error("Error: genie serve exited immediately."),process.exit(1);else console.error("Error: failed to spawn genie serve"),process.exit(1)}async function stopServe(){let pid=readServePid();if(!pid){console.log("genie serve is not running (no PID file).");return}if(!isProcessAlive(pid)){console.log(`Stale PID file (PID ${pid} not running). Cleaning up.`),removeServePid(),killTmuxServer(TUI_SOCKET,tuiTmuxConf());return}console.log(`Stopping genie serve (PID ${pid})...`);try{process.kill(-pid,"SIGTERM")}catch{try{process.kill(pid,"SIGTERM")}catch{}}let deadline=Date.now()+1e4;while(Date.now()<deadline&&isProcessAlive(pid))await new Promise((resolve5)=>setTimeout(resolve5,250));if(isProcessAlive(pid)){console.log("Did not stop within 10s. Sending SIGKILL.");try{process.kill(-pid,"SIGKILL")}catch{try{process.kill(pid,"SIGKILL")}catch{}}}killTmuxServer(TUI_SOCKET,tuiTmuxConf()),removeServePid(),console.log("genie serve stopped.")}async function printPgserveStatus(){try{let{isAvailable:isAvailable2,getActivePort:getActivePort2}=await Promise.resolve().then(() => (init_db(),exports_db)),dbOk=await isAvailable2();console.log(` pgserve: ${dbOk?`healthy (port ${getActivePort2()})`:"unreachable"}`)}catch{console.log(" pgserve: unavailable")}}function printTmuxStatus(){let agentRunning=isTmuxServerRunning(GENIE_SOCKET,genieTmuxConf()),sessions=agentRunning?listAgentSessions():[];if(console.log(` tmux -L ${GENIE_SOCKET}: ${agentRunning?`running (${sessions.length} sessions)`:"stopped"}`),sessions.length>0)console.log(` ${sessions.join(", ")}`);let tuiRunning=isTmuxServerRunning(TUI_SOCKET,tuiTmuxConf());console.log(` tmux -L ${TUI_SOCKET}: ${tuiRunning?"running":"stopped"}`)}async function printDaemonStatus(serveRunning){try{let schedulerPidPath=join31(genieHome(),"scheduler.pid");if(existsSync24(schedulerPidPath)){let sPid=Number.parseInt(readFileSync11(schedulerPidPath,"utf-8").trim(),10),sAlive=!Number.isNaN(sPid)&&isProcessAlive(sPid);console.log(` scheduler: ${sAlive?`running (PID ${sPid})`:"stopped"}`)}else if(serveRunning)console.log(" scheduler: integrated (in-process)");else console.log(" scheduler: stopped")}catch{console.log(" scheduler: unknown")}try{let{getInboxPollIntervalMs:getInboxPollIntervalMs2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log(" inbox: disabled");else console.log(` inbox: ${serveRunning?"watching":"stopped"} (poll ${pollMs/1000}s)`)}catch{console.log(" inbox: unavailable")}}async function statusServe(){let pid=readServePid(),running2=pid!==null&&isProcessAlive(pid);if(console.log(`
1375
- Genie Serve`),console.log("\u2500".repeat(50)),console.log(` Status: ${running2?"running":"stopped"}`),running2&&pid)console.log(` PID: ${pid}`);await printPgserveStatus(),printTmuxStatus(),await printDaemonStatus(running2),console.log(` PID file: ${servePidPath()}`),console.log("")}function registerServeCommands(program2){let serve=program2.command("serve").description("Start all genie infrastructure (pgserve, tmux, scheduler)");serve.command("start",{isDefault:!0}).description("Start genie serve").option("--daemon","Run in background").option("--foreground","Run in foreground (default)").action(async(options)=>{if(options.daemon)await startBackground();else await startForeground()}),serve.command("stop").description("Stop genie serve and all services").action(async()=>{await stopServe()}),serve.command("status").description("Show service health").action(async()=>{await statusServe()})}var GENIE_SOCKET="genie",TUI_SOCKET="genie-tui",TUI_SESSION="genie-tui",NAV_WIDTH=30,KEY_TABLE="genie-tui",TUI_STYLE,handles;var init_serve=__esm(()=>{TUI_STYLE={activeBorder:"#7c3aed",inactiveBorder:"#414868"};handles={schedulerHandle:null,agentWatcher:null}});var exports_tmux2={};__export(exports_tmux2,{attachTuiSession:()=>attachTuiSession,attachProjectWindow:()=>attachProjectWindow});import{execSync as execSync7,spawnSync as spawnSync2}from"child_process";function resolveRightPane(rightPane){try{return execSync7(`${TMUX} display-message -t ${rightPane} -p ''`,{stdio:"ignore"}),rightPane}catch{try{let panes=execSync7(`${TMUX} list-panes -t ${SESSION_NAME}:0 -F '#{pane_id}'`,{encoding:"utf-8"}).trim().split(`
1376
- `);return panes[1]||panes[0]}catch{return rightPane}}}function ensureAgentSession(sessionName){let agentTmux=`tmux -L ${GENIE_AGENT_SOCKET}`;try{execSync7(`${agentTmux} has-session -t '${sessionName}' 2>/dev/null`,{stdio:"ignore"})}catch{try{execSync7(`${agentTmux} new-session -d -s '${sessionName}'`,{stdio:"ignore"})}catch{}}}function attachProjectWindow(rightPane,targetSession,windowIndex){if(targetSession===SESSION_NAME)return;let pane=resolveRightPane(rightPane);if(ensureAgentSession(targetSession),windowIndex!==void 0)try{let agentTmux=`tmux -L ${GENIE_AGENT_SOCKET}`;execSync7(`${agentTmux} select-window -t '${targetSession}:${windowIndex}'`,{stdio:"ignore"})}catch{}try{let{writeFileSync:writeFileSync9}=__require("fs"),{join:join32}=__require("path"),home=process.env.GENIE_HOME??`${process.env.HOME}/.genie`,script=join32(home,"tui-attach.sh");writeFileSync9(script,["#!/bin/sh","clear",`tmux -L ${GENIE_AGENT_SOCKET} set-option -t '${targetSession}' status off 2>/dev/null`,`exec tmux -L ${GENIE_AGENT_SOCKET} attach-session -t '${targetSession}'`,""].join(`
1377
- `),{mode:493}),execSync7(`${TMUX} respawn-pane -k -t ${pane} "TMUX='' ${script}"`,{stdio:"ignore"})}catch{}}function attachTuiSession(){spawnSync2("tmux",["-L",TMUX_SOCKET,"-f",TUI_TMUX_CONF,"attach-session","-t",SESSION_NAME],{stdio:"inherit"})}var SESSION_NAME="genie-tui",TMUX_SOCKET="genie-tui",GENIE_AGENT_SOCKET="genie",TUI_TMUX_CONF,TMUX;var init_tmux2=__esm(()=>{TUI_TMUX_CONF=(()=>{let{existsSync:existsSync25}=__require("fs"),tuiConf=`${process.env.GENIE_HOME??`${process.env.HOME}/.genie`}/tui-tmux.conf`;return existsSync25(tuiConf)?tuiConf:"/dev/null"})(),TMUX=`tmux -L ${TMUX_SOCKET} -f ${TUI_TMUX_CONF}`});function onBridgeEvent(handler){return listeners.add(handler),()=>{listeners.delete(handler)}}function emit2(event){for(let handler of listeners)handler(event)}async function listAgents2(){return(await getConnection())`
1374
+ Shutting down genie serve...`),handles.agentWatcher?.close(),handles.schedulerHandle?.stop(),killTuiSession(),removeServePid(),console.log("genie serve stopped.")};if(process.on("SIGTERM",()=>{shutdown2(),process.exit(143)}),process.on("SIGINT",()=>{shutdown2(),process.exit(130)}),handles.schedulerHandle)await handles.schedulerHandle.done;else await new Promise(()=>{});removeServePid()}async function startBackground(){let existingPid=readServePid();if(existingPid&&isProcessAlive(existingPid))console.log(`genie serve already running (PID ${existingPid})`),process.exit(0);if(existingPid)removeServePid();let bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie",child=spawn3(bunPath,[genieBin,"serve","--foreground"],{detached:!0,stdio:"ignore",env:{...process.env,GENIE_IS_DAEMON:"1"}});if(child.unref(),child.pid)if(await new Promise((resolve5)=>setTimeout(resolve5,1000)),isProcessAlive(child.pid))console.log(`genie serve started (PID ${child.pid})`);else console.error("Error: genie serve exited immediately."),process.exit(1);else console.error("Error: failed to spawn genie serve"),process.exit(1)}async function stopServe(){let pid=readServePid();if(!pid){console.log("genie serve is not running (no PID file).");return}if(!isProcessAlive(pid)){console.log(`Stale PID file (PID ${pid} not running). Cleaning up.`),removeServePid(),killTuiSession();return}console.log(`Stopping genie serve (PID ${pid})...`);try{process.kill(-pid,"SIGTERM")}catch{try{process.kill(pid,"SIGTERM")}catch{}}let deadline=Date.now()+1e4;while(Date.now()<deadline&&isProcessAlive(pid))await new Promise((resolve5)=>setTimeout(resolve5,250));if(isProcessAlive(pid)){console.log("Did not stop within 10s. Sending SIGKILL.");try{process.kill(-pid,"SIGKILL")}catch{try{process.kill(pid,"SIGKILL")}catch{}}}killTuiSession(),removeServePid(),console.log("genie serve stopped.")}async function printPgserveStatus(){try{let{isAvailable:isAvailable2,getActivePort:getActivePort2}=await Promise.resolve().then(() => (init_db(),exports_db)),dbOk=await isAvailable2();console.log(` pgserve: ${dbOk?`healthy (port ${getActivePort2()})`:"unreachable"}`)}catch{console.log(" pgserve: unavailable")}}function printTmuxStatus(){let agentRunning=isGenieTmuxRunning(),sessions=agentRunning?listAgentSessions():[];if(console.log(` tmux -L ${GENIE_SOCKET}: ${agentRunning?`running (${sessions.length} sessions)`:"stopped"}`),sessions.length>0)console.log(` ${sessions.join(", ")}`);let tuiReady=isTuiSessionReady();console.log(` tmux -L genie-tui: ${tuiReady?"running":"stopped"}`)}async function printDaemonStatus(serveRunning){try{let schedulerPidPath=join31(genieHome(),"scheduler.pid");if(existsSync24(schedulerPidPath)){let sPid=Number.parseInt(readFileSync11(schedulerPidPath,"utf-8").trim(),10),sAlive=!Number.isNaN(sPid)&&isProcessAlive(sPid);console.log(` scheduler: ${sAlive?`running (PID ${sPid})`:"stopped"}`)}else if(serveRunning)console.log(" scheduler: integrated (in-process)");else console.log(" scheduler: stopped")}catch{console.log(" scheduler: unknown")}try{let{getInboxPollIntervalMs:getInboxPollIntervalMs2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log(" inbox: disabled");else console.log(` inbox: ${serveRunning?"watching":"stopped"} (poll ${pollMs/1000}s)`)}catch{console.log(" inbox: unavailable")}}async function statusServe(){let pid=readServePid(),running2=pid!==null&&isProcessAlive(pid);if(console.log(`
1375
+ Genie Serve`),console.log("\u2500".repeat(50)),console.log(` Status: ${running2?"running":"stopped"}`),running2&&pid)console.log(` PID: ${pid}`);await printPgserveStatus(),printTmuxStatus(),await printDaemonStatus(running2),console.log(` PID file: ${servePidPath()}`),console.log("")}function registerServeCommands(program2){let serve=program2.command("serve").description("Start all genie infrastructure (pgserve, tmux, scheduler)");serve.command("start",{isDefault:!0}).description("Start genie serve").option("--daemon","Run in background").option("--foreground","Run in foreground (default)").action(async(options)=>{if(options.daemon)await startBackground();else await startForeground()}),serve.command("stop").description("Stop genie serve and all services").action(async()=>{await stopServe()}),serve.command("status").description("Show service health").action(async()=>{await statusServe()})}var GENIE_SOCKET="genie",TUI_SESSION="genie-tui",NAV_WIDTH=30,TUI_STYLE,handles;var init_serve=__esm(()=>{TUI_STYLE={activeBorder:"#7c3aed",inactiveBorder:"#414868"};handles={schedulerHandle:null,agentWatcher:null}});var exports_tmux2={};__export(exports_tmux2,{attachTuiSession:()=>attachTuiSession,attachProjectWindow:()=>attachProjectWindow});import{execSync as execSync7,spawnSync as spawnSync2}from"child_process";function resolveRightPane(rightPane){try{return execSync7(`${TMUX} display-message -t ${rightPane} -p ''`,{stdio:"ignore"}),rightPane}catch{try{let panes=execSync7(`${TMUX} list-panes -t ${SESSION_NAME}:0 -F '#{pane_id}'`,{encoding:"utf-8"}).trim().split(`
1376
+ `);return panes[1]||panes[0]}catch{return rightPane}}}function attachProjectWindow(rightPane,targetSession,windowIndex){if(targetSession===SESSION_NAME)return;let pane=resolveRightPane(rightPane),agentTmux=`tmux -L ${GENIE_AGENT_SOCKET}`;try{execSync7(`${agentTmux} has-session -t '${targetSession}' 2>/dev/null`,{stdio:"ignore"})}catch{return}if(windowIndex!==void 0)try{execSync7(`${agentTmux} select-window -t '${targetSession}:${windowIndex}'`,{stdio:"ignore"})}catch{}try{execSync7(`${agentTmux} set-option -t '${targetSession}' status off 2>/dev/null`,{stdio:"ignore"})}catch{}try{let cmd=`while true; do TMUX='' ${agentTmux} attach-session -t '${targetSession}' 2>/dev/null; sleep 0.3; done`;execSync7(`${TMUX} respawn-pane -k -t ${pane} "bash -c '${cmd}'"`,{stdio:"ignore"})}catch{}try{execSync7(`${TMUX} select-pane -t ${SESSION_NAME}:0.0`,{stdio:"ignore"})}catch{}}function attachTuiSession(){spawnSync2("tmux",["-L",TMUX_SOCKET,"-f",TUI_TMUX_CONF,"attach-session","-t",SESSION_NAME],{stdio:"inherit"})}var SESSION_NAME="genie-tui",TMUX_SOCKET="genie-tui",GENIE_AGENT_SOCKET="genie",TUI_TMUX_CONF,TMUX;var init_tmux2=__esm(()=>{TUI_TMUX_CONF=(()=>{let{existsSync:existsSync25}=__require("fs"),tuiConf=`${process.env.GENIE_HOME??`${process.env.HOME}/.genie`}/tui-tmux.conf`;return existsSync25(tuiConf)?tuiConf:"/dev/null"})(),TMUX=`tmux -L ${TMUX_SOCKET} -f ${TUI_TMUX_CONF}`});function onBridgeEvent(handler){return listeners.add(handler),()=>{listeners.delete(handler)}}function emit2(event){for(let handler of listeners)handler(event)}async function listAgents2(){return(await getConnection())`
1378
1377
  SELECT a.id, a.custom_name, a.role, a.team, a.title, a.state,
1379
1378
  a.reports_to, a.current_executor_id, a.started_at
1380
1379
  FROM agents a
@@ -2072,7 +2071,7 @@ $ bun add react-devtools-core@7 -d
2072
2071
  AND a.ended_at IS NULL
2073
2072
  ORDER BY a.started_at DESC
2074
2073
  `).map(mapAssignment)}function mapExecutor(row){let meta=row.metadata;return{id:String(row.id),agentId:String(row.agent_id),agentName:row.agent_name?String(row.agent_name):null,provider:String(row.provider),transport:String(row.transport),pid:row.pid!=null?Number(row.pid):null,tmuxSession:row.tmux_session?String(row.tmux_session):null,tmuxPaneId:row.tmux_pane_id?String(row.tmux_pane_id):null,state:String(row.state),metadata:typeof meta==="string"?JSON.parse(meta):meta??{},startedAt:row.started_at instanceof Date?row.started_at.toISOString():String(row.started_at),role:row.role?String(row.role):null,team:row.team?String(row.team):null}}function mapAssignment(row){return{id:String(row.id),executorId:String(row.executor_id),taskId:row.task_id?String(row.task_id):null,taskTitle:row.task_title?String(row.task_title):null,wishSlug:row.wish_slug?String(row.wish_slug):null,groupNumber:row.group_number!=null?Number(row.group_number):null,startedAt:row.started_at instanceof Date?row.started_at.toISOString():String(row.started_at)}}import{execSync as execSync16}from"child_process";function execQuiet(cmd){try{return execSync16(cmd,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}}function parsePaneLine(parts){let[sessionName,winIdxStr,winName,winActive,winPanes,paneIdxStr,paneId,panePidStr,paneCmd,paneTitle,paneSize,sessAttached,sessWindows,sessCreated,paneDead]=parts;return{sessionName,winIdxStr,session:{name:sessionName,attached:sessAttached==="1",windowCount:Number.parseInt(sessWindows,10)||0,created:Number.parseInt(sessCreated,10)||0},window:{sessionName,index:Number.parseInt(winIdxStr,10)||0,name:winName,active:winActive==="1",paneCount:Number.parseInt(winPanes,10)||0},pane:{sessionName,windowIndex:Number.parseInt(winIdxStr,10)||0,paneIndex:Number.parseInt(paneIdxStr,10)||0,paneId,pid:Number.parseInt(panePidStr,10)||0,command:paneCmd,title:paneTitle,size:paneSize,isDead:paneDead==="1"}}}function getTmuxInventory(){let paneOutput=execQuiet("tmux -L genie list-panes -a -F '#{session_name}|#{window_index}|#{window_name}|#{window_active}|#{window_panes}|#{pane_index}|#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_title}|#{pane_width}x#{pane_height}|#{session_attached}|#{session_windows}|#{session_created}|#{pane_dead}'");if(!paneOutput)return[];let sessionMap=new Map,windowMap=new Map;for(let line of paneOutput.split(`
2075
- `)){if(!line)continue;let parts=line.split("|");if(parts.length<15)continue;let parsed=parsePaneLine(parts);if(!sessionMap.has(parsed.sessionName))sessionMap.set(parsed.sessionName,{...parsed.session,windows:[]});let winKey=`${parsed.sessionName}:${parsed.winIdxStr}`;if(!windowMap.has(winKey)){let win={...parsed.window,panes:[]};windowMap.set(winKey,win),sessionMap.get(parsed.sessionName)?.windows.push(win)}windowMap.get(winKey)?.panes.push(parsed.pane)}return Array.from(sessionMap.values()).sort((a,b3)=>a.name.localeCompare(b3.name))}function isPidAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function allClaudePanes(sessions2){return sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.command==="claude"||p.title.includes("claude"))}function detectGaps(executors,sessions2){let deadPidExecutors=executors.filter((e)=>e.pid!=null&&!isPidAlive(e.pid)),executorPaneIds=new Set(executors.map((e)=>e.tmuxPaneId).filter(Boolean)),claudePanes=allClaudePanes(sessions2),orphanPanes=claudePanes.filter((p)=>!executorPaneIds.has(p.paneId)),linkedCount=executors.filter((e)=>e.tmuxPaneId&&!deadPidExecutors.some((d2)=>d2.id===e.id)).length,deadPaneCount=sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.isDead).length;return{deadPidExecutors,orphanPanes,linkedCount,totalExecutors:executors.length,totalClaudePanes:claudePanes.length,deadPaneCount}}async function collectDiagnostics(){let{loadExecutors:loadExecutors2,loadAssignments:loadAssignments2}=await Promise.resolve().then(() => exports_db2),sessions2=getTmuxInventory(),executors=await loadExecutors2(),executorIds=executors.map((e)=>e.id),assignments=await loadAssignments2(executorIds),gaps=detectGaps(executors,sessions2);return{sessions:sessions2,executors,assignments,gaps,timestamp:Date.now()}}var init_diagnostics=()=>{};function buildSessionTree(snapshot){let executorByPaneId=new Map;for(let exec3 of snapshot.executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);return snapshot.sessions.filter((s)=>s.name!=="genie-tui").map((session)=>sessionToNode(session,executorByPaneId))}function buildWorkspaceTree(input){let{agentNames,sessions:sessions2,executors}=input,sessionByName=new Map;for(let s of sessions2)if(s.name!=="genie-tui")sessionByName.set(s.name,s);let executorByPaneId=new Map;for(let exec3 of executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);let executorsByAgent=new Map;for(let exec3 of executors){let name=exec3.agentName??exec3.metadata?.agentName;if(typeof name==="string"){let list2=executorsByAgent.get(name)??[];list2.push(exec3),executorsByAgent.set(name,list2)}}let nodes=agentNames.map((name)=>buildAgentNode(name,sessionByName.get(name),executorsByAgent.get(name)??[],executorByPaneId)),agentNameSet=new Set(agentNames);for(let[name,session]of sessionByName)if(!agentNameSet.has(name))nodes.push(sessionToNode(session,executorByPaneId));return nodes}function countClaudePanes(session){return session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0)}function buildAgentNode(name,session,agentExecutors,executorByPaneId){let wsState=deriveWsAgentState(session,agentExecutors),children=[];if(session)for(let win of session.windows){if(win.index===0)continue;children.push(windowToNode(session.name,win,executorByPaneId))}return{id:`agent:${name}`,type:"agent",label:name,depth:0,expanded:children.length>0,children,data:{sessionName:name,windowCount:session?session.windows.length:0},activePanes:session?countClaudePanes(session):0,agentState:agentExecutors.length>0?deriveExecutorState(agentExecutors):void 0,wsAgentState:wsState}}function deriveWsAgentState(session,agentExecutors){if(!session)return"stopped";for(let exec3 of agentExecutors){if(exec3.state==="error"||exec3.state==="terminated")return"error";if(exec3.state==="spawning")return"spawning"}return"running"}function deriveExecutorState(execs){for(let e of execs)if(e.state==="working")return"working";for(let e of execs)if(e.state==="permission")return"permission";for(let e of execs)if(e.state==="error"||e.state==="terminated")return"error";return"idle"}function sessionToNode(session,executorMap){let claudePanes=session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0);return{id:`session:${session.name}`,type:"session",label:session.name,depth:0,expanded:!0,children:session.windows.map((w2)=>windowToNode(session.name,w2,executorMap)),data:{attached:session.attached,windowCount:session.windowCount},activePanes:claudePanes,agentState:void 0,wsAgentState:void 0}}function windowToNode(sessionName,window2,executorMap){let activePanes=window2.panes.filter((p)=>!p.isDead&&(p.command==="claude"||p.title.includes("claude"))).length;return{id:`window:${sessionName}:${window2.index}`,type:"window",label:window2.name,depth:1,expanded:!0,children:window2.panes.map((p)=>paneToNode(sessionName,window2.index,p,executorMap)),data:{active:window2.active,paneCount:window2.paneCount},activePanes,agentState:void 0,wsAgentState:void 0}}function paneToNode(sessionName,windowIndex,pane,executorMap){let executor=executorMap.get(pane.paneId),isClaude=pane.command==="claude"||pane.title.includes("claude");return{id:`pane:${pane.paneId}`,type:"pane",label:derivePaneLabel(pane,executor,isClaude),depth:2,expanded:!1,children:[],data:{command:pane.command,isDead:pane.isDead,pid:pane.pid,size:pane.size,paneId:pane.paneId,sessionName,windowIndex},activePanes:0,agentState:derivePaneState(pane,executor),wsAgentState:void 0}}function derivePaneLabel(pane,executor,isClaude){if(executor?.agentName&&executor?.team)return`${executor.team}/${executor.agentName}`;if(executor?.agentName)return executor.agentName;if(isClaude)return"claude";return pane.command}function derivePaneState(pane,executor){if(pane.isDead)return"error";if(!executor)return;let s=executor.state;if(s==="working")return"working";if(s==="idle"||s==="spawning")return"idle";if(s==="permission")return"permission";if(s==="error"||s==="terminated")return"error";return}function getSessionTarget(node){if(node.type==="agent")return{sessionName:node.data.sessionName};if(node.type==="session")return{sessionName:node.label};if(node.type==="window"){let parts=node.id.split(":");return{sessionName:parts[1],windowIndex:Number(parts[2])}}if(node.type==="pane"){let data=node.data;return{sessionName:data.sessionName,windowIndex:data.windowIndex}}return null}var palette,icons;var init_theme2=__esm(()=>{palette={purple:"#a855f7",violet:"#7c3aed",cyan:"#22d3ee",emerald:"#34d399",bg:"#1a1028",bgLight:"#241838",bgLighter:"#2e2048",text:"#e2e8f0",textDim:"#94a3b8",textMuted:"#64748b",border:"#414868",borderActive:"#7c3aed",scrollTrack:"#414868",scrollThumb:"#7aa2f7",active:"#22d3ee",success:"#34d399",warning:"#fbbf24",error:"#f87171",idle:"#94a3b8"},icons={org:"\u25C6",project:"\u25B8",projectOpen:"\u25BE",board:"\u2261",boardOpen:"\u2261",column:"\u2502",task:"\u25CB",taskActive:"\u25CF",taskDone:"\u2713",agent:"\u25B6",collapsed:"\u25B8",expanded:"\u25BE"}});function flattenTree(nodes){let result=[];function walk(node,depth){if(result.push({node,depth,visible:!0}),node.expanded)for(let child of node.children)walk(child,depth+1)}for(let node of nodes)walk(node,0);return result}function toggleNode(nodes,id){return nodes.map((node)=>{if(node.id===id)return{...node,expanded:!node.expanded};return{...node,children:toggleNode(node.children,id)}})}var import_jsx_dev_runtime2;var init_jsx_dev_runtime=__esm(()=>{import_jsx_dev_runtime2=__toESM(require_react_jsx_dev_runtime_development(),1)});function getNodeIcon(node){if(node.type==="agent")return getAgentIcon(node);switch(node.type){case"session":return node.data.attached?"\u25B6":"\u25B8";case"window":return node.data.active?"\u25A0":"\u25A1";case"pane":return getPaneIcon(node);default:return" "}}function getAgentIcon(node){switch(node.wsAgentState){case"running":return"\u25CF";case"stopped":return"\u25CB";case"error":return"\u2298";case"spawning":return"\u231B";default:return"\u25CB"}}function getPaneIcon(node){if(node.data.isDead)return"\u2718";if(node.agentState==="working")return"\u25CF";if(node.agentState==="idle")return"\u25CB";if(node.agentState==="permission")return"\u26A0";if(node.agentState==="error")return"\u2718";if(node.data.command==="claude")return"\u25C6";return"\u25CB"}function getNodeColor(node){if(node.type==="agent")return getAgentColor(node);switch(node.type){case"session":return node.data.attached?palette.emerald:palette.textDim;case"window":return node.data.active?palette.cyan:palette.text;case"pane":return getPaneColor(node);default:return palette.text}}function getAgentColor(node){switch(node.wsAgentState){case"running":return palette.emerald;case"stopped":return palette.textDim;case"error":return palette.error;case"spawning":return palette.warning;default:return palette.textDim}}function getPaneColor(node){if(node.data.isDead)return palette.error;if(node.agentState==="working")return palette.cyan;if(node.agentState==="permission")return palette.warning;if(node.agentState==="error")return palette.error;if(node.agentState==="idle")return palette.textDim;if(node.data.command==="claude")return palette.cyan;return palette.textDim}function getNodeSuffix(node){if(node.type==="agent"){let wc=node.data.windowCount;if(wc>1)return` (${wc} windows)`;if(wc===1)return" (1 window)";return""}if(node.type==="session"||node.type==="pane"){let count=node.activePanes;if(count>0)return` ${icons.agent}${count}`}return""}function getStateColor(state){switch(state){case"working":return palette.cyan;case"idle":return palette.textDim;case"permission":return palette.warning;case"error":return palette.error;default:return palette.textMuted}}var import_react13,TreeNodeRow;var init_TreeNode=__esm(()=>{init_theme2();init_jsx_dev_runtime();import_react13=__toESM(require_react_development(),1),TreeNodeRow=import_react13.memo(function({node,selected,onSelect,onToggle}){let indent=" ".repeat(node.depth),hasChildren=node.children.length>0,expandIcon=hasChildren?node.expanded?icons.expanded:icons.collapsed:" ",icon=getNodeIcon(node),color=getNodeColor(node),suffix=getNodeSuffix(node);return import_jsx_dev_runtime2.jsxDEV("box",{height:1,width:"100%",backgroundColor:selected?palette.violet:void 0,onMouseDown:()=>{if(onSelect(node.id),hasChildren)onToggle(node.id)},children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[indent,expandIcon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:color,children:[icon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:selected?"#ffffff":palette.text,children:node.label},void 0,!1,void 0,this),suffix?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:suffix},void 0,!1,void 0,this):null,node.agentState?import_jsx_dev_runtime2.jsxDEV("span",{fg:getStateColor(node.agentState),children:[" ",node.agentState]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this)})});function Nav({onTmuxSessionSelect,workspaceRoot,initialAgent}){let[diagnostics,setDiagnostics]=import_react15.useState(null),[sessionTree,setSessionTree]=import_react15.useState([]),[selectedIndex,setSelectedIndex]=import_react15.useState(0),lastTarget=import_react15.useRef(null),genieHome3=import_react15.useRef(process.env.GENIE_HOME??`${process.env.HOME}/.genie`);import_react15.useEffect(()=>{let active=!0;async function refresh(){try{let snap=await collectDiagnostics();if(active)setDiagnostics(snap)}catch(err){console.error("TUI: diagnostics failed:",err)}}refresh();let timer2=setInterval(refresh,2000);return()=>{active=!1,clearInterval(timer2)}},[]),import_react15.useEffect(()=>{if(!diagnostics)return;let newTree;if(workspaceRoot){let agentNames=scanAgents2(workspaceRoot);newTree=buildWorkspaceTree({agentNames,sessions:diagnostics.sessions,executors:diagnostics.executors})}else newTree=buildSessionTree(diagnostics);setSessionTree((prev)=>mergeExpandedState(prev,newTree))},[diagnostics,workspaceRoot]);let flatNodes=import_react15.useMemo(()=>flattenTree(sessionTree),[sessionTree]);import_react15.useEffect(()=>{if(flatNodes.length>0&&selectedIndex>=flatNodes.length)setSelectedIndex(flatNodes.length-1)},[flatNodes.length,selectedIndex]);let[pendingAgent,setPendingAgent]=import_react15.useState(initialAgent);import_react15.useEffect(()=>{if(!diagnostics)return;try{let fs3=__require("fs"),agentFile=`${genieHome3.current}/tui-initial-agent`;if(fs3.existsSync(agentFile)){let agent=fs3.readFileSync(agentFile,"utf-8").trim();if(fs3.unlinkSync(agentFile),agent)setPendingAgent(agent)}}catch{}},[diagnostics]),import_react15.useEffect(()=>{if(!pendingAgent||flatNodes.length===0)return;let idx=flatNodes.findIndex((n)=>n.node.id===`agent:${pendingAgent}`);if(idx>=0)setSelectedIndex(idx),setPendingAgent(void 0)},[pendingAgent,flatNodes]),import_react15.useEffect(()=>{let current=flatNodes[selectedIndex]?.node;if(!current)return;let target=getSessionTarget(current);if(!target)return;let key=`${target.sessionName}:${target.windowIndex??""}`;if(key===lastTarget.current)return;if(lastTarget.current=key,current.type==="agent"&&current.wsAgentState!=="running")return;onTmuxSessionSelect(target.sessionName,target.windowIndex)},[selectedIndex,flatNodes,onTmuxSessionSelect]);let handleSelect=import_react15.useCallback((id)=>{let idx=flatNodes.findIndex((n)=>n.node.id===id);if(idx>=0)setSelectedIndex(idx)},[flatNodes]),handleToggle=import_react15.useCallback((id)=>{setSessionTree((prev)=>toggleNode(prev,id))},[]),handleVerticalNav=import_react15.useCallback((keyName2)=>{let rowCount=flatNodes.length;if(rowCount===0)return;if(keyName2==="up"||keyName2==="k")setSelectedIndex((prev)=>prev===0?rowCount-1:prev-1);else if(keyName2==="down"||keyName2==="j")setSelectedIndex((prev)=>prev>=rowCount-1?0:prev+1)},[flatNodes.length]),handleExpandCollapse=import_react15.useCallback((keyName2)=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if((keyName2==="right"||keyName2==="l")&&node.children.length>0&&!node.expanded)handleToggle(node.id);else if((keyName2==="left"||keyName2==="h")&&node.expanded)handleToggle(node.id)},[flatNodes,selectedIndex,handleToggle]),handleEnter=import_react15.useCallback(()=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if(node.type==="agent"){if(node.wsAgentState!=="running")spawnAgent(node.label);let target2=getSessionTarget(node);if(target2)onTmuxSessionSelect(target2.sessionName,target2.windowIndex);return}if(node.children.length>0)handleToggle(node.id);let target=getSessionTarget(node);if(target)onTmuxSessionSelect(target.sessionName,target.windowIndex)},[flatNodes,selectedIndex,handleToggle,onTmuxSessionSelect]);useKeyboard((key)=>{if(key.name==="up"||key.name==="k"||key.name==="down"||key.name==="j")handleVerticalNav(key.name);else if(key.name==="right"||key.name==="l"||key.name==="left"||key.name==="h")handleExpandCollapse(key.name);else if(key.name==="enter"||key.name==="return")handleEnter()});let agentCount=workspaceRoot?sessionTree.filter((n)=>n.type==="agent").length:diagnostics?.sessions.length??0,runningCount=workspaceRoot?sessionTree.filter((n)=>n.wsAgentState==="running").length:diagnostics?.sessions.reduce((sum,s)=>sum+s.windows.reduce((ws,w2)=>ws+w2.panes.length,0),0)??0,headerLabel=workspaceRoot?"Agents":"Sessions";return import_jsx_dev_runtime2.jsxDEV("box",{flexDirection:"column",width:"100%",height:"100%",backgroundColor:palette.bg,children:[import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.purple,children:headerLabel},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[" ",workspaceRoot?`${runningCount}/${agentCount}`:`${agentCount}s ${runningCount}p`]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("scrollbox",{focused:!0,height:"100%",style:{scrollbarOptions:{showArrows:!1,trackOptions:{foregroundColor:palette.scrollThumb,backgroundColor:palette.scrollTrack}}},children:flatNodes.map((flat,i2)=>import_jsx_dev_runtime2.jsxDEV(TreeNodeRow,{node:flat.node,selected:i2===selectedIndex,onSelect:handleSelect,onToggle:handleToggle},flat.node.id,!1,void 0,this))},void 0,!1,void 0,this):import_jsx_dev_runtime2.jsxDEV("box",{flexGrow:1,justifyContent:"center",alignItems:"center",children:import_jsx_dev_runtime2.jsxDEV("text",{fg:palette.textDim,children:"Collecting..."},void 0,!1,void 0,this)},void 0,!1,void 0,this),import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textMuted,children:["\u2191\u2193",":nav ","\u2190\u2192",":expand Enter:",workspaceRoot?"spawn/attach":"attach"]},void 0,!0,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function spawnAgent(name){try{let{spawn:spawn4}=__require("child_process");spawn4("genie",["spawn",name],{detached:!0,stdio:"ignore"}).unref()}catch{}}function mergeExpandedState(oldTree,newTree){if(oldTree.length===0)return newTree;let oldState=new Map;function collect(nodes){for(let n of nodes)oldState.set(n.id,n.expanded),collect(n.children)}collect(oldTree);function apply(nodes){return nodes.map((n)=>({...n,expanded:oldState.has(n.id)?oldState.get(n.id):n.expanded,children:apply(n.children)}))}return apply(newTree)}var import_react15;var init_Nav=__esm(async()=>{init_workspace();init_diagnostics();init_theme2();init_TreeNode();init_jsx_dev_runtime();await init_react();import_react15=__toESM(require_react_development(),1)});function App({rightPane,workspaceRoot,initialAgent}){let handleTmuxSessionSelect=import_react16.useCallback((sessionName,windowIndex)=>{if(!rightPane)return;attachProjectWindow(rightPane,sessionName,windowIndex)},[rightPane]);return import_jsx_dev_runtime2.jsxDEV(Nav,{onTmuxSessionSelect:handleTmuxSessionSelect,workspaceRoot,initialAgent},void 0,!1,void 0,this)}var import_react16;var init_app=__esm(async()=>{init_tmux2();init_jsx_dev_runtime();await init_Nav();import_react16=__toESM(require_react_development(),1)});var exports_render={};__export(exports_render,{renderNav:()=>renderNav});async function renderNav(){let rightPane=process.env.GENIE_TUI_RIGHT||void 0,workspaceRoot=process.env.GENIE_TUI_WORKSPACE||void 0,initialAgent=process.env.GENIE_TUI_AGENT||void 0,renderer=await createCliRenderer({exitOnCtrlC:!1,useMouse:!0});createRoot(renderer).render(import_jsx_dev_runtime2.jsxDEV(App,{rightPane,workspaceRoot,initialAgent},void 0,!1,void 0,this)),await new Promise((resolve10)=>{renderer.once("destroy",resolve10)})}var init_render=__esm(async()=>{init_jsx_dev_runtime();await __promiseAll([init_core(),init_react(),init_app()])});var exports_tui={};__export(exports_tui,{launchTui:()=>launchTui});async function launchTui(){let{renderNav:renderNav2}=await init_render().then(() => exports_render);await renderNav2()}var import__=__toESM(require_commander(),1),{program,createCommand,createArgument,createOption,CommanderError,InvalidArgumentError,InvalidOptionArgumentError,Command,Argument,Option,Help}=import__.default;import{existsSync as existsSync3,unlinkSync as unlinkSync2}from"fs";import{homedir as homedir3}from"os";import{join as join3}from"path";var{$:$2}=globalThis.Bun;import{existsSync,unlinkSync}from"fs";import{homedir}from"os";import{join}from"path";var CLAUDE_DIR=join(homedir(),".claude"),CLAUDE_HOOKS_DIR=join(CLAUDE_DIR,"hooks"),CLAUDE_SETTINGS_FILE=join(CLAUDE_DIR,"settings.json"),GENIE_HOOK_SCRIPT_NAME="genie-bash-hook.sh";function getClaudeSettingsPath(){return CLAUDE_SETTINGS_FILE}function getGenieHookScriptPath(){return join(CLAUDE_HOOKS_DIR,GENIE_HOOK_SCRIPT_NAME)}function hookScriptExists(){return existsSync(getGenieHookScriptPath())}function removeHookScript(){let scriptPath=getGenieHookScriptPath();if(existsSync(scriptPath))unlinkSync(scriptPath)}function contractClaudePath(path){let home=homedir();if(path.startsWith(`${home}/`))return`~${path.slice(home.length)}`;if(path===home)return"~";return path}init_genie_config2();var{$}=globalThis.Bun;async function checkCommand(cmd){try{let cmdPath=(await $`which ${cmd}`.quiet().text()).trim();if(!cmdPath)return{exists:!1};let version;try{let firstLine=(await $`${cmd} --version`.quiet().text()).split(`
2074
+ `)){if(!line)continue;let parts=line.split("|");if(parts.length<15)continue;let parsed=parsePaneLine(parts);if(!sessionMap.has(parsed.sessionName))sessionMap.set(parsed.sessionName,{...parsed.session,windows:[]});let winKey=`${parsed.sessionName}:${parsed.winIdxStr}`;if(!windowMap.has(winKey)){let win={...parsed.window,panes:[]};windowMap.set(winKey,win),sessionMap.get(parsed.sessionName)?.windows.push(win)}windowMap.get(winKey)?.panes.push(parsed.pane)}return Array.from(sessionMap.values()).sort((a,b3)=>a.name.localeCompare(b3.name))}function isPidAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function allClaudePanes(sessions2){return sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.command==="claude"||p.title.includes("claude"))}function detectGaps(executors,sessions2){let deadPidExecutors=executors.filter((e)=>e.pid!=null&&!isPidAlive(e.pid)),executorPaneIds=new Set(executors.map((e)=>e.tmuxPaneId).filter(Boolean)),claudePanes=allClaudePanes(sessions2),orphanPanes=claudePanes.filter((p)=>!executorPaneIds.has(p.paneId)),linkedCount=executors.filter((e)=>e.tmuxPaneId&&!deadPidExecutors.some((d2)=>d2.id===e.id)).length,deadPaneCount=sessions2.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.isDead).length;return{deadPidExecutors,orphanPanes,linkedCount,totalExecutors:executors.length,totalClaudePanes:claudePanes.length,deadPaneCount}}async function collectDiagnostics(){let{loadExecutors:loadExecutors2,loadAssignments:loadAssignments2}=await Promise.resolve().then(() => exports_db2),sessions2=getTmuxInventory(),executors=await loadExecutors2(),executorIds=executors.map((e)=>e.id),assignments=await loadAssignments2(executorIds),gaps=detectGaps(executors,sessions2);return{sessions:sessions2,executors,assignments,gaps,timestamp:Date.now()}}var init_diagnostics=()=>{};function buildSessionTree(snapshot){let executorByPaneId=new Map;for(let exec3 of snapshot.executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);return snapshot.sessions.filter((s)=>s.name!=="genie-tui").map((session)=>sessionToNode(session,executorByPaneId))}function buildWorkspaceTree(input){let{agentNames,sessions:sessions2,executors}=input,sessionByName=new Map;for(let s of sessions2)if(s.name!=="genie-tui")sessionByName.set(s.name,s);let executorByPaneId=new Map;for(let exec3 of executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);let executorsByAgent=new Map;for(let exec3 of executors){let name=exec3.agentName??exec3.metadata?.agentName;if(typeof name==="string"){let list2=executorsByAgent.get(name)??[];list2.push(exec3),executorsByAgent.set(name,list2)}}let nodes=agentNames.map((name)=>buildAgentNode(name,sessionByName.get(name),executorsByAgent.get(name)??[],executorByPaneId)),agentNameSet=new Set(agentNames);for(let[name,session]of sessionByName)if(!agentNameSet.has(name))nodes.push(sessionToNode(session,executorByPaneId));return nodes}function countClaudePanes(session){return session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0)}function buildAgentNode(name,session,agentExecutors,executorByPaneId){let wsState=deriveWsAgentState(session,agentExecutors),children=[];if(session)for(let win of session.windows){if(win.index===0)continue;children.push(windowToNode(session.name,win,executorByPaneId))}return{id:`agent:${name}`,type:"agent",label:name,depth:0,expanded:children.length>0,children,data:{sessionName:name,windowCount:session?session.windows.length:0},activePanes:session?countClaudePanes(session):0,agentState:agentExecutors.length>0?deriveExecutorState(agentExecutors):void 0,wsAgentState:wsState}}function deriveWsAgentState(session,agentExecutors){if(!session)return"stopped";for(let exec3 of agentExecutors){if(exec3.state==="error"||exec3.state==="terminated")return"error";if(exec3.state==="spawning")return"spawning"}return"running"}function deriveExecutorState(execs){for(let e of execs)if(e.state==="working")return"working";for(let e of execs)if(e.state==="permission")return"permission";for(let e of execs)if(e.state==="error"||e.state==="terminated")return"error";return"idle"}function sessionToNode(session,executorMap){let claudePanes=session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0);return{id:`session:${session.name}`,type:"session",label:session.name,depth:0,expanded:!0,children:session.windows.map((w2)=>windowToNode(session.name,w2,executorMap)),data:{attached:session.attached,windowCount:session.windowCount},activePanes:claudePanes,agentState:void 0,wsAgentState:void 0}}function windowToNode(sessionName,window2,executorMap){let activePanes=window2.panes.filter((p)=>!p.isDead&&(p.command==="claude"||p.title.includes("claude"))).length;return{id:`window:${sessionName}:${window2.index}`,type:"window",label:window2.name,depth:1,expanded:!0,children:window2.panes.map((p)=>paneToNode(sessionName,window2.index,p,executorMap)),data:{active:window2.active,paneCount:window2.paneCount},activePanes,agentState:void 0,wsAgentState:void 0}}function paneToNode(sessionName,windowIndex,pane,executorMap){let executor=executorMap.get(pane.paneId),isClaude=pane.command==="claude"||pane.title.includes("claude");return{id:`pane:${pane.paneId}`,type:"pane",label:derivePaneLabel(pane,executor,isClaude),depth:2,expanded:!1,children:[],data:{command:pane.command,isDead:pane.isDead,pid:pane.pid,size:pane.size,paneId:pane.paneId,sessionName,windowIndex},activePanes:0,agentState:derivePaneState(pane,executor),wsAgentState:void 0}}function derivePaneLabel(pane,executor,isClaude){if(executor?.agentName&&executor?.team)return`${executor.team}/${executor.agentName}`;if(executor?.agentName)return executor.agentName;if(isClaude)return"claude";return pane.command}function derivePaneState(pane,executor){if(pane.isDead)return"error";if(!executor)return;let s=executor.state;if(s==="working")return"working";if(s==="idle"||s==="spawning")return"idle";if(s==="permission")return"permission";if(s==="error"||s==="terminated")return"error";return}function getSessionTarget(node){if(node.type==="agent")return{sessionName:node.data.sessionName};if(node.type==="session")return{sessionName:node.label};if(node.type==="window"){let parts=node.id.split(":");return{sessionName:parts[1],windowIndex:Number(parts[2])}}if(node.type==="pane"){let data=node.data;return{sessionName:data.sessionName,windowIndex:data.windowIndex}}return null}var palette,icons;var init_theme2=__esm(()=>{palette={purple:"#a855f7",violet:"#7c3aed",cyan:"#22d3ee",emerald:"#34d399",bg:"#1a1028",bgLight:"#241838",bgLighter:"#2e2048",text:"#e2e8f0",textDim:"#94a3b8",textMuted:"#64748b",border:"#414868",borderActive:"#7c3aed",scrollTrack:"#414868",scrollThumb:"#7aa2f7",active:"#22d3ee",success:"#34d399",warning:"#fbbf24",error:"#f87171",idle:"#94a3b8"},icons={org:"\u25C6",project:"\u25B8",projectOpen:"\u25BE",board:"\u2261",boardOpen:"\u2261",column:"\u2502",task:"\u25CB",taskActive:"\u25CF",taskDone:"\u2713",agent:"\u25B6",collapsed:"\u25B8",expanded:"\u25BE"}});function flattenTree(nodes){let result=[];function walk(node,depth){if(result.push({node,depth,visible:!0}),node.expanded)for(let child of node.children)walk(child,depth+1)}for(let node of nodes)walk(node,0);return result}function toggleNode(nodes,id){return nodes.map((node)=>{if(node.id===id)return{...node,expanded:!node.expanded};return{...node,children:toggleNode(node.children,id)}})}var import_jsx_dev_runtime2;var init_jsx_dev_runtime=__esm(()=>{import_jsx_dev_runtime2=__toESM(require_react_jsx_dev_runtime_development(),1)});function getNodeIcon(node){if(node.type==="agent")return getAgentIcon(node);switch(node.type){case"session":return node.data.attached?"\u25B6":"\u25B8";case"window":return node.data.active?"\u25A0":"\u25A1";case"pane":return getPaneIcon(node);default:return" "}}function getAgentIcon(node){switch(node.wsAgentState){case"running":return"\u25CF";case"stopped":return"\u25CB";case"error":return"\u2298";case"spawning":return"\u231B";default:return"\u25CB"}}function getPaneIcon(node){if(node.data.isDead)return"\u2718";if(node.agentState==="working")return"\u25CF";if(node.agentState==="idle")return"\u25CB";if(node.agentState==="permission")return"\u26A0";if(node.agentState==="error")return"\u2718";if(node.data.command==="claude")return"\u25C6";return"\u25CB"}function getNodeColor(node){if(node.type==="agent")return getAgentColor(node);switch(node.type){case"session":return node.data.attached?palette.emerald:palette.textDim;case"window":return node.data.active?palette.cyan:palette.text;case"pane":return getPaneColor(node);default:return palette.text}}function getAgentColor(node){switch(node.wsAgentState){case"running":return palette.emerald;case"stopped":return palette.textDim;case"error":return palette.error;case"spawning":return palette.warning;default:return palette.textDim}}function getPaneColor(node){if(node.data.isDead)return palette.error;if(node.agentState==="working")return palette.cyan;if(node.agentState==="permission")return palette.warning;if(node.agentState==="error")return palette.error;if(node.agentState==="idle")return palette.textDim;if(node.data.command==="claude")return palette.cyan;return palette.textDim}function getNodeSuffix(node){if(node.type==="agent"){let wc=node.data.windowCount;if(wc>1)return` (${wc} windows)`;if(wc===1)return" (1 window)";return""}if(node.type==="session"||node.type==="pane"){let count=node.activePanes;if(count>0)return` ${icons.agent}${count}`}return""}function getStateColor(state){switch(state){case"working":return palette.cyan;case"idle":return palette.textDim;case"permission":return palette.warning;case"error":return palette.error;default:return palette.textMuted}}var import_react13,TreeNodeRow;var init_TreeNode=__esm(()=>{init_theme2();init_jsx_dev_runtime();import_react13=__toESM(require_react_development(),1),TreeNodeRow=import_react13.memo(function({node,selected,onSelect,onToggle}){let indent=" ".repeat(node.depth),hasChildren=node.children.length>0,expandIcon=hasChildren?node.expanded?icons.expanded:icons.collapsed:" ",icon=getNodeIcon(node),color=getNodeColor(node),suffix=getNodeSuffix(node);return import_jsx_dev_runtime2.jsxDEV("box",{height:1,width:"100%",backgroundColor:selected?palette.violet:void 0,onMouseDown:()=>{if(onSelect(node.id),hasChildren)onToggle(node.id)},children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[indent,expandIcon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:color,children:[icon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:selected?"#ffffff":palette.text,children:node.label},void 0,!1,void 0,this),suffix?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:suffix},void 0,!1,void 0,this):null,node.agentState?import_jsx_dev_runtime2.jsxDEV("span",{fg:getStateColor(node.agentState),children:[" ",node.agentState]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this)})});function Nav({onTmuxSessionSelect,workspaceRoot,initialAgent}){let[diagnostics,setDiagnostics]=import_react15.useState(null),[sessionTree,setSessionTree]=import_react15.useState([]),[selectedIndex,setSelectedIndex]=import_react15.useState(0),lastTarget=import_react15.useRef(null),initialSelectDone=import_react15.useRef(!1);import_react15.useEffect(()=>{let active=!0;async function refresh(){try{let snap=await collectDiagnostics();if(active)setDiagnostics(snap)}catch(err){console.error("TUI: diagnostics failed:",err)}}refresh();let timer2=setInterval(refresh,2000);return()=>{active=!1,clearInterval(timer2)}},[]),import_react15.useEffect(()=>{if(!diagnostics)return;let newTree;if(workspaceRoot){let agentNames=scanAgents2(workspaceRoot);newTree=buildWorkspaceTree({agentNames,sessions:diagnostics.sessions,executors:diagnostics.executors})}else newTree=buildSessionTree(diagnostics);setSessionTree((prev)=>mergeExpandedState(prev,newTree))},[diagnostics,workspaceRoot]);let flatNodes=import_react15.useMemo(()=>flattenTree(sessionTree),[sessionTree]);import_react15.useEffect(()=>{if(flatNodes.length>0&&selectedIndex>=flatNodes.length)setSelectedIndex(flatNodes.length-1)},[flatNodes.length,selectedIndex]),import_react15.useEffect(()=>{if(!initialAgent||initialSelectDone.current||flatNodes.length===0)return;let idx=flatNodes.findIndex((n)=>n.node.id===`agent:${initialAgent}`);if(idx>=0)setSelectedIndex(idx),initialSelectDone.current=!0},[initialAgent,flatNodes]),import_react15.useEffect(()=>{let current=flatNodes[selectedIndex]?.node;if(!current)return;let target=getSessionTarget(current);if(!target)return;let key=`${target.sessionName}:${target.windowIndex??""}`;if(key===lastTarget.current)return;if(lastTarget.current=key,current.type==="agent"&&current.wsAgentState!=="running")return;onTmuxSessionSelect(target.sessionName,target.windowIndex)},[selectedIndex,flatNodes,onTmuxSessionSelect]);let handleSelect=import_react15.useCallback((id)=>{let idx=flatNodes.findIndex((n)=>n.node.id===id);if(idx>=0)setSelectedIndex(idx)},[flatNodes]),handleToggle=import_react15.useCallback((id)=>{setSessionTree((prev)=>toggleNode(prev,id))},[]),handleVerticalNav=import_react15.useCallback((keyName2)=>{let rowCount=flatNodes.length;if(rowCount===0)return;if(keyName2==="up"||keyName2==="k")setSelectedIndex((prev)=>prev===0?rowCount-1:prev-1);else if(keyName2==="down"||keyName2==="j")setSelectedIndex((prev)=>prev>=rowCount-1?0:prev+1)},[flatNodes.length]),handleExpandCollapse=import_react15.useCallback((keyName2)=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if((keyName2==="right"||keyName2==="l")&&node.children.length>0&&!node.expanded)handleToggle(node.id);else if((keyName2==="left"||keyName2==="h")&&node.expanded)handleToggle(node.id)},[flatNodes,selectedIndex,handleToggle]),handleEnter=import_react15.useCallback(()=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if(node.type==="agent"){if(node.wsAgentState==="stopped")spawnAgent(node.label);let target2=getSessionTarget(node);if(target2)onTmuxSessionSelect(target2.sessionName,target2.windowIndex);return}if(node.children.length>0)handleToggle(node.id);let target=getSessionTarget(node);if(target)onTmuxSessionSelect(target.sessionName,target.windowIndex)},[flatNodes,selectedIndex,handleToggle,onTmuxSessionSelect]);useKeyboard((key)=>{if(key.name==="up"||key.name==="k"||key.name==="down"||key.name==="j")handleVerticalNav(key.name);else if(key.name==="right"||key.name==="l"||key.name==="left"||key.name==="h")handleExpandCollapse(key.name);else if(key.name==="enter"||key.name==="return")handleEnter()});let agentCount=workspaceRoot?sessionTree.filter((n)=>n.type==="agent").length:diagnostics?.sessions.length??0,runningCount=workspaceRoot?sessionTree.filter((n)=>n.wsAgentState==="running").length:diagnostics?.sessions.reduce((sum,s)=>sum+s.windows.reduce((ws,w2)=>ws+w2.panes.length,0),0)??0,headerLabel=workspaceRoot?"Agents":"Sessions";return import_jsx_dev_runtime2.jsxDEV("box",{flexDirection:"column",width:"100%",height:"100%",backgroundColor:palette.bg,children:[import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.purple,children:headerLabel},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[" ",workspaceRoot?`${runningCount}/${agentCount}`:`${agentCount}s ${runningCount}p`]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("scrollbox",{focused:!0,height:"100%",style:{scrollbarOptions:{showArrows:!1,trackOptions:{foregroundColor:palette.scrollThumb,backgroundColor:palette.scrollTrack}}},children:flatNodes.map((flat,i2)=>import_jsx_dev_runtime2.jsxDEV(TreeNodeRow,{node:flat.node,selected:i2===selectedIndex,onSelect:handleSelect,onToggle:handleToggle},flat.node.id,!1,void 0,this))},void 0,!1,void 0,this):import_jsx_dev_runtime2.jsxDEV("box",{flexGrow:1,justifyContent:"center",alignItems:"center",children:import_jsx_dev_runtime2.jsxDEV("text",{fg:palette.textDim,children:"Collecting..."},void 0,!1,void 0,this)},void 0,!1,void 0,this),import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textMuted,children:["\u2191\u2193",":nav ","\u2190\u2192",":expand Enter:",workspaceRoot?"spawn/attach":"attach"]},void 0,!0,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function spawnAgent(name){try{let{spawn:spawn4}=__require("child_process"),{join:join53,resolve:resolve10}=__require("path"),{existsSync:existsSync40}=__require("fs"),wsRoot=process.env.GENIE_TUI_WORKSPACE,cwd;if(wsRoot){let agentDir=resolve10(join53(wsRoot,"agents",name));if(existsSync40(agentDir))cwd=agentDir}spawn4("genie",["spawn",name,"--session",name],{detached:!0,stdio:"ignore",cwd}).unref()}catch{}}function mergeExpandedState(oldTree,newTree){if(oldTree.length===0)return newTree;let oldState=new Map;function collect(nodes){for(let n of nodes)oldState.set(n.id,n.expanded),collect(n.children)}collect(oldTree);function apply(nodes){return nodes.map((n)=>({...n,expanded:oldState.has(n.id)?oldState.get(n.id):n.expanded,children:apply(n.children)}))}return apply(newTree)}var import_react15;var init_Nav=__esm(async()=>{init_workspace();init_diagnostics();init_theme2();init_TreeNode();init_jsx_dev_runtime();await init_react();import_react15=__toESM(require_react_development(),1)});function App({rightPane,workspaceRoot,initialAgent}){let handleTmuxSessionSelect=import_react16.useCallback((sessionName,windowIndex)=>{if(!rightPane)return;attachProjectWindow(rightPane,sessionName,windowIndex)},[rightPane]);return import_jsx_dev_runtime2.jsxDEV(Nav,{onTmuxSessionSelect:handleTmuxSessionSelect,workspaceRoot,initialAgent},void 0,!1,void 0,this)}var import_react16;var init_app=__esm(async()=>{init_tmux2();init_jsx_dev_runtime();await init_Nav();import_react16=__toESM(require_react_development(),1)});var exports_render={};__export(exports_render,{renderNav:()=>renderNav});async function renderNav(){let rightPane=process.env.GENIE_TUI_RIGHT||void 0,workspaceRoot=process.env.GENIE_TUI_WORKSPACE||void 0,initialAgent=process.env.GENIE_TUI_AGENT||void 0,renderer=await createCliRenderer({exitOnCtrlC:!1,useMouse:!0});createRoot(renderer).render(import_jsx_dev_runtime2.jsxDEV(App,{rightPane,workspaceRoot,initialAgent},void 0,!1,void 0,this)),await new Promise((resolve10)=>{renderer.once("destroy",resolve10)})}var init_render=__esm(async()=>{init_jsx_dev_runtime();await __promiseAll([init_core(),init_react(),init_app()])});var exports_tui={};__export(exports_tui,{launchTui:()=>launchTui});async function launchTui(){let{renderNav:renderNav2}=await init_render().then(() => exports_render);await renderNav2()}var import__=__toESM(require_commander(),1),{program,createCommand,createArgument,createOption,CommanderError,InvalidArgumentError,InvalidOptionArgumentError,Command,Argument,Option,Help}=import__.default;import{existsSync as existsSync3,unlinkSync as unlinkSync2}from"fs";import{homedir as homedir3}from"os";import{join as join3}from"path";var{$:$2}=globalThis.Bun;import{existsSync,unlinkSync}from"fs";import{homedir}from"os";import{join}from"path";var CLAUDE_DIR=join(homedir(),".claude"),CLAUDE_HOOKS_DIR=join(CLAUDE_DIR,"hooks"),CLAUDE_SETTINGS_FILE=join(CLAUDE_DIR,"settings.json"),GENIE_HOOK_SCRIPT_NAME="genie-bash-hook.sh";function getClaudeSettingsPath(){return CLAUDE_SETTINGS_FILE}function getGenieHookScriptPath(){return join(CLAUDE_HOOKS_DIR,GENIE_HOOK_SCRIPT_NAME)}function hookScriptExists(){return existsSync(getGenieHookScriptPath())}function removeHookScript(){let scriptPath=getGenieHookScriptPath();if(existsSync(scriptPath))unlinkSync(scriptPath)}function contractClaudePath(path){let home=homedir();if(path.startsWith(`${home}/`))return`~${path.slice(home.length)}`;if(path===home)return"~";return path}init_genie_config2();var{$}=globalThis.Bun;async function checkCommand(cmd){try{let cmdPath=(await $`which ${cmd}`.quiet().text()).trim();if(!cmdPath)return{exists:!1};let version;try{let firstLine=(await $`${cmd} --version`.quiet().text()).split(`
2076
2075
  `)[0].trim(),versionMatch=firstLine.match(/(\d+\.[\d.]+[a-z0-9-]*)/i);version=versionMatch?versionMatch[1]:firstLine.slice(0,50)}catch{try{let firstLine=(await $`${cmd} -v`.quiet().text()).split(`
2077
2076
  `)[0].trim(),versionMatch=firstLine.match(/(\d+\.[\d.]+[a-z0-9-]*)/i);version=versionMatch?versionMatch[1]:firstLine.slice(0,50)}catch{}}return{exists:!0,version,path:cmdPath}}catch{return{exists:!1}}}function printSectionHeader(title){console.log(),console.log(`\x1B[1m${title}:\x1B[0m`)}function printCheckResult(result){let icon={pass:"\x1B[32m\u2713\x1B[0m",fail:"\x1B[31m\u2717\x1B[0m",warn:"\x1B[33m!\x1B[0m"}[result.status],message=result.message?` ${result.message}`:"";if(console.log(` ${icon} ${result.name}${message}`),result.suggestion)console.log(` \x1B[2m${result.suggestion}\x1B[0m`)}async function checkPrerequisites(){let results=[],tmuxCheck=await checkCommand("tmux");if(tmuxCheck.exists)results.push({name:"tmux",status:"pass",message:tmuxCheck.version||""});else results.push({name:"tmux",status:"fail",suggestion:"Install with: brew install tmux (or apt install tmux)"});let jqCheck=await checkCommand("jq");if(jqCheck.exists)results.push({name:"jq",status:"pass",message:jqCheck.version||""});else results.push({name:"jq",status:"fail",suggestion:"Install with: brew install jq (or apt install jq)"});let bunCheck=await checkCommand("bun");if(bunCheck.exists)results.push({name:"bun",status:"pass",message:bunCheck.version||""});else results.push({name:"bun",status:"fail",suggestion:"Install with: curl -fsSL https://bun.sh/install | bash"});let claudeCheck=await checkCommand("claude");if(claudeCheck.exists)results.push({name:"Claude Code",status:"pass",message:claudeCheck.version||""});else results.push({name:"Claude Code",status:"warn",suggestion:"Install with: npm install -g @anthropic-ai/claude-code"});return results}async function checkConfiguration(){let results=[];if(genieConfigExists())results.push({name:"Genie config exists",status:"pass",message:contractClaudePath(getGenieConfigPath())});else results.push({name:"Genie config exists",status:"warn",message:"not found",suggestion:"Run: genie setup"});if(isSetupComplete())results.push({name:"Setup complete",status:"pass"});else results.push({name:"Setup complete",status:"warn",message:"not completed",suggestion:"Run: genie setup"});let claudeSettingsPath=getClaudeSettingsPath();if(existsSync3(claudeSettingsPath))results.push({name:"Claude settings exists",status:"pass",message:contractClaudePath(claudeSettingsPath)});else results.push({name:"Claude settings exists",status:"warn",message:"not found",suggestion:"Claude Code creates this on first run"});return results}async function checkTmux(){let results=[];try{if((await $2`tmux -L genie list-sessions 2>/dev/null`.quiet()).exitCode===0)results.push({name:"Server running",status:"pass"});else return results.push({name:"Server running",status:"warn",message:"no sessions",suggestion:"Start with: tmux new-session -d -s genie"}),results}catch{return results.push({name:"Server running",status:"warn",message:"could not check"}),results}let sessionName=(await loadGenieConfig()).session.name;try{if((await $2`tmux -L genie has-session -t ${sessionName} 2>/dev/null`.quiet()).exitCode===0)results.push({name:`Session '${sessionName}' exists`,status:"pass"});else results.push({name:`Session '${sessionName}' exists`,status:"warn",suggestion:`Start with: tmux new-session -d -s ${sessionName}`})}catch{results.push({name:`Session '${sessionName}' exists`,status:"warn",message:"could not check"})}return results}async function checkWorkerProfiles(){let results=[];if(!genieConfigExists())return results.push({name:"Worker profiles",status:"warn",message:"no genie config",suggestion:"Run: genie setup"}),results;let config=await loadGenieConfig(),profiles=config.workerProfiles;if(!profiles||Object.keys(profiles).length===0)return results.push({name:"Worker profiles",status:"pass",message:"none configured (using defaults)"}),results;let totalProfiles=Object.keys(profiles).length;results.push({name:"Profiles configured",status:"pass",message:`${totalProfiles} profile${totalProfiles===1?"":"s"}`});for(let name of Object.keys(profiles))results.push({name:`Profile '${name}'`,status:"pass",message:"claude (direct)"});if(config.defaultWorkerProfile)if(profiles[config.defaultWorkerProfile])results.push({name:"Default profile",status:"pass",message:config.defaultWorkerProfile});else results.push({name:"Default profile",status:"warn",message:`'${config.defaultWorkerProfile}' not found`,suggestion:"Run: genie profiles default <profile>"});return results}function runCheckSection(label,results,counts){printSectionHeader(label);for(let result of results){if(printCheckResult(result),result.status==="fail")counts.errors=!0;if(result.status==="warn")counts.warnings=!0}}async function doctorCommand(options){if(options?.fix){await doctorFix();return}console.log(),console.log("\x1B[1mGenie Doctor\x1B[0m"),console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`);let counts={errors:!1,warnings:!1};if(runCheckSection("Prerequisites",await checkPrerequisites(),counts),runCheckSection("Configuration",await checkConfiguration(),counts),runCheckSection("Tmux",await checkTmux(),counts),runCheckSection("Worker Profiles",await checkWorkerProfiles(),counts),console.log(),console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`),counts.errors)console.log("\x1B[31mSome checks failed.\x1B[0m Run \x1B[36mgenie setup\x1B[0m to fix.");else if(counts.warnings)console.log("\x1B[33mSome warnings detected.\x1B[0m Everything should still work.");else console.log("\x1B[32mAll checks passed!\x1B[0m");if(console.log(),counts.errors)process.exit(1)}async function killStalePostgres(){console.log(" Killing stale postgres processes...");try{let{execSync}=await import("child_process");execSync('pkill -9 -f "postgres.*pgserve" 2>/dev/null || true',{stdio:"ignore",timeout:5000}),console.log(" \x1B[32m\u2713\x1B[0m Stale postgres processes killed")}catch{console.log(" \x1B[33m!\x1B[0m Could not kill stale postgres processes")}}async function cleanSharedMemory(){console.log(" Cleaning shared memory...");try{let{execSync}=await import("child_process");execSync("ipcs -m 2>/dev/null | awk '$6 == 0 {print $2}' | xargs -I{} ipcrm -m {} 2>/dev/null || true",{stdio:"ignore",timeout:5000}),console.log(" \x1B[32m\u2713\x1B[0m Shared memory cleaned")}catch{console.log(" \x1B[32m\u2713\x1B[0m No stale shared memory")}}async function stopExistingDaemon(pidFile){try{let{readFileSync:readFileSync2}=await import("fs"),pid=Number.parseInt(readFileSync2(pidFile,"utf-8").trim(),10);if(!Number.isNaN(pid)&&pid>0)try{process.kill(pid,"SIGTERM"),console.log(` \x1B[32m\u2713\x1B[0m Stopped existing daemon (PID ${pid})`),await new Promise((r)=>setTimeout(r,1000))}catch{}}catch{}}function removeStaleFiles(genieHome,pidFile){let portFile=join3(genieHome,"pgserve.port"),postmasterPid=join3(genieHome,"data","pgserve","postmaster.pid");for(let file of[portFile,pidFile,postmasterPid])if(existsSync3(file))try{unlinkSync2(file),console.log(` \x1B[32m\u2713\x1B[0m Removed ${file}`)}catch{console.log(` \x1B[33m!\x1B[0m Could not remove ${file}`)}}async function restartDaemon(){console.log(" Restarting daemon...");try{let{spawn}=await import("child_process"),bunPath=process.execPath??"bun",genieBin=process.argv[1]??"genie";spawn(bunPath,[genieBin,"daemon","start"],{detached:!0,stdio:"ignore"}).unref(),await new Promise((resolve)=>setTimeout(resolve,2000)),console.log(" \x1B[32m\u2713\x1B[0m Daemon restart initiated")}catch{console.log(" \x1B[33m!\x1B[0m Could not restart daemon \u2014 run: genie daemon start")}}async function doctorFix(){console.log(`
2078
2077
  \x1B[1mGenie Doctor \u2014 Auto Fix\x1B[0m`),console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m
@@ -2,7 +2,7 @@
2
2
  "id": "genie",
3
3
  "name": "Genie",
4
4
  "description": "Skills, agents, and hooks for the Genie CLI terminal orchestration toolkit",
5
- "version": "4.260330.22",
5
+ "version": "4.260330.24",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260330.22",
3
+ "version": "4.260330.24",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260330.22",
3
+ "version": "4.260330.24",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260330.22",
3
+ "version": "4.260330.24",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -33,3 +33,8 @@ set -g pane-border-status off
33
33
  # Pane borders (visual only, no #() shell commands)
34
34
  set -g pane-border-style "fg=#0f3460"
35
35
  set -g pane-active-border-style "fg=#7b2ff7"
36
+
37
+ # Unbind prefix key — all keystrokes go directly to the active pane
38
+ # TUI keybindings are set in the root table by serve.ts
39
+ set -g prefix None
40
+ unbind C-b
@@ -5,7 +5,7 @@
5
5
  * - pgserve (database)
6
6
  * - tmux -L genie server (agent sessions)
7
7
  * - Agent sessions from workspace manifest
8
- * - tmux -L genie-tui server (TUI display)
8
+ * - TUI session on default tmux server
9
9
  * - Scheduler, event-router, inbox-watcher
10
10
  * - PID file at .genie/serve.pid
11
11
  *
@@ -39,10 +39,7 @@ function genieTmuxConf(): string {
39
39
  return candidates.find((p) => existsSync(p)) ?? '/dev/null';
40
40
  }
41
41
 
42
- function tuiTmuxConf(): string {
43
- const candidates = [join(genieHome(), 'tui-tmux.conf')];
44
- return candidates.find((p) => existsSync(p)) ?? '/dev/null';
45
- }
42
+ // TUI uses default tmux server (no separate socket or config)
46
43
 
47
44
  // ============================================================================
48
45
  // PID helpers
@@ -85,25 +82,27 @@ function isProcessAlive(pid: number): boolean {
85
82
  // ============================================================================
86
83
 
87
84
  const GENIE_SOCKET = 'genie';
88
- const TUI_SOCKET = 'genie-tui';
89
85
  const TUI_SESSION = 'genie-tui';
90
86
 
91
- function tmuxCmd(socket: string, conf: string, subcmd: string): string {
92
- return `tmux -L ${socket} -f ${conf} ${subcmd}`;
87
+ function genieTmux(subcmd: string): string {
88
+ return `tmux -L ${GENIE_SOCKET} -f ${genieTmuxConf()} ${subcmd}`;
93
89
  }
94
90
 
95
- function genieTmux(subcmd: string): string {
96
- return tmuxCmd(GENIE_SOCKET, genieTmuxConf(), subcmd);
91
+ /** TUI tmux config — minimal, no shell probes, no prefix key interference */
92
+ function tuiTmuxConf(): string {
93
+ const candidates = [join(genieHome(), 'tui-tmux.conf')];
94
+ return candidates.find((p) => existsSync(p)) ?? '/dev/null';
97
95
  }
98
96
 
97
+ /** TUI tmux command — uses -L genie-tui socket + minimal TUI config */
99
98
  function tuiTmux(subcmd: string): string {
100
- return tmuxCmd(TUI_SOCKET, tuiTmuxConf(), subcmd);
99
+ return `tmux -L genie-tui -f ${tuiTmuxConf()} ${subcmd}`;
101
100
  }
102
101
 
103
102
  /** Check if a tmux server is running on a socket */
104
- function isTmuxServerRunning(socket: string, conf: string): boolean {
103
+ function isGenieTmuxRunning(): boolean {
105
104
  try {
106
- execSync(tmuxCmd(socket, conf, 'list-sessions'), { stdio: 'ignore' });
105
+ execSync(genieTmux('list-sessions'), { stdio: 'ignore' });
107
106
  return true;
108
107
  } catch {
109
108
  return false;
@@ -111,7 +110,6 @@ function isTmuxServerRunning(socket: string, conf: string): boolean {
111
110
  }
112
111
 
113
112
  const NAV_WIDTH = 30;
114
- const KEY_TABLE = 'genie-tui';
115
113
 
116
114
  /** Theme colors for TUI tmux styling */
117
115
  const TUI_STYLE = {
@@ -124,7 +122,7 @@ function applyTuiStyle(): void {
124
122
  const cmds = [
125
123
  `set-option -t ${TUI_SESSION} pane-border-style 'fg=${TUI_STYLE.inactiveBorder}'`,
126
124
  `set-option -t ${TUI_SESSION} pane-active-border-style 'fg=${TUI_STYLE.activeBorder}'`,
127
- `set-option -t ${TUI_SESSION} mouse off`,
125
+ `set-option -t ${TUI_SESSION} mouse on`,
128
126
  `set-option -t ${TUI_SESSION} status off`,
129
127
  `set-option -t ${TUI_SESSION} pane-border-status off`,
130
128
  ];
@@ -135,18 +133,17 @@ function applyTuiStyle(): void {
135
133
  }
136
134
  }
137
135
 
138
- /** Set up keybindings in a dedicated key table for TUI */
136
+ /** Set up keybindings in root table so they work immediately */
139
137
  function setupTuiKeybindings(): void {
140
138
  const bindings = [
141
139
  // Tab: toggle focus between left nav (pane 0) and right terminal (pane 1)
142
- `bind-key -T ${KEY_TABLE} Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0" \\; switch-client -T ${KEY_TABLE}`,
140
+ `bind-key -T root Tab if-shell "[ '#{pane_index}' = '0' ]" "select-pane -t ${TUI_SESSION}:0.1" "select-pane -t ${TUI_SESSION}:0.0"`,
143
141
  // Ctrl+B: toggle sidebar width (collapse/expand)
144
- `bind-key -T ${KEY_TABLE} C-b if-shell "[ $(tmux -L ${TUI_SOCKET} display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}" \\; switch-client -T ${KEY_TABLE}`,
145
- // Ctrl+Q: detach from TUI (don't kill serve owns the session)
146
- `bind-key -T ${KEY_TABLE} C-q detach-client`,
147
- `bind-key -T ${KEY_TABLE} 'C-\\\\' detach-client`,
148
- // Activate key table on session attach
149
- `set-hook -t ${TUI_SESSION} client-session-changed "switch-client -T ${KEY_TABLE}"`,
142
+ `bind-key -T root C-b if-shell "[ $(tmux display-message -p '#\\{pane_width\\}' -t ${TUI_SESSION}:0.0) -gt 5 ]" "resize-pane -t ${TUI_SESSION}:0.0 -x 0" "resize-pane -t ${TUI_SESSION}:0.0 -x ${NAV_WIDTH}"`,
143
+ // Ctrl+T: new window in agent session (sends C-b c to the nested tmux in right pane)
144
+ `bind-key -T root C-t send-keys -t ${TUI_SESSION}:0.1 C-b c`,
145
+ // Ctrl+Q: detach from TUI
146
+ 'bind-key -T root C-q detach-client',
150
147
  ];
151
148
  for (const cmd of bindings) {
152
149
  try {
@@ -176,7 +173,7 @@ function startTuiTmuxServer(): { leftPane: string; rightPane: string } {
176
173
  const cols = 120;
177
174
  const rows = 40;
178
175
 
179
- execSync(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${rows} -e GENIE_TUI_PANE=left`), {
176
+ execSync(tuiTmux(`new-session -d -s ${TUI_SESSION} -x ${cols} -y ${rows}`), {
180
177
  stdio: 'ignore',
181
178
  });
182
179
  execSync(tuiTmux(`split-window -h -t ${TUI_SESSION}:0 -l ${cols - NAV_WIDTH - 1}`), { stdio: 'ignore' });
@@ -217,10 +214,10 @@ function sendTuiLaunchScript(leftPane: string, rightPane: string, workspaceRoot?
217
214
  } catch {}
218
215
  }
219
216
 
220
- /** Kill a tmux server by socket */
221
- function killTmuxServer(socket: string, conf: string): void {
217
+ /** Kill the TUI tmux server */
218
+ function killTuiSession(): void {
222
219
  try {
223
- execSync(tmuxCmd(socket, conf, 'kill-server'), { stdio: 'ignore' });
220
+ execSync(tuiTmux('kill-server'), { stdio: 'ignore' });
224
221
  } catch {
225
222
  // not running
226
223
  }
@@ -248,7 +245,7 @@ export function isServeRunning(): boolean {
248
245
 
249
246
  /**
250
247
  * Auto-start genie serve in daemon mode and wait until ready.
251
- * Ready = PID file exists + PID alive + genie-tui session exists on -L genie-tui.
248
+ * Ready = PID file exists + PID alive + genie-tui session exists on default server.
252
249
  */
253
250
  export async function autoStartServe(): Promise<void> {
254
251
  if (isServeRunning()) return;
@@ -279,7 +276,7 @@ export async function autoStartServe(): Promise<void> {
279
276
  /** Check if the genie-tui session exists on the TUI socket */
280
277
  export function isTuiSessionReady(): boolean {
281
278
  try {
282
- execSync(tmuxCmd(TUI_SOCKET, tuiTmuxConf(), `has-session -t ${TUI_SESSION}`), { stdio: 'ignore' });
279
+ execSync(tuiTmux(`has-session -t ${TUI_SESSION}`), { stdio: 'ignore' });
283
280
  return true;
284
281
  } catch {
285
282
  return false;
@@ -378,8 +375,8 @@ async function startForeground(): Promise<void> {
378
375
  // 2b. Sync agent directory + start watcher
379
376
  handles.agentWatcher = await startAgentSync();
380
377
 
381
- // 3. Start TUI tmux server with split layout
382
- console.log(` Starting tmux -L ${TUI_SOCKET} server...`);
378
+ // 3. Start TUI session on default tmux server
379
+ console.log(' Setting up TUI session...');
383
380
  const { leftPane, rightPane } = startTuiTmuxServer();
384
381
 
385
382
  // 4. Send launch script to left pane (discovers workspace from serve cwd)
@@ -412,9 +409,9 @@ async function startForeground(): Promise<void> {
412
409
  console.log('\nShutting down genie serve...');
413
410
  handles.agentWatcher?.close();
414
411
  handles.schedulerHandle?.stop();
415
- killTmuxServer(TUI_SOCKET, tuiTmuxConf());
412
+ killTuiSession();
416
413
  // NEVER kill the agent tmux server — agent sessions are eternal and must
417
- // survive serve restarts. Only the TUI display server is owned by serve.
414
+ // survive serve restarts. Only the TUI session is owned by serve.
418
415
  removeServePid();
419
416
  console.log('genie serve stopped.');
420
417
  };
@@ -485,8 +482,8 @@ async function stopServe(): Promise<void> {
485
482
  if (!isProcessAlive(pid)) {
486
483
  console.log(`Stale PID file (PID ${pid} not running). Cleaning up.`);
487
484
  removeServePid();
488
- // Only kill TUI server — agent server is independent
489
- killTmuxServer(TUI_SOCKET, tuiTmuxConf());
485
+ // Only kill TUI session — agent server is independent
486
+ killTuiSession();
490
487
  return;
491
488
  }
492
489
 
@@ -518,8 +515,8 @@ async function stopServe(): Promise<void> {
518
515
  }
519
516
  }
520
517
 
521
- // Only kill TUI display server — agent tmux server is eternal
522
- killTmuxServer(TUI_SOCKET, tuiTmuxConf());
518
+ // Only kill TUI session — agent tmux server is eternal
519
+ killTuiSession();
523
520
 
524
521
  removeServePid();
525
522
  console.log('genie serve stopped.');
@@ -538,15 +535,15 @@ async function printPgserveStatus(): Promise<void> {
538
535
 
539
536
  /** Print tmux server statuses */
540
537
  function printTmuxStatus(): void {
541
- const agentRunning = isTmuxServerRunning(GENIE_SOCKET, genieTmuxConf());
538
+ const agentRunning = isGenieTmuxRunning();
542
539
  const sessions = agentRunning ? listAgentSessions() : [];
543
540
  console.log(` tmux -L ${GENIE_SOCKET}: ${agentRunning ? `running (${sessions.length} sessions)` : 'stopped'}`);
544
541
  if (sessions.length > 0) {
545
542
  console.log(` ${sessions.join(', ')}`);
546
543
  }
547
544
 
548
- const tuiRunning = isTmuxServerRunning(TUI_SOCKET, tuiTmuxConf());
549
- console.log(` tmux -L ${TUI_SOCKET}: ${tuiRunning ? 'running' : 'stopped'}`);
545
+ const tuiReady = isTuiSessionReady();
546
+ console.log(` tmux -L genie-tui: ${tuiReady ? 'running' : 'stopped'}`);
550
547
  }
551
548
 
552
549
  /** Print scheduler and inbox status */
@@ -24,7 +24,7 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
24
24
  const [sessionTree, setSessionTree] = useState<TreeNode[]>([]);
25
25
  const [selectedIndex, setSelectedIndex] = useState(0);
26
26
  const lastTarget = useRef<string | null>(null);
27
- const genieHome = useRef(process.env.GENIE_HOME ?? `${process.env.HOME}/.genie`);
27
+ const initialSelectDone = useRef(false);
28
28
 
29
29
  // Refresh diagnostics every 2s
30
30
  useEffect(() => {
@@ -76,34 +76,15 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
76
76
  }
77
77
  }, [flatNodes.length, selectedIndex]);
78
78
 
79
- // File-based initial agent: thin client writes ~/.genie/tui-initial-agent before attaching.
80
- // Check on each diagnostics refresh (2s) so re-attach from a different agent dir works.
81
- const [pendingAgent, setPendingAgent] = useState<string | undefined>(initialAgent);
82
-
79
+ // Initial agent pre-selection (once)
83
80
  useEffect(() => {
84
- if (!diagnostics) return;
85
- try {
86
- const fs = require('node:fs') as typeof import('node:fs');
87
- const agentFile = `${genieHome.current}/tui-initial-agent`;
88
- if (fs.existsSync(agentFile)) {
89
- const agent = fs.readFileSync(agentFile, 'utf-8').trim();
90
- fs.unlinkSync(agentFile);
91
- if (agent) setPendingAgent(agent);
92
- }
93
- } catch {
94
- // best-effort
95
- }
96
- }, [diagnostics]);
97
-
98
- // Apply pending agent selection (from prop or file)
99
- useEffect(() => {
100
- if (!pendingAgent || flatNodes.length === 0) return;
101
- const idx = flatNodes.findIndex((n) => n.node.id === `agent:${pendingAgent}`);
81
+ if (!initialAgent || initialSelectDone.current || flatNodes.length === 0) return;
82
+ const idx = flatNodes.findIndex((n) => n.node.id === `agent:${initialAgent}`);
102
83
  if (idx >= 0) {
103
84
  setSelectedIndex(idx);
104
- setPendingAgent(undefined);
85
+ initialSelectDone.current = true;
105
86
  }
106
- }, [pendingAgent, flatNodes]);
87
+ }, [initialAgent, flatNodes]);
107
88
 
108
89
  // Auto-switch right pane when cursor moves to a new target
109
90
  useEffect(() => {
@@ -162,11 +143,12 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
162
143
  const node = flatNodes[selectedIndex]?.node;
163
144
  if (!node) return;
164
145
 
165
- // Agent node: spawn if not running, then attach
166
146
  if (node.type === 'agent') {
167
- if (node.wsAgentState !== 'running') {
147
+ // No session → spawn the agent (creates session + window 0 with Claude)
148
+ if (node.wsAgentState === 'stopped') {
168
149
  spawnAgent(node.label);
169
150
  }
151
+ // Attach right pane to the agent's session (existing or just-spawned)
170
152
  const target = getSessionTarget(node);
171
153
  if (target) onTmuxSessionSelect(target.sessionName, target.windowIndex);
172
154
  return;
@@ -255,14 +237,19 @@ export function Nav({ onTmuxSessionSelect, workspaceRoot, initialAgent }: NavPro
255
237
  );
256
238
  }
257
239
 
258
- /** Spawn a stopped agent by launching `genie spawn <name>` in background */
240
+ /** Spawn a stopped agent by launching `genie spawn <name>` from its workspace directory */
259
241
  function spawnAgent(name: string): void {
260
242
  try {
261
243
  const { spawn } = require('node:child_process') as typeof import('node:child_process');
262
- spawn('genie', ['spawn', name], {
263
- detached: true,
264
- stdio: 'ignore',
265
- }).unref();
244
+ const { join, resolve } = require('node:path') as typeof import('node:path');
245
+ const { existsSync } = require('node:fs') as typeof import('node:fs');
246
+ const wsRoot = process.env.GENIE_TUI_WORKSPACE;
247
+ let cwd: string | undefined;
248
+ if (wsRoot) {
249
+ const agentDir = resolve(join(wsRoot, 'agents', name));
250
+ if (existsSync(agentDir)) cwd = agentDir;
251
+ }
252
+ spawn('genie', ['spawn', name, '--session', name], { detached: true, stdio: 'ignore', cwd }).unref();
266
253
  } catch {
267
254
  // best-effort spawn
268
255
  }
package/src/tui/tmux.ts CHANGED
@@ -8,29 +8,16 @@
8
8
  import { execSync, spawnSync } from 'node:child_process';
9
9
 
10
10
  const SESSION_NAME = 'genie-tui';
11
- /** TUI's own tmux socket — isolates the nav+split from everything else */
12
11
  const TMUX_SOCKET = 'genie-tui';
13
- /** Genie's agent tmux socket — where all agents/teams/sessions live. */
14
12
  const GENIE_AGENT_SOCKET = 'genie';
15
- /**
16
- * TUI tmux config — minimal config WITHOUT shell probes.
17
- * The full genie.tmux.conf has #() shell commands in pane-border-format that
18
- * cause garbled escape sequences when the TUI attaches to agent sessions.
19
- * Falls back to /dev/null if tui config not found.
20
- */
21
13
  const TUI_TMUX_CONF = (() => {
22
14
  const { existsSync } = require('node:fs') as typeof import('node:fs');
23
15
  const home = process.env.GENIE_HOME ?? `${process.env.HOME}/.genie`;
24
16
  const tuiConf = `${home}/tui-tmux.conf`;
25
17
  return existsSync(tuiConf) ? tuiConf : '/dev/null';
26
18
  })();
27
- /** Prefix for all TUI tmux commands — TUI socket + TUI config (no shell probes) */
28
19
  const TMUX = `tmux -L ${TMUX_SOCKET} -f ${TUI_TMUX_CONF}`;
29
20
 
30
- /**
31
- * Resolve the right pane ID — self-healing if the pane was killed/recreated.
32
- * Falls back to re-discovering from the session layout.
33
- */
34
21
  function resolveRightPane(rightPane: string): string {
35
22
  try {
36
23
  execSync(`${TMUX} display-message -t ${rightPane} -p ''`, { stdio: 'ignore' });
@@ -47,62 +34,43 @@ function resolveRightPane(rightPane: string): string {
47
34
  }
48
35
  }
49
36
 
50
- /** Ensure a tmux session exists on the agent server, creating it if needed */
51
- function ensureAgentSession(sessionName: string): void {
37
+ /** Switch right pane to a specific agent session. NEVER kills the pane. */
38
+ export function attachProjectWindow(rightPane: string, targetSession: string, windowIndex?: number): void {
39
+ if (targetSession === SESSION_NAME) return;
40
+ const pane = resolveRightPane(rightPane);
52
41
  const agentTmux = `tmux -L ${GENIE_AGENT_SOCKET}`;
42
+
43
+ // Ensure agent session exists
53
44
  try {
54
- execSync(`${agentTmux} has-session -t '${sessionName}' 2>/dev/null`, { stdio: 'ignore' });
45
+ execSync(`${agentTmux} has-session -t '${targetSession}' 2>/dev/null`, { stdio: 'ignore' });
55
46
  } catch {
56
- try {
57
- execSync(`${agentTmux} new-session -d -s '${sessionName}'`, { stdio: 'ignore' });
58
- } catch {
59
- // race: another process created it
60
- }
47
+ return; // No session — don't create empty ones, don't kill the pane
61
48
  }
62
- }
63
49
 
64
- /** Switch right pane to a specific session window on the genie agent server */
65
- export function attachProjectWindow(rightPane: string, targetSession: string, windowIndex?: number): void {
66
- // Guard: never attach the TUI session to itself (causes infinite loop)
67
- if (targetSession === SESSION_NAME) return;
68
- const pane = resolveRightPane(rightPane);
69
- ensureAgentSession(targetSession);
70
50
  if (windowIndex !== undefined) {
71
51
  try {
72
- const agentTmux = `tmux -L ${GENIE_AGENT_SOCKET}`;
73
52
  execSync(`${agentTmux} select-window -t '${targetSession}:${windowIndex}'`, { stdio: 'ignore' });
74
- } catch {
75
- // window may not exist
76
- }
53
+ } catch {}
77
54
  }
55
+
56
+ // Hide green status bar
78
57
  try {
79
- // Write a tiny attach script that:
80
- // 1. Starts the nested tmux attach in background
81
- // 2. Sleeps to let tmux finish terminal probes
82
- // 3. Sends clear-screen escape to hide probe artifacts
83
- // This avoids the garbled "tmux 3.5a^[\^[[1;1R" output.
84
- const { writeFileSync } = require('node:fs') as typeof import('node:fs');
85
- const { join } = require('node:path') as typeof import('node:path');
86
- const home = process.env.GENIE_HOME ?? `${process.env.HOME}/.genie`;
87
- const script = join(home, 'tui-attach.sh');
88
- writeFileSync(
89
- script,
90
- [
91
- '#!/bin/sh',
92
- 'clear',
93
- `tmux -L ${GENIE_AGENT_SOCKET} set-option -t '${targetSession}' status off 2>/dev/null`,
94
- `exec tmux -L ${GENIE_AGENT_SOCKET} attach-session -t '${targetSession}'`,
95
- '',
96
- ].join('\n'),
97
- { mode: 0o755 },
98
- );
99
- execSync(`${TMUX} respawn-pane -k -t ${pane} "TMUX='' ${script}"`, { stdio: 'ignore' });
100
- } catch {
101
- // pane doesn't exist or command failed
102
- }
58
+ execSync(`${agentTmux} set-option -t '${targetSession}' status off 2>/dev/null`, { stdio: 'ignore' });
59
+ } catch {}
60
+
61
+ // respawn-pane with a loop wrapper the pane process is bash running a loop,
62
+ // so if the attach ends (agent exit, detach), the loop retries and the pane survives.
63
+ try {
64
+ const cmd = `while true; do TMUX='' ${agentTmux} attach-session -t '${targetSession}' 2>/dev/null; sleep 0.3; done`;
65
+ execSync(`${TMUX} respawn-pane -k -t ${pane} "bash -c '${cmd}'"`, { stdio: 'ignore' });
66
+ } catch {}
67
+
68
+ // Restore focus to left pane
69
+ try {
70
+ execSync(`${TMUX} select-pane -t ${SESSION_NAME}:0.0`, { stdio: 'ignore' });
71
+ } catch {}
103
72
  }
104
73
 
105
- /** Attach to the TUI session (blocking call) */
106
74
  export function attachTuiSession(): void {
107
75
  spawnSync('tmux', ['-L', TMUX_SOCKET, '-f', TUI_TMUX_CONF, 'attach-session', '-t', SESSION_NAME], {
108
76
  stdio: 'inherit',