Symptom
~/.agent-farm/tower.log fills with a recurring WARN (every 15 min for months, survives Tower restarts):
[WARN] Condition evaluation failed for 'Service Health Check': ReferenceError: exitCode is not defined
The triggering task is a user health-check cron (.af-cron/*.yaml) with:
condition: "exitCode != 0"
message: "Service Health Alert:\n${output}"
target: architect
Root cause (two layers, both in packages/codev/src/agent-farm/servers/tower-cron.ts)
-
evaluateCondition() (line ~293) builds new Function('output', 'return ' + condition) — only output is in scope. Any condition referencing exitCode throws ReferenceError, which is caught at line ~251, logged as WARN, and forces shouldNotify = false. The docs/UX imply exit-code awareness; the evaluator doesn't provide it.
-
Delivery gate (line ~256) is if (shouldNotify && result === 'success'). A monitoring script that detects a problem and exits non-zero produces result = 'failure', whose branch only logs a WARN ("infrastructure noise"). Consequence: "alert me when this command fails" is inexpressible — the alert is suppressed on exactly the runs that matter. (In the wild, task authors work around it by pushing notifications from inside the script.)
Prescribed fix (design decided — carry it in BUGFIX)
-
Capture the exit code in runCommand(): in the exec callback, error.code (number) is the exit code when the process ran and exited non-zero. Resolve with { output, exitCode } (0 on success) instead of rejecting for plain non-zero exits. Keep rejecting/flagging genuinely infrastructural errors (spawn failure, timeout/kill — error.killed/error.signal) as today's failure class.
-
Expose exitCode to conditions: new Function('output', 'exitCode', 'return ' + condition), invoked with both. Conditions using only output are unchanged (backward compatible).
-
Rework the delivery gate: deliver when the condition (now able to see exitCode) evaluates true, regardless of the old success/failure split:
- Task with a
condition: delivery is decided by the condition alone (a non-zero exit is data, not noise).
- Task without a
condition: keep today's behavior (deliver only on exit 0) so existing no-condition tasks don't start alert-spamming on flaky commands.
- True infrastructure errors (spawn fail/timeout) keep the existing WARN-only path, with
exitCode reported as non-zero (e.g. 124/-1) so exitCode != 0 conditions still fire if the author wants them to.
-
State row semantics: last_result in cron_tasks should record success/failure by exit code as before — only the delivery logic changes.
-
Docs: document the condition environment (output: string, exitCode: number) wherever .af-cron YAML is documented (afx skill + agent-farm.md), BOTH trees.
Acceptance
- A task with
condition: "exitCode != 0" produces no ReferenceError; delivers its message exactly when the command exits non-zero.
- A task with
condition: "output.includes('FAIL')" behaves exactly as today.
- A task with no condition delivers only on exit 0 (unchanged).
- Unit tests cover: exitCode condition true/false, output-only condition regression, no-condition regression, timeout path.
Symptom
~/.agent-farm/tower.logfills with a recurring WARN (every 15 min for months, survives Tower restarts):The triggering task is a user health-check cron (
.af-cron/*.yaml) with:Root cause (two layers, both in
packages/codev/src/agent-farm/servers/tower-cron.ts)evaluateCondition()(line ~293) buildsnew Function('output', 'return ' + condition)— onlyoutputis in scope. Any condition referencingexitCodethrowsReferenceError, which is caught at line ~251, logged as WARN, and forcesshouldNotify = false. The docs/UX imply exit-code awareness; the evaluator doesn't provide it.Delivery gate (line ~256) is
if (shouldNotify && result === 'success'). A monitoring script that detects a problem and exits non-zero producesresult = 'failure', whose branch only logs a WARN ("infrastructure noise"). Consequence: "alert me when this command fails" is inexpressible — the alert is suppressed on exactly the runs that matter. (In the wild, task authors work around it by pushing notifications from inside the script.)Prescribed fix (design decided — carry it in BUGFIX)
Capture the exit code in
runCommand(): in theexeccallback,error.code(number) is the exit code when the process ran and exited non-zero. Resolve with{ output, exitCode }(0 on success) instead of rejecting for plain non-zero exits. Keep rejecting/flagging genuinely infrastructural errors (spawn failure, timeout/kill —error.killed/error.signal) as today'sfailureclass.Expose
exitCodeto conditions:new Function('output', 'exitCode', 'return ' + condition), invoked with both. Conditions using onlyoutputare unchanged (backward compatible).Rework the delivery gate: deliver when the condition (now able to see
exitCode) evaluates true, regardless of the old success/failure split:condition: delivery is decided by the condition alone (a non-zero exit is data, not noise).condition: keep today's behavior (deliver only on exit 0) so existing no-condition tasks don't start alert-spamming on flaky commands.exitCodereported as non-zero (e.g. 124/-1) soexitCode != 0conditions still fire if the author wants them to.State row semantics:
last_resultincron_tasksshould record success/failure by exit code as before — only the delivery logic changes.Docs: document the condition environment (
output: string,exitCode: number) wherever.af-cronYAML is documented (afx skill + agent-farm.md), BOTH trees.Acceptance
condition: "exitCode != 0"produces no ReferenceError; delivers its message exactly when the command exits non-zero.condition: "output.includes('FAIL')"behaves exactly as today.