IT Leader · AI Builder · US Air Force Veteran

Jason  Darrow

I don't just manage projects. I build them.
At Bank of America I was responsible for a metadata
portal, the system that told the commercial bank what
data it had and where. Now I build that same idea
with local AI, on my own hardware.

See the Projects GitHub → LinkedIn →
Background
Financial Services Platform Delivery
Active Projects
4
Stack
Python · Flutter · Next.js · Claude API · SQLite · Ollama · AWS
Status
Actively Building
Projects

Four projects, built to be used

Everything here I built for myself and actually use. Tap a card to expand the full write-up, the architecture, the screenshots, and the build log.

Where This Came From

At Bank of America I was application manager for a metadata portal used by the commercial side of the bank. It did not hold customer data. It held data about the data. It tracked what types of information each application stored, and where.

That sounds abstract until you watch what it does. Once the bank could see its own data described in one place, two things fell out that nobody could get at before. Where a given kind of data actually lived, across hundreds of systems. And how much of it was being captured more than once. The same customer attribute collected by four applications, none of them aware of the other three.

The education was not the portal itself. It was the idea underneath it. You do not have to touch the data to get control of it. You describe it, and the description becomes the thing you search and reason about. The systems stay where they are.

This project grew from those seeds, pointed at my own mess instead of the bank's.

What I Built

Fifteen years of documents and photos spread across a Mac, with the ordinary version of the same problems. I could not find things. I was afraid to delete anything. I had no idea how many copies of anything I had.

The same answer applies at this scale. Build the metadata layer first, in three steps, each one earning the next.

1. Find what I misplaced. A scanner walks a read-only replica of my files and populates a SQLite database. Enrichment passes describe what is inside them. Search runs keyword, semantic, or the two fused. This is the step that answers "I know I have it somewhere." It is also the step that surfaces duplicates, the same way the bank's portal did. Wire transfers and wedding photos instead of customer attributes.

2. Label it in categories I did not think of first. A local model reads each document and assigns labels from a taxonomy I define. Document type, lifecycle, sensitivity. Nothing leaves the house. The labels become filters in the same search engine, so a category I invent in month three applies to everything scanned in month one. The taxonomy is a plain text file. Changing it is an edit and a re-run, not a migration.

3. See those labels in the file system itself. The Linux box exports a manifest. A job on the Mac reads it every morning and rebuilds a folder tree of symlinks. by-document-type, by-lifecycle, by-sensitivity. The categories become folders I browse in Finder. Not one original file has moved.

The search page. A hybrid keyword and semantic query for Homepage returns twenty hits, each with document type, lifecycle and sensitivity labels. Finder showing the generated symlink tree: by-camera, by-document-type, by-lifecycle, by-person, by-sensitivity, by-type, by-year, duplicates, faces-not-sure, labels-needs-review and needs-ocr.

Left, the search page across 3,045 indexed files. Right, the same database rendered as folders in Finder. Every folder on the right is built from the labels on the left, and every entry in it is a symlink.

How It Fits Together

