diff --git a/apps/logicsrc-web/src/lib/docs.ts b/apps/logicsrc-web/src/lib/docs.ts index 78f07f2..b45211b 100644 --- a/apps/logicsrc-web/src/lib/docs.ts +++ b/apps/logicsrc-web/src/lib/docs.ts @@ -15,6 +15,8 @@ export const DOC_SLUGS = [ "openontology", "openontology-governance", "openontology-interoperability", + "openjob", + "openresume", "openspec-comparison", "data-model", "cli", diff --git a/docs/openjob.md b/docs/openjob.md new file mode 100644 index 0000000..3612aaa --- /dev/null +++ b/docs/openjob.md @@ -0,0 +1,200 @@ +# OpenJob + +A job posting that an agent can read, and that says out loud whether an agent may +answer it. + +Job postings already have a machine-readable format: schema.org `JobPosting`, which +search engines index and which most boards emit. OpenJob does not replace it. It +extends it with the three things `JobPosting` has no vocabulary for, and which the +next few years of hiring depend on: + +1. **whether the employer accepts applications written with an agent**, stated + rather than discovered by silent rejection; +2. **the application form, as data**, so applying does not require rendering a page; +3. **a description in Markdown**, so the same text is legible to a person, a model + and a terminal without a HTML-to-text round trip. + +## The document + +A listing is a JSON object. Every field below is what the reference implementation +serves at `GET /api/v1/jobs/{slug}`. + +```json +{ + "id": "b7c3...", + "slug": "staff-engineer-agent-platform", + "title": "Staff Engineer, Agent Platform", + "description": "We are building the plumbing...", + "org": { + "slug": "example-works", + "name": "Example Works", + "website": "https://example.com" + }, + "employmentType": "full-time", + "workplace": "remote", + "seniority": "staff", + "location": "European timezones", + "remoteRegions": ["DE", "NL", "PT"], + "salary": { + "min": 180000, + "max": 230000, + "currency": "USD", + "period": "year", + "equity": "0.1% - 0.4%" + }, + "tags": ["infrastructure", "agents"], + "stack": ["typescript", "postgres", "rust"], + "requirements": ["Has run something other people depended on."], + "responsibilities": ["Own the execution layer."], + "agentPolicy": "welcome", + "apply": { "via": "board", "schema": { "fields": [] } }, + "status": "published", + "publishedAt": "2026-09-08T10:00:00.000Z", + "expiresAt": null +} +``` + +`description` is Markdown. Not HTML, and not plain text with the formatting removed. + +## agentPolicy + +The field this format exists for. Required on every listing, with exactly three +values: + +| Value | Means | +| --- | --- | +| `welcome` | Agent-written applications are fine. Nothing is asked. | +| `disclose` | Fine, but say so. The application carries a structured disclosure. | +| `human-only` | The employer is asking for something a person wrote. | + +Three things about it are deliberate. + +**It is required.** An optional field would be omitted by most posters, and "not +stated" is exactly the ambiguity candidates are navigating today by guessing. + +**`human-only` is a request, not a control.** No board can tell who wrote a cover +letter, and one that claims it can is selling something. Stating it plainly is worth +more than pretending to enforce it: a candidate who reads `human-only` and writes it +themselves has been told what the employer wants, which is all anyone can offer. + +**Disclosure is not evidence against the candidate.** A board that collects the +disclosure and then filters those applications out has broken the field for +everybody, because the next candidate learns to lie. `disclose` means the employer +wants to know, and wanting to know is the reason to answer honestly. + +## apply + +Three shapes, and only the first is completable without a browser. + +```json +{ "via": "board", "schema": { "fields": [ ... ] } } +{ "via": "url", "url": "https://example.com/careers/123" } +{ "via": "email", "email": "jobs@example.com" } +``` + +A listing that says `url` or `email` is being honest that an agent cannot finish the +job. That is better than a board pretending every listing is applicable and handing +an agent a form it cannot post. + +### The application schema + +A deliberately small subset of JSON Schema: small enough to render as an HTML form, +small enough for a model to fill in without a validator. + +```json +{ + "fields": [ + { "name": "name", "label": "Your name", "type": "text", "required": true, "maxLength": 120 }, + { "name": "email", "label": "Email", "type": "email", "required": true, "maxLength": 200 }, + { "name": "cover", "label": "Why you", "type": "textarea", "required": true, "maxLength": 5000, + "help": "Plain text. Short is fine." } + ] +} +``` + +`type` is one of `text`, `textarea`, `email`, `url`, `select` or `file`. A `select` +carries `options`. + +An implementation publishes the schema at a stable address alongside the endpoint +that accepts it, so reading and answering are two requests and no guessing: + +``` +GET /api/v1/jobs/{slug}/apply-schema +POST /api/v1/jobs/{slug}/apply +``` + +### The disclosure + +Posted alongside the answers: + +```json +{ + "agent": { "name": "claude-opus-5 via agenticjobs-mcp", "supervised": true } +} +``` + +`supervised` means a person read it before it was sent. It is never inferred - a +guessed disclosure is worthless in both directions. + +### Resumes + +A resume travels as Markdown, in the [OpenResume.md](/docs/openresume) convention, in +a `resume` field. Not a file upload, not a URL to a PDF. An employer receives text +they can read and an agent receives text it can write. + +## Mapping onto schema.org + +An OpenJob listing maps cleanly onto `JobPosting`, and an implementation should emit +both - the JSON-LD for search engines, the OpenJob document for everything else. + +| OpenJob | JobPosting | +| --- | --- | +| `title` | `title` | +| `description` | `description` | +| `org` | `hiringOrganization` | +| `employmentType` | `employmentType` (`FULL_TIME`, `PART_TIME`, `CONTRACTOR`, `INTERN`, `TEMPORARY`) | +| `workplace: "remote"` | `jobLocationType: "TELECOMMUTE"` | +| `remoteRegions` | `applicantLocationRequirements` | +| `location` | `jobLocation` | +| `salary` | `baseSalary`, plus an annualised `estimatedSalary` | +| `expiresAt` | `validThrough` | +| `apply.via === "board"` | `directApply: true` | + +The three fields with no equivalent - `agentPolicy`, `applyVia` and the address of +the application schema - travel in `additionalProperty`, which is the vocabulary's +own escape hatch and passes every validator. + +One trap worth naming, because it catches almost everyone: a remote role needs +`jobLocationType: "TELECOMMUTE"` **and** a location the hire may sit in. A remote +posting with no `jobLocation` and no `applicantLocationRequirements` fails Google's +validation while looking entirely correct. + +## Drafts + +A listing has a `status`, and `draft` is the interesting one. A draft is not +published, not in the feed, not in the API's search results, and not visible to any +other instance. + +This is the employer's human control point. An agent can write the listing; a person +publishes it. The reference implementation makes a job created over the API or by a +tool call a draft *unless the caller explicitly asks otherwise*, which is the correct +default the first time an agent posts a job its author has not read. + +The candidate's side of that seam is an application that can be prepared and held +until a person sends it. Both ends of the transaction have one, or the design is +lopsided. + +## Federation + +An implementation that wants to be discoverable serves a descriptor at +`/.well-known/agenticjobs` naming its search endpoint, its OpenAPI document, its MCP +endpoint and its feed. A directory reads that descriptor from the instance itself +rather than trusting an announcement, and a client fans one query out across every +instance it knows. + +Nothing about OpenJob requires federation, and nothing about it requires a directory. +A single self-hosted board that serves these documents is a complete implementation. + +## Implementations + +- `agenticjobs` - MIT, https://github.com/profullstack/agenticjobs diff --git a/docs/openresume.md b/docs/openresume.md new file mode 100644 index 0000000..3d729e0 --- /dev/null +++ b/docs/openresume.md @@ -0,0 +1,143 @@ +# OpenResume.md + +A resume is a Markdown file. That is the whole idea. + +Not a form, not a PDF, not a proprietary JSON schema that one product understands. +A file a person can read, diff, keep in a repository, paste into any tool, and hand +to an employer without losing anything on the way. And a file an agent can write and +revise without being taught a schema first. + +This document describes the convention. It is deliberately thin, because a resume +that fails to parse still has to be a usable resume. + +## The shape + +```markdown +# Ada Lovelace + +- **Email**: ada@example.com +- **Location**: London +- **Web**: https://example.com + +Mathematician, looking for work on machines that do not exist yet. + +## Experience + +### Analytical Engine | London +Chief Programmer (1842 - 1843) + +- Wrote the first published algorithm intended to be carried out by a machine. +- Described what the engine could do beyond arithmetic, which its designer had not. + +## Skills + +- Languages: analytical notation, French +- Tools: difference engine, correspondence + +## Education + +### University of London +Private tuition in mathematics (1840) +``` + +## The rules + +There are six, and every one of them degrades rather than fails. + +**1. One `#` heading, and it is the person's name.** A document with more than one +is read using the first; a document with none still parses, and a reader that wants a +name can say it does not have one. + +**2. The bullet list directly under the name is the contact block.** Each item is +`Key: value`, with or without `**bold**` on the key, or a bare `[label](url)`. Values +that look like an email address, a phone number or a URL become links; anything else +stays text. Unknown keys are kept as written - `Pronouns`, `Timezone` and +`Availability` all work without anyone having to add them to a list. + +**3. A single prose line between the contact block and the first `##` is a +headline.** One line. More than one, and only the first is treated that way. + +**4. `##` opens a section.** The text is kept verbatim, and separately normalised for +matching, so `Work Experience`, `Experience` and `Employment` are one thing to a +filter and three different words on the page. The normalised names in common use are +`experience`, `education`, `skills`, `projects`, `summary`, `links`, +`certifications`, `languages`, `publications` and `awards`. A section whose name +matches none of them keeps its own name and is not dropped. + +**5. `###` opens an entry within a section: a job, a degree, a project.** A `|` in +the heading splits it into a title and a place - `Company | Berlin`. The first +non-empty line under the heading is the entry's subtitle: a role, a degree, a +one-line description. If that line ends in a bracketed range, the range is read out +of it: + +``` +Chief Programmer (1842 - 1843) +Staff Engineer (Mar 2020 - Present) +Contractor (2019 to 2021) +``` + +Hyphens, en dashes and em dashes are all accepted, because all three appear in real +resumes. `Present`, `Now`, `Current` and `Ongoing` mark a current role. Dates are +kept as the strings they were written as, and never reformatted: `Mar 2020` and +`03/2020` both survive, and a reader that wants a date can parse one. + +**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. + +## What is deliberately absent + +**No required fields.** A document consisting of a name and three paragraphs is a +valid OpenResume.md. + +**No date format.** Every attempt to impose one on resumes has failed, because people +write `Summer 2019` and mean it. + +**No schema version.** Readers ignore what they do not recognise. A resume written +today has to be readable in five years by software nobody has written yet, which +means the format cannot have a version negotiation in it. + +**No structured skills taxonomy.** Skills are the lines people wrote. Mapping them +onto a controlled vocabulary is a job for whatever is reading, and doing it at write +time destroys the information. + +## Sections a parser derives, not the author + +A reader may compute a structured view - name, contact pairs, sections, entries with +parsed date ranges - and use it for search and filtering. That view is derived, and +it is regenerated from the Markdown on every save. + +**The Markdown is the canonical copy.** If the structured view and the document +disagree, the document is right. A product that stores the parse and treats the +Markdown as an export has not implemented this convention; it has implemented a form +with a Markdown skin, and the candidate no longer owns their resume. + +## Converting into it + +PDF, DOCX and plain text can all be converted to Markdown, and none of the +conversions are perfect. The rule that makes this workable: a converted document is +shown to the candidate to edit before it is used. A conversion nobody checks is a +conversion nobody should trust. + +The `agenticjobs` reference implementation converts `.docx` by reading +`word/document.xml` directly, and `.pdf` with `pdftotext -layout`. Both are lossy in +ways a person spots in five seconds and a parser never will. + +## Why Markdown + +Because the alternative formats each fail one of the audiences. + +A PDF is readable by a person and hostile to everything else; extracting a two-column +CV back into text loses the reading order, which is why so much hiring software +mangles them. + +A JSON schema is readable by software and unwritable by a person; nobody drafts a +resume in JSON, so a tool has to generate it, and now the tool owns the resume. + +Markdown is the only format that a person writes directly, a person reads directly, +a model writes well without being taught, `git diff` shows usefully, and a renderer +turns into a PDF when a PDF is genuinely what somebody wants. + +## Implementations + +- `agenticjobs` - the job board this convention was written for. MIT, + https://github.com/profullstack/agenticjobs