@automagik/genie 4.260427.3 → 4.260427.5
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.
- package/README.md +1 -1
- package/dist/genie.js +8 -8
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/db/migrations/053_master_backfill_and_shadow_cleanup.sql +161 -0
- package/src/db/migrations/agents-kind.test.ts +5 -0
- package/src/db/migrations/master-backfill-and-shadow-cleanup.test.ts +310 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
-->
|
|
26
26
|
|
|
27
27
|
<!-- METRICS:START -->
|
|
28
|
-
**🚀
|
|
28
|
+
**🚀 113 commits** this week · **3 releases** · **+12.5K LoC** · **5 contributors**
|
|
29
29
|
|
|
30
30
|

|
|
31
31
|
|
package/dist/genie.js
CHANGED
|
@@ -236,15 +236,15 @@ ${readFileSync5(params.extraArgs[fileIdx+1],"utf-8")}`,params.extraArgs.splice(f
|
|
|
236
236
|
`).slice(0,10);for(let line of lines){if(!line.includes("custom-title"))continue;let entry=JSON.parse(line);if(entry.type==="custom-title"&&entry.customTitle?.toLowerCase()===needle)return!0}}catch{}return!1}function sessionExists(name,cwd){try{let home=process.env.HOME??"/root",projectDir=ccProjectDirName(cwd??process.cwd()),projectPath=join9(home,".claude","projects",projectDir),files;try{files=readdirSync3(projectPath).filter((f)=>f.endsWith(".jsonl"))}catch{return!1}let needle=name.toLowerCase();return files.some((file)=>{let full=join9(projectPath,file);return fileHasSessionName(full,needle)||fileHasSessionName(full,`${needle}-${needle}`)})}catch{return!1}}var init_team_lead_command=__esm(()=>{init_claude_native_teams();init_genie_config2()});var exports_tmux_wrapper={};__export(exports_tmux_wrapper,{prependEnvVars:()=>prependEnvVars,genieTmuxPrefix:()=>genieTmuxPrefix,genieTmuxCmd:()=>genieTmuxCmd,executeTmux:()=>executeTmux});import{exec as execCallback}from"child_process";import{existsSync as existsSync7,mkdirSync as mkdirSync5}from"fs";import{homedir as homedir6}from"os";import{join as join10}from"path";import{promisify}from"util";function resolveGenieTmuxConf(){let home=homedir6(),genieHome3=process.env.GENIE_HOME??join10(home,".genie");return[join10(genieHome3,"tmux.conf"),join10(__dirname,"..","..","scripts","tmux","genie.tmux.conf"),join10(home,".bun","install","global","node_modules","@automagik","genie","scripts","tmux","genie.tmux.conf")].find((p)=>existsSync7(p))??"/dev/null"}function genieTmuxPrefix(){return["-L",GENIE_TMUX_SOCKET,"-f",resolveGenieTmuxConf()]}function genieTmuxCmd(subcommand){return`${tmuxBin()} ${genieTmuxPrefix().join(" ")} ${subcommand}`}function prependEnvVars(command,env){if(!env||Object.keys(env).length===0)return command;return`env ${Object.entries(env).map(([k,v])=>`${k}=${v}`).join(" ")} ${command}`}function getLogDir(){let logDir=join10(homedir6(),".genie","logs","tmux");if(!existsSync7(logDir))mkdirSync5(logDir,{recursive:!0});return logDir}function stripVerboseFlags(args){return args.filter((arg)=>!/^-v+$/.test(arg))}function isTmuxDebugEnabled(){return process.env.GENIE_TMUX_DEBUG==="1"}async function executeTmux(args){let argList=typeof args==="string"?args.split(/\s+/).filter(Boolean):args,finalArgs=stripVerboseFlags(argList),debugMode=isTmuxDebugEnabled(),options={};if(debugMode)finalArgs=["-v",...finalArgs],options.cwd=getLogDir();finalArgs=[...genieTmuxPrefix(),...finalArgs];let command=`${tmuxBin()} ${finalArgs.join(" ")}`,{stdout}=await exec(command,options);return stdout.trim()}var __dirname="/home/runner/_work/genie/genie/src/lib",exec,GENIE_TMUX_SOCKET;var init_tmux_wrapper=__esm(()=>{init_ensure_tmux();exec=promisify(execCallback),GENIE_TMUX_SOCKET=process.env.GENIE_TMUX_SOCKET||"genie"});var exports_tmux={};__export(exports_tmux,{setWindowEnv:()=>setWindowEnv,resolveRepoSession:()=>resolveRepoSession,listWindows:()=>listWindows,listPanes:()=>listPanes,killWindow:()=>killWindow,killSession:()=>killSession,isTmuxServerReachable:()=>isTmuxServerReachable,isPaneProcessRunning:()=>isPaneProcessRunning,isPaneAlive:()=>isPaneAlive,getWindowEnv:()=>getWindowEnv,getCurrentSessionName:()=>getCurrentSessionName,findWindowByName:()=>findWindowByName,findSessionByName:()=>findSessionByName,executeTmux:()=>executeTmux2,ensureTeamWindow:()=>ensureTeamWindow,createSession:()=>createSession,capturePaneContent:()=>capturePaneContent,applyPaneColor:()=>applyPaneColor,TmuxUnreachableError:()=>TmuxUnreachableError});import{existsSync as existsSync8}from"fs";import{basename as basename2,join as join11}from"path";async function executeTmux2(tmuxCommand){let{executeTmux:wrapperExec}=await Promise.resolve().then(() => (init_tmux_wrapper(),exports_tmux_wrapper));try{return await wrapperExec(tmuxCommand)}catch(error){let message=error instanceof Error?error.message:String(error);throw Error(`Failed to execute tmux command: ${message}`)}}async function getCurrentSessionName(hint){if(process.env.TMUX)try{return(await executeTmux2("display-message -p '#{session_name}'")).trim()||null}catch{return null}try{let sessions=await listSessions();if(sessions.length===0)return null;if(hint){let match=sessions.find((s)=>s.name.includes(hint));if(match)return match.name}return sessions[0].name}catch{return null}}async function listSessions(){try{let output=await executeTmux2("list-sessions -F '#{session_id}:#{session_name}:#{?session_attached,1,0}:#{session_windows}'");if(!output)return[];return output.split(`
|
|
237
237
|
`).map((line)=>{let[id,name,attached,windows]=line.split(":");return{id,name,attached:attached==="1",windows:Number.parseInt(windows,10)}})}catch(error){if((error instanceof Error?error.message:String(error)).includes("no server running"))return[];throw error}}async function findSessionByName(name){try{return(await listSessions()).find((session)=>session.name===name)||null}catch(_error){return null}}async function getWindowEnv(target,varName){try{let output=await executeTmux2(`show-environment -t ${shellQuote(target)} ${shellQuote(varName)}`),prefix=`${varName}=`;if(output?.startsWith(prefix))return output.slice(prefix.length).trim();return null}catch{return null}}async function setWindowEnv(target,varName,value){await executeTmux2(`set-environment -t ${shellQuote(target)} ${shellQuote(varName)} ${shellQuote(value)}`)}async function killSession(sessionId){await executeTmux2(`kill-session -t '${sessionId}'`)}async function listWindows(sessionId){try{let output=await executeTmux2(`list-windows -t '=${sessionId}' -F '#{window_id}:#{window_name}:#{window_index}:#{?window_active,1,0}'`);if(!output)return[];return output.split(`
|
|
238
238
|
`).map((line)=>{let[id,name,indexStr,active]=line.split(":");return{id,name,index:Number.parseInt(indexStr,10),active:active==="1",sessionId}})}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("session not found"))return[];throw error}}async function listPanes(windowId){try{let output=await executeTmux2(`list-panes -t '${windowId}' -F '#{pane_id}:#{pane_title}:#{?pane_active,1,0}'`);if(!output)return[];return output.split(`
|
|
239
|
-
`).map((line)=>{let[id,title,active]=line.split(":");return{id,windowId,title,active:active==="1"}})}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("window not found"))return[];throw error}}async function capturePaneContent(paneId,lines=200,includeColors=!1){try{return await executeTmux2(`capture-pane -p ${includeColors?"-e":""} -t '${paneId}' -S -${lines} -E -`)}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("pane not found"))return"";throw error}}async function createSession(name){return await executeTmux2(`new-session -d -s "${name}" -e LC_ALL=C.UTF-8 -e LANG=C.UTF-8`),findSessionByName(name)}async function createWindow(sessionId,name,workingDir){let cdFlag=workingDir?` -c '${workingDir.replace(/'/g,"'\\''")}'`:"",output=await executeTmux2(`new-window -d -P -F '#{window_id}:#{window_index}' -t '${sessionId}:' -n '${name}'${cdFlag}`),[windowId,indexStr]=output.trim().split(":");if(!windowId)return null;try{await executeTmux2(`set-window-option -t '${windowId}' automatic-rename off`)}catch{}return{id:windowId,name,index:Number.parseInt(indexStr,10)||0,active:!1,sessionId}}async function findWindowByName(sessionId,name){return(await listWindows(sessionId)).find((w)=>w.name===name)||null}async function ensureMasterWindow(session,masterName){try{let windows=await listWindows(session);if(windows.length<2)return;let masterWindow=windows.find((w)=>w.name===masterName);if(!masterWindow)return;let minIndex=Math.min(...windows.map((w)=>w.index));if(masterWindow.index===minIndex)return;await executeTmux2(`swap-window -s '${session}:${masterWindow.index}' -t '${session}:${minIndex}'`)}catch{}}async function ensureSessionExists(name){try{await executeTmux2(`new-session -d -s "${name}" -e LC_ALL=C.UTF-8 -e LANG=C.UTF-8`)}catch(error){if((error instanceof Error?error.message:String(error)).includes("duplicate session"))return;throw error}}async function ensureTeamWindow(session,teamName,workingDir){for(let attempt=0;attempt<3;attempt++)try{return await ensureTeamWindowOnce(session,teamName,workingDir)}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("server exited")||message.includes("error connecting")){if(attempt<2){let delay=250*2**attempt;console.warn(`[genie-tmux] tmux server unreachable (attempt ${attempt+1}/3), retrying in ${delay}ms...`),await new Promise((resolve2)=>setTimeout(resolve2,delay));continue}}throw error}throw Error("Failed to ensure team window after 3 attempts")}async function ensureTeamWindowOnce(session,teamName,workingDir){await ensureSessionExists(session);let existing=await findWindowByName(session,teamName);if(existing){try{await executeTmux2(`set-window-option -t '${existing.id}' automatic-rename off`)}catch{}await rehydratePaneColorHook(existing.id);let panes2=await listPanes(existing.id),paneId2=panes2.length>0?panes2[0].id:`${session}:${teamName}.0`;return{windowId:existing.id,windowName:teamName,paneId:paneId2,created:!1}}let windowsBefore=await listWindows(session),masterBefore=windowsBefore.length>0?windowsBefore.reduce((a,b)=>a.index<=b.index?a:b):null,newWindow=await createWindow(session,teamName,workingDir);if(!newWindow)throw Error(`Failed to create team window "${teamName}" in session "${session}"`);if(masterBefore)await ensureMasterWindow(session,masterBefore.name);await rehydratePaneColorHook(newWindow.id);let panes=await listPanes(newWindow.id),paneId=panes.length>0?panes[0].id:`${session}:${teamName}.0`;return{windowId:newWindow.id,windowName:teamName,paneId,created:!0}}function ensurePaneColorScript(){let{existsSync:existsSync9,writeFileSync:writeFileSync6,mkdirSync:mkdirSync6,chmodSync:chmodSync2}=__require("fs"),{dirname:dirname4}=__require("path");if(existsSync9(PANE_COLOR_SCRIPT))return;mkdirSync6(dirname4(PANE_COLOR_SCRIPT),{recursive:!0});let bin=tmuxBin();writeFileSync6(PANE_COLOR_SCRIPT,`#!/bin/bash
|
|
239
|
+
`).map((line)=>{let[id,title,active]=line.split(":");return{id,windowId,title,active:active==="1"}})}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("window not found"))return[];throw error}}async function capturePaneContent(paneId,lines=200,includeColors=!1){try{return await executeTmux2(`capture-pane -p ${includeColors?"-e":""} -t '${paneId}' -S -${lines} -E -`)}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("pane not found"))return"";throw error}}async function createSession(name){return await executeTmux2(`new-session -d -s "${name}" -e LC_ALL=C.UTF-8 -e LANG=C.UTF-8`),findSessionByName(name)}async function createWindow(sessionId,name,workingDir){let cdFlag=workingDir?` -c '${workingDir.replace(/'/g,"'\\''")}'`:"",output=await executeTmux2(`new-window -d -P -F '#{window_id}:#{window_index}' -t '${sessionId}:' -n '${name}'${cdFlag}`),[windowId,indexStr]=output.trim().split(":");if(!windowId)return null;try{await executeTmux2(`set-window-option -t '${windowId}' automatic-rename off`)}catch{}return{id:windowId,name,index:Number.parseInt(indexStr,10)||0,active:!1,sessionId}}async function findWindowByName(sessionId,name){return(await listWindows(sessionId)).find((w)=>w.name===name)||null}async function ensureMasterWindow(session,masterName){try{let windows=await listWindows(session);if(windows.length<2)return;let masterWindow=windows.find((w)=>w.name===masterName);if(!masterWindow)return;let minIndex=Math.min(...windows.map((w)=>w.index));if(masterWindow.index===minIndex)return;await executeTmux2(`swap-window -s '${session}:${masterWindow.index}' -t '${session}:${minIndex}'`)}catch{}}async function ensureSessionExists(name){if(/^[@$]\d+$/.test(name))throw Error(`Refused to create tmux session with id-shaped name "${name}". Names matching /^[@$]\\d+$/ collide with tmux's window-id (@N) and session-id ($N) notation, producing ghost sessions that cannot be safely targeted. Pass a human-readable session name (e.g. the team or agent name) instead.`);try{await executeTmux2(`new-session -d -s "${name}" -e LC_ALL=C.UTF-8 -e LANG=C.UTF-8`)}catch(error){if((error instanceof Error?error.message:String(error)).includes("duplicate session"))return;throw error}}async function ensureTeamWindow(session,teamName,workingDir){for(let attempt=0;attempt<3;attempt++)try{return await ensureTeamWindowOnce(session,teamName,workingDir)}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("server exited")||message.includes("error connecting")){if(attempt<2){let delay=250*2**attempt;console.warn(`[genie-tmux] tmux server unreachable (attempt ${attempt+1}/3), retrying in ${delay}ms...`),await new Promise((resolve2)=>setTimeout(resolve2,delay));continue}}throw error}throw Error("Failed to ensure team window after 3 attempts")}async function ensureTeamWindowOnce(session,teamName,workingDir){await ensureSessionExists(session);let existing=await findWindowByName(session,teamName);if(existing){try{await executeTmux2(`set-window-option -t '${existing.id}' automatic-rename off`)}catch{}await rehydratePaneColorHook(existing.id);let panes2=await listPanes(existing.id),paneId2=panes2.length>0?panes2[0].id:`${session}:${teamName}.0`;return{windowId:existing.id,windowName:teamName,sessionName:session,paneId:paneId2,created:!1}}let windowsBefore=await listWindows(session),masterBefore=windowsBefore.length>0?windowsBefore.reduce((a,b)=>a.index<=b.index?a:b):null,newWindow=await createWindow(session,teamName,workingDir);if(!newWindow)throw Error(`Failed to create team window "${teamName}" in session "${session}"`);if(masterBefore)await ensureMasterWindow(session,masterBefore.name);await rehydratePaneColorHook(newWindow.id);let panes=await listPanes(newWindow.id),paneId=panes.length>0?panes[0].id:`${session}:${teamName}.0`;return{windowId:newWindow.id,windowName:teamName,sessionName:session,paneId,created:!0}}function ensurePaneColorScript(){let{existsSync:existsSync9,writeFileSync:writeFileSync6,mkdirSync:mkdirSync6,chmodSync:chmodSync2}=__require("fs"),{dirname:dirname4}=__require("path");if(existsSync9(PANE_COLOR_SCRIPT))return;mkdirSync6(dirname4(PANE_COLOR_SCRIPT),{recursive:!0});let bin=tmuxBin();writeFileSync6(PANE_COLOR_SCRIPT,`#!/bin/bash
|
|
240
240
|
# Genie tmux pane color router \u2014 reads @genie_color pane option
|
|
241
241
|
PANE_ID="$1"
|
|
242
242
|
COLOR=$(${bin} display-message -p -t "$PANE_ID" '#{@genie_color}' 2>/dev/null)
|
|
243
243
|
[ -z "$COLOR" ] && COLOR="default"
|
|
244
244
|
${bin} set-option -w pane-active-border-style "fg=$COLOR"
|
|
245
|
-
`),chmodSync2(PANE_COLOR_SCRIPT,493)}async function applyPaneColor(paneId,color,windowId){let hex=TMUX_COLOR_MAP[color]??TMUX_COLOR_MAP.blue;try{ensurePaneColorScript(),await executeTmux2(`set-option -p -t '${paneId}' @genie_color '${hex}'`);try{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));await(await getConnection2())`UPDATE executors SET pane_color = ${hex} WHERE tmux_pane_id = ${paneId}`}catch{}if(windowId)await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function rehydratePaneColorHook(windowId){try{ensurePaneColorScript(),await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function resolveRepoSession(repoPath){let derived=basename2(repoPath);try{let sessions=await listSessions(),exact=sessions.find((s)=>s.name===derived);if(exact)return exact.name;if(process.env.TMUX)try{let name=(await executeTmux2("display-message -p '#{session_name}'")).trim();if(name)return name}catch{}let partial=sessions.find((s)=>s.name.includes(derived));if(partial)return partial.name}catch{}return derived}function isTmuxSocketAlive(socketName){if(!socketName)return!1;let uid=process.getuid?.()??501;return existsSync8(join11(`/tmp/tmux-${uid}`,socketName))}async function isTmuxServerReachable(socketName){if(!socketName)return!1;if(!isTmuxSocketAlive(socketName))return!1;try{let{execSync:execSync2}=await import("child_process");return execSync2(`${tmuxBin()} -L ${shellQuote(socketName)} list-sessions -F ''`,{stdio:["ignore","ignore","ignore"],timeout:2000}),!0}catch{return!1}}async function isPaneAlive(paneId){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{return(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_dead}'`)).trim()==="0"}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("server exited")||message.includes("error connecting"))throw new TmuxUnreachableError(message);return!1}}async function isPaneProcessRunning(paneId,processName,execSyncFn){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{let panePid=(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_pid}'`)).trim();if(!panePid||!/^\d+$/.test(panePid))return!1;return(execSyncFn??(await import("child_process")).execSync)(`pgrep -la -P ${panePid} 2>/dev/null; for cpid in $(pgrep -P ${panePid} 2>/dev/null); do pgrep -la -P "$cpid" 2>/dev/null; done; true`,{encoding:"utf-8",timeout:5000}).toLowerCase().includes(processName.toLowerCase())}catch{return!1}}async function killWindow(sessionName,windowName){try{return await executeTmux2(`kill-window -t ${shellQuote(`${sessionName}:${windowName}`)}`),!0}catch{return!1}}var TMUX_COLOR_NAMES,TMUX_COLOR_MAP,PANE_COLOR_SCRIPT,TmuxUnreachableError;var init_tmux=__esm(()=>{init_genie_tokens();init_ensure_tmux();init_team_lead_command();TMUX_COLOR_NAMES=["red","blue","green","yellow","purple","orange","pink","cyan"],TMUX_COLOR_MAP=Object.fromEntries(TMUX_COLOR_NAMES.map((name,i)=>[name,rotateHue(palette.accent,i*45)])),PANE_COLOR_SCRIPT=`${__require("os").homedir()}/.genie/tmux-pane-color.sh`;TmuxUnreachableError=class TmuxUnreachableError extends Error{constructor(message){super(message);this.name="TmuxUnreachableError"}}});var exports_agent_registry={};__export(exports_agent_registry,{update:()=>update,unregister:()=>unregister,setCurrentExecutor:()=>setCurrentExecutor,saveTemplate:()=>saveTemplate,removeSubPane:()=>removeSubPane,register:()=>register,reconcileStaleSpawns:()=>reconcileStaleSpawns,listTemplates:()=>listTemplates,listExhaustedZombies:()=>listExhaustedZombies,listAgents:()=>listAgents,list:()=>list,getTeamLeadEntry:()=>getTeamLeadEntry,getPane:()=>getPane,getElapsedTime:()=>getElapsedTime,getAgentEffectiveState:()=>getAgentEffectiveState,getAgentByName:()=>getAgentByName,getAgent:()=>getAgent,get:()=>get,findOrCreateAgent:()=>findOrCreateAgent,findByWindow:()=>findByWindow,findByTask:()=>findByTask,findByPane:()=>findByPane,filterBySession:()=>filterBySession,auditAgentKind:()=>auditAgentKind,archiveExhaustedZombies:()=>archiveExhaustedZombies,addSubPane:()=>addSubPane});import{createHash,randomUUID}from"crypto";function resolveWorkerSocketName(){return process.env.GENIE_TMUX_SOCKET||"genie"}function ts(v){if(!v)return new Date().toISOString();return v instanceof Date?v.toISOString():v}function rowToAgent(r){let agent={id:r.id,paneId:r.pane_id,session:r.session,worktree:r.worktree??null,startedAt:ts(r.started_at),state:r.state,lastStateChange:ts(r.last_state_change),repoPath:r.repo_path};if(r.task_id!=null)agent.taskId=r.task_id;if(r.task_title!=null)agent.taskTitle=r.task_title;if(r.wish_slug!=null)agent.wishSlug=r.wish_slug;if(r.group_number!=null)agent.groupNumber=r.group_number;if(r.window_name!=null)agent.windowName=r.window_name;if(r.window_id!=null)agent.windowId=r.window_id;if(r.role!=null)agent.role=r.role;if(r.custom_name!=null)agent.customName=r.custom_name;if(r.sub_panes!=null){let sp=typeof r.sub_panes==="string"?JSON.parse(r.sub_panes):r.sub_panes;if(Array.isArray(sp)&&sp.length>0)agent.subPanes=sp}if(r.provider!=null)agent.provider=r.provider;if(r.transport!=null)agent.transport=r.transport;if(r.skill!=null)agent.skill=r.skill;if(r.team!=null)agent.team=r.team;if(r.tmux_window!=null)agent.window=r.tmux_window;if(r.native_agent_id!=null)agent.nativeAgentId=r.native_agent_id;if(r.native_color!=null)agent.nativeColor=r.native_color;if(r.native_team_enabled)agent.nativeTeamEnabled=r.native_team_enabled;if(r.parent_session_id!=null)agent.parentSessionId=r.parent_session_id;if(r.suspended_at!=null)agent.suspendedAt=ts(r.suspended_at);if(r.auto_resume!=null)agent.autoResume=r.auto_resume;if(r.resume_attempts!=null)agent.resumeAttempts=r.resume_attempts;if(r.last_resume_attempt!=null)agent.lastResumeAttempt=ts(r.last_resume_attempt);if(r.max_resume_attempts!=null)agent.maxResumeAttempts=r.max_resume_attempts;if(r.pane_color!=null)agent.paneColor=r.pane_color;if(agent.currentExecutorId=r.current_executor_id??null,agent.reportsTo=r.reports_to??null,agent.title=r.title??null,r.kind!=null)agent.kind=r.kind;return agent}function rowToTemplate(r){let tpl={id:r.id,provider:r.provider,team:r.team,cwd:r.cwd,lastSpawnedAt:ts(r.last_spawned_at)};if(r.role!=null)tpl.role=r.role;if(r.skill!=null)tpl.skill=r.skill;if(r.extra_args!=null){let ea=typeof r.extra_args==="string"?JSON.parse(r.extra_args):r.extra_args;if(Array.isArray(ea)&&ea.length>0)tpl.extraArgs=ea}if(r.native_team_enabled)tpl.nativeTeamEnabled=r.native_team_enabled;return tpl}function shortProjectHash(repoPath){return createHash("sha1").update(repoPath).digest("hex").slice(0,8)}function buildProjectTeamLeadEntryId(teamName,session,repoPath){return`team-lead:${session}:${shortProjectHash(repoPath)}:${teamName}`}function buildSessionTeamLeadEntryId(teamName,session){return`team-lead:${session}:${teamName}`}function buildLegacyTeamLeadEntryId(teamName){return`team-lead:${teamName}`}async function register(agent){let sql=await getConnection(),now=new Date().toISOString();if(agent.team){let existing=await sql`
|
|
245
|
+
`),chmodSync2(PANE_COLOR_SCRIPT,493)}async function applyPaneColor(paneId,color,windowId){let hex=TMUX_COLOR_MAP[color]??TMUX_COLOR_MAP.blue;try{ensurePaneColorScript(),await executeTmux2(`set-option -p -t '${paneId}' @genie_color '${hex}'`);try{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));await(await getConnection2())`UPDATE executors SET pane_color = ${hex} WHERE tmux_pane_id = ${paneId}`}catch{}if(windowId)await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function rehydratePaneColorHook(windowId){try{ensurePaneColorScript(),await executeTmux2(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`)}catch{}}async function resolveRepoSession(repoPath){let derived=basename2(repoPath);try{let sessions=await listSessions(),exact=sessions.find((s)=>s.name===derived);if(exact)return exact.name;if(process.env.TMUX)try{let name=(await executeTmux2("display-message -p '#{session_name}'")).trim();if(name)return name}catch{}let partial=sessions.find((s)=>s.name.includes(derived));if(partial)return partial.name}catch{}return derived}function isTmuxSocketAlive(socketName){if(!socketName)return!1;let uid=process.getuid?.()??501;return existsSync8(join11(`/tmp/tmux-${uid}`,socketName))}async function isTmuxServerReachable(socketName){if(!socketName)return!1;if(!isTmuxSocketAlive(socketName))return!1;try{let{execSync:execSync2}=await import("child_process");return execSync2(`${tmuxBin()} -L ${shellQuote(socketName)} list-sessions -F ''`,{stdio:["ignore","ignore","ignore"],timeout:2000}),!0}catch{return!1}}async function isPaneAlive(paneId){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{return(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_dead}'`)).trim()==="0"}catch(error){let message=error instanceof Error?error.message:String(error);if(message.includes("no server running")||message.includes("server exited")||message.includes("error connecting"))throw new TmuxUnreachableError(message);return!1}}async function isPaneProcessRunning(paneId,processName,execSyncFn){if(!paneId||paneId==="inline")return!1;if(!/^%\d+$/.test(paneId))return!1;try{let panePid=(await executeTmux2(`display-message -t '${paneId}' -p '#{pane_pid}'`)).trim();if(!panePid||!/^\d+$/.test(panePid))return!1;return(execSyncFn??(await import("child_process")).execSync)(`pgrep -la -P ${panePid} 2>/dev/null; for cpid in $(pgrep -P ${panePid} 2>/dev/null); do pgrep -la -P "$cpid" 2>/dev/null; done; true`,{encoding:"utf-8",timeout:5000}).toLowerCase().includes(processName.toLowerCase())}catch{return!1}}async function killWindow(sessionName,windowName){try{return await executeTmux2(`kill-window -t ${shellQuote(`${sessionName}:${windowName}`)}`),!0}catch{return!1}}var TMUX_COLOR_NAMES,TMUX_COLOR_MAP,PANE_COLOR_SCRIPT,TmuxUnreachableError;var init_tmux=__esm(()=>{init_genie_tokens();init_ensure_tmux();init_team_lead_command();TMUX_COLOR_NAMES=["red","blue","green","yellow","purple","orange","pink","cyan"],TMUX_COLOR_MAP=Object.fromEntries(TMUX_COLOR_NAMES.map((name,i)=>[name,rotateHue(palette.accent,i*45)])),PANE_COLOR_SCRIPT=`${__require("os").homedir()}/.genie/tmux-pane-color.sh`;TmuxUnreachableError=class TmuxUnreachableError extends Error{constructor(message){super(message);this.name="TmuxUnreachableError"}}});var exports_agent_registry={};__export(exports_agent_registry,{update:()=>update,unregister:()=>unregister,setCurrentExecutor:()=>setCurrentExecutor,saveTemplate:()=>saveTemplate,removeSubPane:()=>removeSubPane,register:()=>register,reconcileStaleSpawns:()=>reconcileStaleSpawns,listTemplates:()=>listTemplates,listForRender:()=>listForRender,listExhaustedZombies:()=>listExhaustedZombies,listAgentsForRender:()=>listAgentsForRender,listAgents:()=>listAgents,list:()=>list,getTeamLeadEntry:()=>getTeamLeadEntry,getPane:()=>getPane,getElapsedTime:()=>getElapsedTime,getAgentEffectiveState:()=>getAgentEffectiveState,getAgentByName:()=>getAgentByName,getAgent:()=>getAgent,get:()=>get,findOrCreateAgent:()=>findOrCreateAgent,findByWindow:()=>findByWindow,findByTask:()=>findByTask,findByPane:()=>findByPane,filterBySession:()=>filterBySession,auditAgentKind:()=>auditAgentKind,archiveExhaustedZombies:()=>archiveExhaustedZombies,addSubPane:()=>addSubPane});import{createHash,randomUUID}from"crypto";function resolveWorkerSocketName(){return process.env.GENIE_TMUX_SOCKET||"genie"}function ts(v){if(!v)return new Date().toISOString();return v instanceof Date?v.toISOString():v}function rowToAgent(r){let agent={id:r.id,paneId:r.pane_id,session:r.session,worktree:r.worktree??null,startedAt:ts(r.started_at),state:r.state,lastStateChange:ts(r.last_state_change),repoPath:r.repo_path};if(r.task_id!=null)agent.taskId=r.task_id;if(r.task_title!=null)agent.taskTitle=r.task_title;if(r.wish_slug!=null)agent.wishSlug=r.wish_slug;if(r.group_number!=null)agent.groupNumber=r.group_number;if(r.window_name!=null)agent.windowName=r.window_name;if(r.window_id!=null)agent.windowId=r.window_id;if(r.role!=null)agent.role=r.role;if(r.custom_name!=null)agent.customName=r.custom_name;if(r.sub_panes!=null){let sp=typeof r.sub_panes==="string"?JSON.parse(r.sub_panes):r.sub_panes;if(Array.isArray(sp)&&sp.length>0)agent.subPanes=sp}if(r.provider!=null)agent.provider=r.provider;if(r.transport!=null)agent.transport=r.transport;if(r.skill!=null)agent.skill=r.skill;if(r.team!=null)agent.team=r.team;if(r.tmux_window!=null)agent.window=r.tmux_window;if(r.native_agent_id!=null)agent.nativeAgentId=r.native_agent_id;if(r.native_color!=null)agent.nativeColor=r.native_color;if(r.native_team_enabled)agent.nativeTeamEnabled=r.native_team_enabled;if(r.parent_session_id!=null)agent.parentSessionId=r.parent_session_id;if(r.suspended_at!=null)agent.suspendedAt=ts(r.suspended_at);if(r.auto_resume!=null)agent.autoResume=r.auto_resume;if(r.resume_attempts!=null)agent.resumeAttempts=r.resume_attempts;if(r.last_resume_attempt!=null)agent.lastResumeAttempt=ts(r.last_resume_attempt);if(r.max_resume_attempts!=null)agent.maxResumeAttempts=r.max_resume_attempts;if(r.pane_color!=null)agent.paneColor=r.pane_color;if(agent.currentExecutorId=r.current_executor_id??null,agent.reportsTo=r.reports_to??null,agent.title=r.title??null,r.kind!=null)agent.kind=r.kind;return agent}function rowToTemplate(r){let tpl={id:r.id,provider:r.provider,team:r.team,cwd:r.cwd,lastSpawnedAt:ts(r.last_spawned_at)};if(r.role!=null)tpl.role=r.role;if(r.skill!=null)tpl.skill=r.skill;if(r.extra_args!=null){let ea=typeof r.extra_args==="string"?JSON.parse(r.extra_args):r.extra_args;if(Array.isArray(ea)&&ea.length>0)tpl.extraArgs=ea}if(r.native_team_enabled)tpl.nativeTeamEnabled=r.native_team_enabled;return tpl}function shortProjectHash(repoPath){return createHash("sha1").update(repoPath).digest("hex").slice(0,8)}function buildProjectTeamLeadEntryId(teamName,session,repoPath){return`team-lead:${session}:${shortProjectHash(repoPath)}:${teamName}`}function buildSessionTeamLeadEntryId(teamName,session){return`team-lead:${session}:${teamName}`}function buildLegacyTeamLeadEntryId(teamName){return`team-lead:${teamName}`}async function register(agent){let sql=await getConnection(),now=new Date().toISOString();if(agent.team){let existing=await sql`
|
|
246
246
|
SELECT team FROM agents WHERE id = ${agent.id} LIMIT 1
|
|
247
|
-
`;if(existing.length>0&&existing[0].team!==null&&existing[0].team!==agent.team)throw Error(`register: cross-team id collision \u2014 agent id "${agent.id}" already exists in team "${existing[0].team}", refusing to re-register under team "${agent.team}". Unregister the stale row first, or pick a different id/suffix for this spawn.`)}await sql`INSERT INTO agents (id, pane_id, session, worktree, task_id, task_title, wish_slug, group_number, started_at, state, last_state_change, repo_path, window_name, window_id, role, custom_name, sub_panes, provider, transport, skill, team, tmux_window, native_agent_id, native_color, native_team_enabled, parent_session_id, suspended_at, auto_resume, resume_attempts, last_resume_attempt, max_resume_attempts, pane_color) VALUES (${agent.id}, ${agent.paneId}, ${agent.session}, ${agent.worktree??null}, ${agent.taskId??null}, ${agent.taskTitle??null}, ${agent.wishSlug??null}, ${agent.groupNumber??null}, ${agent.startedAt??now}, ${agent.state??"spawning"}, ${agent.lastStateChange??now}, ${agent.repoPath}, ${agent.windowName??null}, ${agent.windowId??null}, ${agent.role??null}, ${agent.customName??null}, ${sql.json(agent.subPanes??[])}, ${agent.provider??null}, ${agent.transport??"tmux"}, ${agent.skill??null}, ${agent.team??null}, ${agent.window??null}, ${agent.nativeAgentId??null}, ${agent.nativeColor??null}, ${agent.nativeTeamEnabled??!1}, ${agent.parentSessionId??null}, ${agent.suspendedAt??null}, ${agent.autoResume??!0}, ${agent.resumeAttempts??0}, ${agent.lastResumeAttempt??null}, ${agent.maxResumeAttempts??3}, ${agent.paneColor??null}) ON CONFLICT (id) DO UPDATE SET pane_id = EXCLUDED.pane_id, session = EXCLUDED.session, state = EXCLUDED.state, last_state_change = EXCLUDED.last_state_change, team = COALESCE(agents.team, EXCLUDED.team), role = COALESCE(agents.role, EXCLUDED.role), custom_name = COALESCE(agents.custom_name, EXCLUDED.custom_name), updated_at = now()`}async function unregister(id){await(await getConnection())`DELETE FROM agents WHERE id = ${id}`}async function get(id){let rows=await(await getConnection())`SELECT * FROM agents WHERE id = ${id}`;return rows.length>0?rowToAgent(rows[0]):null}async function list(){return(await(await getConnection())`SELECT * FROM agents`).map(rowToAgent)}async function reconcileStaleSpawns(thresholdSeconds=60){try{let sql=await getConnection(),rows=await sql`
|
|
247
|
+
`;if(existing.length>0&&existing[0].team!==null&&existing[0].team!==agent.team)throw Error(`register: cross-team id collision \u2014 agent id "${agent.id}" already exists in team "${existing[0].team}", refusing to re-register under team "${agent.team}". Unregister the stale row first, or pick a different id/suffix for this spawn.`)}await sql`INSERT INTO agents (id, pane_id, session, worktree, task_id, task_title, wish_slug, group_number, started_at, state, last_state_change, repo_path, window_name, window_id, role, custom_name, sub_panes, provider, transport, skill, team, tmux_window, native_agent_id, native_color, native_team_enabled, parent_session_id, suspended_at, auto_resume, resume_attempts, last_resume_attempt, max_resume_attempts, pane_color) VALUES (${agent.id}, ${agent.paneId}, ${agent.session}, ${agent.worktree??null}, ${agent.taskId??null}, ${agent.taskTitle??null}, ${agent.wishSlug??null}, ${agent.groupNumber??null}, ${agent.startedAt??now}, ${agent.state??"spawning"}, ${agent.lastStateChange??now}, ${agent.repoPath}, ${agent.windowName??null}, ${agent.windowId??null}, ${agent.role??null}, ${agent.customName??null}, ${sql.json(agent.subPanes??[])}, ${agent.provider??null}, ${agent.transport??"tmux"}, ${agent.skill??null}, ${agent.team??null}, ${agent.window??null}, ${agent.nativeAgentId??null}, ${agent.nativeColor??null}, ${agent.nativeTeamEnabled??!1}, ${agent.parentSessionId??null}, ${agent.suspendedAt??null}, ${agent.autoResume??!0}, ${agent.resumeAttempts??0}, ${agent.lastResumeAttempt??null}, ${agent.maxResumeAttempts??3}, ${agent.paneColor??null}) ON CONFLICT (id) DO UPDATE SET pane_id = EXCLUDED.pane_id, session = EXCLUDED.session, state = EXCLUDED.state, last_state_change = EXCLUDED.last_state_change, team = COALESCE(agents.team, EXCLUDED.team), role = COALESCE(agents.role, EXCLUDED.role), custom_name = COALESCE(agents.custom_name, EXCLUDED.custom_name), updated_at = now()`}async function unregister(id){await(await getConnection())`DELETE FROM agents WHERE id = ${id}`}async function get(id){let rows=await(await getConnection())`SELECT * FROM agents WHERE id = ${id}`;return rows.length>0?rowToAgent(rows[0]):null}async function list(){return(await(await getConnection())`SELECT * FROM agents`).map(rowToAgent)}function dedupeShadowRows(rows,opts){let groups=new Map;for(let r of rows){let k=opts.keyFor(r),arr=groups.get(k);if(arr)arr.push(r);else groups.set(k,[r])}let out=[];for(let arr of groups.values()){if(arr.length===1){out.push(arr[0]);continue}arr.sort((a,b)=>{let aExec=opts.hasExecutor(a)?1:0,bExec=opts.hasExecutor(b)?1:0;if(aExec!==bExec)return bExec-aExec;return opts.startedAt(b).localeCompare(opts.startedAt(a))}),out.push(arr[0])}return out}function shadowKey(customName,id,team){return`${customName??id}\x00${team??""}`}async function listForRender(){let all=await list();return dedupeShadowRows(all,{keyFor:(a)=>shadowKey(a.customName,a.id,a.team),hasExecutor:(a)=>a.currentExecutorId!=null,startedAt:(a)=>a.startedAt})}async function reconcileStaleSpawns(thresholdSeconds=60){try{let sql=await getConnection(),rows=await sql`
|
|
248
248
|
UPDATE agents
|
|
249
249
|
SET state = 'error', last_state_change = now()
|
|
250
250
|
WHERE state = 'spawning'
|
|
@@ -358,7 +358,7 @@ ${bin} set-option -w pane-active-border-style "fg=$COLOR"
|
|
|
358
358
|
native_team_enabled, parent_session_id, current_executor_id, reports_to, title, kind, created_at, updated_at
|
|
359
359
|
FROM agents
|
|
360
360
|
WHERE (${includeArchived} OR state IS DISTINCT FROM 'archived')
|
|
361
|
-
`;return rows.map(rowToAgentIdentity)}async function auditAgentKind(){let rows=await(await getConnection())`
|
|
361
|
+
`;return rows.map(rowToAgentIdentity)}async function listAgentsForRender(filters){let all=await listAgents(filters);return dedupeShadowRows(all,{keyFor:(a)=>shadowKey(a.customName,a.id,a.team),hasExecutor:(a)=>a.currentExecutorId!=null,startedAt:(a)=>a.startedAt})}async function auditAgentKind(){let rows=await(await getConnection())`
|
|
362
362
|
SELECT id, kind, reports_to FROM agents
|
|
363
363
|
`,drifted=[];for(let row of rows){let expected=row.id.startsWith("dir:")||row.reports_to===null?"permanent":"task";if(row.kind!==expected)drifted.push({id:row.id,kind:row.kind,expected})}for(let drift of drifted)recordAuditEvent("worker",drift.id,"agents.kind.audit_drift","auditor",{stored:drift.kind,expected:drift.expected}).catch(()=>{});return{total:rows.length,drifted}}var DEAD_PANE_ZOMBIE_TTL_HOURS=24;var init_agent_registry=__esm(()=>{init_audit();init_db();init_tmux()});var exports_js_yaml={};__export(exports_js_yaml,{types:()=>types2,safeLoadAll:()=>safeLoadAll,safeLoad:()=>safeLoad,safeDump:()=>safeDump,loadAll:()=>loadAll,load:()=>load,dump:()=>dump,default:()=>jsYaml,YAMLException:()=>YAMLException,Type:()=>Type,Schema:()=>Schema,JSON_SCHEMA:()=>JSON_SCHEMA,FAILSAFE_SCHEMA:()=>FAILSAFE_SCHEMA,DEFAULT_SCHEMA:()=>DEFAULT_SCHEMA,CORE_SCHEMA:()=>CORE_SCHEMA});function isNothing(subject){return typeof subject>"u"||subject===null}function isObject(subject){return typeof subject==="object"&&subject!==null}function toArray(sequence){if(Array.isArray(sequence))return sequence;else if(isNothing(sequence))return[];return[sequence]}function extend(target,source){var index,length,key,sourceKeys;if(source){sourceKeys=Object.keys(source);for(index=0,length=sourceKeys.length;index<length;index+=1)key=sourceKeys[index],target[key]=source[key]}return target}function repeat(string,count){var result2="",cycle;for(cycle=0;cycle<count;cycle+=1)result2+=string;return result2}function isNegativeZero(number){return number===0&&Number.NEGATIVE_INFINITY===1/number}function formatError(exception,compact){var where="",message=exception.reason||"(unknown reason)";if(!exception.mark)return message;if(exception.mark.name)where+='in "'+exception.mark.name+'" ';if(where+="("+(exception.mark.line+1)+":"+(exception.mark.column+1)+")",!compact&&exception.mark.snippet)where+=`
|
|
364
364
|
|
|
@@ -1666,7 +1666,7 @@ ${otherList}
|
|
|
1666
1666
|
`,await sql`
|
|
1667
1667
|
DELETE FROM task_actors WHERE task_id = ANY(${ids}) AND role = 'assignee'
|
|
1668
1668
|
`,ids.length}var GroupStatusSchema,GroupStateSchema,WishStateSchema,WishStateMismatchError;var init_wish_state=__esm(()=>{init_zod();init_db();GroupStatusSchema=exports_external.enum(["blocked","ready","in_progress","done"]),GroupStateSchema=exports_external.object({status:GroupStatusSchema,assignee:exports_external.string().optional(),dependsOn:exports_external.array(exports_external.string()).default([]),startedAt:exports_external.string().optional(),completedAt:exports_external.string().optional()}),WishStateSchema=exports_external.object({wish:exports_external.string(),groups:exports_external.record(exports_external.string(),GroupStateSchema),createdAt:exports_external.string(),updatedAt:exports_external.string()});WishStateMismatchError=class WishStateMismatchError extends Error{slug;added;removed;changed;constructor(slug,added,removed,changed){let lines=[`Wish "${slug}" group structure has changed since state was created.`];if(added.length>0)lines.push(` + added: ${added.join(", ")}`);if(removed.length>0)lines.push(` - removed: ${removed.join(", ")}`);if(changed.length>0)lines.push(` ~ changed deps: ${changed.join(", ")}`);lines.push(""),lines.push(`Run \`genie reset ${slug}\` to recreate state from the current WISH.md.`);super(lines.join(`
|
|
1669
|
-
`));this.name="WishStateMismatchError",this.slug=slug,this.added=added,this.removed=removed,this.changed=changed}}});var exports_protocol_router_spawn={};__export(exports_protocol_router_spawn,{spawnWorkerFromTemplate:()=>spawnWorkerFromTemplate,injectResumeContext:()=>injectResumeContext,_deps:()=>_deps2});import{exec as exec2}from"child_process";import{readFile as readFile9}from"fs/promises";import{join as join40}from"path";import{promisify as promisify2}from"util";async function resolveParentSession(_repoPath,team){let leaderName=await resolveLeaderName(team),sanitized=sanitizeTeamName(team),leaderAgent=await getAgentByName(leaderName,sanitized).catch(()=>null);if(leaderAgent){let decision=await shouldResume(leaderAgent.id).catch(()=>null);if(decision?.sessionId)return decision.sessionId}return await discoverClaudeParentSessionId()??`genie-${team}`}function buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId){let isClaude=template.provider==="claude"||template.provider==="claude-sdk",sessionName=template.role?`${template.team}-${template.role}`:void 0,newSessionId=isClaude&&!resumeSessionId?crypto.randomUUID():void 0,params={provider:template.provider,team:template.team,role:template.role,skill:template.skill,extraArgs:template.extraArgs,sessionId:newSessionId,resume:isClaude?resumeSessionId:void 0,name:sessionName};if(isClaude)params.nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:template.role??"general-purpose",agentName:template.role};return params}function buildFullCommand(launch){if(launch.env&&Object.keys(launch.env).length>0)return`env ${Object.entries(launch.env).map(([k,v])=>`${k}=${v}`).join(" ")} ${launch.command}`;return launch.command}async function generateWorkerId(team,role){let base=role?`${team}-${role}`:team;return(await list()).some((w)=>w.id===base)?`${base}-${crypto.randomUUID().slice(0,8)}`:base}async function terminateActiveExecutorIfRunning(agentId){let currentExec=await getCurrentExecutor(agentId);if(!currentExec||currentExec.state==="terminated"||currentExec.state==="done")return;let providerImpl=getProvider(currentExec.provider);if(providerImpl)try{await providerImpl.terminate(currentExec)}catch{}await terminateActiveExecutor(agentId)}async function capturePanePid(paneId){try{let{stdout:pidOut}=await execAsync(genieTmuxCmd(`display -t '${paneId}' -p '#{pane_pid}'`)),parsed=Number.parseInt(pidOut.trim(),10);return parsed>0?parsed:null}catch{return null}}function resolveExecutorTransport(provider){if(provider==="codex")return"api";if(provider==="claude-sdk")return"process";return"tmux"}async function createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow){let agentName=template.role??"worker",agentIdentity=await findOrCreateAgent(agentName,template.team,template.role);await(await getConnection()).begin(async(tx)=>{await tx`SELECT pg_advisory_xact_lock(hashtext(${agentIdentity.id}))`,await terminateActiveExecutorIfRunning(agentIdentity.id);let pid=await capturePanePid(paneId),executor=await createExecutor(agentIdentity.id,template.provider,resolveExecutorTransport(template.provider),{pid,tmuxSession:session,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:effectiveSessionId??null,state:"spawning",repoPath,paneColor:spawnColor??null});await setCurrentExecutor(agentIdentity.id,executor.id)})}async function spawnPaneInSession(session,team,repoPath,fullCommand){let teamWindow=null;try{teamWindow=await ensureTeamWindow(session,team,repoPath)}catch{}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",escapedCmd=fullCommand.replace(/'/g,"'\\''"),{stdout}=await execAsync(genieTmuxCmd(`split-window -d ${splitTarget} -P -F '#{pane_id}' '${escapedCmd}'`)),paneId=stdout.trim(),layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{await execAsync(genieTmuxCmd(buildLayoutCommand(layoutTarget,resolveLayoutMode())))}catch{}return{paneId,teamWindow}}async function registerNativeTeamMember(team,agentName,template,paneId,repoPath,spawnColor,resumeSessionId){let now=new Date().toISOString();await registerNativeMember(team,{agentName,agentType:template.role??"general-purpose",color:spawnColor??"blue",tmuxPaneId:paneId,cwd:repoPath});let leaderInboxTarget;try{let{resolveLeaderName:resolveLeaderName2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));leaderInboxTarget=await resolveLeaderName2(team)}catch{leaderInboxTarget=team}try{await _deps2.writeNativeInbox(team,leaderInboxTarget,{from:agentName,text:`Worker ${agentName} (${template.provider}) auto-spawned${resumeSessionId?" with --resume":""}. Ready for tasks.`,summary:`${agentName} auto-spawned`,timestamp:now,color:spawnColor??"blue",read:!1})}catch(err){let msg=err instanceof Error?err.message:String(err);console.warn(`[protocol-router] Native inbox write failed for team="${team}" target="${leaderInboxTarget}": ${msg}`)}}async function tryAutoBrain(workerId,repoPath){try{let brain=await import("@khal-os/brain");if(brain.autoBrain)await brain.autoBrain({agentId:workerId,workdir:repoPath})}catch{}}async function spawnWorkerFromTemplate(template,resumeSessionId){let repoPath=template.cwd??process.cwd(),team=template.team,parentSessionId=await resolveParentSession(repoPath,team);await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=await assignColor(team),params=buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId),launch=buildLaunchCommand(validateSpawnParams(params)),fullCommand=buildFullCommand(launch),workerId=await generateWorkerId(team,template.role),
|
|
1669
|
+
`));this.name="WishStateMismatchError",this.slug=slug,this.added=added,this.removed=removed,this.changed=changed}}});var exports_protocol_router_spawn={};__export(exports_protocol_router_spawn,{spawnWorkerFromTemplate:()=>spawnWorkerFromTemplate,injectResumeContext:()=>injectResumeContext,_deps:()=>_deps2});import{exec as exec2}from"child_process";import{readFile as readFile9}from"fs/promises";import{join as join40}from"path";import{promisify as promisify2}from"util";async function resolveParentSession(_repoPath,team){let leaderName=await resolveLeaderName(team),sanitized=sanitizeTeamName(team),leaderAgent=await getAgentByName(leaderName,sanitized).catch(()=>null);if(leaderAgent){let decision=await shouldResume(leaderAgent.id).catch(()=>null);if(decision?.sessionId)return decision.sessionId}return await discoverClaudeParentSessionId()??`genie-${team}`}function buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId){let isClaude=template.provider==="claude"||template.provider==="claude-sdk",sessionName=template.role?`${template.team}-${template.role}`:void 0,newSessionId=isClaude&&!resumeSessionId?crypto.randomUUID():void 0,params={provider:template.provider,team:template.team,role:template.role,skill:template.skill,extraArgs:template.extraArgs,sessionId:newSessionId,resume:isClaude?resumeSessionId:void 0,name:sessionName};if(isClaude)params.nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:template.role??"general-purpose",agentName:template.role};return params}function buildFullCommand(launch){if(launch.env&&Object.keys(launch.env).length>0)return`env ${Object.entries(launch.env).map(([k,v])=>`${k}=${v}`).join(" ")} ${launch.command}`;return launch.command}async function generateWorkerId(team,role){let base=role?`${team}-${role}`:team;return(await list()).some((w)=>w.id===base)?`${base}-${crypto.randomUUID().slice(0,8)}`:base}async function terminateActiveExecutorIfRunning(agentId){let currentExec=await getCurrentExecutor(agentId);if(!currentExec||currentExec.state==="terminated"||currentExec.state==="done")return;let providerImpl=getProvider(currentExec.provider);if(providerImpl)try{await providerImpl.terminate(currentExec)}catch{}await terminateActiveExecutor(agentId)}async function capturePanePid(paneId){try{let{stdout:pidOut}=await execAsync(genieTmuxCmd(`display -t '${paneId}' -p '#{pane_pid}'`)),parsed=Number.parseInt(pidOut.trim(),10);return parsed>0?parsed:null}catch{return null}}function resolveExecutorTransport(provider){if(provider==="codex")return"api";if(provider==="claude-sdk")return"process";return"tmux"}async function createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow){let agentName=template.role??"worker",agentIdentity=await findOrCreateAgent(agentName,template.team,template.role);await(await getConnection()).begin(async(tx)=>{await tx`SELECT pg_advisory_xact_lock(hashtext(${agentIdentity.id}))`,await terminateActiveExecutorIfRunning(agentIdentity.id);let pid=await capturePanePid(paneId),executor=await createExecutor(agentIdentity.id,template.provider,resolveExecutorTransport(template.provider),{pid,tmuxSession:session,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:effectiveSessionId??null,state:"spawning",repoPath,paneColor:spawnColor??null});await setCurrentExecutor(agentIdentity.id,executor.id)})}async function spawnPaneInSession(session,team,repoPath,fullCommand){let teamWindow=null;try{teamWindow=await ensureTeamWindow(session,team,repoPath)}catch{}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",escapedCmd=fullCommand.replace(/'/g,"'\\''"),{stdout}=await execAsync(genieTmuxCmd(`split-window -d ${splitTarget} -P -F '#{pane_id}' '${escapedCmd}'`)),paneId=stdout.trim(),layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{await execAsync(genieTmuxCmd(buildLayoutCommand(layoutTarget,resolveLayoutMode())))}catch{}return{paneId,teamWindow}}async function registerNativeTeamMember(team,agentName,template,paneId,repoPath,spawnColor,resumeSessionId){let now=new Date().toISOString();await registerNativeMember(team,{agentName,agentType:template.role??"general-purpose",color:spawnColor??"blue",tmuxPaneId:paneId,cwd:repoPath});let leaderInboxTarget;try{let{resolveLeaderName:resolveLeaderName2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));leaderInboxTarget=await resolveLeaderName2(team)}catch{leaderInboxTarget=team}try{await _deps2.writeNativeInbox(team,leaderInboxTarget,{from:agentName,text:`Worker ${agentName} (${template.provider}) auto-spawned${resumeSessionId?" with --resume":""}. Ready for tasks.`,summary:`${agentName} auto-spawned`,timestamp:now,color:spawnColor??"blue",read:!1})}catch(err){let msg=err instanceof Error?err.message:String(err);console.warn(`[protocol-router] Native inbox write failed for team="${team}" target="${leaderInboxTarget}": ${msg}`)}}async function tryAutoBrain(workerId,repoPath){try{let brain=await import("@khal-os/brain");if(brain.autoBrain)await brain.autoBrain({agentId:workerId,workdir:repoPath})}catch{}}async function spawnWorkerFromTemplate(template,resumeSessionId){let repoPath=template.cwd??process.cwd(),team=template.team,parentSessionId=await resolveParentSession(repoPath,team);await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=await assignColor(team),params=buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId),launch=buildLaunchCommand(validateSpawnParams(params)),fullCommand=buildFullCommand(launch),workerId=await generateWorkerId(team,template.role),teamConfig=await getTeam(team),session=(Boolean(process.env.TMUX)?await getCurrentSessionName():null)??teamConfig?.tmuxSessionName??team,{paneId,teamWindow}=await spawnPaneInSession(session,team,repoPath,fullCommand),now=new Date().toISOString(),agentName=template.role??"worker",isClaude=template.provider==="claude"||template.provider==="claude-sdk",effectiveSessionId=resumeSessionId??params.sessionId,workerEntry={id:workerId,paneId,session,provider:template.provider,transport:"tmux",role:template.role,skill:template.skill,team,worktree:null,startedAt:now,state:"spawning",lastStateChange:now,repoPath,nativeTeamEnabled:isClaude,nativeAgentId:`${agentName}@${team}`,nativeColor:spawnColor,parentSessionId,window:teamWindow?.windowName,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId};await register(workerEntry);try{await createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow)}catch{}if(isClaude)await registerNativeTeamMember(team,agentName,template,paneId,repoPath,spawnColor,resumeSessionId);if(spawnColor)await applyPaneColor(paneId,spawnColor,teamWindow?.windowId);return await injectResumeContext(repoPath,workerId,agentName,team),await tryAutoBrain(workerId,repoPath),{worker:workerEntry,paneId,workerId}}function extractGroupSection(content,groupName){let escaped=groupName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^### Group ${escaped}:`,"m"),match=content.match(pattern);if(!match||match.index===void 0)return null;let start=match.index,nextBoundary=content.slice(start).slice(1).search(/^### Group \d|^---$/m),end=nextBoundary!==-1?start+1+nextBoundary:content.length;return content.slice(start,end).trim()}async function getRecentGitLog(repoPath,count=3){try{let{stdout}=await execAsync(`git -C '${repoPath}' log --oneline -${count} 2>/dev/null`);return stdout.trim()}catch{return""}}async function getGitStatus(repoPath){try{let{stdout}=await execAsync(`git -C '${repoPath}' status --short 2>/dev/null`);return stdout.trim()}catch{return""}}async function injectResumeContext(repoPath,workerId,agentName,_team){try{let match=await _deps2.findAnyGroupByAssignee(workerId,repoPath)??await _deps2.findAnyGroupByAssignee(agentName,repoPath);if(!match)return;let{slug,groupName,group}=match,wishPath=join40(repoPath,".genie","wishes",slug,"WISH.md"),groupSection="";try{let wishContent=await readFile9(wishPath,"utf-8");groupSection=extractGroupSection(wishContent,groupName)??""}catch{}let gitLog=await getRecentGitLog(repoPath),gitStatus=await getGitStatus(repoPath),resumePrompt=[`RESUME CONTEXT: You were working on wish "${slug}", group "${groupName}".`,`Status: ${group.status}. Started at: ${group.startedAt??"unknown"}.`,`Wish file: .genie/wishes/${slug}/WISH.md`,"",groupSection?`Group section:
|
|
1670
1670
|
${groupSection}`:"","",gitLog?`Last git log:
|
|
1671
1671
|
${gitLog}`:"","",gitStatus?`Uncommitted changes:
|
|
1672
1672
|
${gitStatus}`:"","","Pick up where you left off. Read the wish file for full context."].filter(Boolean).join(`
|
|
@@ -1962,7 +1962,7 @@ process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
|
|
1962
1962
|
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
|
1963
1963
|
`,{mode:420});let{spawn:spawnChild}=__require("child_process");spawnChild("node",[scriptFile],{detached:!0,stdio:"ignore"}).unref();for(let i2=0;i2<30;i2++)if(await new Promise((r)=>setTimeout(r,100)),isRelayAlive(pidFile))return!0;return!1}catch{return!1}}async function generateWorkerId2(team,role){let base=role?`${team}-${role}`:team;if(!(await list()).some((w)=>w.id===base))return base;let suffix=crypto.randomUUID().slice(0,8);return`${base}-${suffix}`}async function capturePanePid2(paneId){if(paneId==="inline")return null;try{let{execSync:execSync10}=__require("child_process"),output=execSync10(genieTmuxCmd(`display -t '${paneId}' -p '#{pane_pid}'`),{encoding:"utf-8"}).trim(),pid=Number.parseInt(output,10);return pid>0?pid:null}catch{return null}}function resolveExecutorTransport2(provider,spawnTransport){if(provider==="codex")return"api";if(provider==="claude-sdk")return"process";return spawnTransport==="inline"?"process":"tmux"}async function terminateActiveExecutorWithCleanup(agentIdentityId){try{let currentExec=await getCurrentExecutor(agentIdentityId);if(!currentExec||currentExec.state==="terminated"||currentExec.state==="done")return;let provider=getProvider(currentExec.provider);if(provider)try{await provider.terminate(currentExec)}catch{}await terminateActiveExecutor(agentIdentityId)}catch{}}async function createAndLinkExecutor2(agentIdentityId,provider,transport,opts){try{let executor=await createExecutor(agentIdentityId,provider,transport,opts);return await setCurrentExecutor(agentIdentityId,executor.id),executor.id}catch{return null}}async function registerSpawnWorker(ctx,paneId,windowInfo){let nt=ctx.validated.nativeTeam,workerEntry={id:ctx.workerId,paneId,session:ctx.validated.team,provider:ctx.validated.provider,transport:ctx.transport,role:ctx.validated.role,skill:ctx.validated.skill,team:ctx.validated.team,worktree:null,startedAt:ctx.now,state:"spawning",lastStateChange:ctx.now,repoPath:ctx.cwd,nativeTeamEnabled:nt?.enabled??!1,nativeAgentId:`${ctx.agentName}@${ctx.validated.team}`,nativeColor:nt?.color??ctx.spawnColor,parentSessionId:nt?.parentSessionId??ctx.parentSessionId,window:windowInfo?.windowName,windowName:windowInfo?.windowName,windowId:windowInfo?.windowId,autoResume:ctx.autoResume===!1?!1:void 0,resumeAttempts:0};await register(workerEntry);let role=ctx.validated.role??ctx.agentName;if(role!=="council")try{await hireAgent(ctx.validated.team,role)}catch{}return workerEntry}async function notifySpawnJoin(ctx,paneId){let nt=ctx.validated.nativeTeam;if(!nt?.enabled)return;await registerNativeMember(ctx.validated.team,{agentName:ctx.agentName,agentType:nt.agentType??ctx.validated.role??"general-purpose",color:nt.color??ctx.spawnColor??"blue",tmuxPaneId:paneId,cwd:ctx.cwd,planModeRequired:nt.planModeRequired});let leaderName=await resolveTeamLeaderName(ctx.validated.team);await writeNativeInbox(ctx.validated.team,leaderName,{from:ctx.agentName,text:`Worker ${ctx.agentName} (${ctx.validated.provider}) joined team ${ctx.validated.team}. cwd: ${ctx.cwd}. Ready for tasks.`,summary:`${ctx.agentName} (${ctx.validated.provider}) joined`,timestamp:new Date().toISOString(),color:nt.color??ctx.spawnColor??"blue",read:!1})}function registerOtelRelayPane(workerId,paneId,agentName,spawnColor,repoPath){let{writeFileSync:wfs}=__require("fs"),{join:pjoin}=__require("path"),{homedir:hdir}=__require("os"),rd=pjoin(hdir(),".genie","relay");wfs(pjoin(rd,`${workerId}-pane`),paneId),wfs(pjoin(rd,`${workerId}-meta`),JSON.stringify({agent:agentName,color:spawnColor,repoPath}))}function printSpawnInfo(ctx,paneId,workerEntry){let nt=ctx.validated.nativeTeam;if(console.log(`Agent "${ctx.workerId}" spawned.`),console.log(` Provider: ${ctx.launch.provider}`),console.log(` Command: ${ctx.fullCommand}`),console.log(` Team: ${ctx.validated.team}`),console.log(` Pane: ${paneId}`),ctx.validated.role)console.log(` Role: ${ctx.validated.role}`);if(ctx.executorId)console.log(` Executor: ${ctx.executorId}`);if(ctx.validated.skill)console.log(` Skill: ${ctx.validated.skill}`);if(ctx.claudeSessionId)console.log(` Session: ${ctx.claudeSessionId}`);if(console.log(` Layout: ${ctx.layoutMode}`),nt?.enabled)console.log(" Native: enabled"),console.log(` AgentID: ${workerEntry.nativeAgentId}`),console.log(` Color: ${nt.color}`);if(ctx.otelRelayActive)console.log(` OTel: relay on port ${OTEL_RELAY_PORT}`)}function shellQuote2(arg){return`'${arg.replace(/'/g,"'\\''")}'`}function writeTmuxLaunchScript(workerId,fullCommand){let{chmodSync:chmodSync3,mkdirSync:mkdirSync14,writeFileSync:writeFileSync13}=__require("fs"),{join:join41}=__require("path"),{homedir:homedir27}=__require("os"),dir=join41(homedir27(),".genie","spawn-scripts");mkdirSync14(dir,{recursive:!0});let safeId=workerId.replace(/[^a-zA-Z0-9._-]/g,"-"),scriptPath=join41(dir,`${safeId}-${Date.now().toString(36)}.sh`);return writeFileSync13(scriptPath,`#!/bin/sh
|
|
1964
1964
|
exec ${fullCommand}
|
|
1965
|
-
`,{mode:448}),chmodSync3(scriptPath,448),scriptPath}function buildInitialSplitWindowCommand(windowId,cwd,fullCommand){let cwdFlag=cwd?` -c ${shellQuote2(cwd)}`:"";return genieTmuxCmd(`split-window -d -t ${shellQuote2(windowId)}${cwdFlag} -P -F '#{pane_id}' ${shellQuote2(fullCommand)}`)}async function resolveSpawnTeamWindow(team,cwd,sessionOverride){if(!team)return null;try{let sessionName=sessionOverride;if(!sessionName)sessionName=(await getTeam(team))?.tmuxSessionName;if(!sessionName)sessionName=await resolveRepoSession(cwd);if(!sessionName)sessionName=team;return await ensureTeamWindow(sessionName,team,cwd)}catch(err){return console.warn(`Warning: could not ensure team window for "${team}": ${err instanceof Error?err.message:err}`),null}}async function autoConfirmTrustPrompt(paneId){let{execSync:execSync10}=__require("child_process"),maxWaitMs=15000,pollMs=500,start=Date.now();while(Date.now()-start<15000){await new Promise((r)=>setTimeout(r,500));let content;try{content=execSync10(genieTmuxCmd(`capture-pane -t '${paneId}' -p`),{encoding:"utf-8"})}catch{return}if(content.includes("trust this folder")||content.includes("Quick safety check")){try{execSync10(genieTmuxCmd(`send-keys -t '${paneId}' Enter`),{encoding:"utf-8"})}catch{}return}if(content.includes("Claude Code")||content.includes("\u276F")||content.includes("Churning"))return}}function createTmuxPane(ctx,teamWindow){let{execSync:execSync10}=__require("child_process"),useLaunchScript=ctx.validated.provider==="claude"&&Boolean(ctx.validated.nativeTeam?.enabled),tmuxCommand=useLaunchScript?shellQuote2(writeTmuxLaunchScript(ctx.workerId,ctx.fullCommand)):shellQuote2(ctx.fullCommand),tmuxPrefix=genieTmuxCmd("");if(ctx.validated.windowTarget){let cwdFlag2=ctx.cwd?` -c ${shellQuote2(ctx.cwd)}`:"",cmd=`${tmuxPrefix}split-window -d -t ${shellQuote2(ctx.validated.windowTarget)}${cwdFlag2} -P -F '#{pane_id}' ${tmuxCommand}`;return execSync10(cmd,{encoding:"utf-8"}).trim()}if(ctx.validated.newWindow){let session=ctx.sessionOverride??teamWindow?.
|
|
1965
|
+
`,{mode:448}),chmodSync3(scriptPath,448),scriptPath}function buildInitialSplitWindowCommand(windowId,cwd,fullCommand){let cwdFlag=cwd?` -c ${shellQuote2(cwd)}`:"";return genieTmuxCmd(`split-window -d -t ${shellQuote2(windowId)}${cwdFlag} -P -F '#{pane_id}' ${shellQuote2(fullCommand)}`)}async function resolveSpawnTeamWindow(team,cwd,sessionOverride){if(!team)return null;try{let sessionName=sessionOverride;if(!sessionName)sessionName=(await getTeam(team))?.tmuxSessionName;if(!sessionName)sessionName=await resolveRepoSession(cwd);if(!sessionName)sessionName=team;return await ensureTeamWindow(sessionName,team,cwd)}catch(err){return console.warn(`Warning: could not ensure team window for "${team}": ${err instanceof Error?err.message:err}`),null}}async function autoConfirmTrustPrompt(paneId){let{execSync:execSync10}=__require("child_process"),maxWaitMs=15000,pollMs=500,start=Date.now();while(Date.now()-start<15000){await new Promise((r)=>setTimeout(r,500));let content;try{content=execSync10(genieTmuxCmd(`capture-pane -t '${paneId}' -p`),{encoding:"utf-8"})}catch{return}if(content.includes("trust this folder")||content.includes("Quick safety check")){try{execSync10(genieTmuxCmd(`send-keys -t '${paneId}' Enter`),{encoding:"utf-8"})}catch{}return}if(content.includes("Claude Code")||content.includes("\u276F")||content.includes("Churning"))return}}function createTmuxPane(ctx,teamWindow){let{execSync:execSync10}=__require("child_process"),useLaunchScript=ctx.validated.provider==="claude"&&Boolean(ctx.validated.nativeTeam?.enabled),tmuxCommand=useLaunchScript?shellQuote2(writeTmuxLaunchScript(ctx.workerId,ctx.fullCommand)):shellQuote2(ctx.fullCommand),tmuxPrefix=genieTmuxCmd("");if(ctx.validated.windowTarget){let cwdFlag2=ctx.cwd?` -c ${shellQuote2(ctx.cwd)}`:"",cmd=`${tmuxPrefix}split-window -d -t ${shellQuote2(ctx.validated.windowTarget)}${cwdFlag2} -P -F '#{pane_id}' ${tmuxCommand}`;return execSync10(cmd,{encoding:"utf-8"}).trim()}if(ctx.validated.newWindow){let session=ctx.sessionOverride??teamWindow?.sessionName??ctx.validated.team,cwdFlag2=ctx.cwd?` -c ${shellQuote2(ctx.cwd)}`:"",sessionExists2=!1;try{execSync10(`${tmuxPrefix}has-session -t ${shellQuote2(`=${session}`)}`,{stdio:"ignore"}),sessionExists2=!0}catch{sessionExists2=!1}if(!sessionExists2)execSync10(`${tmuxPrefix}new-session -d -s ${shellQuote2(session)} -n home${cwdFlag2}`,{stdio:"ignore"});let cmd=`${tmuxPrefix}new-window -a -d -t ${shellQuote2(`${session}:`)} -n claude${cwdFlag2} -P -F '#{pane_id}' ${tmuxCommand}`;return execSync10(cmd,{encoding:"utf-8"}).trim()}if(teamWindow?.created){let cwdFlag2=ctx.cwd?` -c ${shellQuote2(ctx.cwd)}`:"",paneId=execSync10(`${tmuxPrefix}split-window -d -t ${shellQuote2(teamWindow.windowId)}${cwdFlag2} -P -F '#{pane_id}' ${tmuxCommand}`,{encoding:"utf-8"}).trim();try{execSync10(genieTmuxCmd(`kill-pane -t '${teamWindow.paneId}'`),{stdio:"ignore"})}catch{}return paneId}let callerPane=process.env.TMUX_PANE;if(!teamWindow&&!callerPane)throw Error("createTmuxPane: refusing to split with no target \u2014 neither teamWindow nor TMUX_PANE is set. "+"This indicates a missing --team or --window flag, or a caller outside tmux. See ~/.genie/reports/trace-genie-spawn-wrong-window.md");let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:`-t '${callerPane}'`,cwdFlag=ctx.cwd?`-c '${ctx.cwd}'`:"";if(useLaunchScript){let splitCmd2=`${tmuxPrefix}split-window -d ${splitTarget} ${cwdFlag} -P -F '#{pane_id}' ${tmuxCommand}`;return execSync10(splitCmd2,{encoding:"utf-8"}).trim()}let escapedCmd=ctx.fullCommand.replace(/'/g,"'\\''"),splitCmd=`${tmuxPrefix}split-window -d ${splitTarget} ${cwdFlag} -P -F '#{pane_id}' '${escapedCmd}'`;return execSync10(splitCmd,{encoding:"utf-8"}).trim()}async function applySpawnLayout(ctx,teamWindow){let{execSync:execSync10}=__require("child_process"),session=await getCurrentSessionName()??ctx.validated.team,layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{execSync10(genieTmuxCmd(buildLayoutCommand(layoutTarget,ctx.layoutMode)),{stdio:"ignore"})}catch{}}async function createTmuxExecutor(ctx,paneId,pid,teamWindow){if(!ctx.agentIdentityId||!ctx.executorId)return;await createAndLinkExecutor2(ctx.agentIdentityId,ctx.validated.provider,resolveExecutorTransport2(ctx.validated.provider,"tmux"),{id:ctx.executorId,pid,tmuxSession:ctx.validated.team,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:ctx.claudeSessionId??null,state:"spawning",repoPath:ctx.cwd,paneColor:ctx.spawnColor})}async function finalizeTmuxSpawn(ctx,paneId,teamWindow,workerEntry){if(ctx.spawnColor&&paneId!=="inline")await applyPaneColor(paneId,ctx.spawnColor,teamWindow?.windowId);if(await saveTemplate({id:ctx.validated.role??ctx.workerId,provider:ctx.validated.provider,team:ctx.validated.team,role:ctx.validated.role,skill:ctx.validated.skill,cwd:ctx.cwd,extraArgs:ctx.extraArgs,nativeTeamEnabled:workerEntry.nativeTeamEnabled,lastSpawnedAt:new Date().toISOString()}),ctx.otelRelayActive&&paneId!=="%0")registerOtelRelayPane(ctx.workerId,paneId,ctx.agentName,ctx.spawnColor,ctx.cwd);if(teamWindow)console.log(` Window: ${teamWindow.windowName} (${teamWindow.windowId})`);printSpawnInfo(ctx,paneId,workerEntry)}async function awaitAgentReadiness(paneId){if(paneId==="inline")return;let result2=await waitForAgentReady(paneId);if(result2.ready)console.log(` \u2713 Agent ready (${(result2.elapsedMs/1000).toFixed(1)}s)`);else console.log(` \u26A0 Agent readiness timeout (${Math.round(result2.elapsedMs/1000)}s) \u2014 proceeding anyway`)}async function launchTmuxSpawn(ctx){let isolatedSessionSpawn=ctx.validated.newWindow===!0&&Boolean(ctx.sessionOverride),teamWindow=ctx.spawnIntoCurrentWindow||isolatedSessionSpawn?null:await resolveSpawnTeamWindow(ctx.validated.team,ctx.cwd,ctx.sessionOverride),paneId;try{paneId=createTmuxPane(ctx,teamWindow)}catch(err){return console.error(`Failed to create tmux pane: ${err instanceof Error?err.message:"unknown error"}`),process.exit(1)}let pid=await capturePanePid2(paneId);if(await createTmuxExecutor(ctx,paneId,pid,teamWindow),await applySpawnLayout(ctx,teamWindow),ctx.validated.provider==="claude")await autoConfirmTrustPrompt(paneId);let workerEntry=await registerSpawnWorker(ctx,paneId,teamWindow);if(await notifySpawnJoin(ctx,paneId),await finalizeTmuxSpawn(ctx,paneId,teamWindow,workerEntry),await awaitAgentReadiness(paneId),ctx.executorId)await updateExecutorState(ctx.executorId,"running").catch(()=>{});return await update(ctx.workerId,{state:"idle"}).catch(()=>{}),paneId}async function runSdkQuery(ctx,permConfig,streamOpts,sdkConfig,runtimeExtraOptions){let{ClaudeSdkProvider:ClaudeSdkProvider2}=await Promise.resolve().then(() => (init_claude_sdk(),exports_claude_sdk)),{startSession:startSession2,recordTurn:recordTurn2,updateTurnCount:updateTurnCount2,endSession:endSession2}=await Promise.resolve().then(() => exports_sdk_session_capture),{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db)),sdkProvider=new ClaudeSdkProvider2,spawnContext={agentId:ctx.agentIdentityId??ctx.workerId,executorId:ctx.executorId??crypto.randomUUID(),team:ctx.validated.team,role:ctx.validated.role,skill:ctx.validated.skill,cwd:ctx.cwd,model:ctx.validated.model,systemPrompt:ctx.validated.systemPrompt,systemPromptFile:ctx.validated.systemPromptFile,initialPrompt:ctx.validated.initialPrompt,name:ctx.validated.name},safePgCall=async(_op,fn,fallback)=>{try{let sql=await getConnection2();return await fn(sql)}catch{return fallback}},prompt2=ctx.validated.initialPrompt??`You are ${ctx.validated.role??"an agent"} on team "${ctx.validated.team}". Awaiting instructions.`,resumeSessionId=typeof runtimeExtraOptions?.resume==="string"?runtimeExtraOptions.resume:void 0,dbSessionId=null,turnIndex=0;if(resumeSessionId){let resolvedClaudeSessionId=resumeSessionId,byPgId=await safePgCall("resolve-session-resume",(sql)=>sql`
|
|
1966
1966
|
SELECT s.id, s.total_turns, COALESCE(s.claude_session_id, e.claude_session_id) as csid
|
|
1967
1967
|
FROM sessions s
|
|
1968
1968
|
LEFT JOIN executors e ON e.id = s.executor_id
|
|
@@ -2001,7 +2001,7 @@ Use a different --role name for a second worker, e.g.: --role ${role}-2`),proces
|
|
|
2001
2001
|
LIMIT 1
|
|
2002
2002
|
`;if(anchor.length>0)await relinkExecutorToAgent(anchor[0].id,agentId),createdAnchor=!0,decision=await shouldResume(agentId)}}}return await recordAuditEvent("agent",agentId,"recover.surgery_applied",getActor(),{flippedAutoResume:flipped.length>0,staleSpawningTerminated:terminated.length,createdAnchor,sessionId:decision.sessionId??null}).catch(()=>{}),{flippedAutoResume:flipped.length>0,staleSpawningTerminated:terminated.length,createdAnchor,sessionId:decision.sessionId??null}}async function jsonlHeadMatchesCustomName(filePath,customName){let{open:open4}=await import("fs/promises"),handle=null;try{handle=await open4(filePath,"r");let buffer2=Buffer.alloc(16384),{bytesRead}=await handle.read(buffer2,0,buffer2.length,0),head=buffer2.toString("utf-8",0,bytesRead);for(let line of head.split(`
|
|
2003
2003
|
`).slice(0,40)){let trimmed=line.trim();if(!trimmed)continue;try{let entry2=JSON.parse(trimmed);if(typeof entry2.agentName==="string"&&entry2.agentName===customName)return!0}catch{}}return!1}catch{return!1}finally{await handle?.close().catch(()=>{})}}async function resolveClaudeProjectDir(cwd){let{join:join41}=await import("path"),{homedir:homedir27}=await import("os"),sanitize=(p)=>p.replace(/[^a-zA-Z0-9]/g,"-"),claudeConfigDir5=process.env.CLAUDE_CONFIG_DIR??join41(homedir27(),".claude");return join41(claudeConfigDir5,"projects",sanitize(cwd))}async function listJsonlsByMtime(projectDir){let{readdir:readdir6,stat:stat5}=await import("fs/promises"),{join:join41}=await import("path"),entries;try{entries=await readdir6(projectDir)}catch{return[]}let jsonls=entries.filter((e)=>e.endsWith(".jsonl")),candidates=[];for(let name of jsonls){let full=join41(projectDir,name);try{let s=await stat5(full);candidates.push({name,full,mtime:s.mtimeMs})}catch{}}return candidates.sort((a,b2)=>b2.mtime-a.mtime),candidates}async function scanJsonlByCustomName(cwd,customName){let projectDir=await resolveClaudeProjectDir(cwd),candidates=await listJsonlsByMtime(projectDir);for(let candidate of candidates)if(await jsonlHeadMatchesCustomName(candidate.full,customName))return candidate.name.replace(/\.jsonl$/,"");return null}async function confirmRecover(agentId){if(!process.stdin.isTTY)return console.error(`Refusing to recover "${agentId}" without --yes in a non-interactive shell. Re-run with \`--yes\` for unattended use.`),!1;let{createInterface:createInterface3}=await import("readline"),rl=createInterface3({input:process.stdin,output:process.stdout}),answer=await new Promise((resolve6)=>{rl.question(`About to recover agent "${agentId}" (flip auto_resume, terminate stale spawning executors, resume). Continue? [y/N] `,(a)=>{rl.close(),resolve6(a.trim().toLowerCase())})});return answer==="y"||answer==="yes"}async function handleWorkerRecover(name,options){let agent=await resolveAgentForRecover(name);if(!options.yes){if(!await confirmRecover(agent.id)){console.log("Recovery cancelled.");return}}console.log(`Recovering agent "${agent.id}"...`);let surgery=await recoverSurgery(agent.id);if(surgery.flippedAutoResume)console.log(" \u2713 auto_resume re-enabled");if(surgery.staleSpawningTerminated>0)console.log(` \u2713 terminated ${surgery.staleSpawningTerminated} stale spawning executor(s)`);if(surgery.createdAnchor)console.log(" \u2713 created executor anchor from JSONL on disk");if(surgery.sessionId)console.log(` \u2713 session UUID located: ${surgery.sessionId}`);else console.error(` \u2717 no recoverable session UUID for "${agent.id}". JSONL scan in ${agent.repoPath??"<no repo_path>"} did not match custom_name="${agent.customName??agent.role??"<none>"}".`),process.exit(1);let resumeName=agent.customName??agent.role??agent.id;await handleWorkerResume(resumeName,{all:!1,noResetAttempts:!1});let post=await get(agent.id);if(post?.paneId&&post.session)console.log(` pane: ${post.paneId}`),console.log(` attach: tmux attach -t ${post.session}`)}async function buildResumeParams(agent,template,resumeSessionId){let agentName=agent.role??agent.id,provider=template?.provider??agent.provider??"claude",team=template?.team??agent.team??await discoverTeamName();if(!team)throw Error(`Cannot resume agent "${agent.id}": no team context (template, agent record, env, or session). Pass --team or set GENIE_TEAM, or run inside a registered tmux session.`);let systemPromptFile,promptMode,dirEntry=await get2(agentName);if(dirEntry?.dir)systemPromptFile=loadIdentity(dirEntry)??void 0,promptMode=dirEntry.promptMode;return{provider,team,role:agentName,skill:template?.skill??agent.skill,extraArgs:template?.extraArgs,resume:resumeSessionId,name:`${team}-${agentName}`,model:dirEntry?.model,systemPromptFile,promptMode}}function formatGroupStatus(name,group,allGroups){let detail=group.status;if(group.completedAt)detail+=` (completed at ${group.completedAt})`;else if(group.startedAt)detail+=` (started at ${group.startedAt})`;if(group.status==="blocked"&&group.dependsOn.length>0){let pending=group.dependsOn.filter((dep)=>allGroups[dep]?.status!=="done");if(pending.length>0)detail+=` (depends on ${pending.join(", ")})`}return`Group ${name}: ${detail}`}async function buildResumeContext(agent){if((agent.role==="team-lead"||agent.team&&agent.role===await resolveTeamLeaderName(agent.team))&&agent.wishSlug)try{let state=await(await Promise.resolve().then(() => (init_wish_state(),exports_wish_state))).getState(agent.wishSlug,agent.repoPath);if(state){let groupLines=Object.entries(state.groups).map(([name,group])=>formatGroupStatus(name,group,state.groups));return["You were resumed after a crash. Here's where you left off:",`Wish: ${state.wish}`,"",...groupLines,"",`Continue from where you left off. Run \`genie status ${state.wish}\` to verify, then dispatch the next wave.`].join(`
|
|
2004
|
-
`)}}catch{}if(agent.team)return"You were resumed. Check your team's current state with `genie status`.";return}async function buildFullResumeParams(agent,template){let decision=await shouldResume(agent.id);if(!decision.resume||!decision.sessionId){let errReason=decision.reason==="unknown_agent"?"no_executor":"null_session";throw new MissingResumeSessionError(agent.id,void 0,errReason)}let params=await buildResumeParams(agent,template,decision.sessionId),resumeContext=await buildResumeContext(agent);if(resumeContext)params.initialPrompt=resumeContext;if(agent.nativeTeamEnabled){let nativeResult=await resolveNativeTeam(params.team,agent.repoPath,{provider:params.provider,role:params.role,color:agent.nativeColor});if(nativeResult.nativeTeam)params.nativeTeam=nativeResult.nativeTeam}return params}async function createResumeExecutor(agent,params,paneId,teamWindow,cwd,spawnColor){let resumeAgentName=agent.role??agent.id,resumeTeam=agent.team??params.team,agentId=params.agentId??(await findOrCreateAgent(resumeAgentName,resumeTeam,agent.role)).id;await terminateActiveExecutorWithCleanup(agentId);let pid=await capturePanePid2(paneId);await createAndLinkExecutor2(agentId,params.provider,resolveExecutorTransport2(params.provider,"tmux"),{id:params.executorId,pid,tmuxSession:params.team,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:params.resume??null,state:"spawning",repoPath:cwd,paneColor:spawnColor})}function resumeTelemetryState(raw){return raw&&TELEMETRY_KNOWN_STATES.has(raw)?raw:"unknown"}function recordManualResumeTelemetry(shouldEmit,eventType,payload){if(!shouldEmit)return;recordAuditEvent("agent.resume",payload.entity_id,eventType,getActor(),{...payload,trigger:"manual"}).catch(()=>{});try{let v2={entity_id:payload.entity_id,attempt_number:payload.attempt_number,state_before:payload.state_before,state_after:payload.state_after,trigger:"manual"};if(payload.last_error)v2.last_error=payload.last_error.slice(0,500);if(eventType==="agent.resume.failed")v2.exhausted=payload.exhausted??!1;emitEvent(eventType,v2,{severity:eventType==="agent.resume.failed"?"warn":"info",source_subsystem:"cli.resume"})}catch{}}function buildResumeSpawnCtx(args){let{agent,validated,launch,fullCommand,now,template,resumeSessionId,teamName,agentIdentityId,executorId}=args;return{workerId:agent.id,validated,launch,layoutMode:resolveLayoutMode(void 0),fullCommand,agentName:agent.role??agent.id,spawnColor:agent.nativeColor??"blue",parentSessionId:agent.parentSessionId??`genie-${teamName}`,claudeSessionId:resumeSessionId,otelRelayActive:!1,now,transport:"tmux",extraArgs:template?.extraArgs,cwd:template?.cwd??agent.repoPath,spawnIntoCurrentWindow:!1,autoResume:agent.autoResume,agentIdentityId,executorId}}function createResumeTmuxPaneOrExit(ctx,teamWindow,telemetry){try{return createTmuxPane(ctx,teamWindow)}catch(err){let errorMessage=err instanceof Error?err.message:"unknown error";recordManualResumeTelemetry(telemetry.shouldEmit,"agent.resume.failed",{entity_id:telemetry.entityId,attempt_number:telemetry.attemptNumber,state_before:telemetry.stateBefore,state_after:telemetry.stateBefore,last_error:`createTmuxPane: ${errorMessage}`,exhausted:!1}),console.error(`Failed to create tmux pane: ${errorMessage}`),process.exit(1)}}function logResumeSuccess(agent,resumeSessionId,paneId,teamWindow){if(console.log(`Agent "${agent.id}" resumed.`),console.log(` Session: ${resumeSessionId??"(none)"}`),console.log(` Pane: ${paneId}`),teamWindow)console.log(` Window: ${teamWindow.windowName} (${teamWindow.windowId})`)}async function resumeAgent(agent,opts={}){let resetAttempts=opts.resetAttempts!==!1,template=(await listTemplates()).find((t)=>t.id===(agent.role??agent.id)),shouldEmitTelemetry=resetAttempts,telemetryStateBefore=resumeTelemetryState(agent.state),telemetryAttemptNumber=1;if(resetAttempts)await update(agent.id,{resumeAttempts:0});recordManualResumeTelemetry(shouldEmitTelemetry,"agent.resume.attempted",{entity_id:agent.id,attempt_number:1,state_before:telemetryStateBefore,state_after:telemetryStateBefore});let params=await buildFullResumeParams(agent,template),agentIdentity=await findOrCreateAgent(agent.role??agent.id,agent.team??params.team,agent.role),executorId=crypto.randomUUID();params.agentId=agentIdentity.id,params.executorId=executorId;let validated=validateSpawnParams(params),launch=buildLaunchCommand(validated),fullCommand=prependEnvVars(launch.command,launch.env),now=new Date().toISOString();if(!process.env.TMUX)console.error("Error: resume requires tmux. Start a tmux session first."),process.exit(1);let ctx=buildResumeSpawnCtx({agent,validated,launch,fullCommand,now,template,resumeSessionId:params.resume,teamName:params.team,agentIdentityId:agentIdentity.id,executorId}),teamWindow=await resolveSpawnTeamWindow(validated.team,ctx.cwd),paneId=createResumeTmuxPaneOrExit(ctx,teamWindow,{shouldEmit:shouldEmitTelemetry,entityId:agent.id,attemptNumber:1,stateBefore:telemetryStateBefore});if(await createResumeExecutor(agent,validated,paneId,teamWindow,ctx.cwd,ctx.spawnColor),await applySpawnLayout(ctx,teamWindow),await update(agent.id,{paneId,state:"spawning",startedAt:now,lastStateChange:now,suspendedAt:void 0,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId,window:teamWindow?.windowName}),await notifySpawnJoin(ctx,paneId),await injectResumeContext(ctx.cwd??agent.repoPath??process.cwd(),agent.id,agent.role??agent.id,params.team),ctx.spawnColor&&paneId!=="inline")await applyPaneColor(paneId,ctx.spawnColor,teamWindow?.windowId);recordAuditEvent("worker",agent.id,"resumed",getActor(),{claudeSessionId:params.resume,team:agent.team}).catch(()=>{}),recordManualResumeTelemetry(shouldEmitTelemetry,"agent.resume.succeeded",{entity_id:agent.id,attempt_number:1,state_before:telemetryStateBefore,state_after:"spawning"}),logResumeSuccess(agent,params.resume,paneId,teamWindow)}async function resolveWorkerLiveness(w){if(/^%\d+$/.test(w.paneId))return{alive:await isPaneAliveOrDead(w.paneId),state:w.state};let execState=await getLiveExecutorState(w.id);return{alive:execState!==null,state:execState??w.state}}async function buildWorkerStatusMap(workers){let statusMap=new Map;for(let w of workers){let name=w.customName??w.role??w.id,{alive,state}=await resolveWorkerLiveness(w);if(alive)statusMap.set(name,{state,team:w.team||"-"});else if(w.state==="suspended"||w.state==="error"){let attempts=w.resumeAttempts??0,max=w.maxResumeAttempts??3,autoStr=w.autoResume===!1?"off":"on";statusMap.set(name,{state:`${w.state} (${attempts}/${max} resumes, auto-resume: ${autoStr})`,team:w.team||"-",resumeAttempts:attempts,maxResumeAttempts:max,autoResume:w.autoResume!==!1})}}return statusMap}async function resolveAgentNamesBySource(source){let executorRegistry=await Promise.resolve().then(() => (init_executor_registry(),exports_executor_registry)),agentRegistry=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry)),executors=await executorRegistry.listExecutors(void 0,source),agentIds=new Set(executors.map((e)=>e.agentId)),agents=await agentRegistry.listAgents({});return new Set(agents.filter((a)=>agentIds.has(a.id)).map((a)=>a.customName??a.role??a.id))}async function handleLsCommand(options){let dirEntries=await ls(),workers=await list(),statusMap=await buildWorkerStatusMap(workers),sourceAgentNames=options.source?await resolveAgentNamesBySource(options.source):void 0,entries=[];for(let entry2 of dirEntries){let running2=statusMap.get(entry2.name);entries.push({name:entry2.name,dir:entry2.dir||"-",status:running2?running2.state:"offline",team:running2?.team||"-",model:entry2.model||"-",resumeAttempts:running2?.resumeAttempts,maxResumeAttempts:running2?.maxResumeAttempts,autoResume:running2?.autoResume}),statusMap.delete(entry2.name)}for(let[name,info]of statusMap)entries.push({name,dir:"(built-in)",status:info.state,team:info.team,model:"-",resumeAttempts:info.resumeAttempts,maxResumeAttempts:info.maxResumeAttempts,autoResume:info.autoResume});if(sourceAgentNames)entries=entries.filter((e)=>sourceAgentNames.has(e.name));if(!options.all)entries=entries.filter((e)=>e.status!=="archived");if(options.json){console.log(JSON.stringify(entries,null,2));return}if(entries.length===0){console.log("No agents registered. Use `genie dir add <name> --dir <path>` to register one.");return}console.log(""),console.log(formatLsRow("NAME","DIR","STATUS","TEAM","MODEL")),console.log("-".repeat(106));for(let e of entries)console.log(formatLsRow(e.name,e.dir,e.status,e.team,e.model));console.log("")}function formatLsRow(name,dir,status,team,model){return`${name.padEnd(20).substring(0,20)}${dir.padEnd(30).substring(0,30)}${status.padEnd(44).substring(0,44)}${team.padEnd(12).substring(0,12)}${model}`}var UUID_REGEX,RecoverAgentNotFoundError,TELEMETRY_KNOWN_STATES;var init_agents=__esm(()=>{init_agent_directory();init_agent_registry();init_audit();init_builtin_agents();init_claude_native_teams();init_codex_config();init_defaults();init_emit();init_ensure_tmux();init_executor_registry();init_otel_receiver();init_protocol_router_spawn();init_protocol_router();init_provider_adapters();init_registry2();init_should_resume();init_spawn_command();init_team_manager();init_tmux_wrapper();init_tmux();init_tmux();init_workspace();UUID_REGEX=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;RecoverAgentNotFoundError=class RecoverAgentNotFoundError extends Error{recoverName;constructor(recoverName){super(`Agent "${recoverName}" not found. Tried exact id, dir:<name>, custom_name, and role lookups. Run \`genie agent list\` to see registered agents.`);this.name="RecoverAgentNotFoundError",this.recoverName=recoverName}};TELEMETRY_KNOWN_STATES=new Set(["spawning","working","idle","permission","question","done","error","suspended"])});var exports_codex_logs={};__export(exports_codex_logs,{parseCodexLine:()=>parseCodexLine,extractCodexContent:()=>extractCodexContent,codexTranscriptProvider:()=>codexTranscriptProvider});import{access as access2,readFile as readFile10,readdir as readdir6}from"fs/promises";import{homedir as homedir27}from"os";import{join as join41}from"path";function getCodexDir(){return join41(homedir27(),".codex")}function getSessionsDir(){return join41(getCodexDir(),"sessions")}function getStateDbPath(){return join41(getCodexDir(),"state_5.sqlite")}async function discoverLogPath(worker){let cwd=worker.worktree||worker.repoPath,sqlitePath=await discoverViaSqlite(cwd);if(sqlitePath)try{return await access2(sqlitePath),sqlitePath}catch{}return discoverViaScan(cwd)}async function discoverViaSqlite(cwd){try{let{Database}=await import("bun:sqlite"),dbPath=getStateDbPath(),db=new Database(dbPath,{readonly:!0});try{return db.query("SELECT rollout_path FROM threads WHERE cwd = ? ORDER BY updated_at DESC LIMIT 1").get(cwd)?.rollout_path??null}finally{db.close()}}catch{return null}}async function listDirsDesc(parent,pattern){return(await readdir6(parent)).filter((d)=>pattern.test(d)).sort().reverse()}async function discoverViaScan(cwd){let sessionsDir=getSessionsDir();try{let years=await listDirsDesc(sessionsDir,/^\d{4}$/);for(let year of years.slice(0,2)){let result2=await scanYear(join41(sessionsDir,year),cwd);if(result2)return result2}}catch{}return null}async function scanYear(yearDir,cwd){let months=await listDirsDesc(yearDir,/^\d{2}$/);for(let month of months.slice(0,2)){let result2=await scanMonth(join41(yearDir,month),cwd);if(result2)return result2}return null}async function scanMonth(monthDir,cwd){let days=await listDirsDesc(monthDir,/^\d{2}$/);for(let day of days.slice(0,3)){let result2=await scanDay(join41(monthDir,day),cwd);if(result2)return result2}return null}async function scanDay(dayDir,cwd){let files=(await readdir6(dayDir)).filter((f)=>f.endsWith(".jsonl")).sort().reverse();for(let file of files.slice(0,5)){let filePath=join41(dayDir,file);if((await readSessionMeta(filePath))?.cwd===cwd)return filePath}return null}async function readSessionMeta(filePath){try{let content=await readFile10(filePath,"utf-8"),nlIdx=content.indexOf(`
|
|
2004
|
+
`)}}catch{}if(agent.team)return"You were resumed. Check your team's current state with `genie status`.";return}async function buildFullResumeParams(agent,template){let decision=await shouldResume(agent.id);if(!decision.resume||!decision.sessionId){let errReason=decision.reason==="unknown_agent"?"no_executor":"null_session";throw new MissingResumeSessionError(agent.id,void 0,errReason)}let params=await buildResumeParams(agent,template,decision.sessionId),resumeContext=await buildResumeContext(agent);if(resumeContext)params.initialPrompt=resumeContext;if(agent.nativeTeamEnabled){let nativeResult=await resolveNativeTeam(params.team,agent.repoPath,{provider:params.provider,role:params.role,color:agent.nativeColor});if(nativeResult.nativeTeam)params.nativeTeam=nativeResult.nativeTeam}return params}async function createResumeExecutor(agent,params,paneId,teamWindow,cwd,spawnColor){let resumeAgentName=agent.role??agent.id,resumeTeam=agent.team??params.team,agentId=params.agentId??(await findOrCreateAgent(resumeAgentName,resumeTeam,agent.role)).id;await terminateActiveExecutorWithCleanup(agentId);let pid=await capturePanePid2(paneId);await createAndLinkExecutor2(agentId,params.provider,resolveExecutorTransport2(params.provider,"tmux"),{id:params.executorId,pid,tmuxSession:params.team,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:params.resume??null,state:"spawning",repoPath:cwd,paneColor:spawnColor})}function resumeTelemetryState(raw){return raw&&TELEMETRY_KNOWN_STATES.has(raw)?raw:"unknown"}function recordManualResumeTelemetry(shouldEmit,eventType,payload){if(!shouldEmit)return;recordAuditEvent("agent.resume",payload.entity_id,eventType,getActor(),{...payload,trigger:"manual"}).catch(()=>{});try{let v2={entity_id:payload.entity_id,attempt_number:payload.attempt_number,state_before:payload.state_before,state_after:payload.state_after,trigger:"manual"};if(payload.last_error)v2.last_error=payload.last_error.slice(0,500);if(eventType==="agent.resume.failed")v2.exhausted=payload.exhausted??!1;emitEvent(eventType,v2,{severity:eventType==="agent.resume.failed"?"warn":"info",source_subsystem:"cli.resume"})}catch{}}function buildResumeSpawnCtx(args){let{agent,validated,launch,fullCommand,now,template,resumeSessionId,teamName,agentIdentityId,executorId}=args;return{workerId:agent.id,validated,launch,layoutMode:resolveLayoutMode(void 0),fullCommand,agentName:agent.role??agent.id,spawnColor:agent.nativeColor??"blue",parentSessionId:agent.parentSessionId??`genie-${teamName}`,claudeSessionId:resumeSessionId,otelRelayActive:!1,now,transport:"tmux",extraArgs:template?.extraArgs,cwd:template?.cwd??agent.repoPath,spawnIntoCurrentWindow:!1,autoResume:agent.autoResume,agentIdentityId,executorId}}function createResumeTmuxPaneOrExit(ctx,teamWindow,telemetry){try{return createTmuxPane(ctx,teamWindow)}catch(err){let errorMessage=err instanceof Error?err.message:"unknown error";recordManualResumeTelemetry(telemetry.shouldEmit,"agent.resume.failed",{entity_id:telemetry.entityId,attempt_number:telemetry.attemptNumber,state_before:telemetry.stateBefore,state_after:telemetry.stateBefore,last_error:`createTmuxPane: ${errorMessage}`,exhausted:!1}),console.error(`Failed to create tmux pane: ${errorMessage}`),process.exit(1)}}function logResumeSuccess(agent,resumeSessionId,paneId,teamWindow){if(console.log(`Agent "${agent.id}" resumed.`),console.log(` Session: ${resumeSessionId??"(none)"}`),console.log(` Pane: ${paneId}`),teamWindow)console.log(` Window: ${teamWindow.windowName} (${teamWindow.windowId})`)}async function resumeAgent(agent,opts={}){let resetAttempts=opts.resetAttempts!==!1,template=(await listTemplates()).find((t)=>t.id===(agent.role??agent.id)),shouldEmitTelemetry=resetAttempts,telemetryStateBefore=resumeTelemetryState(agent.state),telemetryAttemptNumber=1;if(resetAttempts)await update(agent.id,{resumeAttempts:0});recordManualResumeTelemetry(shouldEmitTelemetry,"agent.resume.attempted",{entity_id:agent.id,attempt_number:1,state_before:telemetryStateBefore,state_after:telemetryStateBefore});let params=await buildFullResumeParams(agent,template),agentIdentity=await findOrCreateAgent(agent.role??agent.id,agent.team??params.team,agent.role),executorId=crypto.randomUUID();params.agentId=agentIdentity.id,params.executorId=executorId;let validated=validateSpawnParams(params),launch=buildLaunchCommand(validated),fullCommand=prependEnvVars(launch.command,launch.env),now=new Date().toISOString();if(!process.env.TMUX)console.error("Error: resume requires tmux. Start a tmux session first."),process.exit(1);let ctx=buildResumeSpawnCtx({agent,validated,launch,fullCommand,now,template,resumeSessionId:params.resume,teamName:params.team,agentIdentityId:agentIdentity.id,executorId}),teamWindow=await resolveSpawnTeamWindow(validated.team,ctx.cwd),paneId=createResumeTmuxPaneOrExit(ctx,teamWindow,{shouldEmit:shouldEmitTelemetry,entityId:agent.id,attemptNumber:1,stateBefore:telemetryStateBefore});if(await createResumeExecutor(agent,validated,paneId,teamWindow,ctx.cwd,ctx.spawnColor),await applySpawnLayout(ctx,teamWindow),await update(agent.id,{paneId,state:"spawning",startedAt:now,lastStateChange:now,suspendedAt:void 0,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId,window:teamWindow?.windowName}),await notifySpawnJoin(ctx,paneId),await injectResumeContext(ctx.cwd??agent.repoPath??process.cwd(),agent.id,agent.role??agent.id,params.team),ctx.spawnColor&&paneId!=="inline")await applyPaneColor(paneId,ctx.spawnColor,teamWindow?.windowId);recordAuditEvent("worker",agent.id,"resumed",getActor(),{claudeSessionId:params.resume,team:agent.team}).catch(()=>{}),recordManualResumeTelemetry(shouldEmitTelemetry,"agent.resume.succeeded",{entity_id:agent.id,attempt_number:1,state_before:telemetryStateBefore,state_after:"spawning"}),logResumeSuccess(agent,params.resume,paneId,teamWindow)}async function resolveWorkerLiveness(w){if(/^%\d+$/.test(w.paneId))return{alive:await isPaneAliveOrDead(w.paneId),state:w.state};let execState=await getLiveExecutorState(w.id);return{alive:execState!==null,state:execState??w.state}}async function buildWorkerStatusMap(workers){let statusMap=new Map;for(let w of workers){let name=w.customName??w.role??w.id,{alive,state}=await resolveWorkerLiveness(w);if(alive)statusMap.set(name,{state,team:w.team||"-"});else if(w.state==="suspended"||w.state==="error"){let attempts=w.resumeAttempts??0,max=w.maxResumeAttempts??3,autoStr=w.autoResume===!1?"off":"on";statusMap.set(name,{state:`${w.state} (${attempts}/${max} resumes, auto-resume: ${autoStr})`,team:w.team||"-",resumeAttempts:attempts,maxResumeAttempts:max,autoResume:w.autoResume!==!1})}}return statusMap}async function resolveAgentNamesBySource(source){let executorRegistry=await Promise.resolve().then(() => (init_executor_registry(),exports_executor_registry)),agentRegistry=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry)),executors=await executorRegistry.listExecutors(void 0,source),agentIds=new Set(executors.map((e)=>e.agentId)),agents=await agentRegistry.listAgents({});return new Set(agents.filter((a)=>agentIds.has(a.id)).map((a)=>a.customName??a.role??a.id))}async function handleLsCommand(options){let dirEntries=await ls(),workers=await listForRender(),statusMap=await buildWorkerStatusMap(workers),sourceAgentNames=options.source?await resolveAgentNamesBySource(options.source):void 0,entries=[];for(let entry2 of dirEntries){let running2=statusMap.get(entry2.name);entries.push({name:entry2.name,dir:entry2.dir||"-",status:running2?running2.state:"offline",team:running2?.team||"-",model:entry2.model||"-",resumeAttempts:running2?.resumeAttempts,maxResumeAttempts:running2?.maxResumeAttempts,autoResume:running2?.autoResume}),statusMap.delete(entry2.name)}for(let[name,info]of statusMap)entries.push({name,dir:"(built-in)",status:info.state,team:info.team,model:"-",resumeAttempts:info.resumeAttempts,maxResumeAttempts:info.maxResumeAttempts,autoResume:info.autoResume});if(sourceAgentNames)entries=entries.filter((e)=>sourceAgentNames.has(e.name));if(!options.all)entries=entries.filter((e)=>e.status!=="archived");if(options.json){console.log(JSON.stringify(entries,null,2));return}if(entries.length===0){console.log("No agents registered. Use `genie dir add <name> --dir <path>` to register one.");return}console.log(""),console.log(formatLsRow("NAME","DIR","STATUS","TEAM","MODEL")),console.log("-".repeat(106));for(let e of entries)console.log(formatLsRow(e.name,e.dir,e.status,e.team,e.model));console.log("")}function formatLsRow(name,dir,status,team,model){return`${name.padEnd(20).substring(0,20)}${dir.padEnd(30).substring(0,30)}${status.padEnd(44).substring(0,44)}${team.padEnd(12).substring(0,12)}${model}`}var UUID_REGEX,RecoverAgentNotFoundError,TELEMETRY_KNOWN_STATES;var init_agents=__esm(()=>{init_agent_directory();init_agent_registry();init_audit();init_builtin_agents();init_claude_native_teams();init_codex_config();init_defaults();init_emit();init_ensure_tmux();init_executor_registry();init_otel_receiver();init_protocol_router_spawn();init_protocol_router();init_provider_adapters();init_registry2();init_should_resume();init_spawn_command();init_team_manager();init_tmux_wrapper();init_tmux();init_tmux();init_workspace();UUID_REGEX=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;RecoverAgentNotFoundError=class RecoverAgentNotFoundError extends Error{recoverName;constructor(recoverName){super(`Agent "${recoverName}" not found. Tried exact id, dir:<name>, custom_name, and role lookups. Run \`genie agent list\` to see registered agents.`);this.name="RecoverAgentNotFoundError",this.recoverName=recoverName}};TELEMETRY_KNOWN_STATES=new Set(["spawning","working","idle","permission","question","done","error","suspended"])});var exports_codex_logs={};__export(exports_codex_logs,{parseCodexLine:()=>parseCodexLine,extractCodexContent:()=>extractCodexContent,codexTranscriptProvider:()=>codexTranscriptProvider});import{access as access2,readFile as readFile10,readdir as readdir6}from"fs/promises";import{homedir as homedir27}from"os";import{join as join41}from"path";function getCodexDir(){return join41(homedir27(),".codex")}function getSessionsDir(){return join41(getCodexDir(),"sessions")}function getStateDbPath(){return join41(getCodexDir(),"state_5.sqlite")}async function discoverLogPath(worker){let cwd=worker.worktree||worker.repoPath,sqlitePath=await discoverViaSqlite(cwd);if(sqlitePath)try{return await access2(sqlitePath),sqlitePath}catch{}return discoverViaScan(cwd)}async function discoverViaSqlite(cwd){try{let{Database}=await import("bun:sqlite"),dbPath=getStateDbPath(),db=new Database(dbPath,{readonly:!0});try{return db.query("SELECT rollout_path FROM threads WHERE cwd = ? ORDER BY updated_at DESC LIMIT 1").get(cwd)?.rollout_path??null}finally{db.close()}}catch{return null}}async function listDirsDesc(parent,pattern){return(await readdir6(parent)).filter((d)=>pattern.test(d)).sort().reverse()}async function discoverViaScan(cwd){let sessionsDir=getSessionsDir();try{let years=await listDirsDesc(sessionsDir,/^\d{4}$/);for(let year of years.slice(0,2)){let result2=await scanYear(join41(sessionsDir,year),cwd);if(result2)return result2}}catch{}return null}async function scanYear(yearDir,cwd){let months=await listDirsDesc(yearDir,/^\d{2}$/);for(let month of months.slice(0,2)){let result2=await scanMonth(join41(yearDir,month),cwd);if(result2)return result2}return null}async function scanMonth(monthDir,cwd){let days=await listDirsDesc(monthDir,/^\d{2}$/);for(let day of days.slice(0,3)){let result2=await scanDay(join41(monthDir,day),cwd);if(result2)return result2}return null}async function scanDay(dayDir,cwd){let files=(await readdir6(dayDir)).filter((f)=>f.endsWith(".jsonl")).sort().reverse();for(let file of files.slice(0,5)){let filePath=join41(dayDir,file);if((await readSessionMeta(filePath))?.cwd===cwd)return filePath}return null}async function readSessionMeta(filePath){try{let content=await readFile10(filePath,"utf-8"),nlIdx=content.indexOf(`
|
|
2005
2005
|
`),firstLine=nlIdx===-1?content:content.slice(0,nlIdx),entry2=JSON.parse(firstLine);if(entry2.type==="session_meta"&&entry2.payload?.cwd)return{cwd:entry2.payload.cwd}}catch{}return null}function parseEventMsg(payload,ts3,base){if(payload.type==="user_message"){let text=String(payload.message??"");return text?[{...base,role:"user",timestamp:ts3,text}]:[]}if(payload.type==="agent_message"){let text=String(payload.message??"");return text?[{...base,role:"assistant",timestamp:ts3,text}]:[]}return[]}function parseResponseMessage(payload,ts3,base){let role=payload.role,text=extractCodexContent(payload.content);if(!text)return[];if(role==="user")return[{...base,role:"user",timestamp:ts3,text}];if(role==="developer")return[{...base,role:"system",timestamp:ts3,text}];if(role==="assistant")return[{...base,role:"assistant",timestamp:ts3,text}];return[]}function parseFunctionCall(payload,ts3,base){let name=String(payload.name??payload.type),callId=String(payload.call_id??""),input={};try{input=typeof payload.arguments==="string"?JSON.parse(payload.arguments):{}}catch{input={raw:payload.arguments}}let cmdText=input.command?String(Array.isArray(input.command)?input.command.join(" "):input.command):name;return[{...base,role:"tool_call",timestamp:ts3,text:`${name}: ${cmdText.slice(0,200)}`,toolCall:{id:callId,name,input}}]}function parseWebSearch(payload,ts3,base){let action=payload.action,query2=String(action?.query??"web search");return[{...base,role:"tool_call",timestamp:ts3,text:`web_search: ${query2.slice(0,200)}`,toolCall:{id:"",name:"web_search",input:{query:query2}}}]}function parseResponseItem(payload,ts3,base){if(payload.type==="message")return parseResponseMessage(payload,ts3,base);if(payload.type==="function_call"||payload.type==="shell")return parseFunctionCall(payload,ts3,base);if(payload.type==="function_call_output"){let output=String(payload.output??"").slice(0,500);return[{...base,role:"tool_result",timestamp:ts3,text:output}]}if(payload.type==="web_search_call")return parseWebSearch(payload,ts3,base);return[]}function parseCodexLine(line){if(!line.trim())return[];let raw;try{raw=JSON.parse(line)}catch{return[]}if(!raw.type||!raw.timestamp)return[];let base={provider:"codex",raw};if(!raw.payload||typeof raw.payload!=="object")return[];if(raw.type==="event_msg")return parseEventMsg(raw.payload,raw.timestamp,base);if(raw.type==="response_item")return parseResponseItem(raw.payload,raw.timestamp,base);return[]}function extractCodexContent(content){if(typeof content==="string")return content;if(!Array.isArray(content))return"";let parts=[];for(let item of content)if(typeof item==="string")parts.push(item);else if(item&&typeof item==="object"){if("text"in item)parts.push(String(item.text));else if("input_text"in item)parts.push(String(item.input_text))}return parts.join(" ")}async function readEntries(logPath){let content;try{content=await readFile10(logPath,"utf-8")}catch{return[]}return content.split(`
|
|
2006
2006
|
`).flatMap(parseCodexLine)}var codexTranscriptProvider;var init_codex_logs=__esm(()=>{codexTranscriptProvider={discoverLogPath,readEntries}});var exports_transcript={};__export(exports_transcript,{readTranscript:()=>readTranscript,getProvider:()=>getProvider2,applyFilter:()=>applyFilter});function applyFilter(entries,filter){if(!filter)return entries;let result2=entries;if(filter.since){let sinceMs=new Date(filter.since).getTime();result2=result2.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.roles&&filter.roles.length>0){let roles=new Set(filter.roles);result2=result2.filter((e)=>roles.has(e.role))}if(filter.last&&filter.last>0)result2=result2.slice(-filter.last);return result2}async function getClaudeProvider(){if(!_claudeProvider)_claudeProvider=(await Promise.resolve().then(() => (init_claude_logs(),exports_claude_logs))).claudeTranscriptProvider;return _claudeProvider}async function getCodexProvider(){if(!_codexProvider)_codexProvider=(await Promise.resolve().then(() => (init_codex_logs(),exports_codex_logs))).codexTranscriptProvider;return _codexProvider}async function getProvider2(worker){if((worker.provider??"claude")==="codex")return getCodexProvider();return getClaudeProvider()}async function readTranscript(worker,filter){let provider=await getProvider2(worker),logPath=await provider.discoverLogPath(worker);if(!logPath)return[];let entries=await provider.readEntries(logPath);return applyFilter(entries,filter)}var _claudeProvider,_codexProvider;function isTmuxMarkerOrNoise(line){let trimmed=line.trim();if(trimmed.includes("TMUX_MCP_START")||trimmed.includes("TMUX_MCP_DONE_"))return!0;if(line.includes('echo "TMUX_MCP_START"')||line.includes('echo "TMUX_MCP_DONE_'))return!0;if(line.includes("-bash:")||line.includes("warning: setlocale:")||line.includes("cannot change locale"))return!0;if(trimmed==="or directory")return!0;return!1}function stripTmuxMarkers(content){let filtered=content.split(`
|
|
2007
2007
|
`).filter((line)=>!isTmuxMarkerOrNoise(line));while(filtered.length>0&&filtered[0].trim()==="")filtered.shift();while(filtered.length>0&&filtered[filtered.length-1].trim()==="")filtered.pop();return filtered.join(`
|
|
@@ -4441,7 +4441,7 @@ docs/incident-response/canisterworm.md for the legitimate --unsafe-unverified co
|
|
|
4441
4441
|
ORDER BY sc.timestamp DESC
|
|
4442
4442
|
LIMIT ${limit}
|
|
4443
4443
|
`;if(options.json){console.log(JSON.stringify(rows,null,2));return}if(rows.length===0){console.log(`No results for "${query2}".`);return}for(let r of rows)console.log(`[${formatRelativeTimestamp(r.timestamp)}] ${r.agent_label??"orphaned"} / ${r.session_id.slice(0,12)}`),console.log(` ${r.role}${r.tool_name?` [${r.tool_name}]`:""}: ${r.headline}`);console.log(`
|
|
4444
|
-
(${rows.length} result${rows.length===1?"":"s"})`)}async function sessionsSyncStatusCommand(){if(!await isAvailable())console.error("Database not available."),process.exit(1);let sql=await getConnection(),status=await getBackfillStatus(sql);if(!status){console.log("No backfill has been started. It runs automatically on first daemon start.");return}let pct=status.totalFiles>0?(status.processedFiles/status.totalFiles*100).toFixed(1):"0.0",mbRead=(status.processedBytes/1024/1024).toFixed(1),mbTotal=(status.totalBytes/1024/1024).toFixed(1);console.log(`Session backfill: ${status.processedFiles} / ${status.totalFiles} files (${pct}%)`),console.log(`Bytes read: ${mbRead} MB / ${mbTotal} MB`),console.log(`Errors: ${status.errors}`),console.log(`Status: ${status.status}`)}function registerSessionsCommands(program2){let sessions2=program2.command("sessions").description("Session history \u2014 list, replay, search");sessions2.command("list",{isDefault:!0}).description("List Claude Code sessions").option("--active","Show only active sessions").option("--orphaned","Show only orphaned sessions").option("--agent <name>","Filter by agent").option("--source <name>","Filter by executor metadata source (e.g. omni)").option("--limit <n>","Max number of sessions to return (default: 50)").option("--json","Output as JSON").action(async(options)=>{await sessionsListCommand(options)}),sessions2.command("replay <session-id>").description("Replay a session \u2014 interleave content + events").option("--json","Output as JSON").action(async(sessionId,options)=>{await sessionsReplayCommand(sessionId,options)}),sessions2.command("search <query>").description("Full-text search across session content").option("--json","Output as JSON").option("--limit <n>","Max results","20").action(async(query2,options)=>{await sessionsSearchCommand(query2,options)}),sessions2.command("sync").description("Check session backfill progress").action(async()=>{await sessionsSyncStatusCommand()})}init_state();init_observability_health();init_agent_registry();init_agent_registry();init_derived_signals();init_derived_signals();init_executor_registry();init_should_resume();init_term_format();var ANSI2={reset:"\x1B[0m",dim:"\x1B[2m",bold:"\x1B[1m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",cyan:"\x1B[36m",magenta:"\x1B[35m"};function colorize(text,color2){if(process.env.NO_COLOR||!process.stdout.isTTY)return text;return`${ANSI2[color2]}${text}${ANSI2.reset}`}async function aggregateAgentDecisions(includeArchived){let agents=await
|
|
4444
|
+
(${rows.length} result${rows.length===1?"":"s"})`)}async function sessionsSyncStatusCommand(){if(!await isAvailable())console.error("Database not available."),process.exit(1);let sql=await getConnection(),status=await getBackfillStatus(sql);if(!status){console.log("No backfill has been started. It runs automatically on first daemon start.");return}let pct=status.totalFiles>0?(status.processedFiles/status.totalFiles*100).toFixed(1):"0.0",mbRead=(status.processedBytes/1024/1024).toFixed(1),mbTotal=(status.totalBytes/1024/1024).toFixed(1);console.log(`Session backfill: ${status.processedFiles} / ${status.totalFiles} files (${pct}%)`),console.log(`Bytes read: ${mbRead} MB / ${mbTotal} MB`),console.log(`Errors: ${status.errors}`),console.log(`Status: ${status.status}`)}function registerSessionsCommands(program2){let sessions2=program2.command("sessions").description("Session history \u2014 list, replay, search");sessions2.command("list",{isDefault:!0}).description("List Claude Code sessions").option("--active","Show only active sessions").option("--orphaned","Show only orphaned sessions").option("--agent <name>","Filter by agent").option("--source <name>","Filter by executor metadata source (e.g. omni)").option("--limit <n>","Max number of sessions to return (default: 50)").option("--json","Output as JSON").action(async(options)=>{await sessionsListCommand(options)}),sessions2.command("replay <session-id>").description("Replay a session \u2014 interleave content + events").option("--json","Output as JSON").action(async(sessionId,options)=>{await sessionsReplayCommand(sessionId,options)}),sessions2.command("search <query>").description("Full-text search across session content").option("--json","Output as JSON").option("--limit <n>","Max results","20").action(async(query2,options)=>{await sessionsSearchCommand(query2,options)}),sessions2.command("sync").description("Check session backfill progress").action(async()=>{await sessionsSyncStatusCommand()})}init_state();init_observability_health();init_agent_registry();init_agent_registry();init_derived_signals();init_derived_signals();init_executor_registry();init_should_resume();init_term_format();var ANSI2={reset:"\x1B[0m",dim:"\x1B[2m",bold:"\x1B[1m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",cyan:"\x1B[36m",magenta:"\x1B[35m"};function colorize(text,color2){if(process.env.NO_COLOR||!process.stdout.isTTY)return text;return`${ANSI2[color2]}${text}${ANSI2.reset}`}async function aggregateAgentDecisions(includeArchived){let agents=await listAgentsForRender({includeArchived}),results=Array(agents.length),cursor=0,cap=Math.min(BOOT_PASS_CONCURRENCY_CAP,Math.max(1,agents.length)),workers=Array.from({length:cap},async()=>{while(cursor<agents.length){let i2=cursor++;if(i2>=agents.length)return;let a=agents[i2],decision=await shouldResume(a.id).catch(()=>({resume:!1,reason:"no_session_id",rehydrate:"lazy"})),name=a.customName??a.role??a.id,sessionPreview=decision.sessionId?decision.sessionId.slice(0,8):null,lastWriteAt=null;if(a.currentExecutorId){let exec3=await getExecutor(a.currentExecutorId).catch(()=>null);lastWriteAt=exec3?.updatedAt??exec3?.startedAt??null}results[i2]={agentId:a.id,name,kind:a.kind??null,decision,sessionPreview,lastWriteAt}}});return await Promise.all(workers),results}async function collectHealthChecks(){let report=await collectObservabilityHealth();return[{name:"partition",status:report.partition_health,message:report.next_rotation_at?`next rotation: ${report.next_rotation_at}`:void 0},{name:"watchdog",status:report.watchdog,message:report.watchdog_detail},{name:"spill journal",status:report.spill_journal==="pending"?"warn":report.spill_journal==="unknown"?"unknown":"ok",message:report.spill_path},{name:"watcher metrics",status:report.watcher_metrics,message:report.watcher_metrics==="ok"?"all six recently seen":"one or more meta-events missing"}]}function statusIcon(status){switch(status){case"ok":return colorize("\u2713","green");case"warn":return colorize("!","yellow");case"fail":return colorize("\u2717","red");default:return colorize("?","dim")}}function severityBadge(sev){if(sev==="critical")return colorize("[CRITICAL]","red");if(sev==="warn")return colorize("[WARN]","yellow");return colorize("[INFO]","dim")}function formatAgentLine(line){let kindTag=line.kind==="permanent"?colorize("p","magenta"):colorize("t","cyan"),session=line.sessionPreview?colorize(line.sessionPreview,"dim"):colorize("no-session","yellow"),lastWrite=line.lastWriteAt?formatRelativeTimestamp(line.lastWriteAt):"-",reason=line.decision.reason==="ok"?colorize("resume ready","green"):colorize(line.decision.reason,"yellow");return` [${kindTag}] ${line.name.padEnd(28).slice(0,28)} ${session.padEnd(8)} last:${lastWrite.padEnd(10)} ${reason}`}function renderResumableSection(lines){let resumable=lines.filter((l)=>l.decision.resume);if(resumable.length===0){console.log(colorize(" (no in-flight agents \u2014 every prior anchor is closed or paused)","dim"));return}for(let line of resumable)console.log(formatAgentLine(line))}function renderStuckSection(lines){let stuck=lines.filter((l)=>!l.decision.resume&&l.decision.reason!=="assignment_closed"&&l.decision.reason!=="unknown_agent");if(stuck.length===0)return;console.log(""),console.log(colorize("STUCK / NEEDS ATTENTION","bold")),console.log("-".repeat(60));for(let line of stuck)if(console.log(formatAgentLine(line)),line.decision.reason==="auto_resume_disabled")console.log(colorize(` \u2192 genie agent resume ${line.name}`,"dim"));else if(line.decision.reason==="no_session_id")console.log(colorize(` \u2192 genie agent show ${line.name} # inspect; consider archive`,"dim"))}function renderArchivedSection(lines){let done=lines.filter((l)=>l.decision.reason==="assignment_closed");if(done.length===0)return;console.log(""),console.log(colorize("DONE / ARCHIVED","bold")),console.log("-".repeat(60));for(let line of done)console.log(formatAgentLine(line))}function renderSignalsSection(signals2){if(signals2.length===0){console.log(colorize(" (no active alerts)","dim"));return}for(let sig of signals2){console.log(` ${severityBadge(sig.severity)} ${colorize(sig.type,"bold")} on ${sig.subject}`);let drilldown=SIGNAL_DRILLDOWN[sig.type];if(drilldown)console.log(colorize(` \u2192 ${drilldown}`,"dim"));if(sig.triggeredAt)console.log(colorize(` ${formatRelativeTimestamp(sig.triggeredAt)}`,"dim"))}}function renderHealthSection(checks){for(let check2 of checks){let detail=check2.message?colorize(` ${check2.message}`,"dim"):"";console.log(` ${statusIcon(check2.status)} ${check2.name.padEnd(18)} ${detail}`)}}async function renderDebugSection(){let audit=await auditAgentKind();if(console.log(""),console.log(colorize("DEBUG \u2014 kind audit","bold")),console.log("-".repeat(60)),console.log(` rows scanned: ${audit.total}`),console.log(` drift count : ${audit.drifted.length}`),audit.drifted.length>0)for(let d of audit.drifted.slice(0,10))console.log(colorize(` drift: ${d.id} stored=${d.kind??"null"} expected=${d.expected}`,"yellow"))}async function buildReport(opts){let includeArchived=opts.all===!0,[agents,signals2]=await Promise.all([aggregateAgentDecisions(includeArchived),listActiveDerivedSignals()]),partitionSignal=await detectPartitionMissing().catch(()=>null);if(partitionSignal)await recordDerivedSignal(partitionSignal).catch(()=>{}),signals2.unshift(partitionSignal);let report={agents,signals:signals2};if(opts.health)report.health=await collectHealthChecks();return report}async function statusCommand2(opts={}){let t0=Date.now(),report=await buildReport(opts);if(opts.json){console.log(JSON.stringify(report,null,2));return}if(console.log(""),console.log(colorize("IN-FLIGHT \u2014 should resume","bold")),console.log("-".repeat(60)),renderResumableSection(report.agents),renderStuckSection(report.agents),opts.all)renderArchivedSection(report.agents);if(console.log(""),console.log(colorize("ACTIVE SIGNALS","bold")),console.log("-".repeat(60)),renderSignalsSection(report.signals),report.health)console.log(""),console.log(colorize("HEALTH","bold")),console.log("-".repeat(60)),renderHealthSection(report.health);if(opts.debug)await renderDebugSection();console.log(""),console.log(colorize(` rendered in ${Date.now()-t0}ms \u2014 ${report.agents.length} agents, ${report.signals.length} signals`,"dim")),console.log("")}init_genie_tokens();init_term_format();var _taskService7;async function getTaskService7(){if(!_taskService7)_taskService7=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService7}function registerTagCommands(program2){let tag=program2.command("tag").description("Tag management");tag.command("list").description("List all tags").option("--type <typeId>","Filter by task type").option("--json","Output as JSON").action(async(options)=>{try{let tags=await(await getTaskService7()).listTags(options.type);if(options.json){console.log(JSON.stringify(tags,null,2));return}console.log(` ${padRight("ID",20)} ${padRight("NAME",20)} ${padRight("COLOR",10)} TYPE`),console.log(` ${"\u2500".repeat(55)}`);for(let t of tags)console.log(` ${padRight(t.id,20)} ${padRight(t.name,20)} ${padRight(t.color,10)} ${t.typeId??"-"}`);console.log(`
|
|
4445
4445
|
${tags.length} tag${tags.length===1?"":"s"}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),tag.command("create <name>").description("Create a custom tag").option("--color <hex>","Tag color (hex)",palette.textDim).option("--type <typeId>","Associate with a task type").action(async(name,options)=>{try{let ts3=await getTaskService7(),id=name.toLowerCase().replace(/\s+/g,"-"),t=await ts3.createTag({id,name,color:options.color,typeId:options.type});console.log(`Created tag "${t.name}" (${t.id}) with color ${t.color}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}init_term_format();var _taskService8;async function getTaskService8(){if(!_taskService8)_taskService8=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService8}var _boardService2;async function getBoardService2(){if(!_boardService2)_boardService2=await Promise.resolve().then(() => (init_board_service(),exports_board_service));return _boardService2}var _closeMergedService;async function getCloseMergedService(){if(!_closeMergedService)_closeMergedService=await Promise.resolve().then(() => (init_task_close_merged(),exports_task_close_merged));return _closeMergedService}function localActor2(name){return{actorType:"local",actorId:name}}function currentActor3(){let name=process.env.GENIE_AGENT_NAME??"cli";return localActor2(name)}function getRunId(){return process.env.GENIE_RUN_ID??`run-${Date.now()}`}var PRIORITY_COLORS={urgent:"\x1B[31m",high:"\x1B[33m",normal:"\x1B[0m",low:"\x1B[90m"},RESET2="\x1B[0m";async function resolveDefaultBoardId(){try{let{execSync:execSync17}=await import("child_process"),repoRoot=execSync17("git rev-parse --show-toplevel",{encoding:"utf-8"}).trim(),{join:join69}=await import("path"),configPath2=join69(repoRoot,".genie","config.json"),{existsSync:existsSync57,readFileSync:readFileSync36}=await import("fs");if(existsSync57(configPath2)){let config=JSON.parse(readFileSync36(configPath2,"utf-8"));if(config.activeBoard)return config.activeBoard}}catch{}return null}async function handleInvalidStageError(taskId,message){try{let task=await(await getTaskService8()).getTask(taskId);if(!task?.boardId)return;let board=await(await getBoardService2()).getBoard(task.boardId);if(!board)return;let validCols=board.columns.sort((a,b2)=>a.position-b2.position).map((c)=>c.name).join(" \u2192 ");console.error(`Error: ${message}
|
|
4446
4446
|
Valid columns for board "${board.name}": ${validCols}`),process.exit(1)}catch{}}async function resolveBoardOption(boardName){if(boardName){let board=await(await getBoardService2()).getBoard(boardName);if(!board)console.error(`Error: Board not found: ${boardName}`),process.exit(1);return board.id}return await resolveDefaultBoardId()??void 0}function getProjectName(repoPath){let parts=repoPath.split("/");return parts[parts.length-1]||repoPath}function formatTaskRow(t,showProject,hasExternal){let seq2=showProject?`${getProjectName(t.repoPath)}#${t.seq}`:`#${t.seq}`,title=truncate2(t.title,38),color2=t.status==="archived"?"\x1B[90m":PRIORITY_COLORS[t.priority]??"",due=formatDate(t.dueDate),proj=showProject?`${padRight(getProjectName(t.repoPath),16)} `:"",ext=hasExternal?`${padRight(truncate2(t.externalId??"",25),27)} `:"",statusLabel=t.status==="archived"?"\x1B[90m[archived]\x1B[0m":t.status;return` ${padRight(seq2,showProject?22:6)} ${proj}${padRight(title,40)} ${ext}${padRight(t.stage,12)} ${padRight(statusLabel,12)} ${color2}${padRight(t.priority,10)}${RESET2} ${padRight(due,12)}`}function printTaskList(tasks,showProject=!1){if(tasks.length===0){console.log("No tasks found.");return}let hasExternal=tasks.some((t)=>t.externalId),extCol=hasExternal?`${padRight("EXTERNAL",27)} `:"",projCol=showProject?`${padRight("PROJECT",16)} `:"",header=` ${padRight("#",6)} ${projCol}${padRight("TITLE",40)} ${extCol}${padRight("STAGE",12)} ${padRight("STATUS",12)} ${padRight("PRIORITY",10)} ${padRight("DUE",12)}`,lineLen=(showProject?108:92)+(hasExternal?28:0);console.log(header),console.log(` ${"\u2500".repeat(lineLen)}`);for(let t of tasks)console.log(formatTaskRow(t,showProject,hasExternal));console.log(`
|
|
4447
4447
|
${tasks.length} task${tasks.length===1?"":"s"}`)}function printTaskFields(task){console.log(""),console.log(`Task #${task.seq}: ${task.title}`),console.log("\u2500".repeat(60)),console.log(` ID: ${task.id}`),console.log(` Type: ${task.typeId}`),console.log(` Stage: ${task.stage}`),console.log(` Status: ${task.status}`),console.log(` Priority: ${task.priority}`);let optionalFields=[["Description",task.description],["Criteria",task.acceptanceCriteria],["Effort",task.estimatedEffort],["Start",task.startDate?formatDate(task.startDate):null],["Due",task.dueDate?formatDate(task.dueDate):null],["Blocked",task.blockedReason],["Parent",task.parentId],["Release",task.releaseId],["Wish",task.wishFile],["External",task.externalId],["Ext URL",task.externalUrl]];for(let[label,value]of optionalFields)if(value)console.log(` ${padRight(`${label}:`,12)} ${value}`);if(task.checkoutRunId)console.log(` Checkout: ${task.checkoutRunId} (since ${formatTimestamp(task.executionLockedAt)})`);if(console.log(` Created: ${formatTimestamp(task.createdAt)}`),task.startedAt)console.log(` Started: ${formatTimestamp(task.startedAt)}`);if(task.endedAt)console.log(` Ended: ${formatTimestamp(task.endedAt)}`)}async function printTaskRelations(task){let ts3=await getTaskService8(),actors=await ts3.getTaskActors(task.id,task.repoPath);if(actors.length>0){console.log(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260427.
|
|
3
|
+
"version": "4.260427.5",
|
|
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"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
-- 053_master_backfill_and_shadow_cleanup.sql
|
|
2
|
+
--
|
|
3
|
+
-- master-aware-spawn wish, Wave 2, Group 14 (sub-deliverables 14a + 14b).
|
|
4
|
+
--
|
|
5
|
+
-- The 2026-04-25 power-outage post-mortem surfaced that masters today fall
|
|
6
|
+
-- into two shadow patterns in PG (twin's analysis at
|
|
7
|
+
-- /tmp/genie-recover/group-1-shadow-analysis.json):
|
|
8
|
+
--
|
|
9
|
+
-- Type A — `dir:<name>` + bare-name pair (only `email` today). Both rows
|
|
10
|
+
-- exist; `findLiveWorkerFuzzy(name)` returns the bare row first,
|
|
11
|
+
-- so Group 1's `worker?.id ?? \`dir:\${recipientId}\`` chokepoint
|
|
12
|
+
-- fallback never fires. Result: master `email` re-spawns fresh
|
|
13
|
+
-- every time the live worker dies.
|
|
14
|
+
--
|
|
15
|
+
-- Type B — UUID + bare-name pair, NO `dir:<name>` row (`felipe`, `genie`,
|
|
16
|
+
-- `genie-pgserve`). The bare row's `custom_name=''` blocks the
|
|
17
|
+
-- jsonl-scan fallback (Group 7), and there is no `dir:` row for
|
|
18
|
+
-- Group 1's chokepoint to anchor on. Result: post-`unregister`
|
|
19
|
+
-- recovery is impossible without manual surgery.
|
|
20
|
+
--
|
|
21
|
+
-- This migration closes both gaps in two passes:
|
|
22
|
+
--
|
|
23
|
+
-- 1. **14b — master backfill:** for every agent row with
|
|
24
|
+
-- `kind='permanent' AND repo_path != ''` whose canonical name (custom_name
|
|
25
|
+
-- or role fallback) lacks a `dir:<name>` peer, create the missing
|
|
26
|
+
-- directory row using the bare row's identity columns. Brings
|
|
27
|
+
-- `dir:felipe`, `dir:genie`, `dir:genie-pgserve` into existence.
|
|
28
|
+
--
|
|
29
|
+
-- 2. **14a — bare-name shadow cleanup:** for every `dir:<name>` row that
|
|
30
|
+
-- pairs with a non-UUID, non-dir bare-name row whose
|
|
31
|
+
-- `current_executor_id IS NULL`, archive (state='archived',
|
|
32
|
+
-- auto_resume=false) the bare row. **Heal-not-wipe** — never DELETE.
|
|
33
|
+
-- The Group 3 guardrail in `src/lib/agent-directory.ts:rm()` blocks
|
|
34
|
+
-- DELETE on `kind='permanent' AND repo_path != ''` regardless, but
|
|
35
|
+
-- a SQL-side UPDATE bypasses that lock by design.
|
|
36
|
+
--
|
|
37
|
+
-- 14b runs before 14a so the dir-rows we just created can pair with their
|
|
38
|
+
-- bare shadows in the same migration. After this migration runs:
|
|
39
|
+
--
|
|
40
|
+
-- - `dir:email`, `dir:felipe`, `dir:genie`, `dir:genie-pgserve` all exist
|
|
41
|
+
-- and carry `repo_path`. Group 1's chokepoint extension covers all four.
|
|
42
|
+
-- - bare `email`, `felipe`, `genie`, `genie-pgserve` rows with
|
|
43
|
+
-- `current_executor_id IS NULL` are archived. registry.get(name) now
|
|
44
|
+
-- returns either nothing (so worker is null → Group 1 fallback fires) or
|
|
45
|
+
-- the dir:<name> row directly.
|
|
46
|
+
--
|
|
47
|
+
-- Idempotent: each pass gates on a NOT-EXISTS / DISTINCT-FROM-archived
|
|
48
|
+
-- predicate. Re-running the migration affects zero additional rows.
|
|
49
|
+
--
|
|
50
|
+
-- Audit: every backfilled row emits `directory.master_backfilled`; every
|
|
51
|
+
-- archived bare shadow emits `state_changed` with reason
|
|
52
|
+
-- `bare_name_shadow_archived`.
|
|
53
|
+
|
|
54
|
+
-- ---------------------------------------------------------------------------
|
|
55
|
+
-- Pass 1 (14b): backfill dir:<name> rows for masters that lack one.
|
|
56
|
+
-- ---------------------------------------------------------------------------
|
|
57
|
+
WITH
|
|
58
|
+
-- Pick one canonical source row per "name" equivalence class. The bare
|
|
59
|
+
-- row of a Type-B pair (custom_name='' but role + repo_path set) is the
|
|
60
|
+
-- only source carrying repo_path, so we filter on repo_path-non-empty
|
|
61
|
+
-- candidates here. Prefer rows with non-empty custom_name when both
|
|
62
|
+
-- candidates exist (NULLIF + ORDER BY in DISTINCT ON).
|
|
63
|
+
backfill_targets AS (
|
|
64
|
+
SELECT DISTINCT ON (COALESCE(NULLIF(a.custom_name, ''), a.role))
|
|
65
|
+
COALESCE(NULLIF(a.custom_name, ''), a.role) AS name,
|
|
66
|
+
a.role,
|
|
67
|
+
a.team,
|
|
68
|
+
a.repo_path
|
|
69
|
+
FROM agents a
|
|
70
|
+
WHERE a.kind = 'permanent'
|
|
71
|
+
AND a.id NOT LIKE 'dir:%'
|
|
72
|
+
AND a.repo_path IS NOT NULL AND a.repo_path <> ''
|
|
73
|
+
AND COALESCE(NULLIF(a.custom_name, ''), a.role) IS NOT NULL
|
|
74
|
+
AND a.auto_resume = true
|
|
75
|
+
AND a.state IS DISTINCT FROM 'archived'
|
|
76
|
+
AND NOT EXISTS (
|
|
77
|
+
SELECT 1 FROM agents d
|
|
78
|
+
WHERE d.id = 'dir:' || COALESCE(NULLIF(a.custom_name, ''), a.role)
|
|
79
|
+
)
|
|
80
|
+
ORDER BY
|
|
81
|
+
COALESCE(NULLIF(a.custom_name, ''), a.role),
|
|
82
|
+
-- Prefer rows that already populate custom_name (UUID peer in Type B),
|
|
83
|
+
-- so role/team fields come from the canonical identity row.
|
|
84
|
+
(CASE WHEN a.custom_name IS NOT NULL AND a.custom_name <> '' THEN 0 ELSE 1 END),
|
|
85
|
+
a.id
|
|
86
|
+
),
|
|
87
|
+
inserted AS (
|
|
88
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, metadata)
|
|
89
|
+
SELECT
|
|
90
|
+
'dir:' || c.name,
|
|
91
|
+
c.role,
|
|
92
|
+
-- The unique partial index `idx_agents_custom_name_team` requires
|
|
93
|
+
-- (custom_name, team) to be unique when both are non-null. If a peer
|
|
94
|
+
-- already owns that slot (Type B production: UUID peer with
|
|
95
|
+
-- custom_name=name, team=team), set custom_name=NULL on the dir row
|
|
96
|
+
-- so the new row sits outside the unique index. Session-sync's
|
|
97
|
+
-- `getAgentByName(name, team)` lookup will route to the UUID peer
|
|
98
|
+
-- while it lives; once the peer is unregistered, an UPDATE
|
|
99
|
+
-- backfill (separate migration if the slot ever frees) can repopulate.
|
|
100
|
+
CASE
|
|
101
|
+
WHEN c.team IS NOT NULL AND EXISTS (
|
|
102
|
+
SELECT 1 FROM agents x
|
|
103
|
+
WHERE x.custom_name = c.name AND x.team = c.team
|
|
104
|
+
AND x.id <> 'dir:' || c.name
|
|
105
|
+
) THEN NULL
|
|
106
|
+
ELSE c.name
|
|
107
|
+
END,
|
|
108
|
+
c.team,
|
|
109
|
+
c.repo_path,
|
|
110
|
+
now(),
|
|
111
|
+
NULL,
|
|
112
|
+
'{}'::jsonb
|
|
113
|
+
FROM backfill_targets c
|
|
114
|
+
ON CONFLICT (id) DO NOTHING
|
|
115
|
+
RETURNING id
|
|
116
|
+
)
|
|
117
|
+
INSERT INTO audit_events (entity_type, entity_id, event_type, actor, details)
|
|
118
|
+
SELECT 'agent', i.id, 'directory.master_backfilled',
|
|
119
|
+
'migration:053_master_backfill_and_shadow_cleanup',
|
|
120
|
+
jsonb_build_object('reason', 'master_backfill',
|
|
121
|
+
'wish', 'master-aware-spawn',
|
|
122
|
+
'group', '14b')
|
|
123
|
+
FROM inserted i;
|
|
124
|
+
|
|
125
|
+
-- ---------------------------------------------------------------------------
|
|
126
|
+
-- Pass 2 (14a): archive bare-name shadows whose dir:<name> peer now exists.
|
|
127
|
+
-- Heal-not-wipe — never DELETE. Idempotent via state IS DISTINCT FROM.
|
|
128
|
+
-- ---------------------------------------------------------------------------
|
|
129
|
+
WITH archived AS (
|
|
130
|
+
UPDATE agents bare
|
|
131
|
+
SET state = 'archived',
|
|
132
|
+
auto_resume = false,
|
|
133
|
+
last_state_change = now()
|
|
134
|
+
FROM agents dir
|
|
135
|
+
WHERE dir.id = 'dir:' || bare.id
|
|
136
|
+
AND bare.id NOT LIKE 'dir:%'
|
|
137
|
+
-- Exclude UUID-shaped ids (4 hyphens). Same heuristic as migration 050.
|
|
138
|
+
AND bare.id NOT LIKE '%-%-%-%-%'
|
|
139
|
+
AND bare.current_executor_id IS NULL
|
|
140
|
+
AND bare.state IS DISTINCT FROM 'archived'
|
|
141
|
+
AND bare.repo_path IS NOT NULL
|
|
142
|
+
AND bare.repo_path <> ''
|
|
143
|
+
-- Belt-and-suspenders identity match: the bare row's role or id must
|
|
144
|
+
-- agree with the dir's custom_name. Prevents archiving an unrelated
|
|
145
|
+
-- bare row whose id happens to suffix a dir: id.
|
|
146
|
+
AND (
|
|
147
|
+
bare.role = dir.custom_name
|
|
148
|
+
OR bare.id = dir.custom_name
|
|
149
|
+
OR bare.custom_name = dir.custom_name
|
|
150
|
+
)
|
|
151
|
+
RETURNING bare.id
|
|
152
|
+
)
|
|
153
|
+
INSERT INTO audit_events (entity_type, entity_id, event_type, actor, details)
|
|
154
|
+
SELECT 'worker', a.id, 'state_changed',
|
|
155
|
+
'migration:053_master_backfill_and_shadow_cleanup',
|
|
156
|
+
jsonb_build_object('reason', 'bare_name_shadow_archived',
|
|
157
|
+
'state', 'archived',
|
|
158
|
+
'auto_resume', false,
|
|
159
|
+
'wish', 'master-aware-spawn',
|
|
160
|
+
'group', '14a')
|
|
161
|
+
FROM archived a;
|
|
@@ -187,6 +187,11 @@ describe.skipIf(!DB_AVAILABLE)('migration 049 — agents.kind GENERATED column',
|
|
|
187
187
|
`--glob '!src/db/migrations/049_agents_kind_generated.sql' ` +
|
|
188
188
|
`--glob '!src/db/migrations/046_dir_agents_state_null.sql' ` +
|
|
189
189
|
`--glob '!src/db/migrations/agents-kind.test.ts' ` +
|
|
190
|
+
// Migration 053's test file (master-aware-spawn Group 14a+b)
|
|
191
|
+
// legitimately queries `id LIKE 'dir:%'` to verify dir-row
|
|
192
|
+
// backfill + bare-shadow archive shape post-migration. Distinguishing
|
|
193
|
+
// dir-shape vs bare-shape is the whole point — not permanence inference.
|
|
194
|
+
`--glob '!src/db/migrations/master-backfill-and-shadow-cleanup.test.ts' ` +
|
|
190
195
|
// The state-machine invariants test file legitimately references
|
|
191
196
|
// the pattern in test descriptions, comments, and the rg pattern
|
|
192
197
|
// it itself constructs.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for migration 053 — master_backfill_and_shadow_cleanup.
|
|
3
|
+
*
|
|
4
|
+
* Covers Group 14 sub-deliverables 14a (bare-name shadow archival) and 14b
|
|
5
|
+
* (master backfill of dir:<name> rows).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { getConnection } from '../../lib/db.js';
|
|
12
|
+
import { DB_AVAILABLE, setupTestDatabase } from '../../lib/test-db.js';
|
|
13
|
+
|
|
14
|
+
const MIGRATION_PATH = join(import.meta.dir, '053_master_backfill_and_shadow_cleanup.sql');
|
|
15
|
+
|
|
16
|
+
async function applyMigrationManually(): Promise<void> {
|
|
17
|
+
const sql = await getConnection();
|
|
18
|
+
const body = await readFile(MIGRATION_PATH, 'utf-8');
|
|
19
|
+
await sql.unsafe(body);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe.skipIf(!DB_AVAILABLE)('migration 053 — master_backfill_and_shadow_cleanup', () => {
|
|
23
|
+
let cleanup: () => Promise<void>;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
cleanup = await setupTestDatabase();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await cleanup();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
const sql = await getConnection();
|
|
35
|
+
await sql`DELETE FROM audit_events WHERE actor = 'migration:053_master_backfill_and_shadow_cleanup'`;
|
|
36
|
+
await sql`DELETE FROM assignments`;
|
|
37
|
+
await sql`DELETE FROM executors`;
|
|
38
|
+
await sql`DELETE FROM agents`;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ==========================================================================
|
|
42
|
+
// 14b — master backfill: dir:<name> created from existing canonical fields
|
|
43
|
+
// ==========================================================================
|
|
44
|
+
|
|
45
|
+
test('14b: backfills dir:felipe from a bare felipe row (Type-B shape)', async () => {
|
|
46
|
+
const sql = await getConnection();
|
|
47
|
+
// Twin's analysis: the bare row is the only candidate carrying
|
|
48
|
+
// repo_path. `custom_name` is empty/null in production (the partial
|
|
49
|
+
// unique index `idx_agents_custom_name_team` requires it to be unique
|
|
50
|
+
// when populated alongside team — bare rows skirt that by leaving it
|
|
51
|
+
// null); role is the canonical identity.
|
|
52
|
+
await sql`
|
|
53
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
54
|
+
VALUES ('felipe', 'felipe', NULL, 'felipe', '/home/genie/workspace/agents/felipe', now(), NULL, true, NULL)
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
await applyMigrationManually();
|
|
58
|
+
|
|
59
|
+
const dirRow = await sql<
|
|
60
|
+
{ id: string; role: string; custom_name: string | null; team: string; repo_path: string }[]
|
|
61
|
+
>`
|
|
62
|
+
SELECT id, role, custom_name, team, repo_path
|
|
63
|
+
FROM agents WHERE id = 'dir:felipe'
|
|
64
|
+
`;
|
|
65
|
+
expect(dirRow.length).toBe(1);
|
|
66
|
+
expect(dirRow[0].role).toBe('felipe');
|
|
67
|
+
expect(dirRow[0].custom_name).toBe('felipe');
|
|
68
|
+
expect(dirRow[0].team).toBe('felipe');
|
|
69
|
+
expect(dirRow[0].repo_path).toBe('/home/genie/workspace/agents/felipe');
|
|
70
|
+
|
|
71
|
+
const audit = await sql<{ details: { reason: string; group: string } }[]>`
|
|
72
|
+
SELECT details FROM audit_events
|
|
73
|
+
WHERE actor = 'migration:053_master_backfill_and_shadow_cleanup'
|
|
74
|
+
AND entity_id = 'dir:felipe'
|
|
75
|
+
AND event_type = 'directory.master_backfilled'
|
|
76
|
+
`;
|
|
77
|
+
expect(audit.length).toBe(1);
|
|
78
|
+
expect(audit[0].details.reason).toBe('master_backfill');
|
|
79
|
+
expect(audit[0].details.group).toBe('14b');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('14b: backfills dir:genie and dir:genie-pgserve from twin-shape masters', async () => {
|
|
83
|
+
const sql = await getConnection();
|
|
84
|
+
await sql`
|
|
85
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
86
|
+
VALUES
|
|
87
|
+
('genie', 'genie', NULL, 'genie', '/home/genie/workspace/agents/genie', now(), 'idle', true, NULL),
|
|
88
|
+
('genie-pgserve', 'genie-pgserve', NULL, 'genie', '/home/genie/workspace/agents/genie-pgserve', now(), NULL, true, NULL)
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
await applyMigrationManually();
|
|
92
|
+
|
|
93
|
+
const created = await sql<{ id: string }[]>`
|
|
94
|
+
SELECT id FROM agents WHERE id LIKE 'dir:%' ORDER BY id
|
|
95
|
+
`;
|
|
96
|
+
expect(created.map((r: { id: string }) => r.id)).toEqual(['dir:genie', 'dir:genie-pgserve']);
|
|
97
|
+
|
|
98
|
+
const repoPaths = await sql<{ id: string; repo_path: string }[]>`
|
|
99
|
+
SELECT id, repo_path FROM agents WHERE id LIKE 'dir:%' ORDER BY id
|
|
100
|
+
`;
|
|
101
|
+
const byId = new Map(repoPaths.map((r: { id: string; repo_path: string }) => [r.id, r.repo_path]));
|
|
102
|
+
expect(byId.get('dir:genie')).toBe('/home/genie/workspace/agents/genie');
|
|
103
|
+
expect(byId.get('dir:genie-pgserve')).toBe('/home/genie/workspace/agents/genie-pgserve');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('14b: NULLs custom_name when (name, team) slot is held by another live peer', async () => {
|
|
107
|
+
const sql = await getConnection();
|
|
108
|
+
// Production Type-B: UUID peer holds (custom_name='felipe', team='felipe')
|
|
109
|
+
// in the unique partial index. The bare row lacks dir:<name>. Backfilling
|
|
110
|
+
// dir:felipe with custom_name='felipe' team='felipe' would conflict with
|
|
111
|
+
// the index, so the migration must NULL custom_name on the new row.
|
|
112
|
+
await sql`
|
|
113
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
114
|
+
VALUES
|
|
115
|
+
('00000000-0000-0000-0000-feedfacefeed', 'felipe', 'felipe', 'felipe', '/some/uuid/path', now(), 'idle', true, NULL),
|
|
116
|
+
('felipe', 'felipe', NULL, 'felipe', '/home/genie/workspace/agents/felipe', now(), 'idle', true, NULL)
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
await applyMigrationManually();
|
|
120
|
+
|
|
121
|
+
const dirRow = await sql<{ custom_name: string | null }[]>`
|
|
122
|
+
SELECT custom_name FROM agents WHERE id = 'dir:felipe'
|
|
123
|
+
`;
|
|
124
|
+
expect(dirRow.length).toBe(1);
|
|
125
|
+
expect(dirRow[0].custom_name).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('14b: skips agents that already have a dir:<name> peer (no duplicate insert)', async () => {
|
|
129
|
+
const sql = await getConnection();
|
|
130
|
+
await sql`
|
|
131
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
132
|
+
VALUES ('dir:email', 'email', 'email', 'felipe', '/home/genie/workspace/agents/email', now(), NULL, true, NULL)
|
|
133
|
+
`;
|
|
134
|
+
await sql`
|
|
135
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
136
|
+
VALUES ('email', 'email', NULL, 'felipe', '/home/genie/workspace/agents/email', now(), NULL, true, NULL)
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
await applyMigrationManually();
|
|
140
|
+
|
|
141
|
+
const dirRows = await sql<{ id: string }[]>`
|
|
142
|
+
SELECT id FROM agents WHERE id LIKE 'dir:%' ORDER BY id
|
|
143
|
+
`;
|
|
144
|
+
expect(dirRows.length).toBe(1);
|
|
145
|
+
expect(dirRows[0].id).toBe('dir:email');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('14b: skips bare task-shaped rows (kind=task / archived / no repo_path)', async () => {
|
|
149
|
+
const sql = await getConnection();
|
|
150
|
+
await sql`
|
|
151
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
152
|
+
VALUES
|
|
153
|
+
('engineer-w2g3', 'engineer', 'engineer', 'master-aware-spawn', '/some/path', now(), 'idle', true, 'team-lead-uuid'),
|
|
154
|
+
('archived-master', 'archived', '', 'archived', '/some/path', now(), 'archived', true, NULL),
|
|
155
|
+
('no-repo-master', 'foo', '', 'foo', NULL, now(), NULL, true, NULL)
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
await applyMigrationManually();
|
|
159
|
+
|
|
160
|
+
const dirRows = await sql<{ id: string }[]>`SELECT id FROM agents WHERE id LIKE 'dir:%'`;
|
|
161
|
+
expect(dirRows.length).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ==========================================================================
|
|
165
|
+
// 14a — bare-name shadow cleanup (heal-not-wipe)
|
|
166
|
+
// ==========================================================================
|
|
167
|
+
|
|
168
|
+
test('14a: archives bare-name shadow when dir:<name> peer exists (no executor)', async () => {
|
|
169
|
+
const sql = await getConnection();
|
|
170
|
+
await sql`
|
|
171
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
172
|
+
VALUES
|
|
173
|
+
('dir:email', 'email', 'email', 'felipe', '/home/genie/workspace/agents/email', now(), NULL, false, NULL),
|
|
174
|
+
('email', 'email', NULL, 'felipe', '/home/genie/workspace/agents/email', now(), NULL, true, NULL)
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
await applyMigrationManually();
|
|
178
|
+
|
|
179
|
+
const bare = await sql<{ state: string | null; auto_resume: boolean }[]>`
|
|
180
|
+
SELECT state, auto_resume FROM agents WHERE id = 'email'
|
|
181
|
+
`;
|
|
182
|
+
expect(bare[0].state).toBe('archived');
|
|
183
|
+
expect(bare[0].auto_resume).toBe(false);
|
|
184
|
+
|
|
185
|
+
// dir: row left intact.
|
|
186
|
+
const dir = await sql<{ state: string | null; repo_path: string }[]>`
|
|
187
|
+
SELECT state, repo_path FROM agents WHERE id = 'dir:email'
|
|
188
|
+
`;
|
|
189
|
+
expect(dir[0].state).toBeNull();
|
|
190
|
+
expect(dir[0].repo_path).toBe('/home/genie/workspace/agents/email');
|
|
191
|
+
|
|
192
|
+
const audit = await sql<{ details: { reason: string; group: string } }[]>`
|
|
193
|
+
SELECT details FROM audit_events
|
|
194
|
+
WHERE actor = 'migration:053_master_backfill_and_shadow_cleanup'
|
|
195
|
+
AND entity_id = 'email'
|
|
196
|
+
AND event_type = 'state_changed'
|
|
197
|
+
`;
|
|
198
|
+
expect(audit.length).toBe(1);
|
|
199
|
+
expect(audit[0].details.reason).toBe('bare_name_shadow_archived');
|
|
200
|
+
expect(audit[0].details.group).toBe('14a');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('14a: NEVER deletes bare-name shadow rows (heal-not-wipe contract)', async () => {
|
|
204
|
+
const sql = await getConnection();
|
|
205
|
+
await sql`
|
|
206
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
207
|
+
VALUES
|
|
208
|
+
('dir:email', 'email', 'email', 'felipe', '/home/genie/workspace/agents/email', now(), NULL, false, NULL),
|
|
209
|
+
('email', 'email', NULL, 'felipe', '/home/genie/workspace/agents/email', now(), NULL, true, NULL)
|
|
210
|
+
`;
|
|
211
|
+
|
|
212
|
+
await applyMigrationManually();
|
|
213
|
+
|
|
214
|
+
// Row still exists (just archived). Wholesale deletion is the failure
|
|
215
|
+
// mode the master-aware-spawn wish was born from.
|
|
216
|
+
const stillThere = await sql<{ id: string; state: string | null }[]>`
|
|
217
|
+
SELECT id, state FROM agents WHERE id = 'email'
|
|
218
|
+
`;
|
|
219
|
+
expect(stillThere.length).toBe(1);
|
|
220
|
+
expect(stillThere[0].state).toBe('archived');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('14a: leaves bare-name shadow alone when current_executor_id is set (live peer)', async () => {
|
|
224
|
+
const sql = await getConnection();
|
|
225
|
+
await sql`
|
|
226
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
227
|
+
VALUES
|
|
228
|
+
('dir:email', 'email', 'email', 'felipe', '/some/path', now(), NULL, false, NULL),
|
|
229
|
+
('email', 'email', NULL, 'felipe', '/some/path', now(), 'idle', true, NULL)
|
|
230
|
+
`;
|
|
231
|
+
// Attach a live executor to the bare row.
|
|
232
|
+
await sql`
|
|
233
|
+
INSERT INTO executors (id, agent_id, provider, transport, state)
|
|
234
|
+
VALUES ('exec-live', 'email', 'claude', 'tmux', 'running')
|
|
235
|
+
`;
|
|
236
|
+
await sql`UPDATE agents SET current_executor_id = 'exec-live' WHERE id = 'email'`;
|
|
237
|
+
|
|
238
|
+
await applyMigrationManually();
|
|
239
|
+
|
|
240
|
+
const bare = await sql<{ state: string | null }[]>`
|
|
241
|
+
SELECT state FROM agents WHERE id = 'email'
|
|
242
|
+
`;
|
|
243
|
+
expect(bare[0].state).toBe('idle');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('14a: never archives UUID-shaped rows even if dir:<uuid> exists', async () => {
|
|
247
|
+
const sql = await getConnection();
|
|
248
|
+
const uuid = '11111111-2222-3333-4444-555555555555';
|
|
249
|
+
await sql`
|
|
250
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
251
|
+
VALUES
|
|
252
|
+
(${`dir:${uuid}`}, 'weird', 'weird', 'team-dir', '/p', now(), NULL, false, NULL),
|
|
253
|
+
(${uuid}, 'weird', 'weird', 'team-uuid', '/p', now(), 'idle', true, NULL)
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
await applyMigrationManually();
|
|
257
|
+
|
|
258
|
+
const uuidRow = await sql<{ state: string | null }[]>`
|
|
259
|
+
SELECT state FROM agents WHERE id = ${uuid}
|
|
260
|
+
`;
|
|
261
|
+
expect(uuidRow[0].state).toBe('idle');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ==========================================================================
|
|
265
|
+
// Composite + idempotency
|
|
266
|
+
// ==========================================================================
|
|
267
|
+
|
|
268
|
+
test('14a + 14b run in one pass: bare felipe gets archived AFTER dir:felipe is backfilled', async () => {
|
|
269
|
+
const sql = await getConnection();
|
|
270
|
+
// Only the bare row exists; the migration must first create dir:felipe
|
|
271
|
+
// (14b) and only THEN archive the bare felipe shadow (14a).
|
|
272
|
+
await sql`
|
|
273
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
274
|
+
VALUES ('felipe', 'felipe', NULL, 'felipe', '/home/genie/workspace/agents/felipe', now(), 'idle', true, NULL)
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
await applyMigrationManually();
|
|
278
|
+
|
|
279
|
+
const dir = await sql<{ id: string }[]>`SELECT id FROM agents WHERE id = 'dir:felipe'`;
|
|
280
|
+
expect(dir.length).toBe(1);
|
|
281
|
+
|
|
282
|
+
const bare = await sql<{ state: string | null; auto_resume: boolean }[]>`
|
|
283
|
+
SELECT state, auto_resume FROM agents WHERE id = 'felipe'
|
|
284
|
+
`;
|
|
285
|
+
expect(bare[0].state).toBe('archived');
|
|
286
|
+
expect(bare[0].auto_resume).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('idempotent: re-running the migration touches zero new rows', async () => {
|
|
290
|
+
const sql = await getConnection();
|
|
291
|
+
await sql`
|
|
292
|
+
INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, state, auto_resume, reports_to)
|
|
293
|
+
VALUES ('felipe', 'felipe', NULL, 'felipe', '/home/genie/workspace/agents/felipe', now(), 'idle', true, NULL)
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
await applyMigrationManually();
|
|
297
|
+
const firstAuditCount = await sql<{ cnt: number }[]>`
|
|
298
|
+
SELECT count(*)::int AS cnt FROM audit_events
|
|
299
|
+
WHERE actor = 'migration:053_master_backfill_and_shadow_cleanup'
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
await applyMigrationManually();
|
|
303
|
+
const secondAuditCount = await sql<{ cnt: number }[]>`
|
|
304
|
+
SELECT count(*)::int AS cnt FROM audit_events
|
|
305
|
+
WHERE actor = 'migration:053_master_backfill_and_shadow_cleanup'
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
expect(secondAuditCount[0].cnt).toBe(firstAuditCount[0].cnt);
|
|
309
|
+
});
|
|
310
|
+
});
|