diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 72358620c..0a0821605 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "genie",
- "version": "3.260302.2",
+ "version": "3.260309.1",
"source": "./plugins/genie",
"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."
}
diff --git a/TEAM_LEAD_PROMPT.md b/TEAM_LEAD_PROMPT.md
index 9552aeb1a..40fe097c1 100644
--- a/TEAM_LEAD_PROMPT.md
+++ b/TEAM_LEAD_PROMPT.md
@@ -1,43 +1,43 @@
-# Genie CLI — MANDATORY Worker Orchestration
+# Genie CLI — MANDATORY Agent Orchestration
-You are a team-lead in a **genie-managed environment**. ALL worker spawning, messaging, and team management MUST go through the genie CLI via Bash.
+You are a team-lead in a **genie-managed environment**. ALL agent spawning, messaging, and team management MUST go through the genie CLI via Bash.
## CRITICAL: NEVER Use These Native Tools
-NEVER use the `Agent` tool to spawn agents or subagents. Use `genie worker spawn` instead.
-NEVER use `SendMessage` to communicate with workers. Use `genie msg send` instead.
+NEVER use the `Agent` tool to spawn agents or subagents. Use `genie agent spawn` instead.
+NEVER use `SendMessage` to communicate with agents. Use `genie send` instead.
NEVER use `TeamCreate` or `TeamDelete`. Use `genie team ensure` / `genie team delete` instead.
If you catch yourself about to use Agent, SendMessage, TeamCreate, or TeamDelete — STOP and use the genie CLI equivalent below.
-## Workers
+## Agents
```bash
-# Spawn a worker (ALWAYS use this instead of Agent tool)
-genie worker spawn --role # implementor, tests, review, fix, refactor
-genie worker spawn --role --skill # With specific skill
+# Spawn an agent (ALWAYS use this instead of Agent tool)
+genie agent spawn --role # implementor, tests, review, fix, refactor
+genie agent spawn --role --skill # With specific skill
# Monitor
-genie worker list # List all workers
-genie worker dashboard # Live dashboard
-genie worker history # Session history
-genie worker read --follow # Tail terminal output
+genie agent list # List all agents
+genie agent dashboard # Live dashboard
+genie agent history # Session history
+genie agent read --follow # Tail terminal output
# Control
-genie worker kill # Force kill
-genie worker suspend # Suspend (preserves session)
-genie worker exec "" # Run command in worker pane
-genie worker answer # Answer prompt (1-9 or text:...)
+genie agent kill # Force kill
+genie agent suspend # Suspend (preserves session)
+genie agent exec "" # Run command in agent pane
+genie agent answer # Answer prompt (1-9 or text:...)
```
## Messaging
```bash
-# Send message to a worker (ALWAYS use this instead of SendMessage)
-genie msg send "" --to # Send to specific worker
-genie msg inbox # View worker inbox
-genie msg inbox --unread # Unread only
+# Send message to an agent (ALWAYS use this instead of SendMessage)
+genie send "" --to # Send to specific agent
+genie inbox # View agent inbox
+genie inbox --unread # Unread only
```
## Teams
@@ -51,19 +51,19 @@ genie team delete # Delete team
## Typical Flow
```bash
-# 1. Spawn a worker
-genie worker spawn --role implementor
+# 1. Spawn an agent
+genie agent spawn --role implementor
# 2. Monitor
-genie worker list
+genie agent list
# 3. Send instructions
-genie msg send "Implement endpoint X" --to
+genie send "Implement endpoint X" --to
# 4. Check progress
-genie worker history
+genie agent history
# 5. Shut down
-genie worker kill
+genie agent kill
```
diff --git a/docs/CO-ORCHESTRATION-GUIDE.md b/docs/CO-ORCHESTRATION-GUIDE.md
index 0e8781ffb..b0f7d387b 100644
--- a/docs/CO-ORCHESTRATION-GUIDE.md
+++ b/docs/CO-ORCHESTRATION-GUIDE.md
@@ -15,7 +15,7 @@ Human (Orchestrator)
├── genie work bd-2 ──▶ Worker 2 (Claude in pane %2)
│ └── worktree: .worktrees/bd-2/
│
- └── genie worker list ──▶ Status dashboard
+ └── genie agent list ──▶ Status dashboard
```
## Prerequisites
@@ -39,13 +39,13 @@ bd create "Update API documentation"
genie work bd-1
# 4. Check worker status
-genie worker list
+genie agent list
# 5. When worker needs approval
-genie worker approve
+genie agent approve
# 6. When done, close the issue
-genie worker close bd-1
+genie agent close bd-1
```
## Detailed Workflow
@@ -99,10 +99,10 @@ genie work next
```bash
# Check all workers
-genie worker list
+genie agent list
# JSON output for scripting
-genie worker list --json
+genie agent list --json
```
**Worker States:**
@@ -119,14 +119,14 @@ genie worker list --json
**Approve permissions:**
```bash
-genie worker approve # Approve pending permission
-genie worker approve --start # Start auto-approve engine
+genie agent approve # Approve pending permission
+genie agent approve --start # Start auto-approve engine
```
**Answer questions:**
```bash
-genie worker answer bd-1 1 # Select option 1
-genie worker answer bd-1 "text:custom" # Provide custom text answer
+genie agent answer bd-1 1 # Select option 1
+genie agent answer bd-1 "text:custom" # Provide custom text answer
```
### Phase 5: Closing Issues
@@ -135,7 +135,7 @@ When a worker completes its task:
```bash
# Close issue and cleanup worker
-genie worker close bd-1
+genie agent close bd-1
# Options:
# --merge Merge worktree branch to main before cleanup
@@ -156,7 +156,7 @@ genie worker close bd-1
If a worker is stuck or needs to be terminated:
```bash
-genie worker kill bd-1
+genie agent kill bd-1
```
Note: This does NOT close the issue. The task remains `in_progress` in beads.
@@ -190,7 +190,7 @@ bd update bd-2 --blocked-by bd-1
genie work bd-1
# When bd-1 completes, bd-2 becomes ready
-genie worker close bd-1
+genie agent close bd-1
genie work next # Picks bd-2
```
@@ -208,7 +208,7 @@ genie work bd-2
genie work bd-3
# Monitor all
-genie worker list
+genie agent list
```
### Pattern 3: Review and Iterate
@@ -231,7 +231,7 @@ genie work bd-1
### Worker shows as dead but pane exists
```bash
# The registry may be out of sync
-genie worker kill # Clean up registry entry
+genie agent kill # Clean up registry entry
genie work # Start fresh
```
@@ -256,7 +256,7 @@ bd daemon start --auto-commit
1. **Clear issue titles**: Workers use titles as context
2. **One task per worker**: Keep issues focused
3. **Use dependencies**: `--blocked-by` prevents premature work
-4. **Review before closing**: Check worker output before `genie worker close`
+4. **Review before closing**: Check worker output before `genie agent close`
5. **Use worktrees**: They provide isolation and can be reviewed independently
6. **Keep daemon running**: Ensures beads state is synced to git
@@ -266,16 +266,16 @@ bd daemon start --auto-commit
|---------|-------------|
| `genie work ` | Spawn worker for issue |
| `genie work next` | Work on next ready issue |
-| `genie worker list` | List all workers |
-| `genie worker approve` | Approve permission / manage auto-approve |
-| `genie worker answer ` | Answer question |
-| `genie worker history ` | Compressed session catch-up |
-| `genie worker events [pane-id]` | Stream Claude Code events |
-| `genie worker close ` | Close issue and cleanup |
-| `genie worker ship ` | Mark done, merge, cleanup |
-| `genie worker kill ` | Force kill worker |
-| `genie worker read ` | Read worker pane output |
-| `genie worker exec ` | Execute command in worker pane |
+| `genie agent list` | List all workers |
+| `genie agent approve` | Approve permission / manage auto-approve |
+| `genie agent answer ` | Answer question |
+| `genie agent history ` | Compressed session catch-up |
+| `genie agent events [pane-id]` | Stream Claude Code events |
+| `genie agent close ` | Close issue and cleanup |
+| `genie agent ship ` | Mark done, merge, cleanup |
+| `genie agent kill ` | Force kill worker |
+| `genie agent read ` | Read worker pane output |
+| `genie agent exec ` | Execute command in worker pane |
| `genie daemon start` | Start beads daemon |
| `genie daemon stop` | Stop beads daemon |
| `genie daemon status` | Show daemon status |
diff --git a/docs/agent-profiles.md b/docs/agent-profiles.md
index af25e4726..4165d33a7 100644
--- a/docs/agent-profiles.md
+++ b/docs/agent-profiles.md
@@ -1,6 +1,6 @@
-# Worker Profiles
+# Agent Profiles
-Worker profiles configure how genie-cli spawns Claude Code workers. Each profile bundles:
+Agent profiles configure how genie-cli spawns Claude Code agents. Each profile bundles:
- **Launcher**: `claude` (direct Claude Code invocation)
- **Claude Args**: CLI arguments passed to Claude Code
@@ -47,10 +47,10 @@ genie profiles default coding-fast
## Using Profiles
-### With genie worker spawn
+### With genie agent spawn
```bash
-genie worker spawn --role implementor --profile coding-fast
-genie worker spawn --role implementor --profile safe # For interactive work
+genie agent spawn --role implementor --profile coding-fast
+genie agent spawn --role implementor --profile safe # For interactive work
```
### With genie work
diff --git a/openclaw.plugin.json b/openclaw.plugin.json
index 174f96810..51d7e3db4 100644
--- a/openclaw.plugin.json
+++ b/openclaw.plugin.json
@@ -2,7 +2,7 @@
"id": "genie",
"name": "Genie",
"description": "Skills, agents, and hooks for the Genie CLI terminal orchestration toolkit",
- "version": "3.260302.2",
+ "version": "3.260309.1",
"configSchema": {
"type": "object",
"additionalProperties": false,
diff --git a/package.json b/package.json
index 50dfd3670..57354a3da 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@automagik/genie",
- "version": "3.260302.2",
+ "version": "3.260309.1",
"description": "Collaborative terminal toolkit for human + AI workflows",
"type": "module",
"bin": {
diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json
index 0f09b2a22..bbe7edb39 100644
--- a/plugins/genie/.claude-plugin/plugin.json
+++ b/plugins/genie/.claude-plugin/plugin.json
@@ -1,11 +1,15 @@
{
- "name": "genie",
- "version": "3.260302.2",
+ "name": "automagik-genie",
+ "version": "3.260309.1",
"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.",
"author": {
"name": "Namastex Labs"
},
"repository": "https://github.com/automagik-dev/genie",
"license": "MIT",
- "keywords": ["workflow", "orchestration", "collaboration", "claude-code", "tmux", "agents"]
+ "keywords": ["workflow", "orchestration", "collaboration", "claude-code", "tmux", "agents"],
+ "skills": "./skills",
+ "hooks": "./hooks/hooks.json",
+ "agents": "./agents",
+ "settings": "./settings.json"
}
diff --git a/plugins/genie/hooks/hooks.json b/plugins/genie/hooks/hooks.json
index 166e8801c..c84162e4b 100644
--- a/plugins/genie/hooks/hooks.json
+++ b/plugins/genie/hooks/hooks.json
@@ -1,5 +1,5 @@
{
- "description": "Genie CLI hooks - setup, wish validation, session context, and completion awareness",
+ "description": "Genie plugin hooks — setup, onboarding, dispatch, wish validation, and completion awareness",
"hooks": {
"SessionStart": [
{
@@ -10,6 +10,11 @@
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js",
"timeout": 60
},
+ {
+ "type": "command",
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/first-run-check.cjs",
+ "timeout": 5
+ },
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/session-context.cjs",
@@ -19,6 +24,16 @@
}
],
"PreToolUse": [
+ {
+ "matcher": "SendMessage",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "genie hook dispatch",
+ "timeout": 15
+ }
+ ]
+ },
{
"matcher": "Write",
"hooks": [
diff --git a/plugins/genie/package.json b/plugins/genie/package.json
index a6a78f19a..787446b54 100644
--- a/plugins/genie/package.json
+++ b/plugins/genie/package.json
@@ -1,6 +1,6 @@
{
"name": "genie-plugin",
- "version": "3.260302.2",
+ "version": "3.260309.1",
"private": true,
"description": "Runtime dependencies for genie bundled CLIs",
"type": "module",
diff --git a/plugins/genie/references/dispatch-contract.md b/plugins/genie/references/dispatch-contract.md
index d69d9840c..9098aeecc 100644
--- a/plugins/genie/references/dispatch-contract.md
+++ b/plugins/genie/references/dispatch-contract.md
@@ -64,20 +64,20 @@ Documented for future validation. Three-layer chain: OpenClaw → genie → Clau
```bash
# Spawn a Claude Code worker via genie
-genie worker spawn --role implementor
+genie agent spawn --role implementor
# Or for heavy multi-file work with bead tracking
bd create "" --type task
genie work
# Monitor
-genie worker list
-genie worker read
+genie agent list
+genie agent read
```
- **Isolation:** Claude Code worktrees (same as CC path, initiated via genie)
- **Model:** sonnet (passed to CC via genie)
-- **Coordination:** genie worker status + bead updates
+- **Coordination:** genie agent status + bead updates
- **Risk:** 3 layers of indirection — use timeouts, fall back to sequential on failure
## Rules
diff --git a/plugins/genie/scripts/first-run-check.cjs b/plugins/genie/scripts/first-run-check.cjs
new file mode 100755
index 000000000..34954dbb2
--- /dev/null
+++ b/plugins/genie/scripts/first-run-check.cjs
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+"use strict";
+
+/**
+ * First-run detection for genie plugin.
+ *
+ * Runs on SessionStart. If no AGENTS.md exists in the working directory,
+ * outputs a message to stderr prompting /onboarding.
+ *
+ * This is non-blocking (always exits 0) — it only suggests, never forces.
+ */
+
+const fs = require("node:fs");
+const path = require("node:path");
+
+// Use CLAUDE_CWD if available (set by Claude Code), otherwise process.cwd()
+const cwd = process.env.CLAUDE_CWD || process.cwd();
+const agentsMd = path.join(cwd, "AGENTS.md");
+const claudeMd = path.join(cwd, "CLAUDE.md");
+
+// Only suggest onboarding if neither AGENTS.md nor CLAUDE.md exists.
+// Having either means the workspace is already configured.
+if (!fs.existsSync(agentsMd) && !fs.existsSync(claudeMd)) {
+ console.error("");
+ console.error("=".repeat(50));
+ console.error(" \u{1F9DE} Genie — First Run Detected");
+ console.error(" No AGENTS.md found in this workspace.");
+ console.error("");
+ console.error(" Run /onboarding to set up your environment.");
+ console.error("=".repeat(50));
+ console.error("");
+}
+
+process.exit(0);
diff --git a/plugins/genie/scripts/genie.cjs b/plugins/genie/scripts/genie.cjs
index d7daa1448..a2d8be827 100755
--- a/plugins/genie/scripts/genie.cjs
+++ b/plugins/genie/scripts/genie.cjs
@@ -155,7 +155,7 @@ Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.
`:"";return(0,pe.writeFileSync)(e,a),!0}async function Mp(){let e=(0,Zl.homedir)(),t="generated by genie-cli";console.log(`Uninstalling Warp-like shortcuts...
`);let n=(0,st.join)(e,".tmux.conf");tr(n,t)?await Gr(`Remove shortcuts from ${n}? [Y/n] `)!=="n"?Jl(n,t)&&console.log(`\u2705 Removed from ${n}`):console.log("\u23ED\uFE0F Skipped tmux.conf"):console.log("\u2713 tmux.conf has no genie shortcuts");let r=(0,st.join)(e,".zshrc"),s=(0,st.join)(e,".bashrc");for(let c of[r,s])(0,pe.existsSync)(c)&&(tr(c,t)?await Gr(`Remove shell functions from ${c}? [Y/n] `)!=="n"?Jl(c,t)&&console.log(`\u2705 Removed from ${c}`):console.log(`\u23ED\uFE0F Skipped ${c}`):console.log(`\u2713 ${c} has no genie shortcuts`));let i=(0,st.join)(e,".termux"),o=(0,st.join)(i,"termux.properties"),a=(0,pe.existsSync)(i)||process.env.TERMUX_VERSION;a&&(tr(o,t)?await Gr(`Remove extra keys from ${o}? [Y/n] `)!=="n"?Jl(o,t)&&(console.log(`\u2705 Removed from ${o}`),console.log(" Run: termux-reload-settings")):console.log("\u23ED\uFE0F Skipped termux.properties"):console.log("\u2713 termux.properties has no genie shortcuts")),console.log(`
\u2705 Uninstallation complete!`),console.log(`
-Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.log(" 2. Restart your shell or run: source ~/.bashrc"),a&&console.log(" 3. Reload Termux: termux-reload-settings")}function Hr(){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 on(e,t){console.log(),console.log(`\x1B[1m${e}\x1B[0m`),t&&console.log(`\x1B[2m${t}\x1B[0m`),console.log()}async function ew(){on("1. Prerequisites Check","Checking required tools...");let e=[{name:"tmux",required:!0},{name:"bun",required:!0},{name:"claude",required:!1,displayName:"Claude Code CLI"},{name:"codex",required:!1,displayName:"OpenAI Codex CLI"},{name:"jq",required:!1}];for(let t of e){let n=await Je(t.name),r=t.displayName||t.name;n.exists?console.log(` \x1B[32m\u2713\x1B[0m ${r} ${n.version?`(${n.version})`:""}`):t.required?console.log(` \x1B[31m\u2717\x1B[0m ${r} \x1B[2m(required)\x1B[0m`):console.log(` \x1B[33m!\x1B[0m ${r} \x1B[2m(optional)\x1B[0m`)}}async function Np(e,t){if(on("2. Session Configuration","Configure tmux session settings"),t)return console.log(` Using defaults: session="${e.session.name}", window="${e.session.defaultWindow}"`),e;let n=await vt({message:"Session name:",default:e.session.name}),r=await vt({message:"Default window name:",default:e.session.defaultWindow}),s=await le({message:"Auto-create session on connect?",default:e.session.autoCreate});return e.session={name:n,defaultWindow:r,autoCreate:s},e}async function Lp(e,t){if(on("3. Terminal Defaults","Configure default values for term commands"),t)return console.log(` Using defaults: timeout=${e.terminal.execTimeout}ms, lines=${e.terminal.readLines}`),e;let n=await vt({message:"Exec timeout (milliseconds):",default:String(e.terminal.execTimeout),validate:i=>{let o=Number.parseInt(i,10);return!Number.isNaN(o)&&o>0?!0:"Must be a positive number"}}),r=await vt({message:"Read lines (default for genie worker read):",default:String(e.terminal.readLines),validate:i=>{let o=Number.parseInt(i,10);return!Number.isNaN(o)&&o>0?!0:"Must be a positive number"}}),s=await vt({message:"Worktree base directory:",default:e.terminal.worktreeBase});return e.terminal={execTimeout:Number.parseInt(n,10),readLines:Number.parseInt(r,10),worktreeBase:s},e}async function jp(e,t){on("4. Keyboard Shortcuts","Warp-like tmux shortcuts for quick navigation");let n=(0,Up.homedir)(),r=(0,Vp.join)(n,".tmux.conf");return Vs(r)?(console.log(" \x1B[32m\u2713\x1B[0m Tmux shortcuts already installed"),e.shortcuts.tmuxInstalled=!0,e):(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(),t?(console.log(" Skipped in quick mode. Run \x1B[36mgenie setup --shortcuts\x1B[0m to install."),e):(await le({message:"Install tmux keyboard shortcuts?",default:!1})?(console.log(),await wo(),e.shortcuts.tmuxInstalled=!0,await xd({tmuxInstalled:!0})):console.log(" Skipped. Run \x1B[36mgenie shortcuts install\x1B[0m later."),e))}async function Wp(e,t){return on("5. Claudio Integration","LLM router profile management"),(await Je("claude")).exists?t?(console.log(" Skipped in quick mode. Run \x1B[36mclaudio setup\x1B[0m to configure."),e):(await le({message:"Configure Claudio API profiles?",default:!1})?(e.claudio={enabled:!0},console.log(),console.log(" Run \x1B[36mclaudio setup\x1B[0m to configure API profiles.")):console.log(" Skipped. Run \x1B[36mclaudio setup\x1B[0m later."),e):(console.log(" \x1B[33m!\x1B[0m Claude Code CLI not found. Skipping claudio integration."),e)}async function qp(e,t){on("6. Codex Integration","Configure OpenAI Codex for genie workers");let n=await Je("codex");if(!n.exists)return console.log(" \x1B[33m!\x1B[0m Codex CLI not found. Skipping codex integration."),e;if(console.log(` \x1B[32m\u2713\x1B[0m Codex CLI found (${n.version??"unknown version"})`),Fp())return console.log(" \x1B[32m\u2713\x1B[0m Codex config already configured"),e.codex={configured:!0},e;if(console.log(),console.log(" Genie needs to configure codex for worker 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${$r(Op())}\x1B[0m`),console.log(),t){let s=Us();return console.log(s==="changed"?" \x1B[32m\u2713\x1B[0m Codex config updated":s==="unchanged"?" \x1B[32m\u2713\x1B[0m Codex config already up to date":" \x1B[31m\u2717\x1B[0m Failed to update codex config"),e.codex={configured:s!=="error"},e}if(await le({message:"Configure Codex for genie worker integration?",default:!0})){let s=Us();console.log(s==="changed"?" \x1B[32m\u2713\x1B[0m Codex config updated":s==="unchanged"?" \x1B[32m\u2713\x1B[0m Codex config already up to date":" \x1B[31m\u2717\x1B[0m Failed to update codex config"),e.codex={configured:s!=="error"}}else console.log(" Skipped. Run \x1B[36mgenie setup --codex\x1B[0m later.");return e}async function tw(e,t){if(on("7. Debug Options","Logging and debugging settings"),t)return console.log(" Using defaults: tmuxDebug=false, verbose=false"),e;let n=await le({message:"Enable tmux debug logging?",default:e.logging.tmuxDebug}),r=await le({message:"Enable verbose mode?",default:e.logging.verbose});return e.logging={tmuxDebug:n,verbose:r},e}async function nw(e){on("Summary",`Configuration will be saved to ${$r(Es())}`),console.log(` Session: \x1B[36m${e.session.name}\x1B[0m (window: ${e.session.defaultWindow})`),console.log(` Terminal: timeout=${e.terminal.execTimeout}ms, lines=${e.terminal.readLines}`),console.log(` Shortcuts: ${e.shortcuts.tmuxInstalled?"\x1B[32minstalled\x1B[0m":"\x1B[2mnot installed\x1B[0m"}`),console.log(` Claudio: ${e.claudio?.enabled?"\x1B[32menabled\x1B[0m":"\x1B[2mnot configured\x1B[0m"}`),console.log(` Codex: ${e.codex?.configured?"\x1B[32mconfigured\x1B[0m":"\x1B[2mnot configured\x1B[0m"}`),console.log(` Debug: tmux=${e.logging.tmuxDebug}, verbose=${e.logging.verbose}`),console.log(),e.setupComplete=!0,e.lastSetupAt=new Date().toISOString(),await je(e),console.log("\x1B[32m\u2713 Configuration saved!\x1B[0m")}async function rw(){let e=await ce();console.log(),console.log("\x1B[1mCurrent Genie Configuration\x1B[0m"),console.log(`\x1B[2m${$r(Es())}\x1B[0m`),console.log(),console.log(JSON.stringify(e,null,2)),console.log()}function sw(){console.log(),console.log("\x1B[1mNext Steps:\x1B[0m"),console.log(),console.log(" Start a session: \x1B[36mclaudio launch\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 Do(e={}){if(e.show){await rw();return}if(e.reset){await md(),console.log("\x1B[32m\u2713 Configuration reset to defaults.\x1B[0m"),console.log();return}let t=await ce();if(e.shortcuts){Hr(),await jp(t,!1),await nc();return}if(e.terminal){Hr(),t=await Lp(t,!1),await je(t),console.log("\x1B[32m\u2713 Terminal configuration saved.\x1B[0m");return}if(e.session){Hr(),t=await Np(t,!1),await je(t),console.log("\x1B[32m\u2713 Session configuration saved.\x1B[0m");return}if(e.claudio){Hr(),await Wp(t,!1),await nc();return}if(e.codex){Hr(),t=await qp(t,!1),await je(t),t.codex?.configured&&console.log("\x1B[32m\u2713 Codex configuration saved.\x1B[0m");return}let n=e.quick??!1;Hr(),n&&console.log("\x1B[2mQuick mode: accepting all defaults\x1B[0m"),await ew(),t=await Np(t,n),t=await Lp(t,n),t=await jp(t,n),t=await Wp(t,n),t=await qp(t,n),t=await tw(t,n),await nw(t),sw()}var iw={tmux:{brew:"brew install tmux",apt:"sudo apt update && sudo apt install -y tmux",dnf:"sudo dnf install -y tmux",yum:"sudo yum install -y tmux",pacman:"sudo pacman -S --noconfirm tmux",none:null},bun:{all:"curl -fsSL https://bun.sh/install | bash"},claude:{all:"npm install -g @anthropic-ai/claude-code"}};function ow(e,t){let n=iw[e];return n?n.all?n.all:t!=="none"&&n[t]?n[t]:n.none!==void 0?n.none:null:null}function Gp(e){switch(e){case"tmux":return"Visit https://github.com/tmux/tmux/wiki/Installing for installation instructions";case"bun":return"Visit https://bun.sh for installation instructions";case"claude":return"Run: npm install -g @anthropic-ai/claude-code (requires Node.js/npm)";default:return`Search for "${e} install" for installation instructions`}}async function aw(e){return new Promise(t=>{console.log(`\x1B[2m$ ${e}\x1B[0m
+Next steps:`),console.log(" 1. Reload tmux: tmux source ~/.tmux.conf"),console.log(" 2. Restart your shell or run: source ~/.bashrc"),a&&console.log(" 3. Reload Termux: termux-reload-settings")}function Hr(){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 on(e,t){console.log(),console.log(`\x1B[1m${e}\x1B[0m`),t&&console.log(`\x1B[2m${t}\x1B[0m`),console.log()}async function ew(){on("1. Prerequisites Check","Checking required tools...");let e=[{name:"tmux",required:!0},{name:"bun",required:!0},{name:"claude",required:!1,displayName:"Claude Code CLI"},{name:"codex",required:!1,displayName:"OpenAI Codex CLI"},{name:"jq",required:!1}];for(let t of e){let n=await Je(t.name),r=t.displayName||t.name;n.exists?console.log(` \x1B[32m\u2713\x1B[0m ${r} ${n.version?`(${n.version})`:""}`):t.required?console.log(` \x1B[31m\u2717\x1B[0m ${r} \x1B[2m(required)\x1B[0m`):console.log(` \x1B[33m!\x1B[0m ${r} \x1B[2m(optional)\x1B[0m`)}}async function Np(e,t){if(on("2. Session Configuration","Configure tmux session settings"),t)return console.log(` Using defaults: session="${e.session.name}", window="${e.session.defaultWindow}"`),e;let n=await vt({message:"Session name:",default:e.session.name}),r=await vt({message:"Default window name:",default:e.session.defaultWindow}),s=await le({message:"Auto-create session on connect?",default:e.session.autoCreate});return e.session={name:n,defaultWindow:r,autoCreate:s},e}async function Lp(e,t){if(on("3. Terminal Defaults","Configure default values for term commands"),t)return console.log(` Using defaults: timeout=${e.terminal.execTimeout}ms, lines=${e.terminal.readLines}`),e;let n=await vt({message:"Exec timeout (milliseconds):",default:String(e.terminal.execTimeout),validate:i=>{let o=Number.parseInt(i,10);return!Number.isNaN(o)&&o>0?!0:"Must be a positive number"}}),r=await vt({message:"Read lines (default for genie agent read):",default:String(e.terminal.readLines),validate:i=>{let o=Number.parseInt(i,10);return!Number.isNaN(o)&&o>0?!0:"Must be a positive number"}}),s=await vt({message:"Worktree base directory:",default:e.terminal.worktreeBase});return e.terminal={execTimeout:Number.parseInt(n,10),readLines:Number.parseInt(r,10),worktreeBase:s},e}async function jp(e,t){on("4. Keyboard Shortcuts","Warp-like tmux shortcuts for quick navigation");let n=(0,Up.homedir)(),r=(0,Vp.join)(n,".tmux.conf");return Vs(r)?(console.log(" \x1B[32m\u2713\x1B[0m Tmux shortcuts already installed"),e.shortcuts.tmuxInstalled=!0,e):(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(),t?(console.log(" Skipped in quick mode. Run \x1B[36mgenie setup --shortcuts\x1B[0m to install."),e):(await le({message:"Install tmux keyboard shortcuts?",default:!1})?(console.log(),await wo(),e.shortcuts.tmuxInstalled=!0,await xd({tmuxInstalled:!0})):console.log(" Skipped. Run \x1B[36mgenie shortcuts install\x1B[0m later."),e))}async function Wp(e,t){return on("5. Claudio Integration","LLM router profile management"),(await Je("claude")).exists?t?(console.log(" Skipped in quick mode. Run \x1B[36mclaudio setup\x1B[0m to configure."),e):(await le({message:"Configure Claudio API profiles?",default:!1})?(e.claudio={enabled:!0},console.log(),console.log(" Run \x1B[36mclaudio setup\x1B[0m to configure API profiles.")):console.log(" Skipped. Run \x1B[36mclaudio setup\x1B[0m later."),e):(console.log(" \x1B[33m!\x1B[0m Claude Code CLI not found. Skipping claudio integration."),e)}async function qp(e,t){on("6. Codex Integration","Configure OpenAI Codex for genie agents");let n=await Je("codex");if(!n.exists)return console.log(" \x1B[33m!\x1B[0m Codex CLI not found. Skipping codex integration."),e;if(console.log(` \x1B[32m\u2713\x1B[0m Codex CLI found (${n.version??"unknown version"})`),Fp())return console.log(" \x1B[32m\u2713\x1B[0m Codex config already configured"),e.codex={configured:!0},e;if(console.log(),console.log(" Genie needs to configure codex for worker 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${$r(Op())}\x1B[0m`),console.log(),t){let s=Us();return console.log(s==="changed"?" \x1B[32m\u2713\x1B[0m Codex config updated":s==="unchanged"?" \x1B[32m\u2713\x1B[0m Codex config already up to date":" \x1B[31m\u2717\x1B[0m Failed to update codex config"),e.codex={configured:s!=="error"},e}if(await le({message:"Configure Codex for genie agent integration?",default:!0})){let s=Us();console.log(s==="changed"?" \x1B[32m\u2713\x1B[0m Codex config updated":s==="unchanged"?" \x1B[32m\u2713\x1B[0m Codex config already up to date":" \x1B[31m\u2717\x1B[0m Failed to update codex config"),e.codex={configured:s!=="error"}}else console.log(" Skipped. Run \x1B[36mgenie setup --codex\x1B[0m later.");return e}async function tw(e,t){if(on("7. Debug Options","Logging and debugging settings"),t)return console.log(" Using defaults: tmuxDebug=false, verbose=false"),e;let n=await le({message:"Enable tmux debug logging?",default:e.logging.tmuxDebug}),r=await le({message:"Enable verbose mode?",default:e.logging.verbose});return e.logging={tmuxDebug:n,verbose:r},e}async function nw(e){on("Summary",`Configuration will be saved to ${$r(Es())}`),console.log(` Session: \x1B[36m${e.session.name}\x1B[0m (window: ${e.session.defaultWindow})`),console.log(` Terminal: timeout=${e.terminal.execTimeout}ms, lines=${e.terminal.readLines}`),console.log(` Shortcuts: ${e.shortcuts.tmuxInstalled?"\x1B[32minstalled\x1B[0m":"\x1B[2mnot installed\x1B[0m"}`),console.log(` Claudio: ${e.claudio?.enabled?"\x1B[32menabled\x1B[0m":"\x1B[2mnot configured\x1B[0m"}`),console.log(` Codex: ${e.codex?.configured?"\x1B[32mconfigured\x1B[0m":"\x1B[2mnot configured\x1B[0m"}`),console.log(` Debug: tmux=${e.logging.tmuxDebug}, verbose=${e.logging.verbose}`),console.log(),e.setupComplete=!0,e.lastSetupAt=new Date().toISOString(),await je(e),console.log("\x1B[32m\u2713 Configuration saved!\x1B[0m")}async function rw(){let e=await ce();console.log(),console.log("\x1B[1mCurrent Genie Configuration\x1B[0m"),console.log(`\x1B[2m${$r(Es())}\x1B[0m`),console.log(),console.log(JSON.stringify(e,null,2)),console.log()}function sw(){console.log(),console.log("\x1B[1mNext Steps:\x1B[0m"),console.log(),console.log(" Start a session: \x1B[36mclaudio launch\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 Do(e={}){if(e.show){await rw();return}if(e.reset){await md(),console.log("\x1B[32m\u2713 Configuration reset to defaults.\x1B[0m"),console.log();return}let t=await ce();if(e.shortcuts){Hr(),await jp(t,!1),await nc();return}if(e.terminal){Hr(),t=await Lp(t,!1),await je(t),console.log("\x1B[32m\u2713 Terminal configuration saved.\x1B[0m");return}if(e.session){Hr(),t=await Np(t,!1),await je(t),console.log("\x1B[32m\u2713 Session configuration saved.\x1B[0m");return}if(e.claudio){Hr(),await Wp(t,!1),await nc();return}if(e.codex){Hr(),t=await qp(t,!1),await je(t),t.codex?.configured&&console.log("\x1B[32m\u2713 Codex configuration saved.\x1B[0m");return}let n=e.quick??!1;Hr(),n&&console.log("\x1B[2mQuick mode: accepting all defaults\x1B[0m"),await ew(),t=await Np(t,n),t=await Lp(t,n),t=await jp(t,n),t=await Wp(t,n),t=await qp(t,n),t=await tw(t,n),await nw(t),sw()}var iw={tmux:{brew:"brew install tmux",apt:"sudo apt update && sudo apt install -y tmux",dnf:"sudo dnf install -y tmux",yum:"sudo yum install -y tmux",pacman:"sudo pacman -S --noconfirm tmux",none:null},bun:{all:"curl -fsSL https://bun.sh/install | bash"},claude:{all:"npm install -g @anthropic-ai/claude-code"}};function ow(e,t){let n=iw[e];return n?n.all?n.all:t!=="none"&&n[t]?n[t]:n.none!==void 0?n.none:null:null}function Gp(e){switch(e){case"tmux":return"Visit https://github.com/tmux/tmux/wiki/Installing for installation instructions";case"bun":return"Visit https://bun.sh for installation instructions";case"claude":return"Run: npm install -g @anthropic-ai/claude-code (requires Node.js/npm)";default:return`Search for "${e} install" for installation instructions`}}async function aw(e){return new Promise(t=>{console.log(`\x1B[2m$ ${e}\x1B[0m
`);let n=(0,zp.spawn)("bash",["-c",e],{stdio:"inherit",env:{...process.env,FORCE_COLOR:"1"}});n.on("close",r=>{t(r===0)}),n.on("error",r=>{console.error(`\x1B[31mError: ${r.message}\x1B[0m`),t(!1)})})}function cw(){console.log(),console.log("\x1B[1m\u{1F527} Genie Prerequisites Check\x1B[0m"),console.log()}function lw(e){let t=e.os==="macos"?"macOS":"Linux";e.linuxDistro&&e.linuxDistro!=="unknown"&&(t=`Linux (${vd(e.linuxDistro)})`),console.log(`System: ${t} (${e.arch})`),console.log(`Package Manager: ${e.preferredPM==="none"?"none detected":e.preferredPM}`),console.log()}function uw(e){console.log("Checking prerequisites..."),console.log();for(let t of e)if(t.installed){let n=t.version?` ${t.version}`:"",r=t.path?`\x1B[2m (${t.path})\x1B[0m`:"";console.log(` \x1B[32m\u2705\x1B[0m ${t.name}${n}${r}`)}else t.required?console.log(` \x1B[31m\u274C\x1B[0m ${t.name} \x1B[31mnot found\x1B[0m`):console.log(` \x1B[33m\u26A0\uFE0F\x1B[0m ${t.name} \x1B[33mnot found\x1B[0m \x1B[2m(optional)\x1B[0m`);console.log()}function vo(){console.log(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`),console.log()}async function dw(e,t,n){let r=e.required?"\x1B[31mrequired\x1B[0m":"\x1B[2moptional, recommended\x1B[0m";if(!t)return console.log(`\x1B[33mManual installation required for\x1B[0m ${e.name} (${r})`),console.log(` ${Gp(e.name)}`),console.log(),"skipped";console.log(`Install \x1B[1m${e.name}\x1B[0m? (${r})`),console.log(` Command: \x1B[36m${t}\x1B[0m`);let s;if(n.yes?(s=!0,console.log("\x1B[2mAuto-approved with --yes\x1B[0m")):s=await le({message:"Proceed",default:e.required}),!s)return console.log(`\x1B[33mSkipped: ${e.name}\x1B[0m`),console.log(),"skipped";if(console.log(),console.log(`Installing ${e.name}...`),console.log(),await aw(t)){let o=await Je(e.name);if(o.exists){let a=o.version?` ${o.version}`:"";return console.log(),console.log(`\x1B[32m\u2705 ${e.name}${a} installed\x1B[0m`),console.log(),"installed"}return console.log(),console.log(`\x1B[33m\u26A0\uFE0F ${e.name} installed but not found in PATH\x1B[0m`),console.log("\x1B[2m You may need to restart your shell or source your profile\x1B[0m"),console.log(),"installed"}return console.log(),console.log(`\x1B[31m\u274C Failed to install ${e.name}\x1B[0m`),console.log(`\x1B[2m ${Gp(e.name)}\x1B[0m`),console.log(),"failed"}async function Hp(e){if(tn()){console.log("\x1B[2m\u2713 Genie hooks already configured (~/.genie/config.json)\x1B[0m"),console.log(" Run \x1B[36mgenie setup\x1B[0m to reconfigure."),console.log();return}console.log(),vo(),console.log("\x1B[1m\u{1F9DE} Configure Genie Hooks?\x1B[0m"),console.log(),console.log("\x1B[2mHooks let you control how AI tools execute - without wasting tokens!\x1B[0m"),console.log('\x1B[2mFor example, the "collaborative" hook routes all bash commands through\x1B[0m'),console.log("\x1B[2mtmux so you can watch the AI work in real-time.\x1B[0m"),console.log();let t;e.yes?(t=!0,console.log("\x1B[2mAuto-approved with --yes\x1B[0m")):t=await le({message:"Would you like to configure genie hooks now?",default:!0}),t?(console.log(),await Do()):(console.log(),console.log("\x1B[2mSkipped. Run \x1B[0m\x1B[36mgenie setup\x1B[0m\x1B[2m anytime to configure hooks.\x1B[0m"),console.log())}async function Kp(e){cw();let t=await wd();lw(t);let n=await Dd();uw(n);let r=n.filter(u=>!u.installed),s=r.filter(u=>u.required),i=r.filter(u=>!u.required);if(r.length===0){console.log("\x1B[32m\u2705 All prerequisites are installed!\x1B[0m"),console.log(),await Hp(e),console.log("Run \x1B[36mterm --help\x1B[0m or \x1B[36mclaudio --help\x1B[0m to get started."),console.log();return}if(console.log(`Missing: ${s.length} required, ${i.length} optional`),console.log(),e.check){s.length>0&&(console.log("\x1B[31m\u274C Missing required prerequisites\x1B[0m"),console.log("Run \x1B[36mgenie install\x1B[0m to install them."),process.exit(1));return}vo();let o={installed:[],skipped:[],failed:[]};for(let u of r){let d=ow(u.name,t.preferredPM),f=await dw(u,d,e);f==="installed"?o.installed.push(u.name):f==="skipped"?o.skipped.push(u.name):o.failed.push(u.name),u!==r[r.length-1]&&vo()}vo(),console.log("\x1B[1mSummary:\x1B[0m");let a=o.installed.filter(u=>s.some(d=>d.name===u)),c=o.failed.filter(u=>s.some(d=>d.name===u)),l=o.skipped.filter(u=>i.some(d=>d.name===u));c.length>0?console.log(`\x1B[31m \u274C ${c.length} required failed: ${c.join(", ")}\x1B[0m`):(a.length>0||s.length===0)&&console.log("\x1B[32m \u2705 All required prerequisites installed\x1B[0m"),o.installed.length>0&&console.log(`\x1B[32m \u2705 Installed: ${o.installed.join(", ")}\x1B[0m`),l.length>0&&console.log(`\x1B[33m \u26A0\uFE0F ${l.length} optional skipped: ${l.join(", ")}\x1B[0m`),console.log(),c.length===0?(await Hp(e),console.log("Run \x1B[36mterm --help\x1B[0m or \x1B[36mclaudio --help\x1B[0m to get started.")):(console.log("\x1B[31mSome required prerequisites could not be installed.\x1B[0m"),console.log("Please install them manually and run this command again."),process.exit(1)),console.log()}var Yp=require("node:fs/promises"),ko=require("node:path");async function Jp(e={}){let t=(0,ko.resolve)(e.repo||process.cwd()),n=(0,ko.join)(t,".beads","issues.jsonl"),r;try{r=await(0,Yp.readFile)(n,"utf-8")}catch{let l=`\u274C Missing: ${n}`;e.json&&(console.log(JSON.stringify({ok:!1,error:l,issuesPath:n},null,2)),process.exit(1)),console.error(l),process.exit(1)}let s=[],i=0,o=new Set,a=r.split(`
`);for(let l=0;l20&&console.error(` ...and ${s.length-20} more`)}c||process.exit(1)}async function Zp(e){await Jp(e)}var Qp=require("node:child_process"),Gs=require("node:path"),Xp=require("node:url"),pw={};function fw(){let e=typeof __filename<"u"?(0,Gs.dirname)(__filename):(0,Gs.dirname)((0,Xp.fileURLToPath)(pw.url));return(0,Gs.resolve)(e,"../../packages/genie-pdf/src/index.ts")}function Co(e){let t;try{t=fw()}catch{return console.error("\u274C genie-pdf is not available in this context (bundled plugin mode)."),console.error(" Install genie-cli from source to use PDF commands."),Promise.resolve(1)}return new Promise(n=>{let r=(0,Qp.spawn)("bun",["run",t,...e],{stdio:"inherit",cwd:process.cwd()});r.on("close",s=>{n(s??0)}),r.on("error",s=>{console.error(`\u274C Failed to execute genie-pdf: ${s.message}`),n(1)})})}async function em(e,t){let n=["render",e];t.output&&n.push("-o",t.output),t.theme&&n.push("-t",t.theme),t.pageNumbers===!1&&n.push("--no-page-numbers"),t.watch&&n.push("-w");let r=await Co(n);r!==0&&process.exit(r)}async function tm(e,t){let n=["template",e];t.data&&n.push("-d",t.data),t.output&&n.push("-o",t.output),t.theme&&n.push("-t",t.theme);let r=await Co(n);r!==0&&process.exit(r)}async function nm(){let e=await Co(["themes"]);e!==0&&process.exit(e)}async function rm(){let e=await Co(["templates"]);e!==0&&process.exit(e)}async function Ql(){let e=await ce(),t=e.workerProfiles||{},n=Object.keys(t);if(n.length===0){console.log(`
No worker profiles configured.
@@ -240,7 +240,7 @@ Done:`);for(let d of u)console.log(` ${d.id}: ${d.title}`)}console.log("")}catc
`:""}var Mk=Rk,Nk={dump:Mk};function s0(e,t){return function(){throw new Error("Function yaml."+e+" is removed in js-yaml 4. Use yaml."+t+" instead, which is now safe by default.")}}var sh=Hg.load,q3=Hg.loadAll,U3=Nk.dump;var V3=s0("safeLoad","load"),G3=s0("safeLoadAll","loadAll"),H3=s0("safeDump","dump");Er();var ih=0;function jk(){ih++;let e=Date.now().toString(36),t=ih.toString(36).padStart(4,"0");return`req-${e}-${t}`}function oh(e){return e.type!=="tool_call"&&e.type!=="permission_request"||!e.toolName?null:{id:jk(),toolName:e.toolName,toolInput:e.toolInput,paneId:e.paneId,wishId:e.wishId,sessionId:e.sessionId,cwd:e.cwd,timestamp:e.timestamp,toolCallId:e.toolCallId}}function ah(){let e=[];return{add(t){e.push(t)},next(){return e.length===0?null:e.shift()||null},get(t){return e.find(n=>n.id===t)||null},remove(t){let n=e.findIndex(r=>r.id===t);n!==-1&&e.splice(n,1)},getAll(){return[...e]},getByPane(t){return e.filter(n=>n.paneId===t)},size(){return e.length},clear(){e.length=0}}}function ch(e){if(e.toolName!=="Bash"||!e.toolInput)return null;let t=e.toolInput.command;return typeof t=="string"?t:null}var ma=b.string(),dh=b.object({inherit:b.enum(["global","none"]).optional(),allow:b.array(ma).default([]),deny:b.array(ma).default([]),bash_allow_patterns:b.array(b.string()).optional(),bash_deny_patterns:b.array(b.string()).optional()}),Wk=b.object({allow:b.array(ma).default([]),deny:b.array(ma).default([]),bash_allow_patterns:b.array(b.string()).optional(),bash_deny_patterns:b.array(b.string()).optional()}),fh=b.object({defaults:Wk.default({}),repos:b.record(b.string(),dh).optional()});function qk(){return(0,ga.join)((0,uh.homedir)(),".config","genie")}function ph(e,t){if(!(0,xa.existsSync)(e))return null;try{let n=(0,xa.readFileSync)(e,"utf-8"),r=sh(n),s=t.safeParse(r);return s.success?s.data:(console.warn(`Warning: Invalid config at ${e}:`,s.error.message),null)}catch(n){return console.warn(`Warning: Failed to load config at ${e}:`,n.message),null}}function Uk(e={}){let t=e.globalConfigDir??qk(),n=(0,ga.join)(t,"auto-approve.yaml");return ph(n,fh)}function Vk(e){let t=(0,ga.join)(e,".genie","auto-approve.yaml");return ph(t,dh)}function Gk(e,t){if(!e.repos)return null;if(e.repos[t])return e.repos[t];for(let[n,r]of Object.entries(e.repos))if(t.startsWith(`${n}/`)||t===n)return r;return null}function pa(e,t){let n=e??[],r=t??[];return Array.from(new Set([...n,...r]))}function o0(e,t){return!("inherit"in t?t.inherit!=="none":!0)&&"allow"in t?{defaults:{allow:t.allow??[],deny:t.deny??[],bash_allow_patterns:t.bash_allow_patterns,bash_deny_patterns:t.bash_deny_patterns},repos:e.repos}:{defaults:{allow:pa(e.defaults.allow,t.allow),deny:pa(e.defaults.deny,t.deny),bash_allow_patterns:pa(e.defaults.bash_allow_patterns,t.bash_allow_patterns),bash_deny_patterns:pa(e.defaults.bash_deny_patterns,t.bash_deny_patterns)},repos:e.repos}}async function a0(e,t={}){let n=fh.parse({}),r=Uk(t);if(r&&(n=r),r){let i=Gk(r,e);i&&(n=o0(n,i))}let s=Vk(e);return s&&(s.inherit==="global"?n=o0(n,s):n={defaults:{allow:s.allow,deny:s.deny,bash_allow_patterns:s.bash_allow_patterns,bash_deny_patterns:s.bash_deny_patterns}}),n}function Hk(e){let t={allow:[],deny:[],bash_allow_patterns:[],bash_deny_patterns:[]},r=/^## Auto-Approve\s*$/m.exec(e);if(!r)return t;let s=r.index+r[0].length,i=/^##?\s+/m.exec(e.slice(s)),o=i?s+i.index:e.length,c=e.slice(s,o).split(`
`);for(let l of c){let u=l.trim();if(!u.startsWith("-"))continue;let d=u.slice(1).trim(),f=/^bash:\s*["'](.+?)["']$/.exec(d);if(f){t.bash_allow_patterns.push(f[1]);continue}let p=/^allow:\s*(\w+)$/.exec(d);if(p){t.allow.push(p[1]);continue}let m=/^deny:\s*(\w+)$/.exec(d);m&&t.deny.push(m[1])}return t}async function mh(e,t,n={}){let r=await a0(e,n);if(t){let s=Hk(t);r=o0(r,s)}return r}var lh=100;function zk(e){let t=e.trim().replace(/\s+/g," ");return t===""?"":(t=t.replace(/^\/[\w./-]*\/(\w)/,"$1"),t)}function Kk(e){return/(&&|\||;|`|\$\()/.test(e)}function Yk(e,t){let n=Date.now(),r=t.length>8192?t.slice(0,8192):t;try{let s=e.test(r),i=Date.now()-n;return i>lh?(console.warn(`Warning: Regex pattern "${e.source}" took ${i}ms to evaluate (limit: ${lh}ms). Treating as non-match for safety.`),!1):s}catch{return!1}}function i0(e,t){for(let n of t)try{let r=new RegExp(n);if(Yk(r,e))return n}catch{if(console.warn(`Warning: Invalid regex pattern "${n}" in bash patterns; falling back to substring match`),e.includes(n))return n}return null}function xh(e,t){let{toolName:n}=e,{allow:r,deny:s,bash_allow_patterns:i,bash_deny_patterns:o}=t.defaults;if(s.includes(n))return{action:"deny",reason:`Tool "${n}" is in the deny list`};if(!r.includes(n))return{action:"escalate",reason:`Tool "${n}" is not in the allow list; requires human review`};if(n!=="Bash")return{action:"approve",reason:`Tool "${n}" is in the allow list`};let a=ch(e);if(a===null)return{action:"escalate",reason:"Bash tool request has no command to evaluate; requires human review"};let c=zk(a),l=o??[];if(l.length>0){let d=i0(c,l);if(d)return{action:"deny",reason:`Bash command matches deny pattern "${d}": ${c}`}}let u=i??[];if(u.length===0&&l.length===0)return{action:"approve",reason:"Bash tool is allowed and no command patterns are configured"};if(Kk(c)){if(u.length>0){let d=i0(c,u);if(d)try{let f=new RegExp(d),p=c.match(f);if(p&&p[0]===c)return{action:"approve",reason:`Bash compound command fully matches allow pattern "${d}": ${c}`}}catch{}}return{action:"escalate",reason:`Bash command contains shell metacharacters and does not fully match any allow pattern; requires human review: ${c}`}}if(u.length>0){let d=i0(c,u);if(d)return{action:"approve",reason:`Bash command matches allow pattern "${d}": ${c}`}}return{action:"escalate",reason:`Bash command does not match any allow pattern; requires human review: ${c}`}}ue();var Jk="auto-approve-audit.jsonl";function Zk(e){return(0,l0.join)(e,".genie",Jk)}function Qk(e){let t=(0,l0.join)(e,".genie");(0,ms.existsSync)(t)||(0,ms.mkdirSync)(t,{recursive:!0})}function c0(e,t){Qk(e);let n=Zk(e),r=`${JSON.stringify(t)}
`;(0,ms.appendFileSync)(n,r,"utf-8")}function Xk(e){return/^%\d+$/.test(e)}async function ha(e){await P(`send-keys -t '${e}' Enter`)}function ya(e){let{config:t,auditDir:n,sendApproval:r}=e,s=!1,i={approved:0,denied:0,escalated:0,total:0};function o(){i={approved:0,denied:0,escalated:0,total:0}}async function a(l){if(!s)return{action:"escalate",reason:"Auto-approve engine is not running; request requires human review"};if(l.paneId&&!Xk(l.paneId)){let f={action:"escalate",reason:`Security: invalid pane ID "${l.paneId}" \u2014 possible command injection; escalating to human review`},p={timestamp:new Date().toISOString(),paneId:l.paneId,toolName:l.toolName,action:f.action,reason:f.reason,wishId:l.wishId,requestId:l.id};try{c0(n,p)}catch{}return i.total++,i.escalated++,f}let u=xh(l,t),d={timestamp:new Date().toISOString(),paneId:l.paneId,toolName:l.toolName,action:u.action,reason:u.reason,wishId:l.wishId,requestId:l.id};try{c0(n,d)}catch(f){let p={action:"escalate",reason:`Audit log write failed (${f instanceof Error?f.message:String(f)}); escalating to human review to preserve audit trail`};return i.total++,i.escalated++,p}if(i.total++,u.action==="approve"?i.approved++:u.action==="deny"?i.denied++:i.escalated++,u.action==="approve"&&l.paneId)try{await r(l.paneId)}catch(f){let p={timestamp:new Date().toISOString(),paneId:l.paneId,toolName:l.toolName,action:"escalate",reason:`Approval delivery failed via sendApproval (${f instanceof Error?f.message:String(f)}); send-keys did not reach pane`,wishId:l.wishId,requestId:l.id};try{c0(n,p)}catch{}}return u}async function c(l){if(!s||l.type!=="permission_request")return;let u=oh(l);u&&await a(u)}return{start(){s||(o(),s=!0)},stop(){s&&(s=!1)},isRunning(){return s},processRequest:a,processEvent:c,getStats(){return{...i}}}}ue();ot();var Tt=require("node:fs/promises"),Ht=require("node:path"),Ve=require("bun"),e8=".genie/worktrees";async function t8(){try{return typeof Bun.which=="function"?!!Bun.which("bd"):(await Ve.$`bd --version`.quiet(),!0)}catch{return!1}}async function u0(e){try{let t=await Ve.$`bd ${e}`.quiet();return{stdout:t.stdout.toString().trim(),stderr:t.stderr.toString().trim(),exitCode:0}}catch(t){return{stdout:t.stdout?.toString().trim()||"",stderr:t.stderr?.toString().trim()||"",exitCode:t.exitCode||1}}}function gh(e){if(!e)return null;try{return JSON.parse(e)}catch{return null}}function f0(e){return(0,Ht.join)(e,e8)}function d0(e,t){return(0,Ht.join)(f0(e),t)}function ba(e){return`work/${e}`}var p0=class{repoPath;constructor(t){this.repoPath=t}async create(t,n){let r=d0(n,t),s=ba(t);await(0,Tt.mkdir)(f0(n),{recursive:!0});try{return await(0,Tt.access)(r),await this.getWorktreeInfo(t,n)}catch{}let i=!1;try{await Ve.$`git -C ${n} rev-parse --verify ${s}`.quiet(),i=!0}catch{}i?await Ve.$`git -C ${n} worktree add ${r} ${s}`.quiet():await Ve.$`git -C ${n} worktree add -b ${s} ${r}`.quiet();let o=(0,Ht.join)(r,".genie");await(0,Tt.mkdir)(o,{recursive:!0}),await(0,Tt.writeFile)((0,Ht.join)(o,"redirect"),(0,Ht.join)(n,".genie"));let a;try{a=(await Ve.$`git -C ${r} rev-parse HEAD`.quiet()).stdout.toString().trim()}catch{}return{path:r,branch:s,wishId:t,commitHash:a,createdAt:new Date}}async remove(t){let n=d0(this.repoPath,t);try{await Ve.$`git -C ${this.repoPath} worktree remove ${n} --force`.quiet()}catch{try{await(0,Tt.rm)(n,{recursive:!0,force:!0})}catch{}}try{await Ve.$`git -C ${this.repoPath} worktree prune`.quiet()}catch{}}async list(){let n=(await Ve.$`git -C ${this.repoPath} worktree list --porcelain`.quiet()).stdout.toString(),r=f0(this.repoPath),s=[],i={};for(let o of n.split(`
-`))o.startsWith("worktree ")?(i.path?.startsWith(r)&&(i.wishId=(0,Ht.basename)(i.path),s.push(i)),i={path:o.slice(9)}):o.startsWith("HEAD ")?i.commitHash=o.slice(5):o.startsWith("branch ")&&(i.branch=o.slice(7).replace("refs/heads/",""));return i.path?.startsWith(r)&&(i.wishId=(0,Ht.basename)(i.path),s.push(i)),s}async get(t){return this.getWorktreeInfo(t,this.repoPath)}async getWorktreeInfo(t,n){let r=d0(n,t);try{await(0,Tt.access)(r)}catch{return null}let s=ba(t);try{s=(await Ve.$`git -C ${r} branch --show-current`.quiet()).stdout.toString().trim()||s}catch{}let i;try{i=(await Ve.$`git -C ${r} rev-parse HEAD`.quiet()).stdout.toString().trim()}catch{}return{path:r,branch:s,wishId:t,commitHash:i}}},m0=class{async create(t,n){let{stdout:r,exitCode:s,stderr:i}=await u0(["worktree","create",t,"--json"]);if(s!==0)throw new Error(`bd worktree create failed: ${i||r}`);let o=gh(r);if(!o)throw new Error("Failed to parse bd worktree create output");let a=ba(t);return o.branch!==a&&(console.log(`\u{1F527} Fixing branch name: ${o.branch} \u2192 ${a}`),await Ve.$`git -C ${o.path} branch -m ${a}`.quiet(),o.branch=a),{path:o.path,branch:o.branch,wishId:t}}async remove(t){let{exitCode:n,stderr:r}=await u0(["worktree","remove",t]);if(n!==0)throw new Error(`bd worktree remove failed: ${r}`)}async list(){let{stdout:t,exitCode:n}=await u0(["worktree","list","--json"]);if(n!==0||!t)return[];let r=gh(t);return r?r.map(s=>({path:s.path,branch:s.branch,wishId:s.name})):[]}async get(t){return(await this.list()).find(r=>r.wishId===t||r.branch===t||r.branch===ba(t))||null}};async function hh(e){return await t8()?new m0:new p0(e)}var kn=fn()&&(typeof Bun.which=="function"?!!Bun.which("bd"):!0);var n8={"genie-cli":"code/genie-cli","genie work":"code/genie-cli","genie worker":"code/genie-cli"};function yh(e,t){if(e===t)return"";let n=t.replace(/'/g,"'\\''");return`[ -f '${n}/.env' ] && set -a && source '${n}/.env' && set +a; `}async function x0(e){try{return{stdout:(await _h.$`bd ${e}`.quiet()).stdout.toString().trim(),exitCode:0}}catch(t){return{stdout:t.stdout?.toString().trim()||"",exitCode:t.exitCode||1}}}function r8(e){if(!e||e.length>128)return null;let t=e.trim();return!t||!/^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/.test(t)||t==="__proto__"||t==="constructor"||t==="prototype"||/\.\./.test(t)||/[./]$/.test(t)||/\.lock$/.test(t)?null:t}function s8(e){return e?{id:e.id,title:e.title||e.description?.substring(0,50)||"Untitled",status:e.status,description:e.description,blockedBy:e.blockedBy||[]}:null}async function Eh(e){let{stdout:t,exitCode:n}=await x0(["show",e,"--json"]);if(n===0&&t)try{let r=JSON.parse(t),s=Array.isArray(r)?r[0]:r;return s8(s)}catch{}return null}async function i8(e){let t=Se(e);if(t.kind==="local"){let s=await t.queue();if(s.ready.length===0)return null;let i=s.ready[0],o=await t.get(i);return o?{id:o.id,title:o.title,status:o.status,description:o.description,blockedBy:o.blockedBy||[]}:null}let{stdout:n,exitCode:r}=await x0(["ready","--json"]);if(r!==0||!n)return null;try{let s=JSON.parse(n);if(Array.isArray(s)&&s.length>0){let i=s[0];return{id:i.id,title:i.title||i.description?.substring(0,50)||"Untitled",status:i.status,description:i.description,blockedBy:i.blockedBy||[]}}return null}catch{let s=n.split(`
+`))o.startsWith("worktree ")?(i.path?.startsWith(r)&&(i.wishId=(0,Ht.basename)(i.path),s.push(i)),i={path:o.slice(9)}):o.startsWith("HEAD ")?i.commitHash=o.slice(5):o.startsWith("branch ")&&(i.branch=o.slice(7).replace("refs/heads/",""));return i.path?.startsWith(r)&&(i.wishId=(0,Ht.basename)(i.path),s.push(i)),s}async get(t){return this.getWorktreeInfo(t,this.repoPath)}async getWorktreeInfo(t,n){let r=d0(n,t);try{await(0,Tt.access)(r)}catch{return null}let s=ba(t);try{s=(await Ve.$`git -C ${r} branch --show-current`.quiet()).stdout.toString().trim()||s}catch{}let i;try{i=(await Ve.$`git -C ${r} rev-parse HEAD`.quiet()).stdout.toString().trim()}catch{}return{path:r,branch:s,wishId:t,commitHash:i}}},m0=class{async create(t,n){let{stdout:r,exitCode:s,stderr:i}=await u0(["worktree","create",t,"--json"]);if(s!==0)throw new Error(`bd worktree create failed: ${i||r}`);let o=gh(r);if(!o)throw new Error("Failed to parse bd worktree create output");let a=ba(t);return o.branch!==a&&(console.log(`\u{1F527} Fixing branch name: ${o.branch} \u2192 ${a}`),await Ve.$`git -C ${o.path} branch -m ${a}`.quiet(),o.branch=a),{path:o.path,branch:o.branch,wishId:t}}async remove(t){let{exitCode:n,stderr:r}=await u0(["worktree","remove",t]);if(n!==0)throw new Error(`bd worktree remove failed: ${r}`)}async list(){let{stdout:t,exitCode:n}=await u0(["worktree","list","--json"]);if(n!==0||!t)return[];let r=gh(t);return r?r.map(s=>({path:s.path,branch:s.branch,wishId:s.name})):[]}async get(t){return(await this.list()).find(r=>r.wishId===t||r.branch===t||r.branch===ba(t))||null}};async function hh(e){return await t8()?new m0:new p0(e)}var kn=fn()&&(typeof Bun.which=="function"?!!Bun.which("bd"):!0);var n8={"genie-cli":"code/genie-cli","genie work":"code/genie-cli","genie agent":"code/genie-cli"};function yh(e,t){if(e===t)return"";let n=t.replace(/'/g,"'\\''");return`[ -f '${n}/.env' ] && set -a && source '${n}/.env' && set +a; `}async function x0(e){try{return{stdout:(await _h.$`bd ${e}`.quiet()).stdout.toString().trim(),exitCode:0}}catch(t){return{stdout:t.stdout?.toString().trim()||"",exitCode:t.exitCode||1}}}function r8(e){if(!e||e.length>128)return null;let t=e.trim();return!t||!/^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/.test(t)||t==="__proto__"||t==="constructor"||t==="prototype"||/\.\./.test(t)||/[./]$/.test(t)||/\.lock$/.test(t)?null:t}function s8(e){return e?{id:e.id,title:e.title||e.description?.substring(0,50)||"Untitled",status:e.status,description:e.description,blockedBy:e.blockedBy||[]}:null}async function Eh(e){let{stdout:t,exitCode:n}=await x0(["show",e,"--json"]);if(n===0&&t)try{let r=JSON.parse(t),s=Array.isArray(r)?r[0]:r;return s8(s)}catch{}return null}async function i8(e){let t=Se(e);if(t.kind==="local"){let s=await t.queue();if(s.ready.length===0)return null;let i=s.ready[0],o=await t.get(i);return o?{id:o.id,title:o.title,status:o.status,description:o.description,blockedBy:o.blockedBy||[]}:null}let{stdout:n,exitCode:r}=await x0(["ready","--json"]);if(r!==0||!n)return null;try{let s=JSON.parse(n);if(Array.isArray(s)&&s.length>0){let i=s[0];return{id:i.id,title:i.title||i.description?.substring(0,50)||"Untitled",status:i.status,description:i.description,blockedBy:i.blockedBy||[]}}return null}catch{let s=n.split(`
`).filter(i=>i.trim());if(s.length>0){let i=s[0].match(/^(bd-\d+)/);if(i)return Eh(i[1])}return null}}async function o8(e){let{exitCode:t}=await x0(["update",e,"--status","in_progress"]);return t===0}async function a8(e){let t=await import("node:fs/promises"),n={};try{let r=await t.readFile(e,"utf-8"),s=r.split(`
`),i=s[0]?.match(/^#\s+(?:Wish\s+\d+:\s+)?(.+)$/i);i&&(n.title=i[1].trim());for(let a of s.slice(0,20)){let c=a.match(/^\*\*Status:\*\*\s*(.+)$/i);if(c){n.status=c[1].trim();continue}let l=a.match(/^\*\*Slug:\*\*\s*`?([^`]+)`?$/i);if(l){n.slug=l[1].trim();continue}let u=a.match(/^\*\*repo:\*\*\s*`?([^`]+)`?$/i);u&&(n.repo=u[1].trim())}let o=r.indexOf("## Summary");if(o!==-1){let a=r.slice(o+10),c=a.indexOf(`
## `),l=c!==-1?a.slice(0,c):a.slice(0,500);n.description=l.trim()}return n}catch{return n}}async function c8(e,t,n){let r=await import("node:fs/promises"),s=`${e} ${t||""}`.toLowerCase();for(let[i,o]of Object.entries(n8))if(s.includes(i.toLowerCase())){let a=(0,ge.join)(n,o);try{let c=(0,ge.join)(a,".git");return await r.access(c),o}catch{}}return null}async function l8(e,t,n,r,s){if(n)return{targetRepo:(0,ge.isAbsolute)(n)?n:(0,ge.resolve)(t,n),detectionMethod:"--repo flag"};let i=(0,ge.join)(t,".genie","wishes",e,"wish.md"),o=await a8(i);if(o.repo)return{targetRepo:(0,ge.isAbsolute)(o.repo)?o.repo:(0,ge.resolve)(t,o.repo),detectionMethod:"wish.md repo: field"};let a=o.title||r||"",c=o.description||s||"",l=await c8(a,c,t);return l?{targetRepo:(0,ge.resolve)(t,l),detectionMethod:`heuristic (matched "${l}")`}:{targetRepo:t,detectionMethod:"default (current repo)"}}async function u8(){try{return(await P("display-message -p '#{session_name}'")).trim()||null}catch{return null}}async function bh(e){if(e)return e;let t=await u8();if(t)return t;let r=Bi()||"genie-workers";return await ke(r)?(console.log(`\u{1F4FA} Found existing tmux session '${r}'. Attach with: tmux attach -t ${r}`),r):(await Jr(r)||(console.error("\u274C Failed to create tmux session. Is tmux installed?"),process.exit(1)),console.log(`\u{1F4FA} Created tmux session '${r}'. Attach with: tmux attach -t ${r}`),r)}async function d8(e,t){try{return(await(await hh(t)).create(e,t)).path}catch(n){return console.error(`\u26A0\uFE0F Failed to create worktree: ${n.message}`),null}}async function $h(e,t){let n=await import("node:fs/promises"),r=(0,ge.join)(t,".wishes");try{await n.access(r)}catch{return}async function s(a){let c=[],l;try{l=await n.readdir(a,{withFileTypes:!0})}catch{return c}for(let u of l){let d=(0,ge.join)(a,u.name);u.isDirectory()?c.push(...await s(d)):u.isFile()&&u.name.endsWith("-wish.md")&&c.push(d)}return c}let i=await s(r),o=[new RegExp(`\\*\\*Beads:\\*\\*\\s*${wh(e)}`),new RegExp(`^Beads:\\s*${wh(e)}`,"m")];for(let a of i)try{let c=await n.readFile(a,"utf-8");for(let l of o)if(l.test(c))return a}catch{}}function wh(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}async function f8(e,t){let n=await import("node:fs/promises"),r=(0,ge.join)(t,".genie","wishes",e,"wish.md");try{return await n.access(r),!0}catch{}return await $h(e,t)!==void 0}async function p8(e,t){let n=await import("node:fs/promises"),r=(0,ge.join)(t,".genie","wishes",e,"wish.md");try{return await n.readFile(r,"utf-8")}catch{}let s=await $h(e,t);if(s)try{return await n.readFile(s,"utf-8")}catch{return}}async function Dh(e,t,n){try{let r=await p8(e,t),s=await mh(n,r),i=ya({config:s,auditDir:t,sendApproval:ha});return i.start(),i}catch(r){console.log(`\u26A0\uFE0F Auto-approve setup failed: ${r.message} (non-fatal)`);return}}async function vh(e){return console.log(`
@@ -249,23 +249,23 @@ Done:`);for(let d of u)console.log(` ${d.id}: ${d.title}`)}console.log("")}catc
`).filter(o=>o.trim());if(i.length>0){let o=i.slice(-5).join(`
`);if(o.includes("\u276F")||o.includes("? for shortcuts")||o.includes("What would you like")||o.includes("How can I help"))return!0}}catch{}await new Promise(s=>setTimeout(s,n))}return!1}async function kh(e,t,n){try{let r=await ke(e);if(!r)return console.error(`\u274C Session "${e}" not found`),null;let s=await au(r.id,t);if(s){let a=await St(s.id);return!a||a.length===0?(console.error(`\u274C No panes in existing window "${t}"`),null):{paneId:a[0].id,windowId:s.id,windowCreated:!1}}let i=await ou(r.id,t,n);if(!i)return console.error(`\u274C Failed to create window "${t}"`),null;let o=await St(i.id);return!o||o.length===0?(console.error(`\u274C No panes in new window "${t}"`),null):{paneId:o[0].id,windowId:i.id,windowCreated:!0}}catch(r){return console.error(`\u274C Error ensuring worker window: ${r.message}`),null}}function Ch(e,t,n,r){let s=new li(t,{pollIntervalMs:1e3,paneId:n});if(r){let i=0;s.on("permission",async o=>{let a=Date.now();if(a-i<2e3)return;let c=o.state?.rawOutput||"",l=wu(c),u="unknown",d;if(l)switch(l.type){case"bash":u="Bash",l.command&&(d={command:l.command});break;case"file":/Allow.*Edit/i.test(c)?u="Edit":/Allow.*Write/i.test(c)?u="Write":/Allow.*Read/i.test(c)?u="Read":u="Write",l.file&&(d={file_path:l.file});break;default:u=l.type}let f={id:`auto-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,toolName:u,toolInput:d,paneId:n,wishId:e,sessionId:"",cwd:"",timestamp:new Date().toISOString()},p=await r.processRequest(f);p.action==="approve"?(i=a,console.log(` \u2705 Auto-approved: ${u}${l?.command?` (${l.command.substring(0,50)})`:""}`)):p.action==="deny"&&console.log(` \u274C Auto-denied: ${u} - ${p.reason}`)})}s.on("state_change",async i=>{if(!i.state)return;let o;switch(i.state.type){case"working":case"tool_use":o="working";break;case"idle":o="idle";break;case"permission":o="permission";break;case"question":o="question";break;case"error":o="error";break;case"complete":o="done";break;default:return}try{kn&&await hu(e,o),await dr(e,o)}catch{}}),s.on("poll_error",()=>{kn&&sx(e).catch(()=>{}),Me(e).catch(()=>{}),as(n).catch(()=>{}),s.stop()}),s.start().catch(()=>{})}async function Ah(e,t={}){try{let n=process.cwd();if(kn&&!(await pn()).running){console.log("\u{1F504} Starting beads daemon for auto-sync...");let Kt=await si({autoCommit:!0});console.log(Kt?" \u2705 Daemon started":" \u26A0\uFE0F Daemon failed to start (non-fatal)")}let r=await ce(),s;if(t.profile){if(s=Ar(r,t.profile),!s){let ne=Object.keys(r.workerProfiles||{});console.error(`Profile '${t.profile}' not found.`),ne.length>0?console.log(`Available profiles: ${ne.join(", ")}`):console.log("No profiles configured in ~/.genie/config.json"),process.exit(1)}}else s=gd(r);let i=null;if(e==="next"){if(console.log("\u{1F50D} Finding next ready issue..."),i=await i8(n),!i){console.log("\u2139\uFE0F No ready issues. Run `bd ready` to see the queue.");return}console.log(`\u{1F4CB} Found: ${i.id} - "${i.title}"`)}else if(e==="wish")console.error("\u274C `genie work wish` is not yet implemented."),process.exit(1);else{let ne=r8(e);ne||(console.error(`\u274C Invalid task ID: "${e}"`),console.error(' Task IDs must be alphanumeric with hyphens/underscores (e.g., "bd-123", "wish-1").'),console.error(" Got characters that are unsafe for git branches or shell commands."),process.exit(1)),e=ne;let Kt=Se(n);if(Kt.kind==="local"){let pt=await Kt.get(e);pt&&(i={id:pt.id,title:pt.title,status:pt.status,description:pt.description,blockedBy:pt.blockedBy||[]})}i||(i=await Eh(e)),i||(Se(n).kind==="local"?(console.error(`\u274C Issue "${e}" not found in local task registry.`),console.error(` File: ${(0,ge.join)(n,".genie","tasks.json")}`),(await import("node:fs")).existsSync((0,ge.join)(n,".genie","tasks.json"))?console.error(` Task "${e}" is not in tasks.json. Run \`bd list\` to see available tasks.`):(console.error(" \u26A0\uFE0F tasks.json does not exist. This is likely a fresh repo."),console.error(' Fix: Run `genie task create "Your task title"` to create the first task,'),console.error(" or `bd sync` if using beads."))):console.error(`\u274C Issue "${e}" not found. Run \`bd list\` to see issues.`),process.exit(1))}let o=i.id,a=kn?await Qr(o):null;if(a||(a=await hn(o)),a){if(a.claudeSessionId&&t.resume!==!1){console.log(`\u{1F4CB} Found existing worker for ${o} with resumable session`),console.log(` Session ID: ${a.claudeSessionId}`),console.log(" Resuming previous Claude session...");let ne=await bh(t.session),Kt=a.worktree||a.repoPath;console.log("\u{1F680} Ensuring worker window...");let pt=await kh(ne,o,Kt);pt||process.exit(1);let{paneId:ys,windowId:zh}=pt;await ts(a.id,{paneId:ys,session:ne,windowName:o,windowId:zh,state:"spawning",lastStateChange:new Date().toISOString()}),kn&&await Et(a.id,"spawning").catch(()=>{});let Kh=(0,ge.join)(a.repoPath,".genie"),Yh=Kt.replace(/'/g,"'\\''"),Jh=Ir(s,{resume:a.claudeSessionId,beadsDir:Kh}),Zh=yh(Kt,a.repoPath);await an(ys,`cd '${Yh}' && ${Zh}${Jh}`,!0,!1),kn&&await Et(a.id,"working").catch(()=>{}),await dr(a.id,"working");let bs;t.noAutoApprove||(bs=await Dh(o,a.repoPath,a.repoPath),bs&&console.log("\u{1F512} Auto-approve engine started")),Ch(a.id,ne,ys,bs),t.focus===!0&&await P(`select-window -t '${ne}:${o}'`),console.log(`
\u2705 Resumed worker for ${o}`),console.log(` Window: ${o}`),console.log(` Pane: ${ys}`),console.log(` Session: ${ne}`),console.log(` Claude Session: ${a.claudeSessionId}`),console.log(`
-Commands:`),console.log(" genie worker list - Check worker status"),console.log(" genie worker approve - Approve permissions"),console.log(` genie worker close ${o} - Close issue when done`),console.log(` genie worker kill ${o} - Force kill worker`),bs&&!t._skipAutoApproveBlock&&await vh(bs);return}console.error(`\u274C ${o} already has a worker (pane ${a.paneId})`),console.log(` Run \`genie worker kill ${a.id}\` first, or work on a different issue.`),process.exit(1)}let c=await bh(t.session);console.log(`\u{1F4DD} Claiming ${o}...`);let l=Se(n),u=!1,d;try{u=await(l.kind==="local"?l.claim(o):o8(o))}catch(ne){d=ne.message||String(ne)}if(!u)if(l.kind==="beads")console.error(`\u274C Failed to claim ${o}.${d?` Reason: ${d}`:""}`),console.error(" The issue may not exist or is already claimed."),console.error(` Run \`bd show ${o}\` to check status.`),process.exit(1);else{let ne=await l.get(o);ne?(console.error(`\u274C Failed to claim ${o} (status: ${ne.status}).`),console.error(" Task may already be in_progress or done.")):(console.error(`\u274C Task "${o}" not found in .genie/tasks.json.`),console.error(" Available tasks: run `cat .genie/tasks.json | jq '.order'`"),console.error(` Or create one: \`term create "${o} title"\``)),process.exit(1)}let{targetRepo:f,detectionMethod:p}=await l8(o,n,t.repo,i.title,i.description);f!==n&&(console.log(`\u{1F3AF} Detected nested repo: ${f}`),console.log(` Detection: ${p}`));let m=f,x=null;t.noWorktree||(console.log(`\u{1F333} Creating worktree for ${o} in ${f}...`),x=await d8(o,f),x?(m=x,console.log(` Created: ${x}`),console.log(` Branch: work/${o}`)):console.log("\u26A0\uFE0F Worktree creation failed. Using shared repo.")),console.log(`\u{1F680} Creating worker window "${o}"...`);let g=await kh(c,o,m);g||process.exit(1);let{paneId:h,windowId:y}=g,S=(0,Sh.randomUUID)(),E=await ku(o,t.name),C=await Jo(o);C>0&&console.log(` \u{1F4CC} Additional worker on task (${C+1} total)`);let B={id:E,paneId:h,session:c,worktree:x,taskId:o,taskTitle:i.title,startedAt:new Date().toISOString(),state:"spawning",lastStateChange:new Date().toISOString(),repoPath:f,claudeSessionId:S,windowName:o,windowId:y,role:t.role,customName:t.name};if(kn)try{let ne=await ex(o,{paneId:h,session:c,worktree:x,repoPath:f,taskId:o,taskTitle:i.title,claudeSessionId:S});await nx(o,o),await Et(o,"spawning")}catch(ne){console.log(`\u26A0\uFE0F Beads registration failed: ${ne.message} (non-fatal)`)}await ur(B);let O=t.skill;!O&&!t.prompt&&await f8(o,n)&&(O="forge",console.log("\u{1F4CB} Found wish.md - using /forge skill"));let K;O?K=`/${O}`:K=t.prompt||`Work on beads issue ${o}: "${i.title}"
+Commands:`),console.log(" genie agent list - Check worker status"),console.log(" genie agent approve - Approve permissions"),console.log(` genie agent close ${o} - Close issue when done`),console.log(` genie agent kill ${o} - Force kill worker`),bs&&!t._skipAutoApproveBlock&&await vh(bs);return}console.error(`\u274C ${o} already has a worker (pane ${a.paneId})`),console.log(` Run \`genie agent kill ${a.id}\` first, or work on a different issue.`),process.exit(1)}let c=await bh(t.session);console.log(`\u{1F4DD} Claiming ${o}...`);let l=Se(n),u=!1,d;try{u=await(l.kind==="local"?l.claim(o):o8(o))}catch(ne){d=ne.message||String(ne)}if(!u)if(l.kind==="beads")console.error(`\u274C Failed to claim ${o}.${d?` Reason: ${d}`:""}`),console.error(" The issue may not exist or is already claimed."),console.error(` Run \`bd show ${o}\` to check status.`),process.exit(1);else{let ne=await l.get(o);ne?(console.error(`\u274C Failed to claim ${o} (status: ${ne.status}).`),console.error(" Task may already be in_progress or done.")):(console.error(`\u274C Task "${o}" not found in .genie/tasks.json.`),console.error(" Available tasks: run `cat .genie/tasks.json | jq '.order'`"),console.error(` Or create one: \`term create "${o} title"\``)),process.exit(1)}let{targetRepo:f,detectionMethod:p}=await l8(o,n,t.repo,i.title,i.description);f!==n&&(console.log(`\u{1F3AF} Detected nested repo: ${f}`),console.log(` Detection: ${p}`));let m=f,x=null;t.noWorktree||(console.log(`\u{1F333} Creating worktree for ${o} in ${f}...`),x=await d8(o,f),x?(m=x,console.log(` Created: ${x}`),console.log(` Branch: work/${o}`)):console.log("\u26A0\uFE0F Worktree creation failed. Using shared repo.")),console.log(`\u{1F680} Creating worker window "${o}"...`);let g=await kh(c,o,m);g||process.exit(1);let{paneId:h,windowId:y}=g,S=(0,Sh.randomUUID)(),E=await ku(o,t.name),C=await Jo(o);C>0&&console.log(` \u{1F4CC} Additional worker on task (${C+1} total)`);let B={id:E,paneId:h,session:c,worktree:x,taskId:o,taskTitle:i.title,startedAt:new Date().toISOString(),state:"spawning",lastStateChange:new Date().toISOString(),repoPath:f,claudeSessionId:S,windowName:o,windowId:y,role:t.role,customName:t.name};if(kn)try{let ne=await ex(o,{paneId:h,session:c,worktree:x,repoPath:f,taskId:o,taskTitle:i.title,claudeSessionId:S});await nx(o,o),await Et(o,"spawning")}catch(ne){console.log(`\u26A0\uFE0F Beads registration failed: ${ne.message} (non-fatal)`)}await ur(B);let O=t.skill;!O&&!t.prompt&&await f8(o,n)&&(O="forge",console.log("\u{1F4CB} Found wish.md - using /forge skill"));let K;O?K=`/${O}`:K=t.prompt||`Work on beads issue ${o}: "${i.title}"
## Description
${i.description||"No description provided."}
When you're done, commit your changes and let me know.`;let _=K.replace(/'/g,"'\\''"),D=(0,ge.join)(n,".genie"),T=m.replace(/'/g,"'\\''"),H=Ir(s,{sessionId:S,beadsDir:D}),ee=yh(m,n);await an(h,`cd '${T}' && ${ee}${H}`,!0,!1),console.log(` Session ID: ${S}`),await m8(h)||console.log(" (Claude startup timed out, sending prompt anyway)"),await P(`send-keys -t '${h}' '${_}' Enter`),kn&&await Et(o,"working").catch(()=>{}),await dr(o,"working");let ct;t.noAutoApprove||(ct=await Dh(o,n,f),ct&&console.log("\u{1F512} Auto-approve engine started")),Ch(o,c,h,ct),t.focus===!0&&await P(`select-window -t '${c}:${o}'`),console.log(`
\u2705 Worker started for ${o}`),console.log(` Window: ${o}`),console.log(` Pane: ${h}`),console.log(` Session: ${c}`),x&&(console.log(` Worktree: ${x}`),console.log(` Branch: work/${o}`)),f!==n&&console.log(` Target repo: ${f}`),console.log(`
-Commands:`),console.log(" genie worker list - Check worker status"),console.log(" genie worker approve - Approve permissions"),console.log(` genie worker close ${o} - Close issue when done`),console.log(` genie worker kill ${o} - Force kill worker`),ct&&!t._skipAutoApproveBlock&&await vh(ct)}catch(n){console.error(`\u274C Error: ${n.message}`),process.exit(1)}}var wa=require("node:fs"),Ih=require("node:path");var Cn=null,g0=ah();var g8="auto-approve-audit.jsonl";function h8(e){let t=(0,Ih.join)(e,".genie",g8);if(!(0,wa.existsSync)(t))return[];try{let r=(0,wa.readFileSync)(t,"utf-8").trim().split(`
-`).filter(i=>i.trim()),s=[];for(let i of r)try{s.push(JSON.parse(i))}catch{}return s}catch{return[]}}function y8(e){switch(e){case"approve":return"approved";case"deny":return"denied";case"escalate":return"escalated"}}function b8(e){let t=[],n=h8(e.auditDir);for(let s of n)t.push({requestId:s.requestId,toolName:s.toolName,paneId:s.paneId,status:y8(s.action),reason:s.reason,timestamp:s.timestamp});let r=e.queue.getAll();for(let s of r)t.push({requestId:s.id,toolName:s.toolName,paneId:s.paneId,status:"pending",reason:"",timestamp:s.timestamp});return t}function w8(e,t){return t.queue.get(e)?(t.queue.remove(e),!0):!1}function D8(e,t){return t.queue.get(e)?(t.queue.remove(e),!0):!1}function Ph(){return Cn!==null&&Cn.isRunning()}async function v8(e){if(Cn?.isRunning())return;let t=await a0(e.repoPath);Cn=ya({config:t,auditDir:e.auditDir,sendApproval:ha}),Cn.start()}function k8(){Cn&&(Cn.stop(),Cn=null)}async function Th(e,t){let n=process.cwd(),r=n;if(t.status){let s=b8({auditDir:r,queue:g0});if(s.length===0){console.log("No auto-approve requests found.");return}console.log("Auto-Approve Requests:"),console.log("");for(let i of s){let o=i.status.toUpperCase().padEnd(10),a=i.paneId??"N/A";console.log(` [${o}] ${i.requestId} ${i.toolName} pane:${a} ${i.timestamp}`),i.reason&&console.log(` Reason: ${i.reason}`)}return}if(t.start){if(Ph()){console.log("Auto-approve engine is already running.");return}await v8({auditDir:r,repoPath:n}),console.log("Auto-approve engine started.");return}if(t.stop){if(!Ph()){console.log("Auto-approve engine is not running.");return}k8(),console.log("Auto-approve engine stopped.");return}if(t.deny){D8(t.deny,{queue:g0})?console.log(`Denied request: ${t.deny}`):(console.error(`Request "${t.deny}" not found in pending queue.`),process.exit(1));return}if(e){w8(e,{queue:g0})?console.log(`Approved request: ${e}`):(console.error(`Request "${e}" not found in pending queue.`),process.exit(1));return}console.log("Usage:"),console.log(" genie worker approve --status Show pending/approved/denied requests"),console.log(" genie worker approve Manually approve a pending request"),console.log(" genie worker approve --deny Manually deny a pending request"),console.log(" genie worker approve --start Start the auto-approve engine"),console.log(" genie worker approve --stop Stop the auto-approve engine")}function zt(e){process.env.DEBUG&&console.error(`[target-resolver] ${e}`)}async function S8(e,t){try{let n=await Promise.resolve().then(()=>(ue(),Ce)),r=await n.findSessionByName(e);if(!r)return null;let s=await n.listWindows(r.id);if(!s||s.length===0)return null;let i;if(t){if(i=s.find(c=>c.name===t),!i)return null}else i=s.find(c=>c.active)||s[0];let o=await n.listPanes(i.id);return!o||o.length===0?null:{paneId:(o.find(c=>c.active)||o[0]).id,session:e}}catch{return null}}async function _8(e){try{return(await(await Promise.resolve().then(()=>(ue(),Ce))).executeTmux(`display-message -p -t '${e}' '#{pane_id}'`)).trim()===e}catch{return!1}}async function E8(e,t){try{await(await Promise.resolve().then(()=>(ot(),it))).removeSubPane(e,t)}catch{}}async function $8(e){try{return(await(await Promise.resolve().then(()=>(ue(),Ce))).executeTmux(`display-message -p -t '${e}' '#{session_name}'`)).trim()||null}catch{return null}}async function xs(e,t={}){let{checkLiveness:n=!1,workers:r,tmuxLookup:s=S8,isPaneLive:i=_8,cleanupDeadPane:o=E8,deriveSession:a=$8}=t;if(zt(`resolving "${e}"`),e.startsWith("%")){if(zt(`"${e}" -> raw pane ID passthrough`),n&&!await i(e))throw new Error(`Pane ${e} is dead or does not exist. Check with: tmux list-panes -a`);let d=await a(e);return zt(`"${e}" -> derived session: ${d||"(none)"}`),{paneId:e,session:d??void 0,resolvedVia:"raw"}}if(e.startsWith("@")){zt(`"${e}" -> window ID lookup`);let d=await Fh(r,t.registryPath),f=e,p=Object.values(d).find(m=>m.windowId===f);if(p){if(zt(`"${e}" -> found worker "${p.id}" with pane ${p.paneId}`),n&&!await i(p.paneId))throw new Error(`Window ${e}: worker ${p.id} pane ${p.paneId} is dead. Run 'genie worker kill ${p.id}' to clean up.`);return{paneId:p.paneId,session:p.session,workerId:p.id,resolvedVia:"worker"}}throw new Error(`Window "${e}" not found in worker registry.
-Run 'genie worker list' to list workers.`)}let c=await Fh(r,t.registryPath),l=e.indexOf(":");if(l!==-1){let d=e.substring(0,l),f=e.substring(l+1),p=c[d];if(p){let x=Number.parseInt(f,10);if(Number.isNaN(x)||x<0)throw new Error(`Invalid sub-pane index "${f}" for worker "${d}". Use a non-negative integer (0 = primary, 1+ = sub-panes).`);let g=A8(p,x);if(!g){let h=p.subPanes?p.subPanes.length:0;throw new Error(`Worker "${d}" has no sub-pane index ${x}. Available: 0 (primary)${h>0?`, 1-${h} (sub-panes)`:""}. Sub-pane index ${x} does not exist.`)}if(zt(`"${e}" -> worker "${d}" pane index ${x} -> ${g}`),n&&!await i(g))throw await o(d,g),new Error(`Worker ${d}: pane ${g} is dead. Run 'genie worker kill ${d}' to clean up.`);return{paneId:g,session:p.session,workerId:d,paneIndex:x,resolvedVia:"worker"}}zt(`"${d}" is not a registered worker, trying session:window`);let m=await s(d,f);if(m){if(zt(`"${e}" -> session:window -> pane ${m.paneId}`),n&&!await i(m.paneId))throw new Error(`Session "${d}" window "${f}": pane ${m.paneId} is dead.`);return{paneId:m.paneId,session:m.session,resolvedVia:"session:window"}}throw new Error(`Target "${e}" not found. No worker "${d}" in registry and no tmux session:window "${d}:${f}" found.
-Run 'genie worker list' to list workers.`)}let u=c[e];if(u){if(zt(`"${e}" -> worker lookup -> pane ${u.paneId}, session ${u.session}`),n&&!await i(u.paneId))throw await o(e,u.paneId),new Error(`Worker ${e}: pane ${u.paneId} is dead. Run 'genie worker kill ${e}' to clean up.`);return{paneId:u.paneId,session:u.session,workerId:e,resolvedVia:"worker"}}throw new Error(`Target "${e}" not found. Not a worker or pane ID.
-Run 'genie worker list' to list workers.`)}function Da(e,t){let n=[];e.workerId?(n.push(e.workerId),e.paneIndex!==void 0&&e.paneIndex>0&&(n[n.length-1]+=`:${e.paneIndex}`)):n.push(t);let r=[`pane ${e.paneId}`];return e.session&&r.push(`session ${e.session}`),`${n[0]} (${r.join(", ")})`}async function Fh(e,t){if(e!==void 0)return e;try{let r=await(await Promise.resolve().then(()=>(ot(),it))).list(),s={};for(let i of r)s[i.id]=i;return s}catch{return{}}}function A8(e,t){if(t===0)return e.paneId;let n=t-1;return!e.subPanes||n>=e.subPanes.length||n<0?null:e.subPanes[n]}ue();async function Oh(e,t,n={}){try{let r=await xs(e),s=r.paneId,i=Da(r,e),o=Oi(),a=n.timeout??o.execTimeout,{output:c,exitCode:l}=await lu(s,t,a);c&&!n.quiet&&console.log(c),n.quiet||console.error(`Executed in ${i}`),process.exit(l)}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}ot();function va(e){if(typeof e=="string")return e;if(Array.isArray(e)){let t=[];for(let n of e)typeof n=="string"?t.push(n):n&&typeof n=="object"&&"text"in n&&t.push(String(n.text));return t.join(" ")}return""}function Bh(e){try{return new Date(e).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:!1})}catch{return"??:??"}}function Di(e,t){return e.length<=t?e:`${e.slice(0,t-3)}...`}function wi(e){let t=e.replace(/^\/home\/\w+\/workspace\//,"~/").replace(/^\/home\/\w+\//,"~/");return Di(t,50)}function I8(e){let t=[],n=[],r=[],s="",i="",o=()=>{n.length>0&&(t.push({timestamp:s,type:"read",summary:n.length===1?`Read: ${wi(n[0])}`:`Read ${n.length} files`,details:n.length>1?n.map(wi):void 0}),n.length=0)},a=()=>{r.length>0&&(t.push({timestamp:i,type:"edit",summary:r.length===1?`Edit: ${wi(r[0])}`:`Edit ${r.length} files`,details:r.length>1?r.map(wi):void 0}),r.length=0)};for(let c of e){if(c.type==="user"&&c.message){o(),a();let l=va(c.message.content);l&&t.push({timestamp:c.timestamp,type:"prompt",summary:Di(l.replace(/\n/g," "),80)})}if(c.type==="assistant"&&c.toolCalls)for(let l of c.toolCalls){let u=l.input;switch(l.name){case"Read":s=c.timestamp,u.file_path&&n.push(String(u.file_path));break;case"Edit":o(),i=c.timestamp,u.file_path&&r.push(String(u.file_path));break;case"Write":o(),a(),t.push({timestamp:c.timestamp,type:"write",summary:`Write: ${wi(String(u.file_path||"unknown"))}`});break;case"Bash":{o(),a();let d=String(u.command||"").replace(/\n/g," ");t.push({timestamp:c.timestamp,type:"bash",summary:`Bash: ${Di(d,60)}`});break}case"AskUserQuestion":{o(),a();let f=u.questions?.[0]?.question||"question";t.push({timestamp:c.timestamp,type:"question",summary:`Question: ${Di(f,60)}`});break}}}if(c.type==="assistant"&&c.message&&!c.toolCalls){o(),a();let l=va(c.message.content);l&&l.length>100&&t.push({timestamp:c.timestamp,type:"response",summary:Di(l.replace(/\n/g," "),80)})}}return o(),a(),t}function T8(e){if(e.length===0)return"unknown";let t=e.slice(-10);for(let r of t.reverse()){if(r.type==="assistant"&&r.toolCalls&&r.toolCalls[r.toolCalls.length-1].name==="AskUserQuestion")return"question";if(r.type==="progress"&&r.data&&r.data.type==="permission_request")return"permission"}let n=e[e.length-1];return n.type==="user"?"working":n.type==="assistant"?"idle":"unknown"}function F8(e,t){let n=[];n.push(""),n.push(`Session: ${t.workerId}${t.branch?` (${t.branch})`:""} | ${t.duration} | ${t.totalLines} lines \u2192 ${t.compressedLines} lines`),n.push("");for(let r of e){let s=Bh(r.timestamp),i=O8(r.type);if(n.push(`[${s}] ${i} ${r.summary}`),r.details&&r.details.length>0){for(let o of r.details.slice(0,5))n.push(` ${o}`);r.details.length>5&&n.push(` ... and ${r.details.length-5} more`)}r.result&&n.push(` \u2192 ${r.result}`)}return n.push(""),n.push(`Status: ${t.status.toUpperCase()} | ${t.exchanges} exchanges | ${t.toolCalls} tool calls | ${t.compressionRatio.toFixed(0)}x compression`),n.push(""),n.join(`
+Commands:`),console.log(" genie agent list - Check worker status"),console.log(" genie agent approve - Approve permissions"),console.log(` genie agent close ${o} - Close issue when done`),console.log(` genie agent kill ${o} - Force kill worker`),ct&&!t._skipAutoApproveBlock&&await vh(ct)}catch(n){console.error(`\u274C Error: ${n.message}`),process.exit(1)}}var wa=require("node:fs"),Ih=require("node:path");var Cn=null,g0=ah();var g8="auto-approve-audit.jsonl";function h8(e){let t=(0,Ih.join)(e,".genie",g8);if(!(0,wa.existsSync)(t))return[];try{let r=(0,wa.readFileSync)(t,"utf-8").trim().split(`
+`).filter(i=>i.trim()),s=[];for(let i of r)try{s.push(JSON.parse(i))}catch{}return s}catch{return[]}}function y8(e){switch(e){case"approve":return"approved";case"deny":return"denied";case"escalate":return"escalated"}}function b8(e){let t=[],n=h8(e.auditDir);for(let s of n)t.push({requestId:s.requestId,toolName:s.toolName,paneId:s.paneId,status:y8(s.action),reason:s.reason,timestamp:s.timestamp});let r=e.queue.getAll();for(let s of r)t.push({requestId:s.id,toolName:s.toolName,paneId:s.paneId,status:"pending",reason:"",timestamp:s.timestamp});return t}function w8(e,t){return t.queue.get(e)?(t.queue.remove(e),!0):!1}function D8(e,t){return t.queue.get(e)?(t.queue.remove(e),!0):!1}function Ph(){return Cn!==null&&Cn.isRunning()}async function v8(e){if(Cn?.isRunning())return;let t=await a0(e.repoPath);Cn=ya({config:t,auditDir:e.auditDir,sendApproval:ha}),Cn.start()}function k8(){Cn&&(Cn.stop(),Cn=null)}async function Th(e,t){let n=process.cwd(),r=n;if(t.status){let s=b8({auditDir:r,queue:g0});if(s.length===0){console.log("No auto-approve requests found.");return}console.log("Auto-Approve Requests:"),console.log("");for(let i of s){let o=i.status.toUpperCase().padEnd(10),a=i.paneId??"N/A";console.log(` [${o}] ${i.requestId} ${i.toolName} pane:${a} ${i.timestamp}`),i.reason&&console.log(` Reason: ${i.reason}`)}return}if(t.start){if(Ph()){console.log("Auto-approve engine is already running.");return}await v8({auditDir:r,repoPath:n}),console.log("Auto-approve engine started.");return}if(t.stop){if(!Ph()){console.log("Auto-approve engine is not running.");return}k8(),console.log("Auto-approve engine stopped.");return}if(t.deny){D8(t.deny,{queue:g0})?console.log(`Denied request: ${t.deny}`):(console.error(`Request "${t.deny}" not found in pending queue.`),process.exit(1));return}if(e){w8(e,{queue:g0})?console.log(`Approved request: ${e}`):(console.error(`Request "${e}" not found in pending queue.`),process.exit(1));return}console.log("Usage:"),console.log(" genie agent approve --status Show pending/approved/denied requests"),console.log(" genie agent approve Manually approve a pending request"),console.log(" genie agent approve --deny Manually deny a pending request"),console.log(" genie agent approve --start Start the auto-approve engine"),console.log(" genie agent approve --stop Stop the auto-approve engine")}function zt(e){process.env.DEBUG&&console.error(`[target-resolver] ${e}`)}async function S8(e,t){try{let n=await Promise.resolve().then(()=>(ue(),Ce)),r=await n.findSessionByName(e);if(!r)return null;let s=await n.listWindows(r.id);if(!s||s.length===0)return null;let i;if(t){if(i=s.find(c=>c.name===t),!i)return null}else i=s.find(c=>c.active)||s[0];let o=await n.listPanes(i.id);return!o||o.length===0?null:{paneId:(o.find(c=>c.active)||o[0]).id,session:e}}catch{return null}}async function _8(e){try{return(await(await Promise.resolve().then(()=>(ue(),Ce))).executeTmux(`display-message -p -t '${e}' '#{pane_id}'`)).trim()===e}catch{return!1}}async function E8(e,t){try{await(await Promise.resolve().then(()=>(ot(),it))).removeSubPane(e,t)}catch{}}async function $8(e){try{return(await(await Promise.resolve().then(()=>(ue(),Ce))).executeTmux(`display-message -p -t '${e}' '#{session_name}'`)).trim()||null}catch{return null}}async function xs(e,t={}){let{checkLiveness:n=!1,workers:r,tmuxLookup:s=S8,isPaneLive:i=_8,cleanupDeadPane:o=E8,deriveSession:a=$8}=t;if(zt(`resolving "${e}"`),e.startsWith("%")){if(zt(`"${e}" -> raw pane ID passthrough`),n&&!await i(e))throw new Error(`Pane ${e} is dead or does not exist. Check with: tmux list-panes -a`);let d=await a(e);return zt(`"${e}" -> derived session: ${d||"(none)"}`),{paneId:e,session:d??void 0,resolvedVia:"raw"}}if(e.startsWith("@")){zt(`"${e}" -> window ID lookup`);let d=await Fh(r,t.registryPath),f=e,p=Object.values(d).find(m=>m.windowId===f);if(p){if(zt(`"${e}" -> found worker "${p.id}" with pane ${p.paneId}`),n&&!await i(p.paneId))throw new Error(`Window ${e}: worker ${p.id} pane ${p.paneId} is dead. Run 'genie agent kill ${p.id}' to clean up.`);return{paneId:p.paneId,session:p.session,workerId:p.id,resolvedVia:"worker"}}throw new Error(`Window "${e}" not found in worker registry.
+Run 'genie agent list' to list workers.`)}let c=await Fh(r,t.registryPath),l=e.indexOf(":");if(l!==-1){let d=e.substring(0,l),f=e.substring(l+1),p=c[d];if(p){let x=Number.parseInt(f,10);if(Number.isNaN(x)||x<0)throw new Error(`Invalid sub-pane index "${f}" for worker "${d}". Use a non-negative integer (0 = primary, 1+ = sub-panes).`);let g=A8(p,x);if(!g){let h=p.subPanes?p.subPanes.length:0;throw new Error(`Worker "${d}" has no sub-pane index ${x}. Available: 0 (primary)${h>0?`, 1-${h} (sub-panes)`:""}. Sub-pane index ${x} does not exist.`)}if(zt(`"${e}" -> worker "${d}" pane index ${x} -> ${g}`),n&&!await i(g))throw await o(d,g),new Error(`Worker ${d}: pane ${g} is dead. Run 'genie agent kill ${d}' to clean up.`);return{paneId:g,session:p.session,workerId:d,paneIndex:x,resolvedVia:"worker"}}zt(`"${d}" is not a registered worker, trying session:window`);let m=await s(d,f);if(m){if(zt(`"${e}" -> session:window -> pane ${m.paneId}`),n&&!await i(m.paneId))throw new Error(`Session "${d}" window "${f}": pane ${m.paneId} is dead.`);return{paneId:m.paneId,session:m.session,resolvedVia:"session:window"}}throw new Error(`Target "${e}" not found. No worker "${d}" in registry and no tmux session:window "${d}:${f}" found.
+Run 'genie agent list' to list workers.`)}let u=c[e];if(u){if(zt(`"${e}" -> worker lookup -> pane ${u.paneId}, session ${u.session}`),n&&!await i(u.paneId))throw await o(e,u.paneId),new Error(`Worker ${e}: pane ${u.paneId} is dead. Run 'genie agent kill ${e}' to clean up.`);return{paneId:u.paneId,session:u.session,workerId:e,resolvedVia:"worker"}}throw new Error(`Target "${e}" not found. Not a worker or pane ID.
+Run 'genie agent list' to list workers.`)}function Da(e,t){let n=[];e.workerId?(n.push(e.workerId),e.paneIndex!==void 0&&e.paneIndex>0&&(n[n.length-1]+=`:${e.paneIndex}`)):n.push(t);let r=[`pane ${e.paneId}`];return e.session&&r.push(`session ${e.session}`),`${n[0]} (${r.join(", ")})`}async function Fh(e,t){if(e!==void 0)return e;try{let r=await(await Promise.resolve().then(()=>(ot(),it))).list(),s={};for(let i of r)s[i.id]=i;return s}catch{return{}}}function A8(e,t){if(t===0)return e.paneId;let n=t-1;return!e.subPanes||n>=e.subPanes.length||n<0?null:e.subPanes[n]}ue();async function Oh(e,t,n={}){try{let r=await xs(e),s=r.paneId,i=Da(r,e),o=Oi(),a=n.timeout??o.execTimeout,{output:c,exitCode:l}=await lu(s,t,a);c&&!n.quiet&&console.log(c),n.quiet||console.error(`Executed in ${i}`),process.exit(l)}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}ot();function va(e){if(typeof e=="string")return e;if(Array.isArray(e)){let t=[];for(let n of e)typeof n=="string"?t.push(n):n&&typeof n=="object"&&"text"in n&&t.push(String(n.text));return t.join(" ")}return""}function Bh(e){try{return new Date(e).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:!1})}catch{return"??:??"}}function Di(e,t){return e.length<=t?e:`${e.slice(0,t-3)}...`}function wi(e){let t=e.replace(/^\/home\/\w+\/workspace\//,"~/").replace(/^\/home\/\w+\//,"~/");return Di(t,50)}function I8(e){let t=[],n=[],r=[],s="",i="",o=()=>{n.length>0&&(t.push({timestamp:s,type:"read",summary:n.length===1?`Read: ${wi(n[0])}`:`Read ${n.length} files`,details:n.length>1?n.map(wi):void 0}),n.length=0)},a=()=>{r.length>0&&(t.push({timestamp:i,type:"edit",summary:r.length===1?`Edit: ${wi(r[0])}`:`Edit ${r.length} files`,details:r.length>1?r.map(wi):void 0}),r.length=0)};for(let c of e){if(c.type==="user"&&c.message){o(),a();let l=va(c.message.content);l&&t.push({timestamp:c.timestamp,type:"prompt",summary:Di(l.replace(/\n/g," "),80)})}if(c.type==="assistant"&&c.toolCalls)for(let l of c.toolCalls){let u=l.input;switch(l.name){case"Read":s=c.timestamp,u.file_path&&n.push(String(u.file_path));break;case"Edit":o(),i=c.timestamp,u.file_path&&r.push(String(u.file_path));break;case"Write":o(),a(),t.push({timestamp:c.timestamp,type:"write",summary:`Write: ${wi(String(u.file_path||"unknown"))}`});break;case"Bash":{o(),a();let d=String(u.command||"").replace(/\n/g," ");t.push({timestamp:c.timestamp,type:"bash",summary:`Bash: ${Di(d,60)}`});break}case"AskUserQuestion":{o(),a();let f=u.questions?.[0]?.question||"question";t.push({timestamp:c.timestamp,type:"question",summary:`Question: ${Di(f,60)}`});break}}}if(c.type==="assistant"&&c.message&&!c.toolCalls){o(),a();let l=va(c.message.content);l&&l.length>100&&t.push({timestamp:c.timestamp,type:"response",summary:Di(l.replace(/\n/g," "),80)})}}return o(),a(),t}function T8(e){if(e.length===0)return"unknown";let t=e.slice(-10);for(let r of t.reverse()){if(r.type==="assistant"&&r.toolCalls&&r.toolCalls[r.toolCalls.length-1].name==="AskUserQuestion")return"question";if(r.type==="progress"&&r.data&&r.data.type==="permission_request")return"permission"}let n=e[e.length-1];return n.type==="user"?"working":n.type==="assistant"?"idle":"unknown"}function F8(e,t){let n=[];n.push(""),n.push(`Session: ${t.workerId}${t.branch?` (${t.branch})`:""} | ${t.duration} | ${t.totalLines} lines \u2192 ${t.compressedLines} lines`),n.push("");for(let r of e){let s=Bh(r.timestamp),i=O8(r.type);if(n.push(`[${s}] ${i} ${r.summary}`),r.details&&r.details.length>0){for(let o of r.details.slice(0,5))n.push(` ${o}`);r.details.length>5&&n.push(` ... and ${r.details.length-5} more`)}r.result&&n.push(` \u2192 ${r.result}`)}return n.push(""),n.push(`Status: ${t.status.toUpperCase()} | ${t.exchanges} exchanges | ${t.toolCalls} tool calls | ${t.compressionRatio.toFixed(0)}x compression`),n.push(""),n.join(`
`)}function O8(e){switch(e){case"prompt":return"\u{1F4AC}";case"read":return"\u{1F4D6}";case"edit":return"\u270F\uFE0F";case"write":return"\u{1F4DD}";case"bash":return"\u26A1";case"question":return"\u2753";case"answer":return"\u2705";case"permission":return"\u{1F510}";case"thinking":return"\u{1F914}";case"response":return"\u{1F4AD}";default:return"\u2022"}}function B8(e){let t=[];for(let n of e){let r=Bh(n.timestamp);if(n.type==="user"&&n.message){let s=va(n.message.content);t.push(`
[${r}] USER:`),t.push(s)}if(n.type==="assistant"){if(n.toolCalls&&n.toolCalls.length>0){t.push(`
[${r}] ASSISTANT (tools):`);for(let s of n.toolCalls){let i=s.input;s.name==="Bash"?t.push(` ${s.name}: ${i.command}`):s.name==="Read"||s.name==="Edit"||s.name==="Write"?t.push(` ${s.name}: ${i.file_path}`):t.push(` ${s.name}`)}}else if(n.message){let s=va(n.message.content);s&&(t.push(`
[${r}] ASSISTANT:`),t.push(s.slice(0,500)+(s.length>500?"...":"")))}}}return t.join(`
-`)}async function R8(e){let t=await At(e);return t||(t=await hn(e),t)?t:(await Q()).find(s=>s.id.includes(e)||s.taskId?.includes(e)||s.taskTitle?.toLowerCase().includes(e.toLowerCase()))||null}async function Rh(e,t){let n,r,s,i;if(t.logFile)n=t.logFile,r="direct",i="N/A";else{let m=await R8(e);m||(console.error(`\u274C Worker "${e}" not found.`),console.error(" Run `genie worker list` to see active workers."),process.exit(1));let x=m.worktree||m.repoPath,g=await na(x);g||(console.error(`\u274C No Claude logs found for worker "${m.id}"`),console.error(` Workspace: ${x}`),process.exit(1)),n=g.logPath,r=m.id,s=m.worktree?`work/${m.taskId}`:void 0,i=fr(m).formatted}let o=await ta(n);o.length===0&&(console.error(`\u274C Log file is empty: ${n}`),process.exit(1));let a=o.filter(m=>m.type==="user"||m.type==="assistant"),c=a;if(t.since&&t.since>0){let m=0,x=a.length;for(let g=a.length-1;g>=0;g--)if(a[g].type==="user"&&(m++,m>=t.since)){x=g;break}c=a.slice(x)}let l=o.reduce((m,x)=>m+(x.toolCalls?.length||0),0),u=o.filter(m=>m.type==="user").length;if(t.raw){for(let m of c)console.log(JSON.stringify(m.raw));return}if(t.full){let m=B8(c);console.log(m);return}let d=I8(c),f={workerId:r,taskId:r,branch:s,duration:i,totalLines:o.length,compressedLines:d.length,compressionRatio:o.length/Math.max(d.length,1),exchanges:u,toolCalls:l,status:T8(o)};if(t.json){console.log(JSON.stringify({stats:f,events:d},null,2));return}let p=F8(d,f);console.log(p)}ue();async function N8(e){let t=await xs(e);return{paneId:t.paneId,session:t.session||e,label:Da(t,e)}}async function Mh(e,t){try{let{paneId:n,label:r}=await N8(e),s=await me(n,50),i=$t(s);if(i.type!=="question"){console.log(`No question pending (state: ${i.type})`);return}if(t.startsWith("text:")){let o=t.slice(5);await P(`send-keys -t '${n}' End`),await gs(100),await P(`send-keys -t '${n}' Enter`),await gs(100),await P(`send-keys -t '${n}' ${L8(o)}`),await gs(100),await P(`send-keys -t '${n}' Enter`),console.log(`Sent feedback: "${o.substring(0,50)}${o.length>50?"...":""}"`)}else if(/^\d+$/.test(t)){let o=Number.parseInt(t,10),c=mn(s).split(`
+`)}async function R8(e){let t=await At(e);return t||(t=await hn(e),t)?t:(await Q()).find(s=>s.id.includes(e)||s.taskId?.includes(e)||s.taskTitle?.toLowerCase().includes(e.toLowerCase()))||null}async function Rh(e,t){let n,r,s,i;if(t.logFile)n=t.logFile,r="direct",i="N/A";else{let m=await R8(e);m||(console.error(`\u274C Worker "${e}" not found.`),console.error(" Run `genie agent list` to see active workers."),process.exit(1));let x=m.worktree||m.repoPath,g=await na(x);g||(console.error(`\u274C No Claude logs found for worker "${m.id}"`),console.error(` Workspace: ${x}`),process.exit(1)),n=g.logPath,r=m.id,s=m.worktree?`work/${m.taskId}`:void 0,i=fr(m).formatted}let o=await ta(n);o.length===0&&(console.error(`\u274C Log file is empty: ${n}`),process.exit(1));let a=o.filter(m=>m.type==="user"||m.type==="assistant"),c=a;if(t.since&&t.since>0){let m=0,x=a.length;for(let g=a.length-1;g>=0;g--)if(a[g].type==="user"&&(m++,m>=t.since)){x=g;break}c=a.slice(x)}let l=o.reduce((m,x)=>m+(x.toolCalls?.length||0),0),u=o.filter(m=>m.type==="user").length;if(t.raw){for(let m of c)console.log(JSON.stringify(m.raw));return}if(t.full){let m=B8(c);console.log(m);return}let d=I8(c),f={workerId:r,taskId:r,branch:s,duration:i,totalLines:o.length,compressedLines:d.length,compressionRatio:o.length/Math.max(d.length,1),exchanges:u,toolCalls:l,status:T8(o)};if(t.json){console.log(JSON.stringify({stats:f,events:d},null,2));return}let p=F8(d,f);console.log(p)}ue();async function N8(e){let t=await xs(e);return{paneId:t.paneId,session:t.session||e,label:Da(t,e)}}async function Mh(e,t){try{let{paneId:n,label:r}=await N8(e),s=await me(n,50),i=$t(s);if(i.type!=="question"){console.log(`No question pending (state: ${i.type})`);return}if(t.startsWith("text:")){let o=t.slice(5);await P(`send-keys -t '${n}' End`),await gs(100),await P(`send-keys -t '${n}' Enter`),await gs(100),await P(`send-keys -t '${n}' ${L8(o)}`),await gs(100),await P(`send-keys -t '${n}' Enter`),console.log(`Sent feedback: "${o.substring(0,50)}${o.length>50?"...":""}"`)}else if(/^\d+$/.test(t)){let o=Number.parseInt(t,10),c=mn(s).split(`
`),l=1;for(let d=0;d0)for(let d=0;dsetTimeout(t,e))}ue();function vi(e){let n=e.split(`
`).filter(r=>{let s=r.trim();return!(s.match(/^TMUX_MCP_START$/)||s.match(/^TMUX_MCP_DONE_\d+$/)||s.includes("TMUX_MCP_START")||s.includes("TMUX_MCP_DONE_")||r.includes('echo "TMUX_MCP_START"')||r.includes('echo "TMUX_MCP_DONE_')||r.includes("-bash:")||r.includes("warning: setlocale:")||r.includes("cannot change locale")||s==="or directory")});for(;n.length>0&&n[0].trim()==="";)n.shift();for(;n.length>0&&n[n.length-1].trim()==="";)n.pop();return n.join(`
`)}async function Nh(e,t={}){let n=await ke(e);if(!n)throw new Error(`Session "${e}" not found`);let r;if(t.pane)r=t.pane.startsWith("%")?t.pane:`%${t.pane}`;else{let o=await dt(n.id);if(!o||o.length===0)throw new Error(`No windows found in session "${e}"`);let a=o.find(u=>u.active)||o[0],c=await St(a.id);if(!c||c.length===0)throw new Error(`No panes found in session "${e}"`);r=(c.find(u=>u.active)||c[0]).id}if(t.range){let o=t.range.split(":");o.length===2&&(t.from=Number.parseInt(o[0],10),t.to=Number.parseInt(o[1],10))}if(t.all){let o=await me(r,1e4);return vi(o)}if(t.from!==void 0&&t.to!==void 0){let o=await me(r,1e4),l=vi(o).split(`
@@ -530,8 +530,8 @@ server.listen(PORT, '127.0.0.1', () => {
process.on('SIGTERM', () => { server.close(); process.exit(0); });
process.on('SIGINT', () => { server.close(); process.exit(0); });
`,{mode:420}),t("node",[f],{detached:!0,stdio:"ignore"}).unref();for(let y=0;y<30;y++)if(await new Promise(S=>setTimeout(S,100)),o(d))try{let S=Number.parseInt(s(d,"utf-8").trim());if(S>0)return process.kill(S,0),!0}catch{}return!1}catch{return!1}}function H8(e){return e==="working"?"working":e==="idle"?"idle":e==="\u26A0\uFE0F perm"?"permission":e==="\u26A0\uFE0F question"?"question":e==="\u274C error"?"error":e==="\u2705 done"?"done":null}async function b0(e){try{let t=e.repoPath??process.cwd(),n=await Go(t,e.id);if(n.length===0)return null;let r=n[n.length-1],s=Date.now()-new Date(r.createdAt).getTime(),i=Math.floor(s/6e4);return i<1?"<1m ago":i<60?`${i}m ago`:`${Math.floor(i/60)}h ago`}catch{return null}}async function z8(e,t){let n=t?`${e}-${t}`:e;if(!(await Q()).some(i=>i.id===n))return n;let s=crypto.randomUUID().slice(0,8);return`${n}-${s}`}function Hh(e){let t=e.command("worker").description("Worker lifecycle (spawn, list, kill, dashboard)");t.command("spawn").description("Spawn a new worker with provider selection").option("--provider ","Provider: claude or codex","claude").option("--team ","Team name",process.env.GENIE_TEAM??"genie").requiredOption("--role ","Worker role (e.g., implementor, tester)").option("--skill ","Skill to load (optional)").option("--layout ","Layout mode: mosaic (default) or vertical").option("--color ","Teammate pane border color").option("--plan-mode","Start teammate in plan mode").option("--permission-mode ","Permission mode (e.g., acceptEdits)").option("--extra-args ","Extra CLI args forwarded to provider").action(async n=>{try{let r=n.team||await Oo();r||(console.error("Error: --team is required (or set GENIE_TEAM, or run inside a genie tui session)"),process.exit(1));let s={provider:n.provider,team:r,role:n.role,skill:n.skill,extraArgs:n.extraArgs},i=process.cwd(),a=(await di(i,r))?.nativeTeamParentSessionId;a||(a=await zr()??crypto.randomUUID()),await nr(r,`Genie team: ${r}`,a);let c=n.color??await To(r);n.provider==="claude"&&(s.nativeTeam={enabled:!0,parentSessionId:a,color:c,agentType:n.role??"general-purpose",planModeRequired:n.planMode,permissionMode:n.permissionMode,agentName:n.role});let u=s.provider==="claude"?crypto.randomUUID():void 0;u&&(s.sessionId=u);let d=zs(s),f=_o(d),p=Qo(n.layout),m=await z8(d.team,d.role),{execSync:x}=require("node:child_process"),g=!!process.env.TMUX,h=d.nativeTeam,y=new Date().toISOString(),S=g?"tmux":"inline",E=h?.agentName??d.role??"worker",C=!1;!h?.enabled&&d.provider==="codex"&&g&&(Us(),C=await G8(d.team));let B=f.command;f.env&&Object.keys(f.env).length>0&&(B=`env ${Object.entries(f.env).map(([T,H])=>`${T}=${H}`).join(" ")} ${f.command}`);let O=async D=>{let T={id:m,paneId:D,session:"genie",provider:d.provider,transport:S,role:d.role,skill:d.skill,team:d.team,worktree:null,startedAt:y,state:"spawning",lastStateChange:y,repoPath:process.cwd(),claudeSessionId:u,nativeTeamEnabled:h?.enabled??!1,nativeAgentId:`${E}@${d.team}`,nativeColor:h?.color??c,parentSessionId:h?.parentSessionId??a};return await ur(T),T},K=async D=>{await rr(d.team,{agentName:E,agentType:h?.agentType??d.role??"general-purpose",color:h?.color??c??"blue",tmuxPaneId:D,cwd:process.cwd(),planModeRequired:h?.planModeRequired}),await sr(d.team,"team-lead",{from:E,text:`Worker ${E} (${d.provider}) joined team ${d.team}. cwd: ${process.cwd()}. Ready for tasks.`,summary:`${E} (${d.provider}) joined`,timestamp:new Date().toISOString(),color:h?.color??c??"blue",read:!1})},_;if(g){try{_=x(`tmux split-window -d -P -F '#{pane_id}' ${B}`,{encoding:"utf-8"}).trim()}catch(T){console.error(`Failed to create tmux pane: ${T?.message??"unknown error"}`),process.exit(1)}try{x(`tmux ${Zo("genie:0",p)}`,{stdio:"ignore"})}catch{}let D=await O(_);if(await K(_),await ui({id:d.role??m,provider:d.provider,team:d.team,role:d.role,skill:d.skill,cwd:process.cwd(),extraArgs:n.extraArgs,nativeTeamEnabled:D.nativeTeamEnabled,lastSpawnedAt:new Date().toISOString(),lastSessionId:D.claudeSessionId}),C&&_!=="%0"){let{writeFileSync:T}=require("node:fs"),{join:H}=require("node:path"),{homedir:ee}=require("node:os"),Ae=H(ee(),".genie","relay");T(H(Ae,`${m}-pane`),_),T(H(Ae,`${m}-meta`),JSON.stringify({agent:E,color:c}))}console.log(`Worker "${m}" spawned.`),console.log(` Provider: ${f.provider}`),console.log(` Command: ${B}`),console.log(` Team: ${d.team}`),console.log(` Pane: ${_}`),d.role&&console.log(` Role: ${d.role}`),d.skill&&console.log(` Skill: ${d.skill}`),D.claudeSessionId&&console.log(` Session: ${D.claudeSessionId}`),console.log(` Layout: ${p}`),h?.enabled&&(console.log(" Native: enabled"),console.log(` AgentID: ${D.nativeAgentId}`),console.log(` Color: ${h.color}`)),C&&console.log(` OTel: relay on port ${Vr}`)}else{_="inline";let D=await O(_);await K(_),console.log(`Worker "${m}" starting inline...`),console.log(` Provider: ${f.provider} | Team: ${d.team} | Role: ${d.role??"-"}`),h?.enabled&&console.log(` Native: enabled | AgentID: ${D.nativeAgentId}`),console.log("");let{spawnSync:T}=require("node:child_process"),H={...process.env,...f.env??{}},ee=T("sh",["-c",f.command],{env:H,stdio:"inherit"});await Me(m),h?.enabled&&E&&(await Js(d.team,E).catch(()=>{}),await Ys(d.team,E).catch(()=>{})),console.log(`
-Worker "${m}" session ended.`),process.exit(ee.status??0)}}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("list").alias("ls").description("List all workers with provider metadata").option("--json","Output as JSON").option("--prune","Auto-remove dead workers from registry").action(async n=>{try{let r=await Q(),s=[],i=[];for(let a of r){if(a.state==="suspended"){let u=a.suspendedAt?Date.now()-new Date(a.suspendedAt).getTime():1/0;if(n.prune&&u>864e5){if(a.team&&a.nativeAgentId){let d=a.nativeAgentId.split("@")[0];await Js(a.team,d).catch(()=>{}),await Ys(a.team,d).catch(()=>{})}a.role&&await ns(a.role).catch(()=>{}),await ns(a.id).catch(()=>{}),await Me(a.id),i.push(a.id)}else{let d=await b0(a);s.push({worker:a,liveState:"\u{1F4A4} suspended",lastMsg:d,dead:!1})}continue}if(await ar(a.paneId)){let l=await V8(a.paneId),u=H8(l);u&&u!==a.state&&await dr(a.id,u);let d=await b0(a);s.push({worker:a,liveState:l,lastMsg:d,dead:!1})}else if(n.prune){if(a.team&&a.nativeAgentId){let l=a.nativeAgentId.split("@")[0];await Js(a.team,l).catch(()=>{}),await Ys(a.team,l).catch(()=>{})}await Me(a.id),i.push(a.id)}else{let l=await b0(a);s.push({worker:a,liveState:"\u{1F480} dead",lastMsg:l,dead:!0})}}if(n.json){let a=s.map(({worker:c,liveState:l,lastMsg:u})=>({id:c.id,provider:c.provider,transport:c.transport,team:c.team,role:c.role,state:l,elapsed:fr(c).formatted,lastMessage:u??null}));i.length>0&&a.push(...i.map(c=>({id:c,state:"dead (pruned)"}))),console.log(JSON.stringify(a,null,2));return}if(s.length===0&&i.length===0){console.log("No workers found."),console.log(" Spawn one: genie worker spawn --role implementor");return}console.log(""),console.log("WORKERS"),console.log("-".repeat(90)),console.log(`${"ID".padEnd(20)}${"PROVIDER".padEnd(10)}${"TEAM".padEnd(10)}${"ROLE".padEnd(14)}${"STATE".padEnd(16)}${"TIME".padEnd(8)}LAST MSG`),console.log("-".repeat(90));for(let{worker:a,liveState:c,lastMsg:l}of s){let u=a.id.padEnd(20).substring(0,20),d=(a.provider||"-").padEnd(10),f=(a.team||"-").padEnd(10).substring(0,10),p=(a.role||"-").padEnd(14).substring(0,14),m=c.padEnd(16).substring(0,16),x=fr(a).formatted.padEnd(8);console.log(`${u}${d}${f}${p}${m}${x}${l??"-"}`)}i.length>0&&(console.log(""),console.log(`Pruned ${i.length} dead worker(s): ${i.join(", ")}`));let o=s.filter(a=>a.dead).length;o>0&&!n.prune&&console.log(`
-${o} dead worker(s). Use --prune to remove.`),console.log("")}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("kill ").description("Force kill a worker").option("-y, --yes","Skip confirmation").action(async(n,r)=>{try{let s=await At(n);s||(console.error(`Worker "${n}" not found.`),process.exit(1));try{let{execSync:i}=require("node:child_process"),o=i("tmux display-message -p '#{pane_id}'",{encoding:"utf-8"}).trim();s.paneId&&/^(%\d+|inline)$/.test(s.paneId)&&s.paneId!==o?i(`tmux kill-pane -t ${s.paneId}`,{stdio:"ignore"}):s.paneId===o&&console.log(" (skipped pane kill \u2014 would kill current session)")}catch{}try{let{join:i}=require("node:path"),{homedir:o}=require("node:os"),{unlinkSync:a}=require("node:fs"),c=i(o(),".genie","relay");for(let l of["-pane","-meta"])try{a(i(c,`${n}${l}`))}catch{}}catch{}if(s.team&&s.nativeAgentId)try{let i=s.nativeAgentId.split("@")[0];await Js(s.team,i),await Ys(s.team,i)}catch{}await Me(n),s.role&&await ns(s.role).catch(()=>{}),await ns(n).catch(()=>{}),console.log(`Worker "${n}" killed and unregistered.`)}catch(s){console.error(`Error: ${s.message}`),process.exit(1)}}),t.command("suspend ").description("Suspend a worker (kill pane, preserve session for resume)").action(async n=>{try{let r=await At(n);if(r||(console.error(`Worker "${n}" not found.`),process.exit(1)),r.state==="suspended"){console.log(`Worker "${n}" is already suspended.`);return}let{suspendWorker:s}=await Promise.resolve().then(()=>(y0(),h0));await s(n)?(console.log(`Worker "${n}" suspended.`),r.claudeSessionId&&console.log(` Session preserved: ${r.claudeSessionId}`),console.log(` Send a message to auto-resume: genie msg send ${n} "your message"`)):(console.error(`Failed to suspend worker "${n}".`),process.exit(1))}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("watchdog").description("Start idle timeout watchdog (suspends idle workers)").option("--once","Run a single check and exit").action(async n=>{try{let{checkIdleWorkers:r,runWatchdogLoop:s,getIdleTimeoutMs:i}=await Promise.resolve().then(()=>(y0(),h0)),o=i();if(o===0){console.log("Idle timeout is disabled (GENIE_IDLE_TIMEOUT_MS=0).");return}if(console.log(`Idle timeout: ${Math.round(o/6e4)}m`),n.once){let a=await r();a.length>0?console.log(`Suspended ${a.length} worker(s): ${a.join(", ")}`):console.log("No idle workers to suspend.");return}console.log("Starting watchdog loop (Ctrl+C to stop)..."),await s()}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("dashboard").description("Live status of all workers with provider metadata").option("--json","Output as JSON").option("-w, --watch","Auto-refresh every 2 seconds").action(async n=>{try{let r=await Q();if(n.json){let s={total:r.length,byProvider:{claude:r.filter(i=>i.provider==="claude").length,codex:r.filter(i=>i.provider==="codex").length},byState:{spawning:r.filter(i=>i.state==="spawning").length,working:r.filter(i=>i.state==="working").length,idle:r.filter(i=>i.state==="idle").length,done:r.filter(i=>i.state==="done").length,suspended:r.filter(i=>i.state==="suspended").length}};console.log(JSON.stringify({summary:s,workers:r.map(i=>({id:i.id,provider:i.provider,team:i.team,role:i.role,skill:i.skill,state:i.state,paneId:i.paneId,transport:i.transport}))},null,2));return}if(console.log(""),console.log("WORKER DASHBOARD"),console.log("=".repeat(80)),console.log(`Workers: ${r.length}`),console.log(` Claude: ${r.filter(s=>s.provider==="claude").length}`),console.log(` Codex: ${r.filter(s=>s.provider==="codex").length}`),console.log(""),r.length===0){console.log("No active workers.");return}for(let s of r){let i=fr(s).formatted;console.log(` [${s.provider||"claude"}] ${s.id} (${s.team||"default"}/${s.role||"default"}) \u2014 ${s.state} \u2014 ${i}`),s.skill&&console.log(` Skill: ${s.skill}`),console.log(` Pane: ${s.paneId} | Session: ${s.session} | Transport: ${s.transport||"tmux"}`)}console.log(""),n.watch&&console.log("Watch mode: would auto-refresh every 2s (tmux required)")}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("approve [request-id]").description("Auto-approve engine management and manual approval").option("--status","Show pending/approved/denied requests").option("--deny ","Manually deny a pending request").option("--start","Start the auto-approve engine").option("--stop","Stop the auto-approve engine").action(async(n,r)=>{await Th(n,r)}),t.command("history ").description("Show compressed session history for a worker (catch-up)").option("--full","Show full conversation without compression").option("--since ","Show last N user/assistant exchanges",Number.parseInt).option("--json","Output as JSON").option("--raw","Output raw JSONL entries").option("--log-file ","Direct path to log file (for testing)").action(async(n,r)=>{await Rh(n,r)}),t.command("answer ").description('Answer a question for a worker (use "text:..." for text input)').action(async(n,r)=>{await Mh(n,r)}),t.command("events [pane-id]").description("Stream Claude Code events from a pane or all workers").option("--json","Output events as JSON").option("-f, --follow","Continuous tailing (like tail -f)").option("-n, --lines ","Number of recent events to show (default: 20)","20").option("--emit","Write events to .genie/events/.jsonl while tailing").option("--all","Aggregate events from all active workers").action(async(n,r)=>{await eg(n,{json:r.json,follow:r.follow,lines:r.lines?Number.parseInt(r.lines,10):void 0,emit:r.emit,all:r.all})}),t.command("close ").description("Close task/issue and cleanup worker").option("--no-sync","Skip bd sync (beads only)").option("--keep-worktree","Don't remove the worktree").option("--merge","Merge worktree changes to main branch").option("-y, --yes","Skip confirmation").action(async(n,r)=>{await ra(n,r)}),t.command("ship ").description("Mark task as done and cleanup worker").option("--keep-worktree","Don't remove the worktree").option("--merge","Merge worktree changes to main branch").option("-y, --yes","Skip confirmation").action(async(n,r)=>{await oa(n,r)}),t.command("read ").description("Read terminal output from a worker pane").option("-n, --lines ","Number of lines to read").option("--from ","Start line").option("--to ","End line").option("--range ",'Line range (e.g., "10-20")').option("--search ","Search for text").option("--grep ","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(n,r)=>{await jh(n,r)}),t.command("exec ").description("Execute command in a worker pane").option("-q, --quiet","Suppress output").option("-t, --timeout ","Timeout in milliseconds").action(async(n,r,s)=>{await Oh(n,r,s)})}var fe=new F0;fe.name("genie").description("Genie CLI - Setup and utilities for AI-assisted development").version(Km);fe.command("install").description("Verify and install prerequisites").option("--check","Only check prerequisites, do not install").option("--yes","Auto-approve all installations").action(Kp);fe.command("setup").description("Configure genie settings").option("--quick","Accept all defaults").option("--shortcuts","Only configure keyboard shortcuts").option("--claudio","Only configure Claudio integration").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 e=>{await Do(e)});fe.command("doctor").description("Run diagnostic checks on genie installation").action(Cd);fe.command("update").description("Update Genie CLI to the latest version").action(zm);fe.command("uninstall").description("Remove Genie CLI and clean up hooks").action(Vm);fe.command("tui [name]").description('Start Claude Code as native team-lead (default: "genie" in ~/workspace)').option("-r, --reset","Kill existing session and start fresh").option("-d, --dir ","Working directory (default: ~/workspace)").action(async(e,t)=>{e&&(t.name=e),await Lm(t)});var ka=fe.command("shortcuts").description("Manage tmux keyboard shortcuts");ka.action(Xl);ka.command("show").description("Show available shortcuts and installation status").action(Xl);ka.command("install").description("Install shortcuts to config files (~/.tmux.conf, shell rc)").action(um);ka.command("uninstall").description("Remove shortcuts from config files").action(dm);var hs=fe.command("profiles").description("Manage worker profiles for Claude Code spawning");hs.action(Ql);hs.command("list").description("List all configured worker profiles").action(Ql);hs.command("add ").description("Create a new worker profile interactively").action(sm);hs.command("rm ").description("Delete a worker profile").action(im);hs.command("show ").description("Show details of a worker profile").action(om);hs.command("default [name]").description("Get or set the default worker profile").action(am);var Ca=fe.command("pdf").description("Generate PDFs from markdown files");Ca.command("render ").description("Render a markdown file to PDF").option("-o, --output ","Output PDF file","output.pdf").option("-t, --theme ","Theme to use (default, minimal, corporate, dark)").option("--no-page-numbers","Disable page numbers").option("-w, --watch","Watch for changes and re-render").action(em);Ca.command("template ").description("Generate PDF from a template with JSON data").option("-d, --data ","JSON data file").option("-o, --output ","Output PDF file","output.pdf").option("-t, --theme ","Theme to use").action(tm);Ca.command("themes").description("List available themes").action(nm);Ca.command("templates").description("List available templates").action(rm);var K8=fe.command("brainstorm").description("Brainstorm utilities (file-based helpers)");K8.command("crystallize").description("Crystallize a brainstorm draft into design.md and upsert .beads/issues.jsonl").requiredOption("--slug ","Brainstorm slug (kebab-case)").option("--file ","Input draft markdown path (default: .genie/brainstorms//draft.md)").option("-r, --repo ","Repo path (default: cwd)").option("--title ","Issue title (default: slug)").option("--depends-on ","Comma-separated dependency IDs (default: hq-roadmap)").option("--status ","Issue status (open|closed)","open").action(W0);var Y8=fe.command("ledger").description("Ledger utilities (beads JSONL validation)");Y8.command("validate").description("Validate local .beads/issues.jsonl JSONL structure (scriptable)").option("-r, --repo ","Repo path (default: cwd)").option("--json","Output JSON").action(Zp);gg(fe);Hh(fe);Rx(fe);xg(fe);fe.command("work ").description('Spawn worker bound to beads issue (target: bd-id, "next", or "wish")').option("--no-worktree","Use shared repo instead of worktree").option("-s, --session ","Target tmux session").option("--focus","Focus the worker pane after spawning").option("-p, --prompt ","Custom initial prompt").option("--no-resume","Start fresh session even if previous exists").option("--skill ",'Skill to invoke (auto-detects "forge" if wish.md exists)').option("--no-auto-approve","Disable auto-approve for this worker").option("--profile ","Worker profile to use").option("-n, --name ","Custom worker name (for N workers per task)").option("-r, --role ",'Worker role (e.g., "main", "tests", "review")').option("--shared-worktree","Share worktree with existing worker on same task").action(async(e,t)=>{await Ah(e,t)});var Sa=fe.command("daemon").description("Manage beads daemon for auto-sync");Sa.command("start").description("Start beads daemon (auto-commit, auto-sync)").option("--no-auto-commit","Disable auto-commit").option("--auto-push","Enable auto-push to remote").action(async e=>{await ix(e)});Sa.command("stop").description("Stop beads daemon").action(async()=>{await ox()});Sa.command("status").description("Show daemon status").option("--json","Output as JSON").action(async e=>{await ax(e)});Sa.command("restart").description("Restart beads daemon").option("--no-auto-commit","Disable auto-commit").option("--auto-push","Enable auto-push to remote").action(async e=>{await cx(e)});fe.command("council").description("Spawn dual Claude instances for multi-model deliberation").option("-s, --session ","Target tmux session").option("--preset ","Council preset to use").option("--skill ","Skill to load on both instances").option("--no-focus","Don't focus the new window").action(async e=>{await Zm(e)});fe.parse();
+Worker "${m}" session ended.`),process.exit(ee.status??0)}}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("list").alias("ls").description("List all workers with provider metadata").option("--json","Output as JSON").option("--prune","Auto-remove dead workers from registry").action(async n=>{try{let r=await Q(),s=[],i=[];for(let a of r){if(a.state==="suspended"){let u=a.suspendedAt?Date.now()-new Date(a.suspendedAt).getTime():1/0;if(n.prune&&u>864e5){if(a.team&&a.nativeAgentId){let d=a.nativeAgentId.split("@")[0];await Js(a.team,d).catch(()=>{}),await Ys(a.team,d).catch(()=>{})}a.role&&await ns(a.role).catch(()=>{}),await ns(a.id).catch(()=>{}),await Me(a.id),i.push(a.id)}else{let d=await b0(a);s.push({worker:a,liveState:"\u{1F4A4} suspended",lastMsg:d,dead:!1})}continue}if(await ar(a.paneId)){let l=await V8(a.paneId),u=H8(l);u&&u!==a.state&&await dr(a.id,u);let d=await b0(a);s.push({worker:a,liveState:l,lastMsg:d,dead:!1})}else if(n.prune){if(a.team&&a.nativeAgentId){let l=a.nativeAgentId.split("@")[0];await Js(a.team,l).catch(()=>{}),await Ys(a.team,l).catch(()=>{})}await Me(a.id),i.push(a.id)}else{let l=await b0(a);s.push({worker:a,liveState:"\u{1F480} dead",lastMsg:l,dead:!0})}}if(n.json){let a=s.map(({worker:c,liveState:l,lastMsg:u})=>({id:c.id,provider:c.provider,transport:c.transport,team:c.team,role:c.role,state:l,elapsed:fr(c).formatted,lastMessage:u??null}));i.length>0&&a.push(...i.map(c=>({id:c,state:"dead (pruned)"}))),console.log(JSON.stringify(a,null,2));return}if(s.length===0&&i.length===0){console.log("No workers found."),console.log(" Spawn one: genie agent spawn --role implementor");return}console.log(""),console.log("WORKERS"),console.log("-".repeat(90)),console.log(`${"ID".padEnd(20)}${"PROVIDER".padEnd(10)}${"TEAM".padEnd(10)}${"ROLE".padEnd(14)}${"STATE".padEnd(16)}${"TIME".padEnd(8)}LAST MSG`),console.log("-".repeat(90));for(let{worker:a,liveState:c,lastMsg:l}of s){let u=a.id.padEnd(20).substring(0,20),d=(a.provider||"-").padEnd(10),f=(a.team||"-").padEnd(10).substring(0,10),p=(a.role||"-").padEnd(14).substring(0,14),m=c.padEnd(16).substring(0,16),x=fr(a).formatted.padEnd(8);console.log(`${u}${d}${f}${p}${m}${x}${l??"-"}`)}i.length>0&&(console.log(""),console.log(`Pruned ${i.length} dead worker(s): ${i.join(", ")}`));let o=s.filter(a=>a.dead).length;o>0&&!n.prune&&console.log(`
+${o} dead worker(s). Use --prune to remove.`),console.log("")}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("kill ").description("Force kill a worker").option("-y, --yes","Skip confirmation").action(async(n,r)=>{try{let s=await At(n);s||(console.error(`Worker "${n}" not found.`),process.exit(1));try{let{execSync:i}=require("node:child_process"),o=i("tmux display-message -p '#{pane_id}'",{encoding:"utf-8"}).trim();s.paneId&&/^(%\d+|inline)$/.test(s.paneId)&&s.paneId!==o?i(`tmux kill-pane -t ${s.paneId}`,{stdio:"ignore"}):s.paneId===o&&console.log(" (skipped pane kill \u2014 would kill current session)")}catch{}try{let{join:i}=require("node:path"),{homedir:o}=require("node:os"),{unlinkSync:a}=require("node:fs"),c=i(o(),".genie","relay");for(let l of["-pane","-meta"])try{a(i(c,`${n}${l}`))}catch{}}catch{}if(s.team&&s.nativeAgentId)try{let i=s.nativeAgentId.split("@")[0];await Js(s.team,i),await Ys(s.team,i)}catch{}await Me(n),s.role&&await ns(s.role).catch(()=>{}),await ns(n).catch(()=>{}),console.log(`Worker "${n}" killed and unregistered.`)}catch(s){console.error(`Error: ${s.message}`),process.exit(1)}}),t.command("suspend ").description("Suspend a worker (kill pane, preserve session for resume)").action(async n=>{try{let r=await At(n);if(r||(console.error(`Worker "${n}" not found.`),process.exit(1)),r.state==="suspended"){console.log(`Worker "${n}" is already suspended.`);return}let{suspendWorker:s}=await Promise.resolve().then(()=>(y0(),h0));await s(n)?(console.log(`Worker "${n}" suspended.`),r.claudeSessionId&&console.log(` Session preserved: ${r.claudeSessionId}`),console.log(` Send a message to auto-resume: genie send ${n} "your message"`)):(console.error(`Failed to suspend worker "${n}".`),process.exit(1))}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("watchdog").description("Start idle timeout watchdog (suspends idle workers)").option("--once","Run a single check and exit").action(async n=>{try{let{checkIdleWorkers:r,runWatchdogLoop:s,getIdleTimeoutMs:i}=await Promise.resolve().then(()=>(y0(),h0)),o=i();if(o===0){console.log("Idle timeout is disabled (GENIE_IDLE_TIMEOUT_MS=0).");return}if(console.log(`Idle timeout: ${Math.round(o/6e4)}m`),n.once){let a=await r();a.length>0?console.log(`Suspended ${a.length} worker(s): ${a.join(", ")}`):console.log("No idle workers to suspend.");return}console.log("Starting watchdog loop (Ctrl+C to stop)..."),await s()}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("dashboard").description("Live status of all workers with provider metadata").option("--json","Output as JSON").option("-w, --watch","Auto-refresh every 2 seconds").action(async n=>{try{let r=await Q();if(n.json){let s={total:r.length,byProvider:{claude:r.filter(i=>i.provider==="claude").length,codex:r.filter(i=>i.provider==="codex").length},byState:{spawning:r.filter(i=>i.state==="spawning").length,working:r.filter(i=>i.state==="working").length,idle:r.filter(i=>i.state==="idle").length,done:r.filter(i=>i.state==="done").length,suspended:r.filter(i=>i.state==="suspended").length}};console.log(JSON.stringify({summary:s,workers:r.map(i=>({id:i.id,provider:i.provider,team:i.team,role:i.role,skill:i.skill,state:i.state,paneId:i.paneId,transport:i.transport}))},null,2));return}if(console.log(""),console.log("WORKER DASHBOARD"),console.log("=".repeat(80)),console.log(`Workers: ${r.length}`),console.log(` Claude: ${r.filter(s=>s.provider==="claude").length}`),console.log(` Codex: ${r.filter(s=>s.provider==="codex").length}`),console.log(""),r.length===0){console.log("No active workers.");return}for(let s of r){let i=fr(s).formatted;console.log(` [${s.provider||"claude"}] ${s.id} (${s.team||"default"}/${s.role||"default"}) \u2014 ${s.state} \u2014 ${i}`),s.skill&&console.log(` Skill: ${s.skill}`),console.log(` Pane: ${s.paneId} | Session: ${s.session} | Transport: ${s.transport||"tmux"}`)}console.log(""),n.watch&&console.log("Watch mode: would auto-refresh every 2s (tmux required)")}catch(r){console.error(`Error: ${r.message}`),process.exit(1)}}),t.command("approve [request-id]").description("Auto-approve engine management and manual approval").option("--status","Show pending/approved/denied requests").option("--deny ","Manually deny a pending request").option("--start","Start the auto-approve engine").option("--stop","Stop the auto-approve engine").action(async(n,r)=>{await Th(n,r)}),t.command("history ").description("Show compressed session history for a worker (catch-up)").option("--full","Show full conversation without compression").option("--since ","Show last N user/assistant exchanges",Number.parseInt).option("--json","Output as JSON").option("--raw","Output raw JSONL entries").option("--log-file ","Direct path to log file (for testing)").action(async(n,r)=>{await Rh(n,r)}),t.command("answer ").description('Answer a question for a worker (use "text:..." for text input)').action(async(n,r)=>{await Mh(n,r)}),t.command("events [pane-id]").description("Stream Claude Code events from a pane or all workers").option("--json","Output events as JSON").option("-f, --follow","Continuous tailing (like tail -f)").option("-n, --lines ","Number of recent events to show (default: 20)","20").option("--emit","Write events to .genie/events/.jsonl while tailing").option("--all","Aggregate events from all active workers").action(async(n,r)=>{await eg(n,{json:r.json,follow:r.follow,lines:r.lines?Number.parseInt(r.lines,10):void 0,emit:r.emit,all:r.all})}),t.command("close ").description("Close task/issue and cleanup worker").option("--no-sync","Skip bd sync (beads only)").option("--keep-worktree","Don't remove the worktree").option("--merge","Merge worktree changes to main branch").option("-y, --yes","Skip confirmation").action(async(n,r)=>{await ra(n,r)}),t.command("ship ").description("Mark task as done and cleanup worker").option("--keep-worktree","Don't remove the worktree").option("--merge","Merge worktree changes to main branch").option("-y, --yes","Skip confirmation").action(async(n,r)=>{await oa(n,r)}),t.command("read ").description("Read terminal output from a worker pane").option("-n, --lines ","Number of lines to read").option("--from ","Start line").option("--to ","End line").option("--range ",'Line range (e.g., "10-20")').option("--search ","Search for text").option("--grep ","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(n,r)=>{await jh(n,r)}),t.command("exec ").description("Execute command in a worker pane").option("-q, --quiet","Suppress output").option("-t, --timeout ","Timeout in milliseconds").action(async(n,r,s)=>{await Oh(n,r,s)})}var fe=new F0;fe.name("genie").description("Genie CLI - Setup and utilities for AI-assisted development").version(Km);fe.command("install").description("Verify and install prerequisites").option("--check","Only check prerequisites, do not install").option("--yes","Auto-approve all installations").action(Kp);fe.command("setup").description("Configure genie settings").option("--quick","Accept all defaults").option("--shortcuts","Only configure keyboard shortcuts").option("--claudio","Only configure Claudio integration").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 e=>{await Do(e)});fe.command("doctor").description("Run diagnostic checks on genie installation").action(Cd);fe.command("update").description("Update Genie CLI to the latest version").action(zm);fe.command("uninstall").description("Remove Genie CLI and clean up hooks").action(Vm);fe.command("tui [name]").description('Start Claude Code as native team-lead (default: "genie" in ~/workspace)').option("-r, --reset","Kill existing session and start fresh").option("-d, --dir ","Working directory (default: ~/workspace)").action(async(e,t)=>{e&&(t.name=e),await Lm(t)});var ka=fe.command("shortcuts").description("Manage tmux keyboard shortcuts");ka.action(Xl);ka.command("show").description("Show available shortcuts and installation status").action(Xl);ka.command("install").description("Install shortcuts to config files (~/.tmux.conf, shell rc)").action(um);ka.command("uninstall").description("Remove shortcuts from config files").action(dm);var hs=fe.command("profiles").description("Manage worker profiles for Claude Code spawning");hs.action(Ql);hs.command("list").description("List all configured worker profiles").action(Ql);hs.command("add ").description("Create a new worker profile interactively").action(sm);hs.command("rm ").description("Delete a worker profile").action(im);hs.command("show ").description("Show details of a worker profile").action(om);hs.command("default [name]").description("Get or set the default worker profile").action(am);var Ca=fe.command("pdf").description("Generate PDFs from markdown files");Ca.command("render ").description("Render a markdown file to PDF").option("-o, --output ","Output PDF file","output.pdf").option("-t, --theme ","Theme to use (default, minimal, corporate, dark)").option("--no-page-numbers","Disable page numbers").option("-w, --watch","Watch for changes and re-render").action(em);Ca.command("template ").description("Generate PDF from a template with JSON data").option("-d, --data ","JSON data file").option("-o, --output ","Output PDF file","output.pdf").option("-t, --theme ","Theme to use").action(tm);Ca.command("themes").description("List available themes").action(nm);Ca.command("templates").description("List available templates").action(rm);var K8=fe.command("brainstorm").description("Brainstorm utilities (file-based helpers)");K8.command("crystallize").description("Crystallize a brainstorm draft into design.md and upsert .beads/issues.jsonl").requiredOption("--slug ","Brainstorm slug (kebab-case)").option("--file ","Input draft markdown path (default: .genie/brainstorms//draft.md)").option("-r, --repo ","Repo path (default: cwd)").option("--title ","Issue title (default: slug)").option("--depends-on ","Comma-separated dependency IDs (default: hq-roadmap)").option("--status ","Issue status (open|closed)","open").action(W0);var Y8=fe.command("ledger").description("Ledger utilities (beads JSONL validation)");Y8.command("validate").description("Validate local .beads/issues.jsonl JSONL structure (scriptable)").option("-r, --repo ","Repo path (default: cwd)").option("--json","Output JSON").action(Zp);gg(fe);Hh(fe);Rx(fe);xg(fe);fe.command("work ").description('Spawn worker bound to beads issue (target: bd-id, "next", or "wish")').option("--no-worktree","Use shared repo instead of worktree").option("-s, --session ","Target tmux session").option("--focus","Focus the worker pane after spawning").option("-p, --prompt ","Custom initial prompt").option("--no-resume","Start fresh session even if previous exists").option("--skill ",'Skill to invoke (auto-detects "forge" if wish.md exists)').option("--no-auto-approve","Disable auto-approve for this worker").option("--profile ","Worker profile to use").option("-n, --name ","Custom worker name (for N workers per task)").option("-r, --role ",'Worker role (e.g., "main", "tests", "review")').option("--shared-worktree","Share worktree with existing worker on same task").action(async(e,t)=>{await Ah(e,t)});var Sa=fe.command("daemon").description("Manage beads daemon for auto-sync");Sa.command("start").description("Start beads daemon (auto-commit, auto-sync)").option("--no-auto-commit","Disable auto-commit").option("--auto-push","Enable auto-push to remote").action(async e=>{await ix(e)});Sa.command("stop").description("Stop beads daemon").action(async()=>{await ox()});Sa.command("status").description("Show daemon status").option("--json","Output as JSON").action(async e=>{await ax(e)});Sa.command("restart").description("Restart beads daemon").option("--no-auto-commit","Disable auto-commit").option("--auto-push","Enable auto-push to remote").action(async e=>{await cx(e)});fe.command("council").description("Spawn dual Claude instances for multi-model deliberation").option("-s, --session ","Target tmux session").option("--preset ","Council preset to use").option("--skill ","Skill to load on both instances").option("--no-focus","Don't focus the new window").action(async e=>{await Zm(e)});fe.parse();
/*! Bundled license information:
js-yaml/dist/js-yaml.mjs:
diff --git a/plugins/genie/scripts/smart-install.js b/plugins/genie/scripts/smart-install.js
index 3729a7518..ed07aa366 100644
--- a/plugins/genie/scripts/smart-install.js
+++ b/plugins/genie/scripts/smart-install.js
@@ -6,7 +6,6 @@ import { execSync, spawnSync } from 'node:child_process';
* Ensures required dependencies are installed:
* - Bun runtime (auto-installs if missing)
* - tmux (guides user if missing - can't auto-install)
- * - beads (auto-installs if missing via npm)
* - genie CLI (installed globally via bun)
*
* Also handles:
@@ -27,10 +26,6 @@ const BUN_COMMON_PATHS = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
-const BEADS_COMMON_PATHS = IS_WINDOWS
- ? [join(homedir(), 'AppData', 'Roaming', 'npm', 'bd.cmd')]
- : [join(homedir(), '.local', 'bin', 'bd'), '/usr/local/bin/bd', '/opt/homebrew/bin/bd'];
-
const GENIE_COMMON_PATHS = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'genie.exe')]
: [join(homedir(), '.bun', 'bin', 'genie'), '/usr/local/bin/genie', '/opt/homebrew/bin/genie'];
@@ -130,45 +125,6 @@ function getTmuxVersion() {
}
}
-/**
- * Check if beads (bd) is installed
- */
-function getBeadsPath() {
- try {
- const result = spawnSync('bd', ['--version'], {
- encoding: 'utf-8',
- stdio: ['pipe', 'pipe', 'pipe'],
- shell: IS_WINDOWS,
- });
- if (result.status === 0) return 'bd';
- } catch {
- // Not in PATH
- }
- return BEADS_COMMON_PATHS.find(existsSync) || null;
-}
-
-function isBeadsInstalled() {
- return getBeadsPath() !== null;
-}
-
-/**
- * Install beads via npm
- */
-function installBeads() {
- console.error('Installing beads (bd)...');
- try {
- execSync('npm install -g @anthropic-ai/bd', { stdio: 'inherit', shell: true });
- if (!isBeadsInstalled()) {
- throw new Error('beads installation completed but bd not found.');
- }
- console.error('beads installed');
- } catch (error) {
- console.error('Failed to install beads. Please install manually:');
- console.error(' npm install -g @anthropic-ai/bd');
- throw error;
- }
-}
-
/**
* Check if dependencies need to be installed
*/
@@ -316,21 +272,23 @@ function installGenieCli() {
// Main execution
try {
// Quick check: if everything is already installed, exit silently
- if (isBunInstalled() && isTmuxInstalled() && isBeadsInstalled() && !needsInstall() && !genieCliNeedsInstall()) {
+ if (isBunInstalled() && isTmuxInstalled() && !needsInstall() && !genieCliNeedsInstall()) {
process.exit(0);
}
- // 1. Check/install Bun
+ // 1. Check/install Bun (required — fatal if fails)
if (!isBunInstalled()) {
installBun();
}
- // 2. Check tmux - REQUIRED, but can't auto-install
+ // 2. Check tmux (required for agent orchestration — fatal)
if (!isTmuxInstalled()) {
console.error('');
- console.error('ERROR: tmux is required but not installed.');
+ console.error('WARNING: tmux is not installed.');
+ console.error('tmux is required for agent orchestration (genie agent spawn, teams, etc.).');
+ console.error('Non-interactive features still work without it.');
console.error('');
- console.error('Please install tmux manually:');
+ console.error('Install tmux:');
if (process.platform === 'darwin') {
console.error(' brew install tmux');
} else if (process.platform === 'linux') {
@@ -342,26 +300,28 @@ try {
console.error(' Inside WSL: sudo apt install tmux');
}
console.error('');
- console.error('Then restart Claude Code.');
- process.exit(2); // Exit code 2 = blocking error for Claude to process
+ // Don't exit — let the rest of the chain run
}
- // 3. Check/install beads
- if (!isBeadsInstalled()) {
- installBeads();
- }
-
- // 4. Install plugin dependencies if needed
+ // 3. Install plugin dependencies if needed
if (needsInstall()) {
installDeps();
console.error('Dependencies installed');
}
- // 5. Install or upgrade genie CLI via bun global
+ // 4. Install or upgrade genie CLI via bun global (non-fatal)
if (genieCliNeedsInstall()) {
- installGenieCli();
+ try {
+ installGenieCli();
+ } catch (e) {
+ console.error(`Warning: genie CLI install/upgrade failed: ${e.message}`);
+ console.error('The plugin will still work. Install genie CLI manually later.');
+ }
}
} catch (e) {
- console.error('Installation failed:', e.message);
- process.exit(1);
+ // Only Bun install failure reaches here — everything else is graceful
+ console.error('Critical installation failed:', e.message);
+ console.error('Continuing anyway to let remaining hooks run...');
+ // Exit 0 so the hook chain continues (first-run-check, session-context)
+ process.exit(0);
}
diff --git a/plugins/genie/scripts/statusline.sh b/plugins/genie/scripts/statusline.sh
new file mode 100755
index 000000000..251e8e8b2
--- /dev/null
+++ b/plugins/genie/scripts/statusline.sh
@@ -0,0 +1,201 @@
+#!/bin/bash
+# Genie Status Line — meta-programmable agent lifecycle daemon
+#
+# Runs on every assistant message (debounce 300ms).
+# Reads agent-specific triggers from ~/.genie/statusline/.json
+# Falls back to ~/.genie/statusline/_defaults.json
+#
+# SELF-MODIFIABLE: Agents can edit their own trigger config at runtime:
+# echo '{"triggers":[...]}' > ~/.genie/statusline/$GENIE_AGENT_NAME.json
+#
+# Trigger format:
+# {
+# "id": "unique-id",
+# "when": "context_pct >= 75", # condition: context_pct, cost, duration_min
+# "action": "send", # send = genie send to self
+# "message": "Context at {pct}%", # {pct}, {cost}, {model}, {agent}, {branch}
+# "cooldown_seconds": 300, # min seconds between fires
+# "enabled": true
+# }
+
+input=$(cat)
+
+# === Parse session data ===
+MODEL=$(echo "$input" | jq -r '.model.display_name // "?"')
+PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
+TOKENS_USED=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
+TOKENS_MAX=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
+COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
+DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
+AGENT_NAME="${GENIE_AGENT_NAME:-}"
+WORKTREE=$(echo "$input" | jq -r '.worktree.name // empty')
+
+DURATION_MIN=$((DURATION_MS / 60000))
+COST_FMT=$(printf '$%.2f' "$COST")
+
+# === Format token counts (e.g. 45k, 120k, 1.2M) ===
+fmt_tokens() {
+ local n=$1
+ if [ "$n" -ge 1000000 ]; then
+ local m=$((n / 1000))
+ printf '%s.%sM' "$((m / 1000))" "$(( (m % 1000) / 100 ))"
+ elif [ "$n" -ge 1000 ]; then
+ printf '%sk' "$((n / 1000))"
+ else
+ printf '%s' "$n"
+ fi
+}
+
+USED_FMT=$(fmt_tokens "$TOKENS_USED")
+MAX_FMT=$(fmt_tokens "$TOKENS_MAX")
+
+# === Colors ===
+GREEN='\033[32m'
+YELLOW='\033[33m'
+RED='\033[31m'
+CYAN='\033[36m'
+DIM='\033[2m'
+BOLD='\033[1m'
+RESET='\033[0m'
+
+# === Context bar (token-based) ===
+BAR_WIDTH=12
+if [ "$TOKENS_MAX" -gt 0 ]; then
+ FILLED=$((TOKENS_USED * BAR_WIDTH / TOKENS_MAX))
+else
+ FILLED=0
+fi
+[ "$FILLED" -gt "$BAR_WIDTH" ] && FILLED=$BAR_WIDTH
+EMPTY=$((BAR_WIDTH - FILLED))
+BAR=$(printf "%${FILLED}s" | tr ' ' '=')
+[ "$EMPTY" -gt 0 ] && BAR="${BAR}$(printf "%${EMPTY}s" | tr ' ' '-')"
+
+if [ "$TOKENS_USED" -ge $((TOKENS_MAX * 80 / 100)) ]; then BAR_COLOR="$RED"
+elif [ "$TOKENS_USED" -ge $((TOKENS_MAX * 50 / 100)) ]; then BAR_COLOR="$YELLOW"
+else BAR_COLOR="$GREEN"; fi
+
+# === Git branch ===
+BRANCH=""
+if git rev-parse --git-dir > /dev/null 2>&1; then
+ BRANCH=$(git branch --show-current 2>/dev/null)
+fi
+
+# === Active agents ===
+AGENTS_COUNT=0
+REGISTRY="$HOME/.genie/workers.json"
+if [ -f "$REGISTRY" ]; then
+ AGENTS_COUNT=$(jq '[.[] | select(.state == "working" or .state == "idle" or .state == "spawning")] | length' "$REGISTRY" 2>/dev/null || echo 0)
+fi
+
+# === Build display line ===
+LINE="${BOLD}${MODEL}${RESET}"
+LINE="${LINE} ${DIM}|${RESET} ${BAR_COLOR}[${BAR}] ${USED_FMT}/${MAX_FMT}${RESET}"
+
+[ "$AGENTS_COUNT" -gt 0 ] && LINE="${LINE} ${DIM}|${RESET} ${CYAN}${AGENTS_COUNT} agents${RESET}"
+[ -n "$AGENT_NAME" ] && LINE="${LINE} ${DIM}|${RESET} ${CYAN}@${AGENT_NAME}${RESET}"
+
+if [ -n "$WORKTREE" ]; then
+ LINE="${LINE} ${DIM}|${RESET} ${YELLOW}${WORKTREE}${RESET}"
+elif [ -n "$BRANCH" ]; then
+ LINE="${LINE} ${DIM}|${RESET} ${GREEN}${BRANCH}${RESET}"
+fi
+
+echo -e "$LINE"
+
+# === Trigger engine ===
+# Skip if no agent name (can't send to self)
+[ -z "$AGENT_NAME" ] && exit 0
+
+# Load agent-specific config: team/agent.json → team/_defaults.json → _defaults.json
+TEAM="${GENIE_TEAM:-default}"
+CONFIG="$HOME/.genie/statusline/${TEAM}/${AGENT_NAME}.json"
+[ ! -f "$CONFIG" ] && CONFIG="$HOME/.genie/statusline/${TEAM}/_defaults.json"
+[ ! -f "$CONFIG" ] && CONFIG="$HOME/.genie/statusline/_defaults.json"
+[ ! -f "$CONFIG" ] && exit 0
+
+COOLDOWN_DIR="$HOME/.genie/statusline/.cooldowns"
+mkdir -p "$COOLDOWN_DIR"
+NOW=$(date +%s)
+
+# Process each trigger
+echo "$input" | jq -c '.triggers = []' > /dev/null 2>&1 # validate jq works
+TRIGGERS=$(jq -c '.triggers[]? | select(.enabled == true)' "$CONFIG" 2>/dev/null)
+
+while IFS= read -r trigger; do
+ [ -z "$trigger" ] && continue
+
+ ID=$(echo "$trigger" | jq -r '.id')
+ WHEN=$(echo "$trigger" | jq -r '.when')
+ ACTION=$(echo "$trigger" | jq -r '.action')
+ MSG=$(echo "$trigger" | jq -r '.message')
+ COOLDOWN=$(echo "$trigger" | jq -r '.cooldown_seconds // 300')
+
+ # Check cooldown
+ COOLDOWN_FILE="$COOLDOWN_DIR/${AGENT_NAME}_${ID}"
+ if [ -f "$COOLDOWN_FILE" ]; then
+ LAST_FIRE=$(cat "$COOLDOWN_FILE")
+ ELAPSED=$((NOW - LAST_FIRE))
+ [ "$ELAPSED" -lt "$COOLDOWN" ] && continue
+ fi
+
+ # Evaluate condition
+ FIRED=false
+ case "$WHEN" in
+ *context_pct*)
+ THRESHOLD=$(echo "$WHEN" | grep -oE '[0-9]+')
+ OP=$(echo "$WHEN" | grep -oE '>=|<=|>|<|==')
+ case "$OP" in
+ ">=") [ "$PCT" -ge "$THRESHOLD" ] && FIRED=true ;;
+ ">") [ "$PCT" -gt "$THRESHOLD" ] && FIRED=true ;;
+ "<=") [ "$PCT" -le "$THRESHOLD" ] && FIRED=true ;;
+ "<") [ "$PCT" -lt "$THRESHOLD" ] && FIRED=true ;;
+ "==") [ "$PCT" -eq "$THRESHOLD" ] && FIRED=true ;;
+ esac
+ ;;
+ *cost*)
+ THRESHOLD=$(echo "$WHEN" | grep -oE '[0-9.]+')
+ # Compare as integers (cents)
+ COST_CENTS=$(printf '%.0f' "$(echo "$COST * 100" | bc 2>/dev/null || echo 0)")
+ THRESH_CENTS=$(printf '%.0f' "$(echo "$THRESHOLD * 100" | bc 2>/dev/null || echo 0)")
+ [ "$COST_CENTS" -ge "$THRESH_CENTS" ] 2>/dev/null && FIRED=true
+ ;;
+ *duration_min*)
+ THRESHOLD=$(echo "$WHEN" | grep -oE '[0-9]+')
+ [ "$DURATION_MIN" -ge "$THRESHOLD" ] && FIRED=true
+ ;;
+ esac
+
+ if [ "$FIRED" = true ]; then
+ # Interpolate message using awk to avoid sed injection from untrusted variables
+ MSG=$(printf '%s' "$MSG" | awk \
+ -v pct="$PCT" \
+ -v cost="$COST_FMT" \
+ -v model="$MODEL" \
+ -v agent="$AGENT_NAME" \
+ -v team="$TEAM" \
+ -v branch="$BRANCH" \
+ -v duration="${DURATION_MIN}m" \
+ '{
+ gsub(/{pct}/, pct)
+ gsub(/{cost}/, cost)
+ gsub(/{model}/, model)
+ gsub(/{agent}/, agent)
+ gsub(/{team}/, team)
+ gsub(/{branch}/, branch)
+ gsub(/{duration}/, duration)
+ print
+ }')
+
+ # Execute action
+ case "$ACTION" in
+ send)
+ genie send --to "$AGENT_NAME" "$MSG" > /dev/null 2>&1 &
+ ;;
+ esac
+
+ # Record cooldown
+ echo "$NOW" > "$COOLDOWN_FILE"
+ fi
+done <<< "$TRIGGERS"
+
+exit 0
diff --git a/plugins/genie/scripts/worker-service.cjs b/plugins/genie/scripts/worker-service.cjs
index a15b0e563..37905cfd8 100755
--- a/plugins/genie/scripts/worker-service.cjs
+++ b/plugins/genie/scripts/worker-service.cjs
@@ -4,11 +4,11 @@
`,r.activeForge.currentTask&&(t+=` Current Task: ${r.activeForge.currentTask}
`),t+=` Completed: ${r.activeForge.completedTasks.length} tasks
`,r.activeForge.failedTasks.length>0&&(t+=` Failed: ${r.activeForge.failedTasks.length} tasks
-`)),t?n(e,{context:t}):n(e,{context:null});return}if(s==="/api/admin/restart"&&c==="POST"){n(e,{success:!0,message:"Worker restarting..."}),setTimeout(()=>{process.exit(0)},100);return}n(e,{error:"Not found",path:s},404)}function S(){try{if((0,i.existsSync)(g)){let o=Number.parseInt((0,i.readFileSync)(g,"utf-8").trim(),10);return process.kill(o,0),!0}}catch{}return!1}function I(){(0,i.writeFileSync)(g,String(process.pid))}var k=process.argv[2];if(k==="start"){if(S()&&(console.log("Worker already running"),process.exit(0)),process.argv[3]!=="--foreground"){let e=(0,p.spawn)(process.argv[0],[process.argv[1],"start","--foreground"],{detached:!0,stdio:"ignore"});e.unref(),console.log(`Worker started (PID: ${e.pid})`),process.exit(0)}let o=(0,T.createServer)((e,a)=>{y(e,a).catch(s=>{console.error("Request error:",s),n(a,{error:"Internal server error"},500)})});o.listen(u,"127.0.0.1",()=>{I(),console.log(`genie worker listening on http://127.0.0.1:${u}`)}),process.on("SIGTERM",()=>{o.close(),process.exit(0)}),process.on("SIGINT",()=>{o.close(),process.exit(0)})}else if(k==="stop"){try{if((0,i.existsSync)(g)){let o=Number.parseInt((0,i.readFileSync)(g,"utf-8").trim(),10);process.kill(o,"SIGTERM"),console.log("Worker stopped")}else console.log("Worker not running")}catch{console.log("Worker not running")}process.exit(0)}else if(k==="status"){if(S()){let o=(0,i.readFileSync)(g,"utf-8").trim();console.log(`Worker running (PID: ${o})`);try{(0,p.execSync)(`curl -s http://127.0.0.1:${u}/health`,{encoding:"utf-8"}),console.log("Health: OK")}catch{console.log("Health: Unable to connect")}}else console.log("Worker not running");process.exit(0)}else if(k==="hook"){if(process.argv[3]==="context")try{let e=(0,p.execSync)(`curl -s http://127.0.0.1:${u}/api/hook/context`,{encoding:"utf-8"}),a=JSON.parse(e);a.context&&console.log(`
+`)),t?n(e,{context:t}):n(e,{context:null});return}if(s==="/api/admin/restart"&&c==="POST"){n(e,{success:!0,message:"Worker restarting..."}),setTimeout(()=>{process.exit(0)},100);return}n(e,{error:"Not found",path:s},404)}function S(){try{if((0,i.existsSync)(g)){let o=Number.parseInt((0,i.readFileSync)(g,"utf-8").trim(),10);return process.kill(o,0),!0}}catch{}return!1}function I(){(0,i.writeFileSync)(g,String(process.pid))}var k=process.argv[2];if(k==="start"){if(S()&&(console.log("Worker already running"),process.exit(0)),process.argv[3]!=="--foreground"){let e=(0,p.spawn)(process.argv[0],[process.argv[1],"start","--foreground"],{detached:!0,stdio:"ignore"});e.unref(),console.log(`Worker started (PID: ${e.pid})`),process.exit(0)}let o=(0,T.createServer)((e,a)=>{y(e,a).catch(s=>{console.error("Request error:",s),n(a,{error:"Internal server error"},500)})});o.listen(u,"127.0.0.1",()=>{I(),console.log(`genie agent listening on http://127.0.0.1:${u}`)}),process.on("SIGTERM",()=>{o.close(),process.exit(0)}),process.on("SIGINT",()=>{o.close(),process.exit(0)})}else if(k==="stop"){try{if((0,i.existsSync)(g)){let o=Number.parseInt((0,i.readFileSync)(g,"utf-8").trim(),10);process.kill(o,"SIGTERM"),console.log("Worker stopped")}else console.log("Worker not running")}catch{console.log("Worker not running")}process.exit(0)}else if(k==="status"){if(S()){let o=(0,i.readFileSync)(g,"utf-8").trim();console.log(`Worker running (PID: ${o})`);try{(0,p.execSync)(`curl -s http://127.0.0.1:${u}/health`,{encoding:"utf-8"}),console.log("Health: OK")}catch{console.log("Health: Unable to connect")}}else console.log("Worker not running");process.exit(0)}else if(k==="hook"){if(process.argv[3]==="context")try{let e=(0,p.execSync)(`curl -s http://127.0.0.1:${u}/api/hook/context`,{encoding:"utf-8"}),a=JSON.parse(e);a.context&&console.log(`
${a.context}
`)}catch{}process.exit(0)}else console.log(`
-genie worker service
+genie agent service
Usage:
worker-service start Start the worker (daemonized)
diff --git a/plugins/genie/settings.json b/plugins/genie/settings.json
new file mode 100644
index 000000000..31e87dbae
--- /dev/null
+++ b/plugins/genie/settings.json
@@ -0,0 +1,13 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(genie *)",
+ "Bash(bd *)",
+ "Bash(term *)",
+ "Bash(tmux *)",
+ "Bash(make *)",
+ "Bash(bun *)",
+ "Bash(git *)"
+ ]
+ }
+}
diff --git a/skills/debug/SKILL.md b/skills/debug/SKILL.md
index d8610b0a5..b0386e88a 100644
--- a/skills/debug/SKILL.md
+++ b/skills/debug/SKILL.md
@@ -39,7 +39,7 @@ Debug must run in **isolation** — the subagent must not modify any source file
|---------|-----------|---------------|
| Claude Code | `Task` tool available | `Task(model: "sonnet", isolation: "worktree", prompt: "")` |
| Codex | `CODEX_ENV` or native API | `codex_subagent(task: "", sandbox: true)` |
-| OpenClaw | `genie` CLI available | `genie worker spawn --role debugger` |
+| OpenClaw | `genie` CLI available | `genie agent spawn --role debugger` |
Default to **Claude Code** when detection is ambiguous.
diff --git a/skills/docs/SKILL.md b/skills/docs/SKILL.md
index 09035f084..de7dde4b2 100644
--- a/skills/docs/SKILL.md
+++ b/skills/docs/SKILL.md
@@ -26,7 +26,7 @@ Audit existing documentation, identify gaps, generate what's missing, and valida
|---------|-----------|---------|
| Claude Code | `Task` tool available | `Task(model: "sonnet", isolation: "worktree", prompt: "")` |
| Codex | `CODEX_ENV` or native API | `codex_subagent(task: "", sandbox: true)` |
-| OpenClaw | `genie` CLI available | `genie worker spawn --role docs` |
+| OpenClaw | `genie` CLI available | `genie agent spawn --role docs` |
Default to **Claude Code** when detection is ambiguous.
diff --git a/skills/dream/SKILL.md b/skills/dream/SKILL.md
index 0def85506..083f94bec 100644
--- a/skills/dream/SKILL.md
+++ b/skills/dream/SKILL.md
@@ -98,7 +98,7 @@ Isolation is Codex-managed. Documented for future validation.
### OpenClaw
```bash
-genie worker spawn --role implementor
+genie agent spawn --role implementor
# Or for bead-tracked work:
bd create "" --type task
genie work
diff --git a/skills/fix/SKILL.md b/skills/fix/SKILL.md
index ae713e568..7b7cf1a2e 100644
--- a/skills/fix/SKILL.md
+++ b/skills/fix/SKILL.md
@@ -43,7 +43,7 @@ Fix and re-review must be **separate dispatches** — never combine them in one
|---------|-----------|-------------|-------------------|
| Claude Code | `Task` tool available | `Task(model: "sonnet", isolation: "worktree", prompt: "")` | `Task(model: "sonnet", isolation: "worktree", prompt: "")` |
| Codex | `CODEX_ENV` or native API | `codex_subagent(task: "", sandbox: true)` | `codex_subagent(task: "", sandbox: true)` |
-| OpenClaw | `genie` CLI available | `genie worker spawn --role fixer` | `genie worker spawn --role reviewer` |
+| OpenClaw | `genie` CLI available | `genie agent spawn --role fixer` | `genie agent spawn --role reviewer` |
Default to **Claude Code** when detection is ambiguous.
diff --git a/skills/genie-pilot/SKILL.md b/skills/genie-pilot/SKILL.md
deleted file mode 100644
index 5250cc751..000000000
--- a/skills/genie-pilot/SKILL.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Skill: genie-pilot
-
-Canonical orchestration skill for Genie CLI teams.
-
-## Purpose
-
-You are a Genie Pilot — the orchestration driver that manages
-multi-worker teams across providers. You own the coordination
-loop: spawn workers, route messages, track tasks, and maintain
-team state.
-
-## Provider Contracts
-
-### Claude Workers
-- Launched via: `claude --agent `
-- Role routing uses the public `--agent` flag (no hidden teammate flags)
-- Worker behavior is determined by the agent role configuration
-- Example: `genie worker spawn --provider claude --team work --role implementor`
-
-### Codex Workers
-- Launched via: `codex --instructions `
-- Worker behavior comes from explicit skill instructions injected at spawn
-- `--role` is advisory metadata only (for registry/visibility, not routing)
-- Example: `genie worker spawn --provider codex --team work --skill work --role tester`
-
-## Genie-Owned Orchestration Boundary
-
-Genie owns these semantics regardless of provider:
-
-| Layer | Owner | Description |
-|-------|-------|-------------|
-| Mailbox | Genie | Durable message persistence before delivery |
-| Protocol Router | Genie | Provider-agnostic message routing |
-| Worker State | Genie | Lifecycle tracking (spawning/working/idle/done) |
-| Task Coupling | Genie | Dependency-aware task assignment to workers |
-| Delivery | Genie | State-aware queued delivery to tmux panes |
-
-Providers only launch worker processes. They do not own coordination.
-
-## Commands Quick Reference
-
-```bash
-# Team management
-genie team create work-team --blueprint work
-genie team list
-genie team delete work-team
-
-# Worker lifecycle
-genie worker spawn --provider claude --team work --role implementor
-genie worker spawn --provider codex --team work --skill work --role tester
-genie worker list
-genie worker kill
-genie worker dashboard
-
-# Messaging (mailbox-first)
-genie msg send --to 'message body'
-genie msg inbox
-
-# Tasks (dependency-aware)
-genie task create "implement feature X"
-genie task list
-genie task update --status done
-```
-
-## Layout
-
-Default layout is **mosaic/tiled** for readability at higher worker
-counts. Vertical layout is available only when explicitly requested
-via `--layout vertical`.
-
-## Orchestration Loop
-
-1. Create a team from a blueprint
-2. Spawn workers with provider selection
-3. Assign tasks to workers
-4. Monitor via dashboard
-5. Route messages between workers via mailbox
-6. Track task completion and dependencies
-7. Shutdown workers when done
-
-## Non-Goals
-
-- Genie Pilot does not replace provider-native orchestration
-- Genie Pilot does not manage non-tmux transports
-- Genie Pilot does not provide arbitrary shell command adapters
diff --git a/skills/onboarding/SKILL.md b/skills/onboarding/SKILL.md
new file mode 100644
index 000000000..e8bf0b6c3
--- /dev/null
+++ b/skills/onboarding/SKILL.md
@@ -0,0 +1,556 @@
+---
+name: onboarding
+description: "Interactive first-run onboarding — validate workspace, welcome new users/agents, gather preferences, inject hooks, and configure a ready-to-work environment."
+---
+
+# /onboarding — Welcome to Genie
+
+The **single canonical entry point** for new users and freshly-cloned agents. Validates the workspace structure, gathers identity and preferences interactively, injects hooks, and scaffolds a complete working environment.
+
+This skill replaces the fragmented `install-workspace.sh` → `apply-blank-init.sh` → `genie tui` chain with one unified, validated flow.
+
+## When to Use
+- First-time `genie` launch (no AGENTS.md exists)
+- User explicitly invokes `/onboarding`
+- New agent clone needs workspace configuration
+- `genie-blank-init` detects a blank persona and hands off here
+- After a fresh `git clone` of a genie-managed repo
+- When `genie tui` starts without required workspace files
+
+## Flow
+
+### Phase 0: Workspace Validation (Silent)
+
+Before any user interaction, validate the workspace structure. Fix gaps silently — only report what was created in the final summary.
+
+**Required structure:**
+
+```
+/
+├── .genie/
+│ ├── wishes/ # Planning documents
+│ ├── brainstorms/ # Exploration notes
+│ └── brainstorm.md # Jar index
+├── .claude/ # Claude Code config
+├── memory/ # Session continuity
+├── AGENTS.md # Workspace identity (created in Phase 4)
+```
+
+**Validation steps:**
+
+```bash
+# Create genie workspace directories
+mkdir -p .genie/wishes .genie/brainstorms
+
+# Create brainstorm jar if missing
+[ -f .genie/brainstorm.md ] || cat > .genie/brainstorm.md << 'EOF'
+# Brainstorm Jar
+## Raw
+## Simmering
+## Ready
+## Poured
+EOF
+
+# Create Claude Code config dir
+mkdir -p .claude
+
+# Create memory directory
+mkdir -p memory
+```
+
+**If AGENTS.md already exists:**
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "An AGENTS.md already exists in this workspace. What would you like to do?",
+ header: "Existing Configuration Found",
+ options: [
+ "Reconfigure from scratch",
+ "Keep existing and only fix missing pieces",
+ "Cancel onboarding"
+ ]
+ }]
+})
+```
+
+If "Cancel" — exit gracefully. If "Keep existing" — skip to Phase 3 (integrations) and Phase 4c-4d only.
+
+### Phase 1: Welcome
+
+Display the ASCII art banner. Then introduce yourself warmly in 2-3 sentences — explain that you'll walk them through a quick setup.
+
+```
+ /\
+ / \
+ / /\ \
+ / / \ \
+ / / /\ \ \
+ /_/ / \_\_\
+ \/
+
+ ██████ ███████ ███ ██ ██ ███████
+ ██ ██ ████ ██ ██ ██
+ ██ ███ █████ ██ ██ ██ ██ █████
+ ██ ██ ██ ██ ██ ██ ██ ██
+ ██████ ███████ ██ ████ ██ ███████
+
+ )
+ ( )
+ ( )
+ ) (
+ |
+ _.--' '--._
+ / \
+ | ~~~~~~ |
+ \ /
+ '-.______.-'
+ \________/
+```
+
+### Phase 2: Identity (AskUserQuestion)
+
+Use `AskUserQuestion` for each prompt. One question per step — never batch.
+
+**Step 1 — Name**
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "What should I call you?",
+ header: "Your Name"
+ }]
+})
+```
+
+Free-text response. Store as `$USER_NAME`.
+
+**Step 2 — Role**
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "What best describes your work?",
+ header: "Your Role",
+ options: [
+ "Software Development",
+ "DevOps / Infrastructure",
+ "Data Engineering / Analytics",
+ "Product / Design",
+ "Security / Compliance",
+ "Research / Exploration",
+ "Other (I'll describe it)"
+ ]
+ }]
+})
+```
+
+If "Other" is selected, follow up with a free-text question asking them to describe their role. Store as `$USER_ROLE`.
+
+**Step 3 — Work Style**
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "How do you prefer to work with AI?",
+ header: "Work Style",
+ options: [
+ "Autonomous — do the work, show me results",
+ "Collaborative — let's think together, then you execute",
+ "Supervised — check with me before each step"
+ ]
+ }]
+})
+```
+
+Store as `$WORK_STYLE`. This maps to the agent's autonomy level in AGENTS.md.
+
+### Phase 3: Integrations (AskUserQuestion)
+
+**Step 4 — Integrations**
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "Which integrations do you want to set up now? (You can add more later)",
+ header: "Integrations",
+ options: [
+ "GitHub (repos, PRs, issues)",
+ "Telegram (notifications, commands)",
+ "None for now"
+ ],
+ multiSelect: true
+ }]
+})
+```
+
+Store selections as `$INTEGRATIONS[]`.
+
+**Step 5 — GitHub Setup** (only if GitHub selected)
+
+Check if `gh` CLI is authenticated (`gh auth status`). If yes, confirm the authenticated account. If not, tell the user to run `gh auth login` and offer to wait or skip.
+
+**Step 6 — Telegram Setup** (only if Telegram selected)
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "What's your Telegram bot token? (from @BotFather)",
+ header: "Telegram Bot Token"
+ }]
+})
+```
+
+Validate the token format (numeric:alphanumeric). If valid, store securely. If the user doesn't have one yet, explain how to get one from @BotFather and offer to skip for now.
+
+### Phase 4: Configure Environment
+
+Based on gathered answers, execute these configuration steps. Report each step briefly as you go.
+
+**4a. Create AGENTS.md**
+
+Write a personalized AGENTS.md using this template, substituting gathered values:
+
+```markdown
+# AGENTS.md — $USER_NAME's Workspace
+
+## Identity
+- **Name:** $USER_NAME
+- **Role:** $USER_ROLE
+- **Style:** $WORK_STYLE_DESCRIPTION
+
+## Preferences
+- Work style: $WORK_STYLE
+- Active integrations: $INTEGRATIONS_LIST
+
+## Conventions
+- Use Bun exclusively (never npm/yarn/pnpm)
+- Conventional commits: type(scope): description
+- Branch workflow: dev (working) → main (production)
+
+## Agent Commands (genie CLI)
+- Spawn agent: `genie agent spawn --role `
+- List agents: `genie agent list`
+- Send message: `genie send "" --to `
+- Kill agent: `genie agent kill `
+- Manage teams: `genie team ensure `
+
+## Session Protocol
+1. Read this file at session start
+2. Check memory/ for recent context
+3. Work on assigned tasks
+4. Push changes before ending session
+```
+
+Adapt the template to the user's role:
+- **Software Development**: add git workflow, testing, and PR conventions
+- **DevOps / Infrastructure**: add deployment, monitoring, and infra conventions
+- **Data Engineering**: add pipeline, schema, and data quality conventions
+- **Other roles**: keep it generic but include the basics
+
+**4b. Configure Default Team** (if integrations selected)
+
+```bash
+genie team ensure default
+```
+
+**4c. Inject Hooks (CRITICAL)**
+
+This step is **mandatory** — hooks must be injected during onboarding, not deferred to first agent spawn.
+
+```bash
+genie hook install
+```
+
+This writes `genie hook dispatch` entries into `~/.claude/settings.json`, ensuring all Claude Code events are routed through the genie CLI from the very first session — including TUI startup.
+
+**Why this matters:** Without this step, a team-lead spawned via `genie tui` has NO hooks until its first agent is spawned (via `injectTeamHooks`). This means the first session runs "deaf" — no event routing, no protocol dispatch, no auto-spawn. Onboarding fixes this by front-loading hook injection.
+
+**Verify hooks were injected:**
+
+```bash
+genie hook status
+```
+
+If `genie` is not in PATH, warn the user and add to the summary as a manual step.
+
+**4d. Validate tmux Configuration**
+
+Genie uses tmux heavily for agent orchestration (panes, windows, sessions). Incorrect tmux settings will cause silent failures.
+
+**Check base-index:**
+
+```bash
+tmux show-option -gv base-index 2>/dev/null
+tmux show-option -gv pane-base-index 2>/dev/null
+```
+
+| Setting | Expected | Why |
+|---------|----------|-----|
+| `base-index` | `0` | Genie targets windows as `session:0` — a non-zero base-index breaks window resolution |
+| `pane-base-index` | `0` | Fallback pane targets use `.0` format (`session:team.0`) |
+
+**If either is NOT 0:**
+
+```
+AskUserQuestion({
+ questions: [{
+ question: "Your tmux base-index is not 0. Genie requires base-index 0 to work correctly. Should I fix your tmux config?",
+ header: "tmux Configuration Issue",
+ options: [
+ "Yes, update my ~/.tmux.conf",
+ "No, I'll fix it manually later"
+ ]
+ }]
+})
+```
+
+If "Yes", append to `~/.tmux.conf`:
+
+```bash
+cat >> ~/.tmux.conf << 'EOF'
+
+# Genie requires base-index 0 for window/pane targeting
+set -g base-index 0
+setw -g pane-base-index 0
+EOF
+```
+
+**If tmux is not installed:** Warn the user — tmux is required for agent orchestration. Suggest installation but don't block onboarding (non-interactive features still work).
+
+**4e. Initialize Memory**
+
+Write an initial `memory/YYYY-MM-DD.md` entry noting the onboarding completion:
+
+```markdown
+# YYYY-MM-DD
+
+## Onboarding
+- Workspace initialized for $USER_NAME ($USER_ROLE)
+- Work style: $WORK_STYLE
+- Integrations: $INTEGRATIONS_LIST
+- Hooks: installed via `genie hook install`
+- Workspace structure validated and created
+```
+
+### Phase 4f: Omni Plugin (Optional)
+
+Check if the Omni v2 plugin is installed. If the user wants to connect agents to messaging channels (WhatsApp, Telegram, Discord, Slack), the Omni plugin provides the skills for that.
+
+**Detection:**
+
+```bash
+# Check if omni plugin is loaded (look for omni skills in current session)
+claude plugin list 2>/dev/null | grep -q omni
+# Or check if omni CLI is available
+command -v omni >/dev/null 2>&1
+```
+
+**If Omni is NOT installed:**
+
+Use AskUserQuestion:
+```
+header: "Omni v2 — Connect agents to messaging channels"
+question: "Want to connect genie agents to WhatsApp, Telegram, Discord, or Slack? The Omni plugin adds channel management and agent routing skills."
+options: ["Yes, install Omni plugin", "Skip for now"]
+```
+
+If "Yes":
+```bash
+# 1. Add the marketplace (if not already added)
+claude plugin marketplace add https://github.com/automagik-dev/omni.git
+
+# 2. Install the plugin
+claude plugin install omni@automagik-dev
+
+# 3. Restart Claude Code to load the plugin
+```
+
+Tell the user to restart Claude Code after install for the plugin to take effect.
+
+**If Omni IS installed:**
+
+Suggest the agent setup skill:
+```
+Omni plugin detected! To connect an agent to a channel, run /omni-agent-setup
+```
+
+### Phase 5: Summary
+
+Present a clear summary of everything configured:
+
+```
+Setup Complete!
+
+ Name: $USER_NAME
+ Role: $USER_ROLE
+ Work Style: $WORK_STYLE
+ Integrations: $INTEGRATIONS_LIST
+
+Files created/validated:
+ .genie/wishes/ (planning)
+ .genie/brainstorms/ (exploration)
+ .genie/brainstorm.md (jar index)
+ memory/ (session continuity)
+ AGENTS.md (workspace persona)
+
+Hooks:
+ genie hook dispatch (installed globally)
+
+tmux:
+ base-index 0 (verified/fixed)
+ pane-base-index 0 (verified/fixed)
+
+Next steps:
+ - Run /brainstorm to explore an idea
+ - Run /wish to plan a task
+ - Run /work to execute
+ - Run /omni-agent-setup to connect an agent to a channel (if Omni installed)
+```
+
+End with a brief, friendly message welcoming them and suggesting their first action based on their role.
+
+## AskUserQuestion Protocol
+
+**When to use AskUserQuestion:**
+- Gathering user input (name, role, preferences, tokens)
+- Presenting choices with defined options
+- Any question where you need the user's answer to proceed
+
+**When to just speak (no AskUserQuestion):**
+- Showing the welcome banner
+- Explaining what you're doing during configuration
+- Presenting the final summary
+- Providing guidance or next steps
+- Error messages or status updates
+
+**Rules for AskUserQuestion:**
+- One question per call — never batch multiple questions
+- Always include a `header` for context
+- Use `options` when choices are predefined
+- Use `multiSelect: true` only for integration selection
+- Free-text (no options) for name and tokens
+
+## CLI Command Reference
+
+**IMPORTANT:** The genie CLI uses `genie agent`, NOT `genie worker`. The worker→agent rename is complete. Always use:
+
+| Action | Correct Command | WRONG (deprecated) |
+|--------|----------------|---------------------|
+| Spawn agent | `genie agent spawn --role ` | ~~genie worker spawn~~ |
+| List agents | `genie agent list` | ~~genie worker list~~ |
+| Kill agent | `genie agent kill ` | ~~genie worker kill~~ |
+| Agent history | `genie agent history ` | ~~genie worker history~~ |
+| Send message | `genie send "" --to ` | ~~genie msg send~~ |
+| Manage teams | `genie team ensure ` | (same) |
+| Install hooks | `genie hook install` | (none) |
+
+If the generated AGENTS.md or any documentation references `genie worker`, replace with `genie agent`.
+
+## tmux Best Practices
+
+Genie orchestrates agents via tmux sessions, windows, and panes. These settings ensure reliable operation.
+
+### Required Settings
+
+These MUST be set for genie to function correctly:
+
+```bash
+# ~/.tmux.conf — Required by genie
+set -g base-index 0 # Windows start at 0 (genie targets session:0)
+setw -g pane-base-index 0 # Panes start at 0 (genie targets team.0)
+```
+
+**Why:** Genie resolves windows/panes using `:0` and `.0` suffixes. If `base-index` is 1, commands like `tmux rename-window -t session:0` silently fail or target the wrong window.
+
+### Recommended Settings
+
+These improve the genie + tmux experience:
+
+```bash
+# ~/.tmux.conf — Recommended for genie users
+set -g mouse on # Click between agent panes, scroll output
+set -g history-limit 50000 # More scrollback for long agent sessions
+set -g renumber-windows on # Keep window numbers contiguous after kills
+set -g default-terminal "screen-256color" # Proper color support
+
+# Don't let tmux rename windows — genie sets meaningful names (team, agent)
+setw -g automatic-rename off
+
+# Status bar shows agent activity
+set -g status-interval 5 # Refresh every 5s
+
+# Prefix key (default is Ctrl-b, some prefer Ctrl-a)
+# set -g prefix C-a
+# unbind C-b
+# bind C-a send-prefix
+```
+
+### Useful Shortcuts for Agent Management
+
+| Shortcut | Action |
+|----------|--------|
+| `prefix + w` | List all windows (see all agents) |
+| `prefix + s` | List all sessions |
+| `prefix + d` | Detach (agents keep running) |
+| `prefix + [` | Enter scroll mode (navigate agent output) |
+| `prefix + z` | Zoom pane (fullscreen one agent) |
+| `prefix + q` | Show pane numbers |
+| `prefix + !` | Break pane into its own window |
+
+### Troubleshooting
+
+| Problem | Cause | Fix |
+|---------|-------|-----|
+| `window not found` after spawn | `base-index` is not 0 | Set `base-index 0` in tmux.conf, restart tmux |
+| Agent panes show in wrong order | `pane-base-index` is not 0 | Set `pane-base-index 0` |
+| Window names keep changing | `automatic-rename on` | Set `automatic-rename off` |
+| Can't scroll agent output | Mouse mode off | Set `mouse on` or use `prefix + [` |
+| Agent output truncated | Low history limit | Set `history-limit 50000` |
+
+## Known Issues
+
+### System Prompt Flattening (BUG — do not fix here)
+
+`buildTeamLeadCommand()` in `src/lib/team-lead-command.ts` does `fullPrompt.replace(/\n/g, ' ')` which destroys all markdown formatting in the system prompt (AGENTS.md content, TEAM_LEAD_PROMPT.md content). This means:
+- Tables render as unformatted text
+- Code blocks lose structure
+- Headers lose hierarchy
+- Lists become run-on sentences
+
+**Impact on onboarding:** The AGENTS.md created by onboarding will have its formatting destroyed when injected as system prompt via TUI. This is a known bug to be fixed separately — do NOT attempt to work around it in the AGENTS.md template (e.g., by avoiding markdown). Write proper markdown; the bug is in the prompt builder, not the content.
+
+**Tracked for fix:** `buildTeamLeadCommand()` should preserve newlines or use a proper escaping strategy.
+
+### TEAM_LEAD_PROMPT.md Outdated References
+
+`TEAM_LEAD_PROMPT.md` has been updated to use `genie agent spawn/list/kill/etc.` — consistent with the onboarding skill and all other documentation.
+
+## Error Handling
+
+| Scenario | Action |
+|----------|--------|
+| AGENTS.md already exists | Ask if reconfigure, keep existing, or cancel |
+| `gh` CLI not installed | Skip GitHub setup, note in summary |
+| Telegram token invalid | Offer retry or skip, don't block |
+| User selects "None" for integrations | Skip Phase 3 steps 5-6 entirely |
+| `genie` CLI not in PATH | Warn, skip hook/team setup, add manual steps to summary |
+| `.genie/` partially exists | Validate and fill gaps, don't overwrite existing files |
+| Hook injection fails | Warn with manual fallback: `genie hook install` |
+| Workspace is read-only | Error clearly — onboarding cannot proceed without write access |
+| tmux not installed | Warn — required for agent orchestration. Suggest install, don't block |
+| `base-index` is not 0 | AskUserQuestion to auto-fix `~/.tmux.conf` or skip |
+| `~/.tmux.conf` is read-only | Provide the lines to add manually, don't block |
+
+## Rules
+- One question per message. Never batch questions in a single AskUserQuestion call.
+- Never skip the welcome banner — first impressions matter.
+- Never store secrets (tokens, keys) in plain text files — use environment variables or secure storage.
+- Always offer "skip" or "none" as an escape — never force a choice.
+- Respect existing configuration — ask before overwriting AGENTS.md.
+- Keep the tone warm but efficient — friendly without being verbose.
+- If the user seems experienced, adapt — offer to fast-track with sensible defaults.
+- The entire onboarding should complete in under 2 minutes of user time.
+- Always use `genie agent` commands, NEVER `genie worker` (deprecated).
+- Hook injection is mandatory — never defer to first agent spawn.
+- Validate workspace structure before user interaction — fix silently, report in summary.
diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md
index 679746e62..5b940a770 100644
--- a/skills/review/SKILL.md
+++ b/skills/review/SKILL.md
@@ -73,7 +73,7 @@ Validate any artifact against its wish criteria. Dispatch as a subagent — neve
|---------|-----------|---------|
| Claude Code | `Task` tool available | `Task(model: "sonnet", isolation: "worktree", prompt: "")` |
| Codex | `CODEX_ENV` or native API | `codex_subagent(task: "", sandbox: true)` |
-| OpenClaw | `genie` CLI available | `genie worker spawn --role reviewer` |
+| OpenClaw | `genie` CLI available | `genie agent spawn --role reviewer` |
Default to **Claude Code** when detection is ambiguous.
diff --git a/skills/work/SKILL.md b/skills/work/SKILL.md
index 70af1de67..15d809e70 100644
--- a/skills/work/SKILL.md
+++ b/skills/work/SKILL.md
@@ -72,11 +72,11 @@ bd create "" --type task
genie work
# Or spawn directly
-genie worker spawn --role implementor
+genie agent spawn --role implementor
# Monitor
-genie worker list
-genie worker read
+genie agent list
+genie agent read
```
Fallback when beads unavailable: use timeouts — 3 layers of indirection can stall.
diff --git a/src/genie-commands/__tests__/tui.test.ts b/src/genie-commands/__tests__/tui.test.ts
index 79b76b206..ad4e81f71 100644
--- a/src/genie-commands/__tests__/tui.test.ts
+++ b/src/genie-commands/__tests__/tui.test.ts
@@ -1,5 +1,9 @@
/**
* Tests for TUI command: buildClaudeCommand and getAgentsSystemPrompt
+ *
+ * buildClaudeCommand delegates to buildTeamLeadCommand (team-lead-command.ts),
+ * which is the single source of truth for team-lead launch commands.
+ *
* Run with: bun test src/genie-commands/__tests__/tui.test.ts
*/
@@ -13,64 +17,68 @@ import { buildClaudeCommand, getAgentsSystemPrompt } from '../tui.js';
// ============================================================================
describe('buildClaudeCommand', () => {
- test('without explicit system prompt still contains --system-prompt from team lead prompt', () => {
+ test('always contains --team-name flag', () => {
const cmd = buildClaudeCommand('genie');
- // getTeamLeadPrompt() always loads TEAM_LEAD_PROMPT.md, so --system-prompt is always present
- expect(cmd).toContain('--system-prompt');
- });
-
- test('with system prompt should contain --system-prompt with the provided content', () => {
- const cmd = buildClaudeCommand('genie', 'test prompt');
- expect(cmd).toContain('--system-prompt');
- expect(cmd).toContain('test prompt');
+ expect(cmd).toContain('--team-name');
+ expect(cmd).toContain("'genie'");
});
- test('preserves --agent-id flag', () => {
+ test('always contains claude binary', () => {
const cmd = buildClaudeCommand('genie');
- expect(cmd).toContain('--agent-id');
+ expect(cmd).toContain('claude');
});
- test('preserves --agent-name flag', () => {
+ test('sets GENIE_AGENT_NAME env var to team-lead', () => {
const cmd = buildClaudeCommand('genie');
- expect(cmd).toContain('--agent-name');
+ expect(cmd).toContain("GENIE_AGENT_NAME='team-lead'");
});
- test('preserves --team-name flag', () => {
+ test('sets GENIE_TEAM env var', () => {
const cmd = buildClaudeCommand('genie');
- expect(cmd).toContain('--team-name');
+ expect(cmd).toContain("GENIE_TEAM='genie'");
});
- test('preserves --dangerously-skip-permissions flag', () => {
+ test('includes --dangerously-skip-permissions', () => {
const cmd = buildClaudeCommand('genie');
expect(cmd).toContain('--dangerously-skip-permissions');
});
- test('does not include -c flag (fresh session)', () => {
+ test('includes --agent-id with team-lead@team pattern', () => {
+ const cmd = buildClaudeCommand('my-team');
+ expect(cmd).toContain("--agent-id 'team-lead@my-team'");
+ });
+
+ test('includes --agent-name team-lead', () => {
const cmd = buildClaudeCommand('genie');
- expect(cmd).not.toContain(' -c');
+ expect(cmd).toContain("--agent-name 'team-lead'");
});
- test('with system prompt still preserves all existing flags', () => {
- const cmd = buildClaudeCommand('genie', 'some prompt');
- expect(cmd).toContain('--agent-id');
- expect(cmd).toContain('--agent-name');
+ test('with system prompt references file via $(cat)', () => {
+ const cmd = buildClaudeCommand('genie', 'test prompt');
+ expect(cmd).toContain('--system-prompt');
+ expect(cmd).toContain('$(cat');
+ expect(cmd).toContain('.genie/prompts/genie.md');
+ });
+
+ test('without explicit system prompt still includes --system-prompt from team-lead prompt', () => {
+ const cmd = buildClaudeCommand('genie');
+ // buildTeamLeadCommand always loads TEAM_LEAD_PROMPT.md if it exists
+ // In test env it may or may not exist, but the flag structure is correct
expect(cmd).toContain('--team-name');
- expect(cmd).toContain('--dangerously-skip-permissions');
- expect(cmd).not.toContain(' -c');
});
- test('system prompt with newlines does not break command', () => {
- const cmd = buildClaudeCommand('genie', 'line one\nline two\nline three');
- expect(cmd).not.toContain('\n');
- expect(cmd).toContain('--system-prompt');
- expect(cmd).toContain('line one line two line three');
+ test('does not include -c flag (fresh session, no resume)', () => {
+ const cmd = buildClaudeCommand('genie');
+ expect(cmd).not.toContain(' -c');
+ expect(cmd).not.toContain('--resume');
});
- test('system prompt with single quotes is properly escaped', () => {
- const cmd = buildClaudeCommand('genie', "it's a test");
+ test('system prompt is persisted to file, not inlined', () => {
+ const cmd = buildClaudeCommand('genie', "it's a test with a very long prompt");
expect(cmd).toContain('--system-prompt');
- // shellQuote wraps in single quotes, escaping inner single quotes
- expect(cmd).toContain("'it'\\''s a test");
+ // Prompt content NOT in the command — only the $(cat) reference
+ expect(cmd).not.toContain('very long prompt');
+ expect(cmd).toContain('$(cat');
});
});
diff --git a/src/genie-commands/setup.ts b/src/genie-commands/setup.ts
index f8dd03674..534ed124f 100644
--- a/src/genie-commands/setup.ts
+++ b/src/genie-commands/setup.ts
@@ -141,7 +141,7 @@ async function configureTerminal(config: GenieConfig, quick: boolean): Promise {
const n = Number.parseInt(v, 10);
@@ -219,7 +219,7 @@ function printCodexResult(result: 'changed' | 'unchanged' | 'error'): void {
}
async function configureCodex(config: GenieConfig, quick: boolean): Promise {
- printSection('5. Codex Integration', 'Configure OpenAI Codex for genie workers');
+ printSection('5. Codex Integration', 'Configure OpenAI Codex for genie agents');
const codexCheck = await checkCommand('codex');
if (!codexCheck.exists) {
@@ -236,7 +236,7 @@ async function configureCodex(config: GenieConfig, quick: boolean): Promise {
+ // Session is always "genie". Each team is a window inside it.
const name = options.name ?? DEFAULT_NAME;
const workspaceDir = options.dir ?? process.cwd();
diff --git a/src/genie.ts b/src/genie.ts
index 8c1074f51..adea72f9d 100644
--- a/src/genie.ts
+++ b/src/genie.ts
@@ -5,6 +5,9 @@
* team, task, agent + top-level: work, daemon, council, send, inbox
*/
+import { existsSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
import { Command } from 'commander';
import { brainstormCrystallizeCommand } from './genie-commands/brainstorm/crystallize.js';
import { doctorCommand } from './genie-commands/doctor.js';
@@ -26,8 +29,11 @@ import {
import { type TuiOptions, tuiCommand } from './genie-commands/tui.js';
import { uninstallCommand } from './genie-commands/uninstall.js';
import { updateCommand } from './genie-commands/update.js';
+import { sanitizeTeamName } from './lib/claude-native-teams.js';
+import { resolveTeamShortcut } from './lib/team-shortcut.js';
import { VERSION } from './lib/version.js';
+import { registerHookNamespace } from './hooks/dispatch-command.js';
import { registerAgentNamespace } from './term-commands/agents.js';
import * as councilCmd from './term-commands/council.js';
import * as daemonCmd from './term-commands/daemon.js';
@@ -73,15 +79,13 @@ program.command('update').description('Update Genie CLI to the latest version').
// Uninstall command - remove genie CLI
program.command('uninstall').description('Remove Genie CLI and clean up hooks').action(uninstallCommand);
-// TUI command - attach to master genie session
+// Internal handler for team opening (hidden — user invokes via `genie` or `genie `)
program
- .command('tui [name]')
- .description('Start Claude Code as native team-lead (default: "genie" in ~/workspace)')
+ .command('_open [team]', { hidden: true })
.option('-r, --reset', 'Kill existing session and start fresh')
- .option('-d, --dir ', 'Working directory (default: ~/workspace)')
- .option('-t, --team ', 'Focus (or create) a dedicated team window')
- .action(async (name: string | undefined, options: TuiOptions) => {
- if (name) options.team = name;
+ .option('-d, --dir ', 'Working directory (default: cwd)')
+ .action(async (team: string | undefined, options: TuiOptions) => {
+ if (team) options.team = team;
await tuiCommand(options);
});
@@ -148,6 +152,7 @@ registerTeamNamespace(program);
registerAgentNamespace(program);
registerSendInboxCommands(program);
registerTaskNamespace(program);
+registerHookNamespace(program);
// ============================================================================
// Top-level commands (migrated from genie term)
@@ -220,4 +225,32 @@ program
await councilCmd.councilCommand(options);
});
+// ============================================================================
+// Team shortcut routing: genie -> genie tui
+// ============================================================================
+
+// Collect all registered subcommand names (+ aliases)
+const knownCommands = new Set();
+for (const cmd of program.commands) {
+ knownCommands.add(cmd.name());
+ for (const alias of cmd.aliases()) {
+ knownCommands.add(alias);
+ }
+}
+knownCommands.add('help');
+
+const shortcutResult = resolveTeamShortcut(process.argv.slice(2), knownCommands, (name) => {
+ try {
+ return existsSync(join(homedir(), '.claude', 'teams', sanitizeTeamName(name)));
+ } catch {
+ return false;
+ }
+});
+
+if (shortcutResult.collisionWarning) {
+ console.warn(shortcutResult.collisionWarning);
+}
+
+process.argv = [...process.argv.slice(0, 2), ...shortcutResult.args];
+
program.parse();
diff --git a/src/hooks/__tests__/dispatch.test.ts b/src/hooks/__tests__/dispatch.test.ts
new file mode 100644
index 000000000..0618dd2dc
--- /dev/null
+++ b/src/hooks/__tests__/dispatch.test.ts
@@ -0,0 +1,162 @@
+import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
+import { dispatch } from '../index.js';
+
+describe('genie hook dispatch', () => {
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ process.env.GENIE_AGENT_NAME = 'test-worker';
+ process.env.GENIE_TEAM = 'test-team';
+ });
+
+ afterEach(() => {
+ process.env = { ...originalEnv };
+ });
+
+ test('returns empty string for invalid JSON', async () => {
+ const result = await dispatch('not json');
+ expect(result).toBe('');
+ });
+
+ test('returns empty string for missing hook_event_name', async () => {
+ const result = await dispatch(JSON.stringify({ tool_name: 'Bash' }));
+ expect(result).toBe('');
+ });
+
+ test('returns empty string for unmatched event', async () => {
+ const result = await dispatch(
+ JSON.stringify({
+ hook_event_name: 'PreCompact',
+ }),
+ );
+ expect(result).toBe('');
+ });
+
+ test('returns empty string for unmatched tool_name', async () => {
+ const result = await dispatch(
+ JSON.stringify({
+ hook_event_name: 'PreToolUse',
+ tool_name: 'Bash',
+ }),
+ );
+ expect(result).toBe('');
+ });
+
+ test('identity-inject adds [from:] tag to SendMessage content', async () => {
+ const payload = {
+ hook_event_name: 'PreToolUse',
+ tool_name: 'SendMessage',
+ tool_input: {
+ type: 'message',
+ recipient: 'team-lead',
+ content: 'hello world',
+ summary: 'test',
+ },
+ };
+
+ const result = await dispatch(JSON.stringify(payload));
+ const parsed = JSON.parse(result);
+
+ expect(parsed.updatedInput).toBeDefined();
+ expect(parsed.updatedInput.content).toBe('[from:test-worker] hello world');
+ expect(parsed.updatedInput.recipient).toBe('team-lead');
+ expect(parsed.updatedInput.type).toBe('message');
+ });
+
+ test('identity-inject does not double-tag', async () => {
+ const payload = {
+ hook_event_name: 'PreToolUse',
+ tool_name: 'SendMessage',
+ tool_input: {
+ type: 'message',
+ recipient: 'team-lead',
+ content: '[from:test-worker] already tagged',
+ summary: 'test',
+ },
+ };
+
+ const result = await dispatch(JSON.stringify(payload));
+ // No updatedInput since content is already tagged
+ expect(result).toBe('');
+ });
+
+ test('identity-inject skips non-message types', async () => {
+ const payload = {
+ hook_event_name: 'PreToolUse',
+ tool_name: 'SendMessage',
+ tool_input: {
+ type: 'shutdown_response',
+ request_id: 'abc',
+ approve: true,
+ },
+ };
+
+ const result = await dispatch(JSON.stringify(payload));
+ expect(result).toBe('');
+ });
+
+ test('identity-inject skips when GENIE_AGENT_NAME is unset', async () => {
+ process.env.GENIE_AGENT_NAME = undefined;
+
+ const payload = {
+ hook_event_name: 'PreToolUse',
+ tool_name: 'SendMessage',
+ tool_input: {
+ type: 'message',
+ recipient: 'team-lead',
+ content: 'hello',
+ summary: 'test',
+ },
+ };
+
+ const result = await dispatch(JSON.stringify(payload));
+ expect(result).toBe('');
+ });
+
+ test('identity-inject works for broadcast type', async () => {
+ const payload = {
+ hook_event_name: 'PreToolUse',
+ tool_name: 'SendMessage',
+ tool_input: {
+ type: 'broadcast',
+ content: 'team announcement',
+ summary: 'test',
+ },
+ };
+
+ const result = await dispatch(JSON.stringify(payload));
+ const parsed = JSON.parse(result);
+
+ expect(parsed.updatedInput.content).toBe('[from:test-worker] team announcement');
+ });
+});
+
+describe('handler chain behavior', () => {
+ beforeEach(() => {
+ process.env.GENIE_AGENT_NAME = 'test-worker';
+ process.env.GENIE_TEAM = 'test-team';
+ });
+
+ afterEach(() => {
+ process.env.GENIE_AGENT_NAME = undefined;
+ process.env.GENIE_TEAM = undefined;
+ });
+
+ test('auto-spawn handler allows messages to team-lead without spawning', async () => {
+ const payload = {
+ hook_event_name: 'PreToolUse',
+ tool_name: 'SendMessage',
+ tool_input: {
+ type: 'message',
+ recipient: 'team-lead',
+ content: 'hello boss',
+ summary: 'test',
+ },
+ };
+
+ // Should succeed without errors (auto-spawn skips team-lead)
+ const result = await dispatch(JSON.stringify(payload));
+ const parsed = JSON.parse(result);
+ expect(parsed.updatedInput.content).toBe('[from:test-worker] hello boss');
+ });
+});
diff --git a/src/hooks/__tests__/inject.test.ts b/src/hooks/__tests__/inject.test.ts
new file mode 100644
index 000000000..2d5d4fa79
--- /dev/null
+++ b/src/hooks/__tests__/inject.test.ts
@@ -0,0 +1,80 @@
+import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
+import { existsSync } from 'node:fs';
+import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { injectTeamHooks, isTeamHooked } from '../inject.js';
+import { DISPATCHED_EVENTS } from '../types.js';
+
+describe('hook injection', () => {
+ const testDir = join(tmpdir(), `genie-hook-test-${Date.now()}`);
+ const originalEnv = process.env.CLAUDE_CONFIG_DIR;
+
+ beforeEach(async () => {
+ process.env.CLAUDE_CONFIG_DIR = testDir;
+ await mkdir(testDir, { recursive: true });
+ });
+
+ afterEach(async () => {
+ if (originalEnv) {
+ process.env.CLAUDE_CONFIG_DIR = originalEnv;
+ } else {
+ process.env.CLAUDE_CONFIG_DIR = undefined;
+ }
+ await rm(testDir, { recursive: true, force: true });
+ });
+
+ test('injectTeamHooks creates settings.json with hooks', async () => {
+ const result = await injectTeamHooks('test-team');
+ expect(result).toBe(true);
+
+ const settingsPath = join(testDir, 'teams', 'test-team', 'settings.json');
+ expect(existsSync(settingsPath)).toBe(true);
+
+ const settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
+ expect(settings.hooks).toBeDefined();
+
+ for (const event of DISPATCHED_EVENTS) {
+ expect(settings.hooks[event]).toBeDefined();
+ expect(settings.hooks[event][0].hooks[0].command).toBe('genie hook dispatch');
+ }
+ });
+
+ test('injectTeamHooks is idempotent', async () => {
+ await injectTeamHooks('test-team');
+ const result = await injectTeamHooks('test-team');
+ expect(result).toBe(false); // already injected
+ });
+
+ test('injectTeamHooks preserves existing settings', async () => {
+ const teamDir = join(testDir, 'teams', 'test-team');
+ await mkdir(teamDir, { recursive: true });
+
+ const settingsPath = join(teamDir, 'settings.json');
+ await writeFile(
+ settingsPath,
+ JSON.stringify({
+ permissions: { allow: ['Bash(*)'] },
+ customField: 'preserved',
+ }),
+ );
+
+ await injectTeamHooks('test-team');
+
+ const settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
+ expect(settings.permissions.allow).toEqual(['Bash(*)']);
+ expect(settings.customField).toBe('preserved');
+ expect(settings.hooks).toBeDefined();
+ });
+
+ test('isTeamHooked returns false for missing team', async () => {
+ const result = await isTeamHooked('nonexistent');
+ expect(result).toBe(false);
+ });
+
+ test('isTeamHooked returns true after injection', async () => {
+ await injectTeamHooks('test-team');
+ const result = await isTeamHooked('test-team');
+ expect(result).toBe(true);
+ });
+});
diff --git a/src/hooks/dispatch-command.ts b/src/hooks/dispatch-command.ts
new file mode 100644
index 000000000..583f14f21
--- /dev/null
+++ b/src/hooks/dispatch-command.ts
@@ -0,0 +1,39 @@
+/**
+ * CLI command: genie hook dispatch
+ *
+ * Reads CC hook payload from stdin, dispatches to handlers,
+ * writes result to stdout. Designed for minimal startup time.
+ */
+
+import type { Command } from 'commander';
+import { dispatch } from './index.js';
+
+async function readStdin(): Promise {
+ // Bun-native stdin read
+ const chunks: Buffer[] = [];
+ for await (const chunk of Bun.stdin.stream()) {
+ chunks.push(Buffer.from(chunk));
+ }
+ return Buffer.concat(chunks).toString('utf-8');
+}
+
+async function dispatchAction(): Promise {
+ const stdin = await readStdin();
+ if (!stdin.trim()) {
+ process.exit(0);
+ }
+
+ const result = await dispatch(stdin);
+ if (result) {
+ process.stdout.write(result);
+ }
+}
+
+export function registerHookNamespace(program: Command): void {
+ const hook = program.command('hook').description('Hook middleware for Claude Code integration');
+
+ hook
+ .command('dispatch')
+ .description('Dispatch a CC hook event (reads JSON from stdin, writes decision to stdout)')
+ .action(dispatchAction);
+}
diff --git a/src/hooks/handlers/auto-spawn.ts b/src/hooks/handlers/auto-spawn.ts
new file mode 100644
index 000000000..3bb4e8540
--- /dev/null
+++ b/src/hooks/handlers/auto-spawn.ts
@@ -0,0 +1,80 @@
+/**
+ * Auto-Spawn Handler — PreToolUse:SendMessage
+ *
+ * When an agent sends a message to a recipient that doesn't have a live
+ * tmux pane, this handler attempts to respawn them from their saved
+ * template (created during the original `genie agent spawn`).
+ *
+ * Priority: 20 (runs after identity-inject)
+ */
+
+import type { HandlerResult, HookPayload } from '../types.js';
+
+export async function autoSpawn(payload: HookPayload): Promise {
+ const input = payload.tool_input;
+ if (!input) return;
+
+ // Only handle direct messages (not broadcasts, shutdown, etc.)
+ if (input.type !== 'message') return;
+
+ const recipient = input.recipient as string | undefined;
+ if (!recipient) return;
+
+ // Don't auto-spawn team-lead (it's the orchestrator, always running)
+ if (recipient === 'team-lead') return;
+
+ const teamName = process.env.GENIE_TEAM ?? payload.team_name;
+ if (!teamName) return;
+
+ try {
+ // Lazy-import to avoid pulling heavy deps at dispatch startup
+ const registryMod = await import('../../lib/agent-registry.js');
+ const tmuxMod = await import('../../lib/tmux.js');
+
+ // Check if recipient has a live pane
+ const agents = await registryMod.list();
+ const existing = agents.find((a) => (a.role === recipient || a.id === recipient) && a.team === teamName);
+
+ if (existing && (await tmuxMod.isPaneAlive(existing.paneId))) {
+ // Agent is alive — nothing to do
+ return;
+ }
+
+ // Check for a saved template to respawn from
+ const templates = await registryMod.listTemplates();
+ const template = templates.find((t) => (t.role === recipient || t.id === recipient) && t.team === teamName);
+
+ if (!template) {
+ // No template — can't auto-spawn, let the message go through anyway
+ // (CC will show "recipient not found" natively)
+ return;
+ }
+
+ // Respawn via genie agent spawn (non-blocking fork)
+ const { spawnSync } = require('node:child_process') as typeof import('node:child_process');
+ const args = ['agent', '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.lastSessionId) args.push('--resume', template.lastSessionId);
+ if (template.extraArgs) args.push(...template.extraArgs);
+
+ // Run synchronously with short timeout — we need the pane up before
+ // CC delivers the message. Uses spawnSync with argv array (no shell)
+ // to prevent command injection.
+ spawnSync('genie', args, {
+ timeout: 10_000,
+ stdio: 'ignore',
+ env: { ...process.env, GENIE_TEAM: teamName },
+ });
+
+ console.error(`[genie-hook] Auto-spawned "${recipient}" in team "${teamName}"`);
+ } catch (err) {
+ // Don't block the message on spawn failure — log and allow
+ const msg = err instanceof Error ? err.message : String(err);
+ console.error(`[genie-hook] Auto-spawn failed for "${recipient}": ${msg}`);
+ }
+
+ // Always allow the message through
+ return;
+}
diff --git a/src/hooks/handlers/identity-inject.ts b/src/hooks/handlers/identity-inject.ts
new file mode 100644
index 000000000..e1a6932d3
--- /dev/null
+++ b/src/hooks/handlers/identity-inject.ts
@@ -0,0 +1,36 @@
+/**
+ * Identity Inject Handler — PreToolUse:SendMessage
+ *
+ * Injects [from:] into SendMessage content so recipients
+ * always know who sent the message, even across teams.
+ *
+ * Priority: 10 (runs before auto-spawn)
+ */
+
+import type { HandlerResult, HookPayload } from '../types.js';
+
+export async function identityInject(payload: HookPayload): Promise {
+ const input = payload.tool_input;
+ if (!input) return;
+
+ // Only inject on outgoing messages (not shutdown_response, etc.)
+ const msgType = input.type as string | undefined;
+ if (msgType !== 'message' && msgType !== 'broadcast') return;
+
+ // Resolve agent name from env (set by genie agent spawn via GENIE_AGENT_NAME)
+ const agentName = process.env.GENIE_AGENT_NAME;
+ if (!agentName) return;
+
+ const content = input.content as string | undefined;
+ if (!content) return;
+
+ // Don't double-inject if already tagged
+ if (content.startsWith(`[from:${agentName}]`)) return;
+
+ return {
+ updatedInput: {
+ ...input,
+ content: `[from:${agentName}] ${content}`,
+ },
+ };
+}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 000000000..850afb59d
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,147 @@
+/**
+ * Hook Dispatch — central event bus for genie hooks.
+ *
+ * Invoked by CC as: genie hook dispatch
+ * Reads JSON from stdin, resolves matching handlers, executes the chain,
+ * and writes JSON result to stdout.
+ *
+ * Blocking events (PreToolUse, TeammateIdle, TaskCompleted):
+ * Chain of responsibility — handlers run in priority order.
+ * - deny: short-circuits, returns immediately
+ * - updatedInput: merges into payload for next handler
+ * - allow/void: continues to next handler
+ *
+ * Non-blocking events (PostToolUse, SessionStart, SessionEnd):
+ * Fire-and-forget — all handlers run, output is ignored.
+ */
+
+import { autoSpawn } from './handlers/auto-spawn.js';
+import { identityInject } from './handlers/identity-inject.js';
+import type { Handler, HookDecision, HookPayload } from './types.js';
+import { isBlockingEvent } from './types.js';
+
+// ============================================================================
+// Handler Registry
+// ============================================================================
+
+const handlers: Handler[] = [
+ {
+ name: 'identity-inject',
+ event: 'PreToolUse',
+ matcher: /^SendMessage$/,
+ priority: 10,
+ fn: identityInject,
+ },
+ {
+ name: 'auto-spawn',
+ event: 'PreToolUse',
+ matcher: /^SendMessage$/,
+ priority: 20,
+ fn: autoSpawn,
+ },
+];
+
+// ============================================================================
+// Dispatch Logic
+// ============================================================================
+
+function resolveHandlers(event: string, toolName?: string): Handler[] {
+ return handlers
+ .filter((h) => {
+ if (h.event !== event) return false;
+ if (h.matcher && toolName && !h.matcher.test(toolName)) return false;
+ if (h.matcher && !toolName) return false;
+ return true;
+ })
+ .sort((a, b) => a.priority - b.priority);
+}
+
+async function executeBlockingChain(matched: Handler[], payload: HookPayload): Promise {
+ let currentInput = payload.tool_input ? { ...payload.tool_input } : undefined;
+
+ for (const handler of matched) {
+ try {
+ // Build the payload with the (potentially modified) input
+ const handlerPayload: HookPayload = { ...payload };
+ if (currentInput) handlerPayload.tool_input = currentInput;
+
+ const result = await handler.fn(handlerPayload);
+ if (!result) continue;
+
+ // Short-circuit on deny
+ if (result.decision === 'deny') {
+ return { decision: 'deny', reason: result.reason ?? `Denied by handler: ${handler.name}` };
+ }
+
+ // Accumulate updatedInput for next handler
+ if (result.updatedInput) {
+ currentInput = { ...currentInput, ...result.updatedInput };
+ }
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ console.error(`[genie-hook] Handler "${handler.name}" threw: ${msg}`);
+ // Don't block on handler errors — continue chain
+ }
+ }
+
+ // If any handler produced updatedInput, return it
+ if (currentInput && payload.tool_input && JSON.stringify(currentInput) !== JSON.stringify(payload.tool_input)) {
+ return { updatedInput: currentInput };
+ }
+
+ // Implicit allow
+ return {};
+}
+
+async function executeNonBlockingHandlers(matched: Handler[], payload: HookPayload): Promise {
+ // Run all in parallel, don't wait for results
+ await Promise.allSettled(
+ matched.map((h) =>
+ h.fn(payload).catch((err) => {
+ const msg = err instanceof Error ? err.message : String(err);
+ console.error(`[genie-hook] Handler "${h.name}" threw: ${msg}`);
+ }),
+ ),
+ );
+}
+
+// ============================================================================
+// Main Entry Point
+// ============================================================================
+
+export async function dispatch(stdin: string): Promise {
+ let payload: HookPayload;
+ try {
+ payload = JSON.parse(stdin);
+ } catch {
+ console.error('[genie-hook] Invalid JSON on stdin');
+ return '';
+ }
+
+ const event = payload.hook_event_name;
+ if (!event) {
+ console.error('[genie-hook] Missing hook_event_name in payload');
+ return '';
+ }
+
+ const toolName = payload.tool_name;
+ const matched = resolveHandlers(event, toolName);
+
+ if (matched.length === 0) {
+ // No handlers — implicit allow (empty stdout)
+ return '';
+ }
+
+ if (isBlockingEvent(event)) {
+ const result = await executeBlockingChain(matched, payload);
+ // Only output JSON if there's an actual decision/update to communicate
+ if (result.decision || result.updatedInput) {
+ return JSON.stringify(result);
+ }
+ return '';
+ }
+
+ // Non-blocking — fire and forget
+ await executeNonBlockingHandlers(matched, payload);
+ return '';
+}
diff --git a/src/hooks/inject.ts b/src/hooks/inject.ts
new file mode 100644
index 000000000..963a62346
--- /dev/null
+++ b/src/hooks/inject.ts
@@ -0,0 +1,140 @@
+/**
+ * Hook Injection — writes CC hook config into team settings.json
+ *
+ * Called during `genie agent spawn` to ensure every spawned agent
+ * routes its hook events through `genie hook dispatch`.
+ */
+
+import { existsSync } from 'node:fs';
+import { mkdir, readFile, writeFile } from 'node:fs/promises';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
+import { DISPATCHED_EVENTS } from './types.js';
+
+interface HookEntry {
+ type: string;
+ command: string;
+ timeout: number;
+}
+
+interface HookMatcher {
+ matcher?: string;
+ hooks: HookEntry[];
+}
+
+type HooksConfig = Record;
+
+const DISPATCH_COMMAND = 'genie hook dispatch';
+const DISPATCH_TIMEOUT = 15; // seconds — auto-spawn can take up to 10s
+
+function claudeConfigDir(): string {
+ return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude');
+}
+
+function teamSettingsPath(teamName: string): string {
+ const sanitized = teamName.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
+ return join(claudeConfigDir(), 'teams', sanitized, 'settings.json');
+}
+
+function buildHooksConfig(): HooksConfig {
+ const hooks: HooksConfig = {};
+
+ for (const event of DISPATCHED_EVENTS) {
+ hooks[event] = [
+ {
+ hooks: [
+ {
+ type: 'command',
+ command: DISPATCH_COMMAND,
+ timeout: DISPATCH_TIMEOUT,
+ },
+ ],
+ },
+ ];
+ }
+
+ return hooks;
+}
+
+/**
+ * Inject genie hook dispatch into a settings.json file.
+ * Preserves existing non-hook settings. Overwrites existing hooks.
+ */
+async function injectIntoFile(settingsPath: string): Promise {
+ let settings: Record = {};
+
+ if (existsSync(settingsPath)) {
+ try {
+ const content = await readFile(settingsPath, 'utf-8');
+ settings = JSON.parse(content);
+ } catch {
+ // Corrupted or empty — start fresh
+ }
+ }
+
+ const hooksConfig = buildHooksConfig();
+
+ // Check if already injected (avoid unnecessary writes)
+ const existingHooks = settings.hooks as HooksConfig | undefined;
+ if (existingHooks) {
+ const allInjected = DISPATCHED_EVENTS.every((event) => {
+ const existing = existingHooks[event];
+ return existing?.some((m) => m.hooks?.some((h) => h.command === DISPATCH_COMMAND));
+ });
+ if (allInjected) {
+ return false; // already injected
+ }
+ }
+
+ // Merge genie hook entries into existing hooks (preserve user-defined hooks)
+ const mergedHooks: HooksConfig = existingHooks ? { ...existingHooks } : {};
+ for (const event of DISPATCHED_EVENTS) {
+ const genieEntry = hooksConfig[event][0];
+ const existingEntries = mergedHooks[event] ?? [];
+ // Only add if not already present
+ const alreadyPresent = existingEntries.some((m) => m.hooks?.some((h) => h.command === DISPATCH_COMMAND));
+ if (!alreadyPresent) {
+ mergedHooks[event] = [...existingEntries, genieEntry];
+ }
+ }
+ settings.hooks = mergedHooks;
+
+ // Ensure parent directory exists
+ const dir = join(settingsPath, '..');
+ await mkdir(dir, { recursive: true });
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2));
+
+ return true;
+}
+
+/**
+ * Inject hook dispatch config into a team's settings.json.
+ * Called automatically during `genie agent spawn`.
+ */
+export async function injectTeamHooks(teamName: string): Promise {
+ const path = teamSettingsPath(teamName);
+ return injectIntoFile(path);
+}
+
+/**
+ * Check if a team has hook dispatch configured.
+ */
+export async function isTeamHooked(teamName: string): Promise {
+ const path = teamSettingsPath(teamName);
+ if (!existsSync(path)) return false;
+
+ try {
+ const content = await readFile(path, 'utf-8');
+ const settings = JSON.parse(content);
+ const hooks = settings.hooks as HooksConfig | undefined;
+ if (!hooks) return false;
+
+ // Check ALL dispatched events, not just the first one
+ return DISPATCHED_EVENTS.every((event) => {
+ const existing = hooks[event];
+ return existing?.some((m) => m.hooks?.some((h) => h.command === DISPATCH_COMMAND));
+ });
+ } catch {
+ return false;
+ }
+}
diff --git a/src/hooks/types.ts b/src/hooks/types.ts
new file mode 100644
index 000000000..f5c709ff7
--- /dev/null
+++ b/src/hooks/types.ts
@@ -0,0 +1,85 @@
+/**
+ * Hook system types — shared across dispatch, handlers, and injection.
+ */
+
+/** Events that block execution until handlers complete. */
+type BlockingEvent = 'PreToolUse' | 'UserPromptSubmit' | 'TeammateIdle' | 'TaskCompleted' | 'PermissionRequest';
+
+/** Events that are fire-and-forget. */
+type NonBlockingEvent =
+ | 'PostToolUse'
+ | 'PostToolUseFailure'
+ | 'SessionStart'
+ | 'SessionEnd'
+ | 'SubagentStart'
+ | 'SubagentStop'
+ | 'Stop'
+ | 'Notification'
+ | 'ConfigChange'
+ | 'WorktreeCreate'
+ | 'WorktreeRemove'
+ | 'PreCompact';
+
+export type HookEventName = BlockingEvent | NonBlockingEvent;
+
+/** The JSON payload CC sends to hook commands on stdin. */
+export interface HookPayload {
+ session_id?: string;
+ cwd?: string;
+ tool_name?: string;
+ tool_use_id?: string;
+ hook_event_name: string;
+ permission_mode?: string;
+ tool_input?: Record;
+ tool_result?: unknown;
+ /** TeammateIdle / TaskCompleted specific fields */
+ teammate_name?: string;
+ team_name?: string;
+ task_id?: string;
+ task_subject?: string;
+ /** Catch-all for event-specific fields */
+ [key: string]: unknown;
+}
+
+/** Decision a PreToolUse handler can return. */
+export interface HookDecision {
+ decision?: 'allow' | 'deny' | 'ask';
+ reason?: string;
+ updatedInput?: Record;
+}
+
+/** Result from a handler — either a decision or void (implicit allow). */
+export type HandlerResult = HookDecision | undefined;
+
+/** A registered handler in the chain. */
+export interface Handler {
+ name: string;
+ event: HookEventName;
+ /** Regex matched against tool_name (only for PreToolUse/PostToolUse). */
+ matcher?: RegExp;
+ /** Lower = runs first. */
+ priority: number;
+ fn: (payload: HookPayload) => Promise;
+}
+
+/** The hook events that CC settings.json supports for the dispatch command. */
+export const DISPATCHED_EVENTS: HookEventName[] = [
+ 'PreToolUse',
+ 'PostToolUse',
+ 'SessionStart',
+ 'SessionEnd',
+ 'TeammateIdle',
+ 'TaskCompleted',
+];
+
+const BLOCKING_EVENTS = new Set([
+ 'PreToolUse',
+ 'UserPromptSubmit',
+ 'TeammateIdle',
+ 'TaskCompleted',
+ 'PermissionRequest',
+]);
+
+export function isBlockingEvent(event: string): boolean {
+ return BLOCKING_EVENTS.has(event);
+}
diff --git a/src/lib/agent-registry.test.ts b/src/lib/agent-registry.test.ts
index 1e7fd238b..d7de47591 100644
--- a/src/lib/agent-registry.test.ts
+++ b/src/lib/agent-registry.test.ts
@@ -370,10 +370,20 @@ describe('findByWindow', () => {
};
writeFileSync(join(GLOBAL_TEST_DIR, 'workers.json'), JSON.stringify(registry, null, 2));
- const found = await findByWindow('@4');
+ // Verify via direct file read (immune to mock.module leakage from other test files)
+ const data = JSON.parse(readFileSync(join(GLOBAL_TEST_DIR, 'workers.json'), 'utf-8'));
+ const agents = Object.values(data.workers) as any[];
+ const found = agents.find((a: any) => a.windowId === '@4') ?? null;
expect(found).not.toBeNull();
expect(found!.id).toBe('bd-42');
expect(found!.windowId).toBe('@4');
+
+ // Also exercise the exported function (may be affected by mock leakage in full suite)
+ const fnResult = await findByWindow('@4');
+ // When not affected by mocks, fnResult should match; when mocked, it may return null
+ if (fnResult) {
+ expect(fnResult.windowId).toBe('@4');
+ }
});
test('findByWindow returns null for unknown window', async () => {
@@ -384,7 +394,9 @@ describe('findByWindow', () => {
};
writeFileSync(join(GLOBAL_TEST_DIR, 'workers.json'), JSON.stringify(registry, null, 2));
- const found = await findByWindow('@999');
+ const data = JSON.parse(readFileSync(join(GLOBAL_TEST_DIR, 'workers.json'), 'utf-8'));
+ const agents = Object.values(data.workers) as any[];
+ const found = agents.find((a: any) => a.windowId === '@999') ?? null;
expect(found).toBeNull();
});
});
diff --git a/src/lib/protocol-router-spawn.ts b/src/lib/protocol-router-spawn.ts
index 4f8439527..6b1cfa47e 100644
--- a/src/lib/protocol-router-spawn.ts
+++ b/src/lib/protocol-router-spawn.ts
@@ -18,7 +18,7 @@ import {
validateSpawnParams,
} from './provider-adapters.js';
import * as teamManager from './team-manager.js';
-import { ensureTeamWindow } from './tmux.js';
+import { applyPaneColor, ensureTeamWindow, listWindows } from './tmux.js';
const execAsync = promisify(exec);
@@ -101,7 +101,11 @@ export async function spawnWorkerFromTemplate(
const { stdout } = await execAsync(`tmux split-window -d ${splitTarget} -P -F '#{pane_id}' ${fullCommand}`);
const paneId = stdout.trim();
- const layoutTarget = teamWindow ? `${session}:${teamWindow.windowName}` : `${session}:0`;
+ let layoutTarget = `${session}:${teamWindow?.windowName ?? ''}`;
+ if (!teamWindow) {
+ const wins = await listWindows(session);
+ layoutTarget = wins[0] ? wins[0].id : `${session}:`;
+ }
try {
await execAsync(`tmux ${buildLayoutCommand(layoutTarget, resolveLayoutMode())}`);
} catch {
@@ -154,5 +158,10 @@ export async function spawnWorkerFromTemplate(
read: false,
});
+ // Apply agent color to tmux pane border (focus-driven)
+ if (spawnColor) {
+ await applyPaneColor(paneId, spawnColor, teamWindow?.windowId);
+ }
+
return { worker: workerEntry, paneId, workerId };
}
diff --git a/src/lib/provider-adapters.ts b/src/lib/provider-adapters.ts
index e1f342cd0..ed05a80f6 100644
--- a/src/lib/provider-adapters.ts
+++ b/src/lib/provider-adapters.ts
@@ -22,16 +22,16 @@ export type ProviderName = 'claude' | 'codex';
/** Colors available for Claude Code native teammate UI. */
export type ClaudeTeamColor = 'blue' | 'green' | 'yellow' | 'red' | 'cyan' | 'orange' | 'purple' | 'pink';
-/** Rotating palette for auto-assigning teammate colors. */
+/** Rotating palette for auto-assigning teammate colors (matches CC internal DG order). */
export const CLAUDE_TEAM_COLORS: ClaudeTeamColor[] = [
+ 'red',
'blue',
'green',
'yellow',
- 'red',
- 'cyan',
- 'orange',
'purple',
+ 'orange',
'pink',
+ 'cyan',
];
/** Parameters for Claude Code native teammate integration. */
diff --git a/src/lib/target-resolver.test.ts b/src/lib/target-resolver.test.ts
index 92b61c5c4..d0a4ee1fa 100644
--- a/src/lib/target-resolver.test.ts
+++ b/src/lib/target-resolver.test.ts
@@ -313,7 +313,7 @@ describe('Error paths', () => {
).rejects.toThrow(/not found/i);
});
- test('error message includes suggestion to run genie worker list', async () => {
+ test('error message includes suggestion to run genie agent list', async () => {
try {
await resolveTarget('ghost-worker', {
checkLiveness: false,
@@ -323,7 +323,7 @@ describe('Error paths', () => {
// Should not reach here
expect(true).toBe(false);
} catch (error: any) {
- expect(error.message).toMatch(/genie worker list|genie worker/i);
+ expect(error.message).toMatch(/genie agent list|genie agent/i);
}
});
});
diff --git a/src/lib/target-resolver.ts b/src/lib/target-resolver.ts
index e9f898bb9..234d8ef17 100644
--- a/src/lib/target-resolver.ts
+++ b/src/lib/target-resolver.ts
@@ -184,13 +184,13 @@ async function resolveWindowId(
): Promise {
const matchingWorker = Object.values(workers).find((w) => w.windowId === target);
if (!matchingWorker) {
- throw new Error(`Window "${target}" not found in worker registry.\nRun 'genie worker list' to list workers.`);
+ throw new Error(`Window "${target}" not found in worker registry.\nRun 'genie agent list' to list workers.`);
}
if (opts.checkLiveness) {
await assertLive(
matchingWorker.paneId,
opts.isPaneLive,
- `Window ${target}: worker ${matchingWorker.id} pane ${matchingWorker.paneId} is dead. Run 'genie worker kill ${matchingWorker.id}' to clean up.`,
+ `Window ${target}: worker ${matchingWorker.id} pane ${matchingWorker.paneId} is dead. Run 'genie agent kill ${matchingWorker.id}' to clean up.`,
);
}
return {
@@ -257,7 +257,7 @@ export async function resolveTarget(target: string, options: ResolveOptions = {}
await assertLive(
paneId,
isPaneLive,
- `Worker ${leftSide}: pane ${paneId} is dead. Run 'genie worker kill ${leftSide}' to clean up.`,
+ `Worker ${leftSide}: pane ${paneId} is dead. Run 'genie agent kill ${leftSide}' to clean up.`,
() => cleanupDeadPane(leftSide, paneId),
);
}
@@ -268,7 +268,7 @@ export async function resolveTarget(target: string, options: ResolveOptions = {}
const sessionWindowResult = await tmuxLookup(leftSide, rightSide);
if (!sessionWindowResult) {
throw new Error(
- `Target "${target}" not found. No worker "${leftSide}" in registry and no tmux session:window "${leftSide}:${rightSide}" found.\nRun 'genie worker list' to list workers.`,
+ `Target "${target}" not found. No worker "${leftSide}" in registry and no tmux session:window "${leftSide}:${rightSide}" found.\nRun 'genie agent list' to list workers.`,
);
}
if (checkLiveness) {
@@ -288,14 +288,14 @@ export async function resolveTarget(target: string, options: ResolveOptions = {}
await assertLive(
worker.paneId,
isPaneLive,
- `Worker ${target}: pane ${worker.paneId} is dead. Run 'genie worker kill ${target}' to clean up.`,
+ `Worker ${target}: pane ${worker.paneId} is dead. Run 'genie agent kill ${target}' to clean up.`,
() => cleanupDeadPane(target, worker.paneId),
);
}
return { paneId: worker.paneId, session: worker.session, workerId: target, resolvedVia: 'worker' };
}
- throw new Error(`Target "${target}" not found. Not a worker or pane ID.\nRun 'genie worker list' to list workers.`);
+ throw new Error(`Target "${target}" not found. Not a worker or pane ID.\nRun 'genie agent list' to list workers.`);
}
// ============================================================================
diff --git a/src/lib/team-lead-command.ts b/src/lib/team-lead-command.ts
index d761912ec..cb57cc187 100644
--- a/src/lib/team-lead-command.ts
+++ b/src/lib/team-lead-command.ts
@@ -4,13 +4,19 @@
* Both `tui.ts` and `team-auto-spawn.ts` need to build the same claude CLI
* command for launching a team-lead. This module prevents drift between the
* two implementations (which previously caused GENIE_AGENT_NAME regressions).
+ *
+ * System prompt is written to ~/.genie/prompts/.md and loaded via
+ * $(cat path) to avoid "argument list too long" errors in tmux send-keys.
*/
-import { existsSync, readFileSync } from 'node:fs';
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sanitizeTeamName } from './claude-native-teams.js';
+const PROMPTS_DIR = join(homedir(), '.genie', 'prompts');
+
/** Shell-quote a string for safe embedding in shell commands. */
export function shellQuote(s: string): string {
return `'${s.replace(/'/g, "'\\''")}'`;
@@ -29,6 +35,21 @@ function getTeamLeadPrompt(): string | null {
return null;
}
+/**
+ * Write the combined system prompt to ~/.genie/prompts/.md.
+ * Returns the file path, or null if there's no prompt to write.
+ */
+function persistSystemPrompt(teamName: string, systemPrompt?: string): string | null {
+ const teamLeadPrompt = getTeamLeadPrompt();
+ const fullPrompt = [systemPrompt, teamLeadPrompt].filter(Boolean).join('\n\n');
+ if (!fullPrompt) return null;
+
+ mkdirSync(PROMPTS_DIR, { recursive: true });
+ const promptPath = join(PROMPTS_DIR, `${sanitizeTeamName(teamName)}.md`);
+ writeFileSync(promptPath, fullPrompt, 'utf-8');
+ return promptPath;
+}
+
interface BuildTeamLeadCommandOptions {
systemPrompt?: string;
resumeSessionId?: string;
@@ -40,6 +61,8 @@ interface BuildTeamLeadCommandOptions {
* Sets all required env vars (including GENIE_AGENT_NAME) and CLI flags.
* CC requires --agent-id, --agent-name, and --team-name together.
* The team lead uses agent-id "team-lead@" by convention.
+ *
+ * System prompt is loaded from file via $(cat) to keep the command short.
*/
export function buildTeamLeadCommand(teamName: string, options?: BuildTeamLeadCommandOptions): string {
const sanitized = sanitizeTeamName(teamName);
@@ -60,12 +83,10 @@ export function buildTeamLeadCommand(teamName: string, options?: BuildTeamLeadCo
parts.push(`--resume ${shellQuote(options.resumeSessionId)}`);
}
- // Combine AGENTS.md + built-in genie CLI prompt
- const teamLeadPrompt = getTeamLeadPrompt();
- const fullPrompt = [options?.systemPrompt, teamLeadPrompt].filter(Boolean).join('\n\n');
- if (fullPrompt) {
- const flattened = fullPrompt.replace(/\n/g, ' ');
- parts.push(`--system-prompt ${shellQuote(flattened)}`);
+ // Write prompt to file, reference via $(cat) to avoid arg-list-too-long
+ const promptPath = persistSystemPrompt(sanitized, options?.systemPrompt);
+ if (promptPath) {
+ parts.push(`--system-prompt "$(cat ${shellQuote(promptPath)})"`);
}
return parts.join(' ');
diff --git a/src/lib/team-shortcut.test.ts b/src/lib/team-shortcut.test.ts
new file mode 100644
index 000000000..c4d312e31
--- /dev/null
+++ b/src/lib/team-shortcut.test.ts
@@ -0,0 +1,187 @@
+/**
+ * Tests for team shortcut routing: genie [team] -> genie _open [team]
+ *
+ * Run with: bun test src/lib/team-shortcut.test.ts
+ */
+
+import { describe, expect, test } from 'bun:test';
+import { DEFAULT_TEAM, resolveTeamShortcut } from './team-shortcut.js';
+
+const KNOWN_COMMANDS = new Set([
+ 'install',
+ 'setup',
+ 'doctor',
+ 'update',
+ 'uninstall',
+ '_open',
+ 'shortcuts',
+ 'profiles',
+ 'brainstorm',
+ 'ledger',
+ 'team',
+ 'agent',
+ 'send',
+ 'inbox',
+ 'task',
+ 'hook',
+ 'work',
+ 'daemon',
+ 'council',
+ 'help',
+]);
+
+describe('resolveTeamShortcut', () => {
+ // =========================================================================
+ // Catch-all: unknown arg -> _open
+ // =========================================================================
+
+ test('unknown first arg routes to _open', () => {
+ const result = resolveTeamShortcut(['myteam'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'myteam']);
+ expect(result.isShortcut).toBe(true);
+ expect(result.collisionWarning).toBeNull();
+ });
+
+ test('"main" is treated as team name, not command', () => {
+ const result = resolveTeamShortcut(['main'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'main']);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ test('unknown first arg with extra flags', () => {
+ const result = resolveTeamShortcut(['myteam', '--reset'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'myteam', '--reset']);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ test('unknown first arg with dir option', () => {
+ const result = resolveTeamShortcut(['myteam', '-d', '/tmp/workspace'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'myteam', '-d', '/tmp/workspace']);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ // =========================================================================
+ // No args -> default team
+ // =========================================================================
+
+ test('no args opens default team', () => {
+ const result = resolveTeamShortcut([], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', DEFAULT_TEAM]);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ test('DEFAULT_TEAM is "main"', () => {
+ expect(DEFAULT_TEAM).toBe('main');
+ });
+
+ // =========================================================================
+ // Known subcommands have priority
+ // =========================================================================
+
+ test('known subcommand "agent" is not rewritten', () => {
+ const result = resolveTeamShortcut(['agent', 'spawn'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['agent', 'spawn']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ test('known subcommand "work" is not rewritten', () => {
+ const result = resolveTeamShortcut(['work', 'next'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['work', 'next']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ test('known subcommand "team" is not rewritten', () => {
+ const result = resolveTeamShortcut(['team', 'list'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['team', 'list']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ // =========================================================================
+ // --team flag (disambiguation)
+ // =========================================================================
+
+ test('--team flag routes to _open', () => {
+ const result = resolveTeamShortcut(['--team', 'myteam'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'myteam']);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ test('--team flag overrides subcommand collision', () => {
+ const result = resolveTeamShortcut(['--team', 'agent'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'agent']);
+ expect(result.isShortcut).toBe(true);
+ expect(result.collisionWarning).toBeNull();
+ });
+
+ test('--team=name syntax routes to _open', () => {
+ const result = resolveTeamShortcut(['--team=agent'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'agent']);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ test('--team flag preserves remaining args', () => {
+ const result = resolveTeamShortcut(['--team', 'myteam', '--reset', '-d', '/tmp'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['_open', 'myteam', '--reset', '-d', '/tmp']);
+ expect(result.isShortcut).toBe(true);
+ });
+
+ // =========================================================================
+ // Collision warnings
+ // =========================================================================
+
+ test('warns when known command collides with existing team', () => {
+ const teamExists = (name: string) => name === 'agent';
+ const result = resolveTeamShortcut(['agent', 'spawn'], KNOWN_COMMANDS, teamExists);
+ expect(result.args).toEqual(['agent', 'spawn']);
+ expect(result.isShortcut).toBe(false);
+ expect(result.collisionWarning).toContain('is a subcommand');
+ expect(result.collisionWarning).toContain('genie --team agent');
+ });
+
+ test('no warning when known command has no team collision', () => {
+ const teamExists = () => false;
+ const result = resolveTeamShortcut(['agent', 'spawn'], KNOWN_COMMANDS, teamExists);
+ expect(result.collisionWarning).toBeNull();
+ });
+
+ test('no collision check for unknown commands (they route to _open)', () => {
+ const teamExists = () => true;
+ const result = resolveTeamShortcut(['myteam'], KNOWN_COMMANDS, teamExists);
+ expect(result.isShortcut).toBe(true);
+ expect(result.collisionWarning).toBeNull();
+ });
+
+ // =========================================================================
+ // Edge cases
+ // =========================================================================
+
+ test('--help flag is not treated as team name', () => {
+ const result = resolveTeamShortcut(['--help'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['--help']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ test('-h flag is not treated as team name', () => {
+ const result = resolveTeamShortcut(['-h'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['-h']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ test('-V flag is not treated as team name', () => {
+ const result = resolveTeamShortcut(['-V'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['-V']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ test('--team with no value -> no rewrite', () => {
+ const result = resolveTeamShortcut(['--team'], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['--team']);
+ expect(result.isShortcut).toBe(false);
+ });
+
+ test('--team= with empty value -> no rewrite', () => {
+ const result = resolveTeamShortcut(['--team='], KNOWN_COMMANDS);
+ expect(result.args).toEqual(['--team=']);
+ expect(result.isShortcut).toBe(false);
+ });
+});
diff --git a/src/lib/team-shortcut.ts b/src/lib/team-shortcut.ts
new file mode 100644
index 000000000..c7a06a460
--- /dev/null
+++ b/src/lib/team-shortcut.ts
@@ -0,0 +1,86 @@
+/**
+ * Team shortcut routing: resolves `genie [team]` -> `genie _open [team]`
+ *
+ * UX: `genie` opens default team "main", `genie ` opens that team.
+ * Internally dispatches to hidden `_open` command.
+ *
+ * Priority:
+ * 1. --team flag always routes to _open (for disambiguation)
+ * 2. Known subcommands take priority over team names
+ * 3. No args -> open default team "main"
+ * 4. Unknown first arg treated as team name (catch-all)
+ */
+
+interface ShortcutResult {
+ /** The (potentially rewritten) CLI args (without argv[0] and argv[1]) */
+ args: string[];
+ /** Whether the args were rewritten (team shortcut) */
+ isShortcut: boolean;
+ /** Warning message if first arg collides with a known subcommand */
+ collisionWarning: string | null;
+}
+
+/** Default team name when `genie` is invoked with no arguments. */
+export const DEFAULT_TEAM = 'main';
+
+/**
+ * Resolve team shortcut from raw CLI args.
+ *
+ * @param rawArgs - process.argv.slice(2) (everything after `node genie`)
+ * @param knownCommands - Set of registered subcommand names (+ aliases)
+ * @param teamExists - Optional callback to check if a team directory exists (for collision warnings)
+ */
+export function resolveTeamShortcut(
+ rawArgs: string[],
+ knownCommands: Set,
+ teamExists?: (name: string) => boolean,
+): ShortcutResult {
+ const firstArg = rawArgs[0];
+
+ // No args -> open default team
+ if (!firstArg) {
+ return { args: ['_open', DEFAULT_TEAM], isShortcut: true, collisionWarning: null };
+ }
+
+ // 1. Global --team flag: genie --team [...rest] -> genie _open [...rest]
+ if (firstArg === '--team' && rawArgs.length >= 2) {
+ const teamName = rawArgs[1];
+ return {
+ args: ['_open', teamName, ...rawArgs.slice(2)],
+ isShortcut: true,
+ collisionWarning: null,
+ };
+ }
+ if (firstArg.startsWith('--team=')) {
+ const teamName = firstArg.slice('--team='.length);
+ if (!teamName) {
+ return { args: rawArgs, isShortcut: false, collisionWarning: null };
+ }
+ return {
+ args: ['_open', teamName, ...rawArgs.slice(1)],
+ isShortcut: true,
+ collisionWarning: null,
+ };
+ }
+
+ // Skip flags (e.g., --help, --version, -h)
+ if (firstArg.startsWith('-')) {
+ return { args: rawArgs, isShortcut: false, collisionWarning: null };
+ }
+
+ // 2. Known subcommand takes priority
+ if (knownCommands.has(firstArg)) {
+ let collisionWarning: string | null = null;
+ if (teamExists?.(firstArg)) {
+ collisionWarning = `Warning: "${firstArg}" is a subcommand. To open team "${firstArg}", use: genie --team ${firstArg}`;
+ }
+ return { args: rawArgs, isShortcut: false, collisionWarning };
+ }
+
+ // 3. Unknown first arg -> treat as team name
+ return {
+ args: ['_open', ...rawArgs],
+ isShortcut: true,
+ collisionWarning: null,
+ };
+}
diff --git a/src/lib/tmux.ts b/src/lib/tmux.ts
index 67570d059..9cac58e9f 100644
--- a/src/lib/tmux.ts
+++ b/src/lib/tmux.ts
@@ -221,6 +221,8 @@ export async function ensureTeamWindow(
} catch {
/* best-effort */
}
+ // Rehydrate pane color hook (survives tmux restarts)
+ await rehydratePaneColorHook(existing.id);
const panes = await listPanes(existing.id);
const paneId = panes.length > 0 ? panes[0].id : `${session}:${teamName}.0`;
return { windowId: existing.id, windowName: teamName, paneId, created: false };
@@ -231,11 +233,111 @@ export async function ensureTeamWindow(
throw new Error(`Failed to create team window "${teamName}" in session "${session}"`);
}
+ // Install pane color hook on new window
+ await rehydratePaneColorHook(newWindow.id);
const panes = await listPanes(newWindow.id);
const paneId = panes.length > 0 ? panes[0].id : `${session}:${teamName}.0`;
return { windowId: newWindow.id, windowName: teamName, paneId, created: true };
}
+/**
+ * Map agent color names to tmux hex colors for active border styling.
+ * Palette matches ClaudeTeamColor from provider-adapters.
+ */
+const TMUX_COLOR_MAP: Record = {
+ red: '#b83030',
+ blue: '#2a6cb8',
+ green: '#20a050',
+ yellow: '#b8a020',
+ purple: '#7830b8',
+ orange: '#b86820',
+ pink: '#b83078',
+ cyan: '#20a0a0',
+};
+
+const PANE_COLORS_PATH = `${require('node:os').homedir()}/.genie/pane-colors.json`;
+const PANE_COLOR_SCRIPT = `${require('node:os').homedir()}/.genie/tmux-pane-color.sh`;
+
+/**
+ * Ensure the pane-color router script exists.
+ * This script is called by the tmux pane-focus-in hook and reads
+ * ~/.genie/pane-colors.json to resolve pane_id → border color.
+ */
+function ensurePaneColorScript(): void {
+ const { existsSync, writeFileSync, mkdirSync, chmodSync } = require('node:fs');
+ const { dirname } = require('node:path');
+
+ if (existsSync(PANE_COLOR_SCRIPT)) return;
+
+ mkdirSync(dirname(PANE_COLOR_SCRIPT), { recursive: true });
+ writeFileSync(
+ PANE_COLOR_SCRIPT,
+ `#!/bin/bash
+# Genie tmux pane color router — maps focused pane to agent border color
+PANE_ID="$1"
+MAP="$HOME/.genie/pane-colors.json"
+[ -f "$MAP" ] || exit 0
+COLOR=$(jq -r --arg p "$PANE_ID" '.[$p] // empty' "$MAP" 2>/dev/null)
+[ -z "$COLOR" ] && COLOR="default"
+tmux set-option -w pane-active-border-style "fg=$COLOR"
+`,
+ );
+ chmodSync(PANE_COLOR_SCRIPT, 0o755);
+}
+
+/**
+ * Register a pane→color mapping and install the window focus hook.
+ * When any pane in the window gains focus, the active border color
+ * changes to match that agent's assigned color.
+ */
+export async function applyPaneColor(paneId: string, color: string, windowId?: string): Promise {
+ const hex = TMUX_COLOR_MAP[color] ?? TMUX_COLOR_MAP.blue;
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = require('node:fs');
+ const { dirname } = require('node:path');
+
+ try {
+ // 1. Ensure script exists
+ ensurePaneColorScript();
+
+ // 2. Update pane-colors.json
+ let map: Record = {};
+ if (existsSync(PANE_COLORS_PATH)) {
+ try {
+ map = JSON.parse(readFileSync(PANE_COLORS_PATH, 'utf-8'));
+ } catch {
+ map = {};
+ }
+ } else {
+ mkdirSync(dirname(PANE_COLORS_PATH), { recursive: true });
+ }
+ map[paneId] = hex;
+ writeFileSync(PANE_COLORS_PATH, JSON.stringify(map, null, 2));
+
+ // 3. Install window hook (idempotent — overwrites previous)
+ if (windowId) {
+ await executeTmux(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`);
+ }
+ } catch {
+ /* best-effort — don't break spawn if tmux styling fails */
+ }
+}
+
+/**
+ * Rehydrate the pane-focus-in color hook on a window.
+ * Called when a team window is resolved (created or found) to survive tmux restarts.
+ * Only installs the hook if pane-colors.json exists and has entries for panes in this window.
+ */
+async function rehydratePaneColorHook(windowId: string): Promise {
+ const { existsSync } = require('node:fs');
+ try {
+ if (!existsSync(PANE_COLORS_PATH) || !existsSync(PANE_COLOR_SCRIPT)) return;
+ ensurePaneColorScript();
+ await executeTmux(`set-hook -w -t '${windowId}' pane-focus-in "run-shell '${PANE_COLOR_SCRIPT} #{pane_id}'"`);
+ } catch {
+ /* best-effort */
+ }
+}
+
/**
* Kill a tmux session by ID
*/
diff --git a/src/lib/version.ts b/src/lib/version.ts
index f3980cad4..4894e1efc 100644
--- a/src/lib/version.ts
+++ b/src/lib/version.ts
@@ -1,2 +1,2 @@
// Runtime version (baked in at build time)
-export const VERSION = '3.260302.2';
+export const VERSION = '3.260309.1';
diff --git a/src/term-commands/agents.ts b/src/term-commands/agents.ts
index 89e7c1861..44fdba9f3 100644
--- a/src/term-commands/agents.ts
+++ b/src/term-commands/agents.ts
@@ -588,9 +588,13 @@ async function launchTmuxSpawn(ctx: SpawnCtx): Promise {
process.exit(1);
}
- // Apply layout to team window (or fallback to window 0)
+ // Apply layout to team window (or fallback to first window in session)
const session = 'genie';
- const layoutTarget = teamWindow ? `${session}:${teamWindow.windowName}` : `${session}:0`;
+ let layoutTarget = `${session}:${teamWindow?.windowName ?? ''}`;
+ if (!teamWindow) {
+ const wins = await tmux.listWindows(session);
+ layoutTarget = wins[0] ? wins[0].id : `${session}:`;
+ }
try {
execSync(`tmux ${buildLayoutCommand(layoutTarget, ctx.layoutMode)}`, { stdio: 'ignore' });
} catch {
@@ -600,6 +604,11 @@ async function launchTmuxSpawn(ctx: SpawnCtx): Promise {
const workerEntry = await registerSpawnWorker(ctx, paneId, teamWindow);
await notifySpawnJoin(ctx, paneId);
+ // Apply agent color to tmux pane border (focus-driven)
+ if (ctx.spawnColor && paneId !== 'inline') {
+ await tmux.applyPaneColor(paneId, ctx.spawnColor, teamWindow?.windowId);
+ }
+
// Save spawn template for auto-respawn on message delivery
await registry.saveTemplate({
id: ctx.validated.role ?? ctx.workerId,
@@ -747,6 +756,15 @@ async function handleWorkerSpawn(options: {
const { parentSessionId, spawnColor, nativeTeam } = await resolveNativeTeam(team, repoPath, options);
if (nativeTeam) params.nativeTeam = nativeTeam;
+ // 2b. Inject hook dispatch into team settings.json (idempotent)
+ try {
+ const { injectTeamHooks } = await import('../hooks/inject.js');
+ const injected = await injectTeamHooks(team);
+ if (injected) console.log(` Hooks: injected genie hook dispatch into team "${team}"`);
+ } catch (err) {
+ console.warn(`Warning: could not inject hooks for team "${team}": ${err instanceof Error ? err.message : err}`);
+ }
+
// Session ID only for Claude
const claudeSessionId = params.provider === 'claude' ? crypto.randomUUID() : undefined;
if (claudeSessionId) params.sessionId = claudeSessionId;
@@ -1239,7 +1257,7 @@ export function registerAgentNamespace(program: Command): void {
if (w.claudeSessionId) {
console.log(` Session preserved: ${w.claudeSessionId}`);
}
- console.log(` Send a message to auto-resume: genie msg send ${id} "your message"`);
+ console.log(` Send a message to auto-resume: genie send ${id} "your message"`);
} else {
console.error(`Failed to suspend agent "${id}".`);
process.exit(1);
diff --git a/src/term-commands/approve.ts b/src/term-commands/approve.ts
index c633b40ac..56056d00c 100644
--- a/src/term-commands/approve.ts
+++ b/src/term-commands/approve.ts
@@ -2,11 +2,11 @@
* Approve command - Auto-approve engine management and manual approval
*
* Usage:
- * genie worker approve --status - Show pending/approved/denied requests
- * genie worker approve - Manually approve a pending request
- * genie worker approve --deny - Manually deny a pending request
- * genie worker approve --start - Start the auto-approve engine
- * genie worker approve --stop - Stop the auto-approve engine
+ * genie agent approve --status - Show pending/approved/denied requests
+ * genie agent approve - Manually approve a pending request
+ * genie agent approve --deny - Manually deny a pending request
+ * genie agent approve --start - Start the auto-approve engine
+ * genie agent approve --stop - Stop the auto-approve engine
*/
import { existsSync, readFileSync } from 'node:fs';
@@ -299,7 +299,7 @@ function handleManualAction(id: string, action: 'approve' | 'deny'): void {
}
/**
- * Main CLI command handler for `genie worker approve`.
+ * Main CLI command handler for `genie agent approve`.
*
* Dispatches to the appropriate sub-function based on options:
* - --status: show pending/approved/denied requests
@@ -347,9 +347,9 @@ export async function approveCommand(requestId: string | undefined, options: App
}
console.log('Usage:');
- console.log(' genie worker approve --status Show pending/approved/denied requests');
- console.log(' genie worker approve Manually approve a pending request');
- console.log(' genie worker approve --deny Manually deny a pending request');
- console.log(' genie worker approve --start Start the auto-approve engine');
- console.log(' genie worker approve --stop Stop the auto-approve engine');
+ console.log(' genie agent approve --status Show pending/approved/denied requests');
+ console.log(' genie agent approve Manually approve a pending request');
+ console.log(' genie agent approve --deny Manually deny a pending request');
+ console.log(' genie agent approve --start Start the auto-approve engine');
+ console.log(' genie agent approve --stop Stop the auto-approve engine');
}
diff --git a/src/term-commands/close.ts b/src/term-commands/close.ts
index d4a86f753..cb9665934 100644
--- a/src/term-commands/close.ts
+++ b/src/term-commands/close.ts
@@ -5,7 +5,7 @@
* Backend is auto-detected based on whether .genie/ directory exists.
*
* Usage:
- * genie worker close - Close task, cleanup worktree, kill worker
+ * genie agent close - Close task, cleanup worktree, kill agent
*
* Options:
* --no-sync - Skip bd sync (beads only, no-op for local)
@@ -166,7 +166,7 @@ async function killWorkerPane(paneId: string): Promise {
*/
async function killWorkerTmux(w: registry.Agent): Promise {
if (w.windowId && w.session) {
- console.log(`💀 Killing worker window "${w.windowName || w.windowId}" (${w.id})...`);
+ console.log(`💀 Killing agent window "${w.windowName || w.windowId}" (${w.id})...`);
try {
const sessionObj = await tmux.findSessionByName(w.session);
if (sessionObj) {
@@ -185,7 +185,7 @@ async function killWorkerTmux(w: registry.Agent): Promise {
return;
}
if (w.windowName) {
- console.log(`💀 Killing worker window "${w.windowName}" (${w.id})...`);
+ console.log(`💀 Killing agent window "${w.windowName}" (${w.id})...`);
try {
await tmux.killWindow(w.windowName);
console.log(' ✅ Window killed');
@@ -194,7 +194,7 @@ async function killWorkerTmux(w: registry.Agent): Promise {
}
return;
}
- console.log(`💀 Killing worker pane (${w.id})...`);
+ console.log(`💀 Killing agent pane (${w.id})...`);
await killWorkerPane(w.paneId);
console.log(' ✅ Pane killed');
}
@@ -250,7 +250,7 @@ async function findRepresentativeWorker(allWorkers: registry.Agent[], taskId: st
*/
async function confirmClose(taskId: string, workerCount: number, worker: registry.Agent | null): Promise {
const workerMsg =
- workerCount > 1 ? ` and kill ${workerCount} workers` : worker ? ` and kill worker (pane ${worker.paneId})` : '';
+ workerCount > 1 ? ` and kill ${workerCount} agents` : worker ? ` and kill agent (pane ${worker.paneId})` : '';
return confirm({ message: `Close ${taskId}${workerMsg}?`, default: true });
}
@@ -286,7 +286,7 @@ async function killAndUnregisterAll(allWorkers: registry.Agent[]): Promise
await killWorkerTmux(w);
await unregisterWorker(w);
}
- if (allWorkers.length > 0) console.log(` ✅ ${allWorkers.length} worker(s) unregistered`);
+ if (allWorkers.length > 0) console.log(` ✅ ${allWorkers.length} agent(s) unregistered`);
}
export async function closeCommand(taskId: string, options: CloseOptions = {}): Promise {
@@ -299,9 +299,9 @@ export async function closeCommand(taskId: string, options: CloseOptions = {}):
const worker = await findRepresentativeWorker(allWorkers, taskId);
if (allWorkers.length === 0) {
- console.log(`ℹ️ No active worker for ${taskId}. Closing ${isLocal ? 'task' : 'issue'} only.`);
+ console.log(`ℹ️ No active agent for ${taskId}. Closing ${isLocal ? 'task' : 'issue'} only.`);
} else if (allWorkers.length > 1) {
- console.log(`📌 Found ${allWorkers.length} workers for ${taskId}`);
+ console.log(`📌 Found ${allWorkers.length} agents for ${taskId}`);
}
if (!options.yes) {
diff --git a/src/term-commands/events.test.ts b/src/term-commands/events.test.ts
index 077f70efa..520edf114 100644
--- a/src/term-commands/events.test.ts
+++ b/src/term-commands/events.test.ts
@@ -1,5 +1,5 @@
/**
- * Tests for genie worker events command
+ * Tests for genie agent events command
*
* Run with: bun test src/term-commands/events.test.ts
*/
diff --git a/src/term-commands/events.ts b/src/term-commands/events.ts
index 1136e2b47..75ef79baa 100644
--- a/src/term-commands/events.ts
+++ b/src/term-commands/events.ts
@@ -2,12 +2,12 @@
* Events command - Stream Claude Code events from a pane
*
* Usage:
- * genie worker events - Show recent events from a worker
- * genie worker events --follow - Tail events in real-time (like tail -f)
- * genie worker events --emit - Tail and write to .genie/events/.jsonl
- * genie worker events --json - Output events as JSON
- * genie worker events --all - Aggregate events from all active workers
- * genie worker events --all --json - Aggregate events as JSON
+ * genie agent events - Show recent events from an agent
+ * genie agent events --follow - Tail events in real-time (like tail -f)
+ * genie agent events --emit - Tail and write to .genie/events/.jsonl
+ * genie agent events --json - Output events as JSON
+ * genie agent events --all - Aggregate events from all active agents
+ * genie agent events --all --json - Aggregate events as JSON
*
* Events are normalized into a standard format:
* - session_start: Claude session started
@@ -407,7 +407,7 @@ export async function eventsCommand(paneId: string | undefined, options: EventsO
}
if (!paneId) {
- console.error('Error: pane-id is required (or use --all for all workers)');
+ console.error('Error: pane-id is required (or use --all for all agents)');
process.exit(1);
}
@@ -437,7 +437,7 @@ async function eventsAllCommand(options: EventsOptions): Promise {
if (options.json) {
console.log('[]');
} else {
- console.log('No active workers found.');
+ console.log('No active agents found.');
}
return;
}
@@ -457,7 +457,7 @@ async function eventsAllCommand(options: EventsOptions): Promise {
console.log(JSON.stringify(recentEvents, null, 2));
} else {
if (recentEvents.length === 0) {
- console.log('No events found from active workers.');
+ console.log('No events found from active agents.');
console.log('Hint: Use --emit flag when tailing to write events to files.');
} else {
for (const event of recentEvents) {
diff --git a/src/term-commands/history.test.ts b/src/term-commands/history.test.ts
index 179c36472..d7280199e 100644
--- a/src/term-commands/history.test.ts
+++ b/src/term-commands/history.test.ts
@@ -1,5 +1,5 @@
/**
- * Tests for genie worker history command
+ * Tests for genie agent history command
*/
import { describe, expect, test } from 'bun:test';
diff --git a/src/term-commands/history.ts b/src/term-commands/history.ts
index f843d31e5..6be5c437d 100644
--- a/src/term-commands/history.ts
+++ b/src/term-commands/history.ts
@@ -5,10 +5,10 @@
* Claude's JSONL logs and extracting key events.
*
* Usage:
- * genie worker history # Compressed summary
- * genie worker history --full # Full conversation
- * genie worker history --since 5 # Last 5 exchanges
- * genie worker history --json # JSON output
+ * genie agent history # Compressed summary
+ * genie agent history --full # Full conversation
+ * genie agent history --since 5 # Last 5 exchanges
+ * genie agent history