@automagik/genie 4.260409.5 → 4.260409.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "genie",
13
- "version": "4.260409.5",
13
+ "version": "4.260409.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
@@ -1534,8 +1534,9 @@ exec ${fullCommand}
1534
1534
  LEFT JOIN executors e ON e.id = s.executor_id
1535
1535
  WHERE s.id = ${resumeSessionId} OR s.claude_session_id = ${resumeSessionId}
1536
1536
  ORDER BY s.started_at DESC LIMIT 1
1537
- `,[]);if(byPgId&&byPgId.length>0){if(dbSessionId=byPgId[0].id,turnIndex=byPgId[0].total_turns??0,byPgId[0].csid)resolvedClaudeSessionId=byPgId[0].csid;await safePgCall("reopen-session",(sql)=>sql`UPDATE sessions SET status = 'active', updated_at = now() WHERE id = ${dbSessionId}`,void 0)}if(runtimeExtraOptions)runtimeExtraOptions.resume=resolvedClaudeSessionId}if(!dbSessionId)dbSessionId=await startSession2(safePgCall,spawnContext.executorId,void 0,ctx.agentIdentityId??null,ctx.validated.team,ctx.validated.role);if(dbSessionId)await recordTurn2(safePgCall,dbSessionId,turnIndex++,"user",prompt2);let streaming=streamOpts?.stream??!1,extraOptions={...streaming&&{includePartialMessages:!0},...runtimeExtraOptions},hasExtraOptions=Object.keys(extraOptions).length>0,{messages:messages2}=sdkProvider.runQuery(spawnContext,prompt2,permConfig,hasExtraOptions?extraOptions:void 0,sdkConfig);if(ctx.executorId)await updateExecutorState(ctx.executorId,"running").catch(()=>{});let resultText="",claudeSessionId,toolCalls=[],pendingToolName="",pendingToolInput=null;if(streaming){let{formatSdkMessage:formatSdkMessage2}=await Promise.resolve().then(() => exports_claude_sdk_stream),format=streamOpts?.streamFormat??"text";try{for await(let message of messages2){let formatted=formatSdkMessage2(message,format);if(formatted!==null){if(process.stdout.write(formatted),format==="json")process.stdout.write(`
1538
- `)}if(message.type==="system"&&message.session_id)claudeSessionId=message.session_id;if(message.type==="assistant"&&message.message)for(let block of message.message.content){if(block.type==="tool_use")pendingToolName=block.name??"",pendingToolInput=block.input;if(block.type==="text"&&block.text)resultText+=block.text}if(message.type==="tool_result"){let msg=message;toolCalls.push({name:pendingToolName,input:pendingToolInput,output:typeof msg.content==="string"?msg.content.slice(0,500):"",is_error:!!msg.is_error}),pendingToolName="",pendingToolInput=null}if(message.type==="result"&&message.subtype==="success"){if(message.result&&!resultText)resultText=message.result;if(message.session_id)claudeSessionId=message.session_id}}}catch(err){if(!(err instanceof Error&&err.name==="AbortError"))console.error(`SDK query error: ${err instanceof Error?err.message:err}`)}}else try{for await(let message of messages2){if(message.type==="assistant"&&message.message)for(let block of message.message.content){if(block.type==="tool_use")pendingToolName=block.name??"",pendingToolInput=block.input;if(block.type==="text"&&block.text)process.stdout.write(block.text),resultText+=block.text}if(message.type==="tool_result"){let msg=message;toolCalls.push({name:pendingToolName,input:pendingToolInput,output:typeof msg.content==="string"?msg.content.slice(0,500):"",is_error:!!msg.is_error}),pendingToolName="",pendingToolInput=null}if(message.type==="result"&&message.subtype==="success"){if(message.result){if(console.log(message.result),!resultText)resultText=message.result}if(message.session_id)claudeSessionId=message.session_id}}}catch(err){if(!(err instanceof Error&&err.name==="AbortError"))console.error(`SDK query error: ${err instanceof Error?err.message:err}`)}if(dbSessionId){if(resultText||toolCalls.length>0){let content=toolCalls.length>0?JSON.stringify({text:resultText,tools:toolCalls}):resultText;await recordTurn2(safePgCall,dbSessionId,turnIndex++,"assistant",content)}await updateTurnCount2(safePgCall,dbSessionId,turnIndex),await endSession2(safePgCall,dbSessionId,"completed")}if(claudeSessionId&&spawnContext.executorId)await safePgCall("update-claude-session-id",(sql)=>sql`UPDATE executors SET claude_session_id = ${claudeSessionId} WHERE id = ${spawnContext.executorId}`,void 0);if(claudeSessionId&&dbSessionId)await safePgCall("update-session-claude-id",(sql)=>sql`UPDATE sessions SET claude_session_id = ${claudeSessionId} WHERE id = ${dbSessionId}`,void 0)}async function launchSdkSpawn(ctx,permissionsConfig,streamOpts,sdkConfig,runtimeExtraOptions){if(ctx.agentIdentityId&&ctx.executorId)await createAndLinkExecutor2(ctx.agentIdentityId,"claude-sdk","process",{id:ctx.executorId,claudeSessionId:null,state:"spawning",repoPath:ctx.cwd});if(await registerSpawnWorker(ctx,"sdk"),ctx.executorId)process.env.GENIE_EXECUTOR_ID=ctx.executorId;if(console.log(`Agent "${ctx.workerId}" starting via Claude Agent SDK...`),console.log(` Provider: claude-sdk | Team: ${ctx.validated.team} | Role: ${ctx.validated.role??"-"}`),ctx.executorId)console.log(` Executor: ${ctx.executorId}`);console.log("");let{resolvePermissionConfig:resolvePermissionConfig2}=await Promise.resolve().then(() => (init_claude_sdk_permissions(),exports_claude_sdk_permissions)),permConfig=resolvePermissionConfig2(permissionsConfig);if(await runSdkQuery(ctx,permConfig,streamOpts,sdkConfig,runtimeExtraOptions),ctx.executorId)await updateExecutorState(ctx.executorId,"done").catch(()=>{});return await unregister(ctx.workerId),console.log(`
1537
+ `,[]);if(byPgId&&byPgId.length>0){if(dbSessionId=byPgId[0].id,turnIndex=byPgId[0].total_turns??0,byPgId[0].csid)resolvedClaudeSessionId=byPgId[0].csid;await safePgCall("reopen-session",(sql)=>sql`UPDATE sessions SET status = 'active', updated_at = now() WHERE id = ${dbSessionId}`,void 0)}if(runtimeExtraOptions)runtimeExtraOptions.resume=resolvedClaudeSessionId}if(!dbSessionId)dbSessionId=await startSession2(safePgCall,spawnContext.executorId,void 0,ctx.agentIdentityId??null,ctx.validated.team,ctx.validated.role);if(dbSessionId)await recordTurn2(safePgCall,dbSessionId,turnIndex++,"user",prompt2);let streaming=streamOpts?.stream??!1;if(dbSessionId&&streaming){let fmt=streamOpts?.streamFormat??"text";if(fmt==="ndjson"||fmt==="json")process.stdout.write(`${JSON.stringify({type:"genie_session",session_id:dbSessionId})}
1538
+ `)}let extraOptions={...streaming&&{includePartialMessages:!0},...runtimeExtraOptions},hasExtraOptions=Object.keys(extraOptions).length>0,{messages:messages2}=sdkProvider.runQuery(spawnContext,prompt2,permConfig,hasExtraOptions?extraOptions:void 0,sdkConfig);if(ctx.executorId)await updateExecutorState(ctx.executorId,"running").catch(()=>{});let claudeSessionId,toolNameById=new Map,record=async(role,content,toolName)=>{if(!dbSessionId)return;await recordTurn2(safePgCall,dbSessionId,turnIndex++,role,content,toolName)},processMessage=async(message)=>{if(message.type==="system"&&message.session_id)claudeSessionId=message.session_id;if(message.type==="assistant"&&message.message)for(let block of message.message.content){if(block.type==="tool_use"){let b2=block,name=String(b2.name??""),id=String(b2.id??"");if(id)toolNameById.set(id,name);await record("tool_input",JSON.stringify(b2.input??{}).slice(0,500),name)}if(block.type==="text"&&block.text)await record("assistant",block.text)}if(message.type==="user"&&message.message?.content&&Array.isArray(message.message.content))for(let block of message.message.content){let b2=block;if(b2.type==="tool_result"){let toolId=String(b2.tool_use_id??""),toolName=toolNameById.get(toolId)??"",output=typeof b2.content==="string"?b2.content.slice(0,500):"";await record("tool_output",output,toolName)}}if(message.type==="result"&&message.subtype==="success"){if(message.session_id)claudeSessionId=message.session_id}};if(streaming){let{formatSdkMessage:formatSdkMessage2}=await Promise.resolve().then(() => exports_claude_sdk_stream),format=streamOpts?.streamFormat??"text";try{for await(let message of messages2){let formatted=formatSdkMessage2(message,format);if(formatted!==null){if(process.stdout.write(formatted),format==="json")process.stdout.write(`
1539
+ `)}await processMessage(message)}}catch(err){if(!(err instanceof Error&&err.name==="AbortError"))console.error(`SDK query error: ${err instanceof Error?err.message:err}`)}}else try{for await(let message of messages2){if(message.type==="assistant"&&message.message){for(let block of message.message.content)if(block.type==="text"&&block.text)process.stdout.write(block.text)}if(message.type==="result"&&message.subtype==="success"&&message.result)console.log(message.result);await processMessage(message)}}catch(err){if(!(err instanceof Error&&err.name==="AbortError"))console.error(`SDK query error: ${err instanceof Error?err.message:err}`)}if(dbSessionId)await updateTurnCount2(safePgCall,dbSessionId,turnIndex),await endSession2(safePgCall,dbSessionId,"completed");if(claudeSessionId&&spawnContext.executorId){let csId=claudeSessionId;await safePgCall("update-claude-session-id",(sql)=>sql`UPDATE executors SET claude_session_id = ${csId} WHERE id = ${spawnContext.executorId}`,void 0)}if(claudeSessionId&&dbSessionId){let csId=claudeSessionId,sessId=dbSessionId;await safePgCall("update-session-claude-id",(sql)=>sql`UPDATE sessions SET claude_session_id = ${csId} WHERE id = ${sessId}`,void 0)}}async function launchSdkSpawn(ctx,permissionsConfig,streamOpts,sdkConfig,runtimeExtraOptions){if(ctx.agentIdentityId&&ctx.executorId)await createAndLinkExecutor2(ctx.agentIdentityId,"claude-sdk","process",{id:ctx.executorId,claudeSessionId:null,state:"spawning",repoPath:ctx.cwd});if(await registerSpawnWorker(ctx,"sdk"),ctx.executorId)process.env.GENIE_EXECUTOR_ID=ctx.executorId;if(console.log(`Agent "${ctx.workerId}" starting via Claude Agent SDK...`),console.log(` Provider: claude-sdk | Team: ${ctx.validated.team} | Role: ${ctx.validated.role??"-"}`),ctx.executorId)console.log(` Executor: ${ctx.executorId}`);console.log("");let{resolvePermissionConfig:resolvePermissionConfig2}=await Promise.resolve().then(() => (init_claude_sdk_permissions(),exports_claude_sdk_permissions)),permConfig=resolvePermissionConfig2(permissionsConfig);if(await runSdkQuery(ctx,permConfig,streamOpts,sdkConfig,runtimeExtraOptions),ctx.executorId)await updateExecutorState(ctx.executorId,"done").catch(()=>{});return await unregister(ctx.workerId),console.log(`
1539
1540
  Agent "${ctx.workerId}" SDK session ended.`),ctx.workerId}async function launchInlineSpawn(ctx){let nt=ctx.validated.nativeTeam,paneId="inline";if(ctx.agentIdentityId&&ctx.executorId)await createAndLinkExecutor2(ctx.agentIdentityId,ctx.validated.provider,resolveExecutorTransport2(ctx.validated.provider,"inline"),{id:ctx.executorId,claudeSessionId:ctx.claudeSessionId??null,state:"spawning",repoPath:ctx.cwd});let 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:spawnSync3}=__require("child_process"),envVars={...process.env,...ctx.launch.env??{}},result2=spawnSync3("sh",["-c",ctx.launch.command],{env:envVars,stdio:"inherit"});if(ctx.agentIdentityId&&ctx.executorId)await updateExecutorState(ctx.executorId,"done").catch(()=>{});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(()=>{});return console.log(`
