@automagik/genie 4.260324.3 → 4.260324.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "genie",
13
- "version": "4.260324.3",
13
+ "version": "4.260324.4",
14
14
  "source": "./plugins/genie",
15
15
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
16
16
  }
package/dist/genie.js CHANGED
@@ -42,10 +42,10 @@ disable_paste_burst = true
42
42
  ${content}`;if(params.extraArgs){let fileIdx=params.extraArgs.indexOf("--append-system-prompt-file");if(fileIdx!==-1&&params.extraArgs[fileIdx+1])content=`${content}
43
43
 
44
44
  ${readFileSync6(params.extraArgs[fileIdx+1],"utf-8")}`,params.extraArgs.splice(fileIdx,2)}writeFileSync5(promptFile,content);let flag=params.promptMode==="system"?"--system-prompt-file":"--append-system-prompt-file";parts.push(flag,escapeShellArg(promptFile))}else if(params.systemPromptFile){let flag=params.promptMode==="system"?"--system-prompt-file":"--append-system-prompt-file";parts.push(flag,escapeShellArg(params.systemPromptFile))}}function buildClaudeCommand(params){preflightCheck("claude");let parts=["claude","--dangerously-skip-permissions"],env={};if(env.GENIE_WORKER="1",params.role)env.GENIE_AGENT_NAME=params.role;if(params.team)env.GENIE_TEAM=params.team;if(params.nativeTeam?.enabled)appendNativeTeamFlags(parts,env,params.nativeTeam,params);if(params.resume)parts.push("--resume",escapeShellArg(params.resume));else if(params.sessionId)parts.push("--session-id",escapeShellArg(params.sessionId));if(params.role)parts.push("--agent",escapeShellArg(params.role));if(params.model)parts.push("--model",escapeShellArg(params.model));if(params.name)parts.push("--name",escapeShellArg(params.name));if(appendSystemPromptFlags(parts,params),params.extraArgs)for(let arg of params.extraArgs)parts.push(escapeShellArg(arg));if(params.initialPrompt)parts.push(escapeShellArg(params.initialPrompt));return{command:parts.join(" "),provider:"claude",env:Object.keys(env).length>0?env:void 0,meta:{role:params.role,skill:params.skill}}}function buildCodexCommand(params){preflightCheck("codex");let parts=["codex"];if(parts.push("--yolo"),parts.push("--no-alt-screen"),params.extraArgs)for(let arg of params.extraArgs)parts.push(escapeShellArg(arg));let promptParts=[`Genie worker. Team: ${params.team}.`];if(params.role)promptParts.push(`Role: ${params.role}.`);if(params.skill)promptParts.push(`Execute the ${params.skill} skill instructions.`);let prompt2=promptParts.join(" ");return parts.push(escapeShellArg(prompt2)),{command:parts.join(" "),provider:"codex",meta:{role:params.role,skill:params.skill}}}function buildLaunchCommand(params){let validated=validateSpawnParams(params);switch(validated.provider){case"claude":return buildClaudeCommand(validated);case"codex":return buildCodexCommand(validated);default:throw Error(`Unknown provider "${validated.provider}". Valid providers: claude, codex`)}}var CLAUDE_TEAM_COLORS,spawnParamsSchema;var init_provider_adapters=__esm(()=>{init_zod();CLAUDE_TEAM_COLORS=["red","blue","green","yellow","purple","orange","pink","cyan"],spawnParamsSchema=exports_external.object({provider:exports_external.enum(["claude","codex"]),team:exports_external.string().min(1,"Team name is required"),role:exports_external.string().optional(),skill:exports_external.string().optional(),extraArgs:exports_external.array(exports_external.string()).optional(),nativeTeam:exports_external.object({enabled:exports_external.boolean(),parentSessionId:exports_external.string().optional(),color:exports_external.string().optional(),agentType:exports_external.string().optional(),planModeRequired:exports_external.boolean().optional(),permissionMode:exports_external.string().optional(),agentName:exports_external.string().optional()}).optional(),sessionId:exports_external.string().uuid().optional(),resume:exports_external.string().optional(),systemPromptFile:exports_external.string().optional(),systemPrompt:exports_external.string().optional(),promptMode:exports_external.enum(["system","append"]).optional(),model:exports_external.string().optional(),initialPrompt:exports_external.string().optional(),name:exports_external.string().optional()})});var exports_claude_native_teams={};__export(exports_claude_native_teams,{writeNativeInbox:()=>writeNativeInbox,unregisterNativeMember:()=>unregisterNativeMember,sanitizeTeamName:()=>sanitizeTeamName,registerNativeMember:()=>registerNativeMember,registerAsTeamLead:()=>registerAsTeamLead,loadConfig:()=>loadConfig,isInsideClaudeCode:()=>isInsideClaudeCode,ensureNativeTeam:()=>ensureNativeTeam,discoverTeamName:()=>discoverTeamName,discoverClaudeSessionId:()=>discoverClaudeSessionId,deleteNativeTeam:()=>deleteNativeTeam,clearNativeInbox:()=>clearNativeInbox,assignColor:()=>assignColor});import{existsSync as existsSync10}from"fs";import{mkdir as mkdir3,readFile as readFile2,readdir,rm,stat as stat2,unlink as unlink3,writeFile as writeFile2}from"fs/promises";import{homedir as homedir10}from"os";import{join as join10}from"path";function claudeConfigDir(){return process.env.CLAUDE_CONFIG_DIR??join10(homedir10(),".claude")}function teamsBaseDir(){return join10(claudeConfigDir(),"teams")}function sanitizeTeamName(name){return name.replace(/[^a-zA-Z0-9]/g,"-").toLowerCase()}function teamDir(teamName){return join10(teamsBaseDir(),sanitizeTeamName(teamName))}function configPath(teamName){return join10(teamDir(teamName),"config.json")}function inboxesDir(teamName){return join10(teamDir(teamName),"inboxes")}function inboxPath(teamName,agentName){return join10(inboxesDir(teamName),`${sanitizeTeamName(agentName)}.json`)}function lockPath(filePath){return`${filePath}.lock`}async function acquireLock2(path){let lock=lockPath(path),deadline=Date.now()+LOCK_TIMEOUT_MS2;while(Date.now()<deadline)try{await writeFile2(lock,String(process.pid),{flag:"wx"});return}catch{let jitter=Math.floor(Math.random()*LOCK_POLL_MS);await new Promise((r)=>setTimeout(r,LOCK_POLL_MS+jitter))}console.warn(`[claude-native-teams] Force-acquiring stale lock: ${lock}`),await writeFile2(lock,String(process.pid))}async function releaseLock(path){try{await unlink3(lockPath(path))}catch{}}async function loadConfig(teamName){try{let content=await readFile2(configPath(teamName),"utf-8");return JSON.parse(content)}catch(err){if(err instanceof Error&&"code"in err&&err.code==="ENOENT")return null;let message=err instanceof Error?err.message:String(err);return console.warn(`[claude-native-teams] Failed to load config for "${teamName}": ${message}`),null}}async function saveConfig(teamName,config){await writeFile2(configPath(teamName),JSON.stringify(config,null,2))}async function ensureNativeTeam(teamName,description,leadSessionId){let dir=teamDir(teamName),inboxDir=inboxesDir(teamName);await mkdir3(dir,{recursive:!0}),await mkdir3(inboxDir,{recursive:!0});let existing=await loadConfig(teamName);if(existing)return existing;let sanitized=sanitizeTeamName(teamName),config={name:sanitized,description,createdAt:Date.now(),leadAgentId:`team-lead@${sanitized}`,leadSessionId,members:[]};return await saveConfig(teamName,config),config}async function registerNativeMember(teamName,member){let config=await loadConfig(teamName);if(!config)throw Error(`Native team "${teamName}" not found`);let sanitized=sanitizeTeamName(teamName),agentId=`${sanitizeTeamName(member.agentName)}@${sanitized}`;config.members=config.members.filter((m)=>m.agentId!==agentId),config.members.push({agentId,name:sanitizeTeamName(member.agentName),agentType:member.agentType??"general-purpose",joinedAt:Date.now(),tmuxPaneId:member.tmuxPaneId,cwd:member.cwd??process.cwd(),backendType:"tmux",color:member.color,planModeRequired:member.planModeRequired??!1,isActive:!0}),await saveConfig(teamName,config);let inbox=inboxPath(teamName,member.agentName);if(!existsSync10(inbox))await writeFile2(inbox,"[]")}async function unregisterNativeMember(teamName,agentName){let config=await loadConfig(teamName);if(!config)return;let sanitized=sanitizeTeamName(teamName),agentId=`${sanitizeTeamName(agentName)}@${sanitized}`,member=config.members.find((m)=>m.agentId===agentId);if(member)member.isActive=!1;await saveConfig(teamName,config)}async function writeNativeInbox(teamName,agentName,message){let path=inboxPath(teamName,agentName);await mkdir3(inboxesDir(teamName),{recursive:!0}),await acquireLock2(path);try{let messages=[];try{let content=await readFile2(path,"utf-8");messages=JSON.parse(content)}catch{}messages.push(message),await writeFile2(path,JSON.stringify(messages,null,2))}finally{await releaseLock(path)}}async function assignColor(teamName){let config=await loadConfig(teamName);if(!config)return CLAUDE_TEAM_COLORS[0];let usedColors=new Set(config.members.map((m)=>m.color));for(let color of CLAUDE_TEAM_COLORS)if(!usedColors.has(color))return color;return CLAUDE_TEAM_COLORS[config.members.length%CLAUDE_TEAM_COLORS.length]}async function clearNativeInbox(teamName,agentName){let path=inboxPath(teamName,agentName);await acquireLock2(path);try{await writeFile2(path,"[]")}finally{await releaseLock(path)}}async function deleteNativeTeam(teamName){let dir=teamDir(teamName);if(!existsSync10(dir))return!1;return await rm(dir,{recursive:!0,force:!0}),!0}function sanitizePath(p){return p.replace(/[^a-zA-Z0-9]/g,"-")}async function discoverClaudeSessionId(cwd){let envSessionId=process.env.CLAUDE_CODE_SESSION_ID;if(envSessionId)return envSessionId;let projectDir=join10(claudeConfigDir(),"projects",sanitizePath(cwd??process.cwd()));try{let jsonls=(await readdir(projectDir)).filter((e)=>e.endsWith(".jsonl"));if(jsonls.length===0)return null;let newest=null;for(let name of jsonls){let s=await stat2(join10(projectDir,name));if(!newest||s.mtimeMs>newest.mtime)newest={name,mtime:s.mtimeMs}}if(!newest)return null;return newest.name.replace(".jsonl","")}catch{return null}}function isInsideClaudeCode(){return process.env.CLAUDECODE==="1"}async function discoverTeamName(cwd){let envTeam=process.env.GENIE_TEAM;if(envTeam)return envTeam;let sessionId=await discoverClaudeSessionId(cwd);if(!sessionId)return null;let base=teamsBaseDir();try{let teams=await readdir(base);for(let name of teams){let cfgPath=join10(base,name,"config.json");try{let content=await readFile2(cfgPath,"utf-8"),config=JSON.parse(content);if(config.leadSessionId===sessionId)return config.name}catch{}}}catch{}return null}async function registerAsTeamLead(teamName,opts){let sessionId=await discoverClaudeSessionId(opts?.cwd);if(!sessionId)throw Error("Could not discover Claude Code session ID. Are you running inside Claude Code with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1?");let config=await ensureNativeTeam(teamName,`Genie team: ${teamName}`,sessionId);if(config.leadSessionId!==sessionId)config.leadSessionId=sessionId,await saveConfig(teamName,config);let leadAgentId=`team-lead@${sanitizeTeamName(teamName)}`,existingLead=config.members.find((m)=>m.agentId===leadAgentId),resolvedPaneId=opts?.tmuxPaneId??process.env.TMUX_PANE;if(!existingLead||!existingLead.isActive)await registerNativeMember(teamName,{agentName:"team-lead",agentType:"general-purpose",color:opts?.color??"blue",tmuxPaneId:resolvedPaneId,cwd:opts?.cwd??process.cwd()});else if(resolvedPaneId&&existingLead.tmuxPaneId!==resolvedPaneId)existingLead.tmuxPaneId=resolvedPaneId,await saveConfig(teamName,config);let inbox=inboxPath(teamName,"team-lead");if(!existsSync10(inbox))await writeFile2(inbox,"[]");let finalConfig=await loadConfig(teamName);if(!finalConfig)throw Error(`Failed to load config for team "${teamName}" after creation`);return{sessionId,config:finalConfig}}var LOCK_TIMEOUT_MS2=5000,LOCK_POLL_MS=50;var init_claude_native_teams=__esm(()=>{init_provider_adapters()});var exports_team_lead_command={};__export(exports_team_lead_command,{shellQuote:()=>shellQuote,sessionExists:()=>sessionExists,ccProjectDirName:()=>ccProjectDirName,buildTeamLeadCommand:()=>buildTeamLeadCommand});import{readFileSync as readFileSync6,readdirSync as readdirSync2}from"fs";import{basename,join as join11}from"path";function shellQuote(s){return`'${s.replace(/'/g,"'\\''")}'`}function buildTeamLeadCommand(teamName,options){let sanitized=sanitizeTeamName(teamName),qTeam=shellQuote(sanitized),folderName=basename(process.cwd()),parts=["GENIE_WORKER=1","CLAUDECODE=1","CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",`GENIE_TEAM=${qTeam}`,`GENIE_AGENT_NAME=${shellQuote(folderName)}`,"claude",`--agent-id ${shellQuote(`team-lead@${sanitized}`)}`,"--agent-name team-lead",`--team-name ${qTeam}`,"--agent-type team-lead","--dangerously-skip-permissions"];if(parts.push(`--name ${shellQuote(sanitized)}`),options?.continueName)parts.push(`--resume ${shellQuote(options.continueName)}`);else if(options?.sessionId)parts.push(`--session-id ${shellQuote(options.sessionId)}`);if(options?.systemPromptFile){let promptFlag=(options?.promptMode??loadGenieConfigSync().promptMode)==="system"?"--system-prompt-file":"--append-system-prompt-file";parts.push(`${promptFlag} ${shellQuote(options.systemPromptFile)}`)}return parts.join(" ")}function ccProjectDirName(dir){return dir.replace(/\//g,"-")}function fileHasSessionName(filePath,needle){try{let lines=readFileSync6(filePath,"utf-8").split(`
