Ctrl K
ComunidadGuías…

CLAUDE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Hired — working agreement

Read this before writing code. It is the difference between a change that fits and a
change that has to be reverted.

This file is the briefing for **any** coding agent, not just Claude — `AGENTS.md` is a
symlink to it, so Cursor, Codex, Gemini and whatever comes next all load the same words.
If you change how this project is worked on, change it here; there is no second copy to
keep in sync.

## What this is

A self-hosted career operating system: **Me** (everything you know about every job
you've had), a **resume builder** that assembles documents from that material, and a
**pipeline** CRM for the search. One person per workspace, many people per instance.

**The interface is a conversation.** The web app is real and good, but the product thesis
is that you talk to this thing — "here's what I did last quarter, file it", "tailor my
resume to this posting", "what do I need to chase this week" — and the UI is where you go
to look at, adjust and print the result. Every design decision follows from that.

The audience is the author and his friends. Not enterprise. Optimise for *one person can
deploy this in five minutes and never touch a terminal again*.

---

## Rule zero: MCP first, UI second

**A feature is not done until it is callable from a conversation.**

The order of work is fixed:

1. `prisma/schema.prisma` — model the data, write the migration
2. `src/lib/data/*.ts` — the data function, `userId` first argument
3. `src/lib/mcp/tools.ts` — the MCP tool(s), with a description an assistant can act on
4. `src/server/actions.ts` — the server action, if a human needs to do it by hand
5. `src/app` + `src/components` — the screen

Steps 1–3 are mandatory. Steps 4–5 are mandatory only when a person needs to *see* or
*adjust* the thing. If you find yourself building a form before the tool exists, stop —
you are building the wrong product.

**The three exceptions**, and they are narrow: direct-manipulation editing (dragging a card
between stages, typing into the resume editor), rendering (the paper, the print page), and
display preferences (a column's width, which fields a card shows). Those are UI-native.
Everything the first two manipulate still has to be reachable by tool — a card's stage, a
resume's words. A display preference is not career data and gets no tool: four of them were
cut in October 2026 because every tool costs routing quality on every client, and nobody asks
an assistant to make a column 180px wide.

When you finish a feature, ask literally: *can an assistant connected over MCP do this
end to end with no browser open?* If the answer is no, it is not finished.

---

## Invariants — do not break these

**1. Tenant isolation is a compile-time property.**
Every function in `src/lib/data/` takes the owning `userId` as its **first positional
argument** and every query filters on it. It is positional and required precisely so the
compiler rejects a call site that forgets. Never add a data function with an optional or
object-bag `userId`. Never let a `userId` arrive from the client — server actions resolve
the caller from their session cookie, MCP tools resolve them from the connection token.
Admins manage *accounts*, never *content*: no code path lets one user read another's
career history, resumes or applications.

**2. The MCP tools and the UI share one data layer.**
Both call `src/lib/data/`. If you write logic in a server action that a tool would also
need, you have forked it — move it down. There is exactly one implementation of every
rule about the data.

**3. The MCP transport is stateless.**
`src/lib/mcp/handler.ts` is a hand-written Streamable HTTP implementation. Every POST is
self-contained and re-resolves its user from the token, which is what makes restarts,
replicas and instant suspension work. Do not introduce session state, in-memory caches
keyed by connection, or anything that assumes two requests hit the same process.

**4. The resume document schema is a contract.**
`src/lib/resume-schema.ts` is shared by the database (`Resume.data` JSON), the renderer
and the tools. Every field is optional-with-a-default so a half-generated document still
renders. Keep it flat and boring. Adding a field is fine; changing the meaning of one
means old saved documents render wrong.

**5. Self-hosting stays one variable.**
`DATABASE_URL` is the only required env var. Everything else is configured inside the app
and lives in the `Setting` table. If a feature needs configuration, it gets an admin
panel and an `admin_*` tool — not a new environment variable. The narrow exception is
machine-level facts the app cannot know about its host — where the Chromium binary lives
for PDF export, say — which may be optional env vars with working defaults. The app
provisions its owner account at boot (`src/lib/bootstrap.ts`) and applies migrations on
start.

**6. No fabrication.** The server instructions in `handler.ts` tell every connected client
never to invent experience, employers, dates or metrics. Any new tool or prompt that
generates resume content inherits that rule and should restate it.

---

## The map

```
prisma/schema.prisma          Data model. Migrations in prisma/migrations/, applied on boot.
src/lib/data/                 THE data layer, one file per area — Me (me, wins,
                              transferables, linkedin), documents (resumes, letters,
                              revisions, revision-store, attachments), the search
                              (pipeline, interviews, offers, referrals, proposals,
                              stage-templates, stage-cadence, recurrence, schedule,
                              analytics, views, tags, archive, watch, capture-link),
                              mail (accounts, mail-sweep, outbound, digest), moving data
                              (export, transfer, patch, sample), and the instance (users,
                              connections, onboarding, assistant, waitlist, audit, system,
                              scheduled). userId first wherever content is touched; the
                              instance-level files say in their header comments why they
                              are not exceptions, and pipeline-share carries the app's
                              second unauthenticated read, which its own comment treats
                              accordingly.
src/lib/mcp/tools.ts          Tool + prompt definitions. One array, one source of truth.
src/lib/mcp/handler.ts        Streamable HTTP transport + the server instructions block.
src/lib/mcp/briefing-head.ts  The budgeted head of that block: standing rules and what fits.
src/lib/mcp/clients.ts        Per-client setup recipes. Adding a client = one array entry.
src/lib/resume-schema.ts      The resume document contract (zod).
src/lib/pdf.ts                Server-side PDF rendering. Needs a Chromium on the host;
                              degrades to the print page where there isn't one.
src/server/actions.ts         Server actions for the UI. Never accepts a userId.
src/server/application-detail.ts  The application screen's props, built once for the
                              page and the board's side panel.
src/app/(app)/                The app: dashboard, me (roles, profile, notes, extras,
                              resumes, letters), applications, tasks, crm, archive, docs,
                              settings (pipeline checklists and admin live under it).
src/app/api/digest/[token]/   The only address that makes this instance send mail. The
                              token is a Setting, not an env var; unset means off.
src/app/api/export/[kind]/    The three CSVs, plus `everything` — the whole workspace as
                              one reimportable JSON file.
src/app/api/mcp/[token]/      The connection URL. /api/mcp also accepts a bearer header.
src/app/r/[slug]/             Published resume, no auth. With /p/[slug] (shared pipeline),
                              the only unauthenticated pages in the app — unlisted slugs
                              are the entire privacy model, so treat both with care.
src/app/print/[id]/           US-Letter page for browser "Save as PDF". Auth-gated. The
                              fallback when the host has no Chromium, and the reason the
                              output stays selectable text.
src/components/               UI. ui/ is shadcn — extend, don't rewrite.
.claude/DECISIONS.md          Append-only decision log. Read it before reversing anything.
.claude/skills/               Deep guides for whoever writes the code: mcp-tool (how to
                              write a tool description), ship-a-feature (the build order,
                              in detail). Plain markdown — every agent should read them,
                              not just the one that auto-loads them.
.claude/plans/                Designs, not decisions. workspaces.md describes an unbuilt
                              feature; nothing in this directory is current architecture.
skills/                       Product-facing skills served to *users* of a running
                              instance from its /docs page. Not development guides —
                              don't confuse this tree with .claude/skills/.
docs/                         The manual, published by Mintlify at docs.hired.tools from
                              main. Prose is hand-written; the argument tables under
                              docs/tools/ are generated — see below.
tools/gen-tool-docs.mjs       Rewrites docs/tools/*.mdx from the tools array by evaluating
                              each inputSchema expression, and owns every tool count the
                              manual states — the table and the area cards on
                              docs/tools/overview.mdx sit between generated: markers, each
                              page's frontmatter opens with a count it rewrites, and no
                              other page repeats a figure. Run it after changing a tool;
                              --check fails when a page is stale. Everything above the
                              first "### `" heading is yours to write except that count.
```

Data areas map cleanly onto tool prefixes: me (`search_me`, `list_roles`,
`append_role_background`, …), resumes (`get_resume_format`, `create_resume`,
`export_resume_pdf`, …), pipeline (`list_applications`, `move_application_stage`,
`list_follow_ups`, …), CRM (`list_companies`, `create_contact`, …), admin (`admin_*`,
hidden from members' `tools/list` entirely — not merely refused), tags (`list_tags`,
`create_tag`, …) and the archive (`list_archive`, `restore_records`, …) cutting across all
of them. Don't trust any
hand-written tool count you find, including in old decision-log entries: the authoritative
number is what `node tools/gen-tool-docs.mjs` prints and writes into docs/tools/overview.mdx,
Settings → Connections shows it live, and the README hand-carries it in one paragraph
("Connect your AI") that must be bumped whenever the array changes.

---

## Writing an MCP tool

The description **is** the UX. An assistant with a good description calls the right tool
with the right arguments the first time; with a vague one it guesses and writes garbage
into someone's career history. Budget real effort here.

- Say **when to reach for it**, not just what it does. `search_me`'s description opens
  with "This is the FIRST tool to call when tailoring a resume" — that sentence does more
  work than the whole schema.
- Say what comes back and how to use it — ids, kinds, whether it saved anything.
- Flag destructive or overwriting semantics loudly. `update_resume` and `update_role`
  **replace** what you send; the description has to say "read first, modify, then write
  back whole" or an assistant will silently drop half a role.
- Prefer additive tools where a person would expect additive behaviour.
  `append_role_background` exists because `update_role` was eating people's notes.
- Give a dry-run where a mistake is expensive. `import_resume` takes `dry_run` and says what
  it would add *without* saving; `preview_company_merge` shows what a merge would fold in
  before `merge_companies` does it for good.
- Use the local helpers (`str`, `num`, `bool`, `object`, `required`, `defined`) rather
  than hand-rolling schema objects. `defined()` strips undefined keys so Prisma doesn't
  try to write them.
- Admin tools set `adminOnly: true`. Never rely on the handler refusing a call as the
  only protection.

Multi-step jobs belong in `prompts` (bottom of `tools.ts`), which surface as slash
commands or prompt shortcuts. If you catch yourself writing a tool that does four things
in sequence, it is a prompt composed of four tools.

---

## Current focus: nothing, and that is the point

The product is built. What is worth doing is unglamorous: keeping the surface small, the
numbers right and the manual true. Before building anything, ask whether it fixes something
seen in real use and whether an assistant could not already do it with the tools that exist;
every tool costs routing quality on every client, and October 2026 spent a week cutting
twenty-two of them.

These are shipped and must not be rebuilt — each has a decision-log entry explaining the
call that shaped it, so read that (from the end) before changing one: import
(`import_resume`, the paste dialog, `src/lib/resume-parse.ts`), traceable tailoring
(`Resume.baseResumeId`, `src/lib/resume-diff.ts`, `compare_resumes`,
`trace_resume_evidence`), publishing (`/r/[slug]`) and sharing (`/p/[slug]`), server-side
PDF, posting capture, letters, offers as versions, interview rounds and the question bank
(`src/lib/data/interviews.ts` — the `Interview` model, not free-text activities), stage
checklists, the review queue, full export and import, digests, search_me on Postgres FTS,
and one tag catalogue behind every label.

Three rules live in that history and are easy to break:

- **Deleting is archiving, for three models.** Company, Contact and Application carry
  `archivedAt`, and EVERY read of them has to exclude archived rows — nothing in the
  toolchain catches one that forgets, and a Prisma extension provably cannot help. If you add
  a read, filter it, or say in a comment why not. `Offer` and `Letter` hang off archivable
  models without carrying the column, so the reads in `offers.ts` that do not start from a
  filtered application spell `application: { archivedAt: null }` by hand; the probe checks
  them. Everything else deletable really is gone when deleted, and its copy says so.
- **One catalogue behind every label.** Where a job came from, a company's industry, how you
  know a person — all of it is `Tag` rows keyed by `kind`, managed from one picker and one set
  of tools. The pipeline's saved views still spell the filter `src` in the URL, deliberately.
  Don't add a second labelling mechanism.
- **`src/lib/data/transfer.ts` names every model** in `COVERAGE`, typed over
  `Prisma.ModelName`. A new model fails the typecheck there until you decide whether it is in
  the export.

**Parked, deliberately:** any sharing that involves *accounts* — viewers, editors, workspace
members. The unlisted links (`/r/[slug]`, `/p/[slug]`) are the ceiling: a link is consent,
and an account system is a permissions model this product should not grow as a side quest.
`.claude/plans/workspaces.md` is a design for it, and only a design.

---

## Working style

**Read before you write.** This codebase has opinions and they are documented in comments
at the top of most files — `handler.ts` explains why the transport is hand-rolled,
`clients.ts` explains why the token is in the URL path, `package.json` explains why the
build toolchain sits in `dependencies`. If a comment explains a decision, that decision
was expensive. Ask before reversing it.

**Migrations are real.** Write a migration in `prisma/migrations/` — the deploy runs
`prisma migrate deploy` on start. `db:push` is for local scratch only. Never edit an
applied migration. And after any edit to `prisma/schema.prisma`, run `npx prisma generate`
before you typecheck: only `postinstall` and `build` regenerate the client, so without it
the compiler rejects your new model with an error that looks like your code is wrong when
it is only stale.

**Verify before you claim done:**

```bash
npm run typecheck    # tsc --noEmit — must be clean
npm run check        # tools/rules.test.mjs: the pure rules, no database (Node 22.18+)
npm run probe        # tools/probe.mjs: the callers, against a database named *probe*
npm run build        # must succeed; this is what Railway runs
```

If you touched `src/lib/mcp/tools.ts`, also run `node tools/gen-tool-docs.mjs` and commit
what it rewrites — the manual documents every argument of every tool, and it is generated
precisely so nobody has to keep a hundred of them right by hand. If you changed a
description or a workflow, run `node tools/eval-tool-choice.mjs --only=<word>` and compare
with the last recorded score in the decision log.

These are the *only* gate. There is no CI check on branches or PRs — the first thing that
compiles your code after you push to main is the Docker image build that self-hosters
pull — so "it'll get caught later" is false here. The probe needs its own database
(`createdb hired_probe`, then `DATABASE_URL=…/hired_probe npx prisma migrate deploy`) and
refuses any other, because it creates and deletes accounts. When a rule or a guarantee
changes, change its case in the same commit; a probe that lives only in a session's
scratch directory guards nothing, which the decision log learned the hard way.

Then the parity check: list what you added, and confirm each item exists in both
`src/lib/data/` and `src/lib/mcp/tools.ts`.

**Running it locally** is in the README under "Running it locally": one `DATABASE_URL` in
`.env`, `npm install`, `npx prisma migrate deploy`, `npm run dev`; the owner password
prints once on first boot. `npm run dev` applies no migrations — only `start` does.

**Prose matters here.** The README, tool descriptions and UI copy are written in a
specific voice: plain, direct, second person, no marketing, no exclamation marks, no
emoji, contractions fine. Sentences carry information — "That's expected; the next two
steps fix it" rather than "Don't worry!". Match it. When you add a feature, document it in
the manual under `docs/` in that voice. The README is a front door of about 150 lines that
links there; it gains a sentence only when something changes what a newcomer needs to know
in their first five minutes.

**Log decisions.** When you make a non-obvious call — a tradeoff, a thing you tried that
didn't work, a constraint you discovered — append it to `.claude/DECISIONS.md` with the
date. That file is the memory the next session doesn't have, and it applies to every
agent, whatever the directory is named. Rules of the file: append-only, newest at the
bottom, and a later entry supersedes an earlier one on the same subject — so when you
consult it, search or read from the end rather than trusting the first match. It is far
too long to read whole; don't try.

**Work lands on `main`.** Finished work goes to `main` without waiting to be asked — build it
on a branch, run the gate, then merge. There is no review step to hold it for; this is one
person's instance and the branch is bookkeeping, not a queue. The gate is the thing that
does not move: typecheck, build, the generated tool docs and a real exercise of the change
all happen BEFORE the merge, never after, because the first thing that compiles `main` is
the Docker image a self-hoster pulls. A merge commit rather than a fast-forward, which is
what the log already does.

**Small commits, real messages.** Look at `git log`: each commit is one coherent change
described in the imperative by what it does for the user ("Make MCP the front door: one
connection per client, any platform"), not by what files moved.