From 80dc2eb45e45ba812178f2edeec72c4ea17f7f86 Mon Sep 17 00:00:00 2001 From: Godopu Date: Wed, 30 Sep 2026 15:47:40 +0000 Subject: [PATCH] fix(issue-26): integer knobs that reach bash arithmetic are read as decimal bash read MAM_JOB_TIMEOUT_S=0700 as octal 448 (and failed on 0900) while the loop runner read 700. One rule now, with a per-knob minimum: lib.sh mam_int / mam_int_knob and runner._positive_int (blanks allowed, digits only, decimal, min <= n <= 999999999). - minimum 1: MAM_JOB_TIMEOUT_S, MAM_HEARTBEAT_SEC, MAM_HEARTBEAT_MISSES, MAM_ACK_TIMEOUT_S (invalid env: warning + default) and the wrapper's --timeout/--idle-timeout/--heartbeat/--heartbeat-misses/--ack-timeout (invalid: rc 1); the runner's job_budget, --job-timeout, contract_args. - minimum 0 (0 = check once): MAM_INJECT_ACK_GRACE_S, delegate_wait_started's ack_s, SKS_DIALOG_TIMEOUT, SKS_BUSY_TIMEOUT, SKS_DELIVERY_GRACE_S. - tests/test_issue26_ints.py: INT_KNOBS is the one table of minimums; a static guard keeps env defaults out of arithmetic and checks each normalised knob against it. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/lib.sh | 36 ++- .agents/skills/lib_py/loop/runner.py | 34 +- .../multi-agent-mux-delegate-job | 26 +- tests/test_issue26_ints.py | 295 ++++++++++++++++++ 4 files changed, 367 insertions(+), 24 deletions(-) create mode 100644 tests/test_issue26_ints.py diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index fa931a9..ef5db06 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -2335,6 +2335,35 @@ sys.exit(1) ' 2>/dev/null } +# mam_int +# +# Issue #26: the one integer rule for knobs that reach bash arithmetic (the +# Python twin is lib_py/loop/runner.py::_positive_int). Surrounding blanks are +# allowed, then digits only, read as DECIMAL (bash would read 0700 as octal +# 448 and fail on 0900), and min <= n <= 999999999. Prints n and returns 0, or +# returns 1. The per-knob minimum lives in tests/test_issue26_ints.py. +mam_int() { + [[ "${1-}" =~ ^[[:space:]]*([0-9]+)[[:space:]]*$ ]] || return 1 + local d="${BASH_REMATCH[1]}" + d="${d#"${d%%[!0]*}"}" + [ ${#d} -le 9 ] || return 1 + local n=$(( 10#${d:-0} )) + [ "$n" -ge "$2" ] || return 1 + printf '%s\n' "$n" +} + +# mam_int_knob +# +# The value of $ENV_NAME through mam_int; unset or empty gives , an +# invalid value gives with a warning on stderr. +mam_int_knob() { + local raw="${!1-}" + [ -n "$raw" ] || { printf '%s\n' "$2"; return 0; } + mam_int "$raw" "$3" && return 0 + echo "WARNING: $1=$raw ignored (not a decimal integer >= $3); using $2" >&2 + printf '%s\n' "$2" +} + # delegate_wait_started [ack_timeout_s] [dispatch_token] # # ISSUE-23 / ISSUE-18: delivery is confirmed by the protocol, not by the @@ -2347,6 +2376,7 @@ sys.exit(1) delegate_wait_started() { local job_id="$1" ack_s="${2:-180}" tok="${3:-}" [ -n "$job_id" ] || return 1 + ack_s=$(mam_int "$ack_s" 0) || { echo "WARNING: onboarding ack timeout ${2-} ignored (not a decimal integer >= 0); using 180" >&2; ack_s=180; } local deadline deadline=$(( $(date +%s) + ack_s )) while :; do @@ -2759,7 +2789,7 @@ send_keys_safe() { fi if [ "$_q_rc" != "2" ]; then - deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} )) + deadline=$(( $(date +%s) + $(mam_int_knob SKS_DIALOG_TIMEOUT 30 0) )) while _pane_dialog_open "$sess"; do if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then _sks_herdr send-keys -t "$sess" Escape @@ -2778,7 +2808,7 @@ send_keys_safe() { # Busy gate: wait up to SKS_BUSY_TIMEOUT only if wait_for_agent_idle returned 3 (unknown) if [ "${_SKS_IDLE_RC:-0}" = "3" ]; then - local b_deadline=$(( $(date +%s) + ${SKS_BUSY_TIMEOUT:-120} )) + local b_deadline=$(( $(date +%s) + $(mam_int_knob SKS_BUSY_TIMEOUT 120 0) )) local still_busy=1 while [ "$(date +%s)" -lt "$b_deadline" ]; do local tail_pane @@ -2847,7 +2877,7 @@ send_keys_safe() { # still fails closed (rc 4); the wrapper then waits for `started` up to # the job's ack timeout T before finalising the inject as failed. if printf '%s\n' "$p_out" | grep -Eq '"code": *"(timeout|agent_prompt_stalled)"'; then - local g_deadline=$(( $(date +%s) + ${SKS_DELIVERY_GRACE_S:-15} )) + local g_deadline=$(( $(date +%s) + $(mam_int_knob SKS_DELIVERY_GRACE_S 15 0) )) while [ "$(date +%s)" -lt "$g_deadline" ]; do [ "$(_sks_agent_status "$sess" "$agent_target")" = "working" ] && return 0 _sks_job_started "$job_id" && return 0 diff --git a/.agents/skills/lib_py/loop/runner.py b/.agents/skills/lib_py/loop/runner.py index cbce7b7..679946d 100644 --- a/.agents/skills/lib_py/loop/runner.py +++ b/.agents/skills/lib_py/loop/runner.py @@ -67,6 +67,16 @@ def log_error(msg: str) -> None: print(f"{_COLOR_ERROR} {msg}", file=sys.stderr) + +def _positive_int(text: Optional[str], minimum: int = 1) -> Optional[int]: + """Issue #26: lib.sh mam_int's rule - blanks around, digits only, decimal + (0700 is 700), minimum <= n <= 999999999; else None.""" + s = (text or "").strip(" \t\n\r\v\f") + if not s or not s.isascii() or not s.isdigit(): + return None + n = int(s) + return n if minimum <= n <= 999_999_999 else None + class LoopRunner: """Orchestrates planning, code implementation, and multi-agent peer reviews.""" @@ -142,7 +152,7 @@ class LoopRunner: " --all-reviewer Enforce PASS verdict from all active reviewer sessions", " --max-loop N Max execution-review corrective loop runs (default: 3)", " --max-rebut N Max rebuttal attempts per review iteration (default: 1, 0: disabled)", - " --job-timeout Budget of each delegated job (default: $MAM_JOB_TIMEOUT_S or 3600)", + " --job-timeout Budget of each delegated job (positive decimal integer; default: $MAM_JOB_TIMEOUT_S or 3600)", " --verbose Print detailed execution timeline traces", " --cleanup Purge temporary job directories upon success", " --worktree Source worktree for diff/base/head (control plane stays here)", @@ -214,10 +224,10 @@ class LoopRunner: self.max_loop = int(args[i + 1]) i += 2 elif arg == "--job-timeout": - if i + 1 >= len(args) or not re.match(r"^[0-9]+$", args[i + 1]) or int(args[i + 1]) <= 0: - print("ERROR: --job-timeout requires a positive non-zero integer.", file=sys.stderr) + self.job_timeout = _positive_int(args[i + 1]) if i + 1 < len(args) else None + if self.job_timeout is None: + print("ERROR: --job-timeout requires a positive decimal integer.", file=sys.stderr) sys.exit(1) - self.job_timeout = int(args[i + 1]) i += 2 elif arg == "--max-rebut": if i + 1 >= len(args) or not re.match(r"^[0-9]+$", args[i + 1]): @@ -471,9 +481,9 @@ class LoopRunner: for env_name, flag in (("MAM_HEARTBEAT_SEC", "--heartbeat"), ("MAM_HEARTBEAT_MISSES", "--heartbeat-misses"), ("MAM_ACK_TIMEOUT_S", "--ack-timeout")): - val = os.environ.get(env_name) - if val and val.isdigit() and int(val) > 0: - args += [flag, val] + val = _positive_int(os.environ.get(env_name)) + if val is not None: + args += [flag, str(val)] return args def resolve_all_reviewers(self, state: Dict[str, Any]) -> List[str]: @@ -593,11 +603,17 @@ class LoopRunner: @property def job_budget(self) -> int: - """Issue #33: --job-timeout > $MAM_JOB_TIMEOUT_S (positive integer) > 3600.""" + """Issue #33: --job-timeout > $MAM_JOB_TIMEOUT_S (positive decimal integer) > 3600.""" if self.job_timeout: return self.job_timeout env = os.environ.get("MAM_JOB_TIMEOUT_S", "") - return int(env) if env.isdigit() and int(env) > 0 else 3600 + n = _positive_int(env) + if n is None and env: + if not getattr(self, "_warned_job_timeout", False): + self._warned_job_timeout = True + print(f"WARNING: MAM_JOB_TIMEOUT_S={env} ignored (not a decimal integer >= 1); using 3600", file=sys.stderr) + return 3600 + return n or 3600 @property def job_wait_s(self) -> int: diff --git a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job index 2875cfd..290187f 100755 --- a/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job +++ b/.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job @@ -115,7 +115,7 @@ usage() { multi-agent-mux-delegate-job [options] submit --agent (--prompt | --prompt-file ) [--workdir ] [--agent-session