Commit Graph
460 Commits
Author SHA1 Message Date
Federico Elia eb54b9b4f7 First-run fixes, forms on real sites, and what contributors need 2026-11-04 23:01:27 +01:00
feder-cr 4c68f897a0 Merge branch 'first-run-fixes' into aihawk-app 2026-11-04 22:52:27 +01:00
feder-cr f7ef2564bb First run: a refused key, quitting, 80 columns, plain errors, BOM files, install and CI
A key that OrcaRouter refuses is forgotten when it came from the key file, and asked for again; a key from the
environment is named by its variable and not forgotten. Ctrl+C leaves at every prompt, and leaving while a prompt
is open no longer raises NoMatches from the heading. Log lines wrap to the whole width, and Quit stays on the
footer at 80 columns. Database and model failures say what happened in plain words, with the detail after them.
Files saved with a byte-order mark are read. The README gives the install commands checked in new venvs, the
index size from the manifest, and how to change the key. CI runs the unit job on Python 3.11 and 3.12 on Ubuntu,
Windows and macOS; gate still needs all of it. The classifiers name the three systems.
2026-11-04 22:52:08 +01:00
feder-cr 7e2da5ab31 Merge branch 'captcha-click' into aihawk-app 2026-11-04 22:38:30 +01:00
feder-cr d2f93b5850 Texts: the one sentence about AIHawk everywhere, the README for a one-minute read, and true statements about models, mail and the database 2026-11-04 22:24:51 +01:00
feder-cr dcec5023b5 Captcha: a box to tick is clicked once, a puzzle is blocked
The form model may click a captcha shown as a box to tick (Turnstile,
reCAPTCHA), usually at its coordinates, wait a few seconds, read the page
again and try once more if the box is still unticked. A puzzle or image
challenge, a login wall or a closed posting is still blocked. No solving
service and no script on a captcha.

Tests: a click on the captcha's frame passes the guard, a captcha frame
does not count as sending or as touching the form, and a captcha click
followed by a send is handled through fill_form.
2026-11-04 22:17:10 +01:00
feder-cr 9094020f7f Open-source basics: CONTRIBUTING, the plugin guide, SECURITY, issue and pull request templates, CHANGELOG 2026-11-04 22:15:46 +01:00
feder-cr d1c3e2d0e6 Forms: deepseek-v4.1-flash by default; Enter allowed in a tag field that asks for it
The default form model moves to deepseek/deepseek-v4.1-flash: about 3 minutes and 1.5 US cents a form on 11 real sites,
85 of 100 on the hard local forms, level with the best within noise, where glm-5.3-flash took twice as long at three
times the cost.

