@automagik/genie 4.260325.6 → 4.260325.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/genie.js +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/lib/protocol-router-spawn.ts +3 -1
- package/src/lib/provider-adapters.test.ts +26 -0
- package/src/term-commands/agents.ts +5 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260325.
|
|
13
|
+
"version": "4.260325.7",
|
|
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
|
@@ -245,7 +245,7 @@ ${errCtx.stack}`;d.reject(err)}else d.resolve(msg)}});return sub.requestSubject=
|
|
|
245
245
|
WHERE id = ANY(${ids})
|
|
246
246
|
`,await sql`
|
|
247
247
|
DELETE FROM task_actors WHERE task_id = ANY(${ids}) AND role = 'assignee'
|
|
248
|
-
`,ids.length}var GroupStatusSchema,GroupStateSchema,WishStateSchema;var init_wish_state=__esm(()=>{init_zod();init_db();GroupStatusSchema=exports_external.enum(["blocked","ready","in_progress","done"]),GroupStateSchema=exports_external.object({status:GroupStatusSchema,assignee:exports_external.string().optional(),dependsOn:exports_external.array(exports_external.string()).default([]),startedAt:exports_external.string().optional(),completedAt:exports_external.string().optional()}),WishStateSchema=exports_external.object({wish:exports_external.string(),groups:exports_external.record(exports_external.string(),GroupStateSchema),createdAt:exports_external.string(),updatedAt:exports_external.string()})});var exports_team_manager={};__export(exports_team_manager,{validateBranchName:()=>validateBranchName,updateTeamConfig:()=>updateTeamConfig,setTeamStatus:()=>setTeamStatus,pruneStaleWorktrees:()=>pruneStaleWorktrees,listTeams:()=>listTeams2,listMembers:()=>listMembers,killTeamMembers:()=>killTeamMembers,hireAgent:()=>hireAgent,getTeam:()=>getTeam2,fireAgent:()=>fireAgent,disbandTeam:()=>disbandTeam,createTeam:()=>createTeam});import{existsSync as existsSync15}from"fs";import{mkdir as mkdir6,readFile as readFile5,readdir as readdir2,rm as rm3,unlink as unlink4,writeFile as writeFile5}from"fs/promises";import{homedir as homedir14}from"os";import path2,{join as join18}from"path";var{$:$3}=globalThis.Bun;function getGenieDir2(){return process.env.GENIE_HOME??join18(homedir14(),".genie")}function teamsDir(){return join18(getGenieDir2(),"teams")}function safeFileName(name){return name.replace(/\//g,"--")}function teamFilePath(name){let safeName=safeFileName(path2.basename(name)===name?name:name);return join18(teamsDir(),`${safeName}.json`)}function getWorktreeBase(repoPath){let base=loadGenieConfigSync().terminal?.worktreeBase;if(base&&base!==".worktrees"){if(path2.isAbsolute(base))return base;return join18(repoPath,base)}let projectName=path2.basename(repoPath);return join18(getGenieDir2(),"worktrees",projectName)}function validateBranchName(name){let errors3=[];if(/\s/.test(name))errors3.push("contains spaces");if(name.includes(".."))errors3.push('contains ".."');if(name.includes("~"))errors3.push('contains "~"');if(name.includes("^"))errors3.push('contains "^"');if(name.includes(":"))errors3.push('contains ":"');if(name.includes("?"))errors3.push('contains "?"');if(name.includes("*"))errors3.push('contains "*"');if(name.includes("["))errors3.push('contains "["');if(name.includes("\\"))errors3.push('contains "\\"');if(/[\x00-\x1f\x7f]/.test(name))errors3.push("contains control characters");if(name.endsWith(".lock"))errors3.push('ends with ".lock"');if(name.endsWith("/"))errors3.push('ends with "/"');if(name.endsWith("."))errors3.push('ends with "."');if(name.startsWith("-"))errors3.push('starts with "-"');if(errors3.length>0)throw Error(`Invalid team name '${name}': must be a valid git branch name (${errors3.join(", ")})`)}async function killWorkersByName(agentName,teamName){let matches=(await list()).filter((w)=>(w.role===agentName||w.id===agentName)&&(!teamName||w.team===teamName));for(let w of matches){try{if(w.paneId&&w.paneId!=="inline"){let{execSync:execSync5}=__require("child_process");execSync5(`tmux kill-pane -t ${w.paneId}`,{stdio:"ignore"})}}catch{}await unregister(w.id)}}async function ensureWorktree(repoPath,branchName,worktreePath,baseBranch){try{await $3`git -C ${repoPath} fetch origin ${baseBranch}`.quiet()}catch{}if(await mkdir6(path2.dirname(worktreePath),{recursive:!0}),existsSync15(worktreePath))return;let branchExists=!1;try{await $3`git -C ${repoPath} rev-parse --verify ${branchName}`.quiet(),branchExists=!0}catch{if(!branchExists)try{await $3`git -C ${repoPath} branch ${branchName} origin/${baseBranch}`.quiet()}catch{try{await $3`git -C ${repoPath} branch ${branchName} ${baseBranch}`.quiet()}catch{await $3`git -C ${repoPath} branch ${branchName}`.quiet()}}}await $3`git clone --shared --branch ${branchName} ${repoPath} ${worktreePath}`.quiet();try{let userName=(await $3`git -C ${repoPath} config user.name`.quiet()).text().trim(),userEmail=(await $3`git -C ${repoPath} config user.email`.quiet()).text().trim();if(userName)await $3`git -C ${worktreePath} config user.name ${userName}`.quiet();if(userEmail)await $3`git -C ${worktreePath} config user.email ${userEmail}`.quiet()}catch{}}async function createTeam(name,repo,baseBranch="dev"){validateBranchName(name);let repoPath=path2.resolve(repo),dir=teamsDir();await mkdir6(dir,{recursive:!0});let filePath=teamFilePath(name);if(existsSync15(filePath)){let content=await readFile5(filePath,"utf-8");return JSON.parse(content)}let worktreeBase=getWorktreeBase(repoPath),worktreePath=join18(worktreeBase,name);await ensureWorktree(repoPath,name,worktreePath,baseBranch);let now=new Date().toISOString(),config={name,repo:repoPath,baseBranch,worktreePath,members:[],status:"in_progress",createdAt:now};if(isInsideClaudeCode()){config.nativeTeamsEnabled=!0;try{let result=await registerAsTeamLead(name);config.nativeTeamParentSessionId=result.sessionId}catch{}}return await writeFile5(filePath,JSON.stringify(config,null,2)),config}async function hireAgent(teamName,agentName){let config=await getTeam2(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);let added;if(agentName==="council")added=BUILTIN_COUNCIL_MEMBERS.map((m)=>m.name).filter((n)=>!config.members.includes(n)),config.members.push(...added);else{if(config.members.includes(agentName))return[];config.members.push(agentName),added=[agentName]}let filePath=teamFilePath(teamName);return await writeFile5(filePath,JSON.stringify(config,null,2)),added}async function fireAgent(teamName,agentName){let config=await getTeam2(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);let idx=config.members.indexOf(agentName);if(idx===-1)return!1;config.members.splice(idx,1);let filePath=teamFilePath(teamName);await writeFile5(filePath,JSON.stringify(config,null,2));try{await killWorkersByName(agentName)}catch{}return!0}async function disbandTeam(teamName){let config=await getTeam2(teamName);if(!config)return!1;try{await deleteNativeTeam(teamName)}catch{}for(let member of config.members)try{await killWorkersByName(member,teamName)}catch{}if(config.wishSlug)try{let resetCount=await(await Promise.resolve().then(() => (init_wish_state(),exports_wish_state))).resetInProgressGroups(config.wishSlug,config.repo);if(resetCount>0)console.log(` Reset ${resetCount} in-progress group(s) for wish "${config.wishSlug}"`)}catch{}if(config.worktreePath&&existsSync15(config.worktreePath))try{await rm3(config.worktreePath,{recursive:!0,force:!0})}catch{}let filePath=teamFilePath(teamName);try{await unlink4(filePath)}catch{return!1}return await pruneStaleWorktrees(config.repo),!0}async function pruneStaleWorktrees(_repoPath){let dir=teamsDir(),files;try{files=await readdir2(dir)}catch{return}for(let file of files){if(!file.endsWith(".json"))continue;try{let content=await readFile5(join18(dir,file),"utf-8"),config=JSON.parse(content);if(config.worktreePath&&!existsSync15(config.worktreePath)){try{await deleteNativeTeam(config.name)}catch{}await unlink4(join18(dir,file))}}catch{}}}async function updateTeamConfig(name,config){let filePath=teamFilePath(name);await writeFile5(filePath,JSON.stringify(config,null,2))}async function getTeam2(name){try{let content=await readFile5(teamFilePath(name),"utf-8");return JSON.parse(content)}catch{return null}}async function listTeams2(){let dir=teamsDir();try{let files=await readdir2(dir),teams=[];for(let file of files){if(!file.endsWith(".json"))continue;try{let content=await readFile5(join18(dir,file),"utf-8");teams.push(JSON.parse(content))}catch{}}return teams}catch{return[]}}async function listMembers(teamName){let config=await getTeam2(teamName);if(!config)return null;return config.members}async function killTeamMembers(teamName){let config=await getTeam2(teamName);if(!config)return;for(let member of config.members)try{await killWorkersByName(member,teamName)}catch{}}async function setTeamStatus(teamName,status){let filePath=teamFilePath(teamName),release=await acquireLock(filePath);try{let config=await getTeam2(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);config.status=status,await writeFile5(filePath,JSON.stringify(config,null,2))}finally{await release()}}var init_team_manager=__esm(()=>{init_agent_registry();init_builtin_agents();init_claude_native_teams();init_file_lock();init_genie_config2()});var exports_protocol_router_spawn={};__export(exports_protocol_router_spawn,{spawnWorkerFromTemplate:()=>spawnWorkerFromTemplate,injectResumeContext:()=>injectResumeContext});import{exec as exec2}from"child_process";import{readFile as readFile6}from"fs/promises";import{join as join19}from"path";import{promisify as promisify2}from"util";async function resolveParentSession(_repoPath,team){let teamConfig=await getTeam2(team);if(teamConfig?.nativeTeamParentSessionId)return teamConfig.nativeTeamParentSessionId;return await discoverClaudeSessionId()??`genie-${team}`}function buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId){let isClaude=template.provider==="claude",sessionName=template.role?`${template.team}-${template.role}`:void 0,newSessionId=isClaude&&!resumeSessionId?crypto.randomUUID():void 0,params={provider:template.provider,team:template.team,role:template.role,skill:template.skill,extraArgs:template.extraArgs,sessionId:newSessionId,resume:isClaude?resumeSessionId:void 0,name:sessionName};if(isClaude)params.nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:template.role??"general-purpose",agentName:template.role};return params}function buildFullCommand(launch){if(launch.env&&Object.keys(launch.env).length>0)return`env ${Object.entries(launch.env).map(([k,v])=>`${k}=${v}`).join(" ")} ${launch.command}`;return launch.command}async function generateWorkerId(team,role){let base=role?`${team}-${role}`:team;return(await list()).some((w)=>w.id===base)?`${base}-${crypto.randomUUID().slice(0,8)}`:base}async function spawnPaneInSession(session,team,repoPath,fullCommand){let teamWindow=null;try{teamWindow=await ensureTeamWindow(session,team,repoPath)}catch{}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",{stdout}=await execAsync(`tmux split-window -d ${splitTarget} -P -F '#{pane_id}' ${fullCommand}`),paneId=stdout.trim(),layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{await execAsync(`tmux ${buildLayoutCommand(layoutTarget,resolveLayoutMode())}`)}catch{}return{paneId,teamWindow}}async function spawnWorkerFromTemplate(template,resumeSessionId){let repoPath=template.cwd??process.cwd(),team=template.team,parentSessionId=await resolveParentSession(repoPath,team);await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=await assignColor(team),params=buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId),launch=buildLaunchCommand(validateSpawnParams(params)),fullCommand=buildFullCommand(launch),workerId=await generateWorkerId(team,template.role),session=await getCurrentSessionName()??team,{paneId,teamWindow}=await spawnPaneInSession(session,team,repoPath,fullCommand),now=new Date().toISOString(),agentName=template.role??"worker",isClaude=template.provider==="claude",effectiveSessionId=resumeSessionId??params.sessionId,workerEntry={id:workerId,paneId,session,provider:template.provider,transport:"tmux",role:template.role,skill:template.skill,team,worktree:null,startedAt:now,state:"spawning",lastStateChange:now,repoPath,claudeSessionId:effectiveSessionId,nativeTeamEnabled:isClaude,nativeAgentId:`${agentName}@${team}`,nativeColor:spawnColor,parentSessionId,window:teamWindow?.windowName,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId};if(await register(workerEntry),await registerNativeMember(team,{agentName,agentType:template.role??"general-purpose",color:spawnColor??"blue",tmuxPaneId:paneId,cwd:repoPath}),await writeNativeInbox(team,"team-lead",{from:agentName,text:`Worker ${agentName} (${template.provider}) auto-spawned${resumeSessionId?" with --resume":""}. Ready for tasks.`,summary:`${agentName} auto-spawned`,timestamp:now,color:spawnColor??"blue",read:!1}),spawnColor)await applyPaneColor(paneId,spawnColor,teamWindow?.windowId);return await injectResumeContext(repoPath,workerId,agentName,team),{worker:workerEntry,paneId,workerId}}function extractGroupSection(content,groupName){let escaped=groupName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^### Group ${escaped}:`,"m"),match=content.match(pattern);if(!match||match.index===void 0)return null;let start=match.index,nextBoundary=content.slice(start).slice(1).search(/^### Group \d|^---$/m),end=nextBoundary!==-1?start+1+nextBoundary:content.length;return content.slice(start,end).trim()}async function getRecentGitLog(repoPath,count=3){try{let{stdout}=await execAsync(`git -C '${repoPath}' log --oneline -${count} 2>/dev/null`);return stdout.trim()}catch{return""}}async function getGitStatus(repoPath){try{let{stdout}=await execAsync(`git -C '${repoPath}' status --short 2>/dev/null`);return stdout.trim()}catch{return""}}async function injectResumeContext(repoPath,workerId,agentName,_team){try{let match=await findAnyGroupByAssignee(workerId,repoPath)??await findAnyGroupByAssignee(agentName,repoPath);if(!match)return;let{slug,groupName,group}=match,wishPath=join19(repoPath,".genie","wishes",slug,"WISH.md"),groupSection="";try{let wishContent=await readFile6(wishPath,"utf-8");groupSection=extractGroupSection(wishContent,groupName)??""}catch{}let gitLog=await getRecentGitLog(repoPath),gitStatus=await getGitStatus(repoPath),resumePrompt=[`RESUME CONTEXT: You were working on wish "${slug}", group "${groupName}".`,`Status: ${group.status}. Started at: ${group.startedAt??"unknown"}.`,`Wish file: .genie/wishes/${slug}/WISH.md`,"",groupSection?`Group section:
|
|
248
|
+
`,ids.length}var GroupStatusSchema,GroupStateSchema,WishStateSchema;var init_wish_state=__esm(()=>{init_zod();init_db();GroupStatusSchema=exports_external.enum(["blocked","ready","in_progress","done"]),GroupStateSchema=exports_external.object({status:GroupStatusSchema,assignee:exports_external.string().optional(),dependsOn:exports_external.array(exports_external.string()).default([]),startedAt:exports_external.string().optional(),completedAt:exports_external.string().optional()}),WishStateSchema=exports_external.object({wish:exports_external.string(),groups:exports_external.record(exports_external.string(),GroupStateSchema),createdAt:exports_external.string(),updatedAt:exports_external.string()})});var exports_team_manager={};__export(exports_team_manager,{validateBranchName:()=>validateBranchName,updateTeamConfig:()=>updateTeamConfig,setTeamStatus:()=>setTeamStatus,pruneStaleWorktrees:()=>pruneStaleWorktrees,listTeams:()=>listTeams2,listMembers:()=>listMembers,killTeamMembers:()=>killTeamMembers,hireAgent:()=>hireAgent,getTeam:()=>getTeam2,fireAgent:()=>fireAgent,disbandTeam:()=>disbandTeam,createTeam:()=>createTeam});import{existsSync as existsSync15}from"fs";import{mkdir as mkdir6,readFile as readFile5,readdir as readdir2,rm as rm3,unlink as unlink4,writeFile as writeFile5}from"fs/promises";import{homedir as homedir14}from"os";import path2,{join as join18}from"path";var{$:$3}=globalThis.Bun;function getGenieDir2(){return process.env.GENIE_HOME??join18(homedir14(),".genie")}function teamsDir(){return join18(getGenieDir2(),"teams")}function safeFileName(name){return name.replace(/\//g,"--")}function teamFilePath(name){let safeName=safeFileName(path2.basename(name)===name?name:name);return join18(teamsDir(),`${safeName}.json`)}function getWorktreeBase(repoPath){let base=loadGenieConfigSync().terminal?.worktreeBase;if(base&&base!==".worktrees"){if(path2.isAbsolute(base))return base;return join18(repoPath,base)}let projectName=path2.basename(repoPath);return join18(getGenieDir2(),"worktrees",projectName)}function validateBranchName(name){let errors3=[];if(/\s/.test(name))errors3.push("contains spaces");if(name.includes(".."))errors3.push('contains ".."');if(name.includes("~"))errors3.push('contains "~"');if(name.includes("^"))errors3.push('contains "^"');if(name.includes(":"))errors3.push('contains ":"');if(name.includes("?"))errors3.push('contains "?"');if(name.includes("*"))errors3.push('contains "*"');if(name.includes("["))errors3.push('contains "["');if(name.includes("\\"))errors3.push('contains "\\"');if(/[\x00-\x1f\x7f]/.test(name))errors3.push("contains control characters");if(name.endsWith(".lock"))errors3.push('ends with ".lock"');if(name.endsWith("/"))errors3.push('ends with "/"');if(name.endsWith("."))errors3.push('ends with "."');if(name.startsWith("-"))errors3.push('starts with "-"');if(errors3.length>0)throw Error(`Invalid team name '${name}': must be a valid git branch name (${errors3.join(", ")})`)}async function killWorkersByName(agentName,teamName){let matches=(await list()).filter((w)=>(w.role===agentName||w.id===agentName)&&(!teamName||w.team===teamName));for(let w of matches){try{if(w.paneId&&w.paneId!=="inline"){let{execSync:execSync5}=__require("child_process");execSync5(`tmux kill-pane -t ${w.paneId}`,{stdio:"ignore"})}}catch{}await unregister(w.id)}}async function ensureWorktree(repoPath,branchName,worktreePath,baseBranch){try{await $3`git -C ${repoPath} fetch origin ${baseBranch}`.quiet()}catch{}if(await mkdir6(path2.dirname(worktreePath),{recursive:!0}),existsSync15(worktreePath))return;let branchExists=!1;try{await $3`git -C ${repoPath} rev-parse --verify ${branchName}`.quiet(),branchExists=!0}catch{if(!branchExists)try{await $3`git -C ${repoPath} branch ${branchName} origin/${baseBranch}`.quiet()}catch{try{await $3`git -C ${repoPath} branch ${branchName} ${baseBranch}`.quiet()}catch{await $3`git -C ${repoPath} branch ${branchName}`.quiet()}}}await $3`git clone --shared --branch ${branchName} ${repoPath} ${worktreePath}`.quiet();try{let userName=(await $3`git -C ${repoPath} config user.name`.quiet()).text().trim(),userEmail=(await $3`git -C ${repoPath} config user.email`.quiet()).text().trim();if(userName)await $3`git -C ${worktreePath} config user.name ${userName}`.quiet();if(userEmail)await $3`git -C ${worktreePath} config user.email ${userEmail}`.quiet()}catch{}}async function createTeam(name,repo,baseBranch="dev"){validateBranchName(name);let repoPath=path2.resolve(repo),dir=teamsDir();await mkdir6(dir,{recursive:!0});let filePath=teamFilePath(name);if(existsSync15(filePath)){let content=await readFile5(filePath,"utf-8");return JSON.parse(content)}let worktreeBase=getWorktreeBase(repoPath),worktreePath=join18(worktreeBase,name);await ensureWorktree(repoPath,name,worktreePath,baseBranch);let now=new Date().toISOString(),config={name,repo:repoPath,baseBranch,worktreePath,members:[],status:"in_progress",createdAt:now};if(isInsideClaudeCode()){config.nativeTeamsEnabled=!0;try{let result=await registerAsTeamLead(name);config.nativeTeamParentSessionId=result.sessionId}catch{}}return await writeFile5(filePath,JSON.stringify(config,null,2)),config}async function hireAgent(teamName,agentName){let config=await getTeam2(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);let added;if(agentName==="council")added=BUILTIN_COUNCIL_MEMBERS.map((m)=>m.name).filter((n)=>!config.members.includes(n)),config.members.push(...added);else{if(config.members.includes(agentName))return[];config.members.push(agentName),added=[agentName]}let filePath=teamFilePath(teamName);return await writeFile5(filePath,JSON.stringify(config,null,2)),added}async function fireAgent(teamName,agentName){let config=await getTeam2(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);let idx=config.members.indexOf(agentName);if(idx===-1)return!1;config.members.splice(idx,1);let filePath=teamFilePath(teamName);await writeFile5(filePath,JSON.stringify(config,null,2));try{await killWorkersByName(agentName)}catch{}return!0}async function disbandTeam(teamName){let config=await getTeam2(teamName);if(!config)return!1;try{await deleteNativeTeam(teamName)}catch{}for(let member of config.members)try{await killWorkersByName(member,teamName)}catch{}if(config.wishSlug)try{let resetCount=await(await Promise.resolve().then(() => (init_wish_state(),exports_wish_state))).resetInProgressGroups(config.wishSlug,config.repo);if(resetCount>0)console.log(` Reset ${resetCount} in-progress group(s) for wish "${config.wishSlug}"`)}catch{}if(config.worktreePath&&existsSync15(config.worktreePath))try{await rm3(config.worktreePath,{recursive:!0,force:!0})}catch{}let filePath=teamFilePath(teamName);try{await unlink4(filePath)}catch{return!1}return await pruneStaleWorktrees(config.repo),!0}async function pruneStaleWorktrees(_repoPath){let dir=teamsDir(),files;try{files=await readdir2(dir)}catch{return}for(let file of files){if(!file.endsWith(".json"))continue;try{let content=await readFile5(join18(dir,file),"utf-8"),config=JSON.parse(content);if(config.worktreePath&&!existsSync15(config.worktreePath)){try{await deleteNativeTeam(config.name)}catch{}await unlink4(join18(dir,file))}}catch{}}}async function updateTeamConfig(name,config){let filePath=teamFilePath(name);await writeFile5(filePath,JSON.stringify(config,null,2))}async function getTeam2(name){try{let content=await readFile5(teamFilePath(name),"utf-8");return JSON.parse(content)}catch{return null}}async function listTeams2(){let dir=teamsDir();try{let files=await readdir2(dir),teams=[];for(let file of files){if(!file.endsWith(".json"))continue;try{let content=await readFile5(join18(dir,file),"utf-8");teams.push(JSON.parse(content))}catch{}}return teams}catch{return[]}}async function listMembers(teamName){let config=await getTeam2(teamName);if(!config)return null;return config.members}async function killTeamMembers(teamName){let config=await getTeam2(teamName);if(!config)return;for(let member of config.members)try{await killWorkersByName(member,teamName)}catch{}}async function setTeamStatus(teamName,status){let filePath=teamFilePath(teamName),release=await acquireLock(filePath);try{let config=await getTeam2(teamName);if(!config)throw Error(`Team "${teamName}" not found.`);config.status=status,await writeFile5(filePath,JSON.stringify(config,null,2))}finally{await release()}}var init_team_manager=__esm(()=>{init_agent_registry();init_builtin_agents();init_claude_native_teams();init_file_lock();init_genie_config2()});var exports_protocol_router_spawn={};__export(exports_protocol_router_spawn,{spawnWorkerFromTemplate:()=>spawnWorkerFromTemplate,injectResumeContext:()=>injectResumeContext});import{exec as exec2}from"child_process";import{readFile as readFile6}from"fs/promises";import{join as join19}from"path";import{promisify as promisify2}from"util";async function resolveParentSession(_repoPath,team){let teamConfig=await getTeam2(team);if(teamConfig?.nativeTeamParentSessionId)return teamConfig.nativeTeamParentSessionId;return await discoverClaudeSessionId()??`genie-${team}`}function buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId){let isClaude=template.provider==="claude",sessionName=template.role?`${template.team}-${template.role}`:void 0,newSessionId=isClaude&&!resumeSessionId?crypto.randomUUID():void 0,params={provider:template.provider,team:template.team,role:template.role,skill:template.skill,extraArgs:template.extraArgs,sessionId:newSessionId,resume:isClaude?resumeSessionId:void 0,name:sessionName};if(isClaude)params.nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:template.role??"general-purpose",agentName:template.role};return params}function buildFullCommand(launch){if(launch.env&&Object.keys(launch.env).length>0)return`env ${Object.entries(launch.env).map(([k,v])=>`${k}=${v}`).join(" ")} ${launch.command}`;return launch.command}async function generateWorkerId(team,role){let base=role?`${team}-${role}`:team;return(await list()).some((w)=>w.id===base)?`${base}-${crypto.randomUUID().slice(0,8)}`:base}async function spawnPaneInSession(session,team,repoPath,fullCommand){let teamWindow=null;try{teamWindow=await ensureTeamWindow(session,team,repoPath)}catch{}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",escapedCmd=fullCommand.replace(/'/g,"'\\''"),{stdout}=await execAsync(`tmux split-window -d ${splitTarget} -P -F '#{pane_id}' '${escapedCmd}'`),paneId=stdout.trim(),layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{await execAsync(`tmux ${buildLayoutCommand(layoutTarget,resolveLayoutMode())}`)}catch{}return{paneId,teamWindow}}async function spawnWorkerFromTemplate(template,resumeSessionId){let repoPath=template.cwd??process.cwd(),team=template.team,parentSessionId=await resolveParentSession(repoPath,team);await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=await assignColor(team),params=buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId),launch=buildLaunchCommand(validateSpawnParams(params)),fullCommand=buildFullCommand(launch),workerId=await generateWorkerId(team,template.role),session=await getCurrentSessionName()??team,{paneId,teamWindow}=await spawnPaneInSession(session,team,repoPath,fullCommand),now=new Date().toISOString(),agentName=template.role??"worker",isClaude=template.provider==="claude",effectiveSessionId=resumeSessionId??params.sessionId,workerEntry={id:workerId,paneId,session,provider:template.provider,transport:"tmux",role:template.role,skill:template.skill,team,worktree:null,startedAt:now,state:"spawning",lastStateChange:now,repoPath,claudeSessionId:effectiveSessionId,nativeTeamEnabled:isClaude,nativeAgentId:`${agentName}@${team}`,nativeColor:spawnColor,parentSessionId,window:teamWindow?.windowName,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId};if(await register(workerEntry),await registerNativeMember(team,{agentName,agentType:template.role??"general-purpose",color:spawnColor??"blue",tmuxPaneId:paneId,cwd:repoPath}),await writeNativeInbox(team,"team-lead",{from:agentName,text:`Worker ${agentName} (${template.provider}) auto-spawned${resumeSessionId?" with --resume":""}. Ready for tasks.`,summary:`${agentName} auto-spawned`,timestamp:now,color:spawnColor??"blue",read:!1}),spawnColor)await applyPaneColor(paneId,spawnColor,teamWindow?.windowId);return await injectResumeContext(repoPath,workerId,agentName,team),{worker:workerEntry,paneId,workerId}}function extractGroupSection(content,groupName){let escaped=groupName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^### Group ${escaped}:`,"m"),match=content.match(pattern);if(!match||match.index===void 0)return null;let start=match.index,nextBoundary=content.slice(start).slice(1).search(/^### Group \d|^---$/m),end=nextBoundary!==-1?start+1+nextBoundary:content.length;return content.slice(start,end).trim()}async function getRecentGitLog(repoPath,count=3){try{let{stdout}=await execAsync(`git -C '${repoPath}' log --oneline -${count} 2>/dev/null`);return stdout.trim()}catch{return""}}async function getGitStatus(repoPath){try{let{stdout}=await execAsync(`git -C '${repoPath}' status --short 2>/dev/null`);return stdout.trim()}catch{return""}}async function injectResumeContext(repoPath,workerId,agentName,_team){try{let match=await findAnyGroupByAssignee(workerId,repoPath)??await findAnyGroupByAssignee(agentName,repoPath);if(!match)return;let{slug,groupName,group}=match,wishPath=join19(repoPath,".genie","wishes",slug,"WISH.md"),groupSection="";try{let wishContent=await readFile6(wishPath,"utf-8");groupSection=extractGroupSection(wishContent,groupName)??""}catch{}let gitLog=await getRecentGitLog(repoPath),gitStatus=await getGitStatus(repoPath),resumePrompt=[`RESUME CONTEXT: You were working on wish "${slug}", group "${groupName}".`,`Status: ${group.status}. Started at: ${group.startedAt??"unknown"}.`,`Wish file: .genie/wishes/${slug}/WISH.md`,"",groupSection?`Group section:
|
|
249
249
|
${groupSection}`:"","",gitLog?`Last git log:
|
|
250
250
|
${gitLog}`:"","",gitStatus?`Uncommitted changes:
|
|
251
251
|
${gitStatus}`:"","","Pick up where you left off. Read the wish file for full context."].filter(Boolean).join(`
|
|
@@ -528,7 +528,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
528
528
|
process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
|
529
529
|
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
|
530
530
|
`,{mode:420});let{spawn:spawnChild}=__require("child_process");spawnChild("node",[scriptFile],{detached:!0,stdio:"ignore"}).unref();for(let i2=0;i2<30;i2++)if(await new Promise((r)=>setTimeout(r,100)),isRelayAlive(pidFile))return!0;return!1}catch{return!1}}async function generateWorkerId2(team,role){let base=role?`${team}-${role}`:team;if(!(await list()).some((w)=>w.id===base))return base;let suffix=crypto.randomUUID().slice(0,8);return`${base}-${suffix}`}async function registerSpawnWorker(ctx,paneId,windowInfo){let nt=ctx.validated.nativeTeam,workerEntry={id:ctx.workerId,paneId,session:ctx.validated.team,provider:ctx.validated.provider,transport:ctx.transport,role:ctx.validated.role,skill:ctx.validated.skill,team:ctx.validated.team,worktree:null,startedAt:ctx.now,state:"spawning",lastStateChange:ctx.now,repoPath:ctx.cwd,claudeSessionId:ctx.claudeSessionId,nativeTeamEnabled:nt?.enabled??!1,nativeAgentId:`${ctx.agentName}@${ctx.validated.team}`,nativeColor:nt?.color??ctx.spawnColor,parentSessionId:nt?.parentSessionId??ctx.parentSessionId,window:windowInfo?.windowName,windowName:windowInfo?.windowName,windowId:windowInfo?.windowId,autoResume:ctx.autoResume===!1?!1:void 0,resumeAttempts:0};await register(workerEntry);let role=ctx.validated.role??ctx.agentName;if(role!=="council")try{await hireAgent(ctx.validated.team,role)}catch{}return workerEntry}async function notifySpawnJoin(ctx,paneId){let nt=ctx.validated.nativeTeam;await registerNativeMember(ctx.validated.team,{agentName:ctx.agentName,agentType:nt?.agentType??ctx.validated.role??"general-purpose",color:nt?.color??ctx.spawnColor??"blue",tmuxPaneId:paneId,cwd:ctx.cwd,planModeRequired:nt?.planModeRequired}),await writeNativeInbox(ctx.validated.team,"team-lead",{from:ctx.agentName,text:`Worker ${ctx.agentName} (${ctx.validated.provider}) joined team ${ctx.validated.team}. cwd: ${ctx.cwd}. Ready for tasks.`,summary:`${ctx.agentName} (${ctx.validated.provider}) joined`,timestamp:new Date().toISOString(),color:nt?.color??ctx.spawnColor??"blue",read:!1})}function registerOtelRelayPane(workerId,paneId,agentName,spawnColor,repoPath){let{writeFileSync:wfs}=__require("fs"),{join:pjoin}=__require("path"),{homedir:hdir}=__require("os"),rd=pjoin(hdir(),".genie","relay");wfs(pjoin(rd,`${workerId}-pane`),paneId),wfs(pjoin(rd,`${workerId}-meta`),JSON.stringify({agent:agentName,color:spawnColor,repoPath}))}function printSpawnInfo(ctx,paneId,workerEntry){let nt=ctx.validated.nativeTeam;if(console.log(`Agent "${ctx.workerId}" spawned.`),console.log(` Provider: ${ctx.launch.provider}`),console.log(` Command: ${ctx.fullCommand}`),console.log(` Team: ${ctx.validated.team}`),console.log(` Pane: ${paneId}`),ctx.validated.role)console.log(` Role: ${ctx.validated.role}`);if(ctx.validated.skill)console.log(` Skill: ${ctx.validated.skill}`);if(workerEntry.claudeSessionId)console.log(` Session: ${workerEntry.claudeSessionId}`);if(console.log(` Layout: ${ctx.layoutMode}`),nt?.enabled)console.log(" Native: enabled"),console.log(` AgentID: ${workerEntry.nativeAgentId}`),console.log(` Color: ${nt.color}`);if(ctx.otelRelayActive)console.log(` OTel: relay on port ${OTEL_RELAY_PORT}`)}async function resolveSpawnTeamWindow(team,cwd,sessionOverride){if(!team)return null;try{let sessionName=sessionOverride??await getCurrentSessionName(team);if(!sessionName)sessionName=(await getTeam2(team))?.tmuxSessionName??team;return await ensureTeamWindow(sessionName,team,cwd)}catch(err){return console.warn(`Warning: could not ensure team window for "${team}": ${err instanceof Error?err.message:err}`),null}}async function autoConfirmTrustPrompt(paneId){let{execSync:execSync5}=__require("child_process"),maxWaitMs=15000,pollMs=500,start=Date.now();while(Date.now()-start<15000){await new Promise((r)=>setTimeout(r,500));let content;try{content=execSync5(`tmux capture-pane -t '${paneId}' -p`,{encoding:"utf-8"})}catch{return}if(content.includes("trust this folder")||content.includes("Quick safety check")){try{execSync5(`tmux send-keys -t '${paneId}' Enter`,{encoding:"utf-8"})}catch{}return}if(content.includes("Claude Code")||content.includes("\u276F")||content.includes("Churning"))return}}function createTmuxPane(ctx,teamWindow){let{execSync:execSync5}=__require("child_process");if(teamWindow?.created){let paneId=execSync5(`tmux list-panes -t '${teamWindow.windowId}' -F '#{pane_id}'`,{encoding:"utf-8"}).trim().split(`
|
|
531
|
-
`)[0];if(ctx.cwd)execSync5(`tmux send-keys -t '${paneId}' 'cd ${ctx.cwd.replace(/'/g,"'\\''")}' Enter`,{encoding:"utf-8"});return execSync5(`tmux send-keys -t '${paneId}' '${ctx.fullCommand.replace(/'/g,"'\\''")}' Enter`,{encoding:"utf-8"}),paneId}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",cwdFlag=ctx.cwd?`-c '${ctx.cwd}'`:"",splitCmd=`tmux split-window -d ${splitTarget} ${cwdFlag} -P -F '#{pane_id}' ${
|
|
531
|
+
`)[0];if(ctx.cwd)execSync5(`tmux send-keys -t '${paneId}' 'cd ${ctx.cwd.replace(/'/g,"'\\''")}' Enter`,{encoding:"utf-8"});return execSync5(`tmux send-keys -t '${paneId}' '${ctx.fullCommand.replace(/'/g,"'\\''")}' Enter`,{encoding:"utf-8"}),paneId}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",cwdFlag=ctx.cwd?`-c '${ctx.cwd}'`:"",escapedCmd=ctx.fullCommand.replace(/'/g,"'\\''"),splitCmd=`tmux split-window -d ${splitTarget} ${cwdFlag} -P -F '#{pane_id}' '${escapedCmd}'`;return execSync5(splitCmd,{encoding:"utf-8"}).trim()}async function applySpawnLayout(ctx,teamWindow){let{execSync:execSync5}=__require("child_process"),session=await getCurrentSessionName()??ctx.validated.team,layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{execSync5(`tmux ${buildLayoutCommand(layoutTarget,ctx.layoutMode)}`,{stdio:"ignore"})}catch{}}async function launchTmuxSpawn(ctx){let teamWindow=ctx.spawnIntoCurrentWindow?null:await resolveSpawnTeamWindow(ctx.validated.team,ctx.cwd,ctx.sessionOverride),paneId;try{paneId=createTmuxPane(ctx,teamWindow)}catch(err){console.error(`Failed to create tmux pane: ${err instanceof Error?err.message:"unknown error"}`),process.exit(1)}if(await applySpawnLayout(ctx,teamWindow),ctx.validated.provider==="claude")await autoConfirmTrustPrompt(paneId);let workerEntry=await registerSpawnWorker(ctx,paneId,teamWindow);if(await notifySpawnJoin(ctx,paneId),ctx.spawnColor&&paneId!=="inline")await applyPaneColor(paneId,ctx.spawnColor,teamWindow?.windowId);if(await saveTemplate({id:ctx.validated.role??ctx.workerId,provider:ctx.validated.provider,team:ctx.validated.team,role:ctx.validated.role,skill:ctx.validated.skill,cwd:ctx.cwd,extraArgs:ctx.extraArgs,nativeTeamEnabled:workerEntry.nativeTeamEnabled,lastSpawnedAt:new Date().toISOString()}),ctx.otelRelayActive&&paneId!=="%0")registerOtelRelayPane(ctx.workerId,paneId,ctx.agentName,ctx.spawnColor,ctx.cwd);if(teamWindow)console.log(` Window: ${teamWindow.windowName} (${teamWindow.windowId})`);if(printSpawnInfo(ctx,paneId,workerEntry),paneId!=="inline"){let result=await waitForAgentReady(paneId);if(result.ready)console.log(` \u2713 Agent ready (${(result.elapsedMs/1000).toFixed(1)}s)`);else console.log(` \u26A0 Agent readiness timeout (${Math.round(result.elapsedMs/1000)}s) \u2014 proceeding anyway`)}}async function launchInlineSpawn(ctx){let nt=ctx.validated.nativeTeam,paneId="inline",workerEntry=await registerSpawnWorker(ctx,"inline");if(await notifySpawnJoin(ctx,"inline"),console.log(`Agent "${ctx.workerId}" starting inline...`),console.log(` Provider: ${ctx.launch.provider} | Team: ${ctx.validated.team} | Role: ${ctx.validated.role??"-"}`),nt?.enabled)console.log(` Native: enabled | AgentID: ${workerEntry.nativeAgentId}`);console.log("");let{spawnSync}=__require("child_process"),envVars={...process.env,...ctx.launch.env??{}},result=spawnSync("sh",["-c",ctx.launch.command],{env:envVars,stdio:"inherit"});if(await unregister(ctx.workerId),nt?.enabled&&ctx.agentName)await clearNativeInbox(ctx.validated.team,ctx.agentName).catch(()=>{}),await unregisterNativeMember(ctx.validated.team,ctx.agentName).catch(()=>{});console.log(`
|
|
532
532
|
Agent "${ctx.workerId}" session ended.`),process.exit(result.status??0)}function prependEnvVars(command,env){if(!env||Object.keys(env).length===0)return command;return`env ${Object.entries(env).map(([k,v])=>`${k}=${v}`).join(" ")} ${command}`}async function rejectDuplicateRole(team,role){let existing=await list();for(let w of existing)if(w.role===role&&w.team===team){if(await isPaneAlive(w.paneId))console.error(`Error: Worker with role "${role}" already exists in team "${team}" (state: ${w.state}, pane: ${w.paneId})
|
|
533
533
|
Use a different --role name for a second worker, e.g.: --role ${role}-2`),process.exit(1);await unregister(w.id)}}async function resolveNativeTeam(team,_repoPath,options){let parentSessionId=(await getTeam2(team))?.nativeTeamParentSessionId;if(!parentSessionId)parentSessionId=await discoverClaudeSessionId()??`genie-${team}`;await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=options.color??await assignColor(team),nativeTeam;if(options.provider==="claude")nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:options.role??"general-purpose",planModeRequired:options.planMode,permissionMode:options.permissionMode,agentName:options.role};return{parentSessionId,spawnColor,nativeTeam}}async function resolveAgentForSpawn(name,options){let resolved=await resolve3(name);if(!resolved)console.error(`Error: Agent "${name}" not found in directory or built-ins.`),console.error(` Register with: genie dir add ${name} --dir <path>`),console.error(" Or use a built-in: engineer, reviewer, qa, fix, ..."),process.exit(1);let entry=resolved.entry,identityPath=null;if(resolved.builtin)identityPath=resolveBuiltinAgentPath(name);else if(entry.dir)identityPath=loadIdentity(entry);return{entry,repoPath:options.cwd??(entry.dir||void 0)??process.cwd(),identityPath,model:options.model??entry.model}}async function buildSpawnParams2(name,team,options,agent){let params={provider:options.provider,team,role:name,skill:options.skill,extraArgs:options.extraArgs,model:agent.model,systemPromptFile:agent.identityPath??void 0,promptMode:agent.entry.promptMode,initialPrompt:options.initialPrompt},{parentSessionId,spawnColor,nativeTeam}=await resolveNativeTeam(team,agent.repoPath,{...options,role:name});if(nativeTeam)params.nativeTeam=nativeTeam;try{let{injectTeamHooks:injectTeamHooks2}=await Promise.resolve().then(() => (init_inject(),exports_inject));if(await injectTeamHooks2(team))console.log(` Hooks: injected genie hook dispatch into team "${team}"`)}catch(err){console.warn(`Warning: could not inject hooks for team "${team}": ${err instanceof Error?err.message:err}`)}if(params.provider==="claude")params.sessionId=crypto.randomUUID();return{params,parentSessionId,spawnColor}}async function handleWorkerSpawn(name,options){let effectiveRole=options.role??name,agent=await resolveAgentForSpawn(name,options),teamWasExplicit=Boolean(options.team),team=options.team||await discoverTeamName();if(!team)console.error("Error: --team is required (or set GENIE_TEAM, or run inside a genie session)"),process.exit(1);await rejectDuplicateRole(team,effectiveRole);let teamConfig=await getTeam2(team);if(teamConfig?.worktreePath)agent={...agent,repoPath:teamConfig.worktreePath};let{params,parentSessionId,spawnColor}=await buildSpawnParams2(effectiveRole,team,options,agent);if(!params.name)params.name=`${params.team}-${effectiveRole}`;let validated=validateSpawnParams(params),launch=buildLaunchCommand(validated),layoutMode=resolveLayoutMode(options.layout),workerId=await generateWorkerId2(validated.team,effectiveRole),insideTmux=Boolean(process.env.TMUX),nt=validated.nativeTeam,now=new Date().toISOString(),agentName=nt?.agentName??effectiveRole,otelRelayActive=!1;if(!nt?.enabled&&validated.provider==="codex"&&insideTmux)ensureCodexOtelConfig(),otelRelayActive=await ensureOtelRelay(validated.team);let fullCommand=prependEnvVars(launch.command,launch.env),ctx={workerId,validated,launch,layoutMode,fullCommand,agentName,spawnColor,parentSessionId,claudeSessionId:validated.sessionId,otelRelayActive,now,transport:insideTmux?"tmux":"inline",extraArgs:options.extraArgs,cwd:agent.repoPath,spawnIntoCurrentWindow:!teamWasExplicit&&insideTmux&&!options.session,sessionOverride:options.session,autoResume:options.autoResume};if(insideTmux)await launchTmuxSpawn(ctx);else await launchInlineSpawn(ctx)}async function cleanupWorkerNativeTeam(w){if(!w.team||!w.nativeAgentId)return;let agentName=w.nativeAgentId.split("@")[0];await clearNativeInbox(w.team,agentName).catch(()=>{}),await unregisterNativeMember(w.team,agentName).catch(()=>{})}function killWorkerPane(w){try{let{execSync:execSync5}=__require("child_process"),currentPane=execSync5("tmux display-message -p '#{pane_id}'",{encoding:"utf-8"}).trim();if(w.paneId&&/^(%\d+|inline)$/.test(w.paneId)&&w.paneId!==currentPane)execSync5(`tmux kill-pane -t ${w.paneId}`,{stdio:"ignore"});else if(w.paneId===currentPane)console.log(" (skipped pane kill \u2014 would kill current session)")}catch{}}function cleanupRelayFiles(id){try{let{join:join21}=__require("path"),{homedir:homedir16}=__require("os"),{unlinkSync:unlinkSync4}=__require("fs"),relayDir=join21(homedir16(),".genie","relay");for(let suffix of["-pane","-meta"])try{unlinkSync4(join21(relayDir,`${id}${suffix}`))}catch{}}catch{}}async function resolveWorkerByName(name){let exact=await get(name);if(exact)return exact;let workers=await list(),byRole=workers.filter((w)=>w.role===name);if(byRole.length===1)return byRole[0];if(byRole.length>1){console.error(`Multiple agents with role "${name}". Specify full ID:`);for(let w of byRole)console.error(` ${w.id} (team: ${w.team})`);process.exit(1)}let bySuffix=workers.filter((w)=>w.id.endsWith(`-${name}`));if(bySuffix.length===1)return bySuffix[0];if(bySuffix.length>1){console.error(`Multiple agents matching "${name}". Specify full ID:`);for(let w of bySuffix)console.error(` ${w.id}`);process.exit(1)}console.error(`Agent "${name}" not found.`),console.error(" Run `genie ls` to see agents."),process.exit(1)}async function handleWorkerKill(name){let w=await resolveWorkerByName(name);killWorkerPane(w),cleanupRelayFiles(w.id),await cleanupWorkerNativeTeam(w),await unregister(w.id),console.log(`Agent "${w.id}" killed and unregistered (template preserved).`)}async function handleWorkerStop(name){let w=await resolveWorkerByName(name);if(w.state==="suspended"){console.log(`Agent "${w.id}" is already stopped.`);return}let{suspendWorker:suspendWorker2}=await Promise.resolve().then(() => (init_idle_timeout(),exports_idle_timeout));if(await suspendWorker2(w.id)){if(console.log(`Agent "${w.id}" stopped.`),w.claudeSessionId)console.log(` Session preserved: ${w.claudeSessionId}`);console.log(` Send a message to auto-resume: genie send '...' --to ${w.id}`)}else console.error(`Failed to stop agent "${w.id}".`),process.exit(1)}async function isResumeEligible(w){return(w.state==="suspended"||w.state==="error")&&Boolean(w.claudeSessionId)&&!await isPaneAlive(w.paneId)}async function resumeAllAgents(){let workers=await list(),toResume=[];for(let w of workers)if(await isResumeEligible(w))toResume.push(w);if(toResume.length===0){console.log("No eligible agents to resume.");return}console.log(`Resuming ${toResume.length} agent(s)...`);for(let w of toResume)try{await resumeAgent(w)}catch(err){console.error(` Failed to resume "${w.id}": ${err instanceof Error?err.message:err}`)}}async function handleWorkerResume(name,options){if(options.all)return resumeAllAgents();if(!name)console.error("Error: provide an agent name, or use --all to resume all eligible agents."),process.exit(1);let w=await resolveWorkerByName(name);if(!w.claudeSessionId)console.error(`Error: Agent "${w.id}" has no Claude session ID \u2014 cannot resume.`),console.error(" Only agents spawned with the Claude provider have resumable sessions."),process.exit(1);if(await isPaneAlive(w.paneId)){console.log(`Agent "${w.id}" is already running (pane ${w.paneId} is alive).`);return}await resumeAgent(w)}function buildResumeParams(agent,template){let agentName=agent.role??agent.id,provider=template?.provider??agent.provider??"claude",team=template?.team??agent.team??"genie";return{provider,team,role:agentName,skill:template?.skill??agent.skill,extraArgs:template?.extraArgs,resume:agent.claudeSessionId,name:`${team}-${agentName}`}}function formatGroupStatus(name,group,allGroups){let detail=group.status;if(group.completedAt)detail+=` (completed at ${group.completedAt})`;else if(group.startedAt)detail+=` (started at ${group.startedAt})`;if(group.status==="blocked"&&group.dependsOn.length>0){let pending=group.dependsOn.filter((dep)=>allGroups[dep]?.status!=="done");if(pending.length>0)detail+=` (depends on ${pending.join(", ")})`}return`Group ${name}: ${detail}`}async function buildResumeContext(agent){if(agent.role==="team-lead"&&agent.wishSlug)try{let state2=await(await Promise.resolve().then(() => (init_wish_state(),exports_wish_state))).getState(agent.wishSlug,agent.repoPath);if(state2){let groupLines=Object.entries(state2.groups).map(([name,group])=>formatGroupStatus(name,group,state2.groups));return["You were resumed after a crash. Here's where you left off:",`Wish: ${state2.wish}`,"",...groupLines,"",`Continue from where you left off. Run \`genie status ${state2.wish}\` to verify, then dispatch the next wave.`].join(`
|
|
534
534
|
`)}}catch{}if(agent.team)return"You were resumed. Check your team's current state with `genie status`.";return}async function buildFullResumeParams(agent,template){let params=buildResumeParams(agent,template),resumeContext=await buildResumeContext(agent);if(resumeContext)params.initialPrompt=resumeContext;if(agent.nativeTeamEnabled){let nativeResult=await resolveNativeTeam(params.team,agent.repoPath,{provider:params.provider,role:params.role,color:agent.nativeColor});if(nativeResult.nativeTeam)params.nativeTeam=nativeResult.nativeTeam}return params}async function resumeAgent(agent){let template=(await listTemplates()).find((t)=>t.id===(agent.role??agent.id));await update(agent.id,{resumeAttempts:0});let params=await buildFullResumeParams(agent,template),validated=validateSpawnParams(params),launch=buildLaunchCommand(validated),fullCommand=prependEnvVars(launch.command,launch.env),now=new Date().toISOString();if(!process.env.TMUX)console.error("Error: resume requires tmux. Start a tmux session first."),process.exit(1);let ctx={workerId:agent.id,validated,launch,layoutMode:resolveLayoutMode(void 0),fullCommand,agentName:agent.role??agent.id,spawnColor:agent.nativeColor??"blue",parentSessionId:agent.parentSessionId??`genie-${params.team}`,claudeSessionId:agent.claudeSessionId,otelRelayActive:!1,now,transport:"tmux",extraArgs:template?.extraArgs,cwd:template?.cwd??agent.repoPath,spawnIntoCurrentWindow:!1,autoResume:agent.autoResume},teamWindow=await resolveSpawnTeamWindow(validated.team,ctx.cwd),paneId;try{paneId=createTmuxPane(ctx,teamWindow)}catch(err){console.error(`Failed to create tmux pane: ${err instanceof Error?err.message:"unknown error"}`),process.exit(1)}if(await applySpawnLayout(ctx,teamWindow),await update(agent.id,{paneId,state:"spawning",startedAt:now,lastStateChange:now,suspendedAt:void 0,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId,window:teamWindow?.windowName}),await notifySpawnJoin(ctx,paneId),await injectResumeContext(ctx.cwd??agent.repoPath??process.cwd(),agent.id,agent.role??agent.id,params.team),ctx.spawnColor&&paneId!=="inline")await applyPaneColor(paneId,ctx.spawnColor,teamWindow?.windowId);if(console.log(`Agent "${agent.id}" resumed.`),console.log(` Session: ${agent.claudeSessionId}`),console.log(` Pane: ${paneId}`),teamWindow)console.log(` Window: ${teamWindow.windowName} (${teamWindow.windowId})`)}async function buildWorkerStatusMap(workers){let statusMap=new Map;for(let w of workers){let name=w.role||w.id;if(await isPaneAlive(w.paneId))statusMap.set(name,{state:w.state,team:w.team||"-"});else if(w.state==="suspended"||w.state==="error"){let attempts=w.resumeAttempts??0,max=w.maxResumeAttempts??3,autoStr=w.autoResume===!1?"off":"on";statusMap.set(name,{state:`${w.state} (${attempts}/${max} resumes, auto-resume: ${autoStr})`,team:w.team||"-",resumeAttempts:attempts,maxResumeAttempts:max,autoResume:w.autoResume!==!1})}}return statusMap}async function handleLsCommand(options){let dirEntries=await ls(),workers=await list(),statusMap=await buildWorkerStatusMap(workers),entries=[];for(let entry of dirEntries){let running=statusMap.get(entry.name);entries.push({name:entry.name,dir:entry.dir||"-",status:running?running.state:"offline",team:running?.team||"-",model:entry.model||"-",resumeAttempts:running?.resumeAttempts,maxResumeAttempts:running?.maxResumeAttempts,autoResume:running?.autoResume}),statusMap.delete(entry.name)}for(let[name,info]of statusMap)entries.push({name,dir:"(built-in)",status:info.state,team:info.team,model:"-",resumeAttempts:info.resumeAttempts,maxResumeAttempts:info.maxResumeAttempts,autoResume:info.autoResume});if(options.json){console.log(JSON.stringify(entries,null,2));return}if(entries.length===0){console.log("No agents registered. Use `genie dir add <name> --dir <path>` to register one.");return}console.log(""),console.log(formatLsRow("NAME","DIR","STATUS","TEAM","MODEL")),console.log("-".repeat(106));for(let e of entries)console.log(formatLsRow(e.name,e.dir,e.status,e.team,e.model));console.log("")}function formatLsRow(name,dir,status,team,model){return`${name.padEnd(20).substring(0,20)}${dir.padEnd(30).substring(0,30)}${status.padEnd(44).substring(0,44)}${team.padEnd(12).substring(0,12)}${model}`}var init_agents=__esm(()=>{init_agent_directory();init_agent_registry();init_builtin_agents();init_claude_native_teams();init_codex_config();init_protocol_router_spawn();init_provider_adapters();init_spawn_command();init_team_manager();init_tmux();init_tmux()});function parseDuration(input){let match=input.trim().match(DURATION_RE);if(!match)throw Error(`Invalid duration: "${input}". Expected format: 10m, 2h, 24h, 1d`);let value=Number.parseFloat(match[1]),unit=match[2].toLowerCase(),ms=value*{s:1000,sec:1000,m:60000,min:60000,h:3600000,hr:3600000,d:86400000,day:86400000}[unit];if(ms<=0)throw Error(`Duration must be positive: "${input}"`);return ms}function expandRange(range,step,min,max){if(step===0)throw Error("Cron step value cannot be 0");if(range==="*"){let out=[];for(let i2=min;i2<=max;i2+=step)out.push(i2);return out}if(range.includes("-")){let[start,end]=range.split("-").map(Number),out=[];for(let i2=start;i2<=end;i2+=step)out.push(i2);return out}return[Number.parseInt(range,10)]}function parseCronField(field,min,max){let values2=new Set;for(let part of field.split(",")){let stepMatch=part.match(/^(.+)\/(\d+)$/),step=stepMatch?Number.parseInt(stepMatch[2],10):1,range=stepMatch?stepMatch[1]:part;for(let v of expandRange(range,step,min,max))values2.add(v)}return[...values2].sort((a,b2)=>a-b2)}function getTimeParts(date,tz){if(!tz)return{month:date.getMonth()+1,dom:date.getDate(),dow:date.getDay(),hour:date.getHours(),minute:date.getMinutes()};let parts=new Intl.DateTimeFormat("en-US",{timeZone:tz,year:"numeric",month:"numeric",day:"numeric",hour:"numeric",minute:"numeric",weekday:"short",hour12:!1}).formatToParts(date),get3=(type2)=>Number(parts.find((p)=>p.type===type2)?.value??0),dayMap={Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6},weekday=parts.find((p)=>p.type==="weekday")?.value??"Sun";return{month:get3("month"),dom:get3("day"),dow:dayMap[weekday]??0,hour:get3("hour")===24?0:get3("hour"),minute:get3("minute")}}function parseOpts(afterOrOpts){if(afterOrOpts instanceof Date)return{after:afterOrOpts};if(afterOrOpts)return{after:afterOrOpts.after,timezone:afterOrOpts.timezone};return{}}function advanceToNextDay(candidate,tz){candidate.setTime(candidate.getTime()+86400000);let tp=getTimeParts(candidate,tz);candidate.setTime(candidate.getTime()-tp.hour*3600000-tp.minute*60000)}function parseCronExpr(cronExpr){let parts=cronExpr.trim().split(/\s+/);if(parts.length<5)throw Error(`Invalid cron expression: "${cronExpr}"`);let[minField,hourField,domField,monthField,dowField]=parts;return{minutes:parseCronField(minField,0,59),hours:parseCronField(hourField,0,23),doms:parseCronField(domField,1,31),months:parseCronField(monthField,1,12),dows:parseCronField(dowField,0,6),domRestricted:domField!=="*",dowRestricted:dowField!=="*"}}function computeNextCronDue(cronExpr,afterOrOpts){let{after,timezone}=parseOpts(afterOrOpts),cron=parseCronExpr(cronExpr),candidate=new Date((after??new Date).getTime());candidate.setSeconds(0,0),candidate.setTime(candidate.getTime()+60000);let limit=new Date(candidate.getTime()+31622400000);while(candidate<=limit){let tp=getTimeParts(candidate,timezone);if(!cron.months.includes(tp.month)){advanceToNextDay(candidate,timezone);continue}if(!(cron.domRestricted&&cron.dowRestricted?cron.doms.includes(tp.dom)||cron.dows.includes(tp.dow):cron.doms.includes(tp.dom)&&cron.dows.includes(tp.dow))){advanceToNextDay(candidate,timezone);continue}if(!cron.hours.includes(tp.hour)){candidate.setTime(candidate.getTime()+3600000-tp.minute*60000);continue}if(cron.minutes.includes(tp.minute))return candidate;candidate.setTime(candidate.getTime()+60000)}throw Error(`No next cron occurrence found for "${cronExpr}" within 366 days`)}var DURATION_RE;var init_cron=__esm(()=>{DURATION_RE=/^(\d+(?:\.\d+)?)\s*(s|sec|m|min|h|hr|d|day)s?$/i});import{createHash as createHash2}from"crypto";function parseRoutingHeader(text){if(!text)return null;let match=text.split(`
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260325.
|
|
3
|
+
"version": "4.260325.7",
|
|
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"
|
|
@@ -98,7 +98,9 @@ async function spawnPaneInSession(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const splitTarget = teamWindow ? `-t '${teamWindow.windowId}'` : '';
|
|
101
|
-
|
|
101
|
+
// Wrap fullCommand in shell quotes so it survives the outer-shell → tmux → inner-shell pipeline.
|
|
102
|
+
const escapedCmd = fullCommand.replace(/'/g, "'\\''");
|
|
103
|
+
const { stdout } = await execAsync(`tmux split-window -d ${splitTarget} -P -F '#{pane_id}' '${escapedCmd}'`);
|
|
102
104
|
const paneId = stdout.trim();
|
|
103
105
|
|
|
104
106
|
let layoutTarget = `${session}:${teamWindow?.windowName ?? ''}`;
|
|
@@ -240,6 +240,32 @@ describe('buildClaudeCommand', () => {
|
|
|
240
240
|
expect(result.env).toBeDefined();
|
|
241
241
|
expect(result.env!.GENIE_WORKER).toBe('1');
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
it('initialPrompt with quotes and newlines survives tmux split-window re-quoting (#776)', () => {
|
|
245
|
+
const prompt =
|
|
246
|
+
'Execute Group 1 of wish "db-cleanup".\n\nWhen done:\n1. Run: genie done db-cleanup#1\n2. Run: genie send \'Group 1 complete.\' --to team-lead';
|
|
247
|
+
const result = buildClaudeCommand({
|
|
248
|
+
provider: 'claude',
|
|
249
|
+
team: 'work',
|
|
250
|
+
role: 'engineer-1',
|
|
251
|
+
initialPrompt: prompt,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// The command contains the prompt as a shell-escaped positional arg
|
|
255
|
+
expect(result.command).toContain('claude');
|
|
256
|
+
expect(result.command).toContain('engineer-1');
|
|
257
|
+
|
|
258
|
+
// Simulate the tmux split-window re-quoting fix:
|
|
259
|
+
// fullCommand is re-wrapped in single quotes for the outer shell → tmux → inner shell pipeline.
|
|
260
|
+
const fullCommand = result.command;
|
|
261
|
+
const reQuoted = fullCommand.replace(/'/g, "'\\''");
|
|
262
|
+
|
|
263
|
+
// The re-quoted command round-trips through sh -c back to the original fullCommand.
|
|
264
|
+
// This proves the outer shell → tmux → inner shell pipeline preserves the command.
|
|
265
|
+
const { execSync } = require('node:child_process');
|
|
266
|
+
const roundTripped = execSync(`printf '%s' '${reQuoted}'`, { encoding: 'utf-8' });
|
|
267
|
+
expect(roundTripped).toBe(fullCommand);
|
|
268
|
+
});
|
|
243
269
|
});
|
|
244
270
|
|
|
245
271
|
// ============================================================================
|
|
@@ -633,7 +633,11 @@ function createTmuxPane(ctx: SpawnCtx, teamWindow: TeamWindowInfo | null): strin
|
|
|
633
633
|
|
|
634
634
|
const splitTarget = teamWindow ? `-t '${teamWindow.windowId}'` : '';
|
|
635
635
|
const cwdFlag = ctx.cwd ? `-c '${ctx.cwd}'` : '';
|
|
636
|
-
|
|
636
|
+
// Wrap fullCommand in shell quotes so it survives the outer-shell → tmux → inner-shell pipeline.
|
|
637
|
+
// Without this, single quotes from escapeShellArg (e.g. around the initialPrompt) are consumed
|
|
638
|
+
// by the outer shell, and tmux's inner shell sees unquoted args — splitting multi-word prompts.
|
|
639
|
+
const escapedCmd = ctx.fullCommand.replace(/'/g, "'\\''");
|
|
640
|
+
const splitCmd = `tmux split-window -d ${splitTarget} ${cwdFlag} -P -F '#{pane_id}' '${escapedCmd}'`;
|
|
637
641
|
return execSync(splitCmd, { encoding: 'utf-8' }).trim();
|
|
638
642
|
}
|
|
639
643
|
|