Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5563199
chore(version): bump to 3.260317.18 [skip ci]
github-actions[bot] Mar 17, 2026
040b667
feat: add team-lead liveness checks, inbox watcher, and CI coverage gate
Mar 17, 2026
ced87a7
fix: force main branch in createTempGitRepo test helper for worktree …
Mar 17, 2026
c2c2246
Merge pull request #645 from automagik-dev/feat/genie-hardening
namastex888 Mar 17, 2026
df2870f
feat(session): replace single genie session with per-project sessions
Mar 17, 2026
e8db57c
feat(tmux): add dual status bar with project/task tabs and stub scripts
Mar 17, 2026
a76f265
feat(tmux): enrich status bar scripts with live agent data from worke…
Mar 17, 2026
4631b38
style: fix biome formatting in team-auto-spawn resolveSession signature
Mar 17, 2026
83cd1fb
refactor: use filterBySession in resolveRecipient to reduce complexity
Mar 17, 2026
9414139
fix: resolve null/undefined type mismatch in inbox-watcher and extrac…
Mar 17, 2026
f642101
fix: add sessionName argument to isTeamActive test calls and sort imp…
Mar 17, 2026
29b892c
fix: unset GIT_DIR in husky hooks to prevent test pollution
Mar 17, 2026
82e999a
fix: unset GIT_DIR in husky hooks to prevent test pollution
Mar 17, 2026
ce60fa0
feat(tmux): add dual status bar config with project/task tab scripts
Mar 17, 2026
9d5dae3
feat(msg): add session isolation to genie send for per-project scoping
Mar 17, 2026
d7ae157
fix(tmux): avoid cross-project session collisions and require tmux >=…
Mar 17, 2026
56e2fac
Merge pull request #646 from automagik-dev/tmux-split-tabbar
namastex888 Mar 17, 2026
136e08c
Merge pull request #647 from automagik-dev/fix/gitdir-leak-in-hooks
namastex888 Mar 17, 2026
c5b6121
fix: stabilize session isolation and inbox watcher
automagik-genie Mar 17, 2026
2405981
chore: satisfy dead-code gate
automagik-genie Mar 17, 2026
ba70b22
fix: correct protocol router cleanup typing
automagik-genie Mar 17, 2026
b6486ee
test: boost coverage to 68% to fix CI coverage gate
Mar 17, 2026
b80e277
test: add coverage headroom for CI (69.25% funcs)
Mar 17, 2026
2505055
Merge pull request #648 from automagik-dev/fix/ci-coverage-gate
namastex888 Mar 17, 2026
c72a11a
fix(ci): prevent set -e from aborting coverage capture
Mar 17, 2026
90950d4
Merge pull request #649 from automagik-dev/fix/ci-coverage-capture
namastex888 Mar 17, 2026
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.18",
"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
18 changes: 16 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,22 @@ jobs:
- name: Dead code check (knip)
run: bunx knip

- name: Test
run: bun test || bun test
- name: Test with coverage
run: |
COVERAGE_OUTPUT=$(bun test --coverage 2>&1) || true
echo "$COVERAGE_OUTPUT"
LINE_COV=$(echo "$COVERAGE_OUTPUT" | grep "All files" | awk -F'|' '{print $2}' | tr -d ' ')

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

Coverage parsing is fragile — depends on exact bun test --coverage output format.

If Bun changes the coverage table format, this grep/awk chain will silently fail. Consider adding a fallback or documenting the expected format.

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

In @.github/workflows/ci.yml at line 72, The coverage parsing step that sets
LINE_COV from COVERAGE_OUTPUT using grep/awk is fragile and can silently fail if
`bun test --coverage` output changes; update the logic around LINE_COV so it
first validates that COVERAGE_OUTPUT contains the expected "All files" table row
(or a known regex) and provide a clear fallback path (e.g., attempt alternate
parsing patterns or emit a warnings + fail the job) when the primary grep/awk
extraction returns empty; reference and update the variables and command strings
LINE_COV, COVERAGE_OUTPUT, and the `bun test --coverage` invocation so the
workflow either robustly extracts the coverage percentage or fails fast with a
documented message.

echo "Line coverage: ${LINE_COV}%"
if [ -z "$LINE_COV" ]; then
echo "WARNING: Could not parse coverage — skipping threshold check"
exit 0
fi
Comment on lines +74 to +77

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

Silent pass when coverage parsing fails may mask broken coverage reports.

