<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ranjith Kumar</title>
    <description>The latest articles on Forem by Ranjith Kumar (@ranji2612).</description>
    <link>https://forem.com/ranji2612</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3013241%2F3ad01b20-c950-41ba-891c-3e9ea4259d96.jpeg</url>
      <title>Forem: Ranjith Kumar</title>
      <link>https://forem.com/ranji2612</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ranji2612"/>
    <language>en</language>
    <item>
      <title>From Routines to a Crew — Building a System That Plans Its Own Work &amp; executes it</title>
      <dc:creator>Ranjith Kumar</dc:creator>
      <pubDate>Fri, 22 May 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/ranji2612/from-routines-to-a-crew-building-a-system-that-plans-its-own-work-executes-it-18ob</link>
      <guid>https://forem.com/ranji2612/from-routines-to-a-crew-building-a-system-that-plans-its-own-work-executes-it-18ob</guid>
      <description>&lt;h2&gt;
  
  
  The Gap
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://medium.com/@ranji2612/making-claude-do-your-routines-while-you-sleep-8978884d3a37" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, I built a routine board — a system that runs Claude on a schedule, defined as Markdown files, backed by a Rust engine with cron scheduling, crash recovery, and a self-contained dashboard. It works well for what it does.&lt;/p&gt;

&lt;p&gt;But real work isn't a cron job.&lt;/p&gt;

&lt;p&gt;Consider this: you need to audit all the places a deprecated API is referenced across a large codebase. That means searching multiple code areas, cross-referencing findings, identifying which references are active vs. dead code, and producing a prioritized cleanup plan. No single Claude session handles this well. The context is too broad, the work needs decomposition, and some pieces depend on others.&lt;/p&gt;

&lt;p&gt;That’s the difference between “task execution” and “work management.” Execution is running a prompt. Work management is deciding what to run, in what order, with what context, and what to do when something fails&lt;/p&gt;

&lt;h2&gt;
  
  
  The Build Sprint
&lt;/h2&gt;

&lt;p&gt;I built the entire system — from nothing to a multi-persona task engine with a dashboard — in 3 days of evenings.&lt;/p&gt;

&lt;p&gt;Day 1 was intense: core orchestrator (Phase 0.5), a full Rust dashboard with CRUD operations (Phase 0.75), and a round of silent-failure bug fixes (Phase 0.9) — all in one session. Day 3: hardening with task creation/editing/deletion from the dashboard, POSIX file locking for concurrency safety, and &lt;code&gt;launchd&lt;/code&gt; scheduling (Phase 1), then the big architectural addition — planner/worker decomposition with a Worker Hive visualization (Phase 2).&lt;/p&gt;

&lt;p&gt;The stack: Python for the orchestrator (subprocess management, YAML parsing, straightforward scripting), Rust for the dashboard (HTTP server, real-time worker status, the same "single binary" philosophy from the routine engine).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The recursive part&lt;/strong&gt;: Claude helped build the system that orchestrates Claude. The design document, the orchestrator code, the Rust dashboard — all built with Claude as a pair programmer. I was designing a system for autonomous AI work while doing autonomous AI work. It's turtles all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Task Schema and Activity Log
&lt;/h2&gt;

&lt;p&gt;Every task lives in a YAML file with a rich schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TASK-001"&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Audit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deprecated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;API&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;references"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Search codebase for deprecated API references.&lt;/span&gt;
  &lt;span class="s"&gt;Check related tasks for migration status.&lt;/span&gt;
&lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;P0&lt;/span&gt;
&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;L&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;open&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;investigation&lt;/span&gt;
&lt;span class="na"&gt;requires_human&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;
&lt;span class="na"&gt;human_loop_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blocking&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="na"&gt;sub_tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="na"&gt;activity_log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Priority (P0–P3), size (XS–XL), status, dependencies, human intervention config — the usual project management primitives. But the real innovation is the activity log.&lt;/p&gt;

&lt;p&gt;Every action on a task gets timestamped with who did it (which persona), what they did, and what they found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;activity_log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-02T22:37:29"&lt;/span&gt;
    &lt;span class="na"&gt;persona&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;picked_up&lt;/span&gt;
    &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Selected&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;as&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;highest&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unblocked&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;task"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-02T22:42:10"&lt;/span&gt;
    &lt;span class="na"&gt;persona&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;completed&lt;/span&gt;
    &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Research&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;complete.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Found&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;27&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;references&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;across&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;categories."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the system's memory — and it's surprisingly powerful for how simple it is. When a task fails and retries, the retrying worker sees what was already attempted and tries a different approach. When a planner decomposes a task, it reads the log to understand context. When I look at a task at the end of the day, I can trace exactly what happened — which tools were used, what was found, what failed — without reading pages of raw Claude output.&lt;/p&gt;

&lt;p&gt;The action types tell the story: &lt;code&gt;picked_up&lt;/code&gt;, &lt;code&gt;progress&lt;/code&gt;, &lt;code&gt;planned&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;retry&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt;, &lt;code&gt;human_requested&lt;/code&gt;, &lt;code&gt;human_responded&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;. You can scan a task's log and understand its entire lifecycle in seconds. It's the simplest possible implementation of agent memory, and it carries surprisingly far.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐
│   Open   │
└────┬─────┘
     │
┌────▼─────┐
│ Planning  │◄──────────────────────┐
└────┬─────┘                        │
     │                              │
┌────▼──────┐                       │
│ In Progress├──────────────────────┘
└────┬──────┘   (worker rejects plan → re-plan)
     │
┌────▼─────┐
│ Blocked   │  (needs human input or dependency)
└────┬─────┘
     │
┌────▼─────┐     ┌──────────────┐
│   Done   ├────►│ Spawn Follow-│
└──────────┘     │ up Tasks     │
                 └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Human-in-the-Loop as a Dial
&lt;/h2&gt;

