learn-cli 0.4.2__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. learn_cli-0.5.1/.github/workflows/deploy-site.yml +122 -0
  2. {learn_cli-0.4.2 → learn_cli-0.5.1}/.gitignore +2 -1
  3. {learn_cli-0.4.2 → learn_cli-0.5.1}/.markdownlint-cli2.yaml +9 -0
  4. {learn_cli-0.4.2 → learn_cli-0.5.1}/CHANGELOG.md +27 -0
  5. {learn_cli-0.4.2 → learn_cli-0.5.1}/CLAUDE.md +7 -2
  6. {learn_cli-0.4.2 → learn_cli-0.5.1}/PKG-INFO +2 -1
  7. learn_cli-0.5.1/docs/hosting.md +76 -0
  8. learn_cli-0.5.1/docs/specs/subject-plugin-contract.md +262 -0
  9. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/__init__.py +16 -0
  10. learn_cli-0.5.1/learn/cli/_commands/auth.py +279 -0
  11. learn_cli-0.5.1/learn/cli/_commands/mcp.py +118 -0
  12. learn_cli-0.5.1/learn/cli/_commands/next_cmd.py +88 -0
  13. learn_cli-0.5.1/learn/cli/_commands/progress.py +192 -0
  14. learn_cli-0.5.1/learn/cli/_commands/record.py +121 -0
  15. learn_cli-0.5.1/learn/cli/_commands/site.py +142 -0
  16. learn_cli-0.5.1/learn/cli/_commands/subject.py +112 -0
  17. learn_cli-0.5.1/learn/cli/_commands/subjects.py +51 -0
  18. learn_cli-0.5.1/learn/contract/__init__.py +99 -0
  19. learn_cli-0.5.1/learn/contract/_validate.py +327 -0
  20. learn_cli-0.5.1/learn/contract/schemas/advice.json +34 -0
  21. learn_cli-0.5.1/learn/contract/schemas/doctor.json +37 -0
  22. learn_cli-0.5.1/learn/contract/schemas/error.json +20 -0
  23. learn_cli-0.5.1/learn/contract/schemas/lesson.json +80 -0
  24. learn_cli-0.5.1/learn/contract/schemas/overview.json +52 -0
  25. learn_cli-0.5.1/learn/contract/schemas/practice.json +75 -0
  26. learn_cli-0.5.1/learn/contract/schemas/progress.json +85 -0
  27. learn_cli-0.5.1/learn/contract/schemas/record.json +74 -0
  28. learn_cli-0.5.1/learn/contract/schemas/story.json +131 -0
  29. learn_cli-0.5.1/learn/contract/schemas/story_list.json +33 -0
  30. learn_cli-0.5.1/learn/contract/schemas/story_read.json +40 -0
  31. learn_cli-0.5.1/learn/explain/catalog.py +437 -0
  32. learn_cli-0.5.1/learn/front/__init__.py +175 -0
  33. learn_cli-0.5.1/learn/front/_docs.py +167 -0
  34. learn_cli-0.5.1/learn/front/_driver.py +129 -0
  35. learn_cli-0.5.1/learn/front/_export.py +171 -0
  36. learn_cli-0.5.1/learn/motivation/__init__.py +117 -0
  37. learn_cli-0.5.1/learn/motivation/_constants.py +90 -0
  38. learn_cli-0.5.1/learn/motivation/_decay.py +47 -0
  39. learn_cli-0.5.1/learn/motivation/_models.py +267 -0
  40. learn_cli-0.5.1/learn/motivation/_review.py +47 -0
  41. learn_cli-0.5.1/learn/motivation/_scoring.py +72 -0
  42. learn_cli-0.5.1/learn/motivation/_state.py +113 -0
  43. learn_cli-0.5.1/learn/motivation/_streaks.py +103 -0
  44. learn_cli-0.5.1/learn/motivation/_whatnext.py +180 -0
  45. learn_cli-0.5.1/learn/profile/__init__.py +101 -0
  46. learn_cli-0.5.1/learn/profile/_api.py +135 -0
  47. learn_cli-0.5.1/learn/profile/_auth.py +93 -0
  48. learn_cli-0.5.1/learn/profile/_ledger.py +68 -0
  49. learn_cli-0.5.1/learn/profile/_paths.py +66 -0
  50. learn_cli-0.5.1/learn/profile/_sync.py +81 -0
  51. learn_cli-0.5.1/learn/profile/_syncstate.py +48 -0
  52. learn_cli-0.5.1/learn/subjects/__init__.py +200 -0
  53. learn_cli-0.5.1/learn/subjects/conformance.py +278 -0
  54. learn_cli-0.5.1/learn/subjects/driver.py +119 -0
  55. learn_cli-0.5.1/learn/subjects/registry.json +29 -0
  56. {learn_cli-0.4.2 → learn_cli-0.5.1}/pyproject.toml +5 -2
  57. learn_cli-0.5.1/site-astro/.gitignore +25 -0
  58. learn_cli-0.5.1/site-astro/.nvmrc +1 -0
  59. learn_cli-0.5.1/site-astro/README.md +335 -0
  60. learn_cli-0.5.1/site-astro/astro.config.mjs +20 -0
  61. learn_cli-0.5.1/site-astro/package-lock.json +4135 -0
  62. learn_cli-0.5.1/site-astro/package.json +23 -0
  63. learn_cli-0.5.1/site-astro/public/apple-touch-icon.png +0 -0
  64. learn_cli-0.5.1/site-astro/public/favicon.ico +0 -0
  65. learn_cli-0.5.1/site-astro/public/favicon.svg +16 -0
  66. learn_cli-0.5.1/site-astro/scripts/check-export-pages.mjs +154 -0
  67. learn_cli-0.5.1/site-astro/scripts/check-static-auth.mjs +360 -0
  68. learn_cli-0.5.1/site-astro/src/components/Footer.astro +137 -0
  69. learn_cli-0.5.1/site-astro/src/components/Header.astro +251 -0
  70. learn_cli-0.5.1/site-astro/src/components/LearnerPanelOverview.astro +152 -0
  71. learn_cli-0.5.1/site-astro/src/components/LearnerPanelSubject.astro +180 -0
  72. learn_cli-0.5.1/site-astro/src/components/Mark.astro +27 -0
  73. learn_cli-0.5.1/site-astro/src/components/PageHero.astro +62 -0
  74. learn_cli-0.5.1/site-astro/src/content-export/docs/portal.md +39 -0
  75. learn_cli-0.5.1/site-astro/src/content-export/docs/subject-plugin-contract.md +46 -0
  76. learn_cli-0.5.1/site-astro/src/content-export/docs/subjects.md +27 -0
  77. learn_cli-0.5.1/site-astro/src/content-export/meta.json +9 -0
  78. learn_cli-0.5.1/site-astro/src/content-export/stories-culture-guide.json +311 -0
  79. learn_cli-0.5.1/site-astro/src/content-export/stories-french.json +1086 -0
  80. learn_cli-0.5.1/site-astro/src/content-export/stories-spanish.json +1094 -0
  81. learn_cli-0.5.1/site-astro/src/content-export/subjects.json +95 -0
  82. learn_cli-0.5.1/site-astro/src/data/site.ts +30 -0
  83. learn_cli-0.5.1/site-astro/src/layouts/Layout.astro +104 -0
  84. learn_cli-0.5.1/site-astro/src/lib/api.ts +20 -0
  85. learn_cli-0.5.1/site-astro/src/lib/content.ts +99 -0
  86. learn_cli-0.5.1/site-astro/src/pages/[subject]/index.astro +250 -0
  87. learn_cli-0.5.1/site-astro/src/pages/[subject]/stories/[id]/index.astro +312 -0
  88. learn_cli-0.5.1/site-astro/src/pages/index.astro +175 -0
  89. learn_cli-0.5.1/site-astro/src/scripts/learner.js +278 -0
  90. learn_cli-0.5.1/site-astro/src/styles/global.css +398 -0
  91. learn_cli-0.5.1/site-astro/tsconfig.json +5 -0
  92. learn_cli-0.5.1/tests/conftest.py +182 -0
  93. learn_cli-0.5.1/tests/e2e/__init__.py +0 -0
  94. learn_cli-0.5.1/tests/e2e/conftest.py +84 -0
  95. learn_cli-0.5.1/tests/e2e/test_cli_golden.py +85 -0
  96. learn_cli-0.5.1/tests/e2e/test_mcp_harness.py +80 -0
  97. learn_cli-0.5.1/tests/fixtures/motivation/completed_track_current.json +59 -0
  98. learn_cli-0.5.1/tests/fixtures/motivation/completed_track_decayed.json +137 -0
  99. learn_cli-0.5.1/tests/fixtures/motivation/cross_subject.json +87 -0
  100. learn_cli-0.5.1/tests/fixtures/motivation/single_subject_progression.json +98 -0
  101. learn_cli-0.5.1/tests/fixtures/payloads/advice.json +20 -0
  102. learn_cli-0.5.1/tests/fixtures/payloads/doctor.json +30 -0
  103. learn_cli-0.5.1/tests/fixtures/payloads/error.json +5 -0
  104. learn_cli-0.5.1/tests/fixtures/payloads/lesson.json +42 -0
  105. learn_cli-0.5.1/tests/fixtures/payloads/overview.json +28 -0
  106. learn_cli-0.5.1/tests/fixtures/payloads/practice.json +40 -0
  107. learn_cli-0.5.1/tests/fixtures/payloads/progress.json +42 -0
  108. learn_cli-0.5.1/tests/fixtures/payloads/record.json +28 -0
  109. learn_cli-0.5.1/tests/fixtures/payloads/story.json +41 -0
  110. learn_cli-0.5.1/tests/fixtures/payloads/story_list.json +25 -0
  111. learn_cli-0.5.1/tests/fixtures/payloads/story_read.json +51 -0
  112. learn_cli-0.5.1/tests/fixtures/stories/culture-guide-scenario-overloaded-agent.json +76 -0
  113. learn_cli-0.5.1/tests/fixtures/stories/french-beginner-le-marche.json +86 -0
  114. learn_cli-0.5.1/tests/fixtures/stories/spanish-beginner-la-panaderia.json +86 -0
  115. learn_cli-0.5.1/tests/fixtures/subjects/conformant_subject.py +363 -0
  116. learn_cli-0.5.1/tests/fixtures/subjects/drifted_subject.py +130 -0
  117. learn_cli-0.5.1/tests/test_auth_cli.py +256 -0
  118. learn_cli-0.5.1/tests/test_cli_faces.py +163 -0
  119. learn_cli-0.5.1/tests/test_contract_schemas.py +146 -0
  120. learn_cli-0.5.1/tests/test_contract_validator.py +160 -0
  121. learn_cli-0.5.1/tests/test_front_app.py +177 -0
  122. learn_cli-0.5.1/tests/test_front_driver.py +121 -0
  123. learn_cli-0.5.1/tests/test_motivation_decay_review.py +122 -0
  124. learn_cli-0.5.1/tests/test_motivation_extras.py +110 -0
  125. learn_cli-0.5.1/tests/test_motivation_golden.py +101 -0
  126. learn_cli-0.5.1/tests/test_motivation_purity.py +144 -0
  127. learn_cli-0.5.1/tests/test_motivation_scoring.py +108 -0
  128. learn_cli-0.5.1/tests/test_motivation_streaks.py +110 -0
  129. learn_cli-0.5.1/tests/test_motivation_whatnext.py +240 -0
  130. learn_cli-0.5.1/tests/test_profile_store.py +164 -0
  131. learn_cli-0.5.1/tests/test_progress_next_record.py +290 -0
  132. learn_cli-0.5.1/tests/test_site_export.py +231 -0
  133. learn_cli-0.5.1/tests/test_story_fixtures.py +133 -0
  134. learn_cli-0.5.1/tests/test_subject_doctor.py +167 -0
  135. learn_cli-0.5.1/tests/test_subject_driver.py +132 -0
  136. learn_cli-0.5.1/tests/test_subject_registry.py +151 -0
  137. learn_cli-0.5.1/tests/test_surfaces_agree.py +67 -0
  138. learn_cli-0.5.1/tools/launch-gate/.gitignore +4 -0
  139. learn_cli-0.5.1/tools/launch-gate/README.md +167 -0
  140. learn_cli-0.5.1/tools/launch-gate/api-server.mjs +190 -0
  141. learn_cli-0.5.1/tools/launch-gate/launch_bar.py +177 -0
  142. learn_cli-0.5.1/tools/launch-gate/package.json +13 -0
  143. learn_cli-0.5.1/tools/launch-gate/report.py +117 -0
  144. learn_cli-0.5.1/tools/launch-gate/run.sh +106 -0
  145. learn_cli-0.5.1/tools/launch-gate/walk.mjs +394 -0
  146. learn_cli-0.5.1/uv.lock +1118 -0
  147. learn_cli-0.5.1/workers/learn-api/.gitignore +10 -0
  148. learn_cli-0.5.1/workers/learn-api/README.md +206 -0
  149. learn_cli-0.5.1/workers/learn-api/package.json +20 -0
  150. learn_cli-0.5.1/workers/learn-api/schema.sql +44 -0
  151. learn_cli-0.5.1/workers/learn-api/src/auth.js +50 -0
  152. learn_cli-0.5.1/workers/learn-api/src/db.js +77 -0
  153. learn_cli-0.5.1/workers/learn-api/src/github.js +119 -0
  154. learn_cli-0.5.1/workers/learn-api/src/index.js +336 -0
  155. learn_cli-0.5.1/workers/learn-api/src/progress.js +82 -0
  156. learn_cli-0.5.1/workers/learn-api/src/session.js +65 -0
  157. learn_cli-0.5.1/workers/learn-api/src/util.js +120 -0
  158. learn_cli-0.5.1/workers/learn-api/src/validate.js +112 -0
  159. learn_cli-0.5.1/workers/learn-api/test/helpers.js +167 -0
  160. learn_cli-0.5.1/workers/learn-api/test/session.test.js +45 -0
  161. learn_cli-0.5.1/workers/learn-api/test/validate.test.js +73 -0
  162. learn_cli-0.5.1/workers/learn-api/test/worker.test.js +475 -0
  163. learn_cli-0.5.1/workers/learn-api/wrangler.toml +66 -0
  164. learn_cli-0.4.2/learn/explain/catalog.py +0 -129
  165. learn_cli-0.4.2/uv.lock +0 -478
  166. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/agent-config/SKILL.md +0 -0
  167. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  168. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/agent-config/scripts/show.sh +0 -0
  169. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/ask-colleague/SKILL.md +0 -0
  170. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  171. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  172. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  173. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  174. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  175. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  176. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/cicd/SKILL.md +0 -0
  177. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  178. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  179. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  180. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  181. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  182. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/SKILL.md +0 -0
  183. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  184. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  185. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  186. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  187. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  188. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  189. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  190. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  191. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  192. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  193. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/recall/SKILL.md +0 -0
  194. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/recall/scripts/recall.sh +0 -0
  195. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/remember/SKILL.md +0 -0
  196. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/remember/scripts/remember.sh +0 -0
  197. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/run-tests/SKILL.md +0 -0
  198. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/run-tests/scripts/test.sh +0 -0
  199. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/sonarclaude/SKILL.md +0 -0
  200. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  201. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  202. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  203. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/think/SKILL.md +0 -0
  204. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/think/scripts/think.sh +0 -0
  205. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/version-bump/SKILL.md +0 -0
  206. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills/version-bump/scripts/bump.py +0 -0
  207. {learn_cli-0.4.2 → learn_cli-0.5.1}/.claude/skills.local.yaml.example +0 -0
  208. {learn_cli-0.4.2 → learn_cli-0.5.1}/.devague/current +0 -0
  209. {learn_cli-0.4.2 → learn_cli-0.5.1}/.devague/current_plan +0 -0
  210. {learn_cli-0.4.2 → learn_cli-0.5.1}/.devague/frames/agentculture-org-learn-is-live-one-learning-hub-in.json +0 -0
  211. {learn_cli-0.4.2 → learn_cli-0.5.1}/.devague/plans/agentculture-org-learn-is-live-one-learning-hub-in.json +0 -0
  212. {learn_cli-0.4.2 → learn_cli-0.5.1}/.flake8 +0 -0
  213. {learn_cli-0.4.2 → learn_cli-0.5.1}/.github/workflows/publish.yml +0 -0
  214. {learn_cli-0.4.2 → learn_cli-0.5.1}/.github/workflows/tests.yml +0 -0
  215. {learn_cli-0.4.2 → learn_cli-0.5.1}/AGENTS.colleague.md +0 -0
  216. {learn_cli-0.4.2 → learn_cli-0.5.1}/LICENSE +0 -0
  217. {learn_cli-0.4.2 → learn_cli-0.5.1}/README.md +0 -0
  218. {learn_cli-0.4.2 → learn_cli-0.5.1}/culture.yaml +0 -0
  219. {learn_cli-0.4.2 → learn_cli-0.5.1}/docs/plans/.markdownlint-cli2.yaml +0 -0
  220. {learn_cli-0.4.2 → learn_cli-0.5.1}/docs/plans/2026-07-10-agentculture-org-learn-is-live-one-learning-hub-in.md +0 -0
  221. {learn_cli-0.4.2 → learn_cli-0.5.1}/docs/skill-sources.md +0 -0
  222. {learn_cli-0.4.2 → learn_cli-0.5.1}/docs/specs/.markdownlint-cli2.yaml +0 -0
  223. {learn_cli-0.4.2 → learn_cli-0.5.1}/docs/specs/2026-07-10-agentculture-org-learn-is-live-one-learning-hub-in.md +0 -0
  224. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/__init__.py +0 -0
  225. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/__main__.py +0 -0
  226. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/__init__.py +0 -0
  227. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/cli.py +0 -0
  228. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/doctor.py +0 -0
  229. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/explain.py +0 -0
  230. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/learn.py +0 -0
  231. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/overview.py +0 -0
  232. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_commands/whoami.py +0 -0
  233. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_errors.py +0 -0
  234. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/cli/_output.py +0 -0
  235. {learn_cli-0.4.2 → learn_cli-0.5.1}/learn/explain/__init__.py +0 -0
  236. {learn_cli-0.4.2 → learn_cli-0.5.1}/sonar-project.properties +0 -0
  237. {learn_cli-0.4.2 → learn_cli-0.5.1}/tests/__init__.py +0 -0
  238. {learn_cli-0.4.2 → learn_cli-0.5.1}/tests/test_cli.py +0 -0
  239. {learn_cli-0.4.2 → learn_cli-0.5.1}/tests/test_cli_introspection.py +0 -0
