Skip to content

Let an assistant propose a lesson change instead of making it - #41

Merged
playforge-coding merged 2 commits into
masterfrom
feature/mcp-pr
Aug 10, 2026
Merged

Let an assistant propose a lesson change instead of making it#41
playforge-coding merged 2 commits into
masterfrom
feature/mcp-pr

Conversation

@playforge-coding

@playforge-coding playforge-coding commented Aug 10, 2026

Copy link
Copy Markdown
Owner

An AI assistant connected over MCP can only write a lesson outright, which leaves two things impossible: it can't touch a lesson somebody else wrote (nobody may save over another person's lesson), and it can't offer changes to your own lesson for you to look over first — patch_lesson overwrites it, so there's nothing to review.

This gives it the route a human already has.

fork_lesson({ lessonId })           -> a private draft fork, cloning the git history
patch_lesson({ id: fork.id, … })    -> the assistant edits THE FORK (existing tool)
propose_changes({ forkLessonId })   -> a proposal, with a URL to review it
list_lesson_proposals({ lessonId }) -> what the human decided

The lesson is untouched until a person merges it from the Proposals tab. Merging is deliberately not a tool — it happens in the web app under the reviewer's own credentials, because it's their decision. The tool descriptions steer the model to fork-and-propose when the user wants to review the work, and to patch_lesson for a correction they asked for outright.

Two things had to move

The flow only existed in a browser. browser/git/sync.js is bound to LightningFS. The git engine already takes its filesystem through { fs, gitdir } for exactly this reason, so this needed a filesystem rather than a rewrite: core/git/memfs.js is an in-memory node:fs, which is what lets the same code run on stdio and inside the Worker.

No repository is kept between calls. A fork is a real hub lesson with its own stored pack, so each call clones that pack, does one thing to it, and uploads the result — which survives a restart, a conversation resumed days later, and a connection moving between Worker instances.

The Worker refused a proposal from a lesson's own author, on the grounds that they could simply save. Over MCP the assistant acts as the account it's signed in with, so that refusal landed on exactly the case worth having. It now refuses only when there's nothing behind the request: a proposal carrying a fork you own is allowed, because it means something specific — here is a copy with changes in it, let me read the diff before it lands. A human gets the same route via "fork into a new lesson" in the editor.

Since the proposer's name is then your own, the proposal's body records which MCP client wrote it (getClientVersion() — "Claude Desktop", "claude.ai", …) and the notification reads "Changes are waiting for your review" rather than naming somebody.

On the alternative — a global per-provider user (a "Claude" account, a "ChatGPT" account) — that needs shared Supabase accounts the MCP could authenticate as, and the remote path binds to the user's own OAuth session. Large auth change for attribution alone, so the client-name note covers it instead.

Review notes

  • A proposal is one commit, made when it's opened, holding the fork as it then stands. Intermediate patch_lesson calls aren't separate commits — nothing is watching to record them — so the reviewer gets one clean diff. Documented in the tool description and the docs.
  • forkLesson commits the fork's document over the cloned head if the two disagree (a lesson edited over MCP is saved without committing, so its stored pack can lag). A no-op in the normal case, and it keeps the later proposal's diff to just the assistant's changes.
  • If a fork's history fails to push, the row is left in place rather than auto-deleted — it's a real copy of the document and the user's to keep or remove — with an error saying so.
  • Folded uploadImage onto the same token-refresh helper as the new binary calls, which removed the duplicated retry logic in api.js.

Verified

  • 69 core + 60 MCP + 27 API tests pass; pnpm run fmt && pnpm run lint clean; docs site builds.
  • The new tests drive the real git engine, not a mock of it: that a proposal's packfile genuinely shares ancestry with the lesson it targets (without which a reviewer's three-way merge has no base and the whole thing degrades to "replace the lesson with mine"), that the target lesson is untouched, that a failed pack upload withdraws its proposal instead of leaving an empty one in a queue, and that proposing twice stacks rather than colliding.
  • wrangler deploy --dry-run builds; the Worker bundle grows 1.42 → 1.50 MB gzipped from pulling in isomorphic-git.
  • Versions bumped per AGENTS.md: apps/mcp package + manifest to 0.3.0, SERVER_INFO to 0.2.0.

Not verified: the round trip against the live hub. The flow is tested against a fake that enforces the Worker's pack compare-and-swap and its "pack must match the head it was opened with" rule, but no real request has been made.

🤖 Generated with Claude Code

Summary by Sourcery

Add a fork-and-propose workflow so MCP-connected assistants can offer lesson changes for human review instead of editing lessons directly, including lessons authored by others or by the requesting user.

New Features:

  • Introduce MCP tools to fork a lesson into a private draft, propose a fork’s changes back to the original, and list proposals for a lesson.
  • Expose API endpoints from the MCP server to read and write lesson git history and to open, upload, and close lesson proposals.
  • Support proposals from a lesson’s own author when they originate from a fork the author owns, enabling AI-assisted review flows.

Enhancements:

  • Refactor MCP API calling into a reusable request helper that supports both JSON and binary payloads and reuse it for image uploads.
  • Record MCP client identity on proposals and adjust notification wording and links so authors see assistant-originated changes as items to review with direct links to the proposal page.
  • Document the new fork/propose tooling, proposal semantics, and server-side git flow across MCP, web app, and monorepo version history docs.
  • Add an in-memory filesystem and repository adapter for the core git engine so git operations can run in Node and Workers without persistent storage.
  • Update version numbers for the MCP package, manifest, and server metadata to reflect the new capabilities.

Documentation:

  • Expand MCP server tools documentation with the fork/propose workflow and guidance on when to use proposals versus direct edits.
  • Update web app pull-requests and notifications documentation to cover self-proposals from forks and assistant-originated changes.
  • Document the new memfs git adapter and MCP-side fork/propose flow in the monorepo version history overview.

Tests:

  • Add comprehensive tests for the memfs filesystem and in-memory git repository to ensure compatibility with the real git engine.
  • Add integration-style tests around the MCP git flow to validate that forks clone history correctly, proposals share ancestry with their targets, unchanged forks are rejected, failed uploads are cleaned up, and repeated proposals stack rather than collide.
  • Extend MCP smoke tests to cover the new fork and proposal tools.

Summary by CodeRabbit

  • New Features
    • Added MCP tools to fork lessons, submit changes for review, and view proposal status.
    • Forks preserve lesson history and support repeat proposals.
    • Lesson authors can propose changes to their own lessons using an owned fork.
    • Notifications now link directly to proposals and identify changes awaiting review.
  • Documentation
    • Expanded guidance for MCP workflows, proposal review, permissions, notifications, and fork history.
  • Improvements
    • Added clearer validation and error handling for forks, proposals, and uploads.

An AI assistant connected over MCP could only write a lesson outright, which
left two things impossible. It could not touch a lesson somebody else wrote at
all — nobody may save over another person's lesson — and it could not offer
changes to your own lesson for you to look over first: patch_lesson overwrites
it, and there is nothing to review.

So give it the route a human already has. fork_lesson clones a lesson into a
private draft of its own, the assistant edits that with the ordinary tools, and
propose_changes offers the result back as a proposal, to be read and merged (or
declined) from the web app. The lesson is untouched until a person decides.
list_lesson_proposals is how the assistant finds out what they decided; merging
is deliberately not a tool, because it is theirs.

Two things had to move to make that possible.

The fork-and-propose flow only existed bound to LightningFS, so it ran in a
browser and nowhere else. The git engine already takes its filesystem through
{ fs, gitdir } for exactly this reason, so it needed a filesystem rather than a
rewrite: core/git/memfs.js is an in-memory node:fs, which is what lets this run
on stdio and inside the Worker alike. No repository is kept between calls — a
fork is a real hub lesson with its own stored pack, so each call clones that
pack, does one thing to it, and uploads the result. That survives a restart, a
conversation resumed days later, and a connection moving between instances.

And the Worker refused a proposal from a lesson's own author, on the grounds
that they could simply save. Over MCP the assistant acts as the account it is
signed in with, so that refusal fell on exactly the case worth having. It now
refuses only when there is nothing behind the request: a proposal carrying a
fork you own is allowed, because it means something specific — here is a copy
with changes in it, let me read the diff before it lands. A human gets the same
route via "fork into a new lesson". Since the proposer's name is then your own,
the proposal's body records which client wrote it and the notification reads
"Changes are waiting for your review" rather than naming somebody.

