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

View File

@@ -26,6 +26,8 @@ mm-worktree-task <item-uuid> [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 "<list>"` | — | Extra `--allowedTools` entries merged into the default allowlist (comma-separated). |
| `--timeout <secs>` | `1800` | Max seconds the sub-session may run (`timeout <secs>` wrapper). |
| `--gate "<command>"` | — | 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 <secs>` | `900` | Max seconds `--gate`'s command may run before it is killed and recorded as a timed-out (failed) gate. |
| `--context <name>` | 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 <ref>` | `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/<slug>` branch). |
@@ -81,6 +83,14 @@ mm-worktree-task <item-uuid> [OPTIONS]
```
wrapped in a backgrounded subshell that, on exit, appends `<slug> rc=<rc> elapsed=<s>s` to `<report-dir>/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 "<command>"`, 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 <worktree> 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 "<command>"` 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 -- -<pid>`), 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 `<report-dir>/<slug>-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 `<result-file>` *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 <stdout file>` hint.
10. `--collect`: instead of steps 6-9, for the given `<slug>`'s existing worktree, print `git -C <worktree> log --oneline <base>..HEAD`, `git -C <worktree> diff --stat <base>...HEAD`, and the contents of `<report-dir>/<slug>-result.md` if present. If `--upload` is also given and the report file exists:
- `python3 -m lib.cp_cli planner items documents put <uuid> mm-<slug>-<YYYY-MM-DD>.md --file <report>`
@@ -91,7 +101,7 @@ mm-worktree-task <item-uuid> [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 `<none>` 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
```

View File

@@ -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 "<missing>")"
HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "<no-worktree>")"
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 "<missing>")"
HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "<no-worktree>")"
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 "<missing>")"
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 "<missing>")"
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 "<missing>")"
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 "<missing>")"
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"