agentleFS
strelov1strelov1/freehire/extension/AGENTS.md

freehire / extension

AGENTS.md3.5k starsChanged 3 months agoMITView on GitHub

Guidance for AI agents working in this directory. A browser extension that puts a job-application agent in a side panel — a Claude/Gemini-style side bar with access to whatever page the user is on. The agent is freehire's own, running inside hire's API (internal/ai/assistant): the panel holds a conversation under the browse preset, whose agent can read the open page through the browser-tool relay this extension serves. Current state: the chat runs on hire's assistant. Sign in with freehire and…

  • Reads credentials
  • Installs packages

We split this AGENTS.md into the rules it gives an agent

2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    No local server, and no separate agent service.

    line 25
  2. Distinctive:

    Read-a-page flow — note that the panel is not the one deciding:

    line 90

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md

Guidance for AI agents working in this directory.

What this is

A browser extension that puts a job-application agent in a side panel — a Claude/Gemini-style side bar with access to whatever page the user is on. The agent is freehire's own, running inside hire's API (internal/ai/assistant): the panel holds a conversation under the browse preset, whose agent can read the open page through the browser-tool relay this extension serves.

Current state: the chat runs on hire's assistant. Sign in with freehire and talk to it; there is no "read page" affordance, because the agent calls read_current_page itself when a question needs it. A profile-aware match card, page intake and agent-driven Autofill also ship.

Everything reaches one origin. Auth crosses it with the session JWT the connect flow minted: Authorization: Bearer on the HTTP API, the freehire-jwt subprotocol on the relay's WebSocket (a browser cannot set headers on new WebSocket).

