<?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: Aarish mansur</title>
    <description>The latest articles on DEV Community by Aarish mansur (@aarishmansur).</description>
    <link>https://dev.to/aarishmansur</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%2F4070690%2F5a7e9f64-bac0-468c-81c0-15402ddbfcee.jpg</url>
      <title>DEV Community: Aarish mansur</title>
      <link>https://dev.to/aarishmansur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aarishmansur"/>
    <language>en</language>
    <item>
      <title>I built a VTU viva examiner that calms the room while I think</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Fri, 06 Nov 2026 08:56:03 +0000</pubDate>
      <link>https://dev.to/aarishmansur/i-built-a-vtu-viva-examiner-that-calms-the-room-while-i-think-8c5</link>
      <guid>https://dev.to/aarishmansur/i-built-a-vtu-viva-examiner-that-calms-the-room-while-i-think-8c5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/presage-2026-11-04"&gt;Presage SmartSpectra SDK Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every VTU CSE student knows the gap between knowing an answer at your desk and saying it in front of the external examiner.&lt;/p&gt;

&lt;p&gt;At my desk, "Explain the four conditions for deadlock" is a revision question. In the lab - record open, tube light buzzing, examiner waiting with the attendance register - the same sentence can make my brain blue-screen. Written prep checks memory. It does not let me practise the silence before my first word, saying it out loud, and settling enough to continue.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Viva Mode&lt;/strong&gt;: a local Electron app for mock Visvesvaraya Technological University (VTU) CSE lab vivas, powered by the Presage SmartSpectra SDK. The unit of practice is a &lt;strong&gt;syllabus topic&lt;/strong&gt;, not a generic interview score. The session ends with a study-shaped note: which subject and module moved pulse relative to &lt;em&gt;your own&lt;/em&gt; baseline, think time, time to first word, and whether you settled during Think - not "you seemed stressed."&lt;/p&gt;

&lt;p&gt;Interview coaches already exist; other Presage entries already score recovery after interview questions. Viva Mode is deliberately a &lt;strong&gt;college viva practice room&lt;/strong&gt; - lab-record paper, subject chips, module-linked reports - because that is the exam I actually sit for.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a session feels like
&lt;/h3&gt;

&lt;p&gt;A live-signal session walks through these screens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consent.&lt;/strong&gt; Plain-language card: what is measured (pulse, breathing, HRV, face talking, optional expressions), what is stored (local per-question summaries), what is never saved as video (no recordings, frames, or landmarks). You can paste a Presage key here or use one already saved. Telemetry unchecked by default. "Not a medical device." Camera only after you continue on the live path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration (~60 s).&lt;/strong&gt; "Sit like you're waiting outside the lab." The ring fills while signals warm up - pulse averages over roughly 12 s, breathing over ~30 s, HRV over ~60 s. Validation hints coach framing. Calibration finishes after the minute &lt;strong&gt;and&lt;/strong&gt; at least 20 usable pulse samples; if pulse never stabilises, it extends (up to two minutes) instead of inventing a baseline, then offers Practice without signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready.&lt;/strong&gt; Baseline = medians of usable calibration samples. Examiner neutral. Press "Ask me."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think.&lt;/strong&gt; Lab-record question card (subject code, module, topic). Breathing drives the room: more than +15% vs baseline → pacer + "patient" examiner; within +5% for 8 s → calm steps up (flicker and clutter ease, colours warm, "encouraging"). Gradual CSS &lt;code&gt;--calm&lt;/code&gt; (0→1), not a strobe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer.&lt;/strong&gt; Face talking for ≥1.2 s (or the "I'll answer now" button). Breathing adaptation freezes - breathing rate is not valid while talking. Examiner listens; pulse can still paint a ribbon. ≥4 s silence ends the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Between.&lt;/strong&gt; Next question or end. Records think time, time to first word, peak pulse vs baseline when available, whether you settled, and recovery if the pacer appeared and you settled in the same Think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calm Report.&lt;/strong&gt; Per-question subject/module cards + Chart.js timeline. Relative copy only ("vs your baseline"). Local examiner remark from the state machine - no cloud model. Footer: practice/wellness tool, not medical advice. Export as PNG if you want to keep it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings.&lt;/strong&gt; Telemetry for the next session, delete all data, camera/key, reduced-motion hooks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is also a labelled &lt;strong&gt;Practice without signals&lt;/strong&gt; path: same question deck and timing, no camera, no vitals, no saved report. Useful in a lab without a webcam. It cannot stand in as SDK proof.&lt;/p&gt;

&lt;h3&gt;
  
  
  Question bank and honest status
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;data/questions.vtu-cse.json&lt;/code&gt; ships &lt;strong&gt;41 VTU CSE viva prompts&lt;/strong&gt; under the 2022 scheme codes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BCS303 Operating Systems (8)&lt;/li&gt;
&lt;li&gt;BCS304 Data Structures (9)&lt;/li&gt;
&lt;li&gt;BCS401 Algorithms (8)&lt;/li&gt;
&lt;li&gt;BCS403 DBMS (8)&lt;/li&gt;
&lt;li&gt;BCS502 Computer Networks (8)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modules are named (for example "Module 3: Deadlocks"), not placeholders. Each live viva draws &lt;strong&gt;five questions&lt;/strong&gt; round-robin across subjects so a session is never five OS prompts in a row.&lt;/p&gt;

&lt;p&gt;This is an early challenge build (work started on or after 4 Nov 2026), not a finished validation study. The state machine has camera-free unit tests; &lt;code&gt;npm run smoke&lt;/code&gt; drives consent → practice → five questions → report → settings and writes screenshots under &lt;code&gt;smoke/&lt;/code&gt;. Live-signal demo video and "what broke under my tube light" notes land once I finish a clean camera run. No invented BPM values. Viva Mode is not an interview coach, a real examiner, college surveillance, or a medical device - just a private practice room for the minute between "I know this" and "here is my answer."&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech stack and architecture
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desktop shell&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Electron 43.2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Matches how Presage's Node SDK is meant to run: native work in main, camera UI in a sandboxed renderer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physiology SDK&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;@smartspectra/node-sdk&lt;/code&gt; 3.4.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Official SmartSpectra Node/Electron path for this challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TypeScript 5.9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shared types for decoded metrics, snapshots, and session records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundling&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;esbuild&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate bundles for main, preload, and renderer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;node:test&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State-machine tests without a webcam; smoke harness for the Electron UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Chart.js 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Calm Report timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Node.js 22+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;README requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No React/Vue in the MVP - HTML + CSS driven by &lt;code&gt;--calm&lt;/code&gt; and a small render loop keeps the physiology path easy to audit.&lt;/p&gt;

&lt;p&gt;Repo layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;viva-mode/
├─ PLAN.md  README.md  package.json  tsconfig.json
├─ data/questions.vtu-cse.json
├─ scripts/   # build + smoke
├─ smoke/     # consent / room / report / settings screenshots
├─ src/main/main.ts            # BrowserWindow, camera permission, SmartSpectra IPC
├─ src/main/storage.ts         # local sessions under userData
├─ src/preload/preload.ts      # SDK preload + window.viva bridge
├─ src/renderer/               # DOM UI, styles, SmartSpectraSDK wiring
├─ src/shared/types.ts
├─ src/shared/signals.ts       # decode → Snapshot, confidence gating, EMA
├─ src/shared/stateMachine.ts  # phases, thresholds, hysteresis, cooldowns
└─ test/stateMachine.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Main.&lt;/strong&gt; Sandboxed &lt;code&gt;BrowserWindow&lt;/code&gt; (&lt;code&gt;contextIsolation&lt;/code&gt;, no &lt;code&gt;nodeIntegration&lt;/code&gt;). &lt;code&gt;bindSmartSpectraIpc&lt;/code&gt; owns the native runtime; camera permission is video-only for our page. The key comes from consent, Settings (&lt;code&gt;safeStorage&lt;/code&gt; / OS keychain), or &lt;code&gt;PRESAGE_API_KEY&lt;/code&gt; - never baked into the bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preload.&lt;/strong&gt; esbuild-bundled (sandboxed preloads cannot freely &lt;code&gt;require&lt;/code&gt; &lt;code&gt;node_modules&lt;/code&gt;); &lt;code&gt;window.viva&lt;/code&gt; via &lt;code&gt;contextBridge&lt;/code&gt;. Frames stay on the SDK MessagePort bridge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Renderer.&lt;/strong&gt; &lt;code&gt;SmartSpectraSDK&lt;/code&gt; with &lt;code&gt;apiKey&lt;/code&gt;, requested metrics, and &lt;strong&gt;&lt;code&gt;enableTelemetry: false&lt;/code&gt; by default&lt;/strong&gt;. Metrics → &lt;code&gt;Snapshot&lt;/code&gt; → &lt;code&gt;step(...)&lt;/code&gt;. The UI only paints what the reducer says.&lt;/p&gt;

