""" scheduler.py — APScheduler cron jobs All time-based triggers. Runs inside the HuggingFace Space process. """ import os from datetime import date from apscheduler.schedulers.background import BackgroundScheduler from slack_sdk import WebClient import sheets import handlers import prompts slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) CHANNEL = os.environ["SLACK_CHANNEL_ID"] def _post(text: str, channel: str = None) -> None: slack.chat_postMessage(channel=channel or CHANNEL, text=text) def _dm(slack_user_id: str, text: str) -> None: resp = slack.conversations_open(users=slack_user_id) dm_channel = resp["channel"]["id"] slack.chat_postMessage(channel=dm_channel, text=text) def _today() -> str: """Returns today's date in IST.""" from datetime import datetime, timezone, timedelta IST = timezone(timedelta(hours=5, minutes=30)) return datetime.now(IST).date().isoformat() def _get_current_day(candidate: dict) -> int: """Count working days (Mon-Sat) since start date. Sundays skipped.""" stage = candidate["stage"] if stage == "week0": start = candidate.get("week0_start_date", "") else: start = candidate.get("probation_start_date", "") if not start: return 1 start_date = date.fromisoformat(start) today = date.today() working_days = 0 current = start_date while current <= today: if current.weekday() != 6: # skip Sundays working_days += 1 current = current.replace(day=current.day + 1) if current.day < 28 else ( current.replace(month=current.month + 1, day=1) if current.month < 12 else current.replace(year=current.year + 1, month=1, day=1) ) return max(1, working_days) # ───────────────────────────────────────────── # JOB 1 — MORNING FT BRIEF (9:00 AM daily) # ───────────────────────────────────────────── # Day-by-day FT focus messages — keyed by probation day number FT_DAILY_BRIEFS = { 1: ( "*Probation Day 1 — First contact with real work*\n\n" "Assign the problem today. Watch for:\n" "• Quality of clarifying questions — vague = shallow thinking\n" "• Whether they ask at all before diving in\n" "• End-of-day problem breakdown: does it exist? Does it have structure?\n\n" "_Do NOT suggest how to break the problem down. Wait and read._" ), 2: ( "*Probation Day 2 — Architecture before code*\n\n" "A proposed approach should arrive without you asking.\n" "Watch for:\n" "• Did it arrive proactively?\n" "• Are trade-offs visible — or did they pick the first thing that came to mind?\n" "• Did they name a failure case?\n\n" "_If no approach by EOD: send exactly one message — " "'What is your approach? Come back with it before you write code.'_" ), 3: ( "*Probation Day 3 — First real build, debugging posture*\n\n" "They are building now. Watch from a distance.\n" "Watch for:\n" "• Is the blocker in their report specific or vague?\n" "• Did they try anything before reporting it?\n" "• Are they building in the direction of the plan or drifting?\n\n" "_Do NOT check in. Wait and see what surfaces._" ), 4: ( "*Probation Day 4 — Depth check (your only scheduled touch point this week)*\n\n" "Run a 15-minute check-in today. Prepare three specific questions based on " "the architecture they submitted on Day 2.\n\n" "Ask each question, wait for a full answer, then push back on one.\n" "Log the quality of answers after.\n\n" "_Keep to 15 minutes. Do not fill silences they should be filling._" ), 5: ( "*Probation Day 5 — Week 1 checkpoint*\n\n" "The candidate should submit a checkpoint document by EOD:\n" "what was built, what works, what doesn't, decisions made, Week 2 plan.\n\n" "When it arrives:\n" "• Name one specific thing done well\n" "• Name one specific thing that must change in Week 2\n" "• Confirm or push back on the Week 2 plan\n\n" "_Complete the Week 1 evaluation — the agent will prompt you for signal answers._" ), 6: ( "*Probation Day 6 — Open Week 2 explicitly*\n\n" "Start with a brief, direct conversation today:\n" "• Name one thing they did well in Week 1 (observation, not praise)\n" "• Name the 1-2 things that need to change\n" "• Set deliverables with *specific dates* — not 'by end of week'\n" "• Get verbal confirmation of each deliverable\n\n" "_The agent will log commitments once you confirm them._" ), 7: ( "*Probation Day 7 — Building against commitments*\n\n" "Do NOT check in. The intern should surface updates proactively.\n\n" "Watch for:\n" "• Did an update arrive without you asking?\n" "• Is it specific enough to act on?\n" "• If there is risk — was it surfaced with a proposed solution?\n\n" "_If no update by mid-day: note it. Do not ask. Read the daily report._" ), 8: ( "*Probation Day 8 — Wednesday deliverable (first hard deadline)*\n\n" "The Wednesday component is due EOD today.\n\n" "If it arrives on time: acknowledge the specific thing done well. " "Ask one probe question about a design decision.\n\n" "If it is late but was flagged proactively before deadline: " "use the accountability structure.\n\n" "If it is late with no prior flag: the agent will issue a formal warning. " "Log the outcome immediately.\n\n" "_The agent will check at EOD and act accordingly._" ), 9: ( "*Probation Day 9 — Pushing through the hard part*\n\n" "Day 9 is typically where the work gets hardest.\n\n" "Watch for:\n" "• Is the intern working on the hard part or polishing what is already done?\n" "• Are blockers specific and come with attempted solutions?\n" "• If scope adjustment is proposed — is it reasoned or retreat?\n\n" "_If they are visibly avoiding the hard part: " "'The [component] is the harder piece. What is your current thinking on it?'_" ), 10: ( "*Probation Day 10 — Final delivery and review*\n\n" "Final component due today at the agreed time.\n" "Run the 20-minute final review:\n" "• 3-5 direct questions about the work\n" "• Ask where the system breaks\n" "• Ask about a trade-off they made\n\n" "Complete the final evaluation after the review.\n" "Communicate the decision to the intern *today* — do not leave it open.\n\n" "_The agent will generate the evaluation and send it to HR for confirmation._" ), } def _is_working_day() -> bool: """Returns False on Sundays in IST — no reports expected, no warnings fired.""" from datetime import datetime, timezone, timedelta IST = timezone(timedelta(hours=5, minutes=30)) return datetime.now(IST).weekday() != 6 # 6 = Sunday def job_morning_ft_brief() -> None: """Send FT the day's focus brief at 9:00 AM.""" if not _is_working_day(): return active = sheets.get_active_candidates() for candidate in active: stage = candidate["stage"] if stage not in ("probation_w1", "probation_w2"): continue day = _get_current_day(candidate) brief = FT_DAILY_BRIEFS.get(day) if not brief: continue ft_id = candidate.get("ft_slack_id", "") if ft_id: _dm(ft_id, f"*Re: {candidate['name']}*\n\n{brief}") # ───────────────────────────────────────────── # JOB 2 — MISSED REPORT CHECK (runs after each report window) # ───────────────────────────────────────────── def job_missed_report_check() -> None: """Check for missed reports 30 min after each window closes.""" if not _is_working_day(): return handlers.handle_missed_report_check() # ───────────────────────────────────────────── # JOB 3 — EOD SIGNAL QUESTIONS TO FT (6:00 PM daily) # ───────────────────────────────────────────── # Signal questions keyed by day FT_SIGNAL_QUESTIONS = { 1: "Did {name} ask at least one specific clarifying question about the problem — scope, constraints, or success criteria? Reply: yes / no / partial", 2: "Did {name}'s architecture approach arrive without you asking? Did it name a failure case? Reply: yes / no / partial", 3: "When {name} hit a blocker today — was it reported with specific detail and prior attempts, or was it vague / passive? Reply: specific / vague / not reported", 4: "In today's depth check: could {name} explain their design decisions clearly and hold reasoning under pushback? Reply: yes / no / collapsed", 5: "Did {name} submit the Week 1 checkpoint on time, with real structure? Reply: yes / late / not submitted", 6: "How did {name} receive the raised bar today — seriously engaged or performed enthusiasm? Did they confirm deliverables with specific dates? Reply: engaged / vague / defensive", 7: "Did {name} surface a proactive update on their Wednesday deliverable without you asking? Reply: yes / no / only after prompted", 8: "Did {name}'s Wednesday deliverable arrive on time with a proper handover note? Reply: on_time / late_flagged / late_no_flag / not_delivered", 9: "Is {name} actively working on the harder Friday deliverable — or polishing already-completed work? Reply: pushing / avoiding / unclear", 10: "In the Day 10 final review: could {name} explain all major decisions and identify where the system fails? Reply: yes / partial / no", } def job_eod_signal_questions() -> None: """Send FT the signal question for today at 6:00 PM.""" if not _is_working_day(): return active = sheets.get_active_candidates() for candidate in active: stage = candidate["stage"] if stage not in ("probation_w1", "probation_w2"): continue day = _get_current_day(candidate) question_template = FT_SIGNAL_QUESTIONS.get(day) if not question_template: continue question = question_template.format(name=candidate["name"]) ft_id = candidate.get("ft_slack_id", "") if ft_id: _dm(ft_id, f"*End-of-day signal check — {candidate['name']} (Day {day})*\n\n{question}") # ───────────────────────────────────────────── # JOB 4 — DEADLINE CHECK (Wed EOD + Fri noon) # ───────────────────────────────────────────── def job_deadline_check() -> None: """ Runs at Wednesday EOD (6:30 PM) and Friday noon (12:00 PM). Checks all W2 commitments due today. """ today = _today() due_today = sheets.get_due_commitments(today) for commitment in due_today: cid = commitment["candidate_id"] candidate = next( (c for c in sheets.get_active_candidates() if c["candidate_id"] == cid), None, ) if not candidate: continue # Check if candidate submitted deliverable (any report with matching day) day = _get_current_day(candidate) reports = sheets.get_reports_for_candidate(cid, "probation_w2") submitted_today = [ r for r in reports if r.get("submitted_at", "").startswith(today) and r.get("format_valid") == "True" ] if submitted_today: # Delivered — update commitment sheets.update_commitment_outcome( commitment_id=commitment["commitment_id"], delivered=True, delivered_at=submitted_today[-1]["submitted_at"], flagged_proactively=commitment.get("flagged_proactively", "") == "True", outcome="on_time", ) else: # Not delivered — check if they flagged proactively proactive = commitment.get("flagged_proactively", "") == "True" outcome = "late_flagged" if proactive else "late_no_flag" sheets.update_commitment_outcome( commitment_id=commitment["commitment_id"], delivered=False, delivered_at="", flagged_proactively=proactive, outcome=outcome, ) if not proactive: # Formal warning — no prior flag msg = ( f"<@{candidate['slack_user_id']}> The deliverable committed to for today " f"({commitment['description']}) has not arrived and was not flagged in advance.\n\n" f"This is a formal warning. Your FT and HR have been notified.\n\n" f"If you are still working on it, submit now with a handover note explaining " f"what is done and what is not." ) _post(msg) sheets.log_warning( candidate_id=cid, warning_type="formal_warning", stage="probation_w2", trigger_event=f"deadline_breach_{commitment['commitment_id']}", message_sent=msg, hr_notified=True, ) hr_id = candidate.get("hr_slack_id", "") if hr_id: _dm(hr_id, f"*Formal warning issued — {candidate['name']}*\n\nMissed deadline: {commitment['description']} due {today} with no prior flag.") # ───────────────────────────────────────────── # JOB 5 — END-OF-STAGE EVALUATION (Day 5 + Day 10) # ───────────────────────────────────────────── def job_end_of_stage_eval() -> None: """ Runs at EOD on Day 5 and Day 10 for candidates in probation. Also runs at EOD on Week 0 Day 5. Generates Claude evaluation and sends to HR for confirmation. """ active = sheets.get_active_candidates() for candidate in active: stage = candidate["stage"] day = _get_current_day(candidate) if stage == "week0" and day == 5: _run_week0_eval(candidate) elif stage == "probation_w1" and day == 5: _run_w1_eval(candidate) elif stage == "probation_w2" and day == 10: _run_w2_eval(candidate) def _run_week0_eval(candidate: dict) -> None: cid = candidate["candidate_id"] reports = sheets.get_reports_for_candidate(cid, "week0") warnings = sheets.get_warnings_for_candidate(cid) result = prompts.evaluate_week0(candidate, reports, warnings) eval_id = sheets.log_evaluation( candidate_id=cid, eval_type="week0", signals=result["signals"], summary_text=result.get("reasoning", ""), recommendation=result["recommendation"], recommendation_reasoning=result.get("reasoning", ""), ) hr_id = candidate.get("hr_slack_id", "") if hr_id: signal_lines = "\n".join( f"• {k}: *{v['value']}* — {v['evidence']}" for k, v in result["signals"].items() ) _dm( hr_id, f"*Week 0 Evaluation — {candidate['name']}*\n\n" f"{signal_lines}\n\n" f"*Recommendation:* {result['recommendation']}\n" f"*Reasoning:* {result['reasoning']}\n" f"*Watch in probation:* {result.get('gaps_to_watch', '')}\n\n" f"Reply `approve` to confirm or `reject` to override.\n" f"Eval ID: `{eval_id}`" ) def _run_w1_eval(candidate: dict) -> None: cid = candidate["candidate_id"] reports = sheets.get_reports_for_candidate(cid, "probation_w1") signals = sheets.get_signals_for_candidate(cid, week="w1") week0_eval = sheets.get_latest_evaluation(cid, "week0") or {} # Checkpoint is the Day 5 report with current_status field day5_reports = [r for r in reports if str(r.get("day_number")) == "5"] checkpoint = day5_reports[-1] if day5_reports else None ft_name = os.environ.get("FT_NAME", "FT") result = prompts.evaluate_probation_w1( candidate, ft_name, reports, signals, checkpoint, week0_eval ) eval_id = sheets.log_evaluation( candidate_id=cid, eval_type="probation_w1", signals=result["signals"], summary_text=result.get("reasoning", ""), recommendation=result["w1_path"], recommendation_reasoning=result.get("reasoning", ""), ) hr_id = candidate.get("hr_slack_id", "") if hr_id: signal_lines = "\n".join( f"• {k}: *{v['value']}* — {v['evidence']}" for k, v in result["signals"].items() ) _dm( hr_id, f"*Week 1 Evaluation — {candidate['name']}*\n\n" f"{signal_lines}\n\n" f"*W1 path:* {result['w1_path']}\n" f"*Reasoning:* {result['reasoning']}\n" f"*Watch in W2:* {result.get('w2_watch', '')}\n" f"*Bar raise for W2:* {result.get('w2_bar_raise', '')}\n\n" f"Reply `approve` to confirm or `reject` to override.\n" f"Eval ID: `{eval_id}`" ) def _run_w2_eval(candidate: dict) -> None: cid = candidate["candidate_id"] reports = sheets.get_reports_for_candidate(cid, "probation_w2") signals = sheets.get_signals_for_candidate(cid, week="w2") commitments = sheets.get_commitments_for_candidate(cid) warnings = sheets.get_warnings_for_candidate(cid) week0_eval = sheets.get_latest_evaluation(cid, "week0") or {} w1_eval = sheets.get_latest_evaluation(cid, "probation_w1") or {} # EOP document is the Day 10 report day10_reports = [r for r in reports if str(r.get("day_number")) == "10"] eop_doc = day10_reports[-1] if day10_reports else None ft_name = os.environ.get("FT_NAME", "FT") result = prompts.evaluate_probation_w2( candidate, ft_name, commitments, reports, signals, eop_doc, warnings, week0_eval, w1_eval ) eval_id = sheets.log_evaluation( candidate_id=cid, eval_type="probation_w2", signals=result["w2_signals"], summary_text=result.get("reasoning", ""), recommendation=result["outcome"], recommendation_reasoning=result.get("reasoning", ""), ) hr_id = candidate.get("hr_slack_id", "") if hr_id: signal_lines = "\n".join( f"• {k}: *{v['value']}* — {v['evidence']}" for k, v in result["w2_signals"].items() ) _dm( hr_id, f"*Final Probation Evaluation — {candidate['name']}*\n\n" f"{signal_lines}\n\n" f"*Outcome:* {result['outcome']}\n" f"*Reasoning:* {result['reasoning']}\n\n" f"*HR talking points:*\n{result.get('hr_talking_points', '')}\n\n" f"Reply `approve` to confirm or `reject` to override.\n" f"Eval ID: `{eval_id}`" ) # ───────────────────────────────────────────── # JOB — MORNING AGENDA PROMPT (9:00 AM) # ───────────────────────────────────────────── def job_morning_agenda_prompt(force: bool = False) -> None: """ At 11:00 AM — bot tags each intern in their channel asking for today's agenda. Interns reply in thread. """ if not force and not _is_working_day(): print("[agenda] Skipping — not a working day") return from datetime import datetime, timezone, timedelta IST = timezone(timedelta(hours=5, minutes=30)) today_str = datetime.now(IST).strftime("%d %b %Y") active = sheets.get_active_candidates() print(f"[agenda] Active candidates: {len(active)}") # Group by channel channels = {} for candidate in active: if candidate.get("status") in ("eliminated", "cleared"): continue ch = candidate.get("channel_id", "").strip() if not ch: print(f"[agenda] {candidate.get('name')} has no channel_id — skipping") continue channels.setdefault(ch, []).append(candidate) print(f"[agenda] Channels to post in: {list(channels.keys())}") for channel_id, candidates in channels.items(): print(f"[agenda] Posting in {channel_id} for {[c['name'] for c in candidates]}") tags = " ".join(f"<@{c['slack_user_id']}>" for c in candidates) msg = "\n".join([ "*Good morning!* — " + today_str, "", tags, "", "Share your agenda for today — reply in this thread:", "", "```", "AGENDA — [Your Name] — " + today_str, "", "TODAY'S FOCUS", "- [Most important thing you will complete today]", "", "PROBLEMS I ANTICIPATE", "- [Any blockers or challenges you expect today / None]", "```", ]) try: _post(msg, channel_id) print(f"[agenda] Posted morning prompt in {channel_id} for {len(candidates)} interns") except Exception as e: print(f"[agenda] Failed to post in {channel_id}: {e}") # ───────────────────────────────────────────── # SCHEDULER SETUP # ───────────────────────────────────────────── def start_scheduler() -> BackgroundScheduler: scheduler = BackgroundScheduler(timezone="Asia/Kolkata") # Morning agenda prompt to interns — 11:00 AM IST scheduler.add_job(job_morning_agenda_prompt, "cron", hour=11, minute=0) # Morning FT brief — 9:05 AM IST (staggered to avoid rate limits) scheduler.add_job(job_morning_ft_brief, "cron", hour=9, minute=5) # Missed report check — 11:30 PM IST, once per day scheduler.add_job(job_missed_report_check, "cron", hour=23, minute=30) # EOD signal questions to FT — 6:00 PM IST scheduler.add_job(job_eod_signal_questions, "cron", hour=18, minute=0) # Deadline checks — Wed EOD (18:30) and Fri noon (12:00) scheduler.add_job(job_deadline_check, "cron", day_of_week="wed", hour=18, minute=30) scheduler.add_job(job_deadline_check, "cron", day_of_week="fri", hour=12, minute=0) # End-of-stage evaluations — 8:00 PM IST (after EOD reports) scheduler.add_job(job_end_of_stage_eval, "cron", hour=20, minute=0) scheduler.start() return scheduler