# Project export: Wormie IDE

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Instead of working with AI, teachers have been battling it, tracking down AI code in student assignemts. Wormie IDE allows students to work with AI, but drives to learn as it writes their code.
- Devpost: https://devpost.com/software/wormie-ide
- GitHub: https://github.com/aaditaggarwal26/Wormie-IDE.git
- Video: https://www.youtube.com/embed/1we4XIQexXo?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Aadit Aggarwal (47 commits), lamajoqeri (41 commits), Ryan Panda (16 commits)

## Devpost submission (written by the team)

### Inspiration

Just one year ago, we were high school students, excitedly taking CS classes. However, the CS curriculum has not adapted to the AI revolution. The class became a game of who could take the most advantage of AI without the teacher noticing. To end this cat and mouse chase, and instead introduce schools and classes to the age of AI, we built Wormie.

### What it does

Wormie has two core components - Wormie Classrooms, and the Wormie IDE. Wormie Classrooms is an easy way for teachers to assign computer science projects and assignments. It starts with creating a classroom, which students can then join with an invite code. Teachers upload an incomplete codebase, which the student has to work with, then add specfic tasks that the student must complete. This assignment is automatically given to the students, who can then see their own versions, and complete the code in the Wormie IDE. There, they also have access to our key feature, Wormie Agent. It functions similarly to Copilot within VSCode, but with the crucial difference that it doesn't give free edits and answers. Once a prompt is submitted, the agent forms an appropriately sized mini reading lesson and short quiz to test that the student has an idea of what is going on. If the student passes the quiz accuracy threshold, which can be set by the teacher, then Wormie Agent goes ahead and edits the code within the tight bounds of the student's prompt, so as to not give the student unnecessary help. The student is forced to review each diff before fully accepting the changes.

### How we built it

We built Wormie as a cross-platform Electron application using React and TypeScript. The editor is powered by Monaco, with Zustand for local state, React Query for asynchronous workflows, Tailwind CSS and custom CSS for the interface, and Framer Motion for interaction polish. The Electron main process handles filesystem access, Git operations, terminal sessions, authentication, AI requests, and security-sensitive validation. The renderer communicates with it through a secure IPC bridge with context isolation enabled. For AI capabilities, we integrated the OpenAI Codex API with structured model outputs, streaming activity, workspace context, and proposal generation. We validate model responses with schemas before they reach the user or the filesystem. Wormie Classrooms uses Supabase for authentication, classroom membership, assignment publishing, and teacher-student workflows. Local project data and assignment state are kept close to the workspace so the IDE remains fast and resilient. Throughout development, we treated workspace files and model output as untrusted input. We added path validation, secret redaction, protected-file rules, assignment policies, diff review, and tests around the most important boundaries.

### Challenges we ran into

The hardest challenge was making AI helpful without making learning optional. A normal coding assistant can generate an answer immediately, but Wormie needed to create a meaningful pause for understanding without making the experience feel frustrating. We solved this with structured learning sessions, local quiz grading, adaptive remediation, and a clear proposal review step. The AI can explain concepts and prepare a plan, but code generation stays behind an understanding gate. We also had to make the AI agent safe around real workspaces. Model output can contain invalid paths, unsafe edits, secrets, or assumptions based on incomplete context. We built validation and review systems so the AI can propose changes without silently overwriting files. Another challenge was building a complete classroom workflow alongside a desktop IDE. Authentication, invitation codes, assignment packaging, isolated student workspaces, progress tracking, and teacher review all had to work together as one product.

### Accomplishments we're proud of

Built an end-to-end classroom workflow for teachers and students. Created a learning-first AI coding assistant instead of another code autocomplete tool. Added concept lessons, adaptive quizzes, confidence scoring, and remediation. Added reviewable AI proposals with accept, reject, and partial-change controls. Built protected filesystem and IPC boundaries for safer AI interaction. Added Ask, Plan, and Agent modes directly inside the coding composer. Added support for Codex accounts and OpenAI-compatible model providers. Created a polished, cross-platform Electron IDE experience. Developed a system that helps teachers introduce AI without abandoning the educational goals of programming classes.

### What we learned

We learned that building an AI product is not only about model quality. The surrounding workflow matters just as much: context gathering, structured outputs, validation, permission boundaries, feedback, and user trust all determine whether the system is useful. We also learned that educational guardrails have to be part of the product architecture, not just a message in the interface. If understanding is important, it needs to be represented in the data model, enforced in the backend, and reflected in the user experience. Building Wormie also taught us a lot about Electron security, secure IPC, filesystem handling, authentication, Git workflows, and designing interfaces that make complex AI behavior understandable.

### What's next

Next, we want to make Wormie even more adaptive and useful for both students and teachers. Planned improvements include: A persistent knowledge graph that tracks mastery and prerequisite concepts. More adaptive quizzes based on each student’s mistakes. Challenge mode, where students implement the solution themselves before comparing it with AI. Reverse-engineering mode for understanding existing codebases. Better teacher dashboards with progress, misconceptions, and concept-level analytics. More assignment templates and classroom collaboration tools. Offline-first workflows for schools with unreliable connectivity. More AI providers, models, and agent integrations. Improved accessibility and support for more languages and learning styles.

## README (from the GitHub repository)

# Wormie IDE

<img src="build/icon.png" alt="Wormie logo" width="140">

Wormie is an Electron desktop IDE built around one rule: understand first, generate second.

## Built with Codex and GPT-5.6

We treated Codex as an active engineering partner throughout Wormie’s development, not as the source of the product idea or final decision-maker. We set the learning-first product direction, chose what behavior felt right, defined the security and privacy boundaries, and reviewed the resulting experience. Codex helped turn those decisions into working, tested software much faster than we could have built each cross-cutting system alone.

