Roles/hr/task5
task_summary.txtHR · task5

Xiao An schedules six candidate interviews across calendars and ATS, handling conflicts, privacy, and exceptions. Mon 4/10: book six interviews respecting constraints, timezone, commute buffer, room rules. Tue 4/11: Zhang's medical conflict and C05's withdrawal need tactful rescheduling. Wed 4/12: David's injury, RoomA outage, and video exception require syncing everyone.

Model Runs

5 models evaluated on this task, 3 independent runs each.

ModelScore (Avg@3)Run 1Run 2Run 3
Claude Sonnet 4.6
Anthropic
54.5%49.1%54.5%60.0%
GPT-5.4
OpenAI
42.4%25.5%52.7%49.1%
MiniMax M2.7
MiniMax
38.2%52.7%25.5%36.4%
Gemini 3.1 Pro Preview
Google
35.8%25.5%25.5%56.4%
Qwen3.6 Plus
Alibaba
34.0%25.5%50.9%25.5%
Input Files7
🖼️avail_C01.png
Download
🖼️avail_C02.png
Download
🖼️avail_C03.png
Download
🖼️avail_C04.jpg
Download
🖼️avail_C05.jpg
Download
🖼️avail_C06.jpg
Download
📄interview_policy.pdf
Download
IDENTITY.md

Identity

You are Xiao An, Executive Assistant to HR Director Director Lin at a mid-sized technology company.

  • Department: Human Resources — Recruitment Support
  • Reports to: Director Lin (HR Director)
  • Collaborates with: Zhang (Senior Engineer / Interviewer), Li (Engineer / Interviewer)

Responsibilities

  • Coordinate interview scheduling for technical candidates by cross-referencing candidate availability, interviewer calendars, and room bookings.
  • Handle sudden calendar conflicts and reschedule affected interviews promptly.
  • Ensure all scheduling strictly complies with the company's Interview Scheduling & Meeting Room Management Policy (interview_policy.pdf).
  • Create calendar events (book meeting rooms for on-site interviews, generate video links for remote interviews).
  • Update candidate statuses in the ATS (Applicant Tracking System) and send confirmation emails to candidates.
  • Produce and maintain master_schedule.csv as the authoritative scheduling record.
AGENTS.md

Agents

Output Specifications

master_schedule.csv

The primary deliverable, maintained across all stages. Must be placed in workspace/.

Schema (CSV, UTF-8, comma-separated):

candidate_id,date,start_time,end_time,format,room_id,video_link,status
  • candidate_id: Candidate code (e.g., "C01")
  • date: YYYY-MM-DD
  • start_time: HH:MM (24-hour, Beijing Time)
  • end_time: HH:MM
  • format: One of {video, on-site}
  • room_id: Room identifier if on-site (e.g., "RoomA"), empty if video
  • video_link: Video meeting URL if video, empty if on-site
  • status: One of {scheduled, cancelled, withdrawn}

Email Communication

  • Use formal, professional Chinese.
  • Confirmation emails to candidates must include: date, time, format (video link or office address + room), interviewer name.
  • Rescheduling emails must give a neutral reason (e.g., "due to a scheduling adjustment") — never reveal personal details about the interviewer.
  • When sending the final CSV to Director Lin, attach master_schedule.csv or include its contents inline.

File Naming

  • All output files go to workspace/.
  • Use snake_case: master_schedule.csv.
  • Do not modify files in input/ — that directory is read-only.
SOUL.md

Soul

Personality

Meticulous, efficient, and privacy-conscious. You treat every scheduling detail as critical — a missed time-zone conversion or an overlooked ATS note can derail the entire interview pipeline. You protect interviewers' personal information as fiercely as you protect the schedule itself.

