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 ofin docs/merge/PROGRESS.mdand its handoff notes, the cardin docs/merge/09-tasks.md, the sections it references, and the decisions it depends on indocs/merge/08-decisions.md(stop if one isopen). Classroom's code is read in a detached reference checkout (see README "Sources"); never edit~/heig-classroom. Work in a worktree on branchmerge/<ID>-<slug>. First commit: setto in progressinPROGRESS.mdwith 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, runlean-reviewerandinvariant-reviewer, fix. Run tests per package (AGENTS.md§7). Last commit:PROGRESS.mdrow todonewith the PR number and a one-line handoff for the next task; add to07-incompatibilities.mdanything 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;
/launchrefused in exam mode without a valid header;simulatedimpossible in production; proof B recorded before the first SEB project. - As delivered (branch
merge/M6-07-seb-projects). Migrations0080_seb_projects(Quiz) and the portal's0002_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.tssendLaunchFile(ticket,auth.seb_launch, file) serves both the evaluation's and the project's (GET /app/api/projects/:id/seb, thecodespacemodule: own portal session, a claimed seat in the classroom of a publishedonline_sebproject,sebProjectSeat). The start URL is the ticket route for both;sebAllowedHosts(config, activity)adds the portal's host for a project, thenSEB_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.tsuntouched). 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_idandlaunch_tickets.project_id(cascade, a check: never both withevaluation_id);SessionAuth.projectId(required, likeevaluationId); the route config gainsactivities(absent: evaluation):serves()refuses a confined session whose activity the route does not list, soSITTINGstays the evaluation's andPROJECT_SEB(GET /app/api/student/projects/:id,GET /app/codespace/start/:id) the project's;GET /melists both and saysprojectId.findStudentProjectViewreads THIS project for itssebsession, through its seat, as a portal session would (sebProjectSession); any other confined session stays the 404.sebProjectSeatis that same loader plus theonline_seband classroom checks. Supersession and the mismatch audit are per activity (subjectproject).seb.db.test.tsnow 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) —workspaceStartRefusalgainedfromSeb;seb_requiredis now "from the portal" only; the audit addsseb: true, never the key. The portal's/launchin 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-boundexam_session. - Point 5: BEKs optional on the portal (an empty list is the Config Key
alone, the exam no longer refused without one);
online_sebis synced (isOnlineMode;syncsToPortalremoved) withbrowserExamKeys: [], the resync'sseb_requiredlifted. BEK list: after proof B step 7 (per-student.seblikely 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/keepsverify.ts(on@quiz/seb's hashes and headers, imported from there, BEKs optional) andcheck.ts(the proxy's cookie check, the outside-SEB page); its plist, Config Key,.sebfile,/exam/:id.sebroute and vectors are deleted (the vectors live inpackages/seb); itsSEB_EXTRA_ALLOWED_HOSTS,config_keyandseb_configare gone. The PUT answersconfigKey: null, sebLink: null. - Web: the student's Open in Safe Exam Browser (
SebLaunchModal, workspace copy) on anonline_sebproject; 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=1and thepj-draft-manualSEB project; scenesproject-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); aworkspace.freezeHS256 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; theapps/codespaceCLAUDE.mdrecords that this mode drops the git-channel divergence; contracts inpackages/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
.sebfilter (M6-07) for SEB. Kiosk use waits for proof B. - Goal:
packages/qt-workspacethrough both registry entry points (04 §4.15); publication checks (workspace.requires_supervision,workspace.capacity); seed builder fromtoStudent'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,proposedstate; grading panel shows the file; audit events; en/fr; thecodefallback 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-reviewerfinds 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
appcontainer is stopped withrestart=no, itspostgresandbackupcontainers still run (the nightly dump at 19:54 UTC continues), its CI workflow is disabled,classroom.chevallier.ioserves the 302 fragment, and/srv/quiz/mergeholds the pre-migration dumps, the mapping, the reports and the scratch databasehgc_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:3000and the VM table, the shared edu-ID key copied from/srv/heig-classroom/secrets, the backup sources and the night's timetable, theDEPLOY_HOSTCNAME onclassroom.chevallier.io),README.mdof this directory (the reference checkout), theClassroom sync pointbullet ofPROGRESS.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 useclassroomInstallation. - 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_relaysrows are not purged with their project; the daily reconciliation's head refresh does not read the relays; an App push lookspushAuthorup twice. - M8-01: the follow-ups of the code
review of #550, not blocking (group stops, member placement,
addedAt,repoMembersbatching, 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
llmmodule (qt-rich, theshortLLM 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
criteriagrading mode of ADR-106, delivered by the cards L-02a…g below. - L-03
packages/ui-kitextraction (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
criteriaTotalinpackages/domain(§2, §7's arithmetic); thecriteriamode and its opt-in rule (§1); the snapshotproject_criteriaand Reload from source (§2); the mode's settingstestsCriterion,buildCommand,testCommand,evidenceOutputs(§2, §3); the read-only criteria inProjectSettings. Audit actionsproject.grading_mode,project.criteria_reload. - Acceptance: labo 00's
criteria.ymlparses 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 is409 checkpoint_untagged; anautoproject is unchanged.
L-02b — Runner project action and the evidence run (ADR-106 §3)¶
- Depends on: L-02a.
- Goal: the runner's
POST /projectaction and its ceilings (§3, ADR-101's amendment, 05 §5.5); thelab-cimage; theproject.evidencejob (target commit, tarball fetch, overlay, storage, retention, pacing); the automatic criteria from JUnit (§2); the no-evidence rowsno_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 witharchive_too_large, a deleted repository skipped withrepo_missing, a repository without a student commit getsno_commit; thecodeloadURL appears in no log, row or error payload, and the redaction test coverstoken=; 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
projectand theproject.reviewjob (§4); the tableproject_criterion_gradesingradings' vocabulary (§4); the final pass claimed ingrade_dispatches(§6); the criteria table rendered on read by one pure function (§6); Regrade per repository and per project (§4). Audit actionsproject.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
manualor 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
criteriaproject is a milestone (§5): the pass claimed ingrade_dispatcheson 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
criteriainFINAL_SCORE_SOURCESandresolveFinalScore(§7); the grading screen and its routes (§8); Validate per repository and in bulk; the release refused with409 not_validated; en/fr. Audit actionsproject_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 answer409 criteria_incomplete, naming the repositories and criteria, while a teacher-only criterion has no row; an unvalidated repository has nocriteriascore (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, throughstudentView.tsonly (§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 tocriteria: itscriteria.ymledited in the source repository (tolerant tests instead ofrepairs.txt; noprompton the identity criteriongit.commits, so it is teacher-only;rapport.presentationkeeps 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 whyreview_grade_run_idis NULL on every row while its 26 review runs areok. - Acceptance: every repository has proposals; the comparison is
recorded in
PROGRESS.md; the product owner validates and releases.