From cecc95d15f0ba9b8e45af192e686b87c162941ce Mon Sep 17 00:00:00 2001 From: Ori Nachum Date: Fri, 10 Jul 2026 06:59:15 +0300 Subject: [PATCH 1/2] docs: re-init CLAUDE.md from seed into a full runtime prompt Replace the bootstrap seed (agent description + "run /init") with a runtime prompt derived from how the code actually behaves. Documents the things that cost a fresh session real time to rediscover: - The French-tutor domain is NOT implemented. Every verb is generic template introspection and the package docstrings still describe the repo as a clonable template. - `teken cli doctor . --strict` exits 1 on a clean checkout, so CI's lint job is red before any change: the template rename left dist `french-cli`, console script `french`, and argparse prog `french-cli` disagreeing, and the rubric requires `explain french` to resolve while the catalog keys its root on ("french-cli",). Recorded with a name table and both candidate fixes; choosing one is a naming decision, so it is deferred rather than patched here. - README's quickstart invokes a `french-cli` console script that does not exist (it is `french`), and cites a `git grep` command in CLAUDE.md that never existed there. - The agent-first rubric is a contract, making apparently-dead code load-bearing: overview's ignored `target`, the `cli` noun group, and _CliArgumentParser._json_hint. - backend: colleague => AGENTS.colleague.md, enforced jointly by doctor.py's _PROMPT_FILE and test_doctor_recognizes_declared_backend. The seed had re-introduced the stale `backend: claude` claim that 0.3.4 corrected elsewhere. - `type: command` in a vendored SKILL.md is load-bearing -- core.skill_loader silently skips a SKILL.md without it. Also refresh uv.lock, which still pinned french-cli 0.3.4 while pyproject declared 0.4.0 (the 0.4.0 bump never re-locked), and bump to 0.4.1 per the version-bump-every-PR gate. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019pZenu1fqrz35Hxh9m5VjW --- CHANGELOG.md | 49 +++++++++++ CLAUDE.md | 222 ++++++++++++++++++++++++++++++++++++++++++++----- pyproject.toml | 2 +- uv.lock | 48 +++++------ 4 files changed, 277 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a1037..8b9ae77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,55 @@ 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.4.1] - 2026-07-10 + +### Changed + +- **Re-initialized `CLAUDE.md` from the bootstrap seed into a full runtime + prompt** (`/init`). The seed only carried the agent description and a + "run `/init`" instruction; it is now a working prompt derived from the code + as it actually behaves. Documents: that the French-tutor domain is **not yet + implemented** (every verb is still generic template introspection, and the + package docstrings still describe the repo as a clonable template); the + agent-first contract enforced by `teken cli doctor . --strict` and which + apparently-dead code it makes load-bearing (`overview`'s ignored `target` + positional, the `cli` noun group, `_CliArgumentParser._json_hint`); the + `backend: colleague` → `AGENTS.colleague.md` coupling that `doctor.py`'s + `_PROMPT_FILE` map and `test_doctor_recognizes_declared_backend` enforce + together, so a backend promotion cannot silently skip the mapping; the + cite-don't-import rules for `.claude/skills/` (including that `type: command` + is load-bearing — `core.skill_loader` silently skips a `SKILL.md` without it); + and the version-bump-every-PR gate. + + The seed also re-introduced the stale `backend: claude` / `CLAUDE.md` claim + that 0.3.4 had already corrected elsewhere; the new file states the actual + `colleague` / `AGENTS.colleague.md` identity, and notes that `CLAUDE.md` + guides Claude Code sessions while `AGENTS.colleague.md` is the resident + prompt this agent runs on. + +- **Recorded two known defects rather than silently working around them** + (both pre-existing, neither introduced here): + + - `uv run teken cli doctor . --strict` exits 1 on a clean checkout, so the CI + `lint` job is red before any change. The rename from `culture-agent-template` + left three names disagreeing — dist `french-cli`, console script `french` + (`[project.scripts]`), argparse `prog` `french-cli`. The rubric derives the + tool name from `[project.scripts]` and requires `explain french` to resolve, + but `french/explain/catalog.py` keys its root entry on `("french-cli",)`. + `CLAUDE.md` carries the name table and the two possible fixes; picking one + is a deliberate naming decision, not a mechanical patch, so it is deferred + to a follow-up. + - `README.md`'s quickstart invokes `uv run french-cli whoami` / `learn`, but + no such console script exists (it is `uv run french`), and it points at a + `git grep` discovery command "in `CLAUDE.md`" that has never existed there. + +### Fixed + +- `uv.lock` had drifted out of sync with `pyproject.toml` — the lock still + pinned `french-cli 0.3.4` while the project declared `0.4.0` (the 0.4.0 bump + never re-locked). `uv sync` refreshed the pin and re-sorted the package + blocks into alphabetical order. + ## [0.4.0] - 2026-06-23 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 1133723..02fde4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,28 +1,212 @@ -# CLAUDE.md — seed / bootstrap placeholder +# CLAUDE.md -> **This is a self-initializing seed, not a finished runtime prompt.** -> Run `/init` (or describe the agent's domain to your AI assistant) to -> re-initialize this file into a full runtime prompt, using the description -> below and the scaffolded repo as context. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Agent +## What this repo is right now -This repository hosts the **french-cli** agent. +`french-cli` is intended to become an **agent + CLI that turns Claude into a +private French tutor** (track progress, get an overview, get advice, read +stories, learn & practice French written and spoken from your phone). -## Description +**None of that is implemented yet.** What exists today is the +`culture-agent-template` scaffold, renamed: an agent-first CLI skeleton, a mesh +identity, a vendored skill kit, and a CI/publish baseline. Every verb +(`whoami`, `learn`, `explain`, `overview`, `doctor`, `cli overview`) is generic +template introspection — no French domain code exists. The package docstrings +and `explain` catalog still describe the repo as "a clonable template for +AgentCulture mesh agents," which is accurate for the code but not for the +project's purpose. Expect to write the tutor domain from scratch, and to update +that self-description prose as you do. -Agent + CLI that turns Claude into a private French tutor: track progress, get an overview, get advice, read stories, and learn & practice French (written and spoken) online from your phone. +## Commands -## Re-init instruction +```bash +uv sync # install (creates .venv) +uv run french whoami # run the CLI — note: `french`, not `french-cli` +uv run pytest -n auto # full suite (22 tests, ~1s) +uv run pytest tests/test_cli.py::test_whoami_text -v # a single test +``` -This file is a seed. To expand it into your full runtime prompt: +The four gates CI runs, in the order they'll bite you: -1. Open this repo in Claude Code (or your preferred AI assistant). -2. Run `/init` — the assistant will read the repo, incorporate the description - above, and replace this seed with a complete `CLAUDE.md`. -3. Commit the result. +```bash +uv run black --check french tests +uv run isort --check-only french tests +uv run flake8 french tests +uv run bandit -c pyproject.toml -r french +npx markdownlint-cli2@0.21.0 "**/*.md" "#node_modules" "#.local" "#.claude/skills" "#.teken" +uv run teken cli doctor . --strict # the agent-first rubric gate +``` -Until you run `/init`, `french-cli` satisfies the `steward doctor` -`prompt-file-present` and `backend-consistency` invariants (a `CLAUDE.md` -exists and `culture.yaml` declares `backend: claude`) but the prompt is not -yet tailored to this agent's domain. +CI's coverage invocation (`fail_under = 60`): + +```bash +uv run pytest -n auto --cov=french --cov-report=xml:coverage.xml --cov-report=term -v +``` + +## Known breakage: the rubric gate fails on a clean checkout + +`uv run teken cli doctor . --strict` **exits 1** today, which means the CI +`lint` job is red before you change anything: + +```text +FAIL (error) explain_self: `explain french` exit=1 + hint: add an entry for 'french' (and the root) in the explain catalog +``` + +The cause is an incomplete rename. Three different names are in play: + +| Thing | Value | Where | +|-------|-------|-------| +| Distribution / PyPI name | `french-cli` | `pyproject.toml` `[project] name` | +| Import package | `french` | `french/` | +| Console script | `french` | `pyproject.toml` `[project.scripts]` | +| argparse `prog` | `french-cli` | `french/cli/__init__.py` | + +The rubric derives the tool's own name from `[project.scripts]` (→ `french`) and +requires `explain ` to resolve, but `french/explain/catalog.py` keys +the root entry on `("french-cli",)`. Fix it by picking one name and committing +to it — either add a `("french",)` key to `ENTRIES`, or rename the console +script to `french-cli`. Whichever you choose, the argparse `prog`, the catalog +keys, the `_TEXT`/`_as_json_payload` blocks in `learn.py`, and every `uv run …` +line in `README.md` must agree. + +Related doc drift, worth fixing in the same pass: `README.md` documents +`uv run french-cli whoami` / `uv run french-cli learn` (no such script — it's +`uv run french`), and points at "the `git grep` discovery command in +`CLAUDE.md`" that has never existed in this file. + +## Architecture: the agent-first contract + +The CLI is cited (not imported) from [teken](https://github.com/agentculture/teken)'s +`python-cli` reference. Its shape is enforced by an external rubric — +`teken cli doctor . --strict`, run in CI — so the structure below is a +**contract, not a style preference**. The runtime has zero third-party +dependencies (`dependencies = []`); `teken` is dev-only. + +**Output discipline** (`french/cli/_output.py`) — results go to stdout, +errors and diagnostics go to stderr, and they never mix. Every command takes +`--json`. Agents parse this; don't break it. + +**Errors** (`french/cli/_errors.py`) — every failure raises +`CliError(code, message, remediation)`. `main()` catches it, renders +`error: ` + `hint: ` to stderr, and exits with the code. +Any other exception is wrapped so no Python traceback ever escapes. Exit codes: +`0` success, `1` user error, `2` environment error, `3+` reserved. + +Two subtleties in `french/cli/__init__.py` that look odd until you know why: + +- `_CliArgumentParser` overrides argparse's `.error()` so parse failures + (unknown verb, bad flag) route through the same `error:`/`hint:` contract and + exit `1` — argparse's default writes its own format and exits `2`. +- Parse errors happen *before* `args.json` exists, so `main()` pre-scans raw + argv for `--json` and stashes the answer on the class-level `_json_hint`. + `parser_class=_CliArgumentParser` is passed to `add_subparsers` so every + subparser (including nested noun groups) inherits both behaviors. + +**Adding a verb.** Create `french/cli/_commands/.py` exposing +`register(sub)`, call it from `_build_parser()`, and **add a matching entry to +`french/explain/catalog.py`** — the rubric fails if a registered path has no +`explain` entry. Give it `--json`. Noun groups follow the same pattern; see +`_commands/cli.py`, which exists solely because the rubric requires any noun +with action-verbs to expose `overview`. + +Some non-obvious rubric requirements already encoded in the code, which will +look like dead weight if you refactor blindly: + +- `overview` accepts an **ignored** `target` positional — descriptive verbs must + never hard-fail on a missing path (`overview /no/such/path` exits 0). +- `learn` must be ≥200 chars and mention purpose, the command map, exit codes, + `--json`, and `explain`. Its text and JSON payload are maintained by hand and + drift from reality easily — update both when you add a verb. +- `doctor` must emit `{healthy, checks: [{id, passed, severity, message, remediation}]}`. + +## Identity and the mesh + +`culture.yaml` declares this agent to the AgentCulture IRC mesh: + +```yaml +agents: +- suffix: french-cli + backend: colleague + model: sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP +``` + +The backend is **`colleague`**, so the resident prompt file this agent runs on +is **`AGENTS.colleague.md`** — not this file. `CLAUDE.md` guides Claude Code +sessions; `AGENTS.colleague.md` is what the colleague backend loads. Keep both +in mind when you change how the agent describes itself. + +`french/cli/_commands/doctor.py` enforces the `backend` → prompt-file mapping +(`claude`→`CLAUDE.md`, `colleague`→`AGENTS.colleague.md`, `acp`→`AGENTS.md`, +`gemini`→`GEMINI.md`), mirroring `steward doctor`'s invariants. **Changing +`backend:` in `culture.yaml` requires updating `_PROMPT_FILE` in `doctor.py`**, +or `test_doctor_recognizes_declared_backend` fails. That test exists precisely +to catch a backend promotion that forgets the mapping. + +`whoami.py` parses `culture.yaml` with hand-rolled line scanning rather than a +YAML library, to keep runtime dependencies empty. It walks up from `__file__` +(not the CWD) so the identity is always *this agent's*, not whatever +`culture.yaml` sits in the caller's working directory. In a wheel install no +`culture.yaml` ships, and both `whoami` and `doctor` degrade to defaults. + +## Vendored skills — cite, don't import + +`.claude/skills/` holds 14 skills, vendored verbatim: 11 from +[guildmaster](https://github.com/agentculture/guildmaster), plus `ask-colleague` +from `colleague` and `remember`/`recall` from `eidetic-cli`. Provenance and the +re-sync procedure live in [`docs/skill-sources.md`](docs/skill-sources.md). + +- **Don't hand-edit script bodies.** Re-sync from upstream instead. Where an + in-place patch was unavoidable (the `agex`→`devex` rename, the + `outsource`→`ask-colleague` rename) it is recorded as a tracked divergence in + `docs/skill-sources.md` with an upstream issue. Follow that pattern. +- Every `SKILL.md` must carry `type: command` in its frontmatter. This is + **load-bearing**: `core.skill_loader` silently skips any `SKILL.md` without a + `type:`, even where guildmaster's upstream copy omits it. +- `markdownlint` and `sonar.exclusions` both skip `.claude/skills/**` — vendored + copies are cited verbatim and must not be reformatted. + +Optional runtime tools these skills shell out to: `devex` (>=0.21, the `cicd` PR +lane), `agtag` (>=0.1, `communicate` issue I/O), `colleague` and `eidetic` +(optional; the wrappers exit with an install hint when absent). + +**Reach for `ask-colleague` reflexively.** The read-only verbs (`review`, +`explore`) run isolated in a throwaway git worktree with zero side effects — run +`review` for a diverse second opinion before opening a PR on a non-trivial diff, +and `explore` for a fresh read of an unfamiliar area. The side-effecting +`write --apply` / `write --pr` needs the user's go-ahead first. + +## Conventions + +**Every PR bumps the version — even docs-only, config-only, or CI-only PRs.** +The `version-check` CI job diffs `pyproject.toml`'s version against `main`, +comments on the PR, and fails the build if they match. Use the skill: + +```bash +python3 .claude/skills/version-bump/scripts/bump.py patch # or minor / major +``` + +It updates `pyproject.toml` and prepends a [Keep a Changelog](https://keepachangelog.com/) +entry to `CHANGELOG.md`. Write the changelog entry properly — this repo's +history uses it as the real record of *why* a change happened, and existing +entries are detailed prose, not one-liners. + +Formatting is line-length 100 across `black`, `isort` (profile `black`), and +`flake8` (which extends-ignores `E203`/`W503` to stop fighting `black`). + +`[tool.coverage.run] relative_files = true` is not decorative: without it, +`coverage.xml` emits absolute paths that SonarCloud cannot map to +`sonar.sources=french`, and coverage silently reports as empty. + +Use the `cicd` skill for the PR lifecycle (`create PR`, `review comments`, +`address feedback`, `status`, `await`). It wraps `devex pr` and appends the +`- french-cli (Claude)` signature automatically via `_resolve-nick.sh` — don't +sign PR replies by hand when using it. + +## Publishing + +`.github/workflows/publish.yml` fires only on changes to `pyproject.toml` or +`french/**`. Pull requests publish a `.dev` build to TestPyPI; +pushes to `main` publish to PyPI. Both use Trusted Publishing (OIDC), so fork +PRs skip the publish step rather than failing. diff --git a/pyproject.toml b/pyproject.toml index 9b591b4..90c4a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "french-cli" -version = "0.4.0" +version = "0.4.1" description = "Agent + CLI that turns Claude into a private French tutor: track progress, get an overview, get advice, read stories, and learn & practice French (written and spoken) online from your phone." readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index dddbbd9..abae0d5 100644 --- a/uv.lock +++ b/uv.lock @@ -154,9 +154,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + [[package]] name = "french-cli" -version = "0.3.4" +version = "0.4.1" source = { editable = "." } [package.dev-dependencies] @@ -185,29 +208,6 @@ dev = [ { name = "teken", specifier = ">=0.8" }, ] -[[package]] -name = "execnet" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, -] - -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" From b1caa2fe2e8844ff421fc805f02a395c2a7d12f6 Mon Sep 17 00:00:00 2001 From: Ori Nachum Date: Fri, 10 Jul 2026 07:12:14 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20settle=20the=20CLI=20name=20split=20?= =?UTF-8?q?=E2=80=94=20`french`=20invokes,=20`french-cli`=20identifies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the pre-existing rubric failure that had CI's lint job red on a clean checkout: FAIL (error) explain_self: `explain french` exit=1 The agent-first rubric resolves the tool's own name from [project.scripts] (-> `french`) and requires `explain ` to resolve, but french/explain/catalog.py keyed its root entry only on ("french-cli",), and the argparse prog disagreed with the installed script too. Two names now coexist on purpose, with a stated rule: invocations -> `french` console script, argparse prog, usage lines, command maps, explain paths, help identity -> `french-cli` distribution/PyPI, repo, culture.yaml nick, _FALLBACK_NICK, _pkg_version, overview subject, doctor report header - catalog root is reachable under BOTH ("french",) and ("french-cli",) - prog="french"; every usage string moved onto the `french` invocation (learn.py's _TEXT and _as_json_payload, the explain remediation hint, command docstrings, --help text) - learn --json exposes both: `tool` (distribution) + new `command` field - README quickstart and CLI table now show the real command; the vestigial "Make it your own" section (cloner boilerplate citing a `git grep` command that never existed in CLAUDE.md) is replaced with a Status section stating the tutor domain is unbuilt - CLAUDE.md's "Known breakage" section becomes a "Naming" section, since the breakage is fixed test_explain_self now asserts both names resolve, and a new test_explain_self_matches_console_script pins the catalog key to the console script so this cannot regress silently. Verified: teken cli doctor . --strict -> 26/26 healthy (was 25/26, exit 1); pytest 23 passed; black/isort/flake8/bandit/markdownlint clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019pZenu1fqrz35Hxh9m5VjW --- CHANGELOG.md | 46 ++++++++++++------ CLAUDE.md | 65 ++++++++++++-------------- README.md | 40 ++++++++-------- french/cli/__init__.py | 7 ++- french/cli/_commands/cli.py | 6 +-- french/cli/_commands/doctor.py | 2 +- french/cli/_commands/explain.py | 4 +- french/cli/_commands/learn.py | 34 ++++++++------ french/cli/_commands/overview.py | 2 +- french/cli/_commands/whoami.py | 2 +- french/cli/_errors.py | 2 +- french/explain/__init__.py | 2 +- french/explain/catalog.py | 80 ++++++++++++++++++-------------- tests/test_cli.py | 33 ++++++++++--- 14 files changed, 187 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9ae77..4fb3c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,24 +31,40 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). guides Claude Code sessions while `AGENTS.colleague.md` is the resident prompt this agent runs on. -- **Recorded two known defects rather than silently working around them** - (both pre-existing, neither introduced here): - - - `uv run teken cli doctor . --strict` exits 1 on a clean checkout, so the CI - `lint` job is red before any change. The rename from `culture-agent-template` - left three names disagreeing — dist `french-cli`, console script `french` - (`[project.scripts]`), argparse `prog` `french-cli`. The rubric derives the - tool name from `[project.scripts]` and requires `explain french` to resolve, - but `french/explain/catalog.py` keys its root entry on `("french-cli",)`. - `CLAUDE.md` carries the name table and the two possible fixes; picking one - is a deliberate naming decision, not a mechanical patch, so it is deferred - to a follow-up. - - `README.md`'s quickstart invokes `uv run french-cli whoami` / `learn`, but - no such console script exists (it is `uv run french`), and it points at a - `git grep` discovery command "in `CLAUDE.md`" that has never existed there. +- **Settled the CLI's name as a deliberate two-name split**, and documented the + rule in `CLAUDE.md`: **invocations say `french`** (the console script, the + argparse `prog`, usage lines, command maps, `explain` paths, help text) while + **identity says `french-cli`** (the distribution / PyPI name, the repo, the + `culture.yaml` mesh nick, `_FALLBACK_NICK`, `_pkg_version`, the `overview` + subject, the `doctor` report header). `learn --json` now exposes both: the + existing `tool` field carries the distribution, and a new `command` field + carries the invocation. `README.md`'s CLI table and quickstart were rewritten + to the real command. + +- **`README.md`: replaced the vestigial "Make it your own" section.** It was + cloner boilerplate — this repo *is* the clone — and its step 1 pointed at a + `git grep` discovery command "in `CLAUDE.md`" that never existed there. It is + now a "Status" section stating plainly that the French-tutor domain is not + built yet. ### Fixed +- **The CI `lint` job was red on a clean checkout**, and had been since the + template rename: `uv run teken cli doctor . --strict` exited 1 with + ``FAIL (error) explain_self: `explain french` exit=1``. The agent-first rubric + resolves the tool's own name from `[project.scripts]` (→ `french`) and + requires `explain ` to resolve, but `french/explain/catalog.py` + keyed its root entry only on `("french-cli",)`, and the argparse `prog` also + disagreed with the installed script. The catalog root is now reachable under + **both** `("french",)` and `("french-cli",)`, `prog` is `french`, and every + usage string in the package (`learn.py`'s `_TEXT` *and* `_as_json_payload`, + the `explain` remediation hint, the command docstrings and `--help` text) was + moved onto the `french` invocation. The rubric now passes 26/26. + + `test_explain_self` was widened to assert **both** names resolve, and a new + `test_explain_self_matches_console_script` pins the catalog key to the + console script so this specific regression cannot recur silently. + - `uv.lock` had drifted out of sync with `pyproject.toml` — the lock still pinned `french-cli 0.3.4` while the project declared `0.4.0` (the 0.4.0 bump never re-locked). `uv sync` refreshed the pin and re-sorted the package diff --git a/CLAUDE.md b/CLAUDE.md index 02fde4f..be880eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,18 +12,18 @@ stories, learn & practice French written and spoken from your phone). `culture-agent-template` scaffold, renamed: an agent-first CLI skeleton, a mesh identity, a vendored skill kit, and a CI/publish baseline. Every verb (`whoami`, `learn`, `explain`, `overview`, `doctor`, `cli overview`) is generic -template introspection — no French domain code exists. The package docstrings -and `explain` catalog still describe the repo as "a clonable template for -AgentCulture mesh agents," which is accurate for the code but not for the -project's purpose. Expect to write the tutor domain from scratch, and to update -that self-description prose as you do. +introspection — no French domain code exists. `learn` and the `explain` root +entry now describe the tutor *intent*, but nothing behind them teaches French. +Expect to write the domain from scratch. Some prose still carries the template's +framing (`french/__init__.py`'s docstring, `overview`'s "sibling-pattern +artifacts" section); update it as the real surface lands. ## Commands ```bash uv sync # install (creates .venv) -uv run french whoami # run the CLI — note: `french`, not `french-cli` -uv run pytest -n auto # full suite (22 tests, ~1s) +uv run french whoami # run the CLI — the command is `french` +uv run pytest -n auto # full suite (23 tests, ~1s) uv run pytest tests/test_cli.py::test_whoami_text -v # a single test ``` @@ -44,37 +44,32 @@ CI's coverage invocation (`fail_under = 60`): uv run pytest -n auto --cov=french --cov-report=xml:coverage.xml --cov-report=term -v ``` -## Known breakage: the rubric gate fails on a clean checkout +## Naming: `french` is the command, `french-cli` is the package -`uv run teken cli doctor . --strict` **exits 1** today, which means the CI -`lint` job is red before you change anything: +Two names coexist **on purpose**. Don't "fix" one into the other. -```text -FAIL (error) explain_self: `explain french` exit=1 - hint: add an entry for 'french' (and the root) in the explain catalog -``` +| Thing | Value | +|-------|-------| +| Console script / argparse `prog` — what you type | `french` | +| Distribution, PyPI name, repo, mesh nick (`culture.yaml` `suffix`) | `french-cli` | +| Import package | `french` | + +The rule when you add a string: **invocations say `french`** (usage lines, +command maps, `explain` paths, help text); **identity says `french-cli`** +(`_FALLBACK_NICK`, `learn --json`'s `tool` field, `_pkg_version("french-cli")`, +the `overview` subject, the `doctor` report header). `learn --json` exposes both +— `tool` is the distribution, `command` is what to invoke. + +The agent-first rubric resolves the tool's own name from `[project.scripts]` +and requires `explain ` to resolve, so `french/explain/catalog.py` +keys its root entry on **both** `("french",)` and `("french-cli",)`. Dropping +the `("french",)` key turns the CI `lint` job red with +`FAIL explain_self: explain french exit=1` — that was a real defect, fixed in +0.4.1, and `test_explain_self_matches_console_script` now guards it. -The cause is an incomplete rename. Three different names are in play: - -| Thing | Value | Where | -|-------|-------|-------| -| Distribution / PyPI name | `french-cli` | `pyproject.toml` `[project] name` | -| Import package | `french` | `french/` | -| Console script | `french` | `pyproject.toml` `[project.scripts]` | -| argparse `prog` | `french-cli` | `french/cli/__init__.py` | - -The rubric derives the tool's own name from `[project.scripts]` (→ `french`) and -requires `explain ` to resolve, but `french/explain/catalog.py` keys -the root entry on `("french-cli",)`. Fix it by picking one name and committing -to it — either add a `("french",)` key to `ENTRIES`, or rename the console -script to `french-cli`. Whichever you choose, the argparse `prog`, the catalog -keys, the `_TEXT`/`_as_json_payload` blocks in `learn.py`, and every `uv run …` -line in `README.md` must agree. - -Related doc drift, worth fixing in the same pass: `README.md` documents -`uv run french-cli whoami` / `uv run french-cli learn` (no such script — it's -`uv run french`), and points at "the `git grep` discovery command in -`CLAUDE.md`" that has never existed in this file. +If you ever rename the console script, the argparse `prog`, the catalog keys, +`learn.py`'s hand-maintained `_TEXT` **and** `_as_json_payload`, and the README +must all move together. ## Architecture: the agent-first contract diff --git a/README.md b/README.md index 9d2a538..2267c1b 100644 --- a/README.md +++ b/README.md @@ -19,40 +19,40 @@ Agent + CLI that turns Claude into a private French tutor: track progress, get a ```bash uv sync uv run pytest -n auto # run the test suite -uv run french-cli whoami # identity from culture.yaml -uv run french-cli learn # self-teaching prompt (add --json) +uv run french whoami # identity from culture.yaml +uv run french learn # self-teaching prompt (add --json) uv run teken cli doctor . --strict # the agent-first rubric gate CI runs ``` +The installed command is **`french`**; the PyPI package and repo are +**`french-cli`**. `french explain french` and `french explain french-cli` both +resolve to the root doc entry. + ## CLI | Verb | What it does | |------|--------------| -| `whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. | -| `learn` | Print a structured self-teaching prompt. | -| `explain ` | Markdown docs for any noun/verb path. | -| `overview` | Read-only descriptive snapshot of the agent. | -| `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). | -| `cli overview` | Describe the CLI surface itself. | +| `french whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. | +| `french learn` | Print a structured self-teaching prompt. | +| `french explain ` | 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. -## Make it your own +## Status -1. Rename the package `french/` and the `french-cli` - CLI/dist name throughout `pyproject.toml`, the package, `tests/`, - `sonar-project.properties`, and this `README.md`. The name is hard-coded in - ~100 places, so list every occurrence first — see the `git grep` discovery - command in [`CLAUDE.md`](CLAUDE.md), the authoritative rename procedure. -2. Edit `culture.yaml` with your `suffix` and `backend`. -3. Rewrite `CLAUDE.md` for your agent and run `/init`. -4. Re-vendor only the skills you need from guildmaster (see - [`docs/skill-sources.md`](docs/skill-sources.md)). +**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. -See [`CLAUDE.md`](CLAUDE.md) for the full conventions (version-bump-every-PR, -the `cicd` PR lane, deploy setup). +See [`CLAUDE.md`](CLAUDE.md) for the architecture and the full conventions +(version-bump-every-PR, the `cicd` PR lane, deploy setup). ## License diff --git a/french/cli/__init__.py b/french/cli/__init__.py index 5d834e7..944dacf 100644 --- a/french/cli/__init__.py +++ b/french/cli/__init__.py @@ -69,9 +69,12 @@ def _build_parser() -> argparse.ArgumentParser: from french.cli._commands import overview as _overview_cmd from french.cli._commands import whoami as _whoami_cmd + # prog is the installed console script (`french`), not the distribution + # name (`french-cli`). The agent-first rubric resolves the tool's own name + # from [project.scripts] and requires `explain ` to work. parser = _CliArgumentParser( - prog="french-cli", - description="french-cli — a clonable template for AgentCulture mesh agents.", + prog="french", + description="french — a private French tutor (PyPI package: french-cli).", ) parser.add_argument( "--version", diff --git a/french/cli/_commands/cli.py b/french/cli/_commands/cli.py index cb95b1e..5fcead9 100644 --- a/french/cli/_commands/cli.py +++ b/french/cli/_commands/cli.py @@ -1,4 +1,4 @@ -"""``french-cli cli`` — noun grouping CLI-surface introspection. +"""``french cli`` — noun grouping CLI-surface introspection. Exists to satisfy the agent-first rubric's ``overview_cli_noun_exists`` check: any noun with action-verbs must also expose ``overview``. There are no @@ -23,14 +23,14 @@ def cmd_cli_overview(args: argparse.Namespace) -> int: def _no_verb(args: argparse.Namespace) -> int: - # `french-cli cli` with no sub-verb prints the noun's overview. + # `french cli` with no sub-verb prints the noun's overview. return cmd_cli_overview(args) def register(sub: argparse._SubParsersAction) -> None: p = sub.add_parser( "cli", - help="CLI-surface introspection (see 'french-cli cli overview').", + help="CLI-surface introspection (see 'french cli overview').", ) p.add_argument("--json", action="store_true", help="Emit structured JSON.") p.set_defaults(func=_no_verb, json=False) diff --git a/french/cli/_commands/doctor.py b/french/cli/_commands/doctor.py index ce902b3..9eb07b9 100644 --- a/french/cli/_commands/doctor.py +++ b/french/cli/_commands/doctor.py @@ -1,4 +1,4 @@ -"""``french-cli doctor`` — check the agent-identity invariants. +"""``french doctor`` — check the agent-identity invariants. Mirrors the two invariants ``steward doctor`` verifies for a mesh agent: diff --git a/french/cli/_commands/explain.py b/french/cli/_commands/explain.py index 83a8cf3..793d6f6 100644 --- a/french/cli/_commands/explain.py +++ b/french/cli/_commands/explain.py @@ -1,4 +1,4 @@ -"""``french-cli explain ...`` — global markdown catalog lookup (stable-contract). +"""``french explain ...`` — global markdown catalog lookup (stable-contract). ``explain`` is global (not nested under a noun). It takes zero or more path tokens and resolves them via the catalog in :mod:`french.explain`. @@ -32,7 +32,7 @@ def register(sub: argparse._SubParsersAction) -> None: p.add_argument( "path", nargs="*", - help="Command path tokens; empty = root (same as 'french-cli').", + help="Command path tokens; empty = root (same as 'french').", ) p.add_argument("--json", action="store_true", help="Emit structured JSON.") p.set_defaults(func=cmd_explain) diff --git a/french/cli/_commands/learn.py b/french/cli/_commands/learn.py index 72ebd2c..f40673a 100644 --- a/french/cli/_commands/learn.py +++ b/french/cli/_commands/learn.py @@ -1,4 +1,4 @@ -"""``french-cli learn`` — the learnability affordance. +"""``french learn`` — the learnability affordance. Prints a structured self-teaching prompt. Must satisfy the agent-first rubric: >=200 chars and mention purpose, command map, exit codes, --json, and explain. @@ -12,23 +12,24 @@ from french.cli._output import emit_result _TEXT = """\ -french-cli — a clonable template for AgentCulture mesh agents. +french — a private French tutor (PyPI package: french-cli). Purpose ------- -Scaffold for a new Culture mesh agent: an agent-first CLI (cited from the teken -`python-cli` reference), an identity (culture.yaml + CLAUDE.md), the canonical -guildmaster skill kit under .claude/skills/, and a deploy/CI baseline. Clone it, -rename the package, and edit culture.yaml to mint a new agent. +Track progress, get an overview, get advice, read stories, and learn & practice +French (written and spoken) online from your phone. Built on an agent-first CLI +(cited from the teken `python-cli` reference) with a Culture mesh identity +(culture.yaml + AGENTS.colleague.md) and the guildmaster skill kit under +.claude/skills/. Commands -------- - french-cli whoami Identity from culture.yaml. - french-cli learn This self-teaching prompt. - french-cli explain ... Markdown docs for any noun/verb path. - french-cli overview Descriptive snapshot of the agent. - french-cli doctor Check the agent-identity invariants. - french-cli cli overview Describe the CLI surface itself. + french whoami Identity from culture.yaml. + french learn This self-teaching prompt. + french explain ... Markdown docs for any noun/verb path. + french overview Descriptive snapshot of the agent. + french doctor Check the agent-identity invariants. + french cli overview Describe the CLI surface itself. Machine-readable output ----------------------- @@ -44,15 +45,18 @@ More detail ----------- - french-cli explain french-cli + french explain french """ def _as_json_payload() -> dict[str, object]: return { + # `tool` is the distribution / mesh nick; `command` is what you invoke. "tool": "french-cli", + "command": "french", "version": __version__, - "purpose": "Clonable scaffold for a new AgentCulture mesh agent.", + "purpose": "A private French tutor: track progress, read stories, and " + "practice French written and spoken.", "commands": [ {"path": ["whoami"], "summary": "Identity probe from culture.yaml."}, {"path": ["learn"], "summary": "Self-teaching prompt."}, @@ -67,7 +71,7 @@ def _as_json_payload() -> dict[str, object]: "2": "environment/setup error", }, "json_support": True, - "explain_pointer": "french-cli explain ", + "explain_pointer": "french explain ", } diff --git a/french/cli/_commands/overview.py b/french/cli/_commands/overview.py index 15da735..a82c6fa 100644 --- a/french/cli/_commands/overview.py +++ b/french/cli/_commands/overview.py @@ -1,4 +1,4 @@ -"""``french-cli overview`` — read-only descriptive snapshot of the agent. +"""``french overview`` — read-only descriptive snapshot of the agent. Describes the agent to an agent reader: identity (from culture.yaml), the verb surface, and the sibling-pattern artifacts this template carries. The shared diff --git a/french/cli/_commands/whoami.py b/french/cli/_commands/whoami.py index d9aab66..4268189 100644 --- a/french/cli/_commands/whoami.py +++ b/french/cli/_commands/whoami.py @@ -1,4 +1,4 @@ -"""``french-cli whoami`` — the smallest identity probe. +"""``french whoami`` — the smallest identity probe. Reports the agent's identity as declared in ``culture.yaml``: its nick (``suffix``), the backend it runs on, and the served model (if any) — plus the diff --git a/french/cli/_errors.py b/french/cli/_errors.py index 52af946..5ffeb3d 100644 --- a/french/cli/_errors.py +++ b/french/cli/_errors.py @@ -13,7 +13,7 @@ from dataclasses import dataclass -# Exit-code policy. Documented in ``french-cli learn`` output. +# Exit-code policy. Documented in ``french learn`` output. # 0 = success # 1 = user-input error (bad flag, missing required arg, unknown path) # 2 = environment / setup error (tool not installed, file unreadable) diff --git a/french/explain/__init__.py b/french/explain/__init__.py index fdd6f48..23fd7a2 100644 --- a/french/explain/__init__.py +++ b/french/explain/__init__.py @@ -16,7 +16,7 @@ def resolve(path: tuple[str, ...]) -> str: raise CliError( code=EXIT_USER_ERROR, message=f"no explain entry for: {display}", - remediation="list entries with: french-cli explain french-cli", + remediation="list entries with: french explain french", ) diff --git a/french/explain/catalog.py b/french/explain/catalog.py index eae7d47..e6b8837 100644 --- a/french/explain/catalog.py +++ b/french/explain/catalog.py @@ -1,7 +1,11 @@ -"""Markdown catalog for ``french-cli explain ``. +"""Markdown catalog for ``french explain ``. Each entry is verbatim markdown. Keys are command-path tuples. The empty tuple -and ``("french-cli",)`` both resolve to the root entry. +and both spellings of the tool's name — ``("french",)`` (the installed console +script) and ``("french-cli",)`` (the distribution / repo / mesh nick) — resolve +to the root entry. The agent-first rubric resolves the tool's own name from +``[project.scripts]`` and requires ``explain `` to work; the +distribution alias is kept because that is the name on PyPI and in the mesh. Keep bodies self-contained: an agent reading one entry should get enough context without chaining reads. @@ -10,22 +14,23 @@ from __future__ import annotations _ROOT = """\ -# french-cli +# french -A clonable template for AgentCulture mesh agents. It carries an agent-first CLI -(cited from the teken `python-cli` reference), a mesh identity (`culture.yaml` + -`CLAUDE.md`), the canonical guildmaster skill kit under `.claude/skills/`, and a -buildable/deployable package baseline. Clone it, rename the package, edit -`culture.yaml`, and you have a new agent. +A private French tutor: track progress, get an overview, get advice, read +stories, and learn & practice French (written and spoken) online from your +phone. + +Installed command: `french`. PyPI package and repo: `french-cli`. Mesh nick: +`french-cli`. ## Verbs -- `french-cli whoami` — identity probe from `culture.yaml`. -- `french-cli learn` — structured self-teaching prompt. -- `french-cli explain ` — markdown docs for any noun/verb. -- `french-cli overview` — descriptive snapshot of the agent. -- `french-cli doctor` — check the agent-identity invariants. -- `french-cli cli overview` — describe the CLI surface. +- `french whoami` — identity probe from `culture.yaml`. +- `french learn` — structured self-teaching prompt. +- `french explain ` — markdown docs for any noun/verb. +- `french overview` — descriptive snapshot of the agent. +- `french doctor` — check the agent-identity invariants. +- `french cli overview` — describe the CLI surface. ## Exit-code policy @@ -36,49 +41,52 @@ ## See also -- `french-cli explain whoami` -- `french-cli explain doctor` +- `french explain whoami` +- `french explain doctor` """ _WHOAMI = """\ -# french-cli whoami +# french whoami Reports the agent's identity from `culture.yaml`: nick (`suffix`), backend, served model, and the package version. Read-only. ## Usage - french-cli whoami - french-cli whoami --json + french whoami + french whoami --json """ _LEARN = """\ -# french-cli learn +# french learn Prints a structured self-teaching prompt covering purpose, command map, exit-code policy, `--json` support, and the `explain` pointer. ## Usage - french-cli learn - french-cli learn --json + french learn + french learn --json """ _EXPLAIN = """\ -# french-cli explain +# french explain Prints markdown documentation for any noun/verb path. Unlike `--help` (terse, positional), `explain` is global and addressable by path. +Both `french explain french` and `french explain french-cli` resolve to this +root entry — the command is `french`, the distribution is `french-cli`. + ## Usage - french-cli explain french-cli - french-cli explain whoami - french-cli explain --json + french explain french + french explain whoami + french explain --json """ _OVERVIEW = """\ -# french-cli overview +# french overview Read-only descriptive snapshot of the agent: identity (from `culture.yaml`), the verb surface, and the sibling-pattern artifacts the template carries. Accepts an @@ -86,12 +94,12 @@ ## Usage - french-cli overview - french-cli overview --json + french overview + french overview --json """ _DOCTOR = """\ -# french-cli doctor +# french doctor Checks the agent-identity invariants `steward doctor` verifies: prompt-file-present and backend-consistency (`colleague` → `AGENTS.colleague.md`), plus a @@ -99,25 +107,27 @@ ## Usage - french-cli doctor - french-cli doctor --json + french doctor + french doctor --json """ _CLI = """\ -# french-cli cli +# french cli Noun group for CLI-surface introspection. `cli overview` describes the CLI itself (distinct from the global `overview`, which describes the agent). ## Usage - french-cli cli overview - french-cli cli overview --json + french cli overview + french cli overview --json """ ENTRIES: dict[tuple[str, ...], str] = { (): _ROOT, + # Both the console script and the distribution name resolve to the root. + ("french",): _ROOT, ("french-cli",): _ROOT, ("whoami",): _WHOAMI, ("learn",): _LEARN, diff --git a/tests/test_cli.py b/tests/test_cli.py index 30a123d..4fa8c9e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,7 +21,7 @@ def test_version_flag(capsys: pytest.CaptureFixture[str]) -> None: def test_no_args_prints_help(capsys: pytest.CaptureFixture[str]) -> None: rc = main([]) assert rc == 0 - assert "usage: french-cli" in capsys.readouterr().out + assert "usage: french" in capsys.readouterr().out def test_unknown_command_errors(capsys: pytest.CaptureFixture[str]) -> None: @@ -72,7 +72,9 @@ def test_learn_json(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["learn", "--json"]) assert rc == 0 payload = json.loads(capsys.readouterr().out) + # `tool` is the distribution / mesh nick; `command` is what you invoke. assert payload["tool"] == "french-cli" + assert payload["command"] == "french" assert payload["version"] == __version__ assert payload["json_support"] is True @@ -83,13 +85,32 @@ def test_learn_json(capsys: pytest.CaptureFixture[str]) -> None: def test_explain_root(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["explain"]) assert rc == 0 - assert "# french-cli" in capsys.readouterr().out + out = capsys.readouterr().out + # Titled by the command; names the distribution so both are discoverable. + assert out.startswith("# french\n") + assert "french-cli" in out def test_explain_self(capsys: pytest.CaptureFixture[str]) -> None: - rc = main(["explain", "french-cli"]) - assert rc == 0 - assert capsys.readouterr().out.startswith("#") + # The agent-first rubric resolves the tool's name from [project.scripts] + # and requires `explain ` to work. The console script is + # `french`; `french-cli` is the distribution / mesh nick. Both resolve. + for name in ("french", "french-cli"): + rc = main(["explain", name]) + assert rc == 0, f"explain {name} failed" + assert capsys.readouterr().out.startswith("#") + + +def test_explain_self_matches_console_script() -> None: + """`explain ` must resolve — this is what the rubric checks. + + Guards the defect that made `teken cli doctor --strict` fail: the catalog + keyed its root on the distribution name while the script was named `french`. + """ + from french.explain import known_paths + + assert ("french",) in known_paths() + assert ("french-cli",) in known_paths() def test_explain_json(capsys: pytest.CaptureFixture[str]) -> None: @@ -97,7 +118,7 @@ def test_explain_json(capsys: pytest.CaptureFixture[str]) -> None: assert rc == 0 payload = json.loads(capsys.readouterr().out) assert payload["path"] == ["whoami"] - assert "french-cli whoami" in payload["markdown"] + assert "french whoami" in payload["markdown"] def test_explain_unknown_path_errors(capsys: pytest.CaptureFixture[str]) -> None: