<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruslan Hrekov</title>
    <description>The latest articles on DEV Community by Ruslan Hrekov (@hrekov).</description>
    <link>https://dev.to/hrekov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4153403%2Fd4606199-5154-429f-85e5-48070b3df40a.jpg</url>
      <title>DEV Community: Ruslan Hrekov</title>
      <link>https://dev.to/hrekov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hrekov"/>
    <language>en</language>
    <item>
      <title>Two Claudes, one app</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Wed, 28 Oct 2026 05:39:17 +0000</pubDate>
      <link>https://dev.to/hrekov/two-claudes-one-app-133l</link>
      <guid>https://dev.to/hrekov/two-claudes-one-app-133l</guid>
      <description>&lt;p&gt;You have Claude Code running in a split tmux pane on your laptop, and Claude open in your browser or phone while away from your desk. Both are working on the same product. Both have access to context, both can draft ideas, and both will happily overwrite each other's work if you don't give them a handshake protocol.&lt;/p&gt;

&lt;p&gt;This isn't about running multi-agent swarm experiments with five synthetic personas. It is about the practical everyday friction of developing with AI across two physical devices without losing the plot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Context fragmentation is the silent tax of multi-device development.&lt;/p&gt;

&lt;p&gt;When you kick off an idea from your phone — a architectural critique, a rough feature outline, or an editorial angle — that session doesn't see your local uncommitted worktree. Meanwhile, the CLI session on your laptop has full filesystem access, the compiler, and git, but none of the conversational nuances you just explored on the phone.&lt;/p&gt;

&lt;p&gt;If you paste raw chat transcripts between them, you flood the context window with conversational filler and waste tokens. If you let both edit code concurrently on divergent branches, you spend forty minutes reconciling hallucinated imports and conflicting state schemas.&lt;/p&gt;

&lt;p&gt;The model doesn't know what you did across the room unless you write it down in a medium it can read.&lt;/p&gt;

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

&lt;p&gt;The temptation is to set up a shared memory sync tool or a cloud pasteboard. I tried keeping an active scratchpad in Notes and pasting blocks back and forth. It failed on the third turn: formatting broke, code blocks lost indentation, and neither Claude knew whether the snippet in front of it was current or superseded ten minutes prior.&lt;/p&gt;

&lt;p&gt;Next was branching every quick thought into a remote git branch. That created branch rot: a trail of one-commit branches named &lt;code&gt;feat/nav-idea-mobile&lt;/code&gt; and &lt;code&gt;fix/hero-tweak-draft&lt;/code&gt; that required manual squashing and dirty git reflogs.&lt;/p&gt;

&lt;p&gt;The fix came from treating the handoff like a folded paper note passed between desks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol
&lt;/h2&gt;

&lt;p&gt;The rulebook that settled the noise:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Role division by device capability.&lt;/strong&gt; The phone session does classification, critique, and specification. It never writes blind code it cannot run. The laptop session does implementation, execution, tests, and asset generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The filesystem is the only shared memory.&lt;/strong&gt; No ephemeral chat history crosses devices. If an architectural decision is made on the phone, it lands in a spec or a markdown note in the repo (&lt;code&gt;specs/&lt;/code&gt; or &lt;code&gt;memory/&lt;/code&gt;). If a learning repeats, it enters &lt;code&gt;DEVLOG.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The paper note handoff.&lt;/strong&gt; When switching contexts, the output of the first session must be an artifact: a single prompt command, a frontmatter block, or a targeted diff. The hero image for this post illustrates the exact mechanic — two devices resting on the desk, with a folded paper note between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preflight verification before merge.&lt;/strong&gt; Every change, regardless of where the idea originated, must pass &lt;code&gt;npm run verify:numbers&lt;/code&gt;, linting, and a local production build on the host machine before any commit lands on &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;This exact post was born through that loop. The editorial direction and prompt were drafted on the fly; the hero image (&lt;code&gt;/blog/two-claudes-one-app/hero-studio.jpg&lt;/code&gt;) was generated directly from the terminal via the studio pipeline (&lt;code&gt;node scripts/gen-blog-hero.mjs&lt;/code&gt;); and the resulting frontmatter block was handed off cleanly to the codebase without a single token wasted on intermediate conversational chatter.&lt;/p&gt;

&lt;p&gt;Zero merge conflicts. Zero context drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson
&lt;/h2&gt;

&lt;p&gt;When you collaborate with AI across multiple surfaces, don't try to build a distributed nervous system. Build a mail slot.&lt;/p&gt;

&lt;p&gt;Keep the interface between your sessions dead simple: committed markdown for architecture, git diffs for implementation, and single-purpose commands for execution. The model is sharp enough to pick up the thread instantly if the note you hand it has clean boundaries.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>workflow</category>
      <category>meta</category>
      <category>process</category>
    </item>
    <item>
      <title>Shipping claude-shiploop</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 04:15:28 +0000</pubDate>
      <link>https://dev.to/hrekov/shipping-claude-shiploop-g2e</link>
      <guid>https://dev.to/hrekov/shipping-claude-shiploop-g2e</guid>
      <description>&lt;p&gt;Every case study on this site runs through the same loop: classify the request, write the spec, build to the spec, measure the result, promote what repeats, forget what doesn't. That loop had a working name — ShipLoop — a one-page protocol in &lt;code&gt;CLAUDE.md&lt;/code&gt;, five skills in &lt;code&gt;.claude/skills/&lt;/code&gt;, and a scorecard that reads commits, DEVLOG headings, and session logs to measure how often each project actually followed the loop.&lt;/p&gt;