Enter is refused everywhere except a bare Enter in a text input or textarea whose own placeholder or aria-label asks
for it (Paylocity's skills, Teamtailor's tags). Control+Enter and other combinations stay refused.
2026-11-04 21:18:09 +01:00
Federico Elia 69be2d40b5 AIHawk: find the jobs that fit your CV and apply to them 2026-11-04 19:15:02 +01:00
feder-cr ce01c37a31 Docstrings and plugin text: the selection and one Enter is the yes, no auto-submit; plugin after-sending notes say only what to read 2026-11-04 19:10:53 +01:00
feder-cr cdf2129e86 Merge branch 'send-and-list' into aihawk-app
# Conflicts:
#	README.md
#	tests/test_apply.py
#	tests/test_jobs.py
2026-11-04 19:06:20 +01:00
feder-cr 9840ed6da4 Send the selected jobs on one yes, keep what each page said, and list what was sent
One question before a batch is sent (Enter sends, Escape cancels); no per-job confirmation and no --auto-submit.
A form's questions are asked after the others are sent, then the form is tried again.
After the click the verifying model gets the plugin's text as its rules and the page as untrusted text.
Each clicked job keeps its time, final address, verdict and evidence, page text and a screenshot under data/proofs.
Key s lists every job tried with its plain status; applied.csv is rewritten after each attempt, cells that start like a formula are made text.
2026-11-04 19:04:47 +01:00
feder-cr 193b5ed027 Merge branch 'small-description-files' into aihawk-app 2026-11-04 18:58:54 +01:00
feder-cr a5ac30fbba Descriptions read one query per file: 128 MB files and 2048-row groups, measured at 470 MB to 111 MB per search 2026-11-04 18:58:31 +01:00
feder-cr 2d036b1d2c Lint: what the full rule set finds, unused noqa and an explicit check 2026-11-04 18:57:09 +01:00
feder-cr 776bfee32b Merge branch 'script-rung' into aihawk-app 2026-11-04 18:53:27 +01:00
feder-cr e9a9c7746e AIHawk: offer browser_evaluate as the last rung, watched after each call
The model may run script again, by the owner's decision, knowing a script could send a form without the person's yes.
NEVER_OFFERED is gone. The system prompt says script comes only after selectors, coordinates and a screenshot have
failed, and never to send the application. browser_evaluate counts as acting, so a script that leaves the form's host
stops the form as unconfirmed. The docstrings and the README say what holds.
2026-11-04 18:53:17 +01:00
feder-cr 0c82d4317a plugins: how to tell a sent application was received, per ATS
Each of the 12 site plugins gets a last section: what follows a submit,
with the evidence level per site (real send, saved page, documentation or
nothing). No form was submitted for this.
2026-11-04 18:50:12 +01:00
feder-cr 7179db6da6 Judge with Jev in batches: up to 85 postings and 180k tokens a request, split on failure 2026-11-04 18:33:55 +01:00
feder-cr 4ea8594019 Merge branch 'jev-cloud' into aihawk-app 2026-11-04 18:25:40 +01:00
feder-cr 1a837c5c72 Judge with Jev through OrcaRouter only: drop the local download, the process and the fallback 2026-11-04 18:25:32 +01:00
feder-cr 6f66f1b767 Read the page again after a change before it can end a form
A read that fails after a change says nothing: the page can be loading or still being typed into. It is read again a few
times, and only a page that is read and is on another host stops the form as unconfirmed. Personio's posting goes
straight to its form with apply_path /apply, so its apply link is no longer clicked.
2026-11-04 18:21:10 +01:00
feder-cr 5c676d8151 Place words start after a separator, a dash or a number, not after a word
Keeps Dallas TX, Berlin Office, Milano Via Turati and Dallas-Fort Worth; refuses New London, New Berlin and New South Wales.
2026-11-04 18:05:27 +01:00
feder-cr e2efac4135 Fix the defects of a real end-to-end run
Place words match whole parts of the location, so Wales is not New South Wales.
A location answered no scores 1, out of the top.
Progress counts while descriptions and jevos read; jevos is said to download or start only when it does.
Looking for shows the first wordings of the plan's nodes, not their ids.
A failed jevos verdict is counted once per batch, not taken for no opinion.
A scan's log lines reach the screen from the loop, not from its thread.
A judged posting with no description is marked no text in its status.
2026-11-04 17:57:38 +01:00
feder-cr 1572ff93b4 Merge branch 'guard-acting-tools' into aihawk-app 2026-11-04 17:40:53 +01:00
feder-cr 64ebd4aa42 apply: stop a form that leaves for another site after the model's own act, and hold the model to its sites 2026-11-04 17:40:39 +01:00
feder-cr 7686bcafe1 Judge the shortlist with jevos: the funnel replaces the title screen
jevos weighs the next 100 postings by title with six closed questions; the best 25 by the title's fit and that evidence go to the judge model for a score, and the rest are kept as passed over. The title screen, its status and its tests are gone.

jevos is pinned to jevos-v5 and downloaded at first use, checked by SHA-256 before unpacking. Where it cannot run, the cloud model answers the same questions; where there is no key either, the title decides.
2026-11-04 17:36:19 +01:00
feder-cr 0da41b3b69 README: shorter page, limits left out 2026-11-04 17:23:40 +01:00
feder-cr 99e2a8a16a README: the published database, shorter page 2026-11-04 17:23:34 +01:00
feder-cr 750e65ea80 README: what the app does now, the sites, the plan, the defaults 2026-11-04 17:23:03 +01:00
feder-cr b3338a99e4 apply: close the ways a form could leave without the person's yes
A review found them, each confirmed on the code. Script is no longer offered to the model: requestSubmit?.() passed every
check, and no check can tell what a script will do. Space on a focused control, a new line outside a text area, and an
image input are held back like the rest, and a renamed argument no longer lets Enter through.

The control the model names is clicked only if it is one that sends. A click that answers with an error, or a result that
cannot be read, is unconfirmed and never tried again: a real Ashby form disabled its button and was recorded as failed
when it may have gone.
2026-11-04 17:19:43 +01:00
feder-cr f8de6b19eb aihawk: one browser profile to sign in to, and prompts to read a code from the mail
The browser opens on one profile kept in the person's folder, and the key l opens it for them to sign in to their mail and
any site. Gmail and Outlook are plugins with `when: always`: they join the instructions of every form without making a
site count as supported, and say how to fetch a code or a link, only reading. Their pages come from public documentation
and have not been tried on a real mailbox.