file-organizer components Three columns. On the left, the inputs: an allowlist of roots, the config file that defines it, a guards module that refuses to run when a safety check fails, and a locally hosted Ollama embedding model. In the middle, the Python engine: a command line entry point, a scanner, a resumable enrichment runner, five enrichment passes for hashing, content typing, EXIF, text extraction and embedding, an Ollama client that refuses oversized input, and a search module combining bm25 keyword search with brute-force cosine similarity. On the right, a single SQLite database holding the file inventory, extracted text, a full-text index, embedding vectors, metadata with provenance, per-pass errors and scan history. IN THE ENGINE · ALL MY PYTHON ON DISK config.yaml The allowlist itself. A local override file adapts it per machine. Allowlisted roots Documents, Pictures, Downloads. Anything not named is out of scope. guards.py Containment, count-drop, empty-root. Refuses to run rather than warning and running. Ollama · nomic-embed-text Runs locally. No API key, and no document leaves the box. cli.py doctor · init · scan · status · enrich · dupes · search scan/walk.py Walks the roots, one row per file. Symlinks are never followed. enrich/ runner Resumable. Selects only work not yet done, commits on an interval, and records a bad file instead of dying on it. hash SHA-256 mime magic bytes exif camera, GPS text into FTS5 embed into vectors The enrichment passes. Each one only touches what it has not already done. ollama.py Refuses any input over 8,000 characters rather than let the server truncate it in silence and return a wrong vector. search.py bm25 keyword · brute-force cosine · rank fusion Fuses on rank rather than score, because bm25 and cosine are not on the same scale and never will be. organizer.db SQLite. One file. files · the inventory file_text · extracted text files_fts · the bm25 index embeddings · 768 floats, one row per chunk meta · facts with provenance enrich_errors · per file, per pass scans · run history What it never does No file is moved, renamed or deleted. Ever. Nothing is uploaded anywhere. The database is disposable. search reads Every arrow into the database is a write the runner makes. A pass returns data and never touches the connection, which is what keeps it testable without a database at all. The whole design is one idea: this is a metadata store with renderers attached, not a file mover. Scanning writes rows about your disk, never to it. Delete the database, re-run, and you are back. Green is the safety layer. Red is the only model in the system, and it runs on this machine.
Inputs on the left, my Python in the middle, one SQLite file on the right. The passes shown are the deterministic ones. Labeling and faces run later, on top of the same runner.

The Decision I Would Defend

The database is the source of truth. Every folder tree is a generated, disposable view.

That one rule makes the hard questions easy. Should this file go in Taxes or Insurance? Both. It is two symlinks. What if I change my mind about the categories? Delete the tree and regenerate it. What if a label is wrong? Fix one row and regenerate.

Every one of those is a crisis in a system that organizes by moving files. In a system that organizes by describing them, it is a non-event. It also means the worst thing that can go wrong here is a bad view, not a lost file.

Python SQLite FTS5 sqlite-vec Ollama nomic-embed-text rsync replica systemd launchd pytest Claude Code

Fifteen years of my own files went through this one. Some of that reached test fixtures and commit messages, so the repo stays closed until I have scrubbed the history. It goes public after that. Happy to walk through the code before then.

1
Foundation, Schema, Safety Guards

The guards went in before any features, because the worst failure available here is silent and unattended. Roots are an allowlist. Every path is re-checked with symlinks resolved before descending, so one stray symlink cannot turn a scoped scan into a full-disk scan. A file count drop over 20 percent aborts before any write. Dry run is the default.

Complete
2
The Crawl

A resumable enrichment framework and the passes that fill it. SHA-256 hashing, content typing by magic bytes, EXIF, perceptual image hashing, text extraction into a full-text index, and text embeddings. Each pass only touches what it has not already done, so a week-long backfill survives being interrupted.

In Progress
3
Search and Review

Keyword search, semantic search, and the two fused on rank rather than score, because bm25 and cosine are not on the same scale. A review page with filters for document type, lifecycle, sensitivity, root, file type and year. This is the point where the project stopped being an inventory.

In Progress
4
Client-Side Views

The box exports a manifest. A job on the Mac rebuilds the symlink tree every morning and keeps the last three generations, so a bad build is one symlink swap away from being undone. A stale manifest or a sudden count drop refuses the build rather than replacing a good tree with a broken one.

Shipped
5
Faces

Detect, embed, cluster, review. The policy matters more than the model here. A person only appears under by-person once I have confirmed them. Everything the clustering merely guessed goes to a separate faces-not-sure folder, so a wrong guess is never presented as a fact.

In Progress
6
Labels From a Local Model

Classification runs on a small model that fits entirely in the GPU. A larger model is the escalation tier for files the small one abstains on, which is how most production AI systems actually work. Low-confidence labels land in a review queue instead of the tree.

In Progress
7
Taxonomy, Tombstoned Deletion, Automation