&lt;p&gt;Today the whole thing is open source. One line to install into any Claude Code project you work in:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/ruslan4427/claude-shiploop/main/install.sh | bash&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ruslan4427/claude-shiploop" rel="noopener noreferrer"&gt;github.com/ruslan4427/claude-shiploop&lt;/a&gt;. MIT, no sign-up, no telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this exists
&lt;/h2&gt;

&lt;p&gt;Writing clean code with Claude isn't the hard part anymore — the models are strong enough to carry a feature if you point them well. The hard part is the surrounding discipline: catching the request that should be a quick question before it turns into a four-file refactor, keeping a spec/plan/tasks triple so the model can't drift, measuring whether the output actually shipped or just felt like it shipped.&lt;/p&gt;

&lt;p&gt;Every project where I eventually built that discipline looked the same at month three: a &lt;code&gt;CLAUDE.md&lt;/code&gt;, a DEVLOG, a classifier for inbound requests, a loop that promoted learnings into durable rules. Every project where I didn't build it looked the same at month three too — more tokens burned, more rework, cases where the model wrote a feature I'd already decided to cut.&lt;/p&gt;

&lt;p&gt;The point of claude-shiploop is to skip the slow burn. Clone the discipline, measure yourself, delete whatever doesn't fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;p&gt;Three things, each independently usable:&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;protocol&lt;/strong&gt; — a nine-lane request classifier (Q, B, S, L, R, T, D, M, O, E) plus a spec-plan-tasks gate for anything above small-feature size. Lives in &lt;code&gt;CLAUDE.md&lt;/code&gt;. Takes about two minutes to read. If you only steal one piece, steal this one.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;skills&lt;/strong&gt; — five entry-point slash commands (&lt;code&gt;/shiploop-start&lt;/code&gt;, &lt;code&gt;/shiploop-router&lt;/code&gt;, &lt;code&gt;/shiploop-brainstorm&lt;/code&gt;, &lt;code&gt;/shiploop-tester&lt;/code&gt;, &lt;code&gt;/shiploop-observer&lt;/code&gt;) that live in &lt;code&gt;.claude/skills/&lt;/code&gt;. Each one is a markdown file; nothing is compiled. You can edit them in place without rebuilding anything.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;scorecard&lt;/strong&gt; — a Node parser that reads your project's commits, DEVLOG entries, session logs, and git history, and emits five metrics per project: discipline (how many features went through spec-plan-tasks), rework ratio (reverted or amended lines ÷ merged lines), time-to-ship, cost per shipped feature, and tokens-per-ship. All five are explicitly nullable. If the data isn't there, the metric is null, not estimated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Null is honest
&lt;/h2&gt;

&lt;p&gt;The one rule that holds the whole scorecard together: if a project pre-dates per-project session isolation, or doesn't have the DEVLOG discipline yet, the metric comes back as &lt;code&gt;—&lt;/code&gt;, not a number pulled from thin air.&lt;/p&gt;

&lt;p&gt;Three of my own seven projects have &lt;code&gt;—&lt;/code&gt; in the tokens column. That's visible on &lt;a href="https://hrekov.dev/tools" rel="noopener noreferrer"&gt;/tools&lt;/a&gt; right now. The scorecard refuses to fabricate numbers that would make the methodology look stronger than it is, because the first time a null gets softened to "estimated 15k tokens" is the first time the whole table loses its credibility.&lt;/p&gt;

&lt;p&gt;The three-tier assignment (naked / self-spec / shiploop) is a human call, documented per project in &lt;code&gt;/tools&lt;/code&gt;. It's not auto-detected and it's not pretending to be — that's a judgement about what methodology was actually in play, not a count.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxx59t4p793u2cg9nlix6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxx59t4p793u2cg9nlix6.jpg" alt="An open notebook with faint ruled lines and a pen resting beside it." width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Some rows get numbers. Some get an em-dash. The dash is doing more work than the numbers — it's the promise that the table hasn't been padded.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scorecard shows
&lt;/h2&gt;

&lt;p&gt;Across the seven projects I've shipped from this site, two patterns held up:&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;discipline column&lt;/strong&gt; is a floor for everything else. Projects that ran through spec-plan-tasks had rework ratios between 0.2 and 0.6; projects that didn't ranged from 2.0 to 4.5. The ratio of ratios — about ten times less rework when the discipline is in place — survived across different project sizes, languages, and client types.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;time-to-ship column&lt;/strong&gt; moved the opposite way from what I expected. Projects that spent longer at the spec stage shipped sooner than projects that started coding on day one. The spec time was never visible in the "I'm working" sense, which made it feel slow in the moment. The scorecard made it visible.&lt;/p&gt;

&lt;p&gt;Full tables, methodology aside, and the three-tier call for each project: &lt;a href="https://hrekov.dev/tools" rel="noopener noreferrer"&gt;/tools&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The install, one more time
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/ruslan4427/claude-shiploop/main/install.sh | bash&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Supports &lt;code&gt;--dry-run&lt;/code&gt; (prints the file-list without touching anything) and &lt;code&gt;--target &amp;lt;dir&amp;gt;&lt;/code&gt; (install into a scratch directory for inspection). Reads your existing &lt;code&gt;CLAUDE.md&lt;/code&gt; and merges the ShipLoop block in — doesn't overwrite your rules. The install script is 178 lines of bash, no dependencies beyond &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;git&lt;/code&gt;, and it's the first file worth reading before you let it run.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ruslan4427/claude-shiploop" rel="noopener noreferrer"&gt;github.com/ruslan4427/claude-shiploop&lt;/a&gt;. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>process</category>
      <category>opensource</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Classify before you code</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Sat, 17 Oct 2026 20:47:35 +0000</pubDate>
      <link>https://dev.to/hrekov/classify-before-you-code-24f7</link>
      <guid>https://dev.to/hrekov/classify-before-you-code-24f7</guid>
      <description>&lt;p&gt;Every request you send Claude Code looks the same at the API: tokens in, tokens out. But in your project, every request is a different job. One wants a two-line answer. One wants a careful bug fix. One is a new feature that needs a spec first. One is a brainstorm that will burn an hour and leave no code at all.&lt;/p&gt;

&lt;p&gt;Running them through the same lane is the most expensive habit I've seen teams keep. A quick question turns into a four-file refactor because nothing told the model to stop at "just answer the question." A bug fix drifts into a cleanup nobody asked for. You see it in the token bill at the end of the month.&lt;/p&gt;

&lt;p&gt;The fix takes ninety seconds. You do it in your head before any work starts: pick the lane, pick the model, decide if this one needs a spec. Then begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten lanes
&lt;/h2&gt;

&lt;p&gt;Straight from the project's &lt;code&gt;CLAUDE.md&lt;/code&gt; — the file every session reads first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Q&lt;/strong&gt; Quick question&lt;/td&gt;
&lt;td&gt;"what", "explain", "how does", "why"&lt;/td&gt;
&lt;td&gt;Answer directly&lt;/td&gt;
&lt;td&gt;haiku&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;B&lt;/strong&gt; Bug&lt;/td&gt;
&lt;td&gt;"error", "crash", "doesn't work", "fix"&lt;/td&gt;
&lt;td&gt;Diagnose → fix → verify → DEVLOG&lt;/td&gt;
&lt;td&gt;sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S&lt;/strong&gt; Small feature&lt;/td&gt;
&lt;td&gt;&amp;lt; 3 files, &amp;lt; 1h&lt;/td&gt;
&lt;td&gt;Verify spec exists → implement → DEVLOG&lt;/td&gt;
&lt;td&gt;sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;L&lt;/strong&gt; Large feature&lt;/td&gt;
&lt;td&gt;new screen/service, &amp;gt; 3 files&lt;/td&gt;
&lt;td&gt;Suggest Opus → full spec+plan+tasks cycle&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;opus&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;R&lt;/strong&gt; Refactor&lt;/td&gt;
&lt;td&gt;"refactor", "clean", "extract"&lt;/td&gt;
&lt;td&gt;Analyze → propose → implement&lt;/td&gt;
&lt;td&gt;sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;T&lt;/strong&gt; Testing/QA&lt;/td&gt;
&lt;td&gt;"test", "QA", "UX", "analyze"&lt;/td&gt;
&lt;td&gt;Run the tester skill&lt;/td&gt;
&lt;td&gt;sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;D&lt;/strong&gt; Discovery&lt;/td&gt;
&lt;td&gt;"brainstorm", "design", "ideas", "how should we"&lt;/td&gt;
&lt;td&gt;Suggest Opus → brainstorm skill&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;opus&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;M&lt;/strong&gt; Memory&lt;/td&gt;
&lt;td&gt;"what did we", "status", "where are we"&lt;/td&gt;
&lt;td&gt;Read &lt;code&gt;memory/&lt;/code&gt; → answer&lt;/td&gt;
&lt;td&gt;haiku&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;O&lt;/strong&gt; Observer&lt;/td&gt;
&lt;td&gt;"optimize agents", "improve workflow"&lt;/td&gt;
&lt;td&gt;Chain-health audit&lt;/td&gt;
&lt;td&gt;sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;E&lt;/strong&gt; Expert&lt;/td&gt;
&lt;td&gt;"review", "second opinion", "pre-ship check"&lt;/td&gt;
&lt;td&gt;Expert subagent + domain capsule&lt;/td&gt;
&lt;td&gt;sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table sits at the top of a protocol file every session loads before the first reply. The classifying happens quietly — what you see is just the lane's action, not its name. But the routing underneath is fixed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6gspbvzb670a2s0wtp8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6gspbvzb670a2s0wtp8.jpg" alt="Editorial ink top-down map of a sorting yard. Ten short loading bays run parallel, each with a different-sized package being handled: tiny envelope at bay Q, medium parcel at B, large crate at L, oversized shipping container at D. Workers in flat silhouette carry each package to its own bay. A single conveyor belt approaches from the top and splits into ten." width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each lane does one job well. The letter on the bay tells the worker which pattern to follow — same input, ten different outcomes depending on where it lands.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ten and not three
&lt;/h2&gt;

