#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" TARGET_CWD="${TARGET_CWD:-$PROJECT_DIR}" CONFIG_FILE="${SUBAGENTS_CONFIG:-$PROJECT_DIR/subagents.conf}" AGENTS=(ui pipeline rules tests) declare -A AGENT_SCOPES=( [ui]="templates/ static/ frontend/ src/ui/ src/components/ src/app/" [pipeline]="pipeline/ runtime/ schemas/ guards/ src/pipeline/ src/runtime/" [rules]="rules/ rule-packs/ policies/ validation/ src/rules/" [tests]="tests/ test/ spec/ fixtures/ harness/ e2e/" ) declare -A AGENT_RESPONSIBILITIES=( [ui]="Implement and review user-facing UI, templates, static assets, and component behavior." [pipeline]="Implement and review pipeline flow, runtime behavior, schemas, and guard logic." [rules]="Implement and review rule packs, policy definitions, validation behavior, and rule docs." [tests]="Add and maintain focused test coverage, fixtures, harnesses, and test documentation." ) ACPX_CMD=() usage() { cat <<'USAGE' Usage: acpx-subagents [--cwd DIR] [--config FILE] doctor acpx-subagents [--cwd DIR] [--config FILE] ensure [agent...] acpx-subagents [--cwd DIR] [--config FILE] send [--no-wait] [--file FILE] [prompt...] acpx-subagents [--cwd DIR] [--config FILE] parallel [--no-wait|--wait] [agent...] -- [prompt...] acpx-subagents [--cwd DIR] [--config FILE] sessions acpx-subagents [--cwd DIR] [--config FILE] show acpx-subagents [--cwd DIR] [--config FILE] history [limit] acpx-subagents [--cwd DIR] [--config FILE] close [agent...] Environment: ACPX_BIN Override acpx command, e.g. "acpx" or "npx -y acpx@0.1.15" SUBAGENTS_CONFIG Override config file path TARGET_CWD Default target repository/workspace USAGE } fail() { printf 'error: %s\n' "$*" >&2 exit 1 } absolute_dir() { local dir="$1" [[ -d "$dir" ]] || fail "directory does not exist: $dir" (cd -- "$dir" && pwd) } load_config() { [[ -f "$CONFIG_FILE" ]] || return 0 # shellcheck source=/dev/null source "$CONFIG_FILE" } agent_exists() { local candidate="$1" local agent for agent in "${AGENTS[@]}"; do [[ "$agent" == "$candidate" ]] && return 0 done return 1 } agents_or_all() { if (($# == 0)); then printf '%s\n' "${AGENTS[@]}" return 0 fi local agent for agent in "$@"; do agent_exists "$agent" || fail "unknown agent '$agent' (known: ${AGENTS[*]})" printf '%s\n' "$agent" done } resolve_acpx() { if ((${#ACPX_CMD[@]} > 0)); then return 0 fi if [[ -n "${ACPX_BIN:-}" ]]; then read -r -a ACPX_CMD <<< "$ACPX_BIN" elif command -v acpx >/dev/null 2>&1; then ACPX_CMD=(acpx) else ACPX_CMD=(npx -y acpx@latest) fi } run_acpx() { resolve_acpx "${ACPX_CMD[@]}" --cwd "$TARGET_CWD" codex "$@" } compose_prompt() { local agent="$1" local task="$2" local scope="${AGENT_SCOPES[$agent]:-not configured}" local responsibility="${AGENT_RESPONSIBILITIES[$agent]:-Work only on the assigned task.}" cat </dev/null 2>&1; then printf 'git root: %s\n' "$(git -C "$TARGET_CWD" rev-parse --show-toplevel)" else printf 'git root: none; acpx will scope sessions to the exact target cwd\n' fi local cmd for cmd in node npm npx codex; do if command -v "$cmd" >/dev/null 2>&1; then printf 'ok: %-5s %s\n' "$cmd" "$(command -v "$cmd")" else printf 'missing: %s\n' "$cmd" fi done if [[ -n "${ACPX_BIN:-}" ]]; then printf 'acpx: ACPX_BIN=%s\n' "$ACPX_BIN" elif command -v acpx >/dev/null 2>&1; then printf 'acpx: %s\n' "$(command -v acpx)" else printf 'acpx: not installed; will use npx -y acpx@latest\n' fi printf 'agents: %s\n' "${AGENTS[*]}" } cmd_ensure() { local agent while IFS= read -r agent; do printf 'ensuring session: %s\n' "$agent" >&2 run_acpx sessions ensure --name "$agent" done < <(agents_or_all "$@") } cmd_send() { local no_wait=0 local prompt_file="" while (($# > 0)); do case "$1" in --no-wait) no_wait=1 shift ;; --file) (($# >= 2)) || fail "--file requires a path" prompt_file="$2" shift 2 ;; --) shift break ;; -*) fail "unknown send option: $1" ;; *) break ;; esac done (($# >= 1)) || fail "send requires an agent" local agent="$1" shift agent_exists "$agent" || fail "unknown agent '$agent' (known: ${AGENTS[*]})" while (($# > 0)); do case "$1" in --no-wait) no_wait=1 shift ;; --file) (($# >= 2)) || fail "--file requires a path" prompt_file="$2" shift 2 ;; --) shift break ;; -*) fail "unknown send option: $1" ;; *) break ;; esac done local task="" if [[ -n "$prompt_file" ]]; then [[ -f "$prompt_file" ]] || fail "prompt file does not exist: $prompt_file" task="$(< "$prompt_file")" if (($# > 0)); then task+=$'\n\n' task+="$*" fi else task="$*" fi [[ -n "${task//[[:space:]]/}" ]] || fail "send requires prompt text or --file" local prompt prompt="$(compose_prompt "$agent" "$task")" local args=(-s "$agent") if ((no_wait == 1)); then args+=(--no-wait) fi run_acpx "${args[@]}" "$prompt" } cmd_parallel() { local no_wait=1 local selected=() while (($# > 0)); do case "$1" in --no-wait) no_wait=1 shift ;; --wait) no_wait=0 shift ;; --) shift break ;; *) if agent_exists "$1"; then selected+=("$1") shift else break fi ;; esac done if ((${#selected[@]} == 0)); then selected=("${AGENTS[@]}") fi local task="$*" [[ -n "${task//[[:space:]]/}" ]] || fail "parallel requires prompt text after --" local agent for agent in "${selected[@]}"; do if ((no_wait == 1)); then cmd_send --no-wait "$agent" "$task" else cmd_send "$agent" "$task" fi done } cmd_sessions() { run_acpx sessions list } cmd_show() { (($# == 1)) || fail "show requires exactly one agent" agent_exists "$1" || fail "unknown agent '$1' (known: ${AGENTS[*]})" run_acpx sessions show "$1" } cmd_history() { (($# >= 1 && $# <= 2)) || fail "history requires an agent and optional limit" local agent="$1" local limit="${2:-20}" agent_exists "$agent" || fail "unknown agent '$agent' (known: ${AGENTS[*]})" run_acpx sessions history "$agent" --limit "$limit" } cmd_close() { local agent while IFS= read -r agent; do printf 'closing session: %s\n' "$agent" >&2 run_acpx sessions close "$agent" done < <(agents_or_all "$@") } TARGET_CWD="$(absolute_dir "$TARGET_CWD")" COMMAND="" while (($# > 0)); do case "$1" in --cwd) (($# >= 2)) || fail "--cwd requires a directory" TARGET_CWD="$(absolute_dir "$2")" shift 2 ;; --config) (($# >= 2)) || fail "--config requires a file" CONFIG_FILE="$2" shift 2 ;; -h|--help|help) usage exit 0 ;; doctor|ensure|send|parallel|sessions|show|history|close) COMMAND="$1" shift break ;; *) fail "unknown command or option: $1" ;; esac done load_config case "$COMMAND" in doctor) cmd_doctor "$@" ;; ensure) cmd_ensure "$@" ;; send) cmd_send "$@" ;; parallel) cmd_parallel "$@" ;; sessions) cmd_sessions "$@" ;; show) cmd_show "$@" ;; history) cmd_history "$@" ;; close) cmd_close "$@" ;; "") usage ;; *) fail "unknown command: $COMMAND" ;; esac