<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ashiku Sheban</title>
    <description>The latest articles on DEV Community by Ashiku Sheban (@janabi54).</description>
    <link>https://dev.to/janabi54</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4138093%2F32328a3a-4a6e-4732-9927-b122fb9aedea.jpg</url>
      <title>DEV Community: Ashiku Sheban</title>
      <link>https://dev.to/janabi54</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/janabi54"/>
    <language>en</language>
    <item>
      <title>Building the parts of an AI tutor that don't fit in a demo</title>
      <dc:creator>Ashiku Sheban</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:09:22 +0000</pubDate>
      <link>https://dev.to/janabi54/building-the-parts-of-an-ai-tutor-that-dont-fit-in-a-demo-4lag</link>
      <guid>https://dev.to/janabi54/building-the-parts-of-an-ai-tutor-that-dont-fit-in-a-demo-4lag</guid>
      <description>&lt;p&gt;title: Building the parts of an AI tutor that don't fit in a demo&lt;br&gt;
published: false&lt;br&gt;
tags: ai, javascript, webdev, discuss&lt;/p&gt;

&lt;p&gt;A few weeks ago I &lt;a href="https://dev.to/janabi54/why-my-ai-tutor-wont-fix-your-code-3gb0"&gt;wrote about a debugging tutor&lt;/a&gt; that refuses to give students the answer. It asks questions. It requires a hypothesis before every hint. It points at the general area of a bug and lets the student figure out the rest.&lt;/p&gt;

&lt;p&gt;That post ended with a list of features I'd add if I kept going. This is what happened when I did.&lt;/p&gt;

&lt;p&gt;Four things shipped. Three were on the original list. One — auth — turned out to be a prerequisite I hadn't planned for.&lt;/p&gt;

&lt;p&gt;Post-mortem turns&lt;/p&gt;

&lt;p&gt;After a student fixes their bug, the tutor asks one more question: &lt;em&gt;"In your own words, why did the bug happen?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Their answer gets scored — via an LLM call when the key is set, or a keyword-matching heuristic otherwise — and stored against the bug pattern. The weak-spots dashboard now shows not just "you hit off-by-one five times" but "you correctly explained the cause four out of five times."&lt;/p&gt;

&lt;p&gt;That second number is the honest one. It's the difference between "the hints helped" and "they stumbled into a fix." Without it, every pass looks the same. With it, an instructor can see which exercises students are actually learning from and which ones they're getting through by luck.&lt;/p&gt;

&lt;p&gt;The heuristic scorer isn't good. It reads the student's explanation for words like "because" and "one too many" and marks it &lt;code&gt;partial&lt;/code&gt; if it sees them. The LLM scorer — same prompt, same response shape — does the real work. I built the heuristic because I wanted the pipeline to function without an API key, not because I thought it would be accurate.&lt;/p&gt;

&lt;p&gt;Cross-student fingerprinting&lt;/p&gt;

&lt;p&gt;The same pattern data, aggregated across everyone. If 75% of a class hits an off-by-one, that's not a student problem — it's a teaching signal.&lt;/p&gt;

&lt;p&gt;The instructor dashboard surfaces it as a callout:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"off-by-one" appears in 75% of students (6 of 8)&lt;br&gt;
4 students hit null-undefined on exercise ex-1&lt;br&gt;
Exercise "ex-1" has a 20% completion rate across 5 sessions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are actual output from my test data. Three sentences, and an instructor knows which exercise to walk through tomorrow.&lt;/p&gt;

&lt;p&gt;The interesting thing about this feature is that it's invisible from any single-student view. Students only see their own patterns. Instructors only see the aggregate. Nobody can see both, which means the class-level insight genuinely can't be reconstructed from what a student sees. That's what makes it worth building.&lt;/p&gt;

&lt;p&gt;The implementation is three SQL aggregations over tables that already existed. No schema changes. That's the payoff of building the persistence layer properly the first time — the second feature is a query away.&lt;/p&gt;

&lt;p&gt;Struggle timer&lt;/p&gt;

