<?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: Hal Hunt</title>
    <description>The latest articles on DEV Community by Hal Hunt (@hal_hunt).</description>
    <link>https://dev.to/hal_hunt</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896743%2Fb6dc6261-84d1-4699-aaff-170ac1b39574.png</url>
      <title>DEV Community: Hal Hunt</title>
      <link>https://dev.to/hal_hunt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hal_hunt"/>
    <language>en</language>
    <item>
      <title>My calendar tracked my meetings. Nothing tracked the other 6 hours. So I built something that does.</title>
      <dc:creator>Hal Hunt</dc:creator>
      <pubDate>Tue, 28 Apr 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/hal_hunt/my-calendar-tracked-my-meetings-nothing-tracked-the-other-6-hours-so-i-built-something-that-does-4pgp</link>
      <guid>https://dev.to/hal_hunt/my-calendar-tracked-my-meetings-nothing-tracked-the-other-6-hours-so-i-built-something-that-does-4pgp</guid>
      <description>&lt;p&gt;I have been writing software for 25 years and managing engineering teams for 6. I know how to build things. What I could never figure out was how to &lt;em&gt;remember&lt;/em&gt; everything I built, reviewed, commented on, or responded to on any given day.&lt;/p&gt;

&lt;p&gt;My calendar tracked meetings fine. Everything else just disappeared.&lt;/p&gt;

&lt;p&gt;PR reviews. Jira comments. Ad hoc asks from leadership. One-off conversations that turned into two hours of unplanned architecture work. Performance review prep. By Friday I could barely reconstruct Tuesday. I eventually started keeping a running Google Doc of everything I touched each day. It was exactly as tedious as it sounds, and I stopped maintaining it within a week every single time I tried.&lt;/p&gt;

&lt;p&gt;I looked for a tool that solved this. I could not find one I was willing to actually use. Everything fell into one of four buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time trackers that required manual start/stop timers&lt;/li&gt;
&lt;li&gt;Task managers that needed you to log work before you did it&lt;/li&gt;
&lt;li&gt;Meeting tools that only cared about your calendar&lt;/li&gt;
&lt;li&gt;Desktop "monitoring" agents that tracked everything you did on your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last category was a hard no. I'm not installing surveillance software on my PC to remember what I worked on.&lt;/p&gt;

&lt;p&gt;So I built something myself. This post is about what I built, the technical decisions I made, and what I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Worktrace&lt;/strong&gt; connects to the tools you already use, observes activity &lt;em&gt;metadata&lt;/em&gt;, and builds a chronological daily work log for you to review.&lt;/p&gt;

&lt;p&gt;The key word is metadata. It never reads document bodies, PR diffs, calendar descriptions, or source code. It reads things like: event titles, timestamps, file names, issue status transitions, PR state changes. The signals you already generate just by doing your job.&lt;/p&gt;

&lt;p&gt;At the end of the day (or whenever you want), you open the app, see what it surfaced, confirm what counts, edit anything that's wrong, and move on. The whole review takes a couple of minutes.&lt;/p&gt;

&lt;p&gt;No timers. No manual entry. No agents on your machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 8 Integrations
&lt;/h2&gt;

