<?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: Aaron Correya</title>
    <description>The latest articles on DEV Community by Aaron Correya (@aaronstark1).</description>
    <link>https://dev.to/aaronstark1</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%2F4156678%2F2eca65a2-beec-4273-8ee2-e97fd2476e4d.png</url>
      <title>DEV Community: Aaron Correya</title>
      <link>https://dev.to/aaronstark1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaronstark1"/>
    <language>en</language>
    <item>
      <title>SpecShield QA: Turning NDA-Locked Specs into Playwright Suites with Local Gemma, Mastra and Sentry</title>
      <dc:creator>Aaron Correya</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:52:09 +0000</pubDate>
      <link>https://dev.to/aaronstark1/specshield-qa-turning-nda-locked-specs-into-playwright-suites-with-local-gemma-mastra-and-sentry-3jp0</link>
      <guid>https://dev.to/aaronstark1/specshield-qa-turning-nda-locked-specs-into-playwright-suites-with-local-gemma-mastra-and-sentry-3jp0</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&lt;/a&gt; (Side Hustle)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Boutique agencies and freelance QA engineers live under NDAs that forbid pasting client PRDs into cloud LLMs. I have watched a two-page registration story turn into an afternoon of spreadsheet test cases, then another afternoon of Playwright boilerplate. Four to six unbillable hours per sprint, every sprint, and the work is so mechanical it hurts.&lt;/p&gt;

&lt;p&gt;SpecShield QA is a local-first studio that takes that PRD and returns three things: structured requirements with roles, features and validation rules; a filterable QA matrix across Functional, Boundary, Negative and Security (14 cases from live Gemma on the bundled sample, 30 in the deterministic demo mode); and a downloadable Playwright suite built on the Page Object Model with one class per screen, &lt;code&gt;getByTestId&lt;/code&gt; locators, login fixtures and a &lt;code&gt;// covers FIT-FUNC-001&lt;/code&gt; comment on every test.&lt;/p&gt;

&lt;p&gt;The whole pipeline runs on Gemma locally through Ollama, so the spec never leaves the laptop and inference costs $0.00. Every agent step is traced with Sentry Agent Tracing, past runs persist to a local JSON file, and the app stays fully usable when Ollama is not installed thanks to a deterministic fallback that is honest about its &lt;code&gt;mode&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Side Hustle Business Model
&lt;/h3&gt;

&lt;p&gt;The buyer is a freelance QA engineer billing $85 an hour, or a five-person agency that onboards two clients' worth of regression suites a month. The bundled Freelancer Invoice Tracker spec serves as a deliberately small, public stand-in for a confidential client PRD, so anyone can run it without NDA concern; a live Gemma run produces 14 test cases and 5 page objects. At 12 minutes per case plus 25 minutes per page object and 45 minutes of review ((14 × 12 + 5 × 25 + 45) / 60), that is 5.6 hours, or $476 of billable value, per run. Charge $29 a month for the seat and the tool pays for itself if it saves twenty minutes.&lt;/p&gt;