&lt;p&gt;Lock hint level 1 for N minutes of independent attempts before it's even offered. Per-exercise config; 0 minutes disables it.&lt;/p&gt;

&lt;p&gt;The response when the timer is active is a soft refusal with a countdown, not an error. The server returns 200 with &lt;code&gt;{ requiresStruggle: true, remainingSeconds: 143, ... }&lt;/code&gt;, and the UI renders a yellow card:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PRODUCTIVE STRUGGLE&lt;br&gt;
Give it a bit more time. You'll get more out of this if you try on your own first.&lt;br&gt;
&lt;code&gt;2:23&lt;/code&gt; of focused time&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The frame matters more than the timer. This feature could easily feel punitive — "you can't have help yet" — and I spent more time on the copy than on the code. The version that worked reads like a coach, not a bouncer.&lt;/p&gt;

&lt;p&gt;Two design details worth calling out:&lt;/p&gt;

&lt;p&gt;You can't game it by waiting. The timer requires at least one prior code submission. If a student opens the exercise, walks away for ten minutes, and comes back, the timer has expired but they still haven't engaged. The gate checks both.&lt;/p&gt;

&lt;p&gt;The timer never disables the button.** The client always lets the student click "Ask for a hint." The server decides. If you refresh mid-countdown, you see the same countdown — the state is on the server, not in the UI. This is a small thing but it's the right default for anything involving timing.&lt;/p&gt;

&lt;p&gt;Auth — the thing I didn't plan for&lt;/p&gt;

&lt;p&gt;The original app was one page with four tabs and a shared &lt;code&gt;x-admin-key&lt;/code&gt; gate on the admin endpoints. That was fine for a demo. For actual instructors to use it, they need individual logins, and their view needs to be scoped to their own students.&lt;/p&gt;

&lt;p&gt;So I built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;users&lt;/code&gt; and &lt;code&gt;auth_sessions&lt;/code&gt; tables&lt;/li&gt;
&lt;li&gt;bcrypt-hashed passwords, 12 rounds&lt;/li&gt;
&lt;li&gt;Random 32-byte session tokens with 30-day expiry&lt;/li&gt;
&lt;li&gt;HTTP-only cookies, SameSite=Lax, Secure in production&lt;/li&gt;
&lt;li&gt;Register / login / logout / me endpoints&lt;/li&gt;
&lt;li&gt;Instructor auto-seeded on first boot from an env var&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I gated every existing route. The debug tutor endpoints require authentication; the admin endpoints require the &lt;em&gt;instructor&lt;/em&gt; role specifically.&lt;/p&gt;

&lt;p&gt;The most important change: the server never trusts the client's &lt;code&gt;studentId&lt;/code&gt; anymore. It derives it from the session cookie. The old version had a text field in the top bar where a student typed their ID — which meant a malicious student could read another student's history by changing the field. That was a real hole, and it took adding auth to see how bad it was.&lt;/p&gt;

&lt;p&gt;Instructors can impersonate a specific student via &lt;code&gt;?asStudentId=&lt;/code&gt; for testing, but only instructors, and only through the middleware.&lt;/p&gt;

&lt;p&gt;The old &lt;code&gt;x-admin-key&lt;/code&gt; system is gone. That was a shared secret in a &lt;code&gt;.env&lt;/code&gt; file, which is the "our first customer is a friend who we trust" tier of security. The moment there's more than one instructor, it stops being viable.&lt;/p&gt;

&lt;p&gt;What this bought: two dashboards from one app. Students see the Tutor and Weak Spots tabs. Instructors see those plus Health and Class. Same code, different views, driven by the role on the session.&lt;/p&gt;

