divapply 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- divapply/__init__.py +3 -0
- divapply/__main__.py +5 -0
- divapply/apply/__init__.py +1 -0
- divapply/apply/chrome.py +360 -0
- divapply/apply/dashboard.py +203 -0
- divapply/apply/launcher.py +1404 -0
- divapply/apply/prompt.py +942 -0
- divapply/cli.py +765 -0
- divapply/config/coursework.seed.json +560 -0
- divapply/config/employers.yaml +31 -0
- divapply/config/searches.example.yaml +113 -0
- divapply/config/sites.yaml +107 -0
- divapply/config.py +445 -0
- divapply/database.py +635 -0
- divapply/discovery/__init__.py +0 -0
- divapply/discovery/jobspy.py +557 -0
- divapply/discovery/smartextract.py +1268 -0
- divapply/discovery/workday.py +575 -0
- divapply/enrichment/__init__.py +0 -0
- divapply/enrichment/detail.py +1003 -0
- divapply/llm.py +367 -0
- divapply/pipeline.py +551 -0
- divapply/scoring/__init__.py +1 -0
- divapply/scoring/cover_letter.py +337 -0
- divapply/scoring/pdf.py +789 -0
- divapply/scoring/scorer.py +230 -0
- divapply/scoring/tailor.py +800 -0
- divapply/scoring/ultimate.py +214 -0
- divapply/scoring/validator.py +374 -0
- divapply/social.py +1324 -0
- divapply/view.py +407 -0
- divapply/wizard/__init__.py +0 -0
- divapply/wizard/init.py +396 -0
- divapply-0.4.2.dist-info/METADATA +278 -0
- divapply-0.4.2.dist-info/RECORD +38 -0
- divapply-0.4.2.dist-info/WHEEL +4 -0
- divapply-0.4.2.dist-info/entry_points.txt +2 -0
- divapply-0.4.2.dist-info/licenses/LICENSE +661 -0
divapply/apply/prompt.py
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
"""Prompt builder for the autonomous job application agent.
|
|
2
|
+
|
|
3
|
+
Constructs the full instruction prompt that tells Claude Code / the AI agent
|
|
4
|
+
how to fill out a job application form using Playwright MCP tools. All
|
|
5
|
+
personal data is loaded from the user's profile -- nothing is hardcoded.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from divapply import config
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_profile_summary(profile: dict) -> str:
|
|
20
|
+
"""Format the applicant profile section of the prompt.
|
|
21
|
+
|
|
22
|
+
Reads all relevant fields from the profile dict and returns a
|
|
23
|
+
human-readable multi-line summary for the agent.
|
|
24
|
+
"""
|
|
25
|
+
p = profile
|
|
26
|
+
personal = p.get("personal", {})
|
|
27
|
+
work_auth = p.get("work_authorization", {})
|
|
28
|
+
comp = p.get("compensation", {})
|
|
29
|
+
exp = p.get("experience", {})
|
|
30
|
+
avail = p.get("availability", {})
|
|
31
|
+
eeo = p.get("eeo_voluntary", {})
|
|
32
|
+
|
|
33
|
+
lines = [
|
|
34
|
+
f"Name: {personal['full_name']}",
|
|
35
|
+
f"Email: {personal['email']}",
|
|
36
|
+
f"Phone: {personal['phone']}",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Address -- handle optional fields gracefully
|
|
40
|
+
addr_parts = [
|
|
41
|
+
personal.get("address", ""),
|
|
42
|
+
personal.get("city", ""),
|
|
43
|
+
personal.get("province_state", ""),
|
|
44
|
+
personal.get("country", ""),
|
|
45
|
+
personal.get("postal_code", ""),
|
|
46
|
+
]
|
|
47
|
+
lines.append(f"Address: {', '.join(p for p in addr_parts if p)}")
|
|
48
|
+
|
|
49
|
+
if personal.get("linkedin_url"):
|
|
50
|
+
lines.append(f"LinkedIn: {personal['linkedin_url']}")
|
|
51
|
+
if personal.get("github_url"):
|
|
52
|
+
lines.append(f"GitHub: {personal['github_url']}")
|
|
53
|
+
if personal.get("portfolio_url"):
|
|
54
|
+
lines.append(f"Portfolio: {personal['portfolio_url']}")
|
|
55
|
+
if personal.get("website_url"):
|
|
56
|
+
lines.append(f"Website: {personal['website_url']}")
|
|
57
|
+
|
|
58
|
+
# Work authorization
|
|
59
|
+
lines.append(f"Work Auth: {work_auth.get('legally_authorized_to_work', 'See profile')}")
|
|
60
|
+
lines.append(f"Sponsorship Needed: {work_auth.get('require_sponsorship', 'See profile')}")
|
|
61
|
+
if work_auth.get("work_permit_type"):
|
|
62
|
+
lines.append(f"Work Permit: {work_auth['work_permit_type']}")
|
|
63
|
+
|
|
64
|
+
# Compensation
|
|
65
|
+
currency = comp.get("salary_currency", "USD")
|
|
66
|
+
lines.append(f"Salary Expectation: ${comp['salary_expectation']} {currency}")
|
|
67
|
+
|
|
68
|
+
# Experience
|
|
69
|
+
if exp.get("years_of_experience_total"):
|
|
70
|
+
lines.append(f"Years Experience: {exp['years_of_experience_total']}")
|
|
71
|
+
if exp.get("education_level"):
|
|
72
|
+
lines.append(f"Education: {exp['education_level']}")
|
|
73
|
+
|
|
74
|
+
# Availability
|
|
75
|
+
lines.append(f"Available: {avail.get('earliest_start_date', 'Immediately')}")
|
|
76
|
+
|
|
77
|
+
# Standard responses
|
|
78
|
+
lines.extend([
|
|
79
|
+
"Age 18+: Yes",
|
|
80
|
+
"Background Check: Yes",
|
|
81
|
+
"Felony: No",
|
|
82
|
+
"Previously Worked Here: No",
|
|
83
|
+
"How Heard: Online Job Board",
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
# EEO
|
|
87
|
+
lines.append(f"Gender: {eeo.get('gender', 'Decline to self-identify')}")
|
|
88
|
+
lines.append(f"Race: {eeo.get('race_ethnicity', 'Decline to self-identify')}")
|
|
89
|
+
lines.append(f"Veteran: {eeo.get('veteran_status', 'I am not a protected veteran')}")
|
|
90
|
+
lines.append(f"Disability: {eeo.get('disability_status', 'I do not wish to answer')}")
|
|
91
|
+
|
|
92
|
+
# Education schools
|
|
93
|
+
edu_schools = p.get("education_schools", [])
|
|
94
|
+
if edu_schools:
|
|
95
|
+
lines.append("\n== EDUCATION (list ALL schools in this order on forms) ==")
|
|
96
|
+
for i, sch in enumerate(edu_schools, 1):
|
|
97
|
+
degree_status = "Yes" if sch.get("degree_received") else "No (in progress)" if sch.get("end_year") == "present" else "No"
|
|
98
|
+
lines.append(
|
|
99
|
+
f"School {i}: {sch['school']} | {sch['city_state']} | "
|
|
100
|
+
f"Major: {sch['major']} | Minor: {sch.get('minor','N/A')} | "
|
|
101
|
+
f"Degree: {sch['degree']} | Received: {degree_status} | "
|
|
102
|
+
f"Units: {sch['units']} {sch.get('units_type','Semester')} | GPA: {sch.get('gpa','N/A')} | "
|
|
103
|
+
f"{sch['start_year']}–{sch['end_year']}"
|
|
104
|
+
)
|
|
105
|
+
school_names = ", ".join(s["school"] for s in edu_schools)
|
|
106
|
+
lines.append(f"IMPORTANT: Always enter ALL {len(edu_schools)} schools ({school_names}). Add schools if needed using 'Add Another School'.")
|
|
107
|
+
|
|
108
|
+
# Employer addresses
|
|
109
|
+
emp_addrs = p.get("employer_addresses", {})
|
|
110
|
+
if emp_addrs:
|
|
111
|
+
lines.append("\n== EMPLOYER ADDRESSES (use when work history forms require an address) ==")
|
|
112
|
+
for employer, addr in emp_addrs.items():
|
|
113
|
+
lines.append(f"{employer}: {addr}")
|
|
114
|
+
|
|
115
|
+
# Supplemental answers
|
|
116
|
+
supplemental = p.get("supplemental_answers", {})
|
|
117
|
+
if supplemental:
|
|
118
|
+
lines.append("\n== PRE-WRITTEN ANSWERS (use these verbatim for matching questions) ==")
|
|
119
|
+
for key, val in supplemental.items():
|
|
120
|
+
lines.append(f"{key}: {val}")
|
|
121
|
+
|
|
122
|
+
# Question bank — covers common government/ATS questions
|
|
123
|
+
qbank = p.get("question_bank", {})
|
|
124
|
+
if qbank:
|
|
125
|
+
lines.append("\n== QUESTION BANK (use for any question that matches) ==")
|
|
126
|
+
lines.append("When you encounter any supplemental, screening, or agency question, find the closest match below and use that answer. Do not leave questions blank or guess randomly.")
|
|
127
|
+
for key, val in qbank.items():
|
|
128
|
+
label = key.replace("_", " ").title()
|
|
129
|
+
lines.append(f"{label}: {val}")
|
|
130
|
+
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_location_check(profile: dict, search_config: dict) -> str:
|
|
135
|
+
"""Build the location eligibility check section of the prompt.
|
|
136
|
+
|
|
137
|
+
Uses the accept_patterns from search config to determine which cities
|
|
138
|
+
are acceptable for hybrid/onsite roles.
|
|
139
|
+
"""
|
|
140
|
+
personal = profile["personal"]
|
|
141
|
+
location_cfg = search_config.get("location", {})
|
|
142
|
+
accept_patterns = location_cfg.get("accept_patterns", [])
|
|
143
|
+
primary_city = personal.get("city", location_cfg.get("primary", "your city"))
|
|
144
|
+
|
|
145
|
+
# Build the list of acceptable cities for hybrid/onsite
|
|
146
|
+
if accept_patterns:
|
|
147
|
+
city_list = ", ".join(accept_patterns)
|
|
148
|
+
else:
|
|
149
|
+
city_list = primary_city
|
|
150
|
+
|
|
151
|
+
return f"""== LOCATION CHECK (do this FIRST before any form) ==
|
|
152
|
+
Read the job page. Determine the work arrangement. Then decide:
|
|
153
|
+
- "Remote" or "work from anywhere" -> ELIGIBLE. Apply.
|
|
154
|
+
- "Hybrid" or "onsite" in {city_list} -> ELIGIBLE. Apply.
|
|
155
|
+
- "Hybrid" or "onsite" in another city BUT the posting also says "remote OK" or "remote option available" -> ELIGIBLE. Apply.
|
|
156
|
+
- "Onsite only" or "hybrid only" in any city outside the list above with NO remote option -> NOT ELIGIBLE. Stop immediately. Output RESULT:FAILED:not_eligible_location
|
|
157
|
+
- City is overseas (India, Philippines, Europe, etc.) with no remote option -> NOT ELIGIBLE. Output RESULT:FAILED:not_eligible_location
|
|
158
|
+
- Cannot determine location -> Continue applying. If a screening question reveals it's non-local onsite, answer honestly and let the system reject if needed.
|
|
159
|
+
Do NOT fill out forms for jobs that are clearly onsite in a non-acceptable location. Check EARLY, save time."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _build_salary_section(profile: dict) -> str:
|
|
163
|
+
"""Build the salary negotiation instructions.
|
|
164
|
+
|
|
165
|
+
Adapts floor, range, and currency from the profile's compensation section.
|
|
166
|
+
"""
|
|
167
|
+
comp = profile["compensation"]
|
|
168
|
+
currency = comp.get("salary_currency", "USD")
|
|
169
|
+
floor = comp["salary_expectation"]
|
|
170
|
+
range_min = comp.get("salary_range_min", floor)
|
|
171
|
+
range_max = comp.get("salary_range_max", str(int(floor) + 20000) if floor.isdigit() else floor)
|
|
172
|
+
conversion_note = comp.get("currency_conversion_note", "")
|
|
173
|
+
|
|
174
|
+
# Compute example hourly rates at 3 salary levels
|
|
175
|
+
try:
|
|
176
|
+
floor_int = int(floor)
|
|
177
|
+
examples = [
|
|
178
|
+
(f"${floor_int // 1000}K", floor_int // 2080),
|
|
179
|
+
(f"${(floor_int + 25000) // 1000}K", (floor_int + 25000) // 2080),
|
|
180
|
+
(f"${(floor_int + 55000) // 1000}K", (floor_int + 55000) // 2080),
|
|
181
|
+
]
|
|
182
|
+
hourly_line = ", ".join(f"{sal} = ${hr}/hr" for sal, hr in examples)
|
|
183
|
+
except (ValueError, TypeError):
|
|
184
|
+
hourly_line = "Divide annual salary by 2080"
|
|
185
|
+
|
|
186
|
+
# Currency conversion guidance
|
|
187
|
+
if conversion_note:
|
|
188
|
+
convert_line = f"Posting is in a different currency? -> {conversion_note}"
|
|
189
|
+
else:
|
|
190
|
+
convert_line = "Posting is in a different currency? -> Target midpoint of their range. Convert if needed."
|
|
191
|
+
|
|
192
|
+
return f"""== SALARY (think, don't just copy) ==
|
|
193
|
+
${floor} {currency} is the FLOOR. Never go below it. But don't always use it either.
|
|
194
|
+
|
|
195
|
+
Decision tree:
|
|
196
|
+
1. Job posting shows a range (e.g. "$120K-$160K")? -> Answer with the MIDPOINT ($140K).
|
|
197
|
+
2. Title says Senior, Staff, Lead, Principal, Architect, or level II/III/IV? -> Minimum $110K {currency}. Use midpoint of posted range if higher.
|
|
198
|
+
3. {convert_line}
|
|
199
|
+
4. No salary info anywhere? -> Use ${floor} {currency}.
|
|
200
|
+
5. Asked for a range? -> Give posted midpoint minus 10% to midpoint plus 10%. No posted range? -> "${range_min}-${range_max} {currency}".
|
|
201
|
+
6. Hourly rate? -> Divide your annual answer by 2080. ({hourly_line})"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_screening_section(profile: dict) -> str:
|
|
205
|
+
"""Build the screening questions guidance section."""
|
|
206
|
+
personal = profile["personal"]
|
|
207
|
+
exp = profile.get("experience", {})
|
|
208
|
+
city = personal.get("city", "their city")
|
|
209
|
+
years = exp.get("years_of_experience_total", "multiple")
|
|
210
|
+
target_role = exp.get("target_role", personal.get("current_job_title", "software engineer"))
|
|
211
|
+
work_auth = profile["work_authorization"]
|
|
212
|
+
|
|
213
|
+
return f"""== SCREENING QUESTIONS (be strategic) ==
|
|
214
|
+
Hard facts -> answer truthfully from the profile. No guessing. This includes:
|
|
215
|
+
- Location/relocation: lives in {city}, cannot relocate
|
|
216
|
+
- Work authorization: {work_auth.get('legally_authorized_to_work', 'see profile')}
|
|
217
|
+
- Citizenship, clearance, licenses, certifications: answer from profile only
|
|
218
|
+
- Criminal/background: answer from profile only
|
|
219
|
+
|
|
220
|
+
Skills and tools -> be confident. This candidate is a {target_role} with {years} years experience. If the question asks "Do you have experience with [tool]?" and it's in the same domain (DevOps, backend, ML, cloud, automation), answer YES. Software engineers learn tools fast. Don't sell short.
|
|
221
|
+
|
|
222
|
+
ABSOLUTE RULE: NEVER leave ANY required field blank. NEVER click Next or Submit with unanswered required fields.
|
|
223
|
+
- If a field has "Error: This field is required" or an asterisk (*), it MUST be filled before proceeding.
|
|
224
|
+
- If you don't know the exact answer, use the closest match from the profile/question bank.
|
|
225
|
+
- If you genuinely have no relevant experience for a question, say so honestly but connect transferable skills.
|
|
226
|
+
|
|
227
|
+
Open-ended / essay questions -> NEVER leave a required text field blank. You MUST write an answer. Rules:
|
|
228
|
+
1. Read the question carefully. Write 2-4 sentences directly answering it.
|
|
229
|
+
2. Draw from the resume and job description. Be specific -- name real experiences.
|
|
230
|
+
3. No generic fluff. No "I am passionate about..." Start with a fact or action.
|
|
231
|
+
4. If the question is about an area where the candidate has direct experience, lead with that.
|
|
232
|
+
5. If the question is about an area where experience is indirect, connect transferable skills honestly.
|
|
233
|
+
6. If the candidate has NO experience in the area asked, be honest but frame it constructively: "While I do not have direct [X] experience, I bring [transferable skill] from [real experience]." NEVER leave it blank.
|
|
234
|
+
|
|
235
|
+
BACKGROUND FOR COMMUNITY/RECREATION/SENIOR PROGRAM QUESTIONS:
|
|
236
|
+
- Candidate currently works at City of Roseville Parks, Recreation & Libraries -- direct public recreation experience
|
|
237
|
+
- Serves the public daily at a high-volume municipal counter across Parks, Recreation, and Libraries divisions
|
|
238
|
+
- Has processed program registrations, facility permits, and fee payments for recreation programs
|
|
239
|
+
- Public Health degree (in progress) includes coursework relevant to community health and services
|
|
240
|
+
- Government operations experience spans multiple departments and age groups
|
|
241
|
+
Use this context when answering questions about senior programs, recreation coordination, community engagement, or working with the public.
|
|
242
|
+
|
|
243
|
+
CalPERS questions -> "Are you a CalPERS member?" = No. "Are you a CalPERS retiree?" = No.
|
|
244
|
+
Previously employed here -> No (unless the job site matches current/former employers in the profile).
|
|
245
|
+
Related to employee -> No.
|
|
246
|
+
Under 18 / work permit -> N/A (candidate is an adult).
|
|
247
|
+
Acknowledge salary / background check checkboxes -> Always check/acknowledge these.
|
|
248
|
+
|
|
249
|
+
RADIO BUTTON + CONDITIONAL TEXT BOX PATTERN (extremely common on government forms):
|
|
250
|
+
Many questions are a Yes/No radio followed by a text box. The text box may say "If yes, explain", "If no, put N/A", or it may say NOTHING at all — just a blank text box sitting below the radio.
|
|
251
|
+
RULES:
|
|
252
|
+
- ALWAYS select a radio button. Never leave a radio group unselected.
|
|
253
|
+
- After selecting a radio, look at ANY text box immediately below it and fill it:
|
|
254
|
+
* Selected NO + text box present (regardless of what the label says) -> type N/A
|
|
255
|
+
* Selected YES + text box present -> fill with the real relevant information
|
|
256
|
+
- Do NOT assume a text box is optional just because it doesn't say "required" — fill it anyway
|
|
257
|
+
- Even if the text box appears grayed out or is not marked required, fill it with N/A when you answered No
|
|
258
|
+
- The text box may be on the NEXT line, indented, or appear after a follow-up question number — scroll carefully and check every field after each radio answer
|
|
259
|
+
|
|
260
|
+
COMMON RADIO ANSWERS for government applications:
|
|
261
|
+
- "Have you ever been employed here before?" -> No -> text box = N/A
|
|
262
|
+
- "Are you related to a current employee?" -> No -> text box = N/A
|
|
263
|
+
- "Are you a CalPERS member/retiree?" -> No
|
|
264
|
+
- "Are you under 18?" -> No (or N/A on permit field)
|
|
265
|
+
- "Do you have a valid driver's license?" -> Yes (candidate has a license)
|
|
266
|
+
- "Are you willing to work overtime?" -> Yes
|
|
267
|
+
- "Are you willing to work weekends/evenings?" -> Yes
|
|
268
|
+
- Shift availability checkboxes -> Check ALL options available: Day ✅, Evening ✅, Nights ✅, Weekends ✅, Holidays ✅ — select everything, candidate is fully flexible
|
|
269
|
+
- "Where did you hear about this position?" -> GovernmentJobs Website / Internet / Other (pick whichever matches the site)
|
|
270
|
+
|
|
271
|
+
EEO / Voluntary Self-Identification / Agency Questions -> Use the REAL answers below. Do NOT select "Decline to state" or "Prefer not to say":
|
|
272
|
+
- Gender: Male
|
|
273
|
+
- Preferred Pronoun: he/him
|
|
274
|
+
- Race/Ethnicity: White (Not Hispanic or Latino) — pick whichever option matches "White" and is NOT Hispanic/Latino
|
|
275
|
+
- Veteran status: I am not a protected veteran / Not a veteran / No (select whatever option means not a veteran)
|
|
276
|
+
- Disability status: I do not have a disability / No disability (do NOT select "Decline to state" or "I don't wish to answer")
|
|
277
|
+
- Related to employee at this agency: No -> text box = N/A
|
|
278
|
+
- Currently employed at this agency: No
|
|
279
|
+
- How did you hear about this position: GovernmentJobs.com (or closest match like "Internet", "Online Job Board", "Government Jobs Website")
|
|
280
|
+
- If "Other" for how heard: leave blank or type "Online Job Board"
|
|
281
|
+
- Tribal affiliation: N/A
|
|
282
|
+
These are voluntary disclosures — always fill them with the real answer, never decline.
|
|
283
|
+
CRITICAL: Agency Questions sections often appear BEFORE supplemental questions. Fill ALL of them. Do not skip any.
|
|
284
|
+
|
|
285
|
+
EDUCATION FORM RULES (applies to all ATS / government application education sections):
|
|
286
|
+
The candidate's education path: UNR (first) → TMCC (transferred, got Associate's) → University of the Cumberlands (current/highest, pursuing Bachelor's).
|
|
287
|
+
"Highest education" = University of the Cumberlands (B.S. in progress). On dropdowns, select "Bachelor's Degree" or "Some College" — never stop at "Associate's" even though that's the highest completed.
|
|
288
|
+
Always enter all THREE schools, most recent first:
|
|
289
|
+
1. University of the Cumberlands — Williamsburg, KY — Major: Public Health — Minor: Business — Degree: Bachelor of Science (in progress) — Degree received: No — ~101 semester units — GPA 3.692 — 2024–present
|
|
290
|
+
2. Truckee Meadows Community College — Reno, NV — Major: General Studies — Minor: Business — Degree: Associate of General Studies — Degree received: Yes (May 2024) — 17 semester units — GPA 2.92 — 2023–2024
|
|
291
|
+
3. University of Nevada, Reno — Reno, NV — Major: Community Health Sciences — Minor: Business — Degree: Bachelor of Science (not completed) — Degree received: No — 71 semester units — GPA 3.358 — 2019–2022
|
|
292
|
+
If the form only allows 2 schools, enter #1 and #2 and skip #3.
|
|
293
|
+
If the form has an "Add Another School" or "+ Add Education" button, click it to add the third entry.
|
|
294
|
+
Never leave UNR out if there is room for it.
|
|
295
|
+
|
|
296
|
+
CIVIL SERVICE / GOVERNMENT SUPPLEMENTAL QUESTIONNAIRE RULES:
|
|
297
|
+
Government agencies (NEOGOV, GovernmentJobs, Workday government portals) often have a dedicated "Supplemental Questions" page. These are MANDATORY — you cannot submit without answering all of them.
|
|
298
|
+
|
|
299
|
+
== SPEED STRATEGY: DO THIS IN 3 BROWSER ACTIONS, NOT 50 ==
|
|
300
|
+
|
|
301
|
+
ACTION 1 — READ THE WHOLE PAGE AT ONCE:
|
|
302
|
+
Use browser_evaluate to extract ALL question text and ALL option text in one shot:
|
|
303
|
+
browser_evaluate function: () => {{
|
|
304
|
+
const out = [];
|
|
305
|
+
document.querySelectorAll('input[type=checkbox],input[type=radio],textarea,select').forEach(el => {{
|
|
306
|
+
const lbl = el.labels?.[0]?.textContent?.trim() || el.closest('label')?.textContent?.trim() || el.name || el.id;
|
|
307
|
+
out.push({{ type: el.type || el.tagName, name: el.name, id: el.id, label: lbl, value: el.value }});
|
|
308
|
+
}});
|
|
309
|
+
return out;
|
|
310
|
+
}}
|
|
311
|
+
Read the result. Now you know EVERY input on the page with its id/name. Plan all answers before touching anything.
|
|
312
|
+
|
|
313
|
+
ACTION 2 — RUN THIS EXACT JavaScript (browser_evaluate) — it handles ALL checkboxes and ALL text areas on the page in one call:
|
|
314
|
+
browser_evaluate function: () => {{
|
|
315
|
+
const results = {{}};
|
|
316
|
+
// --- CHECKBOX HELPER: click any checkbox whose label contains any of the given strings ---
|
|
317
|
+
function checkAll(partials) {{
|
|
318
|
+
let n = 0;
|
|
319
|
+
document.querySelectorAll('input[type=checkbox]').forEach(el => {{
|
|
320
|
+
const lbl = (el.labels?.[0]?.textContent || el.closest('label')?.textContent || document.querySelector('label[for="'+el.id+'"]')?.textContent || '').toLowerCase();
|
|
321
|
+
if (partials.some(p => lbl.includes(p.toLowerCase())) && !el.checked) {{ el.click(); n++; }}
|
|
322
|
+
}});
|
|
323
|
+
return n;
|
|
324
|
+
}}
|
|
325
|
+
// --- RADIO HELPER: click radio whose label best matches ---
|
|
326
|
+
function clickRadio(partial) {{
|
|
327
|
+
for (const el of document.querySelectorAll('input[type=radio]')) {{
|
|
328
|
+
const lbl = (el.labels?.[0]?.textContent || el.closest('label')?.textContent || '').toLowerCase();
|
|
329
|
+
if (lbl.includes(partial.toLowerCase()) && !el.checked) {{ el.click(); return true; }}
|
|
330
|
+
}}
|
|
331
|
+
return false;
|
|
332
|
+
}}
|
|
333
|
+
// --- TEXTAREA HELPER: fill first textarea whose id/name/nearby-label contains partial ---
|
|
334
|
+
function fillArea(partial, text) {{
|
|
335
|
+
for (const el of document.querySelectorAll('textarea')) {{
|
|
336
|
+
const key = (el.id + ' ' + el.name + ' ' + (el.closest('[class*="question"],[class*="Question"],[class*="item"]')?.textContent||'')).toLowerCase();
|
|
337
|
+
if (key.includes(partial.toLowerCase())) {{
|
|
338
|
+
el.value = text; el.dispatchEvent(new Event('input',{{bubbles:true}})); el.dispatchEvent(new Event('change',{{bubbles:true}})); return true;
|
|
339
|
+
}}
|
|
340
|
+
}}
|
|
341
|
+
// fallback: fill first empty textarea
|
|
342
|
+
for (const el of document.querySelectorAll('textarea')) {{ if (!el.value.trim()) {{ el.value = text; el.dispatchEvent(new Event('input',{{bubbles:true}})); el.dispatchEvent(new Event('change',{{bubbles:true}})); return true; }} }}
|
|
343
|
+
return false;
|
|
344
|
+
}}
|
|
345
|
+
|
|
346
|
+
// === IT ENVIRONMENTS / TOOLS (Q04 equivalent — 21 options) ===
|
|
347
|
+
results.it_env = checkAll(['applications development','cloud applications','computer hardware','desktop operating system','enterprise resource planning','help desk','service desk','i.t. security','it security','microsoft office','network connectivity','remote assistance','server applications','server operating system','training end users','other']);
|
|
348
|
+
|
|
349
|
+
// === IT SUPPORT EXPERIENCE (Q05 equivalent) ===
|
|
350
|
+
results.it_support = checkAll(['deploying devices','end user technical support','installing','configuring','monitoring applications','monitoring devices','file backup','service request','user account administration']);
|
|
351
|
+
|
|
352
|
+
// === FORMAL TRAINING (Q03 equivalent) ===
|
|
353
|
+
results.training = checkAll(['business software','computer hardware diagnostic','computer operating system','computer programming','customer service','cybersecurity','enterprise resource planning','network operations','server operations','other']);
|
|
354
|
+
|
|
355
|
+
// === DOCUMENTATION EXPERIENCE (Q07 equivalent) ===
|
|
356
|
+
results.docs = checkAll(['application software operation','equipment operating','program documentation','training materials','troubleshooting procedure','other']);
|
|
357
|
+
|
|
358
|
+
// === YEARS EXPERIENCE RADIO — pick "3 years or more" or highest bracket ===
|
|
359
|
+
results.yrs = clickRadio('3 years or more') || clickRadio('3 or more') || clickRadio('more than 2');
|
|
360
|
+
|
|
361
|
+
// === ACKNOWLEDGMENTS — drug test, read requirements, "I have read" ===
|
|
362
|
+
results.ack = checkAll(['i have read','i understand']) + (clickRadio('yes') ? 1 : 0);
|
|
363
|
+
|
|
364
|
+
// === CAREER FAIR RADIO ===
|
|
365
|
+
results.fair = clickRadio('have not attended') || clickRadio('not attended');
|
|
366
|
+
|
|
367
|
+
// === CAREER FAIR TEXT ===
|
|
368
|
+
results.fairText = fillArea('career fair', 'None') || fillArea('name of event', 'None') || fillArea('event', 'None');
|
|
369
|
+
|
|
370
|
+
// === ESSAY / NARRATIVE (Q08) — fill with candidate experience ===
|
|
371
|
+
const essay = `Job Title: Customer Service Specialist
|
|
372
|
+
Employer: City of Roseville
|
|
373
|
+
Department/Unit: Parks, Recreation & Libraries
|
|
374
|
+
Dates: September 2025 – Present
|
|
375
|
+
Duties: Tier 1 IT support for public kiosks and payment terminals via SSH and remote desktop tools. Active Directory user and group account administration. Documented recurring technical issues and authored a troubleshooting reference guide for front desk staff, reducing repeat IT escalations. High-volume transaction processing and real-time system error resolution at public counter.
|
|
376
|
+
|
|
377
|
+
Job Title: Senior Accounting Assistant
|
|
378
|
+
Employer: Nevada County Treasurer-Tax Collector
|
|
379
|
+
Department/Unit: Treasurer-Tax Collector
|
|
380
|
+
Dates: January 2025 – May 2025
|
|
381
|
+
Duties: Operated Megabyte Property Tax System and Workday ERP to process property tax transactions and vendor payments. Researched and resolved data discrepancies in the automated system. Maintained audit-ready documentation of financial workflows. Assisted staff with ERP navigation, data entry procedures, and issue resolution.
|
|
382
|
+
|
|
383
|
+
Self-Directed IT Training – Home Lab Administration (3+ years, ongoing):
|
|
384
|
+
Administer Oracle Cloud Infrastructure running Ubuntu and Oracle Linux 9 servers with Docker containerization. Configure DNS, DHCP, SSH tunnels, port forwarding, and firewall rules. Deploy and monitor web server, media server, and application server environments. File backup and recovery via NAS. Python and Bash automation scripting. PC hardware builds, POST diagnostics, BIOS recovery. Active Directory user account management.
|
|
385
|
+
|
|
386
|
+
Education:
|
|
387
|
+
- B.S. Public Health (Business Administration minor) — University of the Cumberlands, GPA 3.692, in progress (2024–present)
|
|
388
|
+
- A.A. General Studies — Truckee Meadows Community College (May 2024)
|
|
389
|
+
- 71 semester credits — University of Nevada, Reno (2019–2022)`;
|
|
390
|
+
results.essay = fillArea('describe', essay) || fillArea('detail', essay) || fillArea('experience', essay) || fillArea('education', essay);
|
|
391
|
+
|
|
392
|
+
return results;
|
|
393
|
+
}}
|
|
394
|
+
|
|
395
|
+
ACTION 3 — browser_take_screenshot to verify checkboxes are selected. If any results show 0 or false, re-run that specific section. Then scroll full page to find any remaining "Error: This field is required" messages and fix them before clicking Next.
|
|
396
|
+
|
|
397
|
+
== WHAT TO ANSWER FOR EACH QUESTION TYPE ==
|
|
398
|
+
|
|
399
|
+
ACKNOWLEDGMENT / "I have read..." -> clickByLabel("I have read") or select the only checkbox/radio.
|
|
400
|
+
|
|
401
|
+
MINIMUM QUALIFICATIONS (single radio — pick best fit):
|
|
402
|
+
Select the option that is strictly supported by the profile and job history. Do not exaggerate experience or education.
|
|
403
|
+
|
|
404
|
+
"SELECT ALL THAT APPLY" checkbox questions — NEVER assume a group is "partially complete". Always select every applicable box.
|
|
405
|
+
CRITICAL: Do NOT skip a question because it appears to have some boxes checked. Verify and complete it.
|
|
406
|
+
|
|
407
|
+
IT ENVIRONMENTS / TOOLS / TRAINING — YES only when explicitly supported by the profile or transcript knowledge. If uncertain, leave it as NO.
|
|
408
|
+
|
|
409
|
+
IT SUPPORT EXPERIENCE (common question type about what support tasks you have done) — YES only for tasks explicitly supported by the profile, transcripts, or resume. Otherwise NO.
|
|
410
|
+
|
|
411
|
+
NO for all checkbox questions: any skill, system, or certification that is not supported by the profile. "None of the above" only if it is the best factual choice.
|
|
412
|
+
|
|
413
|
+
YEARS OF EXPERIENCE radios:
|
|
414
|
+
IT-specific: 3+ years -> select "3 years or more" or highest bracket
|
|
415
|
+
General experience: 8 years -> select highest available bracket
|
|
416
|
+
|
|
417
|
+
NARRATIVE / ESSAY text areas — write inline, do NOT leave blank:
|
|
418
|
+
City of Roseville, Parks Recreation & Libraries | Customer Service Specialist | Sept 2025–Present:
|
|
419
|
+
Tier 1 IT support for public kiosks and payment terminals via SSH and remote tools. Active Directory user/group administration. Documented recurring issues; authored troubleshooting reference guide for front desk staff. High-volume transaction processing and real-time system error resolution.
|
|
420
|
+
|
|
421
|
+
Nevada County Treasurer-Tax Collector | Senior Accounting Assistant | Jan–May 2025:
|
|
422
|
+
Operated Megabyte Property Tax System and Workday ERP for property tax processing and vendor payments. Researched and resolved discrepancies in automated financial system records. Maintained audit-ready documentation of workflows. Assisted staff with ERP navigation and data entry.
|
|
423
|
+
|
|
424
|
+
Self-Directed Home Lab (3+ years ongoing):
|
|
425
|
+
Administer Oracle Cloud Infrastructure running Ubuntu/Oracle Linux 9 servers with Docker containers. Configure DNS, DHCP, SSH tunnels, port forwarding, and firewall rules. Deploy and monitor applications (web server, Jellyfin, game servers). NAS backup and recovery. Python/Bash automation scripting. PC builds, POST diagnostics, BIOS recovery.
|
|
426
|
+
|
|
427
|
+
Education: B.S. Public Health (Business minor) — University of the Cumberlands (GPA 3.692, in progress). A.A. General Studies — TMCC (May 2024). 71 credits — UNR (2019–2022).
|
|
428
|
+
|
|
429
|
+
DRUG TEST / BACKGROUND CHECK acknowledgment -> ALWAYS "Yes". This is required and is often near the bottom of the page.
|
|
430
|
+
GENERAL REQUIREMENTS / "I have read the job announcement" acknowledgment -> ALWAYS select/check it. Required.
|
|
431
|
+
CAREER FAIR ATTENDANCE -> "Have not attended a career fair and/or job event." -> follow-up text = None.
|
|
432
|
+
HOW DID YOU HEAR -> GovernmentJobs Website / Online Job Board.
|
|
433
|
+
|
|
434
|
+
BEFORE CLICKING PROCEED/NEXT — MANDATORY VERIFICATION:
|
|
435
|
+
1. Scroll to the TOP of the page.
|
|
436
|
+
2. Use browser_evaluate to find every element with error text: document.querySelectorAll('[class*="error"],[class*="Error"]').forEach(e => console.log(e.textContent))
|
|
437
|
+
3. Scroll slowly to the BOTTOM. Count every numbered question. Check that EACH one has a selected radio, checked checkbox, or filled text.
|
|
438
|
+
4. Questions commonly missed: drug test agreement (Yes), "I have read..." acknowledgment, career fair attendance radio, career fair details text box.
|
|
439
|
+
5. Only click Proceed/Next when ZERO errors are visible and EVERY question has an answer.
|
|
440
|
+
|
|
441
|
+
ERROR MESSAGES after clicking Proceed/Next: scroll to TOP, find red error messages, fix each one. Do not click Proceed again until all are cleared."""
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _build_hard_rules(profile: dict) -> str:
|
|
445
|
+
"""Build the hard rules section with work auth and name from profile."""
|
|
446
|
+
personal = profile["personal"]
|
|
447
|
+
work_auth = profile["work_authorization"]
|
|
448
|
+
|
|
449
|
+
full_name = personal["full_name"]
|
|
450
|
+
preferred_name = personal.get("preferred_name", full_name.split()[0])
|
|
451
|
+
preferred_last = full_name.split()[-1] if " " in full_name else ""
|
|
452
|
+
display_name = f"{preferred_name} {preferred_last}".strip() if preferred_last else preferred_name
|
|
453
|
+
|
|
454
|
+
# Build work auth rule dynamically
|
|
455
|
+
auth_info = work_auth.get("legally_authorized_to_work", "")
|
|
456
|
+
sponsorship = work_auth.get("require_sponsorship", "")
|
|
457
|
+
permit_type = work_auth.get("work_permit_type", "")
|
|
458
|
+
|
|
459
|
+
work_auth_rule = "Work auth: Answer truthfully from profile."
|
|
460
|
+
if permit_type:
|
|
461
|
+
work_auth_rule = f"Work auth: {permit_type}. Sponsorship needed: {sponsorship}."
|
|
462
|
+
|
|
463
|
+
name_rule = f'Name: Legal name = {full_name}.'
|
|
464
|
+
if preferred_name and preferred_name != full_name.split()[0]:
|
|
465
|
+
name_rule += f' Preferred name = {preferred_name}. Use "{display_name}" unless a field specifically says "legal name".'
|
|
466
|
+
|
|
467
|
+
return f"""== HARD RULES (never break these) ==
|
|
468
|
+
1. Never lie about: citizenship, work authorization, criminal history, education credentials, security clearance, licenses.
|
|
469
|
+
2. {work_auth_rule}
|
|
470
|
+
3. {name_rule}"""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _build_captcha_section() -> str:
|
|
474
|
+
"""Build the CAPTCHA detection and solving instructions.
|
|
475
|
+
|
|
476
|
+
Reads the CapSolver API key from environment. The CAPTCHA section
|
|
477
|
+
contains no personal data -- it's the same for every user.
|
|
478
|
+
"""
|
|
479
|
+
config.load_env()
|
|
480
|
+
capsolver_key = os.environ.get("CAPSOLVER_API_KEY", "")
|
|
481
|
+
|
|
482
|
+
return f"""== CAPTCHA ==
|
|
483
|
+
You solve CAPTCHAs via the CapSolver REST API. No browser extension. You control the entire flow.
|
|
484
|
+
API key: {capsolver_key or 'NOT CONFIGURED — skip to MANUAL FALLBACK for all CAPTCHAs'}
|
|
485
|
+
API base: https://api.capsolver.com
|
|
486
|
+
|
|
487
|
+
CRITICAL RULE: When ANY CAPTCHA appears (hCaptcha, reCAPTCHA, Turnstile -- regardless of what it looks like visually), you MUST:
|
|
488
|
+
1. Run CAPTCHA DETECT to get the type and sitekey
|
|
489
|
+
2. Run CAPTCHA SOLVE (createTask -> poll -> inject) with the CapSolver API
|
|
490
|
+
3. ONLY go to MANUAL FALLBACK if CapSolver returns errorId > 0
|
|
491
|
+
Do NOT skip the API call based on what the CAPTCHA looks like. CapSolver solves CAPTCHAs server-side -- it does NOT need to see or interact with images, puzzles, or games. Even "drag the pipe" or "click all traffic lights" hCaptchas are solved via API token, not visually. ALWAYS try the API first.
|
|
492
|
+
|
|
493
|
+
--- CAPTCHA DETECT ---
|
|
494
|
+
Run this browser_evaluate after every navigation, Apply/Submit/Login click, or when a page feels stuck.
|
|
495
|
+
IMPORTANT: Detection order matters. hCaptcha elements also have data-sitekey, so check hCaptcha BEFORE reCAPTCHA.
|
|
496
|
+
|
|
497
|
+
browser_evaluate function: () => {{{{
|
|
498
|
+
const r = {{}};
|
|
499
|
+
const url = window.location.href;
|
|
500
|
+
// 1. hCaptcha (check FIRST -- hCaptcha uses data-sitekey too)
|
|
501
|
+
const hc = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]');
|
|
502
|
+
if (hc) {{{{
|
|
503
|
+
r.type = 'hcaptcha'; r.sitekey = hc.dataset.sitekey || hc.dataset.hcaptchaSitekey;
|
|
504
|
+
}}}}
|
|
505
|
+
if (!r.type && document.querySelector('script[src*="hcaptcha.com"], iframe[src*="hcaptcha.com"]')) {{{{
|
|
506
|
+
const el = document.querySelector('[data-sitekey]');
|
|
507
|
+
if (el) {{{{ r.type = 'hcaptcha'; r.sitekey = el.dataset.sitekey; }}}}
|
|
508
|
+
}}}}
|
|
509
|
+
// 2. Cloudflare Turnstile
|
|
510
|
+
if (!r.type) {{{{
|
|
511
|
+
const cf = document.querySelector('.cf-turnstile, [data-turnstile-sitekey]');
|
|
512
|
+
if (cf) {{{{
|
|
513
|
+
r.type = 'turnstile'; r.sitekey = cf.dataset.sitekey || cf.dataset.turnstileSitekey;
|
|
514
|
+
if (cf.dataset.action) r.action = cf.dataset.action;
|
|
515
|
+
if (cf.dataset.cdata) r.cdata = cf.dataset.cdata;
|
|
516
|
+
}}}}
|
|
517
|
+
}}}}
|
|
518
|
+
if (!r.type && document.querySelector('script[src*="challenges.cloudflare.com"]')) {{{{
|
|
519
|
+
r.type = 'turnstile_script_only'; r.note = 'Wait 3s and re-detect.';
|
|
520
|
+
}}}}
|
|
521
|
+
// 3. reCAPTCHA v3 (invisible, loaded via render= param)
|
|
522
|
+
if (!r.type) {{{{
|
|
523
|
+
const s = document.querySelector('script[src*="recaptcha"][src*="render="]');
|
|
524
|
+
if (s) {{{{
|
|
525
|
+
const m = s.src.match(/render=([^&]+)/);
|
|
526
|
+
if (m && m[1] !== 'explicit') {{{{ r.type = 'recaptchav3'; r.sitekey = m[1]; }}}}
|
|
527
|
+
}}}}
|
|
528
|
+
}}}}
|
|
529
|
+
// 4. reCAPTCHA v2 (checkbox or invisible)
|
|
530
|
+
if (!r.type) {{{{
|
|
531
|
+
const rc = document.querySelector('.g-recaptcha');
|
|
532
|
+
if (rc) {{{{ r.type = 'recaptchav2'; r.sitekey = rc.dataset.sitekey; }}}}
|
|
533
|
+
}}}}
|
|
534
|
+
if (!r.type && document.querySelector('script[src*="recaptcha"]')) {{{{
|
|
535
|
+
const el = document.querySelector('[data-sitekey]');
|
|
536
|
+
if (el) {{{{ r.type = 'recaptchav2'; r.sitekey = el.dataset.sitekey; }}}}
|
|
537
|
+
}}}}
|
|
538
|
+
// 5. FunCaptcha (Arkose Labs)
|
|
539
|
+
if (!r.type) {{{{
|
|
540
|
+
const fc = document.querySelector('#FunCaptcha, [data-pkey], .funcaptcha');
|
|
541
|
+
if (fc) {{{{ r.type = 'funcaptcha'; r.sitekey = fc.dataset.pkey; }}}}
|
|
542
|
+
}}}}
|
|
543
|
+
if (!r.type && document.querySelector('script[src*="arkoselabs"], script[src*="funcaptcha"]')) {{{{
|
|
544
|
+
const el = document.querySelector('[data-pkey]');
|
|
545
|
+
if (el) {{{{ r.type = 'funcaptcha'; r.sitekey = el.dataset.pkey; }}}}
|
|
546
|
+
}}}}
|
|
547
|
+
if (r.type) {{{{ r.url = url; return r; }}}}
|
|
548
|
+
return null;
|
|
549
|
+
}}}}
|
|
550
|
+
|
|
551
|
+
Result actions:
|
|
552
|
+
- null -> no CAPTCHA. Continue normally.
|
|
553
|
+
- "turnstile_script_only" -> browser_wait_for time: 3, re-run detect.
|
|
554
|
+
- Any other type -> proceed to CAPTCHA SOLVE below.
|
|
555
|
+
|
|
556
|
+
⚠DO NOT CLICK THE CAPTCHA WIDGET. Do NOT click "I'm not a robot", do NOT click the checkbox, do NOT interact with the CAPTCHA visually AT ALL before or during API solving. Clicking it triggers the interactive visual/audio challenge which you cannot solve. The API returns a token you inject silently — the visual widget never gets clicked.
|
|
557
|
+
|
|
558
|
+
--- CAPTCHA SOLVE ---
|
|
559
|
+
Three steps: createTask -> poll -> inject.
|
|
560
|
+
Steps 1 and 2 use Bash (curl) — NOT browser_evaluate. This bypasses site CSP which would block the fetch.
|
|
561
|
+
|
|
562
|
+
STEP 1 -- CREATE TASK (run as a Bash command, fill in TASK_TYPE, PAGE_URL, SITE_KEY):
|
|
563
|
+
curl -s -X POST https://api.capsolver.com/createTask \
|
|
564
|
+
-H "Content-Type: application/json" \
|
|
565
|
+
-d '{{"clientKey":"{capsolver_key}","task":{{"type":"TASK_TYPE","websiteURL":"PAGE_URL","websiteKey":"SITE_KEY"}}}}'
|
|
566
|
+
|
|
567
|
+
TASK_TYPE values (use EXACTLY these strings):
|
|
568
|
+
hcaptcha -> HCaptchaTaskProxyLess
|
|
569
|
+
recaptchav2 -> ReCaptchaV2TaskProxyLess
|
|
570
|
+
recaptchav3 -> ReCaptchaV3TaskProxyLess
|
|
571
|
+
turnstile -> AntiTurnstileTaskProxyLess
|
|
572
|
+
funcaptcha -> FunCaptchaTaskProxyLess
|
|
573
|
+
|
|
574
|
+
PAGE_URL = the url from detect result. SITE_KEY = the sitekey from detect result.
|
|
575
|
+
For recaptchav3: add ,"pageAction":"submit" inside the task object (or the actual action from page scripts).
|
|
576
|
+
For turnstile: add ,"metadata":{{"action":"...","cdata":"..."}} inside task if those were in detect result.
|
|
577
|
+
|
|
578
|
+
Response: {{"errorId": 0, "taskId": "abc123"}} on success.
|
|
579
|
+
If errorId > 0 -> CAPTCHA SOLVE failed. Go to MANUAL FALLBACK.
|
|
580
|
+
|
|
581
|
+
STEP 2 -- POLL (run as a Bash command, replace TASK_ID with taskId from step 1):
|
|
582
|
+
Loop: sleep 3 between polls. Max 10 polls (30s total).
|
|
583
|
+
curl -s -X POST https://api.capsolver.com/getTaskResult \
|
|
584
|
+
-H "Content-Type: application/json" \
|
|
585
|
+
-d '{{"clientKey":"{capsolver_key}","taskId":"TASK_ID"}}'
|
|
586
|
+
|
|
587
|
+
- status "processing" -> sleep 3, poll again.
|
|
588
|
+
- status "ready" -> extract token:
|
|
589
|
+
reCAPTCHA: solution.gRecaptchaResponse
|
|
590
|
+
hCaptcha: solution.gRecaptchaResponse
|
|
591
|
+
Turnstile: solution.token
|
|
592
|
+
- errorId > 0 or 30s timeout -> MANUAL FALLBACK.
|
|
593
|
+
|
|
594
|
+
STEP 3 -- INJECT TOKEN (replace THE_TOKEN with actual token string):
|
|
595
|
+
|
|
596
|
+
For reCAPTCHA v2/v3:
|
|
597
|
+
browser_evaluate function: () => {{{{
|
|
598
|
+
const token = 'THE_TOKEN';
|
|
599
|
+
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(el => {{{{ el.value = token; el.style.display = 'block'; }}}});
|
|
600
|
+
if (window.___grecaptcha_cfg) {{{{
|
|
601
|
+
const clients = window.___grecaptcha_cfg.clients;
|
|
602
|
+
for (const key in clients) {{{{
|
|
603
|
+
const walk = (obj, d) => {{{{
|
|
604
|
+
if (d > 4 || !obj) return;
|
|
605
|
+
for (const k in obj) {{{{
|
|
606
|
+
if (typeof obj[k] === 'function' && k.length < 3) try {{{{ obj[k](token); }}}} catch(e) {{{{}}}}
|
|
607
|
+
else if (typeof obj[k] === 'object') walk(obj[k], d+1);
|
|
608
|
+
}}}}
|
|
609
|
+
}}}};
|
|
610
|
+
walk(clients[key], 0);
|
|
611
|
+
}}}}
|
|
612
|
+
}}}}
|
|
613
|
+
return 'injected';
|
|
614
|
+
}}}}
|
|
615
|
+
|
|
616
|
+
For hCaptcha:
|
|
617
|
+
browser_evaluate function: () => {{{{
|
|
618
|
+
const token = 'THE_TOKEN';
|
|
619
|
+
const ta = document.querySelector('[name="h-captcha-response"], textarea[name*="hcaptcha"]');
|
|
620
|
+
if (ta) ta.value = token;
|
|
621
|
+
document.querySelectorAll('iframe[data-hcaptcha-response]').forEach(f => f.setAttribute('data-hcaptcha-response', token));
|
|
622
|
+
const cb = document.querySelector('[data-hcaptcha-widget-id]');
|
|
623
|
+
if (cb && window.hcaptcha) try {{{{ window.hcaptcha.getResponse(cb.dataset.hcaptchaWidgetId); }}}} catch(e) {{{{}}}}
|
|
624
|
+
return 'injected';
|
|
625
|
+
}}}}
|
|
626
|
+
|
|
627
|
+
For Turnstile:
|
|
628
|
+
browser_evaluate function: () => {{{{
|
|
629
|
+
const token = 'THE_TOKEN';
|
|
630
|
+
const inp = document.querySelector('[name="cf-turnstile-response"], input[name*="turnstile"]');
|
|
631
|
+
if (inp) inp.value = token;
|
|
632
|
+
if (window.turnstile) try {{{{ const w = document.querySelector('.cf-turnstile'); if (w) window.turnstile.getResponse(w); }}}} catch(e) {{{{}}}}
|
|
633
|
+
return 'injected';
|
|
634
|
+
}}}}
|
|
635
|
+
|
|
636
|
+
For FunCaptcha: inject token into #FunCaptcha-Token or input[name="fc-token"], call window.ArkoseEnforcement.setConfig({{{{data:{{{{blob:token}}}}}}}})) if present.
|
|
637
|
+
|
|
638
|
+
After injecting: browser_wait_for time: 2, then snapshot.
|
|
639
|
+
- Widget gone or green check -> success. Click Submit if needed.
|
|
640
|
+
- No change -> click Submit/Verify/Continue button (some sites need it).
|
|
641
|
+
- Still stuck -> token may have expired (~2 min lifetime). Re-run from STEP 1.
|
|
642
|
+
|
|
643
|
+
--- MANUAL FALLBACK ---
|
|
644
|
+
You should ONLY be here if CapSolver createTask returned errorId > 0. If you haven't tried CapSolver yet, GO BACK and try it first.
|
|
645
|
+
If CapSolver genuinely failed (errorId > 0):
|
|
646
|
+
1. Audio challenge: Look for "audio" or "accessibility" button -> click it for an easier challenge.
|
|
647
|
+
2. Text/logic puzzles: Solve them yourself. Think step by step. Common tricks: "All but 9 die" = 9 left. "3 sisters and 4 brothers, how many siblings?" = 7.
|
|
648
|
+
3. Simple text captchas ("What is 3+7?", "Type the word") -> solve them.
|
|
649
|
+
4. All else fails -> Output RESULT:CAPTCHA."""
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def build_prompt(job: dict, tailored_resume: str,
|
|
653
|
+
cover_letter: str | None = None,
|
|
654
|
+
dry_run: bool = False) -> str:
|
|
655
|
+
"""Build the full instruction prompt for the apply agent.
|
|
656
|
+
|
|
657
|
+
Loads the user profile and search config internally. All personal data
|
|
658
|
+
comes from the profile -- nothing is hardcoded.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
job: Job dict from the database (must have url, title, site,
|
|
662
|
+
application_url, fit_score, tailored_resume_path).
|
|
663
|
+
tailored_resume: Plain-text content of the tailored resume.
|
|
664
|
+
cover_letter: Optional plain-text cover letter content.
|
|
665
|
+
dry_run: If True, tell the agent not to click Submit.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
Complete prompt string for the AI agent.
|
|
669
|
+
"""
|
|
670
|
+
profile = config.load_profile()
|
|
671
|
+
search_config = config.load_search_config()
|
|
672
|
+
personal = profile["personal"]
|
|
673
|
+
|
|
674
|
+
# --- Resolve resume PDF path ---
|
|
675
|
+
resume_path = job.get("tailored_resume_path")
|
|
676
|
+
if not resume_path:
|
|
677
|
+
raise ValueError(f"No tailored resume for job: {job.get('title', 'unknown')}")
|
|
678
|
+
|
|
679
|
+
src_pdf = Path(resume_path).with_suffix(".pdf").resolve()
|
|
680
|
+
if not src_pdf.exists():
|
|
681
|
+
raise ValueError(f"Resume PDF not found: {src_pdf}")
|
|
682
|
+
|
|
683
|
+
# Copy to a clean filename for upload (recruiters see the filename)
|
|
684
|
+
full_name = personal["full_name"]
|
|
685
|
+
name_slug = full_name.replace(" ", "_")
|
|
686
|
+
dest_dir = config.APPLY_WORKER_DIR / "current"
|
|
687
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
688
|
+
upload_pdf = dest_dir / f"{name_slug}_Resume.pdf"
|
|
689
|
+
shutil.copy(str(src_pdf), str(upload_pdf))
|
|
690
|
+
pdf_path = str(upload_pdf)
|
|
691
|
+
|
|
692
|
+
# --- Cover letter handling ---
|
|
693
|
+
cover_letter_text = cover_letter or ""
|
|
694
|
+
cl_upload_path = ""
|
|
695
|
+
cl_path = job.get("cover_letter_path")
|
|
696
|
+
if cl_path and Path(cl_path).exists():
|
|
697
|
+
cl_src = Path(cl_path)
|
|
698
|
+
# Read text from .txt sibling (PDF is binary)
|
|
699
|
+
cl_txt = cl_src.with_suffix(".txt")
|
|
700
|
+
if cl_txt.exists():
|
|
701
|
+
cover_letter_text = cl_txt.read_text(encoding="utf-8")
|
|
702
|
+
elif cl_src.suffix == ".txt":
|
|
703
|
+
cover_letter_text = cl_src.read_text(encoding="utf-8")
|
|
704
|
+
# Upload must be PDF
|
|
705
|
+
cl_pdf_src = cl_src.with_suffix(".pdf")
|
|
706
|
+
if cl_pdf_src.exists():
|
|
707
|
+
cl_upload = dest_dir / f"{name_slug}_Cover_Letter.pdf"
|
|
708
|
+
shutil.copy(str(cl_pdf_src), str(cl_upload))
|
|
709
|
+
cl_upload_path = str(cl_upload)
|
|
710
|
+
|
|
711
|
+
# --- Build all prompt sections ---
|
|
712
|
+
profile_summary = _build_profile_summary(profile)
|
|
713
|
+
location_check = _build_location_check(profile, search_config)
|
|
714
|
+
salary_section = _build_salary_section(profile)
|
|
715
|
+
screening_section = _build_screening_section(profile)
|
|
716
|
+
hard_rules = _build_hard_rules(profile)
|
|
717
|
+
captcha_section = _build_captcha_section()
|
|
718
|
+
|
|
719
|
+
# Cover letter fallback text
|
|
720
|
+
city = personal.get("city", "the area")
|
|
721
|
+
if not cover_letter_text:
|
|
722
|
+
cl_display = (
|
|
723
|
+
f"None available. Skip if optional. If required, write 2 factual "
|
|
724
|
+
f"sentences: (1) relevant experience from the resume that matches "
|
|
725
|
+
f"this role, (2) available immediately and based in {city}."
|
|
726
|
+
)
|
|
727
|
+
else:
|
|
728
|
+
cl_display = cover_letter_text
|
|
729
|
+
|
|
730
|
+
# Phone digits only (for fields with country prefix)
|
|
731
|
+
phone_digits = "".join(c for c in personal.get("phone", "") if c.isdigit())
|
|
732
|
+
|
|
733
|
+
# SSO domains the agent cannot sign into (loaded from config/sites.yaml)
|
|
734
|
+
from divapply.config import load_blocked_sso
|
|
735
|
+
blocked_sso = load_blocked_sso()
|
|
736
|
+
|
|
737
|
+
# Site-specific credentials (overrides personal.password for specific domains)
|
|
738
|
+
site_creds = profile.get("site_credentials", {})
|
|
739
|
+
site_creds_lines = []
|
|
740
|
+
for domain, creds in site_creds.items():
|
|
741
|
+
site_creds_lines.append(
|
|
742
|
+
f" - {domain}: username={creds.get('username', personal['email'])} password={creds.get('password', '')}"
|
|
743
|
+
)
|
|
744
|
+
site_creds_block = (
|
|
745
|
+
"SITE-SPECIFIC LOGINS (use these instead of the default password for these domains):\n"
|
|
746
|
+
+ "\n".join(site_creds_lines)
|
|
747
|
+
if site_creds_lines else ""
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Preferred display name
|
|
751
|
+
preferred_name = personal.get("preferred_name", full_name.split()[0])
|
|
752
|
+
last_name = full_name.split()[-1] if " " in full_name else ""
|
|
753
|
+
display_name = f"{preferred_name} {last_name}".strip()
|
|
754
|
+
|
|
755
|
+
# Dry-run: override submit instruction
|
|
756
|
+
if dry_run:
|
|
757
|
+
submit_instruction = "IMPORTANT: Do NOT click the final Submit/Apply button. Review the form, verify all fields, then output RESULT:APPLIED with a note that this was a dry run."
|
|
758
|
+
else:
|
|
759
|
+
submit_instruction = """BEFORE clicking Submit/Apply, run a mandatory pre-submit check:
|
|
760
|
+
1. Scroll to the top of the page. Take a snapshot.
|
|
761
|
+
2. Scan every visible field. Look specifically for:
|
|
762
|
+
- Any text area or input that is empty or says "Answer" / placeholder text (not filled in)
|
|
763
|
+
- Any required field (*) that is blank
|
|
764
|
+
- Any radio group with nothing selected
|
|
765
|
+
- Any required checkbox that is unchecked
|
|
766
|
+
3. For EVERY empty required text field you find: write a real answer based on the job description and resume. NEVER leave a required field blank. If you don't have a pre-written answer, compose one from context.
|
|
767
|
+
4. Scroll down and repeat until you have checked every page section.
|
|
768
|
+
5. Only after ALL fields are filled and all required items are answered: click Submit/Apply.
|
|
769
|
+
6. Verify all data matches the APPLICANT PROFILE and TAILORED RESUME -- name, email, phone, location, work auth, resume uploaded, cover letter if applicable."""
|
|
770
|
+
|
|
771
|
+
prompt = f"""You are an autonomous job application agent running in HIGH EFFORT mode. Your ONE mission: submit a complete, accurate application and get this candidate an interview. You have all the information and tools needed. Never give up on solvable obstacles — CAPTCHAs get solved via API, supplemental questions get answered from profile data, login walls get bypassed with provided credentials. Think strategically. Act decisively. Use every tool available. The only acceptable reason to stop early is a hard blocker explicitly listed in RESULT CODES (expired job, permanent auth wall, unsafe site). Everything else: push through and submit.
|
|
772
|
+
|
|
773
|
+
== BROWSER TOOLS — CRITICAL ==
|
|
774
|
+
You control a dedicated browser via the Playwright MCP. Use ONLY these tool prefixes:
|
|
775
|
+
mcp__playwright__* (browser_navigate, browser_click, browser_fill, browser_snapshot, etc.)
|
|
776
|
+
Do NOT use any alternate browser tool namespace. Those connect to a different browser and will break the application. Every browser action must go through the playwright MCP tools only.
|
|
777
|
+
|
|
778
|
+
JAVASCRIPT AND PLAYWRIGHT CODE IN THE BROWSER:
|
|
779
|
+
Two valid tools for browser interaction — use whichever is faster for the task:
|
|
780
|
+
browser_evaluate → runs JavaScript directly in the page (best for reading DOM, setting .value, bulk-checking checkboxes)
|
|
781
|
+
browser_run_code → runs Playwright Python API code (best for click, fill, select by label/role)
|
|
782
|
+
NEVER use plain Bash or computer tool to interact with the page — those run outside the browser entirely.
|
|
783
|
+
|
|
784
|
+
SCROLLING — USE browser_scroll, NEVER browser_press_key for scrolling:
|
|
785
|
+
CORRECT: browser_scroll direction: down coordinate: [512, 400]
|
|
786
|
+
WRONG: browser_press_key key: PageDown — extremely slow, burns turns.
|
|
787
|
+
To scroll to bottom: browser_scroll direction: down coordinate: [512, 600] (repeat if page is long).
|
|
788
|
+
|
|
789
|
+
== JOB ==
|
|
790
|
+
URL: {job.get('application_url') or job['url']}
|
|
791
|
+
Title: {job['title']}
|
|
792
|
+
Company: {job.get('site', 'Unknown')}
|
|
793
|
+
Fit Score: {job.get('fit_score', 'N/A')}/10
|
|
794
|
+
|
|
795
|
+
== FILES (absolute paths — use EXACTLY as shown, do NOT modify or retry with different formats) ==
|
|
796
|
+
Resume PDF (upload this): {pdf_path}
|
|
797
|
+
Cover Letter PDF (upload if asked): {cl_upload_path or "N/A"}
|
|
798
|
+
IMPORTANT: These files are pre-staged in your working directory. When using browser_file_upload, pass the EXACT path above. Do NOT waste actions retrying with different path formats — if the first attempt fails, use browser_evaluate to find the <input type="file"> element and set files via JavaScript.
|
|
799
|
+
|
|
800
|
+
== RESUME TEXT (use when filling text fields) ==
|
|
801
|
+
{tailored_resume}
|
|
802
|
+
|
|
803
|
+
== COVER LETTER TEXT (paste if text field, upload PDF if file field) ==
|
|
804
|
+
{cl_display}
|
|
805
|
+
|
|
806
|
+
== APPLICANT PROFILE ==
|
|
807
|
+
{profile_summary}
|
|
808
|
+
|
|
809
|
+
== YOUR MISSION ==
|
|
810
|
+
Submit a complete, accurate application. Use the profile and resume as source data -- adapt to fit each form's format.
|
|
811
|
+
|
|
812
|
+
If something unexpected happens and these instructions don't cover it, figure it out yourself. You are autonomous. Navigate pages, read content, try buttons, explore the site. The goal is always the same: submit the application. Do whatever it takes to reach that goal.
|
|
813
|
+
|
|
814
|
+
{hard_rules}
|
|
815
|
+
|
|
816
|
+
== SCAM DETECTION — CHECK BEFORE APPLYING ==
|
|
817
|
+
Before filling any form, spend 2 actions verifying this is a legitimate employer:
|
|
818
|
+
1. Check the page for a real company name, physical address, or "About Us" link.
|
|
819
|
+
2. If ANY of these are true, output RESULT:FAILED:scam and stop immediately:
|
|
820
|
+
- No company name anywhere on the page or application (just "Confidential" or "Our Client")
|
|
821
|
+
- Page asks for SSN, bank account, routing number, or payment before any interview
|
|
822
|
+
- Page asks you to "pay for training", "purchase a starter kit", or "send a deposit"
|
|
823
|
+
- Job promises unusually high pay with no experience required and no real company behind it
|
|
824
|
+
- Site redirects through 2+ domains before reaching an actual application form
|
|
825
|
+
- Page is asking to "create a contractor profile" or "set your hourly rate"
|
|
826
|
+
- Application is on a site like Craigslist, random Google Forms, or an unknown single-page domain with no business info
|
|
827
|
+
3. If the company checks out (real employer, real ATS, or government site), proceed normally.
|
|
828
|
+
|
|
829
|
+
== NEVER DO THESE (immediate RESULT:FAILED if encountered) ==
|
|
830
|
+
- NEVER grant camera, microphone, screen sharing, or location permissions. If a site requests them -> RESULT:FAILED:unsafe_permissions
|
|
831
|
+
- NEVER do video/audio verification, selfie capture, ID photo upload, or biometric anything -> RESULT:FAILED:unsafe_verification
|
|
832
|
+
- NEVER set up a freelancing profile (Mercor, Toptal, Upwork, Fiverr, Turing, etc.). These are contractor marketplaces, not job applications -> RESULT:FAILED:not_a_job_application
|
|
833
|
+
- NEVER agree to hourly/contract rates, availability calendars, or "set your rate" flows. You are applying for FULL-TIME salaried positions only.
|
|
834
|
+
- NEVER install browser extensions, download executables, or run assessment software.
|
|
835
|
+
- NEVER enter payment info, bank details, or SSN/SIN.
|
|
836
|
+
- NEVER click "Allow" on any browser permission popup. Always deny/block.
|
|
837
|
+
- If the site is NOT a job application form (it's a profile builder, skills marketplace, talent network signup, coding assessment platform) -> RESULT:FAILED:not_a_job_application
|
|
838
|
+
|
|
839
|
+
{location_check}
|
|
840
|
+
|
|
841
|
+
{salary_section}
|
|
842
|
+
|
|
843
|
+
{screening_section}
|
|
844
|
+
|
|
845
|
+
== STEP-BY-STEP ==
|
|
846
|
+
1. browser_navigate to the job URL.
|
|
847
|
+
2. browser_snapshot to read the page. Then run CAPTCHA DETECT (see CAPTCHA section). If a CAPTCHA is found, solve it before continuing.
|
|
848
|
+
3. LOCATION CHECK. Read the page for location info. If not eligible, output RESULT and stop.
|
|
849
|
+
4. Find and click the Apply button. If email-only (page says "email resume to X"):
|
|
850
|
+
- send_email with subject "Application for {job['title']} -- {display_name}", body = 2-3 sentence pitch + contact info, attach resume PDF: ["{pdf_path}"]
|
|
851
|
+
- Output RESULT:APPLIED. Done.
|
|
852
|
+
After clicking Apply: browser_snapshot. Run CAPTCHA DETECT -- many sites trigger CAPTCHAs right after the Apply click. If found, solve before continuing.
|
|
853
|
+
5. Login wall?
|
|
854
|
+
5a. FIRST: check the URL.
|
|
855
|
+
- If you landed on accounts.google.com: this is a Google Sign-In page. The user is already signed in to Google in this browser. Look for the user's account ({personal['email']}) in the account chooser and click it. If prompted with a confirmation screen, click "Continue" or "Allow". Do NOT enter any password -- just select the existing account. After completing, you will be redirected back to the application.
|
|
856
|
+
- If you landed on {', '.join(blocked_sso)}, or any other SSO/OAuth page (Microsoft, Okta, Auth0) -> STOP. Output RESULT:FAILED:sso_required.
|
|
857
|
+
5b. Check for popups. Run browser_tabs action "list". If a new tab/window appeared (login popup), switch to it with browser_tabs action "select". Check the URL there too -- if it's accounts.google.com, select the existing Google account ({personal['email']}) and click Continue. If it's {', '.join(blocked_sso)} -> RESULT:FAILED:sso_required.
|
|
858
|
+
5c. Regular login form (employer's own site)?
|
|
859
|
+
{site_creds_block}
|
|
860
|
+
Check the current URL domain. If it matches a domain in SITE-SPECIFIC LOGINS above, use those credentials.
|
|
861
|
+
Otherwise use default: {personal['email']} / {personal.get('password', '')}
|
|
862
|
+
5d. After clicking Login/Sign-in: run CAPTCHA DETECT. Login pages frequently have invisible CAPTCHAs that silently block form submissions. If found, solve it then retry login.
|
|
863
|
+
5e. Sign in failed? Try sign up with same email and password.
|
|
864
|
+
5f. Need email verification? Use search_emails + read_email to get the code.
|
|
865
|
+
5g. After login, run browser_tabs action "list" again. Switch back to the application tab if needed.
|
|
866
|
+
5h. All failed? Output RESULT:FAILED:login_issue. Do not loop.
|
|
867
|
+
6. Upload resume. ALWAYS upload fresh -- delete any existing resume first, then browser_file_upload with the PDF path above. This is the tailored resume for THIS job. Non-negotiable.
|
|
868
|
+
7. Upload cover letter if there's a field for it. Text field -> paste the cover letter text. File upload -> use the cover letter PDF path.
|
|
869
|
+
8. Check pre-filled fields but be STRATEGIC about edits. ATS systems auto-fill from your profile -- only fix things that MATTER:
|
|
870
|
+
- "Current Job Title" or "Most Recent Title" -> use the title from the TAILORED RESUME summary, NOT whatever the parser guessed.
|
|
871
|
+
- Fix WRONG data (wrong degree type, wrong employer, wrong job title). Fill EMPTY required fields.
|
|
872
|
+
- Do NOT waste actions on trivial differences (units 103 vs 101, minor date variations, formatting differences). These won't affect your application.
|
|
873
|
+
9. Answer screening questions using the rules above.
|
|
874
|
+
10. {submit_instruction}
|
|
875
|
+
11. After submit: browser_snapshot. Run CAPTCHA DETECT -- submit buttons often trigger invisible CAPTCHAs. If found, solve it (the form will auto-submit once the token clears, or you may need to click Submit again). Then check for new tabs (browser_tabs action: "list"). Switch to newest, close old. Snapshot to confirm submission. Look for "thank you" or "application received".
|
|
876
|
+
12. Output your result.
|
|
877
|
+
|
|
878
|
+
== RESULT CODES (output EXACTLY one) ==
|
|
879
|
+
RESULT:APPLIED -- submitted successfully
|
|
880
|
+
RESULT:EXPIRED -- job closed or no longer accepting applications
|
|
881
|
+
RESULT:CAPTCHA -- blocked by unsolvable captcha
|
|
882
|
+
RESULT:LOGIN_ISSUE -- could not sign in or create account
|
|
883
|
+
RESULT:FAILED:not_eligible_location -- onsite outside acceptable area, no remote option
|
|
884
|
+
RESULT:FAILED:not_eligible_work_auth -- requires unauthorized work location
|
|
885
|
+
RESULT:FAILED:reason -- any other failure (brief reason)
|
|
886
|
+
|
|
887
|
+
== BROWSER EFFICIENCY — MINIMIZE ACTIONS AND TOKENS ==
|
|
888
|
+
GOLDEN RULES — every action costs tokens, every screenshot costs tokens:
|
|
889
|
+
|
|
890
|
+
- browser_snapshot: use ONCE per new page to get element refs. Re-snapshot only when navigating to a new page.
|
|
891
|
+
- browser_take_screenshot: use ONLY when you need to visually verify an error or unexpected state. NOT after every action.
|
|
892
|
+
- Trust browser_evaluate return values. If checkAll() returns 9, those 9 boxes are checked — no screenshot needed to verify.
|
|
893
|
+
- Fill ALL fields in ONE call. Never one field at a time.
|
|
894
|
+
- Think SHORT. Do not narrate what you see. Do not list what you just did. Act → move on.
|
|
895
|
+
- Multi-page forms: snapshot once per new page, fill everything, click Next. No mid-page re-snapshots.
|
|
896
|
+
- SCROLLING: use browser_evaluate: () => window.scrollTo(0, document.body.scrollHeight) to jump to bottom instantly. Use browser_scroll for moderate scrolls. NEVER use browser_press_key for scrolling.
|
|
897
|
+
- CAPTCHA AWARENESS: run CAPTCHA DETECT after navigation and Apply/Submit/Login clicks. Invisible CAPTCHAs block silently.
|
|
898
|
+
|
|
899
|
+
== FORM TRICKS ==
|
|
900
|
+
- Popup/new window opened? browser_tabs action "list" to see all tabs. browser_tabs action "select" with the tab index to switch. ALWAYS check for new tabs after clicking login/apply/sign-in buttons.
|
|
901
|
+
- "Upload your resume" pre-fill page (Workday, Lever, etc.): This is NOT the application form yet. Click "Select file" or the upload area, then browser_file_upload with the resume PDF path. Wait for parsing to finish. Then click Next/Continue to reach the actual form.
|
|
902
|
+
- NEOGOV / GovernmentJobs applications — FAST TRACK (saves 60+ actions):
|
|
903
|
+
GovernmentJobs pre-fills Work, Education, References, and Preferences from the saved account. DO NOT read, review, or try to edit these sections. Skip straight to what matters.
|
|
904
|
+
|
|
905
|
+
NEOGOV OPTIMAL FLOW (follow this order, use left-nav tabs to jump directly):
|
|
906
|
+
1. After login: click "Attachments" tab in the left navigation menu.
|
|
907
|
+
2. On Attachments page: upload Resume and Cover Letter (two-step flow below).
|
|
908
|
+
→ browser_take_screenshot to confirm both filenames appear. Then click Next.
|
|
909
|
+
3. Click "Questions" tab. Run the bulk JS from CIVIL SERVICE section to fill ALL supplemental questions in one call.
|
|
910
|
+
→ browser_take_screenshot to confirm checkboxes are checked and essay is filled. Fix anything missing. Then click Proceed/Next.
|
|
911
|
+
4. Click "Review" tab. Scroll to bottom: browser_evaluate: () => window.scrollTo(0, document.body.scrollHeight)
|
|
912
|
+
→ browser_take_screenshot to confirm "Proceed to Certify and Submit" button is visible and no red errors. Then click it.
|
|
913
|
+
5. On Certify page: browser_take_screenshot to confirm certification text loaded. Click "Accept & Submit". Done.
|
|
914
|
+
|
|
915
|
+
NEOGOV Attachments upload (two-step flow):
|
|
916
|
+
STEP 1: Click "Add supplemental attachment". A dropdown "Choose attachment type" appears.
|
|
917
|
+
STEP 2: Set dropdown to "Resume" via JS: browser_evaluate: () => {{ const s = document.querySelector('select[name*="attach"], select[id*="attach"], select'); const opt = [...s.options].find(o => o.text.trim() === 'Resume'); if(opt) {{ s.value = opt.value; s.dispatchEvent(new Event('change',{{bubbles:true}})); }} }}
|
|
918
|
+
STEP 3: Click the "Upload" button that appears. browser_file_upload with exact resume path.
|
|
919
|
+
STEP 4: Wait for filename to confirm. Then repeat steps 1-3 for Cover Letter if available.
|
|
920
|
+
STEP 5: Click Next.
|
|
921
|
+
If browser_file_upload fails: browser_evaluate: () => {{ const i=document.querySelector('input[type=file]'); if(i) i.style.display='block'; }} then retry.
|
|
922
|
+
- File upload not working? Unhide the input: browser_evaluate function: () => {{ const i=document.querySelector('input[type=file]'); if(i) i.style.display='block'; }} then browser_file_upload again.
|
|
923
|
+
- Dropdown won't fill? Try browser_select first. If that fails: browser_click to open, then browser_click the option. If that fails: use browser_evaluate to set .value and fire a change event.
|
|
924
|
+
- Checkbox won't check via fill_form? Use browser_click on it instead. Snapshot to verify.
|
|
925
|
+
- Phone field with country prefix: just type digits {phone_digits}
|
|
926
|
+
- Date fields: {datetime.now().strftime('%m/%d/%Y')}
|
|
927
|
+
- Validation errors after submit? Take BOTH snapshot AND screenshot. Snapshot shows text errors, screenshot shows red-highlighted fields. Fix all, retry.
|
|
928
|
+
- Honeypot fields (hidden, "leave blank"): skip them.
|
|
929
|
+
- Format-sensitive fields: read the placeholder text, match it exactly.
|
|
930
|
+
|
|
931
|
+
{captcha_section}
|
|
932
|
+
|
|
933
|
+
== WHEN TO GIVE UP (fail fast, don't waste turns) ==
|
|
934
|
+
- Same page after 3 attempts with no progress -> RESULT:FAILED:stuck
|
|
935
|
+
- Same action failing 3 times in a row (upload, click, fill) -> try ONE alternative approach, then RESULT:FAILED:stuck
|
|
936
|
+
- Job is closed/expired/page says "no longer accepting" -> RESULT:EXPIRED
|
|
937
|
+
- Page is broken/500 error/blank -> RESULT:FAILED:page_error
|
|
938
|
+
- Login loop (redirected back to login after signing in 2+ times) -> RESULT:FAILED:login_issue
|
|
939
|
+
Stop immediately. Output your RESULT code. Do not loop."""
|
|
940
|
+
|
|
941
|
+
return prompt
|
|
942
|
+
|