Swarm capacity on a candidate profile, and ask for it - #46
Merged
Conversation
An agent's resume has to answer a question a human one never had to: is
this one agent, or ten running in parallel, and what does that cost? It
is the difference between a contractor and a firm, and nothing on the
board carried it.
Two contact-block keys carry it now, because that block is already where
a resume keeps its scalars and candidates.ts is explicit that a profile
restating the document is a second copy to keep in step:
- **Agents**: 10
- **Rate**: $100/hour/agent
The card and the JSON both read "10 agents, $100/hr each, $1,000/hr
total". The per-agent marker is the whole parsing problem: "$1,000/hour"
and "$100/hour/agent" from the same candidate are the same money, and an
unmarked rate is therefore read as the swarm price. Reading it the other
way would quote an employer ten times the real number, which is the
expensive direction to be wrong in.
A count with no rate is still capacity ("10 agents, rate on request").
A rate with no count is not, and comes back null: a price whose unit is
unknown is not information, it is something an employer would budget
against. An unstated capacity is rendered as unstated rather than
omitted, because a blank row reads as "one agent" to anyone skimning and
that is the wrong default for someone running ten.
The convention is optional, like every other rule in OpenResume, which
says in as many words that it has no required fields. Every resume
written before today has no capacity and all of them still render.
`agenticjobs ask-capacity` asks the people who are missing it. Backfill
was the alternative and it is the wrong one -- nobody here knows whether
a given candidate is one agent or ten, and inventing an answer puts a
made-up price on a real person's profile. It prints the recipients and
stops; --send is a separate word because this is the only command in the
CLI that writes to other people.
Two privacy bugs found while reading live data, both fixed here:
Redaction only withheld preamble *bullets* that parsed as contact
fields. An address written as prose under the name went out to every
signed-out reader and to all four download formats -- and one was live,
in a real candidate's profile. That is the exact failure the redaction
exists to prevent, arriving through the one line in the block nobody
checked.
The same line became the public headline, because emphasis was stripped
only at the ends: the directory showed `Operated by:** X (addr)`, with
the markup and the address in it. A headline is now cleaned of markup
anywhere, and one containing an address is dropped rather than trimmed
-- what is left after cutting an address out of that sentence is not a
headline anybody wrote.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DEUXec5um4FY3EbqfwtYm
This was referenced Sep 9, 2026
ralyodio
added a commit
that referenced
this pull request
Sep 9, 2026
) Documenting the two flows turned up the reason they had never been documented: the CLI could not do them. There was no way to create an employer without a browser, and `resume save` sent no visibility, so a resume saved from a terminal was private with no command to list it. The page had to tell you to copy a bearer token out of config.json and curl the API, on a board whose whole pitch is that a client can drive it. Employers get the rest of their CRUD: - PATCH /api/v1/orgs/{slug} changes only the fields it carries. An absent field is left alone and an explicit null clears it, so a caller that knows about a name cannot blank a website it never read. - A rename never moves the slug. That slug is the URL every listing, link and directory row already points at, and a company becoming "Example Works Inc" has not become a different employer. - DELETE /api/v1/orgs/{slug} refuses with 409 once anything has gone live. jobs.org_id cascades and applications.job_id cascades behind it, so the honest delete is one that will not quietly take published listings and the applications people sent to them. Drafts do not count: nobody has seen one, and an employer whose listings are all drafts is almost always one typed in wrong five minutes ago. Resumes get the visibility half they were missing: `--visibility` on save and import, `resume publish`, `unpublish`, `visibility <slug> <value>` and `delete <slug> --yes`. A typo'd value is refused rather than dropped, which is what the old silent isVisibility filter did. "Saved." is now the address instead: the only question a candidate has after saving is whether anyone can read it yet, and the URL is the answer. Both flows are two commands on /docs now, with the REST calls kept as the equivalent rather than the instruction. Also documents the swarm capacity keys from #46, which shipped without reaching the page. Verified end to end against a real board, not only in tests: created an employer, renamed it and watched the slug hold, posted the exact front matter the docs page shows, published it, had the delete refused, then saved a resume, published it, and read the listed candidate back with its skills split and its capacity priced. Tests: 210 pass against Postgres. The four new ones cover the rename, the partial patch, a stranger's 403 and the delete guard. The OpenAPI coverage test caught both new routes missing from the published document, which is what it is for. Claude-Session: https://claude.ai/code/session_01D8ai32QMBsKEs1nigijSiJ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
added a commit
that referenced
this pull request
Sep 10, 2026
Minor rather than patch because commands and endpoints were added, and because the reason to cut this at all is that /docs is currently ahead of the published client: it tells people to run `agenticjobs employer create` and `agenticjobs resume publish`, and npm still ships 0.10.0, where neither exists. Since 0.10.1: - Employers and resume visibility as CRUD (#48). employer list/show/create/update/delete, resume publish/unpublish/visibility/ delete and --visibility on save and import, with PATCH and DELETE /api/v1/orgs/{slug} behind them. A rename keeps the slug; an employer that has published cannot be deleted. - Getting listed and hiring are documented at all (#45), and swarm capacity on a candidate profile (#46). - An address is withheld wherever it is written, and never from cache (#47). - Boolean CLI flags no longer swallow the command after them (#49). - Salary bounds read correctly in summaries and compare correctly across periods in search (#50, #51). - A JSON parse failure on a 200 is reported rather than swallowed (#52). - Job and DOCX imports keep titles, structure, tabs and line breaks (#53, #54). Both version files move together: package.json and VERSION in src/config.ts. Claude-Session: https://claude.ai/code/session_01D8ai32QMBsKEs1nigijSiJ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
An agent's resume has to answer a question a human one never had to: is this one agent, or ten running in parallel, and what does that cost? That is the difference between a contractor and a firm, it changes both the price and the kind of work you would send, and nothing on the board carried it.
The convention
Two contact-block keys — that block is already where a resume keeps its scalars (
Email,Location), andcandidates.tsis explicit that a profile restating the document is a second copy to keep in step:The card and
/api/v1/candidatesboth read10 agents · $100/hr each · $1,000/hr total.The per-agent marker is the whole parsing problem.
$1,000/hourand$100/hour/agentfrom the same ten-agent candidate are the same money. An unmarked rate is read as the swarm price, because reading it per-agent would quote an employer ten times the real number — the expensive direction to be wrong in.Degradation rules, each with a reason:
10 agents · rate on requestnull5000agentsThe convention is optional, like every other rule in OpenResume — which says in as many words that it has no required fields. Every resume written before today still renders. Enforcing "every candidate must state this" belongs at the board level, not in the format; this PR does the asking instead.
Asking, not backfilling
agenticjobs ask-capacitymails the people who are missing it.Backfill was the alternative and it is the wrong one: nobody here knows whether a given candidate is one agent or ten, and inventing an answer puts a made-up price on a real person's profile.
It prints the recipients and stops.
--sendis a separate word, because this is the only command in the CLI that writes to other people, and the operator should have to say so after reading the list. The recipient set is computed with the same parser the directory renders with, so "who needs asking" and "whose card looks empty" cannot drift apart. One mail per person, not per resume.Two privacy bugs found while reading live data
Both were live on
agenticjobs.work, in a real external candidate's profile.redactContactChannelsonly withheld preamble bullets that parsed as contact fields. An address written as prose under the name —**Operated by:** X (someone@example.com)— went out to every signed-out reader, and to all four download formats with it. That is the exact failure the redaction exists to prevent, arriving through the one line in the block nobody checked.The same line became the public headline, because emphasis was stripped only at the ends. The directory rendered
Operated by:** X (someone@example.com)— the markup and the address, on a page whose own comment says the contact block is kept out of the summary because "a directory page listing a hundred email addresses is a mailing list for whoever fetches it once".A headline is now cleaned of markup anywhere, and one containing an address is dropped rather than trimmed: what is left after cutting the address out of that sentence is not a headline anybody wrote. A signed-in reader still sees everything.
Tests
test/capacity.test.ts, 18 cases: per-agent vs swarm pricing in both directions, the derived-vs-quoted distinction, the ways people write "one agent", aliases, currency, formatting, the nonsense-count guard, both privacy bugs, and that a signed-in copy still keeps the address.Full suite: 137 passing, typecheck clean.
🤖 Generated with Claude Code
https://claude.ai/code/session_016DEUXec5um4FY3EbqfwtYm