@automagik/genie 4.260324.18 → 4.260324.19
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 +4 -4
- 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/skills/docs/SKILL.md +19 -0
- package/skills/fix/SKILL.md +30 -0
- package/skills/learn/SKILL.md +30 -0
- package/skills/report/SKILL.md +42 -0
- package/skills/review/SKILL.md +33 -0
- package/skills/trace/SKILL.md +28 -0
- package/skills/work/SKILL.md +32 -0
- package/src/genie-commands/__tests__/update.test.ts +49 -0
- package/src/genie-commands/update.ts +53 -18
- package/src/term-commands/dispatch.test.ts +21 -0
- package/src/term-commands/dispatch.ts +1 -2
- package/src/term-commands/state.test.ts +71 -1
- package/src/term-commands/state.ts +34 -8
- package/src/term-commands/team.test.ts +45 -0
- package/src/term-commands/team.ts +14 -4
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.260324.
|
|
13
|
+
"version": "4.260324.19",
|
|
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
|
@@ -969,7 +969,7 @@ Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.
|
|
|
969
969
|
`:"";return writeFileSync3(filePath,newContent),!0}async function promptUninstallFrom(filePath,marker,label){if(!existsSync5(filePath))return;if(!contentExists(filePath,marker)){console.log(`\u2713 ${label} has no genie shortcuts`);return}if(await prompt(`Remove shortcuts from ${filePath}? [Y/n] `)==="n"){console.log(`\u23ED\uFE0F Skipped ${label}`);return}if(removeMarkedContent(filePath,marker))console.log(`\u2705 Removed from ${filePath}`)}async function uninstallShortcuts(){let home=homedir4(),marker="generated by genie-cli";console.log(`Uninstalling Warp-like shortcuts...
|
|
970
970
|
`),await promptUninstallFrom(join4(home,".tmux.conf"),"generated by genie-cli","tmux.conf");for(let shellRc of[join4(home,".zshrc"),join4(home,".bashrc")])await promptUninstallFrom(shellRc,"generated by genie-cli",shellRc);let termuxDir=join4(home,".termux"),isTermux=existsSync5(termuxDir)||process.env.TERMUX_VERSION;if(isTermux){let termuxProps=join4(termuxDir,"termux.properties");if(await promptUninstallFrom(termuxProps,"generated by genie-cli","termux.properties"),!contentExists(termuxProps,"generated by genie-cli"))console.log(" Run: termux-reload-settings")}if(console.log(`
|
|
971
971
|
\u2705 Uninstallation complete!`),console.log(`
|
|
972
|
-
Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.log(" 2. Restart your shell or run: source ~/.bashrc"),isTermux)console.log(" 3. Reload Termux: termux-reload-settings")}function printHeader(){console.log(),console.log(`\x1B[1m\x1B[36m${"=".repeat(64)}\x1B[0m`),console.log("\x1B[1m\x1B[36m Genie Setup Wizard\x1B[0m"),console.log(`\x1B[1m\x1B[36m${"=".repeat(64)}\x1B[0m`),console.log()}function printSection(title,description){if(console.log(),console.log(`\x1B[1m${title}\x1B[0m`),description)console.log(`\x1B[2m${description}\x1B[0m`);console.log()}async function configureSession(config,quick){if(printSection("2. Session Configuration","Configure tmux session settings"),quick)return console.log(` Using defaults: session="${config.session.name}", window="${config.session.defaultWindow}"`),config;let sessionName=await esm_default3({message:"Session name:",default:config.session.name}),defaultWindow=await esm_default3({message:"Default window name:",default:config.session.defaultWindow}),autoCreate=await esm_default2({message:"Auto-create session on connect?",default:config.session.autoCreate});return config.session={name:sessionName,defaultWindow,autoCreate},config}async function configureTerminal(config,quick){if(printSection("3. Terminal Defaults","Configure default values for term commands"),quick)return console.log(` Using defaults: timeout=${config.terminal.execTimeout}ms, lines=${config.terminal.readLines}`),config;let timeoutStr=await esm_default3({message:"Exec timeout (milliseconds):",default:String(config.terminal.execTimeout),validate:(v)=>{let n=Number.parseInt(v,10);return!Number.isNaN(n)&&n>0?!0:"Must be a positive number"}}),linesStr=await esm_default3({message:"Read lines (default for genie agent read):",default:String(config.terminal.readLines),validate:(v)=>{let n=Number.parseInt(v,10);return!Number.isNaN(n)&&n>0?!0:"Must be a positive number"}}),worktreeBase=await esm_default3({message:"Worktree base directory (leave empty for ~/.genie/worktrees/<project>/):",default:config.terminal.worktreeBase??""});return config.terminal={execTimeout:Number.parseInt(timeoutStr,10),readLines:Number.parseInt(linesStr,10),...worktreeBase?{worktreeBase}:{}},config}async function configureShortcuts(config,quick){printSection("4. Keyboard Shortcuts","Warp-like tmux shortcuts for quick navigation");let home=homedir5(),tmuxConf=join5(home,".tmux.conf");if(isShortcutsInstalled(tmuxConf))return console.log(" \x1B[32m\u2713\x1B[0m Tmux shortcuts already installed"),config.shortcuts.tmuxInstalled=!0,config;if(console.log(" Available shortcuts:"),console.log(" \x1B[36mCtrl+T\x1B[0m \u2192 New tab (window)"),console.log(" \x1B[36mCtrl+S\x1B[0m \u2192 Vertical split"),console.log(" \x1B[36mCtrl+H\x1B[0m \u2192 Horizontal split"),console.log(),quick)return console.log(" Skipped in quick mode. Run \x1B[36mgenie setup --shortcuts\x1B[0m to install."),config;if(await esm_default2({message:"Install tmux keyboard shortcuts?",default:!1}))console.log(),await installShortcuts(),config.shortcuts.tmuxInstalled=!0,await updateShortcutsConfig({tmuxInstalled:!0});else console.log(" Skipped. Run \x1B[36mgenie shortcuts install\x1B[0m later.");return config}function printCodexResult(result){if(result==="changed")console.log(" \x1B[32m\u2713\x1B[0m Codex config updated");else if(result==="unchanged")console.log(" \x1B[32m\u2713\x1B[0m Codex config already up to date");else console.log(" \x1B[31m\u2717\x1B[0m Failed to update codex config")}async function configureCodex(config,quick){printSection("5. Codex Integration","Configure OpenAI Codex for genie agents");let codexCheck=await checkCommand("codex");if(!codexCheck.exists)return console.log(" \x1B[33m!\x1B[0m Codex CLI not found. Skipping codex integration."),config;if(console.log(` \x1B[32m\u2713\x1B[0m Codex CLI found (${codexCheck.version??"unknown version"})`),isCodexConfigured())return console.log(" \x1B[32m\u2713\x1B[0m Codex config already configured"),config.codex={configured:!0},config;if(console.log(),console.log(" Genie needs to configure codex for agent communication:"),console.log(" \x1B[36mdisable_paste_burst\x1B[0m \u2192 Reliable tmux command injection"),console.log(" \x1B[36mOTel exporter\x1B[0m \u2192 Telemetry relay for state detection"),console.log(` Config: \x1B[2m${contractPath(getCodexConfigPath())}\x1B[0m`),console.log(),quick){let result=ensureCodexOtelConfig();return printCodexResult(result),config.codex={configured:result!=="error"},config}if(await esm_default2({message:"Configure Codex for genie agent integration?",default:!0})){let result=ensureCodexOtelConfig();printCodexResult(result),config.codex={configured:result!=="error"}}else console.log(" Skipped. Run \x1B[36mgenie setup --codex\x1B[0m later.");return config}async function configureDebug(config,quick){if(printSection("6. Debug Options","Logging and debugging settings"),quick)return console.log(" Using defaults: tmuxDebug=false, verbose=false"),config;let tmuxDebug=await esm_default2({message:"Enable tmux debug logging?",default:config.logging.tmuxDebug}),verbose=await esm_default2({message:"Enable verbose mode?",default:config.logging.verbose});return config.logging={tmuxDebug,verbose},config}async function configurePromptMode(config,quick){if(printSection("7. Prompt Mode","Controls how genie injects system prompts into Claude Code"),quick)return console.log(` Using default: promptMode="${config.promptMode}"`),config;console.log(" append \u2014 Uses --append-system-prompt-file (preserves Claude Code default system prompt)"),console.log(" system \u2014 Uses --system-prompt-file (replaces Claude Code default system prompt)"),console.log();let promptMode=await esm_default4({message:"Prompt mode:",choices:[{name:"append (recommended \u2014 preserves CC default)",value:"append"},{name:"system (replaces CC default)",value:"system"}],default:config.promptMode});return config.promptMode=promptMode,config}async function showSummaryAndSave(config){printSection("Summary",`Configuration will be saved to ${contractPath(getGenieConfigPath())}`),console.log(` Session: \x1B[36m${config.session.name}\x1B[0m (window: ${config.session.defaultWindow})`),console.log(` Terminal: timeout=${config.terminal.execTimeout}ms, lines=${config.terminal.readLines}`),console.log(` Shortcuts: ${config.shortcuts.tmuxInstalled?"\x1B[32minstalled\x1B[0m":"\x1B[2mnot installed\x1B[0m"}`),console.log(` Codex: ${config.codex?.configured?"\x1B[32mconfigured\x1B[0m":"\x1B[2mnot configured\x1B[0m"}`),console.log(` Debug: tmux=${config.logging.tmuxDebug}, verbose=${config.logging.verbose}`),console.log(` Prompt mode: \x1B[36m${config.promptMode}\x1B[0m`),console.log(),config.setupComplete=!0,config.lastSetupAt=new Date().toISOString(),await saveGenieConfig(config),console.log("\x1B[32m\u2713 Configuration saved!\x1B[0m")}async function showCurrentConfig(){let config=await loadGenieConfig();console.log(),console.log("\x1B[1mCurrent Genie Configuration\x1B[0m"),console.log(`\x1B[2m${contractPath(getGenieConfigPath())}\x1B[0m`),console.log(),console.log(JSON.stringify(config,null,2)),console.log()}function printNextSteps(){console.log(),console.log("\x1B[1mNext Steps:\x1B[0m"),console.log(),console.log(" Start a session: \x1B[36mgenie\x1B[0m"),console.log(" Watch AI work: \x1B[36mtmux attach -t genie\x1B[0m"),console.log(" Check health: \x1B[36mgenie doctor\x1B[0m"),console.log()}async function setupCommand(options={}){if(options.show){await showCurrentConfig();return}if(options.reset){await resetConfig(),console.log("\x1B[32m\u2713 Configuration reset to defaults.\x1B[0m"),console.log();return}let config=await loadGenieConfig();if(options.shortcuts){printHeader(),await configureShortcuts(config,!1),await markSetupComplete();return}if(options.terminal){printHeader(),config=await configureTerminal(config,!1),await saveGenieConfig(config),console.log("\x1B[32m\u2713 Terminal configuration saved.\x1B[0m");return}if(options.session){printHeader(),config=await configureSession(config,!1),await saveGenieConfig(config),console.log("\x1B[32m\u2713 Session configuration saved.\x1B[0m");return}if(options.codex){if(printHeader(),config=await configureCodex(config,!1),await saveGenieConfig(config),config.codex?.configured)console.log("\x1B[32m\u2713 Codex configuration saved.\x1B[0m");return}let quick=options.quick??!1;if(printHeader(),quick)console.log("\x1B[2mQuick mode: accepting all defaults\x1B[0m");config=await configureSession(config,quick),config=await configureTerminal(config,quick),config=await configureShortcuts(config,quick),config=await configureCodex(config,quick),config=await configureDebug(config,quick),config=await configurePromptMode(config,quick),await showSummaryAndSave(config),printNextSteps()}import{existsSync as existsSync6}from"fs";import{homedir as homedir6}from"os";import{join as join6}from"path";async function shortcutsShowCommand(){displayShortcuts();let home=homedir6(),tmuxConf=join6(home,".tmux.conf"),zshrc=join6(home,".zshrc"),bashrc=join6(home,".bashrc");if(console.log("Installation status:"),isShortcutsInstalled(tmuxConf))console.log(" \x1B[32m\u2713\x1B[0m tmux.conf");else console.log(" \x1B[33m-\x1B[0m tmux.conf");let shellRc=existsSync6(zshrc)?zshrc:bashrc;if(isShortcutsInstalled(shellRc))console.log(` \x1B[32m\u2713\x1B[0m ${shellRc.replace(home,"~")}`);else console.log(` \x1B[33m-\x1B[0m ${shellRc.replace(home,"~")}`);console.log(),console.log("Run \x1B[36mgenie shortcuts install\x1B[0m to install shortcuts."),console.log("Run \x1B[36mgenie shortcuts uninstall\x1B[0m to remove shortcuts."),console.log()}async function shortcutsInstallCommand(){await installShortcuts()}async function shortcutsUninstallCommand(){await uninstallShortcuts()}init_esm6();import{existsSync as existsSync7,lstatSync,rmSync,unlinkSync as unlinkSync2}from"fs";import{homedir as homedir7}from"os";import{join as join7}from"path";init_genie_config2();var ORCHESTRATION_RULES_PATH=join7(homedir7(),".claude","rules","genie-orchestration.md"),LOCAL_BIN=join7(homedir7(),".local","bin"),SYMLINKS=["genie","term"];function isGenieSymlink(path){try{if(!existsSync7(path))return!1;if(!lstatSync(path).isSymbolicLink())return!1;return!0}catch{return!1}}function removeSymlinks(){let removed=[];for(let name of SYMLINKS){let symlinkPath=join7(LOCAL_BIN,name);if(isGenieSymlink(symlinkPath))try{unlinkSync2(symlinkPath),removed.push(name)}catch{}}return removed}function tryRemoveStep(label,successMsg,fn){console.log(`\x1B[2m${label}\x1B[0m`);try{fn(),console.log(` \x1B[32m+\x1B[0m ${successMsg}`)}catch(error){let message=error instanceof Error?error.message:String(error);console.log(` \x1B[33m!\x1B[0m ${label.replace("...","")} failed: ${message}`)}}function performUninstall(hasHookScript,existingSymlinks,genieDir,hasGenieDir){if(hasHookScript)tryRemoveStep("Removing hook script...","Hook script removed",()=>removeHookScript());if(existingSymlinks.length>0){console.log("\x1B[2mRemoving symlinks...\x1B[0m");let removed=removeSymlinks();if(removed.length>0)console.log(` \x1B[32m+\x1B[0m Removed: ${removed.join(", ")}`)}if(existsSync7(ORCHESTRATION_RULES_PATH))tryRemoveStep("Removing orchestration rules...","Orchestration rules removed (~/.claude/rules/genie-orchestration.md)",()=>unlinkSync2(ORCHESTRATION_RULES_PATH));if(hasGenieDir)tryRemoveStep("Removing genie directory...","Directory removed",()=>rmSync(genieDir,{recursive:!0,force:!0}))}async function uninstallCommand(){console.log(),console.log("\x1B[1m\x1B[33m Uninstall Genie CLI\x1B[0m"),console.log();let genieDir=getGenieDir(),hasGenieDir=existsSync7(genieDir),hasHookScript=hookScriptExists(),hasOrchestrationRules=existsSync7(ORCHESTRATION_RULES_PATH),existingSymlinks=SYMLINKS.filter((name)=>isGenieSymlink(join7(LOCAL_BIN,name)));if(console.log("\x1B[2mThis will remove:\x1B[0m"),hasHookScript)console.log(" \x1B[31m-\x1B[0m Hook script (~/.claude/hooks/genie-bash-hook.sh)");if(hasOrchestrationRules)console.log(" \x1B[31m-\x1B[0m Orchestration rules (~/.claude/rules/genie-orchestration.md)");if(hasGenieDir)console.log(` \x1B[31m-\x1B[0m Genie directory (${contractPath(genieDir)})`);if(existingSymlinks.length>0)console.log(` \x1B[31m-\x1B[0m Symlinks from ~/.local/bin: ${existingSymlinks.join(", ")}`);if(console.log(),!hasGenieDir&&!hasHookScript&&!hasOrchestrationRules&&existingSymlinks.length===0){console.log("\x1B[33mNothing to uninstall.\x1B[0m"),console.log();return}if(!await esm_default2({message:"Are you sure you want to uninstall Genie CLI?",default:!1})){console.log(),console.log("\x1B[2mUninstall cancelled.\x1B[0m"),console.log();return}console.log(),performUninstall(hasHookScript,existingSymlinks,genieDir,hasGenieDir),console.log(),console.log("\x1B[32m+\x1B[0m Genie CLI uninstalled."),console.log(),console.log("\x1B[2mNote: If you installed via npm/bun, also run:\x1B[0m"),console.log(" \x1B[36mbun remove -g @automagik/genie\x1B[0m"),console.log(" \x1B[2mor\x1B[0m"),console.log(" \x1B[36mnpm uninstall -g @automagik/genie\x1B[0m"),console.log()}init_genie_config2();import{execSync,spawn}from"child_process";import{chmodSync,copyFileSync,existsSync as existsSync8,mkdirSync as mkdirSync4,readFileSync as readFileSync4,readdirSync,rmSync as rmSync2,writeFileSync as writeFileSync4}from"fs";import{chmod,copyFile,mkdir,unlink}from"fs/promises";import{homedir as homedir8}from"os";import{join as join8}from"path";var GENIE_HOME=process.env.GENIE_HOME||join8(homedir8(),".genie"),GENIE_SRC=join8(GENIE_HOME,"src"),GENIE_BIN=join8(GENIE_HOME,"bin"),LOCAL_BIN2=join8(homedir8(),".local","bin");function log(message){console.log(`\x1B[32m\u25B8\x1B[0m ${message}`)}function success(message){console.log(`\x1B[32m\u2714\x1B[0m ${message}`)}function error(message){console.log(`\x1B[31m\u2716\x1B[0m ${message}`)}async function runCommand(command,args,cwd){return new Promise((resolve)=>{let output=[],child=spawn(command,args,{cwd,stdio:["inherit","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}});child.stdout?.on("data",(data)=>{let str=data.toString();output.push(str),process.stdout.write(str)}),child.stderr?.on("data",(data)=>{let str=data.toString();output.push(str),process.stderr.write(str)}),child.on("close",(code)=>{resolve({success:code===0,output:output.join("")})}),child.on("error",(err)=>{error(err.message),resolve({success:!1,output:err.message})})})}async function getGitInfo(cwd){try{let branchResult=await runCommandSilent("git",["rev-parse","--abbrev-ref","HEAD"],cwd),commitResult=await runCommandSilent("git",["rev-parse","--short","HEAD"],cwd),dateResult=await runCommandSilent("git",["log","-1","--format=%ci"],cwd);if(branchResult.success&&commitResult.success&&dateResult.success)return{branch:branchResult.output.trim(),commit:commitResult.output.trim(),commitDate:dateResult.output.trim().split(" ")[0]}}catch{}return null}async function runCommandSilent(command,args,cwd){return new Promise((resolve)=>{let output=[],child=spawn(command,args,{cwd,stdio:["inherit","pipe","pipe"]});child.stdout?.on("data",(data)=>{output.push(data.toString())}),child.stderr?.on("data",(data)=>{output.push(data.toString())}),child.on("close",(code)=>{resolve({success:code===0,output:output.join("")})}),child.on("error",(err)=>{resolve({success:!1,output:err.message})})})}function detectFromBinaryPath(path){if(path.includes(".bun"))return"bun";if(path.includes("node_modules"))return"npm";if(path===join8(LOCAL_BIN2,"genie")||path.startsWith(GENIE_BIN))return"source";return null}async function detectInstallationType(){if(genieConfigExists())try{let config=await loadGenieConfig();if(config.installMethod)return config.installMethod}catch{}if(existsSync8(join8(GENIE_SRC,".git")))return"source";let result=await runCommandSilent("which",["genie"]);if(!result.success)return"unknown";let detected=detectFromBinaryPath(result.output.trim());if(detected)return detected;return(await runCommandSilent("which",["bun"])).success?"bun":"npm"}async function updateViaBun(channel){try{__require("fs").unlinkSync(join8(homedir8(),".bun","install","global","bun.lock"))}catch{}if(log(`Updating via bun (channel: ${channel})...`),!(await runCommand("bun",["add","-g","--force","--no-cache",`@automagik/genie@${channel}`])).success)error("Failed to update via bun"),process.exit(1);console.log(),success(`Genie CLI updated (${channel})!`)}async function updateViaNpm(channel){if(log(`Updating via npm (channel: ${channel})...`),!(await runCommand("npm",["install","-g",`@automagik/genie@${channel}`])).success)error("Failed to update via npm"),process.exit(1);console.log(),success(`Genie CLI updated (${channel})!`)}async function updateSource(){let beforeInfo=await getGitInfo(GENIE_SRC);if(beforeInfo)console.log(`Current: \x1B[2m${beforeInfo.branch}@${beforeInfo.commit} (${beforeInfo.commitDate})\x1B[0m`),console.log();if(log("Fetching latest changes..."),!(await runCommand("git",["fetch","origin"],GENIE_SRC)).success)error("Failed to fetch from origin"),process.exit(1);if(log("Resetting to origin/main..."),!(await runCommand("git",["reset","--hard","origin/main"],GENIE_SRC)).success)error("Failed to reset to origin/main"),process.exit(1);console.log();let afterInfo=await getGitInfo(GENIE_SRC);if(beforeInfo&&afterInfo&&beforeInfo.commit===afterInfo.commit){success("Already up to date!"),console.log();return}if(log("Installing dependencies..."),!(await runCommand("bun",["install"],GENIE_SRC)).success)error("Failed to install dependencies"),process.exit(1);if(console.log(),log("Building..."),!(await runCommand("bun",["run","build"],GENIE_SRC)).success)error("Failed to build"),process.exit(1);console.log(),log("Installing binaries...");try{await mkdir(GENIE_BIN,{recursive:!0}),await mkdir(LOCAL_BIN2,{recursive:!0});let binaries=["genie.js","term.js"],names=["genie","term"];for(let i=0;i<binaries.length;i++){let src=join8(GENIE_SRC,"dist",binaries[i]),binDest=join8(GENIE_BIN,binaries[i]),linkDest=join8(LOCAL_BIN2,names[i]);await copyFile(src,binDest),await chmod(binDest,493),await symlinkOrCopy(binDest,linkDest)}for(let legacy of["claudio.js","claudio"]){let legacyBin=join8(GENIE_BIN,legacy),legacyLink=join8(LOCAL_BIN2,legacy);try{await unlink(legacyBin)}catch{}try{await unlink(legacyLink)}catch{}}success("Binaries installed")}catch(err){error(`Failed to install binaries: ${err}`),process.exit(1)}if(console.log(),console.log("\x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m"),success("Genie CLI updated successfully!"),console.log(),afterInfo)console.log(`Version: \x1B[36m${afterInfo.branch}@${afterInfo.commit}\x1B[0m (${afterInfo.commitDate})`),console.log()}async function symlinkOrCopy(src,dest){let{symlink,unlink:unlink2}=await import("fs/promises");try{if(existsSync8(dest))await unlink2(dest);await symlink(src,dest)}catch{await copyFile(src,dest)}}function copyDirSync(src,dest){mkdirSync4(dest,{recursive:!0});for(let entry of readdirSync(src,{withFileTypes:!0})){let srcPath=join8(src,entry.name),destPath=join8(dest,entry.name);if(entry.isDirectory())copyDirSync(srcPath,destPath);else copyFileSync(srcPath,destPath)}}async function resolveGlobalPkgDir(installType){if(installType==="bun"){let bunPath=join8(homedir8(),".bun","install","global","node_modules","@automagik","genie");if(existsSync8(bunPath))return bunPath}if(installType==="npm"){let npmRootResult=await runCommandSilent("npm",["root","-g"]);if(npmRootResult.success){let npmPath=join8(npmRootResult.output.trim(),"@automagik","genie");if(existsSync8(npmPath))return npmPath}}let bunFallback=join8(homedir8(),".bun","install","global","node_modules","@automagik","genie");if(existsSync8(bunFallback))return bunFallback;let npmRootFallback=await runCommandSilent("npm",["root","-g"]);if(npmRootFallback.success){let npmPath=join8(npmRootFallback.output.trim(),"@automagik","genie");if(existsSync8(npmPath))return npmPath}return null}function updatePluginRegistry(claudePlugins,cacheDir,version){let registryPath=join8(claudePlugins,"installed_plugins.json");try{if(!existsSync8(registryPath))return;let registry=JSON.parse(readFileSync4(registryPath,"utf-8")),entries=registry.plugins?.["genie@automagik"];if(!Array.isArray(entries))return;for(let entry of entries)if(entry.scope==="user")entry.installPath=cacheDir,entry.version=version,entry.lastUpdated=new Date().toISOString();writeFileSync4(registryPath,JSON.stringify(registry,null,2))}catch(err){log(`Registry update failed (non-fatal): ${err}`)}}var GENIE_TMUX_HEADER="# Genie TUI \u2014 tmux configuration";function syncTmuxConf(tmuxScriptsSrc){let tmuxConfSrc=join8(tmuxScriptsSrc,"genie.tmux.conf"),tmuxConfDest=join8(homedir8(),".tmux.conf");if(!existsSync8(tmuxConfSrc)||!existsSync8(tmuxConfDest))return;try{if(!readFileSync4(tmuxConfDest,"utf-8").includes(GENIE_TMUX_HEADER))return;copyFileSync(tmuxConfSrc,tmuxConfDest),success("Updated ~/.tmux.conf (genie-managed)");try{execSync("tmux source-file ~/.tmux.conf",{stdio:"ignore"}),success("Reloaded tmux configuration")}catch{}}catch{}}function syncTmuxScripts(globalPkgDir){let tmuxScriptsSrc=join8(globalPkgDir,"scripts","tmux");if(!existsSync8(tmuxScriptsSrc))return;let scriptsDir=join8(GENIE_HOME,"scripts");mkdirSync4(scriptsDir,{recursive:!0});let scriptCount=0;for(let entry of readdirSync(tmuxScriptsSrc))if(entry.endsWith(".sh")||entry==="genie.tmux.conf"){let src=join8(tmuxScriptsSrc,entry),dest=join8(scriptsDir,entry);copyFileSync(src,dest);try{chmodSync(dest,entry.endsWith(".sh")?493:420)}catch{}scriptCount++}if(scriptCount>0)success(`Refreshed ${scriptCount} tmux scripts at ${scriptsDir}`);syncTmuxConf(tmuxScriptsSrc)}async function syncPlugin(installType){log("Syncing Claude Code plugin...");let globalPkgDir=await resolveGlobalPkgDir(installType);if(!globalPkgDir){log("Could not find installed package \u2014 skipping plugin sync");return}let pluginSrc=join8(globalPkgDir,"plugins","genie");if(!existsSync8(pluginSrc)){log("Plugin source not found in package \u2014 skipping plugin sync");return}let version;try{version=JSON.parse(readFileSync4(join8(globalPkgDir,"package.json"),"utf-8")).version}catch{log("Could not read package version \u2014 skipping plugin sync");return}let claudePlugins=join8(homedir8(),".claude","plugins"),cacheDir=join8(claudePlugins,"cache","automagik","genie",version);try{if(existsSync8(cacheDir))rmSync2(cacheDir,{recursive:!0,force:!0});copyDirSync(pluginSrc,cacheDir);let skillsSrc=join8(globalPkgDir,"skills");if(existsSync8(skillsSrc)&&!existsSync8(join8(cacheDir,"skills")))copyDirSync(skillsSrc,join8(cacheDir,"skills"))}catch(err){error(`Failed to copy plugin: ${err}`);return}updatePluginRegistry(claudePlugins,cacheDir,version),syncTmuxScripts(globalPkgDir),success(`Plugin synced to v${version}`)}async function resolveChannel(options){if(options.next)return"next";if(options.stable)return"latest";if(genieConfigExists())try{let config=await loadGenieConfig();if(config.updateChannel)return config.updateChannel}catch{}return"latest"}async function persistChannel(channel){try{let config=await loadGenieConfig();config.updateChannel=channel,await saveGenieConfig(config)}catch{}}async function updateCommand(options={}){console.log(),console.log("\x1B[1m\uD83E\uDDDE Genie CLI Update\x1B[0m"),console.log("\x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m"),console.log();let channel=await resolveChannel(options);if(options.next||options.stable)await persistChannel(channel);let installType=await detectInstallationType();if(log(`Detected installation: ${installType}`),log(`Channel: ${channel}${channel==="next"?" (dev builds)":" (stable)"}`),console.log(),installType==="unknown")error("No Genie CLI installation found"),console.log(),console.log("Install method not configured. Please reinstall genie:"),console.log("\x1B[36m curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash\x1B[0m"),console.log(),process.exit(1);switch(installType){case"source":await updateSource();break;case"bun":await updateViaBun(channel),await syncPlugin(installType);break;case"npm":await updateViaNpm(channel),await syncPlugin(installType);break}}import{existsSync as existsSync9,readFileSync as readFileSync5}from"fs";import{dirname,resolve}from"path";var __dirname="/home/runner/_work/genie/genie/src/lib",FALLBACK_VERSION="0.0.0-unknown";function readVersionFromPackageJson(){let candidates=[resolve(dirname(import.meta.dir??__dirname),"..","..","package.json"),resolve(dirname(import.meta.dir??__dirname),"..","package.json"),resolve(dirname(import.meta.dir??__dirname),"package.json")];for(let candidate of candidates)try{if(existsSync9(candidate)){let pkg=JSON.parse(readFileSync5(candidate,"utf-8"));if(pkg.version)return pkg.version}}catch{}return FALLBACK_VERSION}var VERSION=readVersionFromPackageJson();function buildSearchNames(recipient,dirEntry){let names=new Set([recipient]);if(dirEntry){if(names.add(dirEntry.entry.name),dirEntry.entry.roles)for(let role of dirEntry.entry.roles)names.add(role)}return names}function buildSpawnArgs(template){let args=["spawn","--provider",template.provider,"--team",template.team];if(template.role)args.push("--role",template.role);if(template.skill)args.push("--skill",template.skill);if(template.cwd)args.push("--cwd",template.cwd);if(template.extraArgs)args.push(...template.extraArgs);return args}async function autoSpawn(payload){let input=payload.tool_input;if(!input||input.type!=="message")return;let recipient=input.recipient;if(!recipient||recipient==="team-lead")return;let teamName=process.env.GENIE_TEAM??payload.team_name;if(!teamName)return;try{let registryMod=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry)),tmuxMod=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),directoryMod=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),existing=(await registryMod.list()).find((a)=>(a.role===recipient||a.id===recipient)&&a.team===teamName);if(existing&&await tmuxMod.isPaneAlive(existing.paneId))return;let dirEntry=await directoryMod.resolve(recipient),templates=await registryMod.listTemplates(),searchNames=buildSearchNames(recipient,dirEntry),template=templates.find((t)=>{if(t.team!==teamName)return!1;return[...searchNames].some((q)=>t.id===q||t.role===q)});if(!template){if(dirEntry)console.error(`[genie-hook] Agent "${recipient}" is registered in directory but has no spawn template in team "${teamName}".`);return}let{spawnSync}=__require("child_process");spawnSync("genie",buildSpawnArgs(template),{timeout:1e4,stdio:"ignore",env:{...process.env,GENIE_TEAM:teamName}}),console.error(`[genie-hook] Auto-spawned "${recipient}" in team "${teamName}"`)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(`[genie-hook] Auto-spawn failed for "${recipient}": ${msg}`)}}async function identityInject(payload){let input=payload.tool_input;if(!input)return;let msgType=input.type;if(msgType!=="message"&&msgType!=="broadcast")return;let agentName=process.env.GENIE_AGENT_NAME;if(!agentName)return;let content=input.content;if(!content)return;if(content.startsWith(`[from:${agentName}]`))return;return{updatedInput:{...input,content:`[from:${agentName}] ${content}`}}}var getAgent=()=>process.env.GENIE_AGENT_NAME??"unknown",getTeam=()=>process.env.GENIE_TEAM;async function emit(subject,event){try{let{publish:publish2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));await publish2(subject,event)}catch{}}async function natsEmitToolCall(payload){let{tool_name:toolName,tool_input:input}=payload;if(!toolName||!input)return;await emit(`genie.tool.${getAgent()}.call`,{timestamp:new Date().toISOString(),kind:"tool_call",agent:getAgent(),team:getTeam(),text:summarizeToolCall(toolName,input),data:{toolCall:{name:toolName,input}},source:"hook"});return}async function natsEmit(payload){let input=payload.tool_input;if(!input)return;let msgType=input.type;if(msgType!=="message"&&msgType!=="broadcast")return;let{to,content}=input;if(!to||!content)return;let subject=msgType==="broadcast"?"genie.msg.broadcast":`genie.msg.${to}`;await emit(subject,{timestamp:new Date().toISOString(),kind:"message",agent:getAgent(),peer:to,direction:"out",text:content,source:"hook"});return}async function natsEmitUserPrompt(payload){let prompt2=payload.prompt;if(!prompt2)return;await emit(`genie.user.${getAgent()}.prompt`,{timestamp:new Date().toISOString(),kind:"user",agent:getAgent(),team:getTeam(),text:prompt2,source:"hook"});return}async function natsEmitAssistantResponse(payload){let lastMessage=payload.last_assistant_message;if(!lastMessage)return;await emit(`genie.agent.${getAgent()}.response`,{timestamp:new Date().toISOString(),kind:"assistant",agent:getAgent(),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(`
|
|
972
|
+
Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.log(" 2. Restart your shell or run: source ~/.bashrc"),isTermux)console.log(" 3. Reload Termux: termux-reload-settings")}function printHeader(){console.log(),console.log(`\x1B[1m\x1B[36m${"=".repeat(64)}\x1B[0m`),console.log("\x1B[1m\x1B[36m Genie Setup Wizard\x1B[0m"),console.log(`\x1B[1m\x1B[36m${"=".repeat(64)}\x1B[0m`),console.log()}function printSection(title,description){if(console.log(),console.log(`\x1B[1m${title}\x1B[0m`),description)console.log(`\x1B[2m${description}\x1B[0m`);console.log()}async function configureSession(config,quick){if(printSection("2. Session Configuration","Configure tmux session settings"),quick)return console.log(` Using defaults: session="${config.session.name}", window="${config.session.defaultWindow}"`),config;let sessionName=await esm_default3({message:"Session name:",default:config.session.name}),defaultWindow=await esm_default3({message:"Default window name:",default:config.session.defaultWindow}),autoCreate=await esm_default2({message:"Auto-create session on connect?",default:config.session.autoCreate});return config.session={name:sessionName,defaultWindow,autoCreate},config}async function configureTerminal(config,quick){if(printSection("3. Terminal Defaults","Configure default values for term commands"),quick)return console.log(` Using defaults: timeout=${config.terminal.execTimeout}ms, lines=${config.terminal.readLines}`),config;let timeoutStr=await esm_default3({message:"Exec timeout (milliseconds):",default:String(config.terminal.execTimeout),validate:(v)=>{let n=Number.parseInt(v,10);return!Number.isNaN(n)&&n>0?!0:"Must be a positive number"}}),linesStr=await esm_default3({message:"Read lines (default for genie agent read):",default:String(config.terminal.readLines),validate:(v)=>{let n=Number.parseInt(v,10);return!Number.isNaN(n)&&n>0?!0:"Must be a positive number"}}),worktreeBase=await esm_default3({message:"Worktree base directory (leave empty for ~/.genie/worktrees/<project>/):",default:config.terminal.worktreeBase??""});return config.terminal={execTimeout:Number.parseInt(timeoutStr,10),readLines:Number.parseInt(linesStr,10),...worktreeBase?{worktreeBase}:{}},config}async function configureShortcuts(config,quick){printSection("4. Keyboard Shortcuts","Warp-like tmux shortcuts for quick navigation");let home=homedir5(),tmuxConf=join5(home,".tmux.conf");if(isShortcutsInstalled(tmuxConf))return console.log(" \x1B[32m\u2713\x1B[0m Tmux shortcuts already installed"),config.shortcuts.tmuxInstalled=!0,config;if(console.log(" Available shortcuts:"),console.log(" \x1B[36mCtrl+T\x1B[0m \u2192 New tab (window)"),console.log(" \x1B[36mCtrl+S\x1B[0m \u2192 Vertical split"),console.log(" \x1B[36mCtrl+H\x1B[0m \u2192 Horizontal split"),console.log(),quick)return console.log(" Skipped in quick mode. Run \x1B[36mgenie setup --shortcuts\x1B[0m to install."),config;if(await esm_default2({message:"Install tmux keyboard shortcuts?",default:!1}))console.log(),await installShortcuts(),config.shortcuts.tmuxInstalled=!0,await updateShortcutsConfig({tmuxInstalled:!0});else console.log(" Skipped. Run \x1B[36mgenie shortcuts install\x1B[0m later.");return config}function printCodexResult(result){if(result==="changed")console.log(" \x1B[32m\u2713\x1B[0m Codex config updated");else if(result==="unchanged")console.log(" \x1B[32m\u2713\x1B[0m Codex config already up to date");else console.log(" \x1B[31m\u2717\x1B[0m Failed to update codex config")}async function configureCodex(config,quick){printSection("5. Codex Integration","Configure OpenAI Codex for genie agents");let codexCheck=await checkCommand("codex");if(!codexCheck.exists)return console.log(" \x1B[33m!\x1B[0m Codex CLI not found. Skipping codex integration."),config;if(console.log(` \x1B[32m\u2713\x1B[0m Codex CLI found (${codexCheck.version??"unknown version"})`),isCodexConfigured())return console.log(" \x1B[32m\u2713\x1B[0m Codex config already configured"),config.codex={configured:!0},config;if(console.log(),console.log(" Genie needs to configure codex for agent communication:"),console.log(" \x1B[36mdisable_paste_burst\x1B[0m \u2192 Reliable tmux command injection"),console.log(" \x1B[36mOTel exporter\x1B[0m \u2192 Telemetry relay for state detection"),console.log(` Config: \x1B[2m${contractPath(getCodexConfigPath())}\x1B[0m`),console.log(),quick){let result=ensureCodexOtelConfig();return printCodexResult(result),config.codex={configured:result!=="error"},config}if(await esm_default2({message:"Configure Codex for genie agent integration?",default:!0})){let result=ensureCodexOtelConfig();printCodexResult(result),config.codex={configured:result!=="error"}}else console.log(" Skipped. Run \x1B[36mgenie setup --codex\x1B[0m later.");return config}async function configureDebug(config,quick){if(printSection("6. Debug Options","Logging and debugging settings"),quick)return console.log(" Using defaults: tmuxDebug=false, verbose=false"),config;let tmuxDebug=await esm_default2({message:"Enable tmux debug logging?",default:config.logging.tmuxDebug}),verbose=await esm_default2({message:"Enable verbose mode?",default:config.logging.verbose});return config.logging={tmuxDebug,verbose},config}async function configurePromptMode(config,quick){if(printSection("7. Prompt Mode","Controls how genie injects system prompts into Claude Code"),quick)return console.log(` Using default: promptMode="${config.promptMode}"`),config;console.log(" append \u2014 Uses --append-system-prompt-file (preserves Claude Code default system prompt)"),console.log(" system \u2014 Uses --system-prompt-file (replaces Claude Code default system prompt)"),console.log();let promptMode=await esm_default4({message:"Prompt mode:",choices:[{name:"append (recommended \u2014 preserves CC default)",value:"append"},{name:"system (replaces CC default)",value:"system"}],default:config.promptMode});return config.promptMode=promptMode,config}async function showSummaryAndSave(config){printSection("Summary",`Configuration will be saved to ${contractPath(getGenieConfigPath())}`),console.log(` Session: \x1B[36m${config.session.name}\x1B[0m (window: ${config.session.defaultWindow})`),console.log(` Terminal: timeout=${config.terminal.execTimeout}ms, lines=${config.terminal.readLines}`),console.log(` Shortcuts: ${config.shortcuts.tmuxInstalled?"\x1B[32minstalled\x1B[0m":"\x1B[2mnot installed\x1B[0m"}`),console.log(` Codex: ${config.codex?.configured?"\x1B[32mconfigured\x1B[0m":"\x1B[2mnot configured\x1B[0m"}`),console.log(` Debug: tmux=${config.logging.tmuxDebug}, verbose=${config.logging.verbose}`),console.log(` Prompt mode: \x1B[36m${config.promptMode}\x1B[0m`),console.log(),config.setupComplete=!0,config.lastSetupAt=new Date().toISOString(),await saveGenieConfig(config),console.log("\x1B[32m\u2713 Configuration saved!\x1B[0m")}async function showCurrentConfig(){let config=await loadGenieConfig();console.log(),console.log("\x1B[1mCurrent Genie Configuration\x1B[0m"),console.log(`\x1B[2m${contractPath(getGenieConfigPath())}\x1B[0m`),console.log(),console.log(JSON.stringify(config,null,2)),console.log()}function printNextSteps(){console.log(),console.log("\x1B[1mNext Steps:\x1B[0m"),console.log(),console.log(" Start a session: \x1B[36mgenie\x1B[0m"),console.log(" Watch AI work: \x1B[36mtmux attach -t genie\x1B[0m"),console.log(" Check health: \x1B[36mgenie doctor\x1B[0m"),console.log()}async function setupCommand(options={}){if(options.show){await showCurrentConfig();return}if(options.reset){await resetConfig(),console.log("\x1B[32m\u2713 Configuration reset to defaults.\x1B[0m"),console.log();return}let config=await loadGenieConfig();if(options.shortcuts){printHeader(),await configureShortcuts(config,!1),await markSetupComplete();return}if(options.terminal){printHeader(),config=await configureTerminal(config,!1),await saveGenieConfig(config),console.log("\x1B[32m\u2713 Terminal configuration saved.\x1B[0m");return}if(options.session){printHeader(),config=await configureSession(config,!1),await saveGenieConfig(config),console.log("\x1B[32m\u2713 Session configuration saved.\x1B[0m");return}if(options.codex){if(printHeader(),config=await configureCodex(config,!1),await saveGenieConfig(config),config.codex?.configured)console.log("\x1B[32m\u2713 Codex configuration saved.\x1B[0m");return}let quick=options.quick??!1;if(printHeader(),quick)console.log("\x1B[2mQuick mode: accepting all defaults\x1B[0m");config=await configureSession(config,quick),config=await configureTerminal(config,quick),config=await configureShortcuts(config,quick),config=await configureCodex(config,quick),config=await configureDebug(config,quick),config=await configurePromptMode(config,quick),await showSummaryAndSave(config),printNextSteps()}import{existsSync as existsSync6}from"fs";import{homedir as homedir6}from"os";import{join as join6}from"path";async function shortcutsShowCommand(){displayShortcuts();let home=homedir6(),tmuxConf=join6(home,".tmux.conf"),zshrc=join6(home,".zshrc"),bashrc=join6(home,".bashrc");if(console.log("Installation status:"),isShortcutsInstalled(tmuxConf))console.log(" \x1B[32m\u2713\x1B[0m tmux.conf");else console.log(" \x1B[33m-\x1B[0m tmux.conf");let shellRc=existsSync6(zshrc)?zshrc:bashrc;if(isShortcutsInstalled(shellRc))console.log(` \x1B[32m\u2713\x1B[0m ${shellRc.replace(home,"~")}`);else console.log(` \x1B[33m-\x1B[0m ${shellRc.replace(home,"~")}`);console.log(),console.log("Run \x1B[36mgenie shortcuts install\x1B[0m to install shortcuts."),console.log("Run \x1B[36mgenie shortcuts uninstall\x1B[0m to remove shortcuts."),console.log()}async function shortcutsInstallCommand(){await installShortcuts()}async function shortcutsUninstallCommand(){await uninstallShortcuts()}init_esm6();import{existsSync as existsSync7,lstatSync,rmSync,unlinkSync as unlinkSync2}from"fs";import{homedir as homedir7}from"os";import{join as join7}from"path";init_genie_config2();var ORCHESTRATION_RULES_PATH=join7(homedir7(),".claude","rules","genie-orchestration.md"),LOCAL_BIN=join7(homedir7(),".local","bin"),SYMLINKS=["genie","term"];function isGenieSymlink(path){try{if(!existsSync7(path))return!1;if(!lstatSync(path).isSymbolicLink())return!1;return!0}catch{return!1}}function removeSymlinks(){let removed=[];for(let name of SYMLINKS){let symlinkPath=join7(LOCAL_BIN,name);if(isGenieSymlink(symlinkPath))try{unlinkSync2(symlinkPath),removed.push(name)}catch{}}return removed}function tryRemoveStep(label,successMsg,fn){console.log(`\x1B[2m${label}\x1B[0m`);try{fn(),console.log(` \x1B[32m+\x1B[0m ${successMsg}`)}catch(error){let message=error instanceof Error?error.message:String(error);console.log(` \x1B[33m!\x1B[0m ${label.replace("...","")} failed: ${message}`)}}function performUninstall(hasHookScript,existingSymlinks,genieDir,hasGenieDir){if(hasHookScript)tryRemoveStep("Removing hook script...","Hook script removed",()=>removeHookScript());if(existingSymlinks.length>0){console.log("\x1B[2mRemoving symlinks...\x1B[0m");let removed=removeSymlinks();if(removed.length>0)console.log(` \x1B[32m+\x1B[0m Removed: ${removed.join(", ")}`)}if(existsSync7(ORCHESTRATION_RULES_PATH))tryRemoveStep("Removing orchestration rules...","Orchestration rules removed (~/.claude/rules/genie-orchestration.md)",()=>unlinkSync2(ORCHESTRATION_RULES_PATH));if(hasGenieDir)tryRemoveStep("Removing genie directory...","Directory removed",()=>rmSync(genieDir,{recursive:!0,force:!0}))}async function uninstallCommand(){console.log(),console.log("\x1B[1m\x1B[33m Uninstall Genie CLI\x1B[0m"),console.log();let genieDir=getGenieDir(),hasGenieDir=existsSync7(genieDir),hasHookScript=hookScriptExists(),hasOrchestrationRules=existsSync7(ORCHESTRATION_RULES_PATH),existingSymlinks=SYMLINKS.filter((name)=>isGenieSymlink(join7(LOCAL_BIN,name)));if(console.log("\x1B[2mThis will remove:\x1B[0m"),hasHookScript)console.log(" \x1B[31m-\x1B[0m Hook script (~/.claude/hooks/genie-bash-hook.sh)");if(hasOrchestrationRules)console.log(" \x1B[31m-\x1B[0m Orchestration rules (~/.claude/rules/genie-orchestration.md)");if(hasGenieDir)console.log(` \x1B[31m-\x1B[0m Genie directory (${contractPath(genieDir)})`);if(existingSymlinks.length>0)console.log(` \x1B[31m-\x1B[0m Symlinks from ~/.local/bin: ${existingSymlinks.join(", ")}`);if(console.log(),!hasGenieDir&&!hasHookScript&&!hasOrchestrationRules&&existingSymlinks.length===0){console.log("\x1B[33mNothing to uninstall.\x1B[0m"),console.log();return}if(!await esm_default2({message:"Are you sure you want to uninstall Genie CLI?",default:!1})){console.log(),console.log("\x1B[2mUninstall cancelled.\x1B[0m"),console.log();return}console.log(),performUninstall(hasHookScript,existingSymlinks,genieDir,hasGenieDir),console.log(),console.log("\x1B[32m+\x1B[0m Genie CLI uninstalled."),console.log(),console.log("\x1B[2mNote: If you installed via npm/bun, also run:\x1B[0m"),console.log(" \x1B[36mbun remove -g @automagik/genie\x1B[0m"),console.log(" \x1B[2mor\x1B[0m"),console.log(" \x1B[36mnpm uninstall -g @automagik/genie\x1B[0m"),console.log()}init_genie_config2();import{execSync,spawn}from"child_process";import{chmodSync,copyFileSync,existsSync as existsSync8,mkdirSync as mkdirSync4,readFileSync as readFileSync4,readdirSync,rmSync as rmSync2,writeFileSync as writeFileSync4}from"fs";import{chmod,copyFile,mkdir,unlink}from"fs/promises";import{homedir as homedir8}from"os";import{join as join8}from"path";var GENIE_HOME=process.env.GENIE_HOME||join8(homedir8(),".genie"),GENIE_SRC=join8(GENIE_HOME,"src"),GENIE_BIN=join8(GENIE_HOME,"bin"),LOCAL_BIN2=join8(homedir8(),".local","bin");function log(message){console.log(`\x1B[32m\u25B8\x1B[0m ${message}`)}function success(message){console.log(`\x1B[32m\u2714\x1B[0m ${message}`)}function error(message){console.log(`\x1B[31m\u2716\x1B[0m ${message}`)}async function runCommand(command,args,cwd){return new Promise((resolve)=>{let output=[],child=spawn(command,args,{cwd,stdio:["inherit","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}});child.stdout?.on("data",(data)=>{let str=data.toString();output.push(str),process.stdout.write(str)}),child.stderr?.on("data",(data)=>{let str=data.toString();output.push(str),process.stderr.write(str)}),child.on("close",(code)=>{resolve({success:code===0,output:output.join("")})}),child.on("error",(err)=>{error(err.message),resolve({success:!1,output:err.message})})})}async function getGitInfo(cwd){try{let branchResult=await runCommandSilent("git",["rev-parse","--abbrev-ref","HEAD"],cwd),commitResult=await runCommandSilent("git",["rev-parse","--short","HEAD"],cwd),dateResult=await runCommandSilent("git",["log","-1","--format=%ci"],cwd);if(branchResult.success&&commitResult.success&&dateResult.success)return{branch:branchResult.output.trim(),commit:commitResult.output.trim(),commitDate:dateResult.output.trim().split(" ")[0]}}catch{}return null}async function runCommandSilent(command,args,cwd){return new Promise((resolve)=>{let output=[],child=spawn(command,args,{cwd,stdio:["inherit","pipe","pipe"]});child.stdout?.on("data",(data)=>{output.push(data.toString())}),child.stderr?.on("data",(data)=>{output.push(data.toString())}),child.on("close",(code)=>{resolve({success:code===0,output:output.join("")})}),child.on("error",(err)=>{resolve({success:!1,output:err.message})})})}function detectFromBinaryPath(path){if(path.includes(".bun"))return"bun";if(path.includes("node_modules"))return"npm";if(path===join8(LOCAL_BIN2,"genie")||path.startsWith(GENIE_BIN))return"source";return null}async function detectInstallationType(){if(genieConfigExists())try{let config=await loadGenieConfig();if(config.installMethod)return config.installMethod}catch{}if(existsSync8(join8(GENIE_SRC,".git")))return"source";let result=await runCommandSilent("which",["genie"]);if(!result.success)return"unknown";let detected=detectFromBinaryPath(result.output.trim());if(detected)return detected;return(await runCommandSilent("which",["bun"])).success?"bun":"npm"}async function updateViaBun(channel){try{__require("fs").unlinkSync(join8(homedir8(),".bun","install","global","bun.lock"))}catch{}if(log(`Updating via bun (channel: ${channel})...`),!(await runCommand("bun",["add","-g","--force","--no-cache",`@automagik/genie@${channel}`])).success)return error("Failed to update via bun"),!1;return console.log(),success(`Genie CLI updated via bun (${channel})!`),!0}async function updateViaNpm(channel){if(log(`Updating via npm (channel: ${channel})...`),!(await runCommand("npm",["install","-g",`@automagik/genie@${channel}`])).success)return error("Failed to update via npm"),!1;return console.log(),success(`Genie CLI updated via npm (${channel})!`),!0}async function detectGlobalInstalls(){let found=new Set,[npmResult,bunResult]=await Promise.all([runCommandSilent("npm",["list","-g","@automagik/genie"]),runCommandSilent("bun",["pm","ls","-g"])]);if(npmResult.success&&!npmResult.output.includes("(empty)"))found.add("npm");if(bunResult.success&&bunResult.output.includes("@automagik/genie"))found.add("bun");return found}async function updateSource(){let beforeInfo=await getGitInfo(GENIE_SRC);if(beforeInfo)console.log(`Current: \x1B[2m${beforeInfo.branch}@${beforeInfo.commit} (${beforeInfo.commitDate})\x1B[0m`),console.log();if(log("Fetching latest changes..."),!(await runCommand("git",["fetch","origin"],GENIE_SRC)).success)error("Failed to fetch from origin"),process.exit(1);if(log("Resetting to origin/main..."),!(await runCommand("git",["reset","--hard","origin/main"],GENIE_SRC)).success)error("Failed to reset to origin/main"),process.exit(1);console.log();let afterInfo=await getGitInfo(GENIE_SRC);if(beforeInfo&&afterInfo&&beforeInfo.commit===afterInfo.commit){success("Already up to date!"),console.log();return}if(log("Installing dependencies..."),!(await runCommand("bun",["install"],GENIE_SRC)).success)error("Failed to install dependencies"),process.exit(1);if(console.log(),log("Building..."),!(await runCommand("bun",["run","build"],GENIE_SRC)).success)error("Failed to build"),process.exit(1);console.log(),log("Installing binaries...");try{await mkdir(GENIE_BIN,{recursive:!0}),await mkdir(LOCAL_BIN2,{recursive:!0});let binaries=["genie.js","term.js"],names=["genie","term"];for(let i=0;i<binaries.length;i++){let src=join8(GENIE_SRC,"dist",binaries[i]),binDest=join8(GENIE_BIN,binaries[i]),linkDest=join8(LOCAL_BIN2,names[i]);await copyFile(src,binDest),await chmod(binDest,493),await symlinkOrCopy(binDest,linkDest)}for(let legacy of["claudio.js","claudio"]){let legacyBin=join8(GENIE_BIN,legacy),legacyLink=join8(LOCAL_BIN2,legacy);try{await unlink(legacyBin)}catch{}try{await unlink(legacyLink)}catch{}}success("Binaries installed")}catch(err){error(`Failed to install binaries: ${err}`),process.exit(1)}if(console.log(),console.log("\x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m"),success("Genie CLI updated successfully!"),console.log(),afterInfo)console.log(`Version: \x1B[36m${afterInfo.branch}@${afterInfo.commit}\x1B[0m (${afterInfo.commitDate})`),console.log()}async function symlinkOrCopy(src,dest){let{symlink,unlink:unlink2}=await import("fs/promises");try{if(existsSync8(dest))await unlink2(dest);await symlink(src,dest)}catch{await copyFile(src,dest)}}function copyDirSync(src,dest){mkdirSync4(dest,{recursive:!0});for(let entry of readdirSync(src,{withFileTypes:!0})){let srcPath=join8(src,entry.name),destPath=join8(dest,entry.name);if(entry.isDirectory())copyDirSync(srcPath,destPath);else copyFileSync(srcPath,destPath)}}async function resolveGlobalPkgDir(installType){if(installType==="bun"){let bunPath=join8(homedir8(),".bun","install","global","node_modules","@automagik","genie");if(existsSync8(bunPath))return bunPath}if(installType==="npm"){let npmRootResult=await runCommandSilent("npm",["root","-g"]);if(npmRootResult.success){let npmPath=join8(npmRootResult.output.trim(),"@automagik","genie");if(existsSync8(npmPath))return npmPath}}let bunFallback=join8(homedir8(),".bun","install","global","node_modules","@automagik","genie");if(existsSync8(bunFallback))return bunFallback;let npmRootFallback=await runCommandSilent("npm",["root","-g"]);if(npmRootFallback.success){let npmPath=join8(npmRootFallback.output.trim(),"@automagik","genie");if(existsSync8(npmPath))return npmPath}return null}function updatePluginRegistry(claudePlugins,cacheDir,version){let registryPath=join8(claudePlugins,"installed_plugins.json");try{if(!existsSync8(registryPath))return;let registry=JSON.parse(readFileSync4(registryPath,"utf-8")),entries=registry.plugins?.["genie@automagik"];if(!Array.isArray(entries))return;for(let entry of entries)if(entry.scope==="user")entry.installPath=cacheDir,entry.version=version,entry.lastUpdated=new Date().toISOString();writeFileSync4(registryPath,JSON.stringify(registry,null,2))}catch(err){log(`Registry update failed (non-fatal): ${err}`)}}var GENIE_TMUX_HEADER="# Genie TUI \u2014 tmux configuration";function syncTmuxConf(tmuxScriptsSrc){let tmuxConfSrc=join8(tmuxScriptsSrc,"genie.tmux.conf"),tmuxConfDest=join8(homedir8(),".tmux.conf");if(!existsSync8(tmuxConfSrc)||!existsSync8(tmuxConfDest))return;try{if(!readFileSync4(tmuxConfDest,"utf-8").includes(GENIE_TMUX_HEADER))return;copyFileSync(tmuxConfSrc,tmuxConfDest),success("Updated ~/.tmux.conf (genie-managed)");try{execSync("tmux source-file ~/.tmux.conf",{stdio:"ignore"}),success("Reloaded tmux configuration")}catch{}}catch{}}function syncTmuxScripts(globalPkgDir){let tmuxScriptsSrc=join8(globalPkgDir,"scripts","tmux");if(!existsSync8(tmuxScriptsSrc))return;let scriptsDir=join8(GENIE_HOME,"scripts");mkdirSync4(scriptsDir,{recursive:!0});let scriptCount=0;for(let entry of readdirSync(tmuxScriptsSrc))if(entry.endsWith(".sh")||entry==="genie.tmux.conf"){let src=join8(tmuxScriptsSrc,entry),dest=join8(scriptsDir,entry);copyFileSync(src,dest);try{chmodSync(dest,entry.endsWith(".sh")?493:420)}catch{}scriptCount++}if(scriptCount>0)success(`Refreshed ${scriptCount} tmux scripts at ${scriptsDir}`);syncTmuxConf(tmuxScriptsSrc)}async function syncPlugin(installType){log("Syncing Claude Code plugin...");let globalPkgDir=await resolveGlobalPkgDir(installType);if(!globalPkgDir){log("Could not find installed package \u2014 skipping plugin sync");return}let pluginSrc=join8(globalPkgDir,"plugins","genie");if(!existsSync8(pluginSrc)){log("Plugin source not found in package \u2014 skipping plugin sync");return}let version;try{version=JSON.parse(readFileSync4(join8(globalPkgDir,"package.json"),"utf-8")).version}catch{log("Could not read package version \u2014 skipping plugin sync");return}let claudePlugins=join8(homedir8(),".claude","plugins"),cacheDir=join8(claudePlugins,"cache","automagik","genie",version);try{if(existsSync8(cacheDir))rmSync2(cacheDir,{recursive:!0,force:!0});copyDirSync(pluginSrc,cacheDir);let skillsSrc=join8(globalPkgDir,"skills");if(existsSync8(skillsSrc)&&!existsSync8(join8(cacheDir,"skills")))copyDirSync(skillsSrc,join8(cacheDir,"skills"))}catch(err){error(`Failed to copy plugin: ${err}`);return}updatePluginRegistry(claudePlugins,cacheDir,version),syncTmuxScripts(globalPkgDir),success(`Plugin synced to v${version}`)}async function resolveChannel(options){if(options.next)return"next";if(options.stable)return"latest";if(genieConfigExists())try{let config=await loadGenieConfig();if(config.updateChannel)return config.updateChannel}catch{}return"latest"}async function persistChannel(channel){try{let config=await loadGenieConfig();config.updateChannel=channel,await saveGenieConfig(config)}catch{}}async function updateCommand(options={}){console.log(),console.log("\x1B[1m\uD83E\uDDDE Genie CLI Update\x1B[0m"),console.log("\x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m"),console.log();let channel=await resolveChannel(options);if(options.next||options.stable)await persistChannel(channel);let installType=await detectInstallationType();if(log(`Detected installation: ${installType}`),log(`Channel: ${channel}${channel==="next"?" (dev builds)":" (stable)"}`),console.log(),installType==="unknown")error("No Genie CLI installation found"),console.log(),console.log("Install method not configured. Please reinstall genie:"),console.log("\x1B[36m curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash\x1B[0m"),console.log(),process.exit(1);if(installType==="source"){await updateSource();return}let globalInstalls=await detectGlobalInstalls(),primaryMethod=installType;if(!(primaryMethod==="bun"?await updateViaBun(channel):await updateViaNpm(channel)))process.exit(1);let secondaryMethod=primaryMethod==="bun"?"npm":"bun";if(globalInstalls.has(secondaryMethod)){if(console.log(),log(`Also updating ${secondaryMethod}-global install...`),!(secondaryMethod==="bun"?await updateViaBun(channel):await updateViaNpm(channel)))error(`Secondary update via ${secondaryMethod} failed (non-blocking)`)}await syncPlugin(installType)}import{existsSync as existsSync9,readFileSync as readFileSync5}from"fs";import{dirname,resolve}from"path";var __dirname="/home/runner/_work/genie/genie/src/lib",FALLBACK_VERSION="0.0.0-unknown";function readVersionFromPackageJson(){let candidates=[resolve(dirname(import.meta.dir??__dirname),"..","..","package.json"),resolve(dirname(import.meta.dir??__dirname),"..","package.json"),resolve(dirname(import.meta.dir??__dirname),"package.json")];for(let candidate of candidates)try{if(existsSync9(candidate)){let pkg=JSON.parse(readFileSync5(candidate,"utf-8"));if(pkg.version)return pkg.version}}catch{}return FALLBACK_VERSION}var VERSION=readVersionFromPackageJson();function buildSearchNames(recipient,dirEntry){let names=new Set([recipient]);if(dirEntry){if(names.add(dirEntry.entry.name),dirEntry.entry.roles)for(let role of dirEntry.entry.roles)names.add(role)}return names}function buildSpawnArgs(template){let args=["spawn","--provider",template.provider,"--team",template.team];if(template.role)args.push("--role",template.role);if(template.skill)args.push("--skill",template.skill);if(template.cwd)args.push("--cwd",template.cwd);if(template.extraArgs)args.push(...template.extraArgs);return args}async function autoSpawn(payload){let input=payload.tool_input;if(!input||input.type!=="message")return;let recipient=input.recipient;if(!recipient||recipient==="team-lead")return;let teamName=process.env.GENIE_TEAM??payload.team_name;if(!teamName)return;try{let registryMod=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry)),tmuxMod=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),directoryMod=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),existing=(await registryMod.list()).find((a)=>(a.role===recipient||a.id===recipient)&&a.team===teamName);if(existing&&await tmuxMod.isPaneAlive(existing.paneId))return;let dirEntry=await directoryMod.resolve(recipient),templates=await registryMod.listTemplates(),searchNames=buildSearchNames(recipient,dirEntry),template=templates.find((t)=>{if(t.team!==teamName)return!1;return[...searchNames].some((q)=>t.id===q||t.role===q)});if(!template){if(dirEntry)console.error(`[genie-hook] Agent "${recipient}" is registered in directory but has no spawn template in team "${teamName}".`);return}let{spawnSync}=__require("child_process");spawnSync("genie",buildSpawnArgs(template),{timeout:1e4,stdio:"ignore",env:{...process.env,GENIE_TEAM:teamName}}),console.error(`[genie-hook] Auto-spawned "${recipient}" in team "${teamName}"`)}catch(err){let msg=err instanceof Error?err.message:String(err);console.error(`[genie-hook] Auto-spawn failed for "${recipient}": ${msg}`)}}async function identityInject(payload){let input=payload.tool_input;if(!input)return;let msgType=input.type;if(msgType!=="message"&&msgType!=="broadcast")return;let agentName=process.env.GENIE_AGENT_NAME;if(!agentName)return;let content=input.content;if(!content)return;if(content.startsWith(`[from:${agentName}]`))return;return{updatedInput:{...input,content:`[from:${agentName}] ${content}`}}}var getAgent=()=>process.env.GENIE_AGENT_NAME??"unknown",getTeam=()=>process.env.GENIE_TEAM;async function emit(subject,event){try{let{publish:publish2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));await publish2(subject,event)}catch{}}async function natsEmitToolCall(payload){let{tool_name:toolName,tool_input:input}=payload;if(!toolName||!input)return;await emit(`genie.tool.${getAgent()}.call`,{timestamp:new Date().toISOString(),kind:"tool_call",agent:getAgent(),team:getTeam(),text:summarizeToolCall(toolName,input),data:{toolCall:{name:toolName,input}},source:"hook"});return}async function natsEmit(payload){let input=payload.tool_input;if(!input)return;let msgType=input.type;if(msgType!=="message"&&msgType!=="broadcast")return;let{to,content}=input;if(!to||!content)return;let subject=msgType==="broadcast"?"genie.msg.broadcast":`genie.msg.${to}`;await emit(subject,{timestamp:new Date().toISOString(),kind:"message",agent:getAgent(),peer:to,direction:"out",text:content,source:"hook"});return}async function natsEmitUserPrompt(payload){let prompt2=payload.prompt;if(!prompt2)return;await emit(`genie.user.${getAgent()}.prompt`,{timestamp:new Date().toISOString(),kind:"user",agent:getAgent(),team:getTeam(),text:prompt2,source:"hook"});return}async function natsEmitAssistantResponse(payload){let lastMessage=payload.last_assistant_message;if(!lastMessage)return;await emit(`genie.agent.${getAgent()}.response`,{timestamp:new Date().toISOString(),kind:"assistant",agent:getAgent(),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(`
|
|
973
973
|
`)[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_types2();var handlers=[{name:"identity-inject",event:"PreToolUse",matcher:/^SendMessage$/,priority:10,fn:identityInject},{name:"auto-spawn",event:"PreToolUse",matcher:/^SendMessage$/,priority:20,fn:autoSpawn},{name:"nats-emit-tool",event:"PreToolUse",matcher:/.*/,priority:30,fn:natsEmitToolCall},{name:"nats-emit-msg",event:"PostToolUse",matcher:/^SendMessage$/,priority:30,fn:natsEmit},{name:"nats-emit-user-prompt",event:"UserPromptSubmit",priority:30,fn:natsEmitUserPrompt},{name:"nats-emit-assistant-response",event:"Stop",priority:30,fn:natsEmitAssistantResponse}];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,b)=>a.priority-b.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;for(let handler of matched){let result=await runHandler(handler,payload,currentInput);if(!result)continue;if(result.decision==="deny")return{decision:"deny",reason:result.reason??`Denied by handler: ${handler.name}`};if(result.updatedInput)currentInput={...currentInput,...result.updatedInput}}if(currentInput&&payload.tool_input&&JSON.stringify(currentInput)!==JSON.stringify(payload.tool_input))return{updatedInput:currentInput};return{}}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(result.decision||result.updatedInput)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_agents();import{existsSync as existsSync17,mkdirSync as mkdirSync8,readFileSync as readFileSync9,unlinkSync as unlinkSync4,writeFileSync as writeFileSync6}from"fs";import{homedir as homedir17}from"os";import{join as join20}from"path";function genieHome(){return process.env.GENIE_HOME??join20(homedir17(),".genie")}function pidFilePath(){return join20(genieHome(),"scheduler.pid")}function logFilePath(){return join20(genieHome(),"logs","scheduler.log")}function systemdDir(){return join20(homedir17(),".config","systemd","user")}function systemdUnitPath(){return join20(systemdDir(),"genie-scheduler.service")}function readPid(){let path2=pidFilePath();if(!existsSync17(path2))return null;let raw=readFileSync9(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function writePid(pid){let dir=genieHome();mkdirSync8(dir,{recursive:!0}),writeFileSync6(pidFilePath(),String(pid),"utf-8")}function removePid(){let path2=pidFilePath();if(existsSync17(path2))try{unlinkSync4(path2)}catch{}}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}function generateSystemdUnit(){let genieBin=process.argv[1]??"genie";return`[Unit]
|
|
974
974
|
Description=Genie Scheduler Daemon
|
|
975
975
|
Documentation=https://github.com/automagik/genie
|
|
@@ -1024,12 +1024,12 @@ Done. ${results.length} migration${results.length===1?"":"s"} applied.`),await s
|
|
|
1024
1024
|
(built-in ${resolved.entry.registeredAt==="(built-in)"?"agent":"agent"})`);console.log(""),printEntry(resolved.entry),console.log("")}async function listEntries(json2,includeBuiltins){let entries=await ls();if(json2){listEntriesJson(entries,includeBuiltins);return}if(entries.length===0&&!includeBuiltins){console.log(`
|
|
1025
1025
|
No agents registered. Add one with: genie dir add <name> --dir <path>`),console.log(`Use --builtins to also see built-in roles and council members.
|
|
1026
1026
|
`);return}if(entries.length>0)printRegisteredTable(entries);if(includeBuiltins)printBuiltinsTable()}function listEntriesJson(entries,includeBuiltins){let result=entries.map((e)=>({...e,builtin:!1}));if(includeBuiltins)for(let b2 of ALL_BUILTINS)result.push({name:b2.name,description:b2.description,model:b2.model,category:b2.category,scope:"built-in",builtin:!0});console.log(JSON.stringify(result,null,2))}function printRegisteredTable(entries){console.log(""),console.log("REGISTERED AGENTS"),console.log("-".repeat(85)),console.log(` ${"NAME".padEnd(22)}${"SCOPE".padEnd(10)}${"DIR".padEnd(30)}${"MODE".padEnd(8)}${"MODEL".padEnd(8)}ROLES`),console.log(` ${"-".repeat(20)} ${"-".repeat(8)} ${"-".repeat(28)} ${"-".repeat(6)} ${"-".repeat(6)} ${"-".repeat(15)}`);for(let entry of entries){let dir=contractPath(entry.dir),truncDir=dir.length>28?`${dir.slice(0,25)}...`:dir,roles=entry.roles?.join(", ")||"-";console.log(` ${entry.name.padEnd(22)}${entry.scope.padEnd(10)}${truncDir.padEnd(30)}${entry.promptMode.padEnd(8)}${(entry.model||"-").padEnd(8)}${roles}`)}console.log("")}function printBuiltinsTable(){console.log("BUILT-IN AGENTS"),console.log("-".repeat(80)),console.log(` ${"NAME".padEnd(22)}${"TYPE".padEnd(10)}${"MODEL".padEnd(8)}DESCRIPTION`),console.log(` ${"-".repeat(20)} ${"-".repeat(8)} ${"-".repeat(6)} ${"-".repeat(30)}`);for(let agent of ALL_BUILTINS)console.log(` ${agent.name.padEnd(22)}${agent.category.padEnd(10)}${(agent.model||"-").padEnd(8)}${agent.description}`);console.log("")}async function handleOmniRegistration(name,options){let omniUrl=await resolveOmniApiUrl();if(!omniUrl)return;console.log(`
|
|
1027
|
-
Registering in Omni (${omniUrl})...`);let existingId=await findOmniAgent(name);if(existingId){console.log(` Agent already exists in Omni: ${existingId}`),await edit(name,{omniAgentId:existingId},{global:options.global}),console.log(" Linked existing Omni agent to directory entry.");return}let omniAgentId=await registerAgentInOmni(name,{model:options.model,roles:options.roles});if(omniAgentId)await edit(name,{omniAgentId},{global:options.global}),console.log(` Omni agent created: ${omniAgentId}`),console.log(" Session isolation: per-person + per-channel")}async function handleAgentRegister(name,options){let promptMode=validatePromptMode(options.promptMode),entry=await add({name,dir:resolvePath(options.dir),repo:options.repo?resolvePath(options.repo):void 0,promptMode,model:options.model,roles:options.roles},{global:options.global}),scope=options.global?"global":"project";if(console.log(`Agent "${entry.name}" registered (${scope}).`),printEntry(entry),!options.skipOmni)await handleOmniRegistration(name,options)}function registerAgentNamespace(program2){program2.command("agent").description("Agent lifecycle management").command("register <name>").description("Register an agent locally and auto-register in Omni when configured").requiredOption("--dir <path>","Agent folder (CWD + AGENTS.md)").option("--repo <path>","Default git repo (overridden by team)").option("--prompt-mode <mode>","Prompt mode: append or system","append").option("--model <model>","Default model (sonnet, opus, codex)").option("--roles <roles...>","Built-in roles this agent can orchestrate").option("--global","Write to global directory instead of project").option("--skip-omni","Skip Omni auto-registration").action(async(name,options)=>{try{await handleAgentRegister(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_protocol_router();init_wish_state();init_agents();import{execSync as execSync6}from"child_process";import{existsSync as existsSync19}from"fs";import{mkdir as mkdir8,readFile as readFile9,writeFile as writeFile7}from"fs/promises";import{tmpdir}from"os";import{join as join24}from"path";init_wish_state();import{execSync as execSync5}from"child_process";import{existsSync as existsSync18}from"fs";import{readFile as readFile8}from"fs/promises";import{join as join23}from"path";function parseRef(ref){let hashIdx=ref.indexOf("#");if(hashIdx===-1)throw Error(`Invalid reference "${ref}". Expected format: <slug>#<group>`);let slug=ref.slice(0,hashIdx),group=ref.slice(hashIdx+1);if(!slug||!group)throw Error(`Invalid reference "${ref}". Both slug and group are required.`);return{slug,group}}var STATUS_ICONS={blocked:"\uD83D\uDD12",ready:"\uD83D\uDFE2",in_progress:"\uD83D\uDD04",done:"\u2705"};function formatTimestamp(iso){if(!iso)return"";return new Date(iso).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",hour12:!1})}function padRight2(str2,len){return str2.length>=len?str2:str2+" ".repeat(len-str2.length)}async function detectWaveCompletion(slug,groupName,cwd){let
|
|
1027
|
+
Registering in Omni (${omniUrl})...`);let existingId=await findOmniAgent(name);if(existingId){console.log(` Agent already exists in Omni: ${existingId}`),await edit(name,{omniAgentId:existingId},{global:options.global}),console.log(" Linked existing Omni agent to directory entry.");return}let omniAgentId=await registerAgentInOmni(name,{model:options.model,roles:options.roles});if(omniAgentId)await edit(name,{omniAgentId},{global:options.global}),console.log(` Omni agent created: ${omniAgentId}`),console.log(" Session isolation: per-person + per-channel")}async function handleAgentRegister(name,options){let promptMode=validatePromptMode(options.promptMode),entry=await add({name,dir:resolvePath(options.dir),repo:options.repo?resolvePath(options.repo):void 0,promptMode,model:options.model,roles:options.roles},{global:options.global}),scope=options.global?"global":"project";if(console.log(`Agent "${entry.name}" registered (${scope}).`),printEntry(entry),!options.skipOmni)await handleOmniRegistration(name,options)}function registerAgentNamespace(program2){program2.command("agent").description("Agent lifecycle management").command("register <name>").description("Register an agent locally and auto-register in Omni when configured").requiredOption("--dir <path>","Agent folder (CWD + AGENTS.md)").option("--repo <path>","Default git repo (overridden by team)").option("--prompt-mode <mode>","Prompt mode: append or system","append").option("--model <model>","Default model (sonnet, opus, codex)").option("--roles <roles...>","Built-in roles this agent can orchestrate").option("--global","Write to global directory instead of project").option("--skip-omni","Skip Omni auto-registration").action(async(name,options)=>{try{await handleAgentRegister(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}init_protocol_router();init_wish_state();init_agents();import{execSync as execSync6}from"child_process";import{existsSync as existsSync19}from"fs";import{mkdir as mkdir8,readFile as readFile9,writeFile as writeFile7}from"fs/promises";import{tmpdir}from"os";import{join as join24}from"path";init_wish_state();import{execSync as execSync5}from"child_process";import{existsSync as existsSync18}from"fs";import{readFile as readFile8}from"fs/promises";import{dirname as dirname6,join as join23}from"path";function resolveWishPath(slug,cwd){let base=cwd??process.cwd(),cwdPath=join23(base,".genie","wishes",slug,"WISH.md");if(existsSync18(cwdPath))return cwdPath;try{let commonDir=execSync5("git rev-parse --path-format=absolute --git-common-dir",{encoding:"utf-8",cwd:base,stdio:["pipe","pipe","pipe"]}).trim(),repoRoot=dirname6(commonDir);if(repoRoot!==base){let repoPath=join23(repoRoot,".genie","wishes",slug,"WISH.md");if(existsSync18(repoPath))return repoPath}}catch{}return null}function parseRef(ref){let hashIdx=ref.indexOf("#");if(hashIdx===-1)throw Error(`Invalid reference "${ref}". Expected format: <slug>#<group>`);let slug=ref.slice(0,hashIdx),group=ref.slice(hashIdx+1);if(!slug||!group)throw Error(`Invalid reference "${ref}". Both slug and group are required.`);return{slug,group}}var STATUS_ICONS={blocked:"\uD83D\uDD12",ready:"\uD83D\uDFE2",in_progress:"\uD83D\uDD04",done:"\u2705"};function formatTimestamp(iso){if(!iso)return"";return new Date(iso).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",hour12:!1})}function padRight2(str2,len){return str2.length>=len?str2:str2+" ".repeat(len-str2.length)}async function detectWaveCompletion(slug,groupName,cwd){let wishPath=resolveWishPath(slug,cwd);if(!wishPath)return null;let content=await readFile8(wishPath,"utf-8"),targetWave=parseExecutionStrategy(content).find((w)=>w.groups.some((g)=>g.group===groupName));if(!targetWave)return null;let state2=await getState(slug,cwd);if(!state2)return null;let waveGroupNames=targetWave.groups.map((g)=>g.group);if(!waveGroupNames.every((g)=>state2.groups[g]?.status==="done"))return null;return{waveName:targetWave.name,waveGroups:waveGroupNames}}async function ensureWorkPushed(slug,group){try{if(execSync5("git status --porcelain",{encoding:"utf-8"}).trim())console.log(" Committing dirty working tree..."),execSync5("git add -A",{encoding:"utf-8"}),execSync5(`git commit -m "wip: ${slug}#${group}"`,{encoding:"utf-8"}),console.log(` Committed as "wip: ${slug}#${group}"`)}catch{}try{if(execSync5("git log @{u}..HEAD --oneline",{encoding:"utf-8"}).trim())console.log(" Pushing unpushed commits..."),execSync5("git push",{encoding:"utf-8",timeout:30000}),console.log(" Push complete.")}catch{try{let branch=execSync5("git rev-parse --abbrev-ref HEAD",{encoding:"utf-8"}).trim();if(branch&&branch!=="HEAD")execSync5(`git push -u origin ${branch}`,{encoding:"utf-8",timeout:30000}),console.log(" Push complete (set upstream).")}catch{console.log(" \u26A0\uFE0F Push failed \u2014 manual push may be needed.")}}}function autoKillPane(){let paneId=process.env.TMUX_PANE;if(paneId)setTimeout(()=>{try{execSync5(`tmux kill-pane -t '${paneId}'`,{encoding:"utf-8"})}catch{process.exit(0)}},1000);else process.exit(0)}async function doneCommand(ref){try{let{slug,group}=parseRef(ref),result=await completeGroup(slug,group);if(console.log(`\u2705 Group "${group}" marked as done in wish "${slug}"`),result.completedAt)console.log(` Completed at: ${formatTimestamp(result.completedAt)}`);let state2=await getState(slug);if(state2){let nowReady=Object.entries(state2.groups).filter(([,g])=>g.status==="ready"&&g.dependsOn.includes(group)).map(([name])=>name);if(nowReady.length>0)console.log(` Unblocked: ${nowReady.join(", ")}`)}await ensureWorkPushed(slug,group);let waveResult=await detectWaveCompletion(slug,group);if(waveResult){console.log(` \uD83C\uDF0A ${waveResult.waveName} complete! All groups done: ${waveResult.waveGroups.join(", ")}`);try{let protocolRouter=await Promise.resolve().then(() => (init_protocol_router(),exports_protocol_router)),repoPath=process.cwd(),message=`${waveResult.waveName} complete. All groups done: [${waveResult.waveGroups.join(", ")}]. Run /review or advance to next wave.`,result2=await protocolRouter.sendMessage(repoPath,"cli","team-lead",message);if(result2&&typeof result2==="object"&&"delivered"in result2&&!result2.delivered)console.warn(" \u26A0\uFE0F Wave-complete notification may not have been delivered.");else console.log(" Notified team-lead of wave completion.")}catch{console.warn(" \u26A0\uFE0F Could not notify team-lead (messaging unavailable).")}}autoKillPane()}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}}async function statusCommand(slug){try{let state2=await getState(slug);if(!state2){let wishPath=resolveWishPath(slug);if(!wishPath)console.error(`\u274C No state found for wish "${slug}" and no WISH.md found in cwd or repo root`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile8(wishPath,"utf-8"),groups=parseWishGroups(content);if(groups.length===0)console.error(`\u274C No execution groups found in ${wishPath}`),process.exit(1);state2=await createState(slug,groups),console.log(`\uD83D\uDCDD Auto-initialized state for wish "${slug}" (${groups.length} groups)`)}console.log(`
|
|
1028
1028
|
Wish: ${state2.wish}`),console.log("\u2500".repeat(60));let entries=Object.entries(state2.groups),maxNameLen=Math.max(...entries.map(([name])=>name.length),5);console.log(` ${padRight2("GROUP",maxNameLen)} STATUS ASSIGNEE STARTED COMPLETED`),console.log(` ${"\u2500".repeat(maxNameLen+62)}`);for(let[name,group]of entries){let icon=STATUS_ICONS[group.status]??"\u2753",status=padRight2(`${icon} ${group.status}`,13),assignee=padRight2(group.assignee??"-",13),started=padRight2(formatTimestamp(group.startedAt)||"-",14),completed=formatTimestamp(group.completedAt)||"-";console.log(` ${padRight2(name,maxNameLen)} ${status} ${assignee} ${started} ${completed}`)}let total=entries.length,done=entries.filter(([,g])=>g.status==="done").length,inProgress=entries.filter(([,g])=>g.status==="in_progress").length,ready=entries.filter(([,g])=>g.status==="ready").length,blocked=entries.filter(([,g])=>g.status==="blocked").length;console.log(""),console.log(` Progress: ${done}/${total} done | ${inProgress} in progress | ${ready} ready | ${blocked} blocked`),console.log("")}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}}function registerStateCommands(program2){program2.command("done <ref>").description("Mark a wish group as done (format: <slug>#<group>)").action(async(ref)=>{await doneCommand(ref)}),program2.command("status <slug>").description("Show wish state overview for all groups").action(async(slug)=>{await statusCommand(slug)}),program2.command("reset <ref>").description("Reset an in-progress group back to ready (format: <slug>#<group>)").action(async(ref)=>{try{let{slug,group}=parseRef(ref),result=await resetGroup(slug,group);if(console.log(`\uD83D\uDD04 Group "${group}" reset to ready in wish "${slug}"`),result.status==="ready")console.log(" Status: ready (assignee cleared)")}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}})}async function writeContextFile(content){let dir=join24(tmpdir(),"genie-dispatch");await mkdir8(dir,{recursive:!0});let ts=Date.now().toString(36),rand=Math.random().toString(36).slice(2,8),filePath=join24(dir,`ctx-${ts}-${rand}.md`);return await writeFile7(filePath,content),filePath}function extractGroup(content,groupName){let pattern=new RegExp(`^### Group ${escapeRegExp(groupName)}:`,"m"),match=content.match(pattern);if(!match||match.index===void 0)return null;let start=match.index,nextBoundary=content.slice(start).slice(1).search(/^### Group \d|^---$/m),end=nextBoundary!==-1?start+1+nextBoundary:content.length;return content.slice(start,end).trim()}function extractWishContext(content){let execGroupsIdx=content.indexOf("## Execution Groups");if(execGroupsIdx!==-1)return content.slice(0,execGroupsIdx).trim();return content.slice(0,2000).trim()}function escapeRegExp(str2){return str2.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function buildContextPrompt(opts){let parts=[`# Dispatch Context (${opts.command})`,"",`**Source file:** \`${opts.filePath}\``,"(Read the full document at the path above for complete context)",""];if(opts.wishContext)parts.push("## Wish Context","",opts.wishContext,"");if(parts.push("## Assigned Section","",opts.sectionContent,""),opts.skill)parts.push("## Initial Command","",`Run \`/${opts.skill}\` to begin.`,"");return parts.join(`
|
|
1029
1029
|
`)}function getGitDiff(){try{let diff=execSync6("git diff HEAD",{encoding:"utf-8",maxBuffer:1048576}),staged=execSync6("git diff --cached",{encoding:"utf-8",maxBuffer:1048576}),combined=[diff,staged].filter(Boolean).join(`
|
|
1030
1030
|
`);if(combined.length>50000)return`${combined.slice(0,50000)}
|
|
1031
1031
|
|
|
1032
|
-
... (diff truncated at 50KB)`;return combined}catch{return""}}function parseWishGroups(content){let groups=[],groupPattern=/^### Group ([A-Za-z0-9]+):/gim,match=groupPattern.exec(content);while(match!==null){let name=match[1],start=match.index,rest=content.slice(start+match[0].length),nextGroupIdx=rest.search(/^### Group [A-Za-z0-9]+:/m),depsMatch=(nextGroupIdx!==-1?rest.slice(0,nextGroupIdx):rest).match(/\*\*depends-on:\*\*\s*(.+)/i),dependsOn=[];if(depsMatch){let
|
|
1032
|
+
... (diff truncated at 50KB)`;return combined}catch{return""}}function parseWishGroups(content){let groups=[],groupPattern=/^### Group ([A-Za-z0-9]+):/gim,match=groupPattern.exec(content);while(match!==null){let name=match[1],start=match.index,rest=content.slice(start+match[0].length),nextGroupIdx=rest.search(/^### Group [A-Za-z0-9]+:/m),depsMatch=(nextGroupIdx!==-1?rest.slice(0,nextGroupIdx):rest).match(/\*\*depends-on:\*\*\s*(.+)/i),dependsOn=[];if(depsMatch){let depsNormalized=depsMatch[1].trim().replace(/\s*\([^)]*\)/g,"").trim();if(depsNormalized.toLowerCase()!=="none")dependsOn=depsNormalized.split(",").map((d)=>d.trim().replace(/^group\s*/i,"").trim()).filter(Boolean)}groups.push({name,dependsOn}),match=groupPattern.exec(content)}return groups}function parseExecutionStrategy(content){let strategyMatch=content.match(/^## Execution Strategy\s*$/m);if(!strategyMatch||strategyMatch.index===void 0)return buildFallbackWaves(content);let strategyStart=strategyMatch.index+strategyMatch[0].length,nextSectionMatch=content.slice(strategyStart).match(/^## /m),strategyEnd=nextSectionMatch?.index!==void 0?strategyStart+nextSectionMatch.index:content.length,strategyContent=content.slice(strategyStart,strategyEnd),waves=[],wavePattern=/^### (Wave \d+[^\n]*)/gm,waveMatch=wavePattern.exec(strategyContent);while(waveMatch!==null){let waveName=waveMatch[1].trim(),waveStart=waveMatch.index+waveMatch[0].length,nextWaveIdx=strategyContent.slice(waveStart).search(/^### /m),waveEnd=nextWaveIdx!==-1?waveStart+nextWaveIdx:strategyContent.length,waveContent=strategyContent.slice(waveStart,waveEnd),waveGroups=[],tableRowPattern=/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*[^|]*\s*\|$/gm,rowMatch=tableRowPattern.exec(waveContent);while(rowMatch!==null){let groupVal=rowMatch[1].trim(),agentVal=rowMatch[2].trim();if(groupVal!=="Group"&&!groupVal.startsWith("-"))waveGroups.push({group:groupVal,agent:agentVal});rowMatch=tableRowPattern.exec(waveContent)}if(waveGroups.length>0)waves.push({name:waveName,groups:waveGroups});waveMatch=wavePattern.exec(strategyContent)}if(waves.length===0)return buildFallbackWaves(content);return waves}function buildFallbackWaves(content){let groups=parseWishGroups(content);if(groups.length===0)return[];return[{name:"Wave 1 (sequential fallback)",groups:groups.map((g)=>({group:g.name,agent:"engineer"}))}]}function detectWorkMode(ref,agent){if(!agent){if(ref.includes("#"))throw Error("Manual dispatch requires an agent: genie work <slug>#<group> <agent>");return{mode:"auto",slug:ref}}if(ref.includes("#"))return{mode:"manual",ref,agent};if(agent.includes("#"))return{mode:"manual",ref:agent,agent:ref};throw Error('Invalid: ref must contain "#" \u2014 use "genie work <slug>" or "genie work <agent> <slug>#<group>"')}async function autoOrchestrateCommand(slug){let wishPath=join24(process.cwd(),".genie","wishes",slug,"WISH.md");if(!existsSync19(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile9(wishPath,"utf-8"),groups=parseWishGroups(content),waves=parseExecutionStrategy(content);if(waves.length===0)console.error("\u274C No execution groups found in wish"),process.exit(1);let state2=await getOrCreateState(slug,groups),nextWave=waves.find((wave)=>wave.groups.some((g)=>{let gs=state2?.groups[g.group];return!gs||gs.status==="ready"}));if(!nextWave){console.log(`\u2705 All waves already dispatched for wish "${slug}"`);return}console.log(`\uD83D\uDE80 Dispatching ${nextWave.name} for wish "${slug}" \u2014 ${nextWave.groups.length} group(s)`),await Promise.all(nextWave.groups.map(({group,agent})=>{let ref=`${slug}#${group}`;return workDispatchCommand(agent,ref)}));let groupList=nextWave.groups.map((g)=>g.group).join(", ");console.log(`
|
|
1033
1033
|
\u2705 Agents dispatched for ${nextWave.name} (groups: ${groupList})`),console.log(` Monitor: genie status ${slug}`),console.log(" Logs: genie read <agent>")}async function brainstormCommand(agentName,slug){let draftPath=join24(process.cwd(),".genie","brainstorms",slug,"DRAFT.md");if(!existsSync19(draftPath))console.error(`\u274C Draft not found: ${draftPath}`),console.error(` Create it first: mkdir -p .genie/brainstorms/${slug} && touch .genie/brainstorms/${slug}/DRAFT.md`),process.exit(1);let content=await readFile9(draftPath,"utf-8"),context=buildContextPrompt({filePath:draftPath,sectionContent:content,command:"brainstorm",skill:"brainstorm"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching brainstorm to ${agentName} for "${slug}"`),console.log(` Draft: ${draftPath}`);let brainstormPrompt=`Brainstorm "${slug}". Your context is in the system prompt. Explore the idea, ask clarifying questions, and build toward a design.`;await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",extraArgs:["--append-system-prompt-file",contextFile],initialPrompt:brainstormPrompt});let repoPath=process.cwd(),result=await sendMessage(repoPath,"cli",agentName,brainstormPrompt);if(!result.delivered)console.warn(`\u26A0 Backup delivery to ${agentName} failed: ${result.reason??"unknown"}`)}async function wishCommand(agentName,slug){let designPath=join24(process.cwd(),".genie","brainstorms",slug,"DESIGN.md");if(!existsSync19(designPath))console.error(`\u274C Design not found: ${designPath}`),console.error(` Run brainstorm first: genie brainstorm <agent> ${slug}`),process.exit(1);let content=await readFile9(designPath,"utf-8"),context=buildContextPrompt({filePath:designPath,sectionContent:content,command:"wish",skill:"wish"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDCDD Dispatching wish to ${agentName} for "${slug}"`),console.log(` Design: ${designPath}`);let wishPrompt=`Create a wish from the design for "${slug}". Your context is in the system prompt. Write the WISH.md with execution groups, acceptance criteria, and validation commands.`;await handleWorkerSpawn(agentName,{provider:"claude",team:process.env.GENIE_TEAM??"genie",extraArgs:["--append-system-prompt-file",contextFile],initialPrompt:wishPrompt});let repoPath=process.cwd(),result=await sendMessage(repoPath,"cli",agentName,wishPrompt);if(!result.delivered)console.warn(`\u26A0 Backup delivery to ${agentName} failed: ${result.reason??"unknown"}`)}async function workDispatchCommand(agentName,ref){let{slug,group}=parseRef(ref),wishPath=join24(process.cwd(),".genie","wishes",slug,"WISH.md");if(!existsSync19(wishPath))console.error(`\u274C Wish not found: ${wishPath}`),console.error(` Create it first: genie wish <agent> ${slug}`),process.exit(1);let content=await readFile9(wishPath,"utf-8"),groupSection=extractGroup(content,group);if(!groupSection){console.error(`\u274C Group "${group}" not found in ${wishPath}`),console.error(" Available groups:");let groups2=content.match(/^### Group [A-Za-z0-9]+:.*$/gm);if(groups2)for(let g of groups2)console.error(` ${g}`);process.exit(1)}let groups=parseWishGroups(content);await getOrCreateState(slug,groups);try{await startGroup(slug,group,agentName),console.log(`\u2705 Group "${group}" set to in_progress (assigned to ${agentName})`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`\u274C ${message}`),process.exit(1)}let wishContext=extractWishContext(content),context=buildContextPrompt({filePath:wishPath,sectionContent:groupSection,wishContext,command:`work ${ref}`,skill:"work"}),contextFile=await writeContextFile(context);console.log(`\uD83D\uDD27 Dispatching work to ${agentName} for "${ref}"`),console.log(` Wish: ${wishPath}`),console.log(` Group: ${group}`);let effectiveRole=`${agentName}-${group}`,workPrompt=`Execute Group ${group} of wish "${slug}". Your full context is in the system prompt. Read the wish at ${wishPath} if needed. Implement all deliverables, run validation, and report completion.
|
|
1034
1034
|
|
|
1035
1035
|
When done:
|
|
@@ -1218,7 +1218,7 @@ History for "${schedule.name}":
|
|
|
1218
1218
|
Tags: ${tags.map((t)=>t.name).join(", ")}`);let blockers=await ts.getBlockers(task.id,task.repoPath);if(blockers.length>0){console.log(`
|
|
1219
1219
|
Dependencies:`);for(let dep of blockers){let depTask=await ts.getTask(dep.dependsOnId,task.repoPath),label=depTask?`#${depTask.seq} ${depTask.title}`:dep.dependsOnId;console.log(` ${dep.depType}: ${label}`)}}let stageLog=await ts.getStageLog(task.id,task.repoPath);if(stageLog.length>0){console.log(`
|
|
1220
1220
|
Stage History:`);for(let entry of stageLog.slice(0,10)){let who=entry.actorId??"system";console.log(` ${formatTimestamp3(entry.createdAt)}: ${entry.fromStage??"(new)"} \u2192 ${entry.toStage} by ${who}`)}}}async function printTaskMessages(task){let ts=await getTaskService6(),conv=await ts.findOrCreateConversation({linkedEntity:"task",linkedEntityId:task.id,name:`Task #${task.seq}`}),messages2=await ts.getMessages(conv.id,{limit:20});if(messages2.length>0){console.log(`
|
|
1221
|
-
Messages:`);for(let msg of messages2){let time=formatTimestamp3(msg.createdAt),reply=msg.replyToId?` (reply to #${msg.replyToId})`:"";console.log(` [${time}] ${msg.senderId}: ${msg.body}${reply}`)}}}async function printTaskDetail(task){printTaskFields(task),await printTaskRelations(task),await printTaskMessages(task),console.log("")}async function handleTaskCreate(title,options){let ts=await getTaskService6(),actor=currentActor2(),repoPath,projectId;if(options.project){let project=await ts.getProjectByName(options.project);if(!project)project=await ts.createProject({name:options.project});projectId=project.id,repoPath=project.repoPath??void 0}let parentId;if(options.parent){if(parentId=await ts.resolveTaskId(options.parent,repoPath)??void 0,!parentId)console.error(`Error: Parent task not found: ${options.parent}`),process.exit(1)}let task=await ts.createTask({title,typeId:options.type,priority:options.priority,dueDate:options.due,startDate:options.start,parentId,description:options.description,estimatedEffort:options.effort},repoPath,projectId);if(await ts.assignTask(task.id,actor,"creator",{},task.repoPath),options.assign)await ts.assignTask(task.id,localActor2(options.assign),"assignee",{},task.repoPath);if(options.tags){let tagIds=options.tags.split(",").map((t)=>t.trim());await ts.tagTask(task.id,tagIds,actor,task.repoPath)}if(options.comment)await ts.commentOnTask(task.id,actor,options.comment,task.repoPath);if(console.log(`Created task #${task.seq}: ${task.title}`),console.log(` ID: ${task.id}`),console.log(` Stage: ${task.stage} | Priority: ${task.priority}`),options.due)console.log(` Due: ${options.due}`)}function registerTaskCommands(program2){let task=program2.command("task").description("Task lifecycle management");task.command("create <title>").description("Create a new task").option("--type <type>","Task type","software").option("--priority <priority>","Priority: urgent, high, normal, low","normal").option("--due <date>","Due date (YYYY-MM-DD)").option("--start <date>","Start date (YYYY-MM-DD)").option("--tags <tags>","Comma-separated tag IDs").option("--parent <id>","Parent task ID or #seq").option("--assign <name>","Assign to local actor").option("--description <text>","Task description").option("--effort <effort>",'Estimated effort (e.g., "2h", "3 points")').option("--comment <msg>","Initial comment on the task").option("--project <name>","Create task in a specific project (overrides CWD)").action(async(title,options)=>{try{await handleTaskCreate(title,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("list").description("List tasks with filters").option("--stage <stage>","Filter by stage").option("--type <type>","Filter by type").option("--status <status>","Filter by status").option("--priority <priority>","Filter by priority").option("--release <release>","Filter by release").option("--due-before <date>","Filter by due date").option("--mine","Show only tasks assigned to me").option("--project <name>","Show tasks for a specific project").option("--all","Show tasks from ALL projects").option("--json","Output as JSON").action(async(options)=>{try{let ts=await getTaskService6(),filters={stage:options.stage,typeId:options.type,status:options.status,priority:options.priority,releaseId:options.release,dueBefore:options.dueBefore,projectName:options.project,allProjects:options.all},tasks;if(options.mine)tasks=await ts.listTasksForActor(currentActor2(),filters);else tasks=await ts.listTasks(filters);if(options.json){console.log(JSON.stringify(tasks,null,2));return}printTaskList(tasks,options.all)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("show <id>").description("Show task detail (accepts task-id or #seq)").option("--json","Output as JSON").action(async(id,options)=>{try{let t=await(await getTaskService6()).getTask(id);if(!t)console.error(`Error: Task not found: ${id}`),process.exit(1);if(options.json){console.log(JSON.stringify(t,null,2));return}await printTaskDetail(t)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("move <id>").description("Move task to a new stage").requiredOption("--to <stage>","Target stage").option("--comment <msg>","Comment on the move").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.moveTask(id,options.to,actor,options.comment);console.log(`Moved task #${t.seq} to stage "${t.stage}".`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("assign <id>").description("Assign an actor to a task").requiredOption("--to <name>","Actor name").option("--role <role>","Actor role","assignee").option("--comment <msg>","Comment on the assignment").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2();if(await ts.assignTask(id,localActor2(options.to),options.role,{}),options.comment)await ts.commentOnTask(id,actor,options.comment);console.log(`Assigned "${options.to}" as ${options.role??"assignee"} on task ${id}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("tag <id> <tags...>").description("Add tags to a task").action(async(id,tags)=>{try{await(await getTaskService6()).tagTask(id,tags,currentActor2()),console.log(`Tagged task ${id} with: ${tags.join(", ")}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("comment <id> <message>").description("Add a comment to a task").option("--reply-to <msgId>","Reply to a specific message ID").action(async(id,message,options)=>{try{let ts=await getTaskService6(),replyTo=options.replyTo?Number(options.replyTo):void 0,msg=await ts.commentOnTask(id,currentActor2(),message,void 0,replyTo);console.log(`Comment #${msg.id} added to task ${id}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("block <id>").description("Mark task as blocked").requiredOption("--reason <reason>","Reason for blocking").option("--comment <msg>","Additional comment").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.blockTask(id,options.reason,actor,options.comment);console.log(`Task #${t.seq} blocked: ${options.reason}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("unblock <id>").description("Unblock a task").option("--comment <msg>","Comment on unblock").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.unblockTask(id,actor,options.comment);console.log(`Task #${t.seq} unblocked.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("done <id>").description("Mark task as done").option("--comment <msg>","Comment on completion").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.markDone(id,actor,options.comment);console.log(`Task #${t.seq} marked as done.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("checkout <id>").description("Atomically claim a task for execution").action(async(id)=>{try{let ts=await getTaskService6(),runId=getRunId(),t=await ts.checkoutTask(id,runId);console.log(`Checked out task #${t.seq} for run: ${runId}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("release <id>").description("Release task checkout claim").action(async(id)=>{try{let ts=await getTaskService6(),runId=getRunId(),t=await ts.releaseTask(id,runId);console.log(`Released task #${t.seq} from run: ${runId}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("unlock <id>").description("Force-release a stale checkout (admin override)").action(async(id)=>{try{let t=await(await getTaskService6()).forceUnlockTask(id);console.log(`Force-unlocked task #${t.seq}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("dep <id>").description("Manage task dependencies").option("--depends-on <id2>","This task depends on id2").option("--blocks <id2>","This task blocks id2").option("--relates-to <id2>","This task relates to id2").option("--remove <id2>","Remove dependency on id2").action(async(id,options)=>{try{let ts=await getTaskService6();if(options.remove){if(await ts.removeDependency(id,options.remove))console.log(`Removed dependency between ${id} and ${options.remove}.`);else console.log("No dependency found to remove.");return}if(options.dependsOn)await ts.addDependency(id,options.dependsOn,"depends_on"),console.log(`${id} now depends on ${options.dependsOn}.`);if(options.blocks)await ts.addDependency(id,options.blocks,"blocks"),console.log(`${id} now blocks ${options.blocks}.`);if(options.relatesTo)await ts.addDependency(id,options.relatesTo,"relates_to"),console.log(`${id} now relates to ${options.relatesTo}.`);if(!options.dependsOn&&!options.blocks&&!options.relatesTo)console.error("Error: Specify --depends-on, --blocks, --relates-to, or --remove."),process.exit(1)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}init_team_manager();import{existsSync as existsSync20}from"fs";import{copyFile as copyFile2,mkdir as mkdir11}from"fs/promises";import{join as join32,resolve as resolve6}from"path";function registerTeamNamespace(program2){let team=program2.command("team").description("Team lifecycle management");team.command("create <name>").description("Create a new team with a git worktree").requiredOption("--repo <path>","Path to the git repository").option("--branch <branch>","Base branch to create from","dev").option("--wish <slug>","Wish slug \u2014 auto-spawns a task leader with wish context").option("--session <name>","Tmux session name (avoids session explosion on parallel creates)").action(async(name,options)=>{try{await handleTeamCreate(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("hire <agent>").description('Add an agent to a team ("council" hires all 10 council members)').option("--team <name>","Team name (auto-detects from leader context if omitted)").action(async(agent,options)=>{try{let teamName=options.team??await autoDetectTeam();if(!teamName)console.error("Error: Could not detect team. Use --team <name> to specify."),process.exit(1);let added=await hireAgent(teamName,agent);if(added.length===0)console.log(`Agent "${agent}" is already a member of "${teamName}".`);else if(agent==="council"){console.log(`Hired ${added.length} council members to "${teamName}":`);for(let name of added)console.log(` + ${name}`)}else console.log(`Hired "${agent}" to team "${teamName}".`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("fire <agent>").description("Remove an agent from a team").option("--team <name>","Team name (auto-detects from leader context if omitted)").action(async(agent,options)=>{try{let teamName=options.team??await autoDetectTeam();if(!teamName)console.error("Error: Could not detect team. Use --team <name> to specify."),process.exit(1);if(await fireAgent(teamName,agent))console.log(`Fired "${agent}" from team "${teamName}".`);else console.error(`Agent "${agent}" is not a member of "${teamName}".`),process.exit(1)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("ls [name]").alias("list").description("List teams or members of a team").option("--json","Output as JSON").action(async(name,options)=>{try{if(name)await printMembers(name,options.json);else await printTeams(options.json)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("disband <name>").description("Disband a team: kill members, remove worktree, delete config").action(async(name)=>{try{if(await disbandTeam(name))console.log(`Team "${name}" disbanded.`);else console.error(`Team "${name}" not found.`),process.exit(1)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("done <name>").description("Mark a team as done and kill all members").action(async(name)=>{try{await setTeamStatus(name,"done"),await killTeamMembers(name),console.log(`Team "${name}" marked as done. All members killed.`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("blocked <name>").description("Mark a team as blocked and kill all members").action(async(name)=>{try{await setTeamStatus(name,"blocked"),await killTeamMembers(name),console.log(`Team "${name}" marked as blocked. All members killed.`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}async function handleTeamCreate(name,options){if(options.wish){let resolvedRepo=resolve6(options.repo),wishPath=join32(resolvedRepo,".genie","wishes",options.wish,"WISH.md");if(!existsSync20(wishPath))console.error(`Error: Wish not found at ${wishPath}`),process.exit(1)}let config=await createTeam(name,options.repo,options.branch);if(options.session)config.tmuxSessionName=options.session,await updateTeamConfig(name,config);if(console.log(`Team "${config.name}" created.`),console.log(` Worktree: ${config.worktreePath}`),console.log(` Branch: ${config.name} (from ${config.baseBranch})`),config.tmuxSessionName)console.log(` Session: ${config.tmuxSessionName}`);if(config.nativeTeamsEnabled)console.log(" Native teams: enabled");if(options.wish)await spawnLeaderWithWish(config,options.wish,options.repo,options.session)}async function spawnLeaderWithWish(config,slug,repoPath,sessionOverride){let{handleWorkerSpawn:handleWorkerSpawn2}=await Promise.resolve().then(() => (init_agents(),exports_agents)),{getCurrentSessionName:getCurrentSessionName2}=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),resolvedRepo=resolve6(repoPath),tmuxSession=sessionOverride??await getCurrentSessionName2(config.name)??config.name;config.tmuxSessionName=tmuxSession,await updateTeamConfig(config.name,config);let sourceWishPath=join32(resolvedRepo,".genie","wishes",slug,"WISH.md");if(!existsSync20(sourceWishPath))console.error(`Error: Wish not found at ${sourceWishPath}`),process.exit(1);let destWishDir=join32(config.worktreePath,".genie","wishes",slug);await mkdir11(destWishDir,{recursive:!0});let destWishPath=join32(destWishDir,"WISH.md");await copyFile2(sourceWishPath,destWishPath),console.log(` Wish: copied ${slug}/WISH.md into worktree`);let standardTeam=["team-lead","engineer","reviewer","qa","fix"];for(let role of standardTeam)await hireAgent(config.name,role);console.log(` Team: hired ${standardTeam.join(", ")}`);let members=standardTeam.filter((r)=>r!=="team-lead").join(", "),kickoffPrompt=`Your team is "${config.name}". Repo: ${config.repo}. Branch: ${config.name}. Worktree: ${config.worktreePath}. Wish slug: ${slug}. Your team members are: ${members} (already hired \u2014 genie work will spawn them automatically). Read the wish at .genie/wishes/${slug}/WISH.md and execute the full lifecycle autonomously.`;await handleWorkerSpawn2("team-lead",{provider:"claude",team:config.name,cwd:config.worktreePath,session:tmuxSession,initialPrompt:kickoffPrompt});let result=await(await Promise.resolve().then(() => (init_protocol_router(),exports_protocol_router))).sendMessage(config.worktreePath,"cli","team-lead",kickoffPrompt);if(!result.delivered)console.warn(`\u26A0 Backup delivery to team-lead failed: ${result.reason??"unknown"}`);console.log(" Leader: spawned and working")}async function autoDetectTeam(){let envTeam=process.env.GENIE_TEAM;if(envTeam)return envTeam;let teams=await listTeams2();if(teams.length===1)return teams[0].name;return null}async function printMembers(name,json2){let members=await listMembers(name);if(members===null)console.error(`Team "${name}" not found.`),process.exit(1);if(json2){console.log(JSON.stringify(members,null,2));return}if(members.length===0){console.log(`Team "${name}" has no members. Hire agents with: genie team hire <agent> --team ${name}`);return}console.log(""),console.log(`MEMBERS of "${name}"`),console.log("-".repeat(60));for(let m of members)console.log(` ${m}`);console.log("")}async function printTeams(json2){let teams=await listTeams2();if(json2){console.log(JSON.stringify(teams,null,2));return}if(teams.length===0){console.log("No teams found. Create one with: genie team create <name> --repo <path>");return}console.log(""),console.log("TEAMS"),console.log("-".repeat(60));for(let t of teams)printTeamSummary(t);console.log("")}function printTeamSummary(t){let status=t.status??"in_progress";console.log(` ${t.name} [${status}]`),console.log(` Repo: ${t.repo}`),console.log(` Branch: ${t.name} (from ${t.baseBranch})`),console.log(` Worktree: ${t.worktreePath}`),console.log(` Members: ${t.members.length}`)}var _taskService7;async function getTaskService7(){if(!_taskService7)_taskService7=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService7}function padRight10(str3,len){return str3.length>=len?str3:str3+" ".repeat(len-str3.length)}function printTypeTable(types3){console.log(` ${padRight10("ID",20)} ${padRight10("NAME",30)} ${padRight10("STAGES",8)} BUILTIN`),console.log(` ${"\u2500".repeat(70)}`);for(let t of types3){let stageCount=Array.isArray(t.stages)?t.stages.length:0,builtin=t.isBuiltin?"yes":"no";console.log(` ${padRight10(t.id,20)} ${padRight10(t.name,30)} ${padRight10(String(stageCount),8)} ${builtin}`)}console.log(`
|
|
1221
|
+
Messages:`);for(let msg of messages2){let time=formatTimestamp3(msg.createdAt),reply=msg.replyToId?` (reply to #${msg.replyToId})`:"";console.log(` [${time}] ${msg.senderId}: ${msg.body}${reply}`)}}}async function printTaskDetail(task){printTaskFields(task),await printTaskRelations(task),await printTaskMessages(task),console.log("")}async function handleTaskCreate(title,options){let ts=await getTaskService6(),actor=currentActor2(),repoPath,projectId;if(options.project){let project=await ts.getProjectByName(options.project);if(!project)project=await ts.createProject({name:options.project});projectId=project.id,repoPath=project.repoPath??void 0}let parentId;if(options.parent){if(parentId=await ts.resolveTaskId(options.parent,repoPath)??void 0,!parentId)console.error(`Error: Parent task not found: ${options.parent}`),process.exit(1)}let task=await ts.createTask({title,typeId:options.type,priority:options.priority,dueDate:options.due,startDate:options.start,parentId,description:options.description,estimatedEffort:options.effort},repoPath,projectId);if(await ts.assignTask(task.id,actor,"creator",{},task.repoPath),options.assign)await ts.assignTask(task.id,localActor2(options.assign),"assignee",{},task.repoPath);if(options.tags){let tagIds=options.tags.split(",").map((t)=>t.trim());await ts.tagTask(task.id,tagIds,actor,task.repoPath)}if(options.comment)await ts.commentOnTask(task.id,actor,options.comment,task.repoPath);if(console.log(`Created task #${task.seq}: ${task.title}`),console.log(` ID: ${task.id}`),console.log(` Stage: ${task.stage} | Priority: ${task.priority}`),options.due)console.log(` Due: ${options.due}`)}function registerTaskCommands(program2){let task=program2.command("task").description("Task lifecycle management");task.command("create <title>").description("Create a new task").option("--type <type>","Task type","software").option("--priority <priority>","Priority: urgent, high, normal, low","normal").option("--due <date>","Due date (YYYY-MM-DD)").option("--start <date>","Start date (YYYY-MM-DD)").option("--tags <tags>","Comma-separated tag IDs").option("--parent <id>","Parent task ID or #seq").option("--assign <name>","Assign to local actor").option("--description <text>","Task description").option("--effort <effort>",'Estimated effort (e.g., "2h", "3 points")').option("--comment <msg>","Initial comment on the task").option("--project <name>","Create task in a specific project (overrides CWD)").action(async(title,options)=>{try{await handleTaskCreate(title,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("list").description("List tasks with filters").option("--stage <stage>","Filter by stage").option("--type <type>","Filter by type").option("--status <status>","Filter by status").option("--priority <priority>","Filter by priority").option("--release <release>","Filter by release").option("--due-before <date>","Filter by due date").option("--mine","Show only tasks assigned to me").option("--project <name>","Show tasks for a specific project").option("--all","Show tasks from ALL projects").option("--json","Output as JSON").action(async(options)=>{try{let ts=await getTaskService6(),filters={stage:options.stage,typeId:options.type,status:options.status,priority:options.priority,releaseId:options.release,dueBefore:options.dueBefore,projectName:options.project,allProjects:options.all},tasks;if(options.mine)tasks=await ts.listTasksForActor(currentActor2(),filters);else tasks=await ts.listTasks(filters);if(options.json){console.log(JSON.stringify(tasks,null,2));return}printTaskList(tasks,options.all)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("show <id>").description("Show task detail (accepts task-id or #seq)").option("--json","Output as JSON").action(async(id,options)=>{try{let t=await(await getTaskService6()).getTask(id);if(!t)console.error(`Error: Task not found: ${id}`),process.exit(1);if(options.json){console.log(JSON.stringify(t,null,2));return}await printTaskDetail(t)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("move <id>").description("Move task to a new stage").requiredOption("--to <stage>","Target stage").option("--comment <msg>","Comment on the move").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.moveTask(id,options.to,actor,options.comment);console.log(`Moved task #${t.seq} to stage "${t.stage}".`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("assign <id>").description("Assign an actor to a task").requiredOption("--to <name>","Actor name").option("--role <role>","Actor role","assignee").option("--comment <msg>","Comment on the assignment").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2();if(await ts.assignTask(id,localActor2(options.to),options.role,{}),options.comment)await ts.commentOnTask(id,actor,options.comment);console.log(`Assigned "${options.to}" as ${options.role??"assignee"} on task ${id}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("tag <id> <tags...>").description("Add tags to a task").action(async(id,tags)=>{try{await(await getTaskService6()).tagTask(id,tags,currentActor2()),console.log(`Tagged task ${id} with: ${tags.join(", ")}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("comment <id> <message>").description("Add a comment to a task").option("--reply-to <msgId>","Reply to a specific message ID").action(async(id,message,options)=>{try{let ts=await getTaskService6(),replyTo=options.replyTo?Number(options.replyTo):void 0,msg=await ts.commentOnTask(id,currentActor2(),message,void 0,replyTo);console.log(`Comment #${msg.id} added to task ${id}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("block <id>").description("Mark task as blocked").requiredOption("--reason <reason>","Reason for blocking").option("--comment <msg>","Additional comment").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.blockTask(id,options.reason,actor,options.comment);console.log(`Task #${t.seq} blocked: ${options.reason}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("unblock <id>").description("Unblock a task").option("--comment <msg>","Comment on unblock").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.unblockTask(id,actor,options.comment);console.log(`Task #${t.seq} unblocked.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("done <id>").description("Mark task as done").option("--comment <msg>","Comment on completion").action(async(id,options)=>{try{let ts=await getTaskService6(),actor=currentActor2(),t=await ts.markDone(id,actor,options.comment);console.log(`Task #${t.seq} marked as done.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("checkout <id>").description("Atomically claim a task for execution").action(async(id)=>{try{let ts=await getTaskService6(),runId=getRunId(),t=await ts.checkoutTask(id,runId);console.log(`Checked out task #${t.seq} for run: ${runId}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("release <id>").description("Release task checkout claim").action(async(id)=>{try{let ts=await getTaskService6(),runId=getRunId(),t=await ts.releaseTask(id,runId);console.log(`Released task #${t.seq} from run: ${runId}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("unlock <id>").description("Force-release a stale checkout (admin override)").action(async(id)=>{try{let t=await(await getTaskService6()).forceUnlockTask(id);console.log(`Force-unlocked task #${t.seq}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),task.command("dep <id>").description("Manage task dependencies").option("--depends-on <id2>","This task depends on id2").option("--blocks <id2>","This task blocks id2").option("--relates-to <id2>","This task relates to id2").option("--remove <id2>","Remove dependency on id2").action(async(id,options)=>{try{let ts=await getTaskService6();if(options.remove){if(await ts.removeDependency(id,options.remove))console.log(`Removed dependency between ${id} and ${options.remove}.`);else console.log("No dependency found to remove.");return}if(options.dependsOn)await ts.addDependency(id,options.dependsOn,"depends_on"),console.log(`${id} now depends on ${options.dependsOn}.`);if(options.blocks)await ts.addDependency(id,options.blocks,"blocks"),console.log(`${id} now blocks ${options.blocks}.`);if(options.relatesTo)await ts.addDependency(id,options.relatesTo,"relates_to"),console.log(`${id} now relates to ${options.relatesTo}.`);if(!options.dependsOn&&!options.blocks&&!options.relatesTo)console.error("Error: Specify --depends-on, --blocks, --relates-to, or --remove."),process.exit(1)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}init_team_manager();import{existsSync as existsSync20}from"fs";import{copyFile as copyFile2,cp,mkdir as mkdir11}from"fs/promises";import{join as join32,resolve as resolve6}from"path";function registerTeamNamespace(program2){let team=program2.command("team").description("Team lifecycle management");team.command("create <name>").description("Create a new team with a git worktree").requiredOption("--repo <path>","Path to the git repository").option("--branch <branch>","Base branch to create from","dev").option("--wish <slug>","Wish slug \u2014 auto-spawns a task leader with wish context").option("--session <name>","Tmux session name (avoids session explosion on parallel creates)").action(async(name,options)=>{try{await handleTeamCreate(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("hire <agent>").description('Add an agent to a team ("council" hires all 10 council members)').option("--team <name>","Team name (auto-detects from leader context if omitted)").action(async(agent,options)=>{try{let teamName=options.team??await autoDetectTeam();if(!teamName)console.error("Error: Could not detect team. Use --team <name> to specify."),process.exit(1);let added=await hireAgent(teamName,agent);if(added.length===0)console.log(`Agent "${agent}" is already a member of "${teamName}".`);else if(agent==="council"){console.log(`Hired ${added.length} council members to "${teamName}":`);for(let name of added)console.log(` + ${name}`)}else console.log(`Hired "${agent}" to team "${teamName}".`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("fire <agent>").description("Remove an agent from a team").option("--team <name>","Team name (auto-detects from leader context if omitted)").action(async(agent,options)=>{try{let teamName=options.team??await autoDetectTeam();if(!teamName)console.error("Error: Could not detect team. Use --team <name> to specify."),process.exit(1);if(await fireAgent(teamName,agent))console.log(`Fired "${agent}" from team "${teamName}".`);else console.error(`Agent "${agent}" is not a member of "${teamName}".`),process.exit(1)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("ls [name]").alias("list").description("List teams or members of a team").option("--json","Output as JSON").action(async(name,options)=>{try{if(name)await printMembers(name,options.json);else await printTeams(options.json)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("disband <name>").description("Disband a team: kill members, remove worktree, delete config").action(async(name)=>{try{if(await disbandTeam(name))console.log(`Team "${name}" disbanded.`);else console.error(`Team "${name}" not found.`),process.exit(1)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("done <name>").description("Mark a team as done and kill all members").action(async(name)=>{try{await setTeamStatus(name,"done"),await killTeamMembers(name),console.log(`Team "${name}" marked as done. All members killed.`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}}),team.command("blocked <name>").description("Mark a team as blocked and kill all members").action(async(name)=>{try{await setTeamStatus(name,"blocked"),await killTeamMembers(name),console.log(`Team "${name}" marked as blocked. All members killed.`)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}})}async function handleTeamCreate(name,options){if(options.wish){let resolvedRepo=resolve6(options.repo),wishPath=join32(resolvedRepo,".genie","wishes",options.wish,"WISH.md");if(!existsSync20(wishPath)){let cwdWishDir=join32(process.cwd(),".genie","wishes",options.wish),cwdWishPath=join32(cwdWishDir,"WISH.md");if(existsSync20(cwdWishPath)){let destDir=join32(resolvedRepo,".genie","wishes",options.wish);await mkdir11(destDir,{recursive:!0}),await cp(cwdWishDir,destDir,{recursive:!0}),console.log(`Wish: copied ${options.wish}/WISH.md to repo`)}else console.error(`Error: Wish not found at ${wishPath}`),process.exit(1)}}let config=await createTeam(name,options.repo,options.branch);if(options.session)config.tmuxSessionName=options.session,await updateTeamConfig(name,config);if(console.log(`Team "${config.name}" created.`),console.log(` Worktree: ${config.worktreePath}`),console.log(` Branch: ${config.name} (from ${config.baseBranch})`),config.tmuxSessionName)console.log(` Session: ${config.tmuxSessionName}`);if(config.nativeTeamsEnabled)console.log(" Native teams: enabled");if(options.wish)await spawnLeaderWithWish(config,options.wish,options.repo,options.session)}async function spawnLeaderWithWish(config,slug,repoPath,sessionOverride){let{handleWorkerSpawn:handleWorkerSpawn2}=await Promise.resolve().then(() => (init_agents(),exports_agents)),{getCurrentSessionName:getCurrentSessionName2}=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),resolvedRepo=resolve6(repoPath),tmuxSession=sessionOverride??await getCurrentSessionName2(config.name)??config.name;config.tmuxSessionName=tmuxSession,await updateTeamConfig(config.name,config);let sourceWishPath=join32(resolvedRepo,".genie","wishes",slug,"WISH.md");if(!existsSync20(sourceWishPath))console.error(`Error: Wish not found at ${sourceWishPath}`),process.exit(1);let destWishDir=join32(config.worktreePath,".genie","wishes",slug);await mkdir11(destWishDir,{recursive:!0});let destWishPath=join32(destWishDir,"WISH.md");await copyFile2(sourceWishPath,destWishPath),console.log(` Wish: copied ${slug}/WISH.md into worktree`);let standardTeam=["team-lead","engineer","reviewer","qa","fix"];for(let role of standardTeam)await hireAgent(config.name,role);console.log(` Team: hired ${standardTeam.join(", ")}`);let members=standardTeam.filter((r)=>r!=="team-lead").join(", "),kickoffPrompt=`Your team is "${config.name}". Repo: ${config.repo}. Branch: ${config.name}. Worktree: ${config.worktreePath}. Wish slug: ${slug}. Your team members are: ${members} (already hired \u2014 genie work will spawn them automatically). Read the wish at .genie/wishes/${slug}/WISH.md and execute the full lifecycle autonomously.`;await handleWorkerSpawn2("team-lead",{provider:"claude",team:config.name,cwd:config.worktreePath,session:tmuxSession,initialPrompt:kickoffPrompt});let result=await(await Promise.resolve().then(() => (init_protocol_router(),exports_protocol_router))).sendMessage(config.worktreePath,"cli","team-lead",kickoffPrompt);if(!result.delivered)console.warn(`\u26A0 Backup delivery to team-lead failed: ${result.reason??"unknown"}`);console.log(" Leader: spawned and working")}async function autoDetectTeam(){let envTeam=process.env.GENIE_TEAM;if(envTeam)return envTeam;let teams=await listTeams2();if(teams.length===1)return teams[0].name;return null}async function printMembers(name,json2){let members=await listMembers(name);if(members===null)console.error(`Team "${name}" not found.`),process.exit(1);if(json2){console.log(JSON.stringify(members,null,2));return}if(members.length===0){console.log(`Team "${name}" has no members. Hire agents with: genie team hire <agent> --team ${name}`);return}console.log(""),console.log(`MEMBERS of "${name}"`),console.log("-".repeat(60));for(let m of members)console.log(` ${m}`);console.log("")}async function printTeams(json2){let teams=await listTeams2();if(json2){console.log(JSON.stringify(teams,null,2));return}if(teams.length===0){console.log("No teams found. Create one with: genie team create <name> --repo <path>");return}console.log(""),console.log("TEAMS"),console.log("-".repeat(60));for(let t of teams)printTeamSummary(t);console.log("")}function printTeamSummary(t){let status=t.status??"in_progress";console.log(` ${t.name} [${status}]`),console.log(` Repo: ${t.repo}`),console.log(` Branch: ${t.name} (from ${t.baseBranch})`),console.log(` Worktree: ${t.worktreePath}`),console.log(` Members: ${t.members.length}`)}var _taskService7;async function getTaskService7(){if(!_taskService7)_taskService7=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService7}function padRight10(str3,len){return str3.length>=len?str3:str3+" ".repeat(len-str3.length)}function printTypeTable(types3){console.log(` ${padRight10("ID",20)} ${padRight10("NAME",30)} ${padRight10("STAGES",8)} BUILTIN`),console.log(` ${"\u2500".repeat(70)}`);for(let t of types3){let stageCount=Array.isArray(t.stages)?t.stages.length:0,builtin=t.isBuiltin?"yes":"no";console.log(` ${padRight10(t.id,20)} ${padRight10(t.name,30)} ${padRight10(String(stageCount),8)} ${builtin}`)}console.log(`
|
|
1222
1222
|
${types3.length} type${types3.length===1?"":"s"}`)}function printTypePipeline(t){if(console.log(`
|
|
1223
1223
|
Type: ${t.name} (${t.id})`),t.description)console.log(`Description: ${t.description}`);if(t.icon)console.log(`Icon: ${t.icon}`);console.log(`Built-in: ${t.isBuiltin?"yes":"no"}`),console.log("\u2500".repeat(60)),console.log(`
|
|
1224
1224
|
Stage Pipeline:`);let stages=t.stages;for(let i2=0;i2<stages.length;i2++){let s=stages[i2],arrow=i2<stages.length-1?" \u2192":"",gate=s.gate?` [gate: ${s.gate}]`:"",action=s.action?` (action: ${s.action})`:"",auto=s.auto_advance?" [auto]":"";console.log(` ${i2+1}. ${s.label??s.name}${gate}${action}${auto}${arrow}`)}console.log("")}async function handleTypeList(options){let types3=await(await getTaskService7()).listTypes();if(options.json){console.log(JSON.stringify(types3,null,2));return}printTypeTable(types3)}async function handleTypeShow(id,options){let t=await(await getTaskService7()).getType(id);if(!t)console.error(`Error: Type not found: ${id}`),process.exit(1);if(options.json){console.log(JSON.stringify(t,null,2));return}printTypePipeline(t)}async function handleTypeCreate(name,options){let ts=await getTaskService7(),stages;try{if(stages=JSON.parse(options.stages),!Array.isArray(stages))throw Error("Stages must be a JSON array")}catch(err){console.error(`Error: Invalid stages JSON. ${err instanceof Error?err.message:String(err)}`),process.exit(1)}for(let s of stages)if(typeof s!=="object"||s===null||!("name"in s))console.error('Error: Each stage must have at least a "name" field.'),process.exit(1);let id=name.toLowerCase().replace(/\s+/g,"-"),t=await ts.createType({id,name,description:options.description,icon:options.icon,stages});console.log(`Created type "${t.name}" (${t.id}) with ${stages.length} stages.`)}function registerTypeCommands(program2){let type2=program2.command("type").description("Task type management");type2.command("list").description("List all task types").option("--json","Output as JSON").action(async(options)=>{try{await handleTypeList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),type2.command("show <id>").description("Show task type detail with stage pipeline").option("--json","Output as JSON").action(async(id,options)=>{try{await handleTypeShow(id,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),type2.command("create <name>").description("Create a custom task type").requiredOption("--stages <json>","Stages JSON array").option("--description <text>","Type description").option("--icon <icon>","Type icon").action(async(name,options)=>{try{await handleTypeCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}try{let{execSync:execSyncStartup}=__require("child_process");if(execSyncStartup("git config core.bare",{encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()==="true")execSyncStartup("git config core.bare false",{stdio:["pipe","pipe","pipe"]})}catch{}var program2=new Command;program2.name("genie").description("Genie CLI - AI-assisted development").version(VERSION);async function startNamedSession(name){let{buildTeamLeadCommand:buildTeamLeadCommand2,sessionExists:sessionExists2}=await Promise.resolve().then(() => (init_team_lead_command(),exports_team_lead_command)),{getAgentsFilePath:getAgentsFilePath2}=await Promise.resolve().then(() => (init_session(),exports_session)),systemPromptFile=getAgentsFilePath2(),hasPriorSession=sessionExists2(name),cmd=buildTeamLeadCommand2(name,{systemPromptFile:systemPromptFile??void 0,continueName:hasPriorSession?name:void 0});console.log(hasPriorSession?`Resuming session: ${name}`:`Starting new session: ${name}`);let{spawnSync:spawnSync2}=await import("child_process"),result=spawnSync2("sh",["-c",cmd],{stdio:"inherit"});if(result.status)process.exit(result.status)}program2.command("setup").description("Configure genie settings").option("--quick","Accept all defaults").option("--shortcuts","Only configure keyboard shortcuts").option("--codex","Only configure Codex integration").option("--terminal","Only configure terminal defaults").option("--session","Only configure session settings").option("--reset","Reset configuration to defaults").option("--show","Show current configuration").action(async(options)=>{await setupCommand(options)});program2.command("doctor").description("Run diagnostic checks on genie installation").action(doctorCommand);program2.command("update").description("Update Genie CLI to the latest version").option("--next","Switch to dev builds (npm @next tag)").option("--stable","Switch to stable releases (npm @latest tag)").action(updateCommand);program2.command("uninstall").description("Remove Genie CLI and clean up hooks").action(uninstallCommand);var shortcuts=program2.command("shortcuts").description("Manage tmux keyboard shortcuts");shortcuts.action(shortcutsShowCommand);shortcuts.command("show").description("Show available shortcuts and installation status").action(shortcutsShowCommand);shortcuts.command("install").description("Install shortcuts to config files (~/.tmux.conf, shell rc)").action(shortcutsInstallCommand);shortcuts.command("uninstall").description("Remove shortcuts from config files").action(shortcutsUninstallCommand);registerTeamNamespace(program2);registerDirNamespace(program2);registerAgentNamespace(program2);registerSendInboxCommands(program2);registerStateCommands(program2);registerDispatchCommands(program2);registerHookNamespace(program2);registerDbCommands(program2);registerScheduleCommands(program2);registerDaemonCommands(program2);registerTaskCommands(program2);registerTypeCommands(program2);registerTagCommands(program2);registerReleaseCommands(program2);registerProjectCommands(program2);registerNotifyCommands(program2);program2.command("spawn <name>").description("Spawn a new agent by name (resolves from directory or built-ins)").option("--provider <provider>","Provider: claude or codex","claude").option("--team <team>","Team name",process.env.GENIE_TEAM??"genie").option("--model <model>","Model override (e.g., sonnet, opus)").option("--skill <skill>","Skill to load (optional)").option("--layout <layout>","Layout mode: mosaic (default) or vertical").option("--color <color>","Teammate pane border color").option("--plan-mode","Start teammate in plan mode").option("--permission-mode <mode>","Permission mode (e.g., acceptEdits)").option("--extra-args <args...>","Extra CLI args forwarded to provider").option("--cwd <path>","Working directory for the agent (overrides directory entry)").option("--session <session>","Tmux session name to spawn into").option("--no-auto-resume","Disable auto-resume on pane death").action(async(name,options)=>{try{await handleWorkerSpawn(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.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)}});program2.command("stop <name>").description("Stop an agent (preserves session for resume)").action(async(name)=>{try{await handleWorkerStop(name)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("resume [name]").description("Resume a suspended/failed agent with its Claude session").option("--all","Resume all eligible agents").action(async(name,options)=>{try{await handleWorkerResume(name,options)}catch(error2){let message=error2 instanceof Error?error2.message:String(error2);console.error(`Error: ${message}`),process.exit(1)}});program2.command("history <name>").description("Show compressed session history for an agent").option("--full","Show full conversation without compression").option("--since <n>","Show last N user/assistant exchanges",Number.parseInt).option("--last <n>","Show last N transcript entries",Number.parseInt).option("--type <role>","Filter by role (user, assistant, tool_call)").option("--after <timestamp>","Only entries after ISO timestamp").option("--json","Output as JSON").option("--ndjson","Output as newline-delimited JSON (pipeable to jq)").option("--raw","Output raw JSONL entries").option("--log-file <path>","Direct path to log file (for testing)").action(async(name,options)=>{await historyCommand(name,options)});program2.command("log [agent]").description("Unified observability feed \u2014 aggregates transcript, DMs, team chat").option("--team <name>","Show interleaved feed for all agents in a team").option("--type <kind>","Filter by event kind (transcript, message, tool_call, state, system)").option("--since <timestamp>","Only events after ISO timestamp").option("--last <n>","Show last N events",Number.parseInt).option("--ndjson","Output as newline-delimited JSON (pipeable to jq)").option("--json","Output as pretty JSON").option("-f, --follow","Follow mode \u2014 real-time streaming").action(async(agent,options)=>{await logCommand(agent,options)});var qaCmd=program2.command("qa").description("QA \u2014 self-testing system for genie CLI");qaCmd.command("run [target]",{isDefault:!0}).description("Run QA specs (all, a domain, or a single spec)").option("--timeout <seconds>","Max seconds per spec",(v)=>Number(v),60).option("--parallel <n>","Max specs to run in parallel",(v)=>Number(v),5).option("--verbose","Show all collected events").option("--ndjson","Machine-readable NDJSON output").action(async(target,options)=>{await qaCommand(target,options)});qaCmd.command("status").description("Show QA dashboard with last results per spec").option("--json","Output as JSON").action(async(options)=>{await qaStatusCommand(options)});qaCmd.command("history").description("Show recent QA runs").action(async()=>{await qaHistoryCommand()});program2.command("qa-report <json>").description("Publish QA result via NATS (called by QA team-lead)").action(async(json2)=>{let team=process.env.GENIE_TEAM;if(!team)console.error("Error: GENIE_TEAM not set. This command must be run by a QA team-lead agent."),process.exit(1);try{let{publish:publish2,close:close2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client)),data=JSON.parse(json2);await publish2(`genie.qa.${team}.result`,data),await close2(),console.log(`QA result published to genie.qa.${team}.result`)}catch(err){console.error(`Failed to publish QA result: ${err}`),process.exit(1)}});program2.command("read <name>").description("Read terminal output from an agent pane").option("-n, --lines <number>","Number of lines to read").option("--from <line>","Start line").option("--to <line>","End line").option("--range <range>",'Line range (e.g., "10-20")').option("--search <text>","Search for text").option("--grep <pattern>","Grep for pattern").option("-f, --follow","Follow mode (like tail -f)").option("--all","Show all output").option("-r, --reverse","Reverse order").option("--json","Output as JSON").action(async(name,options)=>{await readSessionLogs2(name,options)});program2.command("answer <name> <choice>").description('Answer a question for an agent (use "text:..." for text input)').action(async(name,choice)=>{await answerQuestion(name,choice)});program2.command("ls").description("List registered agents with runtime status").option("--json","Output as JSON").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)}});var args=process.argv.slice(2);if(args.length===0||args.every((a)=>a==="--reset")){let{sessionCommand:sessionCommand2}=await Promise.resolve().then(() => (init_session(),exports_session));await sessionCommand2({reset:args.includes("--reset")}),process.exit(0)}var sessionIdx=args.indexOf("--session");if(sessionIdx!==-1&&sessionIdx+1<args.length){let sessionName=args[sessionIdx+1];if(!args.filter((_,i2)=>i2!==sessionIdx&&i2!==sessionIdx+1).some((a)=>!a.startsWith("-")))try{await startNamedSession(sessionName),process.exit(0)}catch(err){console.error(`Error: ${err instanceof Error?err.message:err}`),process.exit(1)}else program2.parse()}else program2.parse();
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260324.
|
|
3
|
+
"version": "4.260324.19",
|
|
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"
|
package/skills/docs/SKILL.md
CHANGED
|
@@ -42,6 +42,25 @@ Audit and maintain these doc types:
|
|
|
42
42
|
genie spawn docs
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Example
|
|
46
|
+
|
|
47
|
+
After shipping a new `genie work` dispatch fix, the orchestrator runs `/docs` to update documentation:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Spawn a docs subagent
|
|
51
|
+
genie spawn docs
|
|
52
|
+
|
|
53
|
+
# 2. Send the task
|
|
54
|
+
genie send 'Audit and update docs after PR #746 (initialPrompt added to dispatch). Check: README.md, CLAUDE.md, CO-ORCHESTRATION-GUIDE.md, skills/work/SKILL.md — verify dispatch examples match current code. Fix any stale references.' --to docs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The docs agent:
|
|
58
|
+
1. Scans all doc surfaces for references to `genie work`, dispatch, `protocolRouter.sendMessage`
|
|
59
|
+
2. Finds CO-ORCHESTRATION-GUIDE.md still references the old dispatch flow
|
|
60
|
+
3. Updates the guide with the new `initialPrompt` pattern
|
|
61
|
+
4. Validates every file path and API reference exists in the codebase
|
|
62
|
+
5. Reports: "Updated 1 file (CO-ORCHESTRATION-GUIDE.md). 3 files verified current. 0 dead references."
|
|
63
|
+
|
|
45
64
|
## Rules
|
|
46
65
|
- Validate every claim against actual code — no fiction.
|
|
47
66
|
- No dead references — every path, function, and API mentioned must exist in the codebase.
|
package/skills/fix/SKILL.md
CHANGED
|
@@ -61,6 +61,36 @@ When a PG task exists for the work being fixed, log each fix attempt as a task c
|
|
|
61
61
|
|
|
62
62
|
**Graceful degradation:** If no PG task exists for the work being fixed, skip all `genie task` commands. Fix loop logging is an enhancement — the fix flow must never fail due to missing tasks.
|
|
63
63
|
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
`/review` returned FIX-FIRST with 2 gaps:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
- [CRITICAL] workDispatchCommand missing initialPrompt — dispatch.ts:532
|
|
70
|
+
- [HIGH] protocolRouter.sendMessage result not checked — dispatch.ts:541
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The orchestrator runs `/fix`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Loop 1: dispatch fixer with gaps
|
|
77
|
+
genie spawn fixer
|
|
78
|
+
genie send 'Fix these gaps from /review on wish fix-dispatch-initial-prompt:
|
|
79
|
+
- [CRITICAL] dispatch.ts:532 — add initialPrompt to handleWorkerSpawn
|
|
80
|
+
- [HIGH] dispatch.ts:541 — check protocolRouter.sendMessage result, log warning on failure
|
|
81
|
+
Reference: qa-runner.ts:334 shows correct pattern.' --to fixer
|
|
82
|
+
|
|
83
|
+
# Wait for fixer to complete
|
|
84
|
+
sleep 60 && genie read fixer
|
|
85
|
+
|
|
86
|
+
# Re-review (separate subagent — never the same as fixer)
|
|
87
|
+
genie spawn reviewer
|
|
88
|
+
genie send 'Review wish fix-dispatch-initial-prompt. Check the fixer changes against acceptance criteria. Run bun test.' --to reviewer
|
|
89
|
+
|
|
90
|
+
# Reviewer returns SHIP → done
|
|
91
|
+
# If FIX-FIRST again → loop 2 (max 2 loops, then escalate)
|
|
92
|
+
```
|
|
93
|
+
|
|
64
94
|
## Rules
|
|
65
95
|
- Never fix and review in the same session — always separate subagents.
|
|
66
96
|
- Never exceed 2 fix loops — escalate, don't spin.
|
package/skills/learn/SKILL.md
CHANGED
|
@@ -68,6 +68,36 @@ When a learning is applied, also save it as a feedback memory:
|
|
|
68
68
|
|
|
69
69
|
This ensures the learning persists across conversations via Claude's native memory system.
|
|
70
70
|
|
|
71
|
+
## Example
|
|
72
|
+
|
|
73
|
+
User corrects the agent: "Stop using `pip install` — this system only has `uv`."
|
|
74
|
+
|
|
75
|
+
The agent runs `/learn`:
|
|
76
|
+
|
|
77
|
+
1. **Analyze:** Agent used `pip install python-dotenv` which failed because pip isn't installed.
|
|
78
|
+
2. **Root cause:** No rule in `~/.claude/rules/` about Python tooling.
|
|
79
|
+
3. **Surface:** Global rules (`~/.claude/rules/python-tooling.md`) — applies to all projects.
|
|
80
|
+
4. **Propose fix (plan mode):**
|
|
81
|
+
```
|
|
82
|
+
Create ~/.claude/rules/python-tooling.md:
|
|
83
|
+
- NEVER use pip or pip3 — not installed
|
|
84
|
+
- Use uv tool install for persistent CLI tools
|
|
85
|
+
- Use uv pip install inside venvs
|
|
86
|
+
```
|
|
87
|
+
5. **User approves.** File written.
|
|
88
|
+
6. **Save to memory:**
|
|
89
|
+
```markdown
|
|
90
|
+
# ~/.claude/memory/feedback_python_tooling.md
|
|
91
|
+
---
|
|
92
|
+
name: use-uv-not-pip
|
|
93
|
+
description: System uses uv for Python package management, pip is not installed
|
|
94
|
+
type: feedback
|
|
95
|
+
---
|
|
96
|
+
Use uv instead of pip for all Python operations.
|
|
97
|
+
**Why:** pip is not installed on this system; uv is the only package manager.
|
|
98
|
+
**How to apply:** Any time a Python package needs installing, use uv tool install or uv pip install.
|
|
99
|
+
```
|
|
100
|
+
|
|
71
101
|
## Rules
|
|
72
102
|
- **Plan mode is mandatory** — never write without user approval via native plan mode.
|
|
73
103
|
- **One learning at a time** — diagnose one surface, propose one fix.
|
package/skills/report/SKILL.md
CHANGED
|
@@ -228,6 +228,48 @@ Priority mapping from severity:
|
|
|
228
228
|
|
|
229
229
|
**Graceful degradation:** If PG is unavailable, skip `genie task` commands. The GitHub issue is the primary artifact — PG task tracking is an enhancement. The report must always be produced regardless of PG availability.
|
|
230
230
|
|
|
231
|
+
## Example
|
|
232
|
+
|
|
233
|
+
User reports: "genie work dispatches engineers but they sit idle."
|
|
234
|
+
|
|
235
|
+
The report agent:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# 1. Collect symptoms (one question at a time)
|
|
239
|
+
# Agent asks: "What command did you run?" → "genie team create rlmx --wish tauri-docs-agent"
|
|
240
|
+
# Agent asks: "What did you see?" → "Engineers show welcome screen but empty prompt"
|
|
241
|
+
|
|
242
|
+
# 2. Run /trace
|
|
243
|
+
genie spawn tracer
|
|
244
|
+
genie send 'Trace: genie work dispatches engineers but they start idle. Check dispatch.ts and protocol-router.ts.' --to tracer
|
|
245
|
+
# Wait for diagnosis...
|
|
246
|
+
|
|
247
|
+
# 3. Capture evidence
|
|
248
|
+
# Screenshot of idle engineer pane showing empty ❯ prompt
|
|
249
|
+
# Output of: genie status <slug> showing "in_progress" but no actual progress
|
|
250
|
+
|
|
251
|
+
# 4. Create GitHub issue with all findings
|
|
252
|
+
gh issue create --title "bug: genie work dispatch — engineers spawn idle without initial task prompt" --body "$(cat <<'EOF'
|
|
253
|
+
## Summary
|
|
254
|
+
Engineers dispatched by genie work start idle because initialPrompt is missing from handleWorkerSpawn.
|
|
255
|
+
|
|
256
|
+
## Root Cause (from /trace)
|
|
257
|
+
dispatch.ts:532 — handleWorkerSpawn called without initialPrompt.
|
|
258
|
+
protocolRouter.sendMessage fails silently under concurrent dispatch (4/6 engineers got no message).
|
|
259
|
+
|
|
260
|
+
## Evidence
|
|
261
|
+
- [Screenshot: idle engineer pane]
|
|
262
|
+
- genie status shows in_progress but engineers at empty prompt
|
|
263
|
+
- Native inbox files: engineer-1 through engineer-4 have no dispatch message
|
|
264
|
+
|
|
265
|
+
## Steps to Reproduce
|
|
266
|
+
1. genie team create test --wish <any-wish-with-2+-groups>
|
|
267
|
+
2. Team-lead runs genie work <slug>
|
|
268
|
+
3. Check engineer panes — they show empty ❯ prompt
|
|
269
|
+
EOF
|
|
270
|
+
)"
|
|
271
|
+
```
|
|
272
|
+
|
|
231
273
|
## Rules
|
|
232
274
|
- Always run `/trace` first — it is the backbone of every report.
|
|
233
275
|
- One question at a time when collecting symptoms — never batch questions.
|
package/skills/review/SKILL.md
CHANGED
|
@@ -124,6 +124,39 @@ When a PG task exists for the reviewed work, log the verdict as a task comment:
|
|
|
124
124
|
|
|
125
125
|
**Graceful degradation:** If no PG task exists for the reviewed work, skip all `genie task` commands. Verdict logging is an enhancement — the review flow must never fail due to missing tasks.
|
|
126
126
|
|
|
127
|
+
## Example
|
|
128
|
+
|
|
129
|
+
After `/work` completes wish `fix-dispatch-initial-prompt`, the orchestrator dispatches `/review`:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
genie spawn reviewer
|
|
133
|
+
genie send 'Review wish fix-dispatch-initial-prompt execution. Criteria:
|
|
134
|
+
1. initialPrompt added to all 5 dispatch call sites
|
|
135
|
+
2. protocolRouter.sendMessage kept as backup with warning logging
|
|
136
|
+
3. bun test passes
|
|
137
|
+
4. bun run typecheck clean
|
|
138
|
+
Check: gh pr diff 746, then run validations.' --to reviewer
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Reviewer output:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
## Review: fix-dispatch-initial-prompt (Execution)
|
|
145
|
+
|
|
146
|
+
### Checklist
|
|
147
|
+
- [x] All acceptance criteria met — 5/5 call sites have initialPrompt
|
|
148
|
+
- [x] Validation: bun test — 1137 pass, 0 fail
|
|
149
|
+
- [x] Validation: bun run typecheck — clean
|
|
150
|
+
- [x] No scope creep — only dispatch.ts and team.ts modified
|
|
151
|
+
- [x] No regressions — test count unchanged
|
|
152
|
+
|
|
153
|
+
### Gaps
|
|
154
|
+
(none)
|
|
155
|
+
|
|
156
|
+
### Verdict: SHIP
|
|
157
|
+
Next: create PR targeting dev
|
|
158
|
+
```
|
|
159
|
+
|
|
127
160
|
## Rules
|
|
128
161
|
- Never mark PASS without evidence — verify, don't assume.
|
|
129
162
|
- Never ship with CRITICAL or HIGH gaps.
|
package/skills/trace/SKILL.md
CHANGED
|
@@ -57,6 +57,34 @@ Include file paths and line numbers in every root cause comment so the task hist
|
|
|
57
57
|
|
|
58
58
|
**Graceful degradation:** If no PG task exists for the investigated work, skip all `genie task` commands. Findings logging is an enhancement — the trace flow must never fail due to missing tasks.
|
|
59
59
|
|
|
60
|
+
## Example
|
|
61
|
+
|
|
62
|
+
An engineer reports that `genie work` dispatches engineers but they sit idle. The orchestrator runs `/trace`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. Spawn a tracer (read-only — no code changes)
|
|
66
|
+
genie spawn tracer
|
|
67
|
+
|
|
68
|
+
# 2. Send the symptoms
|
|
69
|
+
genie send 'Trace: genie work dispatches engineers but they start idle at the prompt. No task received. genie status shows in_progress but nothing happens. Check dispatch.ts workDispatchCommand and protocol-router.ts sendMessage.' --to tracer
|
|
70
|
+
|
|
71
|
+
# 3. Wait for findings
|
|
72
|
+
sleep 60 && genie read tracer
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The tracer investigates and reports back:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Root cause: workDispatchCommand (dispatch.ts:532) spawns without initialPrompt
|
|
79
|
+
Evidence: protocolRouter.sendMessage fails silently under concurrent dispatch — 4/6 engineers got no message
|
|
80
|
+
Causal chain: missing initialPrompt → agent starts with empty prompt → no task → idle forever
|
|
81
|
+
Recommended correction: add initialPrompt to handleWorkerSpawn in dispatch.ts
|
|
82
|
+
Affected scope: brainstormCommand, wishCommand, workDispatchCommand, reviewCommand
|
|
83
|
+
Confidence: high
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The orchestrator then hands the report to `/fix`.
|
|
87
|
+
|
|
60
88
|
## Rules
|
|
61
89
|
- Never fix during trace — investigation only, always separate from correction.
|
|
62
90
|
- Always reproduce before theorizing — if the failure can't be reproduced, the report must say so.
|
package/skills/work/SKILL.md
CHANGED
|
@@ -102,6 +102,38 @@ When PG tasks exist for the wish, use `genie task` commands to track execution:
|
|
|
102
102
|
|
|
103
103
|
**Graceful degradation:** If no PG task exists for the wish (e.g., PG unavailable or wish was created before v4), skip all `genie task` commands and fall back to current behavior. Task integration is optional — the core flow must never break due to missing tasks.
|
|
104
104
|
|
|
105
|
+
## Example: Full Dispatch Cycle
|
|
106
|
+
|
|
107
|
+
Wish `fix-dispatch-initial-prompt` has 1 execution group. The orchestrator runs `/work`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# 1. Dispatch wave (spawns engineers automatically)
|
|
111
|
+
genie work fix-dispatch-initial-prompt
|
|
112
|
+
# Output: 🚀 Dispatching Wave 1 — 1 group(s)
|
|
113
|
+
# ✅ Group "1" set to in_progress
|
|
114
|
+
# 🔧 Dispatching work to engineer for "fix-dispatch-initial-prompt#1"
|
|
115
|
+
|
|
116
|
+
# 2. Monitor (ALWAYS sleep 60 between checks)
|
|
117
|
+
sleep 60 && genie status fix-dispatch-initial-prompt
|
|
118
|
+
# Output: Group 1: 🔄 in_progress
|
|
119
|
+
|
|
120
|
+
# 3. Check again
|
|
121
|
+
sleep 60 && genie status fix-dispatch-initial-prompt
|
|
122
|
+
# Output: Group 1: ✅ done — Progress: 1/1 done
|
|
123
|
+
|
|
124
|
+
# 4. All groups done → local review
|
|
125
|
+
genie spawn reviewer
|
|
126
|
+
genie send 'Review wish fix-dispatch-initial-prompt. Run bun test and check all 5 call sites.' --to reviewer
|
|
127
|
+
# Reviewer returns: SHIP
|
|
128
|
+
|
|
129
|
+
# 5. Create PR
|
|
130
|
+
git add -A && git commit -m "fix: pass initialPrompt to handleWorkerSpawn in all dispatch commands"
|
|
131
|
+
git push origin HEAD
|
|
132
|
+
gh pr create --base dev --title "fix: pass initialPrompt to dispatch" --body "Fixes #745. Wish: fix-dispatch-initial-prompt"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For multi-wave wishes, call `genie work <slug>` again after each wave completes — it dispatches the next wave automatically.
|
|
136
|
+
|
|
105
137
|
## Rules
|
|
106
138
|
- Never execute directly — always dispatch subagents.
|
|
107
139
|
- Never expand scope during execution.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for genie update dual-install detection (#750)
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun test src/genie-commands/__tests__/update.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { detectGlobalInstalls } from '../update.js';
|
|
9
|
+
|
|
10
|
+
// We can't easily mock runCommandSilent inside the module, so we test
|
|
11
|
+
// detectGlobalInstalls by actually running the detection commands.
|
|
12
|
+
// These tests verify the function returns the correct shape and doesn't throw.
|
|
13
|
+
|
|
14
|
+
describe('detectGlobalInstalls', () => {
|
|
15
|
+
test('returns a Set of npm | bun entries', async () => {
|
|
16
|
+
const result = await detectGlobalInstalls();
|
|
17
|
+
expect(result).toBeInstanceOf(Set);
|
|
18
|
+
// Every entry must be either 'npm' or 'bun'
|
|
19
|
+
for (const method of result) {
|
|
20
|
+
expect(['npm', 'bun']).toContain(method);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('detects install methods without throwing', async () => {
|
|
25
|
+
// In CI, genie may not be globally installed — just verify it doesn't throw
|
|
26
|
+
// and returns a valid (possibly empty) Set
|
|
27
|
+
const result = await detectGlobalInstalls();
|
|
28
|
+
expect(result.size).toBeGreaterThanOrEqual(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('updateCommand dual-install logic', () => {
|
|
33
|
+
test('secondary method is the opposite of primary', () => {
|
|
34
|
+
// Unit test for the selection logic extracted from updateCommand
|
|
35
|
+
const getSecondary = (primary: 'npm' | 'bun') => (primary === 'bun' ? 'npm' : 'bun');
|
|
36
|
+
expect(getSecondary('bun')).toBe('npm');
|
|
37
|
+
expect(getSecondary('npm')).toBe('bun');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('detectGlobalInstalls can return both npm and bun', async () => {
|
|
41
|
+
// This is an integration-style test. On CI both may not be installed,
|
|
42
|
+
// so we just verify the function handles both detection paths without error.
|
|
43
|
+
const result = await detectGlobalInstalls();
|
|
44
|
+
// Should not contain anything other than npm/bun
|
|
45
|
+
for (const method of result) {
|
|
46
|
+
expect(method === 'npm' || method === 'bun').toBe(true);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -153,7 +153,7 @@ async function detectInstallationType(): Promise<InstallationType> {
|
|
|
153
153
|
return hasBun ? 'bun' : 'npm';
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
async function updateViaBun(channel: string): Promise<
|
|
156
|
+
async function updateViaBun(channel: string): Promise<boolean> {
|
|
157
157
|
// Delete global lockfile — it pins old versions even with --force --no-cache
|
|
158
158
|
try {
|
|
159
159
|
require('node:fs').unlinkSync(join(homedir(), '.bun', 'install', 'global', 'bun.lock'));
|
|
@@ -165,21 +165,42 @@ async function updateViaBun(channel: string): Promise<void> {
|
|
|
165
165
|
const result = await runCommand('bun', ['add', '-g', '--force', '--no-cache', `@automagik/genie@${channel}`]);
|
|
166
166
|
if (!result.success) {
|
|
167
167
|
error('Failed to update via bun');
|
|
168
|
-
|
|
168
|
+
return false;
|
|
169
169
|
}
|
|
170
170
|
console.log();
|
|
171
|
-
success(`Genie CLI updated (${channel})!`);
|
|
171
|
+
success(`Genie CLI updated via bun (${channel})!`);
|
|
172
|
+
return true;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
async function updateViaNpm(channel: string): Promise<
|
|
175
|
+
async function updateViaNpm(channel: string): Promise<boolean> {
|
|
175
176
|
log(`Updating via npm (channel: ${channel})...`);
|
|
176
177
|
const result = await runCommand('npm', ['install', '-g', `@automagik/genie@${channel}`]);
|
|
177
178
|
if (!result.success) {
|
|
178
179
|
error('Failed to update via npm');
|
|
179
|
-
|
|
180
|
+
return false;
|
|
180
181
|
}
|
|
181
182
|
console.log();
|
|
182
|
-
success(`Genie CLI updated (${channel})!`);
|
|
183
|
+
success(`Genie CLI updated via npm (${channel})!`);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Detect which package-manager global installs exist (npm, bun, or both). */
|
|
188
|
+
export async function detectGlobalInstalls(): Promise<Set<'npm' | 'bun'>> {
|
|
189
|
+
const found = new Set<'npm' | 'bun'>();
|
|
190
|
+
|
|
191
|
+
const [npmResult, bunResult] = await Promise.all([
|
|
192
|
+
runCommandSilent('npm', ['list', '-g', '@automagik/genie']),
|
|
193
|
+
runCommandSilent('bun', ['pm', 'ls', '-g']),
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
if (npmResult.success && !npmResult.output.includes('(empty)')) {
|
|
197
|
+
found.add('npm');
|
|
198
|
+
}
|
|
199
|
+
if (bunResult.success && bunResult.output.includes('@automagik/genie')) {
|
|
200
|
+
found.add('bun');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return found;
|
|
183
204
|
}
|
|
184
205
|
|
|
185
206
|
async function updateSource(): Promise<void> {
|
|
@@ -538,17 +559,31 @@ export async function updateCommand(options: { next?: boolean; stable?: boolean
|
|
|
538
559
|
process.exit(1);
|
|
539
560
|
}
|
|
540
561
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
break;
|
|
545
|
-
case 'bun':
|
|
546
|
-
await updateViaBun(channel);
|
|
547
|
-
await syncPlugin(installType);
|
|
548
|
-
break;
|
|
549
|
-
case 'npm':
|
|
550
|
-
await updateViaNpm(channel);
|
|
551
|
-
await syncPlugin(installType);
|
|
552
|
-
break;
|
|
562
|
+
if (installType === 'source') {
|
|
563
|
+
await updateSource();
|
|
564
|
+
return;
|
|
553
565
|
}
|
|
566
|
+
|
|
567
|
+
// Detect all global installs (npm + bun) to update both when they coexist
|
|
568
|
+
const globalInstalls = await detectGlobalInstalls();
|
|
569
|
+
|
|
570
|
+
// Primary update — exit on failure
|
|
571
|
+
const primaryMethod = installType as 'npm' | 'bun';
|
|
572
|
+
const primaryOk = primaryMethod === 'bun' ? await updateViaBun(channel) : await updateViaNpm(channel);
|
|
573
|
+
if (!primaryOk) {
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Secondary update — warn on failure, don't block
|
|
578
|
+
const secondaryMethod = primaryMethod === 'bun' ? 'npm' : 'bun';
|
|
579
|
+
if (globalInstalls.has(secondaryMethod)) {
|
|
580
|
+
console.log();
|
|
581
|
+
log(`Also updating ${secondaryMethod}-global install...`);
|
|
582
|
+
const secondaryOk = secondaryMethod === 'bun' ? await updateViaBun(channel) : await updateViaNpm(channel);
|
|
583
|
+
if (!secondaryOk) {
|
|
584
|
+
error(`Secondary update via ${secondaryMethod} failed (non-blocking)`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
await syncPlugin(installType);
|
|
554
589
|
}
|
|
@@ -559,6 +559,27 @@ describe('parseWishGroups()', () => {
|
|
|
559
559
|
expect(groups[1].dependsOn).toEqual(['1']);
|
|
560
560
|
expect(groups[2].dependsOn).toEqual(['1', '2']);
|
|
561
561
|
});
|
|
562
|
+
|
|
563
|
+
it('should handle parenthetical descriptions containing commas (#752)', () => {
|
|
564
|
+
const content =
|
|
565
|
+
'### Group 1: Components\n**depends-on:** none\n\n### Group 2: Integration\n**depends-on:** Group 1 (GlassCard, StatusDot, ProgressBar)';
|
|
566
|
+
const groups = parseWishGroups(content);
|
|
567
|
+
expect(groups[0].dependsOn).toEqual([]);
|
|
568
|
+
expect(groups[1].dependsOn).toEqual(['1']);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should handle multiple groups with parenthetical descriptions containing commas (#752)', () => {
|
|
572
|
+
const content =
|
|
573
|
+
'### Group 1: A\n**depends-on:** none\n\n### Group 2: B\n**depends-on:** none\n\n### Group 3: C\n**depends-on:** Group 1, Group 2 (after review)';
|
|
574
|
+
const groups = parseWishGroups(content);
|
|
575
|
+
expect(groups[2].dependsOn).toEqual(['1', '2']);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should parse depends-on: none as empty array', () => {
|
|
579
|
+
const content = '### Group 1: Solo\n**depends-on:** none';
|
|
580
|
+
const groups = parseWishGroups(content);
|
|
581
|
+
expect(groups[0].dependsOn).toEqual([]);
|
|
582
|
+
});
|
|
562
583
|
});
|
|
563
584
|
|
|
564
585
|
// ============================================================================
|
|
@@ -165,13 +165,12 @@ export function parseWishGroups(content: string): GroupDefinition[] {
|
|
|
165
165
|
const depsStr = depsMatch[1].trim();
|
|
166
166
|
const depsNormalized = depsStr.replace(/\s*\([^)]*\)/g, '').trim();
|
|
167
167
|
if (depsNormalized.toLowerCase() !== 'none') {
|
|
168
|
-
dependsOn =
|
|
168
|
+
dependsOn = depsNormalized
|
|
169
169
|
.split(',')
|
|
170
170
|
.map((d) =>
|
|
171
171
|
d
|
|
172
172
|
.trim()
|
|
173
173
|
.replace(/^group\s*/i, '')
|
|
174
|
-
.replace(/\s*\(.*\)\s*$/, '')
|
|
175
174
|
.trim(),
|
|
176
175
|
)
|
|
177
176
|
.filter(Boolean);
|
|
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
|
|
|
7
7
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import * as wishState from '../lib/wish-state.js';
|
|
10
|
-
import { detectWaveCompletion, ensureWorkPushed, parseRef } from './state.js';
|
|
10
|
+
import { detectWaveCompletion, ensureWorkPushed, parseRef, resolveWishPath } from './state.js';
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
// Sample WISH.md with Execution Strategy for wave detection tests
|
|
@@ -273,3 +273,73 @@ describe('ensureWorkPushed()', () => {
|
|
|
273
273
|
await ensureWorkPushed('test-slug', '3');
|
|
274
274
|
});
|
|
275
275
|
});
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// resolveWishPath — repo root fallback via git-common-dir
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
const SIMPLE_WISH = '# Wish: Resolve Test\n\n## Summary\nTest.\n';
|
|
282
|
+
|
|
283
|
+
describe('resolveWishPath()', () => {
|
|
284
|
+
let mainRepo: string;
|
|
285
|
+
let worktreeDir: string;
|
|
286
|
+
|
|
287
|
+
beforeEach(async () => {
|
|
288
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
289
|
+
mainRepo = join('/tmp', `wish-resolve-main-${id}`);
|
|
290
|
+
worktreeDir = join('/tmp', `wish-resolve-wt-${id}`);
|
|
291
|
+
await mkdir(mainRepo, { recursive: true });
|
|
292
|
+
|
|
293
|
+
// Init git repo with a branch for worktree
|
|
294
|
+
execSync('git init', { cwd: mainRepo, encoding: 'utf-8' });
|
|
295
|
+
execSync('git config user.email "test@test.com"', { cwd: mainRepo, encoding: 'utf-8' });
|
|
296
|
+
execSync('git config user.name "Test"', { cwd: mainRepo, encoding: 'utf-8' });
|
|
297
|
+
await writeFile(join(mainRepo, 'README.md'), '# Test');
|
|
298
|
+
execSync('git add -A && git commit -m "init"', { cwd: mainRepo, encoding: 'utf-8' });
|
|
299
|
+
|
|
300
|
+
// Create wish in main repo
|
|
301
|
+
const wishDir = join(mainRepo, '.genie', 'wishes', 'test-slug');
|
|
302
|
+
await mkdir(wishDir, { recursive: true });
|
|
303
|
+
await writeFile(join(wishDir, 'WISH.md'), SIMPLE_WISH);
|
|
304
|
+
|
|
305
|
+
// Create a worktree (wish is NOT in worktree since .genie/ is gitignored)
|
|
306
|
+
execSync(`git worktree add ${worktreeDir} -b test-wt-branch`, { cwd: mainRepo, encoding: 'utf-8' });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
afterEach(async () => {
|
|
310
|
+
try {
|
|
311
|
+
execSync(`git -C ${mainRepo} worktree remove ${worktreeDir} --force`, { encoding: 'utf-8' });
|
|
312
|
+
} catch {
|
|
313
|
+
// Ignore
|
|
314
|
+
}
|
|
315
|
+
await rm(mainRepo, { recursive: true, force: true });
|
|
316
|
+
await rm(worktreeDir, { recursive: true, force: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should find wish directly in cwd', () => {
|
|
320
|
+
const result = resolveWishPath('test-slug', mainRepo);
|
|
321
|
+
expect(result).toBe(join(mainRepo, '.genie', 'wishes', 'test-slug', 'WISH.md'));
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should find wish via repo root when not in cwd (worktree fallback)', () => {
|
|
325
|
+
// Worktree doesn't have .genie/wishes/ — should fall back to main repo
|
|
326
|
+
const result = resolveWishPath('test-slug', worktreeDir);
|
|
327
|
+
expect(result).toBe(join(mainRepo, '.genie', 'wishes', 'test-slug', 'WISH.md'));
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should return null when wish not found anywhere', () => {
|
|
331
|
+
const result = resolveWishPath('nonexistent', worktreeDir);
|
|
332
|
+
expect(result).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should return null for non-git directory', async () => {
|
|
336
|
+
const tmpDir = join('/tmp', `wish-resolve-nogit-${Date.now()}`);
|
|
337
|
+
await mkdir(tmpDir, { recursive: true });
|
|
338
|
+
try {
|
|
339
|
+
const result = resolveWishPath('test-slug', tmpDir);
|
|
340
|
+
expect(result).toBeNull();
|
|
341
|
+
} finally {
|
|
342
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { execSync } from 'node:child_process';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { readFile } from 'node:fs/promises';
|
|
14
|
-
import { join } from 'node:path';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
15
|
import type { Command } from 'commander';
|
|
16
16
|
import * as wishState from '../lib/wish-state.js';
|
|
17
17
|
import { parseExecutionStrategy, parseWishGroups } from './dispatch.js';
|
|
@@ -20,6 +20,34 @@ import { parseExecutionStrategy, parseWishGroups } from './dispatch.js';
|
|
|
20
20
|
// Helpers
|
|
21
21
|
// ============================================================================
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the WISH.md path for a slug.
|
|
25
|
+
* Search order: base/.genie/wishes/ → repoRoot/.genie/wishes/ (via git-common-dir)
|
|
26
|
+
*/
|
|
27
|
+
export function resolveWishPath(slug: string, cwd?: string): string | null {
|
|
28
|
+
const base = cwd ?? process.cwd();
|
|
29
|
+
const cwdPath = join(base, '.genie', 'wishes', slug, 'WISH.md');
|
|
30
|
+
if (existsSync(cwdPath)) return cwdPath;
|
|
31
|
+
|
|
32
|
+
// Fallback: check repo root via git-common-dir
|
|
33
|
+
try {
|
|
34
|
+
const commonDir = execSync('git rev-parse --path-format=absolute --git-common-dir', {
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
cwd: base,
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
}).trim();
|
|
39
|
+
const repoRoot = dirname(commonDir);
|
|
40
|
+
if (repoRoot !== base) {
|
|
41
|
+
const repoPath = join(repoRoot, '.genie', 'wishes', slug, 'WISH.md');
|
|
42
|
+
if (existsSync(repoPath)) return repoPath;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Not in a git repo — no fallback available
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
/**
|
|
24
52
|
* Parse a `slug#group` reference.
|
|
25
53
|
* Examples: "auth-bug#2" → { slug: "auth-bug", group: "2" }
|
|
@@ -77,9 +105,8 @@ export async function detectWaveCompletion(
|
|
|
77
105
|
groupName: string,
|
|
78
106
|
cwd?: string,
|
|
79
107
|
): Promise<{ waveName: string; waveGroups: string[] } | null> {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (!existsSync(wishPath)) return null;
|
|
108
|
+
const wishPath = resolveWishPath(slug, cwd);
|
|
109
|
+
if (!wishPath) return null;
|
|
83
110
|
|
|
84
111
|
const content = await readFile(wishPath, 'utf-8');
|
|
85
112
|
const waves = parseExecutionStrategy(content);
|
|
@@ -238,10 +265,9 @@ export async function statusCommand(slug: string): Promise<void> {
|
|
|
238
265
|
let state = await wishState.getState(slug);
|
|
239
266
|
if (!state) {
|
|
240
267
|
// Auto-initialize state from WISH.md instead of failing
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
console.error(`❌ No state found for wish "${slug}" and no WISH.md at ${wishPath}`);
|
|
268
|
+
const wishPath = resolveWishPath(slug);
|
|
269
|
+
if (!wishPath) {
|
|
270
|
+
console.error(`❌ No state found for wish "${slug}" and no WISH.md found in cwd or repo root`);
|
|
245
271
|
console.error(` Create it first: genie wish <agent> ${slug}`);
|
|
246
272
|
process.exit(1);
|
|
247
273
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
7
8
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
8
9
|
import { join } from 'node:path';
|
|
9
10
|
import { $ } from 'bun';
|
|
@@ -170,4 +171,48 @@ describe('genie team CLI', () => {
|
|
|
170
171
|
const { exitCode } = await genie('team', 'ensure', 'test');
|
|
171
172
|
expect(exitCode).not.toBe(0);
|
|
172
173
|
});
|
|
174
|
+
|
|
175
|
+
test('team create --wish auto-copies wish from cwd to repo', async () => {
|
|
176
|
+
// Create a wish in a separate cwd directory (not the repo)
|
|
177
|
+
const cwdDir = join(TEST_DIR, 'wish-cwd');
|
|
178
|
+
const wishSlug = 'test-autocopy';
|
|
179
|
+
const wishDir = join(cwdDir, '.genie', 'wishes', wishSlug);
|
|
180
|
+
await mkdir(wishDir, { recursive: true });
|
|
181
|
+
await writeFile(join(wishDir, 'WISH.md'), '# Test wish for auto-copy\n\n## Summary\nTest.\n');
|
|
182
|
+
|
|
183
|
+
// Run team create from the cwd directory — wish is NOT in the repo yet
|
|
184
|
+
try {
|
|
185
|
+
await $`bun ${GENIE_BIN} team create feat/autocopy-test --repo ${TEST_REPO} --branch dev --wish ${wishSlug}`
|
|
186
|
+
.quiet()
|
|
187
|
+
.cwd(cwdDir)
|
|
188
|
+
.env({ ...process.env, GENIE_HOME: TEST_GENIE_HOME });
|
|
189
|
+
} catch {
|
|
190
|
+
// Spawn may fail (no tmux) but auto-copy should have happened before spawn
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Verify wish was copied to repo
|
|
194
|
+
const repoWishPath = join(TEST_REPO, '.genie', 'wishes', wishSlug, 'WISH.md');
|
|
195
|
+
expect(existsSync(repoWishPath)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('team create --wish uses existing wish in repo without copying', async () => {
|
|
199
|
+
// Create a wish directly in the repo
|
|
200
|
+
const wishSlug = 'test-inrepo';
|
|
201
|
+
const wishDir = join(TEST_REPO, '.genie', 'wishes', wishSlug);
|
|
202
|
+
await mkdir(wishDir, { recursive: true });
|
|
203
|
+
await writeFile(join(wishDir, 'WISH.md'), '# Test wish already in repo\n\n## Summary\nTest.\n');
|
|
204
|
+
|
|
205
|
+
// Run team create — wish is already in repo, no copy needed
|
|
206
|
+
try {
|
|
207
|
+
await $`bun ${GENIE_BIN} team create feat/inrepo-test --repo ${TEST_REPO} --branch dev --wish ${wishSlug}`
|
|
208
|
+
.quiet()
|
|
209
|
+
.cwd(TEST_REPO)
|
|
210
|
+
.env({ ...process.env, GENIE_HOME: TEST_GENIE_HOME });
|
|
211
|
+
} catch {
|
|
212
|
+
// Spawn may fail but wish validation should pass
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Wish should still be there
|
|
216
|
+
expect(existsSync(join(wishDir, 'WISH.md'))).toBe(true);
|
|
217
|
+
});
|
|
173
218
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
|
-
import { copyFile, mkdir } from 'node:fs/promises';
|
|
13
|
+
import { copyFile, cp, mkdir } from 'node:fs/promises';
|
|
14
14
|
import { join, resolve } from 'node:path';
|
|
15
15
|
import type { Command } from 'commander';
|
|
16
16
|
import type { TeamConfig } from '../lib/team-manager.js';
|
|
@@ -176,13 +176,23 @@ async function handleTeamCreate(
|
|
|
176
176
|
name: string,
|
|
177
177
|
options: { repo: string; branch: string; wish?: string; session?: string },
|
|
178
178
|
): Promise<void> {
|
|
179
|
-
// Validate wish exists before creating team
|
|
179
|
+
// Validate wish exists before creating team — auto-copy from cwd if needed
|
|
180
180
|
if (options.wish) {
|
|
181
181
|
const resolvedRepo = resolve(options.repo);
|
|
182
182
|
const wishPath = join(resolvedRepo, '.genie', 'wishes', options.wish, 'WISH.md');
|
|
183
183
|
if (!existsSync(wishPath)) {
|
|
184
|
-
|
|
185
|
-
process.
|
|
184
|
+
// Auto-copy: search cwd for the wish
|
|
185
|
+
const cwdWishDir = join(process.cwd(), '.genie', 'wishes', options.wish);
|
|
186
|
+
const cwdWishPath = join(cwdWishDir, 'WISH.md');
|
|
187
|
+
if (existsSync(cwdWishPath)) {
|
|
188
|
+
const destDir = join(resolvedRepo, '.genie', 'wishes', options.wish);
|
|
189
|
+
await mkdir(destDir, { recursive: true });
|
|
190
|
+
await cp(cwdWishDir, destDir, { recursive: true });
|
|
191
|
+
console.log(`Wish: copied ${options.wish}/WISH.md to repo`);
|
|
192
|
+
} else {
|
|
193
|
+
console.error(`Error: Wish not found at ${wishPath}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
186
196
|
}
|
|
187
197
|
}
|
|
188
198
|
|