◉ANTIGRAVITY LABJP
Articles/Agents & Manager
◈ Agents & Manager/2026-10-30Beginner

I moved the stop conditions for a nightly Antigravity CLI run outside the CLI

A wrapper script that gives a nightly agy -p run three outer stop conditions: time, output size and overlap. Testing four ways of ending with stub scripts showed that a timeout can exit with 137 instead of 124.

antigravity-cli16headless7automation95bash6timeout8

When I first thought about putting agy -p on a nightly schedule as an indie developer, one question came before everything else: what happens if it is still running when morning comes?

The docs help here. Headless runs have --print-timeout, which defaults to 5 minutes, and a run without cached credentials exits with an authentication error instead of hanging. What I could not find is any mention of a limit on the number of turns.

I would rather not operate on a guarantee that isn't written down. So I decided to keep part of the stopping logic outside the CLI, in the wrapper that calls it.

Three stop conditions that live outside

This is not distrust of the CLI. Behavior changes between versions, and an outer frame stays put while that happens. I added three things:

  1. Time: a hard timeout, independent of the CLI's own wait
  2. Output size: cut standard output at a byte limit so a runaway run can't fill the disk
  3. Overlap: if the previous run is still alive, don't start another

Plus one log line per run, so the morning read is a single glance at one file.

The whole wrapper

#!/usr/bin/env bash
# 夜間に agy を headless で回すときの外側の枠。
# 使い方: agy_nightly.sh "プロンプト"
set -u
 
PROMPT="${1:?prompt required}"
LOG_DIR="${LOG_DIR:-$HOME/agy-nightly-logs}"
HARD_LIMIT="${HARD_LIMIT:-20m}"       # 時間: CLI の待ち時間とは別に、外側で必ず打ち切る
MAX_BYTES="${MAX_BYTES:-200000}"      # 出力: これを超えたら以降は捨てる
LOCK="$LOG_DIR/.lock"
 
mkdir -p "$LOG_DIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="$LOG_DIR/$STAMP.out"
META="$LOG_DIR/$STAMP.meta"
 
# 重なり: 前回が残っていたら今回は始めない
exec 9>"$LOCK"
if ! flock -n 9; then
  echo "$STAMP skipped: previous run still holds the lock" >> "$LOG_DIR/summary.log"
  exit 75
fi
 
START=$(date +%s)
# timeout は TERM を送り、10 秒待っても残れば KILL する
timeout --kill-after=10s "$HARD_LIMIT" \
  ${AGY_BIN:-agy} -p "$PROMPT" --output-format json 2>"$OUT.err" \
  | head -c "$MAX_BYTES" > "$OUT"
CODE=${PIPESTATUS[0]}
END=$(date +%s)
 
BYTES=$(wc -c < "$OUT")
LABEL="ok"
case "$CODE" in
  0)       LABEL="ok" ;;
  124|137) LABEL="timeout" ;;      # 124 = TERM で終了 / 137 = TERM を無視されて KILL
  141)     LABEL="truncated" ;;    # head が先に閉じて SIGPIPE
  *)       LABEL="error" ;;
esac
 
printf '%s exit=%s secs=%s bytes=%s label=%s\n' \
  "$STAMP" "$CODE" "$((END-START))" "$BYTES" "$LABEL" | tee "$META" >> "$LOG_DIR/summary.log"
exit "$CODE"

--output-format json gives one JSON envelope on completion, with fields such as status and error, which makes the log easier to read by machine. AGY_BIN lets me substitute a fake for the real agy while testing; normally it is unset.

I read the exit code from PIPESTATUS[0] so I get timeout's status rather than head's. The failure mode of reading the wrong one is covered in The pipe in my wrapper was swallowing Antigravity CLI's exit code.

Four endings, tried with fake agy scripts

Reproducing a long runaway with the real agy would cost time and quota, so I wrote four tiny scripts that behave differently and plugged them in through AGY_BIN.

