← Back to task

Commit 3d6e642f

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
       ;;