Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GrokBot-Scheduler

Unattended spring-registration prep for one college student, driven by a persistent cloud agent.

It reads public registrar pages, researches candidate sections, flags courses that are not offered every term, assembles ranked schedules, and watches seat availability every 15 to 30 minutes from the day the schedule publishes through the end of add/drop. When a seat opens at 2 a.m., the phone buzzes.

It is general purpose. Everything school-specific lives in a YAML file and a pluggable adapter, so a second university is a configuration change rather than a fork.

What it will not do

  • It never registers, adds, drops, waitlists, or submits any form. It alerts and prepares. Every alert ends with a line saying so.
  • It holds no credentials. No university single sign-on, ever. If a school's schedule of classes cannot be read logged out, that school is unsupported and the tool says so instead of asking for a password.
  • It never publishes the student. The degree audit, transcript, and parsed requirements live in /workspace/scheduler/private/, outside the repository clone, and never appear in a log, a digest, a commit, or a chat message.

The rule the whole thing rests on

An unattended watcher that mistakes a login redirect for "no seats available" is worse than no watcher at all. So every fetch ends in exactly one of three outcomes, decided by scripts/parse_seats.py and never by a language model:

Outcome Means
DATA The page was real and the parser found sections.
EMPTY The page said, in so many words, zero results.
FAILED Anything else.

FAILED covers an unmatched selector, a response under 2 KB, a non-200 status, a redirect off the registrar's host, a title containing sign in / login / error / unavailable, and a CAPTCHA. seats.json is written only on DATA; a FAILED run changes nothing but the health record.

Silence is not evidence of no change. If nothing has produced DATA in three hours, the student gets a message saying the watch is blind and when the last real page was parsed, repeating every six hours until it recovers.

Quick start

git clone https://github.com/<owner>/GrokBot-Scheduler.git
cd GrokBot-Scheduler
export SCHEDULER_HOME=~/scheduler SCHEDULER_REPO=$PWD
bash scripts/setup.sh                      # idempotent; safe to re-run

cp schools/virginia-tech.yml config/school.yml     # or config/school.example.yml
$EDITOR config/school.yml                          # add calendar dates and a watch list

python3 scripts/fetch_seats.py --routine dry_run --dry-run
python3 scripts/health.py
python3 scripts/digest.py

config/school.yml is gitignored. It is the only file that knows which student this instance works for.

How a seat watch run goes

  1. Take the routine's lock. A healthy lock from another run means exit SKIPPED_OVERLAP immediately.
  2. Group the watch list by course, so six sections of two courses cost four page loads rather than twelve.
  3. Fetch, three seconds apart, at most twelve pages per run, one worker. Save every page to cache/.
  4. Screen each response, then parse it. DATA, EMPTY, or FAILED.
  5. Compare against the last observation. Alert only on a transition — opened, filled, and nearing_full where the school publishes seat counts. The same transition does not re-fire until the section has passed through a different one.
  6. Update health.json, write one JSONL log and one row in runs.csv, release the lock.

State writes go through scripts/statefile.py: temp file, fsync, rename, with the previous version kept in state/history/. scripts/restore.py rolls one back. On 429 or 503 the backoff ladder is 30, 60, 120, 240 minutes. The cadence floor lives in the config file, not in a chat message, and asking the Bot to check faster does not move it.

Adding a school

Most schools need no Python. Copy config/school.example.yml, run scripts/detect_adapter.py against the schedule search page, and fill in the registrar.generic block — request shape, the pattern that isolates the results table, and the column order.

Then prove the selectors against a page you actually saved:

python3 scripts/parse_seats.py --probe --term 202601 --subject CS --number 3114 \
  --page cache/all.html --page cache/open.html

--probe passes only when seven required fields parse on at least three sections. Nothing gets scheduled against selectors that have not passed it.

A registrar that needs logic rather than configuration gets its own module in adapters/, roughly a hundred lines: describe the requests, parse the table, return one of the three outcomes.

Virginia Tech

The reference implementation. VT's Timetable of Classes is a Banner-era CGI at selfservice.banner.vt.edu, readable without a login.

  • Current terms: HZSKVTSC.P_ProcRequest
  • Past terms: HZSKVTSC.P_DispHistorical — the same CGI with history=Y, listing about 112 terms. Rotation analysis reads it; seat watch never does.
  • VT publishes a section's capacity but not its remaining seats. Rather than estimate, the adapter reads availability as set membership: the open_only=on filter returns only sections with room, so a CRN present in the all-sections result and absent from the open-only result is full. Alerts fire on opened and filled; nearing_full reports unavailable_at_this_school.
  • Instructor N/A means the assignment is not yet published. That is UNKNOWN, not "staff".

The fixtures in tests/fixtures/vt_timetable/ were saved from the live site on 2026-09-06. If VT changes its markup, the tests fail — which is the point.

Labels

Every line in every artifact is OBSERVED (read on a page, with the page and the time), INFERRED (derived, with the source), or UNKNOWN. Course profiles carry a confidence of HIGH, MEDIUM, or LOW.

Grade distributions come only from the registry in data/grade_sources.yml. A school with no entry gets the literal string unavailable_at_this_school. Spring 2020 through Spring 2021 is excluded, numbers are reported as a delta from that term's department mean, and fewer than 20 students is flagged small_sample.

Layout

bot/role.md          the agent's role, pasted verbatim
bot/routines.md      one paste-ready block per scheduled routine
config/              school.example.yml; school.yml is gitignored
schools/             contributed configs: public URLs and selectors only
adapters/            base contract, vt_timetable, generic
scripts/             fetch, parse, alert, health, digest, state, locks
data/                grade_sources.yml
tests/               fixtures saved from live sites, and the tests over them
logs/                scrubbed weekly copies of run history

On the agent's computer: /workspace/scheduler/{repo,private,cache,state,logs,digests}.

Running the tests

python3 -m pytest tests -q

They run offline against saved pages. scripts/setup.sh refuses to finish if they fail, so a broken parser cannot be scheduled.

Status

Working: the seat-watch pipeline end to end — adapters, the three-outcome parser, fetching, alerting, health and blind detection, locking, atomic state with rollback, run logging and scrubbing, the weekly digest, publication watch, and adapter detection.

Not yet built: intake of a degree audit, candidate research, grade lookup, rotation analysis, and schedule assembly (R1, R3, R4, R5, R7 in bot/routines.md). Their contracts are written; the code is next.

License

MIT

About

Unattended spring-registration prep: a cloud agent that watches public registrar pages for seats and never registers for you.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages