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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] - 2026-07-11

### Added

- Learn subject-plugin contract v1.0: eight tutor verbs (overview, progress, advice, story, lesson, practice, record, doctor) with schema-valid --json and contractual exit codes
- Tutor engine: 3-module French curriculum (5 lessons, 9 items, 18 exercises), per-learner XDG state with atomic writes and resume
- Story ladder: 11 graded-reader stories (A1-C1) with glossaries and comprehension exercises, plus dev fixtures; CI validates all content against the cited story schema
- Cited contract schemas + validator from learn-cli (contract v1.0, provenance ledger in docs/contract-provenance.md)
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Changelog entries lack rationale 📘 Rule violation ⚙ Maintainability

The newly added 0.5.0 CHANGELOG entries are single-sentence/fragments and do not include a second
sentence explaining why the change was made. This violates the requirement that each new changelog
entry include a multi-sentence rationale.
Agent Prompt
## Issue description
New `CHANGELOG.md` entries must contain at least two sentences: one describing what changed and a second explaining why.

## Issue Context
PR Compliance ID 1864313 requires multi-sentence rationale entries for each new or modified changelog bullet.

## Fix Focus Areas
- CHANGELOG.md[8-16]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


## [0.4.1] - 2026-07-10

### Changed
Expand Down
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,60 @@ resolve to the root doc entry.

## CLI