&lt;p&gt;Agencies get the second tier: shared run history in &lt;code&gt;data/runs.json&lt;/code&gt;, compare-with-previous diffs, Markdown and CSV exports for client sign-off, and a telemetry drawer they can screenshot into a status report. The pitch is not "AI writes tests". It is "your NDA stays intact, your junior spends Friday on exploratory testing instead of boilerplate, and you can prove it with a trace".&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;Local Gemma via Ollama&lt;/th&gt;
&lt;th&gt;Closed cloud API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost per 1,000 spec runs (6,915 tokens/run: 4,511 in / 2,404 out)&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;~$50 (1,000 × (4,511/1e6×$3 + 2,404/1e6×$15))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NDA exposure&lt;/td&gt;
&lt;td&gt;None — localhost only, DSN empty by default&lt;/td&gt;
&lt;td&gt;Spec text leaves the laptop on every call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data residency&lt;/td&gt;
&lt;td&gt;Laptop disk (&lt;code&gt;data/runs.json&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Vendor region, retention policy applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limits&lt;/td&gt;
&lt;td&gt;None — unlimited iterations&lt;/td&gt;
&lt;td&gt;Per-minute caps stall the matrix step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline&lt;/td&gt;
&lt;td&gt;Works on a train, in an air-gapped client office&lt;/td&gt;
&lt;td&gt;Requires connectivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model swap / fine-tune&lt;/td&gt;
&lt;td&gt;One env var (&lt;code&gt;GEMMA_MODEL=gemma3:4b&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;New SKU, new price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Margin on a $29 seat&lt;/td&gt;
&lt;td&gt;~$29 minus hosting ($0)&lt;/td&gt;
&lt;td&gt;~$29 minus ~$0.05/run in tokens, metered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The token math uses the real &lt;code&gt;usage&lt;/code&gt; counts from a live &lt;code&gt;gemma2:9b&lt;/code&gt; run of the bundled sample on a 16 GB MacBook Air M4 (4,511 input, 2,404 output, 220.9 s end to end) at the $3/$15 per MTok assumption from &lt;code&gt;.env.example&lt;/code&gt;. On live Gemma the counts come from &lt;code&gt;usage&lt;/code&gt; directly; in fallback they are estimated as chars/4 so the comparison stays meaningful and is labelled "est.".&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmceu5qvobz6elaq795gg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmceu5qvobz6elaq795gg.gif" alt="SpecShield QA demo: the Invoice Tracker sample runs through three Mastra agents on local Gemma 2 9B, then the QA matrix is filtered to Security, a case is expanded, and the Playwright code, Sentry telemetry and run history tabs are shown" width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A real run on local Gemma, with the six-minute wait fast-forwarded about 57x. It was recorded headless by a Playwright script, so a Playwright test generator filmed its own demo. Recording on the same laptop slowed Gemma down, which is why these timings are longer than the traced run further down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Load the sample (Load Sample Spec (Invoice Tracker)), hit Run Pipeline, and watch three agent cards go pending → running → done with durations, token counts and mode chips. Then open the matrix tab (filter by Security, search "overdue" or "password"), the Playwright tab (switch between the combined spec and each page object, copy, download), the telemetry tab (waterfall bars, trace ID, codegen strategy) and history (reload, compare, delete). The demo runs identically with Ollama stopped — the header chip flips to amber Deterministic Fallback and nothing errors.&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/AaronStark1" rel="noopener noreferrer"&gt;
        AaronStark1
      &lt;/a&gt; / &lt;a href="https://github.com/AaronStark1/specshield-qa" rel="noopener noreferrer"&gt;
        specshield-qa
      &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;SpecShield QA — Local-First Spec-to-Test &amp;amp; Playwright E2E Studio&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Local-first studio that turns a pasted PRD into &lt;strong&gt;structured requirements&lt;/strong&gt;, a &lt;strong&gt;filterable QA test matrix&lt;/strong&gt;, and a &lt;strong&gt;downloadable Playwright &lt;code&gt;.spec.ts&lt;/code&gt; suite (Page Object Model)&lt;/strong&gt; — with &lt;strong&gt;zero cloud calls&lt;/strong&gt; by default.&lt;/p&gt;
&lt;p&gt;Three &lt;strong&gt;Mastra&lt;/strong&gt; agents run on &lt;strong&gt;Google Gemma locally through Ollama&lt;/strong&gt;, traced with &lt;strong&gt;Sentry Agent Tracing&lt;/strong&gt;. No Ollama? The app still runs in a polished &lt;strong&gt;deterministic fallback mode&lt;/strong&gt;. No Sentry DSN? Spans become no-ops and the UI reads an in-process collector.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install
cp .env.example .env   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; optional&lt;/span&gt;
npm run dev            &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; API :8787 + UI :5173&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:5173" rel="nofollow noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt; → &lt;strong&gt;Load Sample Spec (Invoice Tracker)&lt;/strong&gt; → &lt;strong&gt;Run Pipeline&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With Ollama for live Gemma inference:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;OLLAMA_CONTEXT_LENGTH=8192 ollama serve
ollama pull gemma2:9b   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or gemma3:4b for faster runs on resource-constrained laptops&lt;/span&gt;
npm run dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Single-port production mode:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run build
npm start              &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; serves dist/&lt;/span&gt;&lt;/pre&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/AaronStark1/specshield-qa" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev   &lt;span class="c"&gt;# API :8787 + UI :5173&lt;/span&gt;
ollama pull gemma2:9b        &lt;span class="c"&gt;# optional: live Gemma; app works without it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The stack is one &lt;code&gt;package.json&lt;/code&gt;: Vite 6, React 19, TypeScript 5, Tailwind 3.4, Hono on &lt;code&gt;@hono/node-server&lt;/code&gt; with &lt;code&gt;tsx&lt;/code&gt;, &lt;code&gt;@mastra/core&lt;/code&gt; 1.77, &lt;code&gt;zod&lt;/code&gt;, &lt;code&gt;@sentry/node&lt;/code&gt;, and a hand-rolled &lt;code&gt;JsonStore&lt;/code&gt; (atomic temp-file + rename, no SQLite). &lt;code&gt;npm run dev&lt;/code&gt; runs API and web with &lt;code&gt;concurrently&lt;/code&gt;; Vite proxies &lt;code&gt;/api&lt;/code&gt; to :8787.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pipeline: three Mastra agents over local Gemma
&lt;/h3&gt;

&lt;p&gt;I defined three agents — Requirement Parser, QA Matrix Architect, Playwright Codegen — each a &lt;code&gt;new Agent({ id, name, instructions, model: gemmaModel })&lt;/code&gt; registered with the workflow in &lt;code&gt;server/mastra/index.ts&lt;/code&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mastra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Mastra&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requirementParser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qaMatrixArchitect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;playwrightCodegen&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;specShieldWorkflow&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 model config is the Ollama OpenAI-compatible form from the Mastra model-providers docs (verified against the installed 1.77 types where &lt;code&gt;OpenAICompatibleConfig&lt;/code&gt; accepts &lt;code&gt;id: \&lt;/code&gt;${string}/${string}``):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`ts&lt;br&gt;
// server/mastra/model.ts&lt;br&gt;
export const gemmaModel = {&lt;br&gt;
  id: `custom/${GEMMA_MODEL_NAME}`,&lt;br&gt;
  url: OLLAMA_BASE_URL,&lt;br&gt;
  apiKey: "ollama",&lt;br&gt;
} as const;&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The workflow chains three &lt;code&gt;createStep&lt;/code&gt;s exactly as the docs prescribe:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`ts&lt;br&gt;
// server/mastra/workflows/specShieldWorkflow.ts&lt;br&gt;
export const specShieldWorkflow = createWorkflow({&lt;br&gt;
  id: "specShieldWorkflow",&lt;br&gt;
  inputSchema: workflowInputSchema,&lt;br&gt;
  outputSchema: codegenOutputSchema,&lt;br&gt;
})&lt;br&gt;
  .then(parseStep)&lt;br&gt;
  .then(matrixStep)&lt;br&gt;
  .then(codegenStep)&lt;br&gt;
  .commit();&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Each step emits &lt;code&gt;step:start&lt;/code&gt;, runs inside &lt;code&gt;withAgentSpan&lt;/code&gt;, and emits &lt;code&gt;step:end&lt;/code&gt; with a preview like &lt;code&gt;3 roles · 22 criteria&lt;/code&gt;, &lt;code&gt;30 test cases&lt;/code&gt;, &lt;code&gt;6 page objects · 30 tests&lt;/code&gt;. The route opens an SSE stream (&lt;code&gt;POST /api/run&lt;/code&gt;), subscribes to those events, and forwards them as &lt;code&gt;event: &amp;lt;type&amp;gt;&lt;/code&gt; / &lt;code&gt;data: &amp;lt;json&amp;gt;&lt;/code&gt;. A &lt;code&gt;: ping&lt;/code&gt; comment every 10 seconds keeps slow Gemma steps alive through proxies. On &lt;code&gt;pipeline:end&lt;/code&gt; the full &lt;code&gt;RunRecord&lt;/code&gt; is persisted.&lt;/p&gt;

&lt;p&gt;I read the installed &lt;code&gt;dist/agent/index.d.ts&lt;/code&gt;, &lt;code&gt;dist/workflows/*.d.ts&lt;/code&gt; and &lt;code&gt;shared.types.d.ts&lt;/code&gt; before writing a line, because the prompt warned that &lt;code&gt;createRun&lt;/code&gt; vs &lt;code&gt;createRunAsync&lt;/code&gt; and &lt;code&gt;usage.inputTokens&lt;/code&gt; vs &lt;code&gt;promptTokens&lt;/code&gt; moved between versions. In 1.77 the call is &lt;code&gt;agent.generate(prompt, { modelSettings: { temperature, maxTokens } })&lt;/code&gt; returning &lt;code&gt;FullOutput&lt;/code&gt; with &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;usage: { inputTokens, outputTokens, totalTokens }&lt;/code&gt;, and the workflow runner is &lt;code&gt;const run = await workflow.createRun(); await run.start({ inputData })&lt;/code&gt;. I normalise both naming schemes so a future Mastra bump does not silently zero the ROI widget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making Gemma reliable on structured output
&lt;/h3&gt;

&lt;p&gt;Gemma through Ollama does not reliably honour JSON-schema tool calling, so each system prompt pastes the TypeScript interface, instructs minified single-line JSON, and ends with "Respond with a single minified JSON on a single line and nothing else. No markdown fences, no commentary." The matrix prompt requests 12–20 test cases with at least 2 per category, and concrete values like "a 65-character password", "quantity 1000", and "a payment $0.01 over the outstanding balance". The codegen prompt generates test body blocks with &lt;code&gt;// covers&lt;/code&gt; comments, paired with deterministic Page Object Model classes.&lt;/p&gt;

&lt;p&gt;The response goes through a small, strict parser:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`ts&lt;br&gt;
// server/services/json.ts&lt;br&gt;
export function extractJson(text: string): unknown {&lt;br&gt;
  let t = text.trim();&lt;br&gt;
  const fence = t.match(/`&lt;/code&gt;(?:json)?\s*([\s\S]&lt;em&gt;?)`&lt;code&gt;&lt;/code&gt;/i);&lt;br&gt;
  if (fence?.[1]) t = fence[1].trim();&lt;br&gt;
  const start = t.indexOf("{");&lt;br&gt;
  const end = t.lastIndexOf("}");&lt;br&gt;
  if (start === -1 || end === -1 || end &amp;lt;= start) {&lt;br&gt;
    throw new Error(&lt;code&gt;No JSON object found in model output (excerpt): ${text.slice(0, 200)}&lt;/code&gt;);&lt;br&gt;
  }&lt;br&gt;
  const slice = t.slice(start, end + 1);&lt;br&gt;
  try {&lt;br&gt;
    return JSON.parse(slice);&lt;br&gt;
  } catch {&lt;br&gt;
    const repaired = slice&lt;br&gt;
      .replace(/[“”]/g, '"')&lt;br&gt;
      .replace(/[‘’]/g, "'")&lt;br&gt;
      .replace(/,\s&lt;/em&gt;([}]])/g, "$1");&lt;br&gt;
    try {&lt;br&gt;
      return JSON.parse(repaired);&lt;br&gt;
    } catch (err) {&lt;br&gt;
      throw new Error(&lt;code&gt;JSON parse failed after repair (excerpt): ${slice.slice(0, 200)} :: ${(err as Error).message}&lt;/code&gt;);&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;zod.safeParse&lt;/code&gt;. On failure the step retries once with a repair prompt containing the zod error; on the second failure it uses the deterministic generator for that step and tags the span &lt;code&gt;fallback&lt;/code&gt;. The run mode becomes &lt;code&gt;mixed&lt;/code&gt; if any step fell back. Nothing ever 500s on the judge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sentry Agent Tracing
&lt;/h3&gt;

&lt;p&gt;I wrap the pipeline in &lt;code&gt;ai.pipeline&lt;/code&gt; and each agent in &lt;code&gt;gen_ai.invoke_agent&lt;/code&gt; wrapping a &lt;code&gt;gen_ai.chat&lt;/code&gt;, because Sentry's AI Agents Insights only lights up from the &lt;code&gt;gen_ai.*&lt;/code&gt; operation names:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`ts&lt;br&gt;
// server/services/telemetry.ts&lt;br&gt;
return await Sentry.startSpan(&lt;br&gt;
  {&lt;br&gt;
    op: "gen_ai.invoke_agent",&lt;br&gt;
    name: `invoke_agent ${agentName}`,&lt;br&gt;
    attributes: {&lt;br&gt;
      "gen_ai.operation.name": "invoke_agent",&lt;br&gt;
      "gen_ai.agent.name": agentName,&lt;br&gt;
      "gen_ai.request.model": model,&lt;br&gt;
      "specshield.step": step,&lt;br&gt;
      "specshield.run_id": runId,&lt;br&gt;
    },&lt;br&gt;
  },&lt;br&gt;
  async (span) =&amp;gt; { /* ... set gen_ai.usage.input_tokens / output_tokens on completion ... */ }&lt;br&gt;
);&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The chat span records &lt;code&gt;gen_ai.provider.name: ollama&lt;/code&gt;, &lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;, &lt;code&gt;gen_ai.input.messages&lt;/code&gt;, &lt;code&gt;specshield.prompt_chars&lt;/code&gt; at start and &lt;code&gt;gen_ai.response.model&lt;/code&gt;, &lt;code&gt;gen_ai.output.messages&lt;/code&gt;, &lt;code&gt;gen_ai.usage.input_tokens / output_tokens / total_tokens&lt;/code&gt; at end. Span context trace/span IDs are copied into the &lt;code&gt;TelemetryStep&lt;/code&gt; so the UI drawer can show them with the DSN empty — the collector is an in-process &lt;code&gt;Map&amp;lt;runId, TelemetryStep[]&amp;gt;&lt;/code&gt;, and &lt;code&gt;Sentry.getActiveSpan()&lt;/code&gt; guards make everything a no-op without init.&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%2Fdtr1a0fr0hz4q7hre49u.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%2Fdtr1a0fr0hz4q7hre49u.png" alt="Sentry trace waterfall: POST /api/run, then the ai.pipeline specShieldWorkflow span, then three invoke_agent spans, each wrapping a chat gemma2:9b span and the HTTP call to Ollama" width="800" height="437"&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%2Fqow8vhu0rzr2a8riv7ak.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%2Fqow8vhu0rzr2a8riv7ak.png" alt="Sentry span attributes for the QA Matrix Architect chat span: gen_ai.usage.input_tokens, output_tokens, request.model" width="800" height="430"&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%2Fppyyw37x3bmz7ls1k49v.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%2Fppyyw37x3bmz7ls1k49v.png" alt="Sentry Agents dashboard: the live run listed with its agents, 4 LLM calls, 7.1K total tokens and $0 total cost" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  What the trace found, and how I debugged it
&lt;/h4&gt;

