Photo-first, privacy-conscious bowel journal with sovereign vision and user-confirmed health logging.
Go to file
Timmy 1aadca91c6
All checks were successful
Quality gates / quality (pull_request) Successful in 1m59s
fix: escalate ordinary fever and pain phrasing without allowlist narrowing
Hostile verification of the fever continuation-word allowlist found ordinary
symptom false negatives at the authoritative zero-Hermes-call boundary:
'I have a fever right now', 'my fever is 103', 'fever started this morning',
plus 'severe pain in/around the abdomen' against the hard-coded 'my' in the
pain pattern.

- fever: drop the continuation-word allowlist entirely; the bare clinical
  word escalates with any continuation. Nonclinical controls become
  structured exclusions (malaria/yellow/dengue/cabin compounds, fever tree,
  figurative 'feverish about'/'feverish with excitement'), so future natural
  symptom wording can never be narrowed again.
- severePain: accept any determiner or none plus location qualifiers
  (lower/upper/left/right) between preposition and organ.
- blood: differential audit against origin/main caught the same regression
  class in the PR's rewrite: 'poop/stool with blood' (escalated on main)
  was lost while 'my stool has been normal' / 'the poop contains seeds'
  became false positives. Restored explicit blood shapes with bounded verbs.

RED regressions first: matrix grows 56 -> 82 pinned positives (fever +20,
severePain +6 with case/punctuation/contraction variants), a pinned
preserved-behavior list for main's blood shapes, and two boundary suites
proving every review phrase and variant escalates at the detector AND
intercepts chat at the service layer with exactly zero Hermes calls.

Gates: npm test 90/90, staging-deploy 20/20 OK, test:ui/test:photo/
test:sleek pass, check:syntax clean, check:diff clean, npm audit 0
vulnerabilities, diff secret scan clean.
2026-08-22 21:46:11 +00:00
.gitea/workflows feat: add hardened atomic staging deployment 2026-08-21 14:49:28 +00:00
artifacts fix: recognize past-tense vomiting in chat safety 2026-08-20 17:14:20 +00:00
assets feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
deploy feat: add hardened atomic staging deployment 2026-08-21 14:49:28 +00:00
docs feat: add hardened atomic staging deployment 2026-08-21 14:49:28 +00:00
research feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
scripts feat: add hardened atomic staging deployment 2026-08-21 14:49:28 +00:00
src fix: escalate ordinary fever and pain phrasing without allowlist narrowing 2026-08-22 21:46:11 +00:00
tests fix: escalate ordinary fever and pain phrasing without allowlist narrowing 2026-08-22 21:46:11 +00:00
video feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
.gitignore feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
AI-EVIDENCE.md feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
app.js fix: make staging compatible with strict CSP 2026-08-21 15:01:05 +00:00
citations.json feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
index.html feat: simplify Timmy and add secure Hermes chat 2026-08-20 16:21:00 +00:00
manifest.webmanifest feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
package-lock.json feat: ship photo-first Timmy prototype and sovereign vision spike 2026-08-19 01:54:29 +00:00
package.json test: expand deterministic symptom escalation regression suite 2026-08-22 20:57:04 +00:00
PRODUCT.md feat: simplify Timmy and add secure Hermes chat 2026-08-20 16:21:00 +00:00
README.md feat: add hardened atomic staging deployment 2026-08-21 14:49:28 +00:00
requirements-test.txt ci: install pinned image test dependency 2026-08-19 08:14:43 +00:00
ROADMAP.md docs: add Gitea roadmap and epic map 2026-08-19 02:00:40 +00:00
server.mjs fix: make staging compatible with strict CSP 2026-08-21 15:01:05 +00:00
service-worker.js feat: add private subpage staging slice 2026-08-21 14:12:17 +00:00
styles.css fix: make staging compatible with strict CSP 2026-08-21 15:01:05 +00:00

Timmy the Talking Turd

