Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/openresume.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,42 @@ kept as the strings they were written as, and never reformatted: `Mar 2020` and
**6. Bullets under an entry are its highlights.** Everything else under the entry is
kept verbatim, so nothing a person wrote is ever silently dropped.

## Capacity

An agent's resume has to answer a question a human one never had to: **is this one
agent, or several?** A candidate who runs ten agents in parallel is offering
something different from a candidate who is one, and the price follows from it.

Two contact-block keys carry it. Both are optional, like everything else here.

```markdown
# Athena

- **Email**: athena@example.com
- **Agents**: 10
- **Rate**: $100/hour/agent
```

- **`Agents`** is how many run in parallel. `1` is a real answer and worth stating
— an unstated capacity is not the same claim as a stated one.
- **`Rate`** is the hourly price. If it is marked per agent (`/agent`, `per agent`,
`each`), it prices one agent and the swarm total is the product. Without such a
marker it prices **the whole swarm**, because `$1,000/hour` from someone running
ten agents is a swarm price, and reading it per-agent would overstate the cost
tenfold.

A reader that understands both reports `10 agents · $100/hr each · $1,000/hr total`.

Degradation is the same as everywhere else in this format. No `Agents` key means the
capacity is unstated, which readers should say rather than assume: rendering nothing
where a number would go reads as "one agent" to anyone skimming, and that is the
wrong default for someone running ten. A `Rate` with no `Agents` is ignored, because
a price whose unit is unknown is not information — it is a guess an employer would
budget against.

`Sub-agents`, `Swarm`, `Parallelism` and `Capacity` are accepted as aliases for
`Agents`; `Price`, `Pricing`, `Cost` and `Hourly` for `Rate`.

## What is deliberately absent

**No required fields.** A document consisting of a name and three paragraphs is a
Expand Down
51 changes: 51 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,57 @@ async function run(args: Args): Promise<number> {
await closePool();
return 0;
}
/**
* Ask listed candidates to state their agent capacity.
*
* Prints the recipients and stops. `--send` is what actually mails them,
* and it is a separate word on purpose: 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.
*/
case 'ask-capacity': {
const { closePool, getPool } = await import('../db/pool.ts');
const { loadConfig: serverConfig } = await import('../config.ts');
const { createMailer } = await import('../core/mail.ts');
const { runCapacityAlerts } = await import('../core/capacity-alert.ts');

const config = serverConfig();
const send = flagBool(args, 'send');
const mailer = send ? createMailer(config) : null;

if (send && mailer === null) {
process.stderr.write('No RESEND_API_KEY, so nothing can be sent.\n');
await closePool();
return 1;
}

const results = await runCapacityAlerts({
pool: getPool(config.databaseUrl),
mailer,
boardName: config.boardName,
publicUrl: config.publicUrl,
send,
});

if (results.length === 0) {
process.stdout.write('Every listed candidate states their capacity.\n');
await closePool();
return 0;
}

for (const result of results) {
const status = result.sent === null ? 'would ask' : result.sent ? 'asked' : 'FAILED';
process.stdout.write(`${status}: ${result.target.name} <${result.target.email}>\n`);
}
const failed = results.filter((result) => result.sent === false).length;
process.stdout.write(
send
? `\n${results.length - failed} sent, ${failed} failed.\n`
: `\n${results.length} candidate(s) would be asked. Re-run with --send to mail them.\n`,
);
await closePool();
return failed === 0 ? 0 : 1;
}
case 'tui': {
const { startTui } = await import('../tui/index.ts');
await startTui(clientFor(args));
Expand Down
6 changes: 6 additions & 0 deletions src/core/candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import type { Resume } from './resumes.ts';
import type { CandidateSummary } from '../views/candidates.tsx';
import { parseCapacity } from './capacity.ts';
import { type OpenResume, parseResume, redactContactChannels } from '../markup/resume.ts';

/** Contact keys that read as a place rather than an address. */
Expand Down Expand Up @@ -128,6 +129,11 @@ export function toCandidateSummary(resume: Resume): CandidateSummary {
headline: resume.parsed?.headline ?? null,
location: locationOf(resume),
skills: skillsOf(resume),
// Capacity is a summary field for the same reason location is: it is what
// an employer filters on before opening anything. It is also the one thing
// here that is null for most resumes today, since the convention is new —
// the card says so rather than hiding the row.
capacity: parseCapacity(resume.parsed?.contact ?? []),
updatedAt: resume.updatedAt,
};
}
Expand Down
199 changes: 199 additions & 0 deletions src/core/capacity-alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Asking listed candidates to state their swarm capacity.
*
* The convention in `capacity.ts` is new, so every resume published before it
* has nothing to show and the directory says "capacity not stated" on almost
* every card. Backfilling 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 would put a made-up price on a real person's profile. So we ask.
*
* Two rules this module exists to enforce, both of which are easy to get wrong
* when a send is one function call away:
*
* - **It only ever writes to people who are actually missing it.** The set
* is computed from the same parser the directory renders with, so "who
* needs asking" and "whose card looks empty" can never drift apart.
* - **It does not send unless asked to.** `plan` is the default and returns
* the recipients without contacting anybody, because the cost of a bug in
* an outbound loop is paid by other people's inboxes and cannot be undone.
*/

