<?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://workwarrior.org/feed.xml" rel="self" type="application/atom+xml" /><link href="https://workwarrior.org/" rel="alternate" type="text/html" /><updated>2026-05-03T14:27:43+00:00</updated><id>https://workwarrior.org/feed.xml</id><title type="html">workwarrior</title><subtitle>Five open-source tools. One command surface. Profile-isolated workspaces for the terminal-first operator.</subtitle><entry><title type="html">Services All the Way Down</title><link href="https://workwarrior.org/2026/04/29/services-all-the-way-down/" rel="alternate" type="text/html" title="Services All the Way Down" /><published>2026-04-29T00:00:00+00:00</published><updated>2026-04-29T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/29/services-all-the-way-down</id><content type="html" xml:base="https://workwarrior.org/2026/04/29/services-all-the-way-down/"><![CDATA[<p>Everything in Workwarrior is a service. The CLI dispatcher (<code class="language-plaintext highlighter-rouge">bin/ww</code>) routes every command to a service script in <code class="language-plaintext highlighter-rouge">services/&lt;category&gt;/</code>. The browser UI is a service. The heuristic compiler is a service. Profile management is a service. Even the weapons are discovered and dispatched as services.</p>

<p>This architecture wasn’t a design decision made upfront. It emerged from the requirement that the system be extensible — that adding a new capability shouldn’t require touching core files.</p>

<h2 id="the-dispatcher">The Dispatcher</h2>

<p><code class="language-plaintext highlighter-rouge">bin/ww</code> is 709 lines. It’s the single entry point for every command. Its job is to:</p>

<ol>
  <li>Check that a profile is active (or that the command is one of the profile-agnostic ones)</li>
  <li>Find the service script for the command</li>
  <li>Validate arguments at the routing level</li>
  <li>Execute the service with the remaining args</li>
</ol>

<p>The discovery mechanism: scan <code class="language-plaintext highlighter-rouge">services/</code> for executables matching the command name. Profile-level services at <code class="language-plaintext highlighter-rouge">profiles/&lt;name&gt;/services/&lt;category&gt;/</code> are checked first — they shadow global services. If no service is found, print an error and exit 1.</p>

<p>This means adding a service is a file operation, not a code change. The dispatcher doesn’t have an explicit registry of known services. It discovers them from the filesystem.</p>

<h2 id="the-service-contract">The Service Contract</h2>

<p>Every service must:</p>
<ul>
  <li>Respond to <code class="language-plaintext highlighter-rouge">--help</code> / <code class="language-plaintext highlighter-rouge">-h</code> with a one-line description and usage example</li>
  <li>Use exit codes: 0 success, 1 user error, 2 system/internal error</li>
  <li>Log via <code class="language-plaintext highlighter-rouge">lib/logging.sh</code>, not raw <code class="language-plaintext highlighter-rouge">echo</code></li>
  <li>Not write to profile directories directly — call lib functions</li>
</ul>

<p>The help output is the documentation. <code class="language-plaintext highlighter-rouge">ww help</code> shows all discovered services with their one-line descriptions, pulled from <code class="language-plaintext highlighter-rouge">--help</code> output at startup. A service that doesn’t implement <code class="language-plaintext highlighter-rouge">--help</code> is invisible to the help system.</p>

<p>Exit codes matter for the browser UI. A <code class="language-plaintext highlighter-rouge">POST /cmd</code> request checks the exit code. Exit 1 means the user made an error — show the error message. Exit 2 means a system problem — show a different error and don’t retry automatically.</p>

<h2 id="the-browser-service">The Browser Service</h2>

<p><code class="language-plaintext highlighter-rouge">services/browser/</code> is the most complex service. It starts a Python HTTP server, manages its process, and shuts it down. It’s also the only service that runs persistently — every other service runs, completes, and exits. The browser service starts a background process and returns immediately.</p>

<p>That asymmetry is handled by the service contract: <code class="language-plaintext highlighter-rouge">ww browser</code> returns 0 if the server started successfully. Status checks use <code class="language-plaintext highlighter-rouge">ww browser status</code>. Shutdown uses <code class="language-plaintext highlighter-rouge">ww browser stop</code>. The dispatcher doesn’t need to know the browser service is different — it just routes the subcommands.</p>

<h2 id="why-this-matters-for-extensions">Why This Matters for Extensions</h2>

<p>The service architecture means Workwarrior is extensible at the seam that matters. You can add a service without touching the dispatcher. You can override a service per-profile without modifying the global service. You can inspect the full service inventory from <code class="language-plaintext highlighter-rouge">ww help</code> without reading source code.</p>

<p>New weapons are services. New data integrations are services. New export formats are services. The 25+ service domains in the current release grew this way — each one is a directory under <code class="language-plaintext highlighter-rouge">services/</code>, each one follows the same contract, each one was added without modifying the ones that existed before it.</p>

<p>The service architecture is not a framework. There’s no base class to inherit, no decorator to register. It’s a filesystem convention and a contract. Conventions are simpler than frameworks, break less often, and are easier to reason about when something goes wrong.</p>