&lt;p&gt;Most frameworks I've read squeeze this into three lanes: question / task / big-task. That shortcut is where it falls apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q and B need to stay apart.&lt;/strong&gt; A quick question wants the shortest answer that clears up the confusion. A bug wants a repro, a root cause, a fix, a verification, and a line in the DEVLOG. Mash them together and Haiku gets handed the bug (slow and risky), while Sonnet gets handed the question (expensive and too wordy).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L and D need to stay apart.&lt;/strong&gt; A large feature arrives with a destination — "ship the booking flow." A discovery arrives with none — "how should we approach bookings?" The feature lane runs spec → plan → tasks → build. The discovery lane runs brainstorm → spec draft → human decision → &lt;em&gt;then maybe&lt;/em&gt; a build. Mash them and you'll get premature code for a question that was still open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E only shows up if you built the mechanism.&lt;/strong&gt; The expert lane loads a domain cheat-sheet for your kind of project and reviews against it. If you don't have the cheat-sheet or the subagent, this lane doesn't fire — the request falls back to R or T. If you're starting fresh you can skip E for weeks and lose nothing.&lt;/p&gt;

&lt;p&gt;The number isn't for show. Every lane earns its row because the lanes next to it would do the job worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model per lane
&lt;/h2&gt;

&lt;p&gt;The model column is the second half of the pattern, and the one that most directly moves the token bill.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[MetricGrid — see full post]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Haiku on Q and M saves ninety percent on every "what does this file do" and every "where are we on sprint 14." Those are the most frequent lanes by count — pushing them to the cheapest model is the single biggest cost lever in the whole protocol.&lt;/p&gt;

&lt;p&gt;Opus on L and D is the opposite move. A deliberate step up to a smarter model when the output will stick around for months — the spec, the plan, the brainstorm transcript. Those documents outlive the session. Paying Opus prices for them is cheaper than paying Sonnet to write a weak spec that turns into three weeks of rework.&lt;/p&gt;

&lt;p&gt;Sonnet in the middle is the general lane. It's where most of the keyboard time happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec gate
&lt;/h2&gt;

&lt;p&gt;The classification feeds one more decision: does this lane need a spec before any file gets touched?&lt;/p&gt;

&lt;p&gt;Two lanes do: &lt;strong&gt;L&lt;/strong&gt; and &lt;strong&gt;D&lt;/strong&gt;. Both say it out loud in the table — "full spec+plan+tasks cycle" / "brainstorm → spec draft." Nothing in those lanes gets built until a &lt;code&gt;specs/&amp;lt;N&amp;gt;-&amp;lt;slug&amp;gt;/&lt;/code&gt; folder has three signed-off documents inside it.&lt;/p&gt;

&lt;p&gt;Every other lane skips the gate. A quick question doesn't need a spec. A bug fix doesn't need a spec — it needs a diagnosis and a DEVLOG line. A refactor needs a quick proposal in chat, not a sprint folder.&lt;/p&gt;

&lt;p&gt;The gate fires only on the heavy lanes because applying it everywhere would be deadly. Picture opening a spec folder for every "what does &lt;code&gt;useEffect&lt;/code&gt; do" — the overhead would eat the project. The point is: the discipline matches the weight of the lane.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lane I keep getting wrong
&lt;/h2&gt;

&lt;p&gt;D (Discovery) and Q (Quick question). These two look nearly the same in the wild and want opposite things.&lt;/p&gt;

&lt;p&gt;"How should we approach caching?" sounds like a question. It isn't. It's a discovery — the honest answer wants trade-offs, not a definition.&lt;/p&gt;

&lt;p&gt;"What does cache invalidation mean?" sounds similar. It is a question — the honest answer is three sentences and a link.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nb5xntjssxl1pfvdzc1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nb5xntjssxl1pfvdzc1.jpg" alt="Editorial ink drawing of a lone traveller with a laptop standing at a path fork. Two wooden signs point in similar directions, their lettering nearly identical: DISCOVERY → pointing up a long winding trail into the hills, and QUESTION → pointing to a short straight path with a bench at the end. The traveller tilts their head, confused — the signs look alike at first glance." width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The signs look the same from a distance. The tell is the walk — one is a short straight path with a bench at the end; the other winds for an hour.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The tell is what the right answer looks like. If the honest answer starts with "it depends on your constraints — tell me about…", you're in D. If it starts with "it's X, which does Y," you're in Q. The question I ask myself: &lt;em&gt;does this want a conversation, or does it want an answer?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Get this one wrong in the D direction and you lose an hour. Get it wrong in the Q direction and you lose a day — because the model is now drafting a spec for a feature you didn't actually want to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this fixes
&lt;/h2&gt;