&lt;p&gt;One of the most useful design decisions was making human involvement a dial, not a switch. It's a 2×2 matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;requires_human&lt;/th&gt;
&lt;th&gt;blocking&lt;/th&gt;
&lt;th&gt;non_blocking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fully autonomous&lt;/td&gt;
&lt;td&gt;fully autonomous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;pauses before close&lt;/td&gt;
&lt;td&gt;closes, sends summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;intervention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;pauses at checkpoints&lt;/td&gt;
&lt;td&gt;continues with best guess&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;approval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;waits for plan sign-off&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A P0 investigation should pause for human review — the stakes are too high for full autonomy. A P3 documentation task can run end-to-end without anyone looking at it. A task that needs plan approval waits after the planner proposes sub-tasks, showing them in the dashboard with Approve/Reject buttons.&lt;/p&gt;

&lt;p&gt;Different tasks need different autonomy levels, and the system supports that as a per-task configuration rather than a global setting. In practice, I found that most tasks start as &lt;code&gt;requires_human: none&lt;/code&gt; (fully autonomous) and I only add friction for high-stakes work. The default is trust, with guardrails where they matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bugs That Taught Me
&lt;/h2&gt;

&lt;p&gt;The most instructive bugs were all variations on the same theme: silent failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty output as success.&lt;/strong&gt; Workers were returning exit code 0 with empty stdout — they'd gotten stuck on a permission prompt and hung until timeout. The orchestrator saw exit code 0 and marked the task as done. Fix: treat empty output as failure. A single &lt;code&gt;if not output:&lt;/code&gt; check that routes through the failure handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Worker returned exit code 0 but produced no output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Timeout gap in loop mode.&lt;/strong&gt; The continuous mode spawned workers as background processes and polled for completion, but it wasn't tracking when each worker started. Workers could run forever, accumulating memory and burning API credits. Fix: track &lt;code&gt;spawn_time&lt;/code&gt; per worker in the PID file (later enriched to full JSON metadata with persona and start time), check elapsed time each poll cycle, &lt;code&gt;proc.kill()&lt;/code&gt; overdue workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock contention silence.&lt;/strong&gt; When the orchestrator tried to run but another instance already held the POSIX flock, it would silently exit. No log entry, no notification, nothing. From the outside, it looked like the system stopped working — you'd check the schedule and see it should have run, but there's no evidence it even tried. Fix: write a "Skipped — lock held" entry to the run log before exiting.&lt;/p&gt;

&lt;p&gt;The meta-lesson: &lt;strong&gt;autonomous systems fail silently by default.&lt;/strong&gt; You have to instrument every exit path, every edge case, every "this shouldn't happen" branch. If a human isn't watching, nobody is — unless you build the observability in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Planners and Workers
&lt;/h2&gt;

&lt;p&gt;The architecture shift in Phase 2 was routing tasks through different &lt;em&gt;personas&lt;/em&gt; based on their size. Tasks sized M, L, or XL go through a "planner" that decomposes them into smaller sub-tasks. XS and S tasks go directly to workers — backward compatible with Phase 1 behavior.&lt;/p&gt;

&lt;p&gt;The routing logic is remarkably compact — three checks that determine the entire system's behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;needs_planning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent_task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# sub-tasks skip planning
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub_tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;     &lt;span class="c1"&gt;# already planned
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not big? Not a parent's child? Not already planned? Send it to the planner. Everything else goes to a worker. That's the entire routing layer.&lt;/p&gt;

&lt;p&gt;The planner gets a specialized prompt asking it to output structured JSON with &lt;code&gt;$N&lt;/code&gt; dependency references. The orchestrator resolves &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt; etc. to actual &lt;code&gt;TASK-NNN&lt;/code&gt; IDs when materializing sub-tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;id_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# $N -&amp;gt; actual task ID (1-indexed)
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub_tasks_spec&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TASK-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;next_num&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;id_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_id&lt;/span&gt;
    &lt;span class="c1"&gt;# Resolve $N dependencies to real IDs
&lt;/span&gt;    &lt;span class="n"&gt;resolved_deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dep&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtle but important design choice: planners run before workers in the task queue. When the orchestrator selects the next task, it prioritizes planner work over worker work. This unblocks sub-tasks sooner — you don't want a planner waiting behind three workers when its output would spawn three more parallelizable tasks.&lt;/p&gt;

&lt;p&gt;When all sub-tasks complete, the parent auto-closes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────┐
                    │  Parent Task     │
                    │  (Size: L)       │
                    │  "Audit all API  │
                    │   endpoints"     │
                    └────────┬─────────┘
                             │
                       ┌─────▼─────┐
                       │  Planner   │
                       │  (Claude)  │
                       └─────┬─────┘
                             │ JSON plan with $N deps
               ┌─────────────┼─────────────┐
               │             │             │
        ┌──────▼──────┐ ┌───▼────────┐ ┌──▼───────────┐
        │ Sub-task 1  │ │ Sub-task 2 │ │ Sub-task 3   │
        │ (Size: XS)  │ │ (Size: S)  │ │ (Size: S)    │
        │ No deps     │ │ No deps    │ │ Depends: 1,2 │
        └──────┬──────┘ └─────┬──────┘ └──────┬───────┘
               │              │               │
          ┌────▼────┐   ┌─────▼────┐    ┌─────▼────┐
          │ Worker  │   │ Worker   │    │ Worker   │
          │ (Claude)│   │ (Claude) │    │ (Claude) │
          └─────────┘   └──────────┘    └──────────┘
               │              │               │
               └──────────────┼───────────────┘
                              │
                    All done → Parent auto-closes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Plan Rejection Protocol
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. Workers can say "this plan is bad."&lt;/p&gt;