A working mobile-first bowel diary with an optional photo-first AI assist: “Your intelligent pooping pal.”

Build a gated review release

python3 scripts/build_release.py

The builder clones the committed main tree into an isolated directory, runs unit/security and both mobile acceptance suites, audits dependencies, checks syntax and secrets, excludes model weights and sensitive/generated media, records a vertical feature demonstration from the working app, fully decodes and probes that MP4, and writes checksummed source/video artifacts plus manifest.json under /root/timmy-releases/. It refuses dirty or non-main source trees.

Promote or roll back private staging

The standard-library deployment tool verifies the declared archive SHA-256 before tar parsing, rejects unsafe or secret-bearing members, creates one immutable releases/<40-character-commit> directory, and atomically repoints current. Restart, bounded commit-specific health, or synthetic mobile smoke failures restore and verify the prior release automatically.

python3 scripts/deploy_staging.py --dry-run promote --tag TAG --archive FILE --sha256 HASH --commit FULL_COMMIT
sudo python3 scripts/deploy_staging.py promote --tag TAG --archive FILE --sha256 HASH --commit FULL_COMMIT
python3 scripts/deploy_staging.py status
sudo python3 scripts/deploy_staging.py rollback --commit PRIOR_FULL_COMMIT

Commands are fixed argv arrays executed with shell=False; JSON argv overrides and --root exist for rootless fixtures. See the staging runbook for host prerequisites, mode-600 environment injection, Caddy validation, smoke credentials, logs, backups, removal, and the explicit no-live-change boundary.

Run with the self-hosted open-weight path

npm install

# Installs the exact llama.cpp commit and verifies both GGUF SHA-256 receipts.
scripts/bootstrap_selfhost_smolvlm.sh install
scripts/bootstrap_selfhost_smolvlm.sh start
scripts/bootstrap_selfhost_smolvlm.sh health

# Run Timmy in another terminal.
TIMMY_VISION_PROFILE=selfhost npm start
# open http://localhost:4173

# When finished:
scripts/bootstrap_selfhost_smolvlm.sh stop

The bootstrap defaults to ${XDG_DATA_HOME:-$HOME/.local/share}/timmy-selfhost, prints immutable source/model receipts with receipt, and binds only to 127.0.0.1:8080. Downloads are rejected unless both pinned SHA-256 hashes match, and model weights remain outside Git. Set TIMMY_SELFHOST_ROOT to choose another private data directory; override host, port, threads, or build jobs with the documented TIMMY_MODEL_*/TIMMY_BUILD_JOBS environment variables.

The selected bootstrap model is SmolVLM2-2.2B-Instruct using the official Apache-2.0 GGUF conversion. It is a provisional labeling worker, not a diagnostic or clinically validated classifier.

Run with a hosted provider

hermes proxy start --provider nous --host 127.0.0.1 --port 8645
npm start

Hosted remains the default compatibility profile. To make it explicit, set TIMMY_VISION_PROFILE=hosted. Credentials stay server-side:

TIMMY_VISION_BASE_URL=https://your-openai-compatible-provider/v1 \
TIMMY_VISION_API_KEY=... \
TIMMY_VISION_MODEL=your-vision-model \
npm start

Set TIMMY_VISION_ENABLED=0 to disable uploads and retain manual-only operation.

Enable the full Hermes Agent chat

Hermes chat is disabled by default because a tool-capable agent can spend the authority of its server-side profile. Timmy requires an exact public origin, a strong operator access code, and an absolute dedicated workspace before the browser route exists.

mkdir -m 700 -p /root/timmy-agent-workspace
install -m 600 docs/TIMMY-AGENT-POLICY.md /root/timmy-agent-workspace/AGENTS.md
hermes profile create timmyapp --description "Private Timmy chat agent"

