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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ fix → patch (`0.3.x`); feature addition → minor (`0.x.0`).
> Ajouté → Added · Modifié → Changed · Corrigé → Fixed · Déprécié → Deprecated ·
> Supprimé → Removed · Sécurité → Security · Validé → Validated · Documentation → Documentation.

## [0.20.0] — 2026-06-06

### Added
- **Posting brief (PB-1) — new `posting-brief-generator` sub-module.** At application intake, the orchestrator now produces a Markdown **posting brief** (job-posting dossier) automatically and **first**, before the widget's six deliverables — it is **not** a selection-widget choice. Content: a header (company, position, recruiter, city, capture date, source URL, posting language), the **verbatim** posting body, and a short model-extracted **digest** (key requirements + application deadline). It reuses the offer already read in the single global analysis, so it adds no separate offer read and nothing to the time-to-widget. Generated via `generate_posting_brief.py`; optional styled PDF via the module's `md_to_pdf.py` (also part of the end-of-run PDF batch). Eighth sub-module of the suite.
- **Script-owned filename** for the brief: `generate_posting_brief.py --output-dir <dir>` builds `Posting_Brief_<Company>_<Position>_<YYYYMMDD>.md` and prints the path (read it to present the file). `--output-path` is accepted as a fallback (gallery / back-compat). The **capture date** is stamped by the script (today's local date; `--timezone` resolved by the model with a safe `Europe/Paris` fallback — same model=zone / script=clock boundary as the tracker), never model-supplied.
- **Tracker id `posting_brief`** recorded in the `deliverables` column at `add_to_tracker`.

### Changed
- **Surface contract (LNG-2):** the brief's section/field labels are model-supplied in the run language via `--labels-json` (exact 13-key set; missing/extra rejected). Structure, `print` output and the verbatim body stay script-owned. Critical fields (`company_name`, `job_title`, `posting_body`) are guarded by the neutral `__MISSING__` sentinel → exit 2 (the model asks, never invents), the same contract as `fill_cover_letter`.
- **Orchestration (`SKILL.md`):** STEP 4 generates the brief first (idempotent — skipped if a `Posting_Brief_*` already exists for the application, STEP 2; regenerated only on an explicit request); STEP 7 includes it in the PDF batch; module map and the sub-module count (7 → 8) updated. `references/assistant_flow.md` documents the out-of-widget `posting_brief` id.

### Tests
- **`tests/test_posting_brief.py`** (new): ISO-639-1 form accept/reject, exact label-set (missing/extra) rejection, required-field and `__MISSING__`/empty critical-field rejection (exit 2), the **script-owned filename + printed-path** invariant, the `--output-path` fallback, and `resolve_timezone` valid/fallback. The brief is also added to the acceptance gallery (`tooling/build_samples.py`).

## [0.19.0] — 2026-06-06

### Changed
Expand Down
14 changes: 8 additions & 6 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
name: candidate-suite
version: 0.19.0
version: 0.20.0
updated: 2026-06-06
description: "All-in-one suite for preparing job applications and interviews, bundled as a single module: cover letter, interview prep, application summary, strategic playbook, one-page reference card, and application tracking, plus candidate-profile configuration. Single entry point: it shows a selection widget, then generates each deliverable through its sub-module's script. Use whenever the user wants to apply for a role, prepare an application or an interview, get tools for a job posting, configure the skill or update their CV or profile, generate a cover letter, produce a summary, playbook, or reference card, track applications or view their application dashboard (e.g. asking where their applications stand), or makes any open-ended request for job-application help. These are intents, not required wordings: match the user's intent regardless of the language the request is written in."
---

# candidate-suite

`Version 0.16.6 — 2026-06-05`
`Version 0.20.0 — 2026-06-06`

**candidate-suite** is an **all-in-one** suite: a single module to install that bundles the orchestrator (this root) and 7 specialized sub-modules. Faced with a job-application request, this skill shows a **selection widget** that turns the vague request into an explicit command, then generates each deliverable through the relevant sub-module's script. **Nothing is written by hand.**
**candidate-suite** is an **all-in-one** suite: a single module to install that bundles the orchestrator (this root) and 8 specialized sub-modules. Faced with a job-application request, this skill shows a **selection widget** that turns the vague request into an explicit command, then generates each deliverable through the relevant sub-module's script. **Nothing is written by hand.**

## 🗺️ Suite architecture — path convention (READ FIRST)

Expand All @@ -28,6 +28,7 @@ The modules' Python code is unchanged: each script receives all its paths as arg
| Deliverable / need | Module | Main script |
|---|---|---|
| Profile / CV / signature / templates config | `modules/candidate-config` | `setup_workflow.py`, `setup_signature.py`, `generate_templates.py` |
| Posting brief (auto at intake) | `modules/posting-brief-generator` | `generate_posting_brief.py` (+ `md_to_pdf.py`) |
| Strategic playbook | `modules/strategic-playbook-generator` | `generate_playbook.py` (+ `md_to_pdf.py`) |
| Application summary | `modules/application-summary-generator` | `generate_application_summary.py` (+ `md_to_pdf.py`) |
| Interview prep | `modules/interview-prep-generator` | `generate_interview_prep.py` (+ `md_to_pdf.py`) |
Expand Down Expand Up @@ -70,7 +71,7 @@ How a chosen language is *realized* (register, idiom, dates, numbers, spacing, l
5. **Gather the relevant memory items** for the widget block (if memory is active): name/profile, CV, address, ongoing applications, preferences. Each item has `id`, `label`, `value`. Present as "non-exhaustive," never as an audit.

### STEP 2 — Already-produced deliverables
Look in `/mnt/user-data/outputs/` for already-generated deliverables (`Strategic_Playbook_*`, `Application_Summary_*`, `Interview_Prep_*`, `Cover_Letter_*`, `Quick_Reference_*`) to pre-check / grey out the boxes.
Look in `/mnt/user-data/outputs/` for already-generated deliverables (`Strategic_Playbook_*`, `Application_Summary_*`, `Interview_Prep_*`, `Cover_Letter_*`, `Quick_Reference_*`) to pre-check / grey out the boxes. Also note any `Posting_Brief_*` for this application: it is **not** a widget box, but its presence makes the auto-intake brief (STEP 4) idempotent — don't regenerate it.

### STEP 3 — Show the widget
```bash
Expand Down Expand Up @@ -102,7 +103,8 @@ Input arrives one of two **equivalent** ways: (a) the **Generate click** — the
- **Circulate the value:** pass it as `--language <code>` to each script below (letter = confirmed **offer** language; help-docs = **working** language, or the override). any ISO 639-1 code works (**L6**): there are no per-language assets anymore — the scripts are language-agnostic and the model supplies the labels/strings in the run language.
- **Beyond `--language` (labels contract — L6):** each deliverable's **structure labels** are produced **by the model in the run language** and passed to its script — `--labels-json '{...}'` for the four `.md` generators (the script enforces the **exact** key set — no invented/omitted section), and `subject_label` / `greeting` / `closing` in the letter's `--data-json`. See each module `GUIDE.md` + `references/language_style_generic.md`.
- **Persist (letter only, opportunistic):** when the letter language was confirmed and the application is (or is being) added to the tracker, write it to the tracker `language` column for silent reuse on resume. Letter-only without a tracker → persist nothing. Help-doc language is **never** persisted.
4. **Generate each chosen deliverable**, in the mandatory order below, **through the module's script** (never by hand). Read the module's `GUIDE.md` for the details, and prefix its paths with `modules/<name>/`:
4. **First, automatically generate the posting brief** (PB-1) — it is **not** a widget choice — then **generate each chosen deliverable**, in the mandatory order below, **through the module's script** (never by hand). Read the module's `GUIDE.md` for the details, and prefix its paths with `modules/<name>/`:
- `posting_brief` → `modules/posting-brief-generator` (**automatic, first, not a widget box**) — produced from a job posting at intake, **reusing the offer already read in the single global analysis** (sub-step 3), so it adds no separate offer read and nothing to the time-to-widget. Header + **verbatim** body + model-extracted digest (key requirements / deadline). The capture date and the **filename are script-owned** (`--output-dir`, read the printed path; never hand-compose `--output-path`). **Idempotent**: skip if a `Posting_Brief_<company>_<position>_*` already exists in outputs (STEP 2); regenerate only on an explicit request. Present it via `present_files`, and record its id `posting_brief` in the tracker `deliverables` (at `add_to_tracker`). Trigger = the presence of a job posting (the same trigger as the STEP 8 title); pure config actions produce no brief. Details: its `GUIDE.md`.
- `strategic_playbook` → `modules/strategic-playbook-generator`
- `application_summary` → `modules/application-summary-generator`
- `interview_prep` → `modules/interview-prep-generator`
Expand All @@ -112,7 +114,7 @@ Input arrives one of two **equivalent** ways: (a) the **Generate click** — the
5. Respect the dependencies (reference card after the documents it condenses).
6. No intermediate validation between deliverables.
7. Offer PDF export at the end, then, if the user agrees, generate the PDFs of **all** produced deliverables:
- `.md` deliverables (playbook, summary, interview prep, reference card) → the relevant module's `md_to_pdf.py`;
- `.md` deliverables (posting brief, playbook, summary, interview prep, reference card) → the relevant module's `md_to_pdf.py`;
- **cover letter `.docx`** (#7) → `python modules/cover-letter-generator/scripts/docx_to_pdf.py --input <letter.docx> --output <letter.pdf>` (LibreOffice; if unavailable the script exits with code 3 and the letter stays usable as `.docx`). Don't forget the letter in the PDF batch.
8. **Suggest the canonical conversation title** `📋 YYYY-MM-DD - Company - Position`, **systematically as soon as a deliverable is generated from a job description** — whatever the flow: full guided flow **OR** creation of a single specific document. The trigger is the **presence of a job description** (so not pure configuration actions: profile, signature, which are tied to no posting). **Title date = date of first activity if the application is already tracked, otherwise today's date** (candidate's local date — same locale resolution as the tracker timestamp) → a **time-stable title** (resuming on another day suggests the same title). It is a **suggestion for the user to apply**: the assistant **cannot rename** the conversation itself. This marker makes it findable in the **sidebar** (on desktop, conversation links aren't clickable). If the conversation already has this name, the suggestion lands correctly — zero cost.

Expand Down
162 changes: 162 additions & 0 deletions modules/posting-brief-generator/GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
> **⚠️ MODULE OF THE "candidate-suite" SUITE — PATH RELOCATION**
> This module lives in `modules/posting-brief-generator/`. All the relative paths below
> (`scripts/…`, `references/…`) are relative to THIS folder.
> From the suite root: prefix them with `modules/posting-brief-generator/`
> (e.g. `python modules/posting-brief-generator/scripts/<script>.py …`), or change into
> `modules/posting-brief-generator/` before running. The scripts' code is UNCHANGED:
> they receive all their paths as arguments, nothing is hard-coded.

---
name: posting-brief-generator
description: Generates a job-posting brief (dossier) in Markdown captured at application intake: header (company, position, recruiter, city, capture date, source, language), the verbatim posting body, and a short model-extracted digest (key requirements + deadline). Produced automatically when an application is opened; can be regenerated on explicit request.
---

# Posting brief generator

Generates a **posting brief** — the job-posting dossier captured at application
**intake**. It is an internal working document (not an outgoing deliverable):
it preserves the offer so the application keeps a single, durable reference.

## ⚠️ EXECUTION RULE (NON-NEGOTIABLE)

This module produces its deliverable ONLY via its Python scripts. Never write the final document "by hand" with `create_file`. Claude's role: PREPARE the content (JSON), then RUN the `generate_posting_brief.py` script, then offer/run `md_to_pdf.py`.

- ✅ Extract the content → build the JSON → run the script
- ❌ DO NOT `create_file` a hand-written .md
- ❌ DO NOT invent an output format different from the script's

## ⚠️ AUTOMATIC AT INTAKE — NOT A WIDGET CHOICE

Unlike the six standard deliverables, the posting brief is **not** a selection-widget
checkbox. The orchestrator (SKILL STEP 4) produces it **automatically, first**, as soon
as an application is opened from a job posting, reusing the offer already read in the
single global analysis. It is **idempotent**: skip it if a `Posting_Brief_<Company>_<Position>_*`
already exists in `/mnt/user-data/outputs/` for this application. Regenerate it only on an
**explicit** user request ("redo the posting brief", "capture this offer again").

## ⚠️ FUNDAMENTAL RULES

1. **Prerequisite**: a job posting (raw text or URL). No CV / config is required — the
brief is about the *offer*, not the candidate.

2. **Output format = Markdown (.md)** in `/mnt/user-data/outputs/`. Offer the PDF export
afterwards (it is also included in the orchestrator's end-of-run PDF batch, STEP 7).

3. **Filename is SCRIPT-OWNED**: pass `--output-dir /mnt/user-data/outputs` and let the
script build `Posting_Brief_<Company>_<Position>_<YYYYMMDD>.md` and **print the path**.
Read the printed path to present the file. Never hand-compose `--output-path`.

4. **Verbatim body**: `posting_body` is the offer text **copied as-is** (not summarized,
not rephrased). The digest is *in addition*, never a replacement.

5. **Extraction = the model's job**, in any language, **no regex**. Header fields and the
digest (key requirements, deadline) are read from the offer by you.

6. **Critical fields** (`company_name`, `job_title`, `posting_body`) must be real values:
the script refuses (exit 2) an empty value or the neutral sentinel `__MISSING__` — ask
the user, never invent.

## Overview

### Inputs
- **Job posting**: raw text OR URL (the orchestrator already has it from the global analysis)

### Outputs
- **`.md` file** in `/mnt/user-data/outputs/` (the brief)
- **`.pdf` file** optional (offered after the .md / batch in STEP 7)
- **Naming** (script-owned): `Posting_Brief_<Company>_<Position>_<YYYYMMDD>.md`

### Tracker
The brief's id is **`posting_brief`**. When the application is added to the tracker
(`add_to_tracker`), include `posting_brief` in the `deliverables` list.

## Structure labels (produced by the model — L6)

This generator does **not** hard-code per-language labels. **You** produce the section/field
labels **in the run language** (the working/conversation language) and pass them as
`--labels-json '{...}'` (alongside `--data-json` and `--language`). The script enforces the
**exact** key set below — a missing or extra key is rejected (anti-hallucination guardrail:
the structure is fixed, only the wording is yours).

**Required keys** (exact set, 13): `title`, `s_meta`, `l_company`, `l_position`,
`l_recruiter`, `l_city`, `l_captured`, `l_source`, `l_language`, `s_digest`,
`sub_requirements`, `sub_deadline`, `s_posting`

Each value is the label for that slot, **written by the model directly in the run language,
by intent** — there is **no per-language phrase table** (L6). Render each label from what it
*is* (e.g. `title` = names the document as a job-posting brief/dossier; `s_posting` = the full
job posting; `sub_deadline` = application deadline). **Never transliterate an English label.**
See `modules/cover-letter-generator/references/language_style_generic.md` for register / locale
conventions.

## Workflow

### STEP 1 — Get the posting
- **Raw text** → continue
- **URL** → `web_fetch`; if it fails → ask for a copy-paste

### STEP 2 — Extract (model, no regex)
From the offer, read out:
- **Header**: `company_name`, `job_title`, `recruiter_name` / `recruiter_title` (if any),
`city` (if any), `source_url` (if any), `posting_language` (human-readable, e.g. "English").
- **Digest**: `requirements` (the key requirements, a short list), `deadline` (if stated).
- **Body**: `posting_body` = the offer text **verbatim**.

The capture date is **not** yours: the script stamps today's local date (resolve the IANA
timezone the same way as the tracker — candidate's city → session locale → fallback — and pass
`--timezone`).

### STEP 3 — Generate the .md

Build the JSON and run:
```bash
python scripts/generate_posting_brief.py \
--language <code> \
--output-dir /mnt/user-data/outputs \
--data-json '<full_json>' \
--labels-json '<labels_json — exact key set, values localized in the run language>' \
--timezone <IANA, e.g. Europe/Paris>
```

**Read the printed path** and present the `.md` with `present_files`.

### STEP 4 — Offer the PDF export

**After presenting the .md, offer**:
```
The posting brief is ready in Markdown. Would you also like a PDF version?
```
If yes (or as part of the orchestrator's end-of-run PDF batch), run:
```bash
python scripts/md_to_pdf.py \
--input /mnt/user-data/outputs/<filename>.md \
--output /mnt/user-data/outputs/<filename>.pdf \
--title "<deliverable title>"
```
Present the `.pdf` with `present_files`.

## Supported user commands

- *(automatic)*: produced first by the orchestrator at intake (SKILL STEP 4)
- `"Redo the posting brief"` / `"Capture this offer again"`: regenerate
- `"Export the brief to PDF"`: convert an existing .md

## Available scripts

| Script | Usage |
|--------|-------|
| `generate_posting_brief.py --language X --output-dir DIR --data-json '...' --labels-json '...' [--timezone TZ]` | Generates the .md (script-owned filename) |
| `md_to_pdf.py --input X.md --output X.pdf [--title "..."]` | Converts .md → styled PDF |

## References

- `references/posting_brief_structure.md`: detailed structure of the brief

## Important rules

- **AUTOMATIC at intake**, first; **idempotent** (skip if already present); regenerate only on explicit request
- **.md format** in `/mnt/user-data/outputs/`; offer the PDF after
- **Filename is script-owned** (`--output-dir`, read the printed path)
- **Body verbatim**; the digest is additive
- **Extraction by the model**, any language, **no regex**
- **Critical fields** empty / `__MISSING__` → exit 2: ask, never invent
Loading