| Area | Decisions we made | How Codex accelerated the work |
| --- | --- | --- |
| Product | Code generation must follow understanding, but routine edits should remain fast. Major-change gates, remediation, challenge mode, and configurable strictness came from that balance. | Codex traced the complete request-to-proposal flow, converted product rules into typed state machines and services, and kept lessons, quizzes, grading, mastery evidence, and proposal review connected. |
| Architecture and security | Wormie is local-first, Electron renderers are untrusted, IPC stays narrow, secrets never enter the renderer, and classroom authority must be verified outside client-provided state. | Codex implemented and reviewed main/preload/renderer boundaries, path validation, secret redaction, secure credential storage, file fingerprints, recovery behavior, Supabase policies, and adversarial tests. |
| Learning system | We chose a persistent knowledge graph, prerequisite detection, spaced review, misconceptions, goals, and evidence-based mastery instead of a generic chatbot memory. | Codex helped model these systems, connect assessment evidence to personalization and review scheduling, and cover migrations and edge cases with tests. |
| UX and design | We chose explicit Sandbox, Classroom, and Assignment modes; a focused IDE layout; review-before-apply controls; restrained motion; and accessibility that never communicates status through color alone. | Codex built and refined the React, Monaco, xterm.js, and Framer Motion interactions, including resizable panels, recovery dialogs, diff review, command navigation, keyboard controls, and accessible review states. |
| Quality | We made final scope and tradeoff calls and rejected behavior that weakened safety, clarity, or the “understand first” principle. | Codex searched existing flows before editing, made coordinated changes across contracts and process boundaries, diagnosed failing checks, and repeatedly ran Vitest, TypeScript, and production builds until the implementation passed. |

GPT-5.6 was especially useful during the final engineering passes because it could reason across a large TypeScript/Electron codebase while preserving interactions between the renderer, preload bridge, main-process services, local persistence, cloud synchronization, and tests. It accelerated repo-wide refactors and caught failure modes such as stale asynchronous responses, unsafe paths, external file changes, untrusted AI output, missing authorization checks, and accessibility inconsistencies.

Codex also shortened the design loop. We could describe the intended behavior, inspect a concrete implementation, test it in the running product, and give targeted feedback in the same session. GPT-5.6 then revised the smallest relevant surface instead of restarting the feature. That loop let us spend more time on product judgment, teaching quality, and interaction details while still understanding and approving the code being shipped.

All Codex-generated work remained subject to human review and the same verification as handwritten code. The final result reflects our product choices and design taste, with Codex and GPT-5.6 contributing implementation speed, breadth, consistency, and a much tighter test-and-refine cycle.


## Run locally

```powershell
npm install
npm run dev
```

Use `npm run build` for a production build and `npm run dist` to create the desktop installer.

## Product modes

After sign-in, Wormie opens a launcher with two explicit destinations:

- Sandbox IDE is an ordinary coding workspace. It contains the editor, Explorer, search, source control, terminal, Tutor, and IDE settings. Opening a folder here never attaches it to a classroom, even if the folder contains an assignment manifest.
- Classrooms is a full-screen portal for teaching and enrolled classrooms. It contains assignments, people, classroom mastery, and teacher settings without editor or terminal chrome.

Opening a classroom assignment or starting a teacher draft launches Assignment IDE. This mode keeps the coding tools and one focused assignment context. Returning to the classroom uses the existing dirty-file guard before leaving the workspace.

The renderer stores only validated portal selection preferences. It does not restore directly into a classroom or assignment after restart. Editor recovery remains independent and workspace-scoped.

## Assignment workflows

For classroom assignments, a teacher authors a starter workspace from the classroom portal and publishes its integrity-checked package to private Supabase storage. A student opens the assignment from that classroom, and Wormie creates or safely reopens an isolated local workspace. Task progress is stored locally first and synchronized through a bounded, versioned retry queue. Once every task is complete, the student submits directly to the classroom. The teacher can review status, assignment-scoped AI-use summaries, and uploaded submission files from the classroom portal.

Wormie also retains a local package workflow for offline or manually distributed assignments:

1. A teacher exports a `*.wormie-package.json` file from an authored assignment.
2. A student imports the package. Wormie creates an isolated copy and records explicit evidence consent.
3. After completing every task, the student saves a `*.wormie-submission.json` outside the project.
4. The teacher opens that file from the matching teacher assignment workspace.

In both workflows, the Electron main process enforces the assignment AI policy. Learning sessions, quizzes, proposals, and applied changes are recorded only when the student accepted the corresponding evidence collection.

Packages and submissions are integrity checked but are not cryptographically signed. See [docs/ASSIGNMENT_FORMAT.md](docs/ASSIGNMENT_FORMAT.md) for schemas, limits, privacy behavior, and the hosted-service migration boundary.

## Classroom cloud migrations

Supabase migrations are additive and must be applied in filename order. The product-mode work adds:

- `202607190001_classroom_roster_management.sql` for privacy-filtered member reads and teacher-authorized add/remove operations.
- `202607190002_classroom_mastery.sql` for classroom/student mastery snapshots, immutable quiz events, and Row Level Security.
- `202607210001_assignment_progress_submissions.sql` for classroom assignment progress, private submission storage, and teacher review access.
- `202607210002_assignment_progress_hardening.sql` for stricter progress validation, rollback safety, storage authorization, and assignment revision checks.
- `202607210003_assignment_ai_analytics.sql` for privacy-bounded, assignment-scoped AI usage summaries.

The desktop uses only the publishable Supabase key. Roster changes, mastery writes, AI analytics, assignment progress, and submissions go through narrow database functions or private storage policies that re-check the authenticated user, membership, classroom ownership, and assignment relationship. Failed mastery, analytics, and assignment-progress synchronization stays in bounded, versioned local queues and does not erase local history.

The Electron renderer receives only named preload methods. Workspace purpose, classroom IDs, assignment IDs, and request bodies are validated in the main process. Assignment context is derived f

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 257 recognized source files, 1545 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Tailwind CSS (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 270)