french-cli is an **LLM-free tutor engine** that implements the
[learn subject-plugin contract](https://github.com/agentculture/learn-cli): it
owns the committed French content (stories, lessons, exercises) and each
learner's mastery state, resolves what to teach next, and emits structured
teaching **directives**. The driving agent (or human) does the conversational
tutoring the directive describes, grades `pass|partial|fail`, and writes the
result back with `record`. french never converses, grades free text, or computes
scores — learn-cli's motivation layer does the scoring.

### Tutor verbs (the contract surface)

| Verb | What it does | Payload `kind` |
|------|--------------|----------------|
| `french overview` | Subject self-description: modules + content counts. | `subject_overview` |
| `french progress` | The learner's mastery, counters, and next step. | `progress` |
| `french advice` | Deterministic study advice from stored state. | `advice` |
| `french story list \| read <id>` | Graded stories + a reading directive. | `story_list` / `story_read` |
| `french lesson start \| next \| repeat` | Teaching directives from the curriculum. | `lesson_directive` |
| `french practice [<scope>]` | A batch of exercises to run (no scope = review). | `practice_directive` |
| `french record --item <id> --result …` | Write back one graded outcome; updates mastery. | `record_ack` |
| `french doctor` | Self-check + the pinned contract version. | `subject_doctor` |

### Agent-first verbs

| Verb | What it does |
|------|--------------|
| `french whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. |
| `french learn` | Print a structured self-teaching prompt. |
| `french explain <path>` | Markdown docs for any noun/verb path. |
| `french overview` | Read-only descriptive snapshot of the agent. |
| `french doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
| `french cli overview` | Describe the CLI surface itself. |

Every command supports `--json`. Results go to stdout, errors/diagnostics to
stderr (never mixed). Exit codes: `0` success, `1` user error, `2` environment
error, `3+` reserved.

## Status
### Learner state

Learner-scoped verbs take `--learner <id>` (default: `$FRENCH_CLI_LEARNER` or the
OS user; the resolved id is echoed back). State persists one JSON file per
learner under `$FRENCH_CLI_LEARN_HOME` (or `$XDG_DATA_HOME/french_cli/learn`, or
`~/.local/share/french_cli/learn`), written atomically and resumed across
sessions.

## Content

Stories are committed JSON files under `content/stories/*.json` (flat, one file
per story, `filename == id`), each validated against the shared `story` schema in
CI (`tests/test_story_content.py`). The starter set ships `dev-`prefixed dev
stories; the full graded ladder is authored separately. The curriculum (modules,
lessons, items, exercises) lives as structured data in
`french/tutor/curriculum.py`.

**The French-tutor domain is not implemented yet.** The scaffold — identity,
skill kit, CI, and the agent-first CLI contract — is in place, but every verb
today is generic introspection (`whoami`, `learn`, `explain`, `overview`,
`doctor`, `cli overview`). Progress tracking, advice, stories, and written /
spoken practice are still to be built.
The contract schemas + validator are **cited (cite-don't-import)** into
`french/contract_cite/` — see [`docs/contract-provenance.md`](docs/contract-provenance.md).

See [`CLAUDE.md`](CLAUDE.md) for the architecture and the full conventions
(version-bump-every-PR, the `cicd` PR lane, deploy setup).
Expand Down
66 changes: 66 additions & 0 deletions content/stories/dev-cafe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"schema_version": "1.0",
"kind": "story",
"id": "dev-cafe",
"subject": "french",
"title": "Un cafe, s'il vous plait",
"level": "beginner",
"level_detail": "A1",
"language": "fr",
"summary": "Paul orders a coffee and a croissant, then pays at a small cafe.",
"body": "Paul entre dans un petit cafe. Il regarde le menu.\n\n« Bonjour ! Je voudrais un cafe et un croissant, s'il vous plait. »\n\nLe serveur sourit. « Tres bien. Ca fait trois euros cinquante. »\n\nPaul paie et s'assoit pres de la fenetre. Le cafe est chaud et le croissant est delicieux.",
"glossary": [
{
"term": "je voudrais",
"definition": "I would like",
"note": "The polite way to order; softer than « je veux »."
},
{
"term": "ca fait trois euros cinquante",
"definition": "that comes to three euros fifty"
},
{
"term": "pres de la fenetre",
"definition": "near the window"
}
],
"exercises": [
{
"id": "dev-cafe-q1",
"type": "multiple_choice",
"item_id": "fr.food.commander",
"prompt": "Qu'est-ce que Paul commande ?",
"choices": [
"Un cafe et un croissant",
"Un the et une tarte",
"Un jus et un pain"
],
"answer": "Un cafe et un croissant"
},
{
"id": "dev-cafe-q2",
"type": "multiple_choice",
"item_id": "fr.numbers.prix",
"prompt": "Combien Paul paie-t-il ?",
"choices": [
"Deux euros",
"Trois euros cinquante",
"Dix euros"
],
"answer": "Trois euros cinquante"
},
{
"id": "dev-cafe-q3",
"type": "cloze",
"item_id": "fr.food.commander",
"prompt": "« Je ___ un cafe, s'il vous plait. »",
"answer": "voudrais"
}
],
"audio": null,
"source": {
"generator": "hand-authored",
"reviewed_by": "french-cli t4",
"generated_at": "2026-07-11"
}
}
66 changes: 66 additions & 0 deletions content/stories/dev-directions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"schema_version": "1.0",
"kind": "story",
"id": "dev-directions",
"subject": "french",
"title": "Ou est la gare ?",
"level": "intermediate",
"level_detail": "A2",
"language": "fr",
"summary": "Lost in town, Hugo asks a passer-by for the way to the station.",
"body": "Hugo est perdu. Il cherche la gare.\n\nIl arrete une dame dans la rue. « Pardon, madame. Ou est la gare, s'il vous plait ? »\n\n« C'est facile », repond la dame. « Continuez tout droit, puis tournez a gauche au coin. La gare est a cinq minutes a pied. »\n\n« Merci beaucoup ! » dit Hugo. Il tourne a gauche et trouve la gare tout de suite.",
"glossary": [
{
"term": "je cherche",
"definition": "I am looking for",
"note": "From « chercher »; « je cherche la gare »."
},
{
"term": "tout droit",
"definition": "straight ahead"
},
{
"term": "tournez a gauche",
"definition": "turn left",
"note": "« a droite » is to the right."
},
{
"term": "a cinq minutes a pied",
"definition": "five minutes away on foot"
}
],
"exercises": [
{
"id": "dev-directions-q1",
"type": "multiple_choice",
"item_id": "fr.ville.directions",
"prompt": "Qu'est-ce que Hugo cherche ?",
"choices": [
"La gare",
"Le marche",
"Le cafe"
],
"answer": "La gare"
},
{
"id": "dev-directions-q2",
"type": "cloze",
"item_id": "fr.ville.directions",
"prompt": "Continuez tout droit, puis tournez a ___ au coin.",
"answer": "gauche"
},
{
"id": "dev-directions-q3",
"type": "short_answer",
"item_id": "fr.ville.directions",
"prompt": "La gare est a combien de minutes a pied ?",
"rubric": "Passes with « cinq minutes » (or « 5 minutes »); anything else is fail."
}
],
"audio": null,
"source": {
"generator": "hand-authored",
"reviewed_by": "french-cli t4",
"generated_at": "2026-07-11"
}
}
78 changes: 78 additions & 0 deletions content/stories/dev-marche.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"schema_version": "1.0",
"kind": "story",
"id": "dev-marche",
"subject": "french",
"title": "Au marche du samedi",
"level": "beginner",
"level_detail": "A1",
"language": "fr",
"summary": "Claire buys apples, bread, and cheese at the Saturday market.",
"body": "Le samedi matin, Claire va au marche.\n\nElle achete un kilo de pommes rouges. « Bonjour, madame ! Un kilo de pommes, s'il vous plait. » Le vendeur dit : « Voila, ca fait deux euros. »\n\nEnsuite, Claire achete une baguette et un fromage de chevre. Elle aime beaucoup le fromage.\n\nA midi, elle rentre a la maison et prepare un dejeuner simple : du pain, du fromage et une pomme.",
"glossary": [
{
"term": "le marche",
"definition": "the market",
"note": "« un marche en plein air » is an open-air market."
},
{
"term": "un kilo de pommes",
"definition": "a kilo of apples",
"note": "Quantities take « de »: un kilo de, une tranche de."
},
{
"term": "le fromage de chevre",
"definition": "goat cheese"
},
{
"term": "elle rentre a la maison",
"definition": "she goes back home"
}
],
"exercises": [
{
"id": "dev-marche-q1",
"type": "multiple_choice",
"item_id": "fr.food.marche",
"prompt": "Quand est-ce que Claire va au marche ?",
"choices": [
"Le samedi matin",
"Le dimanche soir",
"Le lundi a midi"
],
"answer": "Le samedi matin"
},
{
"id": "dev-marche-q2",
"type": "multiple_choice",
"item_id": "fr.numbers.prix",
"prompt": "Combien coutent les pommes ?",
"choices": [
"Deux euros",
"Quatre euros",
"Dix euros"
],
"answer": "Deux euros"
},
{
"id": "dev-marche-q3",
"type": "cloze",
"item_id": "fr.food.marche",
"prompt": "Claire achete une baguette et un ___ de chevre.",
"answer": "fromage"
},
{
"id": "dev-marche-q4",
"type": "short_answer",
"item_id": "fr.food.marche",
"prompt": "Qu'est-ce que Claire mange a midi ?",
"rubric": "Any answer naming bread, cheese, and/or an apple (du pain, du fromage, une pomme) passes; one item is partial."
}
],
"audio": null,
"source": {
"generator": "hand-authored",
"reviewed_by": "french-cli t4",
"generated_at": "2026-07-11"
}
}
81 changes: 81 additions & 0 deletions content/stories/fr-a1-la-rentree.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"schema_version": "1.0",
"kind": "story",
"id": "fr-a1-la-rentree",
"subject": "french",
"title": "La rentrée de Nina",
"level": "beginner",
"level_detail": "A1",
"language": "fr",
"summary": "It is Nina's first day back at school, and she is a little nervous about her new teacher.",
"body": "Aujourd'hui, c'est la rentrée. Nina a huit ans et elle est un peu nerveuse.\n\nLe matin, elle se réveille tôt. Elle prend son petit-déjeuner : du pain, du beurre et un chocolat chaud. Sa mère prépare son cartable avec des cahiers, des crayons et une nouvelle trousse bleue.\n\n« Tu es prête, Nina ? » demande sa mère. « Oui, mais j'ai un peu peur, » répond Nina. « Il y a une nouvelle maîtresse cette année. »\n\nÀ l'école, Nina retrouve ses amis dans la cour. Ils sont contents de se revoir après les vacances d'été. La maîtresse s'appelle Madame Dubois. Elle sourit et dit : « Bonjour, les enfants ! Bienvenue dans la classe de CE2. »\n\nPendant la journée, les élèves apprennent leurs nouveaux noms, rangent leurs affaires et écoutent une histoire. À midi, Nina mange à la cantine avec sa meilleure amie, Léa.\n\nLe soir, Nina raconte sa journée à ses parents. « La maîtresse est très gentille, » dit-elle. « Et j'ai une nouvelle amie dans ma classe ! »",
"glossary": [
{
"term": "la rentrée",
"definition": "back-to-school (day), the start of the school year"
},
{
"term": "le cartable",
"definition": "the school bag"
},
{
"term": "la trousse",
"definition": "the pencil case"
},
{
"term": "la maîtresse",
"definition": "the (female) teacher",
"note": "Used for a primary-school teacher; le maître for a man."
},
{
"term": "la cour",
"definition": "the schoolyard"
},
{
"term": "la cantine",
"definition": "the school canteen"
},
{
"term": "avoir peur",
"definition": "to be scared/afraid"
}
],
"exercises": [
{
"id": "fr-a1-la-rentree-q1",
"type": "multiple_choice",
"item_id": "fr.story.fr-a1-la-rentree.1",
"prompt": "Quel âge a Nina ?",
"choices": ["Six ans", "Huit ans", "Dix ans"],
"answer": "Huit ans"
},
{
"id": "fr-a1-la-rentree-q2",
"type": "multiple_choice",
"item_id": "fr.story.fr-a1-la-rentree.2",
"prompt": "Comment s'appelle la maîtresse de Nina ?",
"choices": ["Madame Dubois", "Madame Martin", "Madame Petit"],
"answer": "Madame Dubois"
},
{
"id": "fr-a1-la-rentree-q3",
"type": "true_false",
"item_id": "fr.story.fr-a1-la-rentree.3",
"prompt": "Nina n'a pas d'amis à l'école.",
"answer": "false"
},
{
"id": "fr-a1-la-rentree-q4",
"type": "open",
"item_id": "fr.story.fr-a1-la-rentree.4",
"prompt": "Pourquoi Nina a-t-elle un peu peur le matin de la rentrée ?",
"rubric": "Passes if the answer mentions the new teacher or general first-day nerves; partial if it just says 'she is scared' with no reason."
}
],
"audio": null,
"source": {
"generator": "hand-authored",
"reviewed_by": "learn-cli t7a",
"generated_at": "2026-07-11"
}
}
Loading
Loading