--- name: course-build description: Use for building and syncing Canvas course CONTENT — mirroring a course to/from local files (canvas_sync pull/push/build), master→blueprint→section propagation (blueprint_sync), module prerequisites/completion settings (module_settings_sync), multi-course orchestration, and offline (.imscc) editing. Load this when the task is "make Canvas match the local source" or "propagate content across sections" — NOT grading and NOT read-only auditing. --- # Course Build (CI / sync) Manage Canvas course content as code: mirror to local files, edit locally, and push approved changes back through the Canvas REST API. > The **constitution** (AGENTS.md) binds you: confirm scope before any write, and > Canvas writes go through the toolkit (never a hand-rolled API call). This skill is > the content-sync *how*. ## Core principle — local is the source of truth The local working folder is authoritative; **Canvas is the sync target.** Pull to mirror, edit locally, push to apply. Canvas has no version history — the local files + git are your history. **This holds only if changes are made through the toolbox.** Faculty who also edit in Canvas by hand break the chain. The rule for the instructor, in plain language: 1. **Preferred:** make changes locally (or ask the agent to) and push. Canvas stays in step with the local files. 2. **If they edited in Canvas anyway:** `canvas_sync.py --pull` *before* any local edit or push. The conflict rule is local-wins, so pushing stale local files erases the Canvas edits. 3. **Preserve it:** once the course is in its final state, push it to the **Master** course (and onward via `blueprint_sync`). Master is a second, Canvas-side source of truth beside the git repo. A course lives in two places, and neither is the live section students are using. ## One course per session **One course = one folder = one `.env` = one IDE window.** At session start, read `.env`, state which course ID(s) are in scope, and act on nothing else. - A request that names or implies another course, or a working folder or `.env` that doesn't match: **stop and ask.** Do not switch, and do not "also" update the other course. - A folder that appears to hold several courses' config: tell the instructor it is unsupported and that each course needs its own folder and window. - If the instructor teaches more than one course, **recommend `~/.canvas/config`** in their user profile with `CANVAS_API_TOKEN` and `CANVAS_BASE_URL` only (`chmod 600`; `_env_loader` reads it as a fallback). Each course keeps its own `CANVAS_COURSE_ID` in that folder's `.env`. A course ID in the global file is ignored on purpose, so a repo with a missing `.env` can't inherit the last-configured course. `cb_secure_token.py` does the move (dry run, then `--apply`): it verifies the token, writes the global file, and deletes the token and base URL from `.env`. It is the only sanctioned reader of the secret; never read or paste the token yourself. - This is the soft layer on top of `canvas_course_guard`; restate course + target (master / blueprint / section) before every write. ## Dropped connections Canvas calls sometimes drop, especially in long tasks. A request that dropped may have already succeeded. 1. **Never blindly re-run a write** (creating an announcement, assignment, page). First read back state, matching by title, to see whether it landed. 2. Resume only the steps verified as not done. Never replay a whole batch. 3. Retry transient failures a bounded number of times with a pause, then **stop and report** instead of looping. 4. Prefer the local-first flow (pull, edit, push) so most work never touches the live API. 5. End an interrupted task with a plain done / not done / unsure summary. ## The multi-course model | Term | What | `.env` id | Folder | |---|---|---|---| | **Master** | Template course where authoring happens | `MASTER_COURSE_ID` | `master/` (or `course/` legacy) | | **Blueprint** | Canvas Blueprint sections clone from (online programs) | `BLUEPRINT_COURSE_ID` | `blueprint/` | | **Section** | Live per-semester course (S1, S2…) | `S1_COURSE_ID`, … | `s1/`, `s2/` | **Confirm scope before every write** — master vs blueprint vs section. A stale `.env` can silently target the wrong course (Canvas API lesson L12); `canvas_course_guard` defends against it, but naming the target explicitly is on you. ## Tools | Task | Tool | |---|---| | Mirror one course (pull/push/build) | `canvas_sync.py --pull` / `--push` / `--build` | | Master → Blueprint propagation | `blueprint_sync.py` | | Validate a blueprint sync | `validate_blueprint_sync.py` + `blueprint_exception_report.py` | | Module prerequisites / completion | `module_settings_sync.py` | | Create a custom grading scheme, optionally attach it to assignments | `grading_scheme_setup.py --preset performance-tiers [--assignment-ids 1,2] --apply` (or `--title ... --tiers ...`) | | Create a peer-review assignment + peer rubric (unpublished) | `peer_review_setup.py --title ... --criteria "Prepared?:yesno;Effort:1-5" --apply` — enrolled course needs `--allow-enrolled`, only on the instructor's explicit say-so | | Pair group members as peer reviewers of each other (counts-only output) | `peer_review_assign.py --title ... --group-set-id N --apply` — same `--allow-enrolled` rule; re-run after late submissions or group changes | | Report per-student peer rating averages (read-only, user_id-keyed, no names) | `peer_review_summary.py --title ... [--csv out.csv]` | | Multi-course wrapper | `sync_context.sh` | | Post-push structural audit | `course_quality_check.py` *(then hand off to the `audit` skill)* | **Always run `course_quality_check.py` after a push** to catch duplicates, floating items, and date-window problems the write may have introduced. ## Canvas API write lessons that bite content sync The full catalog is [`lib/agents/knowledge/canvas_api_lessons_learned.md`](../../lib/agents/knowledge/canvas_api_lessons_learned.md). The ones that matter here: - **Module prerequisites & published state are FORM-ENCODED, not JSON** (L1, L2) — a JSON payload silently no-ops. - **Date writes need the `due_at`/`lock_at`/`unlock_at` trio** (L3); discussions use `todo_date`, not `due_at` (L7). - **`late_policy` PATCH is admin-only** — 403 for teacher tokens (L4). - **Classic quizzes have two IDs** (`quiz_id` + `assignment_id`); `points_possible` reads 0 until questions are pushed (L5, L6). - **NewQuiz / ExternalTool items use a separate REST surface** (L8) — the toolkit currently pulls sidecars and has sandbox-proven CRUD coverage, but production sync writes are still a separate roadmap item; do not assume `canvas_sync.py` can push them yet. - **Blueprint resync can spawn `-N` slug orphan Pages and silently revert section page bodies** (L13, L14) — `blueprint_orphan_pages.py` detects both. - **Page title collisions auto-suffix** `-2`/`-4` (L15). Adding content is a **two-step** operation (course item + module item) — a page/ assignment isn't visible to students until it's also a module item. ## Offline mode (v1.7) Tools read a local `course/` (from `canvas_sync --pull` or `offline_import` of a `.imscc`) and run identically online/offline. The `.imscc` is the source of truth; `course/` is the working folder. `imscc_record` patches your `course/` edits back into the sidecar `.imscc` faithfully (quiz questions / files / LTI preserved byte-for-byte) for re-import via the Canvas UI. ## Quick command map | Ask | Command | |---|---| | "Pull the course from Canvas" | `canvas_sync.py --pull` | | "Push changes to Canvas" | `canvas_sync.py --push` | | "Sync master → blueprint" | `blueprint_sync.py` | | "Fix module prerequisites" | `module_settings_sync.py` |