```
.gitignore
AGENTS.md
demo-video/hook/hook_captions.srt
demo-video/hook/narration.txt
demo-video/hook/render_hook.py
demo-video/hook/synthesize_voice.ps1
demo-video/remotion-hook/.gitignore
demo-video/remotion-hook/narration.txt
demo-video/remotion-hook/package.json
demo-video/remotion-hook/src/Hook.tsx
demo-video/remotion-hook/src/index.ts
demo-video/remotion-hook/src/Root.tsx
demo-video/remotion-hook/src/styles.css
demo-video/remotion-hook/tsconfig.json
docs/AI_AGENT_ARCHITECTURE.md
docs/ASSIGNMENT_FORMAT.md
docs/superpowers/plans/2026-07-14-major-change-understanding.md
docs/superpowers/plans/2026-07-15-agent-activity-and-codex-streaming.md
docs/superpowers/plans/2026-07-19-mastery-knowledge-profile.md
docs/superpowers/specs/2026-07-14-major-change-understanding-design.md
docs/superpowers/specs/2026-07-15-agent-activity-and-codex-streaming-design.md
docs/superpowers/specs/2026-07-15-dependency-xray-design.md
docs/superpowers/specs/2026-07-18-advanced-search-replace-design.md
docs/superpowers/specs/2026-07-18-quick-open-command-palette-design.md
docs/superpowers/specs/2026-07-18-safe-editing-recovery-design.md
docs/superpowers/specs/2026-07-18-typescript-intelligence-design.md
docs/superpowers/specs/2026-07-19-mastery-knowledge-profile-design.md
docs/superpowers/specs/2026-07-19-product-modes-classroom-portal-design.md
electron-builder.yml
electron.vite.config.ts
package.json
PROJECT.md
README.md
scripts/fix-pty-permissions.mjs
src/main/agent/activity.test.ts
src/main/agent/activity.ts
src/main/agent/codexAppServer.test.ts
src/main/agent/codexAppServer.ts
src/main/agent/codexTurnCapture.test.ts
src/main/agent/codexTurnCapture.ts
src/main/agent/context.ts
src/main/agent/grading.test.ts
src/main/agent/grading.ts
src/main/agent/index.ts
src/main/agent/learningDraftSchema.test.ts
src/main/agent/masteryIntegration.test.ts
src/main/agent/masteryIntegration.ts
src/main/agent/proposalEdits.test.ts
src/main/agent/proposalEdits.ts
src/main/agent/proposalReview.test.ts
src/main/agent/proposalReview.ts
src/main/agent/provider.test.ts
src/main/agent/provider.ts
src/main/agent/schemas.ts
src/main/agent/structuredOutputSchema.test.ts
src/main/agent/structuredOutputSchema.ts
src/main/agent/tutorHistory.test.ts
src/main/agent/tutorHistory.ts
src/main/agent/workspaceAgent.test.ts
src/main/agent/workspaceAgent.ts
src/main/agent/workspacePurpose.test.ts
src/main/agent/workspacePurpose.ts
src/main/assignments/activity.ts
src/main/assignments/index.ts
src/main/assignments/package.test.ts
src/main/assignments/package.ts
src/main/assignments/progress.test.ts
src/main/assignments/progress.ts
src/main/assignments/schema.ts
src/main/assignments/storage.test.ts
src/main/assignments/storage.ts
src/main/assignments/submission.test.ts
src/main/assignments/submission.ts
src/main/cloud/aiAnalyticsMigration.test.ts
src/main/cloud/aiAnalyticsSync.test.ts
src/main/cloud/aiAnalyticsSync.ts
src/main/cloud/assignmentProgressMigration.test.ts
src/main/cloud/assignmentProgressSync.test.ts
src/main/cloud/assignmentProgressSync.ts
src/main/cloud/authStoragePolicy.ts
src/main/cloud/classroomDetails.test.ts
src/main/cloud/classroomDetails.ts
src/main/cloud/config.ts
src/main/cloud/index.ts
src/main/cloud/invite.test.ts
src/main/cloud/invite.ts
src/main/cloud/masteryMigration.test.ts
src/main/cloud/masterySync.test.ts
src/main/cloud/masterySync.ts
src/main/cloud/oauth.test.ts
src/main/cloud/oauth.ts
src/main/cloud/rosterMigration.test.ts
src/main/cloud/secureAuthStorage.test.ts
src/main/cloud/secureAuthStorage.ts
src/main/editorRecovery.test.ts
src/main/editorRecovery.ts
src/main/fileIdentity.test.ts
src/main/fileIdentity.ts
src/main/fileVersion.test.ts
src/main/fileVersion.ts
src/main/fileWatchPolicy.test.ts
src/main/fileWatchPolicy.ts
src/main/git.ts
src/main/gitChange.ts
src/main/gitDiscovery.test.ts
src/main/gitDiscovery.ts
src/main/gitUnderstanding.test.ts
src/main/index.ts
src/main/ipcTrust.test.ts
src/main/ipcTrust.ts
src/main/mastery/catalog.test.ts
src/main/mastery/catalog.ts
src/main/mastery/gamification.test.ts
src/main/mastery/gamification.ts
src/main/mastery/goals.test.ts
src/main/mastery/goals.ts
src/main/mastery/graph.test.ts
src/main/mastery/graph.ts
src/main/mastery/ipc.test.ts
src/main/mastery/ipc.ts
[150 more files omitted for size]
```

### Dependencies

- demo-video/remotion-hook/package.json: @remotion/cli@4.0.496, @remotion/media@4.0.496, @types/react@19.2.7, react@19.2.7, react-dom@19.2.7, remotion@4.0.496, typescript@5.9.3
- package.json: @ai-sdk/openai-compatible@^3.0.10, @monaco-editor/react@4.7.0, @openai/codex@^0.144.4, @supabase/supabase-js@^2.110.6, @tailwindcss/vite@4.3.2, @tanstack/react-query@5.101.2, @types/node@26.1.1, @types/react@19.2.17, @types/react-dom@19.2.3, @vitejs/plugin-react@5.2.0, @xterm/addon-fit@0.11.0, @xterm/xterm@6.0.0, ai@^7.0.28, clsx@2.1.1, electron@43.1.0, electron-builder@26.15.3, electron-store@11.0.2, electron-vite@5.0.0, framer-motion@12.42.2, ignore@^7.0.6, lucide-react@1.24.0, monaco-editor@0.53.0, node-pty@1.1.0, react@19.2.7, react-dom@19.2.7, simple-git@3.36.0, tailwind-merge@3.6.0, typescript@7.0.2, vite@7.3.6, vitest@4.1.10, zod@^4.4.3, zustand@5.0.14

### Recent commits (newest first)

- Update README.md
- Merge origin/main into main
- Improve assignment workflow and add demo assets
- Update README.md
- Revert "Personalization/Acessibility Features"
- new logo
- Update README.md
- Harden classroom assignments and judge release
- Personalization/Acessibility Features
- Merge remote-tracking branch 'origin/main'
- Improve autosave and classroom assignments
- Add Tutor recovery and classroom AI analytics
- ui update
- Merge branch 'codex/integrate-active-20260719'
- Merge remote-tracking branch 'origin/main' into codex/integrate-active-20260719
- Fix tutor readability and assignment reopening
- Integrate active Wormie branches
- Merge dependency x-ray design proposal
- Merge mastery knowledge profile
- Merge current main into product modes

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
# Project Instructions

Read `PROJECT.md` before planning or modifying this project. Treat it as the canonical product brief and use its core principle, "Understand first. Generate second.", to guide product and technical decisions.

```

### PROJECT.md

```markdown
# Learn Before You Code

## Product

Learn Before You Code is a production-ready, cross-platform Electron desktop IDE. It should feel as polished and capable as a modern AI IDE while enforcing an educational workflow that helps developers understand concepts before AI generates code.

