From 4cb84cc7bb3eb678e0785bdccbad35cf20845cc4 Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Thu, 12 Feb 2026 23:55:54 -0800 Subject: [PATCH 1/8] Fix nounset crash when running codeman with no args --- bin/codeman | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bin/codeman b/bin/codeman index 7dc33ed..04e8174 100755 --- a/bin/codeman +++ b/bin/codeman @@ -895,7 +895,13 @@ while [ "$#" -gt 0 ]; do ;; esac done -set -- "${PARSED_ARGS[@]}" +# Bash 3.2 + `set -u` treats expanding an empty array as "unbound variable". +# Only expand when non-empty to support running `codeman` with no args. +if [ "${#PARSED_ARGS[@]}" -gt 0 ]; then + set -- "${PARSED_ARGS[@]}" +else + set -- +fi cmd_word="${1:-}" case "$cmd_word" in From 652f5831ec3196cb2000a4c4ca9e805692ef6d7f Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 00:11:48 -0800 Subject: [PATCH 2/8] chore: bump version to v0.1.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 6e8bf73..17e51c3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.1.1 From 142a8ec52f09af097c6a843515438ac7aebcde2e Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 00:24:32 -0800 Subject: [PATCH 3/8] test: add bats regression tests and CI matrix --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++-- tests/test_codeman.bats | 29 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/test_codeman.bats diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d449790..43262b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ permissions: contents: read jobs: - shell: + shellcheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,10 +17,39 @@ jobs: - name: Bash syntax check run: | bash -n bin/codeman + bash -n install.sh - name: ShellCheck (best-effort) run: | sudo apt-get update sudo apt-get install -y shellcheck - shellcheck -x bin/codeman + shellcheck -x bin/codeman install.sh + bats: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install bats + run: | + if [ "${RUNNER_OS}" = "Linux" ]; then + sudo apt-get update + sudo apt-get install -y bats + elif [ "${RUNNER_OS}" = "macOS" ]; then + brew install bats-core + else + echo "Unsupported OS: ${RUNNER_OS}" >&2 + exit 2 + fi + + - name: Show bash version + run: | + bash --version | head -n 1 + + - name: Run bats tests + run: | + bats -r tests diff --git a/tests/test_codeman.bats b/tests/test_codeman.bats new file mode 100644 index 0000000..4c5a32a --- /dev/null +++ b/tests/test_codeman.bats @@ -0,0 +1,29 @@ +#!/usr/bin/env bats + +@test "codeman (no args) prints mode picker and does not crash" { + run ./bin/codeman + + # codeman intentionally exits 2 when no args are provided + [ "$status" -eq 2 ] + + [[ "$output" == *"Pick a mode explicitly."* ]] + [[ "$output" == *"Available modes:"* ]] + + # Regression guard for Bash 3.2 + `set -u` empty-array expansion. + [[ "$output" != *"unbound variable"* ]] + [[ "$output" != *"PARSED_ARGS[@]"* ]] +} + +@test "codeman help prints usage" { + run ./bin/codeman help + [ "$status" -eq 0 ] + [[ "$output" == *"USAGE"* ]] + [[ "$output" == *"codeman"* ]] +} + +@test "codeman completion zsh prints a completion script" { + run ./bin/codeman completion zsh + [ "$status" -eq 0 ] + [[ "$output" == *"#compdef codeman"* ]] +} + From f9a0836a1c284d393e1ccddd6bb86b5ac94c7396 Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 07:28:41 -0800 Subject: [PATCH 4/8] Fix Bash 3.2 incompatibility (mapfile) --- bin/codeman | 13 +++++++++++-- tests/test_codeman.bats | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/bin/codeman b/bin/codeman index 04e8174..6ccb94c 100755 --- a/bin/codeman +++ b/bin/codeman @@ -160,7 +160,11 @@ confirm_mode_if_needed() { esac local -a cmd_lines - mapfile -t cmd_lines < <(printf '%s\n' "$cmd_str" | fold -s -w 96) + cmd_lines=() + # Bash 3.2 (macOS default) doesn't have `mapfile`. + while IFS= read -r _line; do + cmd_lines+=("$_line") + done < <(printf '%s\n' "$cmd_str" | fold -s -w 96) local -a lines lines+=("$status_line") @@ -812,7 +816,12 @@ run_codex() { mode="balanced" fi - mapfile -t flags < <(mode_flags "$mode") + # Bash 3.2 (macOS default) doesn't have `mapfile`. + flags=() + while IFS= read -r _flag; do + [ -n "$_flag" ] || continue + flags+=("$_flag") + done < <(mode_flags "$mode") local notify_text notify_text="$(print_notify_status)" diff --git a/tests/test_codeman.bats b/tests/test_codeman.bats index 4c5a32a..a6ae7ad 100644 --- a/tests/test_codeman.bats +++ b/tests/test_codeman.bats @@ -27,3 +27,18 @@ [[ "$output" == *"#compdef codeman"* ]] } +@test "codeman mode run works on bash 3.2 (no mapfile) with a stubbed codex" { + tmp="$(mktemp -d)" + mkdir -p "$tmp/bin" + cat >"$tmp/bin/codex" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +echo "STUB CODEX OK" +EOF + chmod +x "$tmp/bin/codex" + + # Avoid confirmations/notifications; just verify the mode path doesn't crash. + HOME="$tmp" PATH="$tmp/bin:$PATH" run ./bin/codeman -N -y l3 "hello" + [ "$status" -eq 0 ] + [[ "$output" == *"STUB CODEX OK"* ]] +} From f77e5261bf5fd1f914393314ff0e72a4a9e17468 Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 07:29:19 -0800 Subject: [PATCH 5/8] chore: bump version to v0.1.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 17e51c3..d917d3e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.1 +0.1.2 From 9ec3360e4574519db6c732e2a5878993c1dd4528 Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 08:06:26 -0800 Subject: [PATCH 6/8] feat: discord notifications for each agent message --- bin/codeman | 69 +++++++++++++++++++++++++++++++++++------ tests/test_codeman.bats | 29 +++++++++++++++++ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/bin/codeman b/bin/codeman index 6ccb94c..8565c25 100755 --- a/bin/codeman +++ b/bin/codeman @@ -26,6 +26,7 @@ CONFIG_FILE="$CONFIG_DIR/config.env" CODEMAN_NOTIFY_LAST_WAIT=0 CODEMAN_NOTIFY_LAST_COMPLETE=0 +CODEMAN_NOTIFY_LAST_MESSAGE=0 CODEMAN_NOTIFY_SUPPRESS=0 CODEMAN_CONFIRM="${CODEMAN_CONFIRM:-1}" CODEMAN_SAVED_NOTIFY_PREFIX="${CODEMAN_SAVED_NOTIFY_PREFIX:-}" @@ -286,7 +287,7 @@ WEBHOOK NOTIFICATIONS 🔔 - CODEMAN_NOTIFY_PREFIX="HOST-CODENAME" - CODEMAN_NOTIFY_HOST_CODENAME="HOST-CODENAME" - CODEMAN_NOTIFY_PROJECT_CODENAME="PROJECT-CODENAME" - - CODEMAN_NOTIFY_ON=wait,complete + - CODEMAN_NOTIFY_ON=wait,complete[,message] - CODEMAN_NOTIFY_COOLDOWN_SEC=30 - CODEMAN_NOTIFY_DISABLED=1 @@ -614,6 +615,13 @@ notify_with_cooldown() { local message="$2" local cooldown now last + # Message notifications are opt-in via CODEMAN_NOTIFY_ON and should not be + # throttled by default (users usually want one ping per assistant message). + if [ "$kind" = "message" ]; then + send_notification_message "$message" + return 0 + fi + cooldown="${CODEMAN_NOTIFY_COOLDOWN_SEC:-30}" now="$(date +%s)" @@ -646,22 +654,65 @@ handle_session_line() { local project="$4" local line_type evt out - command -v jq >/dev/null 2>&1 || return 0 - - line_type="$(printf '%s\n' "$line" | jq -r '.type // empty' 2>/dev/null || true)" + # Prefer jq when available, but fall back to string matching so notifications + # still work on systems where jq isn't on PATH. + if command -v jq >/dev/null 2>&1; then + line_type="$(printf '%s\n' "$line" | jq -r '.type // empty' 2>/dev/null || true)" + else + case "$line" in + *'"type":"event_msg"'*) line_type="event_msg" ;; + *'"type":"response_item"'*) line_type="response_item" ;; + *) return 0 ;; + esac + fi case "$line_type" in event_msg) - evt="$(printf '%s\n' "$line" | jq -r '.payload.type // empty' 2>/dev/null || true)" + if command -v jq >/dev/null 2>&1; then + evt="$(printf '%s\n' "$line" | jq -r '.payload.type // empty' 2>/dev/null || true)" + else + case "$line" in + *'"payload":{"type":"task_complete"'*) evt="task_complete" ;; + *'"payload":{"type":"agent_message"'*) evt="agent_message" ;; + *) evt="" ;; + esac + fi if [ "$evt" = "task_complete" ] && event_enabled complete; then notify_with_cooldown complete "✅ ${prefix} 📁 ${project} 🚀 Codeman task complete (mode: ${mode})." fi + if [ "$evt" = "agent_message" ] && event_enabled message; then + local msg + if command -v jq >/dev/null 2>&1; then + msg="$(printf '%s\n' "$line" | jq -r '.payload.message // empty' 2>/dev/null || true)" + else + msg="" + fi + + # Keep the ping readable: flatten whitespace and clip length. + msg="$(printf '%s' "$msg" | tr '\r\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')" + if [ -z "$msg" ]; then + notify_with_cooldown message "💬 ${prefix} 📁 ${project} New Codex message (mode: ${mode})." + else + if [ "${#msg}" -gt 200 ]; then + msg="${msg:0:200}..." + fi + notify_with_cooldown message "💬 ${prefix} 📁 ${project} ${msg}" + fi + fi ;; response_item) - evt="$(printf '%s\n' "$line" | jq -r '.payload.type // empty' 2>/dev/null || true)" - if [ "$evt" = "function_call_output" ] && event_enabled wait; then - out="$(printf '%s\n' "$line" | jq -r '.payload.output // ""' 2>/dev/null || true)" - if printf '%s' "$out" | grep -Eiq 'SandboxDenied|require_escalated|Operation not permitted|permission denied|ask for approval|request approval'; then + if command -v jq >/dev/null 2>&1; then + evt="$(printf '%s\n' "$line" | jq -r '.payload.type // empty' 2>/dev/null || true)" + if [ "$evt" = "function_call_output" ] && event_enabled wait; then + out="$(printf '%s\n' "$line" | jq -r '.payload.output // \"\"' 2>/dev/null || true)" + if printf '%s' "$out" | grep -Eiq 'SandboxDenied|require_escalated|Operation not permitted|permission denied|ask for approval|request approval'; then + notify_with_cooldown wait "🚨 ${prefix} 📁 ${project} ⏳ Codeman is waiting for your input/approval (mode: ${mode})." + fi + fi + else + # Without jq, just look for the denial markers anywhere in the line. + # This keeps "wait" notifications working even in minimal PATH envs. + if event_enabled wait && printf '%s' "$line" | grep -Eiq 'SandboxDenied|require_escalated|Operation not permitted|permission denied|ask for approval|request approval'; then notify_with_cooldown wait "🚨 ${prefix} 📁 ${project} ⏳ Codeman is waiting for your input/approval (mode: ${mode})." fi fi diff --git a/tests/test_codeman.bats b/tests/test_codeman.bats index a6ae7ad..c23b597 100644 --- a/tests/test_codeman.bats +++ b/tests/test_codeman.bats @@ -42,3 +42,32 @@ EOF [ "$status" -eq 0 ] [[ "$output" == *"STUB CODEX OK"* ]] } + +@test "codeman sends per-message notifications when CODEMAN_NOTIFY_ON includes message (no jq required)" { + tmp="$(mktemp -d)" + mkdir -p "$tmp/bin" + mkdir -p "$tmp/.codex/sessions/2026/02/13" + + # Stub curl to capture payloads. + cat >"$tmp/bin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +echo "curl $*" >> "${HOME}/curl_calls.txt" +exit 0 +EOF + chmod +x "$tmp/bin/curl" + + # Stub codex to write an agent_message line into the session log. + cat >"$tmp/bin/codex" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +log="${HOME}/.codex/sessions/2026/02/13/stub.jsonl" +printf '%s\n' '{"type":"event_msg","payload":{"type":"agent_message","message":"hello from stub"}}' >> "$log" +sleep 2 +EOF + chmod +x "$tmp/bin/codex" + + HOME="$tmp" PATH="$tmp/bin:$PATH" CODEMAN_NOTIFY_POLL_SEC=1 CODEMAN_NOTIFY_ON=message CODEMAN_DISCORD_WEBHOOK_URL='http://example.invalid' run ./bin/codeman -y l3 "hello" + [ "$status" -eq 0 ] + [ -f "$tmp/curl_calls.txt" ] +} From 36ec92bdb816a3253e5064df1973bbfbe08ad425 Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 08:10:24 -0800 Subject: [PATCH 7/8] fix: follow CODEX_HOME for session monitoring --- bin/codeman | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/codeman b/bin/codeman index 8565c25..541afdf 100755 --- a/bin/codeman +++ b/bin/codeman @@ -23,6 +23,7 @@ REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" VERSION_FILE="$REPO_ROOT/VERSION" CONFIG_DIR="${CODEMAN_CONFIG_DIR:-$HOME/.config/codeman}" CONFIG_FILE="$CONFIG_DIR/config.env" +CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}" CODEMAN_NOTIFY_LAST_WAIT=0 CODEMAN_NOTIFY_LAST_COMPLETE=0 @@ -644,7 +645,7 @@ notify_with_cooldown() { latest_session_file() { # shellcheck disable=SC2012 - ls -1t "$HOME"/.codex/sessions/*/*/*/*.jsonl 2>/dev/null | head -n 1 || true + ls -1t "$CODEX_HOME_DIR"/sessions/*/*/*/*.jsonl 2>/dev/null | head -n 1 || true } handle_session_line() { From 034e318ecfa4da7d9f5f18885fad0902dac00ec3 Mon Sep 17 00:00:00 2001 From: Lukasz Schabowski Date: Fri, 13 Feb 2026 08:11:56 -0800 Subject: [PATCH 8/8] chore: bump version to v0.1.3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d917d3e..b1e80bb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.2 +0.1.3