Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "genie",
"version": "3.260317.16",
"version": "3.260317.20",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The versioning script (scripts/version.ts) updates this version by targeting the first plugin in the plugins array (plugins[0]). This approach is brittle and may lead to incorrect versioning if more plugins are added to this file or if their order changes. To make the update process more robust, the script should be modified to locate the 'genie' plugin by its name property before updating the version.

"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."
}
Expand Down
2 changes: 1 addition & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "genie",
"name": "Genie",
"description": "Skills, agents, and hooks for the Genie CLI terminal orchestration toolkit",
"version": "3.260317.16",
"version": "3.260317.20",
"configSchema": {
"type": "object",
"additionalProperties": false,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@automagik/genie",
"version": "3.260317.16",
"version": "3.260317.20",
"description": "Collaborative terminal toolkit for human + AI workflows",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion plugins/genie/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genie",
"version": "3.260317.16",
"version": "3.260317.20",
"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"
Expand Down
2 changes: 1 addition & 1 deletion plugins/genie/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "genie-plugin",
"version": "3.260317.16",
"version": "3.260317.20",
"private": true,
"description": "Runtime dependencies for genie bundled CLIs",
"type": "module",
Expand Down
133 changes: 133 additions & 0 deletions scripts/tmux/genie-projects.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Genie TUI — project session tabs for tmux top status bar
# Reads workers.json for agent counts per session, merges with tmux sessions.
#
# Output format per session:
# Active: #[bg=#7b2ff7,fg=#e0e0e0,bold] name (N) ● #[bg=#1a1a2e,fg=#7b2ff7]
# Inactive: #[fg=#b8a9c9,bg=#1a1a2e] name (N)
#
# Env: GENIE_WORKERS — override path to workers.json (for testing)

set -euo pipefail

workers_file="${GENIE_WORKERS:-${HOME}/.genie/workers.json}"

# Get agent counts per session from workers.json (single jq pass)
declare -A agent_counts=()
if [[ -f "$workers_file" ]]; then
worker_data=$(jq -r '
.workers // {} | to_entries
| group_by(.value.session)
| map({session: .[0].value.session, count: length})
| map(.session + "\t" + (.count | tostring))
| .[]
' "$workers_file" 2>/dev/null) || worker_data=""

if [[ -n "$worker_data" ]]; then
while IFS=$'\t' read -r sess count; do
[[ -z "$sess" ]] && continue
agent_counts["$sess"]="$count"
done <<< "$worker_data"
fi
fi

# Get the currently active session name
has_tmux=true
active_session=""
if command -v tmux &>/dev/null; then
active_session=$(tmux display-message -p '#{client_session}' 2>/dev/null) || active_session=""
else
has_tmux=false
fi

max_visible=8
output=""
declare -A seen_sessions=()
declare -a all_sessions=()
active_output=""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove the dead active_output path.

active_output is written once and never read, so this first tmux pass looks stateful when it only populates all_sessions.

Also applies to: 60-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/tmux/genie-projects.sh` at line 47, Remove the dead variable
assignment and any writes to it: delete the `active_output=""` initialization
and the other write at the second occurrence (around the other mentioned line)
so the first tmux pass only populates `all_sessions` without introducing unused
state; search for the symbol `active_output` and remove those lines (and any
associated, now-unused references) to avoid writing an unused path.


if [[ "$has_tmux" == "true" ]]; then
# Collect all sessions with their rendered output
while IFS=$'\t' read -r session_name window_count; do
[[ -z "$session_name" ]] && continue
seen_sessions["$session_name"]=1
all_sessions+=("$session_name")

# Use agent count from workers.json; fall back to window count
task_count="${agent_counts[$session_name]:-$window_count}"

if [[ "$session_name" == "$active_session" ]]; then
active_output="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${session_name} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] "
fi
done < <(tmux list-sessions -F "#{session_name} #{session_windows}" 2>/dev/null)
fi

# Also include sessions from workers.json not yet seen (no tmux session or testing mode)
for sess in "${!agent_counts[@]}"; do
[[ -n "${seen_sessions[$sess]:-}" ]] && continue
all_sessions+=("$sess")
done

total=${#all_sessions[@]}

if [[ "$total" -le "$max_visible" ]]; then
# No overflow — render all sessions
for session_name in "${all_sessions[@]}"; do
task_count="${agent_counts[$session_name]:-0}"
# Prefer window count from tmux if available
if [[ "$has_tmux" == "true" ]]; then
wcount=$(tmux list-windows -t "$session_name" -F "x" 2>/dev/null | wc -l) || wcount=0
task_count="${agent_counts[$session_name]:-$wcount}"
fi

if [[ "$session_name" == "$active_session" ]]; then
output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${session_name} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] "
else
output+="#[fg=#b8a9c9,bg=#1a1a2e] ${session_name} (${task_count}) "
fi
done
else
# Overflow — show up to max_visible with active session always included
shown=0
active_shown=false

for session_name in "${all_sessions[@]}"; do
if [[ "$shown" -ge "$((max_visible - 1))" && "$active_shown" == "false" && "$session_name" != "$active_session" ]]; then
# Reserve last slot for active session
continue
fi
if [[ "$shown" -ge "$max_visible" ]]; then
break
Comment on lines +95 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't reserve an active-session slot when active_session is empty.

In the no-tmux/testing path, active_session stays empty. Once shown reaches 7, this guard skips every remaining session, so you render only 7 entries while +N more is still computed as if 8 were shown.

Suggested fix
-    if [[ "$shown" -ge "$((max_visible - 1))" && "$active_shown" == "false" && "$session_name" != "$active_session" ]]; then
+    if [[ -n "$active_session" && "$shown" -ge "$((max_visible - 1))" && "$active_shown" == "false" && "$session_name" != "$active_session" ]]; then
       # Reserve last slot for active session
       continue
     fi

Also applies to: 128-130

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/tmux/genie-projects.sh` around lines 95 - 100, The reservation logic
incorrectly reserves the last slot even when active_session is empty; update the
conditional that currently checks shown, active_shown, and session_name !=
active_session to also require that active_session is non-empty (e.g., test -n
"$active_session" or "$active_session" != ""), so the "reserve last slot for
active session" branch only runs when an active session exists; apply the same
change to the other occurrence that mirrors this logic (the block around the
second instance at lines referenced in the comment).

fi

task_count="${agent_counts[$session_name]:-0}"
if [[ "$has_tmux" == "true" ]]; then
wcount=$(tmux list-windows -t "$session_name" -F "x" 2>/dev/null | wc -l) || wcount=0
task_count="${agent_counts[$session_name]:-$wcount}"
fi

if [[ "$session_name" == "$active_session" ]]; then
output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${session_name} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] "
active_shown=true
else
output+="#[fg=#b8a9c9,bg=#1a1a2e] ${session_name} (${task_count}) "
fi
((shown++)) || true
done

# If active session wasn't shown yet, append it
if [[ "$active_shown" == "false" && -n "$active_session" ]]; then
task_count="${agent_counts[$active_session]:-0}"
if [[ "$has_tmux" == "true" ]]; then
wcount=$(tmux list-windows -t "$active_session" -F "x" 2>/dev/null | wc -l) || wcount=0
task_count="${agent_counts[$active_session]:-$wcount}"
fi
output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${active_session} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] "
fi

