<?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: Gad Ofir</title>
    <description>The latest articles on Forem by Gad Ofir (@gad_ofir_076c468dd15d483b).</description>
    <link>https://forem.com/gad_ofir_076c468dd15d483b</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%2F3880929%2Fdc7b95cc-8e4c-4ccb-af36-3896c39121e4.png</url>
      <title>Forem: Gad Ofir</title>
      <link>https://forem.com/gad_ofir_076c468dd15d483b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gad_ofir_076c468dd15d483b"/>
    <language>en</language>
    <item>
      <title>How I automated my own LinkedIn + Dev.to publishing in one afternoon (and what broke along the way)</title>
      <dc:creator>Gad Ofir</dc:creator>
      <pubDate>Wed, 15 Apr 2026 17:46:48 +0000</pubDate>
      <link>https://forem.com/gad_ofir_076c468dd15d483b/how-i-automated-my-own-linkedin-devto-publishing-in-one-afternoon-and-what-broke-along-the-way-21m</link>
      <guid>https://forem.com/gad_ofir_076c468dd15d483b/how-i-automated-my-own-linkedin-devto-publishing-in-one-afternoon-and-what-broke-along-the-way-21m</guid>
      <description>&lt;h1&gt;
  
  
  How I automated my own LinkedIn + Dev.to publishing in one afternoon (and what broke along the way)
&lt;/h1&gt;

&lt;p&gt;I'm a backend engineer transitioning to full-stack and trying to build more public presence. The plan: every time I ship something, a single command should write a proper article and post it to LinkedIn and a dev blog — so my GitHub activity actually becomes visible to recruiters without me having to remember.&lt;/p&gt;

&lt;p&gt;What I ended up building is a skill called &lt;code&gt;publish-project&lt;/code&gt; that sits inside my &lt;a href="https://github.com/GadOfir/LOS-starter" rel="noopener noreferrer"&gt;LOS&lt;/a&gt; memory system. You run one command, it reads a GitHub repo plus local project notes, writes a real article, and publishes it to Dev.to and LinkedIn in the same run.&lt;/p&gt;

&lt;p&gt;Here's the honest log of how it came together, including the three dead ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just "write a script"?
&lt;/h2&gt;

&lt;p&gt;I'm tired of writing scripts that generate LinkedIn posts with &lt;code&gt;[Add your motivation here...]&lt;/code&gt; placeholders I then fill in manually. The whole point was to eliminate that step. If I'm still hand-editing the output, automation bought me nothing.&lt;/p&gt;

&lt;p&gt;So the bar was: &lt;strong&gt;the article content has to come from real files on disk, not from a template the LLM fills in later.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead end 1: Medium
&lt;/h2&gt;

&lt;p&gt;I started with Medium because the &lt;a href="https://github.com/Medium/medium-api-docs" rel="noopener noreferrer"&gt;original Medium API docs&lt;/a&gt; describe a clean self-issued integration token flow. Looked straightforward.&lt;/p&gt;

&lt;p&gt;It isn't. Medium's docs now say, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT: We don't allow any new integrations with our API.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your account pre-dates the cutoff, your existing token still works. Mine didn't. There is no way to generate a new integration token for a new Medium account as of 2026. I checked the settings page — the Integration Tokens section literally isn't rendered for new accounts.&lt;/p&gt;

&lt;p&gt;Pivot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev.to: the easy path
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;Dev.to's API&lt;/a&gt; is the opposite experience.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;code&gt;https://dev.to/settings/extensions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Generate an API key (one click)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/articles&lt;/code&gt; with your markdown and tags&lt;/li&gt;
&lt;li&gt;Done
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&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;api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&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;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&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;article&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# max 4, lowercase, alphanumeric
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api/articles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration. Worked first try. I wrapped it in a &lt;code&gt;DevtoClient&lt;/code&gt; class with two methods (&lt;code&gt;get_user&lt;/code&gt;, &lt;code&gt;publish_article&lt;/code&gt;) and moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead end 2: LinkedIn's UGC Posts API
&lt;/h2&gt;