Stack: WXT + Svelte (Chrome MV3; Firefox MV2 from the same source), styled on freehire-design-system (Tailwind v4 tokens + primitives — see the root AGENTS.md's design-system row). No local server, and no separate agent service.

Layout

extension/            WXT + Svelte MV3 extension (npm-managed; the repo's other
                       JS packages, web/ and design-system/, are pnpm)
  entrypoints/
    background.ts     service worker: opens the panel, relays panel <-> content
    content.ts        injected everywhere; reads the page into a PageSnapshot
    sidepanel/        Svelte chat app (owns the relay WebSocket)
      App.svelte      chat UI + match card + apply plan + page intake + Autofill
      MatchCard.svelte  profile-match card
      ApplyPlan.svelte  the open form's questions, answered or not, + the counter
      ToolGroupList.svelte  what the agent is doing, mid-turn
      JobDeck.svelte / JobDeckCard.svelte  the `present_jobs` cards
      app.css         Tailwind + freehire-design-system/theme.css import
      main.ts         Svelte 5 mount
  lib/
    assistant/        the chat: SSE reader, wire types, chat + deck reducers,
                      tool formatters, the session store, and the API and turn
                      clients (+ tests). Ported from the freehire web assistant.
    tools/            browser-tool executor: the wire contract, the executeTool
                      dispatch (+ tests), the ToolChannel socket to hire's relay,
                      and the PageBridge that reaches the tab's frames.
    auth.ts           "Sign in with freehire" (launchWebAuthFlow) + token storage
    analytics.ts      anonymous usage events to GA4 via the Measurement Protocol
                      (gtag.js is remote code MV3 forbids); inert without the
                      WXT_GA_* build vars (+ test)
    freehire.ts       hire API reads (job, match, autofill profile)
    protocol.ts       in-extension RuntimeMessage contract (+ test)
    scraper.ts        DOM -> PageSnapshot + JobPosting JSON-LD check, pure over
                      its Document arg (+ test)
    addJob.ts         whether the empty match state offers "Add this job" (+ test)
    form.ts           form observe/map/act/reveal for Autofill (+ test)
    applyPlan.ts      form fields -> the panel's checklist + required counter (+ test)
    walk.ts           the order an autofill works through the form, as a value (+ test)
    debounce.ts       one call after a burst goes quiet (+ test)
    combobox.ts       drives custom-widget comboboxes (open/options/select/verify);
                      pure over its Document like form.ts, but async (+ test)
  wxt.config.ts       manifest (permissions, side_panel, host_permissions) +
                      the Tailwind Vite plugin

Architecture

  • Side panel (Svelte) owns the relay WebSocket. It's the only context that stays alive while open — unlike the MV3 service worker, which Chrome kills when idle, so it must not hold the durable socket. The chat needs no socket at all; each turn is its own request.
  • Content script reads the live DOM on request. It holds no state.
  • Background is a thin relay: the panel can't message a content script directly, so background forwards a snapshot request to the active tab.

A turn is one POST whose response body streams SSE (lib/assistant/): create a conversation once (POST /assistant/sessions?preset=browse), then post each message and fold the streamed TurnEvents into the message list. Nothing is held open between turns, and cancelling is aborting the fetch — the backend notices its next write fail and stops before spending another model call.

The conversation id lives in storage.local, so closing the panel and reopening it resumes; the transcript is replayed from the server through the same reducer a live turn folds through. A conversation deleted from the web starts a fresh one silently.

Read-a-page flow — note that the panel is not the one deciding:

hire's agent --read_page--> relay --> panel
panel --GET_PAGE_SNAPSHOT--> background --(active tab)--> content
content --PAGE_SNAPSHOT--> background --> panel --> relay --> the agent, mid-turn

Conventions

  • protocol.ts is the in-extension contract — RuntimeMessage (panel <-> background <-> content, discriminated by kind). Neither the chat's wire nor the browser-tool wire is here: they are lib/assistant/wire.ts and lib/tools/wire.ts, each mirroring a contract hire owns.
  • Autofill is watched, not batched. The panel walks the form one question at a time (lib/walk.ts holds the order as a value; App.svelte supplies the ~300ms pauses), each fill carrying reveal so the page scrolls to the question and outlines it as the value lands. The pause is for the eye — the walk IS the audit the user would otherwise have to do afterwards. The agent path fills server-side as before and the panel plays its report back over the page, revealing each label it reported without re-writing anything.
  • The plan is the panel's standing account of the form, computed by buildPlan from what the page reported and rebuilt on every page change, after a walk, on FORM_CHANGED, on returning to the Match tab and on the panel regaining focus. It counts REQUIRED questions only (they are what gates submission) and is null for a page showing no application form.
  • Two different tests for "this is an application". looksLikeApplication (a CV upload) gates the FILLER — it is what stops a job-alert signup being written into. showsApplicationForm gates the CHECKLIST and is deliberately looser: an upload, OR four labelled questions, OR a form the panel has already written into. The upload sits on step ONE of a multi-step ATS form, and every step after it — the screening questions — has none, which is why the strict test showed no checklist where it was most wanted. Nothing is written on the strength of the looser one.
  • PlanItem.key is frame:index, never the label. Two questions on one form can carry the same label, and a keyed {#each} over a duplicate throws each_key_duplicate — which takes the whole card down and looks, from the panel, like the feature simply not existing. That was the real reason the checklist never appeared on a live ATS form.
  • A fill names the scope it was read from, and refuses what it cannot resolve. read_form reports frame and form on every field, and fill_simple carries both back: frame picks one of the tab's documents, form one <form> inside it. Where a label matches more than one question and the fill names no form, fillByLabel writes NOTHING and reports ambiguous — a careers page routinely carries its application beside a job-alert signup, both asking for an email address, and taking the first match writes the candidate's details into a form they did not choose and calls it filled. The three refusals are kept apart because each implies a different next step: ambiguous means re-send naming a form, wrong_form means the index is wrong, not_fillable means no re-send helps until the page changes. This is also why readFills must not drop a field it is sent — hire tagged every fill with its frame for months while this reader destructured only {label, value}, so the tag never arrived and every agent fill was broadcast to every frame and matched by label alone. Both sides' tests were green: one checked the sender, the other checked this reader against arguments written by hand beside it. What catches that now is one fixture written from hire's live struct (lib/tools/testdata/fill-simple-call.json, regenerated by UPDATE_WIRE_FIXTURE=1 go test ./internal/ai/autofillagent/) and read back by this side's own test.
  • Form reads are serialised, not raced. A request-id guard (the matchRequestId pattern) starves here: an ATS page announces DOM changes continuously, so every read finds its id already superseded and returns before assigning. One read at a time with a re-read queued behind it cannot starve and cannot land stale. For the same reason the page announces a mutation only when the NUMBER of controls changes — typing is covered by the input/change listeners, where it does not.
  • "Add this job" is offered only on a plausible vacancy (offersAddJob): a URL /jobs/find reports as a recognised ATS board (meta.ats_board, from the server's atsboard.Recognize — never copy its host list here), or a page carrying schema.org JobPosting JSON-LD. Offered everywhere, it filled the triage queue with LinkedIn feeds, chat apps and localhost (63 submissions, nearly all junk).
  • revealField borrows the page's style and gives it back. The outline is inline style, restored after ~600ms: the extension runs on pages freehire does not own, where an injected class can collide with the ATS's own and a stylesheet outlives our interest in the element.
  • The agent's reading is bounded, and visible. read_page refuses any tab that is not http(s) (lib/tools/readable.ts), decided from the url before the page is read — this extension is the only side that sees a url before scraping it. The panel then names the page each read touched, minus query and fragment, which is where session tokens live. That display lives in lib/assistant/pageRead.ts rather than in tool-formatters.ts, precisely because the latter is a verbatim port and read_current_page cannot occur in the web app.
  • lib/assistant/ mirrors the freehire web assistant (web/src/lib/assistant in this repo). sse, wire, chat, deck and tool-formatters are verbatim ports — keep them aligned. Three files diverge, and they say so at the top: api.ts/client.ts (absolute origin + Bearer, since extension code has no cookie) and jobCache.ts (reads the token the web's cookie made implicit).
  • Test the logic, not the transport. scraper, form, protocol, the assistant's reducers and its API clients are tested (vitest); the chrome message plumbing, the live socket and storage.local helpers are thin glue.
  • TypeScript stays on 5.x — svelte-check does not yet support the native typescript@7 (tsgo).
  • Styling comes from freehire-design-system via a file:../design-system dependency (npm's local-symlink protocol — pnpm's link: has no npm equivalent by that name). Components not covered by an existing primitive keep local scoped CSS, but reference the package's CSS custom properties (var(--brand), var(--muted-foreground), ...) rather than literal colors. design-system/ must have its own dependencies installed (cd design-system && pnpm install) before extension/ will build — same gotcha the root AGENTS.md documents for web/: a file:/link:-linked package's own dependencies are not pulled in by the consumer's install.

Commands

# hire must be reachable at WXT_HIRE_ORIGIN (default: the local SPA at :5173,
# which proxies /api). Any signed-in account will do — the assistant is open
# to every signed-in user.

cd design-system && pnpm install     # once, and after pulling design-system changes
cd extension && npm install          # runs `wxt prepare` (generates .wxt/)
cd extension && npm run dev          # dev build with HMR
cd extension && npm run build        # production build -> .output/chrome-mv3
                                     # targets freehire.me
                                     # (extension/.env.production); dev keeps
                                     # the localhost default
cd extension && npm test             # vitest: lib/**/*.test.ts
cd extension && npm run check        # svelte-check

Load unpacked in Chrome: chrome://extensions → Developer mode → Load unpacked → extension/.output/chrome-mv3. Click the toolbar icon to open the panel.

Firefox

npm run dev:firefox / npm run build:firefox (-> .output/firefox-mv2) / npm run zip:firefox (the add-on plus the sources ZIP addons.mozilla.org reviews; publishing is in docs/firefox-addons.md). Load it at about:debugging → This Firefox → Load Temporary Add-on → the build's manifest.json. The sidepanel entrypoint becomes Firefox's sidebar_action, and the toolbar click toggles it (background.ts).

Two differences, both in wxt.config.ts:

  • No Attach tailored CV. It needs chrome.debugger, which Firefox does not have, so the panel hides the button (import.meta.env.FIREFOX).
  • A different sign-in redirect. Firefox's identity.getRedirectURL() is https://<sha1(gecko id)>.extensions.allizom.org/, so hire's EXTENSION_REDIRECT_ALLOWLIST must carry that hash beside the Chrome ids. For extension@freehire.me it is 13f2644fb08a641dc52d5b7c11faf2fb9775ee26; the gecko id is therefore as fixed as Chrome's key.

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository4 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.