CLM v1.11.0
·
190 commits
to master
since this release
Added
- Multiple release streams per cohort (issue #291). A course can declare
several<release-channels>blocks — one per release stream, each fed by
its ownsource-target(e.g.materialsfrom asharedtarget released
before each session,solutionsfrom acompletedtarget released after
the workshop). With more than one block each needs a uniquename; channels
are addressed asSTREAM/CHANNEL(--channel materials/2026-04) across
clm release,clm git --channel/--all-channels, andclm calendar, with
bare names still accepted when unique. Derived channel repo names gain the
stream segment ({slug}-{channel}-{stream}); spec validation rejects
duplicate stream/channel names and channels sharing a destination or ledger
across streams. - Language-scoped release channels (issue #293).
<channel lang="de">
promotes only that language's files, re-rooted so the cohort repo's root is
the language directory — matching per-language distribution repos like
…/machine-learning-azav-de— and appends-{lang}to the derived repo
name.clm release sync --language de|enoverrides per invocation. A
channel withoutlangkeeps receiving every built language root (now
documented as the defined default). - Declarative GitLab access-group shares (issue #294).
<share-with access="reporter">students/azav-ml/ml-2026-04</share-with>on a<channel>
(or on the block, inherited by every channel) declares which GitLab groups a
channel repo is shared into; the newclm release provisionapplies the
shares via the GitLab API (idempotent, token from
CLM_GITLAB_TOKEN/GITLAB_TOKEN, safe no-op for non-GitLab remotes,
--dry-runpreview). Repo creation itself stays push-to-create/manual. clm gitskips release build-source targets (issue #292). Without
--target,clm git init/status/commit/push/sync/resetno longer creates
or manages repos for output targets that only feed a release stream (named
as a<release-channels source-target>), nor for targets with an explicit
distribute="false"— those are private build inputs whose content reaches
students only throughclm release sync. An explicit--target NAMEstill
selects such a target, anddistribute="true"restores wholesale
distribution for a release source that is also pushed directly.- A failing deck no longer blocks all releases (issue #295). A
whole-course build with topic-attributable errors now writes the provenance
manifest for the cleanly-built subset, excluding the failed topics' entries
and recording them (partial: true,failed_topics).clm release sync
promotes every green topic and refuses the failed ones (skip-failed,
never frozen, loud warning) until a build succeeds for them. Errors that
cannot be attributed to a topic — and timed-out,--watch, or
--only-sectionsbuilds — still suppress the manifest entirely. - Cohort viewing calendars (
clm export calendar,clm calendar check/
status) project a course's schedule onto one cohort's real calendar dates
(issue #283). Whereclm export scheduleis course-relative ("Week 3,
Tuesday"), a calendar maps the same ordered day-buckets onto actual dates
for a cohort, absorbing that cohort's holidays, delayed start, multi-week
breaks, and catch-up days. The trainer maintains only the deltas in a small
hand-editedrelease/<channel>.calendar.toml(start/end, weekly teaching
pattern, single-or-interval holidays, and orderedmerge/split/insert/
pinadjustments) beside the channel's release ledger; the per-video dates
are computed. A holiday removes a teaching date so later content slides
automatically;pins anchor a day to a date and segment the timeline, and an
over-full segment is reported with the exact "merge ≥ N buckets" deficit
rather than silently redistributed.clm export calendarrenders Markdown,
CSV, or a subscribable.icsfeed (stable event UIDs, so a re-export updates
events in place);clm calendar checkvalidates a calendar (date-free,
non-zero exit on errors);clm calendar status [--as-of DATE]shows where a
cohort is today, its plan coordinate, and the drift in days versus the ideal
plan. Seeclm info commands. clm export outline --weekdays [never|always]controls whether a
section's<subsection>weekday/name groupings are rendered as bold labels
in the Markdown outline. The default isnever: every section's decks are
flattened into plain bullets, so weeks read uniformly whether or not they
declare subsections (previously, weeks that declared subsections rendered
weekday groups while weeks that did not — and disabled weeks under
--include-disabled=merge, which ignored subsections entirely — rendered
flat, an inconsistent mix). Pass--weekdays alwaysto group decks under
their weekday/name label in every week, disabled weeks included. The JSON
format is unaffected — it always carries the grouping as a structured
subsectionsarray.- Two new
clm slides synctest oracles close the architecture review's
remaining coverage gaps (issue #289 P4):tests/slides/test_sync_non_python.py
drives the engine end-to-end on a C# (//-token) split pair for the first
time — neutral verbatim copy, id-less re-translation, add-with-insert (the
built twin must carry the// %%header family), tag mirroring, and the
neutral tag-drift alert, on both baselines — and
tests/slides/test_sync_corpus_mutation.py(slow/integration, corpus-gated
like the no-op backstop) is the corpus' first positive propagation
oracle: scripted one-sided mutations of real PythonCourses decks per
change-type (neutral edit/add, id-less localized edit, companion remove,
tag-only retag, judge-reconciled edit), asserting each is propagated to the
other half or alerted — never silently dropped — on pairs selected per
target cell class and verified post-sync-clean.
Changed
- The course-document commands
outline,schedule, andsummarizemoved
under a newclm exportgroup (clm export outline/clm export schedule
/clm export summary), and the flat top-level forms were removed (no
deprecation alias).summarizewas renamed to the nounsummary, with
clm export summarizekept as an alias. The three commands now share a
consistent option vocabulary:--include-optionaland--include-disabled
are available on all three (both off by default, so an outline/summary
that previously listedoptional="true"modules now hides them unless the
flag is given),clm export schedulegained-d/--output-dir, and
-L/--languageis the canonical spelling everywhere (schedulekeeps
--langas an alias). The MCPcourse_outlinetool is unchanged (it still
shows optional content). Seeclm info migration. --include-disablednow takes an optional value on all threeclm export
commands: a bare--include-disabled(or=marked) keeps the previous
behaviour (disabled content tagged(disabled), disabled whole sections
listed after the enabled ones inoutline/summary), while
--include-disabled=mergefolds disabled content into the normal course flow
— in declared order, with no marker — so a roadmap spec reads like a finished
course. Structured outputs (outline --format json,schedule --format csv)
keep the disabled state recorded even under=merge.clm export outline/clm export summarynow filter split-companion decks
to the requested language. When a topic ships splitslides_x.de.py+
slides_x.en.pycompanions, a-L deoutline/summary listed both the
German and the English title (and the JSONslidesarray carried both),
because the section-flat, JSON-slide, summary, and disabled-topic enumerations
skipped theoutput_language_filterthe subsection path already applied. All
enumerations now filter split companions to the requested language (via
output_language_filterfor the built course and the.de/.enfilename
suffix for disabled topics read from disk), so a split pair contributes a
single entry — matching the build's per-language routing. Bilingual decks are
unaffected.clm slides syncparity errors now name the diverging cells, instead of a
generic "a change to a shared cell was not propagated". The shared-cell error
lists the cell text present on one half but missing on the other (or the first
out-of-order cell), and the id-less-localized error points at the slide group
and cell kind — so the divergence can be located without a manual diff.clm slides syncshows progress while it runs. A directory (batch) sweep
prints a[i/N] <deck> …header per pair, and a writing run prints a short
stderr tick per LLM call (· reconciling …/· translating …) so a long
sync is visibly alive. Progress goes to stderr and is suppressed under
--json(stdout stays pure JSON).- The mitmproxy transport now records and replays a per-request response
sequence instead of collapsing a repeated request to its first response.
A non-deterministic endpoint (a temperature>0 LLM, or OpenRouter routing the
same request to different providers) answers an identical request differently
on successive calls; when a later request embeds an earlier response (e.g.
summarize | translate, wheretranslatecarries the generated summary), the
old first-seen-wins dedup dropped every response after the first, so the
downstream request matched nothing on replay and failed withclm_replay_miss.
Recording now keys dedup on(request, response)so distinct responses to the
same request are kept in order, and replay serves them in recorded order via a
per-request cursor — sticking on the last match once exhausted, so a genuinely
repeatable request never misses and a single-entry cassette still serves
repeatably (unchanged). The host-side fold gainedpreserve_sequence=True
(mitmproxy only; the vcrpy path keeps the deduped fold). Decks affected before
this fix:chains_and_lcel/slides_020,prompt_templates/slides_010,
langgraph_intro/slides_010.
Fixed
clm slides syncnow mirrors a tag-only edit on an id-less localized cell
across a concurrent slide-group reorder (issue #285). Live positional
pairing is unsound under a reorder, so the Tier C retag mirror used to
decline — silently before #289, with an error after #290. The baseline
provides a sound, reorder-invariant join instead: the drifted cell is found
by unique body hash against its own baseline (a tag-only edit never changes
the body), its baseline position indexes the twin (the two localized
streams are positional twins at the last sync, verified on the recorded
rows), and the twin's baseline hash locates it in the current, reordered
stream. The tag mirrors and the reorder applies in the same clean pass.
Anything the route cannot anchor still errors rather than guessing or
dropping: byte-identical duplicate bodies (detected via per-group tag
multisets — previously these slipped silently to the validator even in the
alert path), tags drifted on both twins, misaligned baseline streams, or a
pass that also adds/removes cells (a remove would shift the positions the
retag applier targets — sync in two steps).clm slides sync's structural pass no longer calls the translator inside
its region rebuild (issue #289 P2, completing the resolve-then-apply
redesign's last deferred follow-up). The translations a rebuild needs are
pre-resolved into the run's shared outcome cache (the same cache the add
walks use, so a deferred add's outcome is already there when the rebuild
reaches for the cell); the rebuild itself is mechanical, with the documented
inline fallback for a cache miss. Unchanged id-less cells stay on the
verbatim-reuse path (never translated). Behavior-preserving — the full
slides suite is green unchanged. The opt-in--llm-recovertier remains
inline by decision (it fires only when the deterministic id-migration is
stuck, which cannot be known before execute without simulating that tier;
it is already cached, validated, and safe-aborting) — recorded in
docs/claude/design/sync-plan-resolve-apply.md.clm slides sync's baseline plumbing is unified, and the deterministic
id-migration now also runs on a committed (git-HEAD) baseline (issue #289
P1). Both baseline sources now produce one representation (BaselineBundle,
the membership-widened watermark shape — git HEAD is re-derived through the
exact chokepoints a watermark recording uses) consumed by a single code path
for every baseline aspect: the keyed diff, the neutral / id-less / header
drift detectors, tag mirroring, and the apply engine's anchor-reuse and
id-migration passes (which now read the same rows the classifier diffed
against, carried on the plan). This deletes the per-aspect parallel git-HEAD
derivations whose coverage gaps produced the #269 silent drops and the #289
git-HEAD tag drop — the two sources can no longer diverge in coverage by
construction. User-visible improvement: thedef-my-fundrifted-id
migration (and the opt-in--llm-recovertier) previously read only the
watermark, so on the first sync of a committed pair a split id'd cell was
not re-united with its construct; it now migrates identically on both
baselines.clm slides syncno longer silently drops a one-sided tag-only edit
(issue #289, found by the sync architecture review in
docs/claude/sync-engine-architecture-assessment.md). Three tag-channel
gaps — all of which previously reported "decks already consistent" and
advanced the watermark over the divergence, permanently hiding it from
later syncs — are closed:- a tag-only edit on an id-less localized cell is now mirrored on a
committed (git-HEAD) baseline too, not only against a watermark: the
Tier C retag classifier re-derives the baseline rows + tag sets from the
committed text, so the first sync of a freshly-split committed pair
carries akeep/alttag change across; - a tag-only edit on a language-neutral (shared) cell — whose tags must
match across the halves, since a neutral cell is shared verbatim header
included — now errors (the body-hash detectors are blind to it, and
sync has no neutral-retag mirror yet): the watermark holds and nothing is
written. A combined body+tag edit still propagates both via the structural
rebuild's verbatim header copy, with no false alert. The post-apply
shared-cell parity fail-safe also compares tag sets now; - a tag-only edit on an id-less localized cell while the other half
reorders slide groups (issue #285) now errors instead of vanishing:
positional tag mirroring is unsound across a reorder, so the drift is
detected order-blind (hash-keyed against each half's own baseline) and the
watermark holds.
A new channel-coverage meta-test pins the class: every channel the sync
watermark records (body hash, tags, header, order, identity, construct — per
partition) must name a live detector or fail-safe, so a future recorded
channel can no longer ship unconsumed the way shared-partition tags did.
- a tag-only edit on an id-less localized cell is now mirrored on a
clm slides syncno longer errors when a new slide group is inserted next
to a language-neutral cell. Adding a new id'd slide (a localized markdown
cell + its following language-neutral code cells) right after a neutral cell —
e.g. aslide_id-carrying code cell with nolang=— made sync place the new
group in the wrong inter-group position on the other half and then fail with
language-neutral (shared) cells differ …/id-less localized cells … placed differently …, writing nothing. The id-carrying add path anchors a new group
only beside cells it can name by(slide_id, role), so a neutral / id-less
neighbour was skipped; the structural pass rebuilds each group's contents but
never reordered groups, so the misplacement survived as a parity error. Sync
now reconciles slide-group order against the propagation source after the
structural pass (committing only a reorder that reproduces the source's group
and(slide_id, role)order exactly), so such an insertion propagates cleanly.clm slides syncno longer silently drops (or destructively overwrites) a
one-sided edit made while the other half reorders slide groups. When one half
reordered slide groups (amove) while the other half independently edited a
language-neutral (nolang=) or id-less-localized cell, the two changes flowed
in opposite directions. The reorder permutes the source half's neutral/id-less
cell sequence, which the positional drift detectors misread as a "drift" —
masking the target half's edit. The run reported the decks consistent and
advanced the watermark while the edit was lost from both halves; for two or
more reordered neutral cells the positional shift was even misclassified as a
same-cell divergence and auto-healed, overwriting the edit on disk (Issue
#282). Because a group reorder makes positional pairing unsound, sync now
errors whenever one half reorders slide groups while the other half has any
unreconciled neutral / id-less change (an edit, add, remove, or cross-group
reassignment), holding the watermark and leaving both halves untouched on disk so
the change is preserved. A neutral edit applied identically to both halves
alongside a reorder still merges cleanly (the halves agree, so nothing is lost).
Reconcile a genuine conflict by hand (apply the reorder and the edit on the same
half, or sync them in separate steps) and re-run.