The check on what sends a form knew few languages: Gmail's Senden passed it. It knows the main ones now.
2026-11-04 17:11:04 +01:00
feder-cr da5517bd4f aihawk: matching from a weighted plan graph, more sites, and the browser as its server says
The search plan is a graph of nodes and weighted forms with conditional arcs, after the owner's harrier engine, kept in
aihawk.yaml so it can be tuned. Search scores titles in Python from it, by rarity among the postings of the place, and a
town's name no longer matches where the country column says otherwise.

Judging asks closed questions and computes the score from the answers. Plugins now describe how each site's pages are
built; SmartRecruiters, Teamtailor, JazzHR, BambooHR and Personio are new, from rendered pages.

The browser window is visible. The model gets the server's own instructions, its rungs of coordinates and screenshots,
and any tool it marks read-only; a click at a point goes through the same check as a click on a selector. The form rules
now send certifications and unstated facts back as questions, after a real test ticked them. A daily run tries the
newest invisible-playwright-mcp.
2026-11-04 16:53:34 +01:00
feder-cr c88169fbb7 aihawk: one command and a screen in the terminal: give it a resume, select jobs, apply
`aihawk resume.pdf` is all a person types. The model reads the resume and works out what to search and the contact
details it states; the list of jobs, best first, fills in on a terminal screen as they are judged; space selects, a
applies, and what a form asks that the files do not say is asked there and remembered. Nothing is sent without y.

Searching the published files over the network took two minutes each time, so the database now publishes a light index
that is downloaded once a day and searched locally. A description is read from the database only for a job being judged.
Filters match whole words, so java is not javascript and roma is not Romania.
2026-11-04 13:02:20 +01:00
feder-cr 927d01f797 scan: filters match whole words, so java is not javascript and roma is not Romania 2026-11-04 12:22:56 +01:00
feder-cr db192afb8e publish: a release left as a draft is finished, not uploaded again, and a failed gh says why 2026-11-04 12:19:21 +01:00
feder-cr 934f153d05 apply: a plugin per site, a prompt that tells the model how the site's form works
A plugin is a Markdown file: the sites it is for, how a posting's link becomes the form's, and what the model must know
about the form. AIHawk applies where a plugin exists and lists the rest, so adding a site takes no code, and a person
can add their own beside aihawk.yaml. The first seven were written from real forms read in the browser, with nothing
filled in. What protects the person stays in apply.py, ahead of any plugin's words.

Reading those forms showed the refusal was wrong for Ashby: its Yes and No answers are untyped buttons inside the form,
which count as submit buttons by HTML's rule, so the model could never have answered them.
2026-11-04 12:16:59 +01:00
feder-cr 5751d35810 pipeline: leave out roleBase's dead links; each run reads a clone of its own
roleBase's links ending in a 12-character hex code are not the boards' own ids: of 40 at random, 20 answered 404 and 6
answered 403, against none of 40 of its other links. Dropping them takes the database from 16.2 to 10.2 million postings.
A clone left half-deleted on Windows was read as if whole, so each run now clones to a folder of its own.
list: a Windows console's code page no longer stops it on a company's name.
2026-11-04 12:10:15 +01:00
feder-cr 04f93d3e5a scan: names without HTML entities, and the template says locations match as written 2026-11-04 11:48:01 +01:00
feder-cr 8cead59b7b AIHawk: find the jobs that fit your CV and apply to them
This repository is AIHawk again. Dots moved to feder-cr/invisible_dots with its history.

Every day a job merges the public job databases into one (pipeline/), one row per posting, and publishes it as a
release. `aihawk scan` queries it in place, so a user scrapes nothing and downloads only what the search reads. `score`
judges jobs against the CV through OrcaRouter, `apply` tailors the CV and fills the form through invisible-playwright-mcp.

The model never sends an application: it fills the form and names the submit control, and AIHawk clicks it after the
person's yes, or by itself with --auto-submit. While the model works, a click on what would send the form and the Enter
key are refused. What the CV and the answers file do not say comes back as a question, never as a guess.
2026-11-04 11:47:28 +01:00
Federico Elia da1c7ff106 Merge pull request #1456 from feder-cr/boot-order
guest: install.sh alone starts the Dot's units, after the VM proxy
jobs-latest
2026-11-04 10:02:28 +01:00
feder-cr 44a51c67f5 guest: install.sh alone starts the Dot's units, after the VM proxy
The units were enabled, so on every boot after the first systemd started them at multi-user.target, before
cloud-init's final stage ran install.sh (cloud-final.service is After=multi-user.target): a Dot's serial log shows
dot-agentd and the engine up about two seconds before install.sh, which is what sets the VM proxy's firewall and
tunnel. A Dot with a proxy could go out directly in that window, and install.sh's own comment said the opposite.

