commit 3d6e642f024e4c486e272dd4e7c828d24c231b17
Author: Coder Agent <coder@agents.omni>
Date: Wed Feb 11 18:52:11 2026
Omni/Ide: add dev-review-release pipeline status dashboard
- add `status` subcommand with task/runs/failure/stuck summaries
- support machine-readable `status --json` output for Ava polling
- add optional `--watch SEC` refresh mode (text output)
- include worktree paths in dashboard payload
- document status command usage in workflow docs and README
Task-Id: t-587.8
diff --git a/Omni/Ide/DEV_REVIEW_RELEASE.md b/Omni/Ide/DEV_REVIEW_RELEASE.md
index 0ba1eb3d..81fb84c2 100644
--- a/Omni/Ide/DEV_REVIEW_RELEASE.md
+++ b/Omni/Ide/DEV_REVIEW_RELEASE.md
@@ -49,7 +49,14 @@ Optional flags:
- `--once`
- `--dry-run`
-## 3) Branch cleanup
+## 3) Pipeline status dashboard
+
+```bash
+Omni/Ide/dev-review-release.sh status
+Omni/Ide/dev-review-release.sh status --json
+```
+
+## 4) Branch cleanup
Preview only:
diff --git a/Omni/Ide/README.md b/Omni/Ide/README.md
index c52041e7..c6685981 100644
--- a/Omni/Ide/README.md
+++ b/Omni/Ide/README.md
@@ -79,6 +79,10 @@ Omni/Ide/dev-review-release.sh loop --role dev --no-auto-stash
# Cap failed retries per patchset (dev loop circuit breaker)
Omni/Ide/dev-review-release.sh loop --role dev --max-retries 5
+
+# Pipeline dashboard
+Omni/Ide/dev-review-release.sh status
+Omni/Ide/dev-review-release.sh status --json
```
The convention for all programs in the omnirepo is to run their tests if the first argument is `test`. So for example:
diff --git a/Omni/Ide/dev-review-release.sh b/Omni/Ide/dev-review-release.sh
index 20f623e2..874dcf4d 100755
--- a/Omni/Ide/dev-review-release.sh
+++ b/Omni/Ide/dev-review-release.sh
@@ -18,6 +18,7 @@ Usage:
Omni/Ide/dev-review-release.sh setup-worktrees [--root PATH] [--base BRANCH] [--name NAME]
Omni/Ide/dev-review-release.sh loop --role dev|review|integrator [options]
Omni/Ide/dev-review-release.sh cleanup-branches [--apply]
+ Omni/Ide/dev-review-release.sh status [--root PATH] [--json] [--watch SEC]
Commands:
setup-worktrees Create dedicated dev/test/live worktrees for this workflow.
@@ -29,6 +30,8 @@ Commands:
cleanup-branches Delete task branches (t-*) whose tasks are Done.
Dry-run by default.
+ status Show pipeline health summary (tasks, runs, failures, stuck tasks).
+
Setup options:
--root PATH Worktree root (default: _/worktrees/dev-review-release)
--base BRANCH Base branch for worktrees (default: live)
@@ -50,6 +53,11 @@ Loop options:
--once Process at most one task, then exit
--dry-run Print what would run, do not invoke agentd
+Status options:
+ --root PATH Worktree root (default: _/worktrees/dev-review-release)
+ --json Emit machine-readable JSON summary
+ --watch SEC Refresh every SEC seconds (text mode only)
+
Cleanup options:
--apply Actually delete branches. Without this flag, cleanup is dry-run.
--root PATH Worktree root (default: _/worktrees/dev-review-release)
@@ -67,6 +75,9 @@ Examples:
Omni/Ide/dev-review-release.sh cleanup-branches
Omni/Ide/dev-review-release.sh cleanup-branches --apply
+
+ Omni/Ide/dev-review-release.sh status
+ Omni/Ide/dev-review-release.sh status --json
EOF
}
@@ -907,6 +918,150 @@ loop_cmd() {
done
}
+collect_running_runs_json() {
+ local runs_dir="/var/log/agentd"
+ local runs_json="[]"
+
+ if [[ ! -d "$runs_dir" ]]; then
+ echo "$runs_json"
+ return 0
+ fi
+
+ while IFS= read -r run_id; do
+ [[ -z "$run_id" ]] && continue
+ local status_json status
+ status_json="$(agentd status "$run_id" --json 2>/dev/null || true)"
+ status="$(jq -r '.status // empty' <<<"$status_json" 2>/dev/null || true)"
+ if [[ "$status" == "running" ]]; then
+ runs_json="$(jq -c --argjson item "$status_json" '. + [$item]' <<<"$runs_json")"
+ fi
+ done < <(find "$runs_dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
+
+ echo "$runs_json"
+}
+
+status_once_cmd() {
+ local root="$1"
+ local json_mode="$2"
+
+ local task_json
+ task_json="$(task list --json)"
+
+ local tasks_by_status
+ tasks_by_status="$(jq -c 'reduce .[] as $t ({}; .[$t.taskStatus] = ((.[$t.taskStatus] // 0) + 1))' <<<"$task_json")"
+
+ local recent_failures
+ recent_failures="$(jq -c '[ .[] as $t | ($t.taskComments // [])[]? | select(.commentText | test("Automation \\(.+\\) (patchset [0-9]+ )?attempt [0-9]+/[0-9]+ failed")) | {taskId:$t.taskId, createdAt:.commentCreatedAt, comment:.commentText}] | sort_by(.createdAt) | reverse | .[:10]' <<<"$task_json")"
+
+ local stuck_tasks
+ stuck_tasks="$(jq -c '[ .[] | select(any((.taskComments // [])[]?; .commentText | test("exceeded max retries"))) | .taskId ]' <<<"$task_json")"
+
+ local running_runs
+ running_runs="$(collect_running_runs_json)"
+
+ local workspace_health
+ workspace_health="$(jq -nc \
+ --arg dev "$(workspace_for_role "$root" dev)" \
+ --arg review "$(workspace_for_role "$root" review)" \
+ --arg integrator "$(workspace_for_role "$root" integrator)" \
+ '{dev:$dev,review:$review,integrator:$integrator}')"
+
+ if [[ "$json_mode" == "true" ]]; then
+ jq -n \
+ --arg root "$root" \
+ --argjson tasksByStatus "$tasks_by_status" \
+ --argjson runningRuns "$running_runs" \
+ --argjson recentFailures "$recent_failures" \
+ --argjson stuckTasks "$stuck_tasks" \
+ --argjson workspaces "$workspace_health" \
+ '{root:$root,tasksByStatus:$tasksByStatus,runningRuns:$runningRuns,recentFailures:$recentFailures,stuckTasks:$stuckTasks,workspaces:$workspaces}'
+ return 0
+ fi
+
+ log "Pipeline status root=$root"
+ echo ""
+ echo "Tasks by status:"
+ jq -r 'to_entries | sort_by(.key)[] | " \(.key): \(.value)"' <<<"$tasks_by_status"
+
+ local running_count
+ running_count="$(jq -r 'length' <<<"$running_runs")"
+ echo ""
+ echo "Running agentd runs: $running_count"
+ if [[ "$running_count" -gt 0 ]]; then
+ jq -r '.[] | " - \(.run_id) (cost=\(.cost_cents // 0)¢ status=\(.status))"' <<<"$running_runs"
+ fi
+
+ echo ""
+ echo "Tasks stuck at max retries:"
+ local stuck_count
+ stuck_count="$(jq -r 'length' <<<"$stuck_tasks")"
+ if [[ "$stuck_count" -eq 0 ]]; then
+ echo " (none)"
+ else
+ jq -r '.[] | " - \(.)"' <<<"$stuck_tasks"
+ fi
+
+ echo ""
+ echo "Recent retry failures:"
+ local failure_count
+ failure_count="$(jq -r 'length' <<<"$recent_failures")"
+ if [[ "$failure_count" -eq 0 ]]; then
+ echo " (none)"
+ else
+ jq -r '.[] | " - [\(.createdAt)] \(.taskId): \(.comment)"' <<<"$recent_failures"
+ fi
+}
+
+status_cmd() {
+ local root_rel="$DEFAULT_WORKTREE_ROOT"
+ local json_mode="false"
+ local watch_seconds=""
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --root)
+ root_rel="$2"
+ shift 2
+ ;;
+ --json)
+ json_mode="true"
+ shift
+ ;;
+ --watch)
+ watch_seconds="$2"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ usage
+ exit 1
+ ;;
+ esac
+ done
+
+ if [[ -n "$watch_seconds" && "$json_mode" == "true" ]]; then
+ echo "--watch is only supported in text mode" >&2
+ exit 1
+ fi
+
+ local root
+ root="$(resolve_path "$root_rel")"
+
+ if [[ -n "$watch_seconds" ]]; then
+ while true; do
+ clear
+ status_once_cmd "$root" "$json_mode"
+ sleep "$watch_seconds"
+ done
+ else
+ status_once_cmd "$root" "$json_mode"
+ fi
+}
+
branch_checked_out_in_any_worktree() {
local branch="$1"
git -C "$REPO_ROOT" worktree list --porcelain | grep -q "^branch refs/heads/$branch$"
@@ -1009,6 +1164,9 @@ main() {
cleanup-branches)
cleanup_branches_cmd "$@"
;;
+ status)
+ status_cmd "$@"
+ ;;
-h|--help|help)
usage
;;