If coverage output format changes or bun test --coverage fails to produce parseable output, the step exits 0 and the build passes without coverage enforcement. Consider failing the build or at least emitting a more prominent warning.

Suggested alternative
          if [ -z "$LINE_COV" ]; then
-           echo "WARNING: Could not parse coverage — skipping threshold check"
-           exit 0
+           echo "ERROR: Could not parse coverage from output"
+           exit 1
          fi
📝 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
if [ -z "$LINE_COV" ]; then
echo "WARNING: Could not parse coverage — skipping threshold check"
exit 0
fi
if [ -z "$LINE_COV" ]; then
echo "ERROR: Could not parse coverage from output"
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 74 - 77, The current CI step silently
exits 0 when coverage parsing fails (the if [ -z "$LINE_COV" ] branch), which
can mask broken coverage reports; change this to fail the job or at minimum
produce a clear error: replace the exit 0 with exit 1 and update the echo to a
more prominent error (e.g., echo "ERROR: Could not parse coverage — failing
build") so that when LINE_COV is empty the workflow fails and surfaces the
parsing problem.

THRESHOLD=68
if [ "$(echo "$LINE_COV < $THRESHOLD" | bc -l)" = "1" ]; then
echo "FAILED: Coverage ${LINE_COV}% is below ${THRESHOLD}% minimum"
exit 1
fi
echo "PASSED: Coverage ${LINE_COV}% meets ${THRESHOLD}% minimum"

publish-next:
name: Publish @next
Expand Down
4 changes: 4 additions & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Prevent GIT_DIR/GIT_WORK_TREE from leaking into child processes (see pre-push)
unset GIT_DIR
unset GIT_WORK_TREE

bunx commitlint --edit "$1"
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Prevent GIT_DIR/GIT_WORK_TREE from leaking into child processes (see pre-push)
unset GIT_DIR
unset GIT_WORK_TREE

# Check if CI is red on current branch — block commits on broken branches
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$branch" ] && [ "${SKIP_CI_CHECK:-}" != "1" ] && command -v gh >/dev/null 2>&1; then
Expand Down
7 changes: 7 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
set -e

# Prevent GIT_DIR/GIT_WORK_TREE from leaking into child processes.
# Git sets these in hook environments, but bun test spawns git commands
# that must discover their own repos. Without this, test setupTestRepo()
# commits land on the REAL repo branch, causing catastrophic data loss.
unset GIT_DIR
unset GIT_WORK_TREE

remote="$1"
while read local_ref local_oid remote_ref remote_oid; do
if echo "$remote_ref" | grep -qE 'refs/heads/(main|master)$'; then
Expand Down
1 change: 0 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
"$schema": "https://unpkg.com/knip@5.85.0/schema.json",
"project": ["src/**/*.ts"],
"ignoreBinaries": ["tmux", "which"],
"ignore": ["src/lib/team-auto-spawn.ts"],
"ignoreExportsUsedInFile": true
}
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.18",
"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.18",

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.

high

The new version 3.260317.18 appears to be based on a future date (March 17, 2026), according to the versioning scheme 3.YYMMDD.N described in scripts/version.ts. This might be caused by an incorrect system clock in the environment where the version was generated. Using a future date can be misleading and may cause issues with tools or processes that rely on semantic or time-based versioning. It would be best to ensure the version numbers reflect the actual date of creation.

"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.18",
"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.18",
"private": true,
"description": "Runtime dependencies for genie bundled CLIs",
"type": "module",
Expand Down
70 changes: 70 additions & 0 deletions scripts/tmux/genie-projects.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash

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

Guard the Bash 4+ dependency.

declare -A requires Bash 4.0 or later for associative-array support, but #!/usr/bin/env bash does not enforce a minimum version. Systems with Bash 3.x will fail at lines 16 and 44 when the script attempts to declare associative arrays. Add an explicit version check (e.g., [[ ${BASH_VERSINFO[0]} -lt 4 ]]) or refactor to use indexed arrays instead.

🤖 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 1, The script uses associative arrays
via "declare -A" (seen at the spots that fail) but doesn't enforce Bash >=4; add
a Bash version guard at the top using BASH_VERSINFO (e.g., check
BASH_VERSINFO[0] < 4) and print a clear error and exit if too old, or
alternatively refactor the associative-array usage (the "declare -A"
occurrences) to indexed arrays and map keys to indices; implement the
version-check approach by adding the guard before any "declare -A" usage so the
script fails fast with a helpful message if the shell is older than 4.

# 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=""
Comment on lines +18 to +24

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

Exclude team-lead rows from the session count.

saveTeamLeadEntry() now writes role: 'team-lead' entries into workers for the same session (src/lib/agent-registry.ts:333-356). This aggregation counts those daemon rows too, so a project with no actual tasks can still render as (1).

Suggested fix
-  worker_data=$(jq -r '
-    .workers // {} | to_entries
+  worker_data=$(jq -r '
+    .workers // {} | to_entries
+    | map(select(.value.role != "team-lead"))
     | group_by(.value.session)
     | map({session: .[0].value.session, count: length})
📝 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
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=""
worker_data=$(jq -r '
.workers // {} | to_entries
| map(select(.value.role != "team-lead"))
| group_by(.value.session)
| map({session: .[0].value.session, count: length})
| map(.session + "\t" + (.count | tostring))
| .[]
' "$workers_file" 2>/dev/null) || worker_data=""
🤖 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 18 - 24, The session-count
aggregation is including daemon "team-lead" rows written by saveTeamLeadEntry(),
so update the jq pipeline that produces worker_data to exclude entries where
.value.role == "team-lead" before grouping by session; specifically modify the
pipeline starting from ".workers // {} | to_entries" to filter out team-lead
entries (e.g., using map(select(.value.role != "team-lead"))) then continue with
group_by(.value.session) and the existing count logic so only real worker tasks
are counted.


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

output=""
declare -A seen_sessions=()

if [[ "$has_tmux" == "true" ]]; then
# Merge tmux sessions with agent counts
while IFS=$'\t' read -r session_name window_count; do
[[ -z "$session_name" ]] && continue
seen_sessions["$session_name"]=1

# 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
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 < <(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
count="${agent_counts[$sess]}"
output+="#[fg=#b8a9c9,bg=#1a1a2e] ${sess} (${count}) "
done

echo -n "$output"
138 changes: 138 additions & 0 deletions scripts/tmux/genie-tasks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash

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

❓ Verification inconclusive

Script executed:

# First, check if the file exists and get its line count
wc -l scripts/tmux/genie-tasks.sh

Repository: automagik-dev/genie


Repository: automagik-dev/genie
Exit code: 0

stdout:

138 scripts/tmux/genie-tasks.sh

Script executed:

# Read the entire file to verify the shebang, lines 86-87, and any version checks
cat -n scripts/tmux/genie-tasks.sh

Repository: automagik-dev/genie


Repository: automagik-dev/genie
Exit code: 0

stdout:

     1	#!/usr/bin/env bash
     2	# Genie TUI — task window tabs for tmux bottom status bar
     3	# Reads workers.json for agent counts + status emoji per window.
     4	#
     5	# Usage: genie-tasks.sh <session_name>
     6	#
     7	# Output format per window:
     8	#   Active:   #[bg=#7b2ff7,fg=#e0e0e0,bold] name ×count emoji #[bg=#16213e,fg=#7b2ff7]
     9	#   Inactive: #[fg=#b8a9c9,bg=#16213e] name ×count emoji
    10	#
    11	# Env: GENIE_WORKERS — override path to workers.json (for testing)
    12	
    13	set -euo pipefail
    14	
    15	session_name="${1:-}"
    16	if [[ -z "$session_name" ]]; then
    17	  echo -n ""
    18	  exit 0
    19	fi
    20	
    21	# Verify session exists (skip check when running with mock data and no tmux)
    22	has_tmux=true
    23	if ! command -v tmux &>/dev/null; then
    24	  has_tmux=false
    25	elif ! tmux has-session -t "$session_name" 2>/dev/null; then
    26	  # Session doesn't exist in tmux — but if we have workers data, still render
    27	  has_tmux=false
    28	fi
    29	
    30	workers_file="${GENIE_WORKERS:-${HOME}/.genie/workers.json}"
    31	
    32	# If workers file doesn't exist, fall back to plain tmux window list
    33	if [[ ! -f "$workers_file" ]]; then
    34	  if [[ "$has_tmux" == "false" ]]; then
    35	    echo -n ""
    36	    exit 0
    37	  fi
    38	  # Fallback: plain window list without agent data
    39	  output=""
    40	  while IFS=$'\t' read -r window_index window_name window_active; do
    41	    [[ -z "$window_index" ]] && continue
    42	    if [[ "$window_active" == "1" ]]; then
    43	      output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${window_index}:${window_name} #[bg=#16213e,fg=#7b2ff7] "
    44	    else
    45	      output+="#[fg=#b8a9c9,bg=#16213e] ${window_index}:${window_name} "
    46	    fi
    47	  done < <(tmux list-windows -t "$session_name" -F "#{window_index}	#{window_name}	#{window_active}" 2>/dev/null)
    48	  echo -n "$output"
    49	  exit 0
    50	fi
    51	
    52	# State priority for worst-state-wins aggregation (higher = worse)
    53	# error > permission > working > spawning > idle > done > suspended
    54	#
    55	# Single jq pass: filter workers by session, group by windowName,
    56	# compute count + aggregate state per window.
    57	# Output: tab-separated lines: windowName\tcount\tworst_state
    58	worker_data=$(jq -r --arg sess "$session_name" '
    59	  # State priority mapping
    60	  def state_priority:
    61	    {"error": 7, "permission": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};
    62	
    63	  # Emoji mapping
    64	  def state_emoji:
    65	    {"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "suspended": "💤"};
    66	
    67	  .workers // {} | to_entries
    68	  | map(select(.value.session == $sess))
    69	  | group_by(.value.windowName // .value.team // "unknown")
    70	  | map({
    71	      window: (.[0].value.windowName // .[0].value.team // "unknown"),
    72	      count: length,
    73	      worst_state: (
    74	        map(.value.state // "idle")
    75	        | map(. as $s | state_priority[$s] // 0)
    76	        | max
    77	        | . as $max_pri
    78	        | state_priority | to_entries | map(select(.value == $max_pri)) | .[0].key // "idle"
    79	      ),
    80	    })
    81	  | map(.window + "\t" + (.count | tostring) + "\t" + (state_emoji[.worst_state] // ""))
    82	  | .[]
    83	' "$workers_file" 2>/dev/null) || worker_data=""
    84	
    85	# Build lookup table from worker data (avoid subshells in loop)
    86	declare -A window_counts=()
    87	declare -A window_emojis=()
    88	
    89	if [[ -n "$worker_data" ]]; then
    90	  while IFS=$'\t' read -r wname wcount wemoji; do
    91	    [[ -z "$wname" ]] && continue
    92	    window_counts["$wname"]="$wcount"
    93	    window_emojis["$wname"]="$wemoji"
    94	  done <<< "$worker_data"
    95	fi
    96	
    97	# If tmux is available, render with live window info
    98	if [[ "$has_tmux" == "true" ]]; then
    99	  output=""
   100	  while IFS=$'\t' read -r window_index window_name window_active; do
   101	    [[ -z "$window_index" ]] && continue
   102	
   103	    count="${window_counts[$window_name]:-}"
   104	    emoji="${window_emojis[$window_name]:-}"
   105	
   106	    # Build enriched label: name ×count emoji
   107	    label="${window_name}"
   108	    if [[ -n "$count" && "$count" -gt 0 ]]; then
   109	      label+=" ×${count}"
   110	    fi
   111	    if [[ -n "$emoji" ]]; then
   112	      label+=" ${emoji}"
   113	    fi
   114	
   115	    if [[ "$window_active" == "1" ]]; then
   116	      output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${label} #[bg=#16213e,fg=#7b2ff7] "
   117	    else
   118	      output+="#[fg=#b8a9c9,bg=#16213e] ${label} "
   119	    fi
   120	  done < <(tmux list-windows -t "$session_name" -F "#{window_index}	#{window_name}	#{window_active}" 2>/dev/null)
   121	  echo -n "$output"
   122	else
   123	  # No tmux available (testing mode): render from workers.json data only
   124	  output=""
   125	  for wname in "${!window_counts[@]}"; do
   126	    count="${window_counts[$wname]}"
   127	    emoji="${window_emojis[$wname]:-}"
   128	    label="${wname}"
   129	    if [[ -n "$count" && "$count" -gt 0 ]]; then
   130	      label+=" ×${count}"
   131	    fi
   132	    if [[ -n "$emoji" ]]; then
   133	      label+=" ${emoji}"
   134	    fi
   135	    output+="#[fg=#b8a9c9,bg=#16213e] ${label} "
   136	  done
   137	  echo -n "$output"
   138	fi

Add Bash 4+ version check or declare the dependency explicitly.

This script uses associative arrays (declare -A at lines 86–87, with subscript expansion at lines 103–104, 125–126) which require Bash 4.0+, but the shebang #!/usr/bin/env bash does not enforce a minimum version. The script will fail outright on Bash 3.x with no warning. Either add an explicit version check at the start (e.g., if [[ ${BASH_VERSINFO[0]} -lt 4 ]]; then echo "Bash 4+ required"; exit 1; fi) or update the shebang to reflect the actual requirement.

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

In `@scripts/tmux/genie-tasks.sh` at line 1, The script uses associative arrays
via "declare -A" (e.g., the associative arrays created with declare -A and
subscript expansion) which requires Bash 4+, so add an explicit runtime version
guard at the top: check BASH_VERSINFO[0] and exit with a clear message if it's
less than 4 (or alternatively change the shebang to a stricter interpreter that
guarantees Bash 4+); ensure the check runs before any use of declare -A or
associative subscripts so the script fails fast with a helpful error when
running under Bash 3.x.

# Genie TUI — task window tabs for tmux bottom status bar
# Reads workers.json for agent counts + status emoji per window.
#
# Usage: genie-tasks.sh <session_name>
#
# Output format per window:
# Active: #[bg=#7b2ff7,fg=#e0e0e0,bold] name ×count emoji #[bg=#16213e,fg=#7b2ff7]
# Inactive: #[fg=#b8a9c9,bg=#16213e] name ×count emoji
#
# Env: GENIE_WORKERS — override path to workers.json (for testing)

set -euo pipefail

session_name="${1:-}"
if [[ -z "$session_name" ]]; then
echo -n ""
exit 0
fi

# Verify session exists (skip check when running with mock data and no tmux)
has_tmux=true
if ! command -v tmux &>/dev/null; then
has_tmux=false
elif ! tmux has-session -t "$session_name" 2>/dev/null; then
# Session doesn't exist in tmux — but if we have workers data, still render
has_tmux=false
fi

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

# If workers file doesn't exist, fall back to plain tmux window list
if [[ ! -f "$workers_file" ]]; then
if [[ "$has_tmux" == "false" ]]; then
echo -n ""
exit 0
fi
# Fallback: plain window list without agent data
output=""
while IFS=$'\t' read -r window_index window_name window_active; do
[[ -z "$window_index" ]] && continue
if [[ "$window_active" == "1" ]]; then
output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${window_index}:${window_name} #[bg=#16213e,fg=#7b2ff7] "
else
output+="#[fg=#b8a9c9,bg=#16213e] ${window_index}:${window_name} "
fi
done < <(tmux list-windows -t "$session_name" -F "#{window_index} #{window_name} #{window_active}" 2>/dev/null)
echo -n "$output"
exit 0
fi

# State priority for worst-state-wins aggregation (higher = worse)
# error > permission > working > spawning > idle > done > suspended
#
# Single jq pass: filter workers by session, group by windowName,
# compute count + aggregate state per window.
# Output: tab-separated lines: windowName\tcount\tworst_state
worker_data=$(jq -r --arg sess "$session_name" '
# State priority mapping
def state_priority:
{"error": 7, "permission": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};

# Emoji mapping
def state_emoji:
{"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "suspended": "💤"};

.workers // {} | to_entries
| map(select(.value.session == $sess))
| group_by(.value.windowName // .value.team // "unknown")
| map({
window: (.[0].value.windowName // .[0].value.team // "unknown"),
count: length,
worst_state: (
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"
),
})
| map(.window + "\t" + (.count | tostring) + "\t" + (state_emoji[.worst_state] // ""))
| .[]
' "$workers_file" 2>/dev/null) || worker_data=""
Comment on lines +58 to +83

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

Handle the full registry schema here.

AgentState includes question, and the registry type also allows window as an alias of windowName (src/lib/agent-registry.ts:1-90). This query ignores both, so question-blocked agents lose their emoji and any window-only entry will not join the live tmux window name when the bar is rendered.

Suggested fix
-  def state_priority:
-    {"error": 7, "permission": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};
+  def state_priority:
+    {"error": 8, "permission": 7, "question": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};

-  def state_emoji:
-    {"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "suspended": "💤"};
+  def state_emoji:
+    {"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "question": "❔", "suspended": "💤"};

-  | group_by(.value.windowName // .value.team // "unknown")
+  | group_by(.value.windowName // .value.window // .value.team // "unknown")
   | map({
-      window: (.[0].value.windowName // .[0].value.team // "unknown"),
+      window: (.[0].value.windowName // .[0].value.window // .[0].value.team // "unknown"),
📝 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
worker_data=$(jq -r --arg sess "$session_name" '
# State priority mapping
def state_priority:
{"error": 7, "permission": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};
# Emoji mapping
def state_emoji:
{"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "suspended": "💤"};
.workers // {} | to_entries
| map(select(.value.session == $sess))
| group_by(.value.windowName // .value.team // "unknown")
| map({
window: (.[0].value.windowName // .[0].value.team // "unknown"),
count: length,
worst_state: (
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"
),
})
| map(.window + "\t" + (.count | tostring) + "\t" + (state_emoji[.worst_state] // ""))
| .[]
' "$workers_file" 2>/dev/null) || worker_data=""
worker_data=$(jq -r --arg sess "$session_name" '
# State priority mapping
def state_priority:
{"error": 8, "permission": 7, "question": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1};
# Emoji mapping
def state_emoji:
{"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "question": "❔", "suspended": "💤"};
.workers // {} | to_entries
| map(select(.value.session == $sess))
| group_by(.value.windowName // .value.window // .value.team // "unknown")
| map({
window: (.[0].value.windowName // .[0].value.window // .[0].value.team // "unknown"),
count: length,
worst_state: (
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"
),
})
| map(.window + "\t" + (.count | tostring) + "\t" + (state_emoji[.worst_state] // ""))
| .[]
' "$workers_file" 2>/dev/null) || worker_data=""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/tmux/genie-tasks.sh` around lines 58 - 83, The jq pipeline ignores
the registry's alternate fields: add support for the AgentState 'question' and
the 'window' alias so agents with question state get an emoji and entries using
window (not windowName) are grouped correctly. Update the state_priority and
state_emoji defs to include "question" (with appropriate priority and emoji),
and change the selectors used where window name and state are read to prefer
.value.window // .value.windowName and to compute worst_state from (.value.state
// .value.question // "idle"); keep the rest of the pipeline intact (look for
the map/group_by that builds window and worst_state in the worker_data jq
expression).


# Build lookup table from worker data (avoid subshells in loop)
declare -A window_counts=()
declare -A window_emojis=()

if [[ -n "$worker_data" ]]; then
while IFS=$'\t' read -r wname wcount wemoji; do
[[ -z "$wname" ]] && continue
window_counts["$wname"]="$wcount"
window_emojis["$wname"]="$wemoji"
done <<< "$worker_data"
fi

# If tmux is available, render with live window info
if [[ "$has_tmux" == "true" ]]; then
output=""
while IFS=$'\t' read -r window_index window_name window_active; do
[[ -z "$window_index" ]] && continue

count="${window_counts[$window_name]:-}"
emoji="${window_emojis[$window_name]:-}"

# Build enriched label: name ×count emoji
label="${window_name}"
if [[ -n "$count" && "$count" -gt 0 ]]; then
label+=" ×${count}"
fi
if [[ -n "$emoji" ]]; then
label+=" ${emoji}"
fi

if [[ "$window_active" == "1" ]]; then
output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${label} #[bg=#16213e,fg=#7b2ff7] "
else
output+="#[fg=#b8a9c9,bg=#16213e] ${label} "
fi
done < <(tmux list-windows -t "$session_name" -F "#{window_index} #{window_name} #{window_active}" 2>/dev/null)
echo -n "$output"
else
# No tmux available (testing mode): render from workers.json data only
output=""
for wname in "${!window_counts[@]}"; do
count="${window_counts[$wname]}"
emoji="${window_emojis[$wname]:-}"
label="${wname}"
if [[ -n "$count" && "$count" -gt 0 ]]; then
label+=" ×${count}"
fi
if [[ -n "$emoji" ]]; then
label+=" ${emoji}"
fi
output+="#[fg=#b8a9c9,bg=#16213e] ${label} "
done
echo -n "$output"
fi
37 changes: 27 additions & 10 deletions scripts/tmux/genie.tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,34 @@ setw -g mode-style "bg=#7b2ff7,fg=#e0e0e0"
setw -g clock-mode-colour "#7b2ff7"

# ============================================================================
# STATUS BAR
# STATUS BAR — Dual bar (tmux 3.2+) with fallback
# ============================================================================
set -g status on
set -g status-interval 5
set -g status-position top
set -g status-justify left
set -g status-style "bg=#1a1a2e,fg=#e0e0e0"

# --- Left: Genie branding + version ---
# --- Status bar height: 2 lines on tmux >= 3.2, 1 line otherwise ---
# The if-shell at the bottom of this file handles the fallback.
# Default to 2 (overridden by if-shell on old tmux).
set -g status 2

# --- Top bar (status-format[0]): branding + project tabs + system info ---
set -g status-format[0] "#[align=left,bg=#1a1a2e]#[bg=#7b2ff7,fg=#e0e0e0,bold] #(genie --version 2>/dev/null | head -1 || echo 'Genie') #[bg=#1a1a2e,fg=#7b2ff7] #($HOME/.genie/scripts/genie-projects.sh)#[align=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 "

# --- Bottom bar (status-format[1]): task window tabs for active session ---
set -g status-format[1] "#[align=left,bg=#16213e] #($HOME/.genie/scripts/genie-tasks.sh #{session_name}) "

# --- Legacy single-line fallback values (used when dual status is unavailable) ---
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]"

# --- Right: git | CPU | RAM | clock ---
set -g status-right-length 120
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
# WINDOW (TAB) STYLING — clickable tabs (used by bottom bar in dual mode,
# or as main tabs in single-bar fallback)
# ============================================================================

# Tab separator
Expand All @@ -80,14 +90,16 @@ set -g visual-activity off
set -g window-status-activity-style "fg=#f5a623,bg=#1a1a2e"

# ============================================================================
# SECOND STATUS LINE — bottom tab bar (tmux 3.2+)
# VERSION GUARD — fallback for tmux < 3.2 (no dual status bar support)
# ============================================================================
# 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) "
# tmux < 3.2 gets single bar. tmux >= 3.2 keeps dual bar (status-format[0..1]).
# Check major/minor explicitly; tmux 3.0/3.1 report status-format support as unavailable.
if-shell 'tmux -V | sed -nE "s/^tmux ([0-9]+)\\.([0-9]+).*/\\1 \\2/" | awk "{exit !($1 > 3 || ($1 == 3 && $2 >= 2))}"' \
'set -g status 2' \
'set -g status on'
Comment on lines +97 to +99

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:

# First, let me check the actual file content to understand the context better
cat -n scripts/tmux/genie.tmux.conf | sed -n '95,115p'

Repository: automagik-dev/genie

Length of output: 1095


🌐 Web query:

tmux version output format OpenBSD

💡 Result:

The tmux version output format on OpenBSD, obtained by running tmux -V, is a string in the form "openbsd-X.Y" where X.Y corresponds to the OpenBSD release version (e.g., "openbsd-6.8" on OpenBSD 6.8). This differs from portable tmux versions, which output "tmux 3.3a" style numbers. The OpenBSD man page confirms the -V flag "Report the tmux version." As of 2026-03-17, OpenBSD current likely uses a format like "openbsd-7.6" or newer, based on ongoing development.

Citations:


🌐 Web query:

tmux status-format support version history

💡 Result:

tmux status-format support was introduced in tmux 2.9. It is an array option that configures the format of each line in a multi-line status bar (when status is set to 2, 3, 4, or 5). The default status-format references existing status-* options like status-left, window-status-format, and status-right. Prior to 2.9, the status bar was configured using individual options such as status-left, status-right, window-status-format, and window-status-current-format, which supported format expansions (#[...] and #{...}). status-format enables per-line customization for multi-line status bars and full configuration using expanded formats and styles. Examples from documentation: - set -g status 2 - set -g status-format[0] '#[align=left] #{status-left}' - set -g status-format[1] '#[align=centre] #{window-list}' - set -g status-format[2] '#[align=right] #{status-right}' It has been available and unchanged in all subsequent versions up to the latest (3.4+ as of 2024), with ongoing format enhancements like mathematical operations in 3.2.

Citations:


Fix the tmux version gate.

tmux -V on OpenBSD reports openbsd-6.x instead of tmux X.Y, so this regex never matches. The sed pattern expects ^tmux but gets openbsd, producing no output to awk. This causes the version check to fail and forces OpenBSD systems into the single-line fallback (set -g status on) even though they support status 2 and status-format[] (available since tmux 2.9).

Parse both version formats or probe the status-format[] capability directly.

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

In `@scripts/tmux/genie.tmux.conf` around lines 97 - 99, The if-shell version
check in genie.tmux.conf uses a sed regex that only matches lines beginning with
"tmux" so OpenBSD's "openbsd-6.x" tmux -V output fails and falls back to 'set -g
status on'; update the if-shell condition (the line containing if-shell 'tmux -V
| sed ... | awk ...') to either (a) broaden the regex to accept the OpenBSD
prefix (match either "tmux X.Y" or "openbsd-* X.Y") or (b) replace the version
parse with a capability probe that checks for the presence of status-format
(i.e., run a tmux query for the status-format capability and use that result to
choose between 'set -g status 2' and 'set -g status on'); modify that exact
if-shell invocation accordingly so OpenBSD tmux instances that support
status-format use the two-line status.


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

# Reload config
Expand All @@ -96,3 +108,8 @@ bind r source-file ~/.tmux.conf \; display-message "Config reloaded"
# Vi copy mode bindings
bind -T copy-mode-vi v send-keys -X begin-selection
bind -T copy-mode-vi y send-keys -X copy-selection-and-cancel

# --- Session switching (project tabs) ---
# Ctrl+) / Ctrl+( — switch between project sessions
bind -n C-) switch-client -n
bind -n C-( switch-client -p
Comment on lines +112 to +115

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:

# First, let's examine the file and understand the context
cat -n scripts/tmux/genie.tmux.conf | sed -n '100,125p'

Repository: automagik-dev/genie

Length of output: 726


🏁 Script executed:

# Check if there are other key bindings in the file for reference
rg "bind -n" scripts/tmux/genie.tmux.conf -n

Repository: automagik-dev/genie

Length of output: 128


🌐 Web query:

tmux key binding portability C-) shifted keys extended-key support

💡 Result:

Tmux key binding portability for C-), shifted keys, and extended-key support is limited by terminal capabilities and tmux configuration. C-) (Ctrl + )) is not portable without extended keys. The ) key is ASCII 0x29, Ctrl+) would ideally be 0x09 (Tab), but terminals typically send the same sequence for Ctrl+) as Tab, making distinct binding impossible without extended key support. No sources explicitly mention binding C-); similar keys like C-/, C-', C-! are unsupported traditionally as terminals do not send unique sequences ({1,15,16}). Shifted keys (e.g., S-a becomes A): Tmux uses uppercase or alternate names (e.g., no S-a, use A; S-Tab is BTab). For ASCII, Shift sends the shifted character; tmux treats it as such, ignoring layout. S- prefix used for function keys like S-Left. Binding shifted ASCII is not distinct from unshifted uppercase without extended keys ({1}). Extended-key support (tmux 2.4+, improved in 3.x): Enables binding Ctrl/Shift on more keys (e.g., C-1, C-., C-^) via modifyOtherKeys or CSI u sequences from terminals like xterm, mintty, iTerm2, foot. Configure with set -g extended-keys on|always or set -as terminal-features 'xterm*:extkeys' for auto-detection. Tmux recognizes extended sequences for bindings and forwards to inner apps if they request (or always mode). Portability issues: Terminal must support (check with cat outside/inside tmux); mismatches cause failures. Shifted uppercase may require binding as C-S-O not C-S-o ({1,3,5,7,17}). Test bindings with bind C-1 lsk and press key; use cat to verify sequences ({1}). For portability: Use extended-keys on, ensure TERM correct (e.g., xterm-256color), test per-terminal with cat(1). Not all terminals agree on sequences; xterm is reference ({1}).

Citations:


🏁 Script executed:

# Check if extended-keys is configured in the file
rg "extended-keys" scripts/tmux/genie.tmux.conf

Repository: automagik-dev/genie

Length of output: 45


🏁 Script executed:

# Check what other Alt/Meta bindings exist for reference
rg "bind -n M-" scripts/tmux/genie.tmux.conf

Repository: automagik-dev/genie

Length of output: 45


Use a portable session-switch binding.

C-) and C-( won't work on most terminals—tmux needs extended-key support enabled to map shifted ASCII keys distinctly. Without set -g extended-keys on and compatible terminal support (xterm, iTerm2, foot, etc.), these bindings won't be reachable. Use M-Left/M-Right instead, or add explicit extended-key configuration and verify with your terminal.

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

In `@scripts/tmux/genie.tmux.conf` around lines 112 - 115, The current bindings
bind -n C-) and bind -n C-( are not portable because many terminals don't emit
distinct shifted-key sequences; update the configuration by replacing those
bindings with portable alternatives (e.g., bind -n M-Right and bind -n M-Left)
or explicitly enable extended keys by adding set -g extended-keys on at the top
and document that a compatible terminal (xterm/iTerm2/foot) is required; update
the session-switching bind lines (bind -n C-) and (bind -n C-() or their
surrounding comments to reflect the chosen approach so maintainers know which
method is used.

Loading
Loading