diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d381e7..c29d051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ 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-10 + +### Added + +- `explain spanish` resolves the console-script name, satisfying the agent-first rubric's `explain_self` check; `explain spanish-cli` is kept as an alias for the distribution name. +- A full runtime `CLAUDE.md` (replaces the self-initializing seed): the output/error contract, the `register(sub)` extension path, the backend/prompt-file coupling, and the version-bump-every-PR rule. + +### Changed + +- `spanish` is now the canonical CLI name. argparse `prog`, the `learn` command map and `tool` field, every explain-catalog body, and all `hint:` remediations say `spanish`. `spanish-cli` is retained only as the distribution name, the issues URL, and the mesh nick (`culture.yaml` `suffix`). + +### Fixed + +- `uv run teken cli doctor . --strict` (the rubric gate CI's lint job runs) failed `explain_self` at 25/26 — the catalog knew `spanish-cli` but the rubric probes the console-script name `spanish`. Now 26/26. +- README quickstart advertised `uv run spanish-cli whoami`, which exits 2 with `Failed to spawn`; the console script is `spanish`. +- `uv.lock` pinned the workspace package at 0.3.4 while `pyproject.toml` declared 0.4.0. +- README `Make it your own` cited a rename procedure in the (now replaced) seed `CLAUDE.md`. + ## [0.4.0] - 2026-06-23 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 2fa03f3..202690a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,28 +1,85 @@ -# 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 **spanish-cli** agent. +`spanish-cli` is intended to be an agent + CLI that turns Claude into a private Spanish tutor (track progress, overview, advice, stories, written and spoken practice from a phone). -## Description +**None of that exists yet.** The repo was scaffolded from `culture-agent-template` (commit `ae97e2c`) and the tutor domain has not been built. What is here is the template's *agent-first CLI skeleton*: six introspection verbs (`whoami`, `learn`, `explain`, `overview`, `doctor`, `cli overview`), a mesh identity, a vendored skill kit, and a CI/publish baseline. The runtime package has **zero third-party dependencies** — keep it that way unless there's a reason not to. -Agent + CLI that turns Claude into a private Spanish tutor: track progress, get an overview, get advice, read stories, and learn & practice Spanish (written and spoken) online from your phone. +Consequence: the CLI still describes itself as "a clonable template for AgentCulture mesh agents" in `spanish/cli/__init__.py`, `spanish/cli/_commands/learn.py`, `spanish/cli/_commands/overview.py`, and `spanish/explain/catalog.py`. When you implement tutor features, rewrite that prose in the same pass — `learn` and `explain` are the agent-facing docs, not decoration. -## Re-init instruction +## Commands -This file is a seed. To expand it into your full runtime prompt: +```bash +uv sync # create .venv, install dev group -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. +uv run pytest -n auto # full suite (~1s) +uv run pytest tests/test_cli.py::test_whoami_json -v # a single test +bash .claude/skills/run-tests/scripts/test.sh --ci # exactly what CI runs -Until you run `/init`, `spanish-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. +uv run black --check spanish tests # the four lint gates, in CI order +uv run isort --check-only spanish tests +uv run flake8 spanish tests +uv run bandit -c pyproject.toml -r spanish + +uv run teken cli doctor . --strict # the agent-first rubric gate (see below) +markdownlint-cli2 "**/*.md" "#node_modules" "#.local" "#.claude/skills" +``` + +Line length is 100 everywhere (black, isort, flake8). Coverage `fail_under = 60`. + +### Three names, one tool + +The command is **`spanish`**, the distribution on PyPI is **`spanish-cli`**, and the mesh nick (`culture.yaml`'s `suffix`) is also **`spanish-cli`**. `spanish` is canonical for anything user- or agent-facing: argparse's `prog`, every string in `learn` and the explain catalog, every `hint:` remediation. Reserve `spanish-cli` for three things — `importlib.metadata.version("spanish-cli")` in `spanish/__init__.py`, the issues URL, and `_FALLBACK_NICK` in `whoami.py`. `explain spanish-cli` resolves to the root entry as an alias, nothing more. + +## The agent-first rubric gate + +CI's `lint` job runs `uv run teken cli doctor . --strict`, a 26-check rubric from [teken](https://github.com/agentculture/teken) that the CLI's whole shape exists to satisfy. It passes 26/26; keep it that way. + +The check that constrains naming is `explain_self`: the rubric shells out to `explain `, so `ENTRIES` in `spanish/explain/catalog.py` must contain a key matching whatever `[project.scripts]` declares. Rename the script and you must add the matching catalog key in the same commit. + +The rubric is also why the CLI has verbs that look redundant: `learn` must be ≥200 chars and name purpose, commands, exit codes, `--json`, and `explain`; `overview` must not hard-fail on a bogus target path (hence the ignored `target` positional); the `cli` noun exists solely so `cli overview` satisfies `overview_cli_noun_exists`; `doctor` must emit `{healthy, checks: [{id, passed, severity, message, remediation}]}`. Don't "simplify" these away — run the gate first. + +## Architecture + +### Output and error contract (stable — agents parse this) + +Three modules define a contract every command obeys: + +- `spanish/cli/_errors.py` — `CliError(code, message, remediation)`. Exit policy: `0` success, `1` user error, `2` environment error, `3+` reserved. Every failure raises `CliError`; nothing else. +- `spanish/cli/_output.py` — **results to stdout, errors and diagnostics to stderr, never mixed.** `--json` routes structured payloads to the same streams. +- `spanish/cli/__init__.py` — `_dispatch()` catches `CliError`, and wraps any other exception into one, so **no Python traceback ever reaches stderr**. Text-mode errors render as `error: ` + `hint: `; the `hint:` prefix is what the rubric greps for. + +Two subtleties live in `__init__.py`. `_CliArgumentParser` overrides `.error()` so argparse's own failures (unknown verb, bad flag) exit `1` through the structured format instead of argparse's default exit `2`. And because parse-time errors happen before `args.json` exists, `main()` scans raw argv for `--json` and stashes it on the class attribute `_json_hint` before `parse_args`. Any subparser you create must inherit `_CliArgumentParser` (pass `parser_class=type(p)`, as `_commands/cli.py` does) or its parse errors bypass the contract. + +### Adding a noun or verb + +Each module in `spanish/cli/_commands/` exposes `register(sub)`. To add one: + +1. Write the module with `register(sub)` + a `cmd_*(args) -> int | None` handler taking `--json`. +2. Call its `register()` in `_build_parser()` (there's a marked comment). +3. Add a catalog entry in `spanish/explain/catalog.py` — `ENTRIES` is keyed by command-path tuples, e.g. `("cli", "overview")`. `test_every_catalog_path_resolves` iterates every key, and the docstring convention is that every registered path has an entry. +4. Update the `_TEXT` and `_as_json_payload()` command maps in `_commands/learn.py`, and `_VERBS` in `_commands/overview.py`. Nothing enforces this automatically — the rubric only checks `learn`'s markers, not that it's complete. + +### Identity: `culture.yaml` and the backend/prompt-file coupling + +`culture.yaml` declares `suffix: spanish-cli`, `backend: colleague`, `model: sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP`. Two things follow: + +- **`AGENTS.colleague.md` is this agent's resident prompt file, not `CLAUDE.md`.** `doctor` enforces the mapping `claude → CLAUDE.md`, `colleague → AGENTS.colleague.md`, `acp → AGENTS.md`, `gemini → GEMINI.md`. `test_doctor_recognizes_declared_backend` asserts `doctor` stays healthy against whatever `culture.yaml` declares, so changing `backend` without teaching `_PROMPT_FILE` the new mapping fails the suite. (The seed CLAUDE.md this file replaced claimed `backend: claude` — it was wrong.) +- **`whoami` parses `culture.yaml` by hand**, line by line, to preserve the zero-dependency runtime (`_commands/whoami.py`). It walks up from `__file__`, not from the CWD, so it always reports *this* agent's identity rather than whatever `culture.yaml` sits in the caller's directory. In a wheel install no `culture.yaml` ships, and both `whoami` and `doctor` degrade to defaults / a single info check. + +## Conventions + +**Every PR bumps the version — including docs-only, config-only, and CI-only PRs.** The `version-check` CI job compares `pyproject.toml` against `origin/main` and fails the PR (with a bot comment) if they match. Use the skill: `python3 .claude/skills/version-bump/scripts/bump.py patch|minor|major`, optionally piping `{"added":[...],"changed":[...],"fixed":[...]}` on stdin to fill `CHANGELOG.md`. `__version__` reads from package metadata, so there's no `__init__.py` literal to update. + +**PRs go through the `cicd` skill** (`.claude/skills/cicd/scripts/workflow.sh`), which wraps `devex pr` — `lint | open | read | reply | delta`, plus its own `status` and `await` (SonarCloud quality gate + unresolved-thread tally, non-zero exit on a red gate). Requires `devex` >= 0.21, `gh`, and `jq` on PATH. + +**`.claude/skills/` is vendored cite-don't-import from [guildmaster](https://github.com/agentculture/guildmaster)** (`ask-colleague` comes directly from `colleague`; `remember` and `recall` from `eidetic-cli`). Don't edit or reformat those scripts as part of unrelated work — markdownlint ignores the tree, Sonar excludes it, and `docs/skill-sources.md` is the provenance ledger with the re-sync procedure and its two tracked divergences (`agex` → `devex`, `outsource` → `ask-colleague`). If a change belongs upstream, lift it to guildmaster and re-vendor. + +**Publishing** is PyPI Trusted Publishing via OIDC. `publish.yml` fires on changes to `pyproject.toml` or `spanish/**`: same-repo PRs publish a `.dev` build to TestPyPI, pushes to `main` publish to PyPI. Fork PRs skip it. + +## Sonar and CI notes + +`sonar-project.properties` pins `sonar.sources=spanish` and reads `coverage.xml`; `relative_files = true` in `[tool.coverage.run]` is load-bearing, since absolute or `.venv` paths make SonarCloud report empty coverage. The scan step is guarded by `if: env.SONAR_TOKEN != ''`, so token-less and fork-PR runs stay green without it. diff --git a/README.md b/README.md index d42ca3d..a8e8167 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Agent + CLI that turns Claude into a private Spanish tutor: track progress, get - **A mesh identity** — `culture.yaml` (`suffix` + `backend`) and the matching resident prompt file (`AGENTS.colleague.md`, since this template runs `backend: colleague`). -- **The canonical guildmaster skill kit** (11 skills) under `.claude/skills/`, +- **The canonical guildmaster skill kit** under `.claude/skills/`, vendored cite-don't-import. See [`docs/skill-sources.md`](docs/skill-sources.md). - **A build + deploy baseline** — pytest, lint, the agent-first rubric gate, and PyPI Trusted Publishing wired into GitHub Actions. @@ -19,11 +19,14 @@ Agent + CLI that turns Claude into a private Spanish tutor: track progress, get ```bash uv sync uv run pytest -n auto # run the test suite -uv run spanish-cli whoami # identity from culture.yaml -uv run spanish-cli learn # self-teaching prompt (add --json) +uv run spanish whoami # identity from culture.yaml +uv run spanish learn # self-teaching prompt (add --json) uv run teken cli doctor . --strict # the agent-first rubric gate CI runs ``` +The command is `spanish`; `spanish-cli` is the distribution name on PyPI and the +name of this repo. + ## CLI | Verb | What it does | @@ -39,20 +42,16 @@ 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 `spanish/` and the `spanish-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)). +Scaffolded from `culture-agent-template`; the Spanish-tutor domain is not built +yet. What ships today is the template's agent-first CLI skeleton — the six +introspection verbs above — so the self-describing output (`learn`, `explain`) +still calls itself a template. That prose gets rewritten alongside the first +tutor feature. -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 conventions (version-bump-every-PR, the +`cicd` PR lane, the agent-first rubric gate, deploy setup). ## License diff --git a/pyproject.toml b/pyproject.toml index ea1067d..6c89f76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "spanish-cli" -version = "0.4.0" +version = "0.5.0" description = "Agent + CLI that turns Claude into a private Spanish tutor: track progress, get an overview, get advice, read stories, and learn & practice Spanish (written and spoken) online from your phone." readme = "README.md" license = "Apache-2.0" diff --git a/spanish/cli/__init__.py b/spanish/cli/__init__.py index e6b3bab..696ffb7 100644 --- a/spanish/cli/__init__.py +++ b/spanish/cli/__init__.py @@ -70,8 +70,8 @@ def _build_parser() -> argparse.ArgumentParser: from spanish.cli._commands import whoami as _whoami_cmd parser = _CliArgumentParser( - prog="spanish-cli", - description="spanish-cli — a clonable template for AgentCulture mesh agents.", + prog="spanish", + description="spanish — a clonable template for AgentCulture mesh agents.", ) parser.add_argument( "--version", diff --git a/spanish/cli/_commands/cli.py b/spanish/cli/_commands/cli.py index 2af1338..51beb37 100644 --- a/spanish/cli/_commands/cli.py +++ b/spanish/cli/_commands/cli.py @@ -1,4 +1,4 @@ -"""``spanish-cli cli`` — noun grouping CLI-surface introspection. +"""``spanish 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 @@ -15,7 +15,7 @@ def cmd_cli_overview(args: argparse.Namespace) -> int: emit_overview( - "spanish-cli cli", + "spanish cli", cli_sections(), json_mode=bool(getattr(args, "json", False)), ) @@ -23,14 +23,14 @@ def cmd_cli_overview(args: argparse.Namespace) -> int: def _no_verb(args: argparse.Namespace) -> int: - # `spanish-cli cli` with no sub-verb prints the noun's overview. + # `spanish 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 'spanish-cli cli overview').", + help="CLI-surface introspection (see 'spanish cli overview').", ) p.add_argument("--json", action="store_true", help="Emit structured JSON.") p.set_defaults(func=_no_verb, json=False) @@ -38,6 +38,6 @@ def register(sub: argparse._SubParsersAction) -> None: # parser_class); propagate it so `cli overview` parse errors route through # the structured error contract instead of argparse's default stderr/exit 2. noun_sub = p.add_subparsers(dest="cli_command", parser_class=type(p)) - ov = noun_sub.add_parser("overview", help="Describe the spanish-cli CLI surface.") + ov = noun_sub.add_parser("overview", help="Describe the spanish CLI surface.") ov.add_argument("--json", action="store_true", help="Emit structured JSON.") ov.set_defaults(func=cmd_cli_overview) diff --git a/spanish/cli/_commands/doctor.py b/spanish/cli/_commands/doctor.py index 76cd95a..9e147ad 100644 --- a/spanish/cli/_commands/doctor.py +++ b/spanish/cli/_commands/doctor.py @@ -1,4 +1,4 @@ -"""``spanish-cli doctor`` — check the agent-identity invariants. +"""``spanish doctor`` — check the agent-identity invariants. Mirrors the two invariants ``steward doctor`` verifies for a mesh agent: @@ -105,7 +105,7 @@ def cmd_doctor(args: argparse.Namespace) -> int: emit_result(report, json_mode=True) else: status = "healthy" if report["healthy"] else "unhealthy" - lines = [f"spanish-cli doctor: {status}", ""] + lines = [f"spanish doctor: {status}", ""] for check in report["checks"]: mark = "ok" if check["passed"] else "FAIL" lines.append(f"[{mark}] {check['id']}: {check['message']}") diff --git a/spanish/cli/_commands/explain.py b/spanish/cli/_commands/explain.py index 3dc9d3b..f478fb9 100644 --- a/spanish/cli/_commands/explain.py +++ b/spanish/cli/_commands/explain.py @@ -1,4 +1,4 @@ -"""``spanish-cli explain ...`` — global markdown catalog lookup (stable-contract). +"""``spanish 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:`spanish.explain`. @@ -32,7 +32,7 @@ def register(sub: argparse._SubParsersAction) -> None: p.add_argument( "path", nargs="*", - help="Command path tokens; empty = root (same as 'spanish-cli').", + help="Command path tokens; empty = root (same as 'spanish').", ) p.add_argument("--json", action="store_true", help="Emit structured JSON.") p.set_defaults(func=cmd_explain) diff --git a/spanish/cli/_commands/learn.py b/spanish/cli/_commands/learn.py index 83eb44e..40dc363 100644 --- a/spanish/cli/_commands/learn.py +++ b/spanish/cli/_commands/learn.py @@ -1,4 +1,4 @@ -"""``spanish-cli learn`` — the learnability affordance. +"""``spanish 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,7 +12,7 @@ from spanish.cli._output import emit_result _TEXT = """\ -spanish-cli — a clonable template for AgentCulture mesh agents. +spanish — a clonable template for AgentCulture mesh agents. Purpose ------- @@ -23,12 +23,12 @@ Commands -------- - spanish-cli whoami Identity from culture.yaml. - spanish-cli learn This self-teaching prompt. - spanish-cli explain ... Markdown docs for any noun/verb path. - spanish-cli overview Descriptive snapshot of the agent. - spanish-cli doctor Check the agent-identity invariants. - spanish-cli cli overview Describe the CLI surface itself. + spanish whoami Identity from culture.yaml. + spanish learn This self-teaching prompt. + spanish explain ... Markdown docs for any noun/verb path. + spanish overview Descriptive snapshot of the agent. + spanish doctor Check the agent-identity invariants. + spanish cli overview Describe the CLI surface itself. Machine-readable output ----------------------- @@ -44,13 +44,13 @@ More detail ----------- - spanish-cli explain spanish-cli + spanish explain spanish """ def _as_json_payload() -> dict[str, object]: return { - "tool": "spanish-cli", + "tool": "spanish", "version": __version__, "purpose": "Clonable scaffold for a new AgentCulture mesh agent.", "commands": [ @@ -67,7 +67,7 @@ def _as_json_payload() -> dict[str, object]: "2": "environment/setup error", }, "json_support": True, - "explain_pointer": "spanish-cli explain ", + "explain_pointer": "spanish explain ", } diff --git a/spanish/cli/_commands/overview.py b/spanish/cli/_commands/overview.py index fc7d70a..b47fced 100644 --- a/spanish/cli/_commands/overview.py +++ b/spanish/cli/_commands/overview.py @@ -1,4 +1,4 @@ -"""``spanish-cli overview`` — read-only descriptive snapshot of the agent. +"""``spanish 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 @@ -90,7 +90,7 @@ def cmd_overview(args: argparse.Namespace) -> int: # `target` is accepted for rubric compatibility (descriptive verbs must not # hard-fail on a missing path) but overview describes this agent itself. emit_overview( - "spanish-cli", + "spanish", agent_sections(), json_mode=bool(getattr(args, "json", False)), ) diff --git a/spanish/cli/_commands/whoami.py b/spanish/cli/_commands/whoami.py index 85f5553..09e5f28 100644 --- a/spanish/cli/_commands/whoami.py +++ b/spanish/cli/_commands/whoami.py @@ -1,4 +1,4 @@ -"""``spanish-cli whoami`` — the smallest identity probe. +"""``spanish 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/spanish/cli/_errors.py b/spanish/cli/_errors.py index e2db4c0..c73aad4 100644 --- a/spanish/cli/_errors.py +++ b/spanish/cli/_errors.py @@ -13,7 +13,7 @@ from dataclasses import dataclass -# Exit-code policy. Documented in ``spanish-cli learn`` output. +# Exit-code policy. Documented in ``spanish 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/spanish/explain/__init__.py b/spanish/explain/__init__.py index c2b83bb..fd03ad0 100644 --- a/spanish/explain/__init__.py +++ b/spanish/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: spanish-cli explain spanish-cli", + remediation="list entries with: spanish explain spanish", ) diff --git a/spanish/explain/catalog.py b/spanish/explain/catalog.py index 6c32dd7..4c027be 100644 --- a/spanish/explain/catalog.py +++ b/spanish/explain/catalog.py @@ -1,7 +1,8 @@ -"""Markdown catalog for ``spanish-cli explain ``. +"""Markdown catalog for ``spanish explain ``. Each entry is verbatim markdown. Keys are command-path tuples. The empty tuple -and ``("spanish-cli",)`` both resolve to the root entry. +and ``("spanish",)`` both resolve to the root entry; ``("spanish-cli",)`` is +kept as an alias for the distribution name. Keep bodies self-contained: an agent reading one entry should get enough context without chaining reads. @@ -10,7 +11,7 @@ from __future__ import annotations _ROOT = """\ -# spanish-cli +# spanish 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` + @@ -20,12 +21,12 @@ ## Verbs -- `spanish-cli whoami` — identity probe from `culture.yaml`. -- `spanish-cli learn` — structured self-teaching prompt. -- `spanish-cli explain ` — markdown docs for any noun/verb. -- `spanish-cli overview` — descriptive snapshot of the agent. -- `spanish-cli doctor` — check the agent-identity invariants. -- `spanish-cli cli overview` — describe the CLI surface. +- `spanish whoami` — identity probe from `culture.yaml`. +- `spanish learn` — structured self-teaching prompt. +- `spanish explain ` — markdown docs for any noun/verb. +- `spanish overview` — descriptive snapshot of the agent. +- `spanish doctor` — check the agent-identity invariants. +- `spanish cli overview` — describe the CLI surface. ## Exit-code policy @@ -36,49 +37,49 @@ ## See also -- `spanish-cli explain whoami` -- `spanish-cli explain doctor` +- `spanish explain whoami` +- `spanish explain doctor` """ _WHOAMI = """\ -# spanish-cli whoami +# spanish whoami Reports the agent's identity from `culture.yaml`: nick (`suffix`), backend, served model, and the package version. Read-only. ## Usage - spanish-cli whoami - spanish-cli whoami --json + spanish whoami + spanish whoami --json """ _LEARN = """\ -# spanish-cli learn +# spanish learn Prints a structured self-teaching prompt covering purpose, command map, exit-code policy, `--json` support, and the `explain` pointer. ## Usage - spanish-cli learn - spanish-cli learn --json + spanish learn + spanish learn --json """ _EXPLAIN = """\ -# spanish-cli explain +# spanish explain Prints markdown documentation for any noun/verb path. Unlike `--help` (terse, positional), `explain` is global and addressable by path. ## Usage - spanish-cli explain spanish-cli - spanish-cli explain whoami - spanish-cli explain --json + spanish explain spanish + spanish explain whoami + spanish explain --json """ _OVERVIEW = """\ -# spanish-cli overview +# spanish 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 +87,12 @@ ## Usage - spanish-cli overview - spanish-cli overview --json + spanish overview + spanish overview --json """ _DOCTOR = """\ -# spanish-cli doctor +# spanish doctor Checks the agent-identity invariants `steward doctor` verifies: prompt-file-present and backend-consistency (`colleague` → `AGENTS.colleague.md`), plus a @@ -99,25 +100,29 @@ ## Usage - spanish-cli doctor - spanish-cli doctor --json + spanish doctor + spanish doctor --json """ _CLI = """\ -# spanish-cli cli +# spanish cli Noun group for CLI-surface introspection. `cli overview` describes the CLI itself (distinct from the global `overview`, which describes the agent). ## Usage - spanish-cli cli overview - spanish-cli cli overview --json + spanish cli overview + spanish cli overview --json """ ENTRIES: dict[tuple[str, ...], str] = { (): _ROOT, + # The console script name is the canonical self-name: the agent-first + # rubric's `explain_self` check probes `explain `. + ("spanish",): _ROOT, + # Alias for the distribution/repo name, so `explain spanish-cli` resolves. ("spanish-cli",): _ROOT, ("whoami",): _WHOAMI, ("learn",): _LEARN, diff --git a/tests/test_cli.py b/tests/test_cli.py index 1942bd6..605ce00 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: spanish-cli" in capsys.readouterr().out + assert "usage: spanish" in capsys.readouterr().out def test_unknown_command_errors(capsys: pytest.CaptureFixture[str]) -> None: @@ -62,7 +62,7 @@ def test_learn_text(capsys: pytest.CaptureFixture[str]) -> None: assert rc == 0 out = capsys.readouterr().out assert len(out) >= 200 - assert "spanish-cli" in out + assert "spanish whoami" in out assert "Exit-code policy" in out assert "--json" in out assert "explain" in out @@ -72,7 +72,7 @@ def test_learn_json(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["learn", "--json"]) assert rc == 0 payload = json.loads(capsys.readouterr().out) - assert payload["tool"] == "spanish-cli" + assert payload["tool"] == "spanish" assert payload["version"] == __version__ assert payload["json_support"] is True @@ -83,21 +83,29 @@ 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 "# spanish-cli" in capsys.readouterr().out + assert "# spanish" in capsys.readouterr().out def test_explain_self(capsys: pytest.CaptureFixture[str]) -> None: - rc = main(["explain", "spanish-cli"]) + """The rubric's explain_self check probes the console-script name.""" + rc = main(["explain", "spanish"]) assert rc == 0 assert capsys.readouterr().out.startswith("#") +def test_explain_dist_name_alias(capsys: pytest.CaptureFixture[str]) -> None: + """`explain spanish-cli` (the distribution name) still resolves to the root.""" + rc = main(["explain", "spanish-cli"]) + assert rc == 0 + assert "# spanish" in capsys.readouterr().out + + def test_explain_json(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["explain", "whoami", "--json"]) assert rc == 0 payload = json.loads(capsys.readouterr().out) assert payload["path"] == ["whoami"] - assert "spanish-cli whoami" in payload["markdown"] + assert "spanish whoami" in payload["markdown"] def test_explain_unknown_path_errors(capsys: pytest.CaptureFixture[str]) -> None: diff --git a/tests/test_cli_introspection.py b/tests/test_cli_introspection.py index d05d154..21fba60 100644 --- a/tests/test_cli_introspection.py +++ b/tests/test_cli_introspection.py @@ -15,7 +15,7 @@ def test_overview_text(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["overview"]) assert rc == 0 out = capsys.readouterr().out - assert "# spanish-cli" in out + assert "# spanish" in out assert "Identity" in out @@ -23,7 +23,7 @@ def test_overview_json_shape(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["overview", "--json"]) assert rc == 0 payload = json.loads(capsys.readouterr().out) - assert payload["subject"] == "spanish-cli" + assert payload["subject"] == "spanish" assert isinstance(payload["sections"], list) assert payload["sections"] @@ -41,14 +41,14 @@ def test_overview_graceful_on_bad_path(capsys: pytest.CaptureFixture[str]) -> No def test_cli_overview_text(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["cli", "overview"]) assert rc == 0 - assert "# spanish-cli cli" in capsys.readouterr().out + assert "# spanish cli" in capsys.readouterr().out def test_cli_overview_json_shape(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["cli", "overview", "--json"]) assert rc == 0 payload = json.loads(capsys.readouterr().out) - assert payload["subject"] == "spanish-cli cli" + assert payload["subject"] == "spanish cli" assert isinstance(payload["sections"], list) @@ -77,7 +77,7 @@ def test_cli_overview_unknown_flag_structured_error( def test_doctor_text(capsys: pytest.CaptureFixture[str]) -> None: rc = main(["doctor"]) assert rc in (0, 1) - assert "spanish-cli doctor" in capsys.readouterr().out + assert "spanish doctor" in capsys.readouterr().out def test_doctor_json_shape(capsys: pytest.CaptureFixture[str]) -> None: diff --git a/uv.lock b/uv.lock index e3db336..6b3d000 100644 --- a/uv.lock +++ b/uv.lock @@ -154,37 +154,6 @@ 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 = "spanish-cli" -version = "0.3.4" -source = { editable = "." } - -[package.dev-dependencies] -dev = [ - { name = "bandit" }, - { name = "black" }, - { name = "flake8" }, - { name = "isort" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-xdist" }, - { name = "teken" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -dev = [ - { name = "bandit", specifier = ">=1.7.5" }, - { name = "black", specifier = ">=23.7.0" }, - { name = "flake8", specifier = ">=6.1" }, - { name = "isort", specifier = ">=5.12.0" }, - { name = "pytest", specifier = ">=8.0" }, - { name = "pytest-cov", specifier = ">=4.1" }, - { name = "pytest-xdist", specifier = ">=3.0" }, - { name = "teken", specifier = ">=0.8" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -459,6 +428,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] +[[package]] +name = "spanish-cli" +version = "0.5.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "black" }, + { name = "flake8" }, + { name = "isort" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "teken" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.7.5" }, + { name = "black", specifier = ">=23.7.0" }, + { name = "flake8", specifier = ">=6.1" }, + { name = "isort", specifier = ">=5.12.0" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-cov", specifier = ">=4.1" }, + { name = "pytest-xdist", specifier = ">=3.0" }, + { name = "teken", specifier = ">=0.8" }, +] + [[package]] name = "stevedore" version = "5.8.0"