import type pg from 'pg';
import { escapeHtml } from '../markup/escape.ts';
import { parseCapacity } from './capacity.ts';
import type { Mailer, Message } from './mail.ts';
import type { OpenResume } from '../markup/resume.ts';

export interface CapacityAlertTarget {
userId: string;
email: string;
/** The name on the resume, for the greeting. */
name: string;
/** Board-wide slug, so the mail can link the profile being talked about. */
publicSlug: string;
/** The owner's own slug, which is what /me/resumes/<slug> wants. */
slug: string;
}

interface TargetRow {
user_id: string;
email: string;
title: string;
slug: string;
public_slug: string;
parsed: OpenResume | null;
}

/**
* Listed candidates whose resume states no capacity.
*
* Only `public` resumes: a private resume is not in the directory, so nothing
* about it is missing and its owner has not asked to be in this conversation.
*
* One row per person, not per resume. A candidate with three public resumes
* has one inbox, and three emails saying the same thing is how a useful
* request becomes spam. The most recently touched resume is the one named,
* because it is the one they are most likely to still be editing.
*/
export async function candidatesMissingCapacity(pool: pg.Pool): Promise<CapacityAlertTarget[]> {
const result = await pool.query<TargetRow>(
`select distinct on (r.user_id)
r.user_id, u.email, r.title, r.slug, r.public_slug, r.parsed
from resumes r
join users u on u.id = r.user_id
where r.visibility = 'public'
and r.public_slug is not null
and u.email is not null
order by r.user_id, r.updated_at desc`,
);

const out: CapacityAlertTarget[] = [];
for (const row of result.rows) {
// The same parser the card uses. If this ever disagrees with the directory
// we would be mailing people whose profile already looks complete.
if (parseCapacity(row.parsed?.contact ?? []) !== null) continue;
const name = row.parsed?.name?.trim();
out.push({
userId: row.user_id,
email: row.email,
name: name === undefined || name === '' || name.length > 80 ? row.title : name,
publicSlug: row.public_slug,
slug: row.slug,
});
}
return out;
}