&lt;p&gt;What's still on the list&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cohorts. The class fingerprint view currently shows every student in the system. Real classes have boundaries. A &lt;code&gt;cohorts&lt;/code&gt; table with an instructor field, and every aggregation filtered by it. This is the piece that makes multi-instructor deployment meaningful.&lt;/li&gt;
&lt;li&gt;Exercise authoring. The struggle timer config is a hardcoded TypeScript map. It wants to be an &lt;code&gt;exercises&lt;/code&gt; table with a real admin UI, when there's a reason to build one.&lt;/li&gt;
&lt;li&gt;Deployment. Still &lt;code&gt;localhost:3001&lt;/code&gt;. A VPS, Caddy, and a domain is a weekend project.&lt;/li&gt;
&lt;li&gt;The LLM key. The fallback path works, but the LLM-generated hints — the ones that respond to the student's specific hypothesis and code — are the reason this exists. That's the next thing to turn on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The state of things&lt;/p&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/janabi54/codeteach-debug-tutor" rel="noopener noreferrer"&gt;github.com/janabi54/codeteach-debug-tutor&lt;/a&gt;. Around 3,500 lines of TypeScript and vanilla JS. 23 regression tests. SQLite persistence across everything. Working end to end, login included.&lt;/p&gt;

&lt;p&gt;None of it is clever. Most of it is the boring kind of feature that a demo doesn't need: users, sessions, migrations, aggregations. That's the work that turns a prototype into something an instructor might actually use, and it's the work that isn't fun to write about.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why my AI tutor won't fix your code</title>
      <dc:creator>Ashiku Sheban</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:59:34 +0000</pubDate>
      <link>https://dev.to/janabi54/why-my-ai-tutor-wont-fix-your-code-3gb0</link>
      <guid>https://dev.to/janabi54/why-my-ai-tutor-wont-fix-your-code-3gb0</guid>
      <description>&lt;p&gt;Every AI coding assistant does the same thing: you paste broken code, it hands you the fix. ChatGPT, Copilot, Cursor, the one built into your editor — they all optimize for getting you to working code as fast as possible.&lt;/p&gt;

&lt;p&gt;Update: I've since shipped the three features this post ended with, plus auth. &lt;a href="https://dev.to/janabi54/building-the-parts-of-an-ai-tutor-that-dont-fit-in-a-demo-4lag"&gt;Here's what happened next&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's often the wrong goal.&lt;/p&gt;

&lt;p&gt;I built a debugging tutor that refuses to give you the answer. Instead, it asks questions. It makes you write a hypothesis before every hint. It points at a general area and lets you figure out the rest. It's slower on purpose, and by design slightly annoying.&lt;/p&gt;

&lt;p&gt;Here's why that's better — and, more importantly, here's what I learned building it.&lt;/p&gt;

&lt;p&gt;The premise: refusal as a feature&lt;/p&gt;

&lt;p&gt;The tool is called CodeTeach. A student submits broken code. The server does five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs the code through a classifier that identifies the &lt;em&gt;class&lt;/em&gt; of bug (off-by-one, null dereference, mutation during iteration, etc.)&lt;/li&gt;
&lt;li&gt;Checks whether the student has written a hypothesis this attempt&lt;/li&gt;
&lt;li&gt;If not, refuses to give a hint until they do&lt;/li&gt;
&lt;li&gt;If yes, generates a Socratic question — not an answer&lt;/li&gt;
&lt;li&gt;Logs the pattern so the student can see their own tendencies later&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every hint requires a fresh hypothesis. Every pattern is stored. The weak-spots dashboard is the interesting part: it's a mirror, not a grade.&lt;/p&gt;

&lt;p&gt;The pedagogy is not novel. Productive struggle is well-documented. The point is that you can't bolt "productive struggle" onto a tool designed to remove struggle. You have to build a tool whose entire premise* is that struggle is the product.&lt;/p&gt;

&lt;p&gt;The hint ladder&lt;/p&gt;

&lt;p&gt;Hints escalate in four levels:&lt;/p&gt;

&lt;p&gt;Level  What the student gets &lt;/p&gt;

&lt;p&gt;1     Vague nudge — points at a broad region &lt;br&gt;
 2     More specific — narrows to a line or expression &lt;br&gt;
 3     Near-answer — names the class of bug without stating the fix &lt;br&gt;
 4     Final scaffold — asks the student to state the correct edge case                                            in their own words. &lt;/p&gt;

&lt;p&gt;The important constraint is what unlocks the next level. It's not time. It's not "asking again." It's a genuine retry at the current level — the server only escalates after two attempts where the student changed their code.&lt;/p&gt;

&lt;p&gt;This is the piece that turns the tool from "a hint button" into something else. A hint button gets spammed. A ladder that only responds to effort gets respected.&lt;/p&gt;

&lt;p&gt;Implementation-wise, it's a small state machine per (student, exercise) pair, persisted in SQLite. &lt;code&gt;currentLevel&lt;/code&gt;, &lt;code&gt;attemptsAtLevel&lt;/code&gt;, &lt;code&gt;totalAttempts&lt;/code&gt;, &lt;code&gt;resolved&lt;/code&gt;. When a new attempt comes in with &lt;code&gt;passed: false&lt;/code&gt;, increment &lt;code&gt;attemptsAtLevel&lt;/code&gt;. At 2, escalate and reset the counter. Simple. The hard part was resisting the urge to add more states.&lt;/p&gt;

&lt;p&gt;The hypothesis gate&lt;/p&gt;

&lt;p&gt;This is the feature I'm most proud of, because it's the one that changes behavior.&lt;/p&gt;

&lt;p&gt;Before any hint is served — every level, every time — the student must write one sentence: "I think the bug is because ___."&lt;/p&gt;

&lt;p&gt;The server holds the gate. If the student requests a hint without a hypothesis, they get back:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
json
{
  "requiresHypothesis": true,
  "prompt": "Before I give you a hint, tell me in one sentence what you think the bug is.",
  "hintLevel": 1
}                                                                                     No hint. No progress up the ladder. Just the prompt.

Once they write one, it's stored in a hypotheses table with the hint level it unlocked, and the hint is served. Then the gate re-arms for the next request.

Two things happen when you build this:

First, spam becomes impossible. You can't "next hint" your way through the ladder. Every step costs a thought.

Second — and this is the part I didn't anticipate — you get data nobody else has. A stream of structured statements about what students believe is wrong, at specific moments in their debugging. Not "what code did you write" — "what theory of the bug did you hold when you asked for help." That's a different kind of signal entirely. Instructors can cluster them. So can a second LLM. It's the beginning of a teaching tool that understands reasoning, not just output.

The implementation is straightforward: an extra column on hint_sessions (hypothesis_pending), a new hypotheses table, and a gate check at the top of the route handler. The interesting work is not the code — it's deciding that every hint requires one, including the first.

Custom ESLint rules
The classifier does two things: analyzes the code statically, and matches runtime errors against a regex library.

The regex side is easy — TypeError: Cannot read property 'x' of undefined is a string, and strings are matchable. But it only ever identifies symptoms. If a student has an off-by-one error, JavaScript says Cannot read properties of undefined, not "your loop condition should be &amp;lt;, not &amp;lt;=." Regexes can't tell the difference between ten different bugs that all produce the same error.

Static analysis can. So I wrote a small ESLint plugin with three rules.

loop-bound-heuristic — matches i &amp;lt;= arr.length, i &amp;gt;= arr.length at initialization, and loops that start at 1 with a strict &amp;lt; bound. Each of those is a common off-by-one pattern that ESLint doesn't catch out of the box.

mutation-in-iteration — matches arr.push(x) while iterating over arr, arr.splice() inside forEach, and similar. These are the bugs that skip elements or loop forever, and they're invisible to a static linter that doesn't track collection identity.

async-missing-await — matches calls to fetch, save, load, etc. whose promises are never awaited, returned, or .then()'d. The bug is subtle: the code "works" but the value is a Promise, not the resolved result.

Each rule is an AST visitor. loop-bound-heuristic is the simplest — it hooks ForStatement, checks if the test is &amp;lt;= against a .length member expression, and reports. About 40 lines. mutation-in-iteration is the trickiest because it has to walk the loop body and look for method calls against the same identifier the loop is iterating — no scope analysis, just pattern matching on the AST.

The rules are wrapped in a plugin object and passed to ESLint 9's flat config. That's not obvious from the docs. In flat config, you can't use the old linter.defineRule() API — you pass plugins as data in the config array. My first attempt threw This method cannot be used with flat config and it took me longer than I want to admit to find that.

The classifier merges the two sources — ESLint findings and error-regex matches — into a single list of candidates, sorted by confidence. Which brings me to the bug that took me a full day to find.

The classifier priority bug that took me a day to find
The classifier produces a list of candidates — one per pattern it detects, each with a confidence level and a source (eslint or error-output). Then a small function called pickTop decides which one to report as "the pattern" for this request.

The design goal was: prefer the ESLint signal over the error-output signal, because code analysis points at the root cause and error strings describe the symptom.

The first version looked like this:

typescript
function pickTop(candidates: PatternCandidate[]): MistakePattern | null {
  const strong = candidates.find(c =&amp;gt; c.confidence === 'high' || c.confidence === 'medium');
  return strong?.pattern ?? null;
}
That's "highest confidence wins." Reasonable at first glance. Except: error-output heuristics are always high confidence (TypeError: Cannot read properties of undefined really did happen), and custom ESLint rules were medium. So error-output always won. The student with an off-by-one loop was told "which variable might be undefined?" instead of "look at your loop's exit condition."

The fix felt simple:

typescript
function pickTop(candidates: PatternCandidate[]): MistakePattern | null {
  const fromEslint = candidates.find(
    c =&amp;gt; c.source === 'eslint' || c.source === 'both'
  );
  if (fromEslint) return fromEslint.pattern;

  const strong = candidates.filter(
    c =&amp;gt; c.confidence === 'high' || c.confidence === 'medium'
  );
  return strong[0]?.pattern ?? null;
}
ESLint wins whenever it has anything to say. Error-output carries the load only when the code is clean.

This worked — for preClassify. But not for the actual HTTP endpoint.

Because there was a second place that computed the top pattern: the fallback hint generator. And it read candidates[0] directly, bypassing pickTop entirely:

typescript
export function generateFallbackHint(c: PreClassificationResult, reason): FallbackHint {
  const top = c.candidates[0];  // ← bug
  const usable = top &amp;amp;&amp;amp; (top.confidence === 'high' || top.confidence === 'medium');
  return {
    message: usable ? patternNudge(top.pattern) : genericNudge(),
    detectedPattern: usable ? top.pattern : null,
    ...
  };
}
Since candidates was sorted by confidence, candidates[0] was the highest-confidence candidate, not the preferred one. So the fallback path kept returning null-undefined while the direct classifier test kept returning off-by-one.

Every component passed its own tests. The composition didn't do what any of them promised. It's the classic integration-seam bug, and I only found it because I finally ran the classifier directly and compared the output to what the server was returning over HTTP.

The fix was three lines:

typescript
export function generateFallbackHint(c: PreClassificationResult, reason): FallbackHint {
  const pattern = c.topPattern;  // use the classifier's preferred answer
  return {
    message: pattern ? patternNudge(pattern) : genericNudge(),
    detectedPattern: pattern,
    ...
  };
}
The lesson: when you have a preferred-selection function, everything that needs a selection must go through it. Any code path that re-derives the answer from raw data will eventually disagree with the primary path.

The fallback path
The classifier runs even when no LLM is available. That's on purpose.

When you enable Claude, the tutor generates a conversational Socratic question tailored to the student's code and their hypothesis. When you don't, a template-based generator picks a nudge from a bank of three per pattern. The pipeline is identical up to the point where the message is written.

Why build both?

Because LLMs fail. They rate-limit, they time out, they get expensive. If your product depends on a single external API, you don't have a product, you have a demo. The fallback is the difference between "works when everything is fine" and "works."

The fallback has its own constraints. It can't see the student's code, only the detected pattern. So the nudges have to be generic enough to apply to any instance of that pattern, but specific enough to actually help. That's a surprisingly hard writing exercise — writing Socratic questions that are pattern-generic is harder than writing them for a specific bug.

Here's the off-by-one bank:

"Take another look at how your loop decides when to stop. Is that the boundary you actually want?"