The units are no longer enabled (no [Install]): install.sh starts them on every boot after the proxy, as it already
did on the first, and removes the wants links an earlier runtime made. install.sh is also the first point of the
boot that has the seed's config.json, which the same final stage writes.
2026-10-15 09:23:51 +02:00
Federico Elia ef69f5e19c Merge pull request #1454 from feder-cr/libs-latest
browser: never pinned; the image takes the newest release and the engine reads the server it finds
golden-a61faff1b2d9 golden-0657d4ff61e9 golden-73536fa97ad6 golden-798090c64bb0 golden-8dca7f57f998 golden-9c2b3c5b93ef golden-a461e2d8e225 golden-67532b5680e8 golden-a92891c4ded8 golden-0b39ccd1b116 golden-b38d7171e8ec golden-1cf048d96945 golden-ce0e2cde6174 golden-d27023f0394d
2026-10-14 18:38:56 -07:00
feder-cr ed406afb14 smoke and dependencies: the stand-in is a server package, and the capture runs on the server's python
The engine smoke found its stand-in by fake_mcp_server.py on the command line; installed the way the real server is,
it runs as `python -I -B -m invisible_playwright_mcp`, and that command line is what the checks look for.
test_dependencies names server_capture.py as a module another interpreter runs: its invisible_playwright_mcp import
is the installed server's, not a dependency of the engine.
2026-10-15 02:18:52 +02:00
feder-cr 2968ca8372 browser: never pinned; the image takes the newest release and the engine reads the server it finds
A Dot keeps the golden image it was made from and boots every new runtime disk, so the engine already talked to
servers other than the one its pinned copy of the interface described. The engine now reads the interface from the
installed server when it starts (nanobot/dots/server_interface.py, server_capture.py run as dot by the server's own
python): the page tools are what that server serves, a page tool it does not serve is not offered (GET /tools says
so), and a Dot whose server cannot be read offers no page tool and its launches say why.

With nothing in the engine tied to a version, the image no longer pins one: each build asks PyPI for the newest
invisible-playwright-mcp, the newest invisible-playwright it accepts and the invisible-core that one pins
(browser-release.ts). The three enter the inputs digest, pins.env and the manifest, which records what was installed.
mcp-requirements.lock and capture-mcp-interface.py go; the copy of the interface becomes a test fixture. The owner's
no-other-browser rule is checked at build on what was actually installed (builder/other-browsers.txt), and the golden
image workflow runs every six hours so a new release is published as a prebuilt image.
2026-10-15 02:04:31 +02:00
Federico 4d20e947a1 Merge pull request #1450 from feder-cr/libs-0722
image: the browser is invisible-playwright-mcp 0.72.2 (invisible-playwright 0.35.7, invisible-core 45.39.0, firefox-45)
golden-27aac91db9e8
2026-10-13 14:33:32 -07:00
feder-cr 6712104ea5 image: the browser is invisible-playwright-mcp 0.72.2 (invisible-playwright 0.35.7, invisible-core 45.39.0, firefox-45)
The server's snapshot and read_html read again on the page that replaced the one they went to (page.evaluate_read),
and browser_evaluate, the model's code, runs once when its page navigates under it (B285). Under them, through
firefox-45: a page that replaces itself keeps its main world (B286), the real mouse no longer turns a click into a
drag, a session keeps focus when another starts beside it, iframes stay in the frame tree after a few navigations,
go_back/go_forward to a same-document entry answer, a browser that dies ends every wait at once, every input event
is born a few ms before the page sees it, and the first enumerateDevices of a session takes a time the seed
declares. The tools' schemas and descriptions are unchanged: the recaptured interface differs only in its version.
2026-10-13 22:25:33 +02:00
Federico a2584f2619 Merge pull request #1448 from feder-cr/no-char-caps
engine: no fixed cap on a tool's result; the cut and the routing follow the model's window, read from OpenRouter on every turn
golden-f60c6fbd46aa
2026-10-12 08:35:49 -07:00
feder-cr fe03ee6325 floors: the engine suite at the 2493 tests CI measured on this branch 2026-10-12 17:20:59 +02:00
feder-cr 5c920517e4 engine: a model's requests go only to the providers of its widest window; image: invisible-playwright-mcp 0.72.1
OpenRouter routes a request by the answer it asks for but not by the prompt it carries, so a request sized for
the model's widest window could land on a provider with a narrower one (glm-5.3-flash: Reka at 262144 among
providers at 1048576) and fail there. The providers read with the limits on every turn now route the model's
requests: provider.only is the slugs of the widest window's providers. When none of them can serve, OpenRouter
refuses with "No allowed providers are available" and the turn fails with those words.

invisible-playwright-mcp 0.72.1 drops its last caps: browser_type quotes the value the field kept whole, and an
error detail in its chat and routes comes whole. The tools' schemas are unchanged.
2026-10-12 16:35:10 +02:00