&lt;p&gt;Across the seven projects on &lt;a href="https://hrekov.dev/tools" rel="noopener noreferrer"&gt;/tools&lt;/a&gt;, one pattern held up:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[MetricGrid — see full post]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Roughly ten times less rework when the classifying is in place. That ten-times gap held across every project size I tried — Flutter mobile apps, Next.js SaaS, backend pipelines. The thing that moved the number wasn't the model, wasn't the stack. It was those ninety seconds before each request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7cuwc5oeoz1xxrqkfp1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7cuwc5oeoz1xxrqkfp1.jpg" alt="Editorial ink diptych. Left room: a tidy carpentry workshop with four finished chairs lined up along the wall and one broken part on the floor. A small caption below reads '0.4 rework'. Right room: the same workshop in chaos — one finished chair, four broken parts piled around a frustrated carpenter rubbing his forehead. A small caption below reads '4.5 rework'. Both rooms are the same size and layout; the only difference is the pile." width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same tools, same wood, same carpenter. The only difference between the two rooms is whether someone sorted the orders before cutting began.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Time-to-ship moved the opposite way from what it felt like. Projects that classified every incoming request shipped sooner than projects that started typing on request one. The classifying time doesn't feel like progress in the moment — nothing is being built — which is exactly why it was the first discipline to drop whenever I let it. The scorecard made that gap visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to adopt
&lt;/h2&gt;

&lt;p&gt;Shortest path: steal the table.&lt;/p&gt;

&lt;p&gt;Open your project's &lt;code&gt;CLAUDE.md&lt;/code&gt;, paste the ten lanes word for word, and run the next twenty requests through it in your head. Nothing else changes. No subagents, no spec folders, no new files. Just the one question &lt;em&gt;which lane is this?&lt;/em&gt; before every reply. You'll start noticing which lanes you were mixing up.&lt;/p&gt;

&lt;p&gt;Full path: install the protocol.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/ruslan4427/claude-shiploop/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That installs the classifier, the five entry-point skills, the spec+plan+tasks scaffolding, and the scorecard that measures whether you're actually following the lanes you set. MIT, no sign-up, no telemetry. Repo: &lt;a href="https://github.com/ruslan4427/claude-shiploop" rel="noopener noreferrer"&gt;github.com/ruslan4427/claude-shiploop&lt;/a&gt;. The install script merges into your existing &lt;code&gt;CLAUDE.md&lt;/code&gt; without overwriting what's there.&lt;/p&gt;

&lt;p&gt;The scorecard that produced the rework-ratio numbers lives at &lt;a href="https://hrekov.dev/tools" rel="noopener noreferrer"&gt;/tools&lt;/a&gt;. Seven of my own projects, three tiers, five metrics each. Three of them show &lt;code&gt;—&lt;/code&gt; in the tokens column because the data isn't there to count from. That blank is a deliberate rule — the scorecard refuses to guess at numbers it can't measure.&lt;/p&gt;

&lt;p&gt;A full write-up of the launch, the install flow, and the "null is honest" rule sits at &lt;a href="https://dev.to/blog/shipping-claude-shiploop"&gt;/blog/shipping-claude-shiploop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The classifier itself takes ninety seconds per request to run. That's the whole ask. Everything else sits downstream of that one habit.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>process</category>
      <category>claudecode</category>
      <category>pattern</category>
    </item>
    <item>
      <title>No company, no API</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 05:52:10 +0000</pubDate>
      <link>https://dev.to/hrekov/no-company-no-api-59cd</link>
      <guid>https://dev.to/hrekov/no-company-no-api-59cd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fttte6xi286pnpnzue2l5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fttte6xi286pnpnzue2l5.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;The engagement agent on my SMM Factory pilot does two things while the owner is cutting hair: it leaves conversational comments on in-niche Instagram posts, and it replies to incoming DMs. Haiku writes them, the orchestrator rate-limits them, Playwright drives the browser session that posts them. No approval loop — this is the one path in the system that closes the human loop entirely.&lt;/p&gt;

&lt;p&gt;That whole stack is wrong by design. The right stack is Instagram Graph API through Meta Business Suite, with &lt;code&gt;instagram_manage_comments&lt;/code&gt; and &lt;code&gt;instagram_manage_messages&lt;/code&gt; write-scopes gated behind App Review. The reason I'm shipping on Playwright instead is one gate: Meta Business Verification requires a registered legal entity, and I was running this as a solo pilot without one.&lt;/p&gt;

&lt;p&gt;This post is about what that gate actually costs when you can't clear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What App Review wants
&lt;/h2&gt;

&lt;p&gt;The public documentation makes App Review sound like a code review with a privacy policy attached. It isn't. For any Advanced Access scope — which includes anything that writes to another user's account — you have to pass Business Verification first. Business Verification wants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A registered legal entity with a verifiable address.&lt;/li&gt;
&lt;li&gt;A DUNS number, or a local-equivalent registration document (incorporation papers, VAT registration, trade license).&lt;/li&gt;
&lt;li&gt;Supporting documents on file — a utility bill or bank statement with the business name, matched against government registries Meta cross-references.&lt;/li&gt;
&lt;li&gt;A privacy policy published on an active production domain, not a Vercel preview URL.&lt;/li&gt;
&lt;li&gt;An explainer video of the exact user flow, demonstrating the scope's use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an incorporated company this is a two-hour admin task. For a solo developer running a two-month pilot it's the whole deliverable, inverted. The gate didn't exist to filter me out specifically; it exists because this is Meta's single most-abused surface, and the business-entity check is a cheap way to make scaled spam uneconomic. By-design is the right word.&lt;/p&gt;

&lt;p&gt;Also — there is no sandbox mode for write-scopes. Standard Access gives you read-only testing against your own account. The moment you want to write to a client's inbox or feed, you need Advanced Access, and that path begins at Business Verification. You cannot ship a demo, show it to someone, and then fix the paperwork. The shipping step &lt;strong&gt;is&lt;/strong&gt; the paperwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped instead
&lt;/h2&gt;

&lt;p&gt;A Playwright session with a long-lived IG cookie, driven by two agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;publisher&lt;/strong&gt; uses the cookie to post content on Mondays after a Slack approval.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;engagement agent&lt;/strong&gt; uses the same cookie to comment on target feeds daily and reply to DMs inline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubimmd5hucpyy6je5jq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubimmd5hucpyy6je5jq.png" alt="@eternalasting.event's Instagram post — a Barber Shop tent at a lakeside sunset event. Comments drawer open. The noble.booking account commented 16 weeks ago (hearted by author): 'That Hot Mess Bakery cake looks incredible with the barbershop theme, and the picnic setup is such a creative way to celebrate the anniversary with your crew.' The account owner replied '@noble.booking Thank you !' and hearted the exchange." width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Comment landed · heart + thank-you from author&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngn1mblzf0exh7ic9nxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngn1mblzf0exh7ic9nxk.png" alt="@citybarberib's Instagram post — four barbers on a hilltop at dusk. Comments drawer open. The noble.booking account commented 17 weeks ago (hearted by author): 'How do you keep your tools so clean working mobile like that?' The author replied '@noble.booking you mean the clippers or everything?' — the question earned a conversational back-and-forth, not a one-line acknowledgment." width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Started a back-and-forth with the owner&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two auto-comments the engagement agent left on in-niche accounts (Prestige Barbershop event · City Barber). Both earned author replies and hearts. This is what the cookie-session fallback actually produces — Haiku-written, Playwright-posted, per-account rate-limited enough to pass as a human reader of the feed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Haiku generates the text. The orchestrator rate-limits by account and respects an interval floor between actions on the same thread. Everything is logged to Supabase so if an action pattern starts tripping IG's spam heuristics, the ledger can tell us exactly when the pattern changed.&lt;/p&gt;

&lt;p&gt;This works. It also has three real costs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cookie rotates.&lt;/strong&gt; When IG issues a new session cookie I have to re-authenticate manually. In the content pipeline logs you'll see this as &lt;code&gt;login_required&lt;/code&gt; on the trends endpoint — same mechanism, different scope. For the publisher I rotate on a weekly cadence; for engagement the ambient failure rate is higher because the surface is more active.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's outside Terms of Service.&lt;/strong&gt; IG's TOS doesn't make a distinction between a Playwright session with your own cookie and a scraper mimicking your account. If the pattern triggers rate-limit or spam heuristics, the account is paused. The right move at that point is to apologize to the client and accept it, not to look clever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It won't scale past a handful of clients.&lt;/strong&gt; Session cookies are per-account. If I'm running engagement for ten barbershops, I need ten cookies, ten weekly re-auth cycles, ten rate-limit windows to coordinate. The architecture is linear-cost in client count; the Graph API path would be constant-cost in the orchestrator and linear only in client API quotas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the metrics lie about
&lt;/h2&gt;

&lt;p&gt;The results table on the pilot's case study has one line that reads cleaner than it should:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;0 IG violations · 2-month pilot&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a true number. It's also entirely a function of running under rate limits with one client. If I'd scaled the exact same architecture to five clients, the number would've gone up. If I'd shipped under the Graph API with proper scopes, the metric wouldn't exist — compliance is the baseline, not an achievement.&lt;/p&gt;

&lt;p&gt;Any time you see a safety metric reported on a cookie-driven system, read it as a statement about pacing, not architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you ship this way?
&lt;/h2&gt;

&lt;p&gt;Depends on exactly one question: are you running a pilot or building the product?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pilot.&lt;/strong&gt; One client, two-month window, honest conversation with the owner about where the automation touches their account. Playwright + cookie is defensible. You're testing whether the system is worth the paperwork before you buy the paperwork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product.&lt;/strong&gt; Any plural number of clients, any plan to open a seat on a demo page. Register the entity, pass Business Verification, go through App Review. The paperwork isn't the cost of the API — it's the cost of writing to someone else's account at scale, and it's load-bearing on the whole product's story.&lt;/p&gt;

&lt;p&gt;The failure mode is doing a pilot's architecture past the pilot. The cookie that worked for one barbershop in a two-month window is the exact wrong substrate for anything after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Register the entity on day one, not day sixty. The entity doesn't gate the architecture decisions — I'd still have shipped the orchestrator, the Haiku stratification, the Slack approval, the knowledge-markdown system. What it gates is which API the production version runs against, and that question needs answering before the pipeline is load-bearing on a workaround.&lt;/p&gt;