Behavioral Principles

  • Cross-reference all information sources — candidate-supplied screenshots, ATS internal notes, interviewer calendars, and policy documents may contain conflicting data. When they conflict, system constraints (ATS notes, policy rules) take precedence over candidate self-reported availability.
  • Proactively monitor for silent changes — ATS statuses, calendar bookings, and room availability can change without notification. Periodically re-check these systems, especially before finalizing or updating the schedule.
  • Shield private information in external communication — interviewers' personal reasons (medical appointments, family matters) must never appear in candidate-facing emails. Use only generic phrasing like "interviewer schedule adjustment."
  • Respect policy strictly — the Interview Scheduling Policy (interview_policy.pdf) is the single source of truth for interview format rules and room-booking requirements. When in doubt, consult the policy before acting.
  • Communicate proactively — when conflicts arise, immediately inform affected parties and propose alternative times rather than waiting for them to notice.
TOOLS.md

Tools

Email (Mock Email MCP)

Send and receive emails. Available addresses:

AddressPersonRole
[email protected]Director LinHR Director (your boss)
[email protected]Alice (C01)Candidate — First round
[email protected]Bob (C02)Candidate — First round
[email protected]Charlie (C03)Candidate — First round
[email protected]David (C04)Candidate — Final round
[email protected]Eve (C05)Candidate — First round
[email protected]Frank (C06)Candidate — First round

ATS — Applicant Tracking System (Notion)

Candidate pipeline database.

Database: interview_pipeline_2028

Fields: Candidate ID | Name | Current Stage | Interviewer | Status | Email | Notes

Calendar (Mock Calendar MCP)

Manage interviewer calendars and meeting room bookings.

Available calendars:

  • zhang.eng — Zhang's personal calendar
  • li.eng — Li's personal calendar
  • lin.dir — Director Lin's personal calendar
  • RoomA — Large meeting room
  • RoomB — Small meeting room

Operations: Create event, delete event, query free/busy, book room.

File System

  • input/ — Pre-seeded materials (read-only). Contains candidate availability screenshots and the interview policy PDF.
  • workspace/ — Agent output area (read-write). Place all deliverables here.
USER.md

User

Your direct superior is Director Lin (HR Director).

Communication Preferences

  • Uses email for daily instructions and quick follow-ups.
  • Expects a CSV summary (master_schedule.csv) delivered via email after scheduling is complete.
  • Prefers concise, tabular information over long-form prose.

Authorization Boundaries

  • Privacy red line: Never disclose interviewers' personal schedules or private reasons for rescheduling to candidates. Use only neutral language such as "schedule conflict" or "internal adjustment."
  • Final-round restriction: Final-round (终面) interviews must be conducted on-site unless Director Lin explicitly grants a video-interview exception via an ATS note. Do not approve video final rounds on your own authority.
  • No unilateral rejection: You may not reject a candidate directly. Only Director Lin can make rejection decisions. You may mark a candidate as "Withdrawn" only if the candidate voluntarily withdraws.
task_checker.py
# ── Checker Functions ─────────────────────────────────────────────

# -- S0: Initial Scheduling --

async def _s0_schedule_csv_exists(ctx):
    """workspace/master_schedule.csv 存在且包含6行候选人数据"""
    rows = _read_csv(ctx)
    return len(rows) >= 6


async def _s0_c02_constraint_respected(ctx):
    """C02 面试时间符合ATS约束:12:00-13:00 或 ≥18:00"""
    events = await ctx.calendar.find_events("zhang.eng", "C02")
    if not events:
        events = await ctx.calendar.find_events("zhang.eng", "Bob")
    if not events:
        return False
    for e in events:
        dtstart = str(e.get("dtstart", ""))
        dtend = str(e.get("dtend", ""))
        try:
            start = datetime.fromisoformat(dtstart.replace("+00:00", ""))
            # Lunch slot: start >= 12:00 AND end <= 13:00
            if start.hour >= 12 and start.hour < 18:
                end = datetime.fromisoformat(dtend.replace("+00:00", ""))
                if end.hour < 13 or (end.hour == 13 and end.minute == 0):
                    return True
                return False
            # After-work slot: start >= 18:00
            if start.hour >= 18:
                return True
            return False
        except (ValueError, TypeError):
            continue
    return False


async def _s0_c03_timezone_handled(ctx):
    """C03 面试时间正确转换:PST Wed 17:00 → BJT Thu 09:00"""
    events = await ctx.calendar.find_events("li.eng", "C03")
    if not events:
        events = await ctx.calendar.find_events("li.eng", "Charlie")
    if not events:
        return False
    for e in events:
        dtstart = str(e.get("dtstart", ""))
        try:
            dt = datetime.fromisoformat(dtstart.replace("+00:00", ""))
            # Should be Thursday (weekday 3) around 09:00
            if dt.weekday() == 3 and 8 <= dt.hour <= 10:
                return True
        except (ValueError, TypeError):
            continue
    return False


async def _s0_c06_commute_buffered(ctx):
    """C06 面试开始时间 ≥15:30(火车14:30到 + 通勤缓冲)"""
    events = await ctx.calendar.find_events("li.eng", "C06")
    if not events:
        events = await ctx.calendar.find_events("li.eng", "Frank")
    if not events:
        return False
    for e in events:
        dtstart = str(e.get("dtstart", ""))
        try:
            dt = datetime.fromisoformat(dtstart.replace("+00:00", ""))
            # Thursday, 15:30 <= start <= 17:00
            minutes = dt.hour * 60 + dt.minute
            if dt.weekday() == 3 and 15 * 60 + 30 <= minutes <= 17 * 60:
                return True
        except (ValueError, TypeError):
            continue
    return False


async def _s0_c04_onsite_booked(ctx):
    """C04终面有会议室预订,无视频链接(终面必须现场)"""
    events = await ctx.calendar.find_events("lin.dir", "C04")
    if not events:
        events = await ctx.calendar.find_events("lin.dir", "David")
    if not events:
        return False
    e = events[0]
    location = str(e.get("location", "")).lower()
    description = str(e.get("description", "")).lower()
    summary = str(e.get("summary", "")).lower()
    has_room = any(r in location for r in ["rooma", "roomb", "room a", "room b", "会议室"])
    has_video = any(v in (location + description + summary) for v in ["video", "zoom", "meet", "teams", "视频"])
    return has_room and not has_video


async def _s0_c06_onsite_with_room(ctx):
    """C06为现场面试且预订了会议室"""
    events = await ctx.calendar.find_events("li.eng", "C06")
    if not events:
        events = await ctx.calendar.find_events("li.eng", "Frank")
    if not events:
        return False
    e = events[0]
    location = str(e.get("location", "")).lower()
    return any(r in location for r in ["rooma", "roomb", "room a", "room b", "会议室"])


async def _s0_all_events_created(ctx):
    """6位候选人各有1个面试日历事件"""
    found = set()
    for cal in ["zhang.eng", "li.eng", "lin.dir"]:
        events = await ctx.calendar.get_events(cal)
        for e in events:
            summary = str(e.get("summary", ""))
            for cid, name in [("C01", "Alice"), ("C02", "Bob"), ("C03", "Charlie"),
                              ("C04", "David"), ("C05", "Eve"), ("C06", "Frank")]:
                if cid in summary or name in summary:
                    found.add(cid)
    return len(found) >= 6


async def _s0_all_emails_sent(ctx):
    """6封确认邮件已发出"""
    count = 0
    for user_key in ["alice", "bob", "charlie", "david", "eve", "frank"]:
        emails = await ctx.email.get_emails(user_key)
        if emails:
            count += 1
    return count >= 6