Tested against the real git engine rather than a mock of it: that a proposal's
packfile genuinely shares ancestry with the lesson it targets (without which a
reviewer's three-way merge has no base and the whole thing degrades to
"replace the lesson with mine"), that the target is untouched, that a failed
pack upload withdraws its proposal instead of leaving an empty one in someone's
queue, and that proposing twice stacks rather than colliding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a fork-and-propose workflow for MCP assistants by adding git packfile APIs, an in-memory filesystem-backed git engine, new MCP tools for forking lessons and proposing changes, and tightening pull request rules and notifications so assistants can propose reviewed changes instead of overwriting lessons directly.

Sequence diagram for the fork-and-propose MCP workflow

sequenceDiagram
  actor User
  participant Assistant
  participant MCPServer as MCP_Server
  participant GitEngine as Git_memfs_repo
  participant HubAPI as Hub_API_Worker
  actor Reviewer as Web_Reviewer

  User->>Assistant: Request changes to lesson

  %% 1) fork_lesson
  Assistant->>MCPServer: fork_lesson(lessonId)
  MCPServer->>HubAPI: api.getLesson(lessonId)
  HubAPI-->>MCPServer: lesson
  MCPServer->>HubAPI: api.fetchLessonPack(lessonId)
  HubAPI-->>MCPServer: { packfile, head } | null
  MCPServer->>GitEngine: cloneFromPack / commitDoc
  GitEngine-->>MCPServer: packed { packfile, head }
  MCPServer->>HubAPI: api.createLesson({ forkedFrom })
  HubAPI-->>MCPServer: fork lesson
  MCPServer->>HubAPI: api.pushLessonPack(fork.id, { packfile, head })
  HubAPI-->>MCPServer: ok
  MCPServer-->>Assistant: fork { id, head, clonedHistory }

  %% 2) Assistant edits fork (patch_lesson etc.)
  Assistant->>MCPServer: patch_lesson(id = fork.id, ...)
  MCPServer->>HubAPI: api.patchLesson(...)
  HubAPI-->>MCPServer: updated fork

  %% 3) propose_changes
  Assistant->>MCPServer: propose_changes({ forkLessonId })
  MCPServer->>HubAPI: api.getLesson(forkLessonId)
  HubAPI-->>MCPServer: fork with doc
  MCPServer->>HubAPI: api.fetchLessonPack(forkLessonId)
  HubAPI-->>MCPServer: { packfile, head }
  MCPServer->>GitEngine: pendingOps / commitDoc
  GitEngine-->>MCPServer: commit oid, packed { packfile, head }
  MCPServer->>HubAPI: api.pushLessonPack(forkLessonId, { packfile, head, parent })
  HubAPI-->>MCPServer: ok
  MCPServer->>HubAPI: api.fetchLessonHead(targetLessonId)
  HubAPI-->>MCPServer: base head | null
  MCPServer->>HubAPI: api.createPull(targetLessonId, { title, body, head, base, sourceLessonId })
  HubAPI-->>MCPServer: pull
  MCPServer->>HubAPI: api.uploadPullPack(targetLessonId, pull.id, { packfile, head })
  HubAPI-->>MCPServer: pull.ready
  HubAPI-->>Reviewer: pull_request notification (link /hub/:id/proposals/:pullId)
  MCPServer-->>Assistant: { proposalId, url, status, changes }
  Assistant-->>User: Share proposal url for review

  Reviewer->>HubAPI: Open proposal url and merge/decline
  HubAPI-->>Reviewer: Result (lesson updated or unchanged)
Loading

File-Level Changes

Change Details Files
Refactored MCP API client to support generic authenticated requests, git packfile upload/download, and proposal management while deduplicating token-refresh logic.
  • Introduced a low-level request() helper that handles bearer token refresh/retry and returns raw Responses.
  • Added readError() for consistent error surface across JSON and binary calls.
  • Implemented putPack() and getPack() helpers for git packfile PUT/GET with X-Git-Head/X-Git-Parent headers and 404-as-null semantics.
  • Extended createLesson to accept an optional forkedFrom field for recording fork origin.
  • Added fetchLessonPack, fetchLessonHead, pushLessonPack, listPulls, createPull, uploadPullPack, and closePull to manage lesson histories and proposals over MCP.
  • Reworked uploadImage to reuse the shared request()/readError() flow instead of custom token refresh logic.
apps/mcp/src/api.js
Added MCP tools and server-side git orchestration so assistants can fork lessons, edit the fork, propose changes back, and list proposals with reviewer-facing metadata.
  • Registered fork_lesson, propose_changes, and list_lesson_proposals tools with detailed guidance about when to fork vs patch and how humans review/merge.
  • Introduced proposalUrl and clientName helpers to construct hub proposal URLs and capture MCP client identity for attribution.
  • Wire fork_lesson to new git.js forkLesson helper, returning fork metadata, head commit, and a note about history cloning.
  • Wire propose_changes to git.js proposeChanges, returning proposal id, status, commit oid, described changes, and review URL.
  • Wire list_lesson_proposals to api.listPulls and enrich each pull with a hub proposal URL.
  • Bumped SERVER_INFO MCP server version from 0.1.3 to 0.2.0.
apps/mcp/src/tools.js
Documented the new fork/propose workflow and clarified pull request, notification, and version-history behavior for both web app and MCP server.
  • Updated MCP tools overview to include fork_lesson, propose_changes, and list_lesson_proposals, plus a dedicated section on choosing propose vs patch and mechanics of forks/proposals.
  • Extended web-app pull-requests docs to allow self-proposals when they come from owned forks and to explain MCP/self-proposal semantics and notification wording.
  • Clarified pull request API table to reflect author can POST /pulls only from a fork (sourceLessonId).
  • Augmented monorepo version-history docs with memfs and MCP-side fork/propose flow description.
  • Extended MCP server overview to mention fork-and-propose as an alternative to direct writes.
  • Adjusted notifications docs so pull_request notifications link to specific proposals and describe MCP-origin proposals with "Changes are waiting for your review" messaging.
apps/docs/docs/mcp-server/tools.md
apps/docs/docs/web-app/pull-requests.md
apps/docs/docs/monorepo/version-history.md
apps/docs/docs/mcp-server/overview.md
apps/docs/docs/web-app/notifications.md
Relaxed pull request opening rules for lesson authors while enforcing fork-based self-proposals and improved proposal notifications.
  • Changed POST /lessons/:id/pulls to resolve sourceLessonId before author checks and allow authors to open proposals only when sourceLessonId is a valid lesson they own different from the target.
  • Kept rejecting author proposals with no source fork, treating them as mistaken self-requests with a direct-save alternative.
  • Adjusted notification creation on pack upload to always notify lesson author, with special wording for MCP/self proposals and links directly to proposal detail pages instead of lesson root.
  • Ensured best-effort notification behavior without impacting successful proposal creation even if notifications fail.
apps/api/src/routes/pulls.js
Introduced an in-memory filesystem and repository context for isomorphic-git to enable stateless git operations in MCP and Worker environments.
  • Implemented memFs() as a node:fs-compatible promise API with POSIX error codes and in-memory node map, supporting read/write/mkdir/rmdir/readdir/stat/lstat/symlink/chmod/unlink.
  • Added path normalization, directory/parent validation, and symlink handling so isomorphic-git can operate correctly over the virtual fs.
  • Provided memRepo() helper that creates a repo context with memFs and a named gitdir usable by repo.js/pack.js.
  • Exported ./git/memfs from the core package for external consumers.
packages/core/src/git/memfs.js
packages/core/package.json
Implemented MCP-side fork and proposal orchestration atop the in-memory git engine, including assistant-attribution in proposal bodies and robust error handling around pack upload/compare-and-swap.
  • Added commitAuthor() that stamps commits with the signed-in user identity while treating attribution as user-bound rather than assistant-bound.
  • Implemented clamp() helper and proposalBody() to enforce title/body length limits and append an assistant/client provenance note, trimming while preserving the provenance.
  • Implemented cloneRepo() to build in-memory repos from stored packs, preserving ancestry for three-way merges.
  • Implemented forkLesson() to clone target lesson history into a new private draft, record forkedFrom, strip local-only fields, seed missing histories from documents, and push packs with parent=null, surfacing partial-fork failures without deleting the row.
  • Implemented proposeChanges() to validate fork/source relationship, require stored fork history, commit pending operations as a single commit with described ops, push fork history forward using compare-and-swap, open pulls with base/head/sourceLessonId, upload proposal packs, and close failed proposals to avoid empty review items.