The product exists to prevent users from accepting generated code without understanding it. Its purpose is not simply to generate code. Its purpose is to make better programmers.

Core principle: **Understand first. Generate second.**

## Technology

### Frontend

- React
- TypeScript
- Tailwind CSS
- shadcn/ui
- Framer Motion
- Monaco Editor
- Zustand
- React Query

### Desktop

- Electron
- Electron Builder
- Electron Updater
- Electron Store
- Secure IPC architecture and preload scripts
- Context isolation enabled
- Node integration disabled

### AI

- OpenAI Codex API
- Streaming responses
- Conversation memory
- Tool-calling architecture

### Data and system integration

- SQLite with `better-sqlite3`
- IndexedDB where appropriate
- Local filesystem
- xterm.js terminal
- isomorphic-git
- simple-git

### Distribution

- Windows
- macOS
- Linux

Everything should be production-ready.

## Product experience

The application should feel like a premium desktop IDE:

- Dark theme by default
- Fast, responsive interaction
- Professional typography
- Smooth animations
- Rounded panels and polished shadows
- Native window controls
- Resizable panels and dockable views
- Multiple tabs and split editors
- Command palette
- Keyboard-first workflow

## Main layout

### Left sidebar

- Explorer
- Open Files
- Git
- Search
- Knowledge Profile
- Learning Dashboard
- AI History
- Settings

### Center

- Monaco Editor
- Tabbed editing
- Split editor support
- Syntax highlighting
- Minimap
- IntelliSense
- Multi-cursor editing
- Code folding
- Breadcrumb navigation

### Right sidebar

- AI Tutor

### Bottom panel

- Integrated Terminal
- Problems
- Output
- Debug Console
- Quiz Results

## Workspace features

- Open folders and projects
- Save, rename, delete, and drag files
- Search and replace across a project
- Recently opened projects
- Workspace settings
- Multi-root workspace support

## Core AI workflow

When a user asks for a coding change, the AI must not immediately generate code.

### 1. Analyze the request

Identify the concepts required to understand the requested work. For an authentication request, this might include JWTs, cookies, sessions, password hashing, middleware, authorization, and database models.

### 2. Teach the concepts

Generate concise, focused lessons that cover:

- Purpose
- Mental model
- Common mistakes
- Visual diagrams
- Small examples

### 3. Run an adaptive quiz

Use a changing mix of:

- Multiple choice
- Fill in the blank
- Predict the output
- Debug code
- Write a short snippet
- Explain a concept

Difficulty should adapt to the user's demonstrated understanding.

### 4. Evaluate understanding

Assign a confidence score. If the score meets
[truncated — 5768 more characters]
```

### package.json

```
{
  "name": "learn-before-you-code",
  "version": "0.1.0",
  "description": "A learning-first desktop IDE that unlocks AI code generation through understanding.",
  "author": "Wormie",
  "main": "./out/main/index.js",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "node scripts/fix-pty-permissions.mjs && electron-vite dev",
    "build": "node scripts/fix-pty-permissions.mjs && npm run typecheck && electron-vite build",
    "preview": "electron-vite preview",
    "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
    "test": "vitest run",
    "dist": "npm test && npm run build && electron-builder --publish never",
    "postinstall": "install-electron && node scripts/fix-pty-permissions.mjs"
  },
  "dependencies": {
    "@ai-sdk/openai-compatible": "^3.0.10",
    "@monaco-editor/react": "4.7.0",
    "@openai/codex": "^0.144.4",
    "@supabase/supabase-js": "^2.110.6",
    "@tanstack/react-query": "5.101.2",
    "@xterm/addon-fit": "0.11.0",
    "@xterm/xterm": "6.0.0",
    "ai": "^7.0.28",
    "clsx": "2.1.1",
    "electron-store": "11.0.2",
    "framer-motion": "12.42.2",
    "ignore": "^7.0.6",
    "lucide-react": "1.24.0",
    "monaco-editor": "0.53.0",
    "node-pty": "1.1.0",
    "react": "19.2.7",
    "react-dom": "19.2.7",
    "simple-git": "3.36.0",
    "tailwind-merge": "3.6.0",
    "zod": "^4.4.3",
    "zustand": "5.0.14"
  },
  "devDependencies": {
    "@tailwindcss/vite": "4.3.2",
    "@types/node": "26.1.1",
    "@types/react": "19.2.17",
    "@types/react-dom": "19.2.3",
    "@vitejs/plugin-react": "5.2.0",
    "electron": "43.1.0",
    "electron-builder": "26.15.3",
    "electron-vite": "5.0.0",
    "typescript": "7.0.2",
    "vite": "7.3.6",
    "vitest": "4.1.10"
  }
}

```

### demo-video/remotion-hook/package.json

```
{
  "name": "wormie-demo-hook-remotion",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "studio": "remotion studio src/index.ts",
    "render": "remotion render src/index.ts Hook out/hook-v2.mp4 --codec=h264 --crf=16",
    "still": "remotion still src/index.ts Hook out/hook-preview.png --frame=260"
  },
  "dependencies": {
    "@remotion/cli": "4.0.496",
    "@remotion/media": "4.0.496",
    "react": "19.2.7",
    "react-dom": "19.2.7",
    "remotion": "4.0.496"
  },
  "devDependencies": {
    "@types/react": "19.2.7",
    "typescript": "5.9.3"
  }
}

```

### src/preload/index.ts

```typescript
import { contextBridge, ipcRenderer, webUtils, type IpcRendererEvent } from 'electron'
import {
  IPC_CHANNELS,
  type AgentActivityEvent,
  type CloudAuthUpdate,
  type DesktopApi,
  type TerminalData,
  type TerminalExit,
  type WorkspaceFileChange
} from '../shared/contracts'