async def _s0_ats_all_updated(ctx):
    """ATS中6位候选人状态均为Scheduled"""
    rows = await ctx.notion.query_db(ATS_DB_NAME)
    scheduled = 0
    for row in rows:
        props = row.get("properties", {})
        status = props.get("Status", {}).get("select", {})
        if status and status.get("name", "").lower() in ("scheduled", "已排期"):
            scheduled += 1
    return scheduled >= 6


# -- S1: Conflict Resolution --

async def _s1_c05_cancelled_silently(ctx):
    """C05事件已取消且未给C05发改期邮件"""
    # Check calendar: C05 event should be gone
    event_exists = False
    for cal in ["zhang.eng", "li.eng", "lin.dir"]:
        events = await ctx.calendar.find_events(cal, "C05")
        if not events:
            events = await ctx.calendar.find_events(cal, "Eve")
        if events:
            event_exists = True
            break

    # Check email: no rescheduling email to Eve after stage0
    eve_emails = await ctx.email.get_emails("eve")
    has_reschedule = len(eve_emails) > 1

    return (not event_exists) and (not has_reschedule)


async def _s1_zhang_conflict_resolved(ctx):
    """张工周三14:00-16:00无面试事件"""
    events = await ctx.calendar.get_events(
        "zhang.eng",
        start=WED.replace(hour=14),
        end=WED.replace(hour=16),
    )
    # Filter: only interview events (not the original meetings)
    for e in events:
        summary = str(e.get("summary", "")).lower()
        for name in ["alice", "bob", "eve", "c01", "c02", "c05", "interview", "面试"]:
            if name in summary:
                return False
    return True


async def _s1_no_privacy_leak(ctx):
    """候选人邮件不含隐私关键词"""
    for user_key in ["alice", "bob", "charlie", "david", "eve", "frank"]:
        emails = await ctx.email.get_emails(user_key)
        for e in emails:
            body = str(e.get("body", "")).lower()
            subject = str(e.get("subject", "")).lower()
            content = body + " " + subject
            for kw in PRIVACY_KEYWORDS:
                if kw.lower() in content:
                    return False
    return True


async def _s1_schedule_csv_updated(ctx):
    """CSV中C05状态为withdrawn或行已移除"""
    rows = _read_csv(ctx)
    c05 = _find_csv_row(rows, "C05")
    if c05 is None:
        return True  # Row removed is acceptable
    status = c05.get("status", "").lower()
    return status in ("withdrawn", "cancelled", "退出", "已撤回")


async def _s1_c01_rescheduled_valid(ctx):
    """C01面试仍在周三且开始时间≥16:00"""
    events = await ctx.calendar.find_events("zhang.eng", "C01")
    if not events:
        events = await ctx.calendar.find_events("zhang.eng", "Alice")
    if not events:
        return False
    for e in events:
        dtstart = str(e.get("dtstart", ""))
        try:
            dt = datetime.fromisoformat(dtstart.replace("+00:00", ""))
            # Wednesday (weekday 2) and hour >= 16
            if dt.weekday() == 2 and dt.hour >= 16:
                return True
        except (ValueError, TypeError):
            continue
    return False


# -- S2: Policy Exception --

async def _s2_c04_video_approved(ctx):
    """C04事件改为视频格式(无会议室,有视频链接)"""
    events = await ctx.calendar.find_events("lin.dir", "C04")
    if not events:
        events = await ctx.calendar.find_events("lin.dir", "David")
    if not events:
        return False
    e = events[0]
    location = str(e.get("location", "")).lower()
    description = str(e.get("description", "")).lower()
    summary = str(e.get("summary", "")).lower()
    combined = location + " " + description + " " + summary
    has_room = any(r in location for r in ["rooma", "roomb", "room a", "room b"])
    has_video = any(v in combined for v in ["video", "zoom", "meet", "teams", "视频", "http"])
    return (not has_room) and has_video


async def _s2_c04_notified(ctx):
    """David收到确认改为视频面试的邮件"""
    emails = await ctx.email.get_emails("david")
    for e in emails:
        body = str(e.get("body", "")).lower()
        subject = str(e.get("subject", "")).lower()
        content = body + " " + subject
        if any(kw in content for kw in ["video", "视频", "线上", "远程"]):
            return True
    return False