@@ -0,0 +1,122 @@
1
+ name: Deploy site
2
+
3
+ # learn-cli's web face: site-astro/ builds the /learn/ landing page, one
4
+ # sub-page per subject, and a story reader per story, then deploys to
5
+ # Cloudflare Pages. Modeled on org/site-astro's deploy.yml (same steps, same
6
+ # gating pattern) but targets a NEW, learn-cli-owned Pages project
7
+ # ("agentculture-learn") rather than org's "agentculture-org" — how (or
8
+ # whether) agentculture.org/learn/* gets routed to it is a separate,
9
+ # still-open integration decision; see site-astro/README.md.
10
+
11
+ on:
12
+ push:
13
+ branches: [main]
14
+ paths:
15
+ - 'site-astro/**'
16
+ - '.github/workflows/deploy-site.yml'
17
+ pull_request:
18
+ paths:
19
+ - 'site-astro/**'
20
+ - '.github/workflows/deploy-site.yml'
21
+ workflow_dispatch: {}
22
+
23
+ jobs:
24
+ deploy:
25
+ runs-on: ubuntu-latest
26
+ permissions:
27
+ contents: read
28
+ concurrency:
29
+ group: pages-deploy-${{ github.ref }}
30
+ cancel-in-progress: true
31
+ # Mapped here so later steps can gate with `if: env.CLOUDFLARE_API_TOKEN
32
+ # != ''` — secrets.* isn't usable directly in an `if:` conditional.
33
+ env:
34
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
35
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
36
+ steps:
37
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
38
+
39
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
40
+ with:
41
+ node-version-file: site-astro/.nvmrc
42
+ cache: npm
43
+ cache-dependency-path: site-astro/package-lock.json
44
+
45
+ - name: Install dependencies
46
+ working-directory: site-astro
47
+ run: npm ci
48
+
49
+ - name: Build site
50
+ working-directory: site-astro
51
+ run: npm run build
52
+
53
+ # Fails the job if the content-export fixtures and the built pages
54
+ # have drifted apart — a subject/module/story with no page, or a page
55
+ # with no export backing. See scripts/check-export-pages.mjs.
56
+ - name: Check export/page consistency
57
+ working-directory: site-astro
58
+ run: npm run check
59
+
60
+ # astro.config.mjs sets base: '/learn', so Astro's own bundled asset
61
+ # links (fonts, per-page CSS) come out of `astro build` as absolute
62
+ # `/learn/_astro/...` hrefs — but the physical dist/ tree stays flat
63
+ # (dist/_astro/..., no dist/learn/ nesting; only Astro's own dev/
64
+ # preview server understands `base` and rewrites around that). A dumb
65
+ # static host — Cloudflare Pages included — serves dist/ as literal
66
+ # paths, so those asset requests would 404 unless the deployed tree's
67
+ # physical layout matches the emitted hrefs. Wrapping dist/ under a
68
+ # `learn/` folder before upload fixes that for both the Pages preview
69
+ # domain (which then serves the site at .../learn/, not its bare
70
+ # root — intentional, so preview and production share one URL shape)
71
+ # and the eventual agentculture.org/learn mount. See site-astro/README.md
72
+ # ("Base path and relative links").
73
+ - name: Prepare deploy directory
74
+ run: |
75
+ mkdir -p site-astro/dist-deploy/learn
76
+ cp -r site-astro/dist/. site-astro/dist-deploy/learn/
77
+ # Bare-root visits (e.g. the raw *.pages.dev URL) land on /learn/
78
+ # instead of 404ing.
79
+ printf '/ /learn/ 302\n' > site-astro/dist-deploy/_redirects
80
+
81
+ # No cloudflare/wrangler-action — invoking wrangler directly via npx
82
+ # keeps the deploy dependency-light and auditable, matching org's
83
+ # deploy.yml (and this repo's zero-runtime-dependency temperament).
84
+ # site-astro/package.json is not touched: wrangler is fetched on
85
+ # demand, not installed as a devDep.
86
+ #
87
+ # --branch selects production vs. preview: the Cloudflare Pages project
88
+ # (agentculture-learn) has production branch "main", so pushing to main
89
+ # (or a manual workflow_dispatch off main) deploys to production, while
90
+ # a pull_request run passes the PR's head branch and gets a preview
91
+ # deployment alias instead.
92
+ - name: Deploy to Cloudflare Pages
93
+ if: env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != ''
94
+ run: >-
95
+ npx wrangler@4 pages deploy site-astro/dist-deploy
96
+ --project-name agentculture-learn
97
+ --branch "${{ github.head_ref || github.ref_name }}"
98
+
99
+ - name: Skip notice (Cloudflare secrets not configured)
100
+ if: env.CLOUDFLARE_API_TOKEN == '' || env.CLOUDFLARE_ACCOUNT_ID == ''
101
+ run: |
102
+ echo "::notice::CLOUDFLARE_API_TOKEN and/or CLOUDFLARE_ACCOUNT_ID are not configured on this repo yet — skipping the Cloudflare Pages deploy. The build and export/page check above still ran, so this validates the site builds cleanly."
103
+ {
104
+ echo '### Deploy skipped'
105
+ echo ''
106
+ echo 'The Cloudflare secrets (`CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID`) are not both set on this repo yet, so the Cloudflare Pages deploy step was skipped. The build and export/page consistency check above still ran and validated cleanly.'
107
+ } >> "$GITHUB_STEP_SUMMARY"
108
+
109
+ - name: Deployment summary
110
+ if: env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != ''
111
+ run: |
112
+ BRANCH="${{ github.head_ref || github.ref_name }}"
113
+ {
114
+ echo '### Deployed to Cloudflare Pages'
115
+ echo ''
116
+ if [ "$BRANCH" = "main" ]; then
117
+ echo '- Production: https://agentculture-learn.pages.dev/learn/'
118
+ echo '- Production (custom domain, once the agentculture.org/learn route is wired up): https://agentculture.org/learn/'
119
+ else
120
+ echo "- Preview: https://${BRANCH}.agentculture-learn.pages.dev/learn/"
121
+ fi
122
+ } >> "$GITHUB_STEP_SUMMARY"
@@ -14,7 +14,7 @@ dist/
14
14
  downloads/