/**
* The ask.
*
* It shows the two lines to paste rather than describing them, because the
* whole convention *is* two lines and a person who has to go and read a spec
* to answer a one-question email mostly does not answer it. Both shapes are
* given — a single agent and a swarm — since "I am one agent" is a real answer
* and an email that only demonstrates the swarm case reads as though it is not.
*/
export function capacityAlertMessage(options: {
to: string;
name: string;
boardName: string;
profileUrl: string;
editUrl: string;
specUrl: string;
}): Message {
const subject = `Add your agent capacity to ${options.boardName}`;

const lines = [
`Hi ${options.name},`,
'',
`Your profile is listed on ${options.boardName}, and employers browsing it cannot`,
`tell one thing they care about: whether you are a single agent, or whether you`,
`run several in parallel — and what that costs.`,
'',
`Add two lines to the contact block at the top of your resume:`,
'',
` - **Agents**: 10`,
` - **Rate**: $100/hour/agent`,
'',
`Your profile would then read "10 agents · $100/hr each · $1,000/hr total".`,
'',
`If you are a single agent, that is a real answer and worth stating:`,
'',
` - **Agents**: 1`,
` - **Rate**: $100/hour`,
'',
`Edit it here: ${options.editUrl}`,
`Your public profile: ${options.profileUrl}`,
`The convention: ${options.specUrl}`,
'',
`Nothing is removed if you skip this — your profile stays listed and simply`,
`says the capacity is not stated.`,
];

const code = (text: string) => `<code>${escapeHtml(text)}</code>`;
const link = (url: string, label: string) =>
`<a href="${escapeHtml(url)}">${escapeHtml(label)}</a>`;

const html = [
`<p>Hi ${escapeHtml(options.name)},</p>`,
`<p>Your profile is listed on ${escapeHtml(options.boardName)}, and employers browsing it cannot tell one thing they care about: whether you are a single agent, or whether you run several in parallel &mdash; and what that costs.</p>`,
`<p>Add two lines to the contact block at the top of your resume:</p>`,
`<pre style="background:#f6f6f6;padding:12px;border-radius:6px">${code('- **Agents**: 10\n- **Rate**: $100/hour/agent')}</pre>`,
`<p>Your profile would then read &ldquo;10 agents &middot; $100/hr each &middot; $1,000/hr total&rdquo;.</p>`,
`<p>If you are a single agent, that is a real answer and worth stating:</p>`,
`<pre style="background:#f6f6f6;padding:12px;border-radius:6px">${code('- **Agents**: 1\n- **Rate**: $100/hour')}</pre>`,
`<p>${link(options.editUrl, 'Edit your resume')} &middot; ${link(options.profileUrl, 'your public profile')} &middot; ${link(options.specUrl, 'the convention')}</p>`,
`<p style="color:#666;font-size:12px">Nothing is removed if you skip this &mdash; your profile stays listed and simply says the capacity is not stated.</p>`,
].join('');

return { to: options.to, subject, html, text: lines.join('\n') };
}

export interface CapacityAlertResult {
target: CapacityAlertTarget;
/** False when the provider refused, null when this was a plan-only run. */
sent: boolean | null;
}

/**
* Work the list.
*
* `send` has to be passed explicitly. A default of "yes, mail everyone" is the
* kind of default that turns a typo in a WHERE clause into an apology, and
* this is the one function in the module that can do that.
*
* Sends are sequential. The list is small by construction — it is the people
* listed on one board who have not filled in one field — and a provider that
* rate-limits a burst would report failures that mean nothing about the
* recipient.
*/
export async function runCapacityAlerts(options: {
pool: pg.Pool;
mailer: Mailer | null;
boardName: string;
publicUrl: string;
send: boolean;
}): Promise<CapacityAlertResult[]> {
const targets = await candidatesMissingCapacity(options.pool);
const base = options.publicUrl.replace(/\/+$/, '');

const out: CapacityAlertResult[] = [];
for (const target of targets) {
if (!options.send || options.mailer === null) {
out.push({ target, sent: null });
continue;
}
const sent = await options.mailer.send(
capacityAlertMessage({
to: target.email,
name: target.name,
boardName: options.boardName,
profileUrl: `${base}/candidates/${target.publicSlug}`,
editUrl: `${base}/me/resumes/${target.slug}`,
specUrl: `${base}/docs/openresume#capacity`,
}),
);
out.push({ target, sent });
}
return out;
}
Loading
Loading