Skip to content

9. Task cards

One card = one branch = one PR. Ids are M<phase>-<nn>; L-<nn> are after the merge. ‖ = can run in parallel with the tasks named. Section references (§3.1…) point into this folder.

The cards of the tasks done or dropped in PROGRESS.md are in history/09-tasks-delivered.md, under the same headings and anchors; this file keeps the open ones.

The brief to hand to an agent

Replace <ID> and paste as the agent's prompt; the card carries the rest.

You implement task of the heig-classroom → Quiz merge (ADR-035). Read, in this order: CLAUDE.md, AGENTS.md, docs/merge/README.md, the row of in docs/merge/PROGRESS.md and its handoff notes, the card in docs/merge/09-tasks.md, the sections it references, and the decisions it depends on in docs/merge/08-decisions.md (stop if one is open). Classroom's code is read in a detached reference checkout (see README "Sources"); never edit ~/heig-classroom. Work in a worktree on branch merge/<ID>-<slug>. First commit: set to in progress in PROGRESS.md with your branch; push; open a draft PR titled <ID>: … whose body has a State section (done / next / blocked) that you update at every push. Follow /feature: plan, implement, run lean-reviewer and invariant-reviewer, fix. Run tests per package (AGENTS.md §7). Last commit: PROGRESS.md row to done with the PR number and a one-line handoff for the next task; add to 07-incompatibilities.md anything new you found. Do not merge: report back.

Every code task also: keeps its migrations additive (one migration per PR); adds its tables' import step and fixture rows to the import script (from M1-06 on); forwards classroom fixes newer than the sync point for the files it ports; writes en + fr for every string.


M3 — Projects

The pilot's open follow-up has no card of its own; its finding was recorded on the card M3-14 (2026-10-06), and is carried here. M3-14f, its sibling, is delivered.

M3-14g — Leaving the course staff ends one's staff seats

  • Depends on: M3-14c.
  • Note (ADR-077 Q7; product owner, 2026-10-06: keep as is for now, a follow-up): removing a member from a course's staff (ADR-068) leaves their staff seats in its classrooms, and with M3-14c their test repositories' grants. To decide: remove the seats (and revoke) with the staff seat, or keep them.

The pilot's load test (100 repositories at a deadline applied in < 5 min, the tick with 100 due projects) was deferred (product owner, 2026-10-06).

M6 — Online workspace and SEB (after the cutover if D09 says so)

