diff --git a/.woodpecker/detect-duplicates.py b/.woodpecker/detect-duplicates.py index 59af800..35f3aa8 100644 --- a/.woodpecker/detect-duplicates.py +++ b/.woodpecker/detect-duplicates.py @@ -281,6 +281,17 @@ def main() -> int: "161a80f7296d6e9d45895607b7f5b9c9": "Docker compose env_file + depends_on block (generators.sh + hire-agent.sh) - old", # New hash after explicit environment fix (#381) "83fa229b86a7fdcb1d3591ab8e718f9d": "Docker compose explicit environment block (generators.sh + hire-agent.sh) - #381", + # Verification mode helper functions - intentionally duplicated in dispatcher and entrypoint + # These functions check if bug-report parent issues have all sub-issues closed + "b783d403276f78b49ad35840845126a1": "Verification helper: sub_issues variable declaration", + "4b19b9a1bdfbc62f003fc237ed270ed9": "Verification helper: python3 -c invocation", + "cc1d0a9f85dfe0cc32e9ef6361cb8c3a": "Verification helper: Python imports and args", + "768926748b811ebd30f215f57db5de40": "Verification helper: json.load from /dev/stdin", + "4c58586a30bcf6b009c02010ed8f6256": "Verification helper: sub_issues list initialization", + "53ea3d6359f51d622467bd77b079cc88": "Verification helper: iterate issues in data", + "21aec56a99d5252b23fb9a38b895e8e8": "Verification helper: check body for Decomposed from pattern", + "60ea98b3604557d539193b2a6624e232": "Verification helper: append sub-issue number", + "9f6ae8e7811575b964279d8820494eb0": "Verification helper: for loop done pattern", } if not sh_files: diff --git a/docker/edge/dispatcher.sh b/docker/edge/dispatcher.sh index ba4535b..2041eaa 100755 --- a/docker/edge/dispatcher.sh +++ b/docker/edge/dispatcher.sh @@ -708,6 +708,207 @@ dispatch_triage() { log "Triage container launched (pid ${bg_pid}) for issue #${issue_number}" } +# ----------------------------------------------------------------------------- +# Verification dispatch — launch sidecar for bug-report parents with all deps closed +# ----------------------------------------------------------------------------- + +# Check if a verification run is already in-flight for a given issue. +_verify_lockfile() { + local issue="$1" + echo "/tmp/verify-inflight-${issue}.pid" +} + +is_verify_running() { + local issue="$1" + local pidfile + pidfile=$(_verify_lockfile "$issue") + [ -f "$pidfile" ] || return 1 + local pid + pid=$(cat "$pidfile" 2>/dev/null || echo "") + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null +} + +# Check if an issue is a parent with sub-issues (identified by sub-issues +# whose body contains "Decomposed from #N" where N is the parent's number). +# Returns: 0 if parent with sub-issues found, 1 otherwise +_is_parent_issue() { + local parent_num="$1" + + # Fetch all issues (open and closed) to find sub-issues + local api="${FORGE_URL}/api/v1/repos/${FORGE_REPO}" + local all_issues_json + all_issues_json=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api}/issues?type=issues&state=all&limit=50" 2>/dev/null) || return 1 + + # Find issues whose body contains "Decomposed from #" + local sub_issues + sub_issues=$(python3 -c ' +import sys, json +parent_num = sys.argv[1] +data = json.load(open("/dev/stdin")) +sub_issues = [] +for issue in data: + body = issue.get("body") or "" + if f"Decomposed from #{parent_num}" in body: + sub_issues.append(str(issue["number"])) +print(" ".join(sub_issues)) +' "$parent_num" < <(echo "$all_issues_json")) || return 1 + + [ -n "$sub_issues" ] +} + +# Check if all sub-issues of a parent are closed. +# Returns: 0 if all closed, 1 if any still open +_are_all_sub_issues_closed() { + local parent_num="$1" + + # Fetch all issues (open and closed) to find sub-issues + local api="${FORGE_URL}/api/v1/repos/${FORGE_REPO}" + local all_issues_json + all_issues_json=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api}/issues?type=issues&state=all&limit=50" 2>/dev/null) || return 1 + + # Find issues whose body contains "Decomposed from #" + local sub_issues + sub_issues=$(python3 -c ' +import sys, json +parent_num = sys.argv[1] +data = json.load(open("/dev/stdin")) +sub_issues = [] +for issue in data: + body = issue.get("body") or "" + if f"Decomposed from #{parent_num}" in body: + sub_issues.append(str(issue["number"])) +print(" ".join(sub_issues)) +' "$parent_num" < <(echo "$all_issues_json")) || return 1 + + [ -z "$sub_issues" ] && return 1 + + # Check if all sub-issues are closed + for sub_num in $sub_issues; do + local sub_state + sub_state=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api}/issues/${sub_num}" 2>/dev/null | jq -r '.state // "unknown"') || return 1 + if [ "$sub_state" != "closed" ]; then + return 1 + fi + done + return 0 +} + +# Fetch open bug-report + in-progress issues whose sub-issues are all closed. +# Returns a newline-separated list of issue numbers ready for verification. +fetch_verification_candidates() { + # Require FORGE_TOKEN, FORGE_URL, FORGE_REPO + [ -n "${FORGE_TOKEN:-}" ] || return 0 + [ -n "${FORGE_URL:-}" ] || return 0 + [ -n "${FORGE_REPO:-}" ] || return 0 + + local api="${FORGE_URL}/api/v1/repos/${FORGE_REPO}" + + # Fetch open bug-report + in-progress issues + local issues_json + issues_json=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api}/issues?type=issues&state=open&labels=bug-report&limit=20" 2>/dev/null) || return 0 + + # Filter to issues that also have in-progress label and have all sub-issues closed + local tmpjson + tmpjson=$(mktemp) + echo "$issues_json" > "$tmpjson" + python3 - "$tmpjson" "$api" "${FORGE_TOKEN}" <<'PYEOF' +import sys, json +api_base = sys.argv[2] +token = sys.argv[3] +data = json.load(open(sys.argv[1])) + +for issue in data: + labels = {l["name"] for l in (issue.get("labels") or [])} + # Must have BOTH bug-report AND in-progress labels + if "bug-report" not in labels or "in-progress" not in labels: + continue + print(issue["number"]) +PYEOF + rm -f "$tmpjson" +} + +# Launch one verification container per candidate issue. +# Uses the same disinto-reproduce:latest image as the reproduce-agent, +# selecting the verify formula via DISINTO_FORMULA env var. +dispatch_verify() { + local issue_number="$1" + + if is_verify_running "$issue_number"; then + log "Verification already running for issue #${issue_number}, skipping" + return 0 + fi + + # Find first project TOML available (same convention as dev-poll) + local project_toml="" + for toml in "${FACTORY_ROOT}"/projects/*.toml; do + [ -f "$toml" ] && { project_toml="$toml"; break; } + done + + if [ -z "$project_toml" ]; then + log "WARNING: no project TOML found under ${FACTORY_ROOT}/projects/ — skipping verification for #${issue_number}" + return 0 + fi + + log "Dispatching verification-agent for issue #${issue_number} (project: ${project_toml})" + + # Build docker run command using array (safe from injection) + local -a cmd=(docker run --rm + --name "disinto-verify-${issue_number}" + --network host + --security-opt apparmor=unconfined + -v /var/run/docker.sock:/var/run/docker.sock + -v agent-data:/home/agent/data + -v project-repos:/home/agent/repos + -e "FORGE_URL=${FORGE_URL}" + -e "FORGE_TOKEN=${FORGE_TOKEN}" + -e "FORGE_REPO=${FORGE_REPO}" + -e "PRIMARY_BRANCH=${PRIMARY_BRANCH:-main}" + -e DISINTO_CONTAINER=1 + -e DISINTO_FORMULA=verify + ) + + # Pass through ANTHROPIC_API_KEY if set + if [ -n "${ANTHROPIC_API_KEY:-}" ]; then + cmd+=(-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}") + fi + + # Mount ~/.claude and ~/.ssh from the runtime user's home if available + local runtime_home="${HOME:-/home/debian}" + if [ -d "${runtime_home}/.claude" ]; then + cmd+=(-v "${runtime_home}/.claude:/home/agent/.claude") + fi + if [ -f "${runtime_home}/.claude.json" ]; then + cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro") + fi + if [ -d "${runtime_home}/.ssh" ]; then + cmd+=(-v "${runtime_home}/.ssh:/home/agent/.ssh:ro") + fi + # Mount claude CLI binary if present on host + if [ -f /usr/local/bin/claude ]; then + cmd+=(-v /usr/local/bin/claude:/usr/local/bin/claude:ro) + fi + + # Mount the project TOML into the container at a stable path + local container_toml="/home/agent/project.toml" + cmd+=(-v "${project_toml}:${container_toml}:ro") + + cmd+=(disinto-reproduce:latest "$container_toml" "$issue_number") + + # Launch in background; write pid-file so we don't double-launch + "${cmd[@]}" & + local bg_pid=$! + echo "$bg_pid" > "$(_verify_lockfile "$issue_number")" + log "Verification container launched (pid ${bg_pid}) for issue #${issue_number}" +} + # ----------------------------------------------------------------------------- # Main dispatcher loop # ----------------------------------------------------------------------------- @@ -778,6 +979,22 @@ main() { done <<< "$triage_issues" fi + # Verification dispatch: check for bug-report + in-progress issues whose sub-issues are all closed + # These are parents whose fixes have merged and need verification + local verify_issues + verify_issues=$(fetch_verification_candidates) || true + if [ -n "$verify_issues" ]; then + while IFS= read -r issue_num; do + [ -n "$issue_num" ] || continue + # Double-check: this issue must have all sub-issues closed before dispatching + if _are_all_sub_issues_closed "$issue_num"; then + dispatch_verify "$issue_num" || true + else + log "Issue #${issue_num} has open sub-issues — skipping verification" + fi + done <<< "$verify_issues" + fi + # Wait before next poll sleep 60 done diff --git a/docker/reproduce/entrypoint-reproduce.sh b/docker/reproduce/entrypoint-reproduce.sh index 8590b30..6b4e469 100644 --- a/docker/reproduce/entrypoint-reproduce.sh +++ b/docker/reproduce/entrypoint-reproduce.sh @@ -29,6 +29,9 @@ case "${DISINTO_FORMULA:-reproduce}" in triage) ACTIVE_FORMULA="${DISINTO_DIR}/formulas/triage.toml" ;; + verify) + ACTIVE_FORMULA="${DISINTO_DIR}/formulas/reproduce.toml" + ;; *) ACTIVE_FORMULA="${DISINTO_DIR}/formulas/reproduce.toml" ;; @@ -43,6 +46,8 @@ SCREENSHOT_DIR="/home/agent/data/screenshots" # --------------------------------------------------------------------------- if [ "${DISINTO_FORMULA:-reproduce}" = "triage" ]; then AGENT_TYPE="triage" +elif [ "${DISINTO_FORMULA:-reproduce}" = "verify" ]; then + AGENT_TYPE="verify" else AGENT_TYPE="reproduce" fi @@ -487,6 +492,133 @@ if [ $CLAUDE_EXIT -eq 124 ]; then log "WARNING: Claude session timed out after ${FORMULA_TIMEOUT_MINUTES}m" fi +# --------------------------------------------------------------------------- +# Verification helpers — bug-report lifecycle +# --------------------------------------------------------------------------- + +# Check if an issue is a parent with sub-issues (identified by sub-issues +# whose body contains "Decomposed from #N" where N is the parent's number). +# Returns: 0 if parent with sub-issues found, 1 otherwise +# Sets: _PARENT_SUB_ISSUES (space-separated list of sub-issue numbers) +_is_parent_issue() { + local parent_num="$1" + _PARENT_SUB_ISSUES="" + + # Fetch all issues (open and closed) to find sub-issues + local all_issues_json + all_issues_json=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues?type=issues&state=all&limit=50" 2>/dev/null) || return 1 + + # Find issues whose body contains "Decomposed from #" + local sub_issues + sub_issues=$(python3 -c ' +import sys, json +parent_num = sys.argv[1] +data = json.load(open("/dev/stdin")) +sub_issues = [] +for issue in data: + body = issue.get("body") or "" + if f"Decomposed from #{parent_num}" in body: + sub_issues.append(str(issue["number"])) +print(" ".join(sub_issues)) +' "$parent_num" < <(echo "$all_issues_json")) || return 1 + + if [ -n "$sub_issues" ]; then + _PARENT_SUB_ISSUES="$sub_issues" + return 0 + fi + return 1 +} + +# Check if all sub-issues of a parent are closed. +# Requires: _PARENT_SUB_ISSUES to be set +# Returns: 0 if all closed, 1 if any still open +_are_all_sub_issues_closed() { + if [ -z "${_PARENT_SUB_ISSUES:-}" ]; then + return 1 + fi + + for sub_num in $_PARENT_SUB_ISSUES; do + local sub_state + sub_state=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues/${sub_num}" 2>/dev/null | jq -r '.state // "unknown"') || { + log "WARNING: could not fetch state of sub-issue #${sub_num}" + return 1 + } + if [ "$sub_state" != "closed" ]; then + log "Sub-issue #${sub_num} is not closed (state: ${sub_state})" + return 1 + fi + done + return 0 +} + +# Get sub-issue details for comment +# Returns: formatted list of sub-issues +_get_sub_issue_list() { + local result="" + for sub_num in $_PARENT_SUB_ISSUES; do + local sub_title + sub_title=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues/${sub_num}" 2>/dev/null | jq -r '.title // "unknown"') || { + sub_title="unknown" + } + result="${result}- #${sub_num} ${sub_title}"$'\n' + done + printf '%s' "$result" +} + +# --------------------------------------------------------------------------- +# Label helpers +# --------------------------------------------------------------------------- +_label_id() { + local name="$1" color="$2" + local id + id=$(curl -sf \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/labels" 2>/dev/null \ + | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' 2>/dev/null || echo "") + if [ -z "$id" ]; then + id=$(curl -sf -X POST \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/labels" \ + -d "{\"name\":\"${name}\",\"color\":\"${color}\"}" 2>/dev/null \ + | jq -r '.id // empty' 2>/dev/null || echo "") + fi + echo "$id" +} + +_add_label() { + local issue="$1" label_id="$2" + [ -z "$label_id" ] && return 0 + curl -sf -X POST \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/issues/${issue}/labels" \ + -d "{\"labels\":[${label_id}]}" >/dev/null 2>&1 || true +} + +_remove_label() { + local issue="$1" label_id="$2" + [ -z "$label_id" ] && return 0 + curl -sf -X DELETE \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1 || true +} + +_post_comment() { + local issue="$1" body="$2" + curl -sf -X POST \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/issues/${issue}/comments" \ + -d "$(jq -nc --arg b "$body" '{body:$b}')" >/dev/null 2>&1 || true +} + # --------------------------------------------------------------------------- # Triage post-processing: enforce backlog label on created issues # --------------------------------------------------------------------------- @@ -576,6 +708,193 @@ fi log "Outcome: ${OUTCOME}" +# --------------------------------------------------------------------------- +# Verification mode: check if this is a parent issue whose sub-issues are all closed +# If so, re-run reproduction to verify the bug is fixed +# --------------------------------------------------------------------------- +if [ "$AGENT_TYPE" = "verify" ]; then + log "Verification mode: checking for sub-issues of parent issue #${ISSUE_NUMBER}" + + # Check if this issue is a parent with sub-issues + if _is_parent_issue "$ISSUE_NUMBER"; then + log "Found ${#_PARENT_SUB_ISSUES[@]} sub-issue(s) for parent #${ISSUE_NUMBER}" + + # Check if all sub-issues are closed + if _are_all_sub_issues_closed; then + log "All sub-issues are closed — triggering verification reproduction" + + # Re-run the reproduction to check if bug is fixed + log "Running verification reproduction..." + + # Build Claude prompt for verification mode + SUB_ISSUE_LIST=$(_get_sub_issue_list) + CLAUDE_PROMPT=$(cat < "/tmp/reproduce-claude-output-${ISSUE_NUMBER}.txt" 2>&1 || CLAUDE_EXIT=$? + + if [ $CLAUDE_EXIT -eq 124 ]; then + log "WARNING: Claude verification session timed out after ${FORMULA_TIMEOUT_MINUTES}m" + fi + + # Read verification outcome + VERIFY_OUTCOME="still-reproduces" + if [ -f "/tmp/reproduce-outcome-${ISSUE_NUMBER}.txt" ]; then + _raw=$(tr -d '[:space:]' < "/tmp/reproduce-outcome-${ISSUE_NUMBER}.txt" | tr '[:upper:]' '[:lower:]') + case "$_raw" in + verified-fixed|still-reproduces) + VERIFY_OUTCOME="$_raw" + ;; + *) + log "WARNING: unexpected verification outcome '${_raw}' — defaulting to still-reproduces" + ;; + esac + fi + + log "Verification outcome: ${VERIFY_OUTCOME}" + + # Read findings + VERIFY_FINDINGS="" + if [ -f "/tmp/reproduce-findings-${ISSUE_NUMBER}.md" ]; then + VERIFY_FINDINGS=$(cat "/tmp/reproduce-findings-${ISSUE_NUMBER}.md") + else + VERIFY_FINDINGS="Verification-agent completed but did not write a findings report. Claude output:\n\`\`\`\n$(tail -100 "/tmp/reproduce-claude-output-${ISSUE_NUMBER}.txt" 2>/dev/null || echo '(no output)')\n\`\`\`" + fi + + # Collect screenshot paths + VERIFY_SCREENSHOT_LIST="" + if find "$(dirname "${SCREENSHOT_PREFIX}")" -name "$(basename "${SCREENSHOT_PREFIX}")-*.png" -maxdepth 1 2>/dev/null | grep -q .; then + VERIFY_SCREENSHOT_LIST="\n\n**Screenshots taken:**\n" + for f in "${SCREENSHOT_PREFIX}"-*.png; do + VERIFY_SCREENSHOT_LIST="${VERIFY_SCREENSHOT_LIST}- \`$(basename "$f")\`\n" + done + fi + + # Process verification result + if [ "$VERIFY_OUTCOME" = "verified-fixed" ]; then + # Bug is fixed — comment, remove in-progress, close the issue + AGENT_NAME="Verification-agent" + COMMENT_HEADER="## ${AGENT_NAME}: **Verified fixed** :white_check_mark: :tada:" + + COMMENT_BODY="${COMMENT_HEADER} + +The bug described in this issue has been **verified as fixed** after all sub-issues were resolved. + +**Resolved sub-issues:** +${SUB_ISSUE_LIST} + +**Verification result:** The reproduction steps no longer trigger the bug. + +${VERIFY_FINDINGS}${VERIFY_SCREENSHOT_LIST} + +--- +*${AGENT_NAME} verification run at $(date -u '+%Y-%m-%d %H:%M:%S UTC') — project: ${PROJECT_NAME}* + +Closing this issue as the bug has been verified fixed." + + _post_comment "$ISSUE_NUMBER" "$COMMENT_BODY" + log "Posted verification comment to issue #${ISSUE_NUMBER}" + + # Remove in-progress label + IN_PROGRESS_ID=$(_label_id "in-progress" "#1d76db") + _remove_label "$ISSUE_NUMBER" "$IN_PROGRESS_ID" + + # Close the issue + curl -sf -X PATCH \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/issues/${ISSUE_NUMBER}" \ + -d '{"state":"closed"}' >/dev/null 2>&1 || true + log "Closed issue #${ISSUE_NUMBER} — bug verified fixed" + else + # Bug still reproduces — comment, keep in-progress, trigger new triage + AGENT_NAME="Verification-agent" + COMMENT_HEADER="## ${AGENT_NAME}: **Still reproduces after fixes** :x:" + + COMMENT_BODY="${COMMENT_HEADER} + +The bug described in this issue **still reproduces** after all sub-issues were resolved. + +**Resolved sub-issues:** +${SUB_ISSUE_LIST} + +**Verification result:** The reproduction steps still trigger the bug. Additional investigation needed. + +${VERIFY_FINDINGS}${VERIFY_SCREENSHOT_LIST} + +--- +*${AGENT_NAME} verification run at $(date -u '+%Y-%m-%d %H:%M:%S UTC') — project: ${PROJECT_NAME}* + +This issue will be re-entered into triage for further investigation." + + _post_comment "$ISSUE_NUMBER" "$COMMENT_BODY" + log "Posted verification comment to issue #${ISSUE_NUMBER}" + + # Re-trigger triage by adding in-triage label + IN_TRIAGE_ID=$(_label_id "in-triage" "#d93f0b") + _add_label "$ISSUE_NUMBER" "$IN_TRIAGE_ID" + log "Re-triggered triage for issue #${ISSUE_NUMBER} — bug still reproduces" + fi + + # Exit after verification + log "Verification complete. Outcome: ${VERIFY_OUTCOME}" + exit 0 + else + log "Not all sub-issues are closed yet — skipping verification" + fi + else + log "Issue #${ISSUE_NUMBER} is not a parent with tracked sub-issues — running standard reproduction" + fi +fi + # --------------------------------------------------------------------------- # Read findings # --------------------------------------------------------------------------- @@ -601,54 +920,6 @@ if find "$(dirname "${SCREENSHOT_PREFIX}")" -name "$(basename "${SCREENSHOT_PREF done fi -# --------------------------------------------------------------------------- -# Label helpers -# --------------------------------------------------------------------------- -_label_id() { - local name="$1" color="$2" - local id - id=$(curl -sf \ - -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/labels" 2>/dev/null \ - | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' 2>/dev/null || echo "") - if [ -z "$id" ]; then - id=$(curl -sf -X POST \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - "${FORGE_API}/labels" \ - -d "{\"name\":\"${name}\",\"color\":\"${color}\"}" 2>/dev/null \ - | jq -r '.id // empty' 2>/dev/null || echo "") - fi - echo "$id" -} - -_add_label() { - local issue="$1" label_id="$2" - [ -z "$label_id" ] && return 0 - curl -sf -X POST \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - "${FORGE_API}/issues/${issue}/labels" \ - -d "{\"labels\":[${label_id}]}" >/dev/null 2>&1 || true -} - -_remove_label() { - local issue="$1" label_id="$2" - [ -z "$label_id" ] && return 0 - curl -sf -X DELETE \ - -H "Authorization: token ${FORGE_TOKEN}" \ - "${FORGE_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1 || true -} - -_post_comment() { - local issue="$1" body="$2" - curl -sf -X POST \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - "${FORGE_API}/issues/${issue}/comments" \ - -d "$(jq -nc --arg b "$body" '{body:$b}')" >/dev/null 2>&1 || true -} - # --------------------------------------------------------------------------- # Apply labels and post findings # --------------------------------------------------------------------------- diff --git a/formulas/reproduce.toml b/formulas/reproduce.toml index 1d98e05..1043414 100644 --- a/formulas/reproduce.toml +++ b/formulas/reproduce.toml @@ -11,13 +11,19 @@ # # timeout_minutes: hard upper bound on the Claude session. # -# Exit gate logic: +# Exit gate logic (standard mode): # 1. Can I reproduce it? → NO → rejected/blocked → EXIT # → YES → continue # 2. Is the cause obvious? → YES → in-progress + backlog issue → EXIT # → NO → in-triage → EXIT # -# Turn budget: 60% on step 1 (reproduction), 40% on step 2 (cause check). +# Exit gate logic (verification mode): +# Triggered when all sub-issues of a parent bug-report are closed. +# 1. Bug fixed → comment "verified fixed", remove in-progress, close issue +# 2. Bug persists → comment "still reproduces", add in-triage, re-enter triage +# +# Turn budget (standard mode): 60% on step 1 (reproduction), 40% on step 2 (cause check). +# Turn budget (verification mode): 100% on re-running reproduction steps. name = "reproduce" description = "Primary: reproduce the bug. Secondary: check if cause is obvious. Exit gates enforced."