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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 76 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <console-script-name>`, 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: <msg>` + `hint: <remediation>`; 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<run_number>` 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.
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 |
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 2 additions & 2 deletions spanish/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions spanish/cli/_commands/cli.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,29 +15,29 @@

def cmd_cli_overview(args: argparse.Namespace) -> int:
emit_overview(
"spanish-cli cli",
"spanish cli",
cli_sections(),
json_mode=bool(getattr(args, "json", False)),
)
return 0


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)
# `p` is a _CliArgumentParser (the top-level subparsers were built with that
# 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)
4 changes: 2 additions & 2 deletions spanish/cli/_commands/doctor.py
Original file line number Diff line number Diff line change
@@ -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:

Expand Down Expand Up @@ -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']}")
Expand Down
4 changes: 2 additions & 2 deletions spanish/cli/_commands/explain.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``spanish-cli explain <path>...`` — global markdown catalog lookup (stable-contract).
"""``spanish explain <path>...`` — 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`.
Expand Down Expand Up @@ -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)
22 changes: 11 additions & 11 deletions spanish/cli/_commands/learn.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
-------
Expand All @@ -23,12 +23,12 @@

Commands
--------
spanish-cli whoami Identity from culture.yaml.
spanish-cli learn This self-teaching prompt.
spanish-cli explain <path>... 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 <path>... 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
-----------------------
Expand All @@ -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": [
Expand All @@ -67,7 +67,7 @@ def _as_json_payload() -> dict[str, object]:
"2": "environment/setup error",
},
"json_support": True,
"explain_pointer": "spanish-cli explain <path>",
"explain_pointer": "spanish explain <path>",
}


Expand Down
Loading
Loading