<p>Everything is a service. When in doubt, make it a service.</p>]]></content><author><name></name></author><category term="architecture" /><category term="technical" /><summary type="html"><![CDATA[Everything in Workwarrior is a service. The CLI dispatcher (bin/ww) routes every command to a service script in services/&lt;category&gt;/. The browser UI is a service. The heuristic compiler is a service. Profile management is a service. Even the weapons are discovered and dispatched as services.]]></summary></entry><entry><title type="html">UDAs: TaskWarrior’s Extensible Data Model and What We Did With It</title><link href="https://workwarrior.org/2026/04/29/udas-the-extensible-data-model/" rel="alternate" type="text/html" title="UDAs: TaskWarrior’s Extensible Data Model and What We Did With It" /><published>2026-04-29T00:00:00+00:00</published><updated>2026-04-29T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/29/udas-the-extensible-data-model</id><content type="html" xml:base="https://workwarrior.org/2026/04/29/udas-the-extensible-data-model/"><![CDATA[<p>TaskWarrior’s User Defined Attributes system is one of the most underused features in the tool. You can add arbitrary typed fields to every task — strings, numbers, dates, durations, booleans. Workwarrior treats UDAs as a first-class concept and exposes a full management layer on top of them.</p>

<p>The result: profiles can carry 100+ UDAs covering project metadata, financial fields, compliance tracking, people, equipment, and more — and the browser UI renders all of them in the task inline editor.</p>

<h2 id="what-udas-are">What UDAs Are</h2>

<p>A UDA is a field definition in <code class="language-plaintext highlighter-rouge">.taskrc</code>. You declare the field name, type, label, and optional default value. Once declared, every task can carry that field.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uda.client.type=string
uda.client.label=Client
uda.estimate.type=duration
uda.estimate.label=Estimated Duration
uda.billable.type=numeric
uda.billable.label=Billable Hours
</code></pre></div></div>

<p>These are now first-class fields on every task in the profile. <code class="language-plaintext highlighter-rouge">task add "Design review" client:acme estimate:2h billable:0</code> creates a task with those custom fields populated.</p>

<h2 id="the-uda-registry">The UDA Registry</h2>

<p>Workwarrior maintains a UDA registry per profile with source classification:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">[github]</code> — injected by Bugwarrior from issue tracking services</li>
  <li><code class="language-plaintext highlighter-rouge">[extension]</code> — added by TaskWarrior extensions or hooks</li>
  <li><code class="language-plaintext highlighter-rouge">[custom]</code> — defined by the user via <code class="language-plaintext highlighter-rouge">ww profile uda add</code></li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww profile uda list            <span class="c"># All UDAs with source badges</span>
ww profile uda add goals       <span class="c"># Interactive creation wizard</span>
ww profile uda remove &lt;name&gt;   <span class="c"># Remove with safety warnings</span>
ww profile uda group work      <span class="c"># Group UDAs for batch operations</span>
ww profile uda perm goals nosync  <span class="c"># Set sync permissions per-UDA</span>
</code></pre></div></div>

<p>The source badges matter for sync. A <code class="language-plaintext highlighter-rouge">[github]</code> UDA is owned by Bugwarrior — you probably don’t want to push changes to that field back to GitHub. A <code class="language-plaintext highlighter-rouge">[custom]</code> UDA is yours — sync permissions are yours to set.</p>

<h2 id="sync-permissions">Sync Permissions</h2>

<p><code class="language-plaintext highlighter-rouge">ww profile uda perm &lt;name&gt; nosync</code> marks a UDA as excluded from the github-sync engine. This prevents custom internal fields from appearing in GitHub issue labels or comments during two-way sync.</p>

<p>This is the kind of detail that breaks integrations in the wild: you add a <code class="language-plaintext highlighter-rouge">priority-internal</code> UDA that maps to your team’s internal priority scale, and suddenly it’s showing up as a GitHub label because the sync engine doesn’t know it’s internal. The permission system prevents that.</p>

<h2 id="the-browser-ui-integration">The Browser UI Integration</h2>

<p>The browser UI’s task inline editor renders all UDAs defined in the active profile. You don’t configure this — it reads the profile’s <code class="language-plaintext highlighter-rouge">.taskrc</code>, discovers all UDA definitions, and builds the edit form dynamically.</p>

<p>This means a profile with <code class="language-plaintext highlighter-rouge">client</code>, <code class="language-plaintext highlighter-rouge">estimate</code>, <code class="language-plaintext highlighter-rouge">billable</code>, <code class="language-plaintext highlighter-rouge">sprint</code>, and <code class="language-plaintext highlighter-rouge">component</code> UDAs gets a task editor with all five fields, correctly typed (text inputs for strings, numeric inputs for numbers, duration pickers for durations). The profile with no custom UDAs gets the standard task editor.</p>

<h2 id="urgency-tuning">Urgency Tuning</h2>

<p>UDAs can contribute to TaskWarrior’s urgency score. If <code class="language-plaintext highlighter-rouge">billable</code> hours is high, the task should float up in priority. If <code class="language-plaintext highlighter-rouge">sprint</code> is the current sprint number, active sprint tasks should be more urgent.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww profile urgency             <span class="c"># Interactive urgency coefficient tuner</span>
</code></pre></div></div>

<p>The tuner shows all available urgency coefficient inputs — due date weight, priority weight, age weight, tag weights, and custom UDA weights — and lets you adjust them with an interactive prompt. The result is written to the profile’s <code class="language-plaintext highlighter-rouge">.taskrc</code>.</p>

<p>Per-profile urgency tuning is the right abstraction. Your work profile might weight <code class="language-plaintext highlighter-rouge">client</code> UDA heavily (client work is always more urgent). Your personal profile doesn’t have a <code class="language-plaintext highlighter-rouge">client</code> UDA and weights due date proximity more.</p>

<h2 id="the-data-model-goes-deep">The Data Model Goes Deep</h2>

<p>100+ UDAs covering: project metadata (phase, epic, component, sprint, story points), financial fields (estimate, actuals, billable, rate), compliance (owner, reviewer, approval date, compliance tag), people (assignee, stakeholder, reporter), and equipment (asset ID, location, maintenance date).</p>

<p>These aren’t built-in. They’re documented patterns in <code class="language-plaintext highlighter-rouge">docs/setups/</code> that users can apply to their profiles using <code class="language-plaintext highlighter-rouge">ww profile uda group &lt;name&gt;</code>. The templates are starting points, not requirements. The data model is yours.</p>]]></content><author><name></name></author><category term="technical" /><summary type="html"><![CDATA[TaskWarrior’s User Defined Attributes system is one of the most underused features in the tool. You can add arbitrary typed fields to every task — strings, numbers, dates, durations, booleans. Workwarrior treats UDAs as a first-class concept and exposes a full management layer on top of them.]]></summary></entry><entry><title type="html">Why We Wrapped Instead of Built</title><link href="https://workwarrior.org/2026/04/28/why-we-wrapped-instead-of-built/" rel="alternate" type="text/html" title="Why We Wrapped Instead of Built" /><published>2026-04-28T00:00:00+00:00</published><updated>2026-04-28T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/28/why-we-wrapped-instead-of-built</id><content type="html" xml:base="https://workwarrior.org/2026/04/28/why-we-wrapped-instead-of-built/"><![CDATA[<p>TaskWarrior is almost 20 years old. Hledger is over 15. JRNL has been actively maintained for over a decade. These aren’t projects that emerged from a hackathon and might disappear next year. They have communities, changelogs, issue trackers with years of history, and maintainers who care.</p>

<p>Building replacements for any of these tools would mean rebuilding years of accumulated design decisions and then maintaining those decisions indefinitely. Wrapping them means the tools stay excellent, the communities keep them current, and Workwarrior builds only the layer that didn’t exist.</p>

<h2 id="what-that-layer-is">What That Layer Is</h2>

<p>The thing that didn’t exist was the connection layer. TaskWarrior doesn’t know about TimeWarrior. Neither of them knows about JRNL. None of them have a concept of a “profile” — an isolated workspace that all four tools share. None of them have a unified command surface. None of them ship a local web UI.</p>

<p>The connection layer — profile isolation, unified commands, natural language translation, browser dashboard, GitHub sync, weapons — is thin relative to the underlying tools. The underlying tools do the heavy lifting. Workwarrior adds the wiring.</p>

<p>This is the right decomposition. The alternative, a monolithic productivity system that handles tasks, time, journaling, and accounting in a single codebase, exists (several of them). They’re all trapped in a maintenance cycle that gets heavier as the feature surface grows. Workwarrior’s feature surface is bounded by definition: it’s the surface between tools, not the tools themselves.</p>

<h2 id="the-acknowledgement-model">The Acknowledgement Model</h2>

<p>There’s an <code class="language-plaintext highlighter-rouge">tools/acknowledgements.md</code> in the repo. It lists every tool, the maintainers, the license, the upstream repo. This isn’t a legal requirement — it’s a design value. Workwarrior works because these projects exist. The people who built them deserve credit by name.</p>

<p>The extension registry (<code class="language-plaintext highlighter-rouge">ww extensions</code>) takes the same approach. TaskWarrior has a rich ecosystem of community hooks and extensions. Workwarrior indexes them, makes them searchable, and makes them installable from one command. The registry celebrates the ecosystem rather than hiding it.</p>

<p>When a user discovers that <code class="language-plaintext highlighter-rouge">ww gun</code> is powered by a Rust binary called taskgun written by hamzamohdzubair, and follows the link to the GitHub repo, that’s a win. The attribution is explicit. The dependency is visible. The user can contribute to taskgun directly if they want to improve the gun weapon.</p>

<h2 id="when-wrapping-doesnt-work">When Wrapping Doesn’t Work</h2>

<p>There are cases where wrapping breaks down. The wrapper needs to stay thin — when it gets thick, it’s usually a sign that a tool needed a patch rather than wrapping.</p>

<p>The two-way GitHub sync engine is the closest case. There’s no off-the-shelf bidirectional TaskWarrior ↔ GitHub sync solution that fits the profile model. That layer had to be built. Ten library files, a conflict resolution algorithm, field mapping, annotation↔comment sync. It’s the highest-fragility part of the codebase because it’s the part that was actually built from scratch rather than wrapped.</p>

<p>Everything else wraps. The sync engine builds. The difference in fragility is instructive: the built parts are where the complexity lives.</p>

<h2 id="what-gets-contributed-back">What Gets Contributed Back</h2>

<p>When working at the interface layer reveals a limitation in an underlying tool, the right move is to contribute upstream rather than work around it. The profile model revealed several edge cases in how TaskWarrior handles hook execution with non-standard TASKRC paths. Those were reported upstream.</p>

<p>The wrapper relationship is bidirectional. Workwarrior depends on these tools and owes them the bugs found while using them in unusual ways. The upstream communities get better test coverage. The tools get better. Workwarrior benefits directly.</p>

<p>This is what “open source as it gets” means in practice. Not just a public repo and an MIT license. Using the ecosystem, attributing it, finding and reporting bugs, contributing back where possible, being explicit about what you depend on. The whole system works better when the connection layer takes those obligations seriously.</p>]]></content><author><name></name></author><category term="open-source" /><category term="architecture" /><summary type="html"><![CDATA[TaskWarrior is almost 20 years old. Hledger is over 15. JRNL has been actively maintained for over a decade. These aren’t projects that emerged from a hackathon and might disappear next year. They have communities, changelogs, issue trackers with years of history, and maintainers who care.]]></summary></entry><entry><title type="html">Natural Language Should Earn Its Way In</title><link href="https://workwarrior.org/2026/04/27/natural-language-should-earn-its-way-in/" rel="alternate" type="text/html" title="Natural Language Should Earn Its Way In" /><published>2026-04-27T00:00:00+00:00</published><updated>2026-04-27T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/27/natural-language-should-earn-its-way-in</id><content type="html" xml:base="https://workwarrior.org/2026/04/27/natural-language-should-earn-its-way-in/"><![CDATA[<p>There’s a temptation when building a CLI tool in 2024 to make AI the primary interface. Type anything, the model figures it out. No documentation needed. Natural language all the way down.</p>

<p>Workwarrior went the other direction. The heuristic engine is the primary interface. AI is optional, disabled by default, and explicitly opt-in. If you want to type natural language commands, you can — but they go through 627 regex rules first, and only reach an LLM if nothing matches.</p>

<p>This is a deliberate philosophy, not a cost-cutting measure.</p>

<h2 id="the-arguments-for-heuristics-first">The Arguments for Heuristics First</h2>

<p><strong>Determinism.</strong> A heuristic rule either matches or it doesn’t. The output for a given input is always the same. An LLM produces different outputs for the same input on different runs, different days, different model versions. For a system that writes to your task database and time tracking records, determinism matters.</p>

<p><strong>Latency.</strong> A regex match takes microseconds. An LLM API call takes seconds, plus the network round trip. For someone running <code class="language-plaintext highlighter-rouge">ww browser</code> as a persistent dashboard, heuristic commands feel instant. LLM commands feel like waiting.</p>

<p><strong>Privacy.</strong> The heuristic engine processes everything locally. A task description that goes to an LLM API leaves your machine. For professional or client work, that’s a meaningful distinction.</p>

<p><strong>Reliability.</strong> The heuristic engine works offline. It works when your LLM provider is down. It works when you’re in a coffee shop with unreliable internet. It works always.</p>

<h2 id="the-self-improvement-loop">The Self-Improvement Loop</h2>

<p>The most interesting part of the heuristic/AI relationship is the feedback mechanism. Every CMD submission is logged as JSONL: input, route (heuristic or AI), output, success status. Running <code class="language-plaintext highlighter-rouge">ww compile-heuristics --digest</code> reads that log and converts successful AI translations into new heuristic rules.</p>

<p>This creates a loop: the AI handles the edge cases the heuristics can’t, those edge cases become heuristic rules over time, the AI handles fewer things, the heuristics handle more. The AI dependency decreases through use.</p>

<p>It’s a case where AI earns its place by making itself less necessary. The value isn’t persistent dependence — it’s seeding the rule set with patterns that cover real usage.</p>

<h2 id="what-ai-is-good-for">What AI Is Good For</h2>

<p>The cases where AI adds genuine value are the cases where the heuristic coverage is thin: unusual phrasings, compound commands with unusual structure, commands that combine semantics in ways the compiler didn’t anticipate.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"Flag the server migration task as needing review before the deadline"
</code></pre></div></div>

<p>That’s not a standard imperative, declarative, or shorthand phrasing. It’s an unusual sentence that a human would understand immediately. The heuristic engine probably misses it. The LLM handles it.</p>

<p>After the <code class="language-plaintext highlighter-rouge">--digest</code> run, that phrasing becomes a rule. The next person who types something similar gets heuristic speed.</p>

<h2 id="the-configuration-model">The Configuration Model</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/ai.yaml</span>
<span class="na">mode</span><span class="pi">:</span> <span class="s">off</span>              <span class="c1"># Default</span>
<span class="c1"># mode: local-only    # Ollama only — no data leaves machine</span>
<span class="c1"># mode: local+remote  # Local first, remote fallback</span>
</code></pre></div></div>

<p>Three modes. Default is <code class="language-plaintext highlighter-rouge">off</code>. <code class="language-plaintext highlighter-rouge">local-only</code> is for people who have ollama running locally and want AI fallback without external API dependencies. <code class="language-plaintext highlighter-rouge">local+remote</code> for people who want the full fallback chain.</p>

<p>Per-profile overrides in <code class="language-plaintext highlighter-rouge">profiles/&lt;name&gt;/ai.yaml</code>. Work profile might have AI off (deterministic commands for professional tasks). Personal profile might have AI on (more relaxed about sending journal entries to an API).</p>

<p>The controls are available from <code class="language-plaintext highlighter-rouge">ww ctrl</code> and from the browser CTRL panel. No restart required. Toggle AI mode mid-session.</p>

<p>The point is: you decide. The heuristics work without AI. AI helps when you want it to. The system doesn’t require a subscription.</p>]]></content><author><name></name></author><category term="architecture" /><category term="technical" /><summary type="html"><![CDATA[There’s a temptation when building a CLI tool in 2024 to make AI the primary interface. Type anything, the model figures it out. No documentation needed. Natural language all the way down.]]></summary></entry><entry><title type="html">Adding a Service in 20 Lines</title><link href="https://workwarrior.org/2026/04/26/adding-a-service-in-20-lines/" rel="alternate" type="text/html" title="Adding a Service in 20 Lines" /><published>2026-04-26T00:00:00+00:00</published><updated>2026-04-26T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/26/adding-a-service-in-20-lines</id><content type="html" xml:base="https://workwarrior.org/2026/04/26/adding-a-service-in-20-lines/"><![CDATA[<p>The service architecture is designed to be extended. A new service is an executable script in <code class="language-plaintext highlighter-rouge">services/&lt;category&gt;/</code>. The <code class="language-plaintext highlighter-rouge">ww</code> dispatcher discovers it by scanning for executables. No registration step. No config update. Write the script, make it executable, it’s available as <code class="language-plaintext highlighter-rouge">ww &lt;category&gt;</code>.</p>

<p>Here’s a minimal service in full:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nb">source</span> <span class="s2">"</span><span class="nv">$WORKWARRIOR_BASE</span><span class="s2">/lib/logging.sh"</span>

<span class="nv">USAGE</span><span class="o">=</span><span class="s2">"ww myservice — one-line description
Usage: ww myservice &lt;list|info|add&gt;
"</span>

<span class="k">case</span> <span class="s2">"</span><span class="k">${</span><span class="nv">1</span><span class="k">:-}</span><span class="s2">"</span> <span class="k">in</span>
  <span class="nt">--help</span><span class="p">|</span><span class="nt">-h</span><span class="p">)</span>
    <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$USAGE</span><span class="s2">"</span>
    <span class="nb">exit </span>0
    <span class="p">;;</span>
  list<span class="p">)</span>
    <span class="c"># implementation</span>
    log_info <span class="s2">"Listing things..."</span>
    <span class="p">;;</span>
  info<span class="p">)</span>
    <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="k">${</span><span class="nv">2</span><span class="k">:-}</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="o">{</span> log_error <span class="s2">"info requires a name"</span><span class="p">;</span> <span class="nb">exit </span>1<span class="p">;</span> <span class="o">}</span>
    log_info <span class="s2">"Info for: </span><span class="nv">$2</span><span class="s2">"</span>
    <span class="p">;;</span>
  <span class="k">*</span><span class="p">)</span>
    log_error <span class="s2">"Unknown subcommand: '</span><span class="k">${</span><span class="nv">1</span><span class="k">:-}</span><span class="s2">'"</span>
    <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$USAGE</span><span class="s2">"</span>
    <span class="nb">exit </span>1
    <span class="p">;;</span>