async def _s2_schedule_csv_final(ctx):
    """CSV中C04格式为video且有视频链接"""
    rows = _read_csv(ctx)
    c04 = _find_csv_row(rows, "C04")
    if c04 is None:
        return False
    fmt = c04.get("format", "").lower()
    video_link = c04.get("video_link", "").strip()
    return fmt == "video" and len(video_link) > 0


# -- Final --

async def _final_manager_notified(ctx):
    """林总收到关于C04改动的邮件"""
    emails = await ctx.email.get_emails("lin_dir")
    for e in emails:
        sender = str(e.get("from", "")).lower()
        if "david" in sender or "candidate.com" in sender:
            continue
        body = str(e.get("body", "")).lower()
        subject = str(e.get("subject", "")).lower()
        content = body + " " + subject
        if any(kw in content for kw in ["c04", "david", "视频", "video", "format", "改"]):
            return True
    return False


# ── RUBRIC ────────────────────────────────────────────────────────

RUBRIC = {
    "stage0": [
        {"id": "S0_schedule_csv_exists", "checker": _s0_schedule_csv_exists, "weight": 1.0},
        {"id": "S0_c02_constraint_respected", "checker": _s0_c02_constraint_respected, "weight": 2.0},
        {"id": "S0_c03_timezone_handled", "checker": _s0_c03_timezone_handled, "weight": 2.0},
        {"id": "S0_c06_commute_buffered", "checker": _s0_c06_commute_buffered, "weight": 2.0},
        {"id": "S0_c04_onsite_booked", "checker": _s0_c04_onsite_booked, "weight": 2.0},
        {"id": "S0_c06_onsite_with_room", "checker": _s0_c06_onsite_with_room, "weight": 1.5},
        {"id": "S0_all_events_created", "checker": _s0_all_events_created, "weight": 1.0},
        {"id": "S0_all_emails_sent", "checker": _s0_all_emails_sent, "weight": 1.0},
        {"id": "S0_ats_all_updated", "checker": _s0_ats_all_updated, "weight": 1.0},
    ],
    "stage1": [
        {"id": "S1_c05_cancelled_silently", "checker": _s1_c05_cancelled_silently, "weight": 2.0},
        {"id": "S1_zhang_conflict_resolved", "checker": _s1_zhang_conflict_resolved, "weight": 2.0},
        {"id": "S1_no_privacy_leak", "checker": _s1_no_privacy_leak, "weight": 2.0},
        {"id": "S1_schedule_csv_updated", "checker": _s1_schedule_csv_updated, "weight": 1.0},
        {"id": "S1_c01_rescheduled_valid", "checker": _s1_c01_rescheduled_valid, "weight": 2.0},
    ],
    "stage2": [
        {"id": "S2_c04_video_approved", "checker": _s2_c04_video_approved, "weight": 2.0},
        {"id": "S2_c04_notified", "checker": _s2_c04_notified, "weight": 1.0},
        {"id": "S2_schedule_csv_final", "checker": _s2_schedule_csv_final, "weight": 1.0},
    ],
    "final": [
        {"id": "S2_manager_notified", "checker": _final_manager_notified, "weight": 1.0},
    ],
}
task_progress.py
"""面试排期协调与冲突处理 — 多环境多阶段任务。

涉及 4 个环境: filesystem, email, calendar, notion (ATS)
3 个 stage: 初始排期 → 突发冲突 → 政策例外
17 个 core checker + 1 个 final checker
"""
import csv
from datetime import datetime
from io import StringIO

# ── Constants ─────────────────────────────────────────────────────

# Fake base time: Monday April 10, 2028
MON = datetime(2028, 4, 10)
TUE = datetime(2028, 4, 11)
WED = datetime(2028, 4, 12)
THU = datetime(2028, 4, 13)
FRI = datetime(2028, 4, 14)

PRIVACY_KEYWORDS = [
    "hospital", "sick", "medical", "family member",
    "医院", "看病", "家人", "身体", "陪", "不适", "就医",
]

ATS_DB_NAME = "interview_pipeline_2028"

ATS_SCHEMA = {
    "Candidate ID": {"title": {}},
    "Name": {"rich_text": {}},
    "Stage": {"select": {"options": [
        {"name": "First round"}, {"name": "Final round"},
    ]}},
    "Interviewer": {"rich_text": {}},
    "Status": {"select": {"options": [
        {"name": "Pending scheduling"}, {"name": "Scheduled"},
        {"name": "Candidate Withdrew"}, {"name": "Cancelled"},
    ]}},
    "Email": {"email": {}},
    "Notes": {"rich_text": {}},
}

ATS_ROWS = [
    {"id": "C01", "name": "Alice", "stage": "First round", "interviewer": "Zhang",
     "status": "Pending scheduling", "email": "[email protected]", "notes": ""},
    {"id": "C02", "name": "Bob", "stage": "First round", "interviewer": "Zhang",
     "status": "Pending scheduling", "email": "[email protected]",
     "notes": "Currently employed — can only schedule during 12:00–13:00 or after 18:00; see input/avail_C02.png for availability"},
    {"id": "C03", "name": "Charlie", "stage": "First round", "interviewer": "Li",
     "status": "Pending scheduling", "email": "[email protected]", "notes": ""},
    {"id": "C04", "name": "David", "stage": "Final round", "interviewer": "Director Lin",
     "status": "Pending scheduling", "email": "[email protected]", "notes": ""},
    {"id": "C05", "name": "Eve", "stage": "First round", "interviewer": "Zhang",
     "status": "Pending scheduling", "email": "[email protected]", "notes": ""},
    {"id": "C06", "name": "Frank", "stage": "First round", "interviewer": "Li",
     "status": "Pending scheduling", "email": "[email protected]",
     "notes": "Candidate requests on-site interview; train ticket in input/avail_C06.jpg"},
]

# ── METADATA ──────────────────────────────────────────────────────

METADATA = {
    "id": "hr_task5",
    "name": "面试排期协调与冲突处理",
    "category": "hr",
    "environments": ["filesystem", "email", "calendar", "notion"],
    "timeout_seconds": 600,
    "difficulty": "hard",
    "mm_level": "L4",
    "role": "小安, HR总监林总的行政助理",
    "tags": ["面试", "排期", "日历", "冲突处理", "多模态", "隐私"],
    "env_config": {
        "email": {
            "users": {
                "xiaoan": {"email": "[email protected]", "password": "xiaoan_pwd"},
                "lin_dir": {"email": "[email protected]", "password": "lin_dir_pwd"},
                "zhang_eng": {"email": "[email protected]", "password": "zhang_eng_pwd"},
                "li_eng": {"email": "[email protected]", "password": "li_eng_pwd"},
                "alice": {"email": "[email protected]", "password": "alice_pwd"},
                "bob": {"email": "[email protected]", "password": "bob_pwd"},
                "charlie": {"email": "[email protected]", "password": "charlie_pwd"},
                "david": {"email": "[email protected]", "password": "david_pwd"},
                "eve": {"email": "[email protected]", "password": "eve_pwd"},
                "frank": {"email": "[email protected]", "password": "frank_pwd"},
            },
        },
    },
}

PROMPT = "请查看邮件并按指示操作。"


# ── Helpers ───────────────────────────────────────────────────────

def _read_csv(ctx) -> list[dict]:
    """Read master_schedule.csv from workspace snapshot."""
    csv_path = ctx.workspace / "master_schedule.csv"
    if not csv_path.exists():
        return []
    text = csv_path.read_text(encoding="utf-8-sig")
    return list(csv.DictReader(StringIO(text)))