Deletion is the last thing I will build and the one I trust least, so nothing is ever really deleted. The metadata row outlives the bytes and records where they went. One search box then covers files on disk, files in cold storage, and files already gone.

Planned

What I Built

A full-stack personal weekly planner — built from scratch because nothing I tried kept calendar, habits, and weekly goals in one place without a subscription. This runs on a Linux box in my home office and is accessible from any device on the network.

The app is an Outlook-style weekly organizer with a full time grid, all-day events, recurring events, and per-category color coding. Below the calendar sit three weekly planning cards: a free-text Weekly Summary, a Daily Routine habit tracker with checkboxes across all seven days, and three SMART Goals scoped to the current week. All data persists to a local SQLite database via a Next.js API layer. Theme switches between Light and Graphite. The design spec came out of a conversation with Claude. The build I did in Cursor and Claude Code, reaching for Cursor first because I want to see every change as it lands.

It also writes me a morning brief. Every weekday at 6 AM a cron job reads today's events from Google Calendar, unread mail from Gmail, and three Notion databases: tasks, my networking tracker, and my job tracker.

The design decision I would defend in a review is what the model is not allowed to do. It never chooses the priority. A fixed ladder written in Python ranks the day (a high-priority task outranks a live job stage, which outranks an interview on the calendar, and so on down to the first event of the morning). Only after Python has picked the winner is the model handed that one item and asked to phrase it. It also writes the prep notes and a short narrative paragraph, and it makes one batched pass over unread mail to decide what is worth surfacing. Four narrow calls, every one with a deterministic fallback, so a model that is down degrades the brief instead of breaking it.

The model is llama3.1:8b running on my own hardware through Ollama. There is no API bill, and nothing from my calendar or inbox leaves the network.

GTD Weekly Organizer — week view GTD Weekly Organizer — planning panel

How the Daily Brief Works

AI Weekly Organizer daily brief architecture A weekday cron job runs one Python script that reads Google Calendar, Gmail, and three Notion databases. A narrow regex plus one batched local-model call filters the inbox. A fixed priority ladder written in Python then decides which single item matters most, with no model involved. Only after that is a locally hosted Llama 3.1 model asked to phrase the sentence, write prep notes, and draft a narrative paragraph, each with a deterministic fallback. The result is one JSON file the Next.js planner renders as a Daily Brief card. Everything runs on a self-hosted Linux server with no subscription and no data leaving the home network. MY DATA · READ ONLY GREEN DECIDES · RED ONLY WRITES TRIGGER & APP DATA 1 · Collect data/daily_brief.py — one job, five reads Google Calendar today's events Gmail unread, primary inbox Notion Tasks · Network · Jobs Weekday cron 6:00 AM Eastern 2 · Filter the inbox narrow promo regex, then one batched model call when it is unsure it keeps the mail MODEL 3 · Decide what matters a fixed priority ladder in Python, no model involved HIGH task > live job stage > interview on the calendar > overdue task > urgent email > first event of the day PYTHON ONLY 4 · Write the words the sentence, prep notes, the narrative paragraph canned text if the model is unavailable MODEL 5 · Publish data/daily-brief.json — atomic replace, no history Daily Brief card Next.js planner reads GET /api/brief SQLite · gtd.db calendar, habits, goals ME · read it with the first coffee ALL OF THE ABOVE RUNS ON ONE LINUX BOX IN MY HOME OFFICE Ollama · llama3.1:8b four narrow calls, my hardware, no API Ubuntu · PM2 · cron restarts on reboot, no babysitting No subscription and no calendar or mail leaves the network The model never chooses the priority. Python ranks the day from a fixed ladder, then the model is handed the winner and asked only to phrase it. Every model call has a deterministic fallback, so a dead model degrades the brief instead of breaking it.
Green decides, red only writes. The priority ladder is plain Python; the model is handed the winner and asked to phrase it.
Next.js 16 TypeScript React Tailwind CSS SQLite Drizzle ORM Python 3 Ollama · Llama 3.1 Google Calendar API Gmail API Notion API PM2 cron Ubuntu Cursor Claude Code
View on GitHub →
# Run locally $ npm run dev ✓ Ready in 1305ms → http://localhost:3000 # Deploy to Linux server under PM2 $ rsync -av --exclude node_modules --exclude .next \ gtd-app/ darrowj@homelab:~/gtd-app/ $ ssh darrowj@homelab \ "cd ~/gtd-app && npm install && npm run build && pm2 start npm --name gtd -- start" ✓ App running — accessible from any device on the network → http://homelab:3000
1
Full Calendar UI + SQLite Persistence