<span class="k">esac</span>
</code></pre></div></div>

<p>That’s the contract. Five requirements:</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">#!/usr/bin/env bash</code> + <code class="language-plaintext highlighter-rouge">set -euo pipefail</code></li>
  <li>Responds to <code class="language-plaintext highlighter-rouge">--help</code> / <code class="language-plaintext highlighter-rouge">-h</code></li>
  <li>Exit codes: 0 success, 1 user error, 2 system error</li>
  <li>Logs via <code class="language-plaintext highlighter-rouge">lib/logging.sh</code></li>
  <li>Doesn’t write to profile directories directly — calls lib functions</li>
</ol>

<p>The dispatcher (<code class="language-plaintext highlighter-rouge">bin/ww</code>) routes <code class="language-plaintext highlighter-rouge">ww myservice list</code> to the <code class="language-plaintext highlighter-rouge">list</code> case branch. The help output appears in <code class="language-plaintext highlighter-rouge">ww help</code>. The service is available from the browser UI CMD panel immediately.</p>

<h2 id="profile-level-services">Profile-Level Services</h2>

<p>Services at <code class="language-plaintext highlighter-rouge">profiles/&lt;name&gt;/services/&lt;category&gt;/</code> shadow global services. This means you can have a per-profile version of any service. <code class="language-plaintext highlighter-rouge">ww myservice</code> in the <code class="language-plaintext highlighter-rouge">work</code> profile runs the work-profile version; in the <code class="language-plaintext highlighter-rouge">personal</code> profile it runs the global version.</p>

