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
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,71 @@ 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.

- **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 <that name>` 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
blocks into alphabetical order.

## [0.4.0] - 2026-06-23

### Added
Expand Down
217 changes: 198 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,207 @@
# 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
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.

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 — 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
```

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
```

## Naming: `french` is the command, `french-cli` is the package

Two names coexist **on purpose**. Don't "fix" one into the other.

| 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 <that name>` 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.

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

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: <message>` + `hint: <remediation>` 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/<verb>.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<run_number>` build to TestPyPI;
pushes to `main` publish to PyPI. Both use Trusted Publishing (OIDC), so fork
PRs skip the publish step rather than failing.
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` | 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 <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.

## 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

Expand Down
7 changes: 5 additions & 2 deletions french/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <that name>` 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",
Expand Down
Loading
Loading