Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Command line and plumbing
Sheet and NUworks
sheet.py the Google Sheet. open_worksheet() / open_other() (kept OPEN_FOR s: OPEN, forget(), HTTPClient retries),
read_rows(); limits: max_per_week(), week_window(), applied_dates(), total_count() (not_on_nuworks,
rejected_not_withdrawn), check_limits(); writers: set_status, mark_submit_started / resolve_submit,
mark_applied_by_hand, mark_site_done, set_outcome, drop, approve_proposed, unapprove, mark_on_laptop,
append_note, add_proposed; create_sheet(), google_login().
rejected_not_withdrawn, maybe_filled), check_limits(); writers: set_status, mark_submit_started /
resolve_submit, mark_applied_by_hand, mark_site_done, set_outcome, drop, approve_proposed, unapprove,
mark_on_laptop, append_note, add_proposed; create_sheet(), google_login().
manage.py rows by hand: add(), move(), remove() (cells cleared, never deleted), lookup(), check_url(), movable().
setup_sheet.py one-time setup (refuses a sheet with data: HasData); format_sheet(); ensure_dropdown().
browser.py Playwright Firefox. launch() (lock_profile(): one process on the profile), install_domain_lock(),
Expand All @@ -46,15 +46,17 @@ Job pool
jobs.py the NUworks pool. PREFS / set_prefs(), cmd_list, cmd_triage_*, cmd_details (hard_rules), cmd_score_*,
cmd_cat_*, build_pool(), ranked_pool() (taste model), review_queue(), job_view(), rate_viewer(),
cmd_rate / cmd_approve / cmd_suggest / cmd_stats, set_rating(), check_applied(), check_applications()
(what NUworks says about sent rows -> data/applications.json).
(what NUworks says about sent rows -> data/applications.json; application_answer(): applied, and the
job's hiring stage tracked across checks).
fields.py per-field starting preferences (the wizard's "What do you study?").
intern.py Summer internships (Simplify's and vanshb03's lists, one job on both kept once): the second pool, built
like jobs.py (refresh, triage, fetch, score, category, build_pool); approve() -> manage.add("other");
update(), review_queue(), stats(). Short links -> the posting's own (real_links, real_url; used by
assist.other_target). Data in data/intern/.
like jobs.py (refresh, triage, fetch, redate, score, category, build_pool); approve() ->
manage.add("other"); update(), review_queue(), closing_dates(), stats(). Short links -> the posting's
own (real_links, real_url; used by assist.other_target). Data in data/intern/.
postings.py reads one posting from a company's own job site, no login; public addresses only (allowed_host).
daily.py what the homelab timers run. main() -> scan() (via step()), reminders(), record_hand_applications(),
applications(), internships(); run_claude(), notify() / discord(), weekly().
daily.py what the homelab timers run. main() -> scan() (via step()), applications(), reminders() (the morning
message: morning(), its lines; record_hand_applications()), internships(); run_claude(); messages:
notify() / discord(), alert() (a problem, once a day each), later() (a line for the next morning).
insights.py summarize(): pay, places, kinds of work, sheet statuses; text() for `nuauto insights`.
web.py Mark-done page (homelab, port 8765). link() signs URLs, Handler.do_GET (confirm) / do_POST; main() also
starts gui.serve_remote() when the homelab's page is on.
Expand Down Expand Up @@ -149,7 +151,8 @@ MAIN FLOWS

STATE ON DISK (STATE_DIR = repo root; app-data folder when packaged; a temp folder in demo mode and tests)
data/ the pool, owned by the homelab (the laptop pulls a copy): list.json, triage.json, scores.json, categories.json,
category_sets.json, pool.json, details/<id>.json (jobs.py); scans.json, last_run.json (daily.py);
category_sets.json, pool.json, details/<id>.json (jobs.py); scans.json, last_run.json, later.json, told.json
(daily.py);
applications.json (jobs.check_applications); ratings.json (jobs.set_rating, merged between machines);
intern/ (intern.py: listings, source, failed, unread, triage, scores, categories, pool, details/, update.lock).
work/ Claude's batches: <kind>_in_NNN.json / <kind>_out_NNN.json, resume.txt (jobs.py writes the inputs, the claude
Expand Down Expand Up @@ -198,19 +201,20 @@ REPO LAYOUT (what is not a module)
TESTS (`nuauto test` runs every tests/test_*.py, 4 at a time: offline, no sheet, no real NUworks, no network; browser tests
use headless Firefox on local pages or demo mode. Run it before pushing; CI runs it on Ubuntu and macOS.)
test_sync sync.CODE is complete (a new module or prompt must be in it), absolute imports, rsync options, units
test_sheet sheet.py: parsing, week and total limits, max_per_week, not_on_nuworks, submit safety, drop, set_outcome,
mark_on_laptop, retries, the kept sheet (OPEN)
test_jobs hard rules, pool, model; sheet.add_proposed / approve_proposed
test_sheet sheet.py: parsing, week and total limits, max_per_week, not_on_nuworks, maybe_filled, submit safety,
drop, set_outcome, mark_on_laptop, retries, the kept sheet (OPEN)
test_jobs hard rules, pool, model; sheet.add_proposed / approve_proposed; application_answer (the hiring stage)
test_apply apply.check_popup, run_rows and its queue, BETWEEN
test_answers answers.py, apply.fill_extra_fields (a local form), JsonIO, json_ui_allowed
test_browser domain lock, goto retry, profile lock
test_demo the real apply.py on demo mode's fake pages (stops, cap, Ctrl+C, GUI gone, CLAUDECODE refusals)
test_daily daily.py's summary, to-do, Claude errors
test_daily daily.py's morning message, alerts once a day, Claude errors, the applications check
test_web Mark-done links (signatures; GET never changes the sheet)
test_deploy deploy.py on a throwaway git repo
test_doctor_remote doctor.check_remote_assist test_health health.py's checks
test_insights Insights numbers test_selftest `nuauto selftest` test_setup the wizard (onboard.py)
test_intern internship pool, postings.py (links, parsing, the network guard, fetch tries), short links, Review queue
test_intern internship pool, postings.py (links, parsing, close dates, the network guard, fetch tries, redate),
short links, Review queue
test_assist the agent's guard, answer bank, slots, passwords, uploads, by-hand rows
test_accounts accounts.py: sites, Bitwarden export / import of your own logins
test_mail mail.py: Gmail's format, which mail counts, the `email` command
Expand Down
50 changes: 33 additions & 17 deletions docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,47 @@ WHAT RUNS ON THE HOMELAB
Unit files live in this repo (deploy/systemd/) and are installed in ~/.config/systemd/user/. All run with TZ New York.
- nuauto-daily.timer -> nuauto-daily.service: `nuauto daily` at 08:00 and 18:00 New York time
(+0-15 min random, Persistent=true so a missed run happens at boot). Every run, in order:
- the scan (docs/PIPELINE.md): list -> Claude triage -> details -> Claude score -> Claude category -> pool; new pool
jobs are announced; counts go to data/scans.json.
- the morning run only (before noon): the reminders. First, if the scan worked, jobs you applied to by hand on
NUworks become Applied rows (jobs.check_applied). Then messages: Approved rows closing today or tomorrow, pool jobs
closing within 3 days that are not in the sheet, the Google login warning (once 1 day is left), and "NUauto
morning" (always sent): new postings and new pool jobs in the last 24h, then the to-do list (company-site
applications owed, external Needs Human rows) with signed Mark links.
- the scan (docs/PIPELINE.md): list -> Claude triage -> details -> Claude score -> Claude category -> pool; counts
and the best new job go to data/scans.json.
- the applications check: NUworks is asked about every sent row (jobs.check_applications -> data/applications.json), so
the total limit counts what NUworks has; the Status dropdown is set again (Accepted / Rejected reach an older
sheet); a Rejected row NUworks still has is in the log, and in the morning message until you withdraw it there.
sheet). Rejected rows NUworks still has and Applied rows whose job looks filled (its Hiring Status turned Filled, or
it was interviewing and stopped: sheet.maybe_filled) are in the log, and in the morning message until you withdraw
them there.
- the morning run only (before noon): if the scan worked, jobs you applied to by hand on NUworks become Applied rows
(jobs.check_applied); then the morning message.
- the internship update (when "internships" is on).
DISCORD (since 2026-10-26, my request: fewer, shorter messages, glanced at once a day). Two kinds only:
- "NUauto morning" (daily.morning), always sent, a line for each thing that applies: New (pool co-ops and
internships of the last 24h, each with the best one), Closing soon (Approved rows on both tabs closing within a week,
soonest first with the day, an internship's date from its posting; pool co-ops and internships closing within 3
days not in the sheet, counted), To do (company-site applications owed, external Needs Human rows, the first three:
each name links to its job, its ✓ to the Mark-done page), Withdraw on NUworks (Rejected / may be filled), what
other runs kept for it (daily.later -> data/later.json: applications recorded from NUworks, approved internships
now closed, deploys), Week (this week's count, the total, Approved waiting), Wrong (this run stopped, the sheet
can't be read, the Google login runs out), and NUauto's page when there is one.
- a problem when it happens (daily.alert: a run that stopped, a login to renew, the sheet unreadable, a crash): the
same title at most once a day (data/told.json, TELL_AGAIN), so the 2-hourly internship update failing the same way
is told once. The morning run's own problems go in the morning message instead (daily.QUIET).
Not on Discord any more: new pool jobs per update, new internships per 2-hourly update, the separate deadline and
Google login messages, recorded applications, closed internships, "Deployed", the Sunday check-in. Unchanged: the
assistant's messages (it waits for you, wants to Submit, ended: docs/REMOTE.md), a failed deploy, device pairing.
If the job list fails (NUworks slow), it is read once more 2 minutes later (daily.RETRY_WAIT), and a page load gets one
retry after 60 s; the morning reminders still go out, from the sheet and the last saved pool. Why a run stopped goes
retry after 60 s; the morning message still goes out, from the sheet and the last saved pool. Why a run stopped goes
to data/last_run.json (daily.record_run: nuworks_login, nuworks, claude or failed, in a few words); the laptop pulls
it with data/, and the GUI's New jobs check shows it on Today with the fix (Log in to NUworks, Run it again, Log in to
Claude there). Discord says the same: a login only when NUworks really asks for one.
Claude there). Discord says the same (once a day at most): a login only when NUworks really asks for one.
Claude runs as `claude -p --model sonnet`, one call per batch file, shut in work/ (it runs there and may read and
write only there: no shell, web or MCP tools; the batches hold web text, job postings, so nothing in one can reach
local/, the code or your home). Headless browser; never applies to anything.
- nuauto-weekly.timer -> nuauto-weekly.service: `nuauto daily weekly`, Sunday 19:00 New York time
(Discord check-in: this week's count, Approved rows waiting, pool jobs you haven't looked at).
- nuauto-weekly.timer -> nuauto-weekly.service: `nuauto daily weekly`, Sunday 19:00 New York time. Sends nothing
since 2026-10-26 (the morning message has the week's count); kept so the installed timer runs cleanly (deploy never
removes unit files: `systemctl --user disable --now nuauto-weekly.timer` turns it off).
- nuauto-intern.timer -> nuauto-intern.service: `nuauto intern update` every 2 hours (:40 on even hours New York,
+0-10 min). Needs "internships" in local_config.json (docs/PIPELINE.md, SUMMER INTERNSHIPS); without it the run stops at
once saying so. Simplify's and vanshb03's lists, postings from the companies' own job sites, Claude triage / score /
category, Discord for new pool internships. `nuauto daily` runs the same update after the NUworks one; a lock keeps
it to one at a time. The laptop's `nuauto intern update` starts it here.
category; the morning message counts new pool internships. `nuauto daily` runs the same update after the NUworks
one; a lock keeps it to one at a time. The laptop's `nuauto intern update` starts it here.
- nuauto-deploy.timer -> nuauto-deploy.service: `nuauto deploy` 2 min after boot, then every 5 minutes (deploy_from_git
only). Looks at GitHub's main; a new commit is unpacked aside (work/deploy/), imported as a check (deploy.MODULES),
then its changed files are copied in (the old ones kept to undo), the package is reinstalled if pyproject.toml
Expand All @@ -85,7 +100,7 @@ Unit files live in this repo (deploy/systemd/) and are installed in ~/.config/sy
import, or a web that does not come back, is undone and Discord says so once. It waits while an update (`nuauto
daily`, `nuauto intern update`) or a NUworks run started from NUauto's page (`nuauto apply`, `nuauto assist nuworks`:
nuauto-web's children, which a restart would cut off) runs; assistant sessions run in scopes of their own and are
not cut off. Record: local/deployed.json; log: logs/deploy.log; Discord: "Deployed <commit>" when files changed.
not cut off. Record: local/deployed.json; log: logs/deploy.log; the next morning message: "Updated: <commit>".
- nuauto-assist.timer -> nuauto-assist.service: `nuauto assist auto` every 10 minutes: the assistant's queue. Does
nothing unless "remote_assist" and "remote_auto" are on (docs/REMOTE.md).
- nuauto-web.service: `nuauto web`, always on (Restart=on-failure). Serves the signed Mark-done links on
Expand Down Expand Up @@ -170,8 +185,9 @@ SYNC (sync.py; runs automatically inside `nuauto` on the laptop, homelab mode on
DEPLOYING A CHANGE
With deploy_from_git (the homelab pulls main; no unit file or dependency steps by hand):
1. Change code on the laptop (on a branch; merge to main when approved).
2. Merge to main. Within 5 minutes the homelab has it (Discord: "Deployed <commit>"). Now: `nuauto deploy`
(starts the run on the homelab and shows the end of its log). A branch on the laptop never reaches the homelab.
2. Merge to main. Within 5 minutes the homelab has it (the next morning message: "Updated: <commit>"). Now:
`nuauto deploy` (starts the run on the homelab and shows the end of its log). A branch on the laptop never reaches
the homelab.
3. `nuauto doctor` shows the deployed commit; a failed deploy shows as FAIL with the reason (logs/deploy.log).
Roll back: revert the commit on main (a new commit), merge it; the homelab deploys the revert.
What it copies: src/nuauto/, prompts/, docs/, deploy/systemd/ and the top-level pyproject.toml, README.md, CLAUDE.md,
Expand Down
12 changes: 9 additions & 3 deletions docs/GUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,20 @@ A question from a run (an unknown form field, an option to pick, "did it submit?
Human rows only you can finish there: a cover letter, a transcript; the assistant can work them on NUworks
with you, or a chip says why not), Still owed (submitted on NUworks, the company site still wants you:
Mark the company site done; the NUworks side to send again: Submit on NUworks too), Applied ("not on
NUworks" chip when NUworks has no application for it: it doesn't count toward the total; "interviewing
(NUworks)"), Accepted, Rejected, Proposed, Dropped or failed, Other rows; Recent runs here at the end. The
NUworks" chip when NUworks has no application for it: it doesn't count toward the total; the job's own
hiring stage at the last check (jobs.application_answer): "job interviewing", then amber "job stopped
interviewing" or "job filled" (sheet.maybe_filled: it may be filled; withdrawing on NUworks frees its
place in the total). Until 2026-10-26 an "interviewing (NUworks)" chip showed the job's is_interviewing
flag as if it were yours: it is about the job, set also on jobs you never applied to, and NUworks never
shows a student whom an employer picked), Accepted, Rejected, Proposed, Dropped or failed, Other rows;
Recent runs here at the end. The
other tab: Ready to apply (Approved: the assistant), Applied, Accepted, Rejected, Proposed, Dropped or
failed, Other rows (Sort: score, match %, pay, sheet order, and on the homelab's page the queue's order
first; it changes the view only).
Every row looks alike: who and what, score / match % and pay ("score 105 = 75% match + 10 Boston + 20
security" when bonuses add to it: Start goes by score), a line about it (site, notes), chips (when it
closes, or was posted: amber within a week, red once past; "may also want the company's site"; the queue's
closes, amber within a week, red once past; an internship's from its posting (gui.intern_facts), else
"no close date · posted N days ago"; "may also want the company's site"; the queue's
place; "do it on the laptop" when the homelab's browser was refused), what to do next, Open, and More (⋯;
on a phone in the row's top right corner).
Next: Apply on an Approved NUworks row, after a confirm, just that row (`nuauto apply --row N`); while an
Expand Down
Loading
Loading