<p>This is useful for per-profile customizations that shouldn’t affect other contexts. A work profile might have a specialized export service. A client profile might have a service that integrates with the client’s specific tooling.</p>

<h2 id="template-tiers">Template Tiers</h2>

<p><code class="language-plaintext highlighter-rouge">docs/guides/services/service-development.md</code> describes three service tiers:</p>

<p><strong>Tier 1 — Simple</strong> — single-file script, direct TaskWarrior/TimeWarrior calls. Appropriate for services that wrap a single tool command or do simple data reads.</p>

<p><strong>Tier 2 — Compound</strong> — multiple subcommands, uses lib functions for profile management. Appropriate for services with lifecycle (create/list/delete/backup patterns).</p>

<p><strong>Tier 3 — Complex</strong> — state management, external APIs, background processes. Appropriate for services like github-sync that maintain persistent state across invocations.</p>

<p>Most new services start at Tier 1 and grow if needed. The pattern is identical across tiers — the difference is how much of the lib you reach into.</p>

<h2 id="why-this-works">Why This Works</h2>

<p>The architecture is a consequence of the profile model. Because everything resolves through environment variables, a service doesn’t need to know which profile it’s running against. It reads <code class="language-plaintext highlighter-rouge">$WORKWARRIOR_BASE</code>, calls lib functions with the right paths, and the isolation is handled automatically.</p>