# Append overflow indicator
remaining=$((total - max_visible))
output+="#[fg=#6c6c8a,bg=#1a1a2e] +${remaining} more "
fi

echo -n "$output"
19 changes: 19 additions & 0 deletions scripts/tmux/genie-sessions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# List all tmux sessions formatted for status bar display
# Active session gets purple highlight, others get dim text

current_session="$1"
if [ -z "$current_session" ]; then
current_session=$(tmux display-message -p '#{session_name}' 2>/dev/null)
fi

output=""
for sess in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | sort); do
if [ "$sess" = "$current_session" ]; then
output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] $sess #[bg=#16213e,fg=#7b2ff7,nobold]"
else
output+="#[fg=#b8a9c9,bg=#16213e] $sess "
fi
done
Comment on lines +11 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f genie-sessions.sh

Repository: automagik-dev/genie

Length of output: 93


🏁 Script executed:

file_path=$(fd -t f genie-sessions.sh 2>/dev/null | head -1)
if [ -n "$file_path" ]; then
  echo "File found: $file_path"
  wc -l "$file_path"
  head -30 "$file_path" | cat -n
fi

Repository: automagik-dev/genie

Length of output: 839


🌐 Web query:

tmux session names allowed characters spaces special characters constraints

💡 Result:

Tmux session names must be non-empty and cannot contain the characters ':' or '.'. Spaces and other special characters are allowed as long as they are not ':' or '.'. There is no defined maximum length. When creating or renaming a session, tmux validates the name using session_check_name, which returns an error if the name is empty or contains ':' or '.'. In the source code, this check is: return (*name != '\0' && name[strcspn(name, ":.")] == '\0');. If invalid, commands like new-session or rename-session report "bad session name". Colons and periods are prohibited because they are special characters used in tmux targets to separate session:window and window.pane (e.g., mysession:mywindow.1). Tabs are allowed in older versions but sanitized to spaces in tmux 3.2+. Control characters may cause issues like hanging (fixed in 3.0+), but are not explicitly forbidden beyond validation. Examples: - Valid: "my session", "test-123", "user@host" - Invalid: "", "my:session", "my.session", "session:name" Official tmux man pages do not explicitly document these constraints, but they are enforced in the code and confirmed by tmux maintainer in GitHub issues.

