A module is a directory of data -- a manifest, practice decks and the scripts they name --
that StudyLoop validates against seventeen rules (MC001-MC017), installs as an immutable
snapshot, lists under the Web UI's Labs entry and runs one drill phase at a time through
studyloop drill, with your consent typed before anything executes. Core never imports or
executes module code as a library. This page is the shipped contract; the design history is in
Optional Modules (Proposal).
- Scaffold a conforming module with one offline drill. The scaffold passes
module checkas written and its check script (check.shunder the module'sscripts/) printsreadyand exits 0:
studyloop module new my-drills- Edit
module.yaml, the deck underdecks/andmentor/brief.md; keep every phase command as one executable underscripts/plus literal arguments (no shell operators, see MC015). - Run the conformance check from the repository root or in CI. Exit 0 means conforming; exit 1
lists each finding as
MCnnn <file>:<line>: <message>:
studyloop module check ./my-drills- Install it. The check runs again and nothing is written unless it passes; the snapshot lands
under the config directory's
modules/snapshots/<digest>andmodules.lock.jsonrecords the id, version and content digest (plus the git remote and sha when the path is a checkout):
studyloop module add ./my-drills- Confirm the install and reload the Web UI: the Labs entry appears in the sidebar as soon as the lockfile lists one module, and disappears when it lists none:
studyloop module list- Start the drill from Labs, then run its phases in your own terminal. Each phase shows the
exact argv and the script's sha256 and asks
Run this phase now?; there is no--yes:
studyloop drill check my-drills/getting-started/ready-01- Read the two lines after the result:
Mentor told.means the check reached the mentor in your running session;Mentor not told: <reason>names why it did not (for exampleno_active_sessionwhen no session is live). - Remove it when done. The lockfile entry and the snapshot go together:
studyloop module remove my-drills| Path | Required | Rule |
|---|---|---|
module.yaml |
yes | manifest, extra: forbid (MC001, MC002) |
decks/*.json |
at least one | a PracticeDeck with deck and task ids (MC004, MC014) |
the module's scripts/ |
when a deck declares a phase | one executable per phase, regular files only (MC007, MC013, MC015) |
mentor/brief.md |
yes | the module's only channel into coaching; advisory, labelled as data |
README.md |
yes | the headings Prerequisites, What runs, Data collected, Limits (MC011) |
LICENSE, CHANGELOG.md |
yes | MC003 |
tests/check.sh |
recommended | the module's own offline test; the scaffold writes one |
No pyproject.toml, package.json, setup.py, entry points or *.py anywhere (MC012).
module.yaml is read by packages/studyloop/src/studyloop/modules/manifest.py:
| Field | Meaning |
|---|---|
id |
slug, the module id (module/deck/task in every command) |
contract |
1 -- the contract version this StudyLoop speaks (MC002) |
version, title, topic, license, maintainers[] |
required metadata |
prerequisites[].id |
one of core's enum docker, kind, kubectl (MC005); studyloop doctor probes them, drill resolves them on your PATH before asking for consent |
capabilities[] |
terminal, setup, check, cleanup -- each phase a deck uses must be declared, and every declared one used (MC006, MC016) |
target.kind |
workdir or kubeconfig-context (a core-created disposable cluster; --context, --kubeconfig and use-context in a command fail MC008) |
Decks are StudyLoop's existing PracticeDeck JSON with six added fields: deck id, task id,
solution, scaffolding, mentorNotes and verification.cleanupCommand. The learner sees a
task's prompt and successCriteria -- in Labs and in the CLI -- and nothing else; hint,
solution and mentorNotes reach them only through the mentor. A task with a solution needs
a scaffolding level, and every task needs expectedLearningOutcome and at least one success
criterion (MC010). Secret-looking strings anywhere in the module fail MC009.
setup,checkandcleanupare each one argv, run withshell=Falsein a per-attempt directory with an environment built from scratch: a scratchHOME, a minimalPATHplus the declared prerequisites,STUDYLOOP_MODULE_ID,STUDYLOOP_DRILL_ID,STUDYLOOP_ATTEMPT_IDand -- for a cluster target --KUBECONFIGnaming exactly one context. The attempt records the manifest'starget.kind; creating that disposable cluster is not wired yet, so akubeconfig-contextdrill is refused after consent and before anything runs.- Exit codes:
check0 passed, 1 failed (task not complete), 2 or more could not run; a non-zerosetupis an error; a failingcleanupis shown and the attempt still closes. A timeout is an error, never a fail;timeoutSecondsapplies per phase. - Output is capped, redacted and kept for the attempt only. A check records a transient
practice_checkevent -- never a learning record, mastery claim or review item. - The result is delivered to the mentor as one bounded, labelled
practice_checkblock (at most 8 KiB, every output line prefixed|) through the running session's transport, on both the browser terminal and the ACP path. Delivery failure is always shown, never silent.
Labs is one generic sidebar entry, visible only while a module is installed. It lists drills grouped by module, by id and prompt. Start drill starts your study session for that drill -- or binds the drill to the session already running -- and shows the exact commands to run in your terminal; Cancel drill drops the drill and leaves the session as it was. Nothing executes from the browser in v1: checks are triggered from the CLI, and a browser Check progress button is deferred to v1.1 with its own consent surface.
Installing from a git URL or the Web UI; a module registry or index; automatic checks after
each command; a view-only check credential or standing per-attempt grants; a third target.kind.
The conformance engine is packages/studyloop/src/studyloop/modules/check.py; the runner is
packages/studyloop/src/studyloop/modules/runner.py; the fixture modules the tests use are under
packages/studyloop/tests/fixtures/modules/.