Week view and Day view with time grid (6 AM–10 PM), all-day band, recurring events (daily / weekly on chosen days), overlapping event layout, and per-category color coding. Event modal with all-day toggle, repeat weekday chips, reminder settings, and "save this occurrence vs. save all" for recurring edits. Planning panel with Weekly Summary, Daily Routine habit tracker, and SMART Goals — all scoped per week. Light and Graphite theme toggle. All data persists to SQLite via Next.js API routes and Drizzle ORM.

Complete
2
Linux Deploy

Synced to Ubuntu home server, running under PM2 with a startup hook. Accessible from any device on the local network, live and in daily use.

Complete
3
Daily Brief — Calendar, Gmail, Notion, Local LLM

A Python job (data/daily_brief.py) reads today's events from Google Calendar, unread mail from Gmail, and the job and network trackers from Notion, all over OAuth and REST. A deliberately narrow regex hard-drops obvious marketing, then one batched model call sorts the rest, biased to keep when it is unsure. A fixed priority ladder in Python then picks the single most important item, with no model involved. Only then is a locally hosted llama3.1:8b via Ollama asked to phrase that sentence, write prep notes, and draft a narrative paragraph. Four narrow calls in total, each with a deterministic fallback. Output is a single JSON file written by atomic replace and read by the planner at GET /api/brief. Installed on a weekday 6:00 AM cron. If the model is down the card says so, and the planner still works.

Complete
4
Brief Filters

Networking filter (intent detection plus a ±7 day follow-up window) and email noise filter (rule-based promo drop, then an Ollama keep/discard pass) are both live. Job-posting filter polish is the current work.

In Progress
5
Calendar Sync + Reminders

Pull live Google Calendar events into the planner grid itself, not just the brief. Reminder delivery by push and email. Editable routine items.

Planned

What I Built

A job search tool I built for myself — and the first real AI project I have built and actually use. It surfaces relevant listings, gives me the company intelligence I need to decide whether to apply, and then handles the time-consuming parts: selecting the right resume bullets and drafting a cover letter. I stay in the loop at every step.

The pipeline scrapes job boards across multiple titles and locations, then I review each listing and mark the roles I want to pursue. For those roles, the system pulls company background, news, and a Claude-generated match score that compares the job description against my full 59-bullet resume database. I use that score to decide whether to move forward. When I do, Claude selects the best-fit bullets and drafts both a tailored resume and a voice-matched cover letter — both as editable Word documents I review and refine before anything goes out.

The best roles rarely come from a job board. They come from a referral or a company careers page. So the tailoring step takes a role from either source — pulled from the scraper, or typed in by hand — and runs the identical pipeline on both.

Application tracking happens in Notion, not in this pipeline. Every role moves through a tracker I update by hand, status by status, application to offer. Human in the loop, on purpose.

How It Works

