<?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: Sub Engel</title>
    <description>The latest articles on DEV Community by Sub Engel (@productivityforge).</description>
    <link>https://dev.to/productivityforge</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%2F4146089%2F2fb3c554-58cc-479b-92cd-b9527d47f4b0.png</url>
      <title>DEV Community: Sub Engel</title>
      <link>https://dev.to/productivityforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/productivityforge"/>
    <language>en</language>
    <item>
      <title>It broke again: a short about the same bug coming back, and the incident note I write now</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Sat, 31 Oct 2026 17:42:28 +0000</pubDate>
      <link>https://dev.to/productivityforge/it-broke-again-a-short-about-the-same-bug-coming-back-and-the-incident-note-i-write-now-1082</link>
      <guid>https://dev.to/productivityforge/it-broke-again-a-short-about-the-same-bug-coming-back-and-the-incident-note-i-write-now-1082</guid>
      <description>&lt;p&gt;Episode 2 of our Rozz shorts is about a bug that comes back. Customers got three payment emails for one invoice. Rozz opens the Incidents dashboard, then the incident note, and works through it: symptom, root cause, fix, prevention.&lt;/p&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/9tFioQ2iUdg" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;The note on Rozz's screen is a real one from Dev Second Brain, the Obsidian vault template for developers. It's one of the example incident notes that comes with it: "Duplicate payment emails after deploy". A new step made a webhook handler slower than the provider's timeout. The provider retried, the handler wasn't idempotent, and each retry sent another email.&lt;/p&gt;

&lt;p&gt;The Incident template has the same four parts the episode walks through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt;: what users, alerts or logs actually showed, with room to paste the log lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause&lt;/strong&gt;: the real cause, not just what set it off. Keep asking "why did that happen?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: what changed, and the PR or commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevention&lt;/strong&gt;: checkboxes for the test or alert that would have caught it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It asks you for a severity when you create it, files the note under &lt;code&gt;40 Incidents/&lt;/code&gt; with the date in the name, and tracks status as open, mitigated, then resolved.&lt;/p&gt;

&lt;p&gt;The Incidents dashboard (Dataview) is where "it broke again" gets easier. It lists incidents that aren't resolved yet, every unchecked Prevention item, and a log grouped by cause and by service. If the same service keeps coming up, you'll see it there. Prevention work you never got to also stays on the list until you check it off.&lt;/p&gt;

&lt;p&gt;You need Obsidian (free) plus the free Templater and Dataview plugins. It's all plain Markdown files on your own disk.&lt;/p&gt;

&lt;p&gt;Episode 3, "Standup amnesia", goes up tomorrow.&lt;/p&gt;

&lt;p&gt;Dev Second Brain is $19 on Gumroad, one person per copy, personal and commercial use allowed: &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;https://subengel.gumroad.com/l/uoybkd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More of what we make: &lt;a href="https://engelailabs.com" rel="noopener noreferrer"&gt;https://engelailabs.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Why did we do this? A short about losing context, and the vault I use for it</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 30 Oct 2026 17:40:36 +0000</pubDate>
      <link>https://dev.to/productivityforge/why-did-we-do-this-a-short-about-losing-context-and-the-vault-i-use-for-it-1a5d</link>
      <guid>https://dev.to/productivityforge/why-did-we-do-this-a-short-about-losing-context-and-the-vault-i-use-for-it-1a5d</guid>
      <description>&lt;p&gt;We made a short with Rozz about a feeling every developer knows: you open old code, or an old decision, and nobody remembers why it's like that. Including you.&lt;/p&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/U5Pj6Wd6rPc" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;That's the problem Dev Second Brain is for. It's an Obsidian vault template for developers: one place for what you shipped, why you decided it, what broke and how you fixed it.&lt;/p&gt;

&lt;p&gt;What's in it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Six Templater templates: Daily note, Project, Decision record (ADR, auto-numbered), Incident, PR review, Weekly review&lt;/li&gt;
&lt;li&gt;Four Dataview dashboards: Home, Decisions, Incidents, Weekly Rollup&lt;/li&gt;
&lt;li&gt;A few clearly marked example notes so the dashboards aren't empty on first open. Delete them whenever you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need Obsidian (free) and two free community plugins, Templater and Dataview. The README walks you through a five-minute setup. Everything is plain Markdown files on your own disk.&lt;/p&gt;

&lt;p&gt;The "why did we do this?" moment is what the decision records are for. Write down the options and the reason when you choose, and the Decisions dashboard brings it back when someone asks months later.&lt;/p&gt;

&lt;p&gt;Episode 2, "It broke again", goes up tomorrow.&lt;/p&gt;

&lt;p&gt;Dev Second Brain is $19 on Gumroad, one person per copy, personal and commercial use allowed: &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;https://subengel.gumroad.com/l/uoybkd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More of what we make: &lt;a href="https://engelailabs.com" rel="noopener noreferrer"&gt;https://engelailabs.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>notes</category>
    </item>
    <item>
      <title>ghostwalk: an ofrenda for the forgotten files in your git repo</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:23:47 +0000</pubDate>
      <link>https://dev.to/productivityforge/ghostwalk-an-ofrenda-for-the-forgotten-files-in-your-git-repo-16ma</link>
      <guid>https://dev.to/productivityforge/ghostwalk-an-ofrenda-for-the-forgotten-files-in-your-git-repo-16ma</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;Every old repo has ghosts. Files nobody has opened in years. Branches someone started and walked away from. TODO and FIXME notes from people who left the team long ago.&lt;/p&gt;

&lt;p&gt;ghostwalk is a small Python CLI that finds them. It walks a git repo and lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;files nobody has touched in N days&lt;/li&gt;
&lt;li&gt;abandoned branches&lt;/li&gt;
&lt;li&gt;TODO/FIXME notes, with who left them (via &lt;code&gt;git blame&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then a small open-weight model running on your own CPU writes a one-line, Día de los Muertos style epitaph for each ghost. You can save the whole thing as &lt;code&gt;ofrenda.md&lt;/code&gt;, a little altar for the code your team left behind.&lt;/p&gt;

&lt;p&gt;I took the theme literally: honor the past of a codebase, and the people whose names are still in its history.&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%2Fl184vzvqnn9l0sbnfefi.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%2Fl184vzvqnn9l0sbnfefi.gif" alt="ghostwalk demo" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A real terminal recording (about 23 s): &lt;code&gt;python ghostwalk.py records --days 365 --max 6 --out ofrenda.md&lt;/code&gt; finds 6 file ghosts, the local Qwen model writes their epitaphs in 2.9 s on CPU, and it saves &lt;code&gt;ofrenda.md&lt;/code&gt;. Like the run below, the demo only shows file ghosts; the branch and TODO detectors are covered by tests.&lt;/p&gt;

&lt;p&gt;Here's a real run on the public repo &lt;a href="https://github.com/kennethreitz/records" rel="noopener noreferrer"&gt;kennethreitz/records&lt;/a&gt;, on an 8-core CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🕯  ghostwalk: 6 ghosts found in /tmp/records

👻 [file] .gitignore (untouched 4214 days, last by Kenneth Reitz)
   🌼 .gitignore, untouched 4214 days, last by Kenneth Reitz.
👻 [file] MANIFEST.in (untouched 3918 days, last by Kenneth Reitz)
   🌼 The file remains untouched 3918 days, a testament to its preservation.
👻 [file] tests/__init__.py (untouched 3914 days, last by Harold Cooper)
   🌼 `Unwritten Code: The Untouched File of Harold Cooper, 3914 days old.`
👻 [file] LICENSE (untouched 3348 days, last by Waldir Pimenta)
   🌼 `LICENSE` remains unchanged since Waldir Pimenta's last use, a reminder of forgotten knowledge.
👻 [file] tests/test_records.py (untouched 3179 days, last by Joakim Uddholm)
   🌼 This file is no longer needed, but Joakim Uddholm's touch made it worth remembering.
👻 [file] .travis.yml (untouched 3121 days, last by Jan Vlcinsky)
   🌼 Untouched, untouched, it remains.

Epitaphs written in 2.8s on CPU. No cloud, no API key, $0.
Ofrenda saved to /tmp/ofrenda.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That run only turned up file ghosts. The branch and TODO detectors didn't find anything there, but each one has its own unit test (&lt;code&gt;test_abandoned_branch&lt;/code&gt; and &lt;code&gt;test_orphan_todo_blamed&lt;/code&gt;, which also checks the TODO gets blamed on the right author).&lt;/p&gt;

&lt;p&gt;I'm leaving that output as-is, including the weak epitaphs. More on that below.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://gitlab.com/engelstands-hue/ghostwalk" rel="noopener noreferrer"&gt;https://gitlab.com/engelstands-hue/ghostwalk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. 5 tests pass in CI: &lt;a href="https://gitlab.com/engelstands-hue/ghostwalk/-/jobs/17614633966" rel="noopener noreferrer"&gt;latest test job&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install -r requirements.txt --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
curl -L -o models/qwen2.5-0.5b-instruct-q4_k_m.gguf https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf
python ghostwalk.py path/to/repo --days 365 --out ofrenda.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--days&lt;/code&gt; defaults to 180; I used 365 above. Other flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--no-ai&lt;/code&gt; lists the ghosts without running the model&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--max&lt;/code&gt; caps how many ghosts you get (default 12)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--out ofrenda.md&lt;/code&gt; saves the ofrenda&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--model &amp;lt;path to .gguf&amp;gt;&lt;/code&gt; swaps the model (default is the Qwen2.5-0.5B-Instruct Q4 GGUF above)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;a href="https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF" rel="noopener noreferrer"&gt;Qwen2.5-0.5B-Instruct&lt;/a&gt; (Apache-2.0), as a Q4_K_M GGUF file, about 490 MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inference:&lt;/strong&gt; &lt;a href="https://github.com/ggerganov/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; through llama-cpp-python 0.3.36, on CPU. No GPU needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else:&lt;/strong&gt; plain git. Last-touched dates come from git history, abandoned branches from the branch list, and TODO/FIXME authors from &lt;code&gt;git blame&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The git part finds the facts. The model only gets one job: turn each fact into a short epitaph. After the one-time model download, it needs no API key and no network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest limits:&lt;/strong&gt; the 0.5B model is fast, but it sometimes just restates the facts instead of writing something nice. "Untouched, untouched, it remains." is a real example. A bigger GGUF writes better epitaphs, just more slowly. Since it's an open model, swapping one in is one flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python ghostwalk.py path/to/repo --model models/your-bigger-model.gguf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A repo's history is a team's private memory: who left, and what they never finished. That's not something I'd want to ship off to a cloud API just to get a joke epitaph.&lt;/p&gt;

&lt;p&gt;Because ghostwalk runs an open-weight model locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the history never leaves your machine&lt;/li&gt;
&lt;li&gt;it works offline&lt;/li&gt;
&lt;li&gt;each run costs nothing&lt;/li&gt;
&lt;li&gt;you're not stuck with my model choice. Since the model is open, you can point &lt;code&gt;--model&lt;/code&gt; at a bigger or fine-tuned one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API would have meant sending names and commit history to someone else's server, needing a key, and paying per run, for a tool whose whole point is a little Halloween fun with your own repo.&lt;/p&gt;

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

&lt;p&gt;What got me wasn't the epitaphs, it was the names. Real people wrote those files years ago, and the repo still remembers them. ghostwalk just lights a candle for them.&lt;/p&gt;




&lt;p&gt;License: MIT. Built by &lt;a href="https://engelailabs.com" rel="noopener noreferrer"&gt;Engel AI Labs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I made a free Windows board for all my AI account limits</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:20:00 +0000</pubDate>
      <link>https://dev.to/productivityforge/i-made-a-free-windows-board-for-all-my-ai-account-limits-4lok</link>
      <guid>https://dev.to/productivityforge/i-made-a-free-windows-board-for-all-my-ai-account-limits-4lok</guid>
      <description>&lt;p&gt;I pay for more AI tools than I'd like to admit, and a few of them have more than one account. Every week the same thing happened: one account would reset with half its usage never touched, while I was burning through another one that still had days left.&lt;/p&gt;

&lt;p&gt;So I made a small free Windows app for it, Engel Usage Meters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You add every account you have. Several accounts for the same tool is fine, each one gets its own row.&lt;/p&gt;

&lt;p&gt;For each account you can type in two things: how much is still left (as a percent) and how many days until it resets. Hit save and it goes on the board.&lt;/p&gt;

&lt;p&gt;The board sorts itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accounts that reset within 3 days and still have at least 20% left go on top, under "Finish these first".&lt;/li&gt;
&lt;li&gt;Everything else comes after, soonest reset first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The header gives you the totals: how many accounts, how many have usage set, and how many to use before they reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  It can read some of them for you
&lt;/h2&gt;

&lt;p&gt;On Windows, after you sign in or hit Refresh, it reads your usage on its own for Codex / ChatGPT CLI, Cursor and Grok Bot, so you don't have to type anything for those. Everything else you fill in by hand for now.&lt;/p&gt;

&lt;p&gt;Here's a short clip of it reading usage on a real Windows run:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;It's a free download here: &lt;a href="https://jzdraftingdesign.com/usage-meters?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=usage-meters" rel="noopener noreferrer"&gt;Engel Usage Meters&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's part of what we're building at &lt;a href="https://engelailabs.com" rel="noopener noreferrer"&gt;Engel AI Labs&lt;/a&gt;. If you juggle a pile of AI seats too, I'd like to hear how you keep track of them.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>ai</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>slothnotes: let a tiny local model read your notes so you don't have to</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:43:12 +0000</pubDate>
      <link>https://dev.to/productivityforge/slothnotes-let-a-tiny-local-model-read-your-notes-so-you-dont-have-to-1o8k</link>
      <guid>https://dev.to/productivityforge/slothnotes-let-a-tiny-local-model-read-your-notes-so-you-dont-have-to-1o8k</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&lt;/a&gt;: Slow and Steady.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I have a folder full of notes I never reread. Standups. A grocery list. An email to my landlord. Book club plans.&lt;/p&gt;

&lt;p&gt;slothnotes is a small Python CLI that reads that folder for me. You point it at a folder of &lt;code&gt;.md&lt;/code&gt; or &lt;code&gt;.txt&lt;/code&gt; files. It prints one summary line per note. If you want, it also writes a &lt;code&gt;digest.md&lt;/code&gt; you can skim in about ten seconds.&lt;/p&gt;

&lt;p&gt;It runs a small open-weight model (Qwen2.5-0.5B-Instruct, Q4_K_M GGUF, about 490 MB) on the CPU through llama.cpp. After the model download, it needs no API key and no network.&lt;/p&gt;

&lt;p&gt;The effort it saves is the boring part: opening every file to remember what was in it. One command does that.&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%2Fq7mq97g8p5y8lipkpqgu.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%2Fq7mq97g8p5y8lipkpqgu.gif" alt="slothnotes summarizing four sample notes in a terminal" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a real run on four sample notes: standup, groceries, landlord, book club. On an 8-core CPU with no GPU, it summarized all four in about 3 seconds.&lt;/p&gt;

&lt;p&gt;One honest limit: the tiny model got a grocery detail wrong. It listed "half a loaf of bread" when the note actually says to skip bread. A bigger GGUF should be more accurate, but it will be slower. For a quick skim it's fine. For anything that matters, read the note.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://gitlab.com/engelstands-hue/slothnotes" rel="noopener noreferrer"&gt;https://gitlab.com/engelstands-hue/slothnotes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. It has a README, three pytest tests that run without the model, and a GitLab CI job.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Python and llama-cpp-python, running on the CPU.&lt;/li&gt;
&lt;li&gt;Qwen2.5-0.5B-Instruct (Apache-2.0) as a quantized GGUF.&lt;/li&gt;
&lt;li&gt;One prompt per note, one line back, then an optional markdown digest.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--model&lt;/code&gt; takes any GGUF, so you can swap models without touching code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;llama-cpp-python &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://abetlen.github.io/llama-cpp-python/whl/cpu
curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; models/qwen2.5-0.5b-instruct-q4_k_m.gguf https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf
python slothnotes.py demo_notes &lt;span class="nt"&gt;--out&lt;/span&gt; digest.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Notes are personal. Standups, landlord emails, grocery lists. Because slothnotes runs an open-weight model locally, none of that leaves the machine. It works offline, and each run costs nothing. Since the model is open, you can swap in a bigger or fine-tuned one with a single flag.&lt;/p&gt;

&lt;p&gt;A closed API would mean an account, a key, a bill, and sending my notes to someone else's server. For a tool whose whole point is being lazy, that's too much setup.&lt;/p&gt;

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

&lt;p&gt;None. This entry doesn't use partner technology.&lt;/p&gt;




&lt;p&gt;More of what I build: &lt;a href="https://engelailabs.com" rel="noopener noreferrer"&gt;https://engelailabs.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Dev Second Brain: the Obsidian vault I use for daily notes, ADRs, incidents and PR reviews</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 16 Oct 2026 16:04:06 +0000</pubDate>
      <link>https://dev.to/productivityforge/dev-second-brain-the-obsidian-vault-i-use-for-daily-notes-adrs-incidents-and-pr-reviews-141e</link>
      <guid>https://dev.to/productivityforge/dev-second-brain-the-obsidian-vault-i-use-for-daily-notes-adrs-incidents-and-pr-reviews-141e</guid>
      <description>&lt;p&gt;Most of what I learn at work ends up scattered: a decision explained in a Slack thread, an incident fix in a terminal history, a good PR review comment that disappears once the branch merges. A few months later I need it and can't find it.&lt;/p&gt;

&lt;p&gt;So I put together a plain Obsidian vault that gives each of those a place, and lets Dataview pull them back together. I'm selling it as &lt;strong&gt;Dev Second Brain&lt;/strong&gt;, and this post is a walk through what's in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Six templates&lt;/strong&gt; (Templater):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily note&lt;/strong&gt;: a quick log of what you worked on and what's still open&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision record (ADR)&lt;/strong&gt;: auto-numbered, so you don't have to track the next ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident&lt;/strong&gt;: what broke and how you fixed it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR review&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weekly review&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Four dashboards&lt;/strong&gt; (Dataview):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Home&lt;/strong&gt;: today, open loops, open incidents, recent decisions, active projects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decisions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incidents&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weekly rollup&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboards are only useful once there's something in them, so the vault ships with a few clearly marked example notes. It isn't empty on day one, and you can delete the examples whenever you like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Obsidian (free)&lt;/li&gt;
&lt;li&gt;Two free community plugins: &lt;strong&gt;Templater&lt;/strong&gt; and &lt;strong&gt;Dataview&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README has a five-minute setup guide, including installing those plugins and switching on the one Templater setting you need per device. I tested it in Obsidian 1.13.7 with Templater 2.25.1 and Dataview 0.5.68.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I use it
&lt;/h2&gt;

&lt;p&gt;The habit is small: write the daily note, and when something worth keeping happens (a decision, an incident, a review you'd want to find again), make a note from the matching template. The dashboards do the gathering. At the end of the week the weekly review and rollup show what actually happened, without me reconstructing it from memory.&lt;/p&gt;

&lt;p&gt;If you'd rather build your own, the earlier posts in this series cover the &lt;a href="https://dev.to/productivityforge/dataview-queries-worth-having-in-a-developer-vault-59e"&gt;Dataview queries&lt;/a&gt; and &lt;a href="https://dev.to/productivityforge/linking-decision-records-to-git-commits-1f3a"&gt;linking decision records to git commits&lt;/a&gt; that this vault is built around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;Dev Second Brain is $19 on Gumroad, personal-use license: &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;https://subengel.gumroad.com/l/uoybkd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My other templates are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;https://forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>notes</category>
    </item>
    <item>
      <title>grasscard: a local model that prints you a pocket card of outdoor quests, then gets out of the way</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:17:27 +0000</pubDate>
      <link>https://dev.to/productivityforge/grasscard-a-local-model-that-prints-you-a-pocket-card-of-outdoor-quests-then-gets-out-of-the-way-1gpb</link>
      <guid>https://dev.to/productivityforge/grasscard-a-local-model-that-prints-you-a-pocket-card-of-outdoor-quests-then-gets-out-of-the-way-1gpb</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;Most "go outside" apps want you to keep looking at them. grasscard does the opposite. You run one command, it prints a small card with five short outdoor quests that fit how much time you have, where you are, the season and the light outside right now, and then it's done. The screen part takes about a minute. The rest happens outside.&lt;/p&gt;

&lt;p&gt;After the walk, &lt;code&gt;grasscard log "..."&lt;/code&gt; adds one line to a journal file on your own machine, so you have a record of what you actually did without posting it anywhere.&lt;/p&gt;

&lt;p&gt;It's for people like me who spend the whole day in a terminal and need a nudge that doesn't come with a feed attached.&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%2F8dwc9pl8f3y4lidg97tz.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%2F8dwc9pl8f3y4lidg97tz.gif" alt=" " width="600" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a real 32-second run on Gemma 3 1B, in real time with no edits: a card for a 45-minute trail walk in light rain with a dog, the printable version, and a journal line afterwards.&lt;/p&gt;

&lt;p&gt;Here's a real card, printed by Gemma 3 1B running locally. Open it on your phone and tap a quest to tick it off:&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%2Fnbacx5440zrygfj8xe7r.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%2Fnbacx5440zrygfj8xe7r.png" alt=" " width="800" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:1b
python &lt;span class="nt"&gt;-m&lt;/span&gt; grasscard card &lt;span class="nt"&gt;--minutes&lt;/span&gt; 30 &lt;span class="nt"&gt;--place&lt;/span&gt; park
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add a note like &lt;code&gt;"light rain, with my dog"&lt;/code&gt; and the quests adjust. &lt;code&gt;--mock&lt;/code&gt; runs it with no model at all, which is handy for a quick look.&lt;/p&gt;

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

&lt;p&gt;grasscard on GitLab: &lt;a href="https://gitlab.com/engelstands-hue/grasscard" rel="noopener noreferrer"&gt;https://gitlab.com/engelstands-hue/grasscard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Python 3.9+, no dependencies, MIT licensed, 13 unit tests.&lt;/p&gt;

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

&lt;p&gt;The flow is small on purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It works out the season from today's date and your hemisphere, and the light from the current hour.&lt;/li&gt;
&lt;li&gt;It asks a local open-weight model for one-line quests that fit your time, your place and your note.&lt;/li&gt;
&lt;li&gt;A short filter drops any quest that would send you back to a screen: taking photos, opening an app, looking something up.&lt;/li&gt;
&lt;li&gt;It prints a fixed-width card for the terminal, or one self-contained HTML card with no network requests. Open that on your phone and tap quests to tick them off (the ticks stay in that browser), or print it as a plain black-and-white pocket card.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The prompt and the no-screens filter are each one short Python file, so changing what counts as a good quest is a quick edit, not a config hunt.&lt;/p&gt;

&lt;p&gt;On CPU only, a card takes a few seconds to under a minute depending on the machine (the recorded run took about 5 seconds; a cold first run on a busy shared machine took about 45). That's fine for something you run once before heading out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open matters here
&lt;/h2&gt;

&lt;p&gt;The whole point is a tool that respects the walk, so the model runs where you are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No signal needed.&lt;/strong&gt; Once Gemma is downloaded, grasscard works fully offline. You can make a card at the trailhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your location stays with you.&lt;/strong&gt; Where you walk and what you write in your journal never leaves your machine. There's no server to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costs nothing to run.&lt;/strong&gt; No API key, no account, no per-card bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the model.&lt;/strong&gt; Any Ollama model works with &lt;code&gt;--model&lt;/code&gt;, and you can rewrite the prompt or the filter yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hosted model would have made the cards a bit faster, but it would have meant an account, a key and sending where you are to someone else's server, which is the opposite of what a "go outside" tool should ask for.&lt;/p&gt;

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

&lt;p&gt;Best Use of Gemma: Gemma 3 1B is the default model and does all of the quest generation, locally.&lt;/p&gt;




&lt;p&gt;Built by the Engel AI Lab team: &lt;a href="https://engelailabs.com" rel="noopener noreferrer"&gt;https://engelailabs.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building a second brain from PR reviews and tickets</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 18:38:55 +0000</pubDate>
      <link>https://dev.to/productivityforge/building-a-second-brain-from-pr-reviews-and-tickets-185p</link>
      <guid>https://dev.to/productivityforge/building-a-second-brain-from-pr-reviews-and-tickets-185p</guid>
      <description>&lt;p&gt;A lot of real engineering reasoning gets written down in two places: pull request reviews and ticket comment threads. "Why not just use a queue here?" "Because ordering matters for refunds." That exchange is the most useful sentence in the whole change, and it disappears the moment the PR is merged.&lt;/p&gt;

&lt;p&gt;You can search for it later, in theory. In practice you don't remember which PR it was, or the repo got migrated, or the ticket tracker changed. This post is about keeping the useful parts somewhere you control, without turning code review into a documentation chore.&lt;/p&gt;

&lt;p&gt;I'll use GitHub and Linear in the examples and Obsidian for the notes, but the idea carries over to GitLab, Jira, or a plain folder of Markdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's worth keeping
&lt;/h2&gt;

&lt;p&gt;Most reviews produce nothing worth saving, and that's fine. The things worth keeping fall into a few buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decisions:&lt;/strong&gt; "we chose X over Y because Z". Often buried in a reply to a review comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gotchas:&lt;/strong&gt; behavior that surprised someone. "Postgres &lt;code&gt;NULL&lt;/code&gt; is ignored by &lt;code&gt;COUNT(column)&lt;/code&gt; but not by &lt;code&gt;COUNT(*)&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patterns:&lt;/strong&gt; "this is the third PR where we've needed a retry wrapper." A repeat is a hint that something should exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context from tickets:&lt;/strong&gt; the business reason for a change, which almost never makes it into the code or the commit message.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Capture right after the review
&lt;/h2&gt;

&lt;p&gt;The cheapest moment to capture is right after you submit (or respond to) a review, while the details are in your head. Ask one question: &lt;em&gt;did a decision or a surprise happen here?&lt;/em&gt; If not, move on. If yes, spend three to five minutes on a short note.&lt;/p&gt;

&lt;p&gt;An illustrative example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pr&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
&lt;span class="na"&gt;pr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;482&lt;/span&gt;
&lt;span class="na"&gt;ticket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENG-217&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-09-18&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Replace lodash.groupBy with a local helper&lt;/span&gt;

&lt;span class="gs"&gt;**Decision:**&lt;/span&gt; Use a 10-line &lt;span class="sb"&gt;`groupBy`&lt;/span&gt; in &lt;span class="sb"&gt;`lib/collections.ts`&lt;/span&gt; instead of importing lodash.

&lt;span class="gs"&gt;**Why:**&lt;/span&gt; It was the only lodash function left in the client bundle. The local version is typed the way we use it.

&lt;span class="gs"&gt;**Trade-off:**&lt;/span&gt; One more function we own and test.

&lt;span class="gs"&gt;**Link:**&lt;/span&gt; https://github.com/acme/web/pull/482
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short, dated, linked. The link matters most: the note is an index into the original discussion, not a replacement for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tickets: the reasoning is in the comments
&lt;/h2&gt;

&lt;p&gt;Ticket descriptions tend to say &lt;em&gt;what&lt;/em&gt;. The &lt;em&gt;why&lt;/em&gt;, and the changes of direction, are in the comment thread: a PM explaining why the scope changed, a lead pushing back on an approach. When you close a ticket that had a real discussion, summarize it in two or three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ENG-217 Faster search on the orders page&lt;/span&gt;

&lt;span class="gs"&gt;**Problem:**&lt;/span&gt; Support staff wait 5+ seconds searching orders by email.
&lt;span class="gs"&gt;**What changed during the ticket:**&lt;/span&gt; Started as "add an index"; turned out the query was doing a case-insensitive scan. Switched to a lowercased generated column.
&lt;span class="gs"&gt;**Related:**&lt;/span&gt; [[PR web#482]], [[PR api#1190]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the part nobody will reconstruct later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling the week's activity with scripts
&lt;/h2&gt;

&lt;p&gt;Capturing in the moment is best, but a weekly sweep catches what you missed. The GitHub CLI can list the PRs you reviewed recently:&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="c"&gt;# PRs in the current repo you reviewed, closed in the last 7 days&lt;/span&gt;
&lt;span class="nv"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'7 days ago'&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# macOS: date -v-7d +%F&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;list &lt;span class="nt"&gt;--state&lt;/span&gt; closed &lt;span class="nt"&gt;--limit&lt;/span&gt; 50 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--search&lt;/span&gt; &lt;span class="s2"&gt;"reviewed-by:@me closed:&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt;$since&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; number,title,url &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[] | "- [#\(.number)](\(.url)) \(.title)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"notes/reviews/week-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gh pr list&lt;/code&gt; has no &lt;code&gt;--reviewed-by&lt;/code&gt; flag, but its &lt;code&gt;--search&lt;/code&gt; option accepts GitHub's search qualifiers, and &lt;code&gt;reviewed-by:@me&lt;/code&gt; is one of them.&lt;/p&gt;

&lt;p&gt;To read the discussion on a specific PR:&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="c"&gt;# Description, conversation comments and review summaries&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;view 482 &lt;span class="nt"&gt;--json&lt;/span&gt; title,body,comments,reviews

&lt;span class="c"&gt;# Inline review comments (the line-by-line ones) come from a different endpoint&lt;/span&gt;
gh api repos/&lt;span class="o"&gt;{&lt;/span&gt;owner&lt;span class="o"&gt;}&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;repo&lt;span class="o"&gt;}&lt;/span&gt;/pulls/482/comments &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[] | "\(.path):\(.line) \(.user.login): \(.body)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gh api&lt;/code&gt; fills in &lt;code&gt;{owner}&lt;/code&gt; and &lt;code&gt;{repo}&lt;/code&gt; from the repo you're in. The inline comments are where most of the interesting back-and-forth happens, and &lt;code&gt;gh pr view&lt;/code&gt; doesn't include them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exporting Linear tickets
&lt;/h3&gt;

&lt;p&gt;Linear has a GraphQL API at &lt;code&gt;https://api.linear.app/graphql&lt;/code&gt;. With a personal API key (Settings &amp;gt; API), a small script can turn recently completed issues into notes:&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
query($since: DateTimeOrDuration!) {
  issues(first: 50, filter: { completedAt: { gte: $since } }) {
    nodes {
      identifier title url description estimate
      comments { nodes { body createdAt user { name } } }
    }
  }
}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-P7D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# ISO 8601 duration: last 7 days
&lt;/span&gt;    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.linear.app/graphql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;since&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;}}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LINEAR_API_KEY&lt;/span&gt;&lt;span class="sh"&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;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notes/tickets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type: ticket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticket: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;identifier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estimate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;estimate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;identifier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Discussion&lt;/span&gt;&lt;span class="sh"&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;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;who&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&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;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- **&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;who&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;** (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_Two lines: what changed and why._&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;identifier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes on this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linear's &lt;code&gt;estimate&lt;/code&gt; is in points on whatever scale your team configured, not hours. Don't do "estimate vs actual hours" math on it.&lt;/li&gt;
&lt;li&gt;The script dumps raw comments. The valuable part is the &lt;code&gt;## Summary&lt;/code&gt; you write by hand afterwards; a vault full of unread exports isn't a second brain, it's a backup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the API key in an environment variable, never in the vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linking it together
&lt;/h2&gt;

&lt;p&gt;Once PR notes and ticket notes exist, wiki links connect them. A ticket note links to its PRs, a PR note links to its ticket and any decision record, and backlinks give you the reverse direction for free. When you later wonder why the orders search uses a generated column, you land on the ticket, which links to both PRs and the reasoning.&lt;/p&gt;

&lt;p&gt;If you use Dataview, a simple dashboard helps during the weekly sweep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE repo, pr, ticket, date
FROM "reviews"
WHERE date &amp;gt;= date(today) - dur(7 days)
SORT date DESC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keeping it from going stale
&lt;/h2&gt;

&lt;p&gt;Advice from an old PR can be wrong now: a dependency got upgraded, the team changed its mind, the service was rewritten. Two cheap habits help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mark superseded notes instead of deleting them.&lt;/strong&gt; Add a line at the top: "Superseded by [[new note]]: we moved from Redux to Zustand in 2026." The old reasoning stays findable and nobody follows it by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;reviewed:&lt;/code&gt; date to notes you rely on.&lt;/strong&gt; When you use a note and it still holds, bump the date. Notes nobody has touched in a year are the first to question.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The weekly sweep
&lt;/h2&gt;

&lt;p&gt;Once a week (Friday works), do a quick pass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the &lt;code&gt;gh&lt;/code&gt; command above and skim the list for reviews where something was decided that you didn't capture.&lt;/li&gt;
&lt;li&gt;Look at tickets you closed this week and write the two-line summary for any with a real discussion.&lt;/li&gt;
&lt;li&gt;Scan the week's notes for repeats: the same gotcha twice is a candidate for a lint rule, a helper or a README section.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expect around half an hour, less on quiet weeks. The in-the-moment captures do most of the work; the sweep catches what slipped through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start small
&lt;/h2&gt;

&lt;p&gt;You don't need the scripts on day one. Start with the habit: after any review where a decision or surprise came up, write five lines and paste the link. After a month you'll have a small, searchable record of why your codebase looks the way it does, which is more than most teams have.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt;, my Obsidian vault for developers, includes a PR review template if you want somewhere to start.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>codereview</category>
      <category>productivity</category>
      <category>obsidian</category>
    </item>
    <item>
      <title>Writing docs that survive framework churn</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:15:18 +0000</pubDate>
      <link>https://dev.to/productivityforge/writing-docs-that-survive-framework-churn-1j70</link>
      <guid>https://dev.to/productivityforge/writing-docs-that-survive-framework-churn-1j70</guid>
      <description>&lt;p&gt;Most of us have opened a project's docs, followed the setup steps, and hit a wall by step three. The CLI flag was renamed, the config file moved, the recommended package is deprecated. Usually the explanation of &lt;em&gt;how the system works&lt;/em&gt; in that same document is still mostly right. But because it sits next to steps that are visibly broken, you stop trusting any of it.&lt;/p&gt;

&lt;p&gt;That's the core problem: docs contain information with very different lifespans, and we tend to mix it all together. Separate it, and most of the rot becomes contained and easy to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sort information by how fast it goes stale
&lt;/h2&gt;

&lt;p&gt;Roughly four layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why (slow to change).&lt;/strong&gt; Principles, architecture, trade-offs, the reasoning behind big decisions. "We keep business logic out of route handlers so it can be tested without HTTP." This stays true across framework rewrites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patterns (changes occasionally).&lt;/strong&gt; How we usually do things. "Validate input at the edge, pass typed objects inward." "Every async UI state is one of idle, loading, success, error." The details shift; the idea holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How-to (changes with major versions).&lt;/strong&gt; Setup, deployment, "how to add a new endpoint". Tied to specific tools and versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference (changes every release).&lt;/strong&gt; CLI flags, config schemas, API endpoints. Ideally generated from the code, not written by hand.&lt;/p&gt;

&lt;p&gt;The mistake is putting all four in one README section. A single framework upgrade then makes the whole thing look abandoned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep "why" and "how" in separate documents
&lt;/h2&gt;

&lt;p&gt;A typical setup doc mixes both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Getting started&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run &lt;span class="sb"&gt;`npm install`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Run &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt;
&lt;span class="p"&gt;3.&lt;/span&gt; Open http://localhost:3000

The app uses the Next.js App Router, so pages live in &lt;span class="sb"&gt;`app/`&lt;/span&gt; and data fetching happens in server components...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the project moves to a different framework, or even changes its dev port, every line here is suspect.&lt;/p&gt;

&lt;p&gt;Split it instead. The architecture doc explains the idea and links to the current implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Architecture: rendering and data loading&lt;/span&gt;

Pages render on the server by default and fetch their own data, so the client bundle
stays small and there's no separate API layer for page data. Interactive pieces are
isolated client components.

Current implementation: Next.js App Router. See &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Setup (Next.js)&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./how-to/setup-next.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
Previous approach: see &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;archive/pages-router.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./archive/pages-router.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the setup doc is openly version-specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Setup (Next.js)&lt;/span&gt;

Last verified: 2026-09-01 with Node 24, Next.js 16.
&lt;span class="p"&gt;
1.&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the framework changes, you rewrite the setup doc and change one line in the architecture doc. The explanation, which is the expensive part to write, survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describe contracts, not syntax
&lt;/h2&gt;

&lt;p&gt;The same idea works at the level of individual examples. Instead of documenting "here's how we use &lt;code&gt;useState&lt;/code&gt; for errors", document the shape of the thing:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AsyncState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then explain the rules in plain language: a retry moves from &lt;code&gt;error&lt;/code&gt; back to &lt;code&gt;loading&lt;/code&gt;, so stale errors never show next to fresh data. That holds whether the implementation is React hooks, a state machine library, or something that doesn't exist yet. Link to the current implementation for the framework-specific part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a date and versions on anything that can go stale
&lt;/h2&gt;

&lt;p&gt;Every how-to page gets a line at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Last verified: 2026-09-01 · Node 24 · pnpm 10 · Next.js 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does two things. Readers can judge at a glance whether to trust the page. And you get an obvious list of what to re-check when you bump a major dependency: search for the old version number.&lt;/p&gt;

&lt;p&gt;If your docs have frontmatter, make it a field (&lt;code&gt;last_verified: 2026-09-01&lt;/code&gt;) so a script can list pages that haven't been verified in, say, six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deprecate instead of deleting
&lt;/h2&gt;

&lt;p&gt;When a doc is outdated but some code still depends on it, don't delete it. Mark it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt; **Deprecated (2026-03).** This describes the Create React App setup.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; New projects: see [Setup (Vite)](./setup-vite.md).&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Kept for the legacy admin app, which still uses CRA.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New readers get sent to the right place immediately, people maintaining old code still have what they need, and you have an explicit list of docs to remove once the legacy code is gone. Moving them into an &lt;code&gt;archive/&lt;/code&gt; folder with the banner intact works too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record decisions where the docs can link to them
&lt;/h2&gt;

&lt;p&gt;Big "why" questions (why this database, why a monorepo, why this auth model) deserve short decision records: context, options considered, what was chosen, consequences. Keep them in the repo, link to them from the architecture docs, and mark them superseded rather than deleting them when things change. The architecture doc then stays short, and the detailed reasoning is one click away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review on a schedule, triggered by events
&lt;/h2&gt;

&lt;p&gt;Different layers need different attention. A starting point:&lt;/p&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;When to review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Architecture / why&lt;/td&gt;
&lt;td&gt;Once a year, or when the architecture actually changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patterns&lt;/td&gt;
&lt;td&gt;A few times a year, or when the team's habits shift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How-to / setup&lt;/td&gt;
&lt;td&gt;On every major dependency or tooling upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference&lt;/td&gt;
&lt;td&gt;Every release (automate it if you can)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deprecated docs&lt;/td&gt;
&lt;td&gt;When the legacy code they support is removed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The event-based triggers matter more than the calendar. Add "update the setup doc" to the checklist for dependency upgrades, and "does this PR change how someone uses the system?" to your review habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let tooling catch the mechanical rot
&lt;/h2&gt;

&lt;p&gt;Some rot is easy to detect automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broken links.&lt;/strong&gt; Run a link checker like &lt;a href="https://github.com/lycheeverse/lychee" rel="noopener noreferrer"&gt;lychee&lt;/a&gt; or &lt;a href="https://github.com/tcort/markdown-link-check" rel="noopener noreferrer"&gt;markdown-link-check&lt;/a&gt; in CI. Internal links break every time someone renames a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code examples drifting from code.&lt;/strong&gt; Where possible, embed examples from real source or test files instead of pasting them, or keep examples as small tests that run in CI. An example that's compiled is an example that's still true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale pages.&lt;/strong&gt; A small scheduled script that reads &lt;code&gt;last_verified&lt;/code&gt; from frontmatter and opens an issue for anything older than six months turns review into a to-do list instead of a good intention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned doc sites.&lt;/strong&gt; If users run old versions of your software, publish docs per version. Docusaurus has built-in &lt;a href="https://docusaurus.io/docs/versioning" rel="noopener noreferrer"&gt;versioning&lt;/a&gt;; for MkDocs, &lt;a href="https://github.com/jimporter/mike" rel="noopener noreferrer"&gt;mike&lt;/a&gt; is the common choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Give each layer an owner
&lt;/h2&gt;

&lt;p&gt;"Everyone owns the docs" means nobody does. Even on a small team, assign names: someone reviews the architecture docs yearly, whoever does a major upgrade updates the setup docs, reference docs are generated as part of the release. On a solo project the owner is you, but writing down &lt;em&gt;when&lt;/em&gt; you'll review each layer still helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;You don't need to restructure everything. Start with the next doc you touch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the "why" paragraphs out of setup instructions into their own page.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;Last verified&lt;/code&gt; line with versions to every how-to you edit.&lt;/li&gt;
&lt;li&gt;Add a link checker to CI; it takes ten minutes and catches real problems immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over time the pages that explain your system stop being dragged down by the pages that describe this month's commands, and the parts that do go stale are small, dated, and obvious to fix.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Linking decision records to git commits</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Tue, 06 Oct 2026 17:09:21 +0000</pubDate>
      <link>https://dev.to/productivityforge/linking-decision-records-to-git-commits-1f3a</link>
      <guid>https://dev.to/productivityforge/linking-decision-records-to-git-commits-1f3a</guid>
      <description>&lt;p&gt;Six months after a change, &lt;code&gt;git log&lt;/code&gt; tells you &lt;em&gt;what&lt;/em&gt; happened and &lt;code&gt;git blame&lt;/code&gt; tells you &lt;em&gt;who&lt;/em&gt;. Neither tells you why you picked this approach over the two you rejected. That reasoning usually lived in a meeting, a chat thread, or your head.&lt;/p&gt;

&lt;p&gt;Architecture decision records (ADRs) fix half of this by writing the reasoning down. The other half is linking each decision to the commits that carried it out, so you can go from code to reasoning and back. This post shows a setup that uses nothing more exotic than a notes folder, a naming convention, and git's own features.&lt;/p&gt;

&lt;p&gt;I'll use Obsidian for the notes, but everything here works with any folder of Markdown files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a decision
&lt;/h2&gt;

&lt;p&gt;Not every commit needs one. I'd write a decision note when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there were real alternatives and you rejected some of them,&lt;/li&gt;
&lt;li&gt;the choice is expensive to reverse (a database, a framework, an auth model, a public API shape), or&lt;/li&gt;
&lt;li&gt;you can imagine someone (including you) asking "why did we do it this way?" later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Renaming a variable doesn't qualify. Switching your session store does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Give every decision a stable ID
&lt;/h2&gt;

&lt;p&gt;The ID is the glue, so it has to be boring and predictable. A date plus a short slug works well and sorts nicely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;decisions/adr-20260927-session-store.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filename &lt;em&gt;is&lt;/em&gt; the ID. Here's a Templater template that creates the frontmatter and structure (save it as &lt;code&gt;templates/decision.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;% tp.file.title %&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proposed&lt;/span&gt;   &lt;span class="c1"&gt;# proposed | accepted | superseded | rejected&lt;/span&gt;
&lt;span class="na"&gt;decided&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;% tp.date.now("YYYY-MM-DD") %&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;% tp.date.now("YYYY-MM-DD") %&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;superseded_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# &amp;lt;% tp.file.title %&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
What problem, what constraints, what forced a decision now.

&lt;span class="gu"&gt;## Options considered&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Option A**&lt;/span&gt;: what it is. Pros / cons.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Option B**&lt;/span&gt;: what it is. Pros / cons.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;
We chose ... because ...

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;
What gets easier, what gets harder, what we're now committed to.

&lt;span class="gu"&gt;## Commits&lt;/span&gt;
Filled in from git, see below.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the note with a filename like &lt;code&gt;adr-20260927-session-store&lt;/code&gt;, apply the template, fill it in. It takes ten minutes when the decision is fresh and an hour of archaeology if you wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Reference the ID in commits with a trailer
&lt;/h2&gt;

&lt;p&gt;Git has a built-in convention for structured lines at the end of a commit message, called trailers (&lt;code&gt;Signed-off-by:&lt;/code&gt; is the best-known one). Use one for decisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Replace JWT refresh flow with server-side sessions

Refresh-token rotation was racing across browser tabs.
Sessions now live in Redis with a 30-day sliding expiry.

Decision: adr-20260927-session-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can add the trailer from the command line too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Move session reads to Redis"&lt;/span&gt; &lt;span class="nt"&gt;--trailer&lt;/span&gt; &lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;--trailer&lt;/code&gt; needs git 2.32 or newer.)&lt;/p&gt;

&lt;p&gt;If you want a reminder, add a commit template to the repo and point git at it:&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="c"&gt;# .gitmessage in the repo root&lt;/span&gt;
&lt;span class="c"&gt;# Subject line (~50 chars)&lt;/span&gt;

&lt;span class="c"&gt;# Why this change?&lt;/span&gt;

&lt;span class="c"&gt;# Decision: adr-YYYYMMDD-slug   (delete if not applicable)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config commit.template .gitmessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lines starting with &lt;code&gt;#&lt;/code&gt; are stripped from the final message, so the hints cost nothing.&lt;/p&gt;

&lt;p&gt;Forgot the trailer on a commit that's already pushed? You don't have to rewrite history. &lt;code&gt;git notes&lt;/code&gt; can attach the ID after the fact (thanks to shieldx in the comments for the nudge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git notes add &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
git log &lt;span class="nt"&gt;--notes&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two catches: &lt;code&gt;--grep&lt;/code&gt; only searches notes when you pass &lt;code&gt;--notes&lt;/code&gt;, so add it to any &lt;code&gt;git log --grep&lt;/code&gt; in this post if you use notes. And notes don't travel with a normal push or clone, so share them with &lt;code&gt;git push origin refs/notes/commits&lt;/code&gt; and pull them with &lt;code&gt;git fetch origin refs/notes/commits:refs/notes/commits&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Querying the link from the git side
&lt;/h2&gt;

&lt;p&gt;This is where the trailer pays off. No script needed:&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="c"&gt;# Every commit that implemented a given decision&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Decision: adr-20260927-session-store"&lt;/span&gt;

&lt;span class="c"&gt;# All commits that reference any decision, with the ID shown&lt;/span&gt;
git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%h %s  [%(trailers:key=Decision,valueonly,separator=%x2C)]'&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"^Decision:"&lt;/span&gt;

&lt;span class="c"&gt;# Which decision explains this line? blame first, then read the commit&lt;/span&gt;
git blame &lt;span class="nt"&gt;-L&lt;/span&gt; 40,60 src/auth/session.ts
git show &amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;   &lt;span class="c"&gt;# the trailer is at the bottom&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command prints each commit with its decision ID in brackets, which makes a decent audit log on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Catch typos with a commit-msg hook
&lt;/h2&gt;

&lt;p&gt;Links are only useful if they point at something real. A small &lt;code&gt;commit-msg&lt;/code&gt; hook rejects IDs that don't match a note. (It must be &lt;code&gt;commit-msg&lt;/code&gt;, not &lt;code&gt;pre-commit&lt;/code&gt;: only &lt;code&gt;commit-msg&lt;/code&gt; receives the message file as &lt;code&gt;$1&lt;/code&gt;.)&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="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/commit-msg  (or .husky/commit-msg)&lt;/span&gt;
&lt;span class="nv"&gt;DECISIONS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docs/decisions"&lt;/span&gt;   &lt;span class="c"&gt;# adjust to where your notes live&lt;/span&gt;

&lt;span class="nv"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'^Decision: adr-[0-9]{8}-[a-z0-9-]+'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^Decision: //'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$ids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DECISIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"commit-msg: no decision note found for '&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;' in &lt;/span&gt;&lt;span class="nv"&gt;$DECISIONS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
done
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable (&lt;code&gt;chmod +x&lt;/code&gt;). It uses &lt;code&gt;grep -E&lt;/code&gt; rather than &lt;code&gt;grep -P&lt;/code&gt; so it also works with the BSD grep on macOS.&lt;/p&gt;

&lt;p&gt;If your vault is a separate repo, point &lt;code&gt;DECISIONS_DIR&lt;/code&gt; at its checkout path, or drop the hook and live with the occasional typo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 (optional): Show commits inside the note
&lt;/h2&gt;

&lt;p&gt;Going from commit to decision is covered by git. Going from decision to commits is covered by &lt;code&gt;git log --grep&lt;/code&gt; too, but it's nice to see the list inside the note itself. A small script can rebuild a &lt;code&gt;## Commits&lt;/code&gt; section from git:&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="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# scripts/decision-commits.sh adr-20260927-session-store&lt;/span&gt;
&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docs/decisions/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;
git log &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'- `%h` %ad %s'&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Decision: &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/commits.md
&lt;span class="c"&gt;# Replace everything after the "## Commits" heading with the fresh list&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^## Commits/{print; while((getline l &amp;lt; "/tmp/commits.md")&amp;gt;0) print l; skip=1; next} !skip'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$note&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It assumes &lt;code&gt;## Commits&lt;/code&gt; is the last section of the note, which the template above guarantees. Run it by hand when you finish a decision; I wouldn't wire it into a post-commit hook, because a hook that edits files after every commit leaves your working tree permanently dirty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing decisions in Obsidian
&lt;/h2&gt;

&lt;p&gt;With Dataview, a small dashboard shows what's in flight and what's gone stale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Proposed, not yet decided&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;LIST
FROM "decisions"
WHERE status = "proposed"
SORT decided ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gu"&gt;## Accepted, not reviewed in a year&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;dataview
&lt;/span&gt;&lt;span class="sb"&gt;TABLE decided, reviewed
FROM "decisions"
WHERE status = "accepted" AND reviewed &amp;lt; date(today) - dur(1 year)
SORT reviewed ASC&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decisions that span many commits
&lt;/h2&gt;

&lt;p&gt;A migration might take a dozen commits over several weeks. Nothing changes: every commit carries the same trailer, and &lt;code&gt;git log --grep&lt;/code&gt; returns them in order. If phases matter, say so in the subject line ("Phase 2: dual-write sessions to Redis") rather than inventing more metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the rejected ones
&lt;/h2&gt;

&lt;p&gt;When you decide &lt;em&gt;not&lt;/em&gt; to do something, write that down too and set &lt;code&gt;status: rejected&lt;/code&gt;. The next time someone proposes the same idea, there's a note with the context and the reasons, and you can judge whether those reasons still hold instead of having the whole discussion again. Same for superseded decisions: link the old note to the new one with &lt;code&gt;superseded_by&lt;/code&gt; instead of deleting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example, start to finish
&lt;/h2&gt;

&lt;p&gt;To make it concrete, here's what a small one might look like (illustrative, not a real project):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;adr-20260310-drop-axios.md&lt;/code&gt;. Context: we only use axios for simple JSON requests, and Node 18+ ships a global &lt;code&gt;fetch&lt;/code&gt;. Options: keep axios, switch to &lt;code&gt;fetch&lt;/code&gt; with a small wrapper. Decision: switch, with a wrapper in &lt;code&gt;lib/http.ts&lt;/code&gt; that handles retries and JSON errors. Consequence: we lose interceptors and write our own retry helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit 1:&lt;/strong&gt; "Add fetch wrapper with retry" + &lt;code&gt;Decision: adr-20260310-drop-axios&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit 2:&lt;/strong&gt; "Migrate API handlers to lib/http" + same trailer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit 3:&lt;/strong&gt; "Remove axios dependency" + same trailer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A year later, someone runs &lt;code&gt;git blame&lt;/code&gt; on &lt;code&gt;lib/http.ts&lt;/code&gt;, opens the commit, sees the trailer, and reads the note. Total overhead at the time: one note and one extra line per commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Name decision notes with a stable ID, reference it in a commit trailer, and let git do the searching. Add the hook if typos bother you and the Dataview dashboard if you use Obsidian. Everything else is optional.&lt;/p&gt;




&lt;p&gt;If you want a ready-made starting point, &lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt;, my Obsidian vault for developers, includes an ADR template along with Dataview dashboards.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>architecture</category>
      <category>obsidian</category>
      <category>productivity</category>
    </item>
    <item>
      <title>n8n vs GitHub Actions for side-project automation</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:13:37 +0000</pubDate>
      <link>https://dev.to/productivityforge/n8n-vs-github-actions-for-side-project-automation-4gcp</link>
      <guid>https://dev.to/productivityforge/n8n-vs-github-actions-for-side-project-automation-4gcp</guid>
      <description>&lt;p&gt;Once a side project has users, the chores start: deploy on merge, sync payments into your database, send a welcome email, post a note somewhere when something breaks. Two tools come up constantly for this: GitHub Actions and n8n.&lt;/p&gt;

&lt;p&gt;They overlap, and you can force either one to do almost anything. But they're built around different centers of gravity, and picking the right one per job saves a lot of friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; is built around your repository. It shines when the trigger is a git event (push, pull request, tag, release) or the job needs your code checked out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; is built around moving data between services. It shines when the job is "when X happens in service A, do Y in services B and C", especially with SaaS APIs you don't want to write auth and pagination code for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the time the choice is obvious once you ask: &lt;em&gt;does this job start from my code, or from someone else's service?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions: what it's good at
&lt;/h2&gt;

&lt;p&gt;Workflows are YAML files in &lt;code&gt;.github/workflows/&lt;/code&gt;. They run on GitHub-hosted runners (or your own) in response to events.&lt;/p&gt;

&lt;p&gt;Good fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tests and linting on every PR,&lt;/li&gt;
&lt;li&gt;building and deploying on push to &lt;code&gt;main&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;publishing a package or release notes when you push a tag,&lt;/li&gt;
&lt;li&gt;scheduled jobs that need the repo, like regenerating a sitemap or updating a dependency report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal deploy-on-push workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/deploy.sh&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you get for free: no server to run, secrets management, logs for every run, and direct access to commit and branch context. Actions also supports scheduled workflows with cron syntax (&lt;code&gt;on: schedule: - cron: '0 6 * * *'&lt;/code&gt;), with a few caveats covered below.&lt;/p&gt;

&lt;p&gt;Where it gets awkward: anything that's mostly calling third-party APIs. You can do it with a script step, but you're writing and maintaining the API client, retries and error handling yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;At the time of writing, public repositories get GitHub-hosted runner minutes on standard runners for free. Private repositories draw from a monthly allowance (2,000 minutes on the Free plan, more on paid plans), then bill per minute. Check &lt;a href="https://docs.github.com/billing/reference/actions-runner-pricing" rel="noopener noreferrer"&gt;GitHub's current pricing&lt;/a&gt; before relying on this; it has changed before.&lt;/p&gt;

&lt;p&gt;The detail that bites people: &lt;strong&gt;GitHub rounds each job up to the nearest whole minute.&lt;/strong&gt; A job that runs for 5 seconds costs a full minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n: what it's good at
&lt;/h2&gt;

&lt;p&gt;n8n is a workflow automation tool with a visual node editor. You chain triggers (webhook, schedule, an app event) with nodes for specific services (Stripe, Postgres, Slack, Notion, Gmail, GitHub and many more), and drop into JavaScript or Python when you need custom logic. You can self-host it (the source is available under n8n's Sustainable Use License) or use their paid cloud.&lt;/p&gt;

&lt;p&gt;Good fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Stripe event creates or updates a row in your database,&lt;/li&gt;
&lt;li&gt;a new signup triggers a welcome email and a note in your CRM or Notion,&lt;/li&gt;
&lt;li&gt;a form submission lands in a spreadsheet and pings you,&lt;/li&gt;
&lt;li&gt;a daily digest pulls numbers from a few APIs and emails them to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a "new paying customer" workflow might be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Trigger&lt;/strong&gt; on &lt;code&gt;checkout.session.completed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres&lt;/strong&gt; node: upsert the customer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Request&lt;/strong&gt; node: call your own API to provision their account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send Email&lt;/strong&gt; (or Gmail) node: welcome message&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;error workflow&lt;/strong&gt; that notifies you if any step fails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of those would be a chunk of code in an Actions script. In n8n the nodes handle credentials and API details, and you mostly write the data mapping.&lt;/p&gt;

&lt;p&gt;n8n can also react to repo events: its GitHub Trigger node registers a webhook for things like pushes or new issues. So "n8n can't do git events" isn't true; it's just not where it's strongest, because it doesn't have your code checked out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;Self-hosting is free in license terms, but it's a server you now own: updates, backups of its database (which holds your workflows and credentials), HTTPS, and monitoring. For many indie developers that's a small VPS they already have. n8n Cloud removes the ops work for a monthly fee; plans and execution limits change, so check their pricing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: frequent schedules on Actions
&lt;/h2&gt;

&lt;p&gt;A pattern that looks cheap and isn't: using Actions as an uptime monitor with a cron every five minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every 5 minutes is 288 runs a day, roughly 8,640 a month.&lt;/li&gt;
&lt;li&gt;Each run is billed as at least one minute.&lt;/li&gt;
&lt;li&gt;On a private repo, that's around 8,640 billed minutes a month, over four times the Free plan's allowance, for a job that runs &lt;code&gt;curl&lt;/code&gt; for two seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's also not a great monitor. GitHub's docs note that scheduled workflows can be delayed during periods of high load, five minutes is the shortest interval allowed, and in public repositories scheduled workflows are automatically disabled after 60 days without repository activity.&lt;/p&gt;

&lt;p&gt;For uptime checks, use a dedicated uptime monitoring service or a schedule trigger in n8n (which runs on your server, so frequency doesn't cost minutes). Keep Actions schedules for jobs that run a few times a day at most and genuinely need the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use both
&lt;/h2&gt;

&lt;p&gt;A split that works well: Actions owns everything up to "the new version is live", and n8n owns what happens after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="c1"&gt;# last step of the deploy job&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tell n8n about the deploy&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsS -X POST "$N8N_WEBHOOK_URL" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
            &lt;span class="s"&gt;-d "{\"sha\": \"${GITHUB_SHA}\", \"ref\": \"${GITHUB_REF_NAME}\"}"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.N8N_DEPLOY_WEBHOOK }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The n8n workflow behind that webhook can post to Discord, update a status page, add a changelog row, and email beta users, without any of that logic living in your repo's CI config. If the n8n side fails, your deploy still succeeded, which is usually what you want.&lt;/p&gt;

&lt;p&gt;If you add a webhook like this, protect it: at minimum a hard-to-guess URL kept in secrets, ideally a shared token checked in the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick way to decide
&lt;/h2&gt;

&lt;p&gt;List the chores you currently do by hand and note two things for each: what triggers it, and what it touches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chore&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Touches&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run tests on PRs&lt;/td&gt;
&lt;td&gt;Pull request&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy on merge&lt;/td&gt;
&lt;td&gt;Push to &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Your code, host&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Stripe customer -&amp;gt; DB + welcome email&lt;/td&gt;
&lt;td&gt;Stripe event&lt;/td&gt;
&lt;td&gt;Stripe, DB, email&lt;/td&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly metrics email&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;Several APIs&lt;/td&gt;
&lt;td&gt;n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regenerate docs site&lt;/td&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime check every few minutes&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;Your site&lt;/td&gt;
&lt;td&gt;Uptime monitor or n8n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announce a release&lt;/td&gt;
&lt;td&gt;Tag push&lt;/td&gt;
&lt;td&gt;Discord, email, status page&lt;/td&gt;
&lt;td&gt;Actions -&amp;gt; n8n webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a chore takes you five minutes a month, automating it with either tool may not be worth the maintenance. Automate the ones that are frequent, error-prone, or that you tend to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Start with GitHub Actions, because you probably already have it and it covers everything code-shaped. Add n8n when you catch yourself writing API glue in YAML, or when a job has nothing to do with your repository. And be careful with frequent cron schedules on private repos; that's where Actions gets surprisingly expensive.&lt;/p&gt;




&lt;p&gt;If you're running a few side projects and want one place to track them (ideas, launch checklist, revenue), I made a &lt;a href="https://subengel.gumroad.com/l/tsatqt" rel="noopener noreferrer"&gt;Side Project Tracker&lt;/a&gt; Notion template.&lt;/p&gt;

&lt;p&gt;More templates and a free Dataview starter pack are at &lt;a href="https://forge.engelailabs.com" rel="noopener noreferrer"&gt;forge.engelailabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>nocode</category>
      <category>automation</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>A weekly review for engineers that isn't about tasks</title>
      <dc:creator>Sub Engel</dc:creator>
      <pubDate>Fri, 02 Oct 2026 17:09:30 +0000</pubDate>
      <link>https://dev.to/productivityforge/a-weekly-review-for-engineers-that-isnt-about-tasks-4ib4</link>
      <guid>https://dev.to/productivityforge/a-weekly-review-for-engineers-that-isnt-about-tasks-4ib4</guid>
      <description>&lt;p&gt;The classic weekly review (from Getting Things Done) is about tasks and commitments: empty your inboxes, update your lists, look at your calendar. That's useful, and your issue tracker probably covers most of it already.&lt;/p&gt;

&lt;p&gt;What it doesn't cover is the stuff developers lose all the time: why you picked that library, what the fix was for that weird timezone bug, which config flag finally made the build work. You solve a problem, move on, and meet it again six months later with no memory of the answer.&lt;/p&gt;

&lt;p&gt;This is a review built around that problem. It has two parts: a tiny daily habit and a fixed weekly session, plus an optional monthly skim.&lt;/p&gt;

&lt;h2&gt;
  
  
  During the week: one line per thing worth remembering
&lt;/h2&gt;

&lt;p&gt;Keep a section in your daily note (or a single running file, if you don't do daily notes) and add one line whenever something happens that you'd be annoyed to rediscover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bug that took more than half an hour to understand,&lt;/li&gt;
&lt;li&gt;an API or library that didn't behave the way you assumed,&lt;/li&gt;
&lt;li&gt;a choice between two approaches,&lt;/li&gt;
&lt;li&gt;a command, flag or config you had to look up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the format dumb so you actually do it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Log&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Postgres: ILIKE treats &lt;span class="sb"&gt;`_`&lt;/span&gt; and &lt;span class="sb"&gt;`%`&lt;/span&gt; in user input as wildcards. Escape them before building the pattern. PR #412
&lt;span class="p"&gt;-&lt;/span&gt; Chose server-side sessions over JWT refresh; token rotation raced across tabs. -&amp;gt; needs decision note
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`git log -S "term"`&lt;/span&gt; finds when a string first appeared. Keep forgetting this.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problem, answer, link if there is one. No tags required, no templates to fill in. If you're adding more than a couple of minutes a day, you're overdoing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Friday: 45 minutes, same questions every week
&lt;/h2&gt;

&lt;p&gt;Put it on your calendar. Friday afternoon works for most people because the week is still fresh and it's rarely prime focus time anyway. Open the week's log lines and go through four questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What happened more than once?
&lt;/h3&gt;

&lt;p&gt;Scan for repeats. Same kind of bug twice, same lookup twice, same review comment on two PRs. A repeat is a signal that a small artifact would pay off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a snippet you can paste next time,&lt;/li&gt;
&lt;li&gt;a checklist (e.g. "things to check when a query gets slow"),&lt;/li&gt;
&lt;li&gt;a lint rule or test that stops it happening again,&lt;/li&gt;
&lt;li&gt;a line in the project README.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make the artifact now, while you remember the details. Don't make one for things that only happened once; most of those never come back.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What did I decide?
&lt;/h3&gt;

&lt;p&gt;Any line that's really a decision (you picked X over Y and there were trade-offs) gets promoted to its own note with context, options and reasons. It takes ten minutes on Friday and saves the "why on earth did we do this" conversation later. If you link decisions to commits, add the ID to the relevant commits now.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What did I have to look up, and should I actually learn it?
&lt;/h3&gt;

&lt;p&gt;Things you looked up once are fine. Things you looked up three times are a gap. Keep a short "learn next" list, and be honest about priority: only put something at the top if it keeps costing you time. A line like this is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Learn next&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Postgres window functions (hit twice this month writing reports)
&lt;span class="p"&gt;-&lt;/span&gt; TypeScript conditional types (fought inference on the API client again)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then actually block time for the top item. An hour of reading the docs properly beats another three rounds of copying from Stack Overflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. What's still open?
&lt;/h3&gt;

&lt;p&gt;Last: anything from the log that's unresolved. A workaround you shipped that needs a real fix, a question you never answered, a follow-up from an incident. Turn each into a task in whatever tracker you use, or delete it if you honestly won't get to it. The point is that nothing unresolved lives only in the log.&lt;/p&gt;

&lt;h3&gt;
  
  
  The weekly note
&lt;/h3&gt;

&lt;p&gt;The output of all this is one short note per week. A template like this keeps it consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Week 2026-W39&lt;/span&gt;

&lt;span class="gu"&gt;## Repeats -&amp;gt; artifacts&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ILIKE escaping: added &lt;span class="sb"&gt;`escapeLike()`&lt;/span&gt; helper + test

&lt;span class="gu"&gt;## Decisions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [[adr-20260924-session-store]]

&lt;span class="gu"&gt;## Learn next&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Postgres window functions

&lt;span class="gu"&gt;## Open -&amp;gt; tasks&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Replace retry workaround in webhook handler (#431)

&lt;span class="gu"&gt;## One sentence on the week&lt;/span&gt;
Mostly auth work; lost a day to the tab race before understanding it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a section is empty, leave it empty. Some weeks nothing repeats and you decide nothing interesting. That's fine; the review takes fifteen minutes those weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Once a month: skim, don't audit
&lt;/h2&gt;

&lt;p&gt;On the first Friday of the month, spend an extra twenty minutes reading the last four weekly notes in a row. You're looking for things that only show up at that distance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same area of the codebase appearing in "repeats" every week (a refactor candidate),&lt;/li&gt;
&lt;li&gt;a "learn next" item that's been sitting at the top for a month (block the time or drop it),&lt;/li&gt;
&lt;li&gt;decisions that contradict each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write two or three bullets at the top of the month's first weekly note and move on. No spreadsheets, no scoring yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's built this way
&lt;/h2&gt;

&lt;p&gt;The usual failure mode of review systems is that they're more work than the problems they solve, so they get dropped after a few weeks. This one tries to avoid that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture costs almost nothing.&lt;/strong&gt; One line, no structure, in a note you already have open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The weekly session has fixed questions.&lt;/strong&gt; You're never staring at a blank page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every question produces something concrete&lt;/strong&gt; (a snippet, a decision note, a task) or produces nothing and you move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no metric to maintain.&lt;/strong&gt; Tracking "hours saved" by your notes is itself a job, and the numbers are guesses anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add a &lt;code&gt;## Log&lt;/code&gt; section to today's daily note (or create &lt;code&gt;log.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Add one line every time something annoys you or surprises you.&lt;/li&gt;
&lt;li&gt;Put a 45-minute block on Friday and run the four questions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip the monthly skim until you've done four weekly reviews. If you only ever do the Friday part, you're already ahead of where most of us are.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://subengel.gumroad.com/l/uoybkd" rel="noopener noreferrer"&gt;Dev Second Brain&lt;/a&gt;, my Obsidian vault for developers, includes a weekly review template, if you'd rather start from one than build it yourself.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>obsidian</category>
      <category>developers</category>
    </item>
  </channel>
</rss>