&lt;p&gt;If a sub-task's plan is fundamentally unworkable — missing prerequisites, contradictory requirements, impossible constraints — the worker outputs &lt;code&gt;PLAN_REJECTED: &amp;lt;reason&amp;gt;&lt;/code&gt; instead of completing the task. The orchestrator detects this marker, removes all the old sub-tasks, resets the parent task for re-planning, and includes the rejection reason in the next planner prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┐     plan      ┌──────────┐    sub-tasks    ┌──────────┐
│  Parent   │─────────────►│ Planner  │────────────────►│ Workers  │
│  Task     │              │          │                 │          │
└──────────┘              └──────────┘                 └────┬─────┘
     ▲                         ▲                            │
     │                         │     PLAN_REJECTED:         │
     │    reset parent,        │     "missing prereq X"     │
     │    include rejection    └────────────────────────────┘
     │    context
     │
     │   After 3 iterations:
     └── escalate to human intervention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max_plan_iterations&lt;/code&gt; defaults to 3. After three failed plan-reject cycles, the system escalates to human intervention — it sets &lt;code&gt;requires_human: intervention&lt;/code&gt; and writes a notification explaining what happened.&lt;/p&gt;

&lt;p&gt;This isn't error handling. It's a &lt;strong&gt;feedback loop between two AI personas&lt;/strong&gt;. The planner proposes, the worker evaluates, and if the proposal doesn't survive contact with reality, the system iterates. With context. The rejection reason is fed back to the planner, so each iteration is informed by what went wrong before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Results
&lt;/h2&gt;

&lt;p&gt;The system's first real test was a codebase audit — finding all references to a deprecated API across a large repository, checking related tasks for migration status, and producing a prioritized cleanup plan. This is exactly the kind of task that's painful to do manually: boring, sprawling, requires checking dozens of files and cross-referencing with issue trackers.&lt;/p&gt;

&lt;p&gt;The planner decomposed it into 8 sub-tasks — each focused on a different code area or a different type of investigation (search this directory, check that task tracker, analyze this migration path). Workers ran independently, some completing in minutes (quick grep-style searches), others taking longer for deeper analysis.&lt;/p&gt;

&lt;p&gt;Here's what the workers found, collectively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;27 distinct references&lt;/strong&gt; across 6 categories, ranked into 3 priority tiers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 out-of-scope items&lt;/strong&gt; correctly identified as false positives — things that looked like matches but were actually unrelated (different product's API, different naming convention, already fully migrated). That's human time saved: instead of chasing false positives, I got a pre-filtered list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 targets confirmed already removed&lt;/strong&gt; — one sub-task discovered its target had been cleaned up in a previous effort over a year ago. The worker correctly reported "no changes needed" and moved on&lt;/li&gt;
&lt;li&gt;One sub-task found that a supposedly abandoned migration task had actually been stalled since 2022 — useful context for prioritization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final output: a structured action plan with 5 independent code changes, prioritized by risk and effort, with pre-checks and test plans for each. All the changes were small (XS or S sized) and could be submitted in parallel.&lt;/p&gt;

&lt;p&gt;What would have taken a full day of manual investigation — opening files, cross-referencing tasks, checking git history, reading old code reviews — was done by 8 coordinated AI workers. And because each sub-task produced a standalone output file, I could review them individually, at my own pace, in whatever order made sense.&lt;/p&gt;

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

&lt;p&gt;The evolution tells a story: cron job → scheduler → orchestrator → planner/worker system. Each step was driven by a real limitation of the previous one, not by architectural ambition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V0 — Serial                 One worker, one task at a time
╔═══╗     ╔══════╗     ╔══════╗
║ W ║────►║ Task ║────►║ Task ║────► ...
╚═══╝     ╚══════╝     ╚══════╝


V1 — Worker Pool             Independent tasks, concurrent workers
╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗
║ W ║ ║ W ║ ║ W ║ ║ W ║      worker queue
╚═╤═╝ ╚═╤═╝ ╚═╤═╝ ╚═╤═╝
  │     │     │     │
  ▼     ▼     ▼     ▼
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ T │ │ T │ │ T │ │ T │      task pool
└───┘ └───┘ └───┘ └───┘


V2 — Planners + Workers      Decomposition before execution
╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗
║ W ║ ║ W ║ ║ W ║ ║ W ║ ║ W ║ ║ W ║  workers
╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝
╔═══╗ ╔═══╗ ╔═══╗
║ P ║ ║ P ║ ║ P ║                     planners
╚═══╝ ╚═══╝ ╚═══╝


V3 — Team (future)           Specialized personas, handoff protocol
╔═══╗ ╔═══╗
║ P ║ ║ P ║                           planners
╚═══╝ ╚═══╝
╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗
║ W ║ ║ W ║ ║ W ║ ║ W ║ ║ W ║ ║ W ║  workers
╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝
╔════╗ ╔═══╗
║ PM ║ ║ TL║                          creators
╚════╝ ╚═══╝
       ▲ handoff protocol ▲
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four patterns emerged that I think are generalizable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activity log as memory.&lt;/strong&gt; Context survives across retries and sessions because every action is recorded. A retry doesn't start from zero — it starts from "here's what was tried and why it failed." This is the simplest possible implementation of agent memory, and it's surprisingly effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personas as routing logic.&lt;/strong&gt; "Planner" and "worker" aren't separate systems or separate models. They're the same Claude CLI called with different prompts. The persona distinction is a function call — &lt;code&gt;needs_planning()&lt;/code&gt; returns True, you use the planner prompt template. Returns False, you use the worker template. That's it. No framework, no agent registry, no complex orchestration layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-in-the-loop as a dial.&lt;/strong&gt; The 2×2 matrix of &lt;code&gt;requires_human&lt;/code&gt; × &lt;code&gt;blocking/non_blocking&lt;/code&gt; lets each task declare its own autonomy level. This is more useful in practice than a global "autonomous mode" toggle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan rejection as protocol.&lt;/strong&gt; Not error handling — a first-class feedback mechanism. &lt;code&gt;PLAN_REJECTED:&lt;/code&gt; is part of the prompt contract. Both sides know the rules. The system iterates with context rather than retrying blindly.&lt;/p&gt;

