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