From 3734978d569567a5e9e937cea372b0ae85bbcf85 Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Wed, 23 Sep 2026 19:40:44 +1200 Subject: [PATCH] mm-worktree-task: --gate re-runs the test suite itself, never trusts the agent's claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 "" (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 --- scripts/mm-worktree-task | 102 +++++++++++++++++++ specs/mm-worktree-task.spec.md | 19 +++- tests/test-mm-worktree-task.sh | 172 +++++++++++++++++++++++++++++++++ 3 files changed, 292 insertions(+), 1 deletion(-) diff --git a/scripts/mm-worktree-task b/scripts/mm-worktree-task index d868072..28ed31d 100755 --- a/scripts/mm-worktree-task +++ b/scripts/mm-worktree-task @@ -33,6 +33,12 @@ Options: --docs all|recent|none Which item documents to bundle (default: recent) --tools "" Extra --allowedTools entries (comma-separated) --timeout Sub-session timeout (default: 1800) + --gate "" 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 Max seconds the --gate command may run (default: 900) --context 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:-}" + [[ -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="" + 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 diff --git a/specs/mm-worktree-task.spec.md b/specs/mm-worktree-task.spec.md index 9d1126d..03d9b1e 100644 --- a/specs/mm-worktree-task.spec.md +++ b/specs/mm-worktree-task.spec.md @@ -26,6 +26,8 @@ mm-worktree-task [OPTIONS] | `--docs all\|recent\|none` | `recent` | Which item documents to bundle. `recent` = documents updated in the last 14 days; falls back to `all` if the CLI does not expose update dates. The full list of document names is always included in the prompt regardless of which are bundled. | | `--tools ""` | — | Extra `--allowedTools` entries merged into the default allowlist (comma-separated). | | `--timeout ` | `1800` | Max seconds the sub-session may run (`timeout ` wrapper). | +| `--gate ""` | — | Supervisor gate: after the sub-session exits, `mm-worktree-task` itself re-runs this command in the worktree against the sub-session's LAST commit and appends a `## Supervisor gate` section to the result file (see Behaviour §8). Never trust the agent's own "tests pass" claim — this re-run is the check. | +| `--gate-timeout ` | `900` | Max seconds `--gate`'s command may run before it is killed and recorded as a timed-out (failed) gate. | | `--context ` | resolved (see §0) | `cp-cli --context` used for every planner call. | | `--upload` | off | After the sub-session exits, upload its report as a document on the item and patch `flow_state` to `impl-complete`. Implemented only in combination with `--collect` (see Behaviour §6) since launch is detached. | | `--base ` | `main` | Branch or commit the worktree is created from, and the ref `--collect` logs/diffs against. Needed when work lives on a long-running branch, or when one agent's branch must build on another's (e.g. an implementation agent starting from a test agent's `mm/` branch). | @@ -81,6 +83,14 @@ mm-worktree-task [OPTIONS] ``` wrapped in a backgrounded subshell that, on exit, appends ` rc= elapsed=s` to `/launch.log`. The parent process returns immediately. Default allowlist: `Read,Write,Edit,Glob,Grep,Bash(pytest *),Bash(python3 *),Bash(ruff *),Bash(grep *),Bash(ls *),Bash(git add *),Bash(git commit *),Bash(git status*),Bash(git diff*),Bash(git rev-parse*),Bash(git log*)`. + - **Supervisor gate (`--gate ""`, optional).** Runs inside the same backgrounded subshell, immediately after the sub-session's `ask-minimax-run` process exits and before the `launch.log` line is appended — so it always evaluates the worktree exactly as the sub-session left it, never a snapshot from mid-run: + 1. Record `git -C rev-parse HEAD` as the gate sha — this is the sub-session's LAST commit, not whatever commit existed when the sub-session claimed success. + 2. Check whether the worktree is dirty (`git status --porcelain` non-empty). A dirty worktree makes the recorded sha not fully represent what was tested, so it is treated as a **failed** gate regardless of the command's own exit code. + 3. Run the command as `setsid bash -c ""` in the worktree (its own process group, `pgid == pid`), capturing combined stdout+stderr and its exit code, with a watchdog that after `--gate-timeout` (default `900`) seconds sends `SIGTERM` to the whole group (`kill -TERM -- -`), waits 5s, then `SIGKILL`s the group — a command that forks children (a test runner's own workers, say) is fully reaped, not just its immediate wrapper. A timed-out gate is recorded as **TIMED OUT** and failed regardless of the eventual exit code. A command that does not exist exits non-zero (typically `127`) and is failed the same way — there is no separate "skipped" state. + 4. Append `## Supervisor gate (re-run by mm-worktree-task)` to `/-result.md` (created if the sub-session never wrote one, noting that explicitly) with: the HEAD sha, the exact command, the clean/dirty flag, the exit code, an explicit **PASSED**/**FAILED** verdict, and the tail (last 60 lines) of captured output. + 5. If the verdict is FAILED and the text of `` *as the sub-session left it* (before this section was appended) reads as a claim that tests passed, the appended section says so explicitly (`**MISMATCH:** the agent's own report ... claims a passing test run; this independent re-run ... FAILED`) — the agent's claim is never the last word. + 6. `mm-worktree-task`'s own exit code is unaffected by the gate outcome: the launch has already returned 0 before the sub-session (and gate) run at all, detached. The gate's PASSED/FAILED verdict is only ever visible in the result file / `--collect` output — never in a process exit code. + With no `--gate`, none of the above runs and the result file is exactly what the sub-session wrote, unchanged from pre-`--gate` behaviour. 9. Print: report dir, prompt file path, stdout file path, launch.log path, and a one-line `poll with: tail -f ` hint. 10. `--collect`: instead of steps 6-9, for the given ``'s existing worktree, print `git -C log --oneline ..HEAD`, `git -C diff --stat ...HEAD`, and the contents of `/-result.md` if present. If `--upload` is also given and the report file exists: - `python3 -m lib.cp_cli planner items documents put mm--.md --file ` @@ -91,7 +101,7 @@ mm-worktree-task [OPTIONS] With `--dryrun`/`-n`: - No worktree, branch, bundle file, prompt file, or report-dir directory is created; no `ask-minimax-run` process is started. -- Prints, in order: resolved slug/uuid/worktree path/branch, the bundle manifest (section names + document names, bundled vs. available-only), the full assembled prompt text, and the exact `ask-minimax-run` command line that would run (including the resolved allowlist and timeout). +- Prints, in order: resolved slug/uuid/worktree path/branch, the bundle manifest (section names + document names, bundled vs. available-only), the full assembled prompt text, and the exact `ask-minimax-run` command line that would run (including the resolved allowlist and timeout), plus the resolved `--gate` command and `--gate-timeout` (or `` if not given). The gate itself is never executed under `--dryrun`. - Exit 0. ## Edge Cases @@ -105,6 +115,8 @@ With `--dryrun`/`-n`: | `--upload` without `--collect` | Error, exit 1 | | `--docs recent` but CLI gives no `updated_at` | Fall back to `--docs all` behaviour, note this in bundle `## Documents` section | | cp_cli sibling-list command unsupported | Skip `## Siblings` section silently (best-effort) | +| `--gate` command missing, non-zero, or times out | Recorded as `Verdict: **FAILED**` in the `## Supervisor gate` section — never a silent skip | +| `--gate` runs against a dirty worktree | Recorded as `Verdict: **FAILED**` regardless of the command's own exit code (the sha would not represent everything present) | ## Security @@ -130,4 +142,9 @@ mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --collect # Collect and upload the report + mark impl-complete mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --collect --upload + +# Launch with a supervisor gate: re-run the real suite yourself after the +# sub-session's last commit, bounded to 10 minutes +mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate \ + --gate "python3 -m pytest -q" --gate-timeout 600 ``` diff --git a/tests/test-mm-worktree-task.sh b/tests/test-mm-worktree-task.sh index 7629c76..1d3fbb6 100755 --- a/tests/test-mm-worktree-task.sh +++ b/tests/test-mm-worktree-task.sh @@ -153,6 +153,178 @@ UPLOAD_RC=$? assert_eq "upload without collect exits 1" "1" "$UPLOAD_RC" assert_contains "upload error message" "--upload requires --collect" "$UPLOAD_OUT" +echo +echo "== mm-worktree-task: --gate supervisor re-run ==" +# Strategy: a fake `ask-minimax-run` on PATH stands in for the real sub-session +# (it commits in the worktree, optionally writes a result file, optionally +# leaves the worktree dirty) so these tests drive the REAL script end to end — +# real worktree, real backgrounded subshell, real `timeout`/gate re-run — +# without ever launching a real MiniMax session. + +REPORT_ROOT="${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task/$(basename "$REPO_ROOT")" +FAKE_BIN_DIR="$(mktemp -d)" +cat > "$FAKE_BIN_DIR/ask-minimax-run" <<'FAKE_MM' +#!/usr/bin/env bash +# Stand-in for ask-minimax-run: commits in the worktree it is run from (cwd +# is already the worktree — mm-worktree-task cd's there before invoking it), +# optionally writes a result file, optionally leaves an uncommitted change. +set -e +[[ -n "${FAKE_MM_SLEEP:-}" ]] && sleep "$FAKE_MM_SLEEP" +git add -A >/dev/null 2>&1 || true +git -c user.email=fake-mm@test.invalid -c user.name="fake mm" \ + commit -m "fake mm commit" --allow-empty >/dev/null 2>&1 +if [[ -n "${FAKE_MM_RESULT_FILE:-}" ]]; then + mkdir -p "$(dirname "$FAKE_MM_RESULT_FILE")" + printf '%s\n' "${FAKE_MM_RESULT_CONTENT:-Summary: done}" > "$FAKE_MM_RESULT_FILE" +fi +[[ -n "${FAKE_MM_LEAVE_DIRTY:-}" ]] && echo "uncommitted" > uncommitted-change.txt +exit 0 +FAKE_MM +chmod +x "$FAKE_BIN_DIR/ask-minimax-run" + +cleanup_slug() { + local slug=$1 + git -C "$REPO_ROOT" worktree remove --force "$REPO_ROOT/.claude/worktrees/mm-${slug}" >/dev/null 2>&1 || true + git -C "$REPO_ROOT" branch -D "mm/${slug}" >/dev/null 2>&1 || true + rm -f "$REPORT_ROOT/${slug}-bundle.md" "$REPORT_ROOT/${slug}-prompt.txt" \ + "$REPORT_ROOT/${slug}-stdout.txt" "$REPORT_ROOT/${slug}-result.md" +} + +# Bounded poll (not a fixed sleep chain) for the backgrounded subshell — +# ask-minimax-run + gate, per the current code path — to finish and append +# its launch.log line. +wait_for_launch() { + local slug=$1 timeout_s=${2:-20} waited=0 + while (( waited < timeout_s * 5 )); do + grep -q "^${slug} rc=" "$REPORT_ROOT/launch.log" 2>/dev/null && return 0 + sleep 0.2 + waited=$((waited + 1)) + done + return 1 +} + +run_real_launch() { + # args: slug [extra mm-worktree-task args...] + local slug=$1; shift + PATH="$FAKE_BIN_DIR:$PATH" \ + MM_WORKTREE_TASK_FAKE_ITEM="$FIXTURE_ITEM" \ + MM_WORKTREE_TASK_FAKE_DOCS_DIR="$FIXTURE_DOCS_DIR" \ + "$SCRIPT" "$TEST_UUID" --slug "$slug" "$@" >/dev/null 2>&1 +} + +export MM_WORKTREE_TASK_FAKE_ITEM="$FIXTURE_ITEM" +export MM_WORKTREE_TASK_FAKE_DOCS_DIR="$FIXTURE_DOCS_DIR" + +# --- gate passes --- +SLUG="gate-pass-$$" +cleanup_slug "$SLUG" +FAKE_MM_RESULT_FILE="${REPORT_ROOT}/${SLUG}-result.md" \ +FAKE_MM_RESULT_CONTENT=$'Summary: implemented feature\nTests run: all tests passed' \ +run_real_launch "$SLUG" --gate "true" +if wait_for_launch "$SLUG"; then + RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" + HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "")" + assert_contains "gate-pass: section present" "## Supervisor gate" "$RESULT" + assert_contains "gate-pass: verdict PASSED" "Verdict: **PASSED**" "$RESULT" + assert_contains "gate-pass: names the actual worktree HEAD sha" "$HEAD_SHA" "$RESULT" + assert_not_contains "gate-pass: no MISMATCH raised" "MISMATCH" "$RESULT" +else + printf " ${RED}FAIL${NC} gate-pass: backgrounded launch did not complete in time\n" + FAIL=$((FAIL + 1)) +fi +cleanup_slug "$SLUG" + +# --- gate fails, and the agent's own report claimed success (the mismatch case) --- +SLUG="gate-fail-$$" +cleanup_slug "$SLUG" +FAKE_MM_RESULT_FILE="${REPORT_ROOT}/${SLUG}-result.md" \ +FAKE_MM_RESULT_CONTENT=$'Summary: implemented feature\nTests run: all tests passed, 0 failed' \ +run_real_launch "$SLUG" --gate "exit 1" +if wait_for_launch "$SLUG"; then + RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" + HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "")" + assert_contains "gate-fail: verdict FAILED" "Verdict: **FAILED**" "$RESULT" + assert_contains "gate-fail: names the actual worktree HEAD sha" "$HEAD_SHA" "$RESULT" + assert_contains "gate-fail: exit code recorded" "Exit code: 1" "$RESULT" + assert_contains "gate-fail: mismatch called out against agent's own claim" "MISMATCH" "$RESULT" +else + printf " ${RED}FAIL${NC} gate-fail: backgrounded launch did not complete in time\n" + FAIL=$((FAIL + 1)) +fi +cleanup_slug "$SLUG" + +# --- gate command does not exist: failure, never a silent skip --- +SLUG="gate-missing-$$" +cleanup_slug "$SLUG" +run_real_launch "$SLUG" --gate "this-command-truly-does-not-exist-xyz" +if wait_for_launch "$SLUG"; then + RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" + assert_contains "gate-missing: verdict FAILED (not skipped)" "Verdict: **FAILED**" "$RESULT" + assert_not_contains "gate-missing: never reads as a skip" "SKIP" "$RESULT" +else + printf " ${RED}FAIL${NC} gate-missing: backgrounded launch did not complete in time\n" + FAIL=$((FAIL + 1)) +fi +cleanup_slug "$SLUG" + +# --- gate times out: bounded, reaped, recorded as failed --- +# The sleep duration embeds this test run's own PID as a fractional-second +# fingerprint (GNU sleep accepts arbitrary-precision fractions) so the +# leftover-process check below can never false-positive on some unrelated +# "sleep 30" already running on a shared box. +SLUG="gate-timeout-$$" +SLEEP_MARKER="30.$$" +cleanup_slug "$SLUG" +run_real_launch "$SLUG" --gate "sleep ${SLEEP_MARKER}" --gate-timeout 1 +if wait_for_launch "$SLUG" 15; then + RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" + assert_contains "gate-timeout: verdict FAILED" "Verdict: **FAILED**" "$RESULT" + assert_contains "gate-timeout: TIMED OUT noted with the bound" "TIMED OUT after 1s" "$RESULT" + # the marked sleep process must actually have been reaped, not left running + sleep 1 + LEFTOVER="$(pgrep -f "sleep ${SLEEP_MARKER}" 2>/dev/null || true)" + assert_eq "gate-timeout: no leftover marked sleep process" "" "$LEFTOVER" +else + printf " ${RED}FAIL${NC} gate-timeout: backgrounded launch did not complete within the bound\n" + FAIL=$((FAIL + 1)) +fi +cleanup_slug "$SLUG" + +# --- dirty worktree after the sub-session: failed regardless of the gate command's own exit --- +SLUG="gate-dirty-$$" +cleanup_slug "$SLUG" +FAKE_MM_LEAVE_DIRTY=1 run_real_launch "$SLUG" --gate "true" +if wait_for_launch "$SLUG"; then + RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" + assert_contains "gate-dirty: verdict FAILED despite gate command exiting 0" "Verdict: **FAILED**" "$RESULT" + assert_contains "gate-dirty: dirty worktree called out" "DIRTY" "$RESULT" +else + printf " ${RED}FAIL${NC} gate-dirty: backgrounded launch did not complete in time\n" + FAIL=$((FAIL + 1)) +fi +cleanup_slug "$SLUG" + +# --- no --gate at all: result file shape is exactly what the sub-session wrote --- +SLUG="no-gate-$$" +cleanup_slug "$SLUG" +FAKE_MM_RESULT_FILE="${REPORT_ROOT}/${SLUG}-result.md" \ +FAKE_MM_RESULT_CONTENT=$'Summary: implemented feature\nTests run: all tests passed' \ +run_real_launch "$SLUG" +if wait_for_launch "$SLUG"; then + RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" + assert_not_contains "no-gate: never adds a Supervisor gate section" "Supervisor gate" "$RESULT" + assert_eq "no-gate: report is byte-for-byte what the sub-session wrote" \ + $'Summary: implemented feature\nTests run: all tests passed' "$RESULT" +else + printf " ${RED}FAIL${NC} no-gate: backgrounded launch did not complete in time\n" + FAIL=$((FAIL + 1)) +fi +cleanup_slug "$SLUG" + +rm -rf "$FAKE_BIN_DIR" +unset MM_WORKTREE_TASK_FAKE_ITEM +unset MM_WORKTREE_TASK_FAKE_DOCS_DIR + echo echo "==================================" echo " PASS: $PASS FAIL: $FAIL"