<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.unwiredsolutions.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.unwiredsolutions.com/" rel="alternate" type="text/html" /><updated>2026-09-09T15:31:10+00:00</updated><id>https://www.unwiredsolutions.com/feed.xml</id><title type="html">Field Notes</title><subtitle>Working notes on building small, durable personal systems — documents, health data, money, and the infrastructure that decays if nobody tends it.</subtitle><entry><title type="html">From Case Study to Starter Kit</title><link href="https://www.unwiredsolutions.com/from-case-study-to-starter-kit/" rel="alternate" type="text/html" title="From Case Study to Starter Kit" /><published>2026-09-08T00:00:00+00:00</published><updated>2026-09-08T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/from-case-study-to-starter-kit</id><content type="html" xml:base="https://www.unwiredsolutions.com/from-case-study-to-starter-kit/"><![CDATA[<p>The <a href="/personal-decision-system-case-study/">case-study post</a> described the shape of the system: an archive, a set of dashboards, a review loop that kept both honest. That was enough to explain how it worked. It was not enough for anyone to actually use it. A write-up you can read is not the same thing as a repository you can clone, and I wanted to find out how much distance sits between those two.</p>

<p>More than I expected. Most of it wasn’t writing new logic — the mandates and templates already existed. It was deciding, line by line, what belonged in a public starter kit and what belonged only to my own archive.</p>

<ul>
  <li><a href="#format-not-content">Format versus content</a></li>
  <li><a href="#seeding-structure">Seeding structure without seeding facts</a></li>
  <li><a href="#incremental">Why it has to fill in gradually</a></li>
  <li><a href="#distribution">Picking a distribution model</a></li>
  <li><a href="#skills-as-source">Shipping skills as source, not packages</a></li>
  <li><a href="#tooling-boundary">A tooling boundary I didn’t expect</a></li>
  <li><a href="#contents">What’s actually in the repo</a></li>
  <li><a href="#getting-started">Getting started</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="format-not-content">1. Format versus content</h2>

<p>Some of the most useful notes in my own archive are useful because of their format, not their content. A lab-trend note that flags a value as optimal, borderline, or out of range using a consistent set of markers is a good design regardless of whose bloodwork is in it. The specific values are mine. The convention is not.</p>

<p>The first pass at the starter kit blurred that line. I copied over a few reference notes with plausible-looking example content — a glossary entry on a health topic, a renewal-rule note with invented dates — on the theory that a filled-in example teaches faster than a blank one. It doesn’t, not here. That content was specific to a household that doesn’t exist, would need to be un-learned before anyone typed a real fact into the same note, and taught nothing about the mechanism itself.</p>

<p>The fix was to ship the four reference templates as format only: the table structure, the flag convention, the callout style, a sourcing rule that says which claims need a citation and which don’t. No invented lab values, no invented statutes, no invented medications. The actual content of any reference note is supposed to come from whatever the person using the kit files and asks about — that’s the whole point of having an assistant build it up over time instead of a person filling out a form once. A pre-written example doesn’t demonstrate that process; it just hides it behind a plausible-looking fake.</p>

<h2 id="seeding-structure">2. Seeding structure without seeding facts</h2>

<p>A completely empty repository is its own kind of unhelpful. The first real decision anyone has to make with a document archive is what the top-level categories even are, and staring at an empty folder while trying to answer that from scratch is a bad first five minutes.</p>

<p>So the archive template ships with its category folders already created — Health, Identity, Legal, Finance, Home, and the rest — each holding a one-paragraph note that states the placement rule for that category and links back to the rulebook. Someone can read twelve short notes in ten minutes and understand the whole taxonomy before they’ve filed a single document. Renaming or deleting a category later is one line in that rulebook, not a redesign.</p>

<p>That’s a different kind of seeding than the reference-template question above. A folder name and a placement rule are structure. A lab value or a legal deadline is a fact about a specific person. The kit seeds the first and refuses to guess at the second.</p>

<h2 id="incremental">3. Why it has to fill in gradually</h2>

<p>The obvious way to bootstrap a new install is to point an agent at an existing pile of documents and say “sort all of it.” I tried writing the setup instructions that way and then talked myself out of it.</p>

<p>Two separate problems. The practical one is that reading, classifying, and filing a few hundred documents in one sitting spends a large share of a session’s budget on a single run, with no natural place to stop and check the work. The less obvious one is that a long unattended pass is exactly where a filing mistake or a bad extraction is most likely to slip past without anyone noticing, because there’s no natural checkpoint where a person actually looks at what happened.</p>

<p>The setup guide asks for the opposite: drop a few dozen files into an inbox folder, ask the assistant to sort that batch, look at what it did, then drop the next batch whenever you’re ready. There’s no deadline to have the whole archive filed. The system is built to accumulate correctly over weeks of ordinary use, not to be loaded in one afternoon and then hoped it’s mostly right.</p>

<h2 id="distribution">4. Picking a distribution model</h2>

<p>The first version of the kit lived in a shared Google Drive folder — a zip file and a set of docs I could hand to the two people who’d actually asked for it. That was fine for two people. It meant every future update was a re-share, and it meant anyone else who wanted to try it needed to know me first.</p>

<p>Moving it to a <a href="https://github.com/espaillato/DriveOrganizerStarterKit">public git repository</a> under an MIT license changes the actual audience from “people I send a link to” to “anyone who finds it.” That’s a bigger decision than it sounds like, because a public repo gets read by people with no context on why a rule exists, which means every instruction in it has to justify itself on the page rather than relying on a shared conversation to fill in the gaps. Rewriting the setup guide for that audience caught a handful of places where a rule was stated but the reasoning behind it wasn’t, which is worth doing even for something that never leaves a Drive folder.</p>

<h2 id="skills-as-source">5. Shipping skills as source, not packages</h2>

<p>The two skills that do the actual filing and auditing work were originally distributed as built packages — a zip with a manifest and a script inside, ready to install. That’s a reasonable way to hand something to two people. It’s a bad thing to check into a public repository, for the same reason any built artifact is a bad thing to commit: it goes stale the moment the source changes, and nothing forces the two to stay in sync.</p>

<p>The repo now carries the plain source for each skill — the instructions and the one bundled script one of them needs — and a setup script builds the installable packages locally when someone runs it. That’s a small change, but it’s the same discipline any codebase applies to compiled output, applied to something that’s usually hand-packaged and forgotten about.</p>

<h2 id="tooling-boundary">6. A tooling boundary I didn’t expect</h2>

<p>Not every constraint in a project like this comes from the design. Partway through moving the kit into its own repository, I ran into an environment limit that had nothing to do with the content: the sandbox I was working in couldn’t reach the repository’s filesystem path at all for running shell commands, even though it could read and write individual files there directly.</p>

<p>The practical effect was that every file in the new repository got created through direct file writes rather than a shell script, and the actual <code class="language-plaintext highlighter-rouge">git add</code> / <code class="language-plaintext highlighter-rouge">commit</code> / <code class="language-plaintext highlighter-rouge">push</code> had to stay a manual step handed back to me rather than something run automatically at the end. That’s a minor inconvenience for a one-time setup, but it’s worth naming for the same reason the <a href="/recurring-review-council-async-flag-protocol/">unattended-run problem</a> was worth naming: a system’s real constraints aren’t only the ones you designed on purpose. Some of them are just where the tooling stops, and the honest thing to do is say so rather than pretend the automation went further than it did.</p>

<h2 id="contents">7. What’s actually in the repo</h2>

<p>The finished layout:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>File_Archive_Template/
  Organization_Philosophy.md   the rulebook: naming, dating, placement
  Health/, Identity/, ...      category skeleton, one note per folder
  Unsorted/, Trash/            inbox and pending-deletion folders

Living_Reference_Template/
  00_README...md                architecture and conventions
  _Reviewers/                    full mandate per scheduled review
  _Skills/                       full mandate per on-request skill
  _Templates/                    blank dashboard and reference notes

skills/
  file-renamer/, file-archive-audit/   skill source, not built packages

setup.sh, setup.ps1             asks two questions, builds the rest
SETUP_GUIDE.md, BOOTSTRAP_PROMPT.md
</code></pre></div></div>

<p>Running the setup script asks where the two template folders should live, copies them there without touching anything already in place, and builds the two installable skill packages from source. Everything past that point is the setup guide: adapt the rulebook, open the vault, hand the assistant the bootstrap prompt, and start dropping in real documents a few at a time.</p>

<h2 id="getting-started">8. Getting started</h2>

<p>You need a cloud-synced folder your assistant can read (the kit assumes Google Drive), Obsidian for the vault, Claude API access for the scheduled reviews, and git. Then:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/espaillato/DriveOrganizerStarterKit.git
<span class="nb">cd </span>DriveOrganizerStarterKit
./setup.sh        <span class="c"># ./setup.ps1 on Windows</span>
</code></pre></div></div>

<p>The script asks two questions (where the File Archive folder should live, and where the Living Reference vault goes), then copies the templates into those locations without touching anything already there and builds the two <code class="language-plaintext highlighter-rouge">.skill</code> packages from source. <code class="language-plaintext highlighter-rouge">SETUP_GUIDE.md</code> documents the same steps by hand if you’d rather not run the script.</p>

<p>From there: adapt <code class="language-plaintext highlighter-rouge">Organization_Philosophy.md</code> to your own categories, open the vault in Obsidian, install the two skills, and hand the assistant the prompt in <code class="language-plaintext highlighter-rouge">BOOTSTRAP_PROMPT.md</code>. Then start dropping documents into <code class="language-plaintext highlighter-rouge">Unsorted/</code> a batch at a time. There is no target for filing the whole archive at once. As <a href="#incremental">section 3</a> covers, it is meant to accumulate over weeks of ordinary use, with the scheduled reviews keeping it current from there.</p>

<h2 id="end-state">End state</h2>

<ul>
  <li>The four reference templates ship as format scaffolds — table structure and flag conventions, no invented content standing in for real facts.</li>
  <li>The archive template ships with its category skeleton pre-built, so the first real decision is adapting a rulebook, not inventing a taxonomy from nothing.</li>
  <li>Setup instructions ask for a small batch of documents at a time, on purpose, rather than a single bulk import.</li>
  <li>Distribution moved from a shared folder handed to specific people to a public, MIT-licensed repository anyone can clone.</li>
  <li>The two skills are checked in as source; a setup script builds the installable packages instead of the repo carrying stale binaries.</li>
  <li>Some constraints came from the tooling, not the design, and the setup guide says so rather than hiding the gap.</li>
</ul>