TIMMY_AGENT_ENABLED=true \
TIMMY_AGENT_ACCESS_TOKEN="$(openssl rand -hex 24)" \
TIMMY_PUBLIC_ORIGIN=http://127.0.0.1:4173 \
TIMMY_AGENT_WORKDIR=/root/timmy-agent-workspace \
TIMMY_HERMES_COMMAND=/root/.local/bin/timmyapp \
npm start

Authenticate/configure the dedicated timmyapp profile before use. Do not reuse a broad personal profile in a public deployment. The browser receives only an opaque HttpOnly, SameSite session cookie; it never receives provider credentials, Hermes session IDs, model/provider controls, or tool policy. Each conversation is resumed server-side with one in-flight turn, a 4,000-character message cap, a 20-entry text-only ledger context, fixed origin checks, rate and timeout limits, and sanitized errors. Photos are excluded from chat by construction. The applications deterministic urgent-symptom warning runs before Hermes.

For an HTTPS deployment, set TIMMY_PUBLIC_ORIGIN to the exact HTTPS origin so the session cookie is marked Secure. Put the Node service behind authenticated TLS and give the dedicated profile only the host/service authority the intended user should be able to exercise.

Verify

npm test
npm run test:ui       # server required on port 4173
npm run test:photo    # mocked positive suggestion through the real browser UX
npm run test:sleek   # simplified 390x844 shell + free-text Hermes chat
npm audit --audit-level=high

What works

  • Photo-first camera/file capture
  • Client-side image compression before transfer
  • Explicit consent before one-time provider analysis
  • Structured AI suggestions for visible Bristol form and color only
  • Confidence display, low-confidence abstention, and user confirmation
  • Urgency, discomfort, notes, and symptoms remain strictly user-reported
  • Manual logging that never uploads
  • Sleek three-destination mobile shell with one dominant photo action
  • Free-text smart chat with real server-side Hermes session continuity and local fallback
  • Authenticated, same-origin, bounded browser-to-agent gateway with no browser-side credentials
  • Red-flag symptom escalation
  • Local browser ledger, calendar, pattern summary, JSON portability, and delete-all
  • Installable/offline PWA shell for the manual and saved-ledger paths

What is deliberately not faked

The positive photo-prefill interface is covered by a deterministic provider fixture. A live self-hosted SmolVLM2-2.2B model was exercised against a CC-licensed real Type 4 stool photograph. It accepted the content and identified stool, but predicted the wrong type with low confidence; Timmy correctly abstained. The open model removes provider moderation from the path, but its Bristol accuracy is not clinically established.

Timmy does not diagnose disease, identify bleeding, infer pain/urgency, recommend treatment, or clear foods. The selected VLM is a bootstrap worker, not the final classifier. See research/SELF-HOSTED-STOOL-VISION.md for the measured receipts, hardware gate, consented-data pipeline, specialist-classifier plan, and sources.

Architecture

Browser PWA
├── app.js                    sleek photo-first UX, local ledger, free-text chat
├── src/domain.js             tested health/safety and summary rules
├── src/analysis.js           strict AI schema, validation, visual-only merge
├── server.mjs                static server + bounded vision and agent routes
├── src/hermes-agent-service.js  authenticated session-bound Hermes CLI adapter
├── src/vision-service.js     server-side OpenAI-compatible provider adapter
├── src/vision-config.js      hosted/self-hosted profiles and readiness probe
├── scripts/bootstrap_selfhost_smolvlm.sh  pinned install/start/health/stop lifecycle
├── scripts/run_selfhost_smolvlm.sh        custom-path foreground launcher
├── scripts/ingest_training_photo.py
├── localStorage              saved ledger and attached photos
└── llama.cpp / hosted API    selected per server-side profile

The server accepts JPEG/PNG/WebP data URLs up to 4 MB, uses a bounded 60-second hosted or 120-second self-hosted timeout, never accepts an API key from browser input, does not write inference images to disk, and returns only validated visual suggestions. The separate dataset ingester runs only for explicit, consented contributions.