M6-07 — SEB for projects

  • Depends on: M6-02, M6-06.
  • Goal: §6.3 points 2–5.
  • Acceptance: route sweep covers project routes; /launch refused in exam mode without a valid header; simulated impossible in production; proof B recorded before the first SEB project.
  • As delivered (branch merge/M6-07-seb-projects). Migrations 0080_seb_projects (Quiz) and the portal's 0002_platform_seb. Proof B is done for evaluations (2026-10-10) and pending for projects: the manual procedure is 06 §6.3, "Proof B, by hand", for the product owner; nothing opens SEB projects to students before it is recorded here.
  • Point 2, the platform builds every .seb: auth/seb.ts sendLaunchFile (ticket, auth.seb_launch, file) serves both the evaluation's and the project's (GET /app/api/projects/:id/seb, the codespace module: own portal session, a claimed seat in the classroom of a published online_seb project, sebProjectSeat). The start URL is the ticket route for both; sebAllowedHosts(config, activity) adds the portal's host for a project, then SEB_EXTRA_ALLOWED_HOSTS (new Quiz setting, host names, * wildcards but never wildcards alone, empty by default) for both — empty, the evaluation's bytes and Config Key are unchanged (seb.snapshot.test.ts untouched). The ticket route checks the header against either file before consuming the ticket, then against the ticket's own; a project's session lands on /projects/:id. What differs between the two activities is one table, SEB_ACTIVITY; the ticket's activity is read without consuming it (pendingLaunchTicket), so the header is checked against that activity's file and a wrong one never burns the ticket.
  • Point 3, one activity: sessions.project_id and launch_tickets.project_id (cascade, a check: never both with evaluation_id); SessionAuth.projectId (required, like evaluationId); the route config gains activities (absent: evaluation): serves() refuses a confined session whose activity the route does not list, so SITTING stays the evaluation's and PROJECT_SEB (GET /app/api/student/projects/:id, GET /app/codespace/start/:id) the project's; GET /me lists both and says projectId. findStudentProjectView reads THIS project for its seb session, through its seat, as a portal session would (sebProjectSession); any other confined session stays the 404. sebProjectSeat is that same loader plus the online_seb and classroom checks. Supersession and the mismatch audit are per activity (subject project). seb.db.test.ts now runs with the App and the portal configured: its sweeps walk every project route, and a project's session has its own sweep (PROJECT_SEB_ROUTES).
  • Point 4: from that session the start route mints the launch token with seb: {configKey} (the session's stored key) — workspaceStartRefusal gained fromSeb; seb_required is now "from the portal" only; the audit adds seb: true, never the key. The portal's /launch in exam mode refuses a token without the claim, then verifies the header against the claim's key (@quiz/seb), then the BEKs if any, then sets its IP-bound exam_session.
  • Point 5: BEKs optional on the portal (an empty list is the Config Key alone, the exam no longer refused without one); online_seb is synced (isOnlineMode; syncsToPortal removed) with browserExamKeys: [], the resync's seb_required lifted. BEK list: after proof B step 7 (per-student .seb likely gives per-student BEKs): SEB's BEK covers the configuration, and Quiz's file is per student (the ticket in its start URL), so a staff-typed list is expected to match nobody. The Quiz side (column, route, staff field) was written then removed at review; evaluations have none either.
  • Portal: src/seb/ keeps verify.ts (on @quiz/seb's hashes and headers, imported from there, BEKs optional) and check.ts (the proxy's cookie check, the outside-SEB page); its plist, Config Key, .seb file, /exam/:id.seb route and vectors are deleted (the vectors live in packages/seb); its SEB_EXTRA_ALLOWED_HOSTS, config_key and seb_config are gone. The PUT answers configKey: null, sebLink: null.
  • Web: the student's Open in Safe Exam Browser (SebLaunchModal, workspace copy) on an online_seb project; inside SEB the app renders that project page only (Open workspace its one action, no GitHub action, the page framing itself); the staff's Resync under SEB. en/fr; mock ?sebproject=1 and the pj-draft-manual SEB project; scenes project-workspace-seb, student-project-workspace-seb-launch, student-project-in-seb.
  • Plan B (an auto-submitted POST to the portal) not built: Quiz's CSP forbids inline scripts, and the portal would need a POST /launch; it waits for proof B's evidence (06 §6.3).

M6-08 — Freeze and collect contract on the portal (ADR-075)

  • Depends on: M6-03, M6-05. ‖ M6-06.
  • Goal: an exam mode of apps/codespace (no git channel, --network none, work volume only, per attempt); a workspace.freeze HS256 service message and a single-use signed collect endpoint (one file or a capped tar of declared names); destruction of the volume after Quiz acknowledges the hash; a safety cap; the apps/codespace CLAUDE.md records that this mode drops the git-channel divergence; contracts in packages/contracts (M6-01).
  • Acceptance: a frozen workspace refuses any write; collect is single use and refused without a valid token; no container of this mode has a network or a credential (closed list asserted); a destroyed volume is gone; integration against a stub Quiz.

M6-09 — qt-workspace type and Quiz side (ADR-075)

  • Depends on: M6-06, M6-08; D21's .seb filter (M6-07) for SEB. Kiosk use waits for proof B.
  • Goal: packages/qt-workspace through both registry entry points (04 §4.15); publication checks (workspace.requires_supervision, workspace.capacity); seed builder from toStudent's output; the deadline + 3 s freeze-and-collect job (singleton per attempt, the attempt's deadline with accommodations); blob answer with server-stamped hash; two-phase grading on the runner, proposed state; grading panel shows the file; audit events; en/fr; the code fallback swap.
  • Acceptance: a fully configured question through the seed builder, the serialized payload and file tree searched for hidden cases and the reference; a late write is impossible after the freeze; an exercise refuses the type; portal-down opening is refused with the fallback offered; invariant-reviewer finds no second exit of question content.

M8 — Migration and cutover

M8-06 — Rehearsal

  • Depends on: M8-01…05, D22, and #655 (the import in the production image, runbook O6) deployed.
  • Goal: on staging (srvstg) with the staging App and a fresh production dump of both databases: the whole of the runbook's §2, timed.
  • Steps: 10-cutover-runbook.md §7.
  • Acceptance: parity report clean; timings recorded in PROGRESS.md; go/no-go written; §2's target times filled in the runbook.
  • Carried from the delivered cards:
  • M8-01 (ADR-077 Q8, product owner 2026-10-06): this rehearsal is the import's dry run on a production dump; it COUNTS, per classroom, the staff-seat repositories classroom let its teachers accept and reports the number; the product owner decides then whether they are imported (runbook O4, §1.3).
  • M8-03: the freeze from C1 to C8 measured here decides M8-03b, the read-only flag in classroom, built only past 2 h (runbook §0).
  • M8-01: the import script's non-blocking defects from the code review of #550 (see Loose ends below) may show in the rehearsal's report.