&lt;p&gt;My first live run looked fine in the UI and was quietly wrong. All three agent cards finished, but every one had an amber Fallback chip. The Ollama server log showed why: each chat completion was cut off at the two-minute mark. My Mac (a 16 GB MacBook Air M4) generates about 13 tokens per second with &lt;code&gt;gemma2:9b&lt;/code&gt;, and the parser alone wanted more output than two minutes allows. The app's 120-second timeout killed the call, the repair retry was killed too, and the step fell back. Every run would have been 100% deterministic output while the header proudly said "Gemma".&lt;/p&gt;

&lt;p&gt;The fix was to spend fewer tokens rather than wait longer. I made every agent return minified single-line JSON, gave each step its own token budget, capped live matrices at 12 to 20 cases, and let Gemma write only the Playwright test bodies while page-object classes come from templates. I also stopped retrying after a timeout, raised the per-call timeout to five minutes, and raised Ollama's context window from 4,096 to 8,192 tokens.&lt;/p&gt;

&lt;p&gt;The Sentry trace above is the run that confirmed it. The whole pipeline took 3.87 minutes with all three agents on Gemma:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Input tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirement Parser&lt;/td&gt;
&lt;td&gt;83.8 s&lt;/td&gt;
&lt;td&gt;1,133&lt;/td&gt;
&lt;td&gt;932&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QA Matrix Architect&lt;/td&gt;
&lt;td&gt;73.1 s&lt;/td&gt;
&lt;td&gt;1,270&lt;/td&gt;
&lt;td&gt;1,016&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright Codegen&lt;/td&gt;
&lt;td&gt;75.4 s&lt;/td&gt;
&lt;td&gt;2,216&lt;/td&gt;
&lt;td&gt;611&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details in the waterfall answered questions I could not answer from logs. First, each &lt;code&gt;http.client&lt;/code&gt; call to Ollama spans the full length of its &lt;code&gt;gen_ai.chat&lt;/code&gt; span, so the time is all model generation, not my app. Second, Sentry computed the generation speed itself: the QA Matrix span reports 13.9 tokens per second. The latency budget is set by hardware, so the only lever that matters is how many tokens I ask for.&lt;/p&gt;

