Phase 3: server-enforced pedagogy state machine + new session/hint tools#38
Merged
SPerekrestova merged 7 commits intoMay 7, 2026
Conversation
Pure-logic foundation for the server-enforced tutoring contract — no
server wiring yet, no behaviour change for existing tools.
- src/types/session.ts: SessionState + HintLevel + SessionStatus
- src/types/errors.ts: add HINT_LEVEL_TOO_LOW + SESSION_NOT_FOUND
- src/domain/hint-state-machine.ts:
advanceHint / resetSession / assertSolutionUnlocked
(the gate that solution-returning tools will call)
- src/domain/pedagogy.ts: generateHint(problem, level, userCode?) projecting
problem-derived hint text per level (1..4).
userCode parameter is reserved for Phase 5
(workspace awareness) so the contract is stable.
- src/domain/session-store.ts: FileSessionStore — one JSON per slug under
~/.leetcode-mcp/sessions, mode 0o600, with
malformed-file recovery and slug validation.
23 unit tests cover the level transitions, the gate, the hint projections,
and the session round-trip / path-traversal / malformed-file paths.
…ate machine + hint generator Tools should depend on SessionService, not on FileSessionStore / hint-state-machine / pedagogy directly — it's the seam that makes the gate uniform across solution-returning tools and gives the wire layer a single object to wire up. - startOrResume(slug, language?): idempotent — re-running on a slug the user is already mid-way through preserves hint progress. - get(slug): null when no start_problem call. - advance(slug, problem): bumps level + persists + returns generated hint text; throws SESSION_NOT_FOUND when called without start_problem. - reset(slug): zeroes level / attempts / lastLocalRunPassed. - assertSolutionUnlocked(slug): the gate that solution tools call. Pure domain types (no IO) move into FileSessionStore via constructor injection so tests can pass an in-memory or fixture-backed store.
…s with MCP instructions field MCP prompts are opt-in; "agent must remember to call leetcode_learning_mode" is precisely the kind of instruction-following LLMs reliably fail at. The MCP `instructions` field, supported on McpServer's ServerOptions since the SDK shipped MCP 2025-06-18 support, is delivered to clients at handshake regardless of whether the agent ever asks for prompts. The single source of truth for the pedagogy contract now lives there and is read once per session. Kept as a plain exported constant so it's easy to unit-test independently and to evolve as later phases land (workspace awareness, runners, strict-mode submission gating).
…tate / reset_session Four new MCP tools that drive the pedagogy state machine. - start_problem(titleSlug, language?): idempotent — opens (or resumes) a tutoring session. Must be called before request_hint, list_problem_solutions, or get_problem_solution. Re-running on a slug the user is already mid-way through preserves their hint progress. - request_hint(titleSlug): advances the hint level by 1 and returns generated text. Levels: 1 clarification → 2 approach → 3 implementation sketch → 4 solution unlock. - get_session_state(titleSlug): returns the persisted session for a problem, or null if start_problem was never called for it. Useful for restoring context after a restart. - reset_session(titleSlug): clears the session back to level 0 with attempts/lastLocalRunPassed zeroed. Lifecycle status reset to 'started'. Errors flow through a shared errorEnvelope that surfaces the structured `code` field (HINT_LEVEL_TOO_LOW / SESSION_NOT_FOUND / etc) alongside the human message, so clients can dispatch on it. Re-exported so the solution tools can render the same shape when their gate trips.
…nt level Both community-solution tools now reject with HINT_LEVEL_TOO_LOW until the active session for the slug has reached the maximum hint level. - list_problem_solutions: gates on the questionSlug parameter that is already required. - get_problem_solution: adds a required `titleSlug` parameter (the topicId alone doesn't tell us which session to gate on). New parameter, additive: existing clients calling without titleSlug see a clear "required" error. Tool descriptions explicitly call out the GATED status so any agent reading them knows the contract. Errors render through the shared errorEnvelope so a missing or underleveled session surfaces as the structured code, not a free-form text `error: ...`.
…strap - McpServer constructor now receives ServerOptions with `instructions` set to the canonical pedagogy contract — delivered at handshake. - SessionService is constructed once per server lifetime and shared between the new session tools and the gated solution tools. - registerSessionTools and registerSolutionTools take the service via constructor injection, so unit/integration tests can supply a store backed by a per-test temp dir.
- tests/integration/solution-tools-integration.test.ts: rewritten to inject a SessionService over a per-test temp dir. Adds three gate cases (no session, session below unlock, session at unlock) for both list_problem_solutions and get_problem_solution. 8 tests, all pass. - tests/e2e/lifecycle.test.ts: extends the expected-tools list to include start_problem, request_hint, get_session_state, reset_session. Catches drift between the registered tool set and the wire surface. - tests/e2e/pedagogy-gate.test.ts: end-to-end regression for the gate. Drives a real server through start_problem → request_hint × 4 with a mocked GraphQL fixture, asserts list_problem_solutions rejects with HINT_LEVEL_TOO_LOW at every pre-unlock level and only opens up at level 4. Second case: reset_session clamps back to 0 and re-engages the gate. Locks the contract end-to-end so a future refactor that drops the gate fails CI before clients do. 178/178 unit+integration; 11/11 e2e (was 9; +2 gate cases).
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Owner
|
LGTM Generated by Claude Code |
Author
|
Thanks for the LGTM. Continuing autonomously to Phase 4a (Python local runner) on a separate branch — will open the next PR shortly. |
SPerekrestova
approved these changes
May 7, 2026
6b5f02b
into
devin/1778098342-phase-0-hygiene
1 check passed
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of the redesign plan. Pedagogy moves from honor-system prompts to a server-enforced state machine: a per-problem session JSON tracks
hintLevel(0–4),attempts,lastLocalRunPassed,status.list_problem_solutionsandget_problem_solutionare gated at the wire — they reject withHINT_LEVEL_TOO_LOWuntil the active session has reached the maximum level. The MCPinstructionsfield replaces the prompt-based "remember to invoke X first" dance with rules clients receive once at handshake.Stacked on top of #37; base auto-rebases to
mainonce #37 reaches main.Domain layer (
src/domain/, one commit, pure logic + tests):hint-state-machine.ts—advanceHint/resetSession/assertSolutionUnlocked. No IO.pedagogy.ts—generateHint(problem, level, userCode?). Level 1 clarification → 2 approach → 3 implementation sketch → 4 solution unlock.userCodeparameter is reserved for Phase 5 (workspace awareness) so the contract is stable.session-store.ts—FileSessionStore: one JSON per slug under~/.leetcode-mcp/sessions/, mode 0o600, with malformed-file recovery and slug validation (rejects anything not matching^[a-z0-9-]+$to prevent path traversal).tests/domain/cover every transition, gate, hint projection, and store edge case.Application layer (
src/domain/session-service.ts, one commit):SessionServiceis the single seam tools depend on.startOrResumeis idempotent — re-running on a slug the user is already mid-way through preserves hint progress.assertSolutionUnlockedis the gate.Wire layer (four commits):
src/mcp/server-instructions.ts— theinstructionsstring. Plain exported constant so it's unit-testable and easy to evolve.src/mcp/tools/session-tools.ts— four new tools:start_problem,request_hint,get_session_state,reset_session. Errors flow through a sharederrorEnvelopethat surfaces the structuredcodefield alongside the human message.src/mcp/tools/solution-tools.ts— gateslist_problem_solutions(already takesquestionSlug) and adds a requiredtitleSlugparameter toget_problem_solutionso the gate has session context (a topicId alone doesn't tell us which session to verify).src/index.ts—McpServerconstructor receivesServerOptionswithinstructionsset.SessionServiceis constructed once per server lifetime and shared across the new and gated tools.Tests (one commit, additive):
tests/integration/solution-tools-integration.test.tsrewritten to inject aSessionServiceover a per-test temp dir; adds three gate cases (no session, below unlock, at unlock) for each tool. 8 tests, all pass.tests/e2e/lifecycle.test.ts— expected-tools list extended with the four new tools.tests/e2e/pedagogy-gate.test.ts(new) — drives a real server throughstart_problem→request_hint × 4with a mocked GraphQL fixture, assertslist_problem_solutionsrejects withHINT_LEVEL_TOO_LOWat every pre-unlock level and only opens at level 4. Second case:reset_sessionclamps back to 0 and re-engages the gate.Tests: unit/integration 152 → 178 (+26 — 23 domain + 3 gate); e2e 7 → 9 (+2 gate cases).
npm run buildclean;npm run test:typesclean;npm run formatclean; fullnpm run test:allexits 0.Why not amend behavior beyond gating + new tools:
get_problem,submit_solution, etc.) keep their current signatures — Phase 3 deliberately ships only the state machine and the gate. Wiringsubmit_solutionto the session forattemptstracking lands in Phase 6.get_startedis kept as-is; the redesign plan called for it to be deprecated alongside theinstructionsfield rollout, but I'd rather observe that theinstructionsfield is honored by the clients you care about before turningget_startedinto a stub. Trivial follow-up if you want it.learning-prompts.tssurvives unchanged for the same reason — clients on older MCP versions that don't honorinstructionsstill see the prompts.Breaking change (additive):
get_problem_solutionnow requirestitleSlugin addition totopicId. Existing clients calling withouttitleSlugwill see a clear "required" Zod error.Review & Testing Checklist for Human
src/domain/hint-state-machine.tsandassertSolutionUnlocked— this is the gate and any bug here defeats the whole pedagogy contractinstructionsfield copy insrc/mcp/server-instructions.tsreads the way you want — it's the single source of truth the client will read at handshakesrc/domain/session-store.ts(^[a-z0-9-]+$) accepts every slug shape LeetCode actually uses (I checked a sample but you have a longer history with it)npm run test:alllocally to confirm e2e specs pass on your platform (Linux Node 22 in CI)Notes
main, the base will auto-rebase and this PR's diff will shrink to just the seven Phase 3 commits.attemptsfromsubmit_solutionand surface session info onget_problem) lands with Phase 6 (submission hardening) — kept out of this PR to keep the diff focused on pedagogy.Link to Devin session: https://app.devin.ai/sessions/d003a60939484686b2953ae32fe2794d
Requested by: @SPerekrestova