Citations:


Avoid word-splitting session names during iteration.

Line 11 uses command substitution in a for loop, which breaks on whitespace in session names and can render incorrect labels.

Suggested fix
 output=""
-for sess in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | sort); do
+while IFS= read -r sess; do
   if [ "$sess" = "$current_session" ]; then
     output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] $sess #[bg=#16213e,fg=#7b2ff7,nobold]"
   else
     output+="#[fg=#b8a9c9,bg=#16213e] $sess "
   fi
-done
+done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | sort)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/tmux/genie-sessions.sh` around lines 11 - 17, The loop over tmux
sessions is splitting session names on whitespace because it uses for sess in
$(tmux list-sessions ...); to fix this, read the session names line-by-line
instead (e.g., pipe tmux list-sessions -F '#{session_name}' ... | sort into a
while IFS= read -r sess loop or use mapfile -t and iterate the array) so the
variable sess preserves spaces and special characters; update the block that
references sess, current_session and appends to output accordingly to use the
line-oriented reader.


echo "$output"
49 changes: 49 additions & 0 deletions scripts/tmux/genie-window-label.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Genie TUI — per-window agent enrichment label
# Called from window-status-format via #() for each window tab.
# Must be fast (< 50ms) — single jq query, early exit on missing data.
#
# Usage: genie-window-label.sh <session_name> <window_name>
# Output: " ×count emoji" (e.g., " ×3 🔨") or empty string
#
# Env: GENIE_WORKERS — override path to workers.json (for testing)

session="${1:-}"
window="${2:-}"

# Early exit if no args
if [ -z "$session" ] || [ -z "$window" ]; then
exit 0
fi

workers_file="${GENIE_WORKERS:-${HOME}/.genie/workers.json}"

# Early exit if no workers file
if [ ! -f "$workers_file" ]; then
exit 0
fi

# Single jq pass: filter by session + window, count + worst state emoji
result=$(jq -r --arg sess "$session" --arg win "$window" '
def state_priority:
{"error": 7, "permission": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};
def state_emoji:
{"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "suspended": "💤"};

.workers // {} | to_entries
| map(select(.value.session == $sess and (.value.windowName // .value.team // "unknown") == $win))
| if length == 0 then ""
else
length as $count
| map(.value.state // "idle")
| map(. as $s | state_priority[$s] // 0)
| max as $max_pri
| (state_priority | to_entries | map(select(.value == $max_pri)) | .[0].key // "idle") as $worst
| " ×\($count) \(state_emoji[$worst] // "")"
end
' "$workers_file" 2>/dev/null) || exit 0

# Only print if non-empty
if [ -n "$result" ]; then
printf '%s' "$result"
fi
49 changes: 23 additions & 26 deletions scripts/tmux/genie.tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ setw -g mode-keys vi
# --- Pane borders ---
set -g pane-border-style "fg=#0f3460"
set -g pane-active-border-style "fg=#7b2ff7"
set -g pane-border-status off

# --- Message styling ---
set -g message-style "bg=#16213e,fg=#00d2ff"
Expand All @@ -45,54 +46,50 @@ setw -g mode-style "bg=#7b2ff7,fg=#e0e0e0"
setw -g clock-mode-colour "#7b2ff7"

# ============================================================================
# STATUS BAR
# STATUS BAR — Single bar at bottom with clickable tabs + info
# ============================================================================
set -g status on
set -g status-interval 5
set -g status-position top
set -g status-position bottom
set -g status-justify left
set -g status-style "bg=#1a1a2e,fg=#e0e0e0"

# --- Left: Genie branding + version ---
set -g status-left-length 60
set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] #(genie --version 2>/dev/null | head -1 || echo 'Genie') #[bg=#1a1a2e,fg=#7b2ff7]"
# Left: branding
set -g status-left-length 40
set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] 🧞 #(genie --version 2>/dev/null | head -1 || echo Genie) #[bg=#1a1a2e,fg=#7b2ff7]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current behavior: fallback is masked by pipeline status.
current="$(bash -lc 'missing_genie --version 2>/dev/null | head -1 || echo Genie')"
printf "current=[%s]\n" "$current"

# Verify fixed behavior: fallback appears when command is missing.
fixed="$(bash -lc 'missing_genie --version 2>/dev/null || echo Genie')"
printf "fixed=[%s]\n" "$fixed"

Repository: automagik-dev/genie

Length of output: 302


Remove | head -1 to ensure fallback logic executes when genie is missing.

On line 59, the head command masks pipeline failure. When genie --version fails, head still exits successfully (exit 0), preventing the || echo Genie fallback from triggering. The result is blank status output instead of the "Genie" fallback.

Proposed fix
-set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] 🧞 #(genie --version 2>/dev/null | head -1 || echo Genie) #[bg=#1a1a2e,fg=#7b2ff7]"
+set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] 🧞 #(genie --version 2>/dev/null || echo Genie) #[bg=#1a1a2e,fg=#7b2ff7]"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] 🧞 #(genie --version 2>/dev/null | head -1 || echo Genie) #[bg=#1a1a2e,fg=#7b2ff7]"
set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] 🧞 #(genie --version 2>/dev/null || echo Genie) #[bg=#1a1a2e,fg=#7b2ff7]"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/tmux/genie.tmux.conf` at line 59, The tmux status-left command
currently pipes genie --version into head which masks the failure and prevents
the "|| echo Genie" fallback from running; update the status-left line (the
shell command inside the #( ... ) used for status-left) to remove the `| head
-1` so the pipeline returns the genie process exit code and the fallback `||
echo Genie` will execute when `genie --version` fails. Ensure the remainder of
the status-left formatting (the surrounding #[...] tokens and colors) is left
unchanged.


# --- Right: git | CPU | RAM | clock ---
set -g status-right-length 120
# Right: system info
set -g status-right-length 100
set -g status-right "#[fg=#6c6c8a]#($HOME/.genie/scripts/genie-git.sh) #[fg=#0f3460]| #[fg=#00d2ff]CPU #($HOME/.genie/scripts/cpu-info.sh) #[fg=#0f3460]| #[fg=#00d2ff]RAM #($HOME/.genie/scripts/ram-info.sh) #[fg=#0f3460]| #[fg=#e0e0e0]%H:%M "

# ============================================================================
# WINDOW (TAB) STYLING — clickable tabs
# ============================================================================

# Tab separator
set -g window-status-separator ""

# Inactive tabs: lavender text on dark bg
set -g window-status-format "#[fg=#b8a9c9,bg=#1a1a2e] #I:#W "

# Active tab: purple bg, white text, bold
set -g window-status-current-format "#[fg=#1a1a2e,bg=#7b2ff7]#[fg=#e0e0e0,bg=#7b2ff7,bold] #I:#W #[fg=#7b2ff7,bg=#1a1a2e]"

# Activity monitoring
# Activity
setw -g monitor-activity on
set -g visual-activity off
set -g window-status-activity-style "fg=#f5a623,bg=#1a1a2e"

# ============================================================================
# SECOND STATUS LINE — bottom tab bar (tmux 3.2+)
# ============================================================================
# Uncomment the following for a dual status bar (requires tmux 3.2+):
# set -g status 2
# set -g status-format[1] "#[align=centre,fg=#6c6c8a,bg=#16213e] #($HOME/.genie/scripts/genie-update-check.sh) "

# ============================================================================
# KEYBINDINGS (standard — no changes from defaults beyond vi mode)
# KEYBINDINGS
# ============================================================================

# Reload config
bind r source-file ~/.tmux.conf \; display-message "Config reloaded"

# Vi copy mode bindings
bind -n C-t new-window -c "#{?session_path,#{session_path},#{pane_current_path}}"
bind -T copy-mode-vi v send-keys -X begin-selection
bind -T copy-mode-vi y send-keys -X copy-selection-and-cancel
bind -n C-) switch-client -n
bind -n C-( switch-client -p
bind -n M-1 select-window -t 0
bind -n M-2 select-window -t 1
bind -n M-3 select-window -t 2
bind -n M-4 select-window -t 3
bind -n M-5 select-window -t 4
bind -n M-6 select-window -t 5
bind -n M-7 select-window -t 6
bind -n M-8 select-window -t 7
bind -n M-9 select-window -t 8
bind -n DoubleClick1StatusDefault choose-tree -s
10 changes: 4 additions & 6 deletions src/genie-commands/__tests__/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,14 @@ describe('buildClaudeCommand', () => {
expect(cmd).toContain('--dangerously-skip-permissions');
});

test('includes --agent-id with folderName@team pattern', () => {
test('includes --agent-id with team-lead@team pattern', () => {
const cmd = buildClaudeCommand('my-team');
const folderName = basename(process.cwd());
expect(cmd).toContain(`--agent-id '${folderName}@my-team'`);
expect(cmd).toContain(`--agent-id 'team-lead@my-team'`);
});

test('includes --agent-name as folder name', () => {
test('includes --agent-name as team-lead', () => {
const cmd = buildClaudeCommand('genie');
const folderName = basename(process.cwd());
expect(cmd).toContain(`--agent-name '${folderName}'`);
expect(cmd).toContain('--agent-name team-lead');
});

test('with system prompt file references it via --append-system-prompt-file', () => {
Expand Down
20 changes: 16 additions & 4 deletions src/genie-commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async function ensureNativeTeamForLeader(teamName: string, cwd: string): Promise

await registerNativeMember(teamName, {
agentName: basename(cwd),
agentType: 'general-purpose',
agentType: 'team-lead',
color: 'blue',
cwd,
});
Expand Down Expand Up @@ -319,8 +319,8 @@ function attachToWindow(sessionName: string, windowName: string): void {
}

export async function sessionCommand(options: SessionOptions = {}): Promise<void> {
const sessionName = options.name ?? DEFAULT_SESSION_NAME;
const workspaceDir = options.dir ?? process.cwd();
const sessionName = options.name ?? sanitizeWindowName(basename(workspaceDir));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Revert default sessionName to the shared constant.

Defaulting to basename(workspaceDir) shifts behavior from one shared tmux session to per-folder sessions, which conflicts with this command’s documented session model and changes reset/window-collision semantics.

Proposed fix
-  const sessionName = options.name ?? sanitizeWindowName(basename(workspaceDir));
+  const sessionName = options.name ?? DEFAULT_SESSION_NAME;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sessionName = options.name ?? sanitizeWindowName(basename(workspaceDir));
const sessionName = options.name ?? DEFAULT_SESSION_NAME;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/genie-commands/session.ts` at line 323, The fallback for sessionName
currently uses sanitizeWindowName(basename(workspaceDir)) which creates
per-folder sessions; change the expression so options.name falls back to the
existing shared tmux session constant used elsewhere (instead of
basename(workspaceDir)). Edit the assignment to sessionName (the line using
options.name ?? ...) to use that shared session constant as the default,
removing the sanitizeWindowName(basename(workspaceDir)) fallback to restore the
single shared session behavior.


try {
const windowName = await deriveWindowName(sessionName, workspaceDir, options.team);
Expand All @@ -335,12 +335,24 @@ export async function sessionCommand(options: SessionOptions = {}): Promise<void

if (!session) {
await createSession(sessionName, windowName, workspaceDir, systemPromptFile);
attachToWindow(sessionName, windowName);
} else if (process.env.TMUX) {
// Already inside tmux — launch Claude Code in the CURRENT pane
const suffix = Date.now().toString(36).slice(-4);
const currentWindowName = `${windowName}-${suffix}`;
await tmux.executeTmux(`rename-window ${shellQuote(currentWindowName)}`);
await ensureNativeTeamForLeader(currentWindowName, workspaceDir);
const agentName = basename(workspaceDir);
const resumeSessionId = findLastSessionId(sanitizeTeamName(currentWindowName), agentName, workspaceDir);
const cmd = buildClaudeCommand(currentWindowName, systemPromptFile || undefined, resumeSessionId || undefined);
Comment on lines +339 to +347

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Inside-tmux flow can target the wrong window and fragments team identity.

rename-window here acts on the current client window (not explicitly bound to sessionName), and the timestamp suffix creates a new team/window identity every run, which breaks deterministic resume/team continuity. This path also skips the GENIE_CWD window env and registry registration used by other startup paths.

Proposed fix
   } else if (process.env.TMUX) {
-      // Already inside tmux — launch Claude Code in the CURRENT pane
-      const suffix = Date.now().toString(36).slice(-4);
-      const currentWindowName = `${windowName}-${suffix}`;
-      await tmux.executeTmux(`rename-window ${shellQuote(currentWindowName)}`);
+      // Already inside tmux
+      const currentSession = (await tmux.executeTmux("display-message -p '#S'")).trim();
+      if (currentSession !== sessionName) {
+        await focusTeamWindow(sessionName, windowName, workspaceDir, systemPromptFile);
+        attachToWindow(sessionName, windowName);
+        return;
+      }
+      const currentWindowName = windowName;
+      await tmux.executeTmux(`rename-window ${shellQuote(currentWindowName)}`);
+      await tmux.setWindowEnv(`${sessionName}:${currentWindowName}`, 'GENIE_CWD', workspaceDir);
       await ensureNativeTeamForLeader(currentWindowName, workspaceDir);
       const agentName = basename(workspaceDir);
       const resumeSessionId = findLastSessionId(sanitizeTeamName(currentWindowName), agentName, workspaceDir);
+      await registerSessionInRegistry(sessionName, currentWindowName, workspaceDir);
       const cmd = buildClaudeCommand(currentWindowName, systemPromptFile || undefined, resumeSessionId || undefined);

Based on learnings: deriveWindowName is the single entry point for window name resolution and should remain the authoritative naming path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/genie-commands/session.ts` around lines 339 - 347, The inside-tmux branch
must use the canonical deriveWindowName and same registration/env setup as other
flows instead of renaming the current client window with a timestamp suffix;
replace the suffix/currentWindowName logic with a call to
deriveWindowName(sessionName?) to produce the authoritative windowName, remove
the ad-hoc Date.now suffix, call ensureNativeTeamForLeader(derivedWindowName,
workspaceDir) and compute resumeSessionId via
findLastSessionId(sanitizeTeamName(derivedWindowName), agentName, workspaceDir),
and ensure the GENIE_CWD window env and registry registration steps used by
other startup paths are executed before building the command
(buildClaudeCommand) so the inside-tmux path remains deterministic and resumes
the correct team/session.

const { execSync: execSyncCmd } = require('node:child_process');
execSyncCmd(cmd, { stdio: 'inherit', cwd: workspaceDir });
Comment on lines +348 to +349

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "package.json module type:"
jq -r '.type // "undefined"' package.json

echo
echo "tsconfig module settings:"
fd -a '^tsconfig.*\.json$' | while read -r f; do
  echo "--- $f"
  jq -r '.compilerOptions.module // "undefined"' "$f" 2>/dev/null || true
done

echo
echo "dynamic require() usages under src/genie-commands:"
rg -nP --type=ts -C2 '\brequire\s*\(' src/genie-commands

Repository: automagik-dev/genie

Length of output: 1151


🏁 Script executed:

head -50 src/genie-commands/session.ts | cat -n

Repository: automagik-dev/genie

Length of output: 2020


🏁 Script executed:

rg -nA5 -B5 "^import.*from.*node:child_process" src/genie-commands/session.ts

Repository: automagik-dev/genie

Length of output: 637


🏁 Script executed:

sed -n '345,352p' src/genie-commands/session.ts | cat -n

Repository: automagik-dev/genie

Length of output: 657


Use static import for execSync instead of runtime require().

In ESM mode ("type": "module"), dynamic require() at runtime (line 348) is unreliable and can fail. Add execSync to the existing import from node:child_process at line 14 and call it directly.

Proposed fix
-import { spawnSync } from 'node:child_process';
+import { execSync, spawnSync } from 'node:child_process';

Then at line 348–349:

-      const { execSync: execSyncCmd } = require('node:child_process');
-      execSyncCmd(cmd, { stdio: 'inherit', cwd: workspaceDir });
+      execSync(cmd, { stdio: 'inherit', cwd: workspaceDir });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { execSync: execSyncCmd } = require('node:child_process');
execSyncCmd(cmd, { stdio: 'inherit', cwd: workspaceDir });
execSync(cmd, { stdio: 'inherit', cwd: workspaceDir });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/genie-commands/session.ts` around lines 348 - 349, The code uses a
runtime require to get execSync which fails in ESM; instead add a static import
for execSync from 'node:child_process' alongside the existing imports at the top
of the file, remove the require(...) at the call site, and call execSync(cmd, {
stdio: 'inherit', cwd: workspaceDir }) directly where execSyncCmd is currently
used (referencing execSync, cmd, and workspaceDir to find the call).

} else {
// Outside tmux — attach to existing session
console.log(`Session "${sessionName}" already exists`);
await focusTeamWindow(sessionName, windowName, workspaceDir, systemPromptFile);
attachToWindow(sessionName, windowName);
}

attachToWindow(sessionName, windowName);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
Expand Down
Loading
Loading