1540
1541
  Agent "${ctx.workerId}" session ended.`),process.exit(result2.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 findDeadResumable(team,role){let candidate=(await list()).find((w)=>w.role===role&&w.team===team&&w.claudeSessionId&&w.provider==="claude");if(!candidate)return null;return await isPaneAlive(candidate.paneId)?null:candidate}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)&&w.session){if(await getPaneSession(w.paneId)!==w.session){await unregister(w.id);continue}console.error(`Error: Worker with role "${role}" already exists in team "${team}" (state: ${w.state}, pane: ${w.paneId})
1541
1542
  Use a different --role name for a second worker, e.g.: --role ${role}-2`),process.exit(1)}await unregister(w.id)}}async function getPaneSession(paneId){try{return(await executeTmux2(`display-message -t '${paneId}' -p '#{session_name}'`)).trim()||null}catch{return null}}async function resolveNativeTeam(team,_repoPath,options){let parentSessionId=(await getTeam(team))?.nativeTeamParentSessionId;if(!parentSessionId)parentSessionId=await discoverClaudeParentSessionId()??`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);let repoPath=resolveAgentWorkingDir(entry,options.cwd),model=options.model;if(!model){let ctx=buildSpawnResolveContext(name,entry);model=resolveField(entry,"model",ctx)}return{entry,repoPath,identityPath,model}}function buildSpawnResolveContext(agentName,_entry){let ctx={};try{let ws=findWorkspace();if(ws){let wsConfig=getWorkspaceConfig(ws.root);ctx.workspaceDefaults=wsConfig.agents?.defaults}}catch{}if(agentName.includes("/")){let parentName=agentName.split("/")[0];try{let{readFileSync:readFileSync16,existsSync:existsSync26}=__require("fs"),{join:join31}=__require("path"),ws=findWorkspace();if(ws){let parentAgentsMd=join31(ws.root,"agents",parentName,"AGENTS.md");if(existsSync26(parentAgentsMd)){let{parseFrontmatter:parseFrontmatter2}=(init_frontmatter(),__toCommonJS(exports_frontmatter)),parentFm=parseFrontmatter2(readFileSync16(parentAgentsMd,"utf-8"));ctx.parent={name:parentName,fields:parentFm}}}}catch{}}return ctx}function resolveAgentWorkingDir(entry,explicitCwd){if(explicitCwd)return explicitCwd;if(entry.dir)return entry.dir;let repo=entry.repo;if(repo&&__require("fs").existsSync(repo))return repo;return process.cwd()}async function buildSpawnParams2(name,team,options,agent){let resolvedProvider=options.provider??agent.entry.provider??"claude",params={provider:resolvedProvider,team,role:name,skill:options.skill,extraArgs:options.extraArgs,model:agent.model,systemPromptFile:agent.identityPath??void 0,promptMode:agent.entry.promptMode,initialPrompt:options.prompt??options.initialPrompt,newWindow:options.newWindow,windowTarget:options.window},{parentSessionId,spawnColor,nativeTeam}=await resolveNativeTeam(team,agent.repoPath,{...options,provider:resolvedProvider,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();if(params.provider==="claude"){if(await startOtelReceiver())params.otelPort=getOtelPort(),params.otelLogPrompts=!0}return{params,parentSessionId,spawnColor}}async function maybeStartOtelRelay(nt,validated,insideTmux){if(!nt?.enabled&&validated.provider==="codex"&&insideTmux)return ensureCodexOtelConfig(),await ensureOtelRelay(validated.team);return!1}function buildSdkRuntimeExtra(options){let extra={};if(options.sdkMaxTurns!=null)extra.maxTurns=options.sdkMaxTurns;if(options.sdkMaxBudget!=null)extra.maxBudgetUsd=options.sdkMaxBudget;if(options.sdkEffort)extra.effort=options.sdkEffort;if(options.sdkResume)extra.resume=options.sdkResume;return Object.keys(extra).length>0?extra:void 0}async function dispatchSpawn(ctx,validated,options,agent,insideTmux){if(validated.provider==="claude-sdk"){let streamFormat=options.streamFormat??"text",streamOpts=options.stream||options.sdkStream?{stream:!0,streamFormat}:void 0;return await launchSdkSpawn(ctx,agent.entry.permissions,streamOpts,agent.entry.sdk,buildSdkRuntimeExtra(options))}if(insideTmux)return await launchTmuxSpawn(ctx);return await launchInlineSpawn(ctx)}async function resolveTeamAndResume(effectiveRole,options){let teamWasExplicit=Boolean(options.team),team=options.team||await discoverTeamName();if(!team)return console.error("Error: --team is required (or set GENIE_TEAM, or run inside a genie session)"),process.exit(1);let deadResumable=await findDeadResumable(team,effectiveRole);if(deadResumable)return console.log(`Resuming existing session for "${effectiveRole}" (session: ${deadResumable.claudeSessionId?.slice(0,8)}...)`),await resumeAgent(deadResumable),{team,teamWasExplicit,resumed:deadResumable.id};return await rejectDuplicateRole(team,effectiveRole),{team,teamWasExplicit}}async function handleWorkerSpawn(name,options){let effectiveRole=options.role??name,agent=await resolveAgentForSpawn(name,options),{team,teamWasExplicit,resumed}=await resolveTeamAndResume(effectiveRole,options);if(resumed)return resumed;let teamConfig=await getTeam(team);if(teamConfig?.worktreePath&&!agent.entry?.dir)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||options.session),nt=validated.nativeTeam,now=new Date().toISOString(),agentName=nt?.agentName??effectiveRole,agentIdentity=await findOrCreateAgent(agentName,team,effectiveRole);await terminateActiveExecutorWithCleanup(agentIdentity.id);let executorId=crypto.randomUUID(),otelRelayActive=await maybeStartOtelRelay(nt,validated,insideTmux),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,agentIdentityId:agentIdentity.id,executorId};return recordAuditEvent("worker",workerId,"spawn",getActor(),{name,team:validated.team,provider:validated.provider}).catch(()=>{}),await dispatchSpawn(ctx,validated,options,agent,insideTmux)}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:execSync9}=__require("child_process"),currentPane=execSync9(genieTmuxCmd("display-message -p '#{pane_id}'"),{encoding:"utf-8"}).trim();if(w.paneId&&/^(%\d+|inline)$/.test(w.paneId)&&w.paneId!==currentPane)execSync9(genieTmuxCmd(`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:join31}=__require("path"),{homedir:homedir19}=__require("os"),{unlinkSync:unlinkSync7}=__require("fs"),relayDir=join31(homedir19(),".genie","relay");for(let suffix of["-pane","-meta"])try{unlinkSync7(join31(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 agent list` 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).`),recordAuditEvent("worker",w.id,"kill",getActor(),{name}).catch(()=>{})}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}`),recordAuditEvent("worker",w.id,"stop",getActor(),{name}).catch(()=>{})}else console.error(`Failed to stop agent "${w.id}".`),process.exit(1)}async function isResumeEligible(w){if(!w.claudeSessionId)return!1;if(w.state==="done")return!1;let paneAlive=await isPaneAlive(w.paneId);if((w.state==="suspended"||w.state==="error")&&!paneAlive)return!0;if(!paneAlive&&(w.state==="working"||w.state==="idle"||w.state==="spawning"))return!0;return!1}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)}async function buildResumeParams(agent,template){let agentName=agent.role??agent.id,provider=template?.provider??agent.provider??"claude",team=template?.team??agent.team??"genie",systemPromptFile,promptMode,dirEntry=await get2(agentName);if(dirEntry?.dir)systemPromptFile=loadIdentity(dirEntry)??void 0,promptMode=dirEntry.promptMode;return{provider,team,role:agentName,skill:template?.skill??agent.skill,extraArgs:template?.extraArgs,resume:agent.claudeSessionId,name:`${team}-${agentName}`,model:dirEntry?.model,systemPromptFile,promptMode}}function formatGroupStatus(name,group,allGroups){let detail=group.status;if(group.completedAt)detail+=` (completed at ${group.completedAt})`;else if(group.startedAt)detail+=` (started at ${group.startedAt})`;if(group.status==="blocked"&&group.dependsOn.length>0){let pending=group.dependsOn.filter((dep)=>allGroups[dep]?.status!=="done");if(pending.length>0)detail+=` (depends on ${pending.join(", ")})`}return`Group ${name}: ${detail}`}async function buildResumeContext(agent){if((agent.role==="team-lead"||agent.team&&agent.role===await resolveTeamLeaderName(agent.team))&&agent.wishSlug)try{let state=await(await Promise.resolve().then(() => (init_wish_state(),exports_wish_state))).getState(agent.wishSlug,agent.repoPath);if(state){let groupLines=Object.entries(state.groups).map(([name,group])=>formatGroupStatus(name,group,state.groups));return["You were resumed after a crash. Here's where you left off:",`Wish: ${state.wish}`,"",...groupLines,"",`Continue from where you left off. Run \`genie status ${state.wish}\` to verify, then dispatch the next wave.`].join(`
@@ -3070,7 +3071,7 @@ ${context}`}}}catch{return}}var DENY_PATTERNS=[{test:(cmd)=>/git\s+push\b/i.test
3070
3071
  No agents registered. Add one with: genie agent register <name> --dir <path>`),console.log(`Use --builtins to also see built-in roles and council members.
3071
3072
  `);return}if(entries.length>0)printRegisteredAgentsTable(entries);if(includeBuiltins)printBuiltinAgentsTable()}function registerAgentDirectory(parent){parent.command("directory [name]").alias("dir").description("List all agents or show single entry details from directory").option("--json","Output as JSON").option("--builtins","Include built-in roles and council members").option("--all","Include archived agents").action(async(name,options)=>{try{if(name==="sync"){let resolved=await resolve3("sync");if(resolved&&!resolved.builtin)await showEntry("sync",options.json);else await handleSync()}else if(name)await showEntry(name,options.json);else await listEntries(options.json,options.builtins,options.all)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}async function handleSync(){let{findWorkspace:findWorkspace2}=await Promise.resolve().then(() => (init_workspace(),exports_workspace)),ws=findWorkspace2();if(!ws)console.error("Not in a genie workspace. Run `genie init` first."),process.exit(1);console.log(`Syncing agents from ${ws.root}/agents/...`);let result2=await syncAgentDirectory(ws.root);printSyncResult(result2)}init_term_format();init_msg();var _taskService2;async function getTaskService2(){if(!_taskService2)_taskService2=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService2}function printConversation(conv,lastMsg){let name=conv.name??conv.id,type2=conv.type==="dm"?"DM":"Group",linked=conv.linkedEntity?` [${conv.linkedEntity}:${conv.linkedEntityId}]`:"",preview=lastMsg?truncate2(lastMsg.body,50):"(no messages)",time=lastMsg?formatTime(lastMsg.createdAt):"";if(console.log(` ${padRight(name,30)} ${padRight(type2,6)}${linked}`),lastMsg)console.log(` ${time} ${lastMsg.senderId}: ${preview}`);console.log("")}async function handleInbox2(agent,options){let ts3=await getTaskService2(),resolvedAgent=agent??await detectSenderIdentity(),actor={actorType:"local",actorId:resolvedAgent},conversations=await ts3.listConversations(actor);if(options.json){console.log(JSON.stringify(conversations,null,2));return}if(conversations.length===0){console.log(`No conversations for "${resolvedAgent}".`);return}console.log(""),console.log(`INBOX: ${resolvedAgent}`),console.log("\u2500".repeat(60));for(let conv of conversations){let messages2=await ts3.getMessages(conv.id,{limit:1}),lastMsg=messages2.length>0?messages2[messages2.length-1]:null;printConversation(conv,lastMsg)}}function registerAgentInbox(parent){let inbox2=parent.command("inbox").description("Inbox management \u2014 list messages or watch for new ones");inbox2.command("list [agent]",{isDefault:!0}).description("List conversations with recent messages").option("--json","Output as JSON").action(async(agent,options)=>{try{await handleInbox2(agent,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),inbox2.command("watch").description("Run inbox watcher in foreground (Ctrl+C to stop)").action(async()=>{let{checkInboxes:checkInboxes2,getInboxPollIntervalMs:getInboxPollIntervalMs2,startInboxWatcher:startInboxWatcher2,stopInboxWatcher:stopInboxWatcher2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log("Inbox watcher is disabled (GENIE_INBOX_POLL_MS=0)"),process.exit(0);console.log(`Inbox watcher starting (poll every ${pollMs/1000}s)`),console.log(`Press Ctrl+C to stop.
3072
3073
  `);let initial=await checkInboxes2();if(initial.length>0)console.log(`[inbox-watcher] Spawned team-leads for: ${initial.join(", ")}`);let handle=startInboxWatcher2({listTeamsWithUnreadInbox:(await Promise.resolve().then(() => (init_claude_native_teams(),exports_claude_native_teams))).listTeamsWithUnreadInbox,isTeamActive:async(teamName)=>{let{isTeamActive:isTeamActive2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn));return isTeamActive2(teamName)},isAgentAlive:async(agentName)=>{let{isAgentAlive:isAgentAlive2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn));return isAgentAlive2(agentName)},ensureTeamLead:async(teamName,workingDir)=>{let{ensureTeamLead:ensureTeamLead2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn)),result2=await ensureTeamLead2(teamName,workingDir);return console.log(`[inbox-watcher] Spawned team-lead for "${teamName}" in ${workingDir}`),result2},warn:(msg)=>console.log(msg)}),shutdown2=()=>{console.log(`
3073
- Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2),await new Promise(()=>{})})}init_agents();function registerAgentKill(parent){parent.command("kill <name>").description("Force kill an agent by name").action(async(name)=>{try{await handleWorkerKill(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agents();function registerAgentList(parent){parent.command("list").alias("ls").description("List registered agents with runtime status").option("--json","Output as JSON").option("--source <name>","Filter by executor metadata source (e.g. omni)").action(async(options)=>{try{await handleLsCommand(options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agent_registry();init_term_format();init_mailbox();init_runtime_events();init_db();function rowToMessage2(row){return{id:row.id,sender:row.sender,body:row.body,timestamp:row.created_at instanceof Date?row.created_at.toISOString():String(row.created_at)}}async function readMessages(repoPath,teamName,since){let sql=await getConnection();if(since)return(await sql`
3074
+ Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2),await new Promise(()=>{})})}init_agents();function registerAgentKill(parent){parent.command("kill <name>").description("Force kill an agent by name").action(async(name)=>{try{await handleWorkerKill(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agents();function registerAgentList(parent){parent.command("list").alias("ls").description("List registered agents with runtime status").option("--json","Output as JSON").option("--source <name>","Filter by executor metadata source (e.g. omni)").action(async(options)=>{try{await handleLsCommand(options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_agent_registry();init_term_format();init_db();init_mailbox();init_runtime_events();init_db();function rowToMessage2(row){return{id:row.id,sender:row.sender,body:row.body,timestamp:row.created_at instanceof Date?row.created_at.toISOString():String(row.created_at)}}async function readMessages(repoPath,teamName,since){let sql=await getConnection();if(since)return(await sql`
3074
3075
  SELECT * FROM team_chat
3075
3076
  WHERE team = ${teamName} AND repo_path = ${repoPath} AND created_at >= ${since}
3076
3077
  ORDER BY created_at ASC
@@ -3078,9 +3079,17 @@ Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.o
3078
3079
  SELECT * FROM team_chat
3079
3080
  WHERE team = ${teamName} AND repo_path = ${repoPath}
3080
3081
  ORDER BY created_at ASC
3081
- `).map(rowToMessage2)}function mailboxActorKeys(agent){let keys=[agent.id];if(agent.role&&agent.role!==agent.id)keys.push(agent.role);if(agent.customName&&!keys.includes(agent.customName))keys.push(agent.customName);return keys}function isSystemNoise(text){let trimmed=text.trimStart();return trimmed.startsWith("<command-name>")||trimmed.startsWith("<command-message>")||trimmed.startsWith("Base directory for this skill:")||trimmed.startsWith("<system-reminder>")||trimmed.startsWith("<local-command")}function transcriptToLogEvent(entry,agent,team){let kindMap={user:"user",assistant:"assistant",system:"system",tool_call:"tool_call",tool_result:"tool_result"},text=entry.text.trim();if(isSystemNoise(text))return null;if(!text)return null;return{timestamp:entry.timestamp,kind:kindMap[entry.role]??"assistant",agent,team,text,data:{role:entry.role,...entry.toolCall?{toolCall:entry.toolCall}:{},...entry.model?{model:entry.model}:{},...entry.usage?{usage:entry.usage}:{}},source:"provider"}}function inboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"in",peer:msg.from,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to,read:msg.read},source:"mailbox"}}function outboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"out",peer:msg.to,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to},source:"mailbox"}}function chatMessageToLogEvent(msg,team){return{timestamp:msg.timestamp,kind:"message",agent:msg.sender,team,text:msg.body,data:{chatId:msg.id,sender:msg.sender},source:"chat"}}function applyLogFilter(events,filter){if(!filter)return events;let result2=events;if(filter.since){let sinceMs=new Date(filter.since).getTime();result2=result2.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);result2=result2.filter((e)=>kinds.has(e.kind))}if(filter.last&&filter.last>0)result2=result2.slice(-filter.last);return result2}function sortByTimestamp(events){return events.sort((a,b2)=>new Date(a.timestamp).getTime()-new Date(b2.timestamp).getTime())}async function readAgentLog(agent,repoPath,filter){let{id:agentName,team}=agent,mailboxKeys=mailboxActorKeys(agent),[transcriptEntries,inboxMessages,outboxMessages,chatMessages]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,mailboxKeys),readOutbox(repoPath,mailboxKeys),team?readMessages(repoPath,team):Promise.resolve([])]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,team);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,team));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,team));if(team)for(let msg of chatMessages)events.push(chatMessageToLogEvent(msg,team));let sorted=sortByTimestamp(events);return applyLogFilter(sorted,filter)}async function readTeamLog(agents,repoPath,teamName,filter){let chatEvents=(await readMessages(repoPath,teamName)).map((msg)=>chatMessageToLogEvent(msg,teamName)),perAgentEvents=await Promise.all(agents.map(async(agent)=>{let agentName=agent.id,mailboxKeys=mailboxActorKeys(agent),[transcriptEntries,inboxMessages,outboxMessages]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,mailboxKeys),readOutbox(repoPath,mailboxKeys)]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,teamName);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,teamName));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,teamName));return events})),allEvents=[...chatEvents,...perAgentEvents.flat()],sorted=sortByTimestamp(allEvents);return applyLogFilter(sorted,filter)}async function followAgentLog(agent,repoPath,filter,onEvent){return startPgFollow([agent],repoPath,agent.team,filter,onEvent)}async function followTeamLog(agents,repoPath,teamName,filter,onEvent){return startPgFollow(agents,repoPath,teamName,filter,onEvent)}async function startPgFollow(agents,repoPath,team,filter,onEvent){let kindsFilter=filter?.kinds?new Set(filter.kinds):null,agentIds=new Set(agents.map((agent)=>agent.id)),seenKeys=new Set,eventKey=(e)=>`${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0,80)}`,matchesScope=(event)=>{if(team==="all")return!0;if(team&&event.team===team)return!0;return agentIds.has(event.agent)},handleRuntimeEvent=(event)=>{if(!event.timestamp||!event.kind)return;if(kindsFilter&&!kindsFilter.has(event.kind))return;if(!matchesScope(event))return;let key=eventKey(event);if(seenKeys.has(key))return;seenKeys.add(key),onEvent(event)},handle=await followRuntimeEvents({repoPath:team==="all"?void 0:repoPath,agentIds:team==="all"?void 0:[...agentIds],team:team&&team!=="all"?team:void 0,kinds:filter?.kinds,scopeMode:team&&team!=="all"?"any":"all"},handleRuntimeEvent,{pollIntervalMs:500});return{mode:"pg",stop:()=>handle.stop()}}async function readTranscriptSafe(agent){try{let{readTranscript:readTranscript2}=await Promise.resolve().then(() => exports_transcript);return await readTranscript2(agent)}catch{return[]}}function kindIcon(kind){switch(kind){case"user":return"U";case"assistant":return"A";case"message":return"M";case"state":return"S";case"tool_call":return"C";case"tool_result":return"R";case"system":return"*";default:return"?"}}function kindColor(kind){switch(kind){case"user":return"\x1B[33m";case"assistant":return"\x1B[36m";case"message":return"\x1B[35m";case"state":return"\x1B[35m";case"tool_call":return"\x1B[32m";case"tool_result":return"\x1B[90m";case"system":return"\x1B[34m";default:return"\x1B[0m"}}var RESET="\x1B[0m",DIM="\x1B[90m",BOLD="\x1B[1m";function summarizeToolCall2(event){let tc=event.data?.toolCall;if(!tc)return event.text;let input=tc.input;switch(tc.name){case"Read":case"Edit":case"Write":return`${tc.name} ${input.file_path??""}`;case"Bash":return`$ ${String(input.command??"").split(`
3082
+ `).map(rowToMessage2)}function mailboxActorKeys(agent){let keys=[agent.id];if(agent.role&&agent.role!==agent.id)keys.push(agent.role);if(agent.customName&&!keys.includes(agent.customName))keys.push(agent.customName);return keys}function isSystemNoise(text){let trimmed=text.trimStart();return trimmed.startsWith("<command-name>")||trimmed.startsWith("<command-message>")||trimmed.startsWith("Base directory for this skill:")||trimmed.startsWith("<system-reminder>")||trimmed.startsWith("<local-command")}function transcriptToLogEvent(entry,agent,team){let kindMap={user:"user",assistant:"assistant",system:"system",tool_call:"tool_call",tool_result:"tool_result"},text=entry.text.trim();if(isSystemNoise(text))return null;if(!text)return null;return{timestamp:entry.timestamp,kind:kindMap[entry.role]??"assistant",agent,team,text,data:{role:entry.role,...entry.toolCall?{toolCall:entry.toolCall}:{},...entry.model?{model:entry.model}:{},...entry.usage?{usage:entry.usage}:{}},source:"provider"}}function inboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"in",peer:msg.from,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to,read:msg.read},source:"mailbox"}}function outboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"out",peer:msg.to,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to},source:"mailbox"}}function chatMessageToLogEvent(msg,team){return{timestamp:msg.timestamp,kind:"message",agent:msg.sender,team,text:msg.body,data:{chatId:msg.id,sender:msg.sender},source:"chat"}}function applyLogFilter(events,filter){if(!filter)return events;let result2=events;if(filter.since){let sinceMs=new Date(filter.since).getTime();result2=result2.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);result2=result2.filter((e)=>kinds.has(e.kind))}if(filter.last&&filter.last>0)result2=result2.slice(-filter.last);return result2}function sortByTimestamp(events){return events.sort((a,b2)=>new Date(a.timestamp).getTime()-new Date(b2.timestamp).getTime())}var SDK_KIND_MAP={"sdk.user.message":"user","sdk.assistant.message":"assistant","sdk.tool.summary":"tool_call","sdk.system":"system","sdk.result.success":"system","sdk.hook.started":"system","sdk.hook.response":"system","sdk.rate_limit":"system"};function sdkAuditRowToLogEvent(row){let details=row.details??{};return{timestamp:typeof row.created_at==="string"?row.created_at:new Date(row.created_at).toISOString(),kind:SDK_KIND_MAP[row.event_type]??"system",agent:row.actor??"unknown",text:details.textPreview??details.summaryPreview??row.event_type,data:details,source:"sdk"}}async function readSdkAuditEvents(agentId,filter){try{if(!await isAvailable())return[];let sql=await getConnection(),conditions=["entity_type = 'sdk_message'","actor = $1"],values2=[agentId],paramIdx=2;if(filter?.since)conditions.push(`created_at >= $${paramIdx++}::timestamptz`),values2.push(filter.since);let where=`WHERE ${conditions.join(" AND ")}`,limit=filter?.last??500,rows=await sql.unsafe(`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
3083
+ FROM audit_events ${where}
3084
+ ORDER BY created_at ASC
3085
+ LIMIT ${limit}`,values2),events=[];for(let row of rows)if(SDK_KIND_MAP[row.event_type])events.push(sdkAuditRowToLogEvent(row));if(filter?.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);return events.filter((e)=>kinds.has(e.kind))}return events}catch{return[]}}async function readAgentLog(agent,repoPath,filter){let{id:agentName,team}=agent,mailboxKeys=mailboxActorKeys(agent),[transcriptEntries,inboxMessages,outboxMessages,chatMessages,sdkEvents]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,mailboxKeys),readOutbox(repoPath,mailboxKeys),team?readMessages(repoPath,team):Promise.resolve([]),readSdkAuditEvents(agentName,filter)]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,team);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,team));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,team));if(team)for(let msg of chatMessages)events.push(chatMessageToLogEvent(msg,team));events.push(...sdkEvents);let sorted=sortByTimestamp(events);return applyLogFilter(sorted,filter)}async function readTeamLog(agents,repoPath,teamName,filter){let chatEvents=(await readMessages(repoPath,teamName)).map((msg)=>chatMessageToLogEvent(msg,teamName)),perAgentEvents=await Promise.all(agents.map(async(agent)=>{let agentName=agent.id,mailboxKeys=mailboxActorKeys(agent),[transcriptEntries,inboxMessages,outboxMessages,sdkEvents]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,mailboxKeys),readOutbox(repoPath,mailboxKeys),readSdkAuditEvents(agentName,filter)]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,teamName);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,teamName));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,teamName));return events.push(...sdkEvents),events})),allEvents=[...chatEvents,...perAgentEvents.flat()],sorted=sortByTimestamp(allEvents);return applyLogFilter(sorted,filter)}async function followAgentLog(agent,repoPath,filter,onEvent){return startPgFollow([agent],repoPath,agent.team,filter,onEvent)}async function followTeamLog(agents,repoPath,teamName,filter,onEvent){return startPgFollow(agents,repoPath,teamName,filter,onEvent)}async function startPgFollow(agents,repoPath,team,filter,onEvent){let kindsFilter=filter?.kinds?new Set(filter.kinds):null,agentIds=new Set(agents.map((agent)=>agent.id)),seenKeys=new Set,eventKey=(e)=>`${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0,80)}`,dedupAndEmit=(event)=>{if(!event.timestamp||!event.kind)return;if(kindsFilter&&!kindsFilter.has(event.kind))return;let key=eventKey(event);if(seenKeys.has(key))return;seenKeys.add(key),onEvent(event)},matchesScope=(event)=>{if(team==="all")return!0;if(team&&event.team===team)return!0;return agentIds.has(event.agent)},handleRuntimeEvent=(event)=>{if(!matchesScope(event))return;dedupAndEmit(event)},handle=await followRuntimeEvents({repoPath:team==="all"?void 0:repoPath,agentIds:team==="all"?void 0:[...agentIds],team:team&&team!=="all"?team:void 0,kinds:filter?.kinds,scopeMode:team&&team!=="all"?"any":"all"},handleRuntimeEvent,{pollIntervalMs:500}),sdkPollActive=!0,sdkLastId=0;try{if(await isAvailable()){let sql=await getConnection(),[row]=await sql`SELECT COALESCE(MAX(id), 0) AS max_id FROM audit_events WHERE entity_type = 'sdk_message'`;sdkLastId=Number(row?.max_id??0)}}catch{}let drainSdkAuditEvents=async()=>{if(!await isAvailable())return;let sql=await getConnection(),agentList=[...agentIds],rows=await sql.unsafe(`SELECT id, entity_type, entity_id, event_type, actor, details, created_at
3086
+ FROM audit_events
3087
+ WHERE entity_type = 'sdk_message' AND id > $1
3088
+ AND actor = ANY($2)
3089
+ ORDER BY id ASC
3090
+ LIMIT 100`,[sdkLastId,agentList]);for(let row of rows){if(SDK_KIND_MAP[row.event_type])dedupAndEmit(sdkAuditRowToLogEvent(row));sdkLastId=Math.max(sdkLastId,Number(row.id))}};return(async()=>{while(sdkPollActive){try{await drainSdkAuditEvents()}catch{}await new Promise((resolve5)=>setTimeout(resolve5,500))}})(),{mode:"pg",stop:async()=>{sdkPollActive=!1,await handle.stop()}}}async function readTranscriptSafe(agent){try{let{readTranscript:readTranscript2}=await Promise.resolve().then(() => exports_transcript);return await readTranscript2(agent)}catch{return[]}}function kindIcon(kind){switch(kind){case"user":return"U";case"assistant":return"A";case"message":return"M";case"state":return"S";case"tool_call":return"C";case"tool_result":return"R";case"system":return"*";default:return"?"}}function kindColor(kind){switch(kind){case"user":return"\x1B[33m";case"assistant":return"\x1B[36m";case"message":return"\x1B[35m";case"state":return"\x1B[35m";case"tool_call":return"\x1B[32m";case"tool_result":return"\x1B[90m";case"system":return"\x1B[34m";default:return"\x1B[0m"}}var RESET="\x1B[0m",DIM="\x1B[90m",BOLD="\x1B[1m";function summarizeToolCall2(event){let tc=event.data?.toolCall;if(!tc)return event.text;let input=tc.input;switch(tc.name){case"Read":case"Edit":case"Write":return`${tc.name} ${input.file_path??""}`;case"Bash":return`$ ${String(input.command??"").split(`
3082
3091
  `)[0]}`;case"Grep":return`Grep "${input.pattern}" ${input.path??""}`;case"Glob":return`Glob ${input.pattern}`;case"Agent":return`Agent: ${input.description??""}`;case"SendMessage":return`SendMessage \u2192 ${input.to}: ${String(input.message??"").slice(0,80)}`;case"shell":case"exec_command":return`$ ${(Array.isArray(input.command)?input.command.join(" "):String(input.command??"")).split(`
3083
- `)[0]}`;case"web_search":return`Search: ${input.query??""}`;default:return`${tc.name}`}}function formatEventBlock(event){let time=formatTime(event.timestamp,{seconds:!0,fallback:"??:??:??"}),icon=kindIcon(event.kind),color2=kindColor(event.kind),agent=event.agent;if(event.direction==="in")agent=`${event.peer} \u2192 ${event.agent}`;else if(event.direction==="out")agent=`${event.agent} \u2192 ${event.peer}`;let header=`${DIM}${time}${RESET} ${color2}[${icon}]${RESET} ${BOLD}${agent}${RESET}`;if(event.kind==="tool_call"){let summary=summarizeToolCall2(event);return`${header} ${DIM}${summary}${RESET}`}if(event.kind==="tool_result"){let line=event.text.split(`
3092
+ `)[0]}`;case"web_search":return`Search: ${input.query??""}`;default:return`${tc.name}`}}function formatEventBlock(event){let time=formatTime(event.timestamp,{seconds:!0,fallback:"??:??:??"}),icon=kindIcon(event.kind),color2=kindColor(event.kind),agent=event.agent;if(event.direction==="in")agent=`${event.peer} \u2192 ${event.agent}`;else if(event.direction==="out")agent=`${event.agent} \u2192 ${event.peer}`;let sdkTag=event.source==="sdk"?` ${DIM}[SDK]${RESET}`:"",header=`${DIM}${time}${RESET} ${color2}[${icon}]${RESET} ${BOLD}${agent}${RESET}${sdkTag}`;if(event.kind==="tool_call"){let summary=summarizeToolCall2(event);return`${header} ${DIM}${summary}${RESET}`}if(event.kind==="tool_result"){let line=event.text.split(`
3084
3093
  `)[0].slice(0,120);return`${header} ${DIM}${line}${RESET}`}let text=event.text.trim();if(text.length<80&&!text.includes(`
3085
3094
  `))return`${header}
3086
3095
  ${text}`;let indented=text.split(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260409.5",
3
+ "version": "4.260409.7",
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.260409.5",
3
+ "version": "4.260409.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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260409.5",
3
+ "version": "4.260409.7",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared mock for `@anthropic-ai/claude-agent-sdk`.
3
+ *
4
+ * Bun's `mock.module()` is process-global and first-registration-wins. When
5
+ * multiple test files register competing mocks for the same module, whichever
6
+ * loads first locks the global cache, and other files' mocks become dead weight.
7
+ *
8
+ * This file provides a SINGLE `queryMock` instance used by both:
9
+ * - `src/__tests__/sdk-integration.test.ts`
10
+ * - `src/services/executors/__tests__/_sdk-mocks.ts`
11
+ *
12
+ * It ONLY mocks `@anthropic-ai/claude-agent-sdk` — no directory, registry, or
13
+ * other module mocks — so it's safe to import from any test file without
14
+ * polluting unrelated modules.
15
+ */
16
+
17
+ import { mock } from 'bun:test';
18
+
19
+ /** Default SDK query implementation — yields one assistant reply + success result with session_id. */
20
+ const defaultQueryImpl = () => {
21
+ const gen = (async function* () {
22
+ yield { type: 'assistant', message: { content: [{ type: 'text', text: 'reply' }] } };
23
+ yield { type: 'result', subtype: 'success', session_id: 'sdk-session-aaa' };
24
+ })();
25
+ return Object.assign(gen, {
26
+ interrupt: mock(),
27
+ setPermissionMode: mock(),
28
+ setModel: mock(),
29
+ return: mock(async () => ({ value: undefined, done: true })),
30
+ throw: mock(async () => ({ value: undefined, done: true })),
31
+ });
32
+ };
33
+
34
+ export const queryMock = mock(defaultQueryImpl);
35
+
36
+ export function resetQueryMock(): void {
37
+ queryMock.mockReset();
38
+ queryMock.mockImplementation(defaultQueryImpl);
39
+ }
40
+
41
+ // ============================================================================
42
+ // Register the SDK mock — this is the single source of truth for the process.
43
+ // ============================================================================
44
+
45
+ mock.module('@anthropic-ai/claude-agent-sdk', () => ({
46
+ query: queryMock,
47
+ createSdkMcpServer: mock((opts: any) => ({
48
+ type: 'sdk' as const,
49
+ name: opts.name,
50
+ instance: {},
51
+ })),
52
+ tool: mock((_name: string, _desc: string, _schema: any, handler: any) => ({
53
+ name: _name,
54
+ description: _desc,
55
+ inputSchema: _schema,
56
+ handler,
57
+ })),
58
+ }));
@@ -6,33 +6,15 @@
6
6
  * permission gate, frontmatter parsing, and config priority layering.
7
7
  */
8
8
 
9
- import { beforeEach, describe, expect, it, mock } from 'bun:test';
9
+ import { beforeEach, describe, expect, it, type mock } from 'bun:test';
10
10
  import type { SpawnContext } from '../lib/executor-types.js';
11
11
 
12
12
  // ============================================================================
13
- // Mock the SDK module before any provider imports
13
+ // Use the shared SDK mock eliminates process-global mock.module() race with
14
+ // executor tests. See _shared-sdk-query-mock.ts for details.
14
15
  // ============================================================================
15
16
 
16
- const mockQuery = mock(() => {
17
- const gen = (async function* () {
18
- yield { type: 'assistant', message: { content: [{ type: 'text', text: 'hello' }] } };
19
- })();
20
- return Object.assign(gen, {
21
- interrupt: mock(),
22
- setPermissionMode: mock(),
23
- setModel: mock(),
24
- return: mock(async () => ({ value: undefined, done: true })),
25
- throw: mock(async () => ({ value: undefined, done: true })),
26
- });
27
- });
28
-
29
- mock.module('@anthropic-ai/claude-agent-sdk', () => ({
30
- query: mockQuery,
31
- }));
32
-
33
- // No audit mocking — routeSdkMessage is fire-and-forget (.catch(() => {}))
34
- // and these tests never iterate the message stream, so audit is never invoked.
35
- // Removing audit mocks prevents spyOn leaks that corrupt audit.test.ts in the same bun process.
17
+ import { queryMock as mockQuery } from './_shared-sdk-query-mock.js';
36
18
 
37
19
  // ============================================================================
38
20
  // Dynamic imports (must come after mock.module)
@@ -73,7 +73,7 @@ export type RuntimeEventKind =
73
73
  | 'tool_result'
74
74
  | 'system'
75
75
  | 'qa';
76
- export type RuntimeEventSource = 'provider' | 'mailbox' | 'chat' | 'registry' | 'hook';
76
+ export type RuntimeEventSource = 'provider' | 'mailbox' | 'chat' | 'registry' | 'hook' | 'sdk';
77
77
  export type RuntimeEventDirection = 'in' | 'out';
78
78
 
79
79
  export interface RuntimeEvent {
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
10
10
  import type { Agent } from './agent-registry.js';
11
+ import { recordAuditEvent } from './audit.js';
11
12
  import { readOutbox, send } from './mailbox.js';
12
13
  import { publishRuntimeEvent } from './runtime-events.js';
13
14
  import { postMessage } from './team-chat.js';
@@ -23,6 +24,7 @@ import {
23
24
  outboxMessageToLogEvent,
24
25
  readAgentLog,
25
26
  readTeamLog,
27
+ sdkAuditRowToLogEvent,
26
28
  sortByTimestamp,
27
29
  transcriptToLogEvent,
28
30
  } from './unified-log.js';
@@ -593,3 +595,225 @@ describe.skipIf(!DB_AVAILABLE)('mailbox outbox', () => {
593
595
  expect(outbox[1].body).toBe('world');
594
596
  });
595
597
  });
598
+
599
+ // ============================================================================
600
+ // SDK audit event tests
601
+ // ============================================================================
602
+
603
+ describe('sdkAuditRowToLogEvent', () => {
604
+ test('maps sdk.assistant.message to assistant kind with sdk source', () => {
605
+ const event = sdkAuditRowToLogEvent({
606
+ id: 1,
607
+ entity_type: 'sdk_message',
608
+ entity_id: 'executor-123',
609
+ event_type: 'sdk.assistant.message',
610
+ actor: 'my-agent',
611
+ details: { textPreview: 'Hello from SDK agent' },
612
+ created_at: '2026-04-09T10:00:00.000Z',
613
+ });
614
+
615
+ expect(event.kind).toBe('assistant');
616
+ expect(event.agent).toBe('my-agent');
617
+ expect(event.text).toBe('Hello from SDK agent');
618
+ expect(event.source).toBe('sdk');
619
+ });
620
+
621
+ test('maps sdk.user.message to user kind', () => {
622
+ const event = sdkAuditRowToLogEvent({
623
+ id: 2,
624
+ entity_type: 'sdk_message',
625
+ entity_id: 'executor-123',
626
+ event_type: 'sdk.user.message',
627
+ actor: 'my-agent',
628
+ details: { textPreview: 'User said hello' },
629
+ created_at: '2026-04-09T10:01:00.000Z',
630
+ });
631
+
632
+ expect(event.kind).toBe('user');
633
+ expect(event.text).toBe('User said hello');
634
+ });
635
+
636
+ test('maps sdk.tool.summary to tool_call kind', () => {
637
+ const event = sdkAuditRowToLogEvent({
638
+ id: 3,
639
+ entity_type: 'sdk_message',
640
+ entity_id: 'executor-123',
641
+ event_type: 'sdk.tool.summary',
642
+ actor: 'my-agent',
643
+ details: { textPreview: 'Read /tmp/file.ts' },
644
+ created_at: '2026-04-09T10:02:00.000Z',
645
+ });
646
+
647
+ expect(event.kind).toBe('tool_call');
648
+ });
649
+
650
+ test('maps sdk.system to system kind', () => {
651
+ const event = sdkAuditRowToLogEvent({
652
+ id: 4,
653
+ entity_type: 'sdk_message',
654
+ entity_id: 'executor-123',
655
+ event_type: 'sdk.system',
656
+ actor: 'my-agent',
657
+ details: { textPreview: 'Session initialized' },
658
+ created_at: '2026-04-09T10:03:00.000Z',
659
+ });
660
+
661
+ expect(event.kind).toBe('system');
662
+ });
663
+
664
+ test('maps sdk.result.success to system kind', () => {
665
+ const event = sdkAuditRowToLogEvent({
666
+ id: 5,
667
+ entity_type: 'sdk_message',
668
+ entity_id: 'executor-123',
669
+ event_type: 'sdk.result.success',
670
+ actor: 'my-agent',
671
+ details: { textPreview: 'Task completed' },
672
+ created_at: '2026-04-09T10:04:00.000Z',
673
+ });
674
+
675
+ expect(event.kind).toBe('system');
676
+ });
677
+
678
+ test('maps sdk.rate_limit to system kind', () => {
679
+ const event = sdkAuditRowToLogEvent({
680
+ id: 6,
681
+ entity_type: 'sdk_message',
682
+ entity_id: 'executor-123',
683
+ event_type: 'sdk.rate_limit',
684
+ actor: 'my-agent',
685
+ details: {},
686
+ created_at: '2026-04-09T10:05:00.000Z',
687
+ });
688
+
689
+ expect(event.kind).toBe('system');
690
+ expect(event.text).toBe('sdk.rate_limit');
691
+ });
692
+
693
+ test('falls back to event_type as text when no textPreview', () => {
694
+ const event = sdkAuditRowToLogEvent({
695
+ id: 7,
696
+ entity_type: 'sdk_message',
697
+ entity_id: 'executor-123',
698
+ event_type: 'sdk.hook.started',
699
+ actor: 'my-agent',
700
+ details: {},
701
+ created_at: '2026-04-09T10:06:00.000Z',
702
+ });
703
+
704
+ expect(event.kind).toBe('system');
705
+ expect(event.text).toBe('sdk.hook.started');
706
+ });
707
+
708
+ test('unmapped event types default to system kind', () => {
709
+ const event = sdkAuditRowToLogEvent({
710
+ id: 8,
711
+ entity_type: 'sdk_message',
712
+ entity_id: 'executor-123',
713
+ event_type: 'sdk.unknown.future',
714
+ actor: 'my-agent',
715
+ details: { textPreview: 'something new' },
716
+ created_at: '2026-04-09T10:07:00.000Z',
717
+ });
718
+
719
+ expect(event.kind).toBe('system');
720
+ expect(event.text).toBe('something new');
721
+ });
722
+
723
+ test('handles null actor gracefully', () => {
724
+ const event = sdkAuditRowToLogEvent({
725
+ id: 9,
726
+ entity_type: 'sdk_message',
727
+ entity_id: 'executor-123',
728
+ event_type: 'sdk.assistant.message',
729
+ actor: null,
730
+ details: { textPreview: 'hello' },
731
+ created_at: '2026-04-09T10:08:00.000Z',
732
+ });
733
+
734
+ expect(event.agent).toBe('unknown');
735
+ });
736
+ });
737
+
738
+ describe.skipIf(!DB_AVAILABLE)('SDK events in readAgentLog', () => {
739
+ test('includes SDK audit events in agent log', async () => {
740
+ const repo = '/tmp/ulog-sdk-agent';
741
+ const agent = makeAgent('sdk-test-agent', 'sdk-team', repo);
742
+
743
+ // Insert SDK audit events
744
+ await recordAuditEvent('sdk_message', 'executor-1', 'sdk.assistant.message', 'sdk-test-agent', {
745
+ textPreview: 'SDK assistant response',
746
+ });
747
+ await recordAuditEvent('sdk_message', 'executor-1', 'sdk.user.message', 'sdk-test-agent', {
748
+ textPreview: 'SDK user input',
749
+ });
750
+
751
+ const events = await readAgentLog(agent, repo);
752
+ const sdkEvents = events.filter((e) => e.source === 'sdk');
753
+
754
+ expect(sdkEvents.length).toBeGreaterThanOrEqual(2);
755
+ expect(sdkEvents.some((e) => e.kind === 'assistant' && e.text === 'SDK assistant response')).toBe(true);
756
+ expect(sdkEvents.some((e) => e.kind === 'user' && e.text === 'SDK user input')).toBe(true);
757
+ });
758
+
759
+ test('SDK events are sorted with other sources by timestamp', async () => {
760
+ const repo = '/tmp/ulog-sdk-sorted';
761
+ const agent = makeAgent('sdk-sort-agent', undefined, repo);
762
+
763
+ // Insert an SDK event
764
+ await recordAuditEvent('sdk_message', 'executor-2', 'sdk.assistant.message', 'sdk-sort-agent', {
765
+ textPreview: 'SDK msg',
766
+ });
767
+ // Insert a mailbox event
768
+ await send(repo, 'reviewer', 'sdk-sort-agent', 'mailbox msg');
769
+
770
+ const events = await readAgentLog(agent, repo);
771
+
772
+ // Verify chronological order is maintained across sources
773
+ for (let i = 1; i < events.length; i++) {
774
+ expect(new Date(events[i].timestamp).getTime()).toBeGreaterThanOrEqual(
775
+ new Date(events[i - 1].timestamp).getTime(),
776
+ );
777
+ }
778
+ });
779
+
780
+ test('--type filter works with SDK event kinds', async () => {
781
+ const repo = '/tmp/ulog-sdk-filter';
782
+ const agent = makeAgent('sdk-filter-agent', undefined, repo);
783
+
784
+ await recordAuditEvent('sdk_message', 'executor-3', 'sdk.assistant.message', 'sdk-filter-agent', {
785
+ textPreview: 'assistant msg',
786
+ });
787
+ await recordAuditEvent('sdk_message', 'executor-3', 'sdk.system', 'sdk-filter-agent', {
788
+ textPreview: 'system msg',
789
+ });
790
+
791
+ const events = await readAgentLog(agent, repo, { kinds: ['assistant'] });
792
+ const sdkEvents = events.filter((e) => e.source === 'sdk');
793
+
794
+ for (const e of sdkEvents) {
795
+ expect(e.kind).toBe('assistant');
796
+ }
797
+ });
798
+ });
799
+
800
+ describe.skipIf(!DB_AVAILABLE)('SDK events in readTeamLog', () => {
801
+ test('includes SDK events from multiple agents interleaved', async () => {
802
+ const repo = '/tmp/ulog-sdk-team';
803
+ const eng = makeAgent('sdk-team-eng', 'sdk-log-team', repo);
804
+ const rev = makeAgent('sdk-team-rev', 'sdk-log-team', repo);
805
+
806
+ await recordAuditEvent('sdk_message', 'executor-4', 'sdk.assistant.message', 'sdk-team-eng', {
807
+ textPreview: 'eng SDK response',
808
+ });
809
+ await recordAuditEvent('sdk_message', 'executor-5', 'sdk.assistant.message', 'sdk-team-rev', {
810
+ textPreview: 'rev SDK response',
811
+ });
812
+
813
+ const events = await readTeamLog([eng, rev], repo, 'sdk-log-team');
814
+ const sdkEvents = events.filter((e) => e.source === 'sdk');
815
+
816
+ expect(sdkEvents.some((e) => e.agent === 'sdk-team-eng')).toBe(true);
817
+ expect(sdkEvents.some((e) => e.agent === 'sdk-team-rev')).toBe(true);
818
+ });
819
+ });
@@ -11,6 +11,8 @@
11
11
  */
12
12
 
13
13
  import type { Agent } from './agent-registry.js';
14
+ import type { AuditEventRow } from './audit.js';
15
+ import { getConnection, isAvailable } from './db.js';
14
16
  import { type MailboxMessage, inbox, readOutbox } from './mailbox.js';
15
17
  import {
16
18
  type RuntimeEvent,
@@ -188,6 +190,84 @@ export function sortByTimestamp(events: LogEvent[]): LogEvent[] {
188
190
  return events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
189
191
  }
190
192
 
193
+ // ============================================================================
194
+ // SDK Audit Events
195
+ // ============================================================================
196
+
197
+ /** Map SDK event types to existing LogEventKind values. */
198
+ const SDK_KIND_MAP: Record<string, LogEventKind> = {
199
+ 'sdk.user.message': 'user',
200
+ 'sdk.assistant.message': 'assistant',
201
+ 'sdk.tool.summary': 'tool_call',
202
+ 'sdk.system': 'system',
203
+ 'sdk.result.success': 'system',
204
+ 'sdk.hook.started': 'system',
205
+ 'sdk.hook.response': 'system',
206
+ 'sdk.rate_limit': 'system',
207
+ };
208
+
209
+ /** Convert an audit_events row (entity_type=sdk_message) to a LogEvent. */
210
+ export function sdkAuditRowToLogEvent(row: AuditEventRow): LogEvent {
211
+ const details = row.details ?? {};
212
+ return {
213
+ timestamp: typeof row.created_at === 'string' ? row.created_at : new Date(row.created_at).toISOString(),
214
+ kind: SDK_KIND_MAP[row.event_type] ?? 'system',
215
+ agent: row.actor ?? 'unknown',
216
+ text: (details.textPreview as string) ?? (details.summaryPreview as string) ?? row.event_type,
217
+ data: details,
218
+ source: 'sdk',
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Read SDK audit events for an agent from the audit_events table.
224
+ * Filters by entity_type='sdk_message' AND actor=agentId.
225
+ */
226
+ async function readSdkAuditEvents(agentId: string, filter?: LogFilter): Promise<LogEvent[]> {
227
+ try {
228
+ if (!(await isAvailable())) return [];
229
+ const sql = await getConnection();
230
+
231
+ const conditions = [`entity_type = 'sdk_message'`, 'actor = $1'];
232
+ const values: unknown[] = [agentId];
233
+ let paramIdx = 2;
234
+
235
+ if (filter?.since) {
236
+ conditions.push(`created_at >= $${paramIdx++}::timestamptz`);
237
+ values.push(filter.since);
238
+ }
239
+
240
+ const where = `WHERE ${conditions.join(' AND ')}`;
241
+ const limit = filter?.last ?? 500;
242
+
243
+ const rows = (await sql.unsafe(
244
+ `SELECT id, entity_type, entity_id, event_type, actor, details, created_at
245
+ FROM audit_events ${where}
246
+ ORDER BY created_at ASC
247
+ LIMIT ${limit}`,
248
+ values,
249
+ )) as unknown as AuditEventRow[];
250
+
251
+ const events: LogEvent[] = [];
252
+ for (const row of rows) {
253
+ // Only include event types we have a mapping for
254
+ if (SDK_KIND_MAP[row.event_type]) {
255
+ events.push(sdkAuditRowToLogEvent(row));
256
+ }
257
+ }
258
+
259
+ // Apply kinds filter if specified
260
+ if (filter?.kinds && filter.kinds.length > 0) {
261
+ const kinds = new Set(filter.kinds);
262
+ return events.filter((e) => kinds.has(e.kind));
263
+ }
264
+
265
+ return events;
266
+ } catch {
267
+ return [];
268
+ }
269
+ }
270
+
191
271
  // ============================================================================
192
272
  // Aggregators
193
273
  // ============================================================================
@@ -202,11 +282,12 @@ export async function readAgentLog(agent: Agent, repoPath: string, filter?: LogF
202
282
  const mailboxKeys = mailboxActorKeys(agent);
203
283
 
204
284
  // Read all sources in parallel
205
- const [transcriptEntries, inboxMessages, outboxMessages, chatMessages] = await Promise.all([
285
+ const [transcriptEntries, inboxMessages, outboxMessages, chatMessages, sdkEvents] = await Promise.all([
206
286
  readTranscriptSafe(agent),
207
287
  inbox(repoPath, mailboxKeys),
208
288
  readOutbox(repoPath, mailboxKeys),
209
289
  team ? readMessages(repoPath, team) : Promise.resolve([]),
290
+ readSdkAuditEvents(agentName, filter),
210
291
  ]);
211
292
 
212
293
  // Convert to LogEvents
@@ -231,6 +312,8 @@ export async function readAgentLog(agent: Agent, repoPath: string, filter?: LogF
231
312
  }
232
313
  }
233
314
 
315
+ events.push(...sdkEvents);
316
+
234
317
  // Sort by time, then filter
235
318
  const sorted = sortByTimestamp(events);
236
319
  return applyLogFilter(sorted, filter);
@@ -256,10 +339,11 @@ export async function readTeamLog(
256
339
  const agentName = agent.id;
257
340
  const mailboxKeys = mailboxActorKeys(agent);
258
341
 
259
- const [transcriptEntries, inboxMessages, outboxMessages] = await Promise.all([
342
+ const [transcriptEntries, inboxMessages, outboxMessages, sdkEvents] = await Promise.all([
260
343
  readTranscriptSafe(agent),
261
344
  inbox(repoPath, mailboxKeys),
262
345
  readOutbox(repoPath, mailboxKeys),
346
+ readSdkAuditEvents(agentName, filter),
263
347
  ]);
264
348
 
265
349
  const events: LogEvent[] = [];
@@ -273,6 +357,7 @@ export async function readTeamLog(
273
357
  for (const msg of outboxMessages) {
274
358
  events.push(outboxMessageToLogEvent(msg, agentName, teamName));
275
359
  }
360
+ events.push(...sdkEvents);
276
361
  return events;
277
362
  }),
278
363
  );
@@ -321,6 +406,8 @@ export async function followTeamLog(
321
406
  /**
322
407
  * PG-first follow: wake up on LISTEN/NOTIFY, replay by event id cursor, and
323
408
  * filter by agent/team before emitting to the caller.
409
+ *
410
+ * Also polls audit_events for SDK agent events on the same interval.
324
411
  */
325
412
  async function startPgFollow(
326
413
  agents: Agent[],
@@ -335,6 +422,15 @@ async function startPgFollow(
335
422
 
336
423
  const eventKey = (e: LogEvent): string => `${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0, 80)}`;
337
424
 
425
+ const dedupAndEmit = (event: LogEvent) => {
426
+ if (!event.timestamp || !event.kind) return;
427
+ if (kindsFilter && !kindsFilter.has(event.kind)) return;
428
+ const key = eventKey(event);
429
+ if (seenKeys.has(key)) return;
430
+ seenKeys.add(key);
431
+ onEvent(event);
432
+ };
433
+
338
434
  const matchesScope = (event: RuntimeEvent) => {
339
435
  if (team === 'all') return true;
340
436
  if (team && event.team === team) return true;
@@ -342,14 +438,8 @@ async function startPgFollow(
342
438
  };
343
439
 
344
440
  const handleRuntimeEvent = (event: RuntimeEvent) => {
345
- if (!event.timestamp || !event.kind) return;
346
- if (kindsFilter && !kindsFilter.has(event.kind)) return;
347
441
  if (!matchesScope(event)) return;
348
- // Dedup (same event can arrive on multiple matching subjects)
349
- const key = eventKey(event);
350
- if (seenKeys.has(key)) return;
351
- seenKeys.add(key);
352
- onEvent(event);
442
+ dedupAndEmit(event);
353
443
  };
354
444
 
355
445
  const handle = await followRuntimeEvents(
@@ -366,9 +456,64 @@ async function startPgFollow(
366
456
  },
367
457
  );
368
458
 
459
+ // SDK audit event poller — polls audit_events table for sdk_message events
460
+ let sdkPollActive = true;
461
+ let sdkLastId = 0;
462
+
463
+ // Seed the cursor: get the max id so we only see new events
464
+ try {
465
+ if (await isAvailable()) {
466
+ const sql = await getConnection();
467
+ const [row] =
468
+ await sql`SELECT COALESCE(MAX(id), 0) AS max_id FROM audit_events WHERE entity_type = 'sdk_message'`;
469
+ sdkLastId = Number(row?.max_id ?? 0);
470
+ }
471
+ } catch {
472
+ // Best effort — start from 0 if seed fails
473
+ }
474
+
475
+ const drainSdkAuditEvents = async () => {
476
+ if (!(await isAvailable())) return;
477
+ const sql = await getConnection();
478
+ const agentList = [...agentIds];
479
+ const rows = (await sql.unsafe(
480
+ `SELECT id, entity_type, entity_id, event_type, actor, details, created_at
481
+ FROM audit_events
482
+ WHERE entity_type = 'sdk_message' AND id > $1
483
+ AND actor = ANY($2)
484
+ ORDER BY id ASC
485
+ LIMIT 100`,
486
+ [sdkLastId, agentList],
487
+ )) as unknown as (AuditEventRow & { id: number })[];
488
+
489
+ for (const row of rows) {
490
+ if (SDK_KIND_MAP[row.event_type]) {
491
+ dedupAndEmit(sdkAuditRowToLogEvent(row));
492
+ }
493
+ sdkLastId = Math.max(sdkLastId, Number(row.id));
494
+ }
495
+ };
496
+
497
+ const sdkPoll = async () => {
498
+ while (sdkPollActive) {
499
+ try {
500
+ await drainSdkAuditEvents();
501
+ } catch {
502
+ // Best effort — skip failed polls
503
+ }
504
+ await new Promise((resolve) => setTimeout(resolve, 500));
505
+ }
506
+ };
507
+
508
+ // Start SDK poller in background
509
+ sdkPoll();
510
+
369
511
  return {
370
512
  mode: 'pg',
371
- stop: () => handle.stop(),
513
+ stop: async () => {
514
+ sdkPollActive = false;
515
+ await handle.stop();
516
+ },
372
517
  };
373
518
  }
374
519
 
@@ -61,33 +61,16 @@ const directoryResolveMock = mock(async (name: string) => ({
61
61
  builtin: false,
62
62
  }));
63
63
 
64
- /** Default SDK query implementation yields one assistant reply + success result with session_id. */
65
- const defaultQueryImpl = () => {
66
- const gen = (async function* () {
67
- yield { type: 'assistant', message: { content: [{ type: 'text', text: 'reply' }] } };
68
- yield { type: 'result', subtype: 'success', session_id: 'sdk-session-aaa' };
69
- })();
70
- return Object.assign(gen, {
71
- interrupt: mock(),
72
- setPermissionMode: mock(),
73
- setModel: mock(),
74
- return: mock(async () => ({ value: undefined, done: true })),
75
- throw: mock(async () => ({ value: undefined, done: true })),
76
- });
77
- };
78
-
79
- export const queryMock = mock(defaultQueryImpl);
64
+ // SDK query mock is defined in a shared file so that sdk-integration.test.ts
65
+ // (which only needs the SDK mock, not directory/registry mocks) can import the
66
+ // same instance without triggering the registry mocks below.
67
+ import {
68
+ queryMock as _queryMock,
69
+ resetQueryMock as _resetQueryMock,
70
+ } from '../../../__tests__/_shared-sdk-query-mock.js';
80
71
 
81
- /**
82
- * Reset queryMock to its default implementation. Use in beforeEach for tests
83
- * that care about the default behavior — some tests override via
84
- * mockImplementation() and that override persists across files because
85
- * queryMock is shared.
86
- */
87
- export function resetQueryMock(): void {
88
- queryMock.mockReset();
89
- queryMock.mockImplementation(defaultQueryImpl);
90
- }
72
+ export const queryMock = _queryMock;
73
+ export const resetQueryMock = _resetQueryMock;
91
74
 
92
75
  /**
93
76
  * Reset ALL shared mocks to their default implementations and clear call
@@ -155,17 +138,5 @@ mock.module('../../../lib/executor-registry.js', () => ({
155
138
  terminateExecutor: terminateExecutorMock,
156
139
  }));
157
140
 
158
- mock.module('@anthropic-ai/claude-agent-sdk', () => ({
159
- query: queryMock,
160
- createSdkMcpServer: mock((opts: any) => ({
161
- type: 'sdk' as const,
162
- name: opts.name,
163
- instance: {},
164
- })),
165
- tool: mock((_name: string, _desc: string, _schema: any, handler: any) => ({
166
- name: _name,
167
- description: _desc,
168
- inputSchema: _schema,
169
- handler,
170
- })),
171
- }));
141
+ // SDK mock.module registration is handled by _shared-sdk-query-mock.ts
142
+ // (imported above via re-export). No need to register again here.
@@ -435,6 +435,7 @@ export class ClaudeSdkOmniExecutor implements IExecutor {
435
435
  );
436
436
  }
437
437
 
438
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: delivery pipeline with prompt assembly, MCP setup, and session capture
438
439
  private async _processDelivery(
439
440
  session: ExecutorSession,
440
441
  state: SdkSessionState,
@@ -826,6 +826,7 @@ export class OmniBridge {
826
826
  /**
827
827
  * Spawn a new agent session for a chat.
828
828
  */
829
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: spawn orchestration with concurrent guard, env resolution, and error recovery
829
830
  private async spawnSession(message: OmniMessage): Promise<void> {
830
831
  const key = `${message.agent}:${message.chatId}`;
831
832
 
@@ -1029,6 +1029,15 @@ async function runSdkQuery(
1029
1029
  }
1030
1030
 
1031
1031
  const streaming = streamOpts?.stream ?? false;
1032
+
1033
+ // Emit session ID for streaming JSON consumers (ndjson/json)
1034
+ if (dbSessionId && streaming) {
1035
+ const fmt = streamOpts?.streamFormat ?? 'text';
1036
+ if (fmt === 'ndjson' || fmt === 'json') {
1037
+ process.stdout.write(`${JSON.stringify({ type: 'genie_session', session_id: dbSessionId })}\n`);
1038
+ }
1039
+ }
1040
+
1032
1041
  // Runtime overrides: streaming + CLI --sdk-* flags (highest priority, over directory sdkConfig)
1033
1042
  const extraOptions: Record<string, unknown> = {
1034
1043
  ...(streaming && { includePartialMessages: true }),
@@ -1047,11 +1056,54 @@ async function runSdkQuery(
1047
1056
  await executorRegistry.updateExecutorState(ctx.executorId, 'running').catch(() => {});
1048
1057
  }
1049
1058
 
1050
- let resultText = '';
1051
1059
  let claudeSessionId: string | undefined;
1052
- const toolCalls: Array<{ name: string; input: unknown; output?: string; is_error?: boolean }> = [];
1053
- let pendingToolName = '';
1054
- let pendingToolInput: unknown = null;
1060
+ // Map tool_use IDs to tool names for correlating tool_result rows
1061
+ const toolNameById = new Map<string, string>();
1062
+
1063
+ const record = async (
1064
+ role: 'user' | 'assistant' | 'tool_input' | 'tool_output',
1065
+ content: string,
1066
+ toolName?: string,
1067
+ ) => {
1068
+ if (!dbSessionId) return;
1069
+ await recordTurn(safePgCall, dbSessionId, turnIndex++, role, content, toolName);
1070
+ };
1071
+
1072
+ // Process SDK messages — same logic for streaming and non-streaming
1073
+ const processMessage = async (message: import('@anthropic-ai/claude-agent-sdk').SDKMessage) => {
1074
+ if (message.type === 'system' && (message as Record<string, unknown>).session_id) {
1075
+ claudeSessionId = (message as Record<string, unknown>).session_id as string;
1076
+ }
1077
+ if (message.type === 'assistant' && message.message) {
1078
+ for (const block of message.message.content) {
1079
+ if (block.type === 'tool_use') {
1080
+ const b = block as unknown as Record<string, unknown>;
1081
+ const name = String(b.name ?? '');
1082
+ const id = String(b.id ?? '');
1083
+ if (id) toolNameById.set(id, name);
1084
+ await record('tool_input', JSON.stringify(b.input ?? {}).slice(0, 500), name);
1085
+ }
1086
+ if (block.type === 'text' && block.text) {
1087
+ await record('assistant', block.text);
1088
+ }
1089
+ }
1090
+ }
1091
+ // Tool results come as user messages with tool_result content blocks
1092
+ if (message.type === 'user' && message.message?.content && Array.isArray(message.message.content)) {
1093
+ for (const block of message.message.content) {
1094
+ const b = block as unknown as Record<string, unknown>;
1095
+ if (b.type === 'tool_result') {
1096
+ const toolId = String(b.tool_use_id ?? '');
1097
+ const toolName = toolNameById.get(toolId) ?? '';
1098
+ const output = typeof b.content === 'string' ? b.content.slice(0, 500) : '';
1099
+ await record('tool_output', output, toolName);
1100
+ }
1101
+ }
1102
+ }
1103
+ if (message.type === 'result' && message.subtype === 'success') {
1104
+ if (message.session_id) claudeSessionId = message.session_id;
1105
+ }
1106
+ };
1055
1107
 
1056
1108
  if (streaming) {
1057
1109
  const { formatSdkMessage } = await import('../lib/providers/claude-sdk-stream.js');
@@ -1063,34 +1115,7 @@ async function runSdkQuery(
1063
1115
  process.stdout.write(formatted);
1064
1116
  if (format === 'json') process.stdout.write('\n');
1065
1117
  }
1066
- if (message.type === 'system' && (message as Record<string, unknown>).session_id) {
1067
- claudeSessionId = (message as Record<string, unknown>).session_id as string;
1068
- }
1069
- // Collect tool calls
1070
- if (message.type === 'assistant' && message.message) {
1071
- for (const block of message.message.content) {
1072
- if (block.type === 'tool_use') {
1073
- pendingToolName = ((block as unknown as Record<string, unknown>).name as string) ?? '';
1074
- pendingToolInput = (block as unknown as Record<string, unknown>).input;
1075
- }
1076
- if (block.type === 'text' && block.text) resultText += block.text;
1077
- }
1078
- }
1079
- if ((message as unknown as Record<string, unknown>).type === 'tool_result') {
1080
- const msg = message as Record<string, unknown>;
1081
- toolCalls.push({
1082
- name: pendingToolName,
1083
- input: pendingToolInput,
1084
- output: typeof msg.content === 'string' ? msg.content.slice(0, 500) : '',
1085
- is_error: !!msg.is_error,
1086
- });
1087
- pendingToolName = '';
1088
- pendingToolInput = null;
1089
- }
1090
- if (message.type === 'result' && message.subtype === 'success') {
1091
- if (message.result && !resultText) resultText = message.result;
1092
- if (message.session_id) claudeSessionId = message.session_id;
1093
- }
1118
+ await processMessage(message);
1094
1119
  }
1095
1120
  } catch (err) {
1096
1121
  if (!(err instanceof Error && err.name === 'AbortError')) {
@@ -1100,36 +1125,16 @@ async function runSdkQuery(
1100
1125
  } else {
1101
1126
  try {
1102
1127
  for await (const message of messages) {
1128
+ // Print text to stdout
1103
1129
  if (message.type === 'assistant' && message.message) {
1104
1130
  for (const block of message.message.content) {
1105
- if (block.type === 'tool_use') {
1106
- pendingToolName = ((block as unknown as Record<string, unknown>).name as string) ?? '';
1107
- pendingToolInput = (block as unknown as Record<string, unknown>).input;
1108
- }
1109
- if (block.type === 'text' && block.text) {
1110
- process.stdout.write(block.text);
1111
- resultText += block.text;
1112
- }
1131
+ if (block.type === 'text' && block.text) process.stdout.write(block.text);
1113
1132
  }
1114
1133
  }
1115
- if ((message as unknown as Record<string, unknown>).type === 'tool_result') {
1116
- const msg = message as Record<string, unknown>;
1117
- toolCalls.push({
1118
- name: pendingToolName,
1119
- input: pendingToolInput,
1120
- output: typeof msg.content === 'string' ? msg.content.slice(0, 500) : '',
1121
- is_error: !!msg.is_error,
1122
- });
1123
- pendingToolName = '';
1124
- pendingToolInput = null;
1125
- }
1126
- if (message.type === 'result' && message.subtype === 'success') {
1127
- if (message.result) {
1128
- console.log(message.result);
1129
- if (!resultText) resultText = message.result;
1130
- }
1131
- if (message.session_id) claudeSessionId = message.session_id;
1134
+ if (message.type === 'result' && message.subtype === 'success' && message.result) {
1135
+ console.log(message.result);
1132
1136
  }
1137
+ await processMessage(message);
1133
1138
  }
1134
1139
  } catch (err) {
1135
1140
  if (!(err instanceof Error && err.name === 'AbortError')) {
@@ -1138,28 +1143,27 @@ async function runSdkQuery(
1138
1143
  }
1139
1144
  }
1140
1145
 
1141
- // Record assistant response (with tools embedded) and end session
1146
+ // End session
1142
1147
  if (dbSessionId) {
1143
- if (resultText || toolCalls.length > 0) {
1144
- const content = toolCalls.length > 0 ? JSON.stringify({ text: resultText, tools: toolCalls }) : resultText;
1145
- await recordTurn(safePgCall, dbSessionId, turnIndex++, 'assistant', content);
1146
- }
1147
1148
  await updateTurnCount(safePgCall, dbSessionId, turnIndex);
1148
1149
  await endSession(safePgCall, dbSessionId, 'completed');
1149
1150
  }
1150
1151
 
1151
1152
  // Persist Claude SDK session ID on executor AND session for resume lookup
1152
1153
  if (claudeSessionId && spawnContext.executorId) {
1154
+ const csId = claudeSessionId;
1153
1155
  await safePgCall(
1154
1156
  'update-claude-session-id',
1155
- (sql) => sql`UPDATE executors SET claude_session_id = ${claudeSessionId} WHERE id = ${spawnContext.executorId}`,
1157
+ (sql) => sql`UPDATE executors SET claude_session_id = ${csId} WHERE id = ${spawnContext.executorId}`,
1156
1158
  undefined,
1157
1159
  );
1158
1160
  }
1159
1161
  if (claudeSessionId && dbSessionId) {
1162
+ const csId = claudeSessionId;
1163
+ const sessId = dbSessionId;
1160
1164
  await safePgCall(
1161
1165
  'update-session-claude-id',
1162
- (sql) => sql`UPDATE sessions SET claude_session_id = ${claudeSessionId} WHERE id = ${dbSessionId}`,
1166
+ (sql) => sql`UPDATE sessions SET claude_session_id = ${csId} WHERE id = ${sessId}`,
1163
1167
  undefined,
1164
1168
  );
1165
1169
  }
@@ -137,7 +137,8 @@ function formatEventBlock(event: LogEvent): string {
137
137
  if (event.direction === 'in') agent = `${event.peer} → ${event.agent}`;
138
138
  else if (event.direction === 'out') agent = `${event.agent} → ${event.peer}`;
139
139
 
140
- const header = `${DIM}${time}${RESET} ${color}[${icon}]${RESET} ${BOLD}${agent}${RESET}`;
140
+ const sdkTag = event.source === 'sdk' ? ` ${DIM}[SDK]${RESET}` : '';
141
+ const header = `${DIM}${time}${RESET} ${color}[${icon}]${RESET} ${BOLD}${agent}${RESET}${sdkTag}`;
141
142
 
142
143
  // Tool calls: one-line summary
143
144
  if (event.kind === 'tool_call') {
@@ -261,14 +261,14 @@ describe.skipIf(!DB_AVAILABLE)('buildTeamLeadCommand (shared module)', () => {
261
261
 
262
262
  test('includes --append-system-prompt-file when systemPromptFile provided (default promptMode)', async () => {
263
263
  const { buildTeamLeadCommand } = await import('../lib/team-lead-command.js');
264
- const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/tmp/test-agents.md' });
264
+ const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/tmp/test-agents.md', promptMode: 'append' });
265
265
  expect(cmd).toContain('--append-system-prompt-file');
266
266
  expect(cmd).toContain('/tmp/test-agents.md');
267
267
  });
268
268
 
269
269
  test('file path is passed directly, not copied', async () => {
270
270
  const { buildTeamLeadCommand } = await import('../lib/team-lead-command.js');
271
- const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/path/to/AGENTS.md' });
271
+ const cmd = buildTeamLeadCommand('genie', { systemPromptFile: '/path/to/AGENTS.md', promptMode: 'append' });
272
272
  expect(cmd).toContain('--append-system-prompt-file');
273
273
  expect(cmd).toContain('/path/to/AGENTS.md');
274
274
  });