&lt;p&gt;The broader ecosystem is exploring similar ideas. Tools and frameworks for autonomous AI workflows are emerging rapidly. There's no canonical architecture for this yet — and that's what makes it an exciting space to build in. We're all figuring this out in real time.&lt;/p&gt;

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

&lt;p&gt;Phase 3 is where it gets ambitious. There were a few paths i considered for this. Either going deep on exactly defining the personas, for example: TL and PM personas that create new tasks etc. The other option was to focus on the shared ecosystem instead of the individual personas.&lt;br&gt;
There will always be more &amp;amp; more creative agents with different capabilities continue to show up, so rather than focusing on a single agent with different flavor, i found it to be both very interesting &amp;amp; challenging to rather focus on the shared ecosystem they’d operate under. A space where devs and their agents could co-work in a productive way.&lt;/p&gt;

&lt;p&gt;It stops being a tool you use and becomes a team you manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;From a bash cron job to a multi-persona planning system in about 3 days of evenings. That's not a testament to my engineering speed — it's a testament to what becomes possible when you have an AI pair programmer that can help build the infrastructure for its own autonomy.&lt;/p&gt;

&lt;p&gt;Honest assessment: it's still experimental. The failure rate is real. Silent failures lurk in every corner, and you have to instrument your way to reliability. But the ceiling is visible. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pattern of "structured task → AI decomposition → parallel execution → human review" works, and it works better than doing everything interactively.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have Claude Code or any AI with a CLI, try building something autonomous. Start with a single routine — a cron job that generates your standup. See where it takes you. You might end up with a planning system that argues with itself about how to approach your work.&lt;/p&gt;

&lt;p&gt;And that's a surprisingly useful thing to have.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Part 2 of a multi-part series. &lt;a href="https://medium.com/@ranji2612/making-claude-do-your-routines-while-you-sleep-8978884d3a37" rel="noopener noreferrer"&gt;Part 1: "The Routine Board"&lt;/a&gt; covers the routine engine that started it all.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Making Claude do your routines while you sleep!</title>
      <dc:creator>Ranjith Kumar</dc:creator>
      <pubDate>Thu, 21 May 2026 14:00:00 +0000</pubDate>
      <link>https://forem.com/ranji2612/making-claude-do-your-routines-while-you-sleep-oih</link>
      <guid>https://forem.com/ranji2612/making-claude-do-your-routines-while-you-sleep-oih</guid>
      <description>&lt;h2&gt;
  
  
  The Itch
&lt;/h2&gt;

&lt;p&gt;Every morning, same ritual. Open Claude, ask it to summarize my recent pull requests, check for blockers, prep a standup. Three minutes, every single day.&lt;/p&gt;

&lt;p&gt;It’s not a lot of time. But it’s the kind of time that bothers a developer — repetitive, predictable, mechanical. Claude has a CLI. The CLI can run unattended. What if cron just… did this for me?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Learning Loop
&lt;/h2&gt;

&lt;p&gt;These past few months — learning new tools, new patterns, new ways of working with AI — and the thing that keeps surprising me is the speed. Not the speed of the AI itself, but the speed of the development loop. An idea I’d have over morning coffee could be a working prototype by the time I close my laptop that evening. Things that would have been a week-long side project — spread thin across stolen hours — now materialize in a single focused session.&lt;/p&gt;

&lt;p&gt;This isn’t groundbreaking. Plenty of people have built their own versions of “autonomous Claude”: cron wrappers, custom schedulers, Claude Code extensions, full-blown agent frameworks. Some use existing tools like OpenClaw, others write bash scripts, others build elaborate multi-agent systems. The space is full of experimentation, and there’s no canonical answer yet.&lt;/p&gt;

&lt;p&gt;What I’m sharing here is my version — a “routine desk” that lets me define, schedule, and monitor AI tasks through a simple dashboard. It’s the story of building it, what I learned, and the specific design choices that made it useful. Your version would look different, and that’s the point. The interesting part isn’t the artifact — it’s what you discover along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simplest Version
&lt;/h2&gt;

&lt;p&gt;The first attempt was exactly what you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 9 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 1-5 claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Summarize my recent code changes"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/standup.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked! Sort of. For about two days.&lt;/p&gt;

&lt;p&gt;Then the problems compounded. Monday morning: the output file was empty because the CLI had hit an authentication issue overnight — cron swallowed the error silently. Tuesday: two runs overlapped because the first one took 8 minutes instead of the usual 3, and the second kicked off on schedule before the first finished. By Wednesday I had six temp files named &lt;code&gt;standup.txt&lt;/code&gt;, &lt;code&gt;standup2.txt&lt;/code&gt;, &lt;code&gt;standup-final.txt&lt;/code&gt;... you know how that goes.&lt;/p&gt;

&lt;p&gt;No visibility into whether runs succeeded or failed. No error handling. No history. No way to tell at a glance whether the system was healthy or broken. Cron is a fantastic tool for running deterministic commands. An AI CLI call is not deterministic — it can hang, timeout, produce unexpected output, or fail silently. I needed something that understood that.&lt;/p&gt;

&lt;p&gt;I needed something small but proper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routines as Markdown
&lt;/h2&gt;

&lt;p&gt;Here's the core insight that shaped everything: a routine is just a prompt plus scheduling metadata. And there's already a perfect format for "structured metadata + freeform text" — Markdown with YAML frontmatter.&lt;/p&gt;

&lt;p&gt;Here's what my morning standup routine looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Morning&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Standup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Summary"&lt;/span&gt;
&lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1-5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonnet&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;span class="na"&gt;max_turns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="na"&gt;Generate a morning standup summary. Do the following&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;1. **Recent PRs**&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Search for my recent pull requests from the past 24 hours.&lt;/span&gt;
   &lt;span class="s"&gt;List each with its title, status, and a one-line summary.&lt;/span&gt;

&lt;span class="na"&gt;2. **Active Tasks**&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Search for my active tasks. List each with its title,&lt;/span&gt;
   &lt;span class="s"&gt;priority, and current status.&lt;/span&gt;

