Consumer CLI for the learnctl catalog — install and study hands-on courses.
Project description
learnctl
A rich Textual TUI host for the 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
pip install -e .
export LEARNCTL_COACH=/path/to/learning-coach/scripts/coach.py
Commands
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
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 theexpectmatchers 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 viaapp.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
interviewturn collects a score manually (score buttons in the TUI); thediagnosebutton falls back to revealing the solution. v2 will add aninterfaceforinterview()/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.
learnctlconsumes packages; generating them is the skill's job. The CLI doesn't wrapinit/add-scenario/add-itembecause 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file learnctl-0.32.8.tar.gz.
File metadata
- Download URL: learnctl-0.32.8.tar.gz
- Upload date:
- Size: 340.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71866907df86e5ece51da964d1161e200b4e7615393b31f3ca709d37ba610fab
|
|
| MD5 |
885b0d072e3b310761c127c284dea157
|
|
| BLAKE2b-256 |
9fc82bf1eb584785f5f284cba944e4e843f5fccca00cbc52b47aace70ebdd34d
|
Provenance
The following attestation bundles were made for learnctl-0.32.8.tar.gz:
Publisher:
publish-learnctl.yml on rchaganti/learnctl-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
learnctl-0.32.8.tar.gz -
Subject digest:
71866907df86e5ece51da964d1161e200b4e7615393b31f3ca709d37ba610fab - Sigstore transparency entry: 1996727563
- Sigstore integration time:
-
Permalink:
rchaganti/learnctl-stack@428b2f256b866da16de3fa67eac5289faed7c7df -
Branch / Tag:
refs/tags/learnctl-v0.32.8 - Owner: https://github.com/rchaganti
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-learnctl.yml@428b2f256b866da16de3fa67eac5289faed7c7df -
Trigger Event:
push
-
Statement type:
File details
Details for the file learnctl-0.32.8-py3-none-any.whl.
File metadata
- Download URL: learnctl-0.32.8-py3-none-any.whl
- Upload date:
- Size: 189.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0473a76b9384fd6552730f61ee6d25561c4d02d9852a023d793158b34026a9c6
|
|
| MD5 |
8fc1169ea1ac18b736a16e5e6422d482
|
|
| BLAKE2b-256 |
aa23f82f261793b88e428a0afda2a8ee7bd00b57dd5e6524948064bfed112282
|
Provenance
The following attestation bundles were made for learnctl-0.32.8-py3-none-any.whl:
Publisher:
publish-learnctl.yml on rchaganti/learnctl-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
learnctl-0.32.8-py3-none-any.whl -
Subject digest:
0473a76b9384fd6552730f61ee6d25561c4d02d9852a023d793158b34026a9c6 - Sigstore transparency entry: 1996727644
- Sigstore integration time:
-
Permalink:
rchaganti/learnctl-stack@428b2f256b866da16de3fa67eac5289faed7c7df -
Branch / Tag:
refs/tags/learnctl-v0.32.8 - Owner: https://github.com/rchaganti
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-learnctl.yml@428b2f256b866da16de3fa67eac5289faed7c7df -
Trigger Event:
push
-
Statement type: