← Back to task

Commit e38a181b

commit e38a181bf08ebb9a05f682e625e6fbb5f652d10f
Author: Coder Agent <coder@agents.omni>
Date:   Wed Feb 11 18:56:50 2026

    Omni/Ide: add per-task run cost accounting and cap
    
    - record per-run cost and cumulative task cost in task comments
    - log run/cumulative cost after each agentd execution
    - add --max-task-cost cents guard to stop automation for expensive tasks
    - mark over-budget tasks as needs-help with explanatory comment
    - document max-task-cost usage in README and workflow guide
    
    Task-Id: t-587.7

diff --git a/Omni/Ide/DEV_REVIEW_RELEASE.md b/Omni/Ide/DEV_REVIEW_RELEASE.md
index 81fb84c2..65652547 100644
--- a/Omni/Ide/DEV_REVIEW_RELEASE.md
+++ b/Omni/Ide/DEV_REVIEW_RELEASE.md
@@ -45,6 +45,7 @@ Optional flags:
 - `--parent t-587` (scope to one epic)
 - `--task-id t-587.1` (scope to one task)
 - `--max-retries 5` (circuit-break after failed attempts per patchset)
+- `--max-task-cost 2000` (stop automation after cumulative run cost reaches limit)
 - `--no-auto-stash` (disable automatic dirty-workspace recovery)
 - `--once`
 - `--dry-run`
@@ -84,6 +85,10 @@ Retry accounting and circuit-breaker behavior are patchset-aware:
 - Exceeding `--max-retries` opens the circuit for that patchset and skips new runs.
 - Dev retries use exponential backoff between failed attempts (capped).
 
+Cost accounting is recorded per run in task comments:
+- Each run appends `cost_cents=...` and cumulative cost metadata.
+- `--max-task-cost` prevents additional automated runs once the task exceeds the limit.
+
 Expected lifecycle:
 
 `open -> in-progress -> review -> approved -> done`
diff --git a/Omni/Ide/README.md b/Omni/Ide/README.md
index c6685981..a9c44699 100644
--- a/Omni/Ide/README.md
+++ b/Omni/Ide/README.md
@@ -80,6 +80,9 @@ 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
 
+# Cap cumulative automation spend per task (cents)
+Omni/Ide/dev-review-release.sh loop --role dev --max-task-cost 2000
+
 # Pipeline dashboard
 Omni/Ide/dev-review-release.sh status
 Omni/Ide/dev-review-release.sh status --json
diff --git a/Omni/Ide/dev-review-release.sh b/Omni/Ide/dev-review-release.sh
index 874dcf4d..a6d0a34e 100755
--- a/Omni/Ide/dev-review-release.sh
+++ b/Omni/Ide/dev-review-release.sh
@@ -11,6 +11,7 @@ DEFAULT_TIMEOUT_SECONDS=1800
 DEFAULT_MAX_ITER=80
 DEFAULT_MAX_COST_CENTS=300
 DEFAULT_MAX_RETRIES=5
