@automagik/genie 4.260329.25 → 4.260329.26
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 +5 -5
- 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/hooks/handlers/orchestration-guard.ts +10 -3
- package/src/hooks/index.ts +50 -15
- package/src/hooks/types.ts +9 -2
- package/src/lib/task-service.ts +13 -6
- package/src/tui/app.tsx +4 -2
- package/src/tui/session-tree.ts +6 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260329.
|
|
13
|
+
"version": "4.260329.26",
|
|
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
|
@@ -1284,7 +1284,7 @@ $ bun add react-devtools-core@7 -d
|
|
|
1284
1284
|
AND a.ended_at IS NULL
|
|
1285
1285
|
ORDER BY a.started_at DESC
|
|
1286
1286
|
`).map(mapAssignment)}function mapExecutor(row){let meta=row.metadata;return{id:String(row.id),agentId:String(row.agent_id),agentName:row.agent_name?String(row.agent_name):null,provider:String(row.provider),transport:String(row.transport),pid:row.pid!=null?Number(row.pid):null,tmuxSession:row.tmux_session?String(row.tmux_session):null,tmuxPaneId:row.tmux_pane_id?String(row.tmux_pane_id):null,state:String(row.state),metadata:typeof meta==="string"?JSON.parse(meta):meta??{},startedAt:row.started_at instanceof Date?row.started_at.toISOString():String(row.started_at),role:row.role?String(row.role):null,team:row.team?String(row.team):null}}function mapAssignment(row){return{id:String(row.id),executorId:String(row.executor_id),taskId:row.task_id?String(row.task_id):null,taskTitle:row.task_title?String(row.task_title):null,wishSlug:row.wish_slug?String(row.wish_slug):null,groupNumber:row.group_number!=null?Number(row.group_number):null,startedAt:row.started_at instanceof Date?row.started_at.toISOString():String(row.started_at)}}import{execSync as execSync5}from"child_process";function execQuiet(cmd){try{return execSync5(cmd,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}}function parsePaneLine(parts){let[sessionName,winIdxStr,winName,winActive,winPanes,paneIdxStr,paneId,panePidStr,paneCmd,paneTitle,paneSize,sessAttached,sessWindows,sessCreated,paneDead]=parts;return{sessionName,winIdxStr,session:{name:sessionName,attached:sessAttached==="1",windowCount:Number.parseInt(sessWindows,10)||0,created:Number.parseInt(sessCreated,10)||0},window:{sessionName,index:Number.parseInt(winIdxStr,10)||0,name:winName,active:winActive==="1",paneCount:Number.parseInt(winPanes,10)||0},pane:{sessionName,windowIndex:Number.parseInt(winIdxStr,10)||0,paneIndex:Number.parseInt(paneIdxStr,10)||0,paneId,pid:Number.parseInt(panePidStr,10)||0,command:paneCmd,title:paneTitle,size:paneSize,isDead:paneDead==="1"}}}function getTmuxInventory(){let paneOutput=execQuiet("tmux list-panes -a -F '#{session_name}|#{window_index}|#{window_name}|#{window_active}|#{window_panes}|#{pane_index}|#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_title}|#{pane_width}x#{pane_height}|#{session_attached}|#{session_windows}|#{session_created}|#{pane_dead}'");if(!paneOutput)return[];let sessionMap=new Map,windowMap=new Map;for(let line of paneOutput.split(`
|
|
1287
|
-
`)){if(!line)continue;let parts=line.split("|");if(parts.length<15)continue;let parsed=parsePaneLine(parts);if(!sessionMap.has(parsed.sessionName))sessionMap.set(parsed.sessionName,{...parsed.session,windows:[]});let winKey=`${parsed.sessionName}:${parsed.winIdxStr}`;if(!windowMap.has(winKey)){let win={...parsed.window,panes:[]};windowMap.set(winKey,win),sessionMap.get(parsed.sessionName)?.windows.push(win)}windowMap.get(winKey)?.panes.push(parsed.pane)}return Array.from(sessionMap.values()).sort((a,b3)=>a.name.localeCompare(b3.name))}function isPidAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function allClaudePanes(sessions){return sessions.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.command==="claude"||p.title.includes("claude"))}function detectGaps(executors,sessions){let deadPidExecutors=executors.filter((e)=>e.pid!=null&&!isPidAlive(e.pid)),executorPaneIds=new Set(executors.map((e)=>e.tmuxPaneId).filter(Boolean)),claudePanes=allClaudePanes(sessions),orphanPanes=claudePanes.filter((p)=>!executorPaneIds.has(p.paneId)),linkedCount=executors.filter((e)=>e.tmuxPaneId&&!deadPidExecutors.some((d2)=>d2.id===e.id)).length,deadPaneCount=sessions.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.isDead).length;return{deadPidExecutors,orphanPanes,linkedCount,totalExecutors:executors.length,totalClaudePanes:claudePanes.length,deadPaneCount}}async function collectDiagnostics(){let{loadExecutors:loadExecutors2,loadAssignments:loadAssignments2}=await Promise.resolve().then(() => exports_db2),sessions=getTmuxInventory(),executors=await loadExecutors2(),executorIds=executors.map((e)=>e.id),assignments=await loadAssignments2(executorIds),gaps=detectGaps(executors,sessions);return{sessions,executors,assignments,gaps,timestamp:Date.now()}}var init_diagnostics=()=>{};function buildSessionTree(snapshot){let executorByPaneId=new Map;for(let exec3 of snapshot.executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);return snapshot.sessions.map((session)=>sessionToNode(session,executorByPaneId))}function sessionToNode(session,executorMap){let claudePanes=session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0);return{id:`session:${session.name}`,type:"session",label:session.name,depth:0,expanded:!0,children:session.windows.map((w2)=>windowToNode(session.name,w2,executorMap)),data:{attached:session.attached,windowCount:session.windowCount},activePanes:claudePanes,agentState:void 0}}function windowToNode(sessionName,window2,executorMap){let activePanes=window2.panes.filter((p)=>!p.isDead&&(p.command==="claude"||p.title.includes("claude"))).length;return{id:`window:${sessionName}:${window2.index}`,type:"window",label:window2.name,depth:1,expanded:!0,children:window2.panes.map((p)=>paneToNode(sessionName,window2.index,p,executorMap)),data:{active:window2.active,paneCount:window2.paneCount},activePanes,agentState:void 0}}function paneToNode(sessionName,windowIndex,pane,executorMap){let executor=executorMap.get(pane.paneId),isClaude=pane.command==="claude"||pane.title.includes("claude");return{id:`pane:${pane.paneId}`,type:"pane",label:derivePaneLabel(pane,executor,isClaude),depth:2,expanded:!1,children:[],data:{command:pane.command,isDead:pane.isDead,pid:pane.pid,size:pane.size,paneId:pane.paneId,sessionName,windowIndex},activePanes:0,agentState:derivePaneState(pane,executor)}}function derivePaneLabel(pane,executor,isClaude){if(executor?.agentName&&executor?.team)return`${executor.team}/${executor.agentName}`;if(executor?.agentName)return executor.agentName;if(isClaude)return"claude";return pane.command}function derivePaneState(pane,executor){if(pane.isDead)return"error";if(!executor)return;let s=executor.state;if(s==="working")return"working";if(s==="idle"||s==="spawning")return"idle";if(s==="permission")return"permission";if(s==="error"||s==="terminated")return"error";return}function getSessionTarget(node){if(node.type==="session")return{sessionName:node.label};if(node.type==="window"){let parts=node.id.split(":");return{sessionName:parts[1],windowIndex:Number(parts[2])}}if(node.type==="pane"){let data=node.data;return{sessionName:data.sessionName,windowIndex:data.windowIndex}}return null}function flattenTree(nodes){let result=[];function walk(node,depth){if(result.push({node,depth,visible:!0}),node.expanded)for(let child of node.children)walk(child,depth+1)}for(let node of nodes)walk(node,0);return result}function toggleNode(nodes,id){return nodes.map((node)=>{if(node.id===id)return{...node,expanded:!node.expanded};return{...node,children:toggleNode(node.children,id)}})}var import_jsx_dev_runtime2;var init_jsx_dev_runtime=__esm(()=>{import_jsx_dev_runtime2=__toESM(require_react_jsx_dev_runtime_development(),1)});function getNodeIcon(node){switch(node.type){case"session":return node.data.attached?"\u25B6":"\u25B8";case"window":return node.data.active?"\u25A0":"\u25A1";case"pane":return getPaneIcon(node);default:return" "}}function getPaneIcon(node){if(node.data.isDead)return"\u2718";if(node.agentState==="working")return"\u25CF";if(node.agentState==="idle")return"\u25CB";if(node.agentState==="permission")return"\u26A0";if(node.agentState==="error")return"\u2718";if(node.data.command==="claude")return"\u25C6";return"\u25CB"}function getNodeColor(node){switch(node.type){case"session":return node.data.attached?palette.emerald:palette.textDim;case"window":return node.data.active?palette.cyan:palette.text;case"pane":return getPaneColor(node);default:return palette.text}}function getPaneColor(node){if(node.data.isDead)return palette.error;if(node.agentState==="working")return palette.cyan;if(node.agentState==="permission")return palette.warning;if(node.agentState==="error")return palette.error;if(node.agentState==="idle")return palette.textDim;if(node.data.command==="claude")return palette.cyan;return palette.textDim}function getStateColor(state){switch(state){case"working":return palette.cyan;case"idle":return palette.textDim;case"permission":return palette.warning;case"error":return palette.error;default:return palette.textMuted}}var import_react13,TreeNodeRow;var init_TreeNode=__esm(()=>{init_theme2();init_jsx_dev_runtime();import_react13=__toESM(require_react_development(),1),TreeNodeRow=import_react13.memo(function({node,selected,onSelect,onToggle}){let indent=" ".repeat(node.depth),hasChildren=node.children.length>0,expandIcon=hasChildren?node.expanded?icons.expanded:icons.collapsed:" ",icon=getNodeIcon(node),color=getNodeColor(node),activeIndicator=node.activePanes>0?` ${icons.agent}${node.activePanes}`:"";return import_jsx_dev_runtime2.jsxDEV("box",{height:1,width:"100%",backgroundColor:selected?palette.violet:void 0,onMouseDown:()=>{if(onSelect(node.id),hasChildren)onToggle(node.id)},children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[indent,expandIcon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:color,children:[icon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:selected?"#ffffff":palette.text,children:node.label},void 0,!1,void 0,this),activeIndicator?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.cyan,children:activeIndicator},void 0,!1,void 0,this):null,node.agentState?import_jsx_dev_runtime2.jsxDEV("span",{fg:getStateColor(node.agentState),children:[" ",node.agentState]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this)})});function Nav({onTmuxSessionSelect}){let[diagnostics,setDiagnostics]=import_react15.useState(null),[sessionTree,setSessionTree]=import_react15.useState([]),[selectedIndex,setSelectedIndex]=import_react15.useState(0),lastTarget=import_react15.useRef(null);import_react15.useEffect(()=>{let active=!0;async function refresh(){try{let snap=await collectDiagnostics();if(active)setDiagnostics(snap)}catch(err){console.error("TUI: diagnostics failed:",err)}}refresh();let timer2=setInterval(refresh,2000);return()=>{active=!1,clearInterval(timer2)}},[]),import_react15.useEffect(()=>{if(!diagnostics)return;let newTree=buildSessionTree(diagnostics);setSessionTree((prev)=>mergeExpandedState(prev,newTree))},[diagnostics]);let flatNodes=import_react15.useMemo(()=>flattenTree(sessionTree),[sessionTree]);import_react15.useEffect(()=>{if(flatNodes.length>0&&selectedIndex>=flatNodes.length)setSelectedIndex(flatNodes.length-1)},[flatNodes.length,selectedIndex]),import_react15.useEffect(()=>{let current=flatNodes[selectedIndex]?.node;if(!current)return;let target=getSessionTarget(current);if(!target)return;let key=`${target.sessionName}:${target.windowIndex??""}`;if(key===lastTarget.current)return;lastTarget.current=key,onTmuxSessionSelect(target.sessionName,target.windowIndex)},[selectedIndex,flatNodes,onTmuxSessionSelect]);let handleSelect=import_react15.useCallback((id)=>{let idx=flatNodes.findIndex((n)=>n.node.id===id);if(idx>=0)setSelectedIndex(idx)},[flatNodes]),handleToggle=import_react15.useCallback((id)=>{setSessionTree((prev)=>toggleNode(prev,id))},[]),handleVerticalNav=import_react15.useCallback((keyName2)=>{let rowCount=flatNodes.length;if(rowCount===0)return;if(keyName2==="up"||keyName2==="k")setSelectedIndex((prev)=>prev===0?rowCount-1:prev-1);else if(keyName2==="down"||keyName2==="j")setSelectedIndex((prev)=>prev>=rowCount-1?0:prev+1)},[flatNodes.length]),handleExpandCollapse=import_react15.useCallback((keyName2)=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if((keyName2==="right"||keyName2==="l")&&node.children.length>0&&!node.expanded)handleToggle(node.id);else if((keyName2==="left"||keyName2==="h")&&node.expanded)handleToggle(node.id)},[flatNodes,selectedIndex,handleToggle]),handleEnter=import_react15.useCallback(()=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if(node.children.length>0)handleToggle(node.id);let target=getSessionTarget(node);if(target)onTmuxSessionSelect(target.sessionName,target.windowIndex)},[flatNodes,selectedIndex,handleToggle,onTmuxSessionSelect]);useKeyboard((key)=>{if(key.name==="up"||key.name==="k"||key.name==="down"||key.name==="j")handleVerticalNav(key.name);else if(key.name==="right"||key.name==="l"||key.name==="left"||key.name==="h")handleExpandCollapse(key.name);else if(key.name==="enter"||key.name==="return")handleEnter()});let sessionCount=diagnostics?.sessions.length??0,totalPanes=diagnostics?.sessions.reduce((sum,s)=>sum+s.windows.reduce((ws,w2)=>ws+w2.panes.length,0),0)??0,deadPanes=diagnostics?.gaps.deadPaneCount??0;return import_jsx_dev_runtime2.jsxDEV("box",{flexDirection:"column",width:"100%",height:"100%",backgroundColor:palette.bg,children:[import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.purple,children:"Sessions"},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[" ",sessionCount,"s ",totalPanes,"p"]},void 0,!0,void 0,this):null,deadPanes>0?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.error,children:[" ",deadPanes,"\\u2620"]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("scrollbox",{focused:!0,height:"100%",style:{scrollbarOptions:{showArrows:!1,trackOptions:{foregroundColor:palette.scrollThumb,backgroundColor:palette.scrollTrack}}},children:flatNodes.map((flat,i2)=>import_jsx_dev_runtime2.jsxDEV(TreeNodeRow,{node:flat.node,selected:i2===selectedIndex,onSelect:handleSelect,onToggle:handleToggle},flat.node.id,!1,void 0,this))},void 0,!1,void 0,this):import_jsx_dev_runtime2.jsxDEV("box",{flexGrow:1,justifyContent:"center",alignItems:"center",children:import_jsx_dev_runtime2.jsxDEV("text",{fg:palette.textDim,children:"Collecting..."},void 0,!1,void 0,this)},void 0,!1,void 0,this),import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textMuted,children:["\u2191\u2193",":nav ","\u2190\u2192",":expand Enter:attach"]},void 0,!0,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function mergeExpandedState(oldTree,newTree){if(oldTree.length===0)return newTree;let oldState=new Map;function collect(nodes){for(let n of nodes)oldState.set(n.id,n.expanded),collect(n.children)}collect(oldTree);function apply(nodes){return nodes.map((n)=>({...n,expanded:oldState.has(n.id)?oldState.get(n.id):n.expanded,children:apply(n.children)}))}return apply(newTree)}var import_react15;var init_Nav=__esm(async()=>{init_diagnostics();init_theme2();init_TreeNode();init_jsx_dev_runtime();await init_react();import_react15=__toESM(require_react_development(),1)});function App({rightPane}){let renderer=useRenderer();useKeyboard((key)=>{if(key.name==="q"||key.ctrl&&key.name==="c")renderer.destroy(),cleanup()});let handleTmuxSessionSelect=import_react17.useCallback((sessionName,windowIndex)=>{if(!rightPane)return;attachProjectWindow(rightPane,sessionName,windowIndex)},[rightPane]);return import_jsx_dev_runtime2.jsxDEV(Nav,{onTmuxSessionSelect:handleTmuxSessionSelect},void 0,!1,void 0,this)}var import_react17;var init_app=__esm(async()=>{init_tmux2();init_jsx_dev_runtime();await __promiseAll([init_react(),init_Nav()]);import_react17=__toESM(require_react_development(),1)});var exports_render={};__export(exports_render,{renderNav:()=>renderNav});async function renderNav(){let rightPane=process.env.GENIE_TUI_RIGHT||void 0,renderer=await createCliRenderer({exitOnCtrlC:!1,useMouse:!0});createRoot(renderer).render(import_jsx_dev_runtime2.jsxDEV(App,{rightPane},void 0,!1,void 0,this))}var init_render=__esm(async()=>{init_jsx_dev_runtime();await __promiseAll([init_core(),init_react(),init_app()])});var exports_tui={};__export(exports_tui,{launchTui:()=>launchTui});async function launchTui(options={}){if(isInsideTuiSession()){let{renderNav:renderNav2}=await init_render().then(() => exports_render);await renderNav2();return}if(!hasTmux()){console.error("Error: tmux is required for genie tui");return}let{session,leftPane,rightPane}=createTuiSession(),bunPath=process.execPath||"bun",genieBin=process.argv[1]||"genie",{execSync:execSync6}=await import("child_process");if(options.dev)execSync6(`tmux send-keys -t '${leftPane}' "GENIE_TUI_PANE=left GENIE_TUI_RIGHT=${rightPane} bun --watch ${genieBin} tui" Enter`,{stdio:"ignore"});else execSync6(`tmux send-keys -t '${leftPane}' "GENIE_TUI_PANE=left GENIE_TUI_RIGHT=${rightPane} ${bunPath} ${genieBin} tui" Enter`,{stdio:"ignore"});attachTuiSession(),cleanup(session)}var init_tui=__esm(()=>{init_tmux2()});function onBridgeEvent(handler){return listeners.add(handler),()=>{listeners.delete(handler)}}function emit2(event){for(let handler of listeners)handler(event)}async function listAgents2(){return(await getConnection())`
|
|
1287
|
+
`)){if(!line)continue;let parts=line.split("|");if(parts.length<15)continue;let parsed=parsePaneLine(parts);if(!sessionMap.has(parsed.sessionName))sessionMap.set(parsed.sessionName,{...parsed.session,windows:[]});let winKey=`${parsed.sessionName}:${parsed.winIdxStr}`;if(!windowMap.has(winKey)){let win={...parsed.window,panes:[]};windowMap.set(winKey,win),sessionMap.get(parsed.sessionName)?.windows.push(win)}windowMap.get(winKey)?.panes.push(parsed.pane)}return Array.from(sessionMap.values()).sort((a,b3)=>a.name.localeCompare(b3.name))}function isPidAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function allClaudePanes(sessions){return sessions.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.command==="claude"||p.title.includes("claude"))}function detectGaps(executors,sessions){let deadPidExecutors=executors.filter((e)=>e.pid!=null&&!isPidAlive(e.pid)),executorPaneIds=new Set(executors.map((e)=>e.tmuxPaneId).filter(Boolean)),claudePanes=allClaudePanes(sessions),orphanPanes=claudePanes.filter((p)=>!executorPaneIds.has(p.paneId)),linkedCount=executors.filter((e)=>e.tmuxPaneId&&!deadPidExecutors.some((d2)=>d2.id===e.id)).length,deadPaneCount=sessions.flatMap((s)=>s.windows.flatMap((w2)=>w2.panes)).filter((p)=>p.isDead).length;return{deadPidExecutors,orphanPanes,linkedCount,totalExecutors:executors.length,totalClaudePanes:claudePanes.length,deadPaneCount}}async function collectDiagnostics(){let{loadExecutors:loadExecutors2,loadAssignments:loadAssignments2}=await Promise.resolve().then(() => exports_db2),sessions=getTmuxInventory(),executors=await loadExecutors2(),executorIds=executors.map((e)=>e.id),assignments=await loadAssignments2(executorIds),gaps=detectGaps(executors,sessions);return{sessions,executors,assignments,gaps,timestamp:Date.now()}}var init_diagnostics=()=>{};function buildSessionTree(snapshot){let executorByPaneId=new Map;for(let exec3 of snapshot.executors)if(exec3.tmuxPaneId)executorByPaneId.set(exec3.tmuxPaneId,exec3);return snapshot.sessions.filter((s)=>s.name!=="genie-tui").map((session)=>sessionToNode(session,executorByPaneId))}function sessionToNode(session,executorMap){let claudePanes=session.windows.reduce((sum,w2)=>sum+w2.panes.filter((p)=>p.command==="claude"||p.title.includes("claude")).length,0);return{id:`session:${session.name}`,type:"session",label:session.name,depth:0,expanded:!0,children:session.windows.map((w2)=>windowToNode(session.name,w2,executorMap)),data:{attached:session.attached,windowCount:session.windowCount},activePanes:claudePanes,agentState:void 0}}function windowToNode(sessionName,window2,executorMap){let activePanes=window2.panes.filter((p)=>!p.isDead&&(p.command==="claude"||p.title.includes("claude"))).length;return{id:`window:${sessionName}:${window2.index}`,type:"window",label:window2.name,depth:1,expanded:!0,children:window2.panes.map((p)=>paneToNode(sessionName,window2.index,p,executorMap)),data:{active:window2.active,paneCount:window2.paneCount},activePanes,agentState:void 0}}function paneToNode(sessionName,windowIndex,pane,executorMap){let executor=executorMap.get(pane.paneId),isClaude=pane.command==="claude"||pane.title.includes("claude");return{id:`pane:${pane.paneId}`,type:"pane",label:derivePaneLabel(pane,executor,isClaude),depth:2,expanded:!1,children:[],data:{command:pane.command,isDead:pane.isDead,pid:pane.pid,size:pane.size,paneId:pane.paneId,sessionName,windowIndex},activePanes:0,agentState:derivePaneState(pane,executor)}}function derivePaneLabel(pane,executor,isClaude){if(executor?.agentName&&executor?.team)return`${executor.team}/${executor.agentName}`;if(executor?.agentName)return executor.agentName;if(isClaude)return"claude";return pane.command}function derivePaneState(pane,executor){if(pane.isDead)return"error";if(!executor)return;let s=executor.state;if(s==="working")return"working";if(s==="idle"||s==="spawning")return"idle";if(s==="permission")return"permission";if(s==="error"||s==="terminated")return"error";return}function getSessionTarget(node){if(node.type==="session")return{sessionName:node.label};if(node.type==="window"){let parts=node.id.split(":");return{sessionName:parts[1],windowIndex:Number(parts[2])}}if(node.type==="pane"){let data=node.data;return{sessionName:data.sessionName,windowIndex:data.windowIndex}}return null}function flattenTree(nodes){let result=[];function walk(node,depth){if(result.push({node,depth,visible:!0}),node.expanded)for(let child of node.children)walk(child,depth+1)}for(let node of nodes)walk(node,0);return result}function toggleNode(nodes,id){return nodes.map((node)=>{if(node.id===id)return{...node,expanded:!node.expanded};return{...node,children:toggleNode(node.children,id)}})}var import_jsx_dev_runtime2;var init_jsx_dev_runtime=__esm(()=>{import_jsx_dev_runtime2=__toESM(require_react_jsx_dev_runtime_development(),1)});function getNodeIcon(node){switch(node.type){case"session":return node.data.attached?"\u25B6":"\u25B8";case"window":return node.data.active?"\u25A0":"\u25A1";case"pane":return getPaneIcon(node);default:return" "}}function getPaneIcon(node){if(node.data.isDead)return"\u2718";if(node.agentState==="working")return"\u25CF";if(node.agentState==="idle")return"\u25CB";if(node.agentState==="permission")return"\u26A0";if(node.agentState==="error")return"\u2718";if(node.data.command==="claude")return"\u25C6";return"\u25CB"}function getNodeColor(node){switch(node.type){case"session":return node.data.attached?palette.emerald:palette.textDim;case"window":return node.data.active?palette.cyan:palette.text;case"pane":return getPaneColor(node);default:return palette.text}}function getPaneColor(node){if(node.data.isDead)return palette.error;if(node.agentState==="working")return palette.cyan;if(node.agentState==="permission")return palette.warning;if(node.agentState==="error")return palette.error;if(node.agentState==="idle")return palette.textDim;if(node.data.command==="claude")return palette.cyan;return palette.textDim}function getStateColor(state){switch(state){case"working":return palette.cyan;case"idle":return palette.textDim;case"permission":return palette.warning;case"error":return palette.error;default:return palette.textMuted}}var import_react13,TreeNodeRow;var init_TreeNode=__esm(()=>{init_theme2();init_jsx_dev_runtime();import_react13=__toESM(require_react_development(),1),TreeNodeRow=import_react13.memo(function({node,selected,onSelect,onToggle}){let indent=" ".repeat(node.depth),hasChildren=node.children.length>0,expandIcon=hasChildren?node.expanded?icons.expanded:icons.collapsed:" ",icon=getNodeIcon(node),color=getNodeColor(node),activeIndicator=node.activePanes>0?` ${icons.agent}${node.activePanes}`:"";return import_jsx_dev_runtime2.jsxDEV("box",{height:1,width:"100%",backgroundColor:selected?palette.violet:void 0,onMouseDown:()=>{if(onSelect(node.id),hasChildren)onToggle(node.id)},children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[indent,expandIcon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:color,children:[icon," "]},void 0,!0,void 0,this),import_jsx_dev_runtime2.jsxDEV("span",{fg:selected?"#ffffff":palette.text,children:node.label},void 0,!1,void 0,this),activeIndicator?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.cyan,children:activeIndicator},void 0,!1,void 0,this):null,node.agentState?import_jsx_dev_runtime2.jsxDEV("span",{fg:getStateColor(node.agentState),children:[" ",node.agentState]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this)})});function Nav({onTmuxSessionSelect}){let[diagnostics,setDiagnostics]=import_react15.useState(null),[sessionTree,setSessionTree]=import_react15.useState([]),[selectedIndex,setSelectedIndex]=import_react15.useState(0),lastTarget=import_react15.useRef(null);import_react15.useEffect(()=>{let active=!0;async function refresh(){try{let snap=await collectDiagnostics();if(active)setDiagnostics(snap)}catch(err){console.error("TUI: diagnostics failed:",err)}}refresh();let timer2=setInterval(refresh,2000);return()=>{active=!1,clearInterval(timer2)}},[]),import_react15.useEffect(()=>{if(!diagnostics)return;let newTree=buildSessionTree(diagnostics);setSessionTree((prev)=>mergeExpandedState(prev,newTree))},[diagnostics]);let flatNodes=import_react15.useMemo(()=>flattenTree(sessionTree),[sessionTree]);import_react15.useEffect(()=>{if(flatNodes.length>0&&selectedIndex>=flatNodes.length)setSelectedIndex(flatNodes.length-1)},[flatNodes.length,selectedIndex]),import_react15.useEffect(()=>{let current=flatNodes[selectedIndex]?.node;if(!current)return;let target=getSessionTarget(current);if(!target)return;let key=`${target.sessionName}:${target.windowIndex??""}`;if(key===lastTarget.current)return;lastTarget.current=key,onTmuxSessionSelect(target.sessionName,target.windowIndex)},[selectedIndex,flatNodes,onTmuxSessionSelect]);let handleSelect=import_react15.useCallback((id)=>{let idx=flatNodes.findIndex((n)=>n.node.id===id);if(idx>=0)setSelectedIndex(idx)},[flatNodes]),handleToggle=import_react15.useCallback((id)=>{setSessionTree((prev)=>toggleNode(prev,id))},[]),handleVerticalNav=import_react15.useCallback((keyName2)=>{let rowCount=flatNodes.length;if(rowCount===0)return;if(keyName2==="up"||keyName2==="k")setSelectedIndex((prev)=>prev===0?rowCount-1:prev-1);else if(keyName2==="down"||keyName2==="j")setSelectedIndex((prev)=>prev>=rowCount-1?0:prev+1)},[flatNodes.length]),handleExpandCollapse=import_react15.useCallback((keyName2)=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if((keyName2==="right"||keyName2==="l")&&node.children.length>0&&!node.expanded)handleToggle(node.id);else if((keyName2==="left"||keyName2==="h")&&node.expanded)handleToggle(node.id)},[flatNodes,selectedIndex,handleToggle]),handleEnter=import_react15.useCallback(()=>{let node=flatNodes[selectedIndex]?.node;if(!node)return;if(node.children.length>0)handleToggle(node.id);let target=getSessionTarget(node);if(target)onTmuxSessionSelect(target.sessionName,target.windowIndex)},[flatNodes,selectedIndex,handleToggle,onTmuxSessionSelect]);useKeyboard((key)=>{if(key.name==="up"||key.name==="k"||key.name==="down"||key.name==="j")handleVerticalNav(key.name);else if(key.name==="right"||key.name==="l"||key.name==="left"||key.name==="h")handleExpandCollapse(key.name);else if(key.name==="enter"||key.name==="return")handleEnter()});let sessionCount=diagnostics?.sessions.length??0,totalPanes=diagnostics?.sessions.reduce((sum,s)=>sum+s.windows.reduce((ws,w2)=>ws+w2.panes.length,0),0)??0,deadPanes=diagnostics?.gaps.deadPaneCount??0;return import_jsx_dev_runtime2.jsxDEV("box",{flexDirection:"column",width:"100%",height:"100%",backgroundColor:palette.bg,children:[import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:[import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.purple,children:"Sessions"},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textDim,children:[" ",sessionCount,"s ",totalPanes,"p"]},void 0,!0,void 0,this):null,deadPanes>0?import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.error,children:[" ",deadPanes,"\\u2620"]},void 0,!0,void 0,this):null]},void 0,!0,void 0,this)},void 0,!1,void 0,this),diagnostics?import_jsx_dev_runtime2.jsxDEV("scrollbox",{focused:!0,height:"100%",style:{scrollbarOptions:{showArrows:!1,trackOptions:{foregroundColor:palette.scrollThumb,backgroundColor:palette.scrollTrack}}},children:flatNodes.map((flat,i2)=>import_jsx_dev_runtime2.jsxDEV(TreeNodeRow,{node:flat.node,selected:i2===selectedIndex,onSelect:handleSelect,onToggle:handleToggle},flat.node.id,!1,void 0,this))},void 0,!1,void 0,this):import_jsx_dev_runtime2.jsxDEV("box",{flexGrow:1,justifyContent:"center",alignItems:"center",children:import_jsx_dev_runtime2.jsxDEV("text",{fg:palette.textDim,children:"Collecting..."},void 0,!1,void 0,this)},void 0,!1,void 0,this),import_jsx_dev_runtime2.jsxDEV("box",{height:1,paddingX:1,backgroundColor:palette.bgLight,children:import_jsx_dev_runtime2.jsxDEV("text",{children:import_jsx_dev_runtime2.jsxDEV("span",{fg:palette.textMuted,children:["\u2191\u2193",":nav ","\u2190\u2192",":expand Enter:attach"]},void 0,!0,void 0,this)},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}function mergeExpandedState(oldTree,newTree){if(oldTree.length===0)return newTree;let oldState=new Map;function collect(nodes){for(let n of nodes)oldState.set(n.id,n.expanded),collect(n.children)}collect(oldTree);function apply(nodes){return nodes.map((n)=>({...n,expanded:oldState.has(n.id)?oldState.get(n.id):n.expanded,children:apply(n.children)}))}return apply(newTree)}var import_react15;var init_Nav=__esm(async()=>{init_diagnostics();init_theme2();init_TreeNode();init_jsx_dev_runtime();await init_react();import_react15=__toESM(require_react_development(),1)});function App({rightPane}){let renderer=useRenderer();useKeyboard((key)=>{if(key.ctrl&&key.name==="q"||key.ctrl&&key.name==="c")cleanup(),renderer.destroy()});let handleTmuxSessionSelect=import_react17.useCallback((sessionName,windowIndex)=>{if(!rightPane)return;attachProjectWindow(rightPane,sessionName,windowIndex)},[rightPane]);return import_jsx_dev_runtime2.jsxDEV(Nav,{onTmuxSessionSelect:handleTmuxSessionSelect},void 0,!1,void 0,this)}var import_react17;var init_app=__esm(async()=>{init_tmux2();init_jsx_dev_runtime();await __promiseAll([init_react(),init_Nav()]);import_react17=__toESM(require_react_development(),1)});var exports_render={};__export(exports_render,{renderNav:()=>renderNav});async function renderNav(){let rightPane=process.env.GENIE_TUI_RIGHT||void 0,renderer=await createCliRenderer({exitOnCtrlC:!1,useMouse:!0});createRoot(renderer).render(import_jsx_dev_runtime2.jsxDEV(App,{rightPane},void 0,!1,void 0,this))}var init_render=__esm(async()=>{init_jsx_dev_runtime();await __promiseAll([init_core(),init_react(),init_app()])});var exports_tui={};__export(exports_tui,{launchTui:()=>launchTui});async function launchTui(options={}){if(isInsideTuiSession()){let{renderNav:renderNav2}=await init_render().then(() => exports_render);await renderNav2();return}if(!hasTmux()){console.error("Error: tmux is required for genie tui");return}let{session,leftPane,rightPane}=createTuiSession(),bunPath=process.execPath||"bun",genieBin=process.argv[1]||"genie",{execSync:execSync6}=await import("child_process");if(options.dev)execSync6(`tmux send-keys -t '${leftPane}' "GENIE_TUI_PANE=left GENIE_TUI_RIGHT=${rightPane} bun --watch ${genieBin} tui" Enter`,{stdio:"ignore"});else execSync6(`tmux send-keys -t '${leftPane}' "GENIE_TUI_PANE=left GENIE_TUI_RIGHT=${rightPane} ${bunPath} ${genieBin} tui" Enter`,{stdio:"ignore"});attachTuiSession(),cleanup(session)}var init_tui=__esm(()=>{init_tmux2()});function onBridgeEvent(handler){return listeners.add(handler),()=>{listeners.delete(handler)}}function emit2(event){for(let handler of listeners)handler(event)}async function listAgents2(){return(await getConnection())`
|
|
1288
1288
|
SELECT a.id, a.custom_name, a.role, a.team, a.title, a.state,
|
|
1289
1289
|
a.reports_to, a.current_executor_id, a.started_at
|
|
1290
1290
|
FROM agents a
|
|
@@ -1430,7 +1430,7 @@ $ bun add react-devtools-core@7 -d
|
|
|
1430
1430
|
VALUES (${name}, ${repoPath})
|
|
1431
1431
|
ON CONFLICT (name) DO UPDATE SET repo_path = COALESCE(projects.repo_path, EXCLUDED.repo_path)
|
|
1432
1432
|
RETURNING id
|
|
1433
|
-
`)[0].id}function toDateOrNull(v2){return v2?new Date(v2):null}async function resolveColumnId(sql,boardId,stageName){let rows=await sql`SELECT columns FROM boards WHERE id = ${boardId} LIMIT 1`;if(rows.length===0)return null;return rows[0].columns.find((c)=>c.name===stageName)?.id??null}function
|
|
1433
|
+
`)[0].id}function toDateOrNull(v2){return v2?new Date(v2):null}async function resolveColumnId(sql,boardId,stageName){let rows=await sql`SELECT columns FROM boards WHERE id = ${boardId} LIMIT 1`;if(rows.length===0)return null;return rows[0].columns.find((c)=>c.name===stageName)?.id??null}function taskNullables(input){return{desc:input.description??null,ac:input.acceptanceCriteria??null,parent:input.parentId??null,wish:input.wishFile??null,group:input.groupName??null,effort:input.estimatedEffort??null,blocked:input.blockedReason??null,release:input.releaseId??null,boardId:input.boardId??null,columnId:input.columnId??null,externalId:input.externalId??null,externalUrl:input.externalUrl??null}}function buildTaskVals(input){return{...taskNullables(input),type:input.typeId??"software",stage:input.stage??"draft",status:input.status??"ready",priority:input.priority??"normal"}}async function createTask(input,repoPath,projectId){let sql=await getConnection(),repo=repoPath??getRepoPath(),projId=projectId??await ensureProject(repo),vals=buildTaskVals(input);if(vals.boardId&&!vals.columnId&&vals.stage)vals.columnId=await resolveColumnId(sql,vals.boardId,vals.stage);let rows=await sql`
|
|
1434
1434
|
INSERT INTO tasks (
|
|
1435
1435
|
repo_path, project_id, title, description, acceptance_criteria,
|
|
1436
1436
|
type_id, stage, status, priority,
|
|
@@ -2067,9 +2067,9 @@ Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.
|
|
|
2067
2067
|
`+` genie task status <slug> \u2014 check progress
|
|
2068
2068
|
`+" genie agent send --to <a> \u2014 communicate directly"},{test:/sleep\s+\d+\s*&&\s*.*(?:capture-pane|tmux\s+list)/,message:`Consider using genie primitives instead of terminal polling:
|
|
2069
2069
|
genie task status <slug>
|
|
2070
|
-
genie events list --since 5m`}];async function orchestrationGuard(payload){let command=payload.tool_input?.command;if(typeof command!=="string")return;for(let{test,message}of NUDGE_PATTERNS)if(test.test(command))return{
|
|
2071
|
-
`)[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)}`;default:return name}}init_types3();var handlers=[{name:"branch-guard",event:"PreToolUse",matcher:/^Bash$/,priority:1,fn:branchGuard},{name:"orchestration-guard",event:"PreToolUse",matcher:/^Bash$/,priority:2,fn:orchestrationGuard},{name:"identity-inject",event:"PreToolUse",matcher:/^SendMessage$/,priority:10,fn:identityInject},{name:"auto-spawn",event:"PreToolUse",matcher:/^SendMessage$/,priority:20,fn:autoSpawn},{name:"runtime-emit-tool",event:"PreToolUse",matcher:/.*/,priority:30,fn:emitToolCallEvent},{name:"runtime-emit-msg",event:"PostToolUse",matcher:/^SendMessage$/,priority:30,fn:emitMessageEvent},{name:"runtime-emit-user-prompt",event:"UserPromptSubmit",priority:30,fn:emitUserPromptEvent},{name:"runtime-emit-assistant-response",event:"Stop",priority:30,fn:emitAssistantResponseEvent}];function resolveHandlers(event,toolName){return handlers.filter((h)=>{if(h.event!==event)return!1;if(h.matcher&&toolName&&!h.matcher.test(toolName))return!1;if(h.matcher&&!toolName)return!1;return!0}).sort((a,b2)=>a.priority-b2.priority)}async function runHandler(handler,payload,currentInput){let handlerPayload={...payload};if(currentInput)handlerPayload.tool_input=currentInput;try{return await handler.fn(handlerPayload)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(`[genie-hook] Handler "${handler.name}" threw: ${msg}`);return}}async function executeBlockingChain(matched,payload){let currentInput=payload.tool_input?{...payload.tool_input}:void 0,
|
|
2072
|
-
`);return response}async function executeNonBlockingHandlers(matched,payload){await Promise.allSettled(matched.map((h)=>h.fn(payload).catch((err)=>{let msg=err instanceof Error?err.message:String(err);console.error(`[genie-hook] Handler "${h.name}" threw: ${msg}`)})))}async function dispatch(stdin){let payload;try{payload=JSON.parse(stdin)}catch{return console.error("[genie-hook] Invalid JSON on stdin"),""}let event=payload.hook_event_name;if(!event)return console.error("[genie-hook] Missing hook_event_name in payload"),"";let toolName=payload.tool_name,matched=resolveHandlers(event,toolName);if(matched.length===0)return"";if(isBlockingEvent(event)){let result=await executeBlockingChain(matched,payload);if(
|
|
2070
|
+
genie events list --since 5m`}];async function orchestrationGuard(payload){let command=payload.tool_input?.command;if(typeof command!=="string")return;for(let{test,message}of NUDGE_PATTERNS)if(test.test(command))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",additionalContext:`[orchestration-guard] ${message}`}};return}var getAgent2=()=>process.env.GENIE_AGENT_NAME??"unknown",getTeam=()=>process.env.GENIE_TEAM;async function emit(subject,event){try{let{publishSubjectEvent:publishSubjectEvent2}=await Promise.resolve().then(() => (init_runtime_events(),exports_runtime_events));await publishSubjectEvent2(process.cwd(),subject,event)}catch{}}async function emitToolCallEvent(payload){let{tool_name:toolName,tool_input:input}=payload;if(!toolName||!input)return;await emit(`genie.tool.${getAgent2()}.call`,{timestamp:new Date().toISOString(),kind:"tool_call",agent:getAgent2(),team:getTeam(),text:summarizeToolCall(toolName,input),data:{toolCall:{name:toolName,input}},source:"hook"});return}async function emitMessageEvent(payload){let input=payload.tool_input;if(!input)return;let msgType=input.type;if(msgType&&msgType!=="message"&&msgType!=="broadcast")return;let to=input.to,content=input.content??input.message;if(!to||!content)return;let subject=msgType==="broadcast"?"genie.msg.broadcast":`genie.msg.${to}`;await emit(subject,{timestamp:new Date().toISOString(),kind:"message",agent:getAgent2(),team:getTeam(),peer:to,direction:"out",text:content,source:"hook"});return}async function emitUserPromptEvent(payload){let prompt2=payload.prompt;if(!prompt2)return;await emit(`genie.user.${getAgent2()}.prompt`,{timestamp:new Date().toISOString(),kind:"user",agent:getAgent2(),team:getTeam(),text:prompt2,source:"hook"});return}async function emitAssistantResponseEvent(payload){let lastMessage=payload.last_assistant_message;if(!lastMessage)return;await emit(`genie.agent.${getAgent2()}.response`,{timestamp:new Date().toISOString(),kind:"assistant",agent:getAgent2(),team:getTeam(),text:lastMessage,source:"hook"});return}function summarizeToolCall(name,input){switch(name){case"Read":case"Edit":case"Write":return`${name} ${input.file_path??""}`;case"Bash":return`$ ${String(input.command??"").split(`
|
|
2071
|
+
`)[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)}`;default:return name}}init_types3();var handlers=[{name:"branch-guard",event:"PreToolUse",matcher:/^Bash$/,priority:1,fn:branchGuard},{name:"orchestration-guard",event:"PreToolUse",matcher:/^Bash$/,priority:2,fn:orchestrationGuard},{name:"identity-inject",event:"PreToolUse",matcher:/^SendMessage$/,priority:10,fn:identityInject},{name:"auto-spawn",event:"PreToolUse",matcher:/^SendMessage$/,priority:20,fn:autoSpawn},{name:"runtime-emit-tool",event:"PreToolUse",matcher:/.*/,priority:30,fn:emitToolCallEvent},{name:"runtime-emit-msg",event:"PostToolUse",matcher:/^SendMessage$/,priority:30,fn:emitMessageEvent},{name:"runtime-emit-user-prompt",event:"UserPromptSubmit",priority:30,fn:emitUserPromptEvent},{name:"runtime-emit-assistant-response",event:"Stop",priority:30,fn:emitAssistantResponseEvent}];function resolveHandlers(event,toolName){return handlers.filter((h)=>{if(h.event!==event)return!1;if(h.matcher&&toolName&&!h.matcher.test(toolName))return!1;if(h.matcher&&!toolName)return!1;return!0}).sort((a,b2)=>a.priority-b2.priority)}async function runHandler(handler,payload,currentInput){let handlerPayload={...payload};if(currentInput)handlerPayload.tool_input=currentInput;try{return await handler.fn(handlerPayload)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(`[genie-hook] Handler "${handler.name}" threw: ${msg}`);return}}async function executeBlockingChain(matched,payload){let currentInput=payload.tool_input?{...payload.tool_input}:void 0,contextMessages=[],hookEventName=payload.hook_event_name;for(let handler of matched){let result=await runHandler(handler,payload,currentInput);if(!result)continue;if(result.decision==="deny"){if(hookEventName==="PreToolUse")return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:result.reason??`Denied by handler: ${handler.name}`}};return{decision:"block",reason:result.reason??`Denied by handler: ${handler.name}`}}if(result.hookSpecificOutput?.additionalContext)contextMessages.push(result.hookSpecificOutput.additionalContext);let inputUpdate=result.hookSpecificOutput?.updatedInput??result.updatedInput;if(inputUpdate)currentInput={...currentInput,...inputUpdate}}let response={},hasContext=contextMessages.length>0,hasInputChange=currentInput&&payload.tool_input&&JSON.stringify(currentInput)!==JSON.stringify(payload.tool_input);if(hasInputChange)response.updatedInput=currentInput;if(hookEventName==="PreToolUse"&&(hasContext||hasInputChange)){let output={hookEventName:"PreToolUse"};if(hasContext)output.additionalContext=contextMessages.join(`
|
|
2072
|
+
`);if(hasInputChange)output.permissionDecision="allow",output.updatedInput=currentInput;response.hookSpecificOutput=output}return response}async function executeNonBlockingHandlers(matched,payload){await Promise.allSettled(matched.map((h)=>h.fn(payload).catch((err)=>{let msg=err instanceof Error?err.message:String(err);console.error(`[genie-hook] Handler "${h.name}" threw: ${msg}`)})))}async function dispatch(stdin){let payload;try{payload=JSON.parse(stdin)}catch{return console.error("[genie-hook] Invalid JSON on stdin"),""}let event=payload.hook_event_name;if(!event)return console.error("[genie-hook] Missing hook_event_name in payload"),"";let toolName=payload.tool_name,matched=resolveHandlers(event,toolName);if(matched.length===0)return"";if(isBlockingEvent(event)){let result=await executeBlockingChain(matched,payload);if(Object.keys(result).length>0)return JSON.stringify(result);return""}return await executeNonBlockingHandlers(matched,payload),""}async function readStdin(){let chunks=[];for await(let chunk of Bun.stdin.stream())chunks.push(Buffer.from(chunk));return Buffer.concat(chunks).toString("utf-8")}async function dispatchAction(){let stdin=await readStdin();if(!stdin.trim())process.exit(0);let result=await dispatch(stdin);if(result)process.stdout.write(result)}function registerHookNamespace(program2){program2.command("hook").description("Hook middleware for Claude Code integration").command("dispatch").description("Dispatch a CC hook event (reads JSON from stdin, writes decision to stdout)").action(dispatchAction)}init_audit();init_db();init_otel_receiver();init_agents();function registerAppCommand(program2){program2.command("app").description("Launch Genie desktop app (backend sidecar + views)").option("--backend-only","Start only the backend sidecar (IPC on stdin/stdout)").option("--tui","Fall back to terminal UI mode").option("--dev","Development mode").action(async(options)=>{if(options.tui){let{launchTui:launchTui2}=await Promise.resolve().then(() => (init_tui(),exports_tui));await launchTui2({dev:options.dev});return}if(options.backendOnly){await Promise.resolve().then(() => (init_src_backend(),exports_src_backend));return}let{existsSync:existsSync21}=await import("fs"),{join:join26,dirname:dirname8}=await import("path"),{execFileSync,execSync:execSync6}=await import("child_process"),appName="genie-desktop",rootDir=join26(dirname8(new URL(import.meta.url).pathname),"..",".."),searchPaths=[join26(rootDir,"packages","genie-app","src-tauri","target","release",appName),join26(rootDir,"packages","genie-app","src-tauri","target","debug",appName),join26(rootDir,"dist","app",appName),`/usr/local/bin/${appName}`],inPath=!1;try{execSync6(`which ${appName}`,{stdio:"ignore"}),inPath=!0}catch{}let tauriBin=searchPaths.find((p)=>existsSync21(p));if(tauriBin||inPath){console.log("\x1B[35m\u25C6 Genie App\x1B[0m Launching desktop...");try{execFileSync(tauriBin??appName,[],{stdio:"inherit"})}catch{}return}console.log("\x1B[35m\u25C6 Genie App\x1B[0m Starting backend sidecar..."),console.log("\x1B[2mDesktop binary not found \u2014 running in sidecar mode.\x1B[0m"),console.log("\x1B[2mPG bridge + PTY manager + IPC on stdin/stdout\x1B[0m"),console.log(`\x1B[2mUse --tui for terminal UI, or pipe to a frontend shell.\x1B[0m
|
|
2073
2073
|
`),await Promise.resolve().then(() => (init_src_backend(),exports_src_backend))})}init_audit();function padRight(str2,len){return str2.length>=len?str2:str2+" ".repeat(len-str2.length)}function truncate(str2,len){return str2.length<=len?str2:`${str2.slice(0,len-1)}\u2026`}function formatDate(iso){if(!iso)return"-";return new Date(iso).toLocaleDateString("en-US",{month:"short",day:"numeric"})}function formatRelativeTimestamp(ts3){let d2=new Date(ts3),diffMs=Date.now()-d2.getTime();if(diffMs<60000)return`${Math.floor(diffMs/1000)}s ago`;if(diffMs<3600000)return`${Math.floor(diffMs/60000)}m ago`;if(diffMs<86400000)return`${Math.floor(diffMs/3600000)}h ago`;return d2.toISOString().replace("T"," ").slice(0,19)}function formatTimestamp(iso,opts){if(!iso)return opts?.fallback??"-";let d2=iso instanceof Date?iso:new Date(iso),fmt={month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",hour12:!1};if(opts?.seconds)fmt.second="2-digit";return d2.toLocaleString("en-US",fmt)}function formatTime(iso,opts){try{let date=new Date(iso),fmt={hour:"2-digit",minute:"2-digit",hour12:!1};if(opts?.seconds)fmt.second="2-digit";return date.toLocaleTimeString("en-US",fmt)}catch{return opts?.fallback??"??:??"}}function printEventsTable(rows){if(rows.length===0){console.log("No audit events found.");return}let sorted=[...rows].reverse(),headers=["Time","Type","Entity","Event","Actor","Details"],data=sorted.map((r)=>[formatRelativeTimestamp(r.created_at),r.entity_type,r.entity_id,r.event_type,r.actor??"-",summarizeDetails(r.details)]),widths=headers.map((h2,i2)=>{let colVals=data.map((row)=>row[i2]);return Math.min(40,Math.max(h2.length,...colVals.map((v2)=>v2.length)))}),header=headers.map((h2,i2)=>padRight(h2,widths[i2])).join(" | ");console.log(header),console.log(widths.map((w2)=>"-".repeat(w2)).join("-+-"));for(let row of data){let line=row.map((v2,i2)=>padRight(v2.slice(0,widths[i2]),widths[i2])).join(" | ");console.log(line)}console.log(`
|
|
2074
2074
|
(${rows.length} event${rows.length===1?"":"s"})`)}function summarizeDetails(details){if(!details)return"";if(typeof details==="string")try{return summarizeDetails(JSON.parse(details))}catch{return details.slice(0,40)}if(Object.keys(details).length===0)return"";let keys=Object.keys(details);if(keys.length===1){let val=details[keys[0]];if(typeof val==="string")return val.slice(0,40);return JSON.stringify(val).slice(0,40)}if(details.error)return`error: ${String(details.error).slice(0,35)}`;if(details.duration_ms)return`${details.duration_ms}ms`;return JSON.stringify(details).slice(0,40)}function printErrorsTable(patterns2){if(patterns2.length===0){console.log("No error patterns found.");return}let headers=["Count","Event","Command","Error","Last Seen"],data=patterns2.map((p)=>[String(p.count),p.event_type,p.entity_id,p.error_message.slice(0,50),formatRelativeTimestamp(p.last_seen)]),widths=headers.map((h2,i2)=>{let colVals=data.map((row)=>row[i2]);return Math.min(50,Math.max(h2.length,...colVals.map((v2)=>v2.length)))}),header=headers.map((h2,i2)=>padRight(h2,widths[i2])).join(" | ");console.log(header),console.log(widths.map((w2)=>"-".repeat(w2)).join("-+-"));for(let row of data){let line=row.map((v2,i2)=>padRight(v2.slice(0,widths[i2]),widths[i2])).join(" | ");console.log(line)}console.log(`
|
|
2075
2075
|
(${patterns2.length} pattern${patterns2.length===1?"":"s"})`)}async function eventsListCommand(options){try{let queryOpts={type:options.type,entity:options.entity,since:options.since??"1h",errorsOnly:options.errorsOnly,limit:options.limit?Number.parseInt(options.limit,10):50},rows=await queryAuditEvents(queryOpts);if(options.json)console.log(JSON.stringify(rows,null,2));else printEventsTable(rows)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(`Error querying events: ${msg}`),process.exit(1)}}async function eventsErrorsCommand(options){try{let patterns2=await queryErrorPatterns(options.since);if(options.json)console.log(JSON.stringify(patterns2,null,2));else printErrorsTable(patterns2)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(`Error querying error patterns: ${msg}`),process.exit(1)}}function resolveCostsGroupBy(options){if(options.byWish)return"wish";if(options.byModel)return"model";return"agent"}function printCostsTable(rows,groupBy){if(rows.length===0){console.log("No cost data found.");return}let headers=[groupBy==="agent"?"Agent":groupBy==="wish"?"Wish":"Model","Total Cost","Requests","Avg Cost"],data=rows.map((r)=>[r.group_key,`$${r.total_cost.toFixed(4)}`,String(r.request_count),`$${r.avg_cost.toFixed(4)}`]),widths=headers.map((h2,i2)=>{let colVals=data.map((row)=>row[i2]);return Math.min(40,Math.max(h2.length,...colVals.map((v2)=>v2.length)))});console.log(headers.map((h2,i2)=>padRight(h2,widths[i2])).join(" | ")),console.log(widths.map((w2)=>"-".repeat(w2)).join("-+-"));for(let row of data)console.log(row.map((v2,i2)=>padRight(v2.slice(0,widths[i2]),widths[i2])).join(" | "));let totalCost=rows.reduce((sum,r)=>sum+r.total_cost,0),totalReqs=rows.reduce((sum,r)=>sum+r.request_count,0);console.log(`
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260329.
|
|
3
|
+
"version": "4.260329.26",
|
|
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"
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* loops to monitor workers. Suggests structured genie primitives but
|
|
6
6
|
* does NOT block — people may legitimately use tmux directly.
|
|
7
7
|
*
|
|
8
|
+
* Uses hookSpecificOutput.additionalContext to inject the nudge into
|
|
9
|
+
* Claude's context before the tool executes, with permissionDecision: "allow".
|
|
10
|
+
*
|
|
8
11
|
* Priority: 2 (runs after branch-guard, before identity-inject)
|
|
9
12
|
*/
|
|
10
13
|
|
|
@@ -47,9 +50,13 @@ export async function orchestrationGuard(payload: HookPayload): Promise<HandlerR
|
|
|
47
50
|
|
|
48
51
|
for (const { test, message } of NUDGE_PATTERNS) {
|
|
49
52
|
if (test.test(command)) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
return {
|
|
54
|
+
hookSpecificOutput: {
|
|
55
|
+
hookEventName: 'PreToolUse',
|
|
56
|
+
permissionDecision: 'allow',
|
|
57
|
+
additionalContext: `[orchestration-guard] ${message}`,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
|
package/src/hooks/index.ts
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
* Reads JSON from stdin, resolves matching handlers, executes the chain,
|
|
6
6
|
* and writes JSON result to stdout.
|
|
7
7
|
*
|
|
8
|
+
* Output format follows CC hook protocol:
|
|
9
|
+
* - PreToolUse: { hookSpecificOutput: { hookEventName, permissionDecision, ... } }
|
|
10
|
+
* - Other blocking: { decision: "block", reason: "..." }
|
|
11
|
+
* - Non-blocking: fire-and-forget, no output
|
|
12
|
+
*
|
|
8
13
|
* Blocking events (PreToolUse, TeammateIdle, TaskCompleted):
|
|
9
14
|
* Chain of responsibility — handlers run in priority order.
|
|
10
15
|
* - deny: short-circuits, returns immediately
|
|
16
|
+
* - hookSpecificOutput: collected, merged at end
|
|
11
17
|
* - updatedInput: merges into payload for next handler
|
|
12
18
|
* - allow/void: continues to next handler
|
|
13
19
|
*
|
|
@@ -25,7 +31,7 @@ import {
|
|
|
25
31
|
emitToolCallEvent,
|
|
26
32
|
emitUserPromptEvent,
|
|
27
33
|
} from './handlers/runtime-emit.js';
|
|
28
|
-
import type { Handler, HandlerResult,
|
|
34
|
+
import type { Handler, HandlerResult, HookPayload } from './types.js';
|
|
29
35
|
import { isBlockingEvent } from './types.js';
|
|
30
36
|
|
|
31
37
|
// ============================================================================
|
|
@@ -121,31 +127,62 @@ async function runHandler(
|
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
async function executeBlockingChain(matched: Handler[], payload: HookPayload): Promise<
|
|
130
|
+
async function executeBlockingChain(matched: Handler[], payload: HookPayload): Promise<Record<string, unknown>> {
|
|
125
131
|
let currentInput = payload.tool_input ? { ...payload.tool_input } : undefined;
|
|
126
|
-
const
|
|
132
|
+
const contextMessages: string[] = [];
|
|
133
|
+
const hookEventName = payload.hook_event_name;
|
|
127
134
|
|
|
128
135
|
for (const handler of matched) {
|
|
129
136
|
const result = await runHandler(handler, payload, currentInput);
|
|
130
137
|
if (!result) continue;
|
|
131
138
|
|
|
139
|
+
// Legacy deny format — convert to hookSpecificOutput for PreToolUse
|
|
132
140
|
if (result.decision === 'deny') {
|
|
133
|
-
|
|
141
|
+
if (hookEventName === 'PreToolUse') {
|
|
142
|
+
return {
|
|
143
|
+
hookSpecificOutput: {
|
|
144
|
+
hookEventName: 'PreToolUse',
|
|
145
|
+
permissionDecision: 'deny',
|
|
146
|
+
permissionDecisionReason: result.reason ?? `Denied by handler: ${handler.name}`,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { decision: 'block', reason: result.reason ?? `Denied by handler: ${handler.name}` };
|
|
134
151
|
}
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
|
|
153
|
+
// Collect hookSpecificOutput (additionalContext, etc.)
|
|
154
|
+
if (result.hookSpecificOutput?.additionalContext) {
|
|
155
|
+
contextMessages.push(result.hookSpecificOutput.additionalContext);
|
|
137
156
|
}
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
|
|
158
|
+
// Collect updatedInput from either format
|
|
159
|
+
const inputUpdate = result.hookSpecificOutput?.updatedInput ?? result.updatedInput;
|
|
160
|
+
if (inputUpdate) {
|
|
161
|
+
currentInput = { ...currentInput, ...inputUpdate };
|
|
140
162
|
}
|
|
141
163
|
}
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
// Build response
|
|
166
|
+
const response: Record<string, unknown> = {};
|
|
167
|
+
|
|
168
|
+
const hasContext = contextMessages.length > 0;
|
|
169
|
+
const hasInputChange =
|
|
170
|
+
currentInput && payload.tool_input && JSON.stringify(currentInput) !== JSON.stringify(payload.tool_input);
|
|
171
|
+
|
|
172
|
+
// updatedInput at top level (backward compat, used by tests + older CC versions)
|
|
173
|
+
if (hasInputChange) {
|
|
145
174
|
response.updatedInput = currentInput;
|
|
146
175
|
}
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
|
|
177
|
+
// For PreToolUse, ALSO emit hookSpecificOutput (the correct CC format)
|
|
178
|
+
if (hookEventName === 'PreToolUse' && (hasContext || hasInputChange)) {
|
|
179
|
+
const output: Record<string, unknown> = { hookEventName: 'PreToolUse' };
|
|
180
|
+
if (hasContext) output.additionalContext = contextMessages.join('\n');
|
|
181
|
+
if (hasInputChange) {
|
|
182
|
+
output.permissionDecision = 'allow';
|
|
183
|
+
output.updatedInput = currentInput;
|
|
184
|
+
}
|
|
185
|
+
response.hookSpecificOutput = output;
|
|
149
186
|
}
|
|
150
187
|
|
|
151
188
|
return response;
|
|
@@ -186,14 +223,12 @@ export async function dispatch(stdin: string): Promise<string> {
|
|
|
186
223
|
const matched = resolveHandlers(event, toolName);
|
|
187
224
|
|
|
188
225
|
if (matched.length === 0) {
|
|
189
|
-
// No handlers — implicit allow (empty stdout)
|
|
190
226
|
return '';
|
|
191
227
|
}
|
|
192
228
|
|
|
193
229
|
if (isBlockingEvent(event)) {
|
|
194
230
|
const result = await executeBlockingChain(matched, payload);
|
|
195
|
-
|
|
196
|
-
if (result.decision || result.updatedInput || result.systemMessage) {
|
|
231
|
+
if (Object.keys(result).length > 0) {
|
|
197
232
|
return JSON.stringify(result);
|
|
198
233
|
}
|
|
199
234
|
return '';
|
package/src/hooks/types.ts
CHANGED
|
@@ -43,11 +43,18 @@ export interface HookPayload {
|
|
|
43
43
|
|
|
44
44
|
/** Decision a PreToolUse handler can return. */
|
|
45
45
|
export interface HookDecision {
|
|
46
|
+
/** @deprecated Use hookSpecificOutput.permissionDecision instead for PreToolUse */
|
|
46
47
|
decision?: 'allow' | 'deny' | 'ask';
|
|
47
48
|
reason?: string;
|
|
48
49
|
updatedInput?: Record<string, unknown>;
|
|
49
|
-
/**
|
|
50
|
-
|
|
50
|
+
/** CC hookSpecificOutput — the correct format for PreToolUse decisions. */
|
|
51
|
+
hookSpecificOutput?: {
|
|
52
|
+
hookEventName: string;
|
|
53
|
+
permissionDecision?: 'allow' | 'deny' | 'ask';
|
|
54
|
+
permissionDecisionReason?: string;
|
|
55
|
+
additionalContext?: string;
|
|
56
|
+
updatedInput?: Record<string, unknown>;
|
|
57
|
+
};
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/** Result from a handler — either a decision or void (implicit allow). */
|
package/src/lib/task-service.ts
CHANGED
|
@@ -524,15 +524,11 @@ async function resolveColumnId(sql: Sql, boardId: string, stageName: string): Pr
|
|
|
524
524
|
return match?.id ?? null;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
/**
|
|
528
|
-
function
|
|
527
|
+
/** Nullable fields with null default. */
|
|
528
|
+
function taskNullables(input: TaskInput) {
|
|
529
529
|
return {
|
|
530
530
|
desc: input.description ?? null,
|
|
531
531
|
ac: input.acceptanceCriteria ?? null,
|
|
532
|
-
type: input.typeId ?? 'software',
|
|
533
|
-
stage: input.stage ?? 'draft',
|
|
534
|
-
status: input.status ?? 'ready',
|
|
535
|
-
priority: input.priority ?? 'normal',
|
|
536
532
|
parent: input.parentId ?? null,
|
|
537
533
|
wish: input.wishFile ?? null,
|
|
538
534
|
group: input.groupName ?? null,
|
|
@@ -546,6 +542,17 @@ function buildTaskVals(input: TaskInput) {
|
|
|
546
542
|
};
|
|
547
543
|
}
|
|
548
544
|
|
|
545
|
+
/** Extract and normalize task input fields with defaults. */
|
|
546
|
+
function buildTaskVals(input: TaskInput) {
|
|
547
|
+
return {
|
|
548
|
+
...taskNullables(input),
|
|
549
|
+
type: input.typeId ?? 'software',
|
|
550
|
+
stage: input.stage ?? 'draft',
|
|
551
|
+
status: input.status ?? 'ready',
|
|
552
|
+
priority: input.priority ?? 'normal',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
549
556
|
export async function createTask(input: TaskInput, repoPath?: string, projectId?: string): Promise<TaskRow> {
|
|
550
557
|
const sql = await getConnection();
|
|
551
558
|
const repo = repoPath ?? getRepoPath();
|
package/src/tui/app.tsx
CHANGED
|
@@ -10,9 +10,11 @@ export function App({ rightPane }: { rightPane?: string }) {
|
|
|
10
10
|
const renderer = useRenderer();
|
|
11
11
|
|
|
12
12
|
useKeyboard((key) => {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Ctrl+Q or Ctrl+C: kill the entire TUI (both panes)
|
|
14
|
+
if ((key.ctrl && key.name === 'q') || (key.ctrl && key.name === 'c')) {
|
|
15
|
+
// Kill tmux session FIRST (kills both panes), then destroy renderer
|
|
15
16
|
cleanup();
|
|
17
|
+
renderer.destroy();
|
|
16
18
|
}
|
|
17
19
|
});
|
|
18
20
|
|
package/src/tui/session-tree.ts
CHANGED
|
@@ -7,6 +7,9 @@ import type { DiagnosticSnapshot, TmuxPane, TmuxSession, TmuxWindow } from './di
|
|
|
7
7
|
import type { TreeNode, TuiExecutor } from './types.js';
|
|
8
8
|
|
|
9
9
|
/** Build a TreeNode tree from tmux sessions, enriched with executor state. */
|
|
10
|
+
/** The TUI's own session name — filtered from the tree to prevent self-attach loops */
|
|
11
|
+
const TUI_SESSION = 'genie-tui';
|
|
12
|
+
|
|
10
13
|
export function buildSessionTree(snapshot: DiagnosticSnapshot): TreeNode[] {
|
|
11
14
|
const executorByPaneId = new Map<string, TuiExecutor>();
|
|
12
15
|
for (const exec of snapshot.executors) {
|
|
@@ -15,7 +18,9 @@ export function buildSessionTree(snapshot: DiagnosticSnapshot): TreeNode[] {
|
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
return snapshot.sessions
|
|
21
|
+
return snapshot.sessions
|
|
22
|
+
.filter((s) => s.name !== TUI_SESSION)
|
|
23
|
+
.map((session) => sessionToNode(session, executorByPaneId));
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
function sessionToNode(session: TmuxSession, executorMap: Map<string, TuiExecutor>): TreeNode {
|