M8-07 — The cutover

  • Who: the product owner, with an agent following the runbook (10-cutover-runbook.md §2).
  • Depends on: M8-06 go, D20, the runbook's open decisions O1–O5.

M9 — Decommission

M9-01 — Point of no return and decommission

  • Goal: §6.5 E; redirects to 308 (the runbook's §5: 308 keeps the method, 301 may not); issue #143 closed.
  • State since the cutover (2026-10-11, M8-07): classroom's app container is stopped with restart=no, its postgres and backup containers still run (the nightly dump at 19:54 UTC continues), its CI workflow is disabled, classroom.chevallier.io serves the 302 fragment, and /srv/quiz/merge holds the pre-migration dumps, the mapping, the reports and the scratch database hgc_cutover. The runbook's §5 says what leaves and in which order.
  • Documentation to update in the same PR (it describes that stack as present): docs/development/deployment.md (heig-classroom on :3000 and the VM table, the shared edu-ID key copied from /srv/heig-classroom/secrets, the backup sources and the night's timetable, the DEPLOY_HOST CNAME on classroom.chevallier.io), README.md of this directory (the reference checkout), the Classroom sync point bullet of PROGRESS.md, and the runbook's checklist. The merge directory itself stays as the record; its archiving is a later decision.

Loose ends recorded on delivered cards

Open follow-ups that are not tasks of their own; the detail is on each delivered card.

  • M3-03: the journal's targetOf (journal/writes.ts) could use classroomInstallation.
  • M3-04: storing a repository's name without its owner, still open (recorded on M3-08b's card).
  • M3-08b: a manual re-dispatch of an unconfirmed review.
  • M3-12a: the full draft edit (start, grace, grading, scale, publication mode).
  • M6-10: codespace_relays rows are not purged with their project; the daily reconciliation's head refresh does not read the relays; an App push looks pushAuthor up twice.
  • M8-01: the follow-ups of the code review of #550, not blocking (group stops, member placement, addedAt, repoMembers batching, among others); relevant to the M8-06 rehearsal.
  • M8-02: not done, the HMAC-honouring of an old unsubscribe link and an absolute Location; the non-blocking follow-ups of the code review of #547.

L — After the merge

  • L-01 Platform llm module (qt-rich, the short LLM matcher, projects). 2026-10-12: the module exists (the gateway, ADR-058; essay and diagram grading, ADR-063); its projects part is L-02 below.
  • L-02 Runner grading of a project (tarball at the frozen sha, size cap, toolchain images, network closed). 2026-10-12: decided as the criteria grading mode of ADR-106, delivered by the cards L-02a…g below.
  • L-03 packages/ui-kit extraction (what ADR-029 wanted, as a workspace package).
  • L-04 WYSIWYG journal editor (RichText adapter: repository-path images, no markdown normalisation, round-trip tests).
  • L-05 Project templates at course level.

L-02a — Criteria snapshot (ADR-106 §1, §2)

  • Depends on: ADR-106.
  • Goal: the parser and criteriaTotal in packages/domain (§2, §7's arithmetic); the criteria mode and its opt-in rule (§1); the snapshot project_criteria and Reload from source (§2); the mode's settings testsCriterion, buildCommand, testCommand, evidenceOutputs (§2, §3); the read-only criteria in ProjectSettings. Audit actions project.grading_mode, project.criteria_reload.
  • Acceptance: labo 00's criteria.yml parses and totals as StudentScore does; a malformed file is refused with its paths; the switch is refused on a published or released project, allowed on a locked unreleased one, and refused when the project's build or test command needs the network (the CI's workflow may install what it needs; amended by L-02g); a reload that fails keeps the previous snapshot; a reload that drops a checkpoint's tag is 409 checkpoint_untagged; an auto project is unchanged.

L-02b — Runner project action and the evidence run (ADR-106 §3)

  • Depends on: L-02a.
  • Goal: the runner's POST /project action and its ceilings (§3, ADR-101's amendment, 05 §5.5); the lab-c image; the project.evidence job (target commit, tarball fetch, overlay, storage, retention, pacing); the automatic criteria from JUnit (§2); the no-evidence rows no_commit, archive_too_large, repo_missing (§3).
  • Acceptance: an archive with .., an absolute path, a link or a device is refused whole; an output symbolic link is not followed and only regular text files come back, under their caps; the env list stays closed; a student's altered test is replaced by the distribution's and the distribution sha is recorded; an oversized tarball is refused with archive_too_large, a deleted repository skipped with repo_missing, a repository without a student commit gets no_commit; the codeload URL appears in no log, row or error payload, and the redaction test covers token=; the tarball's top-level <owner>-<repo>-<sha> directory is stripped before anything is stored.

L-02c — Final pass, proposals, regrade (ADR-106 §4, §6)

  • Depends on: L-02a, L-02b.
  • Goal: the purpose project and the project.review job (§4); the table project_criterion_grades in gradings' vocabulary (§4); the final pass claimed in grade_dispatches (§6); the criteria table rendered on read by one pure function (§6); Regrade per repository and per project (§4). Audit actions project.regrade, project_repo.regrade.
  • Acceptance: a name, an e-mail, a login and an identity criterion are absent from every request; each ADR-063 §2 failure yields a 0-point proposal with its reason; a regrade never supersedes a manual or a locked row; the fourth regrade of a day is refused; the pure render function's output for a rationale <script>alert(1)</script> carries it as text, never as markup.
  • Follow-up (delivered): the deterministic commit-authors check is the project setting identityCriterion (ADR-106 §2), graded by the evidence job from the commits it reads through the App — the push receipts carry no author — never by the model; refused as the tests criterion too (422 criterion_conflict).

L-02d — Milestone locks (ADR-106 §5)

  • Depends on: L-02c, L-02e.
  • Goal: a checkpoint of a criteria project is a milestone (§5): the pass claimed in grade_dispatches on the tagged criteria, the rows' locked_by_checkpoint_id; the edge cases of §5.
  • Acceptance: the final pass and a Regrade leave locked rows untouched; a teacher's correction of a locked row stays locked; a deadline moved later before the release keeps the locks and supersedes the final pass's rows and their validations; after the release it changes no validation and no released snapshot; an in-flight CI checkpoint is not converted.

L-02e — Teacher grading screen and release gate (ADR-106 §7, §8)

  • Depends on: L-02c.
  • Goal: the source criteria in FINAL_SCORE_SOURCES and resolveFinalScore (§7); the grading screen and its routes (§8); Validate per repository and in bulk; the release refused with 409 not_validated; en/fr. Audit actions project_repo.criterion_graded, project_repo.validated.
  • Acceptance: the screen is seen in the mock; a rationale <script>alert(1)</script> is shown as text on it; every write is refused to a member without the course role; release is refused while a live repository is not validated (a criterion without a validated row) and passes after the bulk validation; Validate and the bulk action answer 409 criteria_incomplete, naming the repositories and criteria, while a teacher-only criterion has no row; an unvalidated repository has no criteria score (empty, never 0: the final score falls through); a staff test repository does not block the release.

L-02f — Student criteria table (ADR-106 §9)

  • Depends on: L-02c, L-02g.
  • Goal: the criteria table on StudentProjectPage, through studentView.ts only (§9); en/fr.
  • Acceptance: N-SEC-20's tests extended (a second student's rows and rationales, a superseded rationale, a milestone rationale read before the freeze, a row's source, a row's reason and the model searched for in every response and event); an llm row is empty before a pass; every unvalidated row is marked indicative; no rationale is in the payload before the definitive freeze; a rationale <script>alert(1)</script> is shown as text.

L-02g — Labo 00 pilot (ADR-106 §1, §10)

  • Depends on: L-02c, L-02e.
  • Goal: labo 00 (heigvd-2026-prog-a/labo-prog-00, locked, unreleased) switched to criteria: its criteria.yml edited in the source repository (tolerant tests instead of repairs.txt; no prompt on the identity criterion git.commits, so it is teacher-only; rapport.presentation keeps a prompt that checks only that a name and first-name line is present, names being masked for the model), a Regrade of its 26 repositories, the result compared with the review slot; find out why review_grade_run_id is NULL on every row while its 26 review runs are ok.
  • Acceptance: every repository has proposals; the comparison is recorded in PROGRESS.md; the product owner validates and releases.