45
- `).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=join11(home,".claude","projects",projectDir),files;try{files=readdirSync2(projectPath).filter((f)=>f.endsWith(".jsonl"))}catch{return!1}let needle=name.toLowerCase();return files.some((file)=>fileHasSessionName(join11(projectPath,file),needle))}catch{return!1}}var init_team_lead_command=__esm(()=>{init_claude_native_teams();init_genie_config2()});import{exec as execCallback}from"child_process";import{existsSync as existsSync11,mkdirSync as mkdirSync5}from"fs";import{homedir as homedir11}from"os";import{join as join12}from"path";import{promisify}from"util";function getLogDir(){let logDir=join12(homedir11(),".genie","logs","tmux");if(!existsSync11(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();let command=`tmux ${finalArgs.join(" ")}`,{stdout}=await exec(command,options);return stdout.trim()}var exec;var init_tmux_wrapper=__esm(()=>{exec=promisify(execCallback)});var exports_tmux={};__export(exports_tmux,{setWindowEnv:()=>setWindowEnv,listWindows:()=>listWindows,listSessions:()=>listSessions,listPanes:()=>listPanes,killSession:()=>killSession,isPaneAlive:()=>isPaneAlive,getWindowEnv:()=>getWindowEnv,getCurrentSessionName:()=>getCurrentSessionName,findWindowByName:()=>findWindowByName,findSessionByName:()=>findSessionByName,executeTmux:()=>executeTmux2,ensureTeamWindow:()=>ensureTeamWindow,createWindow:()=>createWindow,createSession:()=>createSession,capturePaneContent:()=>capturePaneContent,applyPaneColor:()=>applyPaneColor});async function executeTmux2(tmuxCommand){try{return await executeTmux(tmuxCommand)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);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(`
46
- `).map((line)=>{let[id,name,attached,windows]=line.split(":");return{id,name,attached:attached==="1",windows:Number.parseInt(windows,10)}})}catch(error2){if((error2 instanceof Error?error2.message:String(error2)).includes("no server running"))return[];throw error2}}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_active,1,0}'`);if(!output)return[];return output.split(`
47
- `).map((line)=>{let[id,name,active]=line.split(":");return{id,name,active:active==="1",sessionId}})}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("no server running")||message.includes("session not found"))return[];throw error2}}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(`
48
- `).map((line)=>{let[id,title,active]=line.split(":");return{id,windowId,title,active:active==="1"}})}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("no server running")||message.includes("window not found"))return[];throw error2}}async function capturePaneContent(paneId,lines=200,includeColors=!1){try{return await executeTmux2(`capture-pane -p ${includeColors?"-e":""} -t '${paneId}' -S -${lines} -E -`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("no server running")||message.includes("pane not found"))return"";throw error2}}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,"'\\''")}'`:"",windowId=(await executeTmux2(`new-window -d -P -F '#{window_id}' -t '${sessionId}:' -n '${name}'${cdFlag}`)).trim();if(!windowId)return null;try{await executeTmux2(`set-window-option -t '${windowId}' automatic-rename off`)}catch{}return{id:windowId,name,active:!1,sessionId}}async function findWindowByName(sessionId,name){return(await listWindows(sessionId)).find((w)=>w.name===name)||null}async function ensureTeamWindow(session,teamName,workingDir){if(!await findSessionByName(session))await createSession(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 newWindow=await createWindow(session,teamName,workingDir);if(!newWindow)throw Error(`Failed to create team window "${teamName}" in session "${session}"`);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:existsSync12,writeFileSync:writeFileSync5,mkdirSync:mkdirSync6,chmodSync:chmodSync2}=__require("fs"),{dirname:dirname3}=__require("path");if(existsSync12(PANE_COLOR_SCRIPT))return;mkdirSync6(dirname3(PANE_COLOR_SCRIPT),{recursive:!0}),writeFileSync5(PANE_COLOR_SCRIPT,`#!/bin/bash
45
+ `).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=join11(home,".claude","projects",projectDir),files;try{files=readdirSync2(projectPath).filter((f)=>f.endsWith(".jsonl"))}catch{return!1}let needle=name.toLowerCase();return files.some((file)=>fileHasSessionName(join11(projectPath,file),needle))}catch{return!1}}var init_team_lead_command=__esm(()=>{init_claude_native_teams();init_genie_config2()});import{exec as execCallback}from"child_process";import{existsSync as existsSync11,mkdirSync as mkdirSync5}from"fs";import{homedir as homedir11}from"os";import{join as join12}from"path";import{promisify}from"util";function getLogDir(){let logDir=join12(homedir11(),".genie","logs","tmux");if(!existsSync11(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();let command=`tmux ${finalArgs.join(" ")}`,{stdout}=await exec(command,options);return stdout.trim()}var exec;var init_tmux_wrapper=__esm(()=>{exec=promisify(execCallback)});var exports_tmux={};__export(exports_tmux,{setWindowEnv:()=>setWindowEnv,listWindows:()=>listWindows,listSessions:()=>listSessions,listPanes:()=>listPanes,killSession:()=>killSession,isPaneAlive:()=>isPaneAlive,getWindowEnv:()=>getWindowEnv,getCurrentSessionName:()=>getCurrentSessionName,findWindowByName:()=>findWindowByName,findSessionByName:()=>findSessionByName,executeTmux:()=>executeTmux2,ensureTeamWindow:()=>ensureTeamWindow,ensureMasterWindow:()=>ensureMasterWindow,createWindow:()=>createWindow,createSession:()=>createSession,capturePaneContent:()=>capturePaneContent,applyPaneColor:()=>applyPaneColor});async function executeTmux2(tmuxCommand){try{return await executeTmux(tmuxCommand)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);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(`
46
+ `).map((line)=>{let[id,name,attached,windows]=line.split(":");return{id,name,attached:attached==="1",windows:Number.parseInt(windows,10)}})}catch(error2){if((error2 instanceof Error?error2.message:String(error2)).includes("no server running"))return[];throw error2}}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(`
47
+ `).map((line)=>{let[id,name,indexStr,active]=line.split(":");return{id,name,index:Number.parseInt(indexStr,10),active:active==="1",sessionId}})}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("no server running")||message.includes("session not found"))return[];throw error2}}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(`
48
+ `).map((line)=>{let[id,title,active]=line.split(":");return{id,windowId,title,active:active==="1"}})}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("no server running")||message.includes("window not found"))return[];throw error2}}async function capturePaneContent(paneId,lines=200,includeColors=!1){try{return await executeTmux2(`capture-pane -p ${includeColors?"-e":""} -t '${paneId}' -S -${lines} -E -`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);if(message.includes("no server running")||message.includes("pane not found"))return"";throw error2}}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 ensureTeamWindow(session,teamName,workingDir){if(!await findSessionByName(session))await createSession(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:existsSync12,writeFileSync:writeFileSync5,mkdirSync:mkdirSync6,chmodSync:chmodSync2}=__require("fs"),{dirname:dirname3}=__require("path");if(existsSync12(PANE_COLOR_SCRIPT))return;mkdirSync6(dirname3(PANE_COLOR_SCRIPT),{recursive:!0}),writeFileSync5(PANE_COLOR_SCRIPT,`#!/bin/bash
49
49
  # Genie tmux pane color router \u2014 maps focused pane to agent border color
50
50
  PANE_ID="$1"
51
51
  MAP="$HOME/.genie/pane-colors.json"
@@ -569,7 +569,7 @@ Use a different --role name for a second worker, e.g.: --role ${role}-2`),proces
569
569
  AND t.group_name IS NOT NULL
570
570
  AND p.wish_file IS NOT NULL
571
571
  ORDER BY t.created_at
572
- `;for(let row of rows){let assignee=row.assignee;if(assignee!==workerId&&!workerId.endsWith(`-${assignee}`))continue;let slugMatch=row.wish_file.match(/\.genie\/wishes\/([^/]+)\/WISH\.md/);if(!slugMatch)continue;let slug=slugMatch[1],groupName=row.group_name,state2=await getState(slug,cwd);if(!state2)continue;let group=state2.groups[groupName];if(!group)continue;return{slug,groupName,group:{...group}}}return null}async function getState(slug,cwd){let sql=await getConnection(),repoPath=resolveRepoPath(cwd),parent=await findParent(sql,slug,repoPath);if(!parent)return null;let children=await sql`SELECT * FROM tasks WHERE parent_id = ${parent.id} ORDER BY created_at`;if(children.length===0)return null;let childIds=children.map((c)=>c.id),deps=await sql`
572
+ `;for(let row of rows){let assignee=row.assignee;if(assignee!==workerId&&!workerId.endsWith(`-${assignee}`))continue;let slugMatch=row.wish_file.match(/\.genie\/wishes\/([^/]+)\/WISH\.md/);if(!slugMatch)continue;let slug=slugMatch[1],groupName=row.group_name,state2=await getState(slug,cwd);if(!state2)continue;let group=state2.groups[groupName];if(!group)continue;return{slug,groupName,group:{...group}}}return null}async function getOrCreateState(slug,groups,cwd){let existing=await getState(slug,cwd);if(existing)return existing;return createState(slug,groups,cwd)}async function getState(slug,cwd){let sql=await getConnection(),repoPath=resolveRepoPath(cwd),parent=await findParent(sql,slug,repoPath);if(!parent)return null;let children=await sql`SELECT * FROM tasks WHERE parent_id = ${parent.id} ORDER BY created_at`;if(children.length===0)return null;let childIds=children.map((c)=>c.id),deps=await sql`
573
573
  SELECT td.task_id, t.group_name AS dep_group
574
574
  FROM task_dependencies td
575
575
  JOIN tasks t ON t.id = td.depends_on_id
@@ -979,13 +979,13 @@ Done. ${results.length} migration${results.length===1?"":"s"} applied.`),await s
979
979
  (built-in ${resolved.entry.registeredAt==="(built-in)"?"agent":"agent"})`);console.log(""),printEntry(resolved.entry),console.log("")}async function listEntries(json2,includeBuiltins){let entries=await ls();if(json2){listEntriesJson(entries,includeBuiltins);return}if(entries.length===0&&!includeBuiltins){console.log(`
980
980
  No agents registered. Add one with: genie dir add <name> --dir <path>`),console.log(`Use --builtins to also see built-in roles and council members.
981
981
  `);return}if(entries.length>0)printRegisteredTable(entries);if(includeBuiltins)printBuiltinsTable()}function listEntriesJson(entries,includeBuiltins){let result=entries.map((e)=>({...e,builtin:!1}));if(includeBuiltins)for(let b2 of ALL_BUILTINS)result.push({name:b2.name,description:b2.description,model:b2.model,category:b2.category,scope:"built-in",builtin:!0});console.log(JSON.stringify(result,null,2))}function printRegisteredTable(entries){console.log(""),console.log("REGISTERED AGENTS"),console.log("-".repeat(85)),console.log(` ${"NAME".padEnd(22)}${"SCOPE".padEnd(10)}${"DIR".padEnd(30)}${"MODE".padEnd(8)}${"MODEL".padEnd(8)}ROLES`),console.log(` ${"-".repeat(20)} ${"-".repeat(8)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(6)} ${"-".repeat(15)}`);for(let entry of entries){let dir=contractPath(entry.dir),truncDir=dir.length>28?`${dir.slice(0,25)}...`:dir,roles=entry.roles?.join(", ")||"-";console.log(` ${entry.name.padEnd(22)}${entry.scope.padEnd(10)}${truncDir.padEnd(30)}${entry.promptMode.padEnd(8)}${(entry.model||"-").padEnd(8)}${roles}`)}console.log("")}function printBuiltinsTable(){console.log("BUILT-IN AGENTS"),console.log("-".repeat(80)),console.log(` ${"NAME".padEnd(22)}${"TYPE".padEnd(10)}${"MODEL".padEnd(8)}DESCRIPTION`),console.log(` ${"-".repeat(20)} ${"-".repeat(8)} ${"-".repeat(6)} ${"-".repeat(30)}`);for(let agent of ALL_BUILTINS)console.log(` ${agent.name.padEnd(22)}${agent.category.padEnd(10)}${(agent.model||"-").padEnd(8)}${agent.description}`);console.log("")}async function handleOmniRegistration(name,options){let omniUrl=await resolveOmniApiUrl();if(!omniUrl)return;console.log(`