&lt;p&gt;The Agents dashboard adds one more line I did not plan for. Sentry lists 7.1K tokens for the run and a total cost of $0, because there is no per-token price for a model running on my own laptop.&lt;/p&gt;

&lt;p&gt;If a step does fail on Gemma now, it shows up in the trace rather than hiding behind a finished card. A validation failure adds a second &lt;code&gt;gen_ai.chat&lt;/code&gt; span under the same agent for the repair attempt. If that fails too, the agent span is tagged &lt;code&gt;specshield.step_status: fallback&lt;/code&gt; and the error is sent to Sentry, matching the amber chip in the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local persistence and the fallback mode
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;JsonStore&lt;/code&gt; writes &lt;code&gt;./data/runs.json&lt;/code&gt; atomically (temp file + rename), newest first, with &lt;code&gt;GET /api/runs&lt;/code&gt; returning summaries without &lt;code&gt;spec&lt;/code&gt;, &lt;code&gt;combinedCode&lt;/code&gt; and &lt;code&gt;requirements&lt;/code&gt;. &lt;code&gt;GET /api/health&lt;/code&gt; probes &lt;code&gt;${OLLAMA_BASE_URL}/models&lt;/code&gt; with a 1.5 s timeout cached for 30 s and tolerates the &lt;code&gt;:latest&lt;/code&gt; suffix. &lt;code&gt;FORCE_FALLBACK=true&lt;/code&gt; demos fallback even with Ollama running. &lt;code&gt;npm run smoke&lt;/code&gt; boots the app on a random port in fallback, posts the sample spec, and asserts &lt;code&gt;pipeline:end&lt;/code&gt; within 30 s with ≥18 cases, four categories and &lt;code&gt;@playwright/test&lt;/code&gt; in the output. It prints &lt;code&gt;SMOKE: 30 cases [Functional,Boundary,Negative,Security] playwright=ok&lt;/code&gt;.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;100% data privacy and NDA compliance. The spec is POSTed to localhost, sent to &lt;code&gt;http://localhost:11434/v1&lt;/code&gt;, and stored in &lt;code&gt;./data&lt;/code&gt;. With &lt;code&gt;SENTRY_DSN&lt;/code&gt; empty — the default — nothing leaves the machine at all. When Sentry is enabled, only span attributes and optionally &lt;code&gt;gen_ai.input/output.messages&lt;/code&gt; go to Sentry, and &lt;code&gt;SENTRY_SEND_DEFAULT_PII&lt;/code&gt; documents that trade-off in plain language. I can demo this in a client's air-gapped office without a legal review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zero marginal token cost. The live sample run is 6,915 tokens. At closed-API prices that is about five cents a run, roughly $50 per thousand runs. Five cents sounds trivial until you remember the sample is deliberately small: a real client PRD is several times longer, every re-run is billed again, and an agency multiplies it by every client. On Gemma it is $0.00 no matter how often you iterate. That is the difference between a flat $29 seat with predictable margin and a usage-metered tool that quietly punishes iteration. Unlimited re-runs mean the QA engineer actually iterates on the matrix instead of hoarding credits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modularity and control. Swapping &lt;code&gt;gemma2:9b&lt;/code&gt; for &lt;code&gt;gemma3:4b&lt;/code&gt; is one env var. The three Mastra agents are plain TypeScript with prompts in &lt;code&gt;server/mastra/prompts/&lt;/code&gt; — no black-box pipeline. If the invoice tracker gains a new discount or currency rule, I add five lines to the parser prompt and a canned case, not a support ticket. The open-weights path to fine-tuning on past runs is real because the data never left the building.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No vendor lock-in and offline. The studio runs on a train, in a basement test lab, or behind a client's firewall. Vite serves the UI, Hono serves the API and &lt;code&gt;dist/&lt;/code&gt; in production, Ollama serves the model. Kill the network and the only thing that changes is the header chip. That independence is why open innovation compounds: every piece can be replaced, self-hosted, or forked without asking permission.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;a href="https://dev.to/agent_sessions/specshield-qa-one-shot-build-in-opencode-muse-spark-13-dardxr"&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%2F1s3152383x8f7wsc8jws.png" alt="The saved OpenCode session on DEV: my one prompt, the agent reading OPENCODE_PROMPT.md, and 162 of 162 messages" width="799" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/agent_sessions/specshield-qa-one-shot-build-in-opencode-muse-spark-13-dardxr"&gt;Open the full build session on DEV →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the complete one-shot build: one prompt (Part 1 of &lt;a href="https://github.com/AaronStark1/specshield-qa/blob/main/OPENCODE_PROMPT.md" rel="noopener noreferrer"&gt;&lt;code&gt;OPENCODE_PROMPT.md&lt;/code&gt;&lt;/a&gt;) to a passing acceptance checklist, 162 messages and 162 tool calls with OpenCode running Muse Spark 1.3. The follow-up that swapped the sample spec and fixed live Gemma latency (Part 2 of the same file) ran in Antigravity after my OpenCode quota ran out, so it is not in this transcript. Local paths are redacted, and long file contents and tool outputs are truncated.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma — all three agents run &lt;code&gt;gemma2:9b&lt;/code&gt; locally through Ollama's OpenAI-compatible endpoint.&lt;/li&gt;
&lt;li&gt;Mastra — &lt;code&gt;specShieldWorkflow&lt;/code&gt; orchestrates three Mastra agents over an open-weight model.&lt;/li&gt;
&lt;li&gt;Sentry Agent Tracing — &lt;code&gt;gen_ai.invoke_agent&lt;/code&gt; / &lt;code&gt;gen_ai.chat&lt;/code&gt; spans with latency and token usage; screenshots above.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>mastra</category>
    </item>
    <item>
      <title>TrailSnack 🫐 a trail companion that runs Gemma with zero bars, and never says "eat this"</title>
      <dc:creator>Aaron Correya</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:50:52 +0000</pubDate>
      <link>https://dev.to/aaronstark1/trailsnack-a-trail-companion-that-runs-gemma-with-zero-bars-and-never-says-eat-this-pig</link>
      <guid>https://dev.to/aaronstark1/trailsnack-a-trail-companion-that-runs-gemma-with-zero-bars-and-never-says-eat-this-pig</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;DEV Hacktoberfest Week 1 Challenge: 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;TrailSnack&lt;/strong&gt; is an offline-first web app for hikers and trail foragers. You photograph a wild berry, mushroom, or plant. Gemma identifies it, running entirely on your phone. A bundled safety database then hands back a verdict: &lt;strong&gt;EDIBLE · CAUTION · TOXIC · UNKNOWN&lt;/strong&gt;, along with the dangerous look-alikes, a field checklist, the exact rules behind the call, and the poison-control number for your region. All of it works in airplane mode ✈️ after a one-time Wi-Fi setup.&lt;/p&gt;