&lt;p&gt;The pilot proved the system can run at cost. The thing it didn't prove — because the paperwork wasn't done — is whether the system could run at scale legally. Those are two different questions and I conflated them for most of the pilot.&lt;/p&gt;

&lt;p&gt;The clean API wasn't a lie. It was a filter, pointed exactly at builders like me, working exactly as designed.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>instagram</category>
      <category>platformgates</category>
      <category>automation</category>
    </item>
    <item>
      <title>Shipping the cross-poster</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:37:52 +0000</pubDate>
      <link>https://dev.to/hrekov/shipping-the-cross-poster-34hn</link>
      <guid>https://dev.to/hrekov/shipping-the-cross-poster-34hn</guid>
      <description>&lt;p&gt;The previous post promised cross-posts to LinkedIn and dev.to "once Sprint 14 wires the automation." Three days later, Sprint 14 shipped and you're reading proof — this post landed on both platforms through a cron job the site runs on itself.&lt;/p&gt;

&lt;p&gt;Here's what shipping that pipeline looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The itch
&lt;/h2&gt;

&lt;p&gt;Cross-posting manually dies at the third post. Open LinkedIn, strip the markdown, re-check character count, paste. Open dev.to, remember the canonical URL field, paste the body, slugify the tags, confirm. Each post is fifteen minutes of friction between "I wrote something" and "the people who follow me on those platforms can read it." After two tries you stop writing instead of fixing it.&lt;/p&gt;

&lt;p&gt;The fix had to clear a lower bar than you'd think: it had to work, land in the correct places, and not accidentally publish the same post twice. The rest is polish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint
&lt;/h2&gt;

&lt;p&gt;One decision gated everything: no dedicated service, no Zapier, no VPS. The site already deploys on Vercel and the code already lives on GitHub, so the pipeline had to live inside the repo. GitHub Actions free tier cron, scripts in the same language as the site, state in a committed file. If the cron ever misbehaves, I want &lt;code&gt;git log&lt;/code&gt; to tell me when and why.&lt;/p&gt;