AI Job Search System architecture Inputs on the left feed a four-stage pipeline: discovery, intelligence, tailoring, and document generation. Three amber human review steps sit between the stages. Data files and generated documents appear on the right. A shared AI layer of the Claude API, prompt rules, and output guardrails supports stages two through four. INPUTS PIPELINE · AMBER STEPS ARE MINE DATA & OUTPUTS 1 · Discovery job_scraper.py — filter, dedupe YOU · mark roles Interested 2 · Intelligence enrich_jobs.py — company brief + AI match score 0-100 Claude YOU · pick a role 3 · Tailoring resume_tailor.py — select + rewrite Claude 4 · Documents resume + cover letter generators Claude YOU · review, edit, submit Notion tracker Interested → Applied → Interview → Offer JSearch API postings + full descriptions NewsAPI · DuckDuckGo company background Manual entry referrals, careers pages master_resume.json source of truth · 59 bullets job_listings.xlsx canonical + dated archive HTML report company briefs + match badges tailored_COMPANY.json bullets + match score Resume .docx Cover letter .docx SHARED AI LAYER · BEHIND EVERY CLAUDE BADGE ABOVE Claude API bullet selection · match scoring Prompt rules voice · anti-AI · grounded in real bullets Guardrails bullet counts · length · page estimate Every stage runs from the Streamlit dashboard, or standalone from the command line. Claude selects and rewords existing experience — it never invents any, and it never sends anything.
Four stages, three points where I take over. The amber steps are mine.

The Dashboard

Job Search Dashboard, Review tab, showing scraped roles ready to be marked Interested
The Review tab. 177 roles scraped, 13 marked Interested, 3 resumes tailored. Every stage runs from a tab in this panel, with script output streamed live into the page.
Python 3 Claude API Streamlit JSearch API (OpenWeb Ninja) NewsAPI DuckDuckGo python-docx pandas AWS S3 Git
View on GitHub →
# Launch the dashboard — runs the whole pipeline $ streamlit run dashboard.py ✓ Control panel live at localhost:8501 # ...or run each stage from the command line $ python3 job_scraper.py ✓ 42 jobs found · 18 passed filters ✓ Job descriptions captured for each posting ✓ Saved: job_listings.xlsx # Tailor resume to a specific role (JD auto-filled from Excel) $ python3 resume_tailor.py \ --company "Fidelity" \ --title "IT Delivery Manager" \ --description "..." ✓ Match score: 93 / 100 ✓ Bullets selected and summary rewritten # Generate submission-ready Word doc $ python3 resume_generator.py \ --input output/tailored_Fidelity.json ✓ Resume saved: Jason_Darrow_Resume_Fidelity.docx • Length: ~2 page(s) (76/80 lines). # Generate voice-matched cover letter $ python3 cover_letter_generator.py \ --company "Fidelity" \ --title "IT Delivery Manager" \ --description "..." ✓ Cover letter saved: CoverLetter_Fidelity.docx
0
Resume Database

Structured master resume in JSON — 59 bullets tagged by skill, strength scored, ready for AI selection.

Complete
1
Job Scraper

Config-driven job search via JSearch API (OpenWeb Ninja). Captures full job descriptions at scrape time. Filters by salary, recency, job type, and title allowlist. Outputs to dated Excel.

Complete
2
AI Resume Tailor

Claude API reads job description, selects best-fit bullets, rewrites summary, generates Word doc. 93/100 match score.

Complete
2.5
Company Intelligence

Enriches shortlisted roles with company background, recent news, and industry data via DuckDuckGo and NewsAPI. Claude API scores each role against the master resume (0–100 match) and displays the badge in the HTML report.

Complete
3
HTML Job Report

Generates a formatted HTML report of enriched listings with company briefs and one-click resume tailor commands.

Complete
4
Portfolio Site

This site — documenting the project, the journey, and the build. Hosted on AWS S3 with Route 53.

Complete
5
Streamlit Dashboard

Browser control panel for the whole pipeline. Each stage runs from a button with live logs; review and tailor without touching the terminal.

Complete
6
AI Cover Letter Generator

Claude API generates a voice-matched cover letter grounded in the same bullets selected for the tailored resume. Writing rules, anti-AI guardrails, and before/after examples embedded directly in the prompt. Outputs a submission-ready .docx.

Complete
7
Off-Pipeline Roles & Output Guardrails