&lt;p&gt;Its defining feature is restraint. &lt;strong&gt;The app never declares anything safe to eat.&lt;/strong&gt; 🛑 The default answer is "Unidentified, do not ingest." If a death cap is anywhere near the top of the candidate list, the verdict is TOXIC no matter how confident the model is about the chanterelle. Mushrooms can never score higher than CAUTION. The AI advises; a boring, deterministic, fully unit-tested rule engine decides.&lt;/p&gt;

&lt;p&gt;Two Gemma models do two jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Tier 1 "Field ID"&lt;/strong&gt; (EmbeddingGemma 2, about 280 MB) embeds your photo and matches it against 324 hand-written visual descriptions of 81 species by cosine similarity. It is zero-shot retrieval where the vocabulary is exactly the set of species the safety engine knows about, so the model literally cannot name something the safety rules have never heard of.&lt;/li&gt;
&lt;li&gt;🧐 &lt;strong&gt;Tier 2 "Deep Look"&lt;/strong&gt; (Gemma 4 E2B, about 3.3 GB, opt-in, WebGPU only) describes what it sees as constrained JSON against each candidate's checklist: cap colour, gill attachment, stem ring, berry cluster shape. It is forbidden by prompt and by schema from calling anything safe. It can add caution to a verdict. It can never remove it.&lt;/li&gt;
&lt;/ul&gt;

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


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://trailsnack.onrender.com" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;trailsnack.onrender.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Try it in two minutes: open the demo, accept the safety promise, pick a specimen photo on the &lt;strong&gt;Scan&lt;/strong&gt; tab, and watch the verdict badge, look-alike cards, and rule trace appear. Save it to the &lt;strong&gt;Field Log&lt;/strong&gt;, turn on airplane mode, reload. It's still there. 📴 Open &lt;strong&gt;Models&lt;/strong&gt; to see the storage inspector and the Tier 2 capability gate that refuses to download 3.3 GB onto a phone that can't run it.&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%2Ft5pcm740nxlgur5pf8ar.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%2Ft5pcm740nxlgur5pf8ar.png" alt="PokeBerry image upload" width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;📦 &lt;strong&gt;Repository:&lt;/strong&gt; &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/AaronStark1" rel="noopener noreferrer"&gt;
        AaronStark1
      &lt;/a&gt; / &lt;a href="https://github.com/AaronStark1/trailsnack-hacktoberfest-2026" rel="noopener noreferrer"&gt;
        trailsnack-hacktoberfest-2026
      &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;TrailSnack — the offline trail companion that never says "eat this"&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Point your phone at a wild berry, plant, or mushroom with zero bars of signal