Fake behaviorResult
Prints JSON and exits`exit=0` `label=ok`
Sleeps for 300 s (`HARD_LIMIT=2s`)`exit=124` `secs=2` `label=timeout`
Ignores TERM and sleeps (`HARD_LIMIT=2s`)`exit=137` `secs=12` `label=timeout`
Emits 5 MB (`MAX_BYTES=1000`)`exit=141` `bytes=1000` `label=truncated`

The third row stopped me. I had assumed a timeout always returns 124 and had written only [ "$CODE" -eq 124 ]. When the process ignores TERM, timeout sends KILL, and the status becomes 137. My first version logged that run as a plain error. I now count both 124 and 137 as a timeout. I don't think I would have written that line without trying it.

Notice also that the run took 12 seconds, not 2. The 10-second grace in --kill-after=10s is added on top, so budget the nightly window as HARD_LIMIT plus that grace.

The 141 in the last row means head finished reading and closed the pipe, so the producer died from SIGPIPE. That is closer to "output was capped" than to a failure, which is why it has its own label.

For overlap, I started a stub that sits there, launched a second run a second later, and the second one exited with 75 and left a skipped line in summary.log.

What this does not tell you

All of this was checked against fake scripts. How the real agy streams output while it works, and which of --print-timeout and HARD_LIMIT fires first, still need a check with the real thing. I started with HARD_LIMIT=1m and a single light request.

One more thing. --dangerously-skip-permissions looks convenient for unattended runs, but an outer frame only guards time, size and overlap. It cannot stop a file from being deleted. Whether to pair it with --sandbox or narrow the permissions first is a decision that stays outside the frame.

One line for the morning

summary.log has one line per run, so the morning check is just filtering out the ok lines.

# Show everything that is not ok (timeout / truncated / error / skipped)
grep -v 'label=ok' ~/agy-nightly-logs/summary.log | tail -n 5
 
# Show only runs that took longer than 10 minutes (600 s)
awk '{for(i=1;i<=NF;i++) if($i ~ /^secs=/){split($i,a,"="); if(a[2]+0>600) print "SLOW:", $0}}' ~/agy-nightly-logs/summary.log

I ran the first one against the log from the four test endings above, and only the three non-ok lines came back (a skipped line has no label=, so it shows up too). The second picks lines whose seconds exceed the threshold. Once you know your run usually finishes in about the same number of seconds each night, a night that suddenly stretches is easy to notice.

The next step

Before tonight's schedule, run one light request with HARD_LIMIT=1m and confirm that summary.log gains exactly one line. Extend the limit only after you can read that line in the morning.

The CLI's job is to stop; knowing that it stopped is mine. I keep to that line even when the settings change between versions.

If version drift is the bigger worry, Keeping Scheduled Runs Reproducible: Pinning the Antigravity CLI Version to Tame Behavior Drift pairs well with this frame.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

✦ Tips2026-08-21
The pipe in my wrapper was swallowing Antigravity CLI's exit code
CLI 1.1.14 surfaces language server failures as a non-zero exit. That improvement never reaches you if your automation wrapper drops the exit code first. I ran sixteen calling patterns on bash 5.1 and recorded which ones make the failure disappear, plus the fixes that bring it back.
◈ Agents & Manager2026-10-25
An Unattended Job Stopped With 11MB Free. du Counted Only 59MB; the Other 9GB Was Somewhere I Couldn't See
Two bash scripts that measure free space on the filesystem you are about to write to and stop an unattended agent run before it starts. Includes why du and df disagreed and how to spot a shortage you cannot fix.
◈ Agents & Manager2026-10-24
I Added pipefail and a Passing Check Turned Red: The False Failure That Starts Once Output Passes 8 KB
Adding pipefail can turn a passing check red. I reproduced the false failure that appears once output passes 8 KB on 1,122 files, then built a file-based runner and a canary.