&lt;span class="na"&gt;3. **Blockers**&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Identify any code reviews awaiting approval or tasks that are blocked.&lt;/span&gt;

&lt;span class="s"&gt;4. **Today's Focus**&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Based on the above, suggest 2-3 priorities for today.&lt;/span&gt;

&lt;span class="s"&gt;Format the output as a clean markdown summary suitable for posting&lt;/span&gt;
&lt;span class="s"&gt;in a team chat.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filename &lt;em&gt;is&lt;/em&gt; the routine name. &lt;code&gt;morning-standup.md&lt;/code&gt; creates a routine called &lt;code&gt;morning-standup&lt;/code&gt;. Drop a file in the directory, it gets scheduled. Delete it, it stops. Set &lt;code&gt;enabled: false&lt;/code&gt; in the frontmatter to pause it without removing it.&lt;/p&gt;

&lt;p&gt;Each routine picks its own model. Anthropic's Claude models range from fast and cheap (Haiku) to powerful and expensive (Sonnet, Opus) — and different tasks need different trade-offs. My standup uses Sonnet — it needs to search across code changes and synthesize a report, so reasoning quality matters. My health-check routine uses Haiku — it's a quick status ping, optimized for speed and cost. A Haiku call costs a fraction of Sonnet and returns in seconds. When all you need is "are things on fire? yes/no," you don't need the most powerful model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Metrics Health Check&lt;/span&gt;
&lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;haiku&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
&lt;span class="na"&gt;max_turns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="s"&gt;Perform a quick health check on key operational metrics...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Pivot and The Engine
&lt;/h2&gt;

&lt;p&gt;I originally planned to build this in TypeScript. The Claude Code SDK has a clean &lt;code&gt;query()&lt;/code&gt; API, &lt;code&gt;node-cron&lt;/code&gt; handles scheduling, &lt;code&gt;node:sqlite&lt;/code&gt; handles persistence. I had a reference implementation to model after — a review agent built on that exact stack. I started building.&lt;/p&gt;

&lt;p&gt;Then I hit a wall: the package registry wasn't accessible in my locked-down dev environment — no outbound network access to install packages. No &lt;code&gt;npm install&lt;/code&gt;, no dependencies, dead end.&lt;/p&gt;

&lt;p&gt;So I pivoted. Rewrote the whole thing in Rust.&lt;/p&gt;

&lt;p&gt;What started as a constraint became the best architectural decision of the project. The final artifact is a single binary — async with Tokio, embedded SQLite via &lt;code&gt;rusqlite&lt;/code&gt;, zero runtime dependencies. You can copy it to any machine and run it. No &lt;code&gt;node_modules&lt;/code&gt;, no package manager, no runtime version to match. Just one file that contains the scheduler, the executor, the database, and the dashboard. In hindsight, this is exactly what you want for infrastructure that's supposed to run unattended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Executor&lt;/strong&gt; is the core loop. It spawns the &lt;code&gt;claude&lt;/code&gt; CLI as a subprocess, streams stdout and stderr concurrently via &lt;code&gt;tokio::select!&lt;/code&gt;, counts messages and tool uses as they stream by, and enforces a hard timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;stdout_reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;BufReader&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="py"&gt;.stdout&lt;/span&gt;&lt;span class="nf"&gt;.take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.lines&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;stderr_reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;BufReader&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="py"&gt;.stderr&lt;/span&gt;&lt;span class="nf"&gt;.take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.lines&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;select!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stdout_reader&lt;/span&gt;&lt;span class="nf"&gt;.next_line&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;message_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tool_use_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="n"&gt;output_lines&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stderr_reader&lt;/span&gt;&lt;span class="nf"&gt;.next_line&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* log and continue */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Scheduler&lt;/strong&gt; creates one async task per routine. Each task parses its cron expression, calculates the duration until the next trigger using &lt;code&gt;chrono::Utc::now()&lt;/code&gt;, sleeps for exactly that duration, and fires the executor. This means each routine runs in its own Tokio task — no central polling loop, no priority queue, no timer wheel. Each routine is independently responsible for waking itself up.&lt;/p&gt;

&lt;p&gt;Overlap protection is a &lt;code&gt;HashSet&lt;/code&gt; behind a mutex — before running, check if the routine name is in the set. If it is, skip this tick. Five lines that prevent the most common cron footgun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;locks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locks&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;locks&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;routine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Already running, skipping tick"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;locks&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Crash recovery&lt;/strong&gt; is equally minimal. On startup, a &lt;code&gt;cleanup_stale_runs()&lt;/code&gt; function queries SQLite for any runs with status &lt;code&gt;running&lt;/code&gt; or &lt;code&gt;pending&lt;/code&gt; and marks them as &lt;code&gt;failed&lt;/code&gt;. If the process crashed mid-run, those records would be stuck forever without this. Five lines that handle unclean shutdowns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hot-reload&lt;/strong&gt; uses the &lt;code&gt;notify&lt;/code&gt; crate to watch the &lt;code&gt;routines/&lt;/code&gt; directory. When a &lt;code&gt;.md&lt;/code&gt; file changes, the watcher reloads all routines and updates the scheduler. There's one pragmatic hack worth noting: &lt;code&gt;std::mem::forget(watcher)&lt;/code&gt; — leaking the watcher so it lives for the program's lifetime rather than getting dropped at the end of the setup function. In a "proper" codebase you'd store the watcher handle somewhere and drop it on shutdown. Here, the program runs until you kill it, so leaking is functionally correct and saves a bunch of lifetime gymnastics. Pragmatism over purity.&lt;/p&gt;