apps/mcp/src/git.js
Added targeted tests to validate memfs behavior, MCP fork/propose correctness, ancestry guarantees, and failure handling, plus ensured tool exposure in smoke tests.
  • Added memfs.test.js to drive real isomorphic-git over memFs/memRepo, validating commit/pack/clone/merge-base flows and filesystem semantics and isolation between repos.
  • Added fork.test.js that uses a fake hub with pack compare-and-swap and pull pack head-matching to assert that forks clone history, proposals share ancestry with targets, unchanged forks are rejected, failed pack uploads withdraw proposals, proposing twice stacks, and assistant attribution appears in proposal bodies.
  • Extended MCP smoke.test.js to assert presence of fork_lesson, propose_changes, and list_lesson_proposals tools.
  • Verified overall behavior with new tests using real git engine rather than mocks.
packages/core/src/git/memfs.test.js
apps/mcp/test/fork.test.js
apps/mcp/test/smoke.test.js
Bumped MCP server and manifest versions to reflect new capabilities.
  • Updated apps/mcp/package.json version from 0.2.0 to 0.3.0 per AGENTS.md.
  • Updated apps/mcp/manifest.json version to 0.3.0 and added descriptions for new fork/propose/list_proposals tools.
apps/mcp/package.json
apps/mcp/manifest.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5e54cef-2e7c-4133-887a-818732e0a614

📥 Commits

Reviewing files that changed from the base of the PR and between 7e1c3e0 and c3f0a21.

📒 Files selected for processing (11)
  • apps/api/src/lib/lesson.js
  • apps/api/src/routes/pulls.js
  • apps/docs/docs/mcp-server/tools.md
  • apps/docs/docs/web-app/notifications.md
  • apps/docs/docs/web-app/pull-requests.md
  • apps/mcp/src/api.js
  • apps/mcp/src/git.js
  • apps/mcp/src/tools.js
  • apps/mcp/test/fork.test.js
  • packages/core/src/git/memfs.js
  • packages/core/src/git/memfs.test.js
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/docs/docs/mcp-server/tools.md
  • apps/docs/docs/web-app/notifications.md
  • apps/mcp/src/git.js
  • packages/core/src/git/memfs.js
  • apps/mcp/src/api.js
  • apps/api/src/routes/pulls.js
  • apps/docs/docs/web-app/pull-requests.md

📝 Walkthrough

Walkthrough

The MCP server now supports lesson forks, Git-backed proposals, and proposal listing. The API validates fork provenance, supports own-lesson proposals, and sends proposal-specific notifications. Documentation and package manifests describe the new workflow.

Changes

Lesson proposal workflow

Layer / File(s) Summary
In-memory Git repository support
packages/core/src/git/memfs.js, packages/core/src/git/memfs.test.js, packages/core/package.json, apps/docs/docs/monorepo/version-history.md
Adds an isolated promise-based in-memory filesystem and repository factory. Tests cover filesystem behavior, Git history, packing, cloning, ancestry, and repository isolation.
Authenticated lesson and proposal API
apps/mcp/src/api.js, apps/api/src/lib/lesson.js
Centralizes authenticated requests and adds lesson history, packfile, fork provenance, and proposal operations.
MCP fork and proposal tools
apps/mcp/src/git.js, apps/mcp/src/tools.js, apps/mcp/manifest.json, apps/mcp/package.json, apps/mcp/test/fork.test.js, apps/mcp/test/smoke.test.js, apps/docs/docs/mcp-server/*
Adds fork_lesson, propose_changes, and list_lesson_proposals. Tests cover fork creation, ancestry, repeated proposals, validation, cleanup, and proposal metadata.
Proposal permissions and notifications
apps/api/src/routes/pulls.js, apps/docs/docs/web-app/pull-requests.md, apps/docs/docs/web-app/notifications.md
Validates caller-owned source forks, permits valid proposals to the caller’s own lesson, and links notifications to the proposal route with updated wording.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant forkLesson
  participant createApi
  participant Hub
  participant proposeChanges
  MCPClient->>forkLesson: request lesson fork
  forkLesson->>createApi: fetch lesson content and history
  createApi->>Hub: read lesson pack
  Hub-->>MCPClient: return pack or no history
  forkLesson->>createApi: create private lesson and push pack
  MCPClient->>proposeChanges: submit fork changes
  proposeChanges->>createApi: create proposal and upload pack
  createApi->>Hub: store proposal pack
  Hub-->>MCPClient: return proposal metadata and review URL
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: assistants can propose lesson changes for review instead of modifying lessons directly.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/mcp-pr

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add MCP fork-and-propose workflow for lesson changes

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add MCP tools to fork lessons and propose changes for human review and merge.
• Enable self-proposals only when backed by an owned fork, and notify via proposal links.
• Introduce an in-memory git filesystem (memfs) to run fork/propose outside the browser.
Diagram

graph TD
  Client(["MCP Client"]) --> Mcp["MCP server tools"] --> Git["core/git (memfs+pack)"] --> Api["Hub API/Worker"] --> R2[("R2 pack storage")]
  Api --> Web["Web app reviewer"]
  Mcp --> Pulls["/lessons/:id/pulls"] --> Api

  subgraph Legend
    direction LR
    _u(["Client/User"]) ~~~ _svc["Service/Module"] ~~~ _api["API endpoint"] ~~~ _db[("Storage")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist a server-side repo cache between MCP calls
  • ➕ Avoids re-cloning packs each call; faster for repeated edits/proposes.
  • ➕ Could enable multi-commit proposals or richer history in the fork.
  • ➖ Requires durable storage and cache invalidation; harder in Workers.
  • ➖ Increases risk of state leakage/corruption between requests.
  • ➖ Undermines the current robustness goal (survive restarts / worker migrations).

Recommendation: Keep the PR’s approach: explicit fork → edit fork with existing tools → propose a one-commit snapshot. It matches the web app’s model, preserves the “human decides” boundary (no merge tool), and the stateless memfs-based cloning keeps the MCP server robust across restarts and Worker instance moves.

Files changed (16) +1645 / -89

Enhancement (3) +641 / -53
api.jsAdd Worker endpoints for git packs and proposals; refactor request handling +181/-52

Add Worker endpoints for git packs and proposals; refactor request handling

• Introduces a lower-level 'request()' for raw responses (JSON and binary), plus shared error parsing. Adds helpers for fetching/pushing lesson packfiles and creating/uploading/closing proposals, enabling server-side fork/propose workflows.

apps/mcp/src/api.js

git.jsImplement forkLesson/proposeChanges using memfs-backed git repos +298/-0

Implement forkLesson/proposeChanges using memfs-backed git repos

• Adds stateless fork-and-propose logic: clone from stored packs when possible, seed history from docs when not, commit snapshot changes, push fork history, and open/upload proposals. Stamps proposal bodies with the MCP client name for provenance and withdraws empty proposals if pack upload fails.

apps/mcp/src/git.js

tools.jsRegister fork_lesson, propose_changes, and list_lesson_proposals MCP tools +162/-1

Register fork_lesson, propose_changes, and list_lesson_proposals MCP tools

• Wires new git-based tools into the MCP server, including client-version capture for proposal provenance and generated proposal URLs. Updates SERVER_INFO version to reflect the expanded server capabilities.

apps/mcp/src/tools.js

Bug fix (1) +36 / -15
pulls.jsAllow self-proposals when backed by an owned fork; adjust notifications +36/-15

Allow self-proposals when backed by an owned fork; adjust notifications

• Moves 'sourceLessonId' resolution earlier so the self-proposal guard can allow proposals that carry an owned fork. Updates proposal notifications to always fire (including self-proposals), with neutral wording and a deep link to the specific proposal page.

apps/api/src/routes/pulls.js

Tests (3) +572 / -0
fork.test.jsAdd integration-style tests for fork/propose pack ancestry and failure modes +413/-0

Add integration-style tests for fork/propose pack ancestry and failure modes

• Adds a fake hub that stores packfiles with CAS semantics and verifies forks/proposals preserve git ancestry for true three-way merges. Covers edge cases: no history, rename commits, repeated proposals stacking, no-op proposals, and cleanup on upload failure.

apps/mcp/test/fork.test.js

smoke.test.jsUpdate smoke test tool list for new MCP tools +3/-0

Update smoke test tool list for new MCP tools

• Extends the MCP tool exposure test to include fork_lesson, propose_changes, and list_lesson_proposals.

apps/mcp/test/smoke.test.js

memfs.test.jsValidate memfs compatibility with isomorphic-git and repo isolation +156/-0

Validate memfs compatibility with isomorphic-git and repo isolation

• Adds tests proving isomorphic-git accepts memfs (promise API detection, ENOENT behavior, readdir semantics). Exercises real git flows over memfs: commit, pack, clone, merge-base/ancestry, and verifies isolated repos don’t leak state.

packages/core/src/git/memfs.test.js

Documentation (5) +107 / -19
overview.mdDocument MCP review flow via forks and proposals +6/-0

Document MCP review flow via forks and proposals

• Adds overview guidance that MCP can fork and propose changes instead of writing directly. Links to the detailed tools documentation for the fork-and-propose workflow.

apps/docs/docs/mcp-server/overview.md

tools.mdAdd docs for fork_lesson/propose_changes/list_lesson_proposals +63/-13

Add docs for fork_lesson/propose_changes/list_lesson_proposals

• Expands the tools table and adds a dedicated section explaining when to patch directly vs fork-and-propose. Documents mechanics like snapshot commits, draft caps, and why merging is not an MCP tool.

apps/docs/docs/mcp-server/tools.md

version-history.mdDocument memfs and server-side fork/propose implementation +9/-1

Document memfs and server-side fork/propose implementation

• Updates version-history docs to include the new memfs module and explains how the MCP server reuses the browser git flow server-side without LightningFS. Clarifies that memfs is used in Node/Worker/tests.

apps/docs/docs/monorepo/version-history.md

notifications.mdClarify proposal notification links and self-proposal wording +4/-1

Clarify proposal notification links and self-proposal wording

• Updates notification behavior description so pull_request notifications link to proposals, not the lesson root. Documents the special-case wording for proposals opened from the author’s own account (MCP assistant case).

apps/docs/docs/web-app/notifications.md

pull-requests.mdAllow authors to propose via owned forks; document self-proposal rules +25/-4

Allow authors to propose via owned forks; document self-proposal rules

• Updates the permissions matrix and adds a new section explaining self-proposals are allowed only when 'sourceLessonId' is an owned fork. Updates API table text to reflect the new rule.

apps/docs/docs/web-app/pull-requests.md

Other (4) +289 / -2
manifest.jsonBump MCP manifest and expose new proposal-related tools +13/-1

Bump MCP manifest and expose new proposal-related tools

• Increments manifest version to 0.3.0 and adds fork_lesson, propose_changes, and list_lesson_proposals to the tool list with descriptions.

apps/mcp/manifest.json

package.jsonBump MCP package version to 0.3.0 +1/-1

Bump MCP package version to 0.3.0

• Updates the MCP package version to align with the new tool surface and behavior.

apps/mcp/package.json

package.jsonExport core/git/memfs entrypoint +1/-0

Export core/git/memfs entrypoint

• Adds the new memfs module to the package exports so downstream consumers (apps/mcp) can import it via @spelling-creator/core/git/memfs.

packages/core/package.json

memfs.jsAdd in-memory node:fs-compatible filesystem for isomorphic-git +274/-0

Add in-memory node:fs-compatible filesystem for isomorphic-git

• Implements a POSIX-error-code, promise-based in-memory filesystem sufficient for isomorphic-git operations (read/write, dirs, symlinks, stats). Provides 'memRepo()' helper returning '{ fs, gitdir }' contexts for git operations in non-browser hosts.

packages/core/src/git/memfs.js

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In api.fetchLessonHead, all non-OK responses are treated as null, which will silently swallow server-side errors; consider distinguishing 404/no-history from other error statuses so genuine failures are surfaced to callers.
  • Several user-facing error messages in forkLesson/proposeChanges include raw err.message from lower layers in parentheses; you might want to standardise these to clearer, high-level messages to avoid leaking internal details into MCP-facing errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `api.fetchLessonHead`, all non-OK responses are treated as `null`, which will silently swallow server-side errors; consider distinguishing 404/no-history from other error statuses so genuine failures are surfaced to callers.
- Several user-facing error messages in `forkLesson`/`proposeChanges` include raw `err.message` from lower layers in parentheses; you might want to standardise these to clearer, high-level messages to avoid leaking internal details into MCP-facing errors.

## Individual Comments

### Comment 1
<location path="apps/mcp/test/fork.test.js" line_range="158-148" />
<code_context>
+  return { id, doc, head: first.oid };
+}
+
+test("forking clones the lesson's history under a new private draft", async () => {
+  const hub = fakeHub();
+  const source = await seedLesson(hub, {
+    title: "Volcanoes",
+    text: "A volcano ERUPTS.",
+  });
+
+  const { lesson, head, clonedHistory } = await forkLesson(hub.api, {
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for the failure path when storing a fork's history fails in `forkLesson`.

Since the fake hub’s `pushLessonPack` always succeeds, we never exercise the error branch where history storage fails but the fork lesson row is kept. Please add a test that uses an API where `pushLessonPack` throws (e.g. simulating R2 issues) and asserts that:
- `forkLesson` rejects with an error message containing the fork lesson id and guidance to delete/refork, and
- the fork lesson row remains in `hub.lessons`.

This will lock in the intended UX and guard against regressions in this error handling path.

Suggested implementation:

```javascript
  assert.equal(
    head,
    source.head,
    "an unedited fork sits on the original's own commit, so nothing was rewritten",
  );
});

test("forking keeps draft when history storage fails", async () => {
  const hub = fakeHub();
  const source = await seedLesson(hub, {
    title: "Volcanoes",
    text: "A volcano ERUPTS.",
  });

  // Create an API that behaves like the hub API but whose pushLessonPack fails.
  const failingApi = {
    ...hub.api,
    async pushLessonPack(...args) {
      throw new Error("simulated R2 failure while storing fork history");
    },
  };

  let error;
  try {
    await forkLesson(failingApi, { lessonId: source.id });
  } catch (err) {
    error = err;
  }

  // We expect forkLesson to reject with an error containing the fork lesson id
  // and guidance for the user to delete/refork.
  assert.ok(error instanceof Error, "forkLesson should reject when history storage fails");
  assert.match(
    error.message,
    /fork lesson id/i,
    "error message should mention the fork lesson id",
  );
  assert.match(
    error.message,
    /delete.*refork/i,
    "error message should suggest deleting and reforking the lesson",
  );

  // Extract the fork lesson id from the error message (as emitted by forkLesson).
  const forkLessonIdMatch = error.message.match(/fork lesson id\s*[:=]\s*([0-9a-f-]+)/i);
  assert.ok(forkLessonIdMatch, "error message should contain a parseable fork lesson id");
  const forkLessonId = forkLessonIdMatch[1];

  // The fork lesson row should remain present in hub.lessons.
  assert.ok(
    hub.lessons.has(forkLessonId),
    "the fork lesson row should remain in hub.lessons after a history storage failure",
  );
});

```

1. Adjust the regular expressions in the `assert.match` and `error.message.match` calls to match the actual error message format that `forkLesson` produces (for example, if it uses a different phrase than "fork lesson id").
2. Ensure that `hub.lessons` exposes a `has(id)` method; if it is a plain object rather than a `Map`, replace `hub.lessons.has(forkLessonId)` with something like `hub.lessons[forkLessonId]` or `Object.hasOwn(hub.lessons, forkLessonId)` to align with the existing hub implementation.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

doc,
published: true,
forkedFrom: null,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for the failure path when storing a fork's history fails in forkLesson.

Since the fake hub’s pushLessonPack always succeeds, we never exercise the error branch where history storage fails but the fork lesson row is kept. Please add a test that uses an API where pushLessonPack throws (e.g. simulating R2 issues) and asserts that:

  • forkLesson rejects with an error message containing the fork lesson id and guidance to delete/refork, and
  • the fork lesson row remains in hub.lessons.

This will lock in the intended UX and guard against regressions in this error handling path.

Suggested implementation:

  assert.equal(
    head,
    source.head,
    "an unedited fork sits on the original's own commit, so nothing was rewritten",
  );
});

test("forking keeps draft when history storage fails", async () => {
  const hub = fakeHub();
  const source = await seedLesson(hub, {
    title: "Volcanoes",
    text: "A volcano ERUPTS.",
  });

  // Create an API that behaves like the hub API but whose pushLessonPack fails.
  const failingApi = {
    ...hub.api,
    async pushLessonPack(...args) {
      throw new Error("simulated R2 failure while storing fork history");
    },
  };

  let error;
  try {
    await forkLesson(failingApi, { lessonId: source.id });
  } catch (err) {
    error = err;
  }

  // We expect forkLesson to reject with an error containing the fork lesson id
  // and guidance for the user to delete/refork.
  assert.ok(error instanceof Error, "forkLesson should reject when history storage fails");
  assert.match(
    error.message,
    /fork lesson id/i,
    "error message should mention the fork lesson id",
  );
  assert.match(
    error.message,
    /delete.*refork/i,
    "error message should suggest deleting and reforking the lesson",
  );

  // Extract the fork lesson id from the error message (as emitted by forkLesson).
  const forkLessonIdMatch = error.message.match(/fork lesson id\s*[:=]\s*([0-9a-f-]+)/i);
  assert.ok(forkLessonIdMatch, "error message should contain a parseable fork lesson id");
  const forkLessonId = forkLessonIdMatch[1];

  // The fork lesson row should remain present in hub.lessons.
  assert.ok(
    hub.lessons.has(forkLessonId),
    "the fork lesson row should remain in hub.lessons after a history storage failure",
  );
});
  1. Adjust the regular expressions in the assert.match and error.message.match calls to match the actual error message format that forkLesson produces (for example, if it uses a different phrase than "fork lesson id").
  2. Ensure that hub.lessons exposes a has(id) method; if it is a plain object rather than a Map, replace hub.lessons.has(forkLessonId) with something like hub.lessons[forkLessonId] or Object.hasOwn(hub.lessons, forkLessonId) to align with the existing hub implementation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (9)
packages/core/src/git/memfs.test.js (2)

30-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extend the contract test to chmod.

The method list omits chmod, which memFs implements and isomorphic-git binds. Add it so a future removal of chmod fails this test.

♻️ Proposed addition
       "readlink",
       "symlink",
+      "chmod",
     ]) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/git/memfs.test.js` around lines 30 - 43, Extend the method
list in the memFs contract test to include "chmod", ensuring fs.promises.chmod
is validated as a function alongside the existing filesystem methods.

150-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive assertion to the isolation test.

The test asserts only that repo b has no head. If commitDoc on repo a failed silently, the test would still pass. Assert that a has a head, so the test proves isolation instead of absence of work.

💚 Proposed assertion
     const a = memRepo();
     const b = memRepo();
-    await commitDoc({ ...a, doc: doc("A", "a"), author });
+    const committed = await commitDoc({ ...a, doc: doc("A", "a"), author });
+    expect(await headOid(a)).toBe(committed.oid);
     expect(await headOid(b)).toBeNull();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/git/memfs.test.js` around lines 150 - 155, Update the
“keeps two in-memory repos out of each other’s way” test to assert that
headOid(a) is non-null after committing to repo a, while preserving the existing
null assertion for repo b.
apps/mcp/test/fork.test.js (2)

150-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import packRepo statically with the other pack helpers.

cloneFromPack, contains, and mergeBase come from a static import at lines 17-21. packRepo uses a dynamic import inside seedLesson, which repeats the module resolution on every seed and splits one module's imports across two styles.

♻️ Proposed change
@@ imports
 import {
   cloneFromPack,
   contains,
   mergeBase,
+  packRepo,
 } from "`@spelling-creator/core/git/pack`";
   const ctx = memRepo("seed");
   const first = await commitDoc({ ...ctx, doc, author: AUTHOR });
-  const { packRepo } = await import("`@spelling-creator/core/git/pack`");
   const packed = await packRepo(ctx);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mcp/test/fork.test.js` around lines 150 - 156, Move packRepo into the
existing static import alongside cloneFromPack, contains, and mergeBase, then
remove the dynamic import from seedLesson while preserving its current usage.

250-270: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the client name reaches the created proposal.

The test passes client: "Claude Desktop" but asserts nothing about it. Recording the MCP client name on the proposal is a stated objective of this PR, and proposalBody is only tested in isolation at lines 403-413. Assert that result.pull.body names the client, so the wiring from proposeChanges through createPull stays covered.

💚 Proposed assertion
   assert.equal(result.pull.head, result.commit);
+  assert.match(
+    result.pull.body,
+    /Claude Desktop/,
+    "the proposal records which client opened it",
+  );
   assert.deepEqual(result.changes, ["- edit text block b1 (text)"]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mcp/test/fork.test.js` around lines 250 - 270, Extend the assertions for
the result returned by proposeChanges to verify that result.pull.body includes
the passed client name, "Claude Desktop". Keep the existing proposal and change
assertions unchanged, covering the wiring through createPull rather than only
testing proposalBody in isolation.
packages/core/src/git/memfs.js (2)

100-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

put assigns a new inode on every write.

put runs nextIno++ for each call, so overwriting an existing file changes its ino. node:fs keeps the inode stable across writes. Bare repositories have no index, so isomorphic-git's stat cache is not consulted here and the behaviour is safe today. Preserving an existing inode would keep the emulation faithful if a caller later uses fs with a working tree.

♻️ Proposed inode preservation
   function put(path, node) {
-    nodes.set(path, { ino: nextIno++, mtimeMs: now(), ...node });
+    const existing = nodes.get(path);
+    nodes.set(path, {
+      ino: existing?.ino ?? nextIno++,
+      mtimeMs: now(),
+      ...node,
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/git/memfs.js` around lines 100 - 102, Update put so
overwriting an existing path preserves its current ino, while newly inserted
paths continue receiving nextIno++. Keep the existing mtimeMs refresh and node
merge behavior unchanged.

147-159: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider copying bytes on write and on read.

toBytes returns the caller's Uint8Array unchanged, and readFile returns the stored array itself. The volume therefore shares memory with its callers. If any caller reuses or mutates a buffer after a write, or mutates the array it read, the stored object silently changes. LightningFS does not expose this aliasing. A copy on write keeps the volume authoritative for a small allocation cost.

♻️ Proposed defensive copy on write
       put(full, {
         type: "file",
-        data: toBytes(data),
+        data: new Uint8Array(toBytes(data)),
         mode: options?.mode ?? existing?.mode ?? FILE_MODE,
       });

Also applies to: 138-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/git/memfs.js` around lines 147 - 159, Update memfs
writeFile and readFile to defensively copy Uint8Array data at both storage and
retrieval boundaries. Ensure put stores a new byte array rather than the
caller-owned result of toBytes, and readFile returns a separate copy so callers
cannot mutate volume state.
apps/mcp/src/api.js (1)

222-298: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider one shared helper for the "unwrap a named field" pattern.

listPulls, createPull, uploadPullPack, and closePull each repeat data.X || fallback. A small pullOf(data) helper would remove four near-identical unwraps. This is optional and does not change behaviour.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mcp/src/api.js` around lines 222 - 298, Optionally add a shared
pullOf(data) helper for extracting the pull field with the existing null
fallback, then reuse it in listPulls, createPull, uploadPullPack, and closePull
without changing their current behavior or validation.
apps/mcp/src/git.js (1)

63-69: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make clamp safe for a non-positive limit.

proposalBody derives the limit as PULL_BODY_MAX - note.length - 2. If that value is ever non-positive, text.slice(0, limit - 1) uses a negative index and keeps almost the whole string, so the result can exceed PULL_BODY_MAX and the hub rejects the proposal with a 400. A single lower bound removes the case.

♻️ Proposed refactor
 function clamp(value, limit) {
   const text = (value || "").trim();
-  if (text.length <= limit) return text;
+  const max = Math.max(1, limit);
+  if (text.length <= max) return text;
-  const cut = text.slice(0, limit - 1);
+  const cut = text.slice(0, max - 1);
   const space = cut.lastIndexOf(" ");
-  return `${(space > limit * 0.8 ? cut.slice(0, space) : cut).trimEnd()}…`;
+  return `${(space > max * 0.8 ? cut.slice(0, space) : cut).trimEnd()}…`;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mcp/src/git.js` around lines 63 - 69, Update clamp to handle
non-positive limit values before slicing, returning an empty string (or the
established minimal result) when the limit is zero or below; preserve the
existing truncation and ellipsis behavior for positive limits so proposalBody
remains within PULL_BODY_MAX.
apps/api/src/routes/pulls.js (1)

303-329: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Validate sourceLessonId provenance before allowing a self-lesson proposal. An owned, unrelated lesson bypasses the self-proposal guard. Include forked_from in fetchLessonRow and require source.forked_from === lessonId before setting sourceLessonId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/pulls.js` around lines 303 - 329, Update the
source-lesson validation before the self-lesson guard to fetch fork provenance
via fetchLessonRow, including forked_from in its selected fields. Only set
sourceLessonId when the source is owned by user.id and source.forked_from equals
lessonId, while preserving the existing format, non-self, and invalid-value
checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/docs/mcp-server/tools.md`:
- Around line 7-22: Add a create_lesson_file row to the tool table alongside the
other lesson creation tools, describing its registered file-based lesson
creation behavior. Ensure the table matches the server registration and the
existing prose reference without changing unrelated entries.

In `@apps/docs/docs/web-app/notifications.md`:
- Around line 31-36: Update the notification documentation around the proposal
link description to accurately distinguish initial pull_request notifications
from merged and closed notifications, which link to /hub/${lessonId};
alternatively, update those notification paths to link to the proposal
consistently. Keep the documented behavior aligned with the implemented
notification link targets.

In `@apps/mcp/src/git.js`:
- Around line 252-293: Handle a null result from commitDoc before calling
packRepo, pushLessonPack, createPull, or uploadPullPack, and return the existing
no-op outcome or throw the appropriate validation error before any remote write.
Keep the normal flow unchanged for a non-null commit, including using commit.oid
in the successful result.

In `@apps/mcp/src/tools.js`:
- Around line 635-639: Update the title schema in proposeChanges to require at
least one character by adding the minimum-length validation to the existing
z.string() definition, while preserving its current description and handling of
non-empty titles.
- Line 1015: Update SERVER_INFO.version from 0.2.0 to 0.3.0 so it matches the
versions declared in apps/mcp/package.json and apps/mcp/manifest.json, keeping
all three version values aligned.

---

Nitpick comments:
In `@apps/api/src/routes/pulls.js`:
- Around line 303-329: Update the source-lesson validation before the
self-lesson guard to fetch fork provenance via fetchLessonRow, including
forked_from in its selected fields. Only set sourceLessonId when the source is
owned by user.id and source.forked_from equals lessonId, while preserving the
existing format, non-self, and invalid-value checks.

In `@apps/mcp/src/api.js`:
- Around line 222-298: Optionally add a shared pullOf(data) helper for
extracting the pull field with the existing null fallback, then reuse it in
listPulls, createPull, uploadPullPack, and closePull without changing their
current behavior or validation.

In `@apps/mcp/src/git.js`:
- Around line 63-69: Update clamp to handle non-positive limit values before
slicing, returning an empty string (or the established minimal result) when the
limit is zero or below; preserve the existing truncation and ellipsis behavior
for positive limits so proposalBody remains within PULL_BODY_MAX.

In `@apps/mcp/test/fork.test.js`:
- Around line 150-156: Move packRepo into the existing static import alongside
cloneFromPack, contains, and mergeBase, then remove the dynamic import from
seedLesson while preserving its current usage.
- Around line 250-270: Extend the assertions for the result returned by
proposeChanges to verify that result.pull.body includes the passed client name,
"Claude Desktop". Keep the existing proposal and change assertions unchanged,
covering the wiring through createPull rather than only testing proposalBody in
isolation.

In `@packages/core/src/git/memfs.js`:
- Around line 100-102: Update put so overwriting an existing path preserves its
current ino, while newly inserted paths continue receiving nextIno++. Keep the
existing mtimeMs refresh and node merge behavior unchanged.
- Around line 147-159: Update memfs writeFile and readFile to defensively copy
Uint8Array data at both storage and retrieval boundaries. Ensure put stores a
new byte array rather than the caller-owned result of toBytes, and readFile
returns a separate copy so callers cannot mutate volume state.

In `@packages/core/src/git/memfs.test.js`:
- Around line 30-43: Extend the method list in the memFs contract test to
include "chmod", ensuring fs.promises.chmod is validated as a function alongside
the existing filesystem methods.
- Around line 150-155: Update the “keeps two in-memory repos out of each other’s
way” test to assert that headOid(a) is non-null after committing to repo a,
while preserving the existing null assertion for repo b.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc51f9d7-fffc-483a-b0cd-d02f8144f15f

📥 Commits

Reviewing files that changed from the base of the PR and between 64ea8a9 and 7e1c3e0.

📒 Files selected for processing (16)
  • apps/api/src/routes/pulls.js
  • apps/docs/docs/mcp-server/overview.md
  • apps/docs/docs/mcp-server/tools.md
  • apps/docs/docs/monorepo/version-history.md
  • apps/docs/docs/web-app/notifications.md
  • apps/docs/docs/web-app/pull-requests.md
  • apps/mcp/manifest.json
  • apps/mcp/package.json
  • apps/mcp/src/api.js
  • apps/mcp/src/git.js
  • apps/mcp/src/tools.js
  • apps/mcp/test/fork.test.js
  • apps/mcp/test/smoke.test.js
  • packages/core/package.json
  • packages/core/src/git/memfs.js
  • packages/core/src/git/memfs.test.js

Comment thread apps/docs/docs/mcp-server/tools.md
Comment thread apps/docs/docs/web-app/notifications.md Outdated
Comment thread apps/mcp/src/git.js Outdated
Comment thread apps/mcp/src/tools.js
Comment thread apps/mcp/src/tools.js Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Self-proposal guard bypassed by unrelated owned lesson ✓ Resolved 🐞 Bug ≡ Correctness
Description
openPull accepts sourceLessonId as long as the referenced lesson exists and is owned by the caller,
but never verifies that lesson is actually a fork of the target (forked_from === lessonId). This
lets an author bypass the new 'no self-proposal' rule against lesson A by citing any other lesson B
they own as sourceLessonId, defeating the guard and recording false fork provenance on the proposal.
Code

apps/api/src/routes/pulls.js[R309-327]

+	let sourceLessonId = null;
+	const claimed = typeof body.sourceLessonId === 'string' ? body.sourceLessonId.trim() : '';
+	if (LESSON_ID_RE.test(claimed) && claimed !== lessonId) {
+		const source = await fetchLessonRow(env, base, claimed);
+		if (source && source.author_id === user.id) sourceLessonId = claimed;
+	}
+
+	// Proposing to your own lesson is refused when there is nothing behind it: you
+	// can simply save, and a request to yourself out of nowhere is a mistake.
+	//
+	// It is allowed when it carries a fork you own, because then it means something
+	// specific and useful — "here is a copy with changes in it, let me read the diff
+	// before it lands". That is the shape of an AI assistant's work: over MCP the
+	// assistant acts as the account it is signed in with, so changes it proposes to
+	// the user's own lesson arrive from the user's own id (see apps/mcp/src/git.js).
+	// Holding them in the review queue is the whole point — the lesson is untouched
+	// until a person reads the diff and merges it. A human gets the same route via
+	// "fork into a new lesson" in the editor.
+	if (lesson.author_id === user.id && !sourceLessonId) {
Relevance

●●● Strong

Auth/guard logic flaw enabling self-proposal bypass; team likely fixes security/correctness issues
in pull flow.

PR-#39

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code only checks source.author_id === user.id before setting sourceLessonId, never
source.forked_from === lessonId; this sourceLessonId is then the sole condition that lets
lesson.author_id === user.id proposals through at line 327. Any authenticated user with two or
more of their own lessons can trigger a self-proposal on a lesson unrelated to the one cited as
source, since ownership of *any* other lesson satisfies the check.

apps/api/src/routes/pulls.js[309-327]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `openPull`, `sourceLessonId` is validated only by checking that the referenced lesson exists and is owned by the caller (`source.author_id === user.id`). It does not check that the source lesson is actually a fork of the target lesson (i.e. `source.forked_from === lessonId`). Because this `sourceLessonId` is the sole condition that permits a self-proposal (`lesson.author_id === user.id && !sourceLessonId` check), an author can bypass the 'no self-proposal' rule by citing any unrelated lesson they own.

## Issue Context
The self-proposal guard was intentionally relaxed in this PR to allow an AI assistant (or a human via 'fork into a new lesson') to open a proposal against their own lesson when it carries real changes from a fork they own. The validation needs to ensure the cited fork genuinely descends from the target lesson, not just that the caller owns it.

## Fix Focus Areas
- apps/api/src/routes/pulls.js[309-314]
- apps/api/src/routes/pulls.js[327-329]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Failed proposals consume changes ✓ Resolved 🐞 Bug ☼ Reliability
Description
proposeChanges advances the fork’s stored head before creating and uploading the proposal, so a
later failure leaves the document equal to that head and retries report “nothing to propose.” The
changes remain in history, but the exact failed proposal cannot be retried without making another
edit.
Code

apps/mcp/src/git.js[R264-267]

+  await api.pushLessonPack(forkLessonId, {
+    packfile: packed.packfile,
+    head: packed.head,
+    parent: forkPack.head,
Relevance

●● Moderate

Retry/transactionality issue is real, but changing push timing may be debated vs. intended “commit
then propose” flow.

PR-#35
PR-#33

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Retry eligibility is computed only by diffing the saved document against the stored fork head. This
change pushes the new head before createPull and uploadPullPack, while failure cleanup closes
only the pull row, so the next call sees zero pending operations.

apps/mcp/src/git.js[243-268]
apps/mcp/src/git.js[275-296]
apps/mcp/test/fork.test.js[352-374]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed `createPull` or `uploadPullPack` currently leaves the fork history advanced, causing the next `propose_changes` call to see no pending operations and reject the retry.

## Issue Context
Build the proposal snapshot without irreversibly consuming the fork’s pending state. Advance the durable fork head only after successful proposal delivery, or implement rollback/idempotent retry behavior for every failure after the commit is built. Add tests that retry after both pull creation and pack upload failures.

## Fix Focus Areas
- apps/mcp/src/git.js[243-296]
- apps/mcp/test/fork.test.js[352-375]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Concurrent save forks stale content ✓ Resolved 🐞 Bug ≡ Correctness
Description
forkLesson reads the source document before its history, allowing a concurrent browser save to
pair an old document with a newer pack. The reconciliation commit then places stale content atop the
new history, so a later proposal can offer to revert the concurrent edit if merged.
Code

apps/mcp/src/git.js[123]

+  const pack = await api.fetchLessonPack(lessonId);
Relevance

●● Moderate

Plausible race but requires careful ordering/atomicity changes; no close precedent for MCP git
fork/propose concurrency.

PR-#35
PR-#33

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The MCP flow fetches the row at line 118 and the pack later at line 123, then commits the earlier
row document whenever it differs from pack HEAD. The browser explicitly pushes history first and
updates the row afterward, proving an interleaving where MCP observes the old row followed by the
new pack.

apps/mcp/src/git.js[117-158]
apps/web/src/pages/EditorPage.jsx[1128-1161]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fork creation can combine a source row and git pack from different versions, then commit stale row content on top of newer history.

## Issue Context
The browser saves history before updating the lesson row. Make fork creation obtain a consistent document/history snapshot, such as through an atomic API response or by rereading and validating the source head/version before creating the fork; retry when the source changes during capture.

## Fix Focus Areas
- apps/mcp/src/git.js[117-158]
- apps/mcp/src/api.js[222-236]
- apps/web/src/pages/EditorPage.jsx[1128-1161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Server version remains stale ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The package and manifest are versioned 0.3.0, but SERVER_INFO advertises 0.2.0 through both
MCP transports. Runtime diagnostics and client-visible server metadata therefore identify this
release as the previous version.
Code

apps/mcp/src/tools.js[R1013-1016]

export const SERVER_INFO = {
  name: "spelling-creator-hub",
-  version: "0.1.3",
+  version: "0.2.0",
};
Relevance

●●● Strong

Simple version metadata inconsistency; low-risk, deterministic fix aligns with prior
version/metadata corrections.

PR-#37

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both distribution metadata files declare 0.3.0, while the shared SERVER_INFO used to construct
the stdio and Worker servers declares 0.2.0.

apps/mcp/src/tools.js[1013-1016]
apps/mcp/package.json[1-4]
apps/mcp/manifest.json[2-5]
apps/mcp/src/stdio.js[17-29]
apps/mcp/src/worker.js[30-94]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The runtime MCP server version is one release behind the package and manifest version.

## Issue Context
Update `SERVER_INFO.version` to `0.3.0`, and preferably add a consistency assertion so future release bumps cannot diverge across runtime, package, and manifest metadata.

## Fix Focus Areas
- apps/mcp/src/tools.js[1013-1016]
- apps/mcp/package.json[1-4]
- apps/mcp/manifest.json[2-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 32 rules
✅ Skills: shadcn
Review mode: 🧠 Deep: This is a high-density behavioral change spanning API authorization, MCP tools, git pack/merge flows, in-memory filesystem semantics, persistence, and notifications, with 30 independent edit sites and multiple subtle failure modes.

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/mcp/src/git.js Outdated
Comment thread apps/mcp/src/git.js Outdated
Comment thread apps/mcp/src/tools.js
Comment thread apps/api/src/routes/pulls.js Outdated
Three bots, ten findings. The four that were real bugs:

A proposal pushed the fork's history *before* opening the request, so any
failure after that point left the fork's document equal to its own history —
the changes safe, but the retry finding nothing pending and refusing it. The
push is bookkeeping (a proposal's pack is stored with the proposal, and that is
what a reviewer merges), so it now happens last and cannot fail the call; a
failed proposal leaves the fork untouched and the retry simply works.

commitDoc returns null when the tree is unchanged, and pendingOps answers the
looser question of whether the *documents* differ, so the two can disagree. The
null was then dereferenced in the return value — after the proposal had gone
live, reporting failure for something that had succeeded. It fails before
anything is sent instead.

Forking read the source document before its history. A lesson being saved in
the browser writes those in the other order, so that pairing could put stale
content on top of newer commits and quietly revert the save it raced. Reading
the history first makes the bad pairing unreachable.

And openPull accepted any lesson the caller owned as a proposal's source, while
the source is the one thing that unlocks a self-proposal — so citing an
unrelated lesson of your own turned the rule off entirely and recorded a fork
link to something that wasn't one. It now requires the source to be forked from
this lesson, which is what the documentation already claimed.

The rest: require a non-empty proposal title, since the hub rejects an empty one
only after the whole snapshot has been built and sent; align SERVER_INFO with
the package and manifest version, which clients actually display; keep a file's
inode when it is rewritten, and state the by-reference contract memfs departs on;
bound a client's self-reported name before it goes in a proposal; and let
fetchLessonHead throw on a real failure rather than reporting "no history".

The tool table was also missing create_lesson_file, and the notification wording
claimed every pull_request link opens the proposal when merged and closed ones
open the lesson.

Tests for the paths that had none: a fork whose history can't be stored, a
failed proposal retried without further edits, a proposal surviving a failed
history push, the fork read ordering, and that the client name reaches the body
a reviewer reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@playforge-coding

Copy link
Copy Markdown
Owner Author

Thanks — worked through all ten findings in c3f0a21. Four were real bugs.

Fixed (bugs)

Failed proposals consumed the changes (@qodo-code-review #1). The fork's history was pushed before the request was opened, so any failure after that left the fork's document equal to its own history — the changes safe, but the retry finding nothing pending and refusing. Since a proposal's pack is stored with the proposal, and that's what a reviewer merges, the push is bookkeeping: it now happens last and can't fail the call (reported as historyPushed: false). A failed proposal leaves the fork untouched and the retry just works.

commitDoc null deref (@coderabbitai, critical). Correct, and the ordering point was the important half — commit.oid was read after createPull/uploadPullPack, so a null would have reported failure for a proposal that had already gone live. Now guarded before anything is sent.

Fork read a stale document over newer history (@qodo-code-review #2). Confirmed against EditorPage.jsx:1128-1136 — the editor pushes history first and the doc row second, so reading doc-then-pack could pair yesterday's document with today's history, and the reconciliation commit would revert the save it raced. Reading the pack first makes that pairing unreachable.

Narrowing, not elimination: a fully atomic doc+head read would need an API change. Worth noting the residual window is now the harmless direction (a newer document over older history, which the commit just carries forward).

Self-proposal guard was bypassable (@qodo-code-review #4). Right, and it contradicted the docs I'd written ("a fork you own"): ownership alone was satisfied by any other lesson, which turned the rule off entirely and attached a fork link to a non-fork. Now requires source.forked_from === lessonId. sourceLessonId acceptance is unchanged for the non-self case, where it stays informational, so no regression for the web app.

Fixed (the rest)

  • Non-empty title on propose_changes — the hub rejected an empty one only after the snapshot had been built and sent.
  • SERVER_INFO0.3.0. I'd bumped it on its own sequence (0.1.3 → 0.2.0) since it was already out of step; two reviewers flagged it, and there's no upside to a third sequence when this is the version clients display.
  • memFs.put kept a new inode on every overwrite — a rewritten file isn't a different file, and isomorphic-git reads stat data for index caching.
  • clamp guarded for a non-positive limit, and the client's self-reported name bounded before it reaches a proposal body.
  • fetchLessonHead now throws on a real failure instead of flattening every bad status to "no history"; its one caller still opts out via .catch.
  • Docs: added the missing create_lesson_file row (pre-existing gap), and corrected the notification wording — merged/closed link to the lesson, only a proposal-to-review links to the proposal.

New tests

The paths that had none: a fork whose history can't be stored (row kept, clear error), a failed proposal retried with no further edit, a proposal surviving a failed history push, the fork read ordering, and that the client name reaches the body a reviewer actually reads.

Skipped

Copy bytes on read/write in memFs (@coderabbitai nitpick). A real departure from a real filesystem, but packfiles run to megabytes and this would copy every blob and pack twice per operation to protect against a mutation nothing performs — the git engine treats both as immutable. Documented the by-reference contract in the module header instead, so anything else adopting it knows the rule.

A shared "unwrap a named field" helper in api.js (@coderabbitai nitpick). data.lesson || {} / data.pull || null differ in their empty value and each reads fine at the call site; a helper would add indirection for less clarity.

Raw err.message in fork errors (@sourcery-ai). Kept deliberately — those are the Worker's own plain-text reasons, already written to be surfaced ("This lesson has too much history to store."), not internals. Dropping them would leave the assistant unable to tell a size problem from a transient one.

All checks still green: 69 core + 64 MCP + 27 API tests, lint/format clean, docs build, wrangler --dry-run builds.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline c3f0a21
printf '\n--- changed files ---\n'
git diff-tree --no-commit-id --name-only -r c3f0a21

printf '\n--- relevant symbol outlines ---\n'
ast-grep outline apps/mcp/src/git.js --items all --match 'forkLesson|proposeChanges|commitDoc|clamp|proposalBody' || true
ast-grep outline apps/api/src/routes/pulls.js --items all --match 'source|forked_from|create' || true
ast-grep outline packages/core/src/git/memfs.js --items all --match 'memFs|put|readFile|writeFile' || true

printf '\n--- focused diff ---\n'
git show --format=fuller --find-renames --find-copies c3f0a21 -- \
  apps/mcp/src/git.js \
  apps/mcp/test/fork.test.js \
  apps/api/src/routes/pulls.js \
  packages/core/src/git/memfs.js \
  packages/core/src/git/memfs.test.js \
  apps/mcp/src/api.js \
  apps/mcp/src/tools.js

Length of output: 27934


@playforge-coding Confirmed from c3f0a21.

The commitDoc result is now checked before any proposal request is sent. The null dereference cannot report a failure after a live proposal exists.

The proposal flow now uploads the proposal pack before it updates fork history. A proposal creation failure leaves the fork history unchanged. The new retry test covers this case.

The fork flow now reads history before it reads the lesson document. The self-proposal check now requires source.forked_from === lessonId.

The memFs byte aliasing behavior is now an explicit module contract. The performance trade-off is reasonable for packfile handling. No change is needed for that item.

I also confirmed focused regression tests for the new workflow paths. I did not independently run the reported checks.

🐇 ✅


✏️ Learnings added
Learnt from: playforge-coding
URL: https://github.com/playforge-coding/spelling-creator/pull/41

Timestamp: 2026-08-10T22:49:06.205Z
Learning: In `packages/core/src/git/memfs.js`, `memFs` intentionally stores and returns file bytes by reference rather than copying them. Callers that use this module must treat returned and written byte arrays as immutable. This avoids large duplicate copies for Git blobs and packfiles.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@qodo-code-review

Copy link
Copy Markdown

Thanks — worked through all ten findings in c3f0a21. Four were real bugs.

Fixed (bugs)

Failed proposals consumed the changes (@qodo-code-review #1). The fork's history was pushed before the request was opened, so any failure after that left the fork's document equal to its own history — the changes safe, but the retry finding nothing pending and refusing. Since a proposal's pack is stored with the proposal, and that's what a reviewer merges, the push is bookkeeping: it now happens last and can't fail the call (reported as historyPushed: false). A failed proposal leaves the fork untouched and the retry just works.

commitDoc null deref (@coderabbitai, critical). Correct, and the ordering point was the important half — commit.oid was read after createPull/uploadPullPack, so a null would have reported failure for a proposal that had already gone live. Now guarded before anything is sent.

Fork read a stale document over newer history (@qodo-code-review #2). Confirmed against EditorPage.jsx:1128-1136 — the editor pushes history first and the doc row second, so reading doc-then-pack could pair yesterday's document with today's history, and the reconciliation commit would revert the save it raced. Reading the pack first makes that pairing unreachable.

Narrowing, not elimination: a fully atomic doc+head read would need an API change. Worth noting the residual window is now the harmless direction (a newer document over older history, which the commit just carries forward).

Self-proposal guard was bypassable (@qodo-code-review #4). Right, and it contradicted the docs I'd written ("a fork you own"): ownership alone was satisfied by any other lesson, which turned the rule off entirely and attached a fork link to a non-fork. Now requires source.forked_from === lessonId. sourceLessonId acceptance is unchanged for the non-self case, where it stays informational, so no regression for the web app.

Fixed (the rest)

  • Non-empty title on propose_changes — the hub rejected an empty one only after the snapshot had been built and sent.
  • SERVER_INFO0.3.0. I'd bumped it on its own sequence (0.1.3 → 0.2.0) since it was already out of step; two reviewers flagged it, and there's no upside to a third sequence when this is the version clients display.
  • memFs.put kept a new inode on every overwrite — a rewritten file isn't a different file, and isomorphic-git reads stat data for index caching.
  • clamp guarded for a non-positive limit, and the client's self-reported name bounded before it reaches a proposal body.
  • fetchLessonHead now throws on a real failure instead of flattening every bad status to "no history"; its one caller still opts out via .catch.
  • Docs: added the missing create_lesson_file row (pre-existing gap), and corrected the notification wording — merged/closed link to the lesson, only a proposal-to-review links to the proposal.

New tests

The paths that had none: a fork whose history can't be stored (row kept, clear error), a failed proposal retried with no further edit, a proposal surviving a failed history push, the fork read ordering, and that the client name reaches the body a reviewer actually reads.

Skipped

Copy bytes on read/write in memFs (@coderabbitai nitpick). A real departure from a real filesystem, but packfiles run to megabytes and this would copy every blob and pack twice per operation to protect against a mutation nothing performs — the git engine treats both as immutable. Documented the by-reference contract in the module header instead, so anything else adopting it knows the rule.

A shared "unwrap a named field" helper in api.js (@coderabbitai nitpick). data.lesson || {} / data.pull || null differ in their empty value and each reads fine at the call site; a helper would add indirection for less clarity.

Raw err.message in fork errors (@sourcery-ai). Kept deliberately — those are the Worker's own plain-text reasons, already written to be surfaced ("This lesson has too much history to store."), not internals. Dropping them would leave the assistant unable to tell a size problem from a transient one.

All checks still green: 69 core + 64 MCP + 27 API tests, lint/format clean, docs build, wrangler --dry-run builds.

Thanks for the detailed update. The described fixes address the self-proposal validation and retry-consumption bugs, and the ordering/cleanup rationale is sound.

One ordinal correction: in the Qodo index, these are finding 1 (self-proposal guard) and finding 2 (failed proposals consuming changes); finding 4 is the stale server version. The stale document/history race is tracked as finding 3.

The reported tests and checks are encouraging. I can’t independently verify the commit or CI results here, and the review index still shows findings 1–4 as active until their statuses are refreshed.

@playforge-coding
playforge-coding merged commit 9dc8e27 into master Aug 10, 2026
3 checks passed
@playforge-coding
playforge-coding deleted the feature/mcp-pr branch August 10, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant