🧪 Lightcodelab

🎓 Classroom — HQ cockpit

The classroom infrastructure, from zero to ready. The cockpit guides what only a human can click, checks it happened, and does the rest. A session is a private hub repo + a team of the same name; adding a learner to a session grants that team — never blanket org access. Full model in hq/RUNNER_DESIGN_20260718.md.

Classroom cockpit

Sessions & roster

Unlocks once the cockpit above is green. The roster shows all members until you click a session in the top grid — then it narrows to that session. Create / delete a session, add / remove a member via the buttons.

How membership works — one gesture. You manage people per session, never org-wide. “Add member” puts a learner on that session’s team; if they aren’t in the org yet, GitHub sends them an org invite automatically — you never add them to the org by hand. Until they accept they show ⏳ invited; once accepted they are an org member and on the session team, showing ✅ team. So the flow is always: add to a session → (auto org-invite if needed) → accept → on the team.

📋 Invitations — what to press, and where

Every one of these starts the same way: open this page, and click the session row in the top grid (Session · Benches · Team). The member buttons under the roster only wake up once a session is selected.

A student who already has a GitHub account

  1. Click ➕ Add member.
  2. Type their GitHub username (no @), press Add.
  3. They appear as ⏳ invited, and as ✅ team once they accept.

A student with no GitHub account yet — the Canvas case

  1. Click ✉️ Invite by email.
  2. Paste their address — one per line for a whole group — press Send.
  3. Tell them the two things that decide whether it works: sign up with that same address, and do it within 7 days.
  4. They appear as ⏳ invited · 0d.

They say they never got it

  1. Tell them first: they do not need the mail. Sign in to GitHub, open github.com/orgs/<your-org>/invitation, and it is there to accept.
  2. Still stuck? Click their ⏳ invited row in the roster.
  3. Click 🔁 Re-send this one (the button renames itself once a waiting row is selected), press Send again. Fresh mail, and the 7 days restart.

Several have gone quiet, or the week is over

  1. Select the session, click nothing in the roster.
  2. Click 🔁 Nudge pending. It lists everyone still waiting with their age, and marks the ones past 7 days as lapsed.
  3. Press Send again. Only those people get mail — anyone who already joined is a member, not a pending invitation, so they are not in that list at all.

Someone dropped the class

The whole Canvas roster at once — no typing, no command line

  1. Once, before the first run: put the Canvas course number in the course’s __course.yml (canvas_course: …, beside its vault line), and the two secrets in the lab repo (Settings → Secrets and variables → Actions): CANVAS_TOKEN (Canvas → Account → Settings → + New Access Token) and the existing ORG_VAULT_TOKEN, whose grant must also cover Organization permissions → Members: read and write.
  2. Click 🎓 Roster invite, keep Plan, press Run. Nothing is sent.
  3. Keep the dialog open: the verdict — who would be invited, who is skipped and why, timestamped — arrives right there when the run finishes (about a minute).
  4. Happy? Click 🎓 Roster invite again, pick Invite, press Run — the send report arrives the same way. Members and pending invitations are skipped, so re-running after add/drop only fills the gaps — nobody gets mailed twice.

The roster passes through the runner’s memory and is gone — nothing is copied into any file. (tools/course_invite.py is the same machinery from a terminal, if you ever want it.)

A quiz into Canvas — same pattern, same keys

  1. Draft and lint the spec on the quiz desk until it shows no tells — a guessable or greppable key refuses to travel.
  2. Click 📝 Quiz → Canvas here, check the spec path, press Push — the verdict (lint, write, comment counts) arrives in the dialog.
  3. Open the quiz in Canvas — it arrives unpublished, and is published there, by you, after you read it.

A learner can only fork the (private) hub once they’re on the team, so a fork always implies access was granted at some point. If someone shows ⚠️ removed, they were on the team, forked their bench, then were removed — their fork lingers but access is gone.

Classroom console

sessions

session actions

members of the selected session

member actions

📊 Gradebook

📊 Gradebook reads every bench’s own __progress.txt — quizzes answered, features turned green — and shows it here. Press it again to keep the reading: it appends a stamped line per learner to <org>-vault/gradebook/<session>.csv, a repo learners read and cannot write. That series is the record; their file is their working copy.

Two flags come back with each reading. ✍️ hand-edited means the last commit on that file was made through github.com (committer GitHub), not by the app with the learner’s own key. ⚠️ crc means the file’s checksum stopped matching. Neither blocks anything — it is their repo — but neither is silent, and a jump from nothing to everything between two readings is visible in the series.

gradebook

Feature: The classroom console drives sessions with master–detail grids
  As an educator
  I want one console over the platform's own grids
  So that selecting a session shows its members, with modal actions

  Scenario: The console and its two grids render
    Given the classroom page I am on
    :::python
    self.console: list = Object._all(".lc-console, .lc-con-bar")
    self.grids: list = Object._all(".lc-datagrid")
    :::
    When the engine has loaded
    Then the action bars and both master and detail grids are present
    :::python
    assert self.console, "console controls missing"
    assert len(self.grids) >= 2, len(self.grids)
    :::

🖼️ Framing a course page in Canvas

Two flags decide what a learner gets inside an LMS iframe. They ride every same-origin hop, so the scope you set is the scope they stay in.

Flag What it does
?crumb=BUILD-AI The bar stops being a menu: one read-only line — course › module · page — plus the learner’s own avatar. No home link, no menu, no start pill, and the runner drops the file-path chip
?up=0 The folder’s ⬆️ Up pill disappears, so a frame scoped to one module has no door out of it
?strict=1 Every prerequisite in the frame also asks for the pages’ proofs, not only their points — see below

Both are optional and independent:

scoped to one module   /run.html?crumb=BUILD-AI&up=0#src=gh:<vault>/courses/micro_build_ai/module_06/index.md
open to the course     /run.html?crumb=BUILD-AI#src=gh:<vault>/courses/micro_build_ai/index.md

The module’s title in the crumb comes from that module’s own index.md, read once per session — never the folder name. Both flags ride every hop, so a learner clicking from the course cover into module 6 stays in the same scope; and in crumb mode the account chip is a statement, not a menu — no dropdown.

🔒 Gating a page on the proofs, not only the points

A prerequisite opens on what the learner earned on the page before. Add features="true" and it also asks whether that page’s proofs are green in their own record — the stricter reading of “done”, so nobody builds on a lesson they only read.

Two levels, and the local one wins:

Where What it does
?strict=1 on the frame’s URL every prerequisite in that scope asks for the proofs — a whole course walked the strict way, with no page edited
{: .prerequisite features="true" } this gate asks for the proofs, wherever it is
{: .prerequisite features="false" } this gate asks for the points only, even inside ?strict=1
{: .prerequisite features="true" pass="50" } half the points, and all the proofs

Where the page can be read — a course page rendered from a vault — the count is against the features it declares, so running one proof and skipping three is not passing. A strict gate stays closed while it asks: it never shows the page and then takes it back.

Use it where the next page builds directly on a working artefact. Leave it off elsewhere: a gate nobody can open teaches that gates are decoration.

Edit page Modes
📋 · 0/0

Edit

⚙️ engine 30259e4