Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ contracts and workflows:
`validation-run`, and `validation-lease-acquire`;
- **Verifier Workflow**, its compact contract ledger, and
`MULTIAGENT_VERIFIER_MAX_ITERATIONS=3`;
- Codex UI dashboard watching through `bin/watch.sh`, backed by tmux pane logs
under `.multiagent/logs`, blocked-agent state, and workflow DAG nodes;
- preflight checks that prevent a scaffold, shim, or proxy behavior from being
mistaken for the target production system.

Expand Down
23 changes: 18 additions & 5 deletions bin/subagent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set -euo pipefail
SESSION="${MULTIAGENT_SESSION:-multiagent}"
ROOT="${MULTIAGENT_ROOT:-$(pwd)}"
STATE_DIR="${MULTIAGENT_STATE_DIR:-$ROOT/.multiagent}"
LOG_DIR="${MULTIAGENT_LOG_DIR:-$STATE_DIR/logs}"
POLICY_FILE="${MULTIAGENT_WRITE_POLICY:-$ROOT/docs/write-policy.paths}"
CODEX_BIN="${CODEX_BIN:-codex}"
CLAUDE_BIN="${CLAUDE_BIN:-claude}"
Expand Down Expand Up @@ -90,6 +91,14 @@ require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

pipe_log() {
local window="$1"
local log_file="$LOG_DIR/$window.log"
mkdir -p "$LOG_DIR"
touch "$log_file"
tmux pipe-pane -o -t "$SESSION:$window" "cat >> $(printf '%q' "$log_file")"
}

normalize_cli() {
case "$1" in
codex|claude)
Expand Down Expand Up @@ -1296,12 +1305,13 @@ spawn_subagent() {

local dir
dir="$(subagent_dir "$name")"
mkdir -p "$dir"
mkdir -p "$dir" "$LOG_DIR"
cat >"$dir/meta.env" <<EOF
name=$name
session=$SESSION
root=$ROOT
write_policy=$POLICY_FILE
log_file=$LOG_DIR/$name.log
cli=$cli
cli_bin=$bin
helper=$MULTIAGENT_HELPER
Expand All @@ -1323,9 +1333,10 @@ EOF
cat "$prompt_file"
} >>"$dir/transcript.log"
fi
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$POLICY_FILE" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_LOG_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$LOG_DIR" "$POLICY_FILE" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
tmux new-window -d -t "$SESSION" -n "$name" "$command"
pipe_log "$name"
set_status "$name" "running"
if [[ -f "$(assignment_meta_file "$name")" ]]; then
set_assignment_status "$name" "running"
Expand Down Expand Up @@ -1580,16 +1591,18 @@ restore_subagent() {
} >>"$dir/transcript.log"
set_status "$name" "restoring"

mkdir -p "$LOG_DIR"
local prompt_file output_file
prompt_file=""
output_file="$dir/last-message.txt"
if [[ "${MULTIAGENT_CODEX_EXEC:-0}" == "1" && "$cli" == "codex" ]]; then
prompt_file="$dir/restore-instruction.txt"
printf '%s\n' "$instruction" >"$prompt_file"
fi
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q MULTIAGENT_SUBAGENT_RESTORED=1 WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$POLICY_FILE" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
printf -v command "cd %q && export MULTIAGENT_SESSION=%q MULTIAGENT_ROOT=%q MULTIAGENT_STATE_DIR=%q MULTIAGENT_LOG_DIR=%q MULTIAGENT_WRITE_POLICY=%q MULTIAGENT_SUBAGENT_NAME=%q MULTIAGENT_HELPER=%q MULTIAGENT_SUBAGENT_RESTORED=1 WORKER_CLI=%q SUBAGENT_CLI=%q VERIFIER_CLI=%q CODEX_BIN=%q CLAUDE_BIN=%q MULTIAGENT_CODEX_EXEC=%q PATH=%q && %s; rc=\$?; printf '\\nfinal status: codex exec exited rc=%%s\\n' \$rc; sleep infinity" \
"$ROOT" "$SESSION" "$ROOT" "$STATE_DIR" "$LOG_DIR" "$POLICY_FILE" "$name" "$MULTIAGENT_HELPER" "$WORKER_CLI" "$cli" "$VERIFIER_CLI" "$CODEX_BIN" "$CLAUDE_BIN" "${MULTIAGENT_CODEX_EXEC:-0}" "$PATH" "$(build_cli_command "$cli" "$ROOT" "$prompt_file" "$output_file")"
tmux new-window -d -t "$SESSION" -n "$name" "$command"
pipe_log "$name"
set_status "$name" "running"
if ! [[ "${MULTIAGENT_CODEX_EXEC:-0}" == "1" && "$cli" == "codex" ]]; then
deliver_instruction "$name" "$instruction"
Expand Down
222 changes: 222 additions & 0 deletions bin/watch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#!/usr/bin/env bash
set -euo pipefail

SESSION="${MULTIAGENT_SESSION:-multiagent}"
ROOT="${MULTIAGENT_ROOT:-$(pwd)}"
STATE_DIR="${MULTIAGENT_STATE_DIR:-$ROOT/.multiagent}"
LOG_DIR="${MULTIAGENT_LOG_DIR:-$STATE_DIR/logs}"
INTERVAL="${MULTIAGENT_WATCH_INTERVAL:-5}"
LOG_LINES="${MULTIAGENT_WATCH_LOG_LINES:-40}"
ONCE=0

usage() {
cat <<'USAGE'
Usage: bin/watch.sh [--once] [--interval SECONDS] [--log-lines N]

Renders a compact multiagent dashboard for Codex desktop. It combines
assignment/subagent status, workflow DAG summaries, blocked nodes, and the
orchestrator pane log written by launch.sh.

Environment:
MULTIAGENT_SESSION tmux session name, default: multiagent
MULTIAGENT_ROOT project root, default: current directory
MULTIAGENT_STATE_DIR state root, default: $MULTIAGENT_ROOT/.multiagent
MULTIAGENT_LOG_DIR pane log directory, default: $STATE_DIR/logs
MULTIAGENT_WATCH_INTERVAL refresh interval, default: 5
MULTIAGENT_WATCH_LOG_LINES orchestrator tail lines, default: 40
USAGE
}

while [[ $# -gt 0 ]]; do
case "$1" in
--once)
ONCE=1
shift
;;
--interval)
INTERVAL="${2:-}"
shift 2
;;
--log-lines)
LOG_LINES="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "watch: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done

if ! [[ "$INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
echo "watch: --interval must be a positive integer" >&2
exit 2
fi
if ! [[ "$LOG_LINES" =~ ^[0-9]+$ ]]; then
echo "watch: --log-lines must be a non-negative integer" >&2
exit 2
fi

status_text() {
if "$ROOT/bin/status.sh" 2>/dev/null; then
return 0
fi
printf 'TYPE\tNAME\tSTATUS\tWINDOW\tLAST_PROGRESS\tSTATE_DIR\tROLE\tDECISION_ID\tPLAN_ID\tWORKFLOW_ID\tNODE_ID\n'
}

render_status_summary() {
awk -F'\t' '
NR == 1 { next }
$3 != "" { count[$3]++; seen=1 }
END {
if (!seen) {
print "none\t0"
next
}
for (status in count) {
print status "\t" count[status]
}
}
' | sort
}

render_workers() {
awk -F'\t' '
NR == 1 { next }
{
seen=1
progress=$5
if (length(progress) > 90) {
progress=substr(progress, 1, 87) "..."
}
printf "%-9s %-28s %-10s %-7s %s\n", $1, $2, $3, $4, progress
}
END {
if (!seen) {
print "none"
}
}
'
}

render_blocked_agents() {
awk -F'\t' '
NR == 1 { next }
tolower($3) ~ /blocked|delivery-blocked/ {
seen=1
progress=$5
if (length(progress) > 110) {
progress=substr(progress, 1, 107) "..."
}
printf "%-28s %-16s %s\n", $2, $3, progress
}
END {
if (!seen) {
print "none"
}
}
'
}

render_dag_summary() {
local base="$STATE_DIR/workflows"
if [[ ! -d "$base" ]]; then
printf 'No workflows found.\n'
return
fi

local any=0 dir workflow nodes
for dir in "$base"/*; do
[[ -d "$dir" ]] || continue
workflow="$(basename "$dir")"
nodes="$dir/nodes.tsv"
[[ -f "$nodes" ]] || continue
any=1
printf '%s\n' "$workflow"
awk -F'\t' '
NR == 1 { next }
$7 != "" { count[$7]++ }
END {
for (status in count) {
printf " %-8s %s\n", status, count[status]
}
}
' "$nodes" | sort
done
[[ "$any" -eq 1 ]] || printf 'No workflows found.\n'
}

render_blocked_dag_nodes() {
local base="$STATE_DIR/workflows"
if [[ ! -d "$base" ]]; then
printf 'none\n'
return
fi

local any=0 dir workflow nodes
for dir in "$base"/*; do
[[ -d "$dir" ]] || continue
workflow="$(basename "$dir")"
nodes="$dir/nodes.tsv"
[[ -f "$nodes" ]] || continue
while IFS=$'\t' read -r node_id agent assignment_id role branch owned_paths status decision_id plan_id added_at; do
[[ "$node_id" != "node_id" ]] || continue
if [[ "$status" == "blocked" || "$status" == "failed" ]]; then
any=1
printf '%s\t%s\t%s\t%s\n' "$workflow" "$node_id" "$status" "$agent"
fi
done <"$nodes"
done
[[ "$any" -eq 1 ]] || printf 'none\n'
}

render_once() {
local now status_snapshot orchestrator_log
now="$(date '+%Y-%m-%d %H:%M:%S')"
status_snapshot="$(status_text)"
orchestrator_log="$LOG_DIR/orchestrator.log"

printf 'Multiagent Dashboard\n'
printf 'Session: %s Root: %s\n' "$SESSION" "$ROOT"
printf 'State: %s\n' "$STATE_DIR"
printf 'Logs: %s\n' "$LOG_DIR"
printf 'Updated: %s\n\n' "$now"

printf 'Agent Status Summary\n'
printf '%s\n' "$status_snapshot" | render_status_summary

printf '\nAgents\n'
printf '%s\n' "$status_snapshot" | render_workers

printf '\nBlocked Agents\n'
printf '%s\n' "$status_snapshot" | render_blocked_agents

printf '\nDAG Summary\n'
render_dag_summary

printf '\nBlocked DAG Nodes\n'
render_blocked_dag_nodes

printf '\nOrchestrator Tail\n'
if [[ -f "$orchestrator_log" && "$LOG_LINES" -gt 0 ]]; then
tail -n "$LOG_LINES" "$orchestrator_log"
elif [[ "$LOG_LINES" -eq 0 ]]; then
printf '(disabled)\n'
else
printf 'No orchestrator log yet. Start with ./launch.sh or pipe the pane manually with tmux pipe-pane.\n'
fi
}

while true; do
if [[ "$ONCE" -eq 0 ]]; then
printf '\033[H\033[2J'
fi
render_once
[[ "$ONCE" -eq 0 ]] || break
sleep "$INTERVAL"
done
22 changes: 22 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,28 @@ worker windows, polls open named subagents, refreshes subagent state, and prints
a table with agent type, name, status, window state, latest progress line, and
state directory.

For a live Codex desktop view, use the dashboard watcher:

```bash
bin/watch.sh
```

`launch.sh` pipes the orchestrator tmux pane into
`$MULTIAGENT_STATE_DIR/logs/orchestrator.log`. Named subagents spawned or
restored through `bin/subagent.sh` are piped into
`$MULTIAGENT_STATE_DIR/logs/NAME.log`. The watcher renders a compact dashboard
from those logs, `bin/status.sh`, assignment metadata, and workflow DAG state so
the Codex UI can continuously show the orchestrator tail, status counts, blocked
agents, DAG summaries, and blocked DAG nodes.

Useful watcher options:

```bash
bin/watch.sh --once
bin/watch.sh --interval 2 --log-lines 80
MULTIAGENT_LOG_DIR=/tmp/swarm-logs bin/watch.sh
```

## Organizational Learning Workflow

The orchestrator supports exploration/exploitation/reflection cycles for complex decisions requiring multiple approaches.
Expand Down
Loading
Loading