<p>This is also why services are safe to contribute. A new service in <code class="language-plaintext highlighter-rouge">services/my-extension/</code> touches nothing that already exists. It can’t break the sync engine. It can’t break the browser UI. It can’t break the CLI dispatcher (unless it introduces a naming conflict, which the dispatcher checks for at startup).</p>

<p>The extension surface is designed to be safe. Build services. Build weapons. Build profile-level customizations. The architecture gets out of the way.</p>]]></content><author><name></name></author><category term="technical" /><category term="open-source" /><summary type="html"><![CDATA[The service architecture is designed to be extended. A new service is an executable script in services/&lt;category&gt;/. The ww dispatcher discovers it by scanning for executables. No registration step. No config update. Write the script, make it executable, it’s available as ww &lt;category&gt;.]]></summary></entry><entry><title type="html">The Control Plane: What ww/system Actually Is</title><link href="https://workwarrior.org/2026/04/25/the-control-plane/" rel="alternate" type="text/html" title="The Control Plane: What ww/system Actually Is" /><published>2026-04-25T00:00:00+00:00</published><updated>2026-04-25T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/25/the-control-plane</id><content type="html" xml:base="https://workwarrior.org/2026/04/25/the-control-plane/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">ww/system</code> is the directory that doesn’t ship. It’s the project’s internal coordination layer — planning documents, task boards, fragility registers, agent role definitions, gate contracts. Users never see it. But it’s the reason the codebase is coherent.</p>

<p>Describing it as “documentation” is underselling it. It’s an operating model for a codebase that’s complex enough to need one.</p>

<h2 id="why-a-control-plane-at-all">Why a Control Plane at All</h2>

<p>A bash codebase with 24 library files, a bidirectional sync engine, a Python web server, and a compiler that generates regex rules has failure modes that aren’t obvious from the code. The sync engine can corrupt data in two places at once. The shell integration injects functions into every shell session — a bug there affects every command a user runs. The CLI dispatcher routes everything — a routing error silently misdirects commands.</p>

<p>Without a way to classify risk and enforce process around high-risk changes, the natural tendency is to edit files and see what breaks. In a project with irreversible failure modes, that’s not acceptable.</p>

<p>The fragility register classifies every major subsystem. HIGH fragility means changes require an extended risk brief, explicit write scope, and adversarial Verifier sign-off before merge. MEDIUM means standard process. LOW means standard process with lighter documentation.</p>

<h2 id="the-four-roles">The Four Roles</h2>

<p>The agent model defines four always-active roles:</p>

<p><strong>Orchestrator</strong> — owns the task board, writes contracts, makes merge decisions. Never writes production code. The constraint is critical: if the Orchestrator writes code, it loses the perspective needed to evaluate that code.</p>

<p><strong>Builder</strong> — works within explicit write scope defined by the Orchestrator. Produces a risk brief before touching any file in the write scope. The risk brief is required — it forces the Builder to think through failure modes before editing.</p>

<p><strong>Verifier</strong> — adversarial test execution. Runs the test suite, looks for regressions, produces a signed checklist. Never implements. The Verifier’s job is to find problems, not fix them.</p>

<p><strong>Docs</strong> — updates CLAUDE.md files, user documentation, and help strings after merge. Runs last, after the implementation is confirmed correct. Documentation written before code is verified tends to describe intended behavior rather than actual behavior.</p>

<p>No agent self-approves. The Orchestrator’s task card is a contract — the Builder implements against it, the Verifier validates against it, the Docs agent updates to reflect it.</p>

<h2 id="the-gate-model">The Gate Model</h2>

<p>Gate A: Spec complete. Contract written, scope agreed.<br />
Gate B: Implementation ready. Risk brief produced.<br />
Gate C: Tests pass. Verifier sign-off.<br />
Gate D: Docs updated. Task closed.</p>

<p>No skipping gates. A change that skips Gate C and goes straight to Gate D is a change that’s not verified. A change that skips Gate B is a change without a risk brief — meaning nobody thought through what could go wrong before touching the code.</p>

<p>The gate model isn’t bureaucracy for its own sake. It’s the minimum process needed to keep irreversible failure modes from reaching production.</p>

<h2 id="it-ships-as-mcp-tooling">It Ships as MCP Tooling</h2>

<p>The same agent model — Orchestrator, Builder, Verifier, Docs — is available to users via <code class="language-plaintext highlighter-rouge">ww mcp install</code>. The MCP server exposes TaskWarrior data to AI agents. The control plane architecture you’d use to build a project with these roles is the same one that builds Workwarrior.</p>

<p>This means the system is self-demonstrating. The development process produces a tool that implements the development process. Users who want to bring the same coordination model to their own projects can deploy it directly.</p>