&lt;p&gt;The full engine — foundation, scheduling, output handling, dashboard, polish — was built and working in a single sitting. That's the compressed development loop I mentioned at the start: what would have been a multi-week side project materialized in one focused evening with Claude as a pair programmer. Rust's compiler caught entire categories of bugs at compile time that would have been runtime surprises in TypeScript. The borrow checker is annoying until it's saving you from a data race in your async scheduler — then it's your best friend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────┐
│                     Claude Routines Engine                    │
│                                                              │
│  ┌──────────┐    ┌────────────┐    ┌──────────────────────┐  │
│  │  File     │    │            │    │     Executor         │  │
│  │  Watcher  │───►│ Scheduler  │───►│  ┌────────────────┐ │  │
│  │ (notify)  │    │  (cron)    │    │  │ claude -p "..." │ │  │
│  └──────────┘    │            │    │  │ --model sonnet  │ │  │
│       ▲          │ ┌────────┐ │    │  │ --max-turns 50  │ │  │
│       │          │ │Overlap │ │    │  └────────┬───────┘ │  │
│  routines/*.md   │ │ Lock   │ │    │           │         │  │
│                  │ └────────┘ │    │     stdout/stderr   │  │
│                  └────────────┘    └──────────┬─────────┘  │
│                                               │             │
│                  ┌────────────┐    ┌──────────▼─────────┐  │
│                  │  Dashboard  │    │     SQLite Store    │  │
│                  │ :3456       │◄──►│  (WAL mode)        │  │
│                  │             │    │  runs, status,      │  │
│                  └────────────┘    │  crash recovery     │  │
│                                    └────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard is built on raw TCP — no web framework, no Axum, no Actix. One file that parses HTTP requests by hand, routes them, and generates the entire HTML as a Rust string. &lt;code&gt;TcpListener::bind&lt;/code&gt;, &lt;code&gt;tokio::spawn&lt;/code&gt; per connection, pattern-match on &lt;code&gt;(method, path)&lt;/code&gt;. That's the entire web server.&lt;/p&gt;

&lt;p&gt;Why no framework? Because adding a dependency means adding complexity, and this dashboard needed to do exactly four things: show routine cards, show run history, show logs, and trigger manual runs. A framework would have been architecturally correct and practically overkill.&lt;/p&gt;

&lt;p&gt;It serves a dark-themed single-page dashboard: routine cards with status dots (green for last-run-succeeded, red for failed, gray for never-run), a run history table with timing and status for each execution, and a log viewer that shows the raw Claude output for any run. Everything renders server-side — the HTML is a giant Rust string interpolation. No client-side framework, no hydration, no build step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf672a9pcrb4c658l18z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf672a9pcrb4c658l18z.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The killer feature is the "New Routine" button. It opens a modal where you define a routine — title, schedule, model, prompt — and hitting save sends a POST to &lt;code&gt;/api/routines&lt;/code&gt;. The server writes a &lt;code&gt;.md&lt;/code&gt; file to the routines directory. The file watcher detects the new file. The scheduler picks it up and starts running it. The system grows itself from the browser. You don't need SSH access or a text editor to add a new routine — just a browser and an idea for what Claude should do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ──POST /api/routines──► Dashboard Server
                                     │
                              writes metrics-check.md
                                     │
                                     ▼
                              routines/ directory
                                     │
                              File Watcher detects
                                     │
                                     ▼
                              Scheduler reloads
                              &amp;amp; schedules new routine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No CDN, no build step, no external CSS. Auto-refresh with &lt;code&gt;setInterval&lt;/code&gt; every 30 seconds, paused when modals are open so your form doesn't disappear mid-edit. The entire UI — CSS, JavaScript, HTML template — is self-contained in the binary. &lt;code&gt;cargo build --release&lt;/code&gt; and you have everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha" Moment
&lt;/h2&gt;

&lt;p&gt;The first morning I woke up to a standup summary already sitting there — generated at 9 AM while I was making coffee — something shifted. I didn't open Claude and ask it to do something. It had already done it.&lt;/p&gt;

&lt;p&gt;My metrics check ran at 8 AM and flagged an issue before I'd opened my laptop. The notification was waiting. The context was there. I just needed to act on it.&lt;/p&gt;

&lt;p&gt;It's a small thing, objectively. A cron job that calls Claude. But subjectively, it feels different. The mental shift is from "I use Claude" to "Claude works for me." The tool has agency — bounded, scheduled, observable agency, but agency nonetheless.&lt;/p&gt;

&lt;p&gt;And it changes how you think about your own time. Those three minutes I spent every morning preparing a standup? Gone. But it's not just three minutes saved — it's the cognitive overhead of remembering to do it, the context-switching cost of opening a new session, the friction of formulating the same prompt for the hundredth time. All of that evaporates. You just... have the summary. You open your laptop and the work is already done.&lt;/p&gt;

&lt;p&gt;There's a compounding effect too. Once the system exists, the marginal cost of adding a new routine is basically zero — write a markdown file, drop it in the directory. So you start thinking about what &lt;em&gt;else&lt;/em&gt; could run unattended. Weekly report summaries. PR review reminders. Dependency update checks. Each one is a few paragraphs of markdown. Each one saves a few minutes a day. The minutes add up. Before long you have a small fleet of routines quietly doing work in the background, and you're spending your own time on the things that actually require you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and What Comes Next
&lt;/h2&gt;

&lt;p&gt;For all that it does, routines have real constraints. Every run is stateless — each execution starts fresh with no memory of previous runs. Routines can't coordinate with each other. They can't decompose complex work into smaller pieces. They don't learn from failures.&lt;/p&gt;

&lt;p&gt;If a routine fails, it just... fails. There's no retry logic, no fallback strategy, no way to say "try a different approach." The SQLite store records what happened, but nothing acts on that information automatically.&lt;/p&gt;

&lt;p&gt;These are fine constraints for scheduled tasks. A daily standup doesn't need memory. A metrics check doesn't need to coordinate with anything. But real work — the kind that takes hours, requires research across multiple areas, produces structured deliverables, and has pieces that depend on other pieces — needs something more.&lt;/p&gt;

&lt;p&gt;I started with a question: "What if cron ran Claude for me?" That question led to a routine engine. But the engine surfaced a bigger question: "What if Claude could manage its own work?"&lt;/p&gt;

&lt;p&gt;I had a system that could &lt;em&gt;run&lt;/em&gt; tasks. But I wanted a system that could &lt;em&gt;manage work&lt;/em&gt; — decompose it, prioritize it, retry intelligently, and know when to ask for human help.&lt;/p&gt;

&lt;p&gt;That's where Part 2 picks up: a task executor with planners, workers, dependency graphs, retries, and a feedback loop where AI personas negotiate with each other about how to approach a problem. The system that routines couldn't be.&lt;/p&gt;

&lt;p&gt;The broader ecosystem is moving this direction too. Tools like &lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; are enabling a wave of builders to experiment with autonomous AI workflows. There's no canonical way to do this yet — which is what makes it exciting to build.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 2: &lt;a href="https://medium.com/@ranji2612/from-routines-to-a-crew-building-an-ai-task-system-that-plans-its-own-work-e9f3ef75c2b0" rel="noopener noreferrer"&gt;"From Routines to a Crew — Building an AI Task System That Plans Its Own Work"&lt;/a&gt; explores what happens when you give Claude the ability to decompose, plan, and coordinate its own work.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>claude</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Developer Owns the UX. The AI Owns the Code.</title>
      <dc:creator>Ranjith Kumar</dc:creator>
      <pubDate>Mon, 11 May 2026 18:56:54 +0000</pubDate>
      <link>https://forem.com/ranji2612/the-developer-owns-the-ux-the-ai-owns-the-code-1095</link>
      <guid>https://forem.com/ranji2612/the-developer-owns-the-ux-the-ai-owns-the-code-1095</guid>
      <description>&lt;p&gt;My mom does bead art. The kind where you sit with a tray of tiny plastic beads and, over hours — sometimes days — assemble them into an intricate portrait or devotional motif. It's meditative, precise, and deeply personal.&lt;/p&gt;

&lt;p&gt;The bottleneck has always been the pattern. You can't look at a photograph and start placing beads. You need to know &lt;em&gt;exactly&lt;/em&gt; which bead goes where, in what color, on a grid that maps to the physical constraints of the project: how wide it is, how many colors of beads you've bought, how coarse or fine the detail needs to be.&lt;/p&gt;

&lt;p&gt;She was doing this by eye, or with rough printouts. I kept thinking: &lt;em&gt;there has to be a better way.&lt;/em&gt; So I opened Gemini and started a conversation.&lt;/p&gt;

&lt;p&gt;What came out of that is &lt;a href="https://github.com/ranji2612/beads_design" rel="noopener noreferrer"&gt;BeadGen&lt;/a&gt; — a fully local, zero-dependency browser tool that converts any photo into a ready-to-stitch bead pattern. No backend. No npm. No install. You open an HTML file and use it.&lt;/p&gt;

&lt;p&gt;But this post isn't really about the tool. It's about something I learned while building it: &lt;strong&gt;when you let AI write the code, the developer's most important job becomes the experience.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;Before the technical deep-dive, here's the simplest way to show it.&lt;/p&gt;

&lt;p&gt;Take the Golden Gate Bridge at sunset — rich gradients, a complex rust-red structure, water, sky, fog, warm light hitting the cables at an angle. Thousands of colors.&lt;/p&gt;

&lt;p&gt;Run it through BeadGen at 150 beads wide, no gradient mode on, full color palette.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbheaqf2n7q8thn7fs96p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbheaqf2n7q8thn7fs96p.png" alt=" " width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every circle is a bead. Every color in that output is a real, distinct bead color a crafter would need to source and place by hand. The structure of the bridge is preserved. The mood of the sunset is preserved. The complexity is &lt;em&gt;managed&lt;/em&gt; — reduced to something a human can actually execute, one bead at a time.&lt;/p&gt;

&lt;p&gt;That transformation — from photograph to stitchable grid — is the whole product.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "AI Owns the Code" Actually Means
&lt;/h2&gt;

&lt;p&gt;I want to be precise here, because "AI-assisted development" has become a meaningless phrase. Everyone says it now. Here's what it meant for me on this project:&lt;/p&gt;

&lt;p&gt;I wrote very little code from scratch. I used Gemini as the primary implementer — describing what I needed, reviewing what came back, asking it to revise or explain. The logic for color quantization, the Canvas API rendering pipeline, the pixel buffer manipulation — most of that was AI-native code that I read, understood, and occasionally redirected, but didn't author line by line.&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;didn't&lt;/em&gt; delegate: every decision about what the tool should feel like.&lt;/p&gt;

&lt;p&gt;How many controls is too many? Where does the slider sit? What does "No Gradient Mode" actually mean to someone who isn't a developer? What should the output look like when it downloads? Should error states be loud or quiet?&lt;/p&gt;

&lt;p&gt;None of that came from Gemini. All of it required me to stay in the room, stay opinionated, and push back when the generated UI drifted toward "technically correct but confusing to use."&lt;/p&gt;

&lt;p&gt;That division of labor — AI on implementation, human on experience — turned out to be the whole game.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Technical Problem (Because It's Interesting)
&lt;/h2&gt;

&lt;p&gt;BeadGen solves three sub-problems in sequence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Resolution mapping.&lt;/strong&gt; A bead project has a fixed width in bead count — say, 150 beads wide. The photo needs to be downsampled to that exact grid resolution, with aspect ratio preserved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Color quantization.&lt;/strong&gt; The Golden Gate photo has thousands of colors. My mom's bead collection has maybe 10. The image's palette has to collapse to that count without destroying the image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rendering.&lt;/strong&gt; Each cell in the grid gets drawn as a filled circle (the bead shape), not a square pixel, on a canvas the user can download.&lt;/p&gt;

&lt;p&gt;The interesting one is color quantization. The naive approach — round every color to the nearest bucket — looks terrible. You lose the soul of the image because you're treating all of color space uniformly, when an image's color distribution is wildly uneven.&lt;/p&gt;

&lt;p&gt;The right approach is the &lt;strong&gt;Median Cut algorithm&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put all pixels in one bucket.&lt;/li&gt;
&lt;li&gt;Find which color channel (R, G, or B) has the widest range across all pixels in the bucket.&lt;/li&gt;
&lt;li&gt;Sort by that channel and split at the median.&lt;/li&gt;
&lt;li&gt;Recurse until you have N buckets.&lt;/li&gt;
&lt;li&gt;Each bucket's representative color is the average of its pixels.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is that color splits happen &lt;em&gt;where the actual data is most varied&lt;/em&gt; — not uniformly across abstract color space. The Golden Gate image has a massive warm cluster (the bridge, sunset light) and a separate cool cluster (the bay, the fog, the sky). Median Cut finds that divide naturally and allocates palette slots accordingly. The rust-red cables stay rust-red. The blue-gray water stays blue-gray.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nearestColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;closest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;minDist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;closest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Euclidean distance in RGB space isn't perceptually perfect — LAB color space would be more accurate — but it's fast, dependency-free, and more than sufficient for bead-level fidelity. I knew about the tradeoff. I chose simplicity deliberately. That was a developer decision, not an AI one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Feature My Mom Asked For
&lt;/h2&gt;

&lt;p&gt;There's one feature in BeadGen I'm particularly glad I added: &lt;strong&gt;No Gradient Mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Photos have gradients everywhere — smooth transitions between light and shadow, color bleeding, soft backgrounds. The Golden Gate sunset is practically nothing &lt;em&gt;but&lt;/em&gt; gradient. In a photograph, that's beautiful. In a bead pattern, it's a nightmare. You'd need 80 colors to represent that sky faithfully, and no one has 80 colors of beads.&lt;/p&gt;

&lt;p&gt;No Gradient Mode posterizes the output. After quantization, it snaps colors more aggressively to the palette and flattens subtle transitions into solid bands. The pattern looks more graphic, more like flat illustration — and is actually &lt;em&gt;stitchable&lt;/em&gt; by a human working from a printed sheet.&lt;/p&gt;

&lt;p&gt;The AI didn't suggest this. My mom did. She looked at her first output and said the gradients were too much.&lt;/p&gt;

&lt;p&gt;That's the feature you add when your user is in the room. And that's exactly the kind of thing that wouldn't exist if I'd treated the AI as the product owner instead of the implementer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack: Deliberately, Aggressively Simple
&lt;/h2&gt;

&lt;p&gt;BeadGen is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vanilla JavaScript&lt;/li&gt;
&lt;li&gt;HTML5 Canvas API&lt;/li&gt;
&lt;li&gt;Zero dependencies&lt;/li&gt;
&lt;li&gt;Zero build step&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;index.html&lt;/code&gt; + &lt;code&gt;script.js&lt;/code&gt; + &lt;code&gt;style.css&lt;/code&gt;, runs off your local file system — literally &lt;code&gt;file:///your-path/index.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a firm decision. My mom needed to &lt;em&gt;use&lt;/em&gt; this tool, not install it. That means no localhost server, no terminal, no Python environment. She opens a file in Chrome. Done.&lt;/p&gt;

&lt;p&gt;The Canvas API handles everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;drawImage()&lt;/code&gt; to resample the photo to bead-grid resolution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getImageData()&lt;/code&gt; to read pixel values for quantization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;arc()&lt;/code&gt; to draw each bead as a filled circle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toDataURL()&lt;/code&gt; to export the result as a downloadable PNG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Working with raw pixel buffers is verbose and unforgiving, like writing assembly. But it keeps the whole thing self-contained, fast, and completely offline. Those are UX decisions first, technical decisions second.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I Had to Stay Opinionated
&lt;/h2&gt;

&lt;p&gt;Here's where I want to be honest about the limits of delegating to AI.&lt;/p&gt;

&lt;p&gt;Gemini was excellent at implementing what I described. It was not good at knowing &lt;em&gt;what&lt;/em&gt; to describe. When left to generate UI scaffolding on its own, it produced things that were &lt;em&gt;functional but unintuitive&lt;/em&gt; — too many options exposed at once, labels that made sense to a developer but not to someone who just wants to make a bead pattern, layouts that were complete but crowded.&lt;/p&gt;

&lt;p&gt;Every time I let a generated UI suggestion through without interrogating it, my mom got confused. Every time I pushed back — "this should be one toggle, not three settings," "this label needs to say what it does, not what it is" — the tool got better.&lt;/p&gt;

&lt;p&gt;The AI wrote the code correctly. I had to tell it what "correct" meant for this user.&lt;/p&gt;

&lt;p&gt;That gap — between code that works and an experience that works — is where the developer still has to show up. And I don't think that gap is going away anytime soon.&lt;/p&gt;




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

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perceptual color distance&lt;/strong&gt; — Switching from Euclidean RGB to CIEDE2000 in LAB color space for more accurate palette matching, especially for skin tones and subtle warm-to-cool transitions like that bridge sunset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bead inventory input&lt;/strong&gt; — Instead of "give me N colors," let the user input their actual bead colors and map to those exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print layout export&lt;/strong&gt; — A PDF with row-by-row bead counts and a color legend, formatted for A4/Letter printing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-by-row guided mode&lt;/strong&gt; — Step through one row at a time with bead counts, similar to knitting pattern notation.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;The source is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/ranji2612/beads_design" rel="noopener noreferrer"&gt;ranji2612/beads_design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clone it, open &lt;code&gt;index.html&lt;/code&gt; in a browser, upload a photo. No setup. Runs entirely offline.&lt;/p&gt;

&lt;p&gt;If you're a crafter and you make something with it, I'd genuinely love to see it. If you're a developer and you want to tackle LAB-space color distance or a print export feature, PRs are open.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;AI wrote most of the code. I designed the experience. My mom makes the patterns. That's a pretty good division of labor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devex</category>
      <category>ai</category>
      <category>development</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