+DEFAULT_MAX_TASK_COST_CENTS=0
 
 usage() {
   cat <<'EOF'
@@ -49,6 +50,7 @@ Loop options:
   --max-iter N      agentd max iterations per run (default: 80)
   --max-cost CENTS  agentd max cost cents per run (default: 300)
   --max-retries N   Max failed attempts per task patchset before skipping (default: 5, 0 = unlimited)
+  --max-task-cost C Stop automation for task when cumulative run cost reaches C cents (default: 0 = unlimited)
   --no-auto-stash   Disable auto-stashing dirty workspace state before retry
   --once            Process at most one task, then exit
   --dry-run         Print what would run, do not invoke agentd
@@ -287,6 +289,25 @@ task_patchset_count() {
   task show "$tid" --json | jq -r '.taskPatchsetCount // 0'
 }
 
+task_cumulative_cost_cents() {
+  local tid="$1"
+  task show "$tid" --json \
+    | jq -r '[.taskComments[]?.commentText | (match("cost_cents=([0-9]+(\\.[0-9]+)?)")? | .captures[0].string | tonumber)] | add // 0'
+}
+
+run_cost_cents() {
+  local run_name="$1"
+  agentd status "$run_name" --json 2>/dev/null | jq -r '.cost_cents // 0' 2>/dev/null || echo "0"
+}
+
+cost_limit_comment_exists() {
+  local tid="$1"
+  local max_task_cost="$2"
+  task show "$tid" --json \
+    | jq -r '.taskComments[]?.commentText // empty' \
+    | grep -Fq "exceeded max task cost (${max_task_cost}c)"
+}
+
 get_retry_count() {
   local tid="$1"
   local role="$2"
@@ -524,6 +545,7 @@ run_single_task() {
   local auto_stash_dirty="${11}"
   local max_retries="${12}"
   local interval_seconds="${13}"
+  local max_task_cost="${14}"
 
   local tid
   tid="$(select_next_task "$role" "$task_filter" "$parent_filter")"
@@ -538,6 +560,20 @@ run_single_task() {
   local patchset_count
   patchset_count="$(task_patchset_count "$tid")"
 
+  local prior_cost
+  prior_cost="$(task_cumulative_cost_cents "$tid")"
+  if [[ "$max_task_cost" -gt 0 ]] && jq -n -e --argjson c "$prior_cost" --argjson m "$max_task_cost" '$c >= $m' >/dev/null; then
+    log "Task $tid cumulative cost ${prior_cost}c exceeded max task cost ${max_task_cost}c, skipping"
+    if ! cost_limit_comment_exists "$tid" "$max_task_cost"; then
+      task comment "$tid" "Automation ($role) exceeded max task cost (${max_task_cost}c); needs human attention." --json >/dev/null || true
+    fi
+    task update "$tid" needs-help --json >/dev/null || true
+    if [[ -n "$task_filter" && "$task_filter" == "$tid" ]]; then
+      return 3
+    fi
+    return 2
+  fi
+
   # Circuit breaker: skip tasks whose current patchset already exhausted retries.
   if task_exceeded_retries "$tid" "$role" "$patchset_count" "$max_retries"; then
     log "Task $tid patchset $patchset_count exceeded max retries ($max_retries), skipping"
@@ -629,6 +665,12 @@ run_single_task() {
     rc=$?
   fi
 
+  local run_cost cumulative_cost
+  run_cost="$(run_cost_cents "$run_name")"
+  cumulative_cost="$(jq -n --argjson a "$prior_cost" --argjson b "$run_cost" '$a + $b')"
+  log "Run cost for $tid (run=$run_name): ${run_cost}c (cumulative: ${cumulative_cost}c)"
+  task comment "$tid" "Automation ($role) run $run_name cost_cents=$run_cost cumulative_cost_cents=$cumulative_cost." --json >/dev/null || true
+
   if [[ $rc -ne 0 ]]; then
     log "Run failed for $tid (run=$run_name)"
     record_retry_attempt "$tid" "$role" "$patchset_count" "$run_name" "$max_retries" "true"
@@ -795,6 +837,7 @@ loop_cmd() {
   local max_iter="$DEFAULT_MAX_ITER"
   local max_cost="$DEFAULT_MAX_COST_CENTS"
   local max_retries="$DEFAULT_MAX_RETRIES"
+  local max_task_cost="$DEFAULT_MAX_TASK_COST_CENTS"
   local auto_stash_dirty="true"
   local once="false"
   local dry_run="false"
@@ -845,6 +888,10 @@ loop_cmd() {
         max_retries="$2"
         shift 2
         ;;
+      --max-task-cost)
+        max_task_cost="$2"
+        shift 2
+        ;;
       --no-auto-stash)
         auto_stash_dirty="false"
         shift
@@ -886,11 +933,11 @@ loop_cmd() {
   fi
 
   log "Starting $role loop"
-  log "workspace=$workspace base=$base_branch interval=${interval}s dry_run=$dry_run auto_stash_dirty=$auto_stash_dirty max_retries=$max_retries task_filter=${task_filter:-<none>} parent_filter=${parent_filter:-<none>}"
+  log "workspace=$workspace base=$base_branch interval=${interval}s dry_run=$dry_run auto_stash_dirty=$auto_stash_dirty max_retries=$max_retries max_task_cost=$max_task_cost task_filter=${task_filter:-<none>} parent_filter=${parent_filter:-<none>}"
 
   while true; do
     set +e
-    run_single_task "$role" "$workspace" "$base_branch" "$provider" "$timeout" "$max_iter" "$max_cost" "$dry_run" "$task_filter" "$parent_filter" "$auto_stash_dirty" "$max_retries" "$interval"
+    run_single_task "$role" "$workspace" "$base_branch" "$provider" "$timeout" "$max_iter" "$max_cost" "$dry_run" "$task_filter" "$parent_filter" "$auto_stash_dirty" "$max_retries" "$interval" "$max_task_cost"
     rc=$?
     set -e