Tailoring now accepts a role typed in by hand, not just one found by the scraper, and optionally writes it into the tracker so both sources land in one view. Added validation on the model's own output: bullet-count overruns are trimmed, over-length summaries and bullets are reported, and the generator estimates rendered page count and warns before a three-page resume goes out. The page estimate uses wrapped line counts calibrated against real output rather than a PDF conversion, so it adds no dependencies.

Complete

What I Built

A small Android app I built to track my morning cold plunge. It is timer first. Set a duration, log the water and outside temperature, then run a full-screen countdown with an alarm that keeps going until I get out. Afterward I log recovery time and notes. One session per day, on purpose.

It was also how I learned Flutter and mobile development, start to finish. The design came out of a conversation in Claude, a flat modernist look with a single red accent, and I rebuilt it in Flutter to match the mockups closely.

Cold Plunge entry screen with the duration wheel and temperature fields Cold Plunge alarm screen, a full red field with a large stop button Cold Plunge history screen with stats and two trend charts

The Decision I Would Defend

Everything lives on the phone. The data is a local SQLite database, no account and no cloud, so nothing about my routine leaves the device. For a single-user app that extra machinery would not earn its keep. State is a plain Flutter StatefulWidget plus a small repository class, and the data layer is pure Dart with real unit tests behind it.

The one session per day rule is a design choice, not a limit. The point of a cold plunge is showing up, so the app logs one and then shows the completed day until tomorrow.

Flutter Dart SQLite (sqflite) Material audioplayers wakelock_plus vibration google_fonts Android adaptive icon Claude Code
View on GitHub →
1
Design System + Data Layer

Ported the design tokens (color, spacing, type) from the mockups into Dart. Built the SQLite data layer, a Session model, and a repository, all pure Dart with unit tests against an in-memory database.

Complete
2
Entry + Post-Session Screen

The duration wheel, temperature fields, and a degrees F / C toggle. After a plunge the screen shows a completion card and unlocks the recovery log. One session per day, decided on launch by querying today.

Complete
3
Countdown + Alarm

A full-screen countdown with a progress bar that keeps the screen awake. At zero it flips to a red alarm state with looping audio and vibration until you tap stop. The audio and vibration sit behind an interface so the screen stays testable.

Complete
4
History + Trend Charts

A reports page with total sessions, average water temperature, and a day streak, plus two bar charts for duration and water temperature over time. The stat math is a separate, tested pure function.

Complete
5
App Icon + Packaging

A custom launcher icon, a penguin in a cold plunge tub, drawn in code and wired in as an Android adaptive icon at every density.

Complete
Background

About Me

I taught myself to code, starting with C-shell scripting, awk and sed, then Perl, while working as an RF technician at CellularOne. That turned into a full stack developer role at Click2Learn.com, building eLearning software for Fortune 1000 clients. From there I became a web application architect at Bank of America, building systems responsible for billions in managed assets. I also ran the metadata portal for the commercial side of the bank, the system that tracked what data every application held and where it lived. That one is still shaping what I build. Later I moved into IT Delivery Manager at Voya Financial, overseeing programs from $250K to $3M.

Instead of just sending resumes right away, I decided to build the tool I wished existed, an AI system that finds relevant jobs, tailors my resume to each one, and tracks my progress. That became the first of several projects I'm actively building, including a weekly planner app I run my own schedule on today. This site documents all of them.

I'm learning AI by doing, not watching. Every week something gets shipped or written up here. The results have been genuinely surprising.

🎓
Education

MS Information Systems — Bentley College (Distinction)
BS MIS — Northeastern University (Magna Cum Laude)

📋
PMP · CSM Certified

Project Management Professional
Certified ScrumMaster

🛩️
US Air Force Veteran

Senior Airman
Honorable Discharge

🥋
BJJ Black Belt

Brazilian Jiu-Jitsu —
the hardest thing I've ever done

Get In Touch

Let's Talk

I'm currently open to IT Delivery Manager, Program Manager, and AI-Enabled Delivery roles in the Greater Boston area and remote.

If you're building something interesting with AI, or know someone who needs a delivery leader who can actually build — I'd love to hear from you.