From ac923b504651600d66935d387d576cba64c6d724 Mon Sep 17 00:00:00 2001 From: David Bon-Salomon <62355986+David-BS@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:22:25 +0200 Subject: [PATCH] feat(posting-brief): posting brief generator at intake (0.20.0) --- CHANGELOG.md | 14 + SKILL.md | 14 +- modules/posting-brief-generator/GUIDE.md | 162 ++++++++++ .../references/posting_brief_structure.md | 70 +++++ .../scripts/generate_posting_brief.py | 282 ++++++++++++++++++ .../scripts/md_to_pdf.py | 161 ++++++++++ references/assistant_flow.md | 6 + tests/test_examples.py | 6 +- tests/test_posting_brief.py | 209 +++++++++++++ tooling/build_samples.py | 44 +++ 10 files changed, 959 insertions(+), 9 deletions(-) create mode 100644 modules/posting-brief-generator/GUIDE.md create mode 100644 modules/posting-brief-generator/references/posting_brief_structure.md create mode 100644 modules/posting-brief-generator/scripts/generate_posting_brief.py create mode 100644 modules/posting-brief-generator/scripts/md_to_pdf.py create mode 100644 tests/test_posting_brief.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f102410..b9ac77e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` builds `Posting_Brief___.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 diff --git a/SKILL.md b/SKILL.md index 2e4e8e8..4baa637 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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) @@ -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`) | @@ -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 @@ -102,7 +103,8 @@ Input arrives one of two **equivalent** ways: (a) the **Generate click** — the - **Circulate the value:** pass it as `--language ` 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//`: +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//`: + - `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___*` 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` @@ -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 --output ` (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. diff --git a/modules/posting-brief-generator/GUIDE.md b/modules/posting-brief-generator/GUIDE.md new file mode 100644 index 0000000..d3cc5ae --- /dev/null +++ b/modules/posting-brief-generator/GUIDE.md @@ -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/