<p>It’s a strange loop. The cleaner we keep <code class="language-plaintext highlighter-rouge">ww/system</code>, the better the tool that comes out of it.</p>]]></content><author><name></name></author><category term="internals" /><category term="architecture" /><summary type="html"><![CDATA[ww/system is the directory that doesn’t ship. It’s the project’s internal coordination layer — planning documents, task boards, fragility registers, agent role definitions, gate contracts. Users never see it. But it’s the reason the codebase is coherent.]]></summary></entry><entry><title type="html">Two Sync Engines, Two Different Jobs</title><link href="https://workwarrior.org/2026/04/24/two-sync-engines/" rel="alternate" type="text/html" title="Two Sync Engines, Two Different Jobs" /><published>2026-04-24T00:00:00+00:00</published><updated>2026-04-24T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/24/two-sync-engines</id><content type="html" xml:base="https://workwarrior.org/2026/04/24/two-sync-engines/"><![CDATA[<p>Workwarrior ships two GitHub sync engines. They’re complementary, not redundant, and understanding the distinction matters for how you configure each one.</p>

<p><strong>Bugwarrior</strong> — one-way pull from 20+ issue tracking services into TaskWarrior.<br />
<strong>ww github-sync</strong> — two-way bidirectional sync between individual tasks and GitHub issues.</p>

<p>Use both. They handle different parts of the problem.</p>

<h2 id="bugwarrior-pull-everything">Bugwarrior: Pull Everything</h2>

<p>Bugwarrior is a pull tool. Configure it with API credentials for your services — GitHub, GitLab, Jira, Trello, Bitbucket, and 17 more — and <code class="language-plaintext highlighter-rouge">i pull</code> brings all your open issues into TaskWarrior as tasks.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww issues custom          <span class="c"># Configure services interactively</span>
i pull                    <span class="c"># Pull from all configured services</span>
i status                  <span class="c"># Show what was pulled</span>
</code></pre></div></div>

<p>The tasks it creates carry injected UDAs: <code class="language-plaintext highlighter-rouge">githubissue</code>, <code class="language-plaintext highlighter-rouge">githuburl</code>, <code class="language-plaintext highlighter-rouge">githubrepo</code>, <code class="language-plaintext highlighter-rouge">githubauthor</code>. These are tagged <code class="language-plaintext highlighter-rouge">[github]</code> in the UDA source registry — you can see them with <code class="language-plaintext highlighter-rouge">ww profile uda list</code>.</p>

<p>Bugwarrior is strictly one-way. It reads from the service and writes to TaskWarrior. Changes you make to a bugwarrior-created task in TaskWarrior don’t flow back to GitHub. That’s where github-sync comes in.</p>

<h2 id="ww-github-sync-two-way-on-selected-tasks">ww github-sync: Two-Way on Selected Tasks</h2>

<p>The two-way sync engine links individual TaskWarrior tasks to individual GitHub issues. Once linked, changes flow in both directions: update the task locally, it updates the issue. Comment on the issue, the annotation appears on the task.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww issues <span class="nb">enable</span> &lt;task&gt; &lt;issue#&gt; &lt;org/repo&gt;   <span class="c"># Link</span>
ww issues <span class="nb">sync</span>                                 <span class="c"># Two-way sync all linked</span>
ww issues push                                 <span class="c"># Push local → GitHub only</span>
</code></pre></div></div>

<p><strong>What syncs bidirectionally:</strong></p>
<ul>
  <li>Description ↔ Title</li>
  <li>Status ↔ State (pending/started → OPEN, completed/deleted → CLOSED)</li>
  <li>Priority ↔ Labels (H/M/L → priority:high/medium/low)</li>
  <li>Tags ↔ Labels</li>
  <li>Annotations ↔ Comments (with prefixes to prevent sync loops)</li>
</ul>

<p><strong>What pulls only (GitHub → TaskWarrior):</strong></p>
<ul>
  <li>Issue number, URL, repo, author, created date, closed date</li>
</ul>

<p>Conflict resolution is last-write-wins with a configurable conflict window. If both sides changed within the window, the sync reports the conflict rather than silently overwriting.</p>

<h2 id="the-fragility-register">The Fragility Register</h2>

<p>The sync engine is the highest-risk part of the codebase. Ten files in <code class="language-plaintext highlighter-rouge">lib/</code>. Classified HIGH FRAGILITY.</p>

<p>Getting it wrong means data loss in two places simultaneously: a task annotation deleted that was actually a comment on a critical issue, a status change propagated incorrectly that closes an issue that shouldn’t be closed. These failures are hard to detect and harder to reverse.</p>

<p>The development protocol for these files: extended risk brief before any change, explicit write scope, Verifier sign-off before merge, integration tests required. This isn’t excessive — it’s proportionate to the failure modes.</p>

<h2 id="using-them-together">Using Them Together</h2>

<p>The intended workflow:</p>

<ol>
  <li>Configure Bugwarrior to pull from all your services. Run <code class="language-plaintext highlighter-rouge">i pull</code> to get all your issues as tasks.</li>
  <li>Identify the tasks you’re actively working and want to keep in sync. Run <code class="language-plaintext highlighter-rouge">ww issues enable</code> on those.</li>
  <li>Work normally. <code class="language-plaintext highlighter-rouge">i pull</code> refreshes the full issue list. <code class="language-plaintext highlighter-rouge">ww issues sync</code> keeps your linked tasks bidirectionally current.</li>
</ol>

<p>The Bugwarrior-created tasks and the github-sync-linked tasks can coexist in the same TaskWarrior profile. Bugwarrior stamps its tasks with the <code class="language-plaintext highlighter-rouge">[github]</code> source badge on the UDA. github-sync manages its own state file. They don’t interfere with each other.</p>]]></content><author><name></name></author><category term="technical" /><category term="internals" /><summary type="html"><![CDATA[Workwarrior ships two GitHub sync engines. They’re complementary, not redundant, and understanding the distinction matters for how you configure each one.]]></summary></entry><entry><title type="html">Weapons: Gun, Sword, and the Philosophy of Task Manipulation</title><link href="https://workwarrior.org/2026/04/23/weapons-gun-sword-and-task-manipulation/" rel="alternate" type="text/html" title="Weapons: Gun, Sword, and the Philosophy of Task Manipulation" /><published>2026-04-23T00:00:00+00:00</published><updated>2026-04-23T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/23/weapons-gun-sword-and-task-manipulation</id><content type="html" xml:base="https://workwarrior.org/2026/04/23/weapons-gun-sword-and-task-manipulation/"><![CDATA[<p>Weapons are tools that manipulate profile data in special ways — creating, slicing, and packaging tasks beyond what direct TaskWarrior commands produce. They’re called weapons because they’re more powerful than a standard command and require more care.</p>

<p>The current set: Gun, Sword, Next, Schedule. The roadmap: Bat, Fire, Slingshot (purposes TBD). They appear in the browser sidebar as a weapons bar.</p>

<h2 id="sword">Sword</h2>

<p>Sword is native to ww — no external binary. It takes a single task and splits it into N sequential subtasks with dependency chains.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww sword 5 <span class="nt">-p</span> 3                    <span class="c"># Split task 5 into 3 parts</span>
ww sword 5 <span class="nt">-p</span> 4 <span class="nt">--interval</span> 2d     <span class="c"># 2-day intervals between parts</span>
ww sword 12 <span class="nt">-p</span> 2 <span class="nt">--prefix</span> <span class="s2">"Phase"</span> <span class="c"># Custom prefix</span>
</code></pre></div></div>

<p>Each subtask gets:</p>
<ul>
  <li>Description: “Part N of: {original description}”</li>
  <li>The parent task’s project and tags</li>
  <li>A due date offset by N × interval from now</li>
  <li>A dependency on the previous subtask</li>
</ul>

<p>The dependency chain is the key. TaskWarrior’s dependency model prevents a task from being completed if it has uncompleted dependencies. Sword creates a strictly sequential chain — you can’t mark “Part 3 of: Ship API” done until “Part 2 of: Ship API” is done. This is the right behavior for a task that genuinely has ordered phases.</p>

<p>The parent task gets archived (done) after the split, since it’s now represented by the subtask chain. The context — project, tags, urgency tuning — carries forward.</p>

<h2 id="gun">Gun</h2>

<p>Gun wraps <a href="https://github.com/hamzamohdzubair/taskgun">taskgun</a>, a Rust binary for bulk task series generation. Give it a series definition and it creates multiple related tasks with deadline spacing.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww gun &lt;args&gt;
</code></pre></div></div>

<p>The wrapping is thin: Gun respects the active profile (reads TASKRC/TASKDATA from env), passes arguments through to taskgun, and handles the TaskWarrior output. It appears as <code class="language-plaintext highlighter-rouge">ww gun</code> rather than requiring the user to know taskgun’s command syntax.</p>

<h2 id="next">Next</h2>

<p>Next wraps the <code class="language-plaintext highlighter-rouge">next</code> binary — a CFS (Completely Fair Scheduler) inspired algorithm that recommends the optimal next task based on urgency scores, deadline proximity, and current context.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww next
</code></pre></div></div>

<p>One command, one recommendation. The recommendation factors in TaskWarrior’s urgency score plus context signals. When you don’t know what to work on, this is the answer.</p>

<h2 id="schedule">Schedule</h2>

<p>Schedule wraps taskcheck, an auto-scheduler that assigns time blocks to tasks across your calendar.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ww schedule
</code></pre></div></div>

<h2 id="design-principles">Design Principles</h2>

<p>All weapons follow the same rules:</p>
<ul>
  <li>Read TASKRC/TASKDATA from environment (profile isolation is respected)</li>
  <li>Work through <code class="language-plaintext highlighter-rouge">POST /cmd</code> in the browser UI — weapons are available from the weapons bar</li>
  <li>Never modify data outside the active profile’s scope</li>
  <li>Respond to <code class="language-plaintext highlighter-rouge">--help</code></li>
</ul>

<p>The weapons bar in the browser UI shows icons for each weapon. Clicking opens a panel that exposes the weapon’s parameters as a form, so you don’t have to remember the CLI syntax.</p>

<p>The planned weapons — Bat, Fire, Slingshot — don’t have specified purposes yet. The naming scheme is intentional: tools that cut, split, and shape task data. What they shape is still being figured out.</p>]]></content><author><name></name></author><category term="technical" /><summary type="html"><![CDATA[Weapons are tools that manipulate profile data in special ways — creating, slicing, and packaging tasks beyond what direct TaskWarrior commands produce. They’re called weapons because they’re more powerful than a standard command and require more care.]]></summary></entry><entry><title type="html">The Browser UI: No npm Required</title><link href="https://workwarrior.org/2026/04/22/the-browser-ui-no-npm-required/" rel="alternate" type="text/html" title="The Browser UI: No npm Required" /><published>2026-04-22T00:00:00+00:00</published><updated>2026-04-22T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/22/the-browser-ui-no-npm-required</id><content type="html" xml:base="https://workwarrior.org/2026/04/22/the-browser-ui-no-npm-required/"><![CDATA[<p>The browser UI is Python 3 stdlib only. No npm. No node_modules. No build step. No webpack, no bundler, no transpiler. If you can run Python 3, you can run <code class="language-plaintext highlighter-rouge">ww browser</code>.</p>

<p>That constraint — no external dependencies — shaped everything. It’s not a limitation we worked around; it’s a design requirement we built from.</p>

<h2 id="why-it-matters">Why It Matters</h2>

<p>A UI with an npm build pipeline has install friction. <code class="language-plaintext highlighter-rouge">npm install</code> fails for reasons unrelated to your code. Build steps break. Dependencies go stale. Someone with a fresh machine needs to debug package-lock.json before they see anything in the browser.</p>

<p>The alternative: a Python <code class="language-plaintext highlighter-rouge">ThreadingHTTPServer</code> that serves static HTML/CSS/JS from disk, a REST API of 20 endpoints, and a Server-Sent Events channel for real-time updates. The whole server is a single Python file and a directory of static assets.</p>

<p>Start it with <code class="language-plaintext highlighter-rouge">ww browser</code>. It’s running on <code class="language-plaintext highlighter-rouge">localhost:7777</code> in under a second.</p>

<h2 id="the-architecture">The Architecture</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>services/browser/
  server.py         ThreadingHTTPServer, REST API, SSE
  static/
    index.html      Single-page app shell
    style.css       Dark terminal aesthetic, 15+ panel layouts
    app.js          Panel management, command routing, SSE client
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ThreadingHTTPServer</code> handles SSE connections without blocking. An SSE connection holds a socket open — the server pushes profile change events down that socket whenever something changes. Concurrent POST /cmd requests are handled in separate threads without queuing behind the SSE socket.</p>

<p>Static files are served directly from disk on each request. No caching, no preprocessing. Change a CSS file and refresh the browser — the change is visible immediately without a server restart.</p>

<h2 id="the-security-boundary">The Security Boundary</h2>

<p>The biggest risk in a browser UI that executes system commands is remote code execution. <code class="language-plaintext highlighter-rouge">POST /cmd</code> executes a ww subcommand — that has to be constrained.</p>

<p>The constraint: an <code class="language-plaintext highlighter-rouge">ALLOWED_SUBCOMMANDS</code> frozenset. Every POST /cmd request is validated: the first token must appear in the frozenset. No <code class="language-plaintext highlighter-rouge">sh -c</code>. No eval. Unknown subcommands return 400.</p>

<p>This means the attack surface is bounded. An XSS in the UI can only execute valid ww subcommands. Valid ww subcommands can’t exec arbitrary shell commands. The security model is explicit and auditable.</p>

<h2 id="what-the-panels-show">What the Panels Show</h2>

<p>15+ panels covering everything in the active profile:</p>

<ul>
  <li><strong>Tasks</strong> — full task list, inline editing, UDA display, start/stop/done buttons, annotation management</li>
  <li><strong>Time</strong> — today’s total, weekly breakdown, recent intervals, start/stop controls</li>
  <li><strong>Journals</strong> — entry list with expand/collapse, new entry form, multi-journal dropdown</li>
  <li><strong>Ledgers</strong> — account balances, recent transactions, income statement, balance sheet, new transaction form</li>
  <li><strong>CMD</strong> — natural language + direct command input, route indicator (⚡ AI · ⚙ heuristic)</li>
  <li><strong>CTRL</strong> — AI mode toggle, prompt settings</li>
  <li><strong>Sync</strong> — GitHub sync dashboard</li>
  <li><strong>Weapons bar</strong> — Gun, Sword, Next, Schedule icons</li>
</ul>

<p>The SSE channel pushes profile switch events. Switch profiles from the CLI — the browser updates in real time.</p>

<h2 id="the-tradeoff">The Tradeoff</h2>

<p>No npm means no React, no Vue, no TypeScript. The app.js is vanilla JavaScript. For 15+ panels with real-time updates and a command routing layer, that’s manageable. It’s not a framework — it’s a single-page app written carefully in the language that runs in every browser without installation.</p>

<p>The performance is fine. The bundle size is zero. The install step for the browser UI is “you already have Python.”</p>]]></content><author><name></name></author><category term="technical" /><category term="internals" /><summary type="html"><![CDATA[The browser UI is Python 3 stdlib only. No npm. No node_modules. No build step. No webpack, no bundler, no transpiler. If you can run Python 3, you can run ww browser.]]></summary></entry><entry><title type="html">627 Rules and the Case Against Always Needing AI</title><link href="https://workwarrior.org/2026/04/21/627-rules-and-the-case-against-always-needing-ai/" rel="alternate" type="text/html" title="627 Rules and the Case Against Always Needing AI" /><published>2026-04-21T00:00:00+00:00</published><updated>2026-04-21T00:00:00+00:00</updated><id>https://workwarrior.org/2026/04/21/627-rules-and-the-case-against-always-needing-ai</id><content type="html" xml:base="https://workwarrior.org/2026/04/21/627-rules-and-the-case-against-always-needing-ai/"><![CDATA[<p>The heuristic engine came from a simple question: how many natural language command phrasings can you handle with regex before you need a language model? The answer turned out to be: most of them.</p>

