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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ GUI (`nuauto gui`; details: docs/GUI.md)
`python -m` or file paths.

COMPANY-SITE AGENT (`nuauto assist`; every rule: docs/ASSIST.md, read it before changing the agent)
- Only Needs Human rows stopped at an external application, Approved Other jobs rows, and (2026-10-16, my call) Needs
- Only Needs Human rows stopped at an external application, Approved Other jobs rows, Applied rows still owing their
company site (SITE_MARK, the posting's own link: assist.site_target; 2026-11-02), and (2026-10-16, my call) Needs
Human rows stopped on NUworks itself (anything but an external application, e.g. a cover letter, transcript, an answer
in my words; a NUworks job link, deadline not passed, no unresolved Submit click: assist.by_hand_target): on
those it works NUworks itself (their own job page only: a prompt rule; code checks only the starting link), one at a time on NUworks (not beside an Apply run).
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ SHEET (columns in sheet.HEADERS, made by setup_sheet.py)
Needs Human sheet.set_status (apply.py stops, deadline passed); mark_submit_started (the row reads Needs Human
between the Submit click and the result, so it can't be sent twice).
set_status only changes rows still Approved. Notes starting sheet.SITE_MARK are written by apply.submit_flow and cleared
by sheet.mark_site_done. The weekly cap counts sent rows (sheet.SENT) dated in the current week (sheet.week_window); the
by sheet.mark_site_done (Mark the company site done, or a y after the assistant's session on that row). The weekly cap counts sent rows (sheet.SENT) dated in the current week (sheet.week_window); the
total counts those NUworks has (sheet.total_count). The Other jobs tab has neither.

REPO LAYOUT (what is not a module)
Expand Down
6 changes: 6 additions & 0 deletions docs/ASSIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ WHICH ROWS (assist.py + prompts/ASSIST_PROMPT.md, `nuauto assist <row>`; added 2
exact resume label (assist.hand_note; the rules' NUWORKS ROWS): it clicks the job's Apply, fills the popup, uploads
the transcript or the cover letter, and its Submit asks me like any other. After y: Applied ("on NUworks"), no
NUworks side after. The weekly cap is checked at its start and the Submit question says it counts.
- (2026-11-02, my request) Still owed rows: Applied NUworks-tab rows whose Notes start sheet.SITE_MARK (the posting
wanted the company site too; NUworks gave no external link): assist.site_target. The link is the posting's own
(jobs.site_link: the link inside the sentence that asks, else the posting's only link; several and none there: no
start, `nuauto assist <row> --url <link>`); never NUworks. No weekly / total check (it went out on NUworks already);
the session is told so (assist.site_note), and the Submit question says it counts toward no limit. After y:
sheet.mark_site_done (stays Applied, date kept), nothing sent on NUworks. The homelab's queue never starts it by itself.
- Other jobs (added 2026-10-07): `nuauto assist other <row>`: an Approved row of the Other jobs tab (https, never
NUworks: assist.other_target), same agent, rules and hooks, with the layered answer bank (CLAUDE.md, ANSWER BANK).
After y: Applied (dated today, no cap), nothing sent to NUworks; n = stays Approved. `nuauto assist other add <url>
Expand Down
3 changes: 2 additions & 1 deletion docs/GUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ A question from a run (an unknown form field, an option to pick, "did it submit?
needed (Needs Human rows stopped at an external application: the assistant), By hand on NUworks (Needs
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
the assistant's Apply when the posting's link to that site is known (assist.site_target; else a "no
assistant" chip says why), and 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; 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
Expand Down
5 changes: 4 additions & 1 deletion docs/PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ Data comes from NUworks' own search/job data, not page text.
search's postdate) and "featured".
2. Claude triage (Sonnet, prompts/TRIAGE_PROMPT.md, 250 jobs a batch): title, company, place and a snippet;
drops only clearly unrelated roles.
3. details + hard rules (script; one NUworks job page each).
3. details + hard rules (script; one NUworks job page each). Each also keeps the posting's https links (jobs.page_links:
address, what it shows, the text before it; not NUworks / Northeastern ones), so a posting that asks for the company
site too has its link there (jobs.site_link, the assistant's Still owed rows). Details read before 2026-11-02 have
none: every update's check of the sent rows adds them (jobs.keep_links).
4. Claude match % vs your resume (Sonnet, prompts/SCORE_PROMPT.md, 20 jobs a batch). The formula is fixed in the
prompt: 4-8 key requirements, each required or preferred, each met / partial / missing in the resume;
match = 100 * (2 * required met + required partial + preferred met + 0.5 * preferred partial) /
Expand Down
2 changes: 2 additions & 0 deletions docs/REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ THE QUEUE (remote.py; nuauto-assist.timer every 10 minutes runs `nuauto assist a
next run. When it starts sessions, Discord says which.
- By-hand NUworks rows (stopped on NUworks for a cover letter, a transcript...: docs/ASSIST.md) are listed too but never
start by themselves (they need you there: NUworks' sign-in in its browser view, your letter): Start now.
- Still owed rows (Applied, the company site still wanted: docs/ASSIST.md, WHICH ROWS; added 2026-11-02) are listed too,
never start by themselves (remote.SITE_WHY): Start now. They count toward no limit (their NUworks side went out).
- What it starts: Approved rows of the Other jobs tab and Needs Human rows stopped at a company site (not past their
deadline), best first: closing within 7 days (jobs.URGENT_DAYS, soonest first), then match % (the "match NN%" in
Notes), then NUworks rows before Other jobs, then sheet order. Into free slots (assist_slots, 1-3, the same setting
Expand Down
2 changes: 1 addition & 1 deletion docs/SAFETY.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ COMPANY-SITE AGENT (assist.py, prompts/ASSIST_PROMPT.md)
mail.pick, find_code, find_link, about_site, lookup, check_account; assist.email_cmd | test_mail
- Guesses (2026-10-11): recorded with a reason, one line, an option when there are options; never over a saved answer, one given this run, or a leave-blank one; settled only by the user's answer at the end (save / once / confirm / blank / alias); an essay is confirmed, never saved | assist.record_guess, settle, guess_list | test_assist
- Every Submit-type question (click, Enter, type submit) lists the open guesses, the answers the user changed, how many were confirmed, and values typed with no saved answer or guess behind them (the guard's own record of type / fill_form / select_option) | assist.submit_ask, review_lines, unexplained, record_typed | test_assist; that the agent records every guess is prompt-only: the typed-values list is the check
- Rows the agent may take: Needs Human + "External application" (never NUworks, even with --url: check_link), Approved Other jobs rows, and the by-hand rows above | assist.assist_target, other_target, by_hand_target, check_link | test_assist
- Rows the agent may take: Needs Human + "External application" (never NUworks, even with --url: check_link), Approved Other jobs rows, the by-hand rows above, and Applied rows still owing their company site (the posting's own link, never a guess; y only clears the mark, nothing sent on NUworks again; never started by the queue) | assist.assist_target, other_target, by_hand_target, site_target, check_link; jobs.site_link; remote.plan (SITE_WHY) | test_assist, test_jobs, test_remote
- Sheet touched only by the launcher, after you answer y; agent has no sheet access | assist.run (mark_applied_by_hand), tools limited to the list above | test_assist (target rules only; the y/n flow is not tested)
- NUworks side after a company-site submit reuses apply code, outcome appended to Notes | apply.submit_nuworks_side, sheet.append_note | no test (the
NUworks browser busy with another run: "NOT submitted" in Notes, nothing opened, retry allowed: test_apply)
Expand Down
3 changes: 2 additions & 1 deletion prompts/ASSIST_PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ THE USER'S (ask right away: these can't wait for the end list)

When the application is submitted (the user approved Submit and the site confirms), tell the user to type
/exit; the terminal then asks whether it was submitted and updates the sheet (for a company-site job of the NUworks
tab it also submits the job on NUworks; an Other jobs row is not on NUworks; a NUworks row was NUworks itself).
tab it also submits the job on NUworks, unless THIS RUN says that went out already; an Other jobs row is not on
NUworks; a NUworks row was NUworks itself).

Enforced by code (a blocked action comes back as an error; don't look for a way around it, tell the user):
asking before Submit-type clicks and Enter (with every unanswered guess shown to the user); no touching captchas; password boxes take
Expand Down
72 changes: 59 additions & 13 deletions src/nuauto/assist.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,34 @@ def by_hand_target(row):
return row.url, None


def site_target(row, url_override=None):
"""(url, None) if this Applied NUworks-tab row still owes its company-site application (Notes start
sheet.SITE_MARK: the posting asked for both) and the posting's own link to that site is known (jobs.site_link;
--url: yours instead), else (None, reason). The NUworks side has gone out already: the agent works only that
site, and a y at the end clears the mark (sheet.mark_site_done); nothing is sent on NUworks again."""
from nuauto import jobs
from nuauto import sheet
if row.status != "Applied" or not (row.notes or "").startswith(sheet.SITE_MARK):
return None, "not an Applied row whose company site is still owed"
if url_override:
return check_link(url_override)
try:
with open(jobs.path("details", f"{jobs.job_id(row.url)}.json")) as f:
d = json.load(f)
except (OSError, ValueError):
return None, "NUauto has no saved copy of this posting: open it on NUworks and follow its link"
url, why = jobs.site_link(d)
return check_link(url) if url else (None, why)


def site_note(row):
"""THIS RUN's lines for a Still owed row (site_target): the NUworks side went out; what the posting said."""
from nuauto import sheet
said = (row.notes or "")[len(sheet.SITE_MARK):].split("). ", 1)[0].strip(" (")
return ("This job was already submitted on NUworks. Its posting also asks for an application on the company's own "
f"site, which is this run: {said[:200]}\nApply there only; never open NUworks.\n")


NUWORKS_BUSY = "Not now: an assistant is applying on NUworks (a by-hand row): one application at a time there. Finish it first."


Expand Down Expand Up @@ -442,6 +470,8 @@ def review_lines(state):
f"{cut(letter_['paragraphs'][0])!r}), uploaded as {os.path.basename(letter_['pdf'])}.")
if state.get("hand"):
lines.append("This is a NUworks application: it counts toward your weekly limit.")
if state.get("site"):
lines.append("This job is already Applied on NUworks: this is its company-site application (it counts toward no limit).")
return lines


Expand Down Expand Up @@ -1574,14 +1604,20 @@ def attention_main(kind):
# ---------- launcher (nuauto assist) ----------

def find_row(number, url_override):
"""The NUworks tab's row: a company-site application (assist_target), or one that stopped on NUworks for something
only you can give (by_hand_target: the agent works it on NUworks itself). (rows, row, url, by_hand)."""
"""The NUworks tab's row: a company-site application (assist_target), one that stopped on NUworks for something
only you can give (by_hand_target: the agent works it on NUworks itself), or an Applied one whose company site is
still owed (site_target). (rows, row, url, kind): kind "site", "hand" or "" (a company-site application)."""
from nuauto import sheet
ws = sheet.open_worksheet()
rows = sheet.read_rows(ws)
row = next((r for r in rows if r.number == number), None)
if row is not None and row.status == "Applied" and (row.notes or "").startswith(sheet.SITE_MARK):
url, why = site_target(row, url_override)
if why:
sys.exit(f"Row {number} ({row.company}): {why}.")
return rows, row, url, "site"
if row is None or row.status != "Needs Human":
sys.exit(f"Row {number} is not a Needs Human row.")
sys.exit(f"Row {number} is not a Needs Human row (or an Applied one whose company site is still owed).")
if (row.notes or "").startswith("External application"):
url, why = assist_target(row.notes, url_override)
hand = False
Expand All @@ -1592,7 +1628,7 @@ def find_row(number, url_override):
hand = True
if why:
sys.exit(f"Row {number} ({row.company}): {why}.")
return rows, row, url, hand
return rows, row, url, "hand" if hand else ""


def find_other_row(number, url_override):
Expand Down Expand Up @@ -1773,12 +1809,13 @@ def rules_text(answer_cmd):


def context_text(where, row, url, other, resume, read_roots, answer_cmd, writeups=(), view_url=None, transcript=None,
hand=False):
hand=False, site=False):
email = accounts.account_email()
own = accounts.own_login(url) # one of your own logins (imported from Bitwarden) for this site
return (f"\n\nTHIS RUN\n{where.capitalize()}: {row.company} | {row.title}\nPosting: {url}\n"
+ (OTHER_NOTE if other else "")
+ (hand_note(row) if hand else "")
+ (site_note(row) if site else "")
+ ("This browser runs on the user's homelab, not in front of them. They read your messages and answer on "
"their phone (a review page), and they see and use this browser there too (for a captcha: say there is "
"one and wait for them). A Submit-type action waits for their approval on that page: if it comes back "
Expand Down Expand Up @@ -1853,12 +1890,15 @@ def run(number, url_override, other=False):
if other:
rows, row, url = find_other_row(number, url_override)
where = f"{sheet.OTHER_TAB} row {number}"
kind = ""
else:
rows, row, url, hand = find_row(number, url_override)
if not sheet.reapplying(row): # (one being re-applied is the same application: it adds nothing)
rows, row, url, kind = find_row(number, url_override)
# (one being re-applied is the same application: it adds nothing; a Still owed row's went out already)
if not sheet.reapplying(row) and kind != "site":
sheet.check_limits(rows)
where = f"row {number}"
hand = not other and hand
hand = not other and kind == "hand"
site = not other and kind == "site"
if hand: # one application at a time on NUworks: not while an Apply run submits there
from nuauto import browser
holder = browser.profile_holder() or {}
Expand Down Expand Up @@ -1902,7 +1942,7 @@ def run(number, url_override, other=False):
print(f"(Note: {note}.)")
try:
if not hand:
return run_locked(number, other, row, url, where, tab, slot, slot_lock)
return run_locked(number, other, row, url, where, tab, slot, slot_lock, site=site)
from nuauto import remote
try: # one machine submits on NUworks at a time (with NUauto on the homelab, both can apply there)
with remote.nuworks_turn():
Expand All @@ -1917,9 +1957,10 @@ def run(number, url_override, other=False):
lock.close()


def run_locked(number, other, row, url, where, tab, slot, slot_lock, hand=False):
def run_locked(number, other, row, url, where, tab, slot, slot_lock, hand=False, site=False):
"""The session itself, once run() holds this row, a slot and the job site (and, from the laptop, the homelab's
claim): the agent in this terminal, then "Did you press Submit?". hand: a by-hand row, worked on NUworks itself."""
claim): the agent in this terminal, then "Did you press Submit?". hand: a by-hand row, worked on NUworks itself;
site: an Applied row whose company site is still owed (site_target)."""
from nuauto import browser
from nuauto import sheet
log_ = browser.RunLog(f"assist_{'other_' if other else ''}row{number}")
Expand Down Expand Up @@ -1949,7 +1990,7 @@ def run_locked(number, other, row, url, where, tab, slot, slot_lock, hand=False)
"transcript_src": os.path.realpath(transcript_src) if transcript else None, "transcript": transcript,
"company": row.company, "job_title": row.title, "job_url": url, "row_url": row.url, "password_sites": [],
"guesses": {}, "typed": {}, "account_email": accounts.account_email() or "", "where": where,
"slot": slot, "view_url": view.url if view else None, "tmux": tmux_session() if view else None, "hand": hand,
"slot": slot, "view_url": view.url if view else None, "tmux": tmux_session() if view else None, "hand": hand, "site": site,
# the pane this runs in: the review page types only there (never another window of that tmux session)
"tmux_pane": os.environ.get("TMUX_PANE") if view and re.fullmatch(r"%\d+", os.environ.get("TMUX_PANE", "")) else None}
with open(os.path.join(d, "state.json"), "w") as f:
Expand All @@ -1959,7 +2000,8 @@ def run_locked(number, other, row, url, where, tab, slot, slot_lock, hand=False)
if hand and not config.DEMO:
seed_nuworks(profile, d, log_)
cmd = session(d, answer_cmd, read_roots, profile, rules_text(answer_cmd) + context_text(
where, row, url, other, resume, read_roots, answer_cmd, writeups, view.url if view else None, transcript, hand), where,
where, row, url, other, resume, read_roots, answer_cmd, writeups, view.url if view else None, transcript, hand,
site), where,
view.display if view else None)
log_.write(f"{where.capitalize()}: {row.company} | {row.title} -> {host(url)} (assistant slot {slot} of {slots()})")
if view: # the password only here, on your screen: never in a log or a Discord message
Expand Down Expand Up @@ -2007,6 +2049,10 @@ def run_locked(number, other, row, url, where, tab, slot, slot_lock, hand=False)
log_.write(f"Not submitted; {where} stays {row.status}.")
return
ws = sheet.open_other() if other else sheet.open_worksheet()
if site: # the NUworks side went out before: the company site was the part still owed
sheet.mark_site_done(ws, row.number, row.url)
log_.write(f"{where.capitalize()}: company site marked done (it stays Applied; nothing sent on NUworks again).")
return
sheet.mark_applied_by_hand(ws, row.number, row.url, how="on NUworks (nuauto assist; you pressed Submit)" if hand else
"on the company site (nuauto assist; you pressed Submit)")
log_.write(f"{where.capitalize()} marked Applied (dated today).")
Expand Down
Loading
Loading