mm-worktree-task: --gate re-runs the test suite itself, never trusts the agent's claim

Across ~12 supervised MiniMax units, more than half reported a green suite
where a manual re-run found red (see memory/gotchas-minimax.md). Add
--gate "<command>" (with --gate-timeout, default 900s): after the detached
sub-session exits, mm-worktree-task itself re-runs the command in the
worktree against the sub-session's LAST commit, in its own reapable process
group (setsid + group kill, so a hung gate and everything it forked are
bounded and cleaned up), and appends an unambiguous PASSED/FAILED verdict —
plus an explicit MISMATCH callout when the agent's own report claimed
success — to the result file. A missing command, non-zero exit, timeout, or
dirty worktree are all failures, never a silent skip. mm-worktree-task's own
exit code is unchanged: the launch already returns before the sub-session
(and gate) run, detached.

Claude-Session: https://claude.ai/code/session_014KNyfHmHpMbdMrHZXDRkAp
This commit is contained in:
Paul O'Reilly
2026-09-23 19:40:44 +12:00
parent cbab812917
commit 3734978d56
3 changed files with 292 additions and 1 deletions

View File

@@ -33,6 +33,12 @@ Options:
--docs all|recent|none Which item documents to bundle (default: recent)
--tools "<list>" Extra --allowedTools entries (comma-separated)
--timeout <secs> Sub-session timeout (default: 1800)
--gate "<command>" Re-run this command in the worktree yourself, after
the sub-session exits, against its LAST commit.
Appended as a "Supervisor gate" section in the
result file; a missing command, non-zero exit, or
timeout are all reported as FAILED, never skipped.
--gate-timeout <secs> Max seconds the --gate command may run (default: 900)
--context <name> cp-cli planner context (default: the ~/.cpconfig context
whose project_id equals the repo name, else cp-cli's own
current_context)
@@ -62,6 +68,8 @@ BASE="main"
DOCS="recent"
EXTRA_TOOLS=""
TIMEOUT=1800
GATE=""
GATE_TIMEOUT=900
UPLOAD=0
COLLECT=0
DRYRUN=0
@@ -76,6 +84,8 @@ while [[ $# -gt 0 ]]; do
--docs) DOCS="$2"; shift 2 ;;
--tools) EXTRA_TOOLS="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--gate) GATE="$2"; shift 2 ;;
--gate-timeout) GATE_TIMEOUT="$2"; shift 2 ;;
--context) CP_CONTEXT="$2"; shift 2 ;;
--upload) UPLOAD=1; shift ;;
--collect) COLLECT=1; shift ;;
@@ -381,6 +391,8 @@ if [[ $DRYRUN -eq 1 ]]; then
echo " Prompt path: ${PROMPT_FILE}"
echo " Timeout: ${TIMEOUT}s"
echo " Allowlist: ${ALLOWLIST}"
echo " Gate: ${GATE:-<none>}"
[[ -n "$GATE" ]] && echo " Gate timeout: ${GATE_TIMEOUT}s"
echo
echo "--- Bundle manifest ---"
echo "$BUNDLE_CONTENT"
@@ -406,6 +418,93 @@ fi
info "creating worktree ${WORKTREE} on branch ${BRANCH}"
git -C "$REPO_ROOT" worktree add "$WORKTREE" -b "$BRANCH" "$BASE"
# --- supervisor gate: re-run --gate ourselves after the sub-session exits,
# against its LAST commit. Never trust the agent's own "tests pass" claim —
# see memory/gotchas-minimax.md: more than half of supervised units reported
# green where a re-run found red. A missing command, non-zero exit, or
# timeout are all FAILED, never a silent skip.
run_gate() {
local gate_sha status_out dirty_flag=0 gate_out_file gate_rc timed_out=0
local gate_tail verdict had_report=0 agent_claims_pass=0
local gate_pid watchdog_pid timeout_flag
gate_sha="$(git -C "$WORKTREE" rev-parse HEAD 2>&1)" || gate_sha="<unresolvable HEAD: ${gate_sha}>"
status_out="$(git -C "$WORKTREE" status --porcelain 2>&1)"
[[ -n "$status_out" ]] && dirty_flag=1
# Plain `timeout bash -c "$GATE"` only signals the immediate bash wrapper —
# any grandchildren the gate command forks (a test runner's own workers,
# say) are reparented and keep running unbounded. `setsid` makes the gate
# its own process group (pgid == its pid), so the watchdog can signal the
# whole group (`-$gate_pid`) and actually reap everything the gate started.
gate_out_file="$(mktemp)"
timeout_flag="$(mktemp -u)"
set +e
setsid bash -c "$GATE" > "$gate_out_file" 2>&1 &
gate_pid=$!
(
sleep "$GATE_TIMEOUT"
if kill -0 "$gate_pid" 2>/dev/null; then
: > "$timeout_flag"
kill -TERM -- "-${gate_pid}" 2>/dev/null
sleep 5
kill -KILL -- "-${gate_pid}" 2>/dev/null
fi
) &
watchdog_pid=$!
wait "$gate_pid"
gate_rc=$?
kill "$watchdog_pid" 2>/dev/null
wait "$watchdog_pid" 2>/dev/null
set -e
[[ -f "$timeout_flag" ]] && timed_out=1
rm -f "$timeout_flag"
gate_tail="$(tail -n 60 "$gate_out_file" 2>/dev/null)"
rm -f "$gate_out_file"
if [[ $dirty_flag -eq 1 || $gate_rc -ne 0 ]]; then
verdict="FAILED"
else
verdict="PASSED"
fi
[[ -f "$RESULT_FILE" ]] && had_report=1
if [[ $had_report -eq 1 ]] \
&& grep -qiE '(all tests? pass|0 failed|tests? pass(ed)?|passing)' "$RESULT_FILE" 2>/dev/null \
&& ! grep -qiE '[1-9][0-9]* (failed|failing)' "$RESULT_FILE" 2>/dev/null; then
agent_claims_pass=1
fi
{
echo
echo "## Supervisor gate (re-run by mm-worktree-task)"
echo
[[ $had_report -eq 0 ]] && echo "(No agent report was found at this path before the gate ran.)"
echo "- HEAD: ${gate_sha}"
echo "- Command: \`${GATE}\`"
if [[ $dirty_flag -eq 1 ]]; then
echo "- Worktree status: DIRTY — uncommitted changes present; result does not correspond to a fully committed state"
else
echo "- Worktree status: clean"
fi
if [[ $timed_out -eq 1 ]]; then
echo "- Exit code: ${gate_rc} (TIMED OUT after ${GATE_TIMEOUT}s)"
else
echo "- Exit code: ${gate_rc}"
fi
echo "- Verdict: **${verdict}**"
if [[ "$verdict" == "FAILED" && $agent_claims_pass -eq 1 ]]; then
echo
echo "**MISMATCH:** the agent's own report above claims a passing test run; this independent re-run of the gate FAILED. Treat this section as authoritative, not the agent's claim."
fi
echo
echo "Output (tail):"
echo '```'
echo "$gate_tail"
echo '```'
} >> "$RESULT_FILE"
}
info "launching ask-minimax-run (detached, timeout ${TIMEOUT}s)"
(
cd "$WORKTREE"
@@ -417,6 +516,9 @@ info "launching ask-minimax-run (detached, timeout ${TIMEOUT}s)"
> "$STDOUT_FILE" 2>&1
rc=$?
set -e
if [[ -n "$GATE" ]]; then
run_gate
fi
echo "${SLUG} rc=${rc} elapsed=$(( $(date +%s) - start ))s" >> "$LAUNCH_LOG"
) &
disown