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
532 lines
18 KiB
Bash
Executable File
532 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Launch a MiniMax M3 implementation sub-session in an isolated git worktree,
|
|
# with the prompt assembled from a planner work item. See specs/mm-worktree-task.spec.md.
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
|
info() { echo -e "${BLUE}==>${NC} $*"; }
|
|
ok() { echo -e "${GREEN}✓${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}!${NC} $*" >&2; }
|
|
err() { echo -e "${RED}✗${NC} $*" >&2; }
|
|
|
|
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*)"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: mm-worktree-task <item-uuid> [OPTIONS]
|
|
|
|
Launch a MiniMax M3 implementation sub-session (via ask-minimax-run) in an
|
|
isolated git worktree of the current repo, with the prompt assembled from a
|
|
planner work item.
|
|
|
|
Arguments:
|
|
<item-uuid> Full 36-char planner work-item UUID. Required unless
|
|
--brief is given together with --slug.
|
|
|
|
Options:
|
|
--slug <slug> Worktree/branch suffix (default: first 8 chars of uuid)
|
|
--base <ref> Branch/commit the worktree is created from and that
|
|
--collect diffs against (default: main)
|
|
--focus "<line>" One-line focus appended to the task frame
|
|
--brief <file> Use this file as the task body instead of/in addition
|
|
to the fetched item
|
|
--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)
|
|
--upload After --collect, upload the report + mark impl-complete
|
|
--collect Don't launch; print status for an already-run slug
|
|
--dryrun, -n Assemble everything, print it, create nothing
|
|
--help, -h Show this help
|
|
|
|
Environment:
|
|
MM_WORKTREE_TASK_CP_CLI_DIR=<dir> checkout holding lib/cp_cli (default: the
|
|
current repo if it has one, else
|
|
~/dev/claude/projects/agent-runtimes)
|
|
MM_WORKTREE_TASK_CP_CONTEXT=<name> same as --context
|
|
MM_WORKTREE_TASK_CPCONFIG=<file> cpconfig to resolve the context from
|
|
(default: ~/.cpconfig)
|
|
Env overrides (for testing):
|
|
MM_WORKTREE_TASK_FAKE_ITEM=<file> fake `planner items get --json` output
|
|
MM_WORKTREE_TASK_FAKE_DOCS_DIR=<dir> fake docs: <dir>/list.json + <dir>/<name>
|
|
EOF
|
|
}
|
|
|
|
UUID=""
|
|
SLUG=""
|
|
FOCUS=""
|
|
BRIEF=""
|
|
BASE="main"
|
|
DOCS="recent"
|
|
EXTRA_TOOLS=""
|
|
TIMEOUT=1800
|
|
GATE=""
|
|
GATE_TIMEOUT=900
|
|
UPLOAD=0
|
|
COLLECT=0
|
|
DRYRUN=0
|
|
CP_CONTEXT="${MM_WORKTREE_TASK_CP_CONTEXT:-}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--slug) SLUG="$2"; shift 2 ;;
|
|
--focus) FOCUS="$2"; shift 2 ;;
|
|
--brief) BRIEF="$2"; shift 2 ;;
|
|
--base) BASE="$2"; shift 2 ;;
|
|
--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 ;;
|
|
--dryrun|-n) DRYRUN=1; shift ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
-*) err "unknown flag: $1"; usage; exit 1 ;;
|
|
*)
|
|
if [[ -z "$UUID" ]]; then UUID="$1"; else err "unexpected argument: $1"; exit 1; fi
|
|
shift ;;
|
|
esac
|
|
done
|
|
|
|
case "$DOCS" in all|recent|none) ;; *) err "--docs must be all|recent|none"; exit 1 ;; esac
|
|
|
|
if [[ -z "$UUID" ]]; then
|
|
if [[ -z "$BRIEF" || -z "$SLUG" ]]; then
|
|
err "item-uuid is required unless --brief and --slug are both given"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$UUID" && -z "$SLUG" ]]; then
|
|
SLUG="${UUID:0:8}"
|
|
fi
|
|
|
|
if [[ $UPLOAD -eq 1 && $COLLECT -eq 0 ]]; then
|
|
err "--upload requires --collect (launch is detached; upload happens after the sub-session has produced a report)"
|
|
exit 1
|
|
fi
|
|
|
|
# --- repo root ---
|
|
if ! REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
|
err "not inside a git repository"
|
|
exit 1
|
|
fi
|
|
REPO_NAME="$(basename "$REPO_ROOT")"
|
|
|
|
# --- planner CLI: where it runs from, and which context it talks to ---
|
|
# The script is used from any planner-backed project; cp_cli itself lives in
|
|
# agent-runtimes. Resolve the checkout once and the context once, then every
|
|
# planner call goes through cp_cli().
|
|
resolve_cp_cli_dir() {
|
|
local d
|
|
for d in "${MM_WORKTREE_TASK_CP_CLI_DIR:-}" "$REPO_ROOT" "$HOME/dev/claude/projects/agent-runtimes"; do
|
|
[[ -n "$d" && ( -f "$d/lib/cp_cli.py" || -d "$d/lib/cp_cli" ) ]] && { echo "$d"; return 0; }
|
|
done
|
|
return 1
|
|
}
|
|
CP_CLI_DIR="$(resolve_cp_cli_dir || true)"
|
|
|
|
if [[ -z "$CP_CONTEXT" ]]; then
|
|
CPCONFIG="${MM_WORKTREE_TASK_CPCONFIG:-$HOME/.cpconfig}"
|
|
if [[ -f "$CPCONFIG" ]]; then
|
|
CP_CONTEXT="$(python3 - "$CPCONFIG" "$REPO_NAME" <<'PY' 2>/dev/null || true
|
|
import sys
|
|
try:
|
|
import yaml
|
|
cfg = yaml.safe_load(open(sys.argv[1])) or {}
|
|
except Exception:
|
|
sys.exit(0)
|
|
for ctx in cfg.get("contexts") or []:
|
|
if ctx.get("project_id") == sys.argv[2]:
|
|
print(ctx.get("name", ""))
|
|
break
|
|
PY
|
|
)"
|
|
fi
|
|
fi
|
|
|
|
cp_cli() {
|
|
if [[ -z "$CP_CLI_DIR" ]]; then
|
|
err "cannot find lib/cp_cli.py (set MM_WORKTREE_TASK_CP_CLI_DIR or check out agent-runtimes)"
|
|
return 1
|
|
fi
|
|
if [[ -n "$CP_CONTEXT" ]]; then
|
|
( cd "$CP_CLI_DIR" && python3 -m lib.cp_cli --context "$CP_CONTEXT" "$@" )
|
|
else
|
|
( cd "$CP_CLI_DIR" && python3 -m lib.cp_cli "$@" )
|
|
fi
|
|
}
|
|
|
|
REPORT_DIR="${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task/${REPO_NAME}"
|
|
WORKTREE="${REPO_ROOT}/.claude/worktrees/mm-${SLUG}"
|
|
BRANCH="mm/${SLUG}"
|
|
BUNDLE_FILE="${REPORT_DIR}/${SLUG}-bundle.md"
|
|
PROMPT_FILE="${REPORT_DIR}/${SLUG}-prompt.txt"
|
|
STDOUT_FILE="${REPORT_DIR}/${SLUG}-stdout.txt"
|
|
RESULT_FILE="${REPORT_DIR}/${SLUG}-result.md"
|
|
LAUNCH_LOG="${REPORT_DIR}/launch.log"
|
|
|
|
ALLOWLIST="$DEFAULT_ALLOWLIST"
|
|
[[ -n "$EXTRA_TOOLS" ]] && ALLOWLIST="${ALLOWLIST},${EXTRA_TOOLS}"
|
|
|
|
# --- --collect mode ---
|
|
if [[ $COLLECT -eq 1 ]]; then
|
|
if [[ ! -d "$WORKTREE" ]]; then
|
|
err "no worktree found for slug '${SLUG}' (${WORKTREE})"
|
|
exit 1
|
|
fi
|
|
info "git log --oneline ${BASE}..HEAD"
|
|
git -C "$WORKTREE" log --oneline "${BASE}..HEAD" || true
|
|
echo
|
|
info "git diff --stat ${BASE}...HEAD"
|
|
git -C "$WORKTREE" diff --stat "${BASE}...HEAD" || true
|
|
echo
|
|
if [[ -f "$RESULT_FILE" ]]; then
|
|
info "report: $RESULT_FILE"
|
|
cat "$RESULT_FILE"
|
|
else
|
|
warn "no report found at $RESULT_FILE"
|
|
fi
|
|
|
|
if [[ $UPLOAD -eq 1 ]]; then
|
|
if [[ -z "$UUID" ]]; then
|
|
err "--upload requires an item-uuid"
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$RESULT_FILE" ]]; then
|
|
err "cannot upload: no report at $RESULT_FILE"
|
|
exit 1
|
|
fi
|
|
DATE="$(date +%Y-%m-%d)"
|
|
DOC_NAME="mm-${SLUG}-${DATE}.md"
|
|
info "uploading ${DOC_NAME} to item ${UUID}"
|
|
cp_cli planner items documents put "$UUID" "$DOC_NAME" --file "$RESULT_FILE"
|
|
cp_cli planner items patch "$UUID" --flow-state impl-complete \
|
|
--description "mm-worktree-task: ${SLUG} committed on mm/${SLUG}"
|
|
ok "uploaded and marked impl-complete"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# --- gather item + docs ---
|
|
ITEM_JSON=""
|
|
TITLE=""
|
|
DOC_NAMES=()
|
|
DOC_BUNDLED=()
|
|
declare -A DOC_BODY
|
|
|
|
fetch_item() {
|
|
if [[ -n "$UUID" ]]; then
|
|
if [[ -n "${MM_WORKTREE_TASK_FAKE_ITEM:-}" ]]; then
|
|
ITEM_JSON="$(cat "$MM_WORKTREE_TASK_FAKE_ITEM")"
|
|
else
|
|
ITEM_JSON="$(cp_cli planner items get "$UUID" --json)"
|
|
fi
|
|
TITLE="$(echo "$ITEM_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("title",""))')"
|
|
fi
|
|
}
|
|
|
|
fetch_docs() {
|
|
[[ "$DOCS" == "none" ]] && return 0
|
|
local list_json
|
|
if [[ -n "${MM_WORKTREE_TASK_FAKE_DOCS_DIR:-}" ]]; then
|
|
[[ -f "${MM_WORKTREE_TASK_FAKE_DOCS_DIR}/list.json" ]] || return 0
|
|
list_json="$(cat "${MM_WORKTREE_TASK_FAKE_DOCS_DIR}/list.json")"
|
|
elif [[ -n "$UUID" ]]; then
|
|
list_json="$(cp_cli planner items documents list "$UUID" --json 2>/dev/null || echo '{"documents":[]}')"
|
|
else
|
|
return 0
|
|
fi
|
|
|
|
while IFS=$'\t' read -r name updated; do
|
|
[[ -z "$name" ]] && continue
|
|
DOC_NAMES+=("$name")
|
|
local bundle=0
|
|
if [[ "$DOCS" == "all" ]]; then
|
|
bundle=1
|
|
elif [[ "$DOCS" == "recent" ]]; then
|
|
if [[ -n "$updated" && "$updated" != "None" ]]; then
|
|
local cutoff now updated_epoch
|
|
now="$(date +%s)"
|
|
updated_epoch="$(date -d "$updated" +%s 2>/dev/null || echo "$now")"
|
|
cutoff=$(( now - 14*86400 ))
|
|
[[ "$updated_epoch" -ge "$cutoff" ]] && bundle=1
|
|
else
|
|
# no update dates available -> fall back to "all"
|
|
bundle=1
|
|
fi
|
|
fi
|
|
if [[ $bundle -eq 1 ]]; then
|
|
DOC_BUNDLED+=("$name")
|
|
if [[ -n "${MM_WORKTREE_TASK_FAKE_DOCS_DIR:-}" ]]; then
|
|
DOC_BODY["$name"]="$(cat "${MM_WORKTREE_TASK_FAKE_DOCS_DIR}/${name}" 2>/dev/null || echo "")"
|
|
else
|
|
DOC_BODY["$name"]="$(cp_cli planner items documents get "$UUID" "$name" 2>/dev/null || echo "")"
|
|
fi
|
|
fi
|
|
done < <(echo "$list_json" | python3 -c '
|
|
import json, sys
|
|
data = json.load(sys.stdin)
|
|
for d in data.get("documents", []):
|
|
name = d.get("name", "")
|
|
updated = d.get("updated_at", "")
|
|
print(name + "\t" + str(updated))
|
|
')
|
|
}
|
|
|
|
fetch_item
|
|
fetch_docs
|
|
|
|
PARENT_ID=""
|
|
PARENT_JSON=""
|
|
if [[ -n "$ITEM_JSON" ]]; then
|
|
PARENT_ID="$(echo "$ITEM_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("parent_id") or "")')"
|
|
fi
|
|
if [[ -n "$PARENT_ID" && -z "${MM_WORKTREE_TASK_FAKE_ITEM:-}" ]]; then
|
|
PARENT_JSON="$(cp_cli planner items get "$PARENT_ID" --json 2>/dev/null || echo "")"
|
|
fi
|
|
|
|
# --- build bundle content ---
|
|
build_bundle() {
|
|
echo "# Bundle for ${SLUG}"
|
|
echo
|
|
if [[ -n "$ITEM_JSON" ]]; then
|
|
echo "## Item"
|
|
echo '```json'
|
|
echo "$ITEM_JSON"
|
|
echo '```'
|
|
echo
|
|
fi
|
|
if [[ -n "$PARENT_JSON" ]]; then
|
|
echo "## Parent"
|
|
echo '```json'
|
|
echo "$PARENT_JSON"
|
|
echo '```'
|
|
echo
|
|
fi
|
|
echo "## Documents"
|
|
if [[ ${#DOC_NAMES[@]} -eq 0 ]]; then
|
|
echo "(none)"
|
|
else
|
|
for n in "${DOC_NAMES[@]}"; do
|
|
local marker="available only"
|
|
for b in "${DOC_BUNDLED[@]:-}"; do [[ "$b" == "$n" ]] && marker="bundled below"; done
|
|
echo "- ${n} (${marker})"
|
|
done
|
|
fi
|
|
echo
|
|
for n in "${DOC_BUNDLED[@]:-}"; do
|
|
[[ -z "$n" ]] && continue
|
|
echo "## Document: ${n}"
|
|
echo '```'
|
|
echo "${DOC_BODY[$n]:-}"
|
|
echo '```'
|
|
echo
|
|
done
|
|
}
|
|
|
|
BUNDLE_CONTENT="$(build_bundle)"
|
|
|
|
# --- build prompt ---
|
|
DOC_NAME_LIST="$(IFS=', '; echo "${DOC_NAMES[*]:-none}")"
|
|
|
|
build_prompt() {
|
|
cat <<EOF
|
|
You are running non-interactively (-p) as an implementation subagent inside a git worktree of the ${REPO_NAME} repo.
|
|
|
|
WORKTREE: ${WORKTREE} BRANCH: ${BRANCH}
|
|
Your current working directory IS the worktree. Never cd elsewhere. Never touch \`git config\`. Never \`git push\`. Never \`git stash\`.
|
|
|
|
Project rules that apply to you:
|
|
- Spec-driven: if you change behaviour, update the matching spec/*.md requirement in the same commit (add a new numbered requirement ID following the file's existing scheme; keep the rationale line).
|
|
- Python 3.12, type hints, Pydantic v2. Async tests use ONLY \`@pytest.mark.asyncio\` + \`async def\`.
|
|
- Do NOT run the full test suite. Run ONLY the test files you touched or created.
|
|
- Do not get stuck fixing test infrastructure. If a fixture is missing, mock it.
|
|
- Read the spec and existing tests around the code before editing. Match existing names exactly.
|
|
- Keep the diff minimal and focused on this task.
|
|
|
|
When done:
|
|
1. \`git add -A && git commit -m "<type>: <summary> (${UUID:-brief})"\` on branch ${BRANCH}. First verify: \`git rev-parse --abbrev-ref HEAD\` must print ${BRANCH}, otherwise STOP and report.
|
|
2. Write your report to: ${RESULT_FILE} with sections: Summary, Files changed, Tests run, Decisions, Open questions.
|
|
3. One-line stdout acknowledgement only.
|
|
|
|
## Task — ${UUID:-brief only}: ${TITLE}
|
|
|
|
Read the bundle at ${BUNDLE_FILE} fully before editing. The item body is the requirement; documents are supporting context. Documents available on the item (not all bundled): ${DOC_NAME_LIST}.
|
|
EOF
|
|
[[ -n "$FOCUS" ]] && echo -e "\nFocus: ${FOCUS}"
|
|
if [[ -n "$BRIEF" ]]; then
|
|
echo
|
|
echo "## Brief"
|
|
cat "$BRIEF"
|
|
fi
|
|
}
|
|
|
|
PROMPT_CONTENT="$(build_prompt)"
|
|
|
|
CMD="cd ${WORKTREE} && setsid nohup timeout ${TIMEOUT} ask-minimax-run --model \"MiniMax-M3[1m]\" --allowedTools \"${ALLOWLIST}\" --permission-mode acceptEdits --add-dir ${REPORT_DIR} -p \"\$(cat ${PROMPT_FILE})\" > ${STDOUT_FILE} 2>&1"
|
|
|
|
if [[ $DRYRUN -eq 1 ]]; then
|
|
echo "[dryrun] mm-worktree-task — no changes will be made"
|
|
echo " Repo: ${REPO_ROOT}"
|
|
echo " cp-cli from: ${CP_CLI_DIR:-<not found>}"
|
|
echo " cp-cli ctx: ${CP_CONTEXT:-<cp-cli default>}"
|
|
echo " Slug: ${SLUG}"
|
|
echo " Base: ${BASE}"
|
|
echo " UUID: ${UUID:-(none, --brief only)}"
|
|
echo " Worktree: ${WORKTREE}"
|
|
echo " Branch: ${BRANCH}"
|
|
echo " Report dir: ${REPORT_DIR}"
|
|
echo " Bundle path: ${BUNDLE_FILE}"
|
|
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"
|
|
echo
|
|
echo "--- Prompt ---"
|
|
echo "$PROMPT_CONTENT"
|
|
echo
|
|
echo "--- Command ---"
|
|
echo "$CMD"
|
|
exit 0
|
|
fi
|
|
|
|
# --- real run ---
|
|
mkdir -p "$REPORT_DIR"
|
|
echo "$BUNDLE_CONTENT" > "$BUNDLE_FILE"
|
|
echo "$PROMPT_CONTENT" > "$PROMPT_FILE"
|
|
|
|
if [[ -d "$WORKTREE" ]] || git -C "$REPO_ROOT" show-ref --verify --quiet "refs/heads/${BRANCH}"; then
|
|
err "worktree or branch already exists for slug '${SLUG}' (${WORKTREE} / ${BRANCH})"
|
|
exit 1
|
|
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"
|
|
start=$(date +%s)
|
|
set +e
|
|
setsid nohup timeout "$TIMEOUT" ask-minimax-run --model "MiniMax-M3[1m]" \
|
|
--allowedTools "$ALLOWLIST" --permission-mode acceptEdits \
|
|
--add-dir "$REPORT_DIR" -p "$(cat "$PROMPT_FILE")" \
|
|
> "$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
|
|
|
|
ok "launched"
|
|
echo " Report dir: ${REPORT_DIR}"
|
|
echo " Prompt file: ${PROMPT_FILE}"
|
|
echo " Stdout file: ${STDOUT_FILE}"
|
|
echo " Launch log: ${LAUNCH_LOG}"
|
|
echo " poll with: tail -f ${STDOUT_FILE}"
|