&lt;p&gt;The supported providers right now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What Gets Captured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Calendar&lt;/td&gt;
&lt;td&gt;Event titles, start/end times, organizer, recurring status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Calendar&lt;/td&gt;
&lt;td&gt;Event titles, start/end times, organizer (via Graph API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Drive&lt;/td&gt;
&lt;td&gt;File name, last modified time, file type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft OneDrive&lt;/td&gt;
&lt;td&gt;File name, last modified time (via Graph API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;PRs opened/merged/closed/reviewed, commits, issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jira&lt;/td&gt;
&lt;td&gt;Issues, worklogs, status transitions, comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure DevOps&lt;/td&gt;
&lt;td&gt;Work items, PRs, commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitbucket Cloud&lt;/td&gt;
&lt;td&gt;PRs, commits, issues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything is read-only. Nothing is ever written back.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Clean, Intentionally Boring
&lt;/h2&gt;

&lt;p&gt;The stack is .NET 10 with an ASP.NET Minimal APIs backend and an Angular 21 frontend. The architecture follows Clean Architecture strictly, enforced structurally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Domain
  |
Application
  |
Infrastructure
  |
API
  |
Frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies only point inward. No exceptions.&lt;/p&gt;

&lt;p&gt;The Domain layer (&lt;code&gt;Hyprsoft.Worktrace.Domain&lt;/code&gt;) has zero framework dependencies. It compiles if ASP.NET, EF Core, and Azure do not exist. I treat this as a test: if a decision makes the Domain aware of the outside world, the decision is wrong.&lt;/p&gt;

&lt;p&gt;The Application layer uses CQRS via MediatR. Commands and queries are thin orchestrators. The interesting business logic lives in the Domain. Infrastructure is EF Core over SQL Server.&lt;/p&gt;

&lt;p&gt;Each integration is its own project. &lt;code&gt;Hyprsoft.Worktrace.Integrations.GitHub&lt;/code&gt;, &lt;code&gt;Hyprsoft.Worktrace.Integrations.Jira&lt;/code&gt;, and so on. They all implement the same interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IIntegrationWorker&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IntegrationProvider&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AuthenticateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserContext&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActivityEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FetchActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UserContext&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Core API never calls a third-party API. The workers do. This keeps the API clean and keeps failure contained per integration. If GitHub is down, only GitHub stops working.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part I Found Most Interesting: The Inference Engine
&lt;/h2&gt;

&lt;p&gt;Raw activity events are not useful on their own. On a normal day you might have 40 or 50 of them across all your tools. What you actually want is a meaningful, grouped summary.&lt;/p&gt;

&lt;p&gt;The inference engine takes those raw events and produces &lt;code&gt;WorkEntry&lt;/code&gt; suggestions. This is where most of the interesting logic lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why No LLMs?
&lt;/h3&gt;

&lt;p&gt;I want every suggestion to be traceable to specific evidence. I want users to understand &lt;em&gt;why&lt;/em&gt; they got a suggestion. I want the system to be predictable. LLMs don't always produce perfect for all three of these use cases all the time.&lt;/p&gt;

&lt;p&gt;The engine is deterministic and rules-based. Every confidence score is explained, not opaque.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ActivityEvents
   |
   v
Context Extraction (per provider)
   |
   v
Sort by OccurredAt
   |
   v
Phase 1: Attach new events to existing calendar-based entries
   |
   v
Phase 2: Calendar events -&amp;gt; one WorkEntry each
   |
   v
Phase 3: Cluster remaining non-calendar events
   |
   v
Confidence Scoring
   |
   v
WorkEntry Suggestions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Context Extraction
&lt;/h3&gt;

&lt;p&gt;Before grouping, every event goes through a provider-specific &lt;code&gt;IActivityContextExtractor&lt;/code&gt;. This normalizes events into a provider-agnostic &lt;code&gt;ActivityContext&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ActivityContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ProjectIdentifier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SecondaryIdentifiers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CleanedTitle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CorrelationHints&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ProjectIdentifier&lt;/code&gt; is the primary grouping key (repo name, Jira project key)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SecondaryIdentifiers&lt;/code&gt; are exact correlation keys (Jira issue key like &lt;code&gt;WT-43&lt;/code&gt;, PR number)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CleanedTitle&lt;/code&gt; strips provider prefixes for fuzzy matching&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CorrelationHints&lt;/code&gt; carries pattern learning signals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Similarity Scoring
&lt;/h3&gt;

&lt;p&gt;The engine uses a multi-tier similarity system to decide if two events are related:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Exact secondary identifier match (e.g., "WT-43" in both)  -&amp;gt; 1.0
2. Exact project identifier match (same repo name)            -&amp;gt; 0.8
3. FuzzySharp TokenSetRatio &amp;gt;= 60 on CleanedTitle             -&amp;gt; score/100
4. Same organization in correlation hints                     -&amp;gt; 0.3
5. No match                                                   -&amp;gt; 0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimum threshold is 0.6. Anything below that is not considered related.&lt;/p&gt;

&lt;p&gt;I use FuzzySharp (a .NET port of RapidFuzz) for the fuzzy title matching. &lt;code&gt;TokenSetRatio&lt;/code&gt; handles word order differences and extra words well, which matters when you're comparing "Sprint Planning - Platform Team" against "WT-43: Platform sprint items."&lt;/p&gt;

&lt;h3&gt;
  
  
  Calendar Events as Anchors
&lt;/h3&gt;

&lt;p&gt;Calendar events are the primary time anchors. Each calendar event becomes its own &lt;code&gt;WorkEntry&lt;/code&gt;. Non-calendar events that occurred during a meeting's time window and have sufficient similarity get attached to that entry.&lt;/p&gt;

&lt;p&gt;The engine does a &lt;strong&gt;transitive grouping pass&lt;/strong&gt; for this. If &lt;code&gt;PullRequestReviewed&lt;/code&gt; didn't directly match the calendar event's context, but it matches another already-attached event (say, because they both reference &lt;code&gt;WT-43&lt;/code&gt;), it gets pulled in transitively. This handles the case where a meeting is called "Sprint Planning" but all the work items discussed are &lt;code&gt;WT-43&lt;/code&gt;, &lt;code&gt;WT-44&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-Calendar Clustering
&lt;/h3&gt;

&lt;p&gt;Events that don't attach to any calendar entry get clustered in three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Group by project.&lt;/strong&gt; Events with the same &lt;code&gt;ProjectIdentifier&lt;/code&gt; go in the same bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-proximity clustering.&lt;/strong&gt; Within a project group, events are ordered by time. If the gap between consecutive events exceeds 60 minutes, a new cluster starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-cluster merging.&lt;/strong&gt; Clusters are merged if they share secondary identifiers or have fuzzy title similarity and are within 60 minutes of each other. I use Union-Find for transitive merging here.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Confidence Scoring
&lt;/h3&gt;

&lt;p&gt;Confidence is evidence-based, not probabilistic. The score is built from factors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Score Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base&lt;/td&gt;
&lt;td&gt;+0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar provider as primary event&lt;/td&gt;
&lt;td&gt;+0.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple events from different providers&lt;/td&gt;
&lt;td&gt;+0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple events from same provider&lt;/td&gt;
&lt;td&gt;+0.1 per event (max 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit duration on primary event&lt;/td&gt;
&lt;td&gt;+0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern-based adjustment&lt;/td&gt;
&lt;td&gt;-0.2 to +0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scores map to High (&amp;gt;0.85), Medium (0.60-0.85), or Low (&amp;lt;0.60). The numeric score never surfaces in the UI. Users see the level, not the number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learning from Feedback
&lt;/h3&gt;

&lt;p&gt;When a user accepts, rejects, edits, or splits an entry, the engine records it against &lt;code&gt;UserInferencePattern&lt;/code&gt; entities keyed by a multi-granularity pattern signature.&lt;/p&gt;

&lt;p&gt;Pattern keys are extracted at several levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider + title: &lt;code&gt;gcalendar:title:standup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Provider + event type: &lt;code&gt;github:eventtype:pullrequestreviewed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Provider + project: &lt;code&gt;github:owner/repo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pattern hints from context: &lt;code&gt;jira:issuetype:bug&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cross-provider: &lt;code&gt;cross:github+jira&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over time, accept/reject rates shift confidence scores up or down, and duration edit history influences the engine's duration estimates. The adjustment is clamped to ±0.2 confidence and ±30-60 minutes on duration.&lt;/p&gt;

&lt;p&gt;No model training. No retraining loop. Just incrementing counters and doing math.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privacy by Architecture
&lt;/h2&gt;

&lt;p&gt;The metadata-only constraint is not a policy. It's enforced structurally.&lt;/p&gt;

&lt;p&gt;Integration mappers only extract metadata fields. The &lt;code&gt;ActivityEvent.Create()&lt;/code&gt; factory method validates and normalizes inputs. Title normalization collapses whitespace. URLs are validated as absolute HTTP/HTTPS only. The &lt;code&gt;EventMetadata&lt;/code&gt; value object is a controlled key-value store with no free-text content fields.&lt;/p&gt;

&lt;p&gt;OAuth tokens are encrypted at rest. They are never exposed to the frontend. Each user has their own tokens per provider. There are no shared credentials.&lt;/p&gt;

&lt;p&gt;I understand that for a lot of engineers, especially those at larger companies, connecting any third-party tool to code repositories or project trackers is not a personal decision. IT security teams have to sign off, compliance reviews happen, and "trust me, it only reads metadata" is not going to cut it. That is completely reasonable. &lt;/p&gt;

&lt;p&gt;I put together a &lt;strong&gt;detailed 21-page privacy and security whitepaper&lt;/strong&gt; that covers exactly what data is accessed, how OAuth tokens are stored and encrypted, data isolation guarantees, retention policies, and the full threat model. If you need to make the case internally or just want to read the fine print yourself, it is available &lt;a href="https://worktrace.io/security" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Shipped, What's Left
&lt;/h2&gt;

&lt;p&gt;The current free tier gives you Google Calendar and Google Drive. Standard ($5/mo) unlocks all 8 integrations and 90 days of history. Enterprise ($10/mo) adds unlimited history and org controls &lt;strong&gt;but for a limited time all tiers are free&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The weekly digest is one of my favorite features. It runs at 8 AM Monday in your local timezone, emails you the previous week's summary, and links to a time-limited signed PDF report.&lt;/p&gt;

&lt;p&gt;What I'm still working on: Stripe integration when required.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Learned
&lt;/h2&gt;

&lt;p&gt;A few things that surprised me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The inference engine is the hardest part, and it's not glamorous.&lt;/strong&gt; It's a lot of edge case handling. What if the PR was opened before the standup but merged after? What if someone has 12 Jira events in one day across 3 projects? What if a calendar event has no duration? Every case needs a rule, and every rule has a counterexample.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy as a constraint is actually clarifying.&lt;/strong&gt; When you eliminate all content, you're forced to make the metadata work harder. That constraint shaped a lot of the extractor and similarity design in ways I wouldn't have arrived at otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No-LLM doesn't mean dumb.&lt;/strong&gt; The inference is genuinely useful without any language models. Deterministic and explainable is a feature, not a limitation.  LLM integration will be integrated as needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building for yourself is a good start, but not the whole story.&lt;/strong&gt; I use this every day. But I'm also a fairly unusual case: 25 years of tooling opinions, specific integrations, and a very structured workday. I genuinely don't know yet whether the problem is widespread or whether I'm just uniquely disorganized.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Honest Ask
&lt;/h2&gt;

&lt;p&gt;I built this to solve my own problem, and it solves it well for me. But I am one person with one workflow, one set of tools, and 25 years of habits baked in.&lt;/p&gt;

&lt;p&gt;What I genuinely do not know yet is whether the inference engine holds up for people with different rhythms. Someone who lives in Azure DevOps instead of GitHub. A contractor juggling three clients in Jira simultaneously. An EM whose day is 80% calendar and 20% everything else.&lt;/p&gt;

&lt;p&gt;If you try it and something feels off, like the grouping is wrong, the confidence is backwards, a whole category of your work is invisible, etc. I want to know. That feedback is not just appreciated, it is the only way this gets better.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://worktrace.io/app/feedback" rel="noopener noreferrer"&gt;feedback form&lt;/a&gt; built into the app, or you can find me here in the comments. Either way, I am reading everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you want to poke around: &lt;a href="https://worktrace.io" rel="noopener noreferrer"&gt;worktrace.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a limited time all tiers are free for early adopters.  No credit card required.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the inference engine, the OAuth flows, the clean architecture decisions, or anything else.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>buildinpublic</category>
      <category>devtools</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