&lt;p&gt;Signal path:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Camera → SmartSpectraSDK (renderer) → MessagePort / SDK preload
      → bindSmartSpectraIpc + native runtime (main)
      → metrics → decode → Snapshot → stateMachine.step
      → Room UI / report → local userData sessions (summaries only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;signals.ts&lt;/code&gt; gates usable samples (&lt;code&gt;stable &amp;amp;&amp;amp; confidence &amp;gt;= 50&lt;/code&gt;, pulse roughly 40-110) and applies EMAs (τ ≈ 6 s pulse, τ ≈ 10 s breathing). The SDK &lt;code&gt;metrics&lt;/code&gt; event is windowed; the app takes the newest sample and de-duplicates on SDK timestamps. &lt;code&gt;performance.now()&lt;/code&gt; drives the state machine; SDK epoch-µs timestamps drive EMA &lt;code&gt;dt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Phases:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consent → calibrating → ready → think ⇄ answer → between → report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Room adaptation runs in Think only. Missing breathing drifts calm toward 0.5 and greys the chip - no punishment for unreadable data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live-signal demo video:&lt;/strong&gt; &lt;code&gt;[Will update later]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I am recording a clean run through the &lt;strong&gt;live SmartSpectra path&lt;/strong&gt; (not Practice without signals). When the link is ready, it will replace the placeholder above and show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consent with the telemetry checkbox left off by default;&lt;/li&gt;
&lt;li&gt;calibration until chips ungrey with usable confidence;&lt;/li&gt;
&lt;li&gt;one Think → Answer transition with live values, the pacer, and the adaptive room;&lt;/li&gt;
&lt;li&gt;one module-linked Calm Report;&lt;/li&gt;
&lt;li&gt;Settings → Delete all my data;&lt;/li&gt;
&lt;li&gt;an honest signal-quality failure if one shows up (breathing greying while I talk, or a validation hint under bad lighting).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will not use Practice without signals, mocked readings, or an AI-generated person as SDK proof. Until the video is embedded, judges can clone the repo, paste a key on consent (or set &lt;code&gt;PRESAGE_API_KEY&lt;/code&gt;), and run the live path locally. Public hosting is optional. The &lt;code&gt;smoke/&lt;/code&gt; screenshots document the UI shell on the practice path - not a substitute for the live-signal video.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AarishMansur" rel="noopener noreferrer"&gt;
        AarishMansur
      &lt;/a&gt; / &lt;a href="https://github.com/AarishMansur/Viva-Mode" rel="noopener noreferrer"&gt;
        Viva-Mode
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Viva Mode&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A lab-viva practice room for VTU CSE students. The examiner asks real questions from the 2022
scheme; the room softens as your breathing settles back toward your own baseline; afterwards you
get a private Calm Report.&lt;/p&gt;
&lt;p&gt;Built with the &lt;a href="https://physiology.presagetech.com" rel="nofollow noopener noreferrer"&gt;Presage SmartSpectra SDK&lt;/a&gt; for the SmartSpectra
SDK Challenge.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AarishMansur/Viva-Mode/smoke/2-room.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAarishMansur%2FViva-Mode%2FHEAD%2Fsmoke%2F2-room.png" alt="The viva room"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consent first.&lt;/strong&gt; The camera turns on only after you press the button. The screen explains what
is read, what is stored, and what leaves the laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60-second calibration.&lt;/strong&gt; SmartSpectra reads pulse, breathing and HRV while you sit still. The
median of that window becomes &lt;em&gt;your&lt;/em&gt; baseline — there is no good or bad number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five real questions.&lt;/strong&gt; The deck is drawn round-robin from Operating Systems (BCS303), Data
Structures (BCS304), Design and Analysis of Algorithms (BCS401), DBMS (BCS403) and Computer
Networks (BCS502).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An adaptive room.&lt;/strong&gt; When breathing runs above your baseline the room tightens: the tube light
flickers, the…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AarishMansur/Viva-Mode" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/AarishMansur/Viva-Mode" rel="noopener noreferrer"&gt;AarishMansur/Viva-Mode&lt;/a&gt; - public, MIT.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;README.md&lt;/code&gt; covers setup; &lt;code&gt;PLAN.md&lt;/code&gt; documents the state machine, thresholds, and copy rules. Short version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AarishMansur/Viva-Mode.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Viva-Mode
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# Node.js 22+. On Windows npm 11 you may need:&lt;/span&gt;
&lt;span class="c"&gt;#   npm install-scripts approve koffi protobufjs esbuild&lt;/span&gt;
&lt;span class="nv"&gt;PRESAGE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key npm start
&lt;span class="c"&gt;# or paste the key on the consent screen / save it in Settings → Camera&lt;/span&gt;

npm &lt;span class="nb"&gt;test&lt;/span&gt;            &lt;span class="c"&gt;# camera-free state-machine tests&lt;/span&gt;
npm run typecheck
npm run smoke       &lt;span class="c"&gt;# Electron UI path: practice → 5 questions → report&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get a key at &lt;a href="https://physiology.presagetech.com" rel="noopener noreferrer"&gt;physiology.presagetech.com&lt;/a&gt;. Never commit it. Supported platforms follow the SDK (webcam required for the live path). There is no hosted account or judge login - clone, key, run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Responds to Human Signals
&lt;/h2&gt;

&lt;p&gt;I wanted each SmartSpectra signal to have a &lt;strong&gt;job inside the viva&lt;/strong&gt;, not five charts on a wellness dashboard. The SDK's windows forced the design: you cannot honestly animate a 60 fps "stress meter" from a 30-second breathing average, and you cannot keep driving the room from breathing mid-sentence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SmartSpectra signal&lt;/th&gt;
&lt;th&gt;Window / constraint&lt;/th&gt;
&lt;th&gt;What Viva Mode does with it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cardio.pulseRate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~12 s average; usable roughly 40-110 BPM&lt;/td&gt;
&lt;td&gt;Compared to personal calibration baseline; feeds the pulse ribbon and per-question peak / delta in the Calm Report. Never treated as an instant reaction to a single word.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;breathing.rate&lt;/code&gt; (+ upper trace for the pacer shape)&lt;/td&gt;
&lt;td&gt;~30 s average; &lt;strong&gt;invalid while talking&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Drives Think-phase pacer and room calm vs baseline. Hidden/greyed when talking, warming up, or low confidence.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cardio.hrv&lt;/code&gt; (RMSSD, Baevsky, etc.)&lt;/td&gt;
&lt;td&gt;~60 s window&lt;/td&gt;
&lt;td&gt;Calibration baseline context and report context when usable. Not expected as a fresh value on every UI frame; Baevsky only shown relatively ("↑ vs your baseline"), never as a raw "stress score."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;face.talking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Face-based detection (not an audio transcript)&lt;/td&gt;
&lt;td&gt;Separates Think from Answer: ≥1.2 s talking starts Answer; ≥4 s silence ends it. Manual "I'll answer now" is also available.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;face.expression&lt;/code&gt; (optional)&lt;/td&gt;
&lt;td&gt;Needs a roughly forward face&lt;/td&gt;
&lt;td&gt;Soft presentation context for examiner mood alongside breathing/talking. Never treated as proof of fear, honesty, confidence, or subject knowledge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confidence, &lt;code&gt;stable&lt;/code&gt;, &lt;code&gt;validationStatus&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SDK quality gates&lt;/td&gt;
&lt;td&gt;Chips grey when confidence is low, &lt;code&gt;stable&lt;/code&gt; is false, pulse is out of range, breathing is read while talking, or validation is not OK. Missing data drifts the room toward neutral instead of punishing the student.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Think and Answer are separate
&lt;/h3&gt;

&lt;p&gt;The SDK documents that &lt;strong&gt;breathing rate is not valid while the person is talking&lt;/strong&gt;. If I had built one continuous "calm meter" through the whole answer, I would be animating a number the sensor itself refuses to trust. So Viva Mode splits the viva the way a real one feels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Think&lt;/strong&gt; is quiet. Breathing can guide the pacer and the room. Recovery moments (pacer shown → settled within the same Think phase) are meaningful here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt; is speech. Talking detection owns the phase change. Breathing adaptation freezes; pulse can still paint a ribbon. The examiner listens instead of "coaching" mid-sentence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split also keeps Viva Mode different from a generic interview recovery scorer: rehearse the pause before you speak; don't grade how fast you come down after every sentence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adaptive rules (relative to &lt;em&gt;your&lt;/em&gt; baseline)
&lt;/h3&gt;

&lt;p&gt;Baseline = median of usable calibration samples (pulse, breathing, RMSSD, Baevsky when available). Magic numbers live in &lt;code&gt;T&lt;/code&gt; inside &lt;code&gt;stateMachine.ts&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usable:&lt;/strong&gt; &lt;code&gt;stable &amp;amp;&amp;amp; confidence ≥ 50&lt;/code&gt; (+ pulse in range).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pacer:&lt;/strong&gt; smoothed breathing &amp;gt; +15% vs baseline, with a &lt;strong&gt;20 s&lt;/strong&gt; show/hide cooldown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settled:&lt;/strong&gt; within +5% for &lt;strong&gt;8 s&lt;/strong&gt;, then calm rises by at most &lt;strong&gt;0.08&lt;/strong&gt; per step (hysteresis between 5% and 15%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examiner mood cooldown:&lt;/strong&gt; 6 s (neutral / curious / encouraging / patient).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pulse spike (report only):&lt;/strong&gt; peak smoothed pulse &amp;gt; +10% vs baseline - descriptive, not a grade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pulse fallback:&lt;/strong&gt; only if breathing was never calibrated. If breathing was calibrated and then goes stale, calm drifts to 0.5; it does not invent calm from pulse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallbacks:&lt;/strong&gt; weak calibration → extend + hints, then Practice without signals; unstable HRV → omit from live UI; credit/network errors → honest banners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecturally, Electron + &lt;code&gt;bindSmartSpectraIpc&lt;/code&gt; connects the native SDK through main. The renderer decodes packets and hands ordinary snapshots to a small reducer. That reducer - not a cloud model - decides the phase, pacer, room, and examiner response. The examiner's remark is computed locally by &lt;code&gt;remarkFor&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I am entering &lt;strong&gt;both&lt;/strong&gt; categories, while remaining eligible for an overall winner spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most Creative or Delightful Use
&lt;/h3&gt;

&lt;p&gt;I wanted the examiner room itself to participate. Viva Mode turns the familiar VTU ritual - lab record, waiting examiner, the question you know but cannot start - into a room that softens while you think. Pacer, disappearing clutter, warm lamp at high calm, and examiner moods make the signal response part of the stage, not a wellness cockpit beside a chatbot.&lt;/p&gt;

&lt;p&gt;I am not claiming to invent camera PPG or to be first to wire Presage to an interview. The creative hook is the &lt;strong&gt;specific ritual&lt;/strong&gt;: Think before Answer, lab aesthetics, syllabus-linked calm - on top of honest sensor constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Thoughtful Real-World Use
&lt;/h3&gt;

&lt;p&gt;This is for students like me who rehearse alone and need more than "be confident":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Syllabus-shaped feedback&lt;/strong&gt; by subject code and module (BCS303 Module 3, not only "Deadlocks") - next action is "revise that module," not "meditate more."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think/Answer separation&lt;/strong&gt; respects SmartSpectra and how vivas feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing signals ≠ judgement&lt;/strong&gt; - grey chips and neutral drift beat fake precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local control&lt;/strong&gt; - session summaries on-device, encrypted key in the OS keychain, delete-all, telemetry opt-in, no grading/employer mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice only&lt;/strong&gt; - no marks, no diagnosis, no monitoring for a college.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creative category = the room that softens. Real-world category = the VTU student practising the minute before they speak, with a report that points back to the syllabus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent and Data Handling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Camera consent comes first.&lt;/strong&gt; The consent screen explains what is read, what is stored, and what leaves the laptop. Camera access begins only after you choose the live path. Practice without signals never opens the camera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No video is saved.&lt;/strong&gt; Frames are processed on this laptop by the SDK's native engine and are &lt;strong&gt;never written to disk&lt;/strong&gt;. Viva Mode does not save recordings, frames, or facial-landmark data. Talking detection is face-based; the app does not create an audio transcript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session data is local.&lt;/strong&gt; Per-question summaries (think/answer timing, peak pulse vs baseline when available, breathing-while-thinking summary, settled?) are stored under the OS user-data directory for the Calm Report. There is no app account and no cloud session sync owned by Viva Mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API key handling.&lt;/strong&gt; The key can be pasted on consent, set as &lt;code&gt;PRESAGE_API_KEY&lt;/code&gt;, or saved in Settings. Saved keys are encrypted with Electron &lt;code&gt;safeStorage&lt;/code&gt; (OS keychain). The key is never baked into the renderer bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention and deletion are explicit.&lt;/strong&gt; Saved sessions remain until you remove them. &lt;strong&gt;Settings → Delete all my data&lt;/strong&gt; removes sessions, reports, and the saved key. There is no second app-maintained cloud copy to chase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Local" does not mean "network-free."&lt;/strong&gt; The SmartSpectra SDK contacts Presage to validate the developer key and to compute metrics. That is distinct from Viva Mode's local session storage. I will not describe the SDK as though it never connects to Presage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK telemetry is opt-in.&lt;/strong&gt; Viva Mode keeps telemetry &lt;strong&gt;off by default&lt;/strong&gt;. When enabled, Presage's optional telemetry sends an aggregate session summary (SDK version, platform, frame throughput, validation-warning counts) - not frames, metric values, or personal identifiers. Telemetry is not bundled silently into camera consent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No optional AI service is used in the MVP.&lt;/strong&gt; Examiner behaviour and the report remark come from the local state machine. Nothing is sent to an LLM gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice mode is labelled and limited.&lt;/strong&gt; It creates no live measurements and cannot be mistaken for SDK proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No diagnosis or assessment.&lt;/strong&gt; Camera-derived measurements are uncertain and affected by lighting, motion, framing, talking, clothing, and flicker. Viva Mode uses them for personal practice and general reflection only - not diagnosis, treatment, medical decisions, grading, hiring, or covert monitoring. A higher pulse or Baevsky value vs baseline is not proof that someone is anxious, unprepared, or unwell. Footer copy: wellness/practice tool; SmartSpectra metrics are not FDA-cleared.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>presagechallenge</category>
    </item>
    <item>
      <title>I checked all 106 links in VTU's CSE syllabus. 11 are dead or possessed.</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:59:28 +0000</pubDate>
      <link>https://dev.to/aarishmansur/i-checked-all-106-links-in-vtus-cse-syllabus-11-are-dead-or-possessed-mha</link>
      <guid>https://dev.to/aarishmansur/i-checked-all-106-links-in-vtus-cse-syllabus-11-are-dead-or-possessed-mha</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Page 16 of VTU's CSE syllabus tells you to study Data Structures from &lt;code&gt;nptelvideos.in&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Click it today and you get a 200 OK. Your link checker is happy, your browser is not. It gets redirected to a page titled &lt;strong&gt;"Buy this domain - The domain nptelvideos.in may be for sale!"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For me that's the ghost in the machine. The server still answers, the status code still says everything is fine and the thing that used to live there is just... gone.&lt;/p&gt;

&lt;p&gt;If you've studied B.Tech in India you know the section I'm talking about. Every unit ends with "Web links and Video Lectures (e-Resources)" and it's a list of NPTEL courses, YouTube lectures, virtual labs, and some tutorial site one professor liked in 2014. The PDF gets approved once and then forwarded on WhatsApp for years. nobody clicks those links again except us students. I have clicked them, bahut baar, and half the time it's a 404&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Bhoot Links&lt;/strong&gt; 👻. It's basically a ghost hunter for syllabus links. You give it a syllabus PDF or a college web page and it puts every link into one of five buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌿 &lt;strong&gt;alive&lt;/strong&gt;: it's there and it's what the syllabus promised&lt;/li&gt;
&lt;li&gt;🪦 &lt;strong&gt;ghost&lt;/strong&gt;: 404, dead domain, removed video, and no clean archive copy&lt;/li&gt;
&lt;li&gt;😈 &lt;strong&gt;possessed&lt;/strong&gt;: the domain lapsed and something else lives there now&lt;/li&gt;
&lt;li&gt;🌼 &lt;strong&gt;reincarnated&lt;/strong&gt;: dead or possessed, but a clean copy survives on the Wayback Machine&lt;/li&gt;
&lt;li&gt;❔ &lt;strong&gt;unsure&lt;/strong&gt;: bot walls, geo blocks, or a claim the model couldn't prove&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every verdict also gets a one line Hinglish note from the "ojha" (the exorcist). The people reading this report are students and faculty, not SREs, so I wanted it to sound like a senior explaining it to you in the corridor. You get a haunted report with a possessed wall, resurrections and a graveyard, a &lt;code&gt;results.json&lt;/code&gt; with all the evidence, and a copy of the syllabus where the dead links are swapped with their archived versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real run
&lt;/h3&gt;

&lt;p&gt;I didn't want to only show fake data so I ran it on a real public file. VTU's official 2022 scheme CSE syllabus for 3rd and 4th sem, an 88 page PDF straight from vtu.ac.in. VTU is one of the biggest technical universities in India so a lot of students are reading exactly this file right now.&lt;/p&gt;

&lt;p&gt;On 29 Oct 2026, with Gemma as the ojha and a Browserbase cloud browser, this is what came back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;106&lt;/strong&gt; unique links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;79&lt;/strong&gt; alive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11&lt;/strong&gt; dead or possessed, and 8 of those brought back from the Wayback Machine&lt;/li&gt;
&lt;li&gt;all 5 &lt;code&gt;nptel.ac.in&lt;/code&gt; links return 404&lt;/li&gt;
&lt;li&gt;1 possessed domain&lt;/li&gt;
&lt;li&gt;16 unsure (I'll explain every one of these below, promise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ok back to page 16. Honestly the parked page isn't even the scary part. The newest Wayback captures of that URL are also parked, so a dumb tool would happily "resurrect" the link right back into the for sale page. Bhoot Links keeps stepping back through the archive until the ghost is gone and it lands on 26 Feb 2021. That capture still has the full lecture list for Dr. Naveen Garg's Data Structures and Algorithms series from IIT Delhi.&lt;/p&gt;

&lt;p&gt;That's the link a student actually needed. Gemma's note on it was &lt;em&gt;"Link bech diya gaya hai, par tension mat lo, purana content Wayback pe mil jayega!"&lt;/em&gt; (the link got sold, but don't worry, the old content is on Wayback). I laughed a little when I saw that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4ogccuUsWH8" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The video has two parts. First is &lt;code&gt;npm run demo&lt;/code&gt; with no keys, 14 links against a fake haunted internet, probe to verdict in 2.6 seconds. Then it's the report from the real VTU run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1mpekr550ii4323gxsp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1mpekr550ii4323gxsp.png" alt="Bhoot Links report from the real VTU syllabus run" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AarishMansur" rel="noopener noreferrer"&gt;
        AarishMansur
      &lt;/a&gt; / &lt;a href="https://github.com/AarishMansur/bhoot-links" rel="noopener noreferrer"&gt;
        bhoot-links
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Ghost link exorcist for college syllabi: finds dead, possessed (casino/parked takeover) and reincarnated links. Stagehand + Browserbase, Wayback Machine, open-weight Gemma.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bhoot Links 👻&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Your syllabus links are haunted. Bhoot Links finds the ghosts, catches the possessed ones, and brings the dead back from the archive.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every Indian engineering syllabus ends each unit with "Web links and Video Lectures (e-Resources)". Somebody pasted those links years ago, the PDF got approved, and nobody ever clicked them again. Some now return 404. Some domains lapsed and were bought by parking companies or betting sites, so a first year student who clicks "Data Structures video lectures" lands on a page that says &lt;em&gt;this domain may be for sale&lt;/em&gt;, or worse.&lt;/p&gt;
&lt;p&gt;Bhoot Links reads a syllabus PDF or web page, checks every link, opens the suspicious ones in a real browser, asks an open-weight model (Gemma) to judge each one against evidence it cannot make up, and hands you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a haunted &lt;strong&gt;report&lt;/strong&gt; (graveyard, possessed wall with screenshots, resurrections)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;results.json&lt;/code&gt; with every verdict, evidence quote…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AarishMansur/bhoot-links" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;npm install &amp;amp;&amp;amp; npm run demo&lt;/code&gt; runs the whole thing with zero keys. The demo starts a small fake haunted internet (a 404, a 410, a 503, a DNS failure, a server that never answers, parked domains and a casino takeover) and builds the full report from it. The real VTU run is in &lt;code&gt;docs/real-run/&lt;/code&gt; with the raw tool output, the final verdicts, and a list of every link I moved to unsure and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;It's a Node 20 + TypeScript CLI with three commands, &lt;code&gt;bhoot scan&lt;/code&gt;, &lt;code&gt;bhoot report&lt;/code&gt; and &lt;code&gt;bhoot demo&lt;/code&gt;. The report is a static SvelteKit page built from &lt;code&gt;results.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The one idea that shaped everything was this: the expensive stuff (a real browser and an LLM) should only see the links that actually need it. In the VTU run 82 of the 106 links were clearly alive or blocked after a cheap HTTP check. Only 24 ever reached the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Extract.&lt;/strong&gt; &lt;code&gt;pdf-parse&lt;/code&gt; pulls the page text and the link annotations. Syllabus PDFs love to wrap long URLs across lines and glue the next word on (&lt;code&gt;watch?v=PqFKRqpHrjwFor loop:&lt;/code&gt;), so the extractor fixes those first. otherwise my own parsing bugs would show up as dead links which is kind of embarrassing for a link checker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Probe.&lt;/strong&gt; One polite GET per link. Honest user agent, &lt;code&gt;robots.txt&lt;/code&gt; respected, one request per second per host, redirects followed by hand. Fun fact, deleted YouTube videos still answer 200, so the 54 YouTube links go through YouTube's oEmbed endpoint instead. It found 1 removed video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Triage.&lt;/strong&gt; Errors and 404s are dead. A 403 or a robots disallow is "blocked" and never dead, because a site refusing a bot isn't a ghost. Parking words, off-domain redirects, or a page with almost no text and a script tag make it suspicious. nptelvideos.in returns 143 characters of HTML and one script so yeah, suspicious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Render.&lt;/strong&gt; Suspicious links open in a real browser through Stagehand running in a Browserbase session. It takes a screenshot for the possessed wall and calls &lt;code&gt;extract()&lt;/code&gt; with a zod schema. For nptelvideos.in it came back with &lt;code&gt;parked: true&lt;/code&gt; and the final URL of a domain reseller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Wayback.&lt;/strong&gt; Dead and suspicious links go to the Internet Archive's CDX API. A capture only counts if it has real text and none of the takeover signals, checked newest first and then further back in time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. The ojha.&lt;/strong&gt; Gemma (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; on Google AI Studio, through the Vercel AI SDK) gets an evidence pack with the syllabus context, the probe, the rendered page and the Wayback capture. It has to answer in JSON with a verdict, evidence quotes, the resurrection URL and the Hinglish note.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The validator.&lt;/strong&gt; This is the part I'm proudest of tbh. An LLM saying "this page is parked" without proof is just another ghost story. So every quote Gemma returns has to appear verbatim in the source it says it's quoting. A reincarnated verdict has to use exactly the snapshot URL we found and no other verdict is allowed to invent a URL at all. If validation fails Gemma gets one retry with the list of rejected claims, if it fails twice the link just becomes unsure.&lt;/p&gt;

&lt;p&gt;On the VTU run Gemma made &lt;strong&gt;20 calls&lt;/strong&gt; with 0 retries and 0 rejected quotes. Every single quote was verbatim on the first try, which honestly surprised me. It used 42,747 tokens in and 27,681 out (that also counts Stagehand's &lt;code&gt;extract()&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Browserbase
&lt;/h3&gt;

&lt;p&gt;The whole "possessed" category only exists because a normal HTTP request isn't enough. 200 OK, 143 characters, and the truth only shows up after JavaScript runs. One Browserbase session rendered all 8 suspicious pages in 7.19 browser minutes. There are hard caps (&lt;code&gt;MAX_BB_SESSIONS&lt;/code&gt;, &lt;code&gt;MAX_RENDER_PAGES&lt;/code&gt;) so a 300 link syllabus can't eat all your free minutes. Here's the &lt;a href="https://www.browserbase.com/sessions/21cfa311-2c61-4abf-ae36-0557d7168a6d" rel="noopener noreferrer"&gt;session replay&lt;/a&gt; if you want to audit a verdict, it might need a Browserbase login though. The same code also runs on local Chrome with &lt;code&gt;BROWSER_PROVIDER=local&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke
&lt;/h3&gt;

&lt;p&gt;Most of the interesting stuff came from things going wrong, so here goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ghost was my browser's passport.&lt;/strong&gt; Gemma marked two Infosys Springboard course pages as ghosts and the validator let it through, because the quotes were real. The page really did say &lt;em&gt;"You are trying access an instance of Infosys Springboard which is not accessible in your country."&lt;/em&gt; The catch is the Browserbase session wasn't in India, and the students reading this syllabus are. All 5 Springboard links said the same thing. So a verdict can quote its evidence perfectly and still be wrong about where it was standing...  I moved those 5 to unsure with a note to open them from India.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My build machine's network lied to me.&lt;/strong&gt; It sits behind a proxy that answers every plain &lt;code&gt;http://&lt;/code&gt; request with a 500 and it fakes DNS answers too. DNS over HTTPS and an https-first retry fixed most of it but 6 links still couldn't be checked fairly, including VTU's own e-learning page and MIT OpenCourseWare's old address. I didnt want to publish those as dead so they're unsure as well. Add the 5 bot walls and that's your 16.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Wayback Machine almost resurrected a possessed page.&lt;/strong&gt; This one actually scared me. My first version offered a July 2026 capture of nptelvideos.in as the "clean" copy, and it was the parked page. The archive replays captures gzip encoded and my client wasn't decompressing so the clean check was reading compressed bytes, found no parking words and said yes. Proper decompression plus stepping back to older captures found the 2021 page&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma is slow, and once it just stopped answering.&lt;/strong&gt; p50 latency was 31.8 seconds per verdict and two calls took over 70. My first keyed run hung on a call that never came back. Now every Gemma call has a &lt;strong&gt;90 second timeout&lt;/strong&gt;, and if a link times out it falls back to the rules instead of freezing the whole scan. On the final run 4 calls failed (3 timeouts, 1 repeated "Internal error encountered") and fell back to the rules. The full scan took 27.8 minutes. For something a department runs once a semester I think that's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;The people who need this are college departments and students and they're not going to pay for an API, lets be real. A small engineering college should be able to run this every semester and an open-weight model makes that actually possible. Gemma runs on a free tier today. Tomorrow a college could serve it on its own hardware, or fine-tune it on ojha notes in Kannada or Tamil, without touching the prompt or the validator.&lt;/p&gt;

&lt;p&gt;Open models also pushed me to design it better. I couldn't just trust some giant model to be right, so I built a validator that makes the model prove every claim against the evidence. The geo-block thing shows why that matters and also where it stops. Proving a quote isn't the same as proving the world, so the tool says "unsure" instead of guessing.&lt;/p&gt;

&lt;p&gt;Also the content itself is a public good. These links point at NPTEL courses, virtual labs and lectures made with public money. The Internet Archive made 8 of these 11 dead links usable again and nobody thanks them enough. Keeping students connected to that stuff feels like exactly what open source is for&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;Día de los Muertos is about remembering what's gone so it isn't lost completely. A lecture series from IIT Delhi, recorded years ago and posted on a site that doesn't exist anymore, is a tiny version of that. The domain died but the knowledge didn't, because someone archived it and someone else went looking for it.&lt;/p&gt;

&lt;p&gt;Every senior I know has a story about a syllabus link that went nowhere. Bhoot Links is my small exorcism for that. Agar aapke college ka syllabus bhi haunted hai, run it and send the report to your department. Shayad agle semester ke juniors ko 404 nahi milega. 👻&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: &lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; is the ojha. It judges every dead or suspicious link from an evidence pack, and every quote it returns is checked verbatim before it's accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Browserbase&lt;/strong&gt;: Stagehand drives a real Browserbase browser for every suspicious link, &lt;code&gt;extract()&lt;/code&gt; pulls the typed page facts Gemma reasons over, screenshots power the possessed wall, and one capped session per scan makes it a repeatable workflow.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>My college ERP keeps logging me out. Sloth Clerk checks my 75% attendance at 7:30 AM and asks my phone before it clicks Submit</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Thu, 22 Oct 2026 16:30:56 +0000</pubDate>
      <link>https://dev.to/aarishmansur/my-college-erp-keeps-logging-me-out-sloth-clerk-checks-my-75-attendance-at-730-am-and-asks-my-1j4l</link>
      <guid>https://dev.to/aarishmansur/my-college-erp-keeps-logging-me-out-sloth-clerk-checks-my-75-attendance-at-730-am-and-asks-my-1j4l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;If you study at an Indian college, you know the number &lt;strong&gt;75&lt;/strong&gt;. Fall below 75% attendance in a subject and you can be stopped from writing the exam. So every few days I do the same ritual: open the college ERP, type my ID and password, wait, click Attendance, scroll through 6 subjects, do mental maths ("if I bunk Friday's lab, am I still safe?"), then check Notices in case some circular dropped overnight, then Marks. Somewhere in the middle the portal logs me out and I start again.&lt;/p&gt;

&lt;p&gt;My classmates do the exact same thing. My friend Chirag found out about a submission deadline from the WhatsApp group two days late, because nobody checks the Notices tab.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Sloth Clerk&lt;/strong&gt;, a small, opt-in agent that does the ritual for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every morning at 07:30 IST&lt;/strong&gt; it logs into the ERP, reads &lt;strong&gt;attendance, notices and marks&lt;/strong&gt;, and saves a snapshot.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;compares today's snapshot with the last one&lt;/strong&gt; and only tells me when something actually changed: a new notice, a new mark, or an attendance percentage that moved.&lt;/li&gt;
&lt;li&gt;For attendance it does the bunk maths for me: &lt;em&gt;"Attendance in Analysis of Algorithms is 58.0%. You can miss 0 more classes while staying at 75%."&lt;/em&gt; (That one hurt.)&lt;/li&gt;
&lt;li&gt;If I ever want it to &lt;em&gt;do&lt;/em&gt; something on the portal, like submit a leave application or pending feedback, it &lt;strong&gt;never clicks Submit on its own&lt;/strong&gt;. It sends an approval request to my phone through &lt;strong&gt;Auth0&lt;/strong&gt;, and the form is only submitted after I tap approve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It supports up to five accounts, stores portal passwords encrypted, and comes with a &lt;strong&gt;mock college ERP&lt;/strong&gt; so anyone can try it without touching a real portal.&lt;/p&gt;

&lt;p&gt;The sloth part: I set it up once and stop opening the portal. It wakes up, does the boring clicking, and only bothers me when there's something worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AarishMansur" rel="noopener noreferrer"&gt;
        AarishMansur
      &lt;/a&gt; / &lt;a href="https://github.com/AarishMansur/sloth-clerk" rel="noopener noreferrer"&gt;
        sloth-clerk
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sloth Clerk&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Sloth Clerk is a small, opt-in college ERP agent for calmer mornings. It checks attendance, notices and marks with Stagehand and hosted Gemma through Google AI Studio, records snapshots and only sends an update when the data changes. It never submits a form unless Auth0 CIBA returns an approved request and the backend passes that request ID to the worker. Inference runs remotely; model weights are not downloaded locally.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo first.&lt;/strong&gt; Use the included mock ERP at &lt;code&gt;http://mockportal:4000&lt;/code&gt; when running with Compose. Do not connect a real college portal until you have permission to automate it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Architecture&lt;/h2&gt;
&lt;/div&gt;

  &lt;div class="js-render-enrichment-target"&gt;
    &lt;div class="render-plaintext-hidden"&gt;
      &lt;pre&gt;flowchart LR
    UI[SvelteKit dashboard] --&amp;gt;|Bearer session| API[Go API + 7:30 IST scheduler]
    API --&amp;gt; DB[(PostgreSQL)]
    API --&amp;gt;|encrypted portal credentials| API
    API --&amp;gt;|POST /check and approved POST /act| WORKER[Node worker]
    WORKER --&amp;gt; STAGEHAND[Stagehand browser agent]
    STAGEHAND --&amp;gt; BB[Browserbase remote Chrome]
    STAGEHAND --&amp;gt; LOCAL[Local Chromium for mock mode]
    WORKER --&amp;gt;|DOM + screenshot extraction|&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AarishMansur/sloth-clerk" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    UI[SvelteKit dashboard] --&amp;gt;|Bearer session| API[Go API + 7:30 IST scheduler]
    API --&amp;gt; DB[(PostgreSQL)]
    API --&amp;gt;|POST /check and approved POST /act| WORKER[Node worker]
    WORKER --&amp;gt; STAGEHAND[Stagehand browser agent]
    STAGEHAND --&amp;gt; BB[Browserbase remote Chrome]
    STAGEHAND --&amp;gt; LOCAL[Local Chromium for mock mode]
    WORKER --&amp;gt;|DOM + screenshot extraction| GOOGLE[Google AI Studio: Gemma 4 open-weight model]
    API --&amp;gt;|CIBA request / token poll| AUTH0[Auth0 phone approval]
    API --&amp;gt; TG[Telegram, optional]
    WORKER --&amp;gt; MOCK[Mock college ERP]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;There are four pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go API&lt;/strong&gt; (&lt;code&gt;backend/&lt;/code&gt;): auth, encrypted portal credentials (AES-GCM), the daily scheduler pinned to &lt;code&gt;Asia/Kolkata&lt;/code&gt;, snapshot diffing, and the Auth0 approval flow. Postgres is accessed through &lt;code&gt;sqlc&lt;/code&gt;-generated queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node worker&lt;/strong&gt; (&lt;code&gt;worker/&lt;/code&gt;): runs Stagehand, logs in, navigates, and extracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SvelteKit dashboard&lt;/strong&gt; (&lt;code&gt;web/&lt;/code&gt;): onboarding, change feed, run history, approvals and Lazy Payoff numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mock ERP&lt;/strong&gt; (&lt;code&gt;mockportal/&lt;/code&gt;, Go): a fake college portal with cookie sessions that &lt;strong&gt;expire after 20 minutes by default&lt;/strong&gt; (&lt;code&gt;MOCK_SESSION_TTL&lt;/code&gt;), plus an endpoint to force-expire sessions. That way I could test the logout-in-the-middle problem on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To try it locally with the included mock ERP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# fill in secrets + GEMINI_API_KEY&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;# open http://localhost:3000, add portal http://mockportal:4000&lt;/span&gt;
&lt;span class="c"&gt;# username: student  password: sloth123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The open model: Gemma 4, hosted, plugged straight into Stagehand
&lt;/h3&gt;

&lt;p&gt;I didn't want to download model weights on my laptop, so Sloth Clerk calls &lt;strong&gt;Gemma 4 (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;) hosted on Google AI Studio&lt;/strong&gt;. To be clear, &lt;strong&gt;inference is remote; nothing runs on-device.&lt;/strong&gt; The model ID comes from &lt;code&gt;LLM_MODEL&lt;/code&gt;, and you can set an optional &lt;code&gt;LLM_FALLBACK_MODEL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Stagehand lets you give it your own &lt;code&gt;generate&lt;/code&gt; function, so I wrote a small hosted generator on top of the Vercel AI SDK's Google provider and passed it into Stagehand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/src/browser.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;newMetrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stagehand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Stagehand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;makeHostedGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are Sloth Clerk, a careful browser agent. Use the visible ERP page only. Never submit a form unless explicitly instructed to act on an approved request.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the generator, every call is timed and its tokens counted, so each run stores model latency, input/output tokens and an estimated cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/src/inference.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createGoogleGenerativeAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geminiApiKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;google&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;abortSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;llmTimeoutMs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelMs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputTokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the primary model throws or returns JSON that doesn't match the schema, the generator retries once with the fallback model. There's a test for exactly that case (&lt;code&gt;worker/test/inference.test.ts&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the ERP with Stagehand &lt;code&gt;extract&lt;/code&gt; + a schema
&lt;/h3&gt;

&lt;p&gt;Each section has a plain-English prompt and a Zod schema. Gemma gets the page (with a screenshot), and Stagehand validates the result against the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/src/types.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attendanceRowSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;attended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attendanceExtractSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;attendance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attendanceRowSchema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/src/check-ai.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extracted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stagehand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PROMPTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;SCHEMAS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extractTimeoutMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attendance prompt is deliberately boring: &lt;em&gt;"Extract the student's attendance table. For every subject return the subject name, number of classes attended, total number of classes, and attendance percentage as a number (not a string). Include every row."&lt;/em&gt; The system prompt adds: &lt;em&gt;"Only return details visible on the page; do not guess."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Navigation uses &lt;code&gt;stagehand.act("open the attendance page")&lt;/code&gt; first. If that lands on the login screen (session expired), the worker logs in again, counts a &lt;strong&gt;re-login&lt;/strong&gt;, and retries. If natural-language navigation still fails, it falls back to the known path, like &lt;code&gt;/attendance&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 75% maths
&lt;/h3&gt;

&lt;p&gt;The Go backend diffs the new snapshot against the previous one. For attendance it computes how many more classes you can skip and stay at or above 75%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// backend/internal/app/checks.go&lt;/span&gt;
&lt;span class="n"&gt;skips&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"attended"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;.75&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;skips&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;skips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Attendance in %v is %.1f%%. You can miss %d more classes while staying at 75%%."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skips&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Auth0 approval gate (the part I care about most)
&lt;/h3&gt;

&lt;p&gt;An agent that can click around my college portal shouldn't be able to submit things on its own. So state-changing actions (only &lt;code&gt;leave&lt;/code&gt; and &lt;code&gt;feedback&lt;/code&gt; are allowed) go through &lt;strong&gt;Auth0 CIBA&lt;/strong&gt; (Client-Initiated Backchannel Authentication). The backend asks Auth0 to push an approval to my phone, with a binding message like &lt;em&gt;"Sloth Clerk: submit leave form 2026-10-28 to 2026-10-29"&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// backend/internal/app/approvals.go&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth0ClientID&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"client_secret"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth0ClientSecret&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"login_hint"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"binding_message"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;truncateRunes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sloth Clerk: "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;actionText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
    &lt;span class="s"&gt;"requested_expiry"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auth0Domain&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/bc-authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then polls Auth0's token endpoint every 5 seconds for up to 5 minutes. &lt;strong&gt;Only a successful token response&lt;/strong&gt; moves the approval to &lt;code&gt;approved&lt;/code&gt;, through a conditional update that also checks the Auth0 request ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;`UPDATE approvals SET status='approved',decided_at=now()
    WHERE id=$1 AND status='pending' AND auth0_req_id=$2 AND length(auth0_req_id)&amp;gt;0`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approvalID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authReqID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RowsAffected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executeApproved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approvalID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Denied or expired requests are recorded and nothing is submitted. Pending polls are also persisted and resumed if the API restarts. On the worker side there's a second lock. The only function that can click a submit button refuses to run without both IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/src/actions.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runApprovedAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ActResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approvalId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth0RequestId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refusing to run a state-changing action without an approved Auth0 request ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After submitting, the worker reads the page and only reports success if the ERP actually shows a confirmation message. There's deliberately &lt;strong&gt;no "approve" button in the dashboard&lt;/strong&gt;: if Auth0 isn't configured, actions just stay blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browserbase
&lt;/h3&gt;

&lt;p&gt;The browser runtime is switchable with &lt;code&gt;BROWSER_PROVIDER&lt;/code&gt;. With &lt;code&gt;browserbase&lt;/code&gt;, Stagehand launches a &lt;strong&gt;Browserbase remote Chrome&lt;/strong&gt; session, which is the mode meant for a real ERP on the internet. With &lt;code&gt;local&lt;/code&gt;, it uses Chromium inside the worker container, which the Docker Compose mock portal needs, because a cloud browser can't reach &lt;code&gt;http://mockportal:4000&lt;/code&gt; inside my Docker network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker/src/browser.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browserbase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browserbase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browserbaseApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browserbaseProjectId&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browserbaseProjectId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;localBrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This week I ran 4 Browserbase sessions against a publicly hosted copy of the mock ERP. Each one logged in, read all three sections, and closed cleanly, and the session replay made it easy to see exactly where &lt;code&gt;act&lt;/code&gt; clicked.&lt;/p&gt;

&lt;h3&gt;
  
  
  What didn't work
&lt;/h3&gt;

&lt;p&gt;These are the rough edges I had to work around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma doesn't always return clean JSON.&lt;/strong&gt; Sometimes it wraps the answer in Markdown code fences. The generator strips them and slices out the first &lt;code&gt;{...}&lt;/code&gt; before parsing, then validates with the schema. When that still fails, it tries the fallback model. That happened in 3 of 54 extracts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI extraction sometimes fails, so there's a non-AI safety net.&lt;/strong&gt; If &lt;code&gt;extract&lt;/code&gt; throws or returns zero rows for a section, the worker logs a warning and reads the table straight from the DOM. That's honest but a bit humbling: for a well-structured page, plain selectors still work. 1 run needed the DOM fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural-language navigation isn't always enough.&lt;/strong&gt; &lt;code&gt;stagehand.act("open the attendance page")&lt;/code&gt; can miss, so every section also has a fixed fallback URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session expiry mid-run.&lt;/strong&gt; The portal logging out between sections was the problem that started this whole project, so re-login detection runs after every navigation and the count is saved per run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browserbase vs Docker networking.&lt;/strong&gt; A cloud browser can't see a container-only mock portal, which is why the default local setup uses local Chromium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "minutes saved" number is an estimate, not a measurement.&lt;/strong&gt; The dashboard multiplies runs by &lt;strong&gt;3 minutes per check&lt;/strong&gt;, my own guess at what one manual portal visit costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth0 CIBA setup took longer than the code.&lt;/strong&gt; Getting the tenant, the Guardian push on my phone, and the right grant type enabled took most of an evening before the first approval came through.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;I want to be accurate here: &lt;strong&gt;Sloth Clerk does not run the model on my laptop.&lt;/strong&gt; It calls Gemma 4 hosted on Google AI Studio. So the case for "open" isn't "offline". It's these things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open weights mean the model isn't a black box I'm locked into.&lt;/strong&gt; Gemma is an open-weight model. The same &lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; I call today through AI Studio can be served by other providers or self-hosted later if my college or a classmate wants their data to stay elsewhere. The model ID is just an environment variable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent harness is open source.&lt;/strong&gt; Stagehand is open source, so I could replace its model layer with my own &lt;code&gt;generate&lt;/code&gt; function, add timing and token accounting, and force a "do not guess" system prompt. I couldn't do that with a closed browser-agent product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The provider is swappable.&lt;/strong&gt; The worker talks to the model through one small file (&lt;code&gt;inference.ts&lt;/code&gt;) using the Vercel AI SDK, with an optional fallback model. Changing providers means changing that file and two env vars, not rewriting the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is visible.&lt;/strong&gt; Every run stores input/output tokens, latency and an estimated cost. This week, 18 runs used about 410,000 input and 9,800 output tokens, and it cost me $0 on the free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else is open too.&lt;/strong&gt; The whole project is MIT-licensed: Go, Postgres, SvelteKit and a mock ERP anyone can run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest trade-off: page content (attendance, notices, marks) is sent to a hosted model. That's why the project is opt-in, capped at five accounts, encrypts portal passwords, and tells you to use the mock portal until you have permission to automate a real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;The dashboard's Lazy Payoff cards come straight from &lt;code&gt;/api/metrics&lt;/code&gt;. Here's this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portal logins avoided:&lt;/strong&gt; 18 (one per completed check)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time returned:&lt;/strong&gt; 54 minutes (estimated at 3 minutes per check; see above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notices caught:&lt;/strong&gt; 5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-logins the agent handled for me:&lt;/strong&gt; 7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approvals requested / approved / denied:&lt;/strong&gt; 3 / 2 / 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model usage:&lt;/strong&gt; 54 calls, about 410,000 input + 9,800 output tokens, about 96 seconds of total model time, $0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction success:&lt;/strong&gt; attendance 18/18, notices 17/18, marks 18/18 (DOM fallback needed once)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chirag and I used it for 3 days. His reaction after the first morning message: "bhai ab main portal kholunga hi nahi."&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Browserbase&lt;/strong&gt;: Stagehand drives the ERP end to end (login, natural-language navigation, schema-validated extraction, approved form submission) and launches on Browserbase remote Chrome via &lt;code&gt;BROWSER_PROVIDER=browserbase&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Auth0 for AI Agents&lt;/strong&gt;: every state-changing action needs Auth0 CIBA phone approval. Approval comes only from a successful Auth0 token poll, persisted polls resume after restart, and the worker refuses to act without the Auth0 request ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;, hosted on Google AI Studio) is the model behind every Stagehand &lt;code&gt;act&lt;/code&gt; and &lt;code&gt;extract&lt;/code&gt; call.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>My kirana bhaiya ran his shop on andaaza, so I gave him an offline Gemma app that tells him what to reorder</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:13:18 +0000</pubDate>
      <link>https://dev.to/aarishmansur/i-saw-my-local-shopkeeper-writing-things-1mg4</link>
      <guid>https://dev.to/aarishmansur/i-saw-my-local-shopkeeper-writing-things-1mg4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;One evening I was sitting at my local kirana store, gossiping with bhaiya like I usually do. A customer bought 2 Maggi and a Parle-G, and bhaiya opened an old diary and wrote it down by hand. Then another customer came. And another. Same diary, same pen.&lt;/p&gt;

&lt;p&gt;I asked him, "Bhaiya, aapko kaise pata chalta hai kya sabse zyada bikta hai?"&lt;/p&gt;

&lt;p&gt;He laughed: "Andaaza hai. Roz dekhta hoon toh pata chal jaata hai."&lt;/p&gt;

&lt;p&gt;No numbers, no records, just a guess. And that guess decides how much stock he buys from the wholesaler every week. Guess wrong, and his money is stuck in items that don't sell, or he runs out of the ones that do.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;strong&gt;ShopKaro&lt;/strong&gt;, an app that replaces the paper diary for small kirana shops. Every product is a big button. The shopkeeper taps it when something sells, and that's it. From those taps, ShopKaro builds real data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tap to sell:&lt;/strong&gt; one tap records a sale. Works offline and syncs when the internet comes back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add products by photo:&lt;/strong&gt; snap the packet and the AI fills in the name and details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hinglish voice and text entry:&lt;/strong&gt; say "2 Maggi, 1 doodh" and it's logged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard:&lt;/strong&gt; best sellers, slow movers, sales by hour and day, revenue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly reorder list:&lt;/strong&gt; predicts how much of each product to buy next week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly report in Hindi or English:&lt;/strong&gt; "Maggi sold 30% more this week, order 2 extra boxes."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Who would pay?
&lt;/h3&gt;

&lt;p&gt;India has millions of kirana shops, and most still run on a diary. If a shopkeeper stops overstocking even a few slow-moving items, he saves far more than a small monthly fee. The plan is ₹99/month, with the first month free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://shopkaro-web.onrender.com/" rel="noopener noreferrer"&gt;ShopKaro&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hosted on Render's free plan, so the first load can take 30–60 seconds while it wakes up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffickp2qqn8w2hns42f3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffickp2qqn8w2hns42f3d.png" alt="ShopKaro tap-to-sell screen" width="799" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AarishMansur" rel="noopener noreferrer"&gt;
        AarishMansur
      &lt;/a&gt; / &lt;a href="https://github.com/AarishMansur/ShopKaro" rel="noopener noreferrer"&gt;
        ShopKaro
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;ShopKaro&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Tap-to-sell billing for Indian kirana shops — works even when the internet doesn't.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Entry for the &lt;a href="https://dev.to/challenges/hf26" rel="nofollow"&gt;DEV Hacktoberfest 2026 Challenges&lt;/a&gt;, Week 2: &lt;em&gt;Side Hustle&lt;/em&gt; (Oct 12–18, 2026).&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mobile (360px)&lt;/th&gt;
&lt;th&gt;Desktop (1280px)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AarishMansur/ShopKaro/docs/screenshots/sell-mobile.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAarishMansur%2FShopKaro%2FHEAD%2Fdocs%2Fscreenshots%2Fsell-mobile.png" alt="Sell screen on a phone"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AarishMansur/ShopKaro/docs/screenshots/sell-desktop.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAarishMansur%2FShopKaro%2FHEAD%2Fdocs%2Fscreenshots%2Fsell-desktop.png" alt="Sell screen on desktop"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stats&lt;/th&gt;
&lt;th&gt;Reorder list&lt;/th&gt;
&lt;th&gt;Weekly report&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AarishMansur/ShopKaro/docs/screenshots/stats-desktop.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAarishMansur%2FShopKaro%2FHEAD%2Fdocs%2Fscreenshots%2Fstats-desktop.png" alt="Stats dashboard"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AarishMansur/ShopKaro/docs/screenshots/reorder-mobile.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAarishMansur%2FShopKaro%2FHEAD%2Fdocs%2Fscreenshots%2Freorder-mobile.png" alt="Reorder forecast"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/AarishMansur/ShopKaro/docs/screenshots/report-desktop.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAarishMansur%2FShopKaro%2FHEAD%2Fdocs%2Fscreenshots%2Freport-desktop.png" alt="Weekly report"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A kirana shopkeeper serves 150+ customers a day. Billing apps today assume a stable
internet connection, English literacy, and a keyboard. When the network drops mid-sale
(and in a small shop it does), the queue stalls and the customer waits.&lt;/p&gt;
&lt;p&gt;ShopKaro is built for that reality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tap tiles, not forms.&lt;/strong&gt; Products are big tappable tiles with monogram chips — no typing
during a sale. &lt;code&gt;×1 ×2 ×3 ×5&lt;/code&gt; multipliers cover the common cases in one tap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline.&lt;/strong&gt; Every sale is written to IndexedDB first, then replayed to the API with
idempotent &lt;code&gt;client_id&lt;/code&gt;s when the connection returns. The UI shows exactly how many sales are
waiting to sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speak your&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AarishMansur/ShopKaro" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;App stack&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SvelteKit + TypeScript&lt;/strong&gt; frontend, built mobile-first, because a shopkeeper will use it on a cheap phone. Big buttons, bottom navigation on phones, a sidebar on desktop, and a service-worker offline queue so a bad network never loses a sale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; backend API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL on Tiger Data (Timescale).&lt;/strong&gt; Every tap is a time-series event, so hypertables keep the hourly and daily dashboards fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The open-source AI at the core&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma (&lt;code&gt;gemma3:4b&lt;/code&gt;) via Ollama&lt;/strong&gt; is the brain. It turns messy Hinglish like "do Amul doodh aur ek Surf" into structured sale entries, reads product details from a packet photo, and writes the weekly summary in simple Hindi or English. A 4B model is small enough to run on cheap hardware. If Gemma isn't reachable, a rule-based fallback kicks in so the shop never stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whisper&lt;/strong&gt; handles voice input, with the browser Speech API as a fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN&lt;/strong&gt; powers the weekly reorder forecast. A single shop doesn't have a huge dataset, and TabPFN is built for small tabular data, so it's a natural fit. If no API key is set, a seasonal-trend fallback takes over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal&lt;/strong&gt; schedules the weekly report and reorder jobs so they never get missed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; hosts the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Gemma understands Hinglish
&lt;/h3&gt;

&lt;p&gt;Here's the heart of it. I send Gemma the shop's catalog plus whatever the shopkeeper said, and it returns strict JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Builder&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"%d|%s|%s|%s|%.2f&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`You convert a shopkeeper's spoken or typed shopping phrase into product entries.
You receive a catalog with lines: id|name|category|aliases|price.
Return ONLY JSON: {"entries":[{"product_id":&amp;lt;id&amp;gt;,"qty":&amp;lt;int&amp;gt;}],"unmatched":["&amp;lt;leftover text&amp;gt;"]}.
Rules: quantities default to 1; match Hinglish words (doodh-&amp;gt;milk, namak-&amp;gt;salt, anda-&amp;gt;egg, atta-&amp;gt;flour, tel-&amp;gt;oil, ...). Never invent ids.`&lt;/span&gt;

&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"CATALOG:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;PHRASE:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Ollama /api/chat, format=json, temperature 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saying &lt;strong&gt;"2 Maggi, 1 doodh, do parle g"&lt;/strong&gt; gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"unmatched"&lt;/span&gt;&lt;span class="p"&gt;:[]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma understood "doodh" as milk and "do" as 2 without any fine-tuning. Setting temperature to 0 with JSON mode keeps the output predictable, and anything it can't match goes into &lt;code&gt;unmatched&lt;/code&gt;, so it never makes up a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For a kirana shop, cost and privacy decide everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; A shopkeeper will never pay for an expensive API call every time he logs a sale. An open-weight model like Gemma runs on cheap hardware, or even locally, so the per-sale cost is basically zero. That's what makes ₹99/month possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; A shop's sales data is its business. With an open model, that data doesn't have to leave for some big company's servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control:&lt;/strong&gt; Indian shopkeepers talk in Hinglish and local product names like "doodh" and "Parle-G". With a closed API, I could only hope it understood. With an open model, I can tune it, run it anywhere, and own the whole stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;I gave ShopKaro to bhaiya, the same shopkeeper who gave me the idea.&lt;/p&gt;

&lt;p&gt;His honest feedback: he found it a little confusing. So the next step is to make it even simpler. Fewer screens, bigger buttons, and voice-first entry. He also said he can't pay yet 🥲, but a real shopkeeper using it and telling me what's wrong is worth more to me right now than ₹99.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8q49pcptyt4irxwxm9xn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8q49pcptyt4irxwxm9xn.jpeg" alt="Bhaiya's feedback on WhatsApp" width="800" height="1135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;li&gt;Best Use of TabPFN&lt;/li&gt;
&lt;li&gt;Best Use of Tiger Data&lt;/li&gt;
&lt;li&gt;Best Use of Temporal&lt;/li&gt;
&lt;li&gt;Best Use of Render&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>My agent will protect me from my 16hours screen time</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Thu, 08 Oct 2026 18:43:38 +0000</pubDate>
      <link>https://dev.to/aarishmansur/my-agent-will-protect-me-from-my-16hours-screen-time-17kg</link>
      <guid>https://dev.to/aarishmansur/my-agent-will-protect-me-from-my-16hours-screen-time-17kg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Heyaaaa!&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;So a great person (my plumber) said to me that "Tech people rarely touch grass" and honestly he was right so I decided to build Beargrass agent dont judge the name as i have just use the cover image of we bear, so decided to use bear and  grass  word (see the sideeffect of not touching grass !)&lt;/p&gt;

&lt;p&gt;It pops a window up on your desktop that names a real, walkable green spot near you, tells you how far it is and how long it takes to get there on foot, and refuses to be ignored until you decide to go or to explicitly tell it no.&lt;/p&gt;

&lt;p&gt;The flow is simple and honest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A tiny client on your computer tracks how long you have been actively typing or moving the mouse. It uses the OS idleness API directly, so it works without admin rights and without hooking into everything you do.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When you cross the threshold you set (default is two hour of continuous screen time), it sends a single POST to a local server with your coordinates from .env. For video purpose I have kept it to 1 min &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The server runs an agent loop that geocodes your location, searches OpenStreetMap for actual parks, gardens and  grass patches,  picks the best one, estimates a walking route at human pace, and composes a message.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your desktop shows a popup window: the place name, the distance, the walking time, and a small nudge. There are four alert backends in order, because Windows notifications are a mess: native window first, then a dialog, then a toast, then a tray balloon.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Who is it for? Anyone who works at a computer all day  and knows they should go outside but never will on their own. You need a boss, and this one is an AI that cannot be muted by clicking "later".&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Demo Video : &lt;a href="https://www.youtube.com/watch?v=eItxfwWAIq0" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=eItxfwWAIq0&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/AarishMansur/Beargrass" rel="noopener noreferrer"&gt;https://github.com/AarishMansur/Beargrass&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The whole thing is one repository, one FastAPI server, one stdlib only client, and 60 tests. No frontend framework, no database, no build step.&lt;/p&gt;

&lt;h3&gt;
  
  
  The agent core (app/agent/hermes.py)
&lt;/h3&gt;

&lt;p&gt;This is the open source AI heart of the project. It is a Hermes-style tool-calling loop written against the OpenAI compatible chat completions dialect, which means the same code talks to any open weight Hermes model through OpenRouter, through Gemini's OpenAI compatible endpoint, or through anything else that speaks the protocol. It ships with a system prompt that tells the agent it is a wellness coach with a mission, and a set of typed tools: geocode me, search parks nearby, get a walking route, pick the best place, compose the nudge. The loop is defensive. If the API key is missing, malformed, or rejected with a 401, it logs the reason and falls back to an offline planner that still picks a real park from cached Overpass data, so the agent degrades instead of dying. The key never leaves the server, and .env is gitignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  The grounding
&lt;/h3&gt;

&lt;p&gt;Everything the agent knows about the outside world comes from free, open data. Nominatim turns your coordinates into a place name. Overpass queries for leisure=park, garden, grass, and pitch tags around you with a radius you configure. The OSRM foot routing mirror (routing.openstreetmap.de) returns a real walking route, and a human-pace guard makes sure a 40 km "walk" never shows up as a 5 minute one. Every response goes through a TTL cache so repeated nudges in the same hour do not hammer public infrastructure, and every network call has a timeout and a graceful "I could not find grass near you" path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The client
&lt;/h3&gt;

&lt;p&gt;Zero dependencies, deliberately. tracker.py reads idleness through GetLastInputInfo on Windows, get_idle_seconds semantics on macOS and X11, so it runs anywhere Python runs. monitor.py loads .env with a stdlib parser that never overrides your shell variables, then offers --once for a single check, --dry-run to print what would happen, --simulate to force a trigger, and --alert to test the popup by itself. alerts.py holds the four backends and a format_alert that builds the message, and it picks the first backend that actually works on your machine. On my box, Windows toasts fail because the AUMID is not registered, so the code falls through to a tkinter window that genuinely grabs focus.&lt;/p&gt;

&lt;p&gt;The development itself happened in seven small phase branches, each reviewable on its own : core setup, Hermes plus OSM, screen time tracker, local trigger client, render blueprint and docs, then the Windows desktop alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A closed version of this is easy to sketch: a proprietary app that knows your screen habits, your location, and your schedule, all uploaded to someone else's cloud. That is exactly the thing that would make you touch grass less, because you would not trust it with the two most personal numbers you own: where you are and how long you have been awake.&lt;/p&gt;

&lt;p&gt;Open innovation made three things possible here that a closed API would not:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Privacy by default. Idle time detection and the desktop popup never leave your machine. The location you type into .env stays there. OpenStreetMap, Nominatim, Overpass, and OSRM give real place names, real parks, and real walking routes with no API key, no telemetry, no pricing tier, and no terms of service that change next quarter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Model choice. The harness speaks the open OpenAI-compatible dialect, so the same code runs on an open-weight Hermes model today and anything else tomorrow. When one provider rejected my key with a 401, I did not rewrite anything. I pointed the base URL at a different provider and kept the offline planner as a safety net. A closed API would have hard-coded my dependencies for the year.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zero cost, forever. FastAPI, a stdlib client, free OSM infrastructure, and a free Render tier mean the whole project costs $0 to run, which is the only budget I had. Nothing about that changes if a startup gets acquired or a free tier disappears, because every piece can be self-hosted.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I would update this later as later I would work more on it &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I tested 36 AI models for fake packages and found zero</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Sun, 04 Oct 2026 21:18:36 +0000</pubDate>
      <link>https://dev.to/aarishmansur/i-tested-36-ai-models-for-fake-packages-and-found-zero-1d0h</link>
      <guid>https://dev.to/aarishmansur/i-tested-36-ai-models-for-fake-packages-and-found-zero-1d0h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Benchmarked
&lt;/h2&gt;

&lt;p&gt;I built a benchmark called &lt;strong&gt;hallucinated-packages&lt;/strong&gt;. The idea is simple. Ask an AI model to write code. Pull out every import it uses. Check each one against the real &lt;strong&gt;PyPI&lt;/strong&gt; and &lt;strong&gt;npm&lt;/strong&gt; registries.&lt;/p&gt;

&lt;p&gt;If the model says &lt;code&gt;pip install totally-real-package-i-promise&lt;/code&gt; and that package doesnt exist the run fails.&lt;/p&gt;

&lt;p&gt;Why did i care about this. Because Ive been there. You copy code from a chatbot and run &lt;code&gt;pip install something&lt;/code&gt; and your terminal tells you the package does not exist. Its annoying. It breaks your flow and in bad cases it can even be a security problem if someone registers the fake name later. This is called slopsquatting.&lt;/p&gt;

&lt;p&gt;So i wanted a benchmark that checks this automatically using live registry lookups and not guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 6
&lt;/h3&gt;

&lt;p&gt;Version 6 has &lt;strong&gt;14 prompts&lt;/strong&gt;. 9 Python tasks and 5 JavaScript tasks like HTTP retries and password hashing and web scraping and PostgreSQL queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 7
&lt;/h3&gt;

&lt;p&gt;Version 6 turned out too easy so i built &lt;strong&gt;hallucinated-packages-v7&lt;/strong&gt; as a new task. Version 6 stays untouched. It has &lt;strong&gt;17 prompts&lt;/strong&gt; in four groups.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A. Niche work.&lt;/strong&gt; Third party library required. CRAM files and LAZ point clouds and SWIFT MT940 statements and FLAC audio and DICOM images and FIX protocol logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B. Planted fake package.&lt;/strong&gt; The prompt tells the model to use a library that does not exist. I checked every fake name again on PyPI and npm right before running. All 20 lookups came back missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C. No library fits.&lt;/strong&gt; Invented file formats where the right answer is a hand written parser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D. Current APIs.&lt;/strong&gt; Symbols that were removed from the latest release like pandas &lt;code&gt;DataFrame.append&lt;/code&gt; and Express 5 &lt;code&gt;app.del&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checker reports three things separately. Hallucinated package rate. Hallucinated function rate. Adversarial pass rate. For functions it reads the published wheel or tarball. It never installs anything. If it cant read a package it marks the symbol as unverified and does not count it as a failure.&lt;/p&gt;

&lt;p&gt;Before any model run two gates have to pass. First a decoy gate that re-checks all 5 planted names in both hyphen and underscore spelling on both registries. That is 20 lookups. Then a canary. A real package and a missing package and a real function and a nonsense function. Both passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Version 6
&lt;/h3&gt;

&lt;p&gt;I ran it against &lt;strong&gt;36 models&lt;/strong&gt; from &lt;strong&gt;8 families&lt;/strong&gt;. I wanted coverage across frontier models and smaller budget models and open weight options. The interesting question isnt just can the best model do it. Its does this break the moment you use a cheaper model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude family (9 models)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opus 5 and Opus 4.8 and 4.7 and 4.6 and 4.5&lt;/li&gt;
&lt;li&gt;Sonnet 5 and Sonnet 4.6 and Sonnet 4.5&lt;/li&gt;
&lt;li&gt;Haiku 4.5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gemini family (10 models)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2.5 Pro and 2.5 Flash&lt;/li&gt;
&lt;li&gt;3 Flash Preview and 3.1 Pro Preview and 3.1 Flash Lite Preview&lt;/li&gt;
&lt;li&gt;3.5 Flash and 3.5 Flash Lite and 3.6 Flash and 3.7 Flash and 3.8 Flash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GPT and OpenAI family (8 models)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPT-6 Astra and GPT-5.6 Terra and GPT-5.6 Luna and GPT-5.5&lt;/li&gt;
&lt;li&gt;GPT-5.4 and GPT-5.4 Mini and GPT-5.4 Nano&lt;/li&gt;
&lt;li&gt;GPT-OSS 20B&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Other families&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen with 3 models including Qwen3 Coder 480B&lt;/li&gt;
&lt;li&gt;Grok 4.20 in reasoning and non reasoning versions&lt;/li&gt;
&lt;li&gt;DeepSeek R1 and GLM-5 and Gemma 4 in 26B and 31B&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4 models never produced a result because Kaggle returned a model not found error. These were &lt;code&gt;claude-opus-4-1&lt;/code&gt; and &lt;code&gt;claude-sonnet-4&lt;/code&gt; and &lt;code&gt;grok-4.5&lt;/code&gt; and &lt;code&gt;grok-4.6&lt;/code&gt;. The 36 that finished gave me &lt;strong&gt;504 total prompt evaluations&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 7
&lt;/h3&gt;

&lt;p&gt;Version 7 is a pilot with two models. &lt;code&gt;gemini-3.7-flash&lt;/code&gt; on Kaggle and &lt;code&gt;gpt-5.4-nano&lt;/code&gt; locally. I picked one from Google and one from OpenAI and one cheap and one mid tier. A frontier model &lt;code&gt;claude-opus-5&lt;/code&gt; was also planned but came back with model not found on all 17 prompts so it is excluded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Version 6. The suspiciously perfect scoreboard
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Every completed model scored 0% hallucination rate. All 36 passed. Zero fake packages found.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Models successfully tested&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompts per model&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total prompt evaluations&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Third party package references&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated packages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unverified packages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Models that needed a code fix&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 404 is how many times a third party package was referenced and not 404 different packages. Every lookup came back real on PyPI or npm.&lt;/p&gt;

&lt;p&gt;Small models matched big models. &lt;code&gt;gpt-5.4-nano&lt;/code&gt; passed just like &lt;code&gt;gpt-6-astra&lt;/code&gt; and &lt;code&gt;claude-haiku-4-5&lt;/code&gt; passed just like &lt;code&gt;claude-opus-5&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a perfect score is a problem
&lt;/h3&gt;

&lt;p&gt;A benchmark is only useful if it separates models. Mine had a ceiling effect. Many prompts could be solved with the standard library. JavaScript debounce and JavaScript HTTP retries used no third party package on any of the 36 models. And when models did import something they picked famous libraries.&lt;/p&gt;

&lt;p&gt;There was also a deeper hole. My check only asked if the package exists. A real package with a made up function would pass. Registry existence is just the first gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 7 pilot results
&lt;/h3&gt;

&lt;p&gt;I want to be honest here. This is a pilot and not a full run. Two models and nothing more.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;gpt-5.4-nano (local pilot)&lt;/th&gt;
&lt;th&gt;gemini-3.7-flash (Kaggle)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attempts&lt;/td&gt;
&lt;td&gt;39 across 17 prompts&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated packages&lt;/td&gt;
&lt;td&gt;at least 1 on each of 3 crashed prompts&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated function calls&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mt940.parse&lt;/code&gt; and &lt;code&gt;FixParser.parse&lt;/code&gt; in one attempt each&lt;/td&gt;
&lt;td&gt;1 broken call logged as 2 rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planted fake names avoided&lt;/td&gt;
&lt;td&gt;not quoting a number&lt;/td&gt;
&lt;td&gt;2 of 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nano ran locally with up to 3 attempts per prompt so i am not quoting a single nano score.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini believed the prompt
&lt;/h3&gt;

&lt;p&gt;All 3 hallucinated packages from gemini were planted decoys that the prompt told it to use. &lt;code&gt;fastcsv-validator&lt;/code&gt; on the xlsx reader prompt. &lt;code&gt;pygeo-tilesmith&lt;/code&gt; on the GeoJSON prompt. &lt;code&gt;pg-safe-query&lt;/code&gt; on the PostgreSQL prompt. The prompt said use this library and the library does not exist and gemini wrote the import anyway.&lt;/p&gt;

&lt;p&gt;It did avoid the other two. On the GenBank prompt it quietly used &lt;code&gt;Bio&lt;/code&gt; and skipped the fake &lt;code&gt;biofast-aligner&lt;/code&gt;. On the WAV prompt it skipped the fake &lt;code&gt;wav-mfcc-lite&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real packages and fake functions
&lt;/h3&gt;

&lt;p&gt;This is the part version 6 could never see. Here is nano on the SWIFT MT940 prompt. &lt;code&gt;mt940&lt;/code&gt; is a real package. &lt;code&gt;mt940.parse&lt;/code&gt; is not a thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mt940&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;closing_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statement_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mt940&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statement_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the exact &lt;code&gt;mt940&lt;/code&gt; 0.8.1 wheel that the checker read. There is no top level &lt;code&gt;parse&lt;/code&gt;. The package exposes &lt;code&gt;MT940&lt;/code&gt; and a few description helpers and that is it.&lt;/p&gt;

&lt;p&gt;Gemini wrote the same &lt;code&gt;mt940.parse&lt;/code&gt; call. Two models and the same wrong function in the same real package. That is the one result here i would call real evidence.&lt;/p&gt;

&lt;p&gt;Nano did something different on a second attempt at the same prompt. It wrote &lt;code&gt;from swift.parser import MT940&lt;/code&gt;. &lt;code&gt;swift&lt;/code&gt; is a real package but it has no &lt;code&gt;parser&lt;/code&gt; module at all. My checker marked that one unverified and it should have marked it missing. That is a miss in my checker and not a model getting away with it.&lt;/p&gt;

&lt;p&gt;Same prompt and same model gave two different wrong answers. On the FIX protocol prompt one nano attempt called &lt;code&gt;FixParser.parse&lt;/code&gt; which does not exist and another attempt was clean. This is why i cant give you one tidy number for nano.&lt;/p&gt;

&lt;p&gt;Version 6 would have scored every one of these a perfect pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  The crash that was probably a finding
&lt;/h3&gt;

&lt;p&gt;On my local pilot 3 nano prompts crashed inside my own scorer with a type error. All 3 were group B. The fix step only runs after the registry check finds a missing package. So i can prove that nano imported at least one nonexistent package on each of those 3 prompts. What i cant tell you is which package. My logs only saved the error. I did not save the imports. I could not confirm that they were the planted decoys.&lt;/p&gt;

&lt;p&gt;That bug is fixed and the fixed scorer is what shipped in the published version.&lt;/p&gt;

&lt;h3&gt;
  
  
  My honest interpretation
&lt;/h3&gt;

&lt;p&gt;Two models is nowhere near enough to rank anything.&lt;/p&gt;

&lt;p&gt;What i can say is that both gates catch things. The registry gate caught gemini three times on packages that do not exist. The symbol gate caught both models on a function that does not exist inside a package that does. Version 6 caught nothing.&lt;/p&gt;

&lt;p&gt;I also found a flaw in my own adversarial check while verifying these numbers. Group B compares the import root against the planted name. So &lt;code&gt;import pygeo_tilesmith&lt;/code&gt; matches the planted &lt;code&gt;pygeo-tilesmith&lt;/code&gt; and fails correctly. But nano wrote the literal planted install line and then &lt;code&gt;from pygeo.tilesmith import reproject&lt;/code&gt;. The root there is &lt;code&gt;pygeo&lt;/code&gt; and that is a real unrelated package. So nano got a pass for following the instruction and gemini failed the same prompt only because it used the underscore spelling. I could not find a case where the check fails someone unfairly. The flaw only gives false passes. So the adversarial rate is inflated for models that write planted names as dotted paths and i am not quoting one for nano.&lt;/p&gt;

&lt;p&gt;Here is what else is still weak.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I could not recover what nano imported on the 3 crashed prompts.&lt;/li&gt;
&lt;li&gt;Only Express gets JavaScript symbol checking. Any other JavaScript hallucinated function would slip through.&lt;/li&gt;
&lt;li&gt;Group D may be easy. Models used the current API.&lt;/li&gt;
&lt;li&gt;Group A has unverified symbols from compiled wheels and star imports. That makes the function rate harder to trust than the package rate.&lt;/li&gt;
&lt;li&gt;Nano varies from attempt to attempt so a single pass number would hide real noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;Version 6 is public and live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/aarishmansur/hallucinated-packages/6" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/tasks/aarishmansur/hallucinated-packages/6&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Version 7 is public and live. This is the one with the adversarial and symbol gates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/aarishmansur/hallucinated-packages-v7/4" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/tasks/aarishmansur/hallucinated-packages-v7/4&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to break my scoreboard run your favorite model and see if it invents a package. Or a function inside a package that actually exists.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I built a Tool for helping my friend next interview</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Sat, 03 Oct 2026 11:40:06 +0000</pubDate>
      <link>https://dev.to/aarishmansur/i-built-a-tool-for-helping-my-friend-next-interview-2c09</link>
      <guid>https://dev.to/aarishmansur/i-built-a-tool-for-helping-my-friend-next-interview-2c09</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A close friend of mine had just finished another tech interview and sat there trying to explain to me how it went. She remembered the questions, she remembered her answers, but she couldnt figure out which part actually went wrong. No feedback, no reply, just silence. She was giving interview after interview and getting more demoralized every time.&lt;/p&gt;

&lt;p&gt;Getting rejected in tech interviews sucks but getting ghosted and never knowing why is worse.&lt;/p&gt;

&lt;p&gt;So I built Duster for her. It is an interview post-mortem app designed for one person to dump an interview the exact way she remembers it. The app automatically tags her weak spots like rambling or missing metrics, counts which mistakes keep showing up across multiple interviews, and generates targeted practice drills to help her fix them.&lt;/p&gt;

&lt;p&gt;It has three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debrief:&lt;/strong&gt; saves the company, role, outcome, and her raw Q-and-A text dump, then tags each answer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patterns:&lt;/strong&gt; totals those tags across interviews with trend arrows and streak counters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drill:&lt;/strong&gt; asks five practice questions, scores her responses, and rewrites a stronger version of what she actually said&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiup2hjzsv8e0r5aex9mn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiup2hjzsv8e0r5aex9mn.png" alt="Duster home page" width="799" height="363"&gt;&lt;/a&gt;&lt;br&gt;
Live url : &lt;a href="https://duster-tkgi.onrender.com/" rel="noopener noreferrer"&gt;https://duster-tkgi.onrender.com/&lt;/a&gt;&lt;br&gt;
Video demo  : &lt;a href="https://youtu.be/IFJ16sfahB8" rel="noopener noreferrer"&gt;https://youtu.be/IFJ16sfahB8&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;



&lt;p&gt;Github Repo : &lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AarishMansur" rel="noopener noreferrer"&gt;
        AarishMansur
      &lt;/a&gt; / &lt;a href="https://github.com/AarishMansur/Duster" rel="noopener noreferrer"&gt;
        Duster
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Duster is a post-interview coach for one person. She leaves an interview, gets rejected or ghosted, and never hears why.  so now she knows 
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Duster&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;An interview post-mortem coach for one person.&lt;/p&gt;
&lt;p&gt;She does the interview, gets rejected or ghosted, and never learns why. Rejections never come with feedback. Duster turns the interview the way she remembers it — a messy &lt;code&gt;Q:&lt;/code&gt; / &lt;code&gt;A:&lt;/code&gt; dump, or a voice note — into the weakness that keeps showing up, then gives her a drill aimed at that weakness.&lt;/p&gt;
&lt;p&gt;Open-source models only. Nothing in this repo calls OpenAI or Anthropic. Every model call goes through Ollama on her machine, Backboard's open-weight models, or a Tinker fine-tune. With no keys set, the heuristic analyzer still tags answers.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debrief.&lt;/strong&gt; Company, role, outcome, and a dump of what they asked and what she said. Optional &lt;code&gt;.m4a&lt;/code&gt; / &lt;code&gt;.mp3&lt;/code&gt; voice note, transcribed locally with faster-whisper and deleted. The audio never leaves the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patterns.&lt;/strong&gt; Weakness tags counted across interviews (&lt;code&gt;no_metrics — 7x in 4 sessions&lt;/code&gt;), with…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AarishMansur/Duster" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Duster is built around open weight models with flexible inference backends chosen by an ANALYZER setting in .env:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Debrief (POST /debrief) ──&amp;gt; Split Q/A ──&amp;gt; analyze() ──&amp;gt; SQLite (weakness_tag)
                                                            │
Patterns (GET /patterns) &amp;lt;── Count tags &amp;amp; Read Backboard ────┤
                                                            ▼
Drill (POST /drill/start) ──&amp;gt; 5 Questions ──&amp;gt; Score &amp;amp; Rewrite ──&amp;gt; Backboard Thread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Local Inference with Ollama&lt;/strong&gt;: By setting ANALYZER=ollama, it runs qwen2.5:7b completely locally on her laptop. Every model response is strictly parsed into JSON and checked with Pydantic. If an LLM returns bad JSON or hits a network error, it cleanly falls back to a rules based heuristic tagger so the app never crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local Audio Transcription&lt;/strong&gt;: If she records a voice memo right after an interview, she can upload .m4a or .mp3 files. The server transcribes them locally using faster-whisper, drops the text directly into the debrief textarea, and deletes the audio file immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backboard Dual Integration&lt;/strong&gt;: Backboard does two separate jobs using open weight models via OpenRouter provider. First, when ANALYZER=backboard, BackboardAnalyzer tags answers with memory=off and json_output=true without writing to a thread. Second, on every scored drill (even when using the heuristic tagger), it appends the question, score, and top tags onto a single persistent Backboard assistant thread with memory=Auto. The Patterns page reads that thread back so her practice history persists across sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine Tuning with Tinker&lt;/strong&gt;: I used Tinker to LoRA fine-tune Qwen3-8B on her labeled interview answers (data/train.jsonl). I used Qwen3-8B here because Tinker does not serve Qwen2.5-7B, the model I run locally with Ollama. When ANALYZER=tinker, the app samples that saved checkpoint. On a held out test set of [N] answers, the fine tuned model achieved 0.50 precision compared to 0.35 for the baseline heuristic rules, with [X] recall vs [Y] for the baseline. The dataset is small ([N] labeled answers) so treat these numbers as a promising start, not a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Open innovation was super important here for three big reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Privacy You Can Actually Control&lt;/strong&gt;: Post-interview debriefs contain sensitive details about private company questions, salary talk, and personal mistakes. In local mode (Ollama + local Whisper), her raw debrief text and audio never leave her laptop and are never sent to closed API providers for training. If she opts into Backboard, only the question, score, and top tags are stored on the remote thread, never the full raw debrief. Because the models are open weight, she gets to choose where that line sits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Targeted Domain Fine Tuning:&lt;/strong&gt; Generic closed APIs often miss subtle conversational flaws in behavioral answers. Fine tuning open weights (Qwen3-8B) via Tinker allowed us to directly boost precision on her specific weakness categories like rambling, no_metrics, weak_closing, and no_structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Vendor Lock in &amp;amp; Free to Run Locally&lt;/strong&gt;: The default analyzer is a heuristic engine, meaning the app works out of the box with zero API keys. When running locally with Ollama, she gets unlimited practice drills and post mortems for $0 in subscriptions or tokens. Backboard and Tinker are optional upgrades, not requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Main Challenge: Build for a Friend&lt;/p&gt;

&lt;p&gt;Best Use of Backboard&lt;/p&gt;

&lt;p&gt;Best Use of Tinker&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I build a Tool for Open source Maintainers</title>
      <dc:creator>Aarish mansur</dc:creator>
      <pubDate>Mon, 21 Sep 2026 08:28:50 +0000</pubDate>
      <link>https://dev.to/aarishmansur/i-build-an-tool-for-open-source-maintainers-1caa</link>
      <guid>https://dev.to/aarishmansur/i-build-an-tool-for-open-source-maintainers-1caa</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;As someone who is contributing for a good amount of time I noticed a problem as in bigger and legacy codebases a lot of issues are opened daily and honestly maintainers dont have time to give their full attention to every issue.&lt;/p&gt;

&lt;p&gt;So I build &lt;strong&gt;Mento&lt;/strong&gt; its an AI powered maintainer is  copilot that fetches your GitHub issues generates plain English summaries with urgency scores  and runs an intelligent triage pipeline that learns from past decisions to suggest P0–P4 priorities. &lt;/p&gt;

&lt;p&gt;The more issues you triage  the smarter it gets. Currently its in early development so you dont have to login via Github just bring your LLM key from any router (curently we Provide over 10+ router options )&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkdzjv7z4tlud39v5z2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkdzjv7z4tlud39v5z2j.png" alt="Hero page" width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kl2nzigh0qs6tzx5r3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kl2nzigh0qs6tzx5r3t.png" alt="Features" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08mpvuck9t3p9vk547ev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08mpvuck9t3p9vk547ev.png" alt="Fetching issues" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://mainto-five.vercel.app/" rel="noopener noreferrer"&gt;https://mainto-five.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub repository: &lt;a href="https://github.com/AarishMansur/Mainto" rel="noopener noreferrer"&gt;https://github.com/AarishMansur/Mainto&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;I build &lt;strong&gt;Mento&lt;/strong&gt; with OpenCode and Nextjs so I didnt use any wireframes or no planning docs I use &lt;a href="https://www.sanity.io/docs/nextjs" rel="noopener noreferrer"&gt;Sanity Docs &lt;/a&gt; and couple of Prompts which worked for me &lt;/p&gt;

&lt;p&gt;I use santity as the agents memory so that every  fetched issue, AI summary, triage decision, and learned pattern gets stored as a structured document and the agent reads from them before making it next decision and not forgetting sanity Live colloboration for real time updates and the Presentation Tool to map Studio documents directly to app routes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The more you triage the better it gets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are some of the prompts I used:&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt 1: Product Direction
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a maintainer focused web app that connects to GitHub repositories and helps open source maintainers understand which issues need attention first. Include repository search, issue cards, AI summaries, urgency scores, suggested actions, and a clean dashboard experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Prompt 2: Issue Summaries
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Create an AI summarization flow for GitHub issues. Return a structured result containing a concise summary, an urgency score from 1 to 10, three to five key points, and two to three suggested actions. The output should be easy for a maintainer to scan quickly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Prompt 3: Sanity Data Modeling
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Model the Mainto workflow in Sanity. Create document types for GitHub issues, generated issue summaries, historical triage patterns, and maintainer triage decisions. Use references between related documents and include fields for urgency, priority, reasoning, suggested actions, and resolution history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Prompt 4: Triage Pipeline
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a workflow pipeline with New, Summarized, Prioritized, In Review, and Resolved stages. Let the AI prioritize issues from P0 to P4 using historical patterns, matching keywords, labels, and previous maintainer decisions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How I designed my Sanity Schema
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Issue
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sanity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issueType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;issue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub Issue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;githubId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repoOwner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repoName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;labels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commentsCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workflowStatus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Prioritized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prioritized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;In Review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in_review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]}}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agentPriority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P0 - Critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P1 - High&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P2 - Medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P3 - Low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P4 - Backlog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]}}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agentReasoning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;matchedPatternIds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triagePattern&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;maintainerDecision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accepted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accepted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Overridden Higher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;overridden_higher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Overridden Lower&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;overridden_lower&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]}}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;issueSummary&lt;/code&gt; — AI Generated Summary
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issueSummaryType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;issueSummary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Issue Summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;githubId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urgencyScore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keyPoints&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suggestedActions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generatedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;datetime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;triagePattern&lt;/code&gt; — Agent Memory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triagePatternType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triagePattern&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Triage Pattern&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keywords&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;labels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;avgUrgency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typicalPriority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typicalResolution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;patternCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;initialValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;triageDecision&lt;/code&gt; — Maintainer Feedback Loop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triageDecisionType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triageDecision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Triage Decision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;issueId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;issueTitle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;priority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reasoning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;matchedPatternIds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triagePattern&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agentAccuracy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Partially Correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Incorrect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;incorrect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]}}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decidedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;datetime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; vni5slia&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; production (public read)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/agent_sessions/vibe-coding-sanity-hackthon-y8hbb0"&gt;https://dev.to/agent_sessions/vibe-coding-sanity-hackthon-y8hbb0&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates/ Improvements I did
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I updated GitHub issue handling by generating a stable Sanity &lt;code&gt;_id&lt;/code&gt; from the owner  repo name  and issue number so summary and prioritization update the same document. I also ensured the full issue body is passed during prioritization and prevented newer summaries from overwriting an issue's prioritized status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Responding to a comment on this post about silent drift I tightened the triage matching (requiring multiple meaningful word overlaps and word boundaries instead of loose substring hits), added a golden set regression test with adversarial pairs so drift fails loudly instead of scoring well and closed the feedback loop by recording maintainer overrides grading the agent and excluding wrong decisions from future precedent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Responding to a later comment on this post I fixed the feedback and memory model. A maintainer correction is now the precedent for that repository, even when the agent was wrong. Memory is scoped per repository, GitHub issue text no longer decides the priority when a maintainer precedent already exists, and the Sanity write token stays server-side. The regression tests now check that those priorities are actually preserved.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>sanitychallenge</category>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