"What value does your loop variable hold on the very last iteration — and is that a valid position?"

"Trace your loop by hand for an input of size 1. How many times does the body run?"

Each is a question, not a statement. Each points at the boundary without naming the fix. Each can be applied to i &amp;lt;= arr.length, i &amp;gt;= arr.length, i = 1; i &amp;lt; arr.length, and any other flavor of off-by-one you happen to have written.

The fallback path also serves a useful development purpose: it lets you build and test the whole system without spending money on API calls. If I can't get a sensible hint out of the fallback path for a given pattern, the pattern is probably too vague to be useful even with an LLM.

What I'm not sure about
I'll be honest about the doubts.

Is refusal actually the right default for everyone?

There's a version of this that's just annoying. A student stuck on a typo — let i = 0; i i &amp;lt;= arr.length; i++ — doesn't need to write a hypothesis about their reasoning. They need to be told "there's a stray i on line 2." Right now the tutor does tell them something useful (the syntax-error pattern says "read the failing line character by character"), but it's indirect. There's a spectrum between "make them think" and "make them waste time," and I'm not sure I've got the dial in the right place for every kind of bug.

The honest answer is: it depends on the student. For someone learning to debug, refusal is the point. For someone who already knows how to debug and just needs a syntax reminder, refusal is friction.

Is the hypothesis requirement teaching anything, or is it performative?

The theory is: writing "I think the bug is because ___" forces metacognition. The evidence would be: students who use the tool end up writing better hypotheses over time, or make fewer bugs in a category once it's been flagged in their weak spots.

I don't have that evidence. The tool logs everything, but logging isn't learning. To know whether the hypothesis gate actually helps, you'd need a study — pre/post comparison, control group, the whole apparatus. That's beyond what I can build alone.

What I can say is: the hypotheses are interesting to read. They show what students actually believe about their code. That's a real artifact even if the pedagogy is unproven.

Is the classifier good enough to be worth the complexity?

Three custom ESLint rules and a regex library catch maybe a dozen patterns. There are hundreds of ways code breaks. The classifier is honest — when it doesn't know, it falls back to error-output heuristics, which are worse but usable — but "usable" isn't the same as "good."

There's also a bias problem: the patterns I happen to have rules for get more attention in the weak-spots dashboard. If a student's real struggle is something I don't detect, they won't see it there. The dashboard tells the truth about what the classifier sees, not about what the student actually struggles with.

Would anyone use this?

The tool works. The pedagogy is plausible. But "plausible pedagogy" and "students actually change their behavior" are different claims, and I only have evidence for the first.

It's a tool that would need to be tested in a real classroom. Built for a hypothetical user. That's not a flaw — it's how most tools start — but it does mean I can't claim it works, only that it should work, and here's why.

I built it because I wanted to know whether the refusal-as-feature idea would actually feel different to use than a normal hint button. It does. That's a small, real finding. Whether it changes outcomes for students is a bigger question I can't answer yet.

What's next
Three features I'd add if I keep going:

Post-mortem turns. Once a student fixes their bug, one more LLM prompt: "explain in your own words why this happened." Score it loosely, store it against the weak-spot category. That closes the loop and produces the signal the dashboard needs to be honest about whether hints actually helped.

Cross-student fingerprinting. If 40% of a class hits the same off-by-one in the same exercise, that's a teaching signal. Auto-generate a mini-lesson or flag it for a live walkthrough. This is the feature that would make an instructor actually want the tool.

Struggle timer. Lock level 1 for N minutes of independent attempts before a hint is even offered. Instructor-configurable. Framed as "productive struggle," not "we're withholding help." I suspect this one matters more than it sounds, but I haven't built it because it's the most annoying to test.

None of these are needed to make the current tool work. They're needed to make it interesting enough that a classroom would actually adopt it.

The repo is here: github.com/janabi54/codeteach-debug-tutor. If you're building something similar, or if you teach and want to try it, or if you just want to argue with the premise — I'd like to hear from you.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>eslint</category>
      <category>pedagogy</category>
    </item>
  </channel>
</rss>
