6.5 KiB
STCourses WP-C Studio Implementation Plan
For agentic workers: executed inline by the WP-C agent (superpowers:executing-plans). Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Browser studio for studio-managed courses: create a course, edit the manifest, lessons (Markdown with live preview), deck links, attachments and exercises in a per-author draft worktree; every save is a commit by the author; publish merges the draft into the published branch that learners read; Sync with a remote only on request; conflicts are shown and resolved by the author, never automatically.
Architecture:
packages/feature-studio/src/server/git.ts: a thingitCLI wrapper overBun.$(explicit identity, no hooks, no signing) plus a per-course async mutex.packages/feature-studio/src/server/store.ts:StudioStore, the git model of spec section 12:<dataDir>/courses/<id>: a bare repo (it holds nostcourse.yaml, so course discovery never loads it);<dataDir>/worktrees/<id>/published: worktree on the published branch (main); the only tree learners read;<dataDir>/worktrees/<id>/drafts/<author>: worktree ondraft/<author>(depth 3 underworktrees/, so discovery, which stops at depth 2, never loads drafts).
- Publish =
git merge --no-ff draft/<author>inside the published worktree. A conflicting merge is aborted at once, so the published tree is never left half-merged, and the conflicting paths are returned. The author then runs "Update draft" (merge ofmain, ororigin/mainfor Sync, into the draft worktree), resolves each file in the studio, and publishes again. - Save uses optimistic concurrency: each file carries the hash of the content the editor loaded; a mismatch with the file on disk (an external edit) gives 409 with both versions. Nothing is overwritten.
watch.ts: polls the published HEAD of each managed course (default every 3 s, unref'd timer) and callsctx.courses.reload(id)when it changed (external commit, pull, publish).feature.ts:studioFeature(ctx)with/api/studio/*routes. Access: owner,authorrole, or acourse_authorsrow for that course. Courses not managed by the studio (overlays, fixture/imported folders) are listed read-only: "This course is managed outside STCourses".draft-writer.ts:createDraftWriter(ctx)for WP-D.- Web (
src/web/*):/studio(course list, New course) and/studio/:courseId(outline sidebar, editors, status bar with Publish / Update draft / Sync and the conflict view). Work look from the mockup. The lesson preview sits behind aLessonPreviewadapter; v1 is a small safe Markdown renderer; Phase 2 swaps in the learnerLessonView(contract request).
Tech Stack: Bun 1.3, TypeScript strict, git CLI 2.4x via Bun.$, React 19 + React Router 6, @stc/course-format for validation, @stc/ui primitives.
Spec: sections 5, 9.1, 12 (git model), 13. Brief: .superpowers/sdd/2026-10-30-stcourses/phase-1-brief.md (WP-C).
Global Constraints
Inherits every item of the Global Constraints in docs/superpowers/plans/2026-10-30-stcourses.md. Additionally:
- Never write into a course whose root is outside
<dataDir>(overlays) — the studio refuses writes for non-managed courses. - Paths written through the studio must be relative, stay inside the worktree, and never touch
.git. - No new DB tables (authors use the existing
course_authors);migrationsDirsis exported empty.
Review Focus
- A conflicting external commit to
mainis detected at publish and shown; the published tree keeps the external content (test: acceptance). - Drafts never reach learners: before publish the
CourseRepositorystill serves the published content (test: acceptance). - An external edit to a draft file between load and save gives 409, not an overwrite (test: store).
- Paths with spaces and non-ASCII characters save, commit and publish (test: store).
- Path traversal (
.., absolute,.git/…) is rejected (test: store).
Tasks
Task 1: git wrapper and StudioStore (create, drafts, save)
Files: Create packages/feature-studio/src/server/{git.ts,store.ts,paths.ts}; Test packages/feature-studio/test/store.test.ts.
Interfaces (Produces): createStudioStore(dataDir): StudioStore with createCourse, isManaged, listManaged, ensureDraft, readFile, listFiles, saveFiles, status.
- Step 1: failing tests: create gives bare repo + published worktree with a valid manifest; draft worktree on
draft/<author>; save commits with the author's name/email; stale base hash gives a conflict; traversal rejected; non-ASCII path works. - Step 2: run, FAIL. Step 3: implement. Step 4: PASS. Step 5: commit.
Task 2: publish, update draft, conflict resolution, sync
Files: extend store.ts; Test packages/feature-studio/test/publish.test.ts.
- Step 1: failing tests: publish merges; external conflicting commit gives
{ok:false, conflicts}and leavesmainuntouched; dirty published tree refuses publish; update draft shows ours/theirs; resolve + publish succeeds; abort; Sync with a local bare remote (pull fast-forward, push, conflict reported); no remote gives an error. - Steps 2–5 as above.
Task 3: watcher and draft writer
Files: src/server/{watch.ts,draft-writer.ts}; Test test/watch.test.ts, test/draft-writer.test.ts.
- Failing tests: external commit triggers
courses.reload;writeDraftFilescommits ondraft/<author>and the published tree is unchanged. Implement, pass, commit.
Task 4: HTTP feature and acceptance
Files: src/server/feature.ts, src/index.ts; Test test/feature.test.ts, test/acceptance.test.ts.
- Failing tests: 401/403 for anonymous and learners; overlay course read-only; create → edit lesson → publish → learner sees it via
CourseRepositoryand/api/media; conflicting external commit shown, not overwritten; manifest validation errors returned. Implement, pass, commit.
Task 5: Studio web UI
Files: src/web/{api.ts,markdown.ts,preview.tsx,StudioHome.tsx,CourseStudio.tsx,editors/*.tsx,studio.css}, src/web.ts; Test test/markdown.test.ts.
- Failing test for the Markdown renderer (escaping, headings, fences, lists, links without
javascript:). Implement UI. Commit.
Task 6: wiring, dev server, contract requests, verification
- Append to
apps/web/src/features.ts; dev entrypackages/feature-studio/dev/serve.ts;docs/contract-requests/studio.md; runbun test, typecheck, Postgres run; headless Chrome screenshots of/studioand/studio/:id; report.