TrailSnack identifies it with Gemma AI running entirely on your device, then
checks the result against a bundled safety database whose default answer is
&lt;strong&gt;"Unidentified — do not ingest."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://dev.to" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/6432b0f3decdafb5c985647e62c78290e6db6be6098ea4f156f86669577ddb5b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4861636b746f626572666573742d323032362532305765656b253230312d6461726b677265656e" alt="Hacktoberfest 2026 Week 1"&gt;&lt;/a&gt;
&lt;a href="https://dev.to" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/b2c7d6905693aa57125fa6020622ebdab995caf882e2ea24149eb8b019e12f35/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f426573742532305573652532306f662d47656d6d612d626c7565" alt="Best Use of Gemma"&gt;&lt;/a&gt;
&lt;a href="https://render.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ed240796c15cb2b9fa51769e4158810a2ef382f0ad50da1868ff695a196f0dbd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f426573742532305573652532306f662d52656e6465722d343645334237" alt="Best Use of Render"&gt;&lt;/a&gt;
&lt;a href="https://github.com/AaronStark1/trailsnack-hacktoberfest-2026/./LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ad38111f8af1c691a3c7069e0ceb5122d4c7aa1576554958fe982b7e4d5f78c9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4170616368652d2d322e302d79656c6c6f77" alt="License: Apache-2.0"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;code&gt;https://trailsnack.onrender.com&lt;/code&gt; — &lt;em&gt;the author sets the final URL
after the first Render deploy (see Post-deadline commits below).&lt;/em&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What TrailSnack does and how it gets people outside&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;TrailSnack is an offline-first mobile web app for hikers and trail foragers. On a
remote trail with no cell reception, you photograph a berry cluster, a mushroom
or an unfamiliar leaf. The app embeds your photo with a Gemma model running
locally in the browser, matches it against a bundled reference database of 81
species, and shows a verdict — EDIBLE, CAUTION, TOXIC, or UNKNOWN — alongside…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AaronStark1/trailsnack-hacktoberfest-2026" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Built with Vite 8, React 19, Tailwind 4, Transformers.js 4, and a lot of Web Workers. A few files tell the whole story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/workers/gemma.worker.ts&lt;/code&gt;: the only file that imports the ML library. Both Gemma models load and run here, off the main thread.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/engine/safetyEngine.ts&lt;/code&gt;: the deterministic verdict. Pure function, no I/O, every rule covered by a test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public/data/flora_safety_db.json&lt;/code&gt;: 81 species, 12 universal safety rules, emergency numbers for four regions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/check-offline.mjs&lt;/code&gt;: a Playwright script that builds the app, kills the network, and proves it still works with zero CDN traffic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/AGENT_LOG.md&lt;/code&gt;: the complete build journal from the agent that wrote it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I didn't write this code by hand. 🤖 I wrote a specification, roughly 400 lines describing the product, the safety rules, the architecture, and eight verification gates, and handed it to &lt;strong&gt;OpenCode&lt;/strong&gt;, a free and open-source AI coding agent that runs in your terminal. OpenCode is MIT-licensed, works with whatever model you point it at, and costs nothing to use beyond the model itself. It felt right that an app built on open weights should be built &lt;em&gt;by&lt;/em&gt; open tools too.&lt;/p&gt;

