Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ logs/
.mcp*.json

# Python
.venv/
venv/
__pycache__/
*.py[cod]
*$py.class
Expand Down
19 changes: 15 additions & 4 deletions src/applypilot/apply/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import os
import platform
import re
import shutil
import signal
import subprocess
import sys
Expand Down Expand Up @@ -111,7 +112,7 @@ def acquire_job(target_url: str | None = None, min_score: int = 7,
FROM jobs
WHERE (url = ? OR application_url = ? OR application_url LIKE ? OR url LIKE ?)
AND tailored_resume_path IS NOT NULL
AND apply_status != 'in_progress'
AND (apply_status IS NULL OR apply_status != 'in_progress')
LIMIT 1
""", (target_url, target_url, like, like)).fetchone()
else:
Expand Down Expand Up @@ -321,9 +322,12 @@ def run_job(job: dict, port: int, worker_id: int = 0,
mcp_config_path = config.APP_DIR / f".mcp-apply-{worker_id}.json"
mcp_config_path.write_text(json.dumps(_make_mcp_config(port)), encoding="utf-8")

# Build claude command
# Build claude command. Resolve the full path so Windows can spawn the
# `claude.cmd` shim -- subprocess/CreateProcess won't find a bare "claude"
# name (no PATHEXT resolution), causing WinError 2.
claude_exe = shutil.which("claude") or "claude"
cmd = [
"claude",
claude_exe,
"--model", model,
"-p",
"--mcp-config", str(mcp_config_path),
Expand Down Expand Up @@ -465,7 +469,7 @@ def run_job(job: dict, port: int, worker_id: int = 0,
def _clean_reason(s: str) -> str:
return re.sub(r'[*`"]+$', '', s).strip()

for result_status in ["APPLIED", "EXPIRED", "CAPTCHA", "LOGIN_ISSUE"]:
for result_status in ["DRY_RUN", "APPLIED", "EXPIRED", "CAPTCHA", "LOGIN_ISSUE"]:
if f"RESULT:{result_status}" in output:
add_event(f"[W{worker_id}] {result_status} ({elapsed}s): {job['title'][:30]}")
update_state(worker_id, status=result_status.lower(),
Expand Down Expand Up @@ -608,6 +612,13 @@ def worker_loop(worker_id: int = 0, limit: int = 1,
release_lock(job["url"])
add_event(f"[W{worker_id}] Skipped: {job['title'][:30]}")
continue
elif result == "dry_run":
# Preview only -- never record as applied or failed. Clear the
# in_progress lock so the job stays available for a real apply.
release_lock(job["url"])
add_event(f"[W{worker_id}] DRY RUN done: {job['title'][:30]}")
update_state(worker_id, status="dry_run",
last_action=f"dry run ({duration_ms // 1000}s)")
elif result == "applied":
mark_result(job["url"], "applied", duration_ms=duration_ms)
applied += 1
Expand Down
30 changes: 26 additions & 4 deletions src/applypilot/apply/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,14 +507,35 @@ def build_prompt(job: dict, tailored_resume: str,
last_name = full_name.split()[-1] if " " in full_name else ""
display_name = f"{preferred_name} {last_name}".strip()

# Dry-run: override submit instruction
# Dry-run: this is a PREVIEW. It must produce NO real side effects --
# no account creation, no data entry, no submission. Stop early and report
# what a real run would need to do, then emit a DISTINCT result token so the
# launcher never records it as a real application.
if dry_run:
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."
mission_tag = "This is a DRY RUN (preview only) -- DO NOT submit, DO NOT create accounts, DO NOT enter any data."
submit_instruction = "DRY RUN: do not fill or submit anything. Per the DRY RUN RULES you should already be stopping here. Output RESULT:DRY_RUN."
dry_run_banner = (
"\n== DRY RUN RULES (HIGHEST PRIORITY -- THESE OVERRIDE EVERY STEP BELOW) ==\n"
"This is a PREVIEW to inspect the application, not to apply. Do ONLY this:\n"
"1. browser_navigate to the job URL and browser_snapshot.\n"
"2. Do the LOCATION CHECK.\n"
"3. Locate the Apply button. You MAY click Apply ONCE to observe what the first\n"
" step is (e.g. a login wall or account-creation screen).\n"
"Then STOP. You must NOT, under any circumstances: log in, register or create an\n"
"account, enter a password, type or upload ANY personal data, upload the resume or\n"
"cover letter, answer screening questions, or click Continue/Save/Next/Apply/Submit\n"
"on any form, and NEVER send email.\n"
"Finish by outputting RESULT:DRY_RUN followed by a 2-3 line summary of what a real\n"
"run would require (account needed? login/SSO wall? approx number of steps? any\n"
"blockers like CAPTCHA). NEVER output RESULT:APPLIED in a dry run.\n"
)
else:
mission_tag = "Submit the application."
dry_run_banner = ""
submit_instruction = "BEFORE clicking Submit/Apply, take a snapshot and review EVERY field on the page. Verify all data matches the APPLICANT PROFILE and TAILORED RESUME -- name, email, phone, location, work auth, resume uploaded, cover letter if applicable. If anything is wrong or missing, fix it FIRST. Only click Submit after confirming everything is correct."

prompt = f"""You are an autonomous job application agent. Your ONE mission: get this candidate an interview. You have all the information and tools. Think strategically. Act decisively. Submit the application.

prompt = f"""You are an autonomous job application agent. Your ONE mission: get this candidate an interview. You have all the information and tools. Think strategically. Act decisively. {mission_tag}
{dry_run_banner}
== JOB ==
URL: {job.get('application_url') or job['url']}
Title: {job['title']}
Expand Down Expand Up @@ -585,6 +606,7 @@ def build_prompt(job: dict, tailored_resume: str,
12. Output your result.

== RESULT CODES (output EXACTLY one) ==
RESULT:DRY_RUN -- dry-run preview complete, nothing submitted (use ONLY in dry runs)
RESULT:APPLIED -- submitted successfully
RESULT:EXPIRED -- job closed or no longer accepting applications
RESULT:CAPTCHA -- blocked by unsolvable captcha
Expand Down