982
- Registering in Omni (${omniUrl})...`);let existingId=await findOmniAgent(name);if(existingId){console.log(` Agent already exists in Omni: ${existingId}`),await edit(name,{omniAgentId:existingId},{global:options.global}),console.log(" Linked existing Omni agent to directory entry.");return}let omniAgentId=await registerAgentInOmni(name,{model:options.model,roles:options.roles});if(omniAgentId)await edit(name,{omniAgentId},{global:options.global}),console.log(` Omni agent created: ${omniAgentId}`),console.log(" Session isolation: per-person + per-channel")}async function handleAgentRegister(name,options){let promptMode=validatePromptMode(options.promptMode),entry=await add({name,dir:resolvePath(options.dir),repo:options.repo?resolvePath(options.repo):void 0,promptMode,model:options.model,roles:options.roles},{global:options.global}),scope=options.global?"global":"project";if(console.log(`Agent "${entry.name}" registered (${scope}).`),printEntry(entry),!options.skipOmni)await handleOmniRegistration(name,options)}function registerAgentNamespace(program2){program2.command("agent").description("Agent lifecycle management").command("register <name>").description("Register an agent locally and auto-register in Omni when configured").requiredOption("--dir <path>","Agent folder (CWD + AGENTS.md)").option("--repo <path>","Default git repo (overridden by team)").option("--prompt-mode <mode>","Prompt mode: append or system","append").option("--model <model>","Default model (sonnet, opus, codex)").option("--roles <roles...>","Built-in roles this agent can orchestrate").option("--global","Write to global directory instead of project").option("--skip-omni","Skip Omni auto-registration").action(async(name,options)=>{try{await handleAgentRegister(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_protocol_router();init_wish_state();init_agents();import{execSync as execSync6}from"child_process";import{existsSync as existsSync19}from"fs";import{mkdir as mkdir8,readFile as readFile9,writeFile as writeFile7}from"fs/promises";import{tmpdir}from"os";import{join as join24}from"path";init_wish_state();import{execSync as execSync5}from"child_process";import{existsSync as existsSync18}from"fs";import{readFile as readFile8}from"fs/promises";import{join as join23}from"path";function parseRef(ref){let hashIdx=ref.indexOf("#");if(hashIdx===-1)throw Error(`Invalid reference "${ref}". Expected format: <slug>#<group>`);let slug=ref.slice(0,hashIdx),group=ref.slice(hashIdx+1);if(!slug||!group)throw Error(`Invalid reference "${ref}". Both slug and group are required.`);return{slug,group}}var STATUS_ICONS={blocked:"\uD83D\uDD12",ready:"\uD83D\uDFE2",in_progress:"\uD83D\uDD04",done:"\u2705"};function formatTimestamp(iso){if(!iso)return"";return new Date(iso).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",hour12:!1})}function padRight2(str2,len){return str2.length>=len?str2:str2+" ".repeat(len-str2.length)}async function detectWaveCompletion(slug,groupName,cwd){let base=cwd??process.cwd(),wishPath=join23(base,".genie","wishes",slug,"WISH.md");if(!existsSync18(wishPath))return null;let content=await readFile8(wishPath,"utf-8"),targetWave=parseExecutionStrategy(content).find((w)=>w.groups.some((g)=>g.group===groupName));if(!targetWave)return null;let state2=await getState(slug,cwd);if(!state2)return null;let waveGroupNames=targetWave.groups.map((g)=>g.group);if(!waveGroupNames.every((g)=>state2.groups[g]?.status==="done"))return null;return{waveName:targetWave.name,waveGroups:waveGroupNames}}async function ensureWorkPushed(slug,group){try{if(execSync5("git status --porcelain",{encoding:"utf-8"}).trim())console.log(" Committing dirty working tree..."),execSync5("git add -A",{encoding:"utf-8"}),execSync5(`git commit -m "wip: ${slug}#${group}"`,{encoding:"utf-8"}),console.log(` Committed as "wip: ${slug}#${group}"`)}catch{}try{if(execSync5("git log @{u}..HEAD --oneline",{encoding:"utf-8"}).trim())console.log(" Pushing unpushed commits..."),execSync5("git push",{encoding:"utf-8",timeout:30000}),console.log(" Push complete.")}catch{try{let branch=execSync5("git rev-parse --abbrev-ref HEAD",{encoding:"utf-8"}).trim();if(branch&&branch!=="HEAD")execSync5(`git push -u origin ${branch}`,{encoding:"utf-8",timeout:30000}),console.log(" Push complete (set upstream).")}catch{console.log(" \u26A0\uFE0F Push failed \u2014 manual push may be needed.")}}}function autoKillPane(){let paneId=process.env.TMUX_PANE;if(paneId)setTimeout(()=>{try{execSync5(`tmux kill-pane -t '${paneId}'`,{encoding:"utf-8"})}catch{process.exit(0)}},1000);else process.exit(0)}async function doneCommand(ref){try{let{slug,group}=parseRef(ref),result=await completeGroup(slug,group);if(console.log(`\u2705 Group "${group}" marked as done in wish "${slug}"`),result.completedAt)console.log(` Completed at: ${formatTimestamp(result.completedAt)}`);let state2=await getState(slug);if(state2){let nowReady=Object.entries(state2.groups).filter(([,g])=>g.status==="ready"&&g.dependsOn.includes(group)).map(([name])=>name);if(nowReady.length>0)console.log(` Unblocked: ${nowReady.join(", ")}`)}await ensureWorkPushed(slug,group);let waveResult=await detectWaveCompletion(slug,group);if(waveResult){console.log(` \uD83C\uDF0A ${waveResult.waveName} complete! All groups done: ${waveResult.waveGroups.join(", ")}`);try{let protocolRouter=await Promise.resolve().then(() => (init_protocol_router(),exports_protocol_router)),repoPath=process.cwd(),message=`${waveResult.waveName} complete. All groups done: [${waveResult.waveGroups.join(", ")}]. Run /review or advance to next wave.`,result2=await protocolRouter.sendMessage(repoPath,"cli","team-lead",message);if(result2&&typeof result2==="object"&&"delivered"in result2&&!result2.delivered)console.warn(" \u26A0\uFE0F Wave-complete notification may not have been delivered.");else console.log(" Notified team-lead of wave completion.")}catch{console.warn(" \u26A0\uFE0F Could not notify team-lead (messaging unavailable).")}}autoKillPane()}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}}async function statusCommand(slug){try{let state2=await getState(slug);if(!state2)console.error(`\u274C No state found for wish "${slug}"`),console.error(" This means work has not been dispatched yet."),console.error(` Run: genie work ${slug}`),process.exit(1);console.log(`
982
+ Registering in Omni (${omniUrl})...`);let existingId=await findOmniAgent(name);if(existingId){console.log(` Agent already exists in Omni: ${existingId}`),await edit(name,{omniAgentId:existingId},{global:options.global}),console.log(" Linked existing Omni agent to directory entry.");return}let omniAgentId=await registerAgentInOmni(name,{model:options.model,roles:options.roles});if(omniAgentId)await edit(name,{omniAgentId},{global:options.global}),console.log(` Omni agent created: ${omniAgentId}`),console.log(" Session isolation: per-person + per-channel")}async function handleAgentRegister(name,options){let promptMode=validatePromptMode(options.promptMode),entry=await add({name,dir:resolvePath(options.dir),repo:options.repo?resolvePath(options.repo):void 0,promptMode,model:options.model,roles:options.roles},{global:options.global}),scope=options.global?"global":"project";if(console.log(`Agent "${entry.name}" registered (${scope}).`),printEntry(entry),!options.skipOmni)await handleOmniRegistration(name,options)}function registerAgentNamespace(program2){program2.command("agent").description("Agent lifecycle management").command("register <name>").description("Register an agent locally and auto-register in Omni when configured").requiredOption("--dir <path>","Agent folder (CWD + AGENTS.md)").option("--repo <path>","Default git repo (overridden by team)").option("--prompt-mode <mode>","Prompt mode: append or system","append").option("--model <model>","Default model (sonnet, opus, codex)").option("--roles <roles...>","Built-in roles this agent can orchestrate").option("--global","Write to global directory instead of project").option("--skip-omni","Skip Omni auto-registration").action(async(name,options)=>{try{await handleAgentRegister(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_protocol_router();init_wish_state();init_agents();import{execSync as execSync6}from"child_process";import{existsSync as existsSync19}from"fs";import{mkdir as mkdir8,readFile as readFile9,writeFile as writeFile7}from"fs/promises";import{tmpdir}from"os";import{join as join24}from"path";init_wish_state();import{execSync as execSync5}from"child_process";import{existsSync as existsSync18}from"fs";import{readFile as readFile8}from"fs/promises";import{join as join23}from"path";function parseRef(ref){let hashIdx=ref.indexOf("#");if(hashIdx===-1)throw Error(`Invalid reference "${ref}". Expected format: <slug>#<group>`);let slug=ref.slice(0,hashIdx),group=ref.slice(hashIdx+1);if(!slug||!group)throw Error(`Invalid reference "${ref}". Both slug and group are required.`);return{slug,group}}var STATUS_ICONS={blocked:"\uD83D\uDD12",ready:"\uD83D\uDFE2",in_progress:"\uD83D\uDD04",done:"\u2705"};function formatTimestamp(iso){if(!iso)return"";return new Date(iso).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",hour12:!1})}function padRight2(str2,len){return str2.length>=len?str2:str2+" ".repeat(len-str2.length)}async function detectWaveCompletion(slug,groupName,cwd){let base=cwd??process.cwd(),wishPath=join23(base,".genie","wishes",slug,"WISH.md");if(!existsSync18(wishPath))return null;let content=await readFile8(wishPath,"utf-8"),targetWave=parseExecutionStrategy(content).find((w)=>w.groups.some((g)=>g.group===groupName));if(!targetWave)return null;let state2=await getState(slug,cwd);if(!state2)return null;let waveGroupNames=targetWave.groups.map((g)=>g.group);if(!waveGroupNames.every((g)=>state2.groups[g]?.status==="done"))return null;return{waveName:targetWave.name,waveGroups:waveGroupNames}}async function ensureWorkPushed(slug,group){try{if(execSync5("git status --porcelain",{encoding:"utf-8"}).trim())console.log(" Committing dirty working tree..."),execSync5("git add -A",{encoding:"utf-8"}),execSync5(`git commit -m "wip: ${slug}#${group}"`,{encoding:"utf-8"}),console.log(` Committed as "wip: ${slug}#${group}"`)}catch{}try{if(execSync5("git log @{u}..HEAD --oneline",{encoding:"utf-8"}).trim())console.log(" Pushing unpushed commits..."),execSync5("git push",{encoding:"utf-8",timeout:30000}),console.log(" Push complete.")}catch{try{let branch=execSync5("git rev-parse --abbrev-ref HEAD",{encoding:"utf-8"}).trim();if(branch&&branch!=="HEAD")execSync5(`git push -u origin ${branch}`,{encoding:"utf-8",timeout:30000}),console.log(" Push complete (set upstream).")}catch{console.log(" \u26A0\uFE0F Push failed \u2014 manual push may be needed.")}}}function autoKillPane(){let paneId=process.env.TMUX_PANE;if(paneId)setTimeout(()=>{try{execSync5(`tmux kill-pane -t '${paneId}'`,{encoding:"utf-8"})}catch{process.exit(0)}},1000);else process.exit(0)}async function doneCommand(ref){try{let{slug,group}=parseRef(ref),result=await completeGroup(slug,group);if(console.log(`\u2705 Group "${group}" marked as done in wish "${slug}"`),result.completedAt)console.log(` Completed at: ${formatTimestamp(result.completedAt)}`);let state2=await getState(slug);if(state2){let nowReady=Object.entries(state2.groups).filter(([,g])=>g.status==="ready"&&g.dependsOn.includes(group)).map(([name])=>name);if(nowReady.length>0)console.log(` Unblocked: ${nowReady.join(", ")}`)}await ensureWorkPushed(slug,group);let waveResult=await detectWaveCompletion(slug,group);if(waveResult){console.log(` \uD83C\uDF0A ${waveResult.waveName} complete! All groups done: ${waveResult.waveGroups.join(", ")}`);try{let protocolRouter=await Promise.resolve().then(() => (init_protocol_router(),exports_protocol_router)),repoPath=process.cwd(),message=`${waveResult.waveName} complete. All groups done: [${waveResult.waveGroups.join(", ")}]. Run /review or advance to next wave.`,result2=await protocolRouter.sendMessage(repoPath,"cli","team-lead",message);if(result2&&typeof result2==="object"&&"delivered"in result2&&!result2.delivered)console.warn(" \u26A0\uFE0F Wave-complete notification may not have been delivered.");else console.log(" Notified team-lead of wave completion.")}catch{console.warn(" \u26A0\uFE0F Could not notify team-lead (messaging unavailable).")}}autoKillPane()}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}}async function statusCommand(slug){try{let state2=await getState(slug);if(!state2){let base=process.cwd(),wishPath=join23(base,".genie","wishes",slug,"WISH.md");if(!existsSync18(wishPath))console.error(`\u274C No state found for wish "${slug}" and no WISH.md at ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile8(wishPath,"utf-8"),groups=parseWishGroups(content);if(groups.length===0)console.error(`\u274C No execution groups found in ${wishPath}`),process.exit(1);state2=await createState(slug,groups),console.log(`\uD83D\uDCDD Auto-initialized state for wish "${slug}" (${groups.length} groups)`)}console.log(`
983
983
  Wish: ${state2.wish}`),console.log("\u2500".repeat(60));let entries=Object.entries(state2.groups),maxNameLen=Math.max(...entries.map(([name])=>name.length),5);console.log(` ${padRight2("GROUP",maxNameLen)} STATUS ASSIGNEE STARTED COMPLETED`),console.log(` ${"\u2500".repeat(maxNameLen+62)}`);for(let[name,group]of entries){let icon=STATUS_ICONS[group.status]??"\u2753",status=padRight2(`${icon} ${group.status}`,13),assignee=padRight2(group.assignee??"-",13),started=padRight2(formatTimestamp(group.startedAt)||"-",14),completed=formatTimestamp(group.completedAt)||"-";console.log(` ${padRight2(name,maxNameLen)} ${status} ${assignee} ${started} ${completed}`)}let total=entries.length,done=entries.filter(([,g])=>g.status==="done").length,inProgress=entries.filter(([,g])=>g.status==="in_progress").length,ready=entries.filter(([,g])=>g.status==="ready").length,blocked=entries.filter(([,g])=>g.status==="blocked").length;console.log(""),console.log(` Progress: ${done}/${total} done | ${inProgress} in progress | ${ready} ready | ${blocked} blocked`),console.log("")}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}}function registerStateCommands(program2){program2.command("done <ref>").description("Mark a wish group as done (format: <slug>#<group>)").action(async(ref)=>{await doneCommand(ref)}),program2.command("status <slug>").description("Show wish state overview for all groups").action(async(slug)=>{await statusCommand(slug)}),program2.command("reset <ref>").description("Reset an in-progress group back to ready (format: <slug>#<group>)").action(async(ref)=>{try{let{slug,group}=parseRef(ref),result=await resetGroup(slug,group);if(console.log(`\uD83D\uDD04 Group "${group}" reset to ready in wish "${slug}"`),result.status==="ready")console.log(" Status: ready (assignee cleared)")}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}})}async function writeContextFile(content){let dir=join24(tmpdir(),"genie-dispatch");await mkdir8(dir,{recursive:!0});let ts=Date.now().toString(36),rand=Math.random().toString(36).slice(2,8),filePath=join24(dir,`ctx-${ts}-${rand}.md`);return await writeFile7(filePath,content),filePath}function extractGroup(content,groupName){let pattern=new RegExp(`^### Group ${escapeRegExp(groupName)}:`,"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()}function extractWishContext(content){let execGroupsIdx=content.indexOf("## Execution Groups");if(execGroupsIdx!==-1)return content.slice(0,execGroupsIdx).trim();return content.slice(0,2000).trim()}function escapeRegExp(str2){return str2.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function buildContextPrompt(opts){let parts=[`# Dispatch Context (${opts.command})`,"",`**Source file:** \`${opts.filePath}\``,"(Read the full document at the path above for complete context)",""];if(opts.wishContext)parts.push("## Wish Context","",opts.wishContext,"");if(parts.push("## Assigned Section","",opts.sectionContent,""),opts.skill)parts.push("## Initial Command","",`Run \`/${opts.skill}\` to begin.`,"");return parts.join(`
984
984
  `)}function getGitDiff(){try{let diff=execSync6("git diff HEAD",{encoding:"utf-8",maxBuffer:1048576}),staged=execSync6("git diff --cached",{encoding:"utf-8",maxBuffer:1048576}),combined=[diff,staged].filter(Boolean).join(`
985
985
  `);if(combined.length>50000)return`${combined.slice(0,50000)}
986
986
 
987
- ... (diff truncated at 50KB)`;return combined}catch{return""}}function parseWishGroups(content){let groups=[],groupPattern=/^### Group ([A-Za-z0-9]+):/gim,match=groupPattern.exec(content);while(match!==null){let name=match[1],start=match.index,rest=content.slice(start+match[0].length),nextGroupIdx=rest.search(/^### Group [A-Za-z0-9]+:/m),depsMatch=(nextGroupIdx!==-1?rest.slice(0,nextGroupIdx):rest).match(/\*\*depends-on:\*\*\s*(.+)/i),dependsOn=[];if(depsMatch){let depsStr=depsMatch[1].trim();if(depsStr.replace(/\s*\([^)]*\)/g,"").trim().toLowerCase()!=="none")dependsOn=depsStr.split(",").map((d)=>d.trim().replace(/^group\s*/i,"").replace(/\s*\(.*\)\s*$/,"").trim()).filter(Boolean)}groups.push({name,dependsOn}),match=groupPattern.exec(content)}return groups}function parseExecutionStrategy(content){let strategyMatch=content.match(/^## Execution Strategy\s*$/m);if(!strategyMatch||strategyMatch.index===void 0)return buildFallbackWaves(content);let strategyStart=strategyMatch.index+strategyMatch[0].length,nextSectionMatch=content.slice(strategyStart).match(/^## /m),strategyEnd=nextSectionMatch?.index!==void 0?strategyStart+nextSectionMatch.index:content.length,strategyContent=content.slice(strategyStart,strategyEnd),waves=[],wavePattern=/^### (Wave \d+[^\n]*)/gm,waveMatch=wavePattern.exec(strategyContent);while(waveMatch!==null){let waveName=waveMatch[1].trim(),waveStart=waveMatch.index+waveMatch[0].length,nextWaveIdx=strategyContent.slice(waveStart).search(/^### /m),waveEnd=nextWaveIdx!==-1?waveStart+nextWaveIdx:strategyContent.length,waveContent=strategyContent.slice(waveStart,waveEnd),waveGroups=[],tableRowPattern=/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*[^|]*\s*\|$/gm,rowMatch=tableRowPattern.exec(waveContent);while(rowMatch!==null){let groupVal=rowMatch[1].trim(),agentVal=rowMatch[2].trim();if(groupVal!=="Group"&&!groupVal.startsWith("-"))waveGroups.push({group:groupVal,agent:agentVal});rowMatch=tableRowPattern.exec(waveContent)}if(waveGroups.length>0)waves.push({name:waveName,groups:waveGroups});waveMatch=wavePattern.exec(strategyContent)}if(waves.length===0)return buildFallbackWaves(content);return waves}function buildFallbackWaves(content){let groups=parseWishGroups(content);if(groups.length===0)return[];return[{name:"Wave 1 (sequential fallback)",groups:groups.map((g)=>({group:g.name,agent:"engineer"}))}]}function detectWorkMode(ref,agent){if(!agent){if(ref.includes("#"))throw Error("Manual dispatch requires an agent: genie work <slug>#<group> <agent>");return{mode:"auto",slug:ref}}if(ref.includes("#"))return{mode:"manual",ref,agent};if(agent.includes("#"))return{mode:"manual",ref:agent,agent:ref};throw Error('Invalid: ref must contain "#" \u2014 use "genie work <slug>" or "genie work <agent> <slug>#<group>"')}async function autoOrchestrateCommand(slug){let wishPath=join24(process.cwd(),".genie","wishes",slug,"WISH.md");if(!existsSync19(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile9(wishPath,"utf-8"),groups=parseWishGroups(content),waves=parseExecutionStrategy(content);if(waves.length===0)console.error("\u274C No execution groups found in wish"),process.exit(1);let state2=await getState(slug);if(!state2)state2=await createState(slug,groups),console.log(`\uD83D\uDCDD Initialized state for wish "${slug}" (${groups.length} groups)`);let nextWave=waves.find((wave)=>wave.groups.some((g)=>{let gs=state2?.groups[g.group];return!gs||gs.status==="ready"}));if(!nextWave){console.log(`\u2705 All waves already dispatched for wish "${slug}"`);return}console.log(`\uD83D\uDE80 Dispatching ${nextWave.name} for wish "${slug}" \u2014 ${nextWave.groups.length} group(s)`),await Promise.all(nextWave.groups.map(({group,agent})=>{let ref=`${slug}#${group}`;return workDispatchCommand(agent,ref)}));let groupList=nextWave.groups.map((g)=>g.group).join(", ");console.log(`
988
- \u2705 Agents dispatched for ${nextWave.name} (groups: ${groupList})`),console.log(` Monitor: genie status ${slug}`),console.log(" Logs: genie read <agent>")}async function brainstormCommand(agentName,slug){let draftPath=join24(process.cwd(),".genie","brainstorms",slug,"DRAFT.md");if(!existsSync19(draftPath))console.error(`\u274C Draft not found: ${draftPath}`),console.error(` Create it first: mkdir -p .genie/brainstorms/${slug} && touch .genie/brainstorms/${slug}/DRAFT.md`),process.exit(1);let content=await readFile9(draftPath,"utf-8"),context=buildContextPrompt({filePath:draftPath,sectionContent:content,command:"brainstorm",skill:"brainstorm"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching brainstorm to ${agentName} for "${slug}"`),console.log(` Draft: ${draftPath}`),await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",extraArgs:["--append-system-prompt-file",contextFile]});let repoPath=process.cwd();await sendMessage(repoPath,"cli",agentName,`Brainstorm "${slug}". Your context is in the system prompt. Explore the idea, ask clarifying questions, and build toward a design.`)}async function wishCommand(agentName,slug){let designPath=join24(process.cwd(),".genie","brainstorms",slug,"DESIGN.md");if(!existsSync19(designPath))console.error(`\u274C Design not found: ${designPath}`),console.error(` Run brainstorm first: genie brainstorm <agent> ${slug}`),process.exit(1);let content=await readFile9(designPath,"utf-8"),context=buildContextPrompt({filePath:designPath,sectionContent:content,command:"wish",skill:"wish"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching wish to ${agentName} for "${slug}"`),console.log(` Design: ${designPath}`),await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",extraArgs:["--append-system-prompt-file",contextFile]});let repoPath=process.cwd();await sendMessage(repoPath,"cli",agentName,`Create a wish from the design for "${slug}". Your context is in the system prompt. Write the WISH.md with execution groups, acceptance criteria, and validation commands.`)}async function workDispatchCommand(agentName,ref){let{slug,group}=parseRef(ref),wishPath=join24(process.cwd(),".genie","wishes",slug,"WISH.md");if(!existsSync19(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile9(wishPath,"utf-8"),groupSection=extractGroup(content,group);if(!groupSection){console.error(`\u274C Group "${group}" not found in ${wishPath}`),console.error(" Available groups:");let groups=content.match(/^### Group [A-Za-z0-9]+:.*$/gm);if(groups)for(let g of groups)console.error(` ${g}`);process.exit(1)}let state2=await getState(slug);if(!state2){let groups=parseWishGroups(content);state2=await createState(slug,groups),console.log(`\uD83D\uDCDD Initialized state for wish "${slug}" (${groups.length} groups)`)}try{await startGroup(slug,group,agentName),console.log(`\u2705 Group "${group}" set to in_progress (assigned to ${agentName})`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}let wishContext=extractWishContext(content),context=buildContextPrompt({filePath:wishPath,sectionContent:groupSection,wishContext,command:`work ${ref}`,skill:"work"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDD27 Dispatching work to ${agentName} for "${ref}"`),console.log(` Wish: ${wishPath}`),console.log(` Group: ${group}`);let effectiveRole=`${agentName}-${group}`;await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",role:effectiveRole,extraArgs:["--append-system-prompt-file",contextFile]});let repoPath=process.cwd();await sendMessage(repoPath,"cli",effectiveRole,`Execute Group ${group} of wish "${slug}". Your full context is in the system prompt. Read the wish at ${wishPath} if needed. Implement all deliverables, run validation, and report completion.
987
+ ... (diff truncated at 50KB)`;return combined}catch{return""}}function parseWishGroups(content){let groups=[],groupPattern=/^### Group ([A-Za-z0-9]+):/gim,match=groupPattern.exec(content);while(match!==null){let name=match[1],start=match.index,rest=content.slice(start+match[0].length),nextGroupIdx=rest.search(/^### Group [A-Za-z0-9]+:/m),depsMatch=(nextGroupIdx!==-1?rest.slice(0,nextGroupIdx):rest).match(/\*\*depends-on:\*\*\s*(.+)/i),dependsOn=[];if(depsMatch){let depsStr=depsMatch[1].trim();if(depsStr.replace(/\s*\([^)]*\)/g,"").trim().toLowerCase()!=="none")dependsOn=depsStr.split(",").map((d)=>d.trim().replace(/^group\s*/i,"").replace(/\s*\(.*\)\s*$/,"").trim()).filter(Boolean)}groups.push({name,dependsOn}),match=groupPattern.exec(content)}return groups}function parseExecutionStrategy(content){let strategyMatch=content.match(/^## Execution Strategy\s*$/m);if(!strategyMatch||strategyMatch.index===void 0)return buildFallbackWaves(content);let strategyStart=strategyMatch.index+strategyMatch[0].length,nextSectionMatch=content.slice(strategyStart).match(/^## /m),strategyEnd=nextSectionMatch?.index!==void 0?strategyStart+nextSectionMatch.index:content.length,strategyContent=content.slice(strategyStart,strategyEnd),waves=[],wavePattern=/^### (Wave \d+[^\n]*)/gm,waveMatch=wavePattern.exec(strategyContent);while(waveMatch!==null){let waveName=waveMatch[1].trim(),waveStart=waveMatch.index+waveMatch[0].length,nextWaveIdx=strategyContent.slice(waveStart).search(/^### /m),waveEnd=nextWaveIdx!==-1?waveStart+nextWaveIdx:strategyContent.length,waveContent=strategyContent.slice(waveStart,waveEnd),waveGroups=[],tableRowPattern=/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*[^|]*\s*\|$/gm,rowMatch=tableRowPattern.exec(waveContent);while(rowMatch!==null){let groupVal=rowMatch[1].trim(),agentVal=rowMatch[2].trim();if(groupVal!=="Group"&&!groupVal.startsWith("-"))waveGroups.push({group:groupVal,agent:agentVal});rowMatch=tableRowPattern.exec(waveContent)}if(waveGroups.length>0)waves.push({name:waveName,groups:waveGroups});waveMatch=wavePattern.exec(strategyContent)}if(waves.length===0)return buildFallbackWaves(content);return waves}function buildFallbackWaves(content){let groups=parseWishGroups(content);if(groups.length===0)return[];return[{name:"Wave 1 (sequential fallback)",groups:groups.map((g)=>({group:g.name,agent:"engineer"}))}]}function detectWorkMode(ref,agent){if(!agent){if(ref.includes("#"))throw Error("Manual dispatch requires an agent: genie work <slug>#<group> <agent>");return{mode:"auto",slug:ref}}if(ref.includes("#"))return{mode:"manual",ref,agent};if(agent.includes("#"))return{mode:"manual",ref:agent,agent:ref};throw Error('Invalid: ref must contain "#" \u2014 use "genie work <slug>" or "genie work <agent> <slug>#<group>"')}async function autoOrchestrateCommand(slug){let wishPath=join24(process.cwd(),".genie","wishes",slug,"WISH.md");if(!existsSync19(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile9(wishPath,"utf-8"),groups=parseWishGroups(content),waves=parseExecutionStrategy(content);if(waves.length===0)console.error("\u274C No execution groups found in wish"),process.exit(1);let state2=await getOrCreateState(slug,groups),nextWave=waves.find((wave)=>wave.groups.some((g)=>{let gs=state2?.groups[g.group];return!gs||gs.status==="ready"}));if(!nextWave){console.log(`\u2705 All waves already dispatched for wish "${slug}"`);return}console.log(`\uD83D\uDE80 Dispatching ${nextWave.name} for wish "${slug}" \u2014 ${nextWave.groups.length} group(s)`),await Promise.all(nextWave.groups.map(({group,agent})=>{let ref=`${slug}#${group}`;return workDispatchCommand(agent,ref)}));let groupList=nextWave.groups.map((g)=>g.group).join(", ");console.log(`
988
+ \u2705 Agents dispatched for ${nextWave.name} (groups: ${groupList})`),console.log(` Monitor: genie status ${slug}`),console.log(" Logs: genie read <agent>")}async function brainstormCommand(agentName,slug){let draftPath=join24(process.cwd(),".genie","brainstorms",slug,"DRAFT.md");if(!existsSync19(draftPath))console.error(`\u274C Draft not found: ${draftPath}`),console.error(` Create it first: mkdir -p .genie/brainstorms/${slug} && touch .genie/brainstorms/${slug}/DRAFT.md`),process.exit(1);let content=await readFile9(draftPath,"utf-8"),context=buildContextPrompt({filePath:draftPath,sectionContent:content,command:"brainstorm",skill:"brainstorm"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching brainstorm to ${agentName} for "${slug}"`),console.log(` Draft: ${draftPath}`),await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",extraArgs:["--append-system-prompt-file",contextFile]});let repoPath=process.cwd();await sendMessage(repoPath,"cli",agentName,`Brainstorm "${slug}". Your context is in the system prompt. Explore the idea, ask clarifying questions, and build toward a design.`)}async function wishCommand(agentName,slug){let designPath=join24(process.cwd(),".genie","brainstorms",slug,"DESIGN.md");if(!existsSync19(designPath))console.error(`\u274C Design not found: ${designPath}`),console.error(` Run brainstorm first: genie brainstorm <agent> ${slug}`),process.exit(1);let content=await readFile9(designPath,"utf-8"),context=buildContextPrompt({filePath:designPath,sectionContent:content,command:"wish",skill:"wish"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching wish to ${agentName} for "${slug}"`),console.log(` Design: ${designPath}`),await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",extraArgs:["--append-system-prompt-file",contextFile]});let repoPath=process.cwd();await sendMessage(repoPath,"cli",agentName,`Create a wish from the design for "${slug}". Your context is in the system prompt. Write the WISH.md with execution groups, acceptance criteria, and validation commands.`)}async function workDispatchCommand(agentName,ref){let{slug,group}=parseRef(ref),wishPath=join24(process.cwd(),".genie","wishes",slug,"WISH.md");if(!existsSync19(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile9(wishPath,"utf-8"),groupSection=extractGroup(content,group);if(!groupSection){console.error(`\u274C Group "${group}" not found in ${wishPath}`),console.error(" Available groups:");let groups2=content.match(/^### Group [A-Za-z0-9]+:.*$/gm);if(groups2)for(let g of groups2)console.error(` ${g}`);process.exit(1)}let groups=parseWishGroups(content);await getOrCreateState(slug,groups);try{await startGroup(slug,group,agentName),console.log(`\u2705 Group "${group}" set to in_progress (assigned to ${agentName})`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}let wishContext=extractWishContext(content),context=buildContextPrompt({filePath:wishPath,sectionContent:groupSection,wishContext,command:`work ${ref}`,skill:"work"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDD27 Dispatching work to ${agentName} for "${ref}"`),console.log(` Wish: ${wishPath}`),console.log(` Group: ${group}`);let effectiveRole=`${agentName}-${group}`;await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",role:effectiveRole,extraArgs:["--append-system-prompt-file",contextFile]});let repoPath=process.cwd();await sendMessage(repoPath,"cli",effectiveRole,`Execute Group ${group} of wish "${slug}". Your full context is in the system prompt. Read the wish at ${wishPath} if needed. Implement all deliverables, run validation, and report completion.
989
989
 
990
990
  When done:
991
991
  1. Run: genie done ${slug}#${group}
@@ -1024,8 +1024,9 @@ ${indented}`}function formatHumanOutput(events,label){let lines=[];if(lines.push
1024
1024
  ${prefs.length} preference${prefs.length===1?"":"s"}`)}async function handleNotifyList(options){let ts=await getTaskService2(),actor=currentActor(),prefs=await ts.getPreferences(actor);if(options.json){console.log(JSON.stringify(prefs,null,2));return}if(prefs.length===0){console.log("No notification preferences configured.");return}printPrefsTable(prefs)}async function handleNotifyRemove(options){let ts=await getTaskService2(),actor=currentActor();if(await ts.deletePreference(actor,options.channel))console.log(`Removed notification preference for channel: ${options.channel}`);else console.log(`No preference found for channel: ${options.channel}`)}function registerNotifyCommands(program2){let notify=program2.command("notify").description("Notification preference management");notify.command("set").description("Set notification preference for a channel").requiredOption("--channel <channel>","Channel: whatsapp, telegram, email, slack, discord, tmux").option("--priority <priority>","Minimum priority threshold","normal").option("--default","Set as default channel").action(async(options)=>{try{await handleNotifySet(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),notify.command("list").description("List notification preferences").option("--json","Output as JSON").action(async(options)=>{try{await handleNotifyList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),notify.command("remove").description("Remove a notification preference").requiredOption("--channel <channel>","Channel to remove").action(async(options)=>{try{await handleNotifyRemove(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}init_tmux();function debug(msg){if(process.env.DEBUG)console.error(`[target-resolver] ${msg}`)}async function defaultTmuxLookup(sessionName,windowName){try{let tmux=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),session=await tmux.findSessionByName(sessionName);if(!session)return null;let windows=await tmux.listWindows(session.id);if(!windows||windows.length===0)return null;let targetWindow;if(windowName){if(targetWindow=windows.find((w)=>w.name===windowName),!targetWindow)return null}else targetWindow=windows.find((w)=>w.active)||windows[0];let panes=await tmux.listPanes(targetWindow.id);if(!panes||panes.length===0)return null;return{paneId:(panes.find((p)=>p.active)||panes[0]).id,session:sessionName}}catch{return null}}async function defaultIsPaneLive(paneId){try{return(await(await Promise.resolve().then(() => (init_tmux(),exports_tmux))).executeTmux(`display-message -p -t '${paneId}' '#{pane_id}'`)).trim()===paneId}catch{return!1}}async function defaultCleanupDeadPane(workerId,paneId){try{await(await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry))).removeSubPane(workerId,paneId)}catch{}}async function defaultDeriveSession(paneId){try{return(await(await Promise.resolve().then(() => (init_tmux(),exports_tmux))).executeTmux(`display-message -p -t '${paneId}' '#{session_name}'`)).trim()||null}catch{return null}}async function assertLive(paneId,isPaneLive,errorMsg,cleanup){if(!await isPaneLive(paneId)){if(cleanup)await cleanup();throw Error(errorMsg)}}async function resolveRawPane(target,opts){if(opts.checkLiveness)await assertLive(target,opts.isPaneLive,`Pane ${target} is dead or does not exist. Check with: tmux list-panes -a`);let session=await opts.deriveSession(target);return{paneId:target,session:session??void 0,resolvedVia:"raw"}}async function resolveWindowId(target,workers,opts){let matchingWorker=Object.values(workers).find((w)=>w.windowId===target);if(!matchingWorker)throw Error(`Window "${target}" not found in worker registry.
1025
1025
  Run 'genie ls' to list agents.`);if(opts.checkLiveness)await assertLive(matchingWorker.paneId,opts.isPaneLive,`Window ${target}: worker ${matchingWorker.id} pane ${matchingWorker.paneId} is dead. Run 'genie kill ${matchingWorker.id}' to clean up.`);return{paneId:matchingWorker.paneId,session:matchingWorker.session,workerId:matchingWorker.id,resolvedVia:"worker"}}function resolveWorkerSubPane(worker,leftSide,rightSide){let index=Number.parseInt(rightSide,10);if(Number.isNaN(index)||index<0)throw Error(`Invalid sub-pane index "${rightSide}" for worker "${leftSide}". Use a non-negative integer (0 = primary, 1+ = sub-panes).`);let paneId=getPaneByIndex(worker,index);if(!paneId){let maxIndex=worker.subPanes?worker.subPanes.length:0;throw Error(`Worker "${leftSide}" has no sub-pane index ${index}. Available: 0 (primary)${maxIndex>0?`, 1-${maxIndex} (sub-panes)`:""}. Sub-pane index ${index} does not exist.`)}return paneId}function pickUnique(target,candidates,label){if(candidates.length===0)return null;if(candidates.length===1){let[id,w]=candidates[0];return{paneId:w.paneId,session:w.session,workerId:id,resolvedVia:"worker"}}let ids=candidates.map(([id])=>id).join(", ");throw Error(`Ambiguous target "${target}" \u2014 ${label}: ${ids}
1026
1026
  Use the full ID instead.`)}function resolveByRole(target,workers,currentTeam){if(!currentTeam)return null;let candidates=Object.entries(workers).filter(([,w])=>w.role===target&&w.team===currentTeam);return pickUnique(target,candidates,`${candidates.length} workers with role "${target}" in team "${currentTeam}"`)}function resolveByCustomName(target,workers,currentTeam){if(currentTeam){let teamCandidates=Object.entries(workers).filter(([,w])=>w.customName===target&&w.team===currentTeam),teamHit=pickUnique(target,teamCandidates,`${teamCandidates.length} workers with customName "${target}" in team "${currentTeam}"`);if(teamHit)return teamHit}let allCandidates=Object.entries(workers).filter(([,w])=>w.customName===target);return pickUnique(target,allCandidates,`${allCandidates.length} workers with customName "${target}"`)}function resolveByPartialId(target,workers,currentTeam){let candidates=Object.entries(workers).filter(([id])=>id!==target&&id.endsWith(target));if(candidates.length===0)return null;if(candidates.length===1){let[id,w]=candidates[0];return{paneId:w.paneId,session:w.session,workerId:id,resolvedVia:"worker"}}if(currentTeam){let teamCandidates=candidates.filter(([,w])=>w.team===currentTeam);if(teamCandidates.length===1){let[id,w]=teamCandidates[0];return{paneId:w.paneId,session:w.session,workerId:id,resolvedVia:"worker"}}}let ids=candidates.map(([id])=>id).join(", ");throw Error(`Ambiguous target "${target}" \u2014 matches ${candidates.length} workers: ${ids}
1027
+ Use the full ID instead.`)}function resolveBySubstring(target,workers,currentTeam){let candidates=Object.entries(workers).filter(([id])=>id!==target&&!id.endsWith(target)&&id.includes(target));if(candidates.length===0)return null;if(candidates.length===1){let[id,w]=candidates[0];return{paneId:w.paneId,session:w.session,workerId:id,resolvedVia:"worker"}}if(currentTeam){let teamCandidates=candidates.filter(([,w])=>w.team===currentTeam);if(teamCandidates.length===1){let[id,w]=teamCandidates[0];return{paneId:w.paneId,session:w.session,workerId:id,resolvedVia:"worker"}}}let ids=candidates.map(([id])=>id).join(", ");throw Error(`Ambiguous target "${target}" \u2014 matches ${candidates.length} workers: ${ids}
1027
1028
  Use the full ID instead.`)}function resolveByRoleGlobal(target,workers){let candidates=Object.entries(workers).filter(([,w])=>w.role===target);return pickUnique(target,candidates,`${candidates.length} workers with role "${target}"`)}async function resolveColonTarget(target,workers,opts){let colonIndex=target.indexOf(":"),leftSide=target.substring(0,colonIndex),rightSide=target.substring(colonIndex+1),worker=workers[leftSide];if(worker){let paneId=resolveWorkerSubPane(worker,leftSide,rightSide),index=Number.parseInt(rightSide,10);if(opts.checkLiveness)await assertLive(paneId,opts.isPaneLive,`Worker ${leftSide}: pane ${paneId} is dead. Run 'genie kill ${leftSide}' to clean up.`,()=>opts.cleanupDeadPane(leftSide,paneId));return{paneId,session:worker.session,workerId:leftSide,paneIndex:index,resolvedVia:"worker"}}let sessionWindowResult=await opts.tmuxLookup(leftSide,rightSide);if(!sessionWindowResult)throw Error(`Target "${target}" not found. No worker "${leftSide}" in registry and no tmux session:window "${leftSide}:${rightSide}" found.
1028
- Run 'genie ls' to list agents.`);if(opts.checkLiveness)await assertLive(sessionWindowResult.paneId,opts.isPaneLive,`Session "${leftSide}" window "${rightSide}": pane ${sessionWindowResult.paneId} is dead.`);return{paneId:sessionWindowResult.paneId,session:sessionWindowResult.session,resolvedVia:"session:window"}}async function resolveBareName(target,workers,opts){let worker=workers[target];if(worker){if(opts.checkLiveness)await assertLive(worker.paneId,opts.isPaneLive,`Worker ${target}: pane ${worker.paneId} is dead. Run 'genie kill ${target}' to clean up.`,()=>opts.cleanupDeadPane(target,worker.paneId));return{paneId:worker.paneId,session:worker.session,workerId:target,resolvedVia:"worker"}}let currentTeam=opts.getCurrentTeam?await opts.getCurrentTeam():await getCurrentSessionName()??process.env.GENIE_TEAM??null,fuzzyMatch=resolveByRole(target,workers,currentTeam)??resolveByCustomName(target,workers,currentTeam)??resolveByPartialId(target,workers,currentTeam)??resolveByRoleGlobal(target,workers);if(fuzzyMatch){let rid=fuzzyMatch.workerId??target;if(opts.checkLiveness)await assertLive(fuzzyMatch.paneId,opts.isPaneLive,`Worker ${rid}: pane ${fuzzyMatch.paneId} is dead.`,()=>opts.cleanupDeadPane(rid,fuzzyMatch.paneId));return fuzzyMatch}throw Error(`Target "${target}" not found. Not a worker or pane ID.
1029
+ Run 'genie ls' to list agents.`);if(opts.checkLiveness)await assertLive(sessionWindowResult.paneId,opts.isPaneLive,`Session "${leftSide}" window "${rightSide}": pane ${sessionWindowResult.paneId} is dead.`);return{paneId:sessionWindowResult.paneId,session:sessionWindowResult.session,resolvedVia:"session:window"}}async function resolveBareName(target,workers,opts){let worker=workers[target];if(worker){if(opts.checkLiveness)await assertLive(worker.paneId,opts.isPaneLive,`Worker ${target}: pane ${worker.paneId} is dead. Run 'genie kill ${target}' to clean up.`,()=>opts.cleanupDeadPane(target,worker.paneId));return{paneId:worker.paneId,session:worker.session,workerId:target,resolvedVia:"worker"}}let currentTeam=opts.getCurrentTeam?await opts.getCurrentTeam():await getCurrentSessionName()??process.env.GENIE_TEAM??null,fuzzyMatch=resolveByRole(target,workers,currentTeam)??resolveByCustomName(target,workers,currentTeam)??resolveByPartialId(target,workers,currentTeam)??resolveBySubstring(target,workers,currentTeam)??resolveByRoleGlobal(target,workers);if(fuzzyMatch){let rid=fuzzyMatch.workerId??target;if(opts.checkLiveness)await assertLive(fuzzyMatch.paneId,opts.isPaneLive,`Worker ${rid}: pane ${fuzzyMatch.paneId} is dead.`,()=>opts.cleanupDeadPane(rid,fuzzyMatch.paneId));return fuzzyMatch}throw Error(`Target "${target}" not found. Not a worker or pane ID.
1029
1030
  Run 'genie ls' to list agents.`)}async function resolveTarget(target,options={}){let{checkLiveness=!1,workers:injectedWorkers,tmuxLookup=defaultTmuxLookup,isPaneLive=defaultIsPaneLive,cleanupDeadPane=defaultCleanupDeadPane,deriveSession=defaultDeriveSession}=options;if(debug(`resolving "${target}"`),target.startsWith("%"))return resolveRawPane(target,{checkLiveness,isPaneLive,deriveSession});if(target.startsWith("@")){let workers2=await getWorkers(injectedWorkers,options.registryPath);return resolveWindowId(target,workers2,{checkLiveness,isPaneLive})}let workers=await getWorkers(injectedWorkers,options.registryPath);if(target.indexOf(":")!==-1)return resolveColonTarget(target,workers,{checkLiveness,isPaneLive,cleanupDeadPane,tmuxLookup});return resolveBareName(target,workers,{checkLiveness,isPaneLive,cleanupDeadPane,getCurrentTeam:options.getCurrentTeam})}function formatResolvedLabel(resolved,originalTarget){let parts=[];if(resolved.workerId){if(parts.push(resolved.workerId),resolved.paneIndex!==void 0&&resolved.paneIndex>0)parts[parts.length-1]+=`:${resolved.paneIndex}`}else parts.push(originalTarget);let details=[`pane ${resolved.paneId}`];if(resolved.session)details.push(`session ${resolved.session}`);return`${parts[0]} (${details.join(", ")})`}async function getWorkers(injected,_registryPath){if(injected!==void 0)return injected;try{let workersList=await(await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry))).list(),map2={};for(let w of workersList)map2[w.id]=w;return map2}catch{return{}}}function getPaneByIndex(worker,index){if(index===0)return worker.paneId;let subIndex=index-1;if(!worker.subPanes||subIndex>=worker.subPanes.length||subIndex<0)return null;return worker.subPanes[subIndex]}init_tmux();init_orchestrator();async function resolveOrcTarget(target){let resolved=await resolveTarget(target);return{paneId:resolved.paneId,session:resolved.session||target,label:formatResolvedLabel(resolved,target)}}async function sendTextChoice(paneId,text){await executeTmux2(`send-keys -t '${paneId}' End`),await sleep(100),await executeTmux2(`send-keys -t '${paneId}' Enter`),await sleep(100),await executeTmux2(`send-keys -t '${paneId}' ${shellEscape(text)}`),await sleep(100),await executeTmux2(`send-keys -t '${paneId}' Enter`)}function findCurrentOption(output){let lines=stripAnsi(output).split(`
1030
1031
  `);for(let line of lines){let match=line.match(/^\s*\u276F\s*(\d+)\./);if(match)return Number.parseInt(match[1],10)}return 1}async function navigateToOption(paneId,targetOption,currentOption){let diff=targetOption-currentOption,key=diff>0?"Down":"Up";for(let i2=0;i2<Math.abs(diff);i2++)await executeTmux2(`send-keys -t '${paneId}' ${key}`),await sleep(50);await sleep(100),await executeTmux2(`send-keys -t '${paneId}' Enter`)}async function answerQuestion(target,choice){try{let{paneId,label}=await resolveOrcTarget(target),output=await capturePaneContent(paneId,50),state2=detectState(output);if(state2.type!=="question"){console.log(`No question pending (state: ${state2.type})`);return}if(choice.startsWith("text:")){let text=choice.slice(5);await sendTextChoice(paneId,text),console.log(`Sent feedback: "${text.substring(0,50)}${text.length>50?"...":""}"`)}else if(/^\d+$/.test(choice)){let targetOption=Number.parseInt(choice,10);await navigateToOption(paneId,targetOption,findCurrentOption(output)),console.log(`Selected option ${targetOption} for ${label}`)}else await executeTmux2(`send-keys -t '${paneId}' '${choice}'`),console.log(`Sent '${choice}' to ${label}`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}function shellEscape(str3){return`"${str3.replace(/"/g,"\\\"").replace(/\$/g,"\\$")}"`}function sleep(ms){return new Promise((resolve4)=>setTimeout(resolve4,ms))}var _taskService3;async function getTaskService3(){if(!_taskService3)_taskService3=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService3}function padRight5(str3,len){return str3.length>=len?str3:str3+" ".repeat(len-str3.length)}function truncate3(str3,len){return str3.length<=len?str3:`${str3.slice(0,len-1)}\u2026`}function formatDate(iso){if(!iso)return"-";return new Date(iso).toLocaleDateString("en-US",{month:"short",day:"numeric"})}async function printProjectList(ts,projects){let counts={};for(let p of projects){let tasks=await ts.listTasks({projectName:p.name,allProjects:!0});counts[p.id]=tasks.length}console.log(` ${padRight5("NAME",20)} ${padRight5("TYPE",10)} ${padRight5("TASKS",8)} ${padRight5("CREATED",12)} PATH`),console.log(` ${"\u2500".repeat(80)}`);for(let p of projects){let type2=p.repoPath?"repo":"virtual",path3=p.repoPath?truncate3(p.repoPath,40):"-";console.log(` ${padRight5(p.name,20)} ${padRight5(type2,10)} ${padRight5(String(counts[p.id]??0),8)} ${padRight5(formatDate(p.createdAt),12)} ${path3}`)}console.log(`
1031
1032
  ${projects.length} project${projects.length===1?"":"s"}`)}function printProjectDetail(p,tasks){let byStatus={},byStage={};for(let t of tasks)byStatus[t.status]=(byStatus[t.status]??0)+1,byStage[t.stage]=(byStage[t.stage]??0)+1;if(console.log(`
package/knip.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/knip@5.85.0/schema.json",
3
+ "entry": ["src/genie.ts", "src/hooks/dispatch-command.ts"],
3
4
  "project": ["src/**/*.ts"],
4
- "ignoreBinaries": ["tmux", "which"],
5
- "ignoreDependencies": ["nats"],
5
+ "ignoreBinaries": ["tmux", "which", "husky", "biome", "tsc"],
6
+ "ignoreDependencies": ["nats", "@biomejs/biome", "@commitlint/cli", "esbuild", "husky"],
6
7
  "ignore": ["src/lib/team-auto-spawn.ts"],
7
8
  "ignoreExportsUsedInFile": true
8
9
  }
@@ -2,7 +2,7 @@
2
2
  "id": "genie",
3
3
  "name": "Genie",
4
4
  "description": "Skills, agents, and hooks for the Genie CLI terminal orchestration toolkit",
5
- "version": "4.260324.3",
5
+ "version": "4.260324.4",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260324.3",
3
+ "version": "4.260324.4",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260324.3",
3
+ "version": "4.260324.4",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260324.3",
3
+ "version": "4.260324.4",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -5,9 +5,9 @@
5
5
  * First-run detection for genie plugin.
6
6
  *
7
7
  * Runs on SessionStart. If no AGENTS.md exists in the working directory,
8
- * outputs a message to stderr prompting /onboarding.
8
+ * auto-scaffolds a minimal one so the workspace is immediately usable.
9
9
  *
10
- * This is non-blocking (always exits 0) — it only suggests, never forces.
10
+ * This is non-blocking (always exits 0) and deterministic same result every run.
11
11
  */
12
12
 
13
13
  const fs = require("node:fs");
@@ -23,17 +23,25 @@ const cwd = process.env.CLAUDE_CWD || process.cwd();
23
23
  const agentsMd = path.join(cwd, "AGENTS.md");
24
24
  const claudeMd = path.join(cwd, "CLAUDE.md");
25
25
 
26
- // Only suggest onboarding if neither AGENTS.md nor CLAUDE.md exists.
26
+ // Only scaffold if neither AGENTS.md nor CLAUDE.md exists.
27
27
  // Having either means the workspace is already configured.
28
28
  if (!fs.existsSync(agentsMd) && !fs.existsSync(claudeMd)) {
29
- console.error("");
30
- console.error("=".repeat(50));
31
- console.error(" \u{1F9DE} Genie — First Run Detected");
32
- console.error(" No AGENTS.md found in this workspace.");
33
- console.error("");
34
- console.error(" Run /onboarding to set up your environment.");
35
- console.error("=".repeat(50));
36
- console.error("");
29
+ const projectName = path.basename(cwd);
30
+ const content = `# ${projectName}\n\n## Agents\n\nThis project is managed by Genie CLI.\n\n## Conventions\n\n- Follow existing code style and patterns\n- Write tests for new functionality\n- Use conventional commits\n`;
31
+
32
+ try {
33
+ fs.writeFileSync(agentsMd, content, "utf-8");
34
+ console.error("");
35
+ console.error("\u{1F9DE} Created AGENTS.md \u2014 you're ready to go!");
36
+ console.error("");
37
+ } catch (err) {
38
+ // Non-fatal: if we can't write (read-only fs, permissions), just warn
39
+ console.error("");
40
+ console.error("\u{1F9DE} Genie \u2014 First Run Detected");
41
+ console.error(` Could not create AGENTS.md: ${err.message}`);
42
+ console.error(" Create one manually to configure your workspace.");
43
+ console.error("");
44
+ }
37
45
  }
38
46
 
39
47
  process.exit(0);
@@ -513,9 +513,19 @@ try {
513
513
  }
514
514
  }
515
515
  } catch (e) {
516
- // Only Bun install failure reaches here — everything else is graceful
517
- console.error('Critical installation failed:', e.message);
518
- console.error('Continuing anyway to let remaining hooks run...');
516
+ // Only Bun install failure reaches here — everything else is graceful.
517
+ // Don't say "continuing anyway" — be specific about what failed and what to do.
518
+ console.error('');
519
+ console.error('Genie setup failed: Bun runtime could not be installed.');
520
+ console.error(` Error: ${e.message}`);
521
+ console.error('');
522
+ console.error('What to do:');
523
+ console.error(' 1. Install Bun manually: curl -fsSL https://bun.com/install | bash');
524
+ console.error(' 2. Restart your terminal to update PATH');
525
+ console.error(' 3. Start a new Claude Code session');
526
+ console.error('');
527
+ console.error('Genie features will be unavailable until Bun is installed.');
519
528
  // Exit 0 so the hook chain continues (first-run-check, session-context)
529
+ // but session state is not corrupted — no partial markers were written
520
530
  process.exit(0);
521
531
  }
@@ -1024,6 +1024,162 @@ describe('Partial ID suffix resolution', () => {
1024
1024
  });
1025
1025
  });
1026
1026
 
1027
+ // ============================================================================
1028
+ // Substring resolution (fixes #700: short display names from genie ls)
1029
+ // ============================================================================
1030
+
1031
+ describe('Substring resolution', () => {
1032
+ test('resolves "engineer-4" when ID is "sofia-t1re-engineer-4-ec331228"', async () => {
1033
+ const result = await resolveTarget('engineer-4', {
1034
+ checkLiveness: false,
1035
+ getCurrentTeam: async () => null,
1036
+ workers: {
1037
+ 'sofia-t1re-engineer-4-ec331228': {
1038
+ id: 'sofia-t1re-engineer-4-ec331228',
1039
+ paneId: '%40',
1040
+ session: 'my-team',
1041
+ worktree: null,
1042
+ startedAt: new Date().toISOString(),
1043
+ state: 'working',
1044
+ lastStateChange: new Date().toISOString(),
1045
+ repoPath: '/tmp/test',
1046
+ role: 'engineer',
1047
+ team: 'my-team',
1048
+ },
1049
+ },
1050
+ });
1051
+
1052
+ expect(result.paneId).toBe('%40');
1053
+ expect(result.workerId).toBe('sofia-t1re-engineer-4-ec331228');
1054
+ expect(result.resolvedVia).toBe('worker');
1055
+ });
1056
+
1057
+ test('prefers same-team match on ambiguous substring', async () => {
1058
+ const result = await resolveTarget('engineer-4', {
1059
+ checkLiveness: false,
1060
+ getCurrentTeam: async () => 'my-team',
1061
+ workers: {
1062
+ 'teamA-engineer-4-abc123': {
1063
+ id: 'teamA-engineer-4-abc123',
1064
+ paneId: '%40',
1065
+ session: 'other-team',
1066
+ worktree: null,
1067
+ startedAt: new Date().toISOString(),
1068
+ state: 'working',
1069
+ lastStateChange: new Date().toISOString(),
1070
+ repoPath: '/tmp/test',
1071
+ team: 'other-team',
1072
+ },
1073
+ 'teamB-engineer-4-def456': {
1074
+ id: 'teamB-engineer-4-def456',
1075
+ paneId: '%41',
1076
+ session: 'my-team',
1077
+ worktree: null,
1078
+ startedAt: new Date().toISOString(),
1079
+ state: 'working',
1080
+ lastStateChange: new Date().toISOString(),
1081
+ repoPath: '/tmp/test',
1082
+ team: 'my-team',
1083
+ },
1084
+ },
1085
+ });
1086
+
1087
+ expect(result.paneId).toBe('%41');
1088
+ expect(result.workerId).toBe('teamB-engineer-4-def456');
1089
+ });
1090
+
1091
+ test('throws on ambiguous substring without team disambiguation', async () => {
1092
+ await expect(
1093
+ resolveTarget('engineer-4', {
1094
+ checkLiveness: false,
1095
+ getCurrentTeam: async () => null,
1096
+ workers: {
1097
+ 'teamA-engineer-4-abc123': {
1098
+ id: 'teamA-engineer-4-abc123',
1099
+ paneId: '%40',
1100
+ session: 's1',
1101
+ worktree: null,
1102
+ startedAt: new Date().toISOString(),
1103
+ state: 'working',
1104
+ lastStateChange: new Date().toISOString(),
1105
+ repoPath: '/tmp/test',
1106
+ team: 'team-a',
1107
+ },
1108
+ 'teamB-engineer-4-def456': {
1109
+ id: 'teamB-engineer-4-def456',
1110
+ paneId: '%41',
1111
+ session: 's2',
1112
+ worktree: null,
1113
+ startedAt: new Date().toISOString(),
1114
+ state: 'working',
1115
+ lastStateChange: new Date().toISOString(),
1116
+ repoPath: '/tmp/test',
1117
+ team: 'team-b',
1118
+ },
1119
+ },
1120
+ }),
1121
+ ).rejects.toThrow(/ambiguous/i);
1122
+ });
1123
+
1124
+ test('suffix match (endsWith) takes priority over substring match', async () => {
1125
+ const result = await resolveTarget('engineer-4', {
1126
+ checkLiveness: false,
1127
+ getCurrentTeam: async () => null,
1128
+ workers: {
1129
+ 'team-engineer-4': {
1130
+ id: 'team-engineer-4',
1131
+ paneId: '%10',
1132
+ session: 'genie',
1133
+ worktree: null,
1134
+ startedAt: new Date().toISOString(),
1135
+ state: 'working',
1136
+ lastStateChange: new Date().toISOString(),
1137
+ repoPath: '/tmp/test',
1138
+ team: 'my-team',
1139
+ },
1140
+ 'sofia-t1re-engineer-4-ec331228': {
1141
+ id: 'sofia-t1re-engineer-4-ec331228',
1142
+ paneId: '%20',
1143
+ session: 'genie',
1144
+ worktree: null,
1145
+ startedAt: new Date().toISOString(),
1146
+ state: 'working',
1147
+ lastStateChange: new Date().toISOString(),
1148
+ repoPath: '/tmp/test',
1149
+ team: 'my-team',
1150
+ },
1151
+ },
1152
+ });
1153
+
1154
+ // endsWith match wins (resolveByPartialId runs before resolveBySubstring)
1155
+ expect(result.paneId).toBe('%10');
1156
+ expect(result.workerId).toBe('team-engineer-4');
1157
+ });
1158
+
1159
+ test('does not match when target is the exact ID', async () => {
1160
+ // exact ID match should resolve first, substring should not interfere
1161
+ const result = await resolveTarget('worker-1', {
1162
+ checkLiveness: false,
1163
+ getCurrentTeam: async () => null,
1164
+ workers: {
1165
+ 'worker-1': {
1166
+ id: 'worker-1',
1167
+ paneId: '%50',
1168
+ session: 'genie',
1169
+ worktree: null,
1170
+ startedAt: new Date().toISOString(),
1171
+ state: 'working',
1172
+ lastStateChange: new Date().toISOString(),
1173
+ repoPath: '/tmp/test',
1174
+ },
1175
+ },
1176
+ });
1177
+
1178
+ expect(result.paneId).toBe('%50');
1179
+ expect(result.workerId).toBe('worker-1');
1180
+ });
1181
+ });
1182
+
1027
1183
  // ============================================================================
1028
1184
  // Global role resolution (fallback)
1029
1185
  // ============================================================================
@@ -1117,7 +1273,7 @@ describe('Global role resolution', () => {
1117
1273
  // Resolution priority with new steps
1118
1274
  // ============================================================================
1119
1275
 
1120
- describe('Resolution priority: exact ID > role (team) > customName > partial ID > role (global)', () => {
1276
+ describe('Resolution priority: exact ID > role (team) > customName > partial ID > substring > role (global)', () => {
1121
1277
  const baseWorker = {
1122
1278
  worktree: null as string | null,
1123
1279
  startedAt: new Date().toISOString(),
@@ -1202,6 +1358,58 @@ describe('Resolution priority: exact ID > role (team) > customName > partial ID
1202
1358
  expect(result.paneId).toBe('%10');
1203
1359
  expect(result.workerId).toBe('custom-name-worker');
1204
1360
  });
1361
+
1362
+ test('partial ID suffix wins over substring match', async () => {
1363
+ const result = await resolveTarget('engineer-4', {
1364
+ checkLiveness: false,
1365
+ getCurrentTeam: async () => 'my-team',
1366
+ workers: {
1367
+ 'team-engineer-4': {
1368
+ ...baseWorker,
1369
+ id: 'team-engineer-4',
1370
+ paneId: '%10',
1371
+ session: 'my-team',
1372
+ },
1373
+ 'sofia-t1re-engineer-4-ec331228': {
1374
+ ...baseWorker,
1375
+ id: 'sofia-t1re-engineer-4-ec331228',
1376
+ paneId: '%20',
1377
+ session: 'my-team',
1378
+ },
1379
+ },
1380
+ });
1381
+
1382
+ // endsWith match (resolveByPartialId) runs first
1383
+ expect(result.paneId).toBe('%10');
1384
+ expect(result.workerId).toBe('team-engineer-4');
1385
+ });
1386
+
1387
+ test('substring wins over global role', async () => {
1388
+ const result = await resolveTarget('engineer-4', {
1389
+ checkLiveness: false,
1390
+ getCurrentTeam: async () => null,
1391
+ workers: {
1392
+ 'sofia-t1re-engineer-4-ec331228': {
1393
+ ...baseWorker,
1394
+ id: 'sofia-t1re-engineer-4-ec331228',
1395
+ paneId: '%10',
1396
+ session: 'my-team',
1397
+ role: 'other-role',
1398
+ },
1399
+ 'global-role-worker': {
1400
+ ...baseWorker,
1401
+ id: 'global-role-worker',
1402
+ paneId: '%20',
1403
+ session: 'my-team',
1404
+ role: 'engineer-4',
1405
+ },
1406
+ },
1407
+ });
1408
+
1409
+ // substring match runs before global role
1410
+ expect(result.paneId).toBe('%10');
1411
+ expect(result.workerId).toBe('sofia-t1re-engineer-4-ec331228');
1412
+ });
1205
1413
  });
1206
1414
 
1207
1415
  // ============================================================================
@@ -5,7 +5,7 @@
5
5
  * 1. Raw pane ID (starts with %) -> passthrough
6
6
  * 2. Worker[:index] (left side is registered worker) -> registry lookup + subpane index
7
7
  * 3. Session:window (contains :, left side is tmux session) -> tmux lookup
8
- * 4. Bare name -> exact ID → role (team) → customName → partial ID → role (global)
8
+ * 4. Bare name -> exact ID → role (team) → customName → partial ID → substring → role (global)
9
9
  *
10
10
  * Returns { paneId, session, workerId?, paneIndex?, resolvedVia }
11
11
  */
@@ -295,6 +295,36 @@ function resolveByPartialId(
295
295
  );
296
296
  }
297
297
 
298
+ /** Resolve a target by substring match on worker ID. Prefers same-team on ambiguity. */
299
+ function resolveBySubstring(
300
+ target: string,
301
+ workers: Record<string, Agent>,
302
+ currentTeam: string | null,
303
+ ): ResolvedTarget | null {
304
+ // Only match IDs that contain the target but don't end with it (endsWith is handled by resolveByPartialId)
305
+ const candidates = Object.entries(workers).filter(
306
+ ([id]) => id !== target && !id.endsWith(target) && id.includes(target),
307
+ );
308
+ if (candidates.length === 0) return null;
309
+ if (candidates.length === 1) {
310
+ const [id, w] = candidates[0];
311
+ return { paneId: w.paneId, session: w.session, workerId: id, resolvedVia: 'worker' };
312
+ }
313
+
314
+ if (currentTeam) {
315
+ const teamCandidates = candidates.filter(([, w]) => w.team === currentTeam);
316
+ if (teamCandidates.length === 1) {
317
+ const [id, w] = teamCandidates[0];
318
+ return { paneId: w.paneId, session: w.session, workerId: id, resolvedVia: 'worker' };
319
+ }
320
+ }
321
+
322
+ const ids = candidates.map(([id]) => id).join(', ');
323
+ throw new Error(
324
+ `Ambiguous target "${target}" — matches ${candidates.length} workers: ${ids}\nUse the full ID instead.`,
325
+ );
326
+ }
327
+
298
328
  /** Resolve a target by role name across all teams (global fallback). */
299
329
  function resolveByRoleGlobal(target: string, workers: Record<string, Agent>): ResolvedTarget | null {
300
330
  const candidates = Object.entries(workers).filter(([, w]) => w.role === target);
@@ -351,7 +381,7 @@ async function resolveColonTarget(
351
381
  return { paneId: sessionWindowResult.paneId, session: sessionWindowResult.session, resolvedVia: 'session:window' };
352
382
  }
353
383
 
354
- /** Resolve a bare name: exact ID → role (team) → customName → partial ID → role (global). */
384
+ /** Resolve a bare name: exact ID → role (team) → customName → partial ID → substring → role (global). */
355
385
  async function resolveBareName(
356
386
  target: string,
357
387
  workers: Record<string, Agent>,
@@ -383,6 +413,7 @@ async function resolveBareName(
383
413
  resolveByRole(target, workers, currentTeam) ??
384
414
  resolveByCustomName(target, workers, currentTeam) ??
385
415
  resolveByPartialId(target, workers, currentTeam) ??
416
+ resolveBySubstring(target, workers, currentTeam) ??
386
417
  resolveByRoleGlobal(target, workers);
387
418
 
388
419
  if (fuzzyMatch) {
@@ -428,7 +459,7 @@ export async function resolveTarget(target: string, options: ResolveOptions = {}
428
459
  return resolveColonTarget(target, workers, { checkLiveness, isPaneLive, cleanupDeadPane, tmuxLookup });
429
460
  }
430
461
 
431
- // Level 3: Bare name — exact ID → role (team) → customName → partial ID → role (global)
462
+ // Level 3: Bare name — exact ID → role (team) → customName → partial ID → substring → role (global)
432
463
  return resolveBareName(target, workers, {
433
464
  checkLiveness,
434
465
  isPaneLive,
package/src/lib/tmux.ts CHANGED
@@ -12,6 +12,7 @@ interface TmuxSession {
12
12
  interface TmuxWindow {
13
13
  id: string;
14
14
  name: string;
15
+ index: number;
15
16
  active: boolean;
16
17
  sessionId: string;
17
18
  }
@@ -150,16 +151,17 @@ export async function killSession(sessionId: string): Promise<void> {
150
151
  */
151
152
  export async function listWindows(sessionId: string): Promise<TmuxWindow[]> {
152
153
  try {
153
- const format = '#{window_id}:#{window_name}:#{?window_active,1,0}';
154
+ const format = '#{window_id}:#{window_name}:#{window_index}:#{?window_active,1,0}';
154
155
  const output = await executeTmux(`list-windows -t '${sessionId}' -F '${format}'`);
155
156
 
156
157
  if (!output) return [];
157
158
 
158
159
  return output.split('\n').map((line) => {
159
- const [id, name, active] = line.split(':');
160
+ const [id, name, indexStr, active] = line.split(':');
160
161
  return {
161
162
  id,
162
163
  name,
164
+ index: Number.parseInt(indexStr, 10),
163
165
  active: active === '1',
164
166
  sessionId,
165
167
  };
@@ -234,10 +236,12 @@ export async function createSession(name: string): Promise<TmuxSession | null> {
234
236
  */
235
237
  export async function createWindow(sessionId: string, name: string, workingDir?: string): Promise<TmuxWindow | null> {
236
238
  const cdFlag = workingDir ? ` -c '${workingDir.replace(/'/g, "'\\''")}'` : '';
237
- // Use -d (don't switch focus) and -P -F to capture the window ID directly.
239
+ // Use -d (don't switch focus) and -P -F to capture the window ID and index directly.
238
240
  // Avoids relying on findWindowByName which can fail if automatic-rename fires.
239
- const output = await executeTmux(`new-window -d -P -F '#{window_id}' -t '${sessionId}:' -n '${name}'${cdFlag}`);
240
- const windowId = output.trim();
241
+ const output = await executeTmux(
242
+ `new-window -d -P -F '#{window_id}:#{window_index}' -t '${sessionId}:' -n '${name}'${cdFlag}`,
243
+ );
244
+ const [windowId, indexStr] = output.trim().split(':');
241
245
  if (!windowId) return null;
242
246
 
243
247
  // Lock the window name — prevent tmux automatic-rename from overriding it
@@ -247,7 +251,7 @@ export async function createWindow(sessionId: string, name: string, workingDir?:
247
251
  /* best-effort */
248
252
  }
249
253
 
250
- return { id: windowId, name, active: false, sessionId };
254
+ return { id: windowId, name, index: Number.parseInt(indexStr, 10) || 0, active: false, sessionId };
251
255
  }
252
256
 
253
257
  /**
@@ -258,6 +262,37 @@ export async function findWindowByName(sessionId: string, name: string): Promise
258
262
  return windows.find((w) => w.name === name) || null;
259
263
  }
260
264
 
265
+ /**
266
+ * Ensure the master (first) window of a session stays at index 0.
267
+ *
268
+ * When new windows are created, tmux may assign them index 0 if gaps exist
269
+ * (e.g., after renumber-windows or with base-index 0). This pushes the
270
+ * original master window to a higher index. This helper detects that case
271
+ * and uses swap-window to restore the master window to index 0.
272
+ *
273
+ * @param session - The tmux session name
274
+ * @param masterName - The expected name of the master/team-lead window
275
+ */
276
+ export async function ensureMasterWindow(session: string, masterName: string): Promise<void> {
277
+ try {
278
+ const windows = await listWindows(session);
279
+ if (windows.length < 2) return; // Nothing to swap with a single window
280
+
281
+ const masterWindow = windows.find((w) => w.name === masterName);
282
+ if (!masterWindow) return; // Master window not found — nothing to fix
283
+
284
+ // Find the lowest index in the session (respects user's base-index setting)
285
+ const minIndex = Math.min(...windows.map((w) => w.index));
286
+
287
+ if (masterWindow.index === minIndex) return; // Already at the correct position
288
+
289
+ // Swap the master window with whatever is at the lowest index
290
+ await executeTmux(`swap-window -s '${session}:${masterWindow.index}' -t '${session}:${minIndex}'`);
291
+ } catch {
292
+ /* best-effort — don't break window creation if swap fails */
293
+ }
294
+ }
295
+
261
296
  /**
262
297
  * Ensure a tmux window exists for a team within a session.
263
298
  * Idempotent: if the window already exists, returns its first pane.
@@ -289,11 +324,20 @@ export async function ensureTeamWindow(
289
324
  return { windowId: existing.id, windowName: teamName, paneId, created: false };
290
325
  }
291
326
 
327
+ // Remember the current master window (lowest-index window) before creating
328
+ const windowsBefore = await listWindows(session);
329
+ const masterBefore = windowsBefore.length > 0 ? windowsBefore.reduce((a, b) => (a.index <= b.index ? a : b)) : null;
330
+
292
331
  const newWindow = await createWindow(session, teamName, workingDir);
293
332
  if (!newWindow) {
294
333
  throw new Error(`Failed to create team window "${teamName}" in session "${session}"`);
295
334
  }
296
335
 
336
+ // Ensure the master window stays at index 0 after the new window is created
337
+ if (masterBefore) {
338
+ await ensureMasterWindow(session, masterBefore.name);
339
+ }
340
+
297
341
  // Install pane color hook on new window
298
342
  await rehydratePaneColorHook(newWindow.id);
299
343
  const panes = await listPanes(newWindow.id);
@@ -14,6 +14,7 @@ import {
14
14
  findAnyGroupByAssignee,
15
15
  findGroupByAssignee,
16
16
  getGroupState,
17
+ getOrCreateState,
17
18
  getState,
18
19
  resetGroup,
19
20
  startGroup,
@@ -272,6 +273,39 @@ describe('getGroupState', () => {
272
273
  });
273
274
  });
274
275
 
276
+ // ============================================================================
277
+ // getOrCreateState
278
+ // ============================================================================
279
+
280
+ describe('getOrCreateState', () => {
281
+ test('creates state when none exists', async () => {
282
+ const state = await getOrCreateState('new-wish', sampleGroups, cwd);
283
+
284
+ expect(state.wish).toBe('new-wish');
285
+ expect(state.groups['1'].status).toBe('ready');
286
+ expect(state.groups['2'].status).toBe('blocked');
287
+ });
288
+
289
+ test('returns existing state without overwriting', async () => {
290
+ await createState('existing-wish', sampleGroups, cwd);
291
+ await startGroup('existing-wish', '1', 'agent-a', cwd);
292
+
293
+ // Call getOrCreateState — should return existing state, not reset it
294
+ const state = await getOrCreateState('existing-wish', sampleGroups, cwd);
295
+
296
+ expect(state.groups['1'].status).toBe('in_progress');
297
+ expect(state.groups['1'].assignee).toBe('agent-a');
298
+ });
299
+
300
+ test('is idempotent — multiple calls return same state', async () => {
301
+ const first = await getOrCreateState('idem-wish', sampleGroups, cwd);
302
+ const second = await getOrCreateState('idem-wish', sampleGroups, cwd);
303
+
304
+ expect(first.wish).toBe(second.wish);
305
+ expect(Object.keys(first.groups)).toEqual(Object.keys(second.groups));
306
+ });
307
+ });
308
+
275
309
  // ============================================================================
276
310
  // Full lifecycle
277
311
  // ============================================================================
@@ -476,6 +476,16 @@ export async function findAnyGroupByAssignee(
476
476
  return null;
477
477
  }
478
478
 
479
+ /**
480
+ * Get existing state or create it from group definitions.
481
+ * Avoids the "no state file" gap that causes polling loops.
482
+ */
483
+ export async function getOrCreateState(slug: string, groups: GroupDefinition[], cwd?: string): Promise<WishState> {
484
+ const existing = await getState(slug, cwd);
485
+ if (existing) return existing;
486
+ return createState(slug, groups, cwd);
487
+ }
488
+
479
489
  /** Read current state. Returns null if no state exists for this wish. */
480
490
  export async function getState(slug: string, cwd?: string): Promise<WishState | null> {
481
491
  const sql = await getConnection();
@@ -345,12 +345,8 @@ export async function autoOrchestrateCommand(slug: string): Promise<void> {
345
345
  process.exit(1);
346
346
  }
347
347
 
348
- // Auto-initialize wish state
349
- let state = await wishState.getState(slug);
350
- if (!state) {
351
- state = await wishState.createState(slug, groups);
352
- console.log(`📝 Initialized state for wish "${slug}" (${groups.length} groups)`);
353
- }
348
+ // Auto-initialize wish state if missing (prevents polling loop when no state exists)
349
+ const state = await wishState.getOrCreateState(slug, groups);
354
350
 
355
351
  // Find the first wave with groups that are still `ready` (not started/done)
356
352
  const nextWave = waves.find((wave) =>
@@ -503,13 +499,9 @@ export async function workDispatchCommand(agentName: string, ref: string): Promi
503
499
  process.exit(1);
504
500
  }
505
501
 
506
- // Auto-initialize state if no state file exists
507
- let state = await wishState.getState(slug);
508
- if (!state) {
509
- const groups = parseWishGroups(content);
510
- state = await wishState.createState(slug, groups);
511
- console.log(`📝 Initialized state for wish "${slug}" (${groups.length} groups)`);
512
- }
502
+ // Auto-initialize state if missing (prevents polling loop when no state exists)
503
+ const groups = parseWishGroups(content);
504
+ await wishState.getOrCreateState(slug, groups);
513
505
 
514
506
  // Start group in state machine (enforces dependencies)
515
507
  try {
@@ -14,7 +14,7 @@ import { readFile } from 'node:fs/promises';
14
14
  import { join } from 'node:path';
15
15
  import type { Command } from 'commander';
16
16
  import * as wishState from '../lib/wish-state.js';
17
- import { parseExecutionStrategy } from './dispatch.js';
17
+ import { parseExecutionStrategy, parseWishGroups } from './dispatch.js';
18
18
 
19
19
  // ============================================================================
20
20
  // Helpers
@@ -235,12 +235,24 @@ export async function doneCommand(ref: string): Promise<void> {
235
235
  */
236
236
  export async function statusCommand(slug: string): Promise<void> {
237
237
  try {
238
- const state = await wishState.getState(slug);
238
+ let state = await wishState.getState(slug);
239
239
  if (!state) {
240
- console.error(`❌ No state found for wish "${slug}"`);
241
- console.error(' This means work has not been dispatched yet.');
242
- console.error(` Run: genie work ${slug}`);
243
- process.exit(1);
240
+ // Auto-initialize state from WISH.md instead of failing
241
+ const base = process.cwd();
242
+ const wishPath = join(base, '.genie', 'wishes', slug, 'WISH.md');
243
+ if (!existsSync(wishPath)) {
244
+ console.error(`❌ No state found for wish "${slug}" and no WISH.md at ${wishPath}`);
245
+ console.error(` Create it first: genie wish <agent> ${slug}`);
246
+ process.exit(1);
247
+ }
248
+ const content = await readFile(wishPath, 'utf-8');
249
+ const groups = parseWishGroups(content);
250
+ if (groups.length === 0) {
251
+ console.error(`❌ No execution groups found in ${wishPath}`);
252
+ process.exit(1);
253
+ }
254
+ state = await wishState.createState(slug, groups);
255
+ console.log(`📝 Auto-initialized state for wish "${slug}" (${groups.length} groups)`);
244
256
  }
245
257
 
246
258
  console.log(`\nWish: ${state.wish}`);