&lt;p&gt;That ruled out LinkedIn's partner-reviewed Community Management API (multi-week approval gate for a solo-developer app) and ruled in the self-serve &lt;code&gt;w_member_social&lt;/code&gt; scope targeting the legacy &lt;code&gt;/v2/ugcPosts&lt;/code&gt; endpoint. Different surface, same posting outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Three hundred lines of orchestrator (&lt;code&gt;scripts/publish-due.mjs&lt;/code&gt;) read &lt;code&gt;content/blog/schedule.yml&lt;/code&gt;, cross-reference &lt;code&gt;.distribution-ledger.jsonl&lt;/code&gt;, and when a scheduled post is due and not already published, hand it to the matching publisher — &lt;code&gt;lib/distribution/devto.mjs&lt;/code&gt; or &lt;code&gt;lib/distribution/linkedin.mjs&lt;/code&gt;. On success, three things happen in a fixed order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Append to the ledger (&lt;code&gt;{slug, channel, publishedUrl, remoteId, ts}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Mutate the post's frontmatter (&lt;code&gt;status: posted&lt;/code&gt;, &lt;code&gt;publishedUrl&lt;/code&gt;, &lt;code&gt;postedAt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Commit and push with &lt;code&gt;[skip ci]&lt;/code&gt; so Vercel still rebuilds but the cron origin is filterable in &lt;code&gt;git log&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ordering is non-negotiable. If step 2 crashes, step 1 already recorded the publish — the next cron tick sees the ledger entry and skips the slug instead of double-posting. Reversing to frontmatter-first means a crash leaves the post un-marked-as-published; the next tick re-publishes.&lt;/p&gt;

&lt;p&gt;The ledger is checked into git. Had to catch that one in review — my first draft had &lt;code&gt;.distribution-ledger.jsonl&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt; because the filename looks like local state. GitHub Actions runners are ephemeral; an ignored ledger would see empty state on every tick and re-publish every past-scheduled post. The file isn't local state; it's the durable idempotency guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three traps I almost shipped
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/rest/posts&lt;/code&gt; vs &lt;code&gt;/v2/ugcPosts&lt;/code&gt;.&lt;/strong&gt; The LinkedIn docs lead you to the newer &lt;code&gt;/rest/posts&lt;/code&gt; under Community Management API. Looks modern, has nice typed schemas. Returns 403 with a self-serve token because that surface is partner-review-gated. The legacy UGC endpoint with &lt;code&gt;X-Restli-Protocol-Version: 2.0.0&lt;/code&gt; (and no &lt;code&gt;LinkedIn-Version&lt;/code&gt; header) is what self-serve apps actually get. A full afternoon vanished before I noticed the two docs sets live in parallel universes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh tokens don't exist on self-serve.&lt;/strong&gt; The access token is sixty days. The weekly refresh worker I wrote assumed a refresh token would arrive — none did. Not a bug; an empirical limit of the &lt;code&gt;w_member_social&lt;/code&gt; scope. Fix was a graceful no-op with a cliff-countdown warning, and a calendar reminder two weeks before expiry to re-run the OAuth handshake manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Company Pages aren't optional, but they're one-time.&lt;/strong&gt; My app wouldn't bind to a personal profile; LinkedIn's post-2019 verification chain wants a Page as the ownership anchor. Created a minimal Page in fifteen minutes, bound the app, then never used the Page again. All posts go to the personal profile via &lt;code&gt;urn:li:person:XKuTKDehCv&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs now
&lt;/h2&gt;

&lt;p&gt;Hourly on &lt;code&gt;15 * * * *&lt;/code&gt;. Twenty-one seconds end-to-end on a cold GitHub Actions runner. On ticks with nothing due, the output is one line — &lt;code&gt;[publish-due] nothing due at &amp;lt;iso-timestamp&amp;gt;&lt;/code&gt; — and the job exits clean. On ticks with a due post, you see the publish lines, the devlog entry, and a cron commit landing in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first live run shipped the previous post, &lt;a href="https://dev.to/blog/launching-the-journal"&gt;Launching the journal&lt;/a&gt;, to both platforms. That post went live manually because I needed to validate the renderers against real APIs before trusting the cron. This post is the first one going through the full automated path — authored, scheduled, pushed, published by the machine, with a commit back on &lt;code&gt;main&lt;/code&gt; to prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this post itself proves
&lt;/h2&gt;

&lt;p&gt;You're reading a Build Log about a cross-posting pipeline. If you found it on dev.to or LinkedIn, that pipeline is what put it there — the schedule-driven path with no manual step between my &lt;code&gt;git push&lt;/code&gt; and your scroll.&lt;/p&gt;

&lt;p&gt;The whole thing is in the open: three hundred lines of orchestrator at &lt;a href="https://github.com/ruslan4427/portfolio/blob/main/scripts/publish-due.mjs" rel="noopener noreferrer"&gt;scripts/publish-due.mjs&lt;/a&gt;, four invariants locked into &lt;a href="https://github.com/ruslan4427/portfolio/blob/main/STABLE_LOGIC.md" rel="noopener noreferrer"&gt;STABLE_LOGIC.md&lt;/a&gt;, the full ship retro at &lt;a href="https://github.com/ruslan4427/portfolio/blob/main/DEVLOG.md" rel="noopener noreferrer"&gt;DEVLOG.md&lt;/a&gt;. Clone the repo, read the cron, run the dry-run — the pipeline isn't a demo of a clever idea, it's the thing that made this very post appear in two more places than I had to type.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>automation</category>
      <category>distribution</category>
    </item>
    <item>
      <title>Launching the journal</title>
      <dc:creator>Ruslan Hrekov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 02:27:51 +0000</pubDate>
      <link>https://dev.to/hrekov/launching-the-journal-3fh5</link>
      <guid>https://dev.to/hrekov/launching-the-journal-3fh5</guid>
      <description>&lt;p&gt;The site had five case studies and no place to put anything shorter. Everything I wanted to write about — a debugging trace, a pattern that keeps paying off, a skeptical read on a tool the timeline is excited about — needed the same weight as a full case study or it got dropped. That's the wrong shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/blog&lt;/code&gt; supports four formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Case Study&lt;/strong&gt; — long-form, receipts attached, commit hashes referenced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Log&lt;/strong&gt; — chronological, dated, mid-work. This post is one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field Note&lt;/strong&gt; — skeptical take on something popular. Short, arguable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern&lt;/strong&gt; — a rule I keep applying, why, and when it breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each post lands with reading time, tags, and (where relevant) a &lt;strong&gt;Receipts&lt;/strong&gt; aside — links to the actual commits, prompts, or artifacts behind the claims. If I can't cite it, I don't write it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MDX with frontmatter validated at build time (invalid frontmatter throws).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/rss.xml&lt;/code&gt; is generated from the same source with CDATA descriptions.&lt;/li&gt;
&lt;li&gt;Google Search Console meta tag ships via env var, so the feed shows up.&lt;/li&gt;
&lt;li&gt;Analytics: GA4 with IP anonymisation, off by default for EU visitors until you click accept. &lt;code&gt;proxy.ts&lt;/code&gt; (Next.js 16's edge middleware convention) reads Vercel's &lt;code&gt;x-vercel-ip-country&lt;/code&gt; header and sets a &lt;code&gt;geo-eu&lt;/code&gt; cookie so the client knows which path to render.&lt;/li&gt;
&lt;li&gt;No third-party tracking beyond that. No newsletter form yet.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;First real posts land over the next two weeks — one Field Note on prompt patterns that stopped working when I moved from Sonnet to Opus, one Build Log on Sprint 11 itself, and a Pattern write-up of &lt;code&gt;STABLE_LOGIC.md&lt;/code&gt; (the file that keeps AI collaboration from unwinding your own fixes).&lt;/p&gt;

&lt;p&gt;Subscribe via &lt;a href="///rss.xml"&gt;RSS&lt;/a&gt;, or wait for the LinkedIn + dev.to cross-posts — Sprint 14 wires the schedule-driven automation that lands this very post on both platforms.&lt;/p&gt;

</description>
      <category>meta</category>
      <category>portfolio</category>
    </item>
  </channel>
</rss>
