Commit Graph
443 Commits
Author SHA1 Message Date
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 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
feder-cr 440e43a1f5 engine: no fixed cap on a tool's result; the cut is the model's window, read from OpenRouter on every turn
A result comes back whole. exec, glob/grep and read_file lose their fixed caps (10000/50000, 12000 and 2000
per line, 128000) and the general 12000-character cut is gone. A request that outgrows the model's window is
compacted first, as before; only if it still does not fit are the results of its latest round cut to what the
window leaves, measured with the estimate that judges the request, and the cut says how many characters came
through of how many and how to ask for less. A result is stored cut only beyond what no compaction could make
room for.

The window is read on every turn from the providers OpenRouter lists for the model
(GET /models/{model}/endpoints), not once per process from the whole list: of the providers serving it, the
widest window and the longest answer at that window.
2026-10-12 12:43:40 +02:00
Federico 00f63e88c1 Merge pull request #1447 from feder-cr/browser-libs
image: the browser is invisible-playwright-mcp 0.72.0 (invisible-playwright 0.35.0, invisible-core 41.36.0)
golden-e0c6bb5c740a
2026-10-12 07:53:06 +02:00
feder-cr 9610d12e29 image: the browser is invisible-playwright-mcp 0.72.0 (invisible-playwright 0.35.0, invisible-core 41.36.0)
The core keeps a frame's navigation history when Firefox re-announces the main frame on a cross-process
navigation, redoes a by-value call whose execution context a navigation destroyed, and answers a goto that
another navigation overtook with Playwright's "interrupted" error instead of waiting 45 s. The wrapper carries
invisible_playwright#321's fix. The server reads an element's text in one command and returns it whole:
max_chars is gone from browser_read_text and browser_snapshot. The interface is captured again from the lock.
2026-10-12 07:27:49 +02:00
Federico 81eb3ba067 Merge pull request #1446 from feder-cr/hero-from-photos
docs: the README animation is built from photos in the tree, with new characters
2026-10-12 03:01:05 +02:00
feder-cr 9151ce3386 docs: the README animation is built from photos in the tree, with new characters
The six hero GIFs were rendered from pages that lived only in a scratch folder, so
changing one character meant editing frames. docs/hero/ now holds every piece, in
three layers, each made from the one before:

  photos/  characters.png as an image model draws it, on white; cut_out.py turns
           it into one cut-out per theme (shadows kept on light, removed on dark,
           and only the white connected to the edge touched, so the eyes stay)
  html/    wide, mobile and small pages that compose the photos; renderLoop(t)
           draws the frame at second t, and opened in a browser each page plays
           its own loop
  build.mjs  photos -> frames (Chromium, 12 a second) -> docs/images/hero-*.gif,
           with the same palette and dither as before

A fourth page, html/social.html, builds docs/images/social-preview.png, the
repository's social preview, from the same photos.

The characters are new: the same four, colours inverted, each with a role it can
carry (a hard hat, sunglasses, a beret, a magnifying glass), all still angry. The
scenes, their words and their timing are unchanged.
2026-10-12 02:45:30 +02:00
Federico 96eee2ba8c Merge pull request #1445 from feder-cr/telegram-429-test
tests: the 429 poll test has a bot of its own, and the API test removes the channels it adds
2026-10-12 00:11:32 +02:00
feder-cr fc470314f2 api tests: the test of every Dot's channels removes the channels it added
It added two channels and left them running for the rest of the file and its teardown, the only channels there; on
one Windows run that teardown did not finish in 60 s (not reproduced in twelve runs under load here). A test stops
what it starts: the two channels are removed at its end, and the teardown is what it was before they came.
2026-10-12 00:01:03 +02:00
feder-cr 361b770285 channels tests: the 429 poll test has a bot of its own
It failed now and then in CI (four runs since 2026-10-08), always right after the test that polls TOKEN every few
milliseconds with a 409: a poll of that test still on its way when it ended was answered after the next test had
queued its 429 on the same token, as Telegram answers a request whose client has gone, so the stray poll took the
429 and the new poller connected without the error the test waits for. The 429 test now uses a token no other test
polls.
2026-10-11 23:45:40 +02:00
Federico 6a9cc8c113 Merge pull request #1444 from feder-cr/engine-path-and-shell
The engine runs nothing of the model's by name, reads every sentence of its browser server, and the health is its own context
golden-d9b4cfd820af
2026-10-11 23:31:39 +02:00
feder-cr fb3b667c01 ci: the vitest floors are what this branch's CI measured: 2209 on Linux and 2206 on Windows 2026-10-11 23:17:59 +02:00
feder-cr c5d48ac017 smoke: the browser smoke takes dot's PATH from the desktop's unit
GUEST_PATH was one PATH for the engine and the desktop; with the engine's unit on system directories only, the browser
smoke started the desktop and looked for invisible-playwright-mcp with a variable that no longer existed. It reads
dot's PATH from dot-desktop.service, as lib.sh reads the engine's and the daemon's from theirs.
2026-10-11 23:03:02 +02:00
feder-cr b82ca890ff e2e: step j asks the model to leave the browser open, which the step goes on to need
The step keeps research open "as it would be left at night" so that the server restart and the stop find it open, but
its task never said so; a model closed the browser after reading the title, as a tidy one may, and the step failed on
a choice the prompt left open. The task now asks to leave the browser open.
2026-10-11 22:58:37 +02:00
feder-cr 780fb3b46a engine: a browser opened is known by invisible-playwright-mcp's OPENED, captured like its other sentences
invisible-playwright-mcp 0.71.2 exports OPENED, the first sentence of browser_open's answer, which the engine matched
by its words ("browser is open") because the library had no constant for it. The lock moves to 0.71.2 (only that
package and its hashes), the capture records OPENED beside NOT_OPEN and GONE, and the engine reads all three from
invisible_playwright_mcp.json: no sentence of the server is written in the engine any more.
2026-10-11 22:43:34 +02:00
feder-cr 3d0da4e090 web: the health answer is a context of its own
The shell's context held the health with everything else, so its poll every 30 seconds gave the whole value a new
identity and re-rendered every user of the shell. useHealth reads it from its own context; only the rail's status,
the offline banner, the setup checklist, the key form and the host settings follow the poll.
2026-10-11 19:29:55 +02:00
feder-cr bd1b5866cc guest: the engine runs nothing by name from a directory the model writes
The engine's unit put dot's ~/.local/bin first on its PATH, only so the guest check could find
invisible-playwright-mcp by name; but the engine holds the OpenRouter key, and a program a library ran by name would
have been dot's, run as dotengine. The unit's PATH is now system directories only, as dot-agentd's is, and it names
the browser's MCP server by its path (INVISIBLE_DOTS_MCP_COMMAND), which the engine checks and starts as dot through
the relay. The smoke reads both from the unit.
2026-10-11 19:29:54 +02:00
Federico 1935f47e5a Merge pull request #1443 from feder-cr/fixes-batch
Fixes, removals and one owner per fact, from a read of the whole repository
golden-cb749f0e73bd
2026-10-11 19:02:23 +02:00
feder-cr 08d6786421 ci: the floors are what this branch's CI measured: vitest 2207 on Linux and 2204 on Windows, PostgreSQL 744 with 295 pg runs 2026-10-11 18:54:30 +02:00