&lt;p&gt;The agent (running Muse Spark 1.3) worked from an empty directory through eight gated phases: environment verification, a scaffold with every dependency pinned and the ONNX Runtime self-hosted, an IndexedDB data layer, the two-tier Gemma worker (verified against a real one-time Tier 1 download in Node), the 81-species database and rule engine, a sunlight-readable UI, then Playwright end-to-end tests, an airplane-mode check, and the Render deployment blueprint. Each phase ended with a gate: type-check, lint, tests, build. No gate, no next phase.&lt;/p&gt;

&lt;p&gt;The safety engine tests read like a field guide's warnings page. Death cap in the ambiguity band forces TOXIC. ☠️ A confident chanterelle with a jack-o'-lantern look-alike holds at CAUTION. A clean, confident blueberry goes EDIBLE. Deep Look can downgrade a verdict and can never upgrade one. If any of those ever flip, the build fails.&lt;/p&gt;

&lt;p&gt;The hardest fight was offline correctness. 🥊 Transformers.js wants to fetch its WASM binaries from a CDN by default; on a ridgeline that's a blank screen. The fix was copying the runtime into the app, pointing the library at it, keeping the service worker's cache and the library's own model cache from fighting each other, and then writing a test that actually cuts the network and checks what loads. Every failure and fix is in &lt;code&gt;docs/AGENT_LOG.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I verified the agent's work with a second, independent review before trusting it, and it found real bugs the test suite couldn't see: a hang on the second scan, a Deep Look decode error, a missing abort. A second OpenCode session fixed all of them the same day. Both stories are in &lt;code&gt;docs/VERIFICATION_REPORT.md&lt;/code&gt; and &lt;code&gt;docs/AGENT_LOG.md&lt;/code&gt;. Autonomous agents are fast. They still need a second pair of eyes. 👀&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;There is no API on a ridgeline. 🏔️&lt;/p&gt;