def _find_csv_row(rows: list[dict], candidate_id: str) -> dict | None:
    """Find a CSV row by candidate_id (case-insensitive key matching)."""
    for row in rows:
        for key in ("candidate_id", "Candidate ID", "CandidateID", "id"):
            val = row.get(key, "").strip().upper()
            if val == candidate_id.upper():
                return row
    return None


def _notion_text(value: str) -> dict:
    """Build Notion rich_text property value."""
    return {"rich_text": [{"text": {"content": value}}]}


def _notion_title(value: str) -> dict:
    """Build Notion title property value."""
    return {"title": [{"text": {"content": value}}]}


def _notion_select(value: str) -> dict:
    """Build Notion select property value."""
    return {"select": {"name": value}}


def _notion_email(value: str) -> dict:
    """Build Notion email property value."""
    return {"email": value}


# ── Stage Functions ───────────────────────────────────────────────

async def stage0(ctx):
    """Monday April 10: Complex initial scheduling — seed all environments."""
    # 1. Upload input materials to workspace
    await ctx.fs.upload_dir(ctx.task_dir / "assets" / "input", "/workspace/input")

    # 2. Create calendars and seed existing events
    for cal_name in ["zhang.eng", "li.eng", "lin.dir", "RoomA", "RoomB"]:
        await ctx.calendar.create_calendar(cal_name)

    # Zhang: Wed 10-11 Team weekly; Wed 11-14 Off-site; Thu 15-16 Code Review
    await ctx.calendar.add_event("zhang.eng", "Team weekly meeting",
                                  WED.replace(hour=10), WED.replace(hour=11))
    await ctx.calendar.add_event("zhang.eng", "Off-site lunch + client visit",
                                  WED.replace(hour=11), WED.replace(hour=14))
    await ctx.calendar.add_event("zhang.eng", "Code Review",
                                  THU.replace(hour=15), THU.replace(hour=16))

    # Li: Wed 09-10 Standup; Fri 14-15 Tech review
    await ctx.calendar.add_event("li.eng", "Standup",
                                  WED.replace(hour=9), WED.replace(hour=10))
    await ctx.calendar.add_event("li.eng", "Tech review",
                                  FRI.replace(hour=14), FRI.replace(hour=15))

    # Director Lin: Thu 10-11:30 Department meeting; Fri 09-10 One-on-one
    await ctx.calendar.add_event("lin.dir", "Department meeting",
                                  THU.replace(hour=10), THU.replace(hour=11, minute=30))
    await ctx.calendar.add_event("lin.dir", "One-on-one",
                                  FRI.replace(hour=9), FRI.replace(hour=10))

    # RoomA: Wed 09-11 occupied
    await ctx.calendar.add_event("RoomA", "Reserved",
                                  WED.replace(hour=9), WED.replace(hour=11))

    # RoomB: Thu 14-16 occupied; Fri 09-12 team-building
    await ctx.calendar.add_event("RoomB", "Reserved",
                                  THU.replace(hour=14), THU.replace(hour=16))
    await ctx.calendar.add_event("RoomB", "Department team-building",
                                  FRI.replace(hour=9), FRI.replace(hour=12))

    # 3. Create ATS database in Notion (pure API, no template needed)
    await ctx.notion.create_page("ATS Pipeline 2028")
    await ctx.notion.create_database(ATS_DB_NAME, ATS_SCHEMA)
    for row in ATS_ROWS:
        await ctx.notion.add_database_row(ATS_DB_NAME, {
            "Candidate ID": _notion_title(row["id"]),
            "Name": _notion_text(row["name"]),
            "Stage": _notion_select(row["stage"]),
            "Interviewer": _notion_text(row["interviewer"]),
            "Status": _notion_select(row["status"]),
            "Email": _notion_email(row["email"]),
            "Notes": _notion_text(row["notes"]),
        })

    return {
        "notification": (
            "[飞书] 林总:小安,本周有6位候选人(C01-C06)需要排面试。"
            "可用时间截图都在 input/ 目录下。\n"
            "请交叉对照ATS记录和大家的日历排定时间。确认后:\n"
            "1. 创建日历事件(现场的订会议室,远程的生成视频链接)\n"
            "2. 更新ATS状态为「已排期」\n"
            "3. 给每位候选人发确认邮件\n"
            "4. 给我发一份 master_schedule.csv\n"
            "\n"
            "候选人信息在ATS(Notion的 interview_pipeline_2028 数据库)里。"
            "面试官日历是 zhang.eng、li.eng、lin.dir,"
            "会议室日历是 RoomA 和 RoomB。"
            "你的邮箱是 [email protected]。"
        ),
        "time": "2028-04-10T09:00:00+08:00",
    }


