Metadata-Version: 2.4
Name: learnctl
Version: 0.30.0
Summary: Consumer CLI for the learnctl catalog — install and study hands-on courses.
Author-email: Ravikanth Chaganti <ravikanth.chaganti@gmail.com>
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: textual>=0.60

# learnctl

A rich Textual TUI host for the [`learning-coach`](../learning-coach) skill.
`learnctl` reads a Package the skill generates, calls `coach.py` for all
deterministic state, runs declarative lab checks on the host, and walks the
learner through the protocol turn-by-turn with a live terminal UI.

Built on the principle the skill enforces: **the LLM is a build-time author,
not a run-time dependency.** v1 runs the entire free path with no model wired
up — discover, resume, active-recall warm-up, prerequisite check, primer
reading, hands-on task, declarative verifier, hint ladder, solution, scoring,
advance. The two metered LLM turns (interview and the opt-in diagnose) are
defined slots that v2 will fill via a pluggable provider.

## Install

```bash
pip install -e .
export LEARNCTL_COACH=/path/to/learning-coach/scripts/coach.py
```

## Commands

```bash
learnctl discover              # list topics under ./learning + progress
learnctl status <topic>        # one-shot dashboard (no TUI)
learnctl study   <topic>       # launch the TUI session
```

A `<topic>` is a slug (`docker`), a folder path (`learning/docker`), or a
state.json path. `--root <path>` overrides the workspace root for slug lookup.

### Keyboard shortcuts (TUI)

The control bar is docked to the bottom of the screen so it can never scroll
out of view, and every action has a keyboard shortcut as a fallback:

| Key | Action | Available when |
|---|---|---|
| `r` | Reveal answer / Run check | recall reveal, task step, verify retry |
| `h` | Show next hint | verify failed and rungs remain |
| `s` | Show solution | verify failed |
| `g` / `f` / `m` | Got it / Fuzzy / Missed (self-rate recall) | after recall reveal |
| `c` | Continue / Dismiss | most other turns |
| `1` / `2` / `3` | Score 60 / 75 / 90 (manual interview) | after the interview |
| `q` | Quit | always |

## Architecture

Five layers, bottom-up:

| Layer | File | Responsibility |
|---|---|---|
| **engine** | `engine.py` | Typed subprocess wrapper around `coach.py`. The only layer that touches state. Parses the `---JSON---` block from each command into typed results. Subprocess (not import) on purpose — validates the host contract any language could implement. |
| **package** | `package.py` | Read-only loader for `path.json` + `scenario.json` + Markdown. Pure file I/O, no logic. |
| **verifier** | `verifier.py` | Executes the declarative `verify.checks` on the host: `command` and `script` checks run via subprocess with timeouts; `file` checks read the filesystem. Evaluates `expect` (`contains`/`regex`/`equals`/`exit_code`/`json` with path matching, including json-lines). Honors per-check `os` restrictions. v1 runs directly on the host; container isolation is a future opt-in. |
| **session** | `session.py` | The protocol state machine. Walks `WARMUP → PREREQS → PRIMER → TASK → VERIFY → INTERVIEW → SCORE → DONE`, emitting `Turn` objects with `type`, `say`, `data`, `expects`. The active-recall trio (`recall_prompt → recall_reveal → recall_grade`) is enforced here — the answer is withheld from the prompt envelope by construction, so no host can leak it early. Host-agnostic and unit-testable without a terminal. |
| **tui** | `tui.py` | Textual app. Renders `say` as Markdown in a scrolling transcript, shows the right control buttons for each `expects`, streams hint rungs into a log, and resumes the session with the chosen response. |

Plus `__main__.py` for the CLI entry point. The session has a stub for a future
`provider` layer (interview / diagnose) — `has_llm=False` in v1 keeps those turns
manual.

## Tests

```bash
LEARNCTL_COACH=/path/to/coach.py python3 tests/test_foundation.py
LEARNCTL_COACH=/path/to/coach.py python3 tests/test_session.py
LEARNCTL_COACH=/path/to/coach.py python3 tests/test_tui.py
```

- `test_foundation.py` — 8 unit tests for the engine, package, and verifier
  layers including the `expect` matchers and the Docker-style json-lines path.
- `test_session.py` — drives the full free-path session end-to-end and asserts
  the turn sequence.
- `test_tui.py` — runs the Textual app headlessly via `app.run_test()`, clicking
  every button along the path and asserting the final mastery score.

All three currently pass against the real `coach.py` from the skill.

## What's intentionally not in v1

- **LLM provider.** The `interview` turn collects a score manually (score
  buttons in the TUI); the `diagnose` button falls back to revealing the
  solution. v2 will add an `interface` for `interview()` / `diagnose()` /
  `ask()` with a null implementation for the offline case and concrete
  providers (Anthropic, etc.) for hosts that wire one up.
- **Container sandbox.** The verifier executes on the host with per-check
  timeouts; v2 can opt-in to a Docker-based sandbox. The skill's authoring
  safety contract (scoped, non-destructive, transparent, reversible) is the
  current safety boundary.
- **Generation commands.** `learnctl` consumes packages; generating them is
  the skill's job. The CLI doesn't wrap `init`/`add-scenario`/`add-item`
  because those are author-side commands a chat agent or another tool runs.

## Cost model

Zero LLM calls per session in v1 — the entire free path is deterministic and
free. v2's metered turns (interview, optional diagnose) match the skill's
design: ~1 model call per stage plus opt-in diagnoses when novel-stuck.