<p>627 compiled rules. 19 command domains. 6 phrasing variations per command. No network. No latency. No API key. You type “add a task to review the budget due friday” and the rule for imperative task creation with a date expression fires in microseconds.</p>

<h2 id="why-this-matters">Why This Matters</h2>

<p>If AI is required for basic commands, the system has an external dependency for routine operations. That’s fine when internet is reliable and latency is acceptable. It’s a problem when you’re on a plane, when your LLM provider is down, when you don’t want to send your task descriptions to a third party.</p>

<p>The heuristic engine makes the CMD service useful offline and without configuration. Install workwarrior, run <code class="language-plaintext highlighter-rouge">ww browser</code>, type commands in plain English — it works before you’ve set up a single LLM provider.</p>

<h2 id="what-the-engine-covers">What the Engine Covers</h2>

<p>Six phrasing variations per command, with different confidence scores:</p>

<table>
  <thead>
    <tr>
      <th>Variation</th>
      <th>Confidence</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Passthrough</td>
      <td>1.0</td>
      <td><code class="language-plaintext highlighter-rouge">task add review budget</code></td>
    </tr>
    <tr>
      <td>Imperative</td>
      <td>0.95</td>
      <td><code class="language-plaintext highlighter-rouge">add a task to review the budget</code></td>
    </tr>
    <tr>
      <td>Declarative</td>
      <td>0.90</td>
      <td><code class="language-plaintext highlighter-rouge">I need a task for reviewing the budget</code></td>
    </tr>
    <tr>
      <td>Interrogative</td>
      <td>0.90</td>
      <td><code class="language-plaintext highlighter-rouge">can you create a task to review the budget</code></td>
    </tr>
    <tr>
      <td>Shorthand</td>
      <td>0.90</td>
      <td><code class="language-plaintext highlighter-rouge">task: review budget due friday</code></td>
    </tr>
    <tr>
      <td>Verbose</td>
      <td>0.85</td>
      <td><code class="language-plaintext highlighter-rouge">I would like to add a new task for reviewing the budget</code></td>
    </tr>
  </tbody>