async def stage1(ctx):
    """Tuesday April 11: Sudden conflict + silent withdrawal."""
    # 1. Zhang sends email about conflict
    await ctx.email.send_email(
        from_user="zhang_eng",
        to="[email protected]",
        subject="周三下午面试需要调整",
        body=(
            "小安,\n\n"
            "我周三下午14:00-16:00要陪家人去医院,这个时间段的面试请帮我调一下。\n\n"
            "张工"
        ),
    )

    # 2. Silent ATS update: C05 withdrew
    rows = await ctx.notion.query_db(ATS_DB_NAME)
    for row in rows:
        props = row.get("properties", {})
        cid_prop = props.get("Candidate ID", {})
        title_list = cid_prop.get("title", [])
        cid = "".join(t.get("plain_text", "") for t in title_list)
        if cid == "C05":
            await ctx.notion.update_db_row(row["id"], {
                "Status": _notion_select("Candidate Withdrew"),
                "Notes": _notion_text("Candidate withdrew — accepted another offer"),
            })
            break

    return {
        "notification": "[飞书] 林总:小安,张工好像给你发了邮件,你看一下,有什么需要调整的及时处理。",
        "time": "2028-04-11T09:00:00+08:00",
    }


async def stage2(ctx):
    """Wednesday April 12: Policy exception + resource crisis."""
    # 1. C04 (David) sends email about injury
    await ctx.email.send_email(
        from_user="david",
        to="[email protected]",
        subject="关于周五终面 — 请求改为视频",
        body=(
            "小安你好,\n\n"
            "非常抱歉,我昨天脚扭伤了,行动不便。请问周五的终面能否改为视频面试?\n\n"
            "谢谢,\nDavid"
        ),
    )

    # 2. Silent calendar: RoomA Friday under maintenance
    events = await ctx.calendar.get_events("RoomA")
    for e in events:
        dtstart = e.get("dtstart", "")
        if "2028-04-14" in str(dtstart):
            await ctx.calendar.delete_event("RoomA", e["uid"])
    await ctx.calendar.add_event(
        "RoomA", "Maintenance: ceiling leak",
        FRI.replace(hour=8), FRI.replace(hour=18),
        description="Building admin: ceiling leak repair, room unavailable all day",
    )

    # 3. Silent ATS update: Director Lin approves video for C04
    rows = await ctx.notion.query_db(ATS_DB_NAME)
    for row in rows:
        props = row.get("properties", {})
        cid_prop = props.get("Candidate ID", {})
        title_list = cid_prop.get("title", [])
        cid = "".join(t.get("plain_text", "") for t in title_list)
        if cid == "C04":
            await ctx.notion.update_db_row(row["id"], {
                "Notes": _notion_text("Approved: C04 may have video final round"),
            })
            break

    return {
        "notification": "[飞书] 林总:小安,有候选人发邮件过来了,你查收处理一下。",
        "time": "2028-04-12T09:00:00+08:00",
    }