<p>None of this changes what the system does. It changes who can pick it up without having talked to me first, which was the actual point of writing the case study in the first place.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The case-study post described the shape of the system: an archive, a set of dashboards, a review loop that kept both honest. That was enough to explain how it worked. It was not enough for anyone to actually use it. A write-up you can read is not the same thing as a repository you can clone, and I wanted to find out how much distance sits between those two.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/starter-kit-from-case-study.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/starter-kit-from-case-study.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A Personal Decision System Built on Top of a Document Archive</title><link href="https://www.unwiredsolutions.com/personal-decision-system-case-study/" rel="alternate" type="text/html" title="A Personal Decision System Built on Top of a Document Archive" /><published>2026-09-04T00:00:00+00:00</published><updated>2026-09-04T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/personal-decision-system-case-study</id><content type="html" xml:base="https://www.unwiredsolutions.com/personal-decision-system-case-study/"><![CDATA[<p>The archive described in <a href="/google-drive-file-archive-canonical-reorg/">an earlier post</a> solved a filing problem. Documents had stable names, a small set of categories, and explicit rules for the cases that did not fit neatly. That made the archive easier to maintain and search, but it was still an archive: useful when I knew what to look for, less useful when the question was what deserved attention now.</p>

<p>I added a decision layer on top of it. Source documents stay in the archive. Short dashboards describe the current state of a few important areas. Deeper reference notes hold the supporting detail. Scheduled reviewers check both the facts and the recommendations, while a separate maintenance process keeps the underlying files organized and the summaries current.</p>

<p>This post describes that method as a sanitized case study. The personal data is not the reusable part. The reusable part is the shape of the system, the division of responsibility, and the rules that keep a concise summary from becoming either stale or enormous.</p>

<ul>
  <li><a href="#archive-limit">The problem the archive did not solve</a></li>
  <li><a href="#architecture">The architecture</a></li>
  <li><a href="#markdown-obsidian">Why Markdown and Obsidian</a></li>
  <li><a href="#dashboard">What belongs in a dashboard</a></li>
  <li><a href="#evidence">Keeping evidence separate from conclusions</a></li>
  <li><a href="#maintenance-loops">Two maintenance loops</a></li>
  <li><a href="#review-layer">The review layer</a></li>
  <li><a href="#unattended-runs">The unattended-run problem</a></li>
  <li><a href="#sanitization">What has to be sanitized</a></li>
  <li><a href="#starter-version">A reusable starter version</a></li>
  <li><a href="#measurement">What I would measure next</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="archive-limit">0. The problem the archive did not solve</h2>

<p>A well-organized archive answers retrieval questions: where is the latest statement, which document superseded the old one, what was the result of a prior appointment, when does an identification document expire. Those are valuable questions, but they begin with someone remembering to ask.</p>

<p>The more useful questions cut across documents:</p>

<ul>
  <li>What has changed since the last review?</li>
  <li>Which open items have become time-sensitive?</li>
  <li>Does a recommendation still fit the current facts?</li>
  <li>Are two individually reasonable recommendations inconsistent when combined?</li>
  <li>What should I bring to the next conversation with a doctor, accountant, attorney, or institution?</li>
</ul>

<p>Answering those directly from a large archive every time is possible, but inefficient. It repeats extraction work, produces long answers, and makes it difficult to distinguish a source fact from a current interpretation of that fact. The system needed a small current-state layer that could be read in minutes and checked against the evidence behind it.</p>

<p>The result is less a second archive than a set of maintained views over the first one.</p>

<h2 id="architecture">1. The architecture</h2>

<p>The system has three layers:</p>

<ol>
  <li><strong>Source archive.</strong> Original statements, reports, forms, scans, exports, and correspondence. These remain the record of what actually happened.</li>
  <li><strong>Living reference.</strong> Short dashboards for current state and actions, plus linked reference notes for analysis, history, definitions, and calculations.</li>
  <li><strong>Review loop.</strong> Recurring maintenance and specialist reviews that keep the first two layers aligned and reconsider conclusions as circumstances or outside standards change.</li>
</ol>

<p>The flow is intentionally one-way at the evidence boundary:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>source documents -&gt; extracted facts -&gt; current dashboard -&gt; review -&gt; action or question
</code></pre></div></div>

<p>A review may correct an extracted fact after checking the source, but a polished dashboard does not become evidence merely because it is concise and confidently written. The source remains the source.</p>

<p>The archive in this case contains several hundred documents across a dozen broad categories. Only a subset needs a living dashboard. Most documents are point-in-time records that can be filed once and left alone. A tax return from several years ago does not need a monthly rewrite. A current financial allocation, medication list, renewal date, or unresolved estate-plan item might.</p>

<p>That distinction keeps the maintained surface small enough to be credible.</p>

<h2 id="markdown-obsidian">2. Why Markdown and Obsidian</h2>

<p>An early design decision was to store the Living Reference as Markdown and use Obsidian as its primary view.</p>

<p>Markdown keeps the underlying material portable. The notes are plain text, readable without a particular application, easy to version and search, and straightforward for scripts or review tools to process. If the viewing tool changes later, the information does not need to be extracted from a proprietary database first.</p>

<p>Plain text alone is not a particularly good daily interface, though. Obsidian adds the human-facing layer: navigation, backlinks, cross-note links, headings, tables, search, and a graph of how related information connects. A dashboard can link to its detailed history, a current action can link to the reasoning behind it, and a shared definition can support several notes without being copied into each one.</p>

<p>That combination matters because the system has two audiences. Automated processes need predictable, tool-friendly text. A person needs to move through the information naturally and understand why one item connects to another. Markdown serves the first requirement without obstructing the second; Obsidian makes the same files comfortable to use without taking ownership of them.</p>

<p>The design is therefore not “an Obsidian database.” It is a portable Markdown knowledge base with Obsidian as a particularly useful reader and navigator. The distinction is small until the day a tool changes, at which point it becomes the whole portability plan.</p>

<h2 id="dashboard">3. What belongs in a dashboard</h2>

<p>Each dashboard is meant to answer, quickly:</p>

<ul>
  <li>What is the current state?</li>
  <li>What is driving the outcome?</li>
  <li>What needs action?</li>
  <li>What is missing or uncertain?</li>
  <li>When was this last checked?</li>
</ul>

<p>Everything else has to justify its place.</p>

<p>The first versions accumulated chronological narratives, definitions, old decisions, and long explanations inside table cells. All of that was relevant, but relevance is not the same as usefulness on the main page. A dashboard that takes twenty minutes to read has stopped being a dashboard.</p>

<p>The working rule became: conclusions and current actions stay in the dashboard; evidence, derivations, and history move to linked reference notes. A line such as “confirm whether this account still matches the household allocation” belongs in the dashboard. The account history, allocation calculation, and reasoning behind the concern belong one link away.</p>

<p>The same rule applies to update logs. A log is useful evidence that a note is maintained, but an unlimited log makes every future read more expensive. Recent entries stay in the dashboard; older entries roll into a history note. Nothing is deleted, and the current page does not have to carry its entire biography around with it.</p>

<h2 id="evidence">4. Keeping evidence separate from conclusions</h2>

<p>Each current fact links back to a real source document when one exists. The link is not decorative. It makes the summary auditable and gives a professional the full context without requiring a second search through the archive.</p>

<p>The reference layer handles material that is useful but too large or too stable for the dashboard:</p>

<ul>
  <li>multi-year numeric trends;</li>
  <li>detailed visit or transaction history;</li>
  <li>calculations and assumptions;</li>
  <li>definitions shared by more than one person or dashboard;</li>
  <li>the reasoning behind a recommendation;</li>
  <li>the full history of prior changes.</li>
</ul>

<p>This separation also makes corrections less confusing. A source value, a calculated trend, and a recommendation are three different kinds of claim. When they live in different places, a correction to one does not silently rewrite the others.</p>

<p>There is a practical trust rule underneath this: a summary should say which sources were read and which visible sources were not. “Current through this date based on these documents” is a checkable statement. “Current” by itself is mostly optimism.</p>

<h2 id="maintenance-loops">5. Two maintenance loops</h2>

<p>The system separates mechanical maintenance from strategic review.</p>

<p>The <strong>frequent maintenance loop</strong> handles work with a mostly deterministic answer: naming and placement, missing links, stale metadata, malformed tables, a newer source document that has not been reflected in a dashboard, or an unresolved question waiting in a note. It runs often because the checks are comparatively cheap.</p>

<p>The <strong>slower review loop</strong> handles questions that require judgment: whether a financial strategy still matches its constraints, whether an estate plan still covers the risks it was designed for, whether a health priority should change, or whether a renewal rule has changed. Those reviews run less often and use a written domain mandate.</p>

<p>This division prevents the filing process from casually becoming a financial, legal, or medical reviewer merely because it noticed something while moving a file. It also prevents the specialist review from spending most of its time fixing filenames and broken links.</p>

<p>The two loops communicate through inline flags placed next to the relevant text. A person can leave a question or correction in a note. A maintenance pass can leave a question for a named reviewer. The marker remains until the named party resolves it, and duplicate checks prevent the same open issue from being added again on every run.</p>

<p>That mechanism is described in more detail in <a href="/recurring-review-council-async-flag-protocol/">the review-council post</a>. The important point here is that the communication lives with the artifact, not only in a chat transcript or run report.</p>

<h2 id="review-layer">6. The review layer</h2>

<p>Each domain review has a written mandate: what it owns, which files it reads, what standards it applies, what it may change, and what it must never execute.</p>

<p>The reviewers are expected to do more than fact-check. They reconsider whether the current recommendation is still sensible given the full set of constraints. They also check the combined effect of several recommendations. Five reasonable changes can still add up to one unreasonable plan.</p>

<p>The output follows a simple contract:</p>

<ul>
  <li>state the finding plainly;</li>
  <li>distinguish fact from inference;</li>
  <li>provide the reasoning and relevant numbers;</li>
  <li>identify what needs confirmation from a professional;</li>
  <li>update the dashboard and deeper reference note in their respective roles;</li>
  <li>never perform the real-world transaction or filing.</li>
</ul>

<p>That last boundary matters. The system can prepare a better discussion with a professional and make omissions easier to spot. It does not place trades, change beneficiaries, submit government forms, prescribe treatment, or otherwise confuse analysis with authority.</p>

<p>An editorial reviewer has a different job. It does not make domain judgments. It keeps dashboards short, relocates historical narration, checks terminology and links, and makes sure a useful note has not gradually become a tome.</p>

<h2 id="unattended-runs">7. The unattended-run problem</h2>

<p>One operational issue appeared only when the reviews ran on a schedule. The frequent maintenance task sometimes needed the standards used by a specialist reviewer, but the review personas originally lived inside task-specific instructions that unattended runs could not reliably access because each reviewer runs in their own sandbox.</p>

<p>The first fallback was a short persona summary in a shared note. It kept the task from failing, but it created two versions of the standard: a full mandate for the specialist and a condensed one for everyone else. They could drift, and a shallow answer could look more complete than it was.</p>

<p>The fix was to externalize the review context into ordinary, versioned reference files that every relevant process can read. Each reviewer now has a mandate document, and the mechanics shared by all reviewers live in one shared protocol. Scheduled tasks point to those files instead of carrying private copies of the rules.</p>

<p>This is a small design change with a useful general lesson: if a recurring process depends on context, that context is part of the system’s data. It should be stored somewhere accessible, reviewable, and versioned, not hidden inside the configuration of the one task that usually uses it.</p>

<p>Externalizing the context has a second benefit: it makes the setup model-agnostic. Everything a reviewer needs is in the documents, so the instruction is just “read your persona document and do what it says.” I have started running the same review on different models to see how their suggestions differ. It also allows a split by capability: the frequent maintenance loop can run against a small local model, while the specialist reviews run on higher-capability ones.</p>

<h2 id="sanitization">8. What has to be sanitized</h2>

<p>Publishing the method is not the same as publishing a sample of the real system with a few names changed.</p>

<p>A safe case study removes or generalizes:</p>

<ul>
  <li>names, account values, diagnoses, medications, document numbers, institutions, addresses, and exact dates tied to personal events;</li>
  <li>screenshots of dashboards, because combinations of harmless-looking fields can identify people;</li>
  <li>source links and file identifiers;</li>
  <li>folder names that reveal relationships, employers, providers, or legal structures;</li>
  <li>examples copied closely enough that the underlying event remains recognizable.</li>
</ul>

<p>The sanitized version keeps the schema, rules, workflow, and failure modes. Example data should be synthetic from the start rather than redacted after the fact. Redaction is easy to get almost right, which is not a particularly reassuring privacy standard.</p>

<p>There is also a product boundary here. A reusable starter kit can operate entirely on a person’s own storage and notes. A hosted service that ingests financial, health, identity, and legal documents takes on a much larger security and compliance problem.</p>

<h2 id="starter-version">9. A reusable starter version</h2>

<p>The smallest useful version does not need software beyond a file store and a note system. It needs a good template and a disciplined setup process.</p>

<p>I would package it as:</p>

<ol>
  <li><strong>A source-archive rulebook.</strong> A small category list, a one-sentence placement test for each category, naming rules, a visible unsorted area, and explicit exceptions.</li>
  <li><strong>A portable Markdown workspace.</strong> Obsidian-ready notes and links, with no dependency on Obsidian-specific storage for the underlying content.</li>
  <li><strong>A dashboard template.</strong> Current status, drivers, actions, missing information, sources, and last-reviewed metadata.</li>
  <li><strong>A reference-note template.</strong> Analysis, calculations, history, and definitions linked from the dashboard.</li>
  <li><strong>A reviewer mandate template.</strong> Scope, required inputs, review procedure, authority boundaries, and output format.</li>
  <li><strong>A shared review protocol.</strong> How to handle inline questions, how to label uncertainty, how to check combined recommendations, and when to stop for human judgment.</li>
  <li><strong>A privacy worksheet.</strong> Where files live, what is allowed in summaries, what must remain only in source documents, and which actions are prohibited.</li>
  <li><strong>A worked synthetic example.</strong> Enough fake documents and dashboards to show the method without requiring anyone to inspect a stranger’s bloodwork or tax return over breakfast.</li>
</ol>

<p>The first implementation should be guided and local. A handful of pilots would show which rules generalize, where setup remains too dependent on the original archive, and whether the dashboards change decisions often enough to justify maintaining them.</p>

<h2 id="measurement">10. What I would measure next</h2>

<p>The system already measures its own activity reasonably well: files processed, notes updated, reviews completed, open markers found. Those are operational counts, not outcomes.</p>

<p>A productized method needs a smaller set of measures tied to usefulness:</p>

<ul>
  <li>time from a new source document to an updated dashboard;</li>
  <li>age of the oldest unresolved action or question;</li>
  <li>number of decisions or professional conversations materially prepared by the system;</li>
  <li>number of stale or conflicting recommendations found during review;</li>
  <li>time spent maintaining the system versus time saved retrieving and reconstructing context;</li>
  <li>percentage of review runs that require manual repair;</li>
  <li>number of sensitive facts duplicated outside the source archive.</li>
</ul>

<p>The last measure should remain close to zero. Convenience has a habit of volunteering other people’s privacy as its first optimization.</p>

<h2 id="end-state">End state</h2>

<ul>
  <li>Original documents remain the evidence layer rather than being copied into a new database by default.</li>
  <li>Living Reference stays portable as Markdown, while Obsidian supplies navigation, backlinks, and a practical human interface.</li>
  <li>A small set of current dashboards answers what matters now and links back to the source.</li>
  <li>Detailed reasoning, calculations, and history sit one level deeper instead of inflating the dashboard.</li>
  <li>Mechanical maintenance and specialist judgment run on separate loops with separate authority.</li>
  <li>Review mandates and shared protocols live in accessible, versioned files, including for unattended runs.</li>
  <li>Inline flags keep questions attached to the relevant artifact until the named person or reviewer resolves them.</li>
  <li>The reusable package contains structure, rules, templates, and synthetic examples — none of the household data that proved the method on the first implementation.</li>
</ul>

<p>The useful product here is not a pile of personal documents and not a chatbot pointed at a folder. It is a maintained path from evidence to a current summary, from that summary to a review, and from the review to a decision someone can inspect before acting. The documents will vary. The path does not have to.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The archive described in an earlier post solved a filing problem. Documents had stable names, a small set of categories, and explicit rules for the cases that did not fit neatly. That made the archive easier to maintain and search, but it was still an archive: useful when I knew what to look for, less useful when the question was what deserved attention now.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/personal-decision-system.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/personal-decision-system.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Separation of Duties: Making Parallel Agents Behave Like a Team</title><link href="https://www.unwiredsolutions.com/separation-of-duties-ai-agent-authority-manifest/" rel="alternate" type="text/html" title="Separation of Duties: Making Parallel Agents Behave Like a Team" /><published>2026-08-31T00:00:00+00:00</published><updated>2026-08-31T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/separation-of-duties-ai-agent-authority-manifest</id><content type="html" xml:base="https://www.unwiredsolutions.com/separation-of-duties-ai-agent-authority-manifest/"><![CDATA[<p><a href="/recurring-review-council-async-flag-protocol/">The previous post</a> split one generalist sweep into several narrower agents, each with its own scope, talking to each other through a shared inline-flag protocol. That solved coordination: agents could leave each other messages across a scheduling gap without me relaying them. It didn’t solve the problem underneath. Running several agents in parallel is not the same as having several accountable specialists. Without something more explicit, they collapse back into one authority that happens to run in more than one process.</p>

<p>This post is about the failure that made that obvious, and the authority model I built to fix it. The pattern isn’t specific to a document archive, so it’s written to lift and adapt.</p>

<ul>
  <li><a href="#the-bug">The bug that exposed it</a></li>
  <li><a href="#what-makes-a-team">What makes it a team</a></li>
  <li><a href="#auto-flag-never">A concrete authority model: AUTO / FLAG / NEVER</a></li>
  <li><a href="#the-boundary-line">Where the boundary line goes</a></li>
  <li><a href="#verification-both-directions">Verification has to run both directions</a></li>
  <li><a href="#precedent-confirmation">The rule about the rules</a></li>
  <li><a href="#generalizing">Generalizing past one project</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="the-bug">0. The bug that exposed it</h2>

<p>A large sweep had more to cover than fit in one pass, so the work got split across several parallel subagents. Each one got a slice of the checklist and a short instruction: apply the rules, report back. One rule mattered more than the others. Certain findings couldn’t just sit in a report; they also had to be written as a standing marker at the spot they applied to, so the next run — or I — would see them without digging through old chat logs.</p>

<p>That rule was clear in the source instructions. It didn’t survive the handoff. Each subagent was told, in effect, “list the findings you’d flag for a human in your report.” Reasonable-sounding, and it dropped the second half of the rule: also write the marker. The subagents did good work otherwise and the reports were accurate. The markers just never got written, because the instruction that would have produced them got summarized away between the rulebook and the delegation.</p>

<p>Nobody chose to skip the step. It fell through a summarization gap that nothing was checking.</p>

<hr />

<h2 id="what-makes-a-team">1. What makes it a team</h2>

<p>The easy reading of that bug is that the subagents slipped up. The more useful reading is that the setup never divided responsibility in the first place. It divided labor. Three processes ran instead of one, but the authority over what each could do and had to do was still a single blob, re-derived and re-summarized on every handoff.</p>

<p>That’s the distinction that matters: parallelism makes copies of one undivided authority. It doesn’t split responsibility. A team needs each class of decision to have an owner, needs a delegated task to carry its obligations with it, and needs a finding to be cleared by its owner rather than by whoever reaches it first. Without that, five agents instead of one is five shots at the same mistake, not five narrower areas of accountability.</p>

<p>The fix is unglamorous: the full obligation travels with the delegation, word for word, every time. Not a summary, not a paraphrase, not a pointer to a document. “Follow the rules in the shared doc” is not the same as restating the rule in the subagent’s instructions. The first survives summarization badly. The second has nowhere to lose the requirement, because the rule and the instruction are the same text.</p>

<hr />

<h2 id="auto-flag-never">2. A concrete authority model: AUTO / FLAG / NEVER</h2>

<p>The fix is a three-tier authority manifest, written out in full at the top of every delegation instead of referenced by name.</p>

<p><strong>AUTO — do it without asking, then report what you did.</strong> For mechanical actions only: no judgment, no ambiguity, same input always gives the same right output. Formatting fixes, applying a documented convention, moving something within a boundary that was never in doubt.</p>

<p><strong>FLAG — stop, write the question at the spot it applies to, and do nothing else until it’s answered.</strong> For anything that needs judgment: a classification that could go more than one way, a change that alters what something appears to mean, anything that would require a new standing rule. Writing the flag is the action. There’s no version of this tier where the agent also does the thing “just in case,” however confident it is.</p>

<p><strong>NEVER — a short, absolute list, no exceptions no matter how good the reasoning sounds at the time.</strong> This tier exists to survive a persuasive edge case. Keep it short enough to memorize and specific enough that applying it needs no judgment.</p>

<p>The part that fixes the bug: every subagent dispatch restates this manifest — the specific AUTO/FLAG line for that task — as literal text in its own instructions, not a citation. Pointing an agent at a shared document by name is the exact thing that failed once already. The fix isn’t to trust the reference more. It’s to not depend on a reference surviving summarization at all.</p>

<hr />

<h2 id="the-boundary-line">3. Where the boundary line goes</h2>

<p>An authority model is only as good as where it draws its lines, and the first version drew this one too conservatively to live with. The original rule was blunt: anything already in its intended place needed a confirmation before it moved, anywhere. Safe, but it meant routine reorganizing — splitting a folder into subfolders, renaming a folder once its contents outgrew the name — raised as many questions as a genuinely ambiguous reclassification. Ask about everything and the answers turn into rubber stamps, which defeats the point of asking.</p>

<p>The fix was a narrower line that’s still safe. Restructuring within a top-level category that’s already correct is AUTO; a subfolder split or a rename doesn’t change what category something belongs to. Moving something across a top-level boundary is FLAG; that’s a reclassification, not a tidy-up, however good the new spot looks. Both are “moving a file,” but they answer different questions, and only one needs me in the loop.</p>

<p>The test isn’t “did something move.” It’s “after this move, is it a fundamentally different category of thing.” A rename that changes apparent ownership or purpose is a reclassification even if the file never leaves its folder. A restructure that stays inside the same top-level category isn’t, even if every file in it got renamed and re-nested.</p>

<hr />

<h2 id="verification-both-directions">4. Verification has to run both directions</h2>

<p>Once FLAG items are required, it’s easy to treat “did every ambiguous item get a flag” as the whole verification job. It’s half. The other half is the AUTO side: for every action a subagent called routine, check afterward that it actually stayed inside its authorized boundary, not just that the report said so.</p>

<p>Skipping this half is the more dangerous choice, because the failure is invisible. A missing flag looks incomplete — there’s a gap where an answer should be, and someone notices. An action that was carried out and reported as routine, but actually crossed a line that needed a confirmation, looks like every other handled item in the report. Nothing about it says “check me.” The only way to catch it is to re-derive, independently, whether the boundary held — not to take the report’s word for it.</p>

<p>In practice: before folding a subagent’s work into the record, re-check every move it labeled routine against the rule it was following, using the real before-and-after state rather than the subagent’s summary of it. If something crossed a line it shouldn’t have, that’s not a quiet fix in the log. It’s a finding on its own, because the action already happened and editing the report doesn’t undo it.</p>

<hr />

<h2 id="precedent-confirmation">5. The rule about the rules</h2>

<p>One more case needs its own rule, because it’s the one most likely to feel like a fair exception: mid-task, an agent proposes a plausible new standing rule — not a decision about one file, but something that would become “how we always do this” if nobody pushed back.</p>

<p>The answer is the same however solid the reasoning looks: no agent adopts a new standing rule on its own. It goes through the same gate as any other judgment call — proposed, reviewed, written into the rulebook only after a human has signed off — and only then do future runs rely on it. A one-off mistake affects one file. An unapproved rule that becomes precedent affects every future decision that matches it, for as long as it takes someone to notice it was never approved.</p>

<p>This sits at the same level as the rule against permanent deletion without review. Both are about the same thing: not letting one pass’s judgment turn into permanent, hard-to-reverse state without a checkpoint.</p>

<hr />

<h2 id="generalizing">6. Generalizing past one project</h2>

<p>None of this is specific to a document archive. The same shape fits a software team putting several review agents around a shared codebase: a design reviewer, a security reviewer, a correctness reviewer, each commenting on a change without waiting on the others, plus one executor agent that applies changes based on what comes back.</p>

<p>Wiring several agents to comment on the same artifact is the easy part, and the tooling for it is getting commoditized; plenty of it does async multi-agent review well enough. The hard question is the one this post keeps circling: what is the executor allowed to apply on its own, what needs a human confirmation first, and what checks afterward — independently — that the executor stayed inside that line rather than trusting its own account of what it did?</p>

<p>That last part is easy to skip. A system that only checks “did the reviewers comment” looks fine right up until an executor acts past its authority and files it as routine. Make the authority manifest explicit, restate it at every delegation instead of linking to it, and verify in both directions. Then the agents start behaving like a team with real boundaries instead of one authority running in parallel.</p>

<hr />

<h2 id="end-state">End state</h2>

<ul>
  <li>A specific failure — a paraphrased delegation dropping a hard requirement — traced back to its cause: authority and obligation weren’t written down, so they didn’t survive the handoff.</li>
  <li>A three-tier authority manifest (AUTO / FLAG / NEVER), restated in full at every delegation instead of referenced by name, closing the gap that caused the failure.</li>
  <li>A boundary narrow enough to live with: restructuring inside a correct scope is routine, crossing the scope boundary is a judgment call, however similar the two moves look.</li>
  <li>Verification in both directions — that ambiguous items got flagged, and that routine-labeled actions stayed in bounds — since the second failure hides better than the first.</li>
  <li>A rule about rules: no new standing rule gets adopted mid-task on an agent’s judgment until a human has confirmed it.</li>
</ul>

<p>The throughline across both posts: more agents only helps if something explicit keeps them from acting like one. That something isn’t a scheduler, and it isn’t a chat channel — <a href="/recurring-review-council-async-flag-protocol/#two-channels">both of those already existed</a> before this problem showed up. It’s an authority contract, stated in full at every handoff and checked both ways afterward, and the idea is the same whether the team is reviewing tax documents or a pull request.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The previous post split one generalist sweep into several narrower agents, each with its own scope, talking to each other through a shared inline-flag protocol. That solved coordination: agents could leave each other messages across a scheduling gap without me relaying them. It didn’t solve the problem underneath. Running several agents in parallel is not the same as having several accountable specialists. Without something more explicit, they collapse back into one authority that happens to run in more than one process.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/agent-authority-boundaries.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/agent-authority-boundaries.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">From One Auditor to a Council of Specialists</title><link href="https://www.unwiredsolutions.com/recurring-review-council-async-flag-protocol/" rel="alternate" type="text/html" title="From One Auditor to a Council of Specialists" /><published>2026-08-29T00:00:00+00:00</published><updated>2026-08-29T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/recurring-review-council-async-flag-protocol</id><content type="html" xml:base="https://www.unwiredsolutions.com/recurring-review-council-async-flag-protocol/"><![CDATA[<p>The <a href="/google-drive-file-archive-canonical-reorg/">archive-reorg post</a> described a weekly auditor: one skill that swept my document archive and the reference notes layered on top of it, fixed the mechanical problems, and flagged the rest for me. That was enough while “the rest” meant judgment calls about naming and placement. It stopped being enough once the reference notes grew past a single domain. The questions changed from “is this filed correctly” to “does this financial strategy still make sense,” “does this estate plan still cover what it was written to cover,” “does this reading of a lab result still hold up.” Those need a specialist, not a librarian.</p>

<p>So I split the one generalist sweep into several narrower reviewers, each on its own schedule. That created a new problem. These agents don’t share a conversation, and they can run days apart. How do they talk to each other, and to the person they work for?</p>

<ul>
  <li><a href="#where-we-left-off">Where the series left off</a></li>
  <li><a href="#mechanical-vs-strategic">Splitting mechanical from strategic</a></li>
  <li><a href="#the-cadence-gap">The cadence gap</a></li>
  <li><a href="#two-channels">Two channels, one grammar</a></li>
  <li><a href="#the-sandbox-boundary">A sandbox boundary that broke the fallback</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="where-we-left-off">0. Where the series left off</h2>

<p>The original auditor did three things on every run: check the archive against its naming and placement rules, check whether the reference notes were still in sync with new source material, and check the notes for internal rot — dead cross-references, stale metadata, sections that had drifted out of shape. All three are mechanical. Given the rule and the data, there’s one right answer and you don’t need any special training to find it.</p>

<p>That covered the need until the reference notes spread across several domains: finance, legal, health, identity. Each one carries open questions with no mechanically correct answer. Does this asset allocation still match the risk tolerance it was built for? Does this beneficiary designation still line up with the estate plan? Does a new lab result change what’s worth prioritizing? Answering those well takes a domain expert’s judgment, applied on a schedule, against outside standards that keep moving — tax law, medical guidance, immigration rules.</p>

<p>Piling that onto the weekly sweep would have made one skill responsible for both “is this file named correctly” and “is this retirement strategy still sound.” Those are two very different jobs, and one skill wasn’t going to do both well.</p>

<hr />

<h2 id="mechanical-vs-strategic">1. Splitting mechanical from strategic</h2>

<p>The weekly sweep kept its job unchanged: cheap, mechanical, runs often, fixes the obvious stuff, flags the rest. Each domain then got its own reviewer on a monthly schedule. Each reviewer has a persona (a tax planner for finance, an estate and immigration attorney for legal, a physician for health), a mandate written down in one place, and a narrow job: read the domain’s notes, check whether the strategy they encode still holds up against current best practice, and update them if it doesn’t.</p>

<p>Every reviewer carries the same boundary, worded identically in each persona: advisors advise, they don’t execute. The finance reviewer can recommend rebalancing, but it never places a trade. The legal reviewer can point out that a beneficiary designation looks stale, but it never files anything with an institution. The rule is absolute on purpose. A note that appears to grant an exception doesn’t get to override it.</p>

<p>If you’re building something similar: one frequent shallow sweep that owns mechanical correctness everywhere, plus one slower deep reviewer per domain for the calls that need real expertise. Resist the urge to let the fast sweep pick those up just because it’s already running. That’s the pressure that wears the split down over time.</p>

<hr />

<h2 id="the-cadence-gap">2. The cadence gap</h2>

<p>Splitting by depth leaves a gap. The fast sweep runs several times a week; the deep review runs monthly. When the fast sweep turns up something that needs expert judgment, it can’t sit untouched for three weeks until the monthly review notices it. It also shouldn’t get a quick guess passed off as a considered answer.</p>

<p>So the fast sweep answers it right away, but against a shallower standard: a condensed version of the persona instead of the reviewer’s full mandate, with the answer labeled as shallow. It then queues the item for the next deep review, marked higher priority than the rest of the backlog because it has had less scrutiny so far.</p>

<p>A fast answer and a considered one should never look the same in hindsight. If you answer with less depth than you’d like, say so in the record. Otherwise nobody knows there was a gap to close, and the deep review layer might as well not exist.</p>

<hr />

<h2 id="two-channels">3. Two channels, one grammar</h2>

<p>None of this works unless the separately-scheduled agents can communicate — with the person they work for, and with each other across the gap between a Wednesday sweep and next month’s review. Two channels cover it, both using the same inline mechanism so there’s nothing new to learn for the second one.</p>

<p><strong>Human to agent.</strong> I annotate a note inline, next to whatever it’s about: a question, a correction, an answer to something asked earlier. On its next pass the agent finds every annotation, resolves what it can — research it, act on it, apply the fix — and removes the marker once it’s handled. It never deletes an unanswered one just to clear it. In practice, three marker types cover it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; [!human-question]
&gt; Is the online renewal window actually 90 days, or 60?

&gt; [!human-comment]
&gt; That case really did run 2009 to 2011. Don't re-flag the date.

&gt; [!human-answer]
&gt; Yes, go ahead and standardize those filenames.
</code></pre></div></div>

<p>A question gets researched and answered in place. A comment gets treated as an instruction and acted on. An answer resolves whatever it’s replying to. All three get removed once handled, with a one-line note in the file’s history log about what happened, rather than left in place.</p>

<p><strong>Agent to agent</strong> (and agent back to human). When a finding needs judgment the finding agent doesn’t own, or the fast sweep wants to leave something for the deep reviewer, it uses the same inline marker, addressed by name, at the spot it applies to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; [!agent-question]
&gt; For tax-reviewer: this account's cost basis doesn't match the last statement.
&gt; Flagging instead of guessing which figure is stale.

&gt; [!agent-comment]
&gt; For docs-reviewer: this note has gotten long enough to need a table of
&gt; contents. Not my job to fix, just noting it.
</code></pre></div></div>

<p>Only a reply from the named party clears it. An agent that finds a marker addressed to someone else doesn’t get to decide it’s close enough and resolve it. That would defeat the point of naming an owner.</p>

<p>This carries over to a setting with nothing to do with document filing. Picture a codebase with a handful of review agents attached to it instead of one linter — a security reviewer, a design reviewer, a migration reviewer — plus one agent that applies changes. The security reviewer finds something outside its own lane and leaves <code class="language-plaintext highlighter-rouge">[!agent-question]</code> / <code class="language-plaintext highlighter-rouge">For migration-reviewer: this index change looks like it'll lock the table under load, can you confirm before it merges?</code> right on the diff. The migration reviewer, not the security reviewer, is the one who clears it, and not before then. The grammar and the ownership rule carry over unchanged; only the domain is different.</p>

<p>Two small details made this reliable:</p>

<ul>
  <li>A live marker has to look structurally different from a past mention of a resolved one. A note’s history log says things like “resolved an open question about X” in plain prose. The live marker uses a fixed, greppable form — a specific block-quote prefix here — that a search for “question” won’t confuse with a sentence describing a closed one.</li>
  <li>Dedup before writing. When several runs touch the same note, each one checks for an existing open marker on the same issue before adding another. Otherwise a slow-to-answer item collects a duplicate every run that notices it.</li>
</ul>

<hr />

<h2 id="the-sandbox-boundary">4. A sandbox boundary that broke the fallback</h2>

<p>One failure here is worth writing down, because the design didn’t predict it. It only turned up once the thing ran unattended.</p>

<p>The fast sweep’s fallback for a strategic question (from <a href="#the-cadence-gap">the cadence gap</a> above) was meant to read the relevant reviewer’s mandate document and apply just the one relevant section — cheaper than the full procedure, but still based on the real standard. That worked in an interactive session. On a scheduled run it didn’t: the cross-skill file reads failed every time, across every domain, on the same day the same paths read fine interactively. So this wasn’t a bad path or a flaky file. A scheduled run can’t reliably read another task’s files at all.</p>

<p>The fix was to stop routing around the boundary and plan for it. There’s now a shared one-paragraph fallback — the same persona summary for every domain — living in a document every skill already loads for other reasons. When the unattended read would fail, the sweep uses that instead of burning a turn on an attempt it knows will fail. It’s a downgrade from the full standard, and it’s labeled as one. That’s acceptable because the alternative was a crash or a skipped check, both worse than a shallow but honest answer.</p>

<p>Don’t assume something that works interactively works the same on a schedule. Test the real failure on a real scheduled run before you design around it. “This should work” and “this fails this exact way every time” led to different fixes here.</p>

<hr />

<h2 id="end-state">End state</h2>

<ul>
  <li>One fast mechanical sweep, unchanged in scope, still owning naming, placement, freshness, and internal-consistency checks across everything.</li>
  <li>One deeper reviewer per domain, each with its own persona and mandate, none of them allowed to execute anything irreversible no matter what a note seems to authorize.</li>
  <li>A labeled fallback standard for the gap between a fast finding and the next deep review, so a shallow answer never reads as a considered one.</li>
  <li>Two communication channels — human to agent and agent to agent — sharing one mechanism: inline, addressed by name, cleared only by the named party, and easy to tell apart from a past mention that’s already closed.</li>
  <li>A known limit on what a scheduled run can reach, found by testing the real thing instead of trusting the design.</li>
</ul>

<p>What this setup still lacked mattered more than any single piece above: one shared answer to who is allowed to act on what without asking first, across all of these separate agents rather than inside any one of them. <a href="/separation-of-duties-ai-agent-authority-manifest/">The next post</a> is about that gap and the failure that exposed it.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The archive-reorg post described a weekly auditor: one skill that swept my document archive and the reference notes layered on top of it, fixed the mechanical problems, and flagged the rest for me. That was enough while “the rest” meant judgment calls about naming and placement. It stopped being enough once the reference notes grew past a single domain. The questions changed from “is this filed correctly” to “does this financial strategy still make sense,” “does this estate plan still cover what it was written to cover,” “does this reading of a lab result still hold up.” Those need a specialist, not a librarian.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/recurring-review-council.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/recurring-review-council.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Turning a Health Chart Into an Actual Plan, and Keeping It Current</title><link href="https://www.unwiredsolutions.com/longevity-plan-monthly-review/" rel="alternate" type="text/html" title="Turning a Health Chart Into an Actual Plan, and Keeping It Current" /><published>2026-08-10T00:00:00+00:00</published><updated>2026-08-10T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/longevity-plan-monthly-review</id><content type="html" xml:base="https://www.unwiredsolutions.com/longevity-plan-monthly-review/"><![CDATA[<p>Two earlier posts here cover getting health data collected (<a href="/health-connect-google-drive-sync-android/">synced automatically from a phone into a structured file</a>) and getting it organized (<a href="/google-drive-file-archive-canonical-reorg/">filed into a clean, current-state layer on top of a personal archive</a>). Neither one, on its own, changes what you do at a doctor’s visit. A well-organized chart is still a chart. This post is about turning it into a plan, and keeping that plan from going stale the way health resolutions usually do.</p>

<ul>
  <li><a href="#what-it-is">What this actually is (and isn’t)</a></li>
  <li><a href="#strictly-additive">The rule that makes it useful: strictly additive</a></li>
  <li><a href="#structure">Structure: reasoning attached to every line, not just a task list</a></li>
  <li><a href="#monthly-review">The monthly review loop</a></li>
  <li><a href="#silent-is-stale">A silent plan is a stale plan</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="what-it-is">0. What this actually is (and isn’t)</h2>

<p>Call it a longevity plan or a healthspan plan, whatever you like. The content is a short, chart-derived list of labs, imaging, functional tests, and behavior changes worth raising at a doctor’s visit, each one tied to a specific reason from the chart rather than a generic “things people over 50 should think about” list.</p>

<p>Two framing choices matter more than anything in the content:</p>

<p><strong>It’s not a diagnosis or a treatment plan.</strong> It’s preparation material. The goal is a better-informed conversation at an appointment, not a replacement for one. That distinction has to survive being written down, or the document turns into something it shouldn’t be.</p>

<p><strong>It exists to coordinate across specialists who don’t see each other’s records.</strong> A cardiologist, an endocrinologist, and a sleep clinic each get a narrow slice of the picture. Nobody except the person living in that body — and now this document — sees all three slices at once. That’s the gap it fills.</p>

<hr />

<h2 id="strictly-additive">1. The rule that makes it useful: strictly additive</h2>

<p>The most important constraint on this document: it never duplicates what a standard checkup already covers. If a lab is already on the checklist for an upcoming visit, it doesn’t also appear here. It stays where it already lives, once.</p>

<p>This matters because the common failure mode is a “personalized” health document that’s mostly generic filler — “eat more vegetables,” “get enough sleep” — with a name at the top. That kind of document loses your trust by the second read, because none of it needed to know anything about you. Here it’s the opposite: every line exists because of something specific in the chart, and a section at the bottom lists what the standard checklist already handles well, so nothing gets duplicated by accident later.</p>

<p>The list stays narrow and evidence-weighted rather than aspirational. Longevity medicine covers a wide range of tests and interventions with very different evidence behind them; this document sticks to the well-established, low-downside end and skips the speculative end.</p>

<hr />

<h2 id="structure">2. Structure: reasoning attached to every line, not just a task list</h2>

<p>The plan has three categories — additive labs, imaging and functional tests, behavioral priorities — and every item carries its reasoning inline, not just a name. Not “ApoB” but why ApoB, given what the last lipid panel showed and what ApoB catches that a standard panel doesn’t. Not “resistance training” but which gap in an otherwise-strong fitness picture it addresses.</p>

<p>That’s the point. A bare list of test names is something you could get from a search engine. A list where every item explains why it matters for this chart only exists because someone read the chart.</p>

<p>The tracking half is a checklist: checkbox, status, and once something is done at a visit it moves to a completed line with a date and a pointer to where the result landed. The plan should show progress over time, not sit as a wish list that gets ignored or forgotten.</p>

<hr />

<h2 id="monthly-review">3. The monthly review loop</h2>

<p>This is a separate, recurring task from the one that built the plan in the first place — a scheduled monthly pass, per person, that:</p>

<ol>
  <li>Reads the plan’s current checklist.</li>
  <li>Reads the chart notes the plan is derived from (the running summary, the lab-trend table, the wearable-trend table).</li>
  <li>Checks whether anything on the checklist has since been actioned, and whether any new finding changes what should be prioritized.</li>
  <li>Updates the plan in place — never a rewrite from scratch, just edits to what changed.</li>
  <li>Logs a dated entry describing exactly what changed this pass.</li>
</ol>

<p>The cost discipline is the part worth calling out. A monthly review that re-extracts everything from source documents each time would be expensive, and it would duplicate work two other parts of this system already do: filing new documents, and a weekly audit that catches anything not yet summarized. So this review only reads the already-processed notes. If it spots a source document newer than the plan’s last update that hasn’t been folded into those notes yet, it flags that gap for the other layer rather than doing the extraction itself. It stays cheap by staying narrow about whose job is whose.</p>

<hr />

<h2 id="silent-is-stale">4. A silent plan is a stale plan</h2>

<p>Even a review that finds nothing to change still writes a log entry: “reviewed, no changes, N items still open, nothing new shifts priorities.” That’s not busywork. A plan with no review history looks the same as a plan nobody checks anymore, so a monthly cadence only means something if “no news” and “no one looked” stay distinguishable on the page.</p>

<p>This is the same instinct as a rule from <a href="/google-drive-file-archive-canonical-reorg/#living-notes-honest">the archive-reorg post</a>: a stale note is worse than no note, because it looks authoritative right up until it’s wrong. Here it’s a habit rather than a check — silence gets logged on purpose, so it reads as “confirmed current” instead of “abandoned.”</p>

<hr />

<h2 id="end-state">End state</h2>

<ul>
  <li>A short, chart-derived plan per person — labs, imaging, functional tests, behavior changes — every item carrying its own reasoning, not a generic checklist with a name on it.</li>
  <li>Strictly additive to standard care, with a record of what’s already covered elsewhere, so scope can’t creep.</li>
  <li>A visible progress tracker: checkboxes move to dated, linked completions instead of sitting untouched.</li>
  <li>A monthly review that stays cheap by only reading already-processed notes, handing off anything that needs real extraction to the layer built for it.</li>
  <li>A logged entry on every review, changes or not — the plan’s own proof that someone is still watching it.</li>
</ul>

<p>The series so far is one idea at three levels: collect the data, organize the data, then use it for something, and don’t let any of the three quietly stop. This is the “use it” layer, and the monthly loop is what keeps it from becoming one more good intention that fades by March.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Two earlier posts here cover getting health data collected (synced automatically from a phone into a structured file) and getting it organized (filed into a clean, current-state layer on top of a personal archive). Neither one, on its own, changes what you do at a doctor’s visit. A well-organized chart is still a chart. This post is about turning it into a plan, and keeping that plan from going stale the way health resolutions usually do.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/longevity-plan-review-loop.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/longevity-plan-review-loop.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Syncing Samsung Health Data to Google Drive via Android Health Connect</title><link href="https://www.unwiredsolutions.com/health-connect-google-drive-sync-android/" rel="alternate" type="text/html" title="Syncing Samsung Health Data to Google Drive via Android Health Connect" /><published>2026-08-06T00:00:00+00:00</published><updated>2026-08-06T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/health-connect-google-drive-sync-android</id><content type="html" xml:base="https://www.unwiredsolutions.com/health-connect-google-drive-sync-android/"><![CDATA[<p>Two people in my household wear Samsung devices and use Samsung Health. Its dashboard is fine for a quick glance, but it doesn’t give you a portable, queryable, long-term record, and it can’t show trends across months or years, or across two people at once.</p>

<p>This is the Android app I built to fix that. It reads everything Health Connect exposes, aggregates it sensibly, and appends it to a per-person CSV file in Google Drive. No backend server, no third-party service, just the phone and a Drive folder.</p>

<p>The source is open: <a href="https://github.com/espaillato/HealthSync">espaillato/HealthSync</a>. The sections below are the design decisions and bugs behind it.</p>

<ul>
  <li><a href="#why-this-exists">Why this exists: source data for something else</a></li>
  <li><a href="#why-health-connect">Why Health Connect, not Samsung Health directly</a></li>
  <li><a href="#architecture">Architecture</a></li>
  <li><a href="#data-model">Data model: daily aggregation and the sleep-day problem</a></li>
  <li><a href="#drive-service-account">Google Drive as the sync target (service account gotchas)</a></li>
  <li><a href="#workmanager-bug">The WorkManager scheduling bug that cost a day</a></li>
  <li><a href="#dedup-and-cursors">Dedup, cursors, and retroactive permission grants</a></li>
  <li><a href="#android-14-manifest">Android 14’s second permission-rationale requirement</a></li>
  <li><a href="#two-phones">Running it on two sideloaded phones</a></li>
  <li><a href="#blood-pressure-import">Blood pressure: a hardware limit, a Health Connect wall, and a manual import path</a></li>
  <li><a href="#getting-started">Running it yourself</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="why-this-exists">0. Why this exists: source data for something else</h2>

<p>Fair question: why build this instead of glancing at the Samsung Health app now and then? The data is source material for something else.</p>

<p>I keep a structured <a href="/google-drive-file-archive-canonical-reorg/">personal document archive</a> organized around a small set of life categories, Health among them. Within each category a <code class="language-plaintext highlighter-rouge">_Core</code> folder holds <em>living reference</em> documents: the kind meant to reflect current understanding and get updated as things change, rather than frozen at whenever someone last wrote them. Keeping a health overview current used to mean sitting down and rewriting it by hand every few months, which is the kind of maintenance that stops happening.</p>

<p>What I’m working toward: have an AI model read the structured data in the archive and use it to spot trends, flag things worth a second look, and keep the <code class="language-plaintext highlighter-rouge">_Core</code> reference documents up to date from real data instead of remembered impressions. That needs real structured data to read. A phone’s health dashboard doesn’t produce it, and neither does a folder of screenshots. A clean, deduplicated, sensibly-aggregated CSV landing in the right place in the archive does. That’s what this app is for: the data-collection layer under a larger system.</p>

<hr />

<h2 id="why-health-connect">1. Why Health Connect, not Samsung Health directly</h2>

<p>Samsung Health has its own proprietary sensor pipeline for a handful of metrics, and reading those directly means special-casing Samsung’s SDK. I didn’t want that. The rule I settled on:</p>

<ul>
  <li>If Health Connect exposes a metric through its normal permission model, read it.</li>
  <li>If it’s noisy at native resolution (heart rate, speed, cadence…), aggregate it — don’t drop it.</li>
  <li>If reading it requires bespoke, vendor-specific code outside Health Connect’s own API, leave it out.</li>
</ul>

<p>That one rule kept the scope honest. It also means the app works for <em>any</em> Health Connect source, not just Samsung Health — body-composition data (weight, body fat, etc.) written by a different app or a different manufacturer’s scale comes through the exact same path.</p>

<hr />

<h2 id="architecture">2. Architecture</h2>

<ul>
  <li><strong>Android app</strong> (Kotlin, single Activity + an app widget) — reads Health Connect via <code class="language-plaintext highlighter-rouge">androidx.health.connect:connect-client</code>, uploads via the Drive v3 API.</li>
  <li><strong>WorkManager</strong> — a once-a-day background sync plus an on-demand “Sync Now” that runs whenever the app is opened.</li>
  <li><strong>Google Drive, service-account auth</strong> — no OAuth login flow on-device, no user-facing Google sign-in. A service account key (JSON) is dropped into the app’s private storage once, and the app authenticates headlessly from then on.</li>
  <li><strong>One CSV per person</strong> in a shared Drive folder — <code class="language-plaintext highlighter-rouge">user1_health_sync.csv</code>, <code class="language-plaintext highlighter-rouge">user2_health_sync.csv</code>, dead simple to open in a spreadsheet or load with <code class="language-plaintext highlighter-rouge">pandas</code>.</li>
</ul>

<p>The CSV schema is deliberately flat:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>timestamp_utc,owner,metric,value,unit,source_record_id
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">source_record_id</code> is the dedup key — more on that below.</p>

<hr />

<h2 id="data-model">3. Data model: daily aggregation and the sleep-day problem</h2>

<p>Early on the app synced Health Connect’s raw records verbatim. That’s fine for a day or two, then steps and heart-rate data balloon into thousands of rows that no longer serve the actual goal: seeing trends over weeks, months, years.</p>

<p>The fix is a metric-by-metric aggregation policy:</p>

<ul>
  <li><strong>Additive metrics</strong> (steps, calories, distance, floors climbed…) → summed per day.</li>
  <li><strong>Dense sampled metrics</strong> (heart rate, speed, cadence…) → daily min/avg/max, three rows a day instead of hundreds.</li>
  <li><strong>Point-in-time metrics</strong> (weight, height, body fat, blood pressure…) → left as-is, unaggregated. There’s exactly one meaningful value per reading; collapsing it into a “daily average” would just be lossy for no reason. (Blood pressure didn’t start out here — <a href="#blood-pressure-import">more on that below</a>.)</li>
</ul>

<p>One wrinkle: <strong>a calendar day is the wrong bucket for sleep.</strong> Steps taken between midnight and midnight map cleanly onto “today.” A sleep session that starts at 11pm and ends at 7am doesn’t. Bucketing it by calendar day either splits one session across two days or assigns it to whichever day it started on, and both corrupt night-over-night trends.</p>

<p>The fix: sleep metrics use a <strong>noon-to-noon “sleep day”</strong> instead of a calendar day (<code class="language-plaintext highlighter-rouge">instant.minusHours(12)</code>, then take the date). Everything else stays on calendar-day boundaries. So the app has two definitions of “day” running side by side, on purpose. That’s worth a loud comment in the code, since a future edit could break it without noticing.</p>

<p>That in turn creates a <strong>completeness problem</strong>: a calendar day is “done” at midnight, but a sleep day isn’t “done” until noon the <em>next</em> day. A single query boundary can’t correctly serve both. The app resolves this by separating two concerns that used to be conflated into one timestamp:</p>

<ul>
  <li><strong>How far back does this query look?</strong> — always as fresh as possible, up to “now.”</li>
  <li><strong>Which aggregated buckets are safe to emit?</strong> — a bucket only gets included once its boundary has actually passed (<code class="language-plaintext highlighter-rouge">isCompleteCalendarDay</code> / <code class="language-plaintext highlighter-rouge">isCompleteSleepDay</code>), regardless of how fresh the query itself was.</li>
</ul>

<p>The sync cursor only ever advances to the latest point that’s safe for <em>every</em> metric — never past a boundary that could still receive more data.</p>

<hr />

<h2 id="drive-service-account">4. Google Drive as the sync target</h2>

<p>A service account is the right tool here: no user-facing login, no refresh-token dance on-device, just a JSON key file and a folder shared with the service account’s email as Editor.</p>

<p>Two gotchas that cost real time:</p>

<p><strong>Service accounts have no storage quota of their own.</strong> <code class="language-plaintext highlighter-rouge">files.create()</code> against a folder the service account only has <em>Editor</em> access to (not ownership) fails with <code class="language-plaintext highlighter-rouge">storageQuotaExceeded</code> — service accounts can only create files up to their own (zero) quota. The workaround is boring but reliable: pre-create the empty CSV files yourself, share the folder, and let the app only ever <code class="language-plaintext highlighter-rouge">files.update()</code> an existing file it never has to “create.”</p>

<p><strong>Dedup has to be data-driven, not timestamp-driven.</strong> Health Connect write sources can backfill <em>already-passed</em> timestamps — a source might not get write permission until well after it started collecting data, then dump hours of retroactive history the moment permission is granted. If the sync cursor had already advanced past that window, that backfilled data becomes permanently unreachable, since the cursor never looks backward. Two things fix this together: the cursor only advances when a sync actually finds <em>something</em> (an empty result never moves it forward), and every upload is deduped against a synthetic, deterministic <code class="language-plaintext highlighter-rouge">source_record_id</code> parsed out of the CSV’s last column before anything gets appended. That combination means a manual “resync everything from scratch” is always safe to run — nothing above the file’s high-water mark can duplicate.</p>

<hr />

<h2 id="workmanager-bug">5. The WorkManager scheduling bug that cost a day</h2>

<p>This is the one worth writing down for anyone else scheduling periodic background work on Android.</p>

<p>The background sync used <code class="language-plaintext highlighter-rouge">PeriodicWorkRequestBuilder(1, DAYS, 1, HOURS)</code> — a one-day interval with a one-hour flex window, <code class="language-plaintext highlighter-rouge">setInitialDelay()</code> computed to land on a specific hour, <code class="language-plaintext highlighter-rouge">ExistingPeriodicWorkPolicy.UPDATE</code> on every app launch so re-opening the app would re-anchor the schedule if the target time ever changed.</p>

<p>On a real device, <code class="language-plaintext highlighter-rouge">dumpsys jobscheduler</code> showed the job’s actual <code class="language-plaintext highlighter-rouge">Minimum latency</code> landing roughly <strong>23 hours later</strong> than the computed initial delay implied it should. Reproducible too: the same offset showed up on two separate phones.</p>

<p>The cause: <strong><code class="language-plaintext highlighter-rouge">setInitialDelay</code> on a periodic work request only shifts when the period <em>starts</em>. It does not let the first execution skip ahead within that period.</strong> The first run still waits out <code class="language-plaintext highlighter-rouge">(interval − flex)</code> beyond the initial delay, like every run after it. With a 1-hour flex on a 24-hour interval, that’s <code class="language-plaintext highlighter-rouge">initialDelay + 23h</code> for the first execution, so almost a full extra day passes before anything runs.</p>

<p>Two separate fixes were needed:</p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">ExistingPeriodicWorkPolicy.UPDATE</code> doesn’t reliably re-anchor</strong> an already-scheduled periodic work to a freshly computed initial delay — confirmed by changing the target hour and watching <code class="language-plaintext highlighter-rouge">UPDATE</code> leave the old cadence in place. Switched to <code class="language-plaintext highlighter-rouge">CANCEL_AND_REENQUEUE</code>, which fully removes and reinserts the WorkSpec instead of trying to patch it in place.</li>
  <li><strong>Drop the flex window entirely.</strong> Omitting it makes the effective flex equal to the full interval, so the first run fires right at the initial delay as intended — the OS still has the whole day to batch/optimize the job exactly as it would with any flex value.</li>
</ol>

<p>Verified with <code class="language-plaintext highlighter-rouge">adb shell dumpsys jobscheduler</code>, comparing the job’s reported <code class="language-plaintext highlighter-rouge">Minimum latency</code> against <code class="language-plaintext highlighter-rouge">adb shell date</code> on the device, rather than reasoning about the API from the docs. The docs don’t mention any of this; the device state showed it plainly.</p>

<hr />

<h2 id="dedup-and-cursors">6. Dedup, cursors, and retroactive permission grants</h2>

<p>One more real bug, caught by actually using the app rather than reading the code: after granting Health Connect a <em>new</em> permission (a metric that wasn’t being read before), the app’s existing sync cursor was already ahead of that metric’s entire history. From the cursor’s point of view, that metric simply started existing the moment the permission was granted — everything Health Connect still retained from before that was silently unreachable.</p>

<p>The fix is a manual “Resync Full History” action: reset the cursor to null, re-run the sync. It’s safe to hit at any time, for any reason, precisely because of the <code class="language-plaintext highlighter-rouge">source_record_id</code> dedup backstop described above — already-uploaded days get filtered out on the way to Drive, not duplicated.</p>

<hr />

<h2 id="android-14-manifest">7. Android 14’s second permission-rationale requirement</h2>

<p>Health Connect’s permission screen needs an activity the OS can deep-link into for “why is this app asking for this” — but the manifest entry for that changed on Android 14, and the two requirements coexist rather than one replacing the other:</p>

<ul>
  <li>Pre-Android-14: an activity-alias responding to <code class="language-plaintext highlighter-rouge">ACTION_SHOW_PERMISSIONS_RATIONALE</code>.</li>
  <li>Android 14+: a <strong>second</strong>, separate activity-alias, with the <code class="language-plaintext highlighter-rouge">VIEW_PERMISSION_USAGE</code> action, the <code class="language-plaintext highlighter-rouge">HEALTH_PERMISSIONS</code> category, and the <code class="language-plaintext highlighter-rouge">START_VIEW_PERMISSION_USAGE</code> permission.</li>
</ul>

<p>Missing the second one doesn’t throw an error — it just makes the permission screen flash open and immediately close, which is a genuinely confusing failure mode to debug from logs alone (<code class="language-plaintext highlighter-rouge">logcat</code> showed both activities created and destroyed within the same millisecond). Both aliases need to be declared side by side; there’s no version-gating trick that lets you skip one.</p>

<hr />

<h2 id="two-phones">8. Running it on two sideloaded phones</h2>

<p>No Play Store listing — this is a two-person household tool, sideloaded via a signed release build on each phone. A couple of things that mattered in practice:</p>

<ul>
  <li><strong>Debug builds let you cheat; release builds don’t.</strong> Getting the Drive service-account key onto a debug build’s private storage is a one-line <code class="language-plaintext highlighter-rouge">adb shell run-as ... cp</code>. Release builds aren’t debuggable, so that’s blocked outright — the real answer is an in-app “Import Key” flow using <code class="language-plaintext highlighter-rouge">ActivityResultContracts.OpenDocument()</code>, letting the user pick the downloaded key file from wherever it landed (Downloads, an email attachment) and having the app copy its bytes into its own storage via the returned <code class="language-plaintext highlighter-rouge">content://</code> URI.</li>
  <li><strong>The UI needed a real “did anything just happen” signal.</strong> The first version only showed “data through: <code class="language-plaintext highlighter-rouge">&lt;cutoff&gt;</code>” — which barely moves day to day and gives zero feedback about whether a sync attempt just ran and failed versus never running at all. Splitting that into two separate fields — <em>last sync attempt</em> (wall-clock time, updates on success or failure) and <em>data through</em> (the actual cutoff) — made failures visible instead of silent.</li>
  <li><strong>Keep the primary action’s position fixed.</strong> The “Sync Now” button used to be positioned directly below the status text, so a longer error message would shove the button further down the screen between taps. Small thing, very annoying in practice — the fix is just reordering the layout so the button has a stable anchor and the status text flows <em>beneath</em> it.</li>
</ul>

<hr />

<h2 id="blood-pressure-import">9. Blood pressure: a hardware limit, a Health Connect wall, and a manual import path</h2>

<p>This one started as a hardware problem. A Bluetooth blood-pressure cuff generally pairs with one phone at a time — fine for one person, awkward for a two-person household unless you buy two cuffs and keep track of whose is whose. When the cuff was paired, the readings flowed through Health Connect cleanly; I confirmed that with real synced data. Rather than double the hardware, the better option was the watch’s on-device blood-pressure feature, which only needs the cuff occasionally for calibration.</p>

<p>That’s where it stopped working. The watch’s BP readings live in a separate app from the main health-tracking one, and that app’s blood-pressure data never reaches Health Connect, on any version. Querying its content provider by hand threw a <code class="language-plaintext highlighter-rouge">SecurityException</code> demanding a <code class="language-plaintext highlighter-rouge">signature|privileged</code> permission. No third-party app can hold that permission, sideloaded or not, and there’s no settings toggle or future update that changes it.</p>

<p><strong>The workaround: make the app a share target.</strong> The BP app does support exporting readings and sharing that export elsewhere — PDF, in the version this was built against (an HTML option existed at some point but wasn’t available by the time this got built, so it isn’t handled). Registering an <code class="language-plaintext highlighter-rouge">ACTION_SEND</code> intent-filter for <code class="language-plaintext highlighter-rouge">application/pdf</code> turns the sync app itself into a destination in the share sheet: export, share, done.</p>

<p><strong>The PDF parsing gotcha deserves its own paragraph.</strong> The export is small and machine-generated with real embedded text, not a scan, so text extraction instead of OCR — good news, since OCR would add a misread-digit risk that this data can’t afford. I built a parser and verified it against a desktop PDF text-extraction library’s output for a real sample export, which laid each reading out as five separate lines. Shipped it, tested on the device, got “0 readings found.” The on-device PDF library, chosen because it could do real extraction without OCR, laid the same file out one line per reading, space-separated fields, no line breaks between them. Two outputs, both correct, for the same PDF. The fix was mechanical once I had the real structure: rewrite the pattern, dry-run it against a captured real extraction before touching the build. The recurring lesson on this project is to check the actual library against the actual device, not another library’s output on the same file.</p>

<p><strong>Not a second upload path.</strong> Confirming an import doesn’t talk to Drive directly. It stages the parsed rows locally and triggers the same background sync Health Connect data goes through, which folds both sources into one upload. One place in the app authenticates and writes to Drive; a second path would drift from it over time.</p>

<p><strong>The aggregation question got revisited once there was real data to check it against.</strong> Blood pressure started in the same bucket as heart rate: dense, fluctuating, aggregate to daily min/avg/max. That was a guess made before any real data existed. Actual exports showed 2-3 deliberate spot readings a day, not hundreds of continuous samples, closer to a scale weigh-in than a heart-rate stream. It moved to the same point-in-time, one-row-per-reading treatment as weight and height. That also dropped some complexity: a metric with no daily bucket has no “is today’s bucket finished yet” problem, which the aggregated version had to handle.</p>

<p><strong>Every export overlaps the last one, so duplicate handling runs every time, not occasionally.</strong> The BP app’s own export options are fixed, overlapping windows (a week, two weeks, a month, three months, year-to-date), so re-exporting routinely re-covers ground already synced. Two layers handle that: each reading’s <code class="language-plaintext highlighter-rouge">source_record_id</code> is derived from its own timestamp, so an already-uploaded reading is skipped automatically, and a second dedup step inside the upload catches what that alone can’t — two overlapping <em>staged but not-yet-synced</em> imports both producing the same ID within a single upload batch.</p>

<p>One more thing: an early debug aid wrote the raw parsed report to external storage so it could be pulled and inspected during development. Once it had served its purpose it kept writing anyway — a name, a date of birth, and every reading, sitting on disk with nothing to clean it up. I removed it once I noticed. The on-screen preview already shows what was parsed, and closing the screen should be the end of it.</p>

<hr />

<h2 id="getting-started">10. Running it yourself</h2>

<p>The <a href="https://github.com/espaillato/HealthSync">repo</a> has a full build-it-yourself guide; the short version is three parts.</p>

<p><strong>Build the APK.</strong> You need JDK 17+ and the Android SDK (<code class="language-plaintext highlighter-rouge">minSdk 26</code>, so any phone on Android 8 or newer that has Health Connect). Then:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/espaillato/HealthSync
cd HealthSync
./gradlew assembleRelease
</code></pre></div></div>

<p>For a quick trial <code class="language-plaintext highlighter-rouge">assembleDebug</code> is fine; for anything you’ll keep updating, set up a release keystore first, because regenerating it later invalidates every future install, so back the <code class="language-plaintext highlighter-rouge">.jks</code> up somewhere durable.</p>

<p><strong>Set up the Google side once.</strong> Create a service account, add a JSON key, and enable the Drive API. In Drive, make a folder shared to the service account’s email as Editor, and pre-create one empty CSV per person inside it, because the service account can’t create files itself (the quota gotcha from <a href="#drive-service-account">section 4</a>).</p>

<p><strong>Install and configure on each phone.</strong> Sideload the APK, tap <strong>Import Drive Key</strong> and pick the JSON file, enter a name (it becomes the CSV owner and the filename), and grant Health Connect permissions on both screens: the per-category one, and the “additional access” one for history and background sync.</p>

<p>After that it syncs on its own around 2am; <strong>Sync Now</strong> forces an immediate run. Sync frequency is a single constant (<code class="language-plaintext highlighter-rouge">SYNC_INTERVAL_DAYS</code> in <code class="language-plaintext highlighter-rouge">SyncWorker.kt</code>). The repo’s <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> will walk an AI coding agent through the same setup if you’d rather not do it by hand.</p>

<hr />

<h2 id="end-state">End state</h2>

<ul>
  <li>One Android app, two installs, one shared Drive folder.</li>
  <li>Roughly 30 Health Connect metrics read, aggregated where it matters, left alone where aggregation would lose information.</li>
  <li>Calendar-day and sleep-day boundaries handled as the genuinely different things they are.</li>
  <li>A background sync that actually lands when it’s supposed to, verified against the real OS scheduler rather than assumed from the API surface.</li>
  <li>A second data source (a manually-shared PDF export, for the one metric Health Connect structurally can’t reach) folding into the same upload path as everything else.</li>
  <li>Zero servers, zero recurring cost, zero third-party services beyond Drive itself.</li>
  <li>Clean, structured data landing exactly where <a href="/google-drive-file-archive-canonical-reorg/">the archive’s Health category</a> expects it — the actual raw material the living-reference layer needs.</li>
  <li>The full source is on GitHub: <a href="https://github.com/espaillato/HealthSync">espaillato/HealthSync</a>.</li>
</ul>

<p>Most of these bugs — the scheduling gap, the flash-closing permission screen, the unreachable backfilled history, the PDF layout mismatch — weren’t visible from reading code or docs. They showed up by running the app on real devices and checking system state: <code class="language-plaintext highlighter-rouge">dumpsys</code>, <code class="language-plaintext highlighter-rouge">logcat</code>, the Drive file contents. Health and scheduling APIs on Android have a wide gap between what the docs say and what the OS does.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Two people in my household wear Samsung devices and use Samsung Health. Its dashboard is fine for a quick glance, but it doesn’t give you a portable, queryable, long-term record, and it can’t show trends across months or years, or across two people at once.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/health-connect-drive-sync.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/health-connect-drive-sync.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What Actually Fixed a Decade of Google Drive Sprawl</title><link href="https://www.unwiredsolutions.com/google-drive-file-archive-canonical-reorg/" rel="alternate" type="text/html" title="What Actually Fixed a Decade of Google Drive Sprawl" /><published>2026-05-11T00:00:00+00:00</published><updated>2026-05-11T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/google-drive-file-archive-canonical-reorg</id><content type="html" xml:base="https://www.unwiredsolutions.com/google-drive-file-archive-canonical-reorg/"><![CDATA[<p>A shared household Google Drive accumulates documents for years before anyone treats it as a system rather than a dumping ground. Mine had two dozen top-level folders, several of them near-duplicates: a typo’d second category next to the real one, two folders that meant almost the same thing under different names, plus a scattering of one-off folders sitting at the same level as major life categories. No convention for whose document something was, whether it was shared, or whether it was a stable reference or a one-time record.</p>

<p>This is the story of fixing that: how the first approach failed at the part that mattered most, and what it produced along the way that made the second approach work.</p>

<ul>
  <li><a href="#starting-mess">The starting mess</a></li>
  <li><a href="#attempt-one">Attempt one: a scripted, rules-based pipeline</a></li>
  <li><a href="#the-ceiling">Where the script hit its ceiling</a></li>
  <li><a href="#what-worked">What actually worked: two skills, one rulebook</a></li>
  <li><a href="#reading-a-file">How the filer actually reads a file</a></li>
  <li><a href="#automation-boundary">The automation boundary: when to just do it vs. when to ask</a></li>
  <li><a href="#living-notes-honest">Keeping the living notes honest, not just the archive</a></li>
  <li><a href="#the-philosophy-doc">Inside the philosophy document</a></li>
  <li><a href="#the-lesson">What carried over from the first attempt</a></li>
  <li><a href="#living-reference">The living-reference layer</a></li>
  <li><a href="#end-state">End state</a></li>
</ul>

<hr />

<h2 id="starting-mess">0. The starting mess</h2>

<p>A read-only audit script walked the archive and produced a blunt table: folder name, subfolder count, file count. The top of that table told the story — near-duplicate category names differing only by a typo or a synonym, several folders with a handful of files each that existed only because something needed somewhere to go at the time, and nothing anywhere indicating whose document it was or whether it was shared.</p>

<p>None of that is unusual. It’s what any shared drive looks like after years of “just put it somewhere for now,” and it doesn’t sort itself out.</p>

<hr />

<h2 id="attempt-one">1. Attempt one: a scripted, rules-based pipeline</h2>

<p>The first pass was straightforward automation: a Python pipeline against the Drive API that classified files with an ordered list of <code class="language-plaintext highlighter-rouge">(category, [keywords])</code> rules, deduplicated exact-content matches by hash with a deterministic scoring function to auto-pick a “winner,” repointed shortcuts instead of leaving them dangling, and — critically — split into a <strong>read-only planning phase</strong> and a <strong>separate, explicitly-scoped apply phase</strong>, so nothing with write access ever ran until the proposed classification had been reviewed as plain CSV data first.</p>

<p>Some of that held up well and is worth keeping regardless of what does the classifying:</p>

<ul>
  <li><strong>A small, closed list of top-level categories</strong>, instead of letting new ones spring up ad hoc.</li>
  <li><strong>A consistent internal shape per category</strong> — a subfolder per person, a <code class="language-plaintext highlighter-rouge">_Joint</code> folder for shared documents, a <code class="language-plaintext highlighter-rouge">_Core</code> folder for reference material, a <code class="language-plaintext highlighter-rouge">General</code> catch-all, plus an explicit <code class="language-plaintext highlighter-rouge">Unsorted</code> bucket so “not yet categorized” is a visible state instead of a silent one.</li>
  <li><strong>Dedup by content hash, not filename</strong>, with duplicates quarantined rather than deleted outright — a script should never get to unilaterally decide a real record is disposable.</li>
  <li><strong>Plan, then apply, as genuinely separate phases.</strong> This is what turned a real failure into a non-event: an actual apply run against roughly 500 planned moves left about 90 failing outright with a 403 <code class="language-plaintext highlighter-rouge">insufficient authentication scopes</code> error, because the cached token had been minted under the planning phase’s narrower, read-only scope. Because the plan itself was just reviewable data and every apply attempt was logged, that failure was just a log line to go fix — not a mess to clean up.</li>
</ul>

<hr />

<h2 id="the-ceiling">2. Where the script hit its ceiling</h2>

<p>What the script couldn’t do was hold a reason, only a keyword. Real placement rules look like “if it proves who you are, it belongs here regardless of who issued it,” or “if a doctor would care about it, it belongs in Health,” or “this abbreviation looks like it belongs in one category but is excluded because of how a particular process works.” Those are judgment calls rather than string matches, and each one eventually showed up as a misfile that had to be caught by hand and turned into another keyword-list exception, in a rule list that only grew.</p>

<p>That ceiling is why the first project’s main output was a document, not a script. Writing down every “if X, then Y, because Z” rule in prose, in one place, as the reasoning rather than a keyword fragment, was worth more than the code that tried to enforce it. Calling the pipeline a failure isn’t quite fair — it did the dedup, the shortcut hygiene, and the plan/apply safety rails correctly, and those still hold. But as a classifier it plateaued well below what the archive needed, and the document it forced into existence outlasted it.</p>

<hr />

<h2 id="what-worked">3. What actually worked: two skills, one rulebook</h2>

<p>The fix replaced the keyword list with something that can actually <em>read</em>: two narrow, purpose-built Claude skills sitting on top of the archive, both anchored to the same reasoning document instead of a hard-coded rule table.</p>

<ul>
  <li><strong>A filer</strong>, triggered by anything that looks like filing work — a specific file pointed at, a batch of downloads to sort, someone just asking “where does this go?” It reads a file’s actual content, decides what it is, and derives the correct name and location from the rulebook.</li>
  <li><strong>An auditor</strong>, triggered by review requests (“are my files organized?”, “anything out of place?”) and also run proactively on a weekly schedule regardless of whether anyone asks. It sweeps the archive for naming/placement/duplicate violations and classifies each finding into “fix it automatically,” “hand off to the filer,” or “ask the user” — never all the way to silent judgment calls.</li>
</ul>

<p>Both skills open with the same requirement: read the rulebook first, every run, and refuse to proceed if it’s missing. Never fall back to memory or convention. That constraint carries a lot of weight. It’s the difference between a rule document a person consults now and then and one an agent reloads fresh every session, which matters because an agent has no memory of last week’s edge-case decision unless something outside it holds that decision in writing.</p>

<hr />

<h2 id="reading-a-file">4. How the filer actually reads a file</h2>

<p>This is where “an agent instead of a script” turns from an abstract upgrade into a concrete one.</p>

<p>Identifying a document means reading it, and real-world scans are messy. The filer’s fallback chain handles that rather than giving up:</p>

<ol>
  <li>Extract embedded text directly, when the file has any.</li>
  <li>If that comes back empty — a scanned, image-only PDF — run OCR against just the first couple of pages. Deliberately not the whole document: identifying what something is and who it’s about doesn’t need every page, and burning that cost on the full file for every scan adds up fast.</li>
  <li>If the OCR text itself comes back garbled — which happens more with some scripts and fonts than others — fall back one level further and actually look at the extracted page image directly instead of trusting the unreliable text. A slightly-rotated or mirrored scanner export gets corrected before that final look, rather than just accepted as unreadable.</li>
</ol>

<p>Only when nothing can be extracted — an encrypted file, an unreadable scan even after all of that — does it stop and ask instead of guessing. That three-tier fallback, ending in “look at the page before giving up,” is not something a keyword-matching script could approximate.</p>

<p>From there the naming itself follows the rulebook directly: a date prefix (only if a date can actually be confidently determined from the content — never invented), a descriptive body specific enough to identify the document without opening it, consistent handling of acronyms and institution names, the original extension preserved.</p>

<hr />

<h2 id="automation-boundary">5. The automation boundary: when to just do it vs. when to ask</h2>

<p>Where a file currently sits changes how much autonomy the filer has, and the reason is simple: a file’s current location is itself information. Something in an inbox-style holding folder isn’t a deliberate choice yet, so the filer renames and moves it outright, no confirmation, and only stops to ask if the content can’t be identified at all.</p>

<p>A file already inside a real category folder is different. Someone put it there on purpose, even if the name is wrong, so moving it always gets proposed and confirmed rather than applied silently. Even a same-folder rename gets a check first: does the new name change what the file appears to be about — who it belongs to, what type of document it is, what it’s for? A pure formatting fix (casing, spacing, date format) goes through without asking. Anything that shifts apparent ownership or purpose comes back as a proposal with the reasoning attached, because that name might have been chosen deliberately, and guessing wrong is worse than asking.</p>

<p>The same instinct shows up in the auditor’s own triage: fix silently only when it’s mechanical and reasoning-free, hand off anything content-dependent to the filer, and ask the user for anything that requires guessing intent — including a flat rule to never silently repoint a broken cross-reference, never invent a missing piece of metadata, and never reorder or merge content without a human confirming the guess first.</p>

<hr />

<h2 id="living-notes-honest">6. Keeping the living notes honest, not just the archive</h2>

<p>The archive isn’t the only thing that can drift — a parallel set of running reference notes (a per-topic, always-current summary, separate from the point-in-time source documents backing it) has two independent ways of rotting, and the auditor checks both, deliberately treating them as unrelated failure modes rather than one:</p>

<p><strong>Freshness against the source archive.</strong> When a new document lands in a category that a note is meant to summarize, that note should get updated as part of filing it — but if that sync step gets skipped, the note quietly falls behind whatever the archive itself now shows. The check is simple: compare the newest relevant source file’s date against what the note’s own metadata claims it reflects. Deliberately <em>not</em> re-extracting anything to check this — that’s real, costly work, and the freshness check is meant to stay a cheap weekly sweep that just flags what’s behind, handing the actual re-sync off separately.</p>

<p><strong>Internal consistency, independent of the archive.</strong> A note can be perfectly in sync with the archive and still be quietly broken, because these are notes a person edits directly, by hand, not exclusively through the filing skill. So the auditor separately checks: cross-references between notes that no longer resolve to anything (a rename or deletion elsewhere silently orphaned the link), metadata that says a note was last touched on one date while its own content log shows a later edit, section ordering drifting back toward an older layout after a manual edit, reference notes nothing links to anymore, and markdown tables where a row’s been hand-edited into having the wrong number of columns. There’s also a specific content-drift heuristic — a “snapshot, not encyclopedia” principle, where general explanatory content that belongs in a shared reference note has crept back into a note that’s supposed to hold only person-specific facts. That one’s explicitly a heuristic to flag for review, not something to silently move on its own judgment.</p>

<p>One category of note gets a dedicated secondary sync path of its own, because it’s structurally different from a narrative summary: a running numeric-trend table, wide format, one column added per new data point, tracked separately from the main note it’s linked from. Keeping that kind of table honest needs its own small set of hard rules, learned the hard way rather than assumed upfront: standardize a new entry’s label against what’s already in the table before adding it as a new row — the same underlying measurement legitimately shows up under different names across different sources, and silently merging two rows that turn out not to be the same thing is worse than a harmless duplicate row flagged for a human to reconcile later. Guard explicitly against transposed or misread digits when transcribing a number by hand. And when a single source file happens to bundle more than one date’s worth of readings together, double-check which figure belongs to which date rather than assuming page order lines up with chronological order — a real, named failure mode, not a hypothetical one.</p>

<p>A few operational choices behind this layer are worth calling out on their own, because they’re the kind of thing that only becomes obvious after building it, not before:</p>

<ul>
  <li><strong>A stale note is worse than no note</strong>, because it looks authoritative right up until it’s wrong. Every note tracks exactly which source files were actually read to produce it — and, just as importantly, which visible-but-unread files were skipped — so “trust this” is always checkable against a concrete list, not just an implicit claim.</li>
  <li><strong>Data sensitivity, residency, and access control are worth deciding deliberately, up front, rather than defaulting into.</strong> A note-taking layer like this tends to outlive its first storage/sync choice, and a later change to that setup can silently move where sensitive content actually lives and who/what can reach it. Whoever’s building this should settle that policy for their own situation before the notes exist, not after.</li>
  <li><strong>There’s no API for “edit this note in place” the way there is for moving a Drive file.</strong> An agent can read source material and draft an updated note, but landing that draft still needs a live local connection to wherever the notes live, or a manual step through the archive’s version history. Automating the extraction and the drafting is most of the value, but it’s worth being clear about where the automation stops instead of implying the whole loop closes itself.</li>
  <li><strong>Plain links back to the source document, not an embedded copy.</strong> Slightly more friction to open a source file from a note, in exchange for zero extra sync infrastructure and something that works identically from any device. A small tradeoff, made deliberately rather than defaulted into.</li>
</ul>

<hr />

<h2 id="the-philosophy-doc">7. Inside the philosophy document</h2>

<p>Stripped of anything specific to my own household, the rulebook’s shape is worth describing on its own, because the <em>shape</em> is the reusable part — and the top-level category list specifically is worth sharing close to verbatim. Landing on a <em>stable</em> category boundary is the hardest part of this whole exercise, harder than any of the automation built on top of it, and it’s also the most reusable across households: what a document <em>is about</em> generalizes; the specifics of any one archive don’t.</p>

<p>The rule that matters more than any individual category name: <strong>every category gets a one-line test, not just a label.</strong> “Finance” as a bare word invites endless debate about where a mortgage statement goes; “if it affects net worth, cash flow, or taxes” settles it in one read, for that document and every future one like it.</p>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>Placement test</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Identity</td>
      <td>Proves who you are — belongs here regardless of who issued it.</td>
    </tr>
    <tr>
      <td>Immigration</td>
      <td>Affects where you’re legally allowed to live.</td>
    </tr>
    <tr>
      <td>Health</td>
      <td>Something a doctor would care about.</td>
    </tr>
    <tr>
      <td>Insurance (non-health)</td>
      <td>Limits financial damage from an adverse event.</td>
    </tr>
    <tr>
      <td>Legal</td>
      <td>Creates or modifies a binding obligation or authority.</td>
    </tr>
    <tr>
      <td>Finance</td>
      <td>Affects net worth, cash flow, or taxes.</td>
    </tr>
    <tr>
      <td>Home</td>
      <td>Changes when you move.</td>
    </tr>
    <tr>
      <td>Work</td>
      <td>Matters only because of where you’re employed.</td>
    </tr>
    <tr>
      <td>Family</td>
      <td>The organizing axis is a specific person (non-primary family member), not a topic — see the override rule below.</td>
    </tr>
    <tr>
      <td>Personal</td>
      <td>Losing it is annoying, not dangerous.</td>
    </tr>
    <tr>
      <td>Individual</td>
      <td>About you as a person, outside of work or family.</td>
    </tr>
    <tr>
      <td>Unsorted</td>
      <td>Explicitly “not yet categorized” — a real bucket, not a place things drift by default.</td>
    </tr>
  </tbody>
</table>

<p>A few things about that list generalize past any one household. It’s short on purpose — a dozen or so buckets is few enough to hold in your head, which matters more than being exhaustive, since an <code class="language-plaintext highlighter-rouge">Unsorted</code> escape hatch means it never <em>needs</em> to be exhaustive. Every row is a test, not a topic — “what would someone searching for this actually assume it’s filed under” beats “what’s this document about” as the question to design each row around. And a couple of categories exist specifically to prevent a subtler failure than plain sprawl: <strong>Individual</strong> exists because “about you, but not work and not family” is a real, recurring bucket that otherwise silently collapses into an ever-vaguer “Personal,” and <strong>Family</strong> deliberately overrides every other category for a defined set of people, because <em>who</em> a document is about occasionally needs to outrank <em>what</em> it’s about — worth deciding once, explicitly, rather than re-litigating per document.</p>

<ul>
  <li><strong>A subfolder-pattern table</strong>, separate from the category table — person-based, joint, by-institution, by-year, by-asset, by-project, or a staging/inbox pattern for anything not yet triaged — because <em>what</em> a document is about and <em>how</em> it should be sliced within its category are genuinely different questions.</li>
  <li><strong>Explicit naming conventions</strong>: a consistent case style, dates always prefixed and always in a sortable numeric form, acronyms handled consistently, every file required to have a real extension.</li>
  <li><strong>Small, explicit exception tables</strong> — a handful of institution names canonicalized to one spelling instead of drifting across variants, and a short list of terms that look like typos but are actually correct and should never get “corrected” or flagged.</li>
  <li><strong>An override rule for a specific subset of people</strong> whose documents get filed by <em>who</em> rather than by <em>topic</em>, regardless of what category the document would otherwise match.</li>
  <li><strong>A conflict-resolution priority ladder</strong> for the genuinely ambiguous cases, plus a fallback heuristic (“who would actually go looking for this document?”) for the ones the ladder still doesn’t resolve.</li>
  <li><strong>An explicit philosophy on shortcuts</strong>: pointers only, never primary storage, never allowed to point at a folder, and — learned the hard way — certain sync-client-managed link files should never be touched directly by an automated process at all, because doing so corrupts the sync state rather than just misfiling something.</li>
</ul>

<p>It ends with a line that’s stuck with me past this one project: <em>clever systems decay, obvious systems persist.</em> The entire document is built around that — every rule is written to be re-derivable by a tired human (or a fresh agent with no memory of the last conversation) glancing at it cold, not just executable by whatever wrote it originally.</p>

<hr />

<h2 id="the-lesson">8. What carried over from the first attempt</h2>

<p>It’s easy to write off the first attempt once the second one works better. But the discipline the script enforced — plan before you apply, quarantine before you delete, log every action, never let the thing doing the classifying also hold write access — still matters with an agent in the classifier’s place. An agent making placement judgment calls needs those guardrails as much as a keyword matcher did, probably more. The ask-vs-auto boundary above is the same instinct pointed at a different kind of decision-maker.</p>

<p>The reasoning document that made the second approach work came directly out of the first attempt’s constraint: a keyword list forces you to write “why does this belong here” as an explicit rule, one category at a time. Without that constraint, the document might never have gotten written.</p>

<hr />

<h2 id="living-reference">9. The living-reference layer</h2>

<p>Most of what lives in a personal archive is a point-in-time record — filed once, correct forever, never meant to change. A smaller category of document is meant to stay current instead. The running reference notes described above are that category — a summary meant to be trusted as accurate <em>today</em>.</p>

<p>Keeping something like that current used to mean remembering to sit down and manually re-edit it every few months, which is exactly the kind of maintenance that quietly stops happening. Now an agent reads whatever new, structured source material lands in a relevant category and folds it into the note that summarizes it, the same way it’s already reading the archive’s own rulebook to decide where things belong. That needs real structured data to read, which is what <a href="/health-connect-google-drive-sync-android/">the Health Connect → Drive sync project</a> exists to produce: clean, structured, machine-readable source data landing in exactly the right place in this same archive, rather than one more pile of point-in-time records nothing goes back and reads again.</p>

<p>Building this inside Google Drive, rather than some other cloud store used as dumb storage, has a payoff that’s easy to miss: the same organized archive ends up queryable three ways, because Drive is a first-class data source for Google’s own AI tooling. The Living Reference notes are one interface — curated, narrow, current-state summaries with links back to source. The archive itself is a second: Gemini can be pointed at that same Drive and asked open-ended questions across the whole corpus, with no separate indexing pipeline or export step to maintain. Drive’s own search and folder browsing is a third, for when a file just needs to be found by hand. None of the three needed extra infrastructure once the archive was clean, and the work that went into cleaning the data is what makes all three trustworthy.</p>

<hr />

<h2 id="end-state">End state</h2>

<ul>
  <li>A small, closed set of top-level categories with a consistent internal shape, carried over unchanged from the first attempt because that part was right from the start.</li>
  <li>Placement and naming decided by something that can actually read a document — text extraction, falling back to OCR, falling back to just looking at the page — not match a keyword against a filename.</li>
  <li>A sharp automation boundary: act freely on genuinely unfiled material and purely cosmetic fixes, always propose and confirm anything that could change what a file appears to mean.</li>
  <li>A weekly audit that checks the archive’s naming and placement <em>and</em> a separate layer of running reference notes for two independent kinds of drift — falling behind new source material, and rotting internally from hand-edits — rather than assuming “in sync” and “internally consistent” are the same thing.</li>
  <li>A <code class="language-plaintext highlighter-rouge">_Core</code>/reference layer maintained less by remembering to revisit it and more by an agent reading real source material on an ongoing basis, with hard-won rules against the specific failure modes (mismatched labels, transposed digits, misattributed dates) that actually showed up.</li>
  <li>Three independent ways to ask the same archive a question — curated reference notes, open-ended natural-language search across the whole corpus, and plain file browsing — none of which needed separate infrastructure, because the underlying data was clean.</li>
  <li>A reasoning document that turned out to be the actual deliverable of the “failed” first attempt, not the classifier it was built to be.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[A shared household Google Drive accumulates documents for years before anyone treats it as a system rather than a dumping ground. Mine had two dozen top-level folders, several of them near-duplicates: a typo’d second category next to the real one, two folders that meant almost the same thing under different names, plus a scattering of one-off folders sitting at the same level as major life categories. No convention for whose document something was, whether it was shared, or whether it was a stable reference or a one-time record.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/google-drive-archive-reorganization.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/google-drive-archive-reorganization.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Windows + WSL Development Environment (Setup Guide)</title><link href="https://www.unwiredsolutions.com/wsl-based-development-environment-setup-windows-linux/" rel="alternate" type="text/html" title="Windows + WSL Development Environment (Setup Guide)" /><published>2025-12-24T00:00:00+00:00</published><updated>2025-12-24T00:00:00+00:00</updated><id>https://www.unwiredsolutions.com/wsl-based-development-environment-setup-windows-linux</id><content type="html" xml:base="https://www.unwiredsolutions.com/wsl-based-development-environment-setup-windows-linux/"><![CDATA[<p>This is how to reproduce my current development environment:</p>

<ul>
  <li><a href="#preconditions">Windows as host OS</a></li>
  <li><a href="#wsl2-ubuntu">WSL2 (Ubuntu) as the development OS</a></li>
  <li><a href="#bash-starship">Bash + Starship</a></li>
  <li><a href="#java-toolchain">Java</a>, <a href="#python-pyenv">Python</a>, <a href="#rust">Rust</a>, <a href="#nodejs">Node.js</a> (clean, isolated, reproducible)</li>
  <li><a href="#vscode-wsl">VS Code + AI (ChatGPT for reasoning, Continue for execution)</a></li>
  <li><a href="#git-ssh-github-privacy">Git hardened for SSH + GitHub privacy</a></li>
  <li><a href="#common-cli-tools">Minimal but powerful CLI tooling</a></li>
</ul>

<p>It isn’t a generic guide. Follow it top to bottom for the same result.</p>

<hr />

<h2 id="preconditions">0. Preconditions</h2>

<ul>
  <li>Windows 10 22H2+ or Windows 11</li>
  <li>Virtualization enabled in BIOS</li>
  <li>A GitHub account</li>
  <li>ChatGPT Plus account (for reasoning; API optional)</li>
</ul>

<hr />

<h2 id="wsl2-ubuntu">1. Install WSL2 + Ubuntu</h2>

<p>Open <strong>PowerShell as Administrator</strong>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">--install</span><span class="w">
</span></code></pre></div></div>

<p>Reboot when prompted.</p>

<p>After reboot, open <strong>Windows Terminal</strong> and select <strong>Ubuntu</strong>.</p>

<p>Create:</p>
<ul>
  <li>Linux username</li>
  <li>Linux password</li>
</ul>

<h3 id="verify-wsl-version">Verify WSL version</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">-l</span><span class="w"> </span><span class="nt">-v</span><span class="w">
</span></code></pre></div></div>

<p>Expected:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ubuntu    Running    2
</code></pre></div></div>

<hr />

<h2 id="2-update-linux-base-system">2. Update Linux Base System</h2>

<p>Inside Ubuntu:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt upgrade <span class="nt">-y</span>
</code></pre></div></div>

<p>Install baseline packages:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  build-essential <span class="se">\</span>
  curl wget unzip zip <span class="se">\</span>
  ca-certificates gnupg lsb-release
</code></pre></div></div>

<hr />

<h2 id="bash-starship">3. Shell: Bash + Starship</h2>

<h3 id="ensure-bash-is-default-shell">Ensure Bash is default shell</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="nv">$SHELL</span>
</code></pre></div></div>

<p>If not <code class="language-plaintext highlighter-rouge">/bin/bash</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chsh <span class="nt">-s</span> /bin/bash
<span class="nb">exec </span>bash
</code></pre></div></div>

<h3 id="install-starship">Install Starship</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sS</span> https://starship.rs/install.sh | sh
</code></pre></div></div>

<p>Enable it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'eval "$(starship init bash)"'</span> <span class="o">&gt;&gt;</span> ~/.bashrc
<span class="nb">exec </span>bash
</code></pre></div></div>

<h3 id="starship-configuration">Starship configuration</h3>

<p>Create config:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.config
nano ~/.config/starship.toml
</code></pre></div></div>

<p>Paste:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">format</span> <span class="p">=</span> <span class="s">"$username$directory$git_branch$git_status$java$cmd_duration $character"</span>
<span class="py">add_newline</span> <span class="p">=</span> <span class="kc">false</span>

<span class="nn">[username]</span>
<span class="py">show_always</span> <span class="p">=</span> <span class="kc">false</span>
<span class="py">style_user</span> <span class="p">=</span> <span class="s">"dimmed white"</span>
<span class="py">style_root</span> <span class="p">=</span> <span class="s">"bold red"</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">"[$user]($style) "</span>

<span class="nn">[directory]</span>
<span class="py">truncate_to_repo</span> <span class="p">=</span> <span class="kc">true</span>
<span class="py">style</span> <span class="p">=</span> <span class="s">"bold cyan"</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">"[$path]($style)"</span>

<span class="nn">[git_branch]</span>
<span class="py">symbol</span> <span class="p">=</span> <span class="s">"  "</span>
<span class="py">style</span> <span class="p">=</span> <span class="s">"bold purple"</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">"[$symbol$branch]($style)"</span>

<span class="nn">[git_status]</span>
<span class="py">style</span> <span class="p">=</span> <span class="s">"yellow"</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">"[$all_status]($style)"</span>

<span class="nn">[java]</span>
<span class="py">symbol</span> <span class="p">=</span> <span class="s">" ☕ "</span>
<span class="py">style</span> <span class="p">=</span> <span class="s">"red"</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">"[$symbol$version]($style)"</span>

<span class="nn">[cmd_duration]</span>
<span class="py">min_time</span> <span class="p">=</span> <span class="mi">500</span>
<span class="py">style</span> <span class="p">=</span> <span class="s">"dimmed white"</span>
<span class="py">format</span> <span class="p">=</span> <span class="s">" [⏱ $duration]($style)"</span>

<span class="nn">[character]</span>
<span class="py">success_symbol</span> <span class="p">=</span> <span class="s">"</span><span class="se">\\</span><span class="s">$"</span>
<span class="py">error_symbol</span> <span class="p">=</span> <span class="s">"</span><span class="se">\\</span><span class="s">$!"</span>

<span class="nn">[hostname]</span>
<span class="py">disabled</span> <span class="p">=</span> <span class="kc">true</span>
<span class="nn">[time]</span>
<span class="py">disabled</span> <span class="p">=</span> <span class="kc">true</span>
<span class="nn">[battery]</span>
<span class="py">disabled</span> <span class="p">=</span> <span class="kc">true</span>
<span class="nn">[memory_usage]</span>
<span class="py">disabled</span> <span class="p">=</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Reload:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">exec </span>bash
</code></pre></div></div>

<p>Verify:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ $
</code></pre></div></div>

<hr />

<h2 id="vscode-wsl">4. VS Code + WSL</h2>

<h3 id="install-vs-code-windows">Install VS Code (Windows)</h3>

<p>Download: https://code.visualstudio.com</p>

<p>Install with defaults.</p>

<h3 id="install-extensions">Install extensions</h3>

<p>In VS Code → Extensions (Windows side):</p>

<ul>
  <li>Remote - WSL</li>
</ul>

<p>Inside the WSL environment (server-side extensions):</p>

<ul>
  <li>Python</li>
  <li>Pylance</li>
  <li>Python Environments</li>
  <li>Debugpy</li>
  <li>Extension Pack for Java</li>
  <li>Maven for Java</li>
  <li>Gradle for Java</li>
  <li>Java Debugger</li>
  <li>Java Test Runner</li>
  <li>Java Dependency Viewer</li>
  <li>ShellCheck</li>
  <li>Continue</li>
</ul>

<h3 id="launch-vs-code-from-wsl">Launch VS Code from WSL</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~
code <span class="nb">.</span>
</code></pre></div></div>

<p>Verify bottom-left shows:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WSL: Ubuntu
</code></pre></div></div>

<hr />

<h2 id="java-toolchain">5. Java Toolchain (SDKMAN)</h2>

<h3 id="install-sdkman">Install SDKMAN</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> <span class="s2">"https://get.sdkman.io"</span> | bash
<span class="nb">source</span> <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.sdkman/bin/sdkman-init.sh"</span>
</code></pre></div></div>

<p>Verify:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sdk version
</code></pre></div></div>

<h3 id="install-jdks">Install JDKs</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sdk <span class="nb">install </span>java 21-tem
sdk <span class="nb">install </span>java 17-tem
sdk default java 21-tem
</code></pre></div></div>

<p>Verify:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java <span class="nt">-version</span>
</code></pre></div></div>

<h3 id="maven--gradle">Maven &amp; Gradle</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sdk <span class="nb">install </span>maven
sdk <span class="nb">install </span>gradle
</code></pre></div></div>

<hr />

<h2 id="python-pyenv">6. Python (pyenv + venv-first)</h2>

<h3 id="install-dependencies">Install dependencies</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  python3 python3-venv python3-pip <span class="se">\</span>
  pipx <span class="se">\</span>
  libssl-dev zlib1g-dev libbz2-dev <span class="se">\</span>
  libreadline-dev libsqlite3-dev <span class="se">\</span>
  libffi-dev xz-utils tk-dev
</code></pre></div></div>

<h3 id="install-pyenv">Install pyenv</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://pyenv.run | bash
</code></pre></div></div>

<p>Add to <code class="language-plaintext highlighter-rouge">~/.bashrc</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PYENV_ROOT</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.pyenv"</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$PYENV_ROOT</span><span class="s2">/bin:</span><span class="nv">$PATH</span><span class="s2">"</span>
<span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>pyenv init -<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>Reload:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">exec </span>bash
</code></pre></div></div>

<p>Install Python:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pyenv <span class="nb">install </span>3.12.8
pyenv global 3.12.8
python <span class="nt">--version</span>
</code></pre></div></div>

<h3 id="project-workflow">Project workflow</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> venv .venv
<span class="nb">source</span> .venv/bin/activate
pip <span class="nb">install</span> <span class="nt">-U</span> pip
</code></pre></div></div>

<hr />

<h2 id="nodejs">7. Node.js (nvm)</h2>

<h3 id="install-nvm">Install nvm</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
<span class="nb">exec </span>bash
</code></pre></div></div>

<h3 id="install-node">Install Node</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvm <span class="nb">install</span> <span class="nt">--lts</span>
nvm <span class="nb">alias </span>default lts/<span class="k">*</span>
node <span class="nt">-v</span>
</code></pre></div></div>

<hr />

<h2 id="rust">8. Rust</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--proto</span> <span class="s1">'=https'</span> <span class="nt">--tlsv1</span>.2 <span class="nt">-sSf</span> https://sh.rustup.rs | sh
<span class="nb">exec </span>bash
rustup component add rustfmt clippy
</code></pre></div></div>

<hr />

<h2 id="common-cli-tools">9. Common CLI Tools</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  jq ripgrep fd-find bat tree htop <span class="se">\</span>
  lsof dnsutils file less httpie shellcheck
</code></pre></div></div>

<p>Aliases:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"alias fd=fdfind"</span> <span class="o">&gt;&gt;</span> ~/.bashrc
<span class="nb">echo</span> <span class="s2">"alias bat=batcat"</span> <span class="o">&gt;&gt;</span> ~/.bashrc
<span class="nb">exec </span>bash
</code></pre></div></div>

<hr />

<h2 id="10-personal-bin-symlinked">10. Personal <code class="language-plaintext highlighter-rouge">~/bin</code> (symlinked)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/projects/oss/dotfiles/bin
<span class="nb">ln</span> <span class="nt">-s</span> ~/projects/oss/dotfiles/bin ~/bin
</code></pre></div></div>

<p>Add to PATH (if not already):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'export PATH="$HOME/bin:$PATH"'</span> <span class="o">&gt;&gt;</span> ~/.bashrc
<span class="nb">exec </span>bash
</code></pre></div></div>

<hr />

<h2 id="git-ssh-github-privacy">11. Git (SSH + GitHub privacy)</h2>

<h3 id="ssh-preference">SSH preference</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> url.<span class="s2">"git@github.com:"</span>.insteadOf <span class="s2">"https://github.com/"</span>
</code></pre></div></div>

<h3 id="conditional-identity">Conditional identity</h3>

<p>Create <code class="language-plaintext highlighter-rouge">~/.gitconfig-github</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[user]</span>
  <span class="py">name</span> <span class="p">=</span> <span class="s">&lt;Name&gt;</span>
  <span class="py">email</span> <span class="p">=</span> <span class="s">&lt;Github Public Email&gt;</span>
</code></pre></div></div>

<p>Add to <code class="language-plaintext highlighter-rouge">~/.gitconfig</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[includeIf "hasconfig:remote.*.url:https://github.com/**"]</span>
  <span class="py">path</span> <span class="p">=</span> <span class="s">~/.gitconfig-github</span>
</code></pre></div></div>

<p>Verify:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--show-origin</span> <span class="nt">--get</span> user.email
</code></pre></div></div>

<hr />

<h2 id="12-final-verification-checklist">12. Final Verification Checklist</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>which java
which python
which node
which cargo
which git
</code></pre></div></div>

<p>All should resolve inside <code class="language-plaintext highlighter-rouge">$HOME</code>.</p>

<p>Prompt should show <code class="language-plaintext highlighter-rouge">$</code>, not <code class="language-plaintext highlighter-rouge">&gt;</code>.</p>

<hr />

<h2 id="end-state">End State</h2>

<p>You now have:</p>

<ul>
  <li>Windows as host</li>
  <li>Linux as dev OS</li>
  <li>Clean multi-language toolchains</li>
  <li>Deterministic shell</li>
  <li>SSH-first Git</li>
  <li>Minimal, professional CLI environment</li>
</ul>

<p>It’s a boring setup, which is the point. It should keep working for years without attention.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is how to reproduce my current development environment:]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.unwiredsolutions.com/assets/images/windows-wsl-development-environment.webp" /><media:content medium="image" url="https://www.unwiredsolutions.com/assets/images/windows-wsl-development-environment.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>