&lt;p&gt;LinkedIn was supposed to be the easy one. I followed an old tutorial, built the request, and got this back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error 422: com.linkedin.common.error.BadRequest
"com.linkedin.ugc.UGCContent" is not a member type of union [...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some digging I found the cause: &lt;strong&gt;LinkedIn deprecated the &lt;code&gt;/v2/ugcPosts&lt;/code&gt; endpoint in favour of a new &lt;code&gt;/rest/posts&lt;/code&gt; endpoint.&lt;/strong&gt; The old payload shape (&lt;code&gt;specificContent.com.linkedin.ugc.UGCContent.shareCommentary.text&lt;/code&gt;) is gone. The new one is dramatically simpler:&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;POST&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linkedin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;

&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bearer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Restli&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="n"&gt;Linkedin&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202604&lt;/span&gt;
  &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&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;author&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;urn:li:person:&amp;lt;id&amp;gt;&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;commentary&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;Your post text&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;visibility&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;PUBLIC&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;distribution&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feedDistribution&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;MAIN_FEED&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;targetEntities&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thirdPartyDistributionChannels&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;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lifecycleState&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;PUBLISHED&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;isReshareDisabledByAuthor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things that tripped me up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The endpoint is &lt;code&gt;https://api.linkedin.com/rest/posts&lt;/code&gt;, &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;https://api.linkedin.com/v2/rest/posts&lt;/code&gt;. I had &lt;code&gt;LINKEDIN_API = "https://api.linkedin.com/v2"&lt;/code&gt; as a constant and was string-concatenating &lt;code&gt;/rest/posts&lt;/code&gt; onto it, which produced a 404 &lt;code&gt;RESOURCE_NOT_FOUND&lt;/code&gt;. I split the constant into &lt;code&gt;LINKEDIN_API_V2&lt;/code&gt; (for &lt;code&gt;/v2/userinfo&lt;/code&gt;) and &lt;code&gt;LINKEDIN_API&lt;/code&gt; (bare base URL) to fix this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A successful &lt;code&gt;POST /rest/posts&lt;/code&gt; returns &lt;strong&gt;201 with an empty body&lt;/strong&gt;. The post ID comes back in the &lt;code&gt;x-restli-id&lt;/code&gt; response header. My client initially tried to &lt;code&gt;.json()&lt;/code&gt; the empty body and crashed with "Expecting value: line 1 column 1". Fixed with a &lt;code&gt;try/except&lt;/code&gt; that falls back to reading the header.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;LinkedIn also has content-hash deduplication. If you POST the exact same commentary twice within a short window, the second call fails with &lt;code&gt;DUPLICATE_POST&lt;/code&gt;. This is actually a gift — it saved me from spamming my own feed while testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead end 3: generating text that looked real but wasn't
&lt;/h2&gt;

&lt;p&gt;This is where I spent the most time and made the most mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1:&lt;/strong&gt; I wrote a template function that produced markdown with placeholders: "Here's what I learned: [Add 3-5 takeaways]". It "worked" in that the script ran. But the published article was garbage because I forgot that &lt;em&gt;nobody was going to fill those placeholders in&lt;/em&gt;. I published it to Dev.to before I read what it looked like. I had to delete it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2:&lt;/strong&gt; I hardcoded all the text directly into the Python script — a long f-string with generic "the task system was the whole point" narration. Better than placeholders, but it was the same article regardless of which repo I pointed it at. Not reusable. Still technically wrong in places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3:&lt;/strong&gt; I rewrote the whole generator to read real files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET https://api.github.com/repos/{owner}/{repo}&lt;/code&gt; for metadata&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /repos/{owner}/{repo}/readme&lt;/code&gt; (with &lt;code&gt;Accept: application/vnd.github.v3.raw&lt;/code&gt;) for full README content&lt;/li&gt;
&lt;li&gt;Local filesystem reads from &lt;code&gt;memory/projects/&amp;lt;project&amp;gt;/tasks.md&lt;/code&gt; and &lt;code&gt;decisions.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A small markdown table parser that extracts rows into dicts, keyed by column header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table parser turned out to be the crucial piece. My &lt;code&gt;tasks.md&lt;/code&gt; and &lt;code&gt;decisions.md&lt;/code&gt; follow a consistent shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Task | Why | Priority | Status |
|------|-----|----------|--------|
| Adopt BMAD patterns (task-001) | Onboarding gate, state tracking | 1 | Done |
| Cross-repo sync (task-002) | Both repos need identical task-system | 1 | Done |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser walks the lines, finds a header row matching a required set of columns, and yields each data row as &lt;code&gt;{"task": ..., "why": ..., "status": ...}&lt;/code&gt;. The generator then interpolates real task names and real decisions into the article body — no placeholders, no hardcoded claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three mistakes I made after that
&lt;/h2&gt;

&lt;p&gt;Even with real data, I still shipped bad articles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I claimed "5 tasks built it"&lt;/strong&gt; based on the fact that &lt;code&gt;los-starter/tasks.md&lt;/code&gt; has 5 rows. But those 5 are just the infrastructure-bootstrap tasks for one subfolder — the actual LOS project is many skills, a landing page, a memory system, an update mechanism. The number was technically correct but the framing was dishonest. Cut it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I called LOS a "file-based OS for my dev work"&lt;/strong&gt;. It isn't. LOS is a &lt;em&gt;memory system&lt;/em&gt; for Claude Code — markdown files that Claude reads at session start so every conversation picks up where the last one left off. Calling it an "OS" flattened the core idea.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I included 8 skills&lt;/strong&gt; in the skills list. The LOS-starter README says 6. I had added &lt;code&gt;/update-los&lt;/code&gt; and &lt;code&gt;/publish-project&lt;/code&gt; to my "core skills" set because they exist on disk, but they aren't in the public skill lineup. Trimmed to 6.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these was a two-line fix. Each one was also the kind of subtle wrong you can only spot by reading the output carefully, not by running tests. If you're automating publishing, &lt;strong&gt;read every post before it goes live, even in &lt;code&gt;--confirm&lt;/code&gt; mode.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the skill does now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Preview everything without posting&lt;/span&gt;
python publish_project.py &lt;span class="nt"&gt;--repo&lt;/span&gt; LOS-starter

&lt;span class="c"&gt;# Publish article to Dev.to + post to LinkedIn&lt;/span&gt;
python publish_project.py &lt;span class="nt"&gt;--repo&lt;/span&gt; LOS-starter &lt;span class="nt"&gt;--confirm&lt;/span&gt;

&lt;span class="c"&gt;# Post a fresh LinkedIn post pointing at an existing Dev.to article&lt;/span&gt;
python publish_project.py &lt;span class="nt"&gt;--repo&lt;/span&gt; LOS-starter &lt;span class="nt"&gt;--linkedin-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--article-url&lt;/span&gt; https://dev.to/you/your-article &lt;span class="nt"&gt;--confirm&lt;/span&gt;

&lt;span class="c"&gt;# Just write the article to a local markdown file for review&lt;/span&gt;
python publish_project.py &lt;span class="nt"&gt;--repo&lt;/span&gt; LOS-starter &lt;span class="nt"&gt;--save&lt;/span&gt; out.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetch GitHub&lt;/strong&gt; — repo metadata and full README from the GitHub API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read local project&lt;/strong&gt; — look for &lt;code&gt;memory/projects/&amp;lt;name&amp;gt;/&lt;/code&gt;, parse &lt;code&gt;tasks.md&lt;/code&gt; and &lt;code&gt;decisions.md&lt;/code&gt; as markdown tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read core skills&lt;/strong&gt; — scan &lt;code&gt;.claude/skills/*/SKILL.md&lt;/code&gt;, extract description from frontmatter, filter to the skills actually listed in my public README&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the richer README&lt;/strong&gt; — compare section counts between GitHub README and local README, use whichever has more structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate article&lt;/strong&gt; — interpolate all of the above into an article body with real sections: idea, stack, features, skills, one skill deep-dive, real tasks table, real decisions list, architecture notes, try-it block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build LinkedIn post&lt;/strong&gt; — short version with both the Dev.to URL and the GitHub URL on their own lines (LinkedIn renders link previews only when a URL is on its own line)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; — Dev.to first (because we need the URL for the LinkedIn post), then LinkedIn with the fresh URL included&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to feed it
&lt;/h2&gt;

&lt;p&gt;Three inputs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub repo name&lt;/strong&gt; via &lt;code&gt;--repo &amp;lt;name&amp;gt;&lt;/code&gt;. Bare repo names are resolved against my GitHub username; full &lt;code&gt;owner/repo&lt;/code&gt; works too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A local project folder at &lt;code&gt;memory/projects/&amp;lt;slug&amp;gt;/&lt;/code&gt;&lt;/strong&gt; (optional but strongly recommended). This is where the interesting data lives: &lt;code&gt;README.md&lt;/code&gt;, &lt;code&gt;tasks.md&lt;/code&gt;, &lt;code&gt;decisions.md&lt;/code&gt;. Without this, the article is just README paraphrase. With it, the article has specific decisions with dates and real task names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; with three tokens&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;   &lt;span class="py"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ghp_...&lt;/span&gt;
   &lt;span class="py"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;
   &lt;span class="py"&gt;LINKEDIN_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub token is optional (raises the rate limit from 60/hr unauthenticated to 5000/hr). The other two are required if you want to actually publish.&lt;/p&gt;

&lt;p&gt;That's the whole contract. If you keep your project notes in the LOS format — one folder per project with a README, a tasks table, and a decisions table — the skill has everything it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;Two things I'd fix if I were building this again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent publishing.&lt;/strong&gt; The current script calls &lt;code&gt;POST /api/articles&lt;/code&gt; on every &lt;code&gt;--confirm&lt;/code&gt; run, which means every iteration creates a &lt;em&gt;new&lt;/em&gt; Dev.to article. I generated four stale drafts before I realised this. The right design: maintain a local &lt;code&gt;.published.json&lt;/code&gt; state file mapping &lt;code&gt;repo_name → article_id&lt;/code&gt;, and on subsequent runs hit &lt;code&gt;PUT /api/articles/{id}&lt;/code&gt; to update the existing article in place. Dev.to supports it; I just didn't wire it up. Next iteration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview the final LinkedIn text.&lt;/strong&gt; My preview mode showed the LinkedIn post &lt;em&gt;before&lt;/em&gt; the Dev.to URL was known, so the preview was missing the "Full write-up on Dev.to:" line even though the published version had it. That's a confusing UX — the preview didn't match the output. I patched it to use a placeholder URL in the preview and rebuild the post with the real URL at publish time, but the cleaner fix is to publish to Dev.to first (always), then show the final LinkedIn text, then prompt for confirmation.&lt;/p&gt;

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

&lt;p&gt;I am trying to go from "person with a decent GitHub" to "person recruiters find". The difference isn't the code — it's whether anyone sees it. A project that ships with an accompanying write-up every time will, over a year, build far more public signal than a project that ships in silence.&lt;/p&gt;

&lt;p&gt;This skill is not glamorous. It's a markdown table parser, two HTTP clients, and one generator function. It cost me an afternoon plus three hours of debugging the old LinkedIn API. But it means my next project ships with a real article and a real LinkedIn post, and the one after that, and the one after that — without me having to remember to write them.&lt;/p&gt;

&lt;p&gt;Full LOS repo: &lt;a href="https://github.com/GadOfir/LOS-starter" rel="noopener noreferrer"&gt;https://github.com/GadOfir/LOS-starter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More on YouTube: &lt;a href="https://www.youtube.com/@GadOfir" rel="noopener noreferrer"&gt;https://www.youtube.com/@GadOfir&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>claudecode</category>
      <category>linkedin</category>
      <category>devtools</category>
    </item>
    <item>
      <title>LOS-starter: The Markdown Memory System That Makes Claude Code Remember</title>
      <dc:creator>Gad Ofir</dc:creator>
      <pubDate>Wed, 15 Apr 2026 17:38:17 +0000</pubDate>
      <link>https://forem.com/gad_ofir_076c468dd15d483b/los-starter-the-markdown-memory-system-that-makes-claude-code-remember-1k7</link>
      <guid>https://forem.com/gad_ofir_076c468dd15d483b/los-starter-the-markdown-memory-system-that-makes-claude-code-remember-1k7</guid>
      <description>&lt;h1&gt;
  
  
  LOS-starter: The Markdown Memory System That Makes Claude Code Remember
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;Public cold-start template for the Life Operating System. Users clone it, run onboarding, and get a working LOS instance with task-system, skills, and memory. Also the upstream source for &lt;code&gt;/update-los&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it exists:&lt;/strong&gt; Gives people a single place to understand LOS and bootstrap their own instance. Managed files are pulled from this repo during updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/GadOfir/LOS-starter" rel="noopener noreferrer"&gt;https://github.com/GadOfir/LOS-starter&lt;/a&gt;  &lt;strong&gt;Version:&lt;/strong&gt; v0.3.0  &lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Static HTML landing page (GitHub Pages) + Claude Code skills + markdown-based system.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Task system (BMAD-inspired plan phase, loop mode, cross-repo support)&lt;/li&gt;
&lt;li&gt;Self-assembling routing (skills declare their own triggers via SKILL.md)&lt;/li&gt;
&lt;li&gt;Update mechanism (&lt;code&gt;/update-los&lt;/code&gt; — migration + regular updates)&lt;/li&gt;
&lt;li&gt;Identity in &lt;code&gt;memory/identity.md&lt;/code&gt; (survives updates)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LOS:managed&lt;/code&gt; markers on all updatable files&lt;/p&gt;
&lt;h2&gt;
  
  
  The skills that make it work
&lt;/h2&gt;

&lt;p&gt;Every workflow in LOS is a Claude Code skill. Here's what's actually in &lt;code&gt;.claude/skills/&lt;/code&gt;:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/learn&lt;/code&gt;&lt;/strong&gt; — Captures and structures knowledge into memory/knowledge/. Handles two modes: topic learning and source ingestion&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/build-project&lt;/code&gt;&lt;/strong&gt; — Creates a new project in memory/projects/ with standard structure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/task-system&lt;/code&gt;&lt;/strong&gt; — Manages a single-task-at-a-time project workflow inside the repo&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/evolve&lt;/code&gt;&lt;/strong&gt; — Runs a full LOS system review, health check, and improvement suggestions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/create-skill&lt;/code&gt;&lt;/strong&gt; — Creates new Claude Code skills for LOS following Skills 2.0 conventions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/design-html&lt;/code&gt;&lt;/strong&gt; — Creates beautiful single-file HTML pages with modern CSS. No frameworks, no build tools&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills self-register by dropping a &lt;code&gt;SKILL.md&lt;/code&gt; file into &lt;code&gt;.claude/skills/{name}/&lt;/code&gt;. No central registry. No hardcoded lists. The routing layer reads the frontmatter and dispatches automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory is the whole point
&lt;/h2&gt;

&lt;p&gt;I didn't want another Notion database. I wanted Claude to remember what I'm working on — across conversations, across compactions, across weeks.&lt;/p&gt;

&lt;p&gt;LOS does that by storing everything in plain markdown files under &lt;code&gt;memory/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memory/
├── identity.md         # who I am, current state, active task
├── projects/           # one folder per active project
│   └── my-project/
│       ├── README.md
│       ├── tasks.md
│       └── decisions.md
└── knowledge/          # things I've learned, organised by topic
    ├── docker.md
    └── react.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At session start, Claude reads &lt;code&gt;memory/identity.md&lt;/code&gt; and knows: who I am, what projects are active, and what I was working on last. No onboarding, no re-explaining context.&lt;/p&gt;

&lt;p&gt;Updates to LOS replace managed files cleanly — your memory never gets touched because &lt;code&gt;identity.md&lt;/code&gt; is outside the managed scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  One skill deep-dive: /task-system
&lt;/h2&gt;

&lt;p&gt;Of the six skills, &lt;code&gt;/task-system&lt;/code&gt; is the one with the most structure. It runs a BMAD-inspired lifecycle on a single task at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plan → build → verify → fix → learn → close
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan phase wears three thinking hats in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Analyst&lt;/strong&gt; — what are we really solving, what's the domain context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architect&lt;/strong&gt; — what's the technical approach, what could break&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PM&lt;/strong&gt; — break it into 8-10 concrete steps, each one atomic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three hats write into &lt;strong&gt;one file&lt;/strong&gt; — &lt;code&gt;.tasks/active/task-NNN.md&lt;/code&gt;. Analysis, architecture notes, plan, work log, verify results, fix attempts, learnings — one file is the truth. No scattered briefs, no Google Docs, no side files.&lt;/p&gt;

&lt;p&gt;Two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gated&lt;/strong&gt; — human approves each phase. Default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop&lt;/strong&gt; — Claude auto-advances through phases until STUCK, BLOCKED, or PASS. Use this when you're away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Status vocabulary is deliberately small: &lt;code&gt;IN_PROGRESS&lt;/code&gt;, &lt;code&gt;FAIL&lt;/code&gt;, &lt;code&gt;STUCK&lt;/code&gt;, &lt;code&gt;BLOCKED&lt;/code&gt;, &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;ABANDONED&lt;/code&gt;. STUCK is the only full-stop — two failed fix attempts and the loop exits, waiting for a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real tasks that built this project
&lt;/h2&gt;

&lt;p&gt;This isn't theoretical. Here are the actual tasks from &lt;code&gt;memory/projects/LOS-starter/tasks.md&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Adopt BMAD patterns (task-001)&lt;/td&gt;
&lt;td&gt;Onboarding gate, state tracking, enriched skills&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-repo sync (task-002)&lt;/td&gt;
&lt;td&gt;Both repos need identical task-system structure&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autonomy + handoff guards (task-003)&lt;/td&gt;
&lt;td&gt;Loop mode pausing, cross-repo build in wrong repo&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update mechanism (task-004)&lt;/td&gt;
&lt;td&gt;No safe way to update old LOS instances&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-assembling routing (task-005)&lt;/td&gt;
&lt;td&gt;Updates wiped custom skill entries from CLAUDE.md&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these was a single task file, planned with three hats, built, verified, and closed. The fact that they're all &lt;code&gt;Done&lt;/code&gt; is the task system working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decisions I had to make
&lt;/h2&gt;

&lt;p&gt;Every decision is logged in &lt;code&gt;decisions.md&lt;/code&gt; with a reason. This file answers the question &lt;em&gt;"why is it done this way?"&lt;/em&gt; without anyone having to read the code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Host on GitHub Pages as static HTML&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;2026-03-28&lt;/em&gt; — Zero-cost, zero-ops, fast to iterate&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single landing page (no framework)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;2026-03-28&lt;/em&gt; — Keeps it simple, matches LOS philosophy&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity in memory/identity.md, not CLAUDE.md&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;2026-03-29&lt;/em&gt; — Makes CLAUDE.md 100% replaceable on update&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LOS:managed markers for update boundary&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;2026-03-29&lt;/em&gt; — Any file with marker gets replaced, everything else untouched&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-assembling routing from skill frontmatter&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;2026-03-29&lt;/em&gt; — No hardcoded skill lists — custom skills auto-route&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No CLI for updates — Claude is the update tool&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;2026-03-29&lt;/em&gt; — /update-los skill fetches from GitHub, simpler than npm tooling&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;p&gt;Most dev tooling fights Claude Code. LOS works with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory is the foundation.&lt;/strong&gt; &lt;code&gt;memory/identity.md&lt;/code&gt; is the source of truth for who I am and what's active. Claude reads it at session start — no re-onboarding, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity survives updates.&lt;/strong&gt; &lt;code&gt;/update-los&lt;/code&gt; can replace CLAUDE.md, routing, and skills cleanly because your identity lives outside the managed scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills are self-assembling.&lt;/strong&gt; Drop a new skill into &lt;code&gt;.claude/skills/my-thing/SKILL.md&lt;/code&gt;, and the routing layer finds it via frontmatter. No config to edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LOS:managed&lt;/code&gt; markers&lt;/strong&gt; — every updatable file is tagged so the update mechanism knows exactly what it's allowed to touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One file per task.&lt;/strong&gt; The &lt;code&gt;/task-system&lt;/code&gt; skill enforces a single active task file. No scatter, no lost context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/GadOfir/LOS-starter.git
&lt;span class="nb"&gt;cd &lt;/span&gt;LOS-starter
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then say &lt;code&gt;"start session"&lt;/code&gt; and let it bootstrap your first task.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/GadOfir/LOS-starter" rel="noopener noreferrer"&gt;https://github.com/GadOfir/LOS-starter&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Built with:&lt;/strong&gt; Claude Code + markdown + zero frameworks&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>tooling</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