&lt;p&gt;TrailSnack exists only because every piece of its intelligence is open. Google releases Gemma's weights under Apache-2.0, so the model can live on your phone instead of in a data centre. The onnx-community team on Hugging Face converts those weights to a browser-ready format and shares them freely. Transformers.js, open source, runs them on WebGPU. ONNX Runtime, open source, falls back to WASM when WebGPU isn't there. The agent that built the app is open source. The app is open source.&lt;/p&gt;

&lt;p&gt;That openness is not a nice-to-have here. It is the difference between a tool that works on the trail and one that doesn't. Your specimen photos never leave your phone. 🔒 There is no per-query bill. There is no outage. There is no signal requirement. A closed, cloud-hosted model could be smarter and it would still be useless at the exact moment a hiker needs it.&lt;/p&gt;

&lt;p&gt;Open weights also make the safety story auditable. Anyone can read the species database, read the rule engine, run the tests, and argue with a threshold. That matters when the downside is a hospital visit.&lt;/p&gt;

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

&lt;p&gt;The entire build was one autonomous OpenCode session, free and open-source agent included. The full journal, with every command, every failed gate, and every decision the agent had to make on its own, is &lt;code&gt;docs/AGENT_LOG.md&lt;/code&gt; in the repo, and the design decisions it recorded along the way are in &lt;code&gt;docs/DECISIONS.md&lt;/code&gt;.&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; Gemma runs locally, in the browser, on the hiker's own phone. EmbeddingGemma 2 does zero-shot retrieval over the safety database itself; Gemma 4 E2B does constrained-JSON feature verification. No cloud, no API key, no signal.&lt;/li&gt;
&lt;li&gt;🏅 &lt;strong&gt;Best Use of Render:&lt;/strong&gt; the front end is a Render Static Site deployed from &lt;code&gt;render.yaml&lt;/code&gt;. The Blueprint sets the cross-origin isolation headers that multi-threaded WASM and WebGPU need, immutable caching for the runtime binaries, a service-worker-friendly no-cache rule, SPA rewrites, and pull-request previews. Render serves the app shell; Hugging Face serves the weights; the phone keeps both.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Credits: Google DeepMind for Gemma; Hugging Face and the onnx-community maintainers for Transformers.js and the ONNX conversions; Microsoft for ONNX Runtime; Lucide for icons; Render for hosting; the OpenCode maintainers for a free, open-source agent. The species database was authored by an AI agent from general field-guide knowledge and has not yet been reviewed by a qualified expert. Never eat anything based on an app.&lt;/em&gt; 🌿&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>render</category>
    </item>
  </channel>
</rss>