</table>

<p>Date expressions are normalized: “tomorrow”, “next week”, “friday”, “end of month”, “in 3 days” all map to the correct TaskWarrior date format.</p>

<p>Compound commands are split on conjunctions and matched independently:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"finish task 5 and stop tracking"
  → task 5 done
  → timew stop

"add task review and start tracking time"
  → task add review
  → timew start review
</code></pre></div></div>

<p>If any segment fails to match above the confidence threshold (0.8), the entire compound goes to AI. This is the right behavior — a half-translated compound command is worse than no translation.</p>

<h2 id="the-compiler">The Compiler</h2>

<p>Rules aren’t written by hand. <code class="language-plaintext highlighter-rouge">ww compile-heuristics</code> scans the codebase — <code class="language-plaintext highlighter-rouge">bin/ww</code> case branches, <code class="language-plaintext highlighter-rouge">config/shortcuts.yaml</code>, <code class="language-plaintext highlighter-rouge">config/command-syntax.yaml</code> — generates regex patterns, validates them against a synthetic corpus, resolves conflicts, fills coverage gaps, and writes the output.</p>

<p>The <code class="language-plaintext highlighter-rouge">--verbose</code> flag shows every rule with test results. The <code class="language-plaintext highlighter-rouge">--digest</code> flag reads <code class="language-plaintext highlighter-rouge">services/cmd/cmd.log</code> — every CMD submission is logged as JSONL — and converts successful AI translations into new heuristic rules.</p>

<p>That last part is the self-improvement loop. Every time the AI handles a command the heuristics couldn’t, that’s a potential new rule. Run <code class="language-plaintext highlighter-rouge">--digest</code> regularly and the AI dependency shrinks over time.</p>

<h2 id="the-fallback-chain">The Fallback Chain</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Input
  → Compound split (if "and"/"then"/"also"/"plus")
  → Each segment: heuristic match at confidence &gt; 0.8
  → No match? → AI fallback if configured
  → AI not configured? → Clear error message
</code></pre></div></div>

<p>The error message on a heuristic miss is explicit: “No matching rule. Try the full ww command or enable AI mode.” That’s better than silently failing or silently sending data to an API.</p>

<p>AI is an option, not a requirement. Most commands go through heuristics. The engine gets better over time. The AI dependency decreases.</p>]]></content><author><name></name></author><category term="technical" /><category term="architecture" /><summary type="html"><![CDATA[The heuristic engine came from a simple question: how many natural language command phrasings can you handle with regex before you need a language model? The answer turned out to be: most of them.]]></summary></entry></feed>