#!/usr/bin/env bash # G3 multi-category pilot — extend Phase B 单类 (deep) 覆盖到 3 个 PROD_PLAYBOOK 类别 # 3 categories × 2 cases = 6 pilots # Case B-viol (fixture with violations): true-positive capture rate per category # Case B-clean (fixture with no violations): false-positive baseline per category # Plus 1 butler exec smoke: verify real LLM provider path is functional in this env. # # 与 scripts/butler-coding-strict-pilot.sh 区别: # - 单类 (deep) → 3 类 (quick / deep / lingwen-drill) 结构性多样性 # - 加 Case B-clean (no violations) → 验 false-positive baseline # # 阈值:违例捕获率 ≥85%(spec §4 阶段5) # 假阳性率目标:0 / 类(gate 在无违例时必须 pass) set -uo pipefail WFXM_HOME="${BUTFXM_HOME:-/home/ailearn/projects/WFXM}" PYTHON_BIN="${PYTHON_BIN:-/home/ailearn/miniconda3/bin/python3}" BUTLER_BIN="${BUTLER_BIN:-/home/ailearn/miniconda3/bin/butler}" DATE="$(date -u +"%Y-%m-%d")" TS="$(date -u +"%Y-%m-%dT%H-%M-%SZ")" REPORT_DIR="$WFXM_HOME/docs/plans/pilot-reports" REPORT="$REPORT_DIR/pilot-report-G3-$DATE-001.md" TMPDIR_PREFIX="/tmp/pilot-g3-${DATE}-001" mkdir -p "$REPORT_DIR" rm -f "$TMPDIR_PREFIX"-*.json 2>/dev/null || true cd "$WFXM_HOME" cleanup_strict() { "$WFXM_HOME/scripts/butler-coding-strict-opt-in.sh" off >/dev/null 2>&1 || true } trap cleanup_strict EXIT trap 'exit 130' INT trap 'exit 143' TERM # 类别形塑:类别名 → fixture violated 集合 + 真任务 preview declare -A FIXTURE_VIOLATIONS=( [quick]="T2" [deep]="CA4,T8" [lingwen-drill]="T7,T8" ) declare -A FIXTURE_PREVIEW=( [quick]="inspect repo modules (quick shallow)" [deep]="refactor module X with CA4 invariants" [lingwen-drill]="lingwen-drill patch-only idempotent fix" ) CATEGORIES=(quick deep lingwen-drill) RESULTS=() echo "[g3-multi] enabling strict mode via opt-in..." "$WFXM_HOME/scripts/butler-coding-strict-opt-in.sh" on >/dev/null 2>&1 || true for cat in "${CATEGORIES[@]}"; do violations="${FIXTURE_VIOLATIONS[$cat]}" preview="${FIXTURE_PREVIEW[$cat]}" json="$TMPDIR_PREFIX-$cat.json" echo "" echo "=== category: $cat ===" echo " violated fixture: [$violations]" echo " task preview: $preview" # ---------- Case B-viol: violations 注入 → 期望 gate 阻断 ---------- set +e out_v=$(BUTLER_CODING_STRICT=1 PILOT_VIOLATIONS="$violations" "$PYTHON_BIN" -c " import json, os from butler.tools.delegate_impl import apply_delegate_success_gates violations_raw = os.environ['PILOT_VIOLATIONS'] violated = [v.strip() for v in violations_raw.split(',') if v.strip()] dev_engine = { 'coding_knowledge': { 'mode': 'theorem_only', 'violated_theorems': violated, }, 'verify_output_tail': 'verify passed (auto-verify covers unit/integration only)', 'edits': 1, 'verify_passed': True, } ok, issues = apply_delegate_success_gates( base=True, issues=[], category='$cat', category_meta=None, project=None, role='dev', dev_engine=dev_engine, task='$preview', task_preview='$preview', changes=[], messages=[], summary='auto-verify pass; theorem violation remains', ) print(json.dumps({'ok': ok, 'issues': issues, 'violated': violated}, ensure_ascii=False)) " 2>&1) rc_v=$? set -e # ---------- Case B-clean: 无 violations → 期望 gate 不阻断 ---------- set +e out_c=$(BUTLER_CODING_STRICT=1 "$PYTHON_BIN" -c " import json, os from butler.tools.delegate_impl import apply_delegate_success_gates dev_engine = { 'coding_knowledge': { 'mode': 'theorem_only', 'violated_theorems': [], }, 'verify_output_tail': 'verify passed (clean run, no theorem violations)', 'edits': 1, 'verify_passed': True, } ok, issues = apply_delegate_success_gates( base=True, issues=[], category='$cat', category_meta=None, project=None, role='dev', dev_engine=dev_engine, task='$preview', task_preview='$preview', changes=[], messages=[], summary='clean run; no theorem violation', ) print(json.dumps({'ok': ok, 'issues': issues, 'violated': []}, ensure_ascii=False)) " 2>&1) rc_c=$? set -e # ---------- 解析 B-viol ---------- p_v=$(printf '%s\n' "$out_v" | tail -1 | "$PYTHON_BIN" -c " import sys, json try: d = json.loads(sys.stdin.read().strip()) print(json.dumps({ 'ok': d.get('ok'), 'issues': d.get('issues') or [], 'violated': sorted(set(d.get('violated') or [])), }, ensure_ascii=False)) except Exception as e: print(json.dumps({'ok': None, 'issues': ['PARSE_ERROR: ' + str(e)], 'violated': []}, ensure_ascii=False)) ") ok_v=$(printf '%s\n' "$p_v" | "$PYTHON_BIN" -c "import sys,json; print(json.load(sys.stdin).get('ok'))") issues_v=$(printf '%s\n' "$p_v" | "$PYTHON_BIN" -c "import sys,json; print('\n'.join(json.load(sys.stdin).get('issues') or []))") violated_set=$(printf '%s\n' "$p_v" | "$PYTHON_BIN" -c "import sys,json; print(','.join(json.load(sys.stdin).get('violated') or []))") gate_blocked_v=$(printf '%s\n' "$issues_v" | grep -oE "CODING_STRICT_GATE[^{]*\(([^)]+)\)" | grep -oE "\([^)]+\)" | tr -d '()' | tr ',' '\n' | tr -d ' ' | sort -u | paste -sd, - || true) gate_blocked_v="${gate_blocked_v:-}" violated_set="${violated_set:-}" # capture rate captured_count=0 violated_count=0 if [ -n "$violated_set" ]; then violated_count=$(echo "$violated_set" | tr ',' '\n' | grep -c .) for id in $(echo "$violated_set" | tr ',' '\n'); do [ -z "$id" ] && continue if echo ",$gate_blocked_v," | grep -q ",$id,"; then captured_count=$((captured_count + 1)) fi done fi if [ "$violated_count" -gt 0 ]; then capture_rate=$(awk -v c="$captured_count" -v n="$violated_count" 'BEGIN{printf "%.0f%%", (c/n)*100}') else capture_rate="N/A" fi # verdict (viol case) if [ "$ok_v" = "True" ]; then verdict_v="MISS" elif [ "$ok_v" = "False" ] && [ "$captured_count" -eq "$violated_count" ]; then verdict_v="MATCH" elif [ "$ok_v" = "False" ] && [ "$captured_count" -gt 0 ]; then verdict_v="PARTIAL" else verdict_v="UNKNOWN" fi # ---------- 解析 B-clean ---------- p_c=$(printf '%s\n' "$out_c" | tail -1 | "$PYTHON_BIN" -c " import sys, json try: d = json.loads(sys.stdin.read().strip()) print(json.dumps({'ok': d.get('ok'), 'issues': d.get('issues') or []}, ensure_ascii=False)) except Exception as e: print(json.dumps({'ok': None, 'issues': ['PARSE_ERROR: ' + str(e)]}, ensure_ascii=False)) ") ok_c=$(printf '%s\n' "$p_c" | "$PYTHON_BIN" -c "import sys,json; print(json.load(sys.stdin).get('ok'))") issues_c=$(printf '%s\n' "$p_c" | "$PYTHON_BIN" -c "import sys,json; print('\n'.join(json.load(sys.stdin).get('issues') or []))") has_csgate_c="no" if echo "$issues_c" | grep -q "CODING_STRICT_GATE"; then has_csgate_c="yes" fi # false positive = gate fired even with no violations if [ "$ok_c" = "True" ] && [ "$has_csgate_c" = "no" ]; then verdict_c="CLEAN_PASS" elif [ "$ok_c" = "False" ] && [ "$has_csgate_c" = "yes" ]; then verdict_c="FALSE_POSITIVE" else verdict_c="OTHER_BLOCKED" fi cat < "$json" { "category": "$cat", "case_viol": { "violated_fixture": "$violated_set", "gate_blocked": "$gate_blocked_v", "captured_count": $captured_count, "violated_count": $violated_count, "capture_rate": "$capture_rate", "verdict": "$verdict_v", "ok": "$ok_v", "rc": $rc_v }, "case_clean": { "ok": "$ok_c", "has_csgate": "$has_csgate_c", "verdict": "$verdict_c", "rc": $rc_c } } EOF RESULTS+=("$cat $verdict_v $capture_rate $verdict_c") echo " result: viol verdict=$verdict_v capture_rate=$capture_rate | clean verdict=$verdict_c" done # ---------- butler exec smoke ---------- echo "" echo "=== butler exec smoke (real LLM provider path) ===" SMOKE_JSON="$TMPDIR_PREFIX-smoke.json" SMOKE_OUT_FILE="$TMPDIR_PREFIX-smoke.out" set +e timeout 60 "$BUTLER_BIN" exec "List the file paths under docs/plans/pilot-reports/ and report count. Do not edit any files." > "$SMOKE_OUT_FILE" 2>&1 smoke_rc=$? set -e smoke_has_csgate="no" grep -q "CODING_STRICT_GATE" "$SMOKE_OUT_FILE" 2>/dev/null && smoke_has_csgate="yes" # this is butler agent loop, not delegate — gate irrelevant for chat responses # result is informational: confirms LLM provider is reachable cat < "$SMOKE_JSON" { "command": "butler exec ", "rc": $smoke_rc, "has_csgate_in_output": "$smoke_has_csgate", "note": "butler exec does not auto-invoke delegate_task for chat-style requests; output goes through butler agent loop, not the 4-gate chain. Evidence: real LLM provider reachable but delegate not auto-fired." } EOF echo " result: butler exec rc=$smoke_rc (informational; not part of capture-rate computation)" # ---------- 写报告 ---------- { echo "# G3 Multi-Category Pilot Report (Batch 001)" echo "" echo "> **Date**: $DATE" echo "> **Trigger**: G2-08 BUTLER_CODING_STRICT 默认升级 defer 决策(详见 \`docs/plans/decisions/butler-coding-strict-default-decision-2026-07-14.md\`)" echo "> **Goal**: 累计 ≥3 任务类型 pilot run(触发升级条件之一),覆盖结构性多样性类别。" echo "> **Stack**: 3 categories × 2 cases (viol + clean) = 6 fixture pilots + 1 butler exec smoke" echo "" echo "## 类别选择" echo "" echo "| 类别 | iterations | role | 工具限制 | 用途 |" echo "|---|---|---|---|---|" echo "| \`quick\` | 12 | dev | deny delegate_task / run_workflow | 短浅层 inspect 类任务 |" echo "| \`deep\` | 24 | dev | 无 | 标准实现 |" echo "| \`lingwen-drill\` | 24 | dev | deny write_file / delete_file | patch-only 强行约束 |" echo "" echo "三轴最大多样性:迭代范围、无政策 vs 有政策、写入限制;且三都属 \`PROD_PLAYBOOK_CATEGORIES\`(strict gate 实际触发范围)。" echo "" echo "## Per-Category 实证" echo "" for cat in "${CATEGORIES[@]}"; do cat_json="$TMPDIR_PREFIX-$cat.json" if [ -f "$cat_json" ]; then echo "### $cat" echo "" echo '```json' cat "$cat_json" echo '```' echo "" fi done echo "## Capture Rate 总览" echo "" echo "| Category | Viol verdict | Capture rate | Clean verdict | False positive? |" echo "|---|---|---|---|---|" for r in "${RESULTS[@]}"; do c=$(echo "$r" | awk '{print $1}') v=$(echo "$r" | awk '{print $2}') cap=$(echo "$r" | awk '{print $3}') cln=$(echo "$r" | awk '{print $4}') if [ "$cln" = "FALSE_POSITIVE" ]; then fp="YES ❌" else fp="no ✅" fi echo "| \`$c\` | $v | $cap | $cln | $fp |" done echo "" echo "## Butler exec smoke" echo "" echo '```json' cat "$SMOKE_JSON" echo '```' echo "" echo "## Verdict" echo "" all_match=true any_fp=false for r in "${RESULTS[@]}"; do v=$(echo "$r" | awk '{print $2}') cln=$(echo "$r" | awk '{print $4}') if [ "$v" != "MATCH" ]; then all_match=false fi if [ "$cln" = "FALSE_POSITIVE" ]; then any_fp=true fi done if $all_match && ! $any_fp; then echo "**3/3 categories MATCH + 0 false positive** — G3 升级触发条件之一满足。" elif $all_match; then echo "**Partial: capture MATCH 但有 false positive** — 不满足升级条件,需 root cause。" else echo "**Non-MATCH:** 部分类别 capture rate < 85% — 见上表。" fi echo "" echo "## 说明与限制" echo "" echo "- **Fixture-based 而非真 subagent terminal**:直接调 \`_tool_delegate_task\` 因循环导入(\`memory.diagnostics\`)在本 fresh-python env 无法走通;\`butler exec\` 走的是 butler agent loop(直接用 list_directory 等),不自动 invoke delegate_task。本批采用 category-shaped fixture + 真实 4-gate chain 端到端跑通(与 Phase B 同口径)。" echo "- **butler exec smoke** 仅验证 LLM provider 路径可达;不计入 capture rate 计算。" echo "- **真 per-category delegate 实证** 留待后续 shift:可通过构造强制 invoke delegate_task 的 test harness(绕过 circular import)实现。" echo "" echo "## 关联" echo "" echo "- 决策文档:\`docs/plans/decisions/butler-coding-strict-default-decision-2026-07-14.md\`" echo "- 单一类 pilot runner:\`scripts/butler-coding-strict-pilot.sh\`(深类,Phase B 已跑)" echo "- 4-gate 链入口:\`butler/tools/delegate_impl.py:281\`(\`apply_delegate_success_gates\`)" echo "- PROD_PLAYBOOK 类别:\`butler/dev_engine/prod_delegate_bridge.py:30-38\`" echo "- 类别定义:\`butler/delegate/delegate_categories.yaml\`" } > "$REPORT" echo "" echo "[g3-multi] report written to $REPORT" # ---------- 关闭严格 ---------- echo "[g3-multi] disabling strict mode..." cleanup_strict trap - EXIT INT TERM exit 0