15
15
  eggs/
16
16
  .eggs/
17
- lib/
17
+ /lib/
18
18
  lib64/
19
19
  parts/
20
20
  sdist/
@@ -231,3 +231,4 @@ skills.local.yaml
231
231
 
232
232
  # devague working state (not committed by default)
233
233
  .devague/reviews/
234
+ .claude/settings.json
@@ -16,6 +16,13 @@ config:
16
16
 
17
17
  ignores:
18
18
  - "node_modules/**"
19
+ # site-astro/ (and any future npm-managed subproject) has its own nested
20
+ # node_modules/ — a bare "node_modules/**" only matches the root one, so
21
+ # this recursive form is needed too (learn-cli#<t13>: the site-astro build).
22
+ - "**/node_modules/**"
23
+ # The uv virtualenv — third-party packages (starlette, uvicorn, httpx, …) ship
24
+ # their own LICENSE.md / README.md; never lint installed dependencies.
25
+ - ".venv/**"
19
26
  - ".local/**"
20
27
  - ".afi/**"
21
28
  - ".teken/**"
@@ -23,3 +30,5 @@ ignores:
23
30
  - ".claude/skills/**"
24
31
  # devague working state (generated review artifacts, gitignored).
25
32
  - ".devague/**"
33
+ # Generated by `learn site export` — registry content, not authored prose.
34
+ - "site-astro/src/content-export/**"
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). This project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.1] - 2026-07-11
9
+
10
+ ### Fixed
11
+
12
+ - API worker: the /learn zone-mount proxy strips Cookie/Authorization before forwarding to the Pages origin (review finding: session leak)
13
+ - API worker: withCors appends to an existing Vary header instead of overwriting it
14
+ - Profile ledger: created 0600 like auth.json (learner data is private)
15
+ - Profile ledger: a malformed ledger line is skipped instead of crashing progress/next/sync
16
+
17
+ ## [0.5.0] - 2026-07-11
18
+
19
+ ### Added
20
+
21
+ - The learn platform: subject registry fronting french-cli, spanish-cli, and culture-guide as external runtimes (`learn subjects`, `learn subject doctor <name>` conformance gate)
22
+ - Subject-plugin contract v1.0: 11 versioned JSON schemas + stdlib validator as package data (learn/contract), directive pattern, story schema shared by all subjects
23
+ - Motivation layer (learn/motivation): deterministic scores, streaks, time-decayed mastery, review queues, and never-ending what-next over recorded history
24
+ - Learner profile (learn/profile + auth/progress/next/record verbs): GitHub device-flow sign-in, offline-first local ledger with best-effort sync
25
+ - Three faces via agentfront>=0.20: learn mcp serve (stdio MCP), learn site serve (agent-readable web), learn site export (pinned content export), surfaces-agree CI test
26
+ - Web face (site-astro/): 29 static pages in org's design system (zero new tokens), signed-out/in split with client hydration and a zero-API static proof, Cloudflare Pages deploy workflow
27
+ - API worker (workers/learn-api): GitHub OAuth, KV/D1 learner ledger, tutoring broker, and the agentculture.org/learn/* zone mount (API + static proxy in one worker)
28
+ - E2E launch gate (tools/launch-gate): 66 machine-checked assertions across web/CLI/MCP audiences with live-mode rerun
29
+
30
+ ### Changed
31
+
32
+ - Runtime dependencies: agentfront[mcp]>=0.20 (was zero-dependency scaffold)
33
+ - CLAUDE.md and lint config updated for the new surfaces
34
+
8
35
  ## [0.4.2] - 2026-07-11
9
36
 
10
37
  ### Added
@@ -46,8 +46,13 @@ consistent → cross-subject learner state → hosting + model-access wiring.
46
46
 
47
47
  ## Commands
48
48
 
49
- Python ≥3.12, managed with **uv**. The runtime package has **zero third-party
50
- dependencies** (`dependencies = []`); all tooling lives in the `dev` group.
49
+ Python ≥3.12, managed with **uv**. The runtime package depends on
50
+ **`agentfront[mcp]>=0.20`** (`dependencies = ["agentfront[mcp]>=0.20"]`) the
51
+ one library the three faces are derived from: the `mcp` extra powers
52
+ `learn mcp serve`, while the CLI/HTTP/export faces work without it. Everything
53
+ else (test + lint tooling) lives in the `dev` group. (The hand-rolled argparse
54
+ CLI under `learn/cli/` stays; agentfront backs the *parallel* App registry in
55
+ `learn/front/` that the MCP server, HTTP site, and `learn site export` read.)
51
56
 
52
57
  ```bash
53
58
  uv sync # install deps + dev tools into .venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learn-cli
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: A CLI, MCP server, and web front for learning — a hosted site where humans and agents learn a subject step by step. Fronts the french-cli and spanish-cli language tutors, and generalizes to other learnable domains such as leading teams of agents. The learn command is the local operator surface and how agents drive it.
5
5
  Project-URL: Homepage, https://github.com/agentculture/learn-cli
6
6
  Project-URL: Issues, https://github.com/agentculture/learn-cli/issues
@@ -13,6 +13,7 @@ Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Topic :: Software Development
15
15
  Requires-Python: >=3.12
16
+ Requires-Dist: agentfront[mcp]>=0.20
16
17
  Description-Content-Type: text/markdown
17
18
 
18
19
  # learn-cli
@@ -0,0 +1,76 @@
1
+ # Hosting and cost-when-busy note
2
+
3
+ This records the hosting choice for `agentculture.org/learn` and its
4
+ cost-when-busy profile, per the brief's honesty condition h8 (the hosting
5
+ choice must be recorded with a cost-when-busy note before build) and spec
6
+ decisions c21/c22.
7
+
8
+ ## Shape of the deployment
9
+
10
+ Two surfaces, deliberately split by cost:
11
+
12
+ 1. **Static shell + signed-out tour — Cloudflare Pages.** The subject catalog,
13
+ module sub-pages, sample stories, and the whole visitor tour are pre-built
14
+ static content (the `org` house style, Astro `output: 'static'`). Serving
15
+ them is free and infinitely cacheable at the edge.
16
+ 2. **Dynamic layer — the learn API Worker** (`workers/learn-api/`). Auth
17
+ sessions, the learner ledger (KV + D1), and the tutoring broker. Only
18
+ signed-in learners reach it.
19
+
20
+ The load-bearing invariant: **signed-out traffic is served entirely from the
21
+ static pages and can never reach the broker.** Sign-in is the resource gate
22
+ (spec c21). `POST /api/tutor` is the only route that spends model tokens, and it
23
+ sits behind auth middleware that runs before the route body — so a visitor who
24
+ never signs in costs nothing beyond static bandwidth (free) and cannot trigger a
25
+ model call. This is enforced in code and proven by test
26
+ (`workers/learn-api/test/worker.test.js`).
27
+
28
+ ## Cost-when-busy
29
+
30
+ Rough order-of-magnitude figures (Cloudflare pricing as of 2026; confirm
31
+ against the current plan before launch — these move):
32
+
33
+ | Component | When idle | When busy | Notes |
34
+ | --- | --- | --- | --- |
35
+ | Cloudflare Pages (static shell + tour) | free | free | Signed-out visitors, however many, cost ~0. Static assets cache at the edge. |
36
+ | Workers (the learn API) | ~$5/mo base (paid plan) | + per-request beyond the included allotment (~10M requests/mo included; ~$0.30 per additional million) | Only signed-in requests hit the Worker. CPU-time billing is negligible for this thin JSON layer. |
37
+ | KV (`SESSIONS`) | pennies | usage-priced reads/writes + storage | Only device-flow codes + revocation tombstones — tiny. |
38
+ | D1 (`learn-ledger`) | free tier covers early usage | usage-priced (rows read/written + storage) | Append-only ledger; one small row per recorded result. Reads on progress. |
39
+ | Inference (tutoring broker) | $0 | **per-use, and the dominant cost when busy** | Live tutoring tokens billed per-call through the served `cloudai-cli` / `ec2bedrock-cli` endpoint. Signed-in interactive lessons only. |
40
+
41
+ ### The dominant term when busy is inference, and only signed-in learners incur it
42
+
43
+ Everything except inference is either free (static, Pages) or cheap and
44
+ usage-metered (Workers/KV/D1 for signed-in JSON traffic). The one materially
45
+ variable cost is **live model tokens**, and by construction it is gated behind
46
+ sign-in:
47
+
48
+ - A traffic spike of signed-out visitors (a launch, a link on the front page)
49
+ scales on free static bandwidth — **zero marginal model cost**.
50
+ - Only a signed-in learner running an interactive lesson brokers a model call,
51
+ and each such call is billed per-use by the served inference endpoint
52
+ (`cloudai-cli` / `ec2bedrock-cli`), never a bespoke provider SDK.
53
+ - Pre-generated content (stories, starter lessons) is batch-produced, human
54
+ reviewed, and committed to each subject repo (spec decision on model access),
55
+ so serving it is static and free — live calls are reserved for genuinely
56
+ interactive, personalized tutoring.
57
+
58
+ ### Containing the busy-case bill
59
+
60
+ Levers available if signed-in inference volume grows:
61
+
62
+ - Rate-limit `POST /api/tutor` per learner (add a KV counter — the binding is
63
+ already present).
64
+ - Point `INFERENCE_URL` at a cheaper served model or an autoscaled
65
+ `ec2bedrock-cli` endpoint that scales to zero when idle.
66
+ - Cache/pre-generate more content so fewer interactions need a live call.
67
+ - Keep the token TTL short (already 1h) so abandoned sessions can't be replayed
68
+ to spend tokens.
69
+
70
+ ## Why this split
71
+
72
+ It honors the brief's cost discipline: the expensive resource (model tokens) is
73
+ spent only for signed-in, interactive learning; the free resource (static edge
74
+ content) carries every anonymous visitor. The dynamic layer stays thin and
75
+ Cloudflare-native (Worker + KV + D1) rather than a always-on server, so idle
76
+ cost is near the Workers base plan and busy cost tracks actual signed-in usage.
@@ -0,0 +1,262 @@
1
+ # The subject-plugin contract (v1.0)
2
+
3
+ **Status:** authoritative for contract version `1.0` · **Owner:** learn-cli ·
4
+ **Machine-readable half:** `learn/contract/schemas/*.json` (package data, importable as `learn.contract`)
5
+
6
+ This is the central design of the learn uplift: the interface a subject CLI must satisfy to be
7
+ hosted by learn-cli. It descends from the converged spec
8
+ ([`docs/specs/2026-07-10-agentculture-org-learn-is-live-one-learning-hub-in.md`](2026-07-10-agentculture-org-learn-is-live-one-learning-hub-in.md),
9
+ claims c9, c15/c17, c18, c27/c28, c29) and generalizes culture-guide's proven `teach` engine.
10
+ Everything downstream composes on it: the registry + conformance gate (t3), the french/spanish
11
+ implementations (t4/t5), the culture-guide adapter (t6), story content (t7/t8), and the
12
+ motivation layer (t9).
13
+
14
+ ## 1. The model: portal, subject, driver
15
+
16
+ Three roles, three kinds of intelligence:
17
+
18
+ - **The subject CLI** (french, spanish, culture-guide, …) is an **LLM-free engine**. It owns
19
+ committed content (stories, lessons, exercises) and per-learner mastery state, resolves *what*
20
+ to teach next within itself, and emits **structured teaching directives**. It never converses,
21
+ never grades free text, never calls a model, and **never computes scores**.
22
+ - **The driver** (an agent or human harness — Claude Code, a colleague backend, the learn API's
23
+ tutoring broker) does the conversational tutoring the directive describes: presents, explains,
24
+ quizzes, grades `pass|partial|fail` against the answer or rubric, and **writes raw results
25
+ back** via the subject's `record` verb.
26
+ - **learn-cli** (the portal) drives subjects **as external runtimes over subprocess `--json`** —
27
+ it never imports subject code — and owns the uniform **motivation layer**: numeric scores per
28
+ exercise, per-subject and cross-subject streaks, time-decayed review queues, and adaptive
29
+ what-next, all computed **deterministically** from the recorded-result history. No LLM in the
30
+ scoring path; the same history yields the same numbers on web, CLI, and MCP.
31
+
32
+ This is the **directive pattern** (culture-guide's `teach` shape, generalized): the subject emits
33
+ a directive; the driver tutors; the driver records; the subject updates mastery; learn-cli
34
+ ledgers the raw result and derives motivation.
35
+
36
+ ### State division (normative)
37
+
38
+ | Concern | Owner |
39
+ | --- | --- |
40
+ | Content: stories, lessons, exercises (committed files) | subject repo |
41
+ | Per-learner mastery ladder + item history, per subject | subject CLI (XDG-path state file) |
42
+ | Within-subject next-step recommendation | subject CLI (deterministic) |
43
+ | Conversational tutoring, grading of answers | driver |
44
+ | Raw-result ledger across subjects | learn-cli |
45
+ | Numeric scores, streaks, review queues, cross-subject what-next | learn-cli (pure functions of the ledger) |
46
+ | Cross-subject learner profile / identity | learn-cli |
47
+
48
+ Two hard prohibitions fall out of this:
49
+
50
+ 1. **Subjects never emit scores.** The `record` ack's `recorded` object structurally forbids
51
+ `score`/`grade`/`points` fields (a `not` clause in `record.json`). Subjects report raw
52
+ observations only: `result`, optional `correct`/`total` counts, `duration_seconds`.
53
+ 2. **learn-cli never contains subject content or progression logic.** Deleting a subject's
54
+ registry entry removes it from every face.
55
+
56
+ ## 2. Conventions every verb follows
57
+
58
+ - **Invocation:** `<subject-command> <verb> [args] --json [--learner <id>]`. The subject command
59
+ is a console script on PATH (e.g. `french`), declared in the registry entry.
60
+ - **`--json` everywhere.** learn-cli only consumes `--json` output. Text mode exists for humans
61
+ and is uncontracted (rubric rules still apply inside the subject repo).
62
+ - **`--learner <id>`** on every learner-scoped verb. When omitted, the subject resolves a default
63
+ (its own env var, then the OS user) and **must echo the resolved id** in the payload's
64
+ `learner` field. The learner may be a human or an agent.
65
+ - **Streams:** results → stdout, errors/diagnostics → stderr, never mixed.
66
+ - **Exit codes:** `0` success · `1` user error · `2` environment error · `3+` reserved.
67
+ - **Errors:** on any failure, stderr carries the `error.json` shape
68
+ `{code, message, remediation}` in `--json` mode (`error: …` + `hint: …` in text mode), and the
69
+ process exits with `code`. No traceback ever reaches stderr.
70
+ - **Versioning:** every payload carries `schema_version` (see §5).
71
+ - **Open payloads:** schemas validate required structure but allow extra fields, so subjects can
72
+ extend (e.g. culture-guide's `unclear_questions_for_ori`) and minor contract versions can add
73
+ optional fields without breaking older validators. Consumers must ignore unknown fields.
74
+
75
+ ## 3. The eight verbs
76
+
77
+ Schema files live in `learn/contract/schemas/`; golden example payloads (validated in CI) in
78
+ `tests/fixtures/payloads/`. "Learner-scoped" means the verb reads or writes per-learner state.
79
+
80
+ | # | Verb | Invocation | Payload schema | `kind` | Learner-scoped | Writes state |
81
+ | - | --- | --- | --- | --- | --- | --- |
82
+ | 1 | overview | `overview --json` | `overview.json` | `subject_overview` | no | no |
83
+ | 2 | progress | `progress --json` | `progress.json` | `progress` | yes | no |
84
+ | 3 | advice | `advice --json` | `advice.json` | `advice` | yes | no |
85
+ | 4 | story | `story list --json` / `story read <id> --json` | `story_list.json` / `story_read.json` | `story_list` / `story_read` | list: no · read: yes | read: may set current position |
86
+ | 5 | lesson | `lesson start [<target>]` / `lesson next` / `lesson repeat [<id>] [--harder]`, all `--json` | `lesson.json` | `lesson_directive` | yes | yes (current lesson; first exposure lifts items to `introduced`) |
87
+ | 6 | practice | `practice [<scope>] --json` | `practice.json` | `practice_directive` | yes | no |
88
+ | 7 | record | `record --item <id> --result pass\|partial\|fail […] --json` | `record.json` | `record_ack` | yes | yes (history + mastery) |
89
+ | 8 | doctor | `doctor --json` | `doctor.json` | `subject_doctor` | no | no |
90
+
91
+ ### 3.1 `overview` — the subject describes itself
92
+
93
+ Identity, description, ordered **modules** (the web face renders one sub-page per module), and
94
+ content counts. Learner-independent and deterministic, so the static site can be built from it
95
+ with zero model or state access.
96
+
97
+ ### 3.2 `progress` — where the learner stands
98
+
99
+ Per-item mastery map on the shared ladder (§4), counters
100
+ (`items_total`/`items_touched`/`items_mastered`), completed and weak item lists, timestamps, and
101
+ the subject's own `next` recommendation `{done, text, command, item_id?, module_id?}`. `done:
102
+ true` never dead-ends the learner: it is the signal for learn-cli to switch to maintenance +
103
+ depth mode (review decayed items, `lesson repeat --harder`, fresh stories).
104
+
105
+ ### 3.3 `advice` — deterministic study advice
106
+
107
+ What to shore up and why, each entry with a runnable `command`. A pure function of stored state —
108
+ the "tutor's opinion" a driver can relay verbatim. May be empty for a brand-new learner.
109
+
110
+ ### 3.4 `story` — the shared content surface
111
+
112
+ `story list` returns level-tagged summaries (id, title, level, exercise count) — enough for the
113
+ public catalog. `story read <id>` returns the **full story object exactly as committed**
114
+ (`story.json`, §6) wrapped in a directive: present paragraph-at-a-time, use the glossary on
115
+ demand, run the comprehension exercises, record each result. Unknown story id → exit 1 with the
116
+ error shape.
117
+
118
+ ### 3.5 `lesson` — start / next / repeat
119
+
120
+ The subject resolves *what* to teach — `start` (optionally targeted at a module or item), `next`
121
+ (continue from mastery state), or `repeat [<id>] [--harder]` (re-issue a completed lesson;
122
+ `--harder` increments the lesson's integer `difficulty` rung — the repeatable-lessons half of
123
+ never-ending progression). The payload carries the lesson's items (each with teachable `points`)
124
+ plus the directive (optionally with a `persona`). Item ids in the lesson are exactly what
125
+ `record --item` expects back.
126
+
127
+ ### 3.6 `practice` — exercises to run
128
+
129
+ A batch of exercises scoped to an item, a module, or `review` (no argument: the subject picks its
130
+ weakest touched items). Exercise types: `multiple_choice`, `true_false`, `cloze`, `short_answer`,
131
+ `translation`, `open`, `discussion`. Checkable types carry `answer`; open types carry `rubric`
132
+ (what passes, what is partial).
133
+
134
+ ### 3.7 `record` — the write-back (the motivation layer's input)
135
+
136
+ The driver reports one graded outcome:
137
+
138
+ ```bash
139
+ french record --learner ori --item numbers-money --activity practice \
140
+ --exercise nm-p1 --result pass --correct 1 --total 1 --duration-seconds 38 --json
141
+ ```
142
+
143
+ The subject appends the raw result to its history, updates the item's mastery (inferred from
144
+ `result` unless `--mastery` is given explicitly; inference never regresses a level), and acks
145
+ with:
146
+
147
+ - `recorded` — the normalized raw-result object `{item_id, activity, exercise_id?, story_id?,
148
+ lesson_id?, result, correct?, total?, duration_seconds?, notes?, at}`. **This object is the
149
+ contract's scoring input:** learn-cli appends it verbatim to its cross-subject ledger, and the
150
+ motivation layer (t9) computes scores/streaks/review-queues from the ledger alone. It
151
+ structurally rejects `score`/`grade`/`points`.
152
+ - `mastery` — the item's post-record level.
153
+ - `next` — the refreshed within-subject recommendation.
154
+
155
+ Result inference default (culture-guide's proven mapping): `fail → introduced`,
156
+ `partial → practiced`, `pass → mastered`, never regressing.
157
+
158
+ ### 3.8 `doctor` — self-check + contract pin
159
+
160
+ Health checks in the established doctor shape (`{id, passed, severity, message, remediation}`)
161
+ plus **`contract_version`** — the contract version this subject pins. `learn subject doctor`
162
+ (t3) reads the pin first, then validates the other seven verbs' payloads against that version's
163
+ schemas. Exit 0 when healthy, 2 when not. Recommended checks: content files validate against
164
+ `story.json`, learner-state dir writable, pinned contract version supported.
165
+
166
+ ## 4. Shared vocabularies
167
+
168
+ Exported as constants from `learn.contract` and embedded in the schemas (tests assert they
169
+ agree):
170
+
171
+ - **Mastery ladder** (ordered): `unknown → introduced → practiced → mastered`.
172
+ - **Raw results:** `pass | partial | fail`.
173
+ - **Story/lesson levels:** `beginner | intermediate | advanced` (finer grading goes in
174
+ `level_detail`, e.g. `"A1"`, `"B2"`, `"scenario"`).
175
+ - **Ids:** subjects match `^[a-z][a-z0-9-]*$`; items/stories/lessons/exercises/checks match
176
+ `^[a-z0-9][a-z0-9._-]*$`. Item ids are the join key across `lesson`/`practice`/`story`
177
+ exercises, `record`, and `progress.mastery`.
178
+
179
+ ## 5. Versioning (`schema_version`)
180
+
181
+ - The contract version is a `major.minor` string; this document and the shipped schemas are
182
+ **`1.0`** (`learn.contract.CONTRACT_VERSION`).
183
+ - **Every payload** (and every story content file) carries `schema_version`; v1 schemas pin the
184
+ pattern `^1\.[0-9]+$`, so a v2 payload fails v1 validation loudly.
185
+ - **Minor bump** (`1.0 → 1.1`): additive only — new optional fields, new enum members at the
186
+ tail. Older consumers keep validating (open payloads, §2).
187
+ - **Major bump** (`2.0`): anything breaking — removed/renamed fields, changed semantics, new
188
+ required fields. Requires new schema files and a coordinated subject migration.
189
+ - Subjects **pin** the version they satisfy in `doctor.contract_version`; each subject repo runs
190
+ the conformance gate in its own CI, so drift fails the subject's build, not learn-cli's
191
+ runtime.
192
+ - The `error.json` stderr shape predates the contract (it is the template chassis's `CliError`)
193
+ and carries no `schema_version`.
194
+
195
+ ## 6. The story schema (`story.json`)
196
+
197
+ One schema for all subjects (spec c29): language subjects ship **graded-reader ladders**;
198
+ culture-guide ships **narrative scenarios** (an agent-team incident the learner reasons through)
199
+ in the same shape. Stories are committed content files in each subject repo, validated against
200
+ this schema in the subject's CI, and embedded verbatim in `story read` payloads.
201
+
202
+ | Field | Req | Meaning |
203
+ | --- | --- | --- |
204
+ | `schema_version` | ✓ | contract version family (`^1\.[0-9]+$`) |
205
+ | `kind` | ✓ | const `story` |
206
+ | `id` | ✓ | stable slug, unique within the subject |
207
+ | `subject` | ✓ | owning subject id |
208
+ | `title` | ✓ | display title |
209
+ | `level` | ✓ | `beginner \| intermediate \| advanced` |
210
+ | `level_detail` | – | finer grading: CEFR tag or `scenario` |
211
+ | `language` | – | BCP-47 tag of the body (`fr`, `es`, `en`) |
212
+ | `summary` | – | one-line teaser, safe for the signed-out catalog |
213
+ | `body` | ✓ | the story text (markdown, blank-line paragraphs) |
214
+ | `glossary` | ✓ | glossary/annotations: `[{term, definition, note?}]` (may be empty) |
215
+ | `exercises` | ✓ | ≥1 comprehension exercises (same exercise shape as `practice`) |
216
+ | `audio` | – | **reserved slot** for the spoken-practice follow-up: `null` or `{url?, format?, voice?, duration_seconds?}`; producers omit/null until the audio pipeline exists |
217
+ | `source` | – | provenance for the batch pipeline: `{generator?, model?, reviewed_by?, generated_at?}` |
218
+
219
+ Three reference fixtures validate against it in this repo's test suite
220
+ (`tests/test_story_fixtures.py`), one per launch subject:
221
+
222
+ - `tests/fixtures/stories/french-beginner-le-marche.json` — French graded story (A1).
223
+ - `tests/fixtures/stories/spanish-beginner-la-panaderia.json` — Spanish graded story (A1).
224
+ - `tests/fixtures/stories/culture-guide-scenario-overloaded-agent.json` — culture-guide
225
+ narrative scenario.
226
+
227
+ ## 7. How a new subject registers (registration, not a fork)
228
+
229
+ Adding subject #4 requires **zero new platform code** in learn-cli:
230
+
231
+ 1. **Build a conformant CLI** — implement the eight verbs over the subject's own content and
232
+ state (the french/spanish implementation is the reference; culture-guide's adapter shows the
233
+ wrap-an-existing-engine path: `teach status → progress`, `teach lesson → lesson`,
234
+ `teach quiz → practice`, `teach record → record`).
235
+ 2. **Add a registry entry** in learn-cli's data-driven subject registry (t3) — no code, just
236
+ data: subject `id`, `display_name`, the executable `command` (console script on PATH), and
237
+ the pinned `contract_version`.
238
+ 3. **Prove conformance** — `learn subject doctor <subject>` must pass: it spawns the subject's
239
+ verbs as subprocesses, validates each `--json` payload against the pinned version's schemas,
240
+ and checks the exit-code and stream contracts. Wire the same gate into the subject repo's CI.
241
+ 4. **Ship content** — committed story/lesson files that validate against `story.json` (launch
242
+ bar: ≥10 stories across ≥3 levels for a language, ≥3 scenarios for a non-language subject).
243
+
244
+ Deleting the registry entry cleanly removes the subject from all three faces.
245
+
246
+ ## 8. Using the machine-readable contract
247
+
248
+ ```python
249
+ from learn import contract
250
+
251
+ contract.CONTRACT_VERSION # "1.0"
252
+ contract.SCHEMA_NAMES # the 11 shipped schemas
253
+ schema = contract.load_schema("story")
254
+ errors = contract.validate(payload, "story") # [] means valid
255
+ ```
256
+
257
+ Schemas are package data (`learn/contract/schemas/*.json`) loaded via `importlib.resources`, so
258
+ the installed wheel carries them — t3's conformance gate and any subject repo's CI can validate
259
+ without this repo checked out. Validation is stdlib-only (`learn/contract/_validate.py`);
260
+ runtime dependencies stay empty. The validator supports exactly the keyword subset the schemas
261
+ use, and a guard test (`test_schema_uses_only_supported_keywords`) keeps schemas inside that
262
+ subset. Cross-file references are limited to sibling schemas (`story_read.json` → `story.json`).
@@ -62,11 +62,19 @@ def _argv_has_json(argv: list[str] | None) -> bool:
62
62
 
63
63
 
64
64
  def _build_parser() -> argparse.ArgumentParser:
65
+ from learn.cli._commands import auth as _auth_group
65
66
  from learn.cli._commands import cli as _cli_group
66
67
  from learn.cli._commands import doctor as _doctor_cmd
67
68
  from learn.cli._commands import explain as _explain_cmd
68
69
  from learn.cli._commands import learn as _learn_cmd
70
+ from learn.cli._commands import mcp as _mcp_group
71
+ from learn.cli._commands import next_cmd as _next_cmd
69
72
  from learn.cli._commands import overview as _overview_cmd
73
+ from learn.cli._commands import progress as _progress_cmd
74
+ from learn.cli._commands import record as _record_cmd
75
+ from learn.cli._commands import site as _site_group
76
+ from learn.cli._commands import subject as _subject_group
77
+ from learn.cli._commands import subjects as _subjects_cmd
70
78
  from learn.cli._commands import whoami as _whoami_cmd
71
79
 
72
80
  parser = _CliArgumentParser(
@@ -88,6 +96,14 @@ def _build_parser() -> argparse.ArgumentParser:
88
96
  _overview_cmd.register(sub)
89
97
  _doctor_cmd.register(sub)
90
98
  _cli_group.register(sub)
99
+ _subjects_cmd.register(sub)
100
+ _subject_group.register(sub)
101
+ _mcp_group.register(sub)
102
+ _site_group.register(sub)
103
+ _auth_group.register(sub)
104
+ _progress_cmd.register(sub)
105
+ _next_cmd.register(sub)
106
+ _record_cmd.register(sub)
91
107
  # Register your own noun groups here:
92
108
  # from learn.cli._commands import my_noun as _my_noun_group
93
109
  # _my_noun_group.register(sub)