const desktopApi: DesktopApi = {
  platform: process.platform,
  setWorkspacePurpose: (purpose) => ipcRenderer.invoke(IPC_CHANNELS.workspaceSetPurpose, purpose),
  openWorkspace: (purpose) => ipcRenderer.invoke(IPC_CHANNELS.openWorkspace, purpose),
  closeWorkspace: () => ipcRenderer.invoke(IPC_CHANNELS.closeWorkspace),
  restoreWorkspace: () => ipcRenderer.invoke(IPC_CHANNELS.restoreWorkspace),
  refreshWorkspace: () => ipcRenderer.invoke(IPC_CHANNELS.refreshWorkspace),
  readFile: (filePath) => ipcRenderer.invoke(IPC_CHANNELS.readFile, filePath),
  writeFile: (request) => ipcRenderer.invoke(IPC_CHANNELS.writeFile, request),
  createEntry: (parentPath, name, type) => ipcRenderer.invoke(IPC_CHANNELS.createEntry, parentPath, name, type),
  renameEntry: (entryPath, name) => ipcRenderer.invoke(IPC_CHANNELS.renameEntry, entryPath, name),
  deleteEntry: (entryPath) => ipcRenderer.invoke(IPC_CHANNELS.deleteEntry, entryPath),
  searchWorkspace: (options) => ipcRenderer.invoke(IPC_CHANNELS.searchWorkspace, options),
  replaceWorkspace: (request) => ipcRenderer.invoke(IPC_CHANNELS.replaceWorkspace, request),
  listWorkspaceFiles: () => ipcRenderer.invoke(IPC_CHANNELS.listWorkspaceFiles),
  copyWorkspacePath: (filePath, kind) => ipcRenderer.invoke(IPC_CHANNELS.copyWorkspacePath, filePath, kind),
  watchWorkspaceFiles: (filePaths) => ipcRenderer.invoke(IPC_CHANNELS.watchWorkspaceFiles, filePaths),
  onWorkspaceFileChanged: (callback) => {
    const listener = (_event: IpcRendererEvent, change: WorkspaceFileChange) => callback(change)
    ipcRenderer.on(IPC_CHANNELS.workspaceFileChanged, listener)
    return () => ipcRenderer.removeListener(IPC_CHANNELS.workspaceFileChanged, listener)
  },
  loadEditorRecovery: (workspaceRoot) => ipcRenderer.invoke(IPC_CHANNELS.editorRecoveryLoad, workspaceRoot),
  saveEditorRecovery: (state) => ipcRenderer.invoke(IPC_CHANNELS.editorRecoverySave, state),
  onBeforeAppClose: (callback) => {
    const listener = () => callback()
    ipcRenderer.on(IPC_CHANNELS.appBeforeClose, listener)
    return () => ipcRenderer.removeListener(IPC_CHANNELS.appBeforeClose, listener)
  },
  finishAppClose: (proceed) => ipcRenderer.send(IPC_CHANNELS.appCloseReady, proceed),
  getGitStatus: () => ipcRenderer.invoke(IPC_CHANNELS.gitStatus),
  trustGitRepository: (repositoryRoot) => ipcRenderer.invoke(IPC_CHANNELS.gitTrustRepository, repositoryRoot),
  startTerminal: (request) => ipcRenderer.invoke(IPC_CHANNELS.terminalStart, request),
  writeTerminal: (sessionId, data) => ipcRenderer.send(IPC_CHANNELS.terminalWrite, { sessionId, data }),
  resizeTerminal: (sessionId, columns, rows) => ipcRenderer.send(IPC_CHANNELS.terminalResize, { sessionId, columns, rows }),
  stopTerminal: (sessionId) => ipcRenderer.send(IPC_CHANNELS.terminalStop, sessionId),
  copyTerminalText: (text) => ipcRenderer.invoke(IPC_CHANNELS.terminalCopy, text),
  readTerminalClipboard: () => ipcRenderer.invoke(IPC_CHANNELS.terminalReadClipboard),
  onTerminalData: (callback) => {
    const listener = (_event: IpcRendererEvent, data: TerminalData) => callback(data)
    ipcRenderer.on(IPC_CHANNELS.terminalData, listener)
    return () => ipcRenderer.removeListener(IPC_CHANNELS.terminalData, listener)
  },
  onTerminalExit: (callback) => {
    const listener = (_event: IpcRendererEvent, exit: TerminalExit) => callback(exit)
    ipcRenderer.on(IPC_CHANNELS.terminalExit, listener)
    return () => ipcRenderer.removeListener(IPC_CHANNELS.terminalExit, listener)
  },
  getAgentConfig: () => ipcRenderer.invoke(IPC_CHANNELS.agentGetConfig),
  saveAgentConfig: (config) => ipcRenderer.invoke(IPC_CHANNELS.agentSaveConfig, config),
  setAgentPassingScore: (score) => ipcRenderer.invoke(IPC_CHANNELS.agentSetPassingScore, score),
  getCodexAccount: () => ipcRenderer.invoke(IPC_CHANNELS.agentGetCodexAccount),
  connectCodexAccount: () => ipcRenderer.invoke(IPC_CHANNELS.agentConnectCodexAccount),
  listCodexModels: () => ipcRenderer.invoke(IPC_CHANNELS.agentListCodexModels),
  listAgentModels: () => ipcRenderer.invoke(IPC_CHANNELS.agentListModels),
  getTutorHistory: (workspaceRoot) => ipcRenderer.invoke(IPC_CHANNELS.agentGetTutorHistory, workspaceRoot),
  pathForFile: (file) => webUtils.getPathForFile(file),
  startLearning: (request) => ipcRenderer.invoke(IPC_CHANNELS.agentStartLearning, request),
  submitQuiz: (submission) => ipcRenderer.invoke(IPC_CHANNELS.agentSubmitQuiz, submission),
  generateProposal: (sessionId) => ipcRenderer.invoke(IPC_CHANNELS.agentGenerateProposal, sessionId),
  applyProposal: (request) => ipcRenderer.invoke(IPC_CHANNELS.agentApplyProposal, request),
  prepareProposalQuiz: (proposalId) => ipcRenderer.invoke(IPC_CHANNELS.agentPrepareProposalQuiz, proposalId),
  rejectProposal: (proposalId) => ipcRenderer.invoke(IPC_CHANNELS.agentRejectProposal, proposalId),
  getUnderstandingSettings: () => ipcRenderer.invoke(IPC_CHANNELS.understandingGetSettings),
  saveUnderstandingSettings: (settings) => ipcRenderer.invoke(IPC_CHANNELS.understandingSaveSettings, settings),
  getUnderstandingHistory: () => ipcRenderer.invoke(IPC_CHANNELS.understandingGetHistory),
  getUnderstandingGate: (changeId, fingerprint) => ipcRenderer.invoke(IPC_CHANNELS.understandingGetGate, changeId, fingerprint),
  saveUnderstandingAnswers: (quizId, answers) => ipcRenderer.invoke(IPC_CHANNELS.understandingSaveAnswers, quizId, answers),
  submitUnderstanding: (submission) => ipcRenderer.invoke(IPC_CHANNELS.understandingSubmit, submission),
  bypassUnderstanding: (quizId, reason) => ipcRenderer.invoke(IPC_CHANNELS.understandingBypass, quizId, reason),
  getMasteryOverview: () => ipcRenderer.invoke(IPC_CHANNELS.masteryOverview),
  getMasteryDomains: () => ipcRenderer.invoke(IP
[truncated — 6098 more characters]
```

### src/main/index.ts

```typescript
import path from 'node:path'
import { randomUUID } from 'node:crypto'
import { app, BrowserWindow, dialog, ipcMain, shell, type IpcMainEvent, type IpcMainInvokeEvent } from 'electron'
import Store from 'electron-store'
import { registerAgentHandlers, type AgentClassroomAnalyticsInput, type AgentClassroomAnalyticsScope } from './agent'
import { registerAssignmentHandlers } from './assignments'
import { registerCloudHandlers } from './cloud'
import { registerGitHandlers } from './git'
import { createRendererUrlValidator } from './ipcTrust'
import type { AppPreferences } from './preferences'
import { registerTerminalHandlers } from './terminal'
import { UnderstandingController } from './understanding'
import { UnderstandingRepository } from './understanding/store'
import { registerEditorRecoveryHandlers } from './editorRecovery'
import { MasteryRepository } from './mastery/repository'
import { MasteryService } from './mastery/service'
import { KnowledgeGraph } from './mastery/graph'
import { canonicalConcepts } from './mastery/catalog'
import { registerMasteryIpc } from './mastery/ipc'
import { registerWorkspaceHandlers } from './workspace'
import { IPC_CHANNELS, type ClassroomAssignmentContext, type CloudAuthUpdate, type WorkspacePurpose } from '../shared/contracts'
import { classroomInviteFromArguments, classroomInviteLink } from './cloud/invite'
import { MasterySyncQueue } from './cloud/masterySync'
import { AiAnalyticsSyncQueue, type AiAnalyticsSyncEvent } from './cloud/aiAnalyticsSync'
import { AssignmentProgressSyncQueue } from './cloud/assignmentProgressSync'
import {
  authCallback,
  authCallbackFromArguments,
  type AuthCallback
} from './cloud/oauth'

const store = new Store<AppPreferences>({ name: 'preferences' })
const trustedWebContents = new Set<number>()
const rendererFilePath = path.join(__dirname, '../renderer/index.html')
const devIconPath = path.join(__dirname, '../../build/icon.png')
const isTrustedRendererUrl = createRendererUrlValidator(process.env.ELECTRON_RENDERER_URL, rendererFilePath)
const understandingStore = new Store({ name: 'understanding-state' })
const editorRecoveryStore = new Store<{ state?: unknown }>({ name: 'editor-recovery' })
const masterySyncStore = new Store<{ queue?: unknown }>({ name: 'mastery-sync' })
const masterySyncQueue = new MasterySyncQueue(masterySyncStore)
const analyticsSyncStore = new Store<{ queue?: unknown }>({ name: 'classroom-ai-analytics-sync' })
const analyticsSyncQueue = new AiAnalyticsSyncQueue(analyticsSyncStore)
const assignmentProgressSyncStore = new Store<{ queue?: unknown }>({ name: 'assignment-progress-sync' })
const assignmentProgressSyncQueue = new AssignmentProgressSyncQueue(assignmentProgressSyncStore)
let workspacePurpose: WorkspacePurpose = 'sandbox'
let activeAssignmentContext: (ClassroomAssignmentContext & { userId: string }) | null = null
const understandingRepository = new UnderstandingRepository(understandingStore)
const masteryStore = new Store({ name: 'mastery-state' })
const masteryRepository = new MasteryRepository(masteryStore, Object.values(understandingRepository.read().mastery))
const mastery = new MasteryService(masteryRepository, new KnowledgeGraph(canonicalConcepts))
const understanding = new UnderstandingController(understandingRepository, mastery, () => {
  if (!activeAssignmentContext || activeAssignmentContext.role !== 'student') return null
  return {
    classroomId: activeAssignmentContext.classroomId,
    assignmentId: activeAssignmentContext.assignmentId,
    userId: activeAssignmentContext.userId
  }
})
let pendingClassroomInvite = classroomInviteFromArguments(process.argv)
let pendingAuthCallback = authCallbackFromArguments(process.argv)
let handleAuthCallback: ((callback: AuthCallback) => Promise<void>) | null = null
let recordAiAnalyticsEvent: ((event: AiAnalyticsSyncEvent) => void) | null = null
const isTrustedSender = (event: IpcMainEvent | IpcMainInvokeEvent) =>
  trustedWebContents.has(event.sender.id) &&
  event.senderFrame === event.sender.mainFrame &&
  isTrustedRendererUrl(event.senderFrame.url)

function samePath(left: string, right: string): boolean {
  const normalize = (value: string) => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value)
  return normalize(left) === normalize(right)
}

for (const protocol of ['wormie', 'wormie-ide']) {
  if (process.defaultApp) {
    if (process.argv[1]) app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])])
  } else {
    app.setAsDefaultProtocolClient(protocol)
  }
}

function queueClassroomInvite(value: string): void {
  const inviteLink = classroomInviteLink(value)
  if (!inviteLink) return
  pendingClassroomInvite = inviteLink
  for (const window of BrowserWindow.getAllWindows()) {
    if (!window.isDestroyed()) window.webContents.send(IPC_CHANNELS.cloudInviteReceived, inviteLink)
  }
}

function takePendingClassroomInvite(): string | null {
  const inviteLink = pendingClassroomInvite
  pendingClassroomInvite = null
  return inviteLink
}

function queueAuthCallback(callback: AuthCallback): void {
  if (handleAuthCallback) {
    void handleAuthCallback(callback)
    return
  }
  pendingAuthCallback = callback
}

function notifyCloudAuthChanged(update: CloudAuthUpdate): void {
  for (const window of BrowserWindow.getAllWindows()) {
    if (!window.isDestroyed()) window.webContents.send(IPC_CHANNELS.cloudAuthChanged, update)
  }
}

app.on('open-url', (event, url) => {
  event.preventDefault()
  const callback = authCallback(url)
  if (callback) queueAuthCallback(callback)
  else queueClassroomInvite(url)
})

function createWindow(): void {
  const savedBounds = store.get('windowBounds')
  const mainWindow = new BrowserWindow({
    width: savedBounds?.width ?? 1440,
    height: savedBounds?.height ?? 900,
    minWidth: 1040,
    minHeight: 680,
    title: 'Wormie',
    icon: app.isPackaged ? undefined : devIconPath,
    backgroundColor: '#090b0d',
    show: false,
    t
[truncated — 9774 more characters]
```

### demo-video/remotion-hook/src/index.ts

```typescript
import {registerRoot} from 'remotion';
import {RemotionRoot} from './Root';

registerRoot(RemotionRoot);

```

### src/renderer/src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
import { RendererErrorBoundary } from './components/RendererErrorBoundary'
import './styles.css'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { retry: 1, staleTime: 30_000 },
    mutations: { retry: 0 }
  }
})

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RendererErrorBoundary>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </RendererErrorBoundary>
  </StrictMode>
)

```

### src/main/understanding/index.ts

```typescript
import { randomUUID } from 'node:crypto'
import { ipcMain, type IpcMainInvokeEvent } from 'electron'
import type {
  ChangeInput,
  ChangeSignificanceResult,
  ChangeUnderstandingPreparation,
  PrivateQuizQuestion,
  UnderstandingAnswer,
  UnderstandingQuiz
} from '../../shared/contracts'
import { IPC_CHANNELS } from '../../shared/contracts'
import type { ChangeConceptDraft, RemediationDraft, SemanticGradeDraft, UnderstandingQuizDraft } from '../agent/schemas'
import { fingerprintChange } from './fingerprint'
import { UnderstandingGateService, type ClassroomUnderstandingScope, type UnderstandingCompletion } from './gate'
import { toPublicQuestion } from './grading'
import { buildConceptExtractionPrompt, buildQuizGenerationPrompt, buildRemediationPrompt, buildSemanticGradingPrompt } from './prompts'
import { sanitizeChangeContext } from './redaction'
import { classifyChange } from './significance'
import type { UnderstandingRepository } from './store'
import type { MasteryService } from '../mastery/service'
import { resolveOrRegisterConcept } from '../mastery/catalog'

type UnderstandingAi = {
  extractConcepts: (prompt: string) => Promise<ChangeConceptDraft>
  generateQuiz: (prompt: string) => Promise<UnderstandingQuizDraft>
  gradeAnswer: (prompt: string) => Promise<SemanticGradeDraft>
  generateRemediation: (prompt: string) => Promise<RemediationDraft>
  modelIdentifier: () => string
}

export class UnderstandingController {
  readonly gates: UnderstandingGateService
  private ai: UnderstandingAi | null = null
  private readonly quizContexts = new Map<string, string>()
  private readonly mastery?: MasteryService

  constructor(
    readonly repository: UnderstandingRepository,
    masteryOrScope?: MasteryService | (() => ClassroomUnderstandingScope | null),
    getClassroomScope?: () => ClassroomUnderstandingScope | null
  ) {
    const mastery = typeof masteryOrScope === 'function' ? undefined : masteryOrScope
    const classroomScope = typeof masteryOrScope === 'function' ? masteryOrScope : getClassroomScope
    this.mastery = mastery
    this.gates = new UnderstandingGateService(
      repository,
      async (question, answer) => {
        if (!this.ai) throw new Error('AI grading is unavailable.')
        const result = await this.ai.gradeAnswer(buildSemanticGradingPrompt(question, answer, this.quizContexts.get(question.id.split(':')[0]) ?? ''))
        return { correct: result.isCorrect, score: result.score / 100, explanation: result.feedback, misconception: result.misconceptions.join(' ') || undefined }
      },
      async (quiz, feedback) => {
        if (!this.ai) throw new Error('AI remediation is unavailable.')
        const result = await this.ai.generateRemediation(buildRemediationPrompt(quiz, feedback, this.quizContexts.get(quiz.id) ?? ''))
        return result.lesson
      },
      mastery,
      classroomScope
    )
  }

  setCompletionListener(listener: (completion: UnderstandingCompletion) => void): void {
    this.gates.setCompletionListener(listener)
  }

  setAi(ai: UnderstandingAi): void {
    this.ai = ai
  }

  analyze(change: ChangeInput): { fingerprint: string; significance: ChangeSignificanceResult } {
    return {
      fingerprint: fingerprintChange(change),
      significance: classifyChange(change, this.gates.getSettings())
    }
  }

  async prepare(change: ChangeInput, forceNew = false): Promise<ChangeUnderstandingPreparation> {
    const { fingerprint, significance } = this.analyze(change)
    if (!significance.quizRequired) return { changeId: change.id, fingerprint, significance, gate: null }
    const existing = this.gates.getStatus(change.id, fingerprint)
    if (existing && !forceNew) return { changeId: change.id, fingerprint, significance, gate: existing }
    if (!this.ai) throw new Error('Configure an AI provider before generating an understanding check.')

    const safeChange = sanitizeChangeContext(change)
    if (safeChange.files.length === 0) throw new Error('No safe text diff is available for a grounded understanding check.')
    const extracted = await this.ai.extractConcepts(buildConceptExtractionPrompt(safeChange, significance))
    const conceptIdMap = new Map(extracted.concepts.map((concept) => [concept.id, resolveOrRegisterConcept(concept.id || concept.name)]))
    const concepts = { ...extracted, concepts: extracted.concepts.map((concept) => {
      const canonical = conceptIdMap.get(concept.id)!
      return { ...concept, id: canonical.id, name: canonical.name }
    }) }
    const promptContext = this.mastery?.promptContext()
    const draft = await this.ai.generateQuiz(buildQuizGenerationPrompt(safeChange, significance, concepts, this.gates.getSettings(), promptContext?.profile ?? [], promptContext?.personalization))
    this.validateGrounding(draft, safeChange, significance)

    const quizId = randomUUID()
    const draftConceptMap = new Map(draft.concepts.map((concept) => [concept.id, resolveOrRegisterConcept(concept.id || concept.name)]))
    const canonicalConcepts = [...new Map(draft.concepts.map((concept) => {
      const canonical = draftConceptMap.get(concept.id)!
      return [canonical.id, { id: canonical.id, name: canonical.name, summary: concept.summary }]
    })).values()]
    const privateQuestions: PrivateQuizQuestion[] = draft.questions.map((question, index) => ({
      ...question,
      conceptId: draftConceptMap.get(question.conceptId)?.id ?? resolveOrRegisterConcept(question.conceptId).id,
      id: `${quizId}:${index}`
    }))
    const settings = this.gates.getSettings()
    const quiz: UnderstandingQuiz = {
      id: quizId,
      changeId: change.id,
      source: change.source,
      fingerprint,
      diffFingerprint: fingerprint,
      quizVersion: 1,
      promptVersion: 'major-change-understanding-v1',
      modelIdentifier: this.ai.modelIdentifier().slice(0, 200),
      title: draft.title,
      summary: draft.summary,
      whyThisMatters: draft.whyThisMatters,
      flowSummary: draft.flowSummary
[truncated — 3783 more characters]
```

### src/main/assignments/index.ts

```typescript
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { dialog, ipcMain, shell, type IpcMainInvokeEvent } from 'electron'
import type Store from 'electron-store'
import {
  IPC_CHANNELS,
  type AssignmentExportResult,
  type AssignmentImportResult,
  type AssignmentProgress,
  type AssignmentSaveRequest,
  type AssignmentStartRequest,
  type AssignmentSubmitRequest,
  type AssignmentSubmission,
  type AssignmentSubmissionExportResult,
  type AssignmentTaskProgressRequest,
  type AssignmentWorkspaceState,
  type ClassroomAssignmentContext
} from '../../shared/contracts'
import type { AppPreferences } from '../preferences'
import { isPathInside } from '../pathSafety'
import { createAssignmentPackage, importAssignmentPackage } from './package'
import { readAiActivity } from './activity'
import { commitSubmittedProgress, prepareSubmittedProgress, readProgress, startProgress, updateTaskProgress } from './progress'
import { getAssignmentManifestPath, readAssignment, readAssignmentRevision, saveAssignment } from './storage'
import { createAssignmentSubmission, readAssignmentSubmission } from './submission'

const maxAssignmentIpcBytes = 256 * 1024

type ActiveAssignmentContext = ClassroomAssignmentContext & { userId: string }

export type AssignmentCloudHooks = {
  getContext: () => ActiveAssignmentContext | null
  syncProgress: (context: ActiveAssignmentContext, progress: AssignmentProgress) => Promise<void>
  uploadSubmission: (context: ActiveAssignmentContext, submission: AssignmentSubmission, payload: string) => Promise<void>
  removeSubmission: (context: ActiveAssignmentContext, progress: AssignmentProgress) => Promise<void>
}

export function registerAssignmentHandlers(
  store: Store<AppPreferences>,
  progressStorageRoot: string,
  getWorkspaceRoot: () => string | null,
  setWorkspace: (rootPath: string) => Promise<import('../../shared/contracts').WorkspaceSnapshot>,
  isTrustedSender: (event: IpcMainInvokeEvent) => boolean,
  cloudHooks: AssignmentCloudHooks
): void {
  function workspaceKey(rootPath: string): string {
    const resolved = path.resolve(rootPath)
    return process.platform === 'win32' ? resolved.toLowerCase() : resolved
  }

  async function isStudentWorkspace(rootPath: string): Promise<boolean> {
    const key = workspaceKey(rootPath)
    if ((store.get('studentWorkspaces') ?? []).some((candidate) => workspaceKey(candidate) === key)) return true
    const markerPath = path.join(rootPath, '.wormie', 'student.json')
    const markerStats = await fs.lstat(markerPath).catch((error: NodeJS.ErrnoException) => {
      if (error.code === 'ENOENT') return null
      throw error
    })
    if (!markerStats) return false
    if (!markerStats.isFile() || markerStats.isSymbolicLink() || markerStats.size > 4_096) throw new Error('The student workspace marker is invalid.')
    let marker: unknown
    try {
      marker = JSON.parse(await fs.readFile(markerPath, 'utf8'))
    } catch (error) {
      if (error instanceof SyntaxError) throw new Error('The student workspace marker contains invalid JSON.')
      throw error
    }
    if (!marker || typeof marker !== 'object' || (marker as { schemaVersion?: unknown }).schemaVersion !== 1 || typeof (marker as { packageId?: unknown }).packageId !== 'string') {
      throw new Error('The student workspace marker is invalid.')
    }
    markStudentWorkspace(rootPath)
    return true
  }

  function markStudentWorkspace(rootPath: string): void {
    const roots = store.get('studentWorkspaces') ?? []
    if (!roots.some((candidate) => workspaceKey(candidate) === workspaceKey(rootPath))) {
      store.set('studentWorkspaces', [...roots.slice(-99), rootPath])
    }
  }

  function requireWorkspaceRoot(expectedRoot?: string): string {
    const workspaceRoot = getWorkspaceRoot()
    if (!workspaceRoot) throw new Error('Open a workspace first.')
    if (expectedRoot && workspaceKey(expectedRoot) !== workspaceKey(workspaceRoot)) {
      throw new Error('The active workspace changed. Reload the assignment and try again.')
    }
    return workspaceRoot
  }

  function assertTrustedSender(event: IpcMainInvokeEvent): void {
    if (!isTrustedSender(event)) throw new Error('Assignment access was denied for this window.')
  }

  async function withProgress(
    workspaceRoot: string,
    state: AssignmentWorkspaceState
  ): Promise<AssignmentWorkspaceState> {
    const role = await isStudentWorkspace(workspaceRoot) ? 'student' : 'teacher'
    if (!state.manifest || !state.revision) return { ...state, workspaceRoot, role, progress: null }
    try {
      return { ...state, workspaceRoot, role, progress: await readProgress(progressStorageRoot, workspaceRoot, state.manifest, state.revision), progressError: undefined }
    } catch (error) {
      return { ...state, workspaceRoot, role, progress: null, progressError: error instanceof Error ? error.message : 'Assignment progress is invalid.' }
    }
  }

  ipcMain.handle(IPC_CHANNELS.assignmentGet, async (event, expectedRoot: string): Promise<AssignmentWorkspaceState> => {
    assertTrustedSender(event)
    const workspaceRoot = requireWorkspaceRoot(expectedRoot)
    try {
      const state = await readAssignment(workspaceRoot)
      return withProgress(workspaceRoot, state)
    } catch (error) {
      let revision: string | null = null
      try {
        revision = await readAssignmentRevision(workspaceRoot)
      } catch {
        revision = null
      }
      return {
        workspaceRoot,
        role: await isStudentWorkspace(workspaceRoot) ? 'student' : 'teacher',
        manifest: null,
        manifestPath: getAssignmentManifestPath(workspaceRoot),
        revision,
        progress: null,
        error: error instanceof Error ? error.message : 'The assignment manifest is invalid.'
      }
    }
  })

  ipcMain.handle(
    IPC_CHANNELS.assignmentSave,
    async (event, request: AssignmentSaveRequest): Promise<AssignmentWorkspaceState> => {
      assertTrustedSender(event)
      
[truncated — 10226 more characters]
```

[246 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]