<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Productivity - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Productivity - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 30 Jul 2026 03:40:16 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/productivity/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Make Your Antigravity Agent Skills Configurable (Without Forking Them) ]]>
                </title>
                <description>
                    <![CDATA[ Antigravity Agent Skills are a great way to teach your AI agent a workflow once and reuse it everywhere. You write a short SKILL.md file, drop it in a folder, and the agent picks it up whenever it's r ]]>
                </description>
                <link>https://www.freecodecamp.org/news/make-your-antigravity-agent-skills-configurable-without-forking-them/</link>
                <guid isPermaLink="false">6a69c58763daca7bbbf2320b</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Developer Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Google Antigravity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Google ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Obum ]]>
                </dc:creator>
                <pubDate>Wed, 29 Jul 2026 09:19:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/7edaf407-ce56-4eff-8b1c-5e1c31e71067.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Antigravity Agent Skills are a great way to teach your AI agent a workflow once and reuse it everywhere. You write a short <code>SKILL.md</code> file, drop it in a folder, and the agent picks it up whenever it's relevant.</p>
<p>But these skills have a hidden limitation: they're static. If you download a skill someone else wrote and you want it to behave a little differently, you'll have to copy the whole thing and edit it by hand. And as you may have noticed lately, there are many "skills" forks floating around that are difficult to maintain.</p>
<p>In this tutorial, I'll show you I built a small convention that fixes this. It lets any Agent Skill read a per-project config file, so you can adopt any skill and customize how it behaves by editing a few lines of YAML (without ever touching the skill itself).</p>
<p>You'll build it step by step, test it, and see how to share it so other people can plug into it.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-are-antigravity-agent-skills">What Are Antigravity Agent Skills</a>?</p>
</li>
<li><p><a href="#heading-why-static-skills-are-a-problem">Why Static Skills Are a Problem</a></p>
</li>
<li><p><a href="#heading-the-configurable-skills-solution">The Configurable Skills Solution</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-config-loader">How to Build the Config Loader</a></p>
</li>
<li><p><a href="#heading-how-to-make-a-skill-configurable">How to Make a Skill Configurable</a></p>
</li>
<li><p><a href="#heading-how-to-add-project-overrides">How to Add Project Overrides</a></p>
</li>
<li><p><a href="#heading-how-to-test-your-configurable-skill">How to Test Your Configurable Skill</a></p>
</li>
<li><p><a href="#heading-two-more-example-skills">Two More Example Skills</a></p>
</li>
<li><p><a href="#heading-how-to-share-your-agent-skills-with-others">How to Share Your Agent Skills With Others</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>You will build a tiny, reusable layer called <strong>Configurable Agent Skills</strong>. It has three parts:</p>
<ol>
<li><p>A small Python script, <code>resolve_config.py</code>, that merges a skill's default settings with your project settings and prints the result.</p>
</li>
<li><p>A convention: each skill ships 2 files, a <code>config.default.yaml</code> file with its "knobs" and a <code>SKILL.md</code> file. They both guide the agent's behavior.</p>
</li>
<li><p>A per-project file, <code>.agent/skills.config.yaml</code>, where anyone using your skill sets their own values.</p>
</li>
</ol>
<p>By the end, you'll have a working <code>git-commit-formatter</code> skill that one team can run in Conventional Commits mode and another team can switch to gitmoji mode, all using the exact same skill files with no forking.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you'll need:</p>
<ul>
<li><p>Google Antigravity installed (the IDE, CLI, or SDK. Any of them work, since skills are just files.).</p>
</li>
<li><p>Python 3 installed, with PyYAML. You can install PyYAML with <code>python -m pip install pyyaml</code>.</p>
</li>
<li><p>Basic comfort with the terminal and YAML. You don't need to be an expert in either.</p>
</li>
</ul>
<p>If you've never written an Agent Skill before, the next two sections will bring you up to speed.</p>
<h2 id="heading-what-are-antigravity-agent-skills">What Are Antigravity Agent Skills?</h2>
<p>A Skill in Antigravity is a folder that contains a <code>SKILL.md</code> file and, optionally, some scripts, templates, or examples. The <code>SKILL.md</code> file has a short block of YAML "frontmatter" at the top (a <code>name</code> and a <code>description</code>), followed by a set of instructions written in plain Markdown.</p>
<p>Here's the important part: skills are loaded on demand. The agent reads only the short <code>description</code> of each skill at first. When your request matches that description, the agent pulls in the full instructions and follows them. This keeps the agent's context small and focused.</p>
<p>A minimal skill that enforces Conventional Commits looks like this:</p>
<pre><code class="language-markdown">---
name: git-commit-formatter
description: Formats git commit messages using the Conventional Commits specification. Use this when the user asks to commit changes or write a commit message.
---

# Git Commit Formatter

When writing a commit message, follow the Conventional Commits format:
`type(scope): description`

Allowed types: feat, fix, docs, style, refactor, perf, test, chore.
</code></pre>
<p>Drop that in your skills folder, ask the agent to "commit these changes," and it will write a properly formatted message. Simple and useful, right?</p>
<h2 id="heading-why-static-skills-are-a-problem">Why Static Skills Are a Problem</h2>
<p>Now look closely at that skill. The allowed types (<code>feat</code>, <code>fix</code>, <code>docs</code>, and so on) are baked directly into the instructions.</p>
<p>That's fine until someone wants something slightly different. Maybe your team also uses a <code>ci</code> type. Maybe you prefer gitmoji, where each commit starts with an emoji. Maybe you want to require a scope on every commit.</p>
<p>With a static skill, there's only one way to get any of that: copy the whole skill and edit the Markdown. When you do this across a team, everyone ends up with their own private fork. When the original author ships an improvement, none of the forks get it. The skill stops being something you <em>share</em> and becomes something everyone <em>rewrites</em>.</p>
<p>The core issue is that there's no clean line between the skill's logic (which everyone should share) and its settings (which each project wants to control). How do we solve this?</p>
<h2 id="heading-the-configurable-skills-solution">The Configurable Skills Solution</h2>
<p>The idea is simple. Instead of hard-coding settings in the instructions, the skill will:</p>
<ol>
<li><p>Ship its settings and their defaults in a separate <code>config.default.yaml</code> file.</p>
</li>
<li><p>Read a merged config (defaults plus any project-level overrides) before it acts.</p>
</li>
</ol>
<p>The project-level overrides live in a file called <code>.agent/skills.config.yaml</code>, which sits at the root of the user's project:</p>
<pre><code class="language-yaml"># .agent/skills.config.yaml 
# (edit this file in your project instead of the skill globally)
git-commit-formatter:
  style: gitmoji
  extra_types: [ci, build]
  scope_required: true
</code></pre>
<p>That's the easy flow. Drop the skill in, set a few keys, and you're done. The skill's own files never change.</p>
<p>To make this work, you need a script that reads both files, merges them, and hands the result to the agent. Let's build it.</p>
<h2 id="heading-how-to-build-the-config-loader">How to Build the Config Loader</h2>
<p>Create a file called <code>resolve_config.py</code>. Its job is to take a skill's name, load that skill's <code>config.default.yaml</code>, find the user's <code>.agent/skills.config.yaml</code>, and merge the two so that user values win.</p>
<p>Start with a deep-merge helper. This is the heart of the loader:</p>
<pre><code class="language-python">def deep_merge(base, override):
    """Recursively merge override onto base.

    Dicts merge key by key. Anything else (scalars, lists) is replaced
    wholesale by the override value.
    """
    if isinstance(base, dict) and isinstance(override, dict):
        merged = dict(base)
        for key, value in override.items():
            merged[key] = deep_merge(merged[key], value) if key in merged else value
        return merged
    return override
</code></pre>
<p>Notice the deliberate choice here: dictionaries merge key by key, but lists are replaced, not appended. That keeps the behavior predictable. If you want to handle "defaults plus extras", use the explicit <code>extra_types</code> key in the skill as you'll see in the example below.</p>
<p>Next, you need to find your "per-project" config. The loader walks up from the current directory looking for an <code>.agent/skills.config.yaml</code> file:</p>
<pre><code class="language-python">from pathlib import Path

def find_project_config(start: Path):
    """Walk upward from start looking for .agent/skills.config.yaml."""
    start = start.resolve()
    for folder in [start, *start.parents]:
        candidate = folder / ".agent" / "skills.config.yaml"
        if candidate.is_file():
            return candidate
    return None
</code></pre>
<p>Now put it together. The loader locates the skill's defaults (which sit next to the script), loads your overrides for that skill's name, merges them, and prints the result:</p>
<pre><code class="language-python">import sys, yaml
from pathlib import Path

def resolve(skill_name, skill_dir, project_root):
    defaults = yaml.safe_load((Path(skill_dir) / "config.default.yaml").read_text()) or {}

    user_path = find_project_config(Path(project_root))
    user_all = yaml.safe_load(user_path.read_text()) if user_path else {}
    user_cfg = (user_all or {}).get(skill_name, {}) or {}

    return deep_merge(defaults, user_cfg)
</code></pre>
<p>That completes the whole idea. The full version in the sample repo adds a command-line interface, JSON output, and clear error messages, but the logic above is all you really need.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f92a5e56aa1ed54804bb866/ca8d7095-21f6-4433-8c82-b6b0ca34b9ef.png" alt="Terminal output showing the resolved configuration for the git-commit-formatter skill." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-how-to-make-a-skill-configurable">How to Make a Skill Configurable</h2>
<p>Now you'll convert the static commit skill into a configurable one. This takes two files.</p>
<p>First, create <code>config.default.yaml</code> next to the skill. It lists every setting and a safe default, so the skill works even when the user has no config at all:</p>
<pre><code class="language-yaml"># Default configuration for the git-commit-formatter skill.
style: conventional          # conventional | gitmoji
types:                       # base set of allowed commit types
  - feat
  - fix
  - docs
  - style
  - refactor
  - perf
  - test
  - chore
extra_types: []              # additional types, merged on top of `types`
scope_required: false        # if true, require a scope: type(scope): ...
max_subject_length: 72       # hard cap on the subject line
</code></pre>
<p>Second, update <code>SKILL.md</code> so that its very first instruction is to resolve the config and apply it. This is the key move: you're telling the agent to read the settings before it does anything else:</p>
<pre><code class="language-markdown">---
name: git-commit-formatter
description: Formats git commit messages to a team's chosen convention (Conventional Commits or gitmoji). Use this when the user asks to commit changes or write a commit message. Reads per-project settings so teams customize commit style without editing this skill.
---

# Git Commit Formatter (Configurable)

## Step 1 - Resolve configuration (always do this first)

Run the loader and read its output:

`python scripts/resolve_config.py git-commit-formatter --project-root .`

Apply exactly those settings:

- `style`: `conventional` or `gitmoji`.
- `types` + `extra_types`: the full set of allowed commit types.
- `scope_required`: if true, a scope is mandatory.
- `max_subject_length`: hard cap on the subject line.

## Step 2 - Compose the message

Pick the primary type from `types` + `extra_types`, build the subject in the
chosen `style`, and enforce `scope_required` and `max_subject_length`.
</code></pre>
<p>This pattern ("make the agent run a script and obey its output") is the same one Antigravity's own validation skills use. It keeps the behavior deterministic instead of leaving it to the model's memory.</p>
<p>Notice how <code>extra_types</code> solves the additive-list question. The default list stays put, and the user's extras are simply added on top by the skill. No fork is required to add a <code>ci</code> type.</p>
<h2 id="heading-how-to-add-project-overrides">How to Add Project Overrides</h2>
<p>Let's say you want gitmoji commits with two extra types. Create a single file in your project:</p>
<pre><code class="language-yaml"># .agent/skills.config.yaml
git-commit-formatter:
  style: gitmoji
  extra_types: [ci, build]
  scope_required: true
</code></pre>
<p>You just changed three lines of config and didn't open the skill or fork any code. The next time the agent commits, it will use this project settings.</p>
<p>And a different project, with no config file at all, keeps getting the sensible Conventional Commits defaults. You have one skill with many behaviors.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f92a5e56aa1ed54804bb866/c9cb7c61-4a6f-4963-94ed-321bb20bde30.png" alt="The agent proposing a commit message that starts with an emoji, driven by the project config.&quot;" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-how-to-test-your-configurable-skill">How to Test Your Configurable Skill</h2>
<p>You don't need the agent to check that the merge works. Run the loader directly and read the output.</p>
<p>With no overrides, you get the defaults:</p>
<pre><code class="language-bash">$ python scripts/resolve_config.py git-commit-formatter --project-root .
style: conventional
scope_required: false
...
</code></pre>
<p>Now add the <code>.agent/skills.config.yaml</code> override from the last section and run it again:</p>
<pre><code class="language-bash">$ python scripts/resolve_config.py git-commit-formatter --project-root . --print-sources
style: gitmoji
scope_required: true
extra_types:
- ci
- build
types:
- feat
- fix
- docs
...
</code></pre>
<p>The <code>style</code> flipped to <code>gitmoji</code>, <code>scope_required</code> became <code>true</code>, and your extra types appeared (while the base <code>types</code> list stayed intact). That confirms the merge does exactly what you want.</p>
<p>It's worth writing a small automated test too, so a future change to the loader can't silently break the merge. A test can create a fake skill and a fake project config in a temp folder, run the loader, and assert that user values override defaults while untouched defaults survive.</p>
<h2 id="heading-two-more-example-skills">Two More Example Skills</h2>
<p>The same pattern works for any skill. Here are two more to show the range.</p>
<h3 id="heading-a-changelog-generator">A Changelog Generator</h3>
<p>Its <code>config.default.yaml</code> exposes the output <code>format</code> (like Keep a Changelog), which commit <code>types</code> to include, and whether to link commit hashes to a repo URL. One project can generate a formal changelog grouped by type, while another can generate a simple bulleted list. It's the same skill with a different config.</p>
<pre><code class="language-yaml"># changelog-generator config.default.yaml (excerpt)
format: keepachangelog       # keepachangelog | conventional | simple
include_types: [feat, fix, perf]
include_authors: false
repo_url: ""                 # if set, hashes link to commits
</code></pre>
<h3 id="heading-a-license-header-adder">A License-Header Adder</h3>
<p>Its config exposes the <code>license</code> (Apache-2.0, MIT, or custom), the <code>holder</code>, and a map of file extensions to comment styles. A company sets the holder once in their project config, and every new file gets the right header in the right comment style, without editing the skill.</p>
<pre><code class="language-yaml"># license-header-adder config.default.yaml (excerpt)
license: apache-2.0          # apache-2.0 | mit | custom
holder: "Your Name or Org"
year: auto                   # auto = current year
</code></pre>
<p>The lesson is that almost any skill has a few decisions baked into it. When you pull those decisions into a <code>config.default.yaml</code>, you convert a one-off skill into a tool that anyone can reuse and tune.</p>
<h2 id="heading-how-to-share-your-agent-skills-with-others">How to Share Your Agent Skills With Others</h2>
<p>Once your agent skills follow the convention, they compose into something bigger. To make your agent skills easy for others to adopt, you have to:</p>
<ul>
<li><p><strong>Keep each skill self-contained:</strong> Vendor a copy of <code>resolve_config.py</code> inside each skill's <code>scripts/</code> folder, so someone can copy a single skill folder anywhere and it just works.</p>
</li>
<li><p><strong>Document every config key</strong> in the <code>SKILL.md</code>, so users know exactly what they can tune.</p>
</li>
<li><p><strong>Publish a small index:</strong> A simple <code>index.json</code> that lists each skill's name, path, and config keys makes it easy for others to discover what you've built and contribute their own.</p>
</li>
</ul>
<p>Because the convention is just "read a config file first," anyone can publish a compatible skill. Each new configurable skill makes the whole ecosystem more useful. In addition to shipping a skill, you're shipping a small standard that other people can build on.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>You started with a static skill whose behavior was frozen in Markdown, and you turned it into a configurable one that anyone can tune from a single project file.</p>
<p>The entire setup is relatively small. It has the merge function, one convention, and a <code>config.default.yaml</code> per skill.</p>
<p>It also changes how skills are shared. Instead of forking a skill to change one setting, you can keep the shared logic and adjust your own config. Improvements to the skill flow to everyone, and everyone still gets the behavior they want.</p>
<p>If you want to try it, build the <code>git-commit-formatter</code> skill from this tutorial, drop it into your Antigravity skills folder, and add an <code>.agent/skills.config.yaml</code> to a project. Then flip <code>style</code> from <code>conventional</code> to <code>gitmoji</code> and watch the same skill behave differently.</p>
<p>From there, make one of your own skills configurable. Find the settings you baked into the instructions, move them into a <code>config.default.yaml</code>, and let your users take it from there.</p>
<p>The full sample code (the loader, its tests, and all three example skills) is on GitHub at <a href="https://github.com/keepdeploying/configurable-agent-skills">github.com/keepdeploying/configurable-agent-skills</a>.</p>
<p>Thanks for reading. If you build a configurable skill of your own, share it. Let's keep the ecosystem growing.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Top AI Productivity Tools for Developers and Professionals ]]>
                </title>
                <description>
                    <![CDATA[ Artificial intelligence has moved far beyond simple chatbots and basic automation. Today, developers and non-developers alike use AI tools to research information, write content, create presentations, ]]>
                </description>
                <link>https://www.freecodecamp.org/news/top-ai-productivity-tools-for-developers-and-professionals/</link>
                <guid isPermaLink="false">6a47d78cf77fda0eb25967c4</guid>
                
                    <category>
                        <![CDATA[ #ai-tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 03 Jul 2026 15:38:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a15e540a-4f26-4fa5-a15e-5f8f4e2fd0c3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Artificial intelligence has moved far beyond simple chatbots and basic automation. Today, developers and non-developers alike use AI tools to research information, write content, create presentations, manage projects, and analyse data faster than ever before.</p>
<p>The biggest advantage of AI is not that it replaces people. Instead, it helps people spend less time on repetitive work and more time on tasks that require creativity, strategy, and decision-making.</p>
<p>Whether you're a marketer, consultant, business owner, educator, or knowledge worker, the right AI tools can help you improve productivity and reduce the time spent on routine activities.</p>
<p>In this article, we'll look at some of the most useful AI productivity tools and explore how they can streamline everyday work.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-notion-ai-for-knowledge-management">Notion AI for Knowledge Management</a></p>
</li>
<li><p><a href="#heading-glean-best-enterprise-ai-platform">Glean - Best Enterprise AI Platform</a></p>
</li>
<li><p><a href="#heading-quillbot-for-faster-slide-creation">QuillBot for Faster Slide Creation</a></p>
</li>
<li><p><a href="#heading-transkriptor-for-meeting-documentation">Transkriptor for Meeting Documentation</a></p>
</li>
<li><p><a href="#heading-cardscanner-for-text-digitisation-and-extraction">Cardscanner for Text Digitisation and Extraction</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-notion-ai-for-knowledge-management">Notion AI for Knowledge Management</h2>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/2e8d2b08-be92-4b00-90c5-16e96063b055.webp" alt="Notion" style="display:block;margin:0 auto" width="1920" height="1199" loading="lazy">

<p>Information overload is a common challenge in modern workplaces. Teams create documents, meeting notes, project plans, and reports every day. Finding the right information later can become difficult.</p>
<p><a href="https://www.notion.com/">Notion AI</a> helps you organise and manage knowledge more effectively. It can summarise notes, generate content, answer questions about stored information, and assist with document creation.</p>
<p>Imagine attending several meetings in a week. Instead of manually reviewing pages of notes, you can use Notion AI to create summaries and extract key action items.</p>
<p>The tool also helps teams maintain centralised documentation. Employees can quickly search for information and receive concise answers rather than reading lengthy documents.</p>
<p>This capability improves collaboration and reduces the time spent looking for information across different systems.</p>
<p>For teams that prefer self-hosted solutions, <a href="https://github.com/appflowy-io/appflowy">AppFlowy AI</a> offers an open-source workspace with AI capabilities similar to Notion. <a href="https://github.com/outline/outline">Outline</a> is another open-source knowledge base that can be paired with local LLMs such as Ollama to provide AI-powered search, summaries, and document assistance.</p>
<h2 id="heading-glean-best-enterprise-ai-platform">Glean – Best Enterprise AI Platform</h2>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/7ae85a5e-b93f-4694-9fff-4cd307425271.png" alt="Glean" style="display:block;margin:0 auto" width="1458" height="641" loading="lazy">

<p><a href="https://www.glean.com/">Glean</a> is not just another AI tool. It's the unified AI layer for enterprise knowledge, a platform that sits across your entire business and makes company knowledge instantly accessible, contextual, and secure. No other tool on this list operates at this level of breadth and depth for large organisations.</p>
<p>Rather than being a standalone chatbot or a note-taking app with AI, Glean functions as a work AI layer across your business. It connects to over 100 enterprise applications, including Google Workspace, Microsoft 365, Salesforce, Jira, Confluence, Slack, ServiceNow, and more, building a deep, permission-aware understanding of your company's content, structure, and people.</p>
<p>The key differentiator is how Glean handles permissions. It doesn't flatten your organisation's access controls. It reinforces them. A salesperson only sees what they're authorised to see. An engineer gets code-repo context. A support rep surfaces relevant customer history automatically. The AI inherits and respects the same permissions already configured in your source systems.</p>
<p>Organisations looking for an open-source enterprise search platform can explore <a href="https://github.com/open-webui/open-webui">Open WebUI</a> connected to local LLMs and document stores, or <a href="https://github.com/mintplex-labs/anything-llm">AnythingLLM</a>, which allows teams to build private AI knowledge assistants over internal documents while retaining full control of their data.</p>
<h2 id="heading-quillbot-for-faster-slide-creation">QuillBot for Faster Slide Creation</h2>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/a3bf2750-2342-40fb-b81c-38aec8f14d58.jpg" alt="Quillbot" style="display:block;margin:0 auto" width="1946" height="1096" loading="lazy">

<p>Creating presentations can be a very time-consuming task. Whether you're preparing a client proposal, a sales pitch, a business review, or a training session, building slides from scratch can take hours.</p>
<p>This is where <a href="https://quillbot.com/ai-presentation-maker">QuillBot's Presentation Maker</a> can help.</p>
<p>The process is designed to be simple. You begin by entering a presentation idea or describing your requirements in a sentence or two. The AI then generates presentation content and creates slides automatically. After the initial draft is ready, you can edit and customise slide text, titles, and slide order to match your specific needs.</p>
<p>Instead of spending valuable time creating every slide manually, you can start with a complete presentation draft and focus on refining the content.</p>
<p>This can be useful when preparing client-ready decks, business proposals, meeting summaries, or executive presentations. Marketing teams can create client pitches, campaign strategies, and proposal documents more efficiently. Educators can quickly build lesson slides and visual teaching materials without spending hours on presentation design.</p>
<p>As workplace communication increasingly relies on presentations, these presentation tools can help you create polished materials in a fraction of the time.</p>
<p>There are few mature open-source AI presentation generators, but <a href="https://sid.libreoffice.org/discover/impress/">LibreOffice Impress</a> can be combined with local AI assistants like Open WebUI or AnythingLLM to generate slide content before exporting it into presentations. This provides a privacy-focused alternative without relying on proprietary AI services.</p>
<h2 id="heading-transkriptor-for-meeting-documentation">Transkriptor for Meeting Documentation</h2>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/005ae66f-6fbf-4dcc-a7fe-4d4ca96be8b9.png" alt="Transkriptor" style="display:block;margin:0 auto" width="2724" height="1516" loading="lazy">

<p>Meetings play a critical role in collaboration, but capturing every detail can take attention away from meaningful discussions. Manual note-taking often makes it difficult for participants to stay fully engaged during conversations.</p>
<p><a href="https://transkriptor.com/">Transkriptor</a> uses AI-powered transcription to automatically convert meeting conversations into accurate, searchable text. Teams can stay focused on the discussion while the platform records and organises meeting content in real time.</p>
<p>Once the meeting is complete, you can review transcripts, extract key insights, identify action points, and share summaries with your colleagues for better alignment.</p>
<p>This functionality is particularly valuable for remote and hybrid teams that rely on frequent virtual meetings and need reliable documentation of decisions and discussions. Instead of spending time creating manual notes, your team can maintain structured records with minimal effort.</p>
<p>Users who want full control over meeting recordings can use <a href="https://github.com/openai/whisper">Whisper</a> or <a href="https://github.com/m-bain/whisperx">WhisperX</a> to generate accurate transcripts on their own hardware. These projects support multiple languages and can be integrated into custom workflows without sending audio to third-party services.</p>
<p>By automating meeting documentation, you can dedicate more time to collaboration and less time to administrative tasks.</p>
<h2 id="heading-cardscanner-for-text-digitisation-and-extraction">Cardscanner for Text Digitisation and Extraction</h2>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/f7104805-8c08-41a5-85ee-4649ce66ce13.png" alt="Cardscanner" style="display:block;margin:0 auto" width="1686" height="871" loading="lazy">

<p>Not every engineering task is about formulas, graphs, or code. Researchers also spend a lot of time working with handwritten notes, scanned pages, diagrams, embedded text, and printed reference material.</p>
<p>Cardscanner helps make that material easier to use by turning static image text into editable digital content. With its <a href="https://www.cardscanner.co/image-to-text">image to text converter</a>, you can quickly extract text from screenshots, lecture notes, research pages, or printed handouts without retyping everything manually.</p>
<p>This is especially useful when organising research material, converting notes into digital files, or collecting information from charts and documents for reports. Instead of spending extra time copying content by hand, you can focus more on understanding and applying the information.</p>
<p><a href="https://github.com/tesseract-ocr/tesseract">Tesseract OCR</a> remains one of the most widely used open-source OCR engines for extracting text from images and scanned documents. For higher accuracy on complex layouts, <a href="https://github.com/PADDLEPADDLE/PADDLEOCR">PaddleOCR</a> provides modern deep-learning-based text recognition with support for multiple languages.</p>
<p>For researchers, this kind of tool saves time and reduces unnecessary complexity in day-to-day academic work. This is especially convenient when they have to manage large amounts of reading and reference material for extracting text.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>AI productivity tools are transforming the way we work by reducing repetitive tasks, improving access to information, and accelerating content creation.</p>
<p>Whether you need an AI-powered knowledge base like Notion AI, enterprise search through Glean, automated presentation creation with QuillBot, meeting transcription using Transkriptor, or text extraction with Cardscanner, each tool addresses a different part of the modern workflow.</p>
<p>At the same time, open-source alternatives continue to mature. Solutions such as AppFlowy AI, AnythingLLM, Open WebUI, Whisper, Tesseract OCR, and PaddleOCR give you and your team the flexibility to self-host your AI stack, maintain greater control over your data, and reduce reliance on proprietary platforms.</p>
<p>The best productivity strategy is rarely built around a single application. Instead, you should combine the tools that best fit your workflow, balancing ease of use, privacy, cost, and integration requirements.</p>
<p>As AI capabilities continue to evolve, those who embrace the right mix of commercial and open-source solutions will be better positioned to work more efficiently, collaborate more effectively, and focus on the high-value work that matters most.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Unblock Your AI PR Review Bottleneck: A Tech Lead’s Guide to Building a Codebase-Aware Reviewer ]]>
                </title>
                <description>
                    <![CDATA[ A few months ago, I was reviewing a pull request that added three new API endpoints. The diff was clean. Tests passed. The agent that generated it had even written sensible authorisation checks. By ev ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-unblock-ai-pr-review-bottleneck-handbook/</link>
                <guid isPermaLink="false">69f906a346610fd60629a300</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ code review ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ leadership ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Qudrat Ullah ]]>
                </dc:creator>
                <pubDate>Mon, 04 May 2026 20:50:43 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c94dff21-66d0-4256-bf3e-25c1978364d9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A few months ago, I was reviewing a pull request that added three new API endpoints. The diff was clean. Tests passed. The agent that generated it had even written sensible authorisation checks. By every signal I usually rely on, it was ready to merge.</p>
<p>The problem only showed up when I checked which authentication middleware the agent had imported.</p>
<p>Our codebase had two: a v1 middleware backed by MongoDB and a v2 middleware backed by MySQL, which we had spent the previous quarter migrating.</p>
<p>New endpoints were supposed to use v2. The agent had used v1 for all three. Tests passed because user records still existed in both databases (that was the point of the migration), and the v1 middleware happily authenticated them. The code worked. But every new endpoint we shipped was reinforcing the legacy auth path we had just spent a quarter trying to retire.</p>
<p>I caught it on the second read. Twenty minutes after the comments, the engineer fixed it and reopened the PR. The third reviewer probably wouldn't have caught it. The migration timeline lived in a Slack thread from six months earlier. The rule that "new endpoints use v2" lived in my head.</p>
<p>This kind of catch is the slow-burn version of why AI changed my job as a tech lead. Code generation got faster. My review queue got longer. The hardest reviews were the ones where everything looked right, and the only thing wrong was something that lived in the team's collective memory rather than in the diff.</p>
<p>This handbook is about what we did to fix that. It's the story of how we went from drowning in clean-looking PRs to running a custom AI PR reviewer that catches a meaningful share of these mistakes before any human is pulled in. The fix turned out to be less about buying a better tool and more about moving the team's memory into a place the AI could actually read.</p>
<p>The lessons should transfer whether your team uses Claude Code, Cursor, Cline, GitHub Copilot, or any combination. The structure matters more than the tool.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-old-bottleneck-and-the-one-ai-created">The Old Bottleneck, and the One AI Created</a></p>
</li>
<li><p><a href="#heading-what-the-new-review-work-actually-looks-like">What the New Review Work Actually Looks Like</a></p>
</li>
<li><p><a href="#heading-why-i-did-not-just-buy-a-tool">Why I Did Not Just Buy a Tool</a></p>
</li>
<li><p><a href="#heading-the-realisation-move-the-rules-into-the-codebase">The Realisation: Move the Rules Into the Codebase</a></p>
</li>
<li><p><a href="#heading-two-files-that-changed-everything-agentsmd-and-claudemd">Two Files That Changed Everything: AGENTS.md and CLAUDE.md</a></p>
</li>
<li><p><a href="#heading-where-per-service-memory-files-earn-their-keep">Where Per-Service Memory Files Earn Their Keep</a></p>
</li>
<li><p><a href="#heading-what-this-looks-like-on-disk">What This Looks Like on Disk</a></p>
</li>
<li><p><a href="#heading-generated-documentation-as-a-side-effect">Generated Documentation as a Side Effect</a></p>
</li>
<li><p><a href="#heading-building-the-pr-review-command">Building the PR Review Command</a></p>
</li>
<li><p><a href="#heading-guardrails-read-only-by-default">Guardrails: Read-Only by Default</a></p>
</li>
<li><p><a href="#heading-the-compounding-loop-that-made-the-real-difference">The Compounding Loop That Made the Real Difference</a></p>
</li>
<li><p><a href="#heading-starting-from-zero-on-an-existing-project">Starting From Zero on an Existing Project</a></p>
</li>
<li><p><a href="#heading-what-still-needs-human-review">What Still Needs Human Review</a></p>
</li>
<li><p><a href="#heading-a-two-week-setup-plan">A Two-Week Setup Plan</a></p>
</li>
<li><p><a href="#heading-what-is-working-what-i-am-still-improving">What Is Working, What I Am Still Improving</a></p>
</li>
<li><p><a href="#heading-sources">Sources</a></p>
</li>
</ul>
<h2 id="heading-the-old-bottleneck-and-the-one-ai-created">The Old Bottleneck, and the One AI Created</h2>
<p>To understand why this fix was needed, it helps to remember what reviewing code looked like a couple of years ago.</p>
<p>Back then, the slow part was upstream of the PR. A ticket would land, and before anyone could open a branch, there was a long preamble of context-gathering.</p>
<p>Junior engineers needed time to understand what the change was for. Senior engineers had to explain business rules and architectural decisions. Tickets sat in "ready" columns for days while someone with the right context made themselves available. Then the writing itself took time, because typing real code is slower than typing comments about it.</p>
<p>That bottleneck mostly dissolved when the team got serious about AI-assisted development. Engineers used the agent to read the codebase, ask clarifying questions, draft an implementation plan, and produce a working branch in hours instead of days. Tickets moved through the queue faster. Junior engineers shipped more without blocking on senior availability. From the outside, this looked like an unambiguous win.</p>
<p>But the bottleneck didn't disappear. It moved.</p>
<p>Within a few weeks of widespread AI adoption, my review queue had doubled. Then tripled. Engineers were opening PRs faster than I could read them.</p>
<p>The PRs themselves looked clean: well-formatted, with sensible variable names, passing tests, and AI-generated descriptions that read better than most human-written ones.</p>
<p>On the surface, this was great. In practice, it was creating a different kind of pain. I was the senior engineer who knew which patterns mattered and which paths through the codebase were the right ones, and I was the bottleneck. The team's velocity was now capped by my reading speed.</p>
<p>The CircleCI 2026 State of Software Delivery report confirmed I was not alone. Drawing on more than 28 million CI workflow runs across over 22,000 organisations, the report showed feature branch throughput had grown 59% year over year, the largest jump CircleCI had ever measured. Main branch throughput, where code actually gets promoted to production, fell by 7% for the median team in the same period. Build success rates dropped to 70.8%, the lowest in five years.</p>
<p>The pattern was consistent across the industry. AI accelerated writing. The rest of the system absorbed the cost.</p>
<p>So the question for me, as a tech lead, became concrete: how do I unblock myself without lowering the bar?</p>
<h2 id="heading-what-the-new-review-work-actually-looks-like">What the New Review Work Actually Looks Like</h2>
<p>Before I explain the fix, it helps to know what kinds of issues were actually piling up. They weren't the dramatic kind. None of them would crash production. They were small, recurring, and looked plausible at a glance.</p>
<p>Take the simplest case I kept catching. An engineer would ask the agent to add a delete button on a new screen. The button needed to call our existing backend delete endpoint. Instead of reusing the hook the team already had for that endpoint, the agent would write the fetch call inline.</p>
<p>The code worked. The tests passed. But a week later, when someone changed the backend response shape, only one of the two call sites got updated.</p>
<p>That kind of duplication doesn't show up in a code review unless the reviewer happens to remember that a hook exists.</p>
<p>Another example I saw constantly: the agent comparing a status field against the literal string <code>"completed"</code> instead of using the <code>Status.Completed</code> enum that the rest of the services used. The code ran. The tests ran. The next refactor of the enum quietly skipped the file. After a few days, someone would spend half a day debugging a state machine that was working fine until the agent's literal silently fell out of sync.</p>
<p>These were two-minute fixes once spotted, but spotting them took me a reasonable time per PR. The friction wasn't the difficulty. It was the repetition.</p>
<p>The pattern repeated across larger problems, too.</p>
<p>I once asked an agent to build an event creation wizard. The wizard needed several dropdowns and one new component.</p>
<p>We have a design system folder where shared UI components live, and the rule on the team is simple: check there first, and if you build something new, register it there.</p>
<p>The agent had no way to know that. It only loaded the wizard's own files, so it never opened the design system folder. It generated brand new dropdowns inline, with APIs that were almost identical to the ones we already had. The new component went straight into the wizard rather than into the design system. CI passed. The wizard worked. We caught the duplication in human review, but it was the kind of catch that depended entirely on a reviewer who happened to know the design system existed.</p>
<p>The same pattern hit in one of the repos I was looking at for backend architecture. Backend follows a strict four-layer pattern: route, controller, app, repo. Controllers must never call repository functions directly. That rule keeps authorisation centralised, business logic testable, and database concerns isolated.</p>
<p>One PR I reviewed had the agent calling repo functions straight from a controller, skipping the app layer entirely. The code worked. The tests passed because the agent had also written tests against the new shape. But it broke a discipline the team had spent years building. If that PR had landed, the next AI-assisted PR could have used it as a template, and the layering would have eroded one diff at a time.</p>
<p>The common thread is that all of these mistakes had something written down somewhere, in code, in a Slack thread, in a senior engineer's head, that would have prevented them. The information existed. The agent just couldn't see it.</p>
<h2 id="heading-why-i-did-not-just-buy-a-tool">Why I Did Not Just Buy a Tool</h2>
<p>The obvious next move was to install one of the AI PR reviewers that flooded the market in 2026.</p>
<p>I evaluated several. Anthropic launched Claude Code Review in March 2026, billed on token usage and averaging \(15 to \)25 per review. CodeRabbit Pro charges \(24 per developer per month on annual billing, or \)30 per developer per month on monthly billing, with seats counted against developers who actually open PRs. Greptile in March 2026 moved to a base-plus-usage model at $30 per seat per month, including 50 reviews, after which each additional review costs a dollar. GitHub announced that all Copilot plans will transition to usage-based billing on June 1, 2026, with code reviews consuming both AI Credits and GitHub Actions minutes from that date.</p>
<p>For a small team with low PR volume, none of these is a dealbreaker. For a larger team running heavy AI-assisted development, the costs compound fast. A 10-person team running five PRs each per day blows through Greptile's included reviews in a single week. CodeRabbit Pro at \(24 per seat scales linearly with developers. The premium Claude Code Review at \)15 to $25 per PR is the most expensive option per review by an order of magnitude.</p>
<p>I looked at the cost numbers, but cost wasn't actually the deciding factor. The deciding factor was that none of these tools would have caught the problems I just listed.</p>
<p>A generic reviewer wouldn't have caught the v1/v2 middleware. It had no way to know v2 was the canonical path. A generic reviewer wouldn't have caught the duplicate dropdowns. It had no way to know our design system existed. A generic reviewer wouldn't have caught the bypassed architecture. It had no way to know that controllers must not call repositories.</p>
<p>The information that lets a reviewer flag any of these is exactly the information that lives in the team's head, not in any tool's default prompt.</p>
<p>The better-rated tools support custom rules, and that's where I started to see the real shape of the problem. Once you are configuring custom rules, you've already accepted that the value is in the rules. The tool is just whatever runs them.</p>
<p>This raised a different question: if the rules are the product, why pay per seat or per review for someone else's wrapper around them?</p>
<p>This is what made me change direction.</p>
<h2 id="heading-the-realisation-move-the-rules-into-the-codebase">The Realisation: Move the Rules Into the Codebase</h2>
<p>Once I started thinking of the rules as the product, the path forward got clearer.</p>
<p>I asked myself a simple question: what was I actually doing in code review that the AI was not? The answer turned out to be the same thing, over and over. I was typing review comments that captured a piece of the team's memory.</p>
<p>"Use the Status enum, not a string literal." "There is already a hook for this in <code>/hooks/useDeleteItem</code>." "Controllers must not import from the repo layer; route this through the app layer." "Check the design system folder before creating new components."</p>
<p>Each of those comments was knowledge that lived in my head and arrived in the codebase one PR comment at a time. None of it was available to the agent the next time it generated a similar PR.</p>
<p>So the fix was not to buy a smarter reviewer. The fix was to write the rules down in a place every agent on the team would read before any review happened.</p>
<p>If I had typed "use the enum, not a literal" three times in three different PRs, that was a rule the agent should know about from now on. If I had pointed at the design system folder for the fourth time, that was a rule. If I had explained the four-layer architecture twice in PR comments, that was a rule.</p>
<p>I needed somewhere to put these rules. That turned out to be a less obvious decision than I expected.</p>
<h2 id="heading-two-files-that-changed-everything-agentsmd-and-claudemd">Two Files That Changed Everything: AGENTS.md and CLAUDE.md</h2>
<p>If you start looking into how to give an AI agent a persistent project context, you run into two competing conventions almost immediately.</p>
<p>The first is <strong>AGENTS.md</strong>, an open standard that has gathered real momentum. According to InfoQ, by mid-2025, the format had already been adopted by more than 20,000 GitHub repositories and was being positioned as a complement to traditional documentation: machine-readable context that lives alongside human-facing files like README.md.</p>
<p>The standard's own site reports it is now used by more than 60,000 open-source projects and has moved to stewardship under the Agentic AI Foundation, which sits inside the Linux Foundation. The format is supported by OpenAI Codex, GitHub Copilot, Google Gemini, Cursor, and Windsurf, among others.</p>
<p>The second is <strong>CLAUDE.md</strong>, which is Anthropic's convention for Claude Code. The Claude Code documentation describes two complementary memory systems: CLAUDE.md, where you write the persistent context yourself, and an auto-memory mechanism that lets Claude save its own notes from corrections and observed patterns. By default, Claude Code reads CLAUDE.md, not AGENTS.md.</p>
<p>This split mattered for us because half the team uses Claude Code and the other half uses Cursor. We had two practical options: maintain both files with the same content (and accept the duplication), or symlink one filename to the other so both ecosystems read the same source of truth. We went with the symlink. It's one less thing to drift.</p>
<p>The next question was what to actually put in the file. After a few iterations, here's the shape that worked. Think of it as a briefing document for a new engineer who has read no code and seen no Slack threads. The minimum content was:</p>
<ul>
<li><p>The tech stack (languages, frameworks, package manager)</p>
</li>
<li><p>The project structure, especially important for our monorepo</p>
</li>
<li><p>Where shared utilities, components, and helpers live, and the rule that new code should reuse them before creating new versions</p>
</li>
<li><p>Architectural patterns the project follows, with file path examples</p>
</li>
<li><p>Anti-patterns and what to do instead</p>
</li>
<li><p>Test conventions and where good examples live</p>
</li>
<li><p>Pointers to deeper documentation when more detail is needed</p>
</li>
</ul>
<p>Two practical rules emerged from the first month of using these files.</p>
<p><strong>Keep them lean:</strong> There is a counterintuitive failure mode with long instruction lists: the agent doesn't just skip the new ones at the bottom. The average compliance across all of them drops. A bloated memory file becomes a memory file that the agent skims. If a section runs more than a paragraph or two, move it to a separate document and link to it.</p>
<p><strong>Phrase rules as imperatives, not aspirations:</strong> "Controllers must not call repositories. Route through the app layer." beats "Try to keep controllers thin." The first is testable. The second is decorative.</p>
<p>That was the entry point. But a single root-level file was not enough for a monorepo with multiple services and frontends, which led to the next decision.</p>
<h2 id="heading-where-per-service-memory-files-earn-their-keep">Where Per-Service Memory Files Earn Their Keep</h2>
<p>A single <code>AGENTS.md</code> at the root of a monorepo collapses under its own weight pretty quickly. Each service in our codebase has its own architecture, conventions, and business rules. Trying to fit all of that into one file produced a long document that the agent treated as background noise, and we were back to the bloat problem from the previous section.</p>
<p>The pattern that worked: every service or app gets its own <code>AGENTS.md</code> at its root, and the project-level <code>AGENTS.md</code> becomes an index that points to them.</p>
<p>A per-service <code>AGENTS.md</code> covers things like:</p>
<ul>
<li><p>The architecture for this service (the four-layer pattern, the directory layout)</p>
</li>
<li><p>Naming conventions specific to this service</p>
</li>
<li><p>Test patterns and where good examples live</p>
</li>
<li><p>Business rules that this service is responsible for</p>
</li>
<li><p>Inter-service contracts and what other services consume from this one</p>
</li>
<li><p>Pointers to deeper docs in <code>docs/</code></p>
</li>
<li><p>A "Lessons learned" section, which I'll come back to in the section on the compounding loop</p>
</li>
</ul>
<p>The same lean rule applies. Keep it short, point at examples, and phrase guidance as imperatives.</p>
<p>The reason this works mechanically is that the agent loads the right files for the work at hand. When an engineer asks the agent to change something in <code>backend/</code>, the agent reads the project-level <code>AGENTS.md</code>, sees that work in <code>backend/</code> should be guided by <code>backend/AGENTS.md</code>, and loads that file. It doesn't load the frontend's <code>AGENTS.md</code>, because that work is somewhere else. The context window stays focused on what's relevant.</p>
<p>Without this split, you have two bad options. Either you put everything in the root file, where the agent ignores most of it, or you put nothing in the root file, where the agent has no team context at all. The per-service split gives you both depth and signal.</p>
<p>But these files only work if the deeper docs they point to actually exist, which is where the next piece of the system came in.</p>
<h2 id="heading-what-this-looks-like-on-disk">What This Looks Like on Disk</h2>
<p>Before going further, it helps to see the whole structure laid out. Here's the shape we settled on for our monorepo. The exact folder names follow Claude Code's conventions. If you use Cursor, it would be <code>.cursor/</code>, and if you use Cline, it would be <code>.clinerules</code> – but the shape transfers directly.</p>
<pre><code class="language-plaintext">project-root/
├── AGENTS.md                       # symlink to CLAUDE.md
├── CLAUDE.md                       # root memory file
├── README.md                       # human-facing project readme
│
├── .claude/                        # tool-specific config folder
│   ├── README.md                   # explains the .claude/ layout
│   ├── settings.json               # permissions and guardrails
│   ├── agents/                     # specialised subagents (optional)
│   ├── commands/                   # slash commands engineers run
│   │   ├── review-pr.md            # the PR review command
│   │   └── plan-feature.md         # implementation plan command
│   ├── hooks/                      # lifecycle hooks (optional)
│   ├── pr-rules/                   # rule files for PR review
│   │   ├── common.md               # rules that apply to every PR
│   │   ├── frontend.md             # rules for frontend changes
│   │   ├── backend.md              # rules for backend changes
│   │   ├── service-a.md            # rules for service-a
│   │   └── service-b.md            # rules for service-b
│   └── skills/                     # reusable workflows
│
├── frontend/
│   ├── AGENTS.md                   # frontend conventions
│   ├── docs/
│   │   ├── overview.md
│   │   ├── architecture.md         # routing, state, data layer
│   │   ├── design-system.md        # design system reference
│   │   └── testing.md              # test conventions
│   └── src/
│
├── backend/
│   ├── AGENTS.md                   # the four-layer pattern
│   ├── docs/
│   │   ├── overview.md
│   │   ├── architecture.md         # route -&gt; controller -&gt; app -&gt; repo
│   │   ├── auth.md                 # v1 vs v2 middleware
│   │   ├── business-rules.md
│   │   └── integrations.md
│   └── src/
│
├── service-a/
│   ├── AGENTS.md
│   ├── docs/
│   │   ├── overview.md
│   │   ├── business-rules.md
│   │   └── integrations.md
│   └── src/
│
└── service-b/
    ├── AGENTS.md
    ├── docs/
    │   ├── overview.md
    │   ├── business-rules.md
    │   └── integrations.md
    └── src/
</code></pre>
<p>A few things worth pointing out:</p>
<p>The <code>.claude/</code> folder uses standard subfolder names: <code>commands</code>, <code>agents</code>, <code>hooks</code>, <code>skills</code>. These follow Claude Code's plugin model, but most modern AI coding tools have similar slots. Following the conventions makes the structure recognisable to anyone on the team and lowers the cost of switching tools later.</p>
<p>The <code>pr-rules/</code> folder isn't a standard convention. It's a folder we created to hold per-area review rules that the PR review command loads selectively. You don't have to call it <code>pr-rules</code> – the name matters less than having one place where review rules live.</p>
<p>Each service has its own <code>AGENTS.md</code> plus a <code>docs/</code> folder. The root <code>AGENTS.md</code> is short and acts as an index. It tells the agent things like "if you touch files in <code>backend/</code>, also read <code>backend/AGENTS.md</code> first." The per-service file then points at the deeper docs as needed.</p>
<h2 id="heading-generated-documentation-as-a-side-effect">Generated Documentation as a Side Effect</h2>
<p>Setting up per-service <code>AGENTS.md</code> files surfaced a problem I had been quietly avoiding. Most of our services didn't have decent documentation. Not API reference material, which lives in code, but the higher-level "what does this service do, what business rules does it enforce, what does it consume and produce" information that lives in nobody's head except the original author's.</p>
<p>The honest reason was that writing this kind of documentation by hand had never paid back the time it took. By the time the doc was finished, half of it was already stale.</p>
<p>So I tried something I wouldn't have considered earlier. I used the AI itself to generate a first draft for each service. I pointed the agent at each service's code and asked it to produce a <code>docs/</code> folder with a specific structure: an overview, a list of business rules, an integrations document, a domain model, and any quirks worth knowing. The agent read the code, traced the call paths, and wrote a draft.</p>
<p>I then reviewed the output by hand, corrected the things it got wrong, and committed the result. The first drafts were 70-80% correct. The remaining 20-30% was where the agent had made plausible but wrong inferences, and those were exactly the cases where human review mattered.</p>
<p>The generated docs ended up serving two audiences. The agent uses them when reasoning about changes, which means it has real context for the service it's touching rather than guessing from local files. And new engineers use them on their first day, which has cut our onboarding time noticeably.</p>
<p>We used to write onboarding documents that drifted out of date within months. These docs stay closer to current because the agent reads them on every PR, and any drift gets surfaced when the agent gives wrong advice based on stale information.</p>
<p>The pattern that works is to keep the per-service <code>AGENTS.md</code> short and pointing at the docs, rather than duplicating their content. <code>AGENTS.md</code> is the always-loaded index. <code>docs/</code> holds the details. The agent loads the relevant doc on demand when the task calls for it.</p>
<p>With the rules in place and the docs in place, I had everything I needed to build the actual reviewer.</p>
<h2 id="heading-building-the-pr-review-command">Building the PR Review Command</h2>
<p>This is the piece that most directly unblocked my queue.</p>
<p>This command didn't appear out of nowhere. It started as the checklist I was running through in my head every time I opened a PR. I was reviewing every change manually, leaving the same comments, flagging the same patterns. So I wrote that checklist down, expanded it with references to the per-service docs for the harder rules, and turned it into a command anyone on the team could run.</p>
<p>Then I handed it to the engineers and changed the rule: run this on your own branch before marking the PR ready for review. That single shift moved the work from after the PR was opened to before. Engineers now catch 90-95% of the blockers, improvements, and nice-to-haves on their own machine, fix them locally, and only then push the change.</p>
<p>The PR description includes the AI's summary, so when anyone opens the PR, they can see the reviewer's green signal at the top before even reading the diff.</p>
<p>GitHub stays clean. The conversation on the PR becomes about the things that actually need a human, not the recurring stuff the team already knows how to fix.</p>
<p>The command lives in <code>.claude/commands/review-pr.md</code>. Here's a generalised version. Your tool's command structure may differ, but the shape is what matters.</p>
<pre><code class="language-markdown"># Review PR

Review the current branch's PR. Be direct. Cite `file:line`. Surface real issues,
no padding.

## 1. Scope the diff

Run, in order:

    gh pr view --json number,title,body,headRefName 2&gt;/dev/null || true
    git fetch origin main
    git log --no-merges origin/main..HEAD --oneline
    git diff origin/main...HEAD --stat
    git diff origin/main...HEAD

Read the PR body. Note the stated intent. Every change should trace to it. Flag
anything that does not.

Use `...` (three dots) for the diff. It compares against the merge base and
excludes commits brought in by merging main.

## 2. Load rules

Always read `.claude/pr-rules/common.md`.

Then read the per-area file for each workspace touched in the diff:

| Workspace path | Rules file                      |
| -------------- | ------------------------------- |
| `frontend/**`  | `.claude/pr-rules/frontend.md`  |
| `backend/**`   | `.claude/pr-rules/backend.md`   |
| `service-a/**` | `.claude/pr-rules/service-a.md` |
| `service-b/**` | `.claude/pr-rules/service-b.md` |

For non-trivial changes, follow doc pointers inside the rules files (for
example, `backend/AGENTS.md`, `backend/docs/architecture.md`).

Apply every entry under each file's "Lessons learned" section as a check.

## 3. Output

Use exactly this format.

    ## Summary
    &lt;one paragraph: what the PR does, whether it matches the stated intent&gt;

    ## Blocking
    - [file:line] issue, why it blocks

    ## Should fix
    - [file:line] issue

    ## Nice to have
    - issue

    ## Verified
    - what was checked and looks good

If nothing blocks, say so. Do not manufacture concerns.

If you find an issue worth remembering for future PRs, suggest the bullet to
add to the relevant rules file's "Lessons learned" section. Do not edit the
rules file yourself, leave that to the human.
</code></pre>
<p>A few of the design choices in this command turned out to matter more than I expected.</p>
<p>The structured output format (Summary, Blocking, Should fix, Nice to have, Verified) keeps the review easy to scan and easy to paste into a PR description. The "Verified" section is the most underrated of the five: it tells the human reviewer what the AI already checked, so they can spend their attention elsewhere. Without it, the human reviewer ends up doing the same checks twice.</p>
<p>The instruction to be direct and stop padding does real work. Without it, AI reviewers tend to manufacture concerns to look thorough, which trains engineers to skim past the bot. Telling it explicitly to say "nothing blocks" when nothing blocks made the signal-to-noise ratio of the output much better.</p>
<p>The "suggest a bullet for the rules file" instruction at the end is the heart of the whole system, and I'll explain why in the section on the compounding loop. The key constraint here is that the agent suggests the bullet but doesn't commit to it. A human evaluates whether it's general enough to be a rule, and only then adds it to the file. That manual step is what keeps the rules sharp instead of bloated.</p>
<p>With each PR, if humans fix something or the AI suggests something, you keep adding those to your MD files and keep improving your agents for the future. The result compounds quickly.</p>
<p>One more thing here: the diff-scoping commands are all read-only. The command shouldn't be able to push, edit PRs, or close anything. Which is the next piece of the system.</p>
<h2 id="heading-guardrails-read-only-by-default">Guardrails: Read-Only by Default</h2>
<p>Giving an AI agent broad permissions on your codebase is a security incident waiting to happen. Even if you trust the model to behave, an LLM occasionally does unexpected things, and a fast-moving agent on an unrestricted shell can cause damage in seconds.</p>
<p>The fix is a <code>settings.json</code> (in Claude Code – other tools have their own equivalents) at the root of <code>.claude/</code> that explicitly declares what the agent can and can't do. The deny list matters more than the allow list, and a good one is organised around four categories of risk.</p>
<p>The first is <strong>secrets and configuration</strong>. Any read against anything that appears to be a credential is blocked. That covers <code>.env</code> files of every variant (<code>.env</code>, <code>.env.local</code>, <code>.env.production</code>, <code>.env.test</code>, and so on), <code>.npmrc</code>, <code>.netrc</code>, <code>.pgpass</code>, <code>id_rsa</code>, <code>id_ed25519</code>, <code>*.pem</code>, <code>*.key</code>, <code>*.p12</code>, <code>**/credentials.json</code>, <code>**/secrets.json</code>, <code>**/.aws/**</code>, <code>**/.ssh/**</code>, <code>**/.gcloud/**</code>, and <code>**/.kube/**</code>. Environment dumps are blocked too: <code>env</code>, <code>printenv</code>, <code>set</code>, <code>export</code>. The agent has no legitimate reason to read or echo any of these, ever.</p>
<p>The second is <strong>destructive Git operations</strong>. The agent can read Git history but can't rewrite or push it. Blocked: <code>git push</code>, <code>git commit</code>, <code>git revert</code>, <code>git cherry-pick</code>, <code>git merge</code>, <code>git rebase</code>, <code>git reset --hard</code>, <code>git tag</code>. Allowed: <code>git fetch</code>, <code>git status</code>, <code>git log</code>, <code>git diff</code>, <code>git show</code>, <code>git branch</code>, <code>git rev-parse</code>, <code>git merge-base</code>, <code>git config --get</code>.</p>
<p>The third is <strong>write operations on PRs and issues</strong>. The agent can read your GitHub state but can't act on it. Blocked: <code>gh pr create</code>, <code>gh pr edit</code>, <code>gh pr merge</code>, <code>gh pr close</code>, <code>gh pr comment</code>, <code>gh pr review</code>, <code>gh issue create</code>, <code>gh issue edit</code>, <code>gh issue close</code>, <code>gh issue comment</code>, <code>gh release create</code>, <code>gh repo create</code>, <code>gh repo edit</code>, <code>gh repo delete</code>. Allowed: <code>gh pr view</code>, <code>gh pr list</code>, <code>gh pr diff</code>, <code>gh pr checks</code>, <code>gh issue view</code>, <code>gh issue list</code>, <code>gh release view</code>.</p>
<p>The fourth is <strong>workflow and automation control</strong>. These are the surfaces where a compromised or misled agent could do the most damage. Blocked: <code>gh workflow run</code>, <code>gh run rerun</code>, <code>gh run cancel</code>, <code>gh secret</code>, <code>gh variable</code>, <code>gh auth</code>, <code>gh ssh-key</code>, <code>gh gpg-key</code>, and the unrestricted <code>gh api</code>.</p>
<p>For shell commands the agent legitimately needs to run, like build and test commands, allowlist specific patterns: <code>pnpm test</code>, <code>pnpm lint</code>, <code>pnpm format:check</code>, <code>pnpm build</code>, <code>pnpm vitest</code>. Anything outside the allowed list requires human confirmation. These are your own settings&nbsp;– I've just mentioned what I prefer.</p>
<p>The pattern is simple: read-only by default, write-allowed only for the specific commands you have explicitly approved. The agent can investigate, plan, and recommend. It can't ship.</p>
<p>With the structure in place and the guardrails set, the system started doing its job. What I didn't expect was how much better it would get over the months that followed.</p>
<h2 id="heading-the-compounding-loop-that-made-the-real-difference">The Compounding Loop That Made the Real Difference</h2>
<p>When we started, the AI reviewer was useful but not transformative. It caught some obvious issues, missed plenty of subtle ones, and produced a fair amount of noise.</p>
<p>The first month, my review burden dropped by 35%. The time I was spending on PR checking was reduced to 1/3, almost. Decent, not life-changing.</p>
<p>What changed over time wasn't the tool. It was the rules.</p>
<p>Every time a PR creator and reviewer caught something the AI had missed, we were adding bullets to the relevant rules file. Every time the AI flagged something useful that turned out to be a recurring pattern, the agent's own suggestion at the end of the review went into the file.</p>
<p>After a few days, the rules files had grown into something that captured a meaningful fraction of the team's collective review knowledge, written down in a place every agent on the team would read.</p>
<p>The catch rate went up. The noise went down because the rules also said what was acceptable and what we already considered solved. New engineers stopped getting the same comments on their first three PRs because the AI caught the comments first. Engineers joining the team didn't have to absorb the conventions through six months of review feedback. They installed the project, opened it in their editor, and the agent already knew.</p>
<p>This is the part most teams miss when they evaluate AI PR review tools. They look at the catch rate today and decide whether the tool is worth the price. The catch rate today isn't the right number. The right number is what the catch rate looks like in six months, after the rules file has absorbed every recurring mistake your team has made.</p>
<p>A single rule written down today saves a small amount of review time. Over a hundred PRs, it saves more. After a year, the rules file is a written-down version of a tech lead's accumulated taste. We've switched between Claude Code, the GitHub Copilot CLI, and Cursor for various tasks during this period. The AI tool changes, but the rules file in the repo stays the same.</p>
<p>The discipline that makes this work is treating the rules file as living documentation. Every recurring review comment is a candidate for promotion into the file. If you catch yourself typing the same feedback in two different PRs, that's a rule that belongs in <code>pr-rules/</code>. The "suggest a bullet" instruction in the review command is what makes this practical: the AI does the typing, the human does the deciding.</p>
<p>This is also what made me realise the system was worth the time it took to set up. The PR review command, on its own, is useful but unremarkable. The compounding loop is what turns it into infrastructure.</p>
<h2 id="heading-starting-from-zero-on-an-existing-project">Starting From Zero on an Existing Project</h2>
<p>If you've read this far and feel like the gap between your project and what I just described is a sprint of work, that's the most common reaction. It's also not correct.</p>
<p>The blank <code>AGENTS.md</code> is intimidating, especially on an existing codebase. You know your team has a thousand conventions, and writing a thousand rules sounds like a project that takes weeks before it produces any value.</p>
<p>The honest answer is that you can't write all the rules up front, and you shouldn't try. The first version of any of these files should take an afternoon, not a sprint.</p>
<p>Here's how I would actually start.</p>
<p>Run <code>/init</code> (or your tool's equivalent). In Claude Code, <code>/init</code> scans the project, infers the obvious shape (language, framework, entry points, build commands), and writes an initial <code>CLAUDE.md</code>. The output is a starting point, not a finished file. Read it, delete most of what it generates, and keep the bones.</p>
<p>Then add three things, each one bullet long.</p>
<p>First, an architecture rule. Pick the single most important convention your team enforces. For us, that was the four-layer pattern. The bullet was: "Controllers must not call repository functions directly. They must go through the app layer."</p>
<p>Second, a discoverability rule. Pick the single most important shared resource the team has, the one new code is most likely to duplicate. For us, that was the design system. The bullet was: "Before creating a new UI component, check <code>/src/design-system/</code> first."</p>
<p>Third, a "do not touch" rule. Pick the single most dangerous file or area in the codebase. Auth, billing, or migrations whichever has the most production risk. The bullet was: "Do not modify files in <code>/auth/</code> without human approval."</p>
<p>That's enough to start. Three rules, ten minutes of writing, and most of your team's recurring AI mistakes start to drop.</p>
<p>If even three rules feels like too much, start with one. Pick a single line that matters in your codebase and write it down.</p>
<p>"No <code>any</code> types in TypeScript." "Always use the enum, never compare against the string literal." "Run the linter before opening a PR." It doesn't have to be sophisticated. It doesn't have to cover edge cases. It just has to capture one piece of judgement that lives in your head today and would otherwise stay there.</p>
<p>Tomorrow, add another. The first week, you might catch 5% of the recurring mistakes. By 20 or 30 PRs in, you might catch 20-30%. The rules file doesn't need to be impressive on day one. It needs to exist and keep growing.</p>
<p>This is the compounding effect I'll come back to soon, and it's the reason this approach works on real projects rather than just in theory.</p>
<p>From there, the file grows the same way it would grow for any team. Every review catch becomes a candidate rule. After a few weeks, you have ten or fifteen rules. After a few months, you have a real review system.</p>
<p>The mistake is trying to write the perfect file on day one. The right file is the one you start with and keep editing.</p>
<h2 id="heading-what-still-needs-human-review">What Still Needs Human Review</h2>
<p>This system doesn't replace human review, and it shouldn't be allowed to.</p>
<p>The AI reviewer catches what the rules describe, plus a fair number of obvious things it would have spotted anyway. It doesn't catch problems that depend on context the rules don't capture. It doesn't catch product judgement. It doesn't catch the question of whether the change should have been built at all.</p>
<p>It also has an important blind spot when reviewing AI-authored code. The reviewer shares the same training data and reasoning patterns as the agent that wrote the code. If the original agent missed the v1/v2 distinction because it had no way to see the migration timeline, an AI reviewer reading the same diff has the same problem. Two AIs in a review loop are not two independent reviewers. They share blind spots.</p>
<p>That is why the AI reviewer in this setup never approves a PR. It produces a structured review that goes into the PR description. A human still reads the change and approves it. The AI is the first pass, not the gate.</p>
<p>Accountability also has to live with a human. When something the AI approved breaks production, someone has to own the post-mortem and decide what changes are needed for next time. The AI can't be that person. What it can do, well, is reduce the stack of small mistakes a human reviewer has to find before they get to the harder questions.</p>
<h2 id="heading-a-two-week-setup-plan">A Two-Week Setup Plan</h2>
<p>If you want to set this up for your own team, here's a concrete plan that fits in a couple of weeks. None of this needs to happen in a single push.</p>
<h3 id="heading-day-1-bootstrap-the-memory-file">Day 1: Bootstrap the memory file.</h3>
<p>Run <code>/init</code> (or your tool's equivalent) at the root of the project. Read the generated <code>CLAUDE.md</code> (or <code>AGENTS.md</code>). Delete most of it. Keep the tech stack and project structure sections.</p>
<p>Add the three rules from the previous section: one architecture rule, one discoverability rule, and one "do not touch" rule. Decide whether you want both files or a symlink.</p>
<h3 id="heading-day-2-add-per-service-files-for-your-highest-risk-areas">Day 2: Add per-service files for your highest-risk areas</h3>
<p>Pick the two or three areas of the codebase that change most often or carry the most risk. Add an <code>AGENTS.md</code> to each, following the same lean pattern. Include the architectural pattern for that area, the naming conventions, where to find good test examples, and pointers to any existing docs. Skip anything that doesn't need to be there yet.</p>
<h3 id="heading-day-3-set-up-the-directory-structure-and-guardrails">Day 3: Set up the directory structure and guardrails</h3>
<p>Create a <code>.claude/</code> folder (or your tool's equivalent) at the root, with <code>commands/</code> and <code>pr-rules/</code> subfolders. Add a <code>settings.json</code> with the deny list categories from the guardrails section. Test that the agent can't read a <code>.env</code> file, run <code>git push</code>, or create a PR. If any of those work, fix the settings before doing anything else.</p>
<h3 id="heading-day-4-write-the-pr-review-command">Day 4: Write the PR review command</h3>
<p>Adapt the command in this article to your structure. Include the diff scoping, the rule loading, the output format, and the "suggest a new rule" instruction at the end. Run it on a branch you've already merged, and tune the output until it's useful.</p>
<h3 id="heading-day-5-run-it-on-real-prs">Day 5: Run it on real PRs</h3>
<p>Have one or two engineers run the command on their next PRs before opening them. Read the output. Note what it caught, what it missed, and what was noise. Add the missing catches to the rules files. The first week is mostly tuning.</p>
<h3 id="heading-week-2-roll-out-and-document">Week 2: Roll out and document</h3>
<p>Once the command produces useful output reliably, ask the whole team to run it before opening PRs and paste the output into the PR description. Add a short section to your contributing guide explaining the workflow. Set a recurring item in your team's rituals to review the rules files monthly and trim anything that has gone stale.</p>
<p>That gets you to a working system. From there, the maintenance is incremental. Every recurring review comment becomes a candidate rule. Every architectural decision becomes a candidate update to the relevant <code>AGENTS.md</code>. The system improves as a side effect of the work the team is already doing.</p>
<h2 id="heading-what-is-working-what-i-am-still-improving">What Is Working, What I Am Still Improving</h2>
<p>Here's my honest assessment after a few months of running this:</p>
<h3 id="heading-whats-working">What's Working</h3>
<p>My review burden is meaningfully smaller. Engineers fix most of the easy mistakes before I see the PR. The "Verified" section of the AI's output tells me what to skip past. New engineers ramp faster because the conventions live in a place their tooling reads. The rules files have grown into something I would actually use to onboard someone new.</p>
<h3 id="heading-what-isnt-finished">What Isn't Finished</h3>
<p>The AI still misses problems that depend on context, and the rules don't capture them. The rules files grow, but they also need pruning, and we haven't been disciplined about that.</p>
<p>We're still figuring out how to handle rules that apply only conditionally. Docs are helping in that case, but we need to keep those up to date. And no system survives a determined engineer who skips the workflow or docs when they're in a rush.</p>
<p>There's no shortcut here. The work is real, ongoing, and mostly about discipline. The discipline is treating your codebase as something the AI needs to learn, and treating every recurring review comment as something that should be written down once instead of typed thirty times. If you're willing to do that, the tools take care of the rest.</p>
<p>If you take three things from this article, take these.</p>
<ol>
<li><p>First, don't pay for a generic reviewer to do a job your codebase needs to inform. Generic reviewers catch generic problems. Most of your real review work is specific to your team.</p>
</li>
<li><p>Second, put the rules in a file the AI reads, not in your head. <code>AGENTS.md</code>, <code>CLAUDE.md</code>, per-service files, per-area rules files. Pick a structure and stick to it.</p>
</li>
<li><p>Third, treat every human review catch as a chance to update the rules. The compounding effect over months is the entire point. A review system that improves itself is worth more than any single tool.</p>
</li>
</ol>
<p>That's the system. It took a couple of weeks to build the foundation and a few months for the rules to mature. It costs very little to run, and it has done more for our PR throughput than any tool I evaluated.</p>
<h2 id="heading-sources">Sources</h2>
<ul>
<li><p>CircleCI's 2026 State of Software Delivery report, analysing more than 28 million CI workflows from over 22,000 organisations: <a href="https://circleci.com/resources/2026-state-of-software-delivery/">https://circleci.com/resources/2026-state-of-software-delivery/</a></p>
</li>
<li><p>CircleCI's blog post detailing the year-over-year throughput numbers, including the 59% feature branch growth and the main branch decline: <a href="https://circleci.com/blog/five-takeaways-2026-software-delivery-report/">https://circleci.com/blog/five-takeaways-2026-software-delivery-report/</a></p>
</li>
<li><p>GitHub announcement of Copilot's transition to usage-based billing on June 1, 2026: <a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/">https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/</a></p>
</li>
<li><p>GitHub changelog confirming Copilot code review will start consuming GitHub Actions minutes on June 1, 2026: <a href="https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/">https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/</a></p>
</li>
<li><p>AGENTS.md, the open standard's official site, including its stewardship under the Agentic AI Foundation and the Linux Foundation: <a href="https://agents.md/">https://agents.md/</a></p>
</li>
<li><p>Anthropic's Claude Code documentation on the memory system, including CLAUDE.md, auto memory, and the /init command: <a href="https://code.claude.com/docs/en/memory">https://code.claude.com/docs/en/memory</a></p>
</li>
<li><p>Anthropic's Claude Code GitHub Actions documentation, including notes on token-based billing and recommended cost controls: <a href="https://code.claude.com/docs/en/github-actions">https://code.claude.com/docs/en/github-actions</a></p>
</li>
<li><p>CodeRabbit's pricing documentation, confirming the per-developer-per-month seat model: <a href="https://docs.coderabbit.ai/management/plans">https://docs.coderabbit.ai/management/plans</a></p>
</li>
<li><p>Greptile's March 2026 pricing announcement, introducing the base-plus-usage model at $30 per seat per month with 50 included reviews: <a href="https://www.greptile.com/blog/greptile-v4">https://www.greptile.com/blog/greptile-v4</a></p>
</li>
<li><p>HumanLayer's write-up on writing a good CLAUDE.md, including data on instruction-following degradation: <a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md">https://www.humanlayer.dev/blog/writing-a-good-claude-md</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Keep Human Experts Visible in Your AI-Assisted Codebase ]]>
                </title>
                <description>
                    <![CDATA[ Six months ago, Stack Overflow processed 108,563 questions in a single month. By December 2025, that number had fallen to 3,862. A 78% collapse in two years. The explanation everyone reaches for is th ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-keep-human-experts-visible-in-your-ai-assisted-codebase/</link>
                <guid isPermaLink="false">69dd18d4217f5dfcbd13e964</guid>
                
                    <category>
                        <![CDATA[ claude.ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude-code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Daniel Nwaneri ]]>
                </dc:creator>
                <pubDate>Mon, 13 Apr 2026 16:24:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/21d160a8-af66-4048-9fda-1d83b2e26148.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Six months ago, Stack Overflow processed 108,563 questions in a single month. By December 2025, that number had fallen to 3,862. A 78% collapse in two years.</p>
<p>The explanation everyone reaches for is that AI replaced it. That's partly true. But it misses the structural problem underneath: every time a developer asks Claude or ChatGPT to write code, the knowledge that shaped the answer disappears.</p>
<p>The GitHub discussion where someone spent two hours documenting why cursor-based pagination beats offset for live-updating datasets. The Stack Overflow answer from 2019 where one engineer, after a week of debugging, documented exactly why that approach fails under concurrent writes.</p>
<p>The AI consumed all of it. The humans who produced it got nothing — no citation in the codebase, no signal that their work mattered.</p>
<p>Over time, those people stopped contributing. Stack Overflow isn't dying because it's bad. It's dying because AI extracted its value and the feedback loop that kept humans contributing broke down.</p>
<p>This tutorial builds a tool that puts that loop back together. <strong>proof-of-contribution</strong> is a Claude Code skill that links every AI-generated artifact back to the human knowledge that inspired it — and surfaces exactly where the AI made choices with no human source at all.</p>
<p>I'll show you how to install proof-of-contribution, how to record your first provenance entry, how to use the spec-writer integration that makes Knowledge Gaps deterministic, and how to run <code>poc.py verify</code> — a static analyser that detects gaps without a single API call.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-quickstart-in-5-minutes">Quickstart in 5 Minutes</a></p>
</li>
<li><p><a href="#heading-how-the-tool-works">How the Tool Works</a></p>
</li>
<li><p><a href="#heading-how-to-install-proof-of-contribution">How to Install proof-of-contribution</a></p>
</li>
<li><p><a href="#heading-how-to-scaffold-your-project">How to Scaffold Your Project</a></p>
</li>
<li><p><a href="#heading-how-to-record-your-first-provenance-entry">How to Record Your First Provenance Entry</a></p>
</li>
<li><p><a href="#heading-how-to-use-import-spec-to-seed-knowledge-gaps">How to Use import-spec to Seed Knowledge Gaps</a></p>
</li>
<li><p><a href="#heading-how-to-trace-human-attribution">How to Trace Human Attribution</a></p>
</li>
<li><p><a href="#heading-how-to-verify-with-static-analysis">How to Verify with Static Analysis</a></p>
</li>
<li><p><a href="#heading-how-to-enable-pr-enforcement">How to Enable PR Enforcement</a></p>
</li>
<li><p><a href="#heading-where-to-go-next">Where to Go Next</a></p>
</li>
</ol>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>proof-of-contribution is a Claude Code skill with a local CLI. Together they give you:</p>
<ul>
<li><p><strong>Provenance Blocks</strong>: Claude appends a structured attribution block to every generated artifact, listing the human sources that inspired it and flagging what it synthesized without any traceable source.</p>
</li>
<li><p><strong>Knowledge Gaps</strong>: the parts of AI-generated code that have no human citation, surfaced before they become production incidents</p>
</li>
<li><p><code>poc.py trace</code>: a CLI command that shows the full human attribution chain for any file in thirty seconds</p>
</li>
<li><p><code>poc.py import-spec</code>: bridges proof-of-contribution with spec-writer, seeding knowledge gaps from your spec's assumptions list before the agent builds anything</p>
</li>
<li><p><code>poc.py verify</code>: a static analyser that cross-checks your file's structure against seeded claims using Python's AST. Zero API calls. Exit code 0 means clean, exit code 1 means gaps found — wires directly into CI</p>
</li>
<li><p><strong>A GitHub Action</strong>: optional PR enforcement that fails PRs missing attribution, for teams that want a standard</p>
</li>
</ul>
<p>The complete source is at <a href="https://github.com/dannwaneri/proof-of-contribution">github.com/dannwaneri/proof-of-contribution</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This is a beginner-to-intermediate tutorial. You should be comfortable with:</p>
<ul>
<li><p><strong>Command line basics</strong>: navigating directories, running scripts</p>
</li>
<li><p><strong>Git</strong>: basic commits and PRs</p>
</li>
<li><p><strong>Python 3.8 or higher</strong>: the CLI is pure Python with no dependencies</p>
</li>
</ul>
<p>You will need:</p>
<ul>
<li><p><strong>Python installed</strong>: check with <code>python --version</code> or <code>python3 --version</code></p>
</li>
<li><p><strong>Git installed</strong>: check with <code>git --version</code></p>
</li>
<li><p><strong>Claude Code</strong> (or any agent that supports the Agent Skills standard — Cursor and Gemini CLI also work)</p>
</li>
</ul>
<p>There's no database to install. No API keys. No paid services. The default storage is SQLite, which Python includes out of the box.</p>
<h2 id="heading-quickstart-in-5-minutes">Quickstart in 5 Minutes</h2>
<p>If you want to try the tool before reading the full tutorial, here are the five commands that take you from zero to your first gap detection:</p>
<p><strong>Mac and Linux:</strong></p>
<pre><code class="language-bash"># 1. Install
mkdir -p ~/.claude/skills
git clone https://github.com/dannwaneri/proof-of-contribution.git \
  ~/.claude/skills/proof-of-contribution

# 2. Scaffold your project (run in your repo root)
python ~/.claude/skills/proof-of-contribution/assets/scripts/poc_init.py

# 3. Record attribution for an AI-generated file
python poc.py add src/utils/parser.py

# 4. Detect gaps via static analysis
python poc.py verify src/utils/parser.py

# 5. See the full provenance chain
python poc.py trace src/utils/parser.py
</code></pre>
<p><strong>Windows PowerShell:</strong></p>
<pre><code class="language-powershell"># 1. Install
New-Item -ItemType Directory -Force -Path "$HOME\.claude\skills"
git clone https://github.com/dannwaneri/proof-of-contribution.git `
  "$HOME\.claude\skills\proof-of-contribution"

# 2. Scaffold your project
python "$HOME\.claude\skills\proof-of-contribution\assets\scripts\poc_init.py"

# 3. Record attribution
python poc.py add src\utils\parser.py

# 4. Detect gaps
python poc.py verify src\utils\parser.py

# 5. See the full provenance chain
python poc.py trace src\utils\parser.py
</code></pre>
<p>That's the whole tool. The sections below walk through each step in detail with real terminal output at every stage.</p>
<h2 id="heading-how-the-tool-works">How the Tool Works</h2>
<p>Before you install anything, you need a clear mental model of what proof-of-contribution actually does — because the most important part isn't obvious.</p>
<h3 id="heading-the-archaeology-problem">The Archaeology Problem</h3>
<p>Here's a scenario that happens on every team using AI-assisted development.</p>
<p>A developer joins. They go through six months of AI-generated codebase. They hit a bug in the pagination logic — cursor-based, unusual implementation, nobody remembers why it was built that way. The original developer has left.</p>
<p>Old answer: two days of archaeology. <code>git blame</code> points to a commit message that says "fix pagination." The commit before that says "implement pagination." Dead end.</p>
<p>With <code>poc.py trace src/utils/paginator.py</code>, that same developer sees this in thirty seconds:</p>
<pre><code class="language-plaintext">Provenance trace: src/utils/paginator.py
────────────────────────────────────────────────────────────
  [HIGH]  @tannerlinsley on github
          Cursor pagination discussion
          https://github.com/TanStack/query/discussions/123
          Insight: cursor beats offset for live-updating datasets

Knowledge gaps (AI-synthesized, no human source):
  • Error retry strategy — no human source cited
  • Concurrent write handling — AI chose this arbitrarily
</code></pre>
<p>They now know where the pattern came from and — critically — which parts have no traceable human source. The concurrent write handling is where the bug lives. The AI made a choice nobody reviewed.</p>
<p>That's what this tool does. Not enforcement first. Archaeology first.</p>
<h3 id="heading-how-knowledge-gaps-are-detected">How Knowledge Gaps Are Detected</h3>
<p>The obvious assumption is that Claude introspects and reports what it doesn't know. That assumption is wrong. LLMs hallucinate confidently. An AI that could reliably detect its own knowledge gaps wouldn't produce them.</p>
<p>The detection mechanism is a comparison, not introspection.</p>
<p>When you use <a href="https://github.com/dannwaneri/spec-writer">spec-writer</a> before building, it generates a spec with an explicit <code>## Assumptions to review</code> section — every decision the AI is making that you didn't specify, each one impact-rated. That list is the contract.</p>
<p>When you run <code>poc.py import-spec spec.md --artifact src/utils/paginator.py</code>, those assumptions get seeded into the database as unresolved knowledge gaps. After the agent builds, <code>poc.py trace</code> shows which assumptions made it into code with no human source ever cited.</p>
<p>The AI isn't grading its own exam. The spec is the answer key.</p>
<p><code>poc.py verify</code> takes this further. After the agent builds, it parses the file's actual structure using Python's built-in <code>ast</code> module — extracting every function definition, conditional branch, and return path. It cross-checks each one against the seeded claims. Any structural unit with no resolved claim surfaces as a deterministic Knowledge Gap, regardless of how confident the model was when it wrote the code.</p>
<h2 id="heading-how-to-install-proof-of-contribution">How to Install proof-of-contribution</h2>
<h3 id="heading-mac-and-linux">Mac and Linux</h3>
<pre><code class="language-bash">mkdir -p ~/.claude/skills
git clone https://github.com/dannwaneri/proof-of-contribution.git \
  ~/.claude/skills/proof-of-contribution
</code></pre>
<h3 id="heading-windows-powershell">Windows PowerShell</h3>
<pre><code class="language-powershell">New-Item -ItemType Directory -Force -Path "$HOME\.claude\skills"
git clone https://github.com/dannwaneri/proof-of-contribution.git `
  "$HOME\.claude\skills\proof-of-contribution"
</code></pre>
<p>That's the entire installation. No package to install, no configuration file to edit. The skill is a markdown file the agent reads. The CLI is a Python script that runs locally.</p>
<h3 id="heading-verify-the-install">Verify the Install:</h3>
<pre><code class="language-bash">ls ~/.claude/skills/proof-of-contribution/
</code></pre>
<p>You should see <code>SKILL.md</code>, <code>poc.py</code>, <code>assets/</code>, and <code>references/</code>. If the directory is empty, the clone failed — check your internet connection and try again.</p>
<h2 id="heading-how-to-scaffold-your-project">How to Scaffold Your Project</h2>
<p>The scaffold script creates the database, config, CLI, and GitHub integration in your project root. Run it once per project.</p>
<h3 id="heading-mac-and-linux">Mac and Linux</h3>
<pre><code class="language-bash">cd /path/to/your/project
python ~/.claude/skills/proof-of-contribution/assets/scripts/poc_init.py
</code></pre>
<h3 id="heading-windows-powershell">Windows PowerShell</h3>
<pre><code class="language-powershell">cd C:\path\to\your\project
python "$HOME\.claude\skills\proof-of-contribution\assets\scripts\poc_init.py"
</code></pre>
<p>You should see output like this:</p>
<pre><code class="language-plaintext">🔗 Proof of Contribution — init

  →  Project root: /path/to/your/project
  ✔  Created .poc/config.json
  ✔  Created .poc/.gitignore  (db excluded from git, config tracked)
  ✔  Created .poc/provenance.db  (SQLite — no extra infra needed)
  ✔  Created .github/PULL_REQUEST_TEMPLATE.md
  ✔  Created .github/workflows/poc-check.yml
  ✔  Created poc.py  (local CLI — includes import-spec command)
  ✔  Created .gitignore

✔ Proof of Contribution initialised for 'your-project'
</code></pre>
<p>This creates four things in your project:</p>
<pre><code class="language-plaintext">your-project/
├── .poc/
│   ├── config.json      ← project settings (commit this)
│   ├── provenance.db    ← SQLite database (local only, gitignored)
│   └── .gitignore
├── .github/
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       └── poc-check.yml
└── poc.py               ← your local CLI
</code></pre>
<ul>
<li><p><code>.poc/</code> — the tool's local data directory. <code>config.json</code> stores project settings and is committed to git. <code>provenance.db</code> is the SQLite database where attribution records and knowledge gaps are stored — local only, gitignored.</p>
</li>
<li><p><code>poc.py</code> — your local CLI, copied into the project root. Run <code>python poc.py trace</code>, <code>python poc.py verify</code>, and every other command directly without a global install.</p>
</li>
<li><p><code>.github/PULL_REQUEST_TEMPLATE.md</code> — a PR template with the <code>## 🤖 AI Provenance</code> section pre-filled. Developers fill it in when submitting PRs that contain AI-generated code.</p>
</li>
<li><p><code>.github/workflows/poc-check.yml</code> — the optional GitHub Action for PR enforcement. Installed but dormant until you push the workflow file and enable it in your repo settings.</p>
</li>
</ul>
<p><strong>Windows note:</strong> if the scaffold fails with a <code>UnicodeEncodeError</code>, the emoji in the PR template is hitting a Windows encoding limit. Open <code>assets/scripts/poc_init.py</code> in a text editor and find every line ending with <code>.write_text(...)</code>. Change each one to <code>.write_text(..., encoding="utf-8")</code>. Save and re-run.</p>
<h3 id="heading-verify-the-scaffold-worked">Verify the Scaffold Worked</h3>
<pre><code class="language-bash">python poc.py report
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Proof of Contribution Report
────────────────────────────────────────
  Artifacts tracked    : 0
  With provenance      : 0  (0%)
  Unresolved gaps      : 0
  Resolved claims      : 0
  Human experts        : 0
</code></pre>
<p>Empty database, clean state. You're ready.</p>
<h2 id="heading-how-to-record-your-first-provenance-entry">How to Record Your First Provenance Entry</h2>
<p>Before we dive in here, I just want to clear something up. Earlier, I described <code>poc.py verify</code> as detecting Knowledge Gaps automatically — and it does. But the static analyser can only tell you <em>that</em> a function has no human citation. It can't tell you <em>which</em> human source inspired it. That knowledge lives in your head, not in the code.</p>
<p><code>poc.py add</code> is where you supply that context. After the agent builds a file, you record the human sources you actually drew on: the GitHub discussion you read before prompting, the Stack Overflow answer that shaped the approach. Those records become the attribution chain <code>poc.py trace</code> surfaces — and what closes the gaps <code>poc.py verify</code> flags.</p>
<p><code>verify</code> finds the gaps. <code>add</code> fills them.</p>
<p><code>poc.py add</code> records attribution for a file interactively. You can run it on any AI-generated file in your project.</p>
<pre><code class="language-bash">python poc.py add src/utils/parser.py
</code></pre>
<p>You'll see a prompt:</p>
<pre><code class="language-plaintext">Recording provenance for: src/utils/parser.py
(Press Ctrl+C to cancel)

  Human source URL (or Enter to finish):
</code></pre>
<p>Enter the URL of the human-authored source that inspired the code. This could be a GitHub discussion, a Stack Overflow answer, a documentation page, a blog post, or an RFC.</p>
<pre><code class="language-plaintext">  Human source URL (or Enter to finish): https://github.com/TanStack/query/discussions/123
  Author handle: tannerlinsley
  Platform (github/stackoverflow/docs/other): github
  Source title: Cursor pagination discussion
  What specific insight came from this? cursor beats offset for live-updating datasets
  Confidence HIGH/MEDIUM/LOW [MEDIUM]: HIGH
  ✔ Recorded.
</code></pre>
<p>Add as many sources as apply. Press Enter on a blank URL when you're done.</p>
<pre><code class="language-plaintext">  Human source URL (or Enter to finish): 
✔ Provenance saved. Run: python poc.py trace src/utils/parser.py
</code></pre>
<h3 id="heading-check-what-you-recorded">Check What You Recorded</h3>
<pre><code class="language-bash">python poc.py trace src/utils/parser.py
</code></pre>
<pre><code class="language-plaintext">Provenance trace: src/utils/parser.py
────────────────────────────────────────────────────────────
  [HIGH]  @tannerlinsley on github
          Cursor pagination discussion
          https://github.com/TanStack/query/discussions/123
          Insight: cursor beats offset for live-updating datasets
</code></pre>
<p>No knowledge gaps — because you recorded a source. If the file had parts with no human source, they would appear below as gaps.</p>
<h3 id="heading-see-all-experts-in-your-graph">See All Experts in Your Graph</h3>
<p>Every <code>poc.py add</code> call stores not just the URL but the author — their handle, platform, and the specific insight they contributed. Run it across enough files, and those authors accumulate into a <strong>knowledge graph</strong>: a local record of which human experts your codebase drew from, which files their knowledge shaped, and how many artifacts trace back to their work.</p>
<p><code>poc.py experts</code> surfaces the top contributors. On a new project, it'll be one or two entries. On a mature codebase, it becomes a map of whose knowledge is load-bearing — the people you'd want to consult if that part of the code ever needed to change.</p>
<pre><code class="language-bash">python poc.py experts
</code></pre>
<pre><code class="language-plaintext">Top Human Experts in Knowledge Graph
──────────────────────────────────────────────────────
  @tannerlinsley            github          1 artifact(s)
</code></pre>
<h2 id="heading-how-to-use-import-spec-to-seed-knowledge-gaps">How to Use import-spec to Seed Knowledge Gaps</h2>
<p>This is the most important command in the tool. It connects proof-of-contribution with spec-writer and makes Knowledge Gaps deterministic.</p>
<p>When you use spec-writer before building a feature, it generates an <code>## Assumptions to review</code> section — every implicit decision is impact-rated HIGH, MEDIUM, or LOW. The <code>import-spec</code> command reads that section and seeds those assumptions into the database as unresolved gaps before the agent writes a line of code.</p>
<p>After the agent builds, any assumption that made it into the implementation without a cited human source surfaces automatically in <code>poc.py trace</code>. You don't need to know which parts of the code are uncertain. The spec already told you.</p>
<h3 id="heading-step-1-create-a-test-spec">Step 1 — Create a Test Spec</h3>
<p>If you don't have a spec-writer output yet, create one manually to see how the import works.</p>
<p><strong>Mac and Linux:</strong></p>
<pre><code class="language-bash">cat &gt; test-spec.md &lt;&lt; 'EOF'
## Assumptions to review

1. SQLite is sufficient for single-developer use — Impact: HIGH
   Correct this if: you need team-shared provenance

2. Filepath is the artifact identifier — Impact: MEDIUM
   Correct this if: you use content hashing instead

3. REST pattern for any future API — Impact: LOW
   Correct this if: you prefer GraphQL
EOF
</code></pre>
<p><strong>Windows PowerShell:</strong></p>
<pre><code class="language-powershell">python -c "
content = '''## Assumptions to review

1. SQLite is sufficient for single-developer use - Impact: HIGH
   Correct this if: you need team-shared provenance

2. Filepath is the artifact identifier - Impact: MEDIUM
   Correct this if: you use content hashing instead

3. REST pattern for any future API - Impact: LOW
   Correct this if: you prefer GraphQL'''
open('test-spec.md', 'w', encoding='utf-8').write(content)
print('test-spec.md created')
"
</code></pre>
<p><strong>Windows note:</strong> don't use PowerShell's <code>echo</code> to create spec files. PowerShell saves files as UTF-16, which causes a <code>UnicodeDecodeError</code> when <code>import-spec</code> reads them. The <code>python -c</code> approach above writes UTF-8 correctly.</p>
<h3 id="heading-step-2-import-the-assumptions">Step 2 — Import the Assumptions</h3>
<pre><code class="language-bash">python poc.py import-spec test-spec.md --artifact src/utils/parser.py
</code></pre>
<pre><code class="language-plaintext">Spec assumptions imported — 3 Knowledge Gap(s) seeded
───────────────────────────────────────────────────────
  1. [HIGH] SQLite is sufficient for single-developer use
       Correct if: you need team-shared provenance
  2. [MEDIUM] Filepath is the artifact identifier
       Correct if: you use content hashing instead
  3. [LOW] REST pattern for any future API
       Correct if: you prefer GraphQL

  →  Bound to: src/utils/parser.py
  After the agent builds, run:
  python poc.py trace src/utils/parser.py
  python poc.py add src/utils/parser.py
</code></pre>
<h3 id="heading-step-3-trace-the-gaps">Step 3 — Trace the Gaps</h3>
<pre><code class="language-bash">python poc.py trace src/utils/parser.py
</code></pre>
<pre><code class="language-plaintext">Knowledge gaps (AI-synthesized, no human source):
  • REST pattern for any future API [Correct if: you prefer GraphQL]
  • SQLite is sufficient for single-developer use [Correct if: you need team-shared provenance]
  • Filepath is the artifact identifier [Correct if: you use content hashing instead]

  Resolve gaps: python poc.py add src/utils/parser.py
</code></pre>
<p>Three gaps, colour-coded by urgency. The HIGH-impact assumption — SQLite for single-developer use — appears in red. The LOW-impact one appears in green. When you run <code>poc.py add</code> and record a human source with an insight that overlaps the gap text, the gap auto-closes.</p>
<h3 id="heading-preview-without-writing">Preview Without Writing</h3>
<pre><code class="language-bash">python poc.py import-spec test-spec.md --dry-run
</code></pre>
<p>This parses the spec and prints what would be seeded without touching the database. This is useful before committing to an import.</p>
<h3 id="heading-check-the-overall-health">Check the Overall Health</h3>
<pre><code class="language-bash">python poc.py report
</code></pre>
<pre><code class="language-plaintext">Proof of Contribution Report
────────────────────────────────────────
  Artifacts tracked    : 1
  With provenance      : 0  (0%)
  Unresolved gaps      : 3
  Resolved claims      : 0
  Human experts        : 1
  ⚠ Less than 50% of artifacts have provenance records.
  ⚠ 3 unresolved Knowledge Gap(s).
    Run `poc.py trace &lt;filepath&gt;` to locate them.
</code></pre>
<h2 id="heading-how-to-trace-human-attribution">How to Trace Human Attribution</h2>
<p><code>poc.py trace</code> is the command you'll use most. It shows the full human attribution chain for any file and lists any knowledge gaps — parts of the code with no traceable human source.</p>
<pre><code class="language-bash">python poc.py trace src/utils/parser.py
</code></pre>
<p>A file with both attribution and gaps looks like this:</p>
<pre><code class="language-plaintext">Provenance trace: src/utils/parser.py
────────────────────────────────────────────────────────────
  [HIGH]  @juliandeangelis on github
          Spec Driven Development methodology
          https://github.com/dannwaneri/spec-writer
          Insight: separate functional from technical spec

  [MEDIUM] @tannerlinsley on github
           Cursor pagination discussion
           https://github.com/TanStack/query/discussions/123
           Insight: cursor beats offset for live-updating datasets

Knowledge gaps (AI-synthesized, no human source):
  • Error retry strategy — no human source cited
  • CSV column ordering — AI chose this arbitrarily

  Resolve gaps: python poc.py add src/utils/parser.py
</code></pre>
<p>The human attribution section shows every cited source, colour-coded by confidence. The knowledge gaps section shows every assumption that shipped without a human citation — either seeded from a spec via <code>import-spec</code>, or flagged by Claude in the Provenance Block.</p>
<h3 id="heading-resolving-gaps">Resolving Gaps</h3>
<p>Run <code>poc.py add</code> on any file with open gaps:</p>
<pre><code class="language-bash">python poc.py add src/utils/parser.py
</code></pre>
<p>When you enter an insight that shares words with an open gap claim, the gap auto-closes. Run <code>poc.py trace</code> again to confirm it's resolved.</p>
<h2 id="heading-how-to-verify-with-static-analysis">How to Verify with Static Analysis</h2>
<p><code>poc.py verify</code> is the command that closes the epistemic trust gap completely. It detects Knowledge Gaps by analysing the file's actual code structure — not by asking the AI what it doesn't know.</p>
<p>Run it after the agent builds, once you've seeded gaps with <code>import-spec</code>:</p>
<pre><code class="language-bash">python poc.py verify src/utils/parser.py
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Verify: src/utils/parser.py
────────────────────────────────────────────────────────────
  Structural units detected : 11
  Seeded claims             : 3
  Covered by cited source   : 2
  Deterministic gaps        : 1

Deterministic Knowledge Gaps (no human source):
  • function: handle_concurrent_writes (lines 47–61)
      Seeded assumption: concurrent write handling — AI chose this arbitrarily

  Resolve: python poc.py add src/utils/parser.py
</code></pre>
<p>The gap shown is not something Claude admitted. It's something the analyser found by comparing the file's function list against your seeded claims. The function <code>handle_concurrent_writes</code> exists in the code but has no resolved human citation in the database. That's the gap.</p>
<h3 id="heading-what-the-exit-codes-mean">What the Exit Codes Mean</h3>
<pre><code class="language-bash">python poc.py verify src/utils/parser.py
echo $?   # Mac/Linux

python poc.py verify src/utils/parser.py
echo $LASTEXITCODE   # Windows PowerShell
</code></pre>
<ul>
<li><p><strong>Exit code 0</strong> — no gaps, all detected units have cited sources</p>
</li>
<li><p><strong>Exit code 1</strong> — gaps found, resolve with <code>poc.py add</code></p>
</li>
<li><p><strong>Exit code 2</strong> — file not found or unsupported language</p>
</li>
</ul>
<p>Exit code 1 integrates directly into CI pipelines. Add <code>poc.py verify</code> to your GitHub Action or pre-commit hook and gaps block the build before they reach production.</p>
<h3 id="heading-run-it-without-a-seeded-spec">Run it Without a Seeded Spec</h3>
<p>If you haven't run <code>import-spec</code> first, <code>verify</code> still works — it falls back to structural analysis and surfaces every uncited function and branch as a gap:</p>
<pre><code class="language-bash">python poc.py verify src/utils/parser.py
</code></pre>
<pre><code class="language-plaintext">⚠ No spec imported — showing all uncited structural units.
  Run: python poc.py import-spec spec.md --artifact src/utils/parser.py
  for deterministic gap detection.

Deterministic Knowledge Gaps (no human source):
  • function: parse_query (lines 1–7)
  • branch: if not text (lines 2–3)
  • function: fetch_results (lines 9–12)
  ...
</code></pre>
<p>It's less precise than the spec-writer path — every structural unit shows rather than only the ones tied to named assumptions — but it's useful as a baseline on any file, new or old.</p>
<h3 id="heading-the-strict-flag">The <code>--strict</code> Flag</h3>
<pre><code class="language-bash">python poc.py verify src/utils/parser.py --strict
</code></pre>
<p>Strict mode flags every uncited structural unit as a gap even when claims are seeded. You can use it when you want zero tolerance: any function or branch without a resolved human source fails the check.</p>
<h2 id="heading-how-to-enable-pr-enforcement">How to Enable PR Enforcement</h2>
<p>Once <code>poc.py trace</code> has saved you real hours — not before — enable the GitHub Action. The distinction matters. Turning it on day one frames the tool as overhead. Turning it on after the team already finds value frames it as a standard.</p>
<pre><code class="language-bash">git add .github/ .poc/config.json poc.py
git commit -m "chore: add proof-of-contribution"
git push
</code></pre>
<p>After that, every PR is checked for an <code>## 🤖 AI Provenance</code> section. The scaffold already created the PR template with that section included. Developers fill it in naturally once they're already running <code>poc.py trace</code> locally — the template just asks them to record what they already know.</p>
<p>Developers who write fully human code opt out by adding <code>100% human-written</code> anywhere in the PR body. The action skips the check automatically.</p>
<h3 id="heading-what-the-action-checks">What the Action Checks</h3>
<p>The action reads the PR description and looks for:</p>
<ol>
<li><p>The <code>## 🤖 AI Provenance</code> heading</p>
</li>
<li><p>At least one populated row in the attribution table</p>
</li>
</ol>
<p>If the section is missing or the table is empty, the action fails and posts a comment explaining what to add. The comment includes a link to <code>poc.py trace &lt;filepath&gt;</code> so the developer knows exactly where to look.</p>
<h2 id="heading-where-to-go-next">Where to Go Next</h2>
<h3 id="heading-use-it-with-spec-writer-on-a-real-feature">Use it with spec-writer on a Real Feature</h3>
<p>The real value of <code>import-spec</code> is on actual features, not test specs. If you use <a href="https://github.com/dannwaneri/spec-writer">spec-writer</a>, the workflow is:</p>
<pre><code class="language-plaintext">/spec-writer "your feature description"
</code></pre>
<p>Save the output to <code>spec.md</code>. Then:</p>
<pre><code class="language-bash">python poc.py import-spec spec.md --artifact src/path/to/output.py
</code></pre>
<p>Build the feature with your agent. Then run <code>poc.py trace</code> to see which assumptions made it into code with no human source. Resolve the HIGH-impact gaps first — those are the ones that will cause production incidents.</p>
<h3 id="heading-activate-the-claude-code-skill">Activate the Claude Code Skill</h3>
<p>The SKILL.md file makes Claude automatically append a Provenance Block to every generated artifact when the skill is active. The block lists human sources Claude drew from and flags what it synthesized without any traceable source.</p>
<p>To activate it in Claude Code, the skill is already installed at <code>~/.claude/skills/proof-of-contribution/</code>. Claude Code loads it automatically when you are in a project that has <code>.poc/config.json</code>.</p>
<p>A generated Provenance Block looks like this:</p>
<pre><code class="language-plaintext">## PROOF OF CONTRIBUTION
Generated artifact: fetch_github_discussions()
Confidence: MEDIUM

## HUMAN SOURCES THAT INSPIRED THIS

[1] GitHub GraphQL API Documentation Team
    Source type: Official Docs
    URL: docs.github.com/en/graphql
    Contribution: cursor-based pagination pattern

[2] GitHub Community (multiple contributors)
    Source type: GitHub Discussions
    URL: github.com/community/community
    Contribution: "ghost" fallback for deleted accounts
                  surfaced in bug reports

## KNOWLEDGE GAPS (AI synthesized, no human cited)
- Error handling / retry logic
- Rate limit strategy

## RECOMMENDED HUMAN EXPERTS TO CONSULT
- github.com/octokit community for pagination
</code></pre>
<p>The Knowledge Gaps section is the part no other tool produces. It's where AI admits what it synthesized without a traceable human source — before that gap becomes a production incident.</p>
<h3 id="heading-upgrade-when-you-outgrow-sqlite">Upgrade When You Outgrow SQLite</h3>
<p>The default database is SQLite — local only, no infra required. When you need team sharing or graph queries, the <code>references/</code> directory in the repo has migration guides:</p>
<table>
<thead>
<tr>
<th>Need</th>
<th>File</th>
</tr>
</thead>
<tbody><tr>
<td>Team sharing a provenance DB</td>
<td><code>references/relational-schema.md</code></td>
</tr>
<tr>
<td>Graph traversal queries</td>
<td><code>references/neo4j-implementation.md</code></td>
</tr>
<tr>
<td>Semantic web / interoperability</td>
<td><code>references/jsonld-schema.md</code></td>
</tr>
</tbody></table>
<h2 id="heading-manual-tracking-vs-proof-of-contribution">Manual Tracking vs. proof-of-contribution</h2>
<table>
<thead>
<tr>
<th></th>
<th>Manual tracking</th>
<th>proof-of-contribution</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Finding who wrote the code</strong></td>
<td>Search Slack, ask the team, dig through commits</td>
<td><code>poc.py trace &lt;file&gt;</code> — thirty seconds</td>
</tr>
<tr>
<td><strong>Knowing which parts the AI guessed</strong></td>
<td>You don't, until it breaks in production</td>
<td>Knowledge Gaps section — surfaced before the code ships</td>
</tr>
<tr>
<td><strong>Detecting gaps after the build</strong></td>
<td>Code review, if someone notices</td>
<td><code>poc.py verify</code> — static analysis, zero API calls</td>
</tr>
<tr>
<td><strong>Enforcing attribution on PRs</strong></td>
<td>Honor system</td>
<td>GitHub Action fails the PR if attribution is missing</td>
</tr>
<tr>
<td><strong>Connecting to your spec</strong></td>
<td>Copy-paste assumptions into comments manually</td>
<td><code>poc.py import-spec</code> seeds them as tracked claims automatically</td>
</tr>
<tr>
<td><strong>Infrastructure required</strong></td>
<td>None (usually a spreadsheet or nothing)</td>
<td>None — SQLite, pure Python, no paid services</td>
</tr>
</tbody></table>
<p>The tool doesn't replace code review. It gives code review the context it needs to catch the right things.</p>
<p>The archaeology scenario — two days tracing a bug through dead-end commit messages — takes thirty seconds with <code>poc.py trace</code>. The code still has gaps, and it always will. But now you know where they are.</p>
<p><em>Built by</em> <a href="https://dev.to/dannwaneri"><em>Daniel Nwaneri</em></a><em>. The spec-writer skill that feeds</em> <code>import-spec</code> <em>is at</em> <a href="https://github.com/dannwaneri/spec-writer"><em>github.com/dannwaneri/spec-writer</em></a><em>. The full proof-of-contribution repo is at</em> <a href="https://github.com/dannwaneri/proof-of-contribution"><em>github.com/dannwaneri/proof-of-contribution</em></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Your Own Claude Code Skill ]]>
                </title>
                <description>
                    <![CDATA[ Every developer eventually has a workflow they repeat. A way they write commit messages. A checklist they run before opening a pull request. A structure they follow when reviewing code. They do it man ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-your-own-claude-code-skill/</link>
                <guid isPermaLink="false">69c6ecde7cf27065104cd8a1</guid>
                
                    <category>
                        <![CDATA[ claude.ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Daniel Nwaneri ]]>
                </dc:creator>
                <pubDate>Fri, 27 Mar 2026 20:47:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c0947834-4b11-46c6-ab61-994667e70a7e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every developer eventually has a workflow they repeat. A way they write commit messages. A checklist they run before opening a pull request. A structure they follow when reviewing code. They do it manually, explain it to their agents in every session, and watch the agent interpret it differently each time.</p>
<p>Agent skills fix this. A skill is a markdown file that loads into Claude Code's context automatically when you need it. You write the workflow once. The agent follows it every time. And because skills follow an open standard, the same file works in Claude Code, GitHub Copilot, Cursor, and Gemini CLI.</p>
<p>This tutorial shows you how to build a skill from scratch. You will build a commit-message-writer — a skill that reads your staged changes and generates a structured commit message following the Conventional Commits standard. By the end, you will have a working skill installed and ready to use, and you will understand the structure well enough to build any skill you need.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-what-an-agent-skill-is">What an Agent Skill Is</a></p>
</li>
<li><p><a href="#heading-how-to-choose-what-to-build">How to Choose What to Build</a></p>
</li>
<li><p><a href="#heading-how-to-structure-your-skill">How to Structure Your Skill</a></p>
</li>
<li><p><a href="#heading-how-to-write-the-description">How to Write the Description</a></p>
</li>
<li><p><a href="#heading-how-to-write-the-instructions">How to Write the Instructions</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-commit-message-writer-skill">How to Build the commit-message-writer Skill</a></p>
</li>
<li><p><a href="#heading-how-to-install-and-test-your-skill">How to Install and Test Your Skill</a></p>
</li>
<li><p><a href="#heading-how-to-improve-your-skill-over-time">How to Improve Your Skill Over Time</a></p>
</li>
<li><p><a href="#heading-where-to-go-next">Where to Go Next</a></p>
</li>
</ol>
<h2 id="heading-what-an-agent-skill-is">What an Agent Skill Is</h2>
<p>A skill is a folder containing a <code>SKILL.md</code> file. That file has two parts: a YAML frontmatter block at the top, and a markdown body below it.</p>
<pre><code class="language-plaintext">my-skill/
└── SKILL.md
</code></pre>
<p>The frontmatter tells the agent what the skill is called and when to use it. The body tells the agent what to do when it loads the skill. Here is the minimal structure:</p>
<pre><code class="language-yaml">---
name: my-skill
description: What this skill does and when to use it.
---
 
# My Skill
 
Instructions for the agent go here.
</code></pre>
<p>When you invoke a skill — either explicitly with <code>/skill-name</code> or by describing what you want — the agent reads the SKILL.md body and follows the instructions inside it. The frontmatter never reaches the agent's instructions. It's metadata the skill system uses to decide whether to load the skill at all.</p>
<h3 id="heading-how-the-agent-decides-to-load-a-skill">How the Agent Decides to Load a Skill</h3>
<p>This is the most important thing to understand before you write your first skill: <strong>the agent decides whether to load your skill based entirely on the description field.</strong></p>
<p>Skills appear in Claude Code's context as a list of names and descriptions. When you make a request, the agent scans that list and loads any skill whose description matches what you're asking for. If the description is vague, the skill won't load when you need it. If the description is too narrow, it won't load for variations of the same request.</p>
<p>The instructions in the body only matter after the skill loads. Getting the description right is what determines whether the skill loads at all.</p>
<h3 id="heading-what-skills-are-not">What Skills Are Not</h3>
<p>Skills are instruction files. They cannot run code on their own — but they can instruct the agent to run code using its existing tools. They are not plugins, extensions, or packages. They have no runtime. They are markdown files the agent reads, like a recipe a chef follows.</p>
<h2 id="heading-how-to-choose-what-to-build">How to Choose What to Build</h2>
<p>The best skills share three properties.</p>
<ol>
<li><p><strong>They encode a repeatable workflow.</strong> If you do something differently every time, a skill won't help. If you follow the same steps every session — even if you explain them differently each time — that's a skill candidate.</p>
</li>
<li><p><strong>They have a clear trigger.</strong> You should be able to finish the sentence "I need this skill when I want to...". If you can't finish that sentence in one clause, the workflow isn't scoped enough for a skill.</p>
</li>
<li><p><strong>They produce a consistent output format.</strong> Skills that output in a fixed structure — a commit message, a code review, a spec — are easier to build and test than skills that produce open-ended prose.</p>
</li>
</ol>
<p>Good candidates: commit messages, pull request descriptions, code reviews, changelog entries. Bad candidates: "help me think through this", "make this better" — too open-ended to encode in a skill.</p>
<p>For this tutorial, commit message generation is the right scope. The trigger is obvious (you want to commit), the workflow is defined (read staged changes, apply Conventional Commits format), and the output is structured (a commit message with a specific shape).</p>
<h2 id="heading-how-to-structure-your-skill">How to Structure Your Skill</h2>
<p>Every skill starts as a single folder with a single file:</p>
<pre><code class="language-plaintext">commit-message-writer/
└── SKILL.md
</code></pre>
<p>As skills grow, they can include additional files the agent loads as needed:</p>
<pre><code class="language-plaintext">commit-message-writer/
├── SKILL.md          ← always loaded when skill triggers
└── references/
    └── examples.md   ← loaded only when the agent needs examples
</code></pre>
<p>The SKILL.md body should stay under 500 lines. If your instructions are growing beyond that, move supporting detail into a <code>references/</code> subfolder and tell the agent when to read those files. This keeps the skill lean — the agent only loads what it needs.</p>
<p>For this tutorial, a single SKILL.md is enough.</p>
<h2 id="heading-how-to-write-the-description">How to Write the Description</h2>
<p>The description field is the trigger condition. It determines when your skill loads and when it doesn't. Most skills fail not because the instructions are wrong, but because the description doesn't match how people actually ask for help.</p>
<p>Here is a weak description:</p>
<pre><code class="language-yaml">description: Generates commit messages.
</code></pre>
<p>This will undertrigger. "Generate a commit message" will load it. "Write a commit for my changes" probably won't. "Summarize my staged diff" definitely won't — even though all three are asking for the same thing.</p>
<p>Here is a stronger description:</p>
<pre><code class="language-yaml">description: Generates structured commit messages following the Conventional Commits standard. Use when you want to commit your changes and need a well-formatted message. Triggers on "write a commit message", "commit my changes", "summarize my staged diff", "what should my commit say", or any request to describe or document code changes for version control.
</code></pre>
<p>The pattern is: <strong>what the skill does + when to use it + specific trigger phrases</strong>. The trigger phrases cover the different ways a developer might ask for the same thing.</p>
<p>Two rules for descriptions:</p>
<p><strong>Be specific about the output.</strong> "Generates commit messages" is vague. "Generates structured commit messages following the Conventional Commits standard" tells the agent and the user exactly what they'll get.</p>
<p><strong>Be slightly pushy.</strong> The agent has a natural tendency to undertrigger skills — to handle requests itself rather than loading a skill. A description that explicitly lists trigger phrases counteracts this. You are not being redundant. You are training the trigger.</p>
<h2 id="heading-how-to-write-the-instructions">How to Write the Instructions</h2>
<p>The body of SKILL.md is where you define what the agent does when the skill loads. Good instructions follow two principles.</p>
<p><strong>Generate first, clarify second.</strong> The agent should produce output immediately rather than asking clarifying questions. If it needs to make assumptions, it should make them and flag them — not ask. Asking questions before producing output adds friction and loses the benefit of having a skill at all.</p>
<p><strong>Define the output format explicitly.</strong> Don't say "write a good commit message." Say exactly what the structure is, what fields are required, what the character limits are. The more specific the output format, the more consistent the results.</p>
<p>Here is what weak instructions look like:</p>
<pre><code class="language-markdown"># Commit Message Writer
 
Look at the staged changes and write a commit message that describes what changed.
</code></pre>
<p>That will produce different results every time — different formats, different lengths, different conventions. It's not a skill. It's a prompt.</p>
<p>Here is what strong instructions look like:</p>
<pre><code class="language-markdown"># Commit Message Writer
 
Read the staged diff using `git diff --staged`. Generate a commit message
following the Conventional Commits standard.
 
Output format:
type(scope): short description under 72 characters
 
Body (if changes are non-trivial):
- What changed and why, not how
- One bullet per logical change
 
Footer (if applicable):
BREAKING CHANGE: description
Closes #issue-number
</code></pre>
<p>The agent knows exactly what to produce. The output will be consistent across sessions, across projects, and across agents that support the standard.</p>
<h2 id="heading-how-to-build-the-commit-message-writer-skill">How to Build the <code>commit-message-writer</code> Skill</h2>
<p>Now build it. Create the skill directory:</p>
<pre><code class="language-bash">mkdir -p ~/.claude/skills/commit-message-writer
</code></pre>
<p>On Windows PowerShell:</p>
<p><strong>Note:</strong> PowerShell uses backtick (<code>`</code>) for line continuation, not backslash.</p>
<pre><code class="language-powershell">New-Item -ItemType Directory -Force -Path "$HOME\.claude\skills\commit-message-writer"
</code></pre>
<p>Create the SKILL.md file inside that directory. Here is the complete content:</p>
<pre><code class="language-markdown">---
name: commit-message-writer
description: Generates structured commit messages following the Conventional Commits
  standard. Use when you want to commit your changes and need a well-formatted message.
  Triggers on "write a commit message", "commit my changes", "summarize my staged
  diff", "what should my commit say", or any request to describe or document staged
  changes for version control.
---
 
# commit-message-writer
 
You generate structured commit messages from staged git changes.
 
## How to invoke
 
Run `git diff --staged` to read the staged changes. If nothing is staged, tell the
user and suggest they run `git add` first.
 
Generate first. Do not ask clarifying questions before producing the commit message.
If you need to make assumptions about scope or type, make them and note them after
the output.
 
## Output format
 
~~~
type(scope): short description
 
[body — optional, include if changes are non-trivial]
 
[footer — optional]
~~~
 
**Type** — choose one:
- `feat` — a new feature
- `fix` — a bug fix
- `docs` — documentation changes only
- `refactor` — code change that neither fixes a bug nor adds a feature
- `test` — adding or updating tests
- `chore` — build process, tooling, or dependency updates
 
**Scope** — the module, file, or area affected. Use the directory name or component
name. Omit if the change spans the entire codebase.
 
**Short description** — imperative mood, under 72 characters, no period at the end.
"Add user authentication" not "Added user authentication" or "Adds user authentication."
 
**Body** — what changed and why, not how. One bullet per logical change. Skip if the
short description is self-explanatory.
 
**Footer** — include `BREAKING CHANGE:` if the commit breaks backward compatibility.
Include `Closes #N` if it resolves a GitHub issue.
 
## Quality rules
 
- Never use "updated", "changed", or "modified" in the short description — be specific
- Never write "various improvements" or "misc fixes" — name what improved
- If more than three files changed across unrelated concerns, flag it:
  "These changes may be better split into separate commits: [list concerns]"
- The short description must be under 72 characters — count before outputting
 
## Example output
 
Input: staged changes adding a rate limiter to an API endpoint
 
~~~
feat(api): add rate limiting to /query endpoint
 
- Limits requests to 100 per minute per IP using Cloudflare's rate limit binding
- Returns 429 with Retry-After header when limit is exceeded
- Adds rate limit configuration to wrangler.toml
 
Closes #47
~~~
</code></pre>
<p>Save that file. The skill is built.</p>
<h2 id="heading-how-to-install-and-test-your-skill">How to Install and Test Your Skill</h2>
<h3 id="heading-verify-the-file-exists">Verify the File Exists</h3>
<pre><code class="language-bash">cat ~/.claude/skills/commit-message-writer/SKILL.md
</code></pre>
<p>You should see the full SKILL.md content. If you get an error, check the directory path.</p>
<h3 id="heading-test-the-skill">Test the Skill</h3>
<p>Open Claude Code in any git repository that has staged changes. Type:</p>
<pre><code class="language-plaintext">/commit-message-writer
</code></pre>
<p>The agent will read your staged diff and produce a commit message following the format you defined.</p>
<p>You can also trigger it naturally:</p>
<pre><code class="language-plaintext">write a commit message for my staged changes
</code></pre>
<pre><code class="language-plaintext">what should my commit say
</code></pre>
<pre><code class="language-plaintext">summarize my diff for git
</code></pre>
<p>All three should load the skill and produce a structured commit message. If the skill doesn't trigger on natural language requests, the description needs more trigger phrases — see the improvement section below.</p>
<h3 id="heading-test-edge-cases">Test Edge Cases</h3>
<p>Test these cases before relying on the skill in production:</p>
<pre><code class="language-bash"># Stage nothing, then ask for a commit message
git add -p  # stage nothing
# In Claude Code: "write a commit message"
# Expected: skill tells you nothing is staged and suggests git add
</code></pre>
<pre><code class="language-bash"># Stage changes across unrelated files
git add src/api.ts src/styles.css README.md
# In Claude Code: "write a commit message"  
# Expected: skill flags that commits may be better split
</code></pre>
<h2 id="heading-how-to-improve-your-skill-over-time">How to Improve Your Skill Over Time</h2>
<p>The first version of any skill is a draft. You improve it by observing where it produces inconsistent or wrong output, then updating the instructions.</p>
<h3 id="heading-when-the-skill-undertriggers">When the Skill Undertriggers</h3>
<p>If you type "summarize my changes for git" and the skill doesn't load, add that phrase to the description's trigger list:</p>
<pre><code class="language-yaml">description: ... Triggers on "write a commit message", "commit my changes",
  "summarize my staged diff", "summarize my changes for git", ...
</code></pre>
<p>The description is your primary lever for fixing triggering problems.</p>
<h3 id="heading-when-the-output-format-drifts">When the Output Format Drifts</h3>
<p>If the agent starts producing commit messages that don't match your format — wrong type, missing scope, body in the wrong style — the instructions need to be more explicit. Add a concrete example that shows the failure and the correct output:</p>
<pre><code class="language-markdown">## Common mistakes to avoid
 
Wrong: "Updated the authentication flow"
Right: "refactor(auth): simplify token validation logic"
 
Wrong: "Fixed bugs"
Right: "fix(api): handle null response from upstream service"
</code></pre>
<p>Concrete counterexamples are more effective than abstract rules.</p>
<h3 id="heading-when-the-scope-grows">When the Scope Grows</h3>
<p>If you find yourself wanting the skill to handle related tasks — reviewing commit messages, generating changelogs, writing PR descriptions — resist the urge to add everything to one skill. Build separate skills. Each skill should do one thing well. The Agent Skills standard is designed for composition, not for monolithic instructions.</p>
<h2 id="heading-where-to-go-next">Where to Go Next</h2>
<p>The commit-message-writer covers the core pattern. The same structure works for any repeatable workflow.</p>
<p><strong>Pull request descriptions</strong> follow the same shape — read the diff, apply a structure, produce consistent output. The trigger phrases are different ("write a PR description", "summarize my branch for review") and the output format adds sections for motivation and testing, but the SKILL.md structure is identical.</p>
<p><strong>Code review checklists</strong> work well as skills when your team has a standard review process. The trigger is "review this code" or "check this PR", and the instructions encode whatever your team actually checks — security concerns, test coverage, naming conventions.</p>
<p>The commit-message-writer is the simplest skill architecture — instructions only. As your skills grow more specialized, two other patterns become useful.</p>
<p>The first adds a <code>references/</code> directory: the voice-humanizer skill loads a CORPUS.md file containing the author's published writing, which the agent reads when it needs to check output against a specific style. The second adds quality rules and structured output formats that make results stricter and more consistent — that's the pattern spec-writer uses to surface assumptions inline. Each is the same SKILL.md structure at a different level of complexity.</p>
<p>Start with instructions only. Add references when the agent needs external context. Add output format rules when consistency matters more than flexibility.</p>
<p>The Agent Skills standard is supported in Claude Code, GitHub Copilot in VS Code, Cursor, and Gemini CLI. A skill you build once installs across all of them. The install path differs by agent:</p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>Skills directory</th>
</tr>
</thead>
<tbody><tr>
<td>Claude Code</td>
<td><code>~/.claude/skills/</code></td>
</tr>
<tr>
<td>GitHub Copilot</td>
<td><code>~/.copilot/skills/</code> or <code>.github/skills/</code></td>
</tr>
<tr>
<td>Cursor</td>
<td><code>~/.cursor/skills/</code></td>
</tr>
<tr>
<td>Gemini CLI</td>
<td><code>~/.gemini/skills/</code></td>
</tr>
</tbody></table>
<p>The SKILL.md format is the same across all of them.</p>
<p>The commit-message-writer you just built is a working skill. The next one will take less time. By the third, you will start seeing workflows you repeat and immediately think: that should be a skill.</p>
<p>That's the point.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Override API Responses and Headers in Chrome DevTools: A Step-by-Step Guide ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever faced a situation as a frontend developer where you needed to show a demo to your product manager, and something was broken in the API response? Or, a production bug where you were block ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-override-api-responses-and-headers-in-chrome-devtools/</link>
                <guid isPermaLink="false">69c5a33110e664c5da34c6d3</guid>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tapas Adhikary ]]>
                </dc:creator>
                <pubDate>Thu, 26 Mar 2026 21:20:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a2fee8e9-cb71-4065-af8e-ef0357e6ca2c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever faced a situation as a frontend developer where you needed to show a demo to your product manager, and something was broken in the API response? Or, a production bug where you were blocked by waiting on your backend team to provide you with a fix to implement further on the frontend?</p>
<p>To make things even worse, how about a CORS error that completely prevented you from showing the page?</p>
<p>It happens, right? Going back to the backend team and getting a quick fix for these issues would be ideal, but may not be realistic in most cases. It depends on the availability of the backend developers, the priority items they're working on, communication protocols between teams, and even interpersonal relationships.</p>
<p>Now, the question is, can you afford to wait? Wait for things to work in your favour within the given deadline so that you can show that demo or deliver the work to your customer? The answer is NO. You may not have that luxury.</p>
<p>Today, in this article, you'll learn a couple of mind-blowing tips and tricks that will save you from these situations. You'll understand how to set up your Chrome browser so that you can continue with your frontend development even when the backend API returns an incorrect response or a CORS error.</p>
<p>This is a step-by-step guide that'll help make you comfortable with all the required configurations and get you set up to use it on your web projects. This guide is also available as a video tutorial as part of the <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwT8Msc4H3_CP5Tf8MqqqVZ">Thinking in Debugging</a> series. You can check it out if you’d like:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/ndrPcDNmwFk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>Let's get started with the problems and their solutions.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-problem-1-backend-response-is-wrong">Problem 1: The Backend Response Is Wrong</a></p>
</li>
<li><p><a href="#heading-problem-2-validating-a-ui-scenario-without-backend-changes">Problem 2: Validating a UI Scenario Without Backend Changes</a></p>
</li>
<li><p><a href="#heading-problem-3-handling-cors-errors">Problem 3: Handling CORS Errors</a></p>
</li>
<li><p><a href="#heading-additional-tips">Additional Tips</a></p>
<ul>
<li><p><a href="#heading-applying-overrides-globally">Applying Overrides Globally</a></p>
</li>
<li><p><a href="#heading-disabling-or-removing-overrides">Disabling or Removing Overrides</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-learn-more-from-the-thinking-in-debugging-mindset">Learn More From the Thinking in Debugging Mindset</a></p>
</li>
<li><p><a href="#heading-before-we-end">Before We End…</a></p>
</li>
</ol>
<h2 id="heading-problem-1-the-backend-response-is-wrong">Problem 1: The Backend Response Is Wrong</h2>
<p>Here is a classic case of a wrong API response, but you still need to continue with your frontend work.</p>
<p>Take a look at the image below. Do you see that something is off? Yeah, on the first card, the spelling of <code>Banana</code> seems to be misspelled as <code>Banananana</code>. This user interface is constructed using the data we have received as an API response.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/2bef7ac2-98c0-419a-b783-ee64900f8121.png" alt="Incorrect spelling of banana on first card" style="display:block;margin:0 auto" width="2076" height="1546" loading="lazy">

<p>We can go to the backend team and request that they fix it as soon as possible. But it may not happen until the next sprint starts, which might be 15 days from now.</p>
<p>So, what can we do to continue with our work and all the validations on the frontend side? We can use the <code>Content Overriding</code> feature of the Chrome browser to mitigate this situation.</p>
<h3 id="heading-how-to-use-content-overriding">How to Use <code>Content Overriding</code></h3>
<p>First, open up the DevTools of your Chrome browser by pressing the F12 (on Mac, Cmd+F12) key. Then move to the network tab and inspect the API request that returns the incorrect response.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/f9689133-c7cd-400e-b0cc-5275fe1ca7ec.png" alt="Network Request" style="display:block;margin:0 auto" width="1870" height="808" loading="lazy">

<p>Next, right-click on the API request and select the <code>Override content</code> option from the context menu.</p>
<p>You may wonder what content means here, and what I am overriding? You're overriding the API response so that it can reflect on the UI locally.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/31d4e340-d9fa-4dec-81cc-08fbf644aa58.png" alt="Override Content Option" style="display:block;margin:0 auto" width="814" height="1090" loading="lazy">

<p>This will bring up a UI at the top where you can select a folder to store override files. It's important to understand that all the content overrides are locally stored on your machine's hard disk. This means you can use these persisted overrides again and again until someone fixes the issue permanently at the backend.</p>
<p>Now click on the <code>Select folder</code> button.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/7d238b9d-146c-4592-9008-26c7c189a0de.png" alt="Select a Folder" style="display:block;margin:0 auto" width="1890" height="1530" loading="lazy">

<p>This will open up the folder explorer for you. Create a new folder and select it, or select an existing folder where you want to save the overrides. In my case, I've named the folder as <code>debug_devtools</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/7bc13c7b-0ffa-442a-899e-98a805fcddac.png" alt="Select a Folder" style="display:block;margin:0 auto" width="1564" height="1062" loading="lazy">

<p>Now, Chrome DevTools will ask for confirmation that you're allowing DevTools to edit files on your local system. Just click on the <code>Allow</code> button.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/07ff30f6-8bef-40c3-aee9-bcd55545c67f.png" alt="Allow" style="display:block;margin:0 auto" width="1504" height="682" loading="lazy">

<p>That's all for the setup. Now, you'll find the same response in the editable mode under the <code>Sources</code> tab of DevTools. Let's take a deeper look at the image below:</p>
<ol>
<li><p>The Local overrides are listed under the <code>Sources &gt; Oveerides</code> tab of the DevTools.</p>
</li>
<li><p>On the left side, the <code>Enable Local Overrides</code> checkbox is selected, and the overrides are listed below. You can find the same folder you created before, and under that, you'll see another folder called <code>localhost:3001</code> and a file called <code>edibles</code> under it. The localhost:3001 folder name is related to the API endpoint namespace we're connecting to. The edibles file name under it goes with the request name.</p>
</li>
<li><p>On the right side, you can see the content (that is, the response to the edibles request) in editable mode.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/fc918250-c1ab-4b56-8505-3032e698b8cc.png" alt="fc918250-c1ab-4b56-8505-3032e698b8cc" style="display:block;margin:0 auto" width="1818" height="1410" loading="lazy">

<p>You can even cross-check now by traversing to the file system's <code>debug_devtools</code> folder. You should find the same folders and files as you saw in the DevTools.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/81fd4e82-ae7d-4d07-88d8-5a757edbc3ef.png" alt="Response in folder" style="display:block;margin:0 auto" width="1460" height="662" loading="lazy">

<p>You can open up the <code>edibles</code> file. The file content should match exactly the response you saw before.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/4ba75464-0a29-4f11-9bc6-be0463e10dd5.png" alt="edibles content" style="display:block;margin:0 auto" width="1686" height="1148" loading="lazy">

<p>Now, it's time to override. Coming to the Sources tab's editable response panel, you can fix the spelling. Save your changes using Ctrl + S (or Cmd + S).</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/c33701f0-ddb1-478c-8fe6-d68ddf8dd638.png" alt="edit text" style="display:block;margin:0 auto" width="1634" height="700" loading="lazy">

<p>Now, hard refresh your browser. You should be able to see your change reflected on the UI.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/1414be9d-adf4-41a6-8cbc-d115a49bb8a6.png" alt="Banana Fixed" style="display:block;margin:0 auto" width="2982" height="1590" loading="lazy">

<p>Awesome!! You can now share this overridden response (the <code>edibles</code> file) with other developers to point to from their Chrome DevTools to get the same local fix until the backend fixes it.</p>
<h2 id="heading-problem-2-validating-a-ui-scenario-without-backend-changes">Problem 2: Validating a UI Scenario Without Backend Changes</h2>
<p>Imagine you need to validate that certain items are low in stock on an item listing page. If the stock quantity of an item hits 50 or below, you want to show a <code>Low Stock</code> label for that item.</p>
<p>Now, what if the API response doesn't return a quantity of 50 or below? Content overriding can come to the rescue once again!</p>
<p>You can edit the response to set the quantity value to 50 or below and follow the same process as before to reflect the change on the UI. Look at the image below:</p>
<ol>
<li><p>We have edited the quantity on the right-side panel.</p>
</li>
<li><p>Once saved and refreshed, we not only see the updated count on the UI, but it also runs the underlying JavaScript logic to show the <code>Low Stock</code> label automatically. This is a superpower.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/77b8578e-ecc9-4397-b980-e24cc0ab099e.png" alt="Stock" style="display:block;margin:0 auto" width="2660" height="684" loading="lazy">

<h2 id="heading-problem-3-handling-cors-errors">Problem 3: Handling CORS Errors</h2>
<p><a href="https://www.youtube.com/shorts/lPiQClBVYY4">Cross-Origin Resource Sharing (CORS)</a> is a browser security feature that allows a web server to explicitly grant requests coming from a domain other than its own. By default, browsers don't allow these cross-origin requests and follow a strict rule called <code>Same Origin Resource Sharing</code>.</p>
<p>In many cases, your API server and the web server could be hosted on different domains. In those cases, when the web application attempts to access an API, it faces the CORS error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/f739eb08-6bb1-4534-9080-0417d511396c.png" alt="CORS Error" style="display:block;margin:0 auto" width="2982" height="742" loading="lazy">

<p>On the server side, you need to have explicit configurations to allow cross-origin requests. For example, you need to add the following response headers:</p>
<pre><code class="language-shell">Access-Control-Allow-Origin: http://localhost:5174
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: *
</code></pre>
<p>So, again, it may not be guaranteed that your CORS error will be resolved at the server side as soon as you want. But you cann't afford to get blocked due to it. So, what's the way around? Yes! The overriding, but this time, overriding the response header.</p>
<p>Go to the network tab of the Chrome DevTools and right-click on the request that has the CORS error. Now, select the <code>Override headers</code> option from the context menu.</p>
<p>By the way, have you noticed that the <code>Override content</code> option is disabled here? This is because we don't have any response as content from this request, as it got an error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/4d81fbb1-e960-48f2-bdd7-597e686faff6.png" alt="4d81fbb1-e960-48f2-bdd7-597e686faff6" style="display:block;margin:0 auto" width="1280" height="966" loading="lazy">

<p>Clicking on the <code>Overriding headers</code> will take you to the <code>Headers</code> tab where you can find the option to add additional headers to the response headers. Click on the <code>+ Add header</code> button to add the CORS-related headers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/9acdbde3-0b12-4cef-9437-844ca92d502c.png" alt="Add Header" style="display:block;margin:0 auto" width="1736" height="1036" loading="lazy">

<p>Add all three headers with their respective values one by one:</p>
<pre><code class="language-bash">Access-Control-Allow-Origin: http://localhost:5174 
Access-Control-Allow-Methods: GET 
Access-Control-Allow-Headers: *
</code></pre>
<p>Each of these headers has its own important use:</p>
<ol>
<li><p>With the <code>Access-Control-Allow-Origin</code> header, you can specify the origin domains that are allowed to have a cross-origin request to the server. In this case, the value is <code>http://localhost:5174</code> where we're running a Vite-based ReactJS app.</p>
</li>
<li><p>The header <code>Access-Control-Allow-Methods</code> specifies what kind of HTTP methods are allowed from the originating domain. In this case, we're allowing only the <code>GET</code> method.</p>
</li>
<li><p>The <code>Access-Control-Allow-Headers</code> HTTP response headers specify which HTTP headers can be safely used during a cross-origin request.</p>
</li>
</ol>
<p>Alright, let's add them all and save.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/02fc7203-9ca6-47cf-a478-f20d928ece54.png" alt="CORS Headers" style="display:block;margin:0 auto" width="1714" height="690" loading="lazy">

<p>Like overriding content, overriding the header will also create a folder with the context of the server domain, and under that, a file called <code>.headers</code>. As the file name starts with a dot(.), it may be treated as a hidden file by most operating systems. So make sure you go through the OS settings to view the hidden files to view this file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/ab93a8d4-b87d-4cf4-8a0e-1d6ce4d26537.png" alt="Hidden headers file" style="display:block;margin:0 auto" width="1394" height="530" loading="lazy">

<p>Once you view and open the file, you'll see the headers you have added with overriding.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/8dfe3ee3-2250-489a-a465-f9796934a470.png" alt="headers content" style="display:block;margin:0 auto" width="1678" height="1122" loading="lazy">

<p>Now, hard refresh your browser, and try to perform the same operation that was giving you the CORS error before. Wow, the error has gone now! You should be able to see the request success and the response coming back from the server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/351dcb19-af06-4058-b08d-d449b92ab898.png" alt="User Data" style="display:block;margin:0 auto" width="2558" height="1080" loading="lazy">

<p>Just imagine, not a single line of server-side code changes, and you're unblocked so you can move forward with your client-side UI work. Fantastic, isn't it?</p>
<h2 id="heading-additional-tips">Additional Tips</h2>
<p>Before we end, let's learn about a couple more handy tips.</p>
<h3 id="heading-applying-overrides-globally">Applying Overrides Globally</h3>
<p>We applied the CORS error-related header overriding only on the <code>/user</code> API endpoint. What if you need to apply the same overriding for other endpoints, too? You can do it easily by following these simple steps:</p>
<ol>
<li><p>Navigate to the <code>Sources</code> tab.</p>
</li>
<li><p>Select the <code>Overrides</code> sub-tab.</p>
</li>
<li><p>Click on the <code>.headers</code> override.</p>
</li>
<li><p>On the right-side panel, change the value of the <code>Apply to</code> to <code>*</code>.</p>
</li>
</ol>
<p>That's it. Now, the same response headers will be applied as overrides for all the endpoints.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/c15a94e4-a2e9-48b1-8ae1-c781424be388.png" alt="Apply To" style="display:block;margin:0 auto" width="1678" height="666" loading="lazy">

<h3 id="heading-disabling-or-removing-overrides">Disabling or Removing Overrides</h3>
<p>Sometimes, you might want to disable or remove overrides. To disable overrides without removing them, just uncheck the <code>Enable Local Overrides</code> checkbox. To remove all the overrides permanently, click on the stop icon at the top-right corner. Also, to selectively remove an override, right-click on it and delete.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/80c59dc2-0eea-48ad-b499-be1d4493512c.png" alt="80c59dc2-0eea-48ad-b499-be1d4493512c" style="display:block;margin:0 auto" width="848" height="642" loading="lazy">

<h2 id="heading-learn-more-from-the-thinking-in-debugging-mindset">Learn More From the Thinking in Debugging Mindset</h2>
<p>If you've liked this practical, example-driven guide, you'll enjoy my other debugging-related content from the <em>Thinking in Debugging</em> series. Please <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwT8Msc4H3_CP5Tf8MqqqVZ">check it out</a>.</p>
<p><a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwT8Msc4H3_CP5Tf8MqqqVZ"><img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/9046f04f-71f1-4303-b39c-7267fd3814bc.png" alt="Thinking in Debugging" style="display:block;margin:0 auto" width="2330" height="1046" loading="lazy"></a></p>
<h2 id="heading-before-we-end">Before We End…</h2>
<p>That’s all! I hope you found this insightful.</p>
<p>Let’s connect:</p>
<ul>
<li><p>Subscribe to my <a href="https://www.youtube.com/tapasadhikary?sub_confirmation=1">YouTube Channel</a>.</p>
</li>
<li><p>Check out my courses, <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRw2Fwwjt6cPC_tk5vcSICCu">40 Days of JavaScript</a> and <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRyQVT__uFZvaVfWPdfyMFHC">15 Days of React Design Patterns</a></p>
</li>
<li><p>Follow on <a href="https://www.linkedin.com/in/tapasadhikary/">LinkedIn</a> if you don't want to miss the daily dose of up-skilling tips.</p>
</li>
<li><p>Join my <a href="https://discord.gg/zHHXx4vc2H">Discord Server</a>, and let’s learn together.</p>
</li>
<li><p>Follow my work on <a href="https://github.com/tapascript">GitHub</a>.</p>
</li>
</ul>
<p>See you soon with my next article. Until then, please take care of yourself and keep learning.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Stop Letting AI Agents Guess Your Requirements ]]>
                </title>
                <description>
                    <![CDATA[ I spent 64% of my weekly Claude budget before Wednesday building a tool designed to reduce Claude usage. That's the kind of irony that deserves its own specification. The tool is spec-writer: a Claude ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-stop-letting-ai-agents-guess-your-requirements/</link>
                <guid isPermaLink="false">69c1dc5930a9b81e3ac400fc</guid>
                
                    <category>
                        <![CDATA[ claude.ai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ClaudeCode ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdevelopment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Daniel Nwaneri ]]>
                </dc:creator>
                <pubDate>Tue, 24 Mar 2026 00:35:37 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/06a3ff85-4d60-4e05-b494-8d2f3e6024ac.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>I spent 64% of my weekly Claude budget before Wednesday building a tool designed to reduce Claude usage. That's the kind of irony that deserves its own specification.</p>
<p>The tool is spec-writer: a Claude Code skill that takes a vague feature request and generates a structured spec, technical plan, and task breakdown before a single line of code gets written.</p>
<p>The problem it solves is one most developers hit within their first week of using AI coding agents seriously: the agent writes confidently in the wrong direction and you pay for it twice, once in tokens, once in rewrites.</p>
<p>This tutorial shows you how to install spec-writer, how to invoke it on a real feature, and how to read the output so you can catch the assumptions that would have wasted your time.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-the-problem-with-prompting-agents-directly">The Problem with Prompting Agents Directly</a></p>
</li>
<li><p><a href="#heading-what-specdriven-development-is">What Spec-Driven Development Is</a></p>
</li>
<li><p><a href="#heading-how-spec-writer-works">How spec-writer Works</a></p>
</li>
<li><p><a href="#heading-how-to-install-spec-writer">How to Install spec-writer</a></p>
</li>
<li><p><a href="#heading-how-to-write-your-first-spec">How to Write Your First Spec</a></p>
</li>
<li><p><a href="#heading-how-to-read-the-output">How to Read the Output</a></p>
</li>
<li><p><a href="#heading-how-to-hand-the-spec-to-your-agent">How to Hand the Spec to Your Agent</a></p>
</li>
<li><p><a href="#heading-where-to-go-next">Where to Go Next</a></p>
</li>
</ol>
<h2 id="heading-the-problem-with-prompting-agents-directly">The Problem with Prompting Agents Directly</h2>
<p>Here is what happens when you skip the spec.</p>
<p>You have a feature in your head: "Add a way for users to export their data." You open Claude Code and describe it. The agent produces code. It looks right. You run it. It's mostly right – except it exports everything including soft-deleted records, it doesn't paginate, it times out on large accounts, and it has no authentication check on the export endpoint.</p>
<p>None of those things were in your prompt. The agent guessed, and it guessed plausibly – which is worse than guessing obviously wrong. You didn't notice until testing.</p>
<p>This is the fundamental problem with prompting agents directly on anything non-trivial: your prompt carries your conscious requirements, but every feature has a shadow of requirements you didn't think to state. And the agent fills that shadow with assumptions.</p>
<p>Most of the time, those assumptions are reasonable. Some of the time, they're wrong in ways that take hours to unravel.</p>
<p>The failure mode isn't hallucination. It's the agent being exactly as helpful as the prompt allowed, which wasn't helpful enough.</p>
<p>Spec-Driven Development addresses this directly. The methodology – documented extensively by practitioners like Julián Deangelis – argues that a written spec isn't documentation overhead. It's the mechanism that forces you to make decisions before the agent does.</p>
<h2 id="heading-what-spec-driven-development-is">What Spec-Driven Development Is</h2>
<p>Spec-Driven Development is the practice of writing a structured specification before you write code or prompt an agent. The spec defines what the feature must do, what assumptions are being made, and what tasks the implementation breaks into.</p>
<p>The key insight is what a spec is <em>for</em>. A spec is not trying to replace code. It's trying to surface the decisions that would otherwise be invisible. The agent will make those decisions either way: with a spec, you make them first. Without a spec, you discover them during testing.</p>
<p>The strongest counterargument to SDD comes from Gabriella Gonzalez: <em>a sufficiently detailed spec is just code</em>. She's right that some specs devolve into pseudocode so specific they might as well be implementations.</p>
<p>But that's a spec written at the wrong level of abstraction. The goal is to name the decisions, not to pre-implement them. "Only authenticated users can trigger this export" is a decision. "Call <code>verifyJWT(token)</code> and return 401 if it fails" is implementation. The spec needs the first. The agent handles the second.</p>
<p>SDD has three levels:</p>
<ol>
<li><p><strong>Spec-First</strong>: write a spec before every feature and hand it to the agent as context. This is the entry point and the workflow this tutorial focuses on.</p>
</li>
<li><p><strong>Spec-Anchored</strong>: the spec lives in the repository and evolves alongside the code. When requirements change, you update the spec and re-prompt the agent to realign.</p>
</li>
<li><p><strong>Spec-as-Source</strong>: the spec is the primary artifact. Code is generated from it and considered disposable. This is the most ambitious level and the direction many teams are moving toward.</p>
</li>
</ol>
<p>spec-writer gets you to Spec-First immediately, with no ceremony.</p>
<h2 id="heading-how-spec-writer-works">How spec-writer Works</h2>
<p>spec-writer is a Claude Code skill – a markdown file that loads into the agent's context and changes how it responds when invoked.</p>
<p>The skill follows one rule: generate first, flag assumptions inline. Instead of asking you clarifying questions before producing output, it generates the full spec immediately and marks every decision it made without your explicit input using <code>[ASSUMPTION: ...]</code> tags. Then you correct what's wrong.</p>
<p>This is faster than Q&amp;A because it makes the decisions visible in a form you can react to rather than anticipate.</p>
<p>The output has three sections in fixed order:</p>
<ol>
<li><p><strong>SPEC</strong>: the what. One-line purpose, user stories, requirements, edge cases, and acceptance criteria in Given/When/Then format.</p>
</li>
<li><p><strong>PLAN</strong>: the how. Stack and architecture decisions, data model changes, API contracts, testing strategy, and security constraints.</p>
</li>
<li><p><strong>TASKS</strong>: the breakdown. Ordered, self-contained tasks each completable in a single agent session, each with its own acceptance criteria.</p>
</li>
</ol>
<p>After the three sections, the skill produces an <strong>Assumptions summary</strong>: every <code>[ASSUMPTION: ...]</code> from the output, ranked by impact. This is the part you review before handing anything to the agent.</p>
<p>The skill is compatible with <a href="https://github.com/github/spec-kit">GitHub Spec Kit</a> and <a href="https://github.com/Fission-AI/OpenSpec">OpenSpec</a>. If you use either framework, save the spec output to your <code>.specify/</code> or <code>openspec/changes/</code> directory and continue from there.</p>
<h2 id="heading-how-to-install-spec-writer">How to Install spec-writer</h2>
<p>spec-writer uses the Agent Skills standard, which means the same SKILL.md file works across Claude Code, Cursor, GitHub Copilot, Gemini CLI, and any other agent that supports the standard. You install it once and it works everywhere.</p>
<h3 id="heading-installation">Installation</h3>
<p>Create the skills directory if it doesn't exist and clone the repo:</p>
<pre><code class="language-bash">mkdir -p ~/.claude/skills
git clone https://github.com/dannwaneri/spec-writer.git ~/.claude/skills/spec-writer
</code></pre>
<p>On Windows PowerShell:</p>
<p><strong>(Note:</strong> PowerShell uses backtick (<code>`</code>) for line continuation, not backslash.)</p>
<pre><code class="language-powershell">New-Item -ItemType Directory -Force -Path "$HOME\.claude\skills"
git clone https://github.com/dannwaneri/spec-writer.git "$HOME\.claude\skills\spec-writer"
</code></pre>
<p>That's the entire installation. No package to install, no configuration file to edit, no API key. The skill is a markdown file. The agent reads it.</p>
<h3 id="heading-verification">Verification</h3>
<p>Open Claude Code and type:</p>
<pre><code class="language-plaintext">/spec-writer test
</code></pre>
<p>If the skill is installed correctly, the agent will read the SKILL.md and produce a spec structure – even for "test" as input. You'll see the three sections and the Assumptions summary. If nothing happens, confirm that the <code>~/.claude/skills/spec-writer/SKILL.md</code> file exists.</p>
<h2 id="heading-how-to-write-your-first-spec">How to Write Your First Spec</h2>
<p>The invocation pattern is:</p>
<pre><code class="language-plaintext">/spec-writer [your feature description]
</code></pre>
<p>The description can be as rough as you'd write in a Slack message. The skill's job is to structure it. You don't need to be precise. Being precise is what the spec is for.</p>
<h3 id="heading-a-real-example">A Real Example</h3>
<p>Here is a real invocation from the Foundation project, a federated knowledge base built on Cloudflare Workers:</p>
<pre><code class="language-plaintext">/spec-writer Add CLI capture for Claude Code sessions stored in ~/.claude/projects/
</code></pre>
<p>That's twelve words. No mention of the tech stack, no data model details, no authentication requirements, no error handling spec. All of that comes out in the spec.</p>
<p>Here is the abbreviated output:</p>
<p><strong>SPEC</strong></p>
<p><strong>One-line purpose:</strong> Developers can capture Claude Code session logs from their local filesystem into Foundation before they are deleted after 30 days.</p>
<p><strong>Users and use cases:</strong></p>
<ul>
<li><p>As a developer using Claude Code daily, I want my coding sessions captured automatically so that architectural decisions and debugging patterns don't disappear after 30 days.</p>
</li>
<li><p>As a Foundation user, I want CLI sessions and browser extension captures in the same knowledge base so that I have one place to query across all my AI interactions.</p>
</li>
</ul>
<p><strong>Acceptance criteria:</strong></p>
<pre><code class="language-plaintext">Given a developer with Claude Code sessions in ~/.claude/projects/
When they run the Foundation CLI capture command
Then a list of uncaptured sessions is displayed, sorted by most recent first
 
Given a session already in Foundation
When the developer runs capture again
Then that session does not appear in the review list
 
Given a malformed .jsonl file
When the capture command processes it
Then an error is logged for that file and processing continues on remaining sessions
</code></pre>
<p><strong>PLAN</strong></p>
<p><strong>Stack and architecture:</strong></p>
<pre><code class="language-plaintext">CLI capture runs locally — it needs to read the local filesystem, which
Cloudflare Workers cannot do. [ASSUMPTION: CLI capture is a local Node.js
or Bun script that calls the Foundation API to insert sessions, rather
than a Worker itself]
</code></pre>
<p><strong>API contracts:</strong></p>
<ul>
<li><p><code>POST /api/sessions/import</code> – accepts parsed session JSON, returns session ID</p>
</li>
<li><p>Returns 409 if session already exists</p>
</li>
<li><p>Triggers evaluator pipeline after successful insert</p>
</li>
</ul>
<p><strong>TASKS</strong></p>
<pre><code class="language-plaintext">## Task 1: .jsonl parser
 
**What to build:** A parser that reads a Claude Code .jsonl session file
and outputs Foundation's internal message format.
**Files likely affected:** cli/src/parsers/claudeCode.js (create)
**Acceptance criteria:**
1. Parses a valid .jsonl file into the correct message format
2. Handles malformed lines without crashing — logs and skips
3. Returns empty array for empty file
**Dependencies:** none
</code></pre>
<p><strong>Assumptions to review:</strong></p>
<pre><code class="language-plaintext">1. CLI capture is a local script calling the Foundation API, not a Worker
   Impact: HIGH
   Correct this if: you want a purely serverless approach
 
2. Manual curation before capture, not automatic bulk import
   Impact: HIGH
   Correct this if: you want automatic background capture
 
3. Session ID from .jsonl filename is the deduplication key
   Impact: MEDIUM
   Correct this if: session IDs are stored differently in your schema
 
4. No sensitive data scrubbing in v1
   Impact: MEDIUM
   Correct this if: your sessions contain credentials or keys
</code></pre>
<p>Twelve words in, four decisions surfaced immediately – three of which had real architectural implications.</p>
<p>The third assumption ("Session ID from .jsonl filename is the deduplication key") is the one that would have caused the most subtle bug. The agent would have implemented deduplication based on the filename and it would have worked until a session was renamed. The spec caught it before a line of code was written.</p>
<h2 id="heading-how-to-read-the-output">How to Read the Output</h2>
<p>The output is designed to be scanned for <code>[ASSUMPTION: ...]</code> tags first, read for the tasks second.</p>
<h3 id="heading-reading-the-assumptions">Reading the Assumptions</h3>
<p>Every <code>[ASSUMPTION: ...]</code> tag marks a place where the agent filled in something you didn't specify. Your job is to go through the Assumptions summary and decide for each one:</p>
<ul>
<li><p><strong>Correct</strong>: the assumption is right, leave it</p>
</li>
<li><p><strong>Override</strong>: the assumption is wrong, restate it and re-run the spec</p>
</li>
<li><p><strong>Defer</strong>: the assumption doesn't matter for this iteration, mark it and move on</p>
</li>
</ul>
<p>The impact rating tells you which assumptions to fix before you start coding. HIGH-impact assumptions affect architecture or data model. If they're wrong, fixing them requires rework. LOW-impact assumptions affect behavior details that are easy to change later.</p>
<h3 id="heading-reading-the-acceptance-criteria">Reading the Acceptance Criteria</h3>
<p>The acceptance criteria in Given/When/Then format are the most useful part of the spec for catching scope errors. Read each one and ask: is this actually what I want?</p>
<p>Criteria are binary by design. "Returns 401 when unauthenticated" is a criterion. "Works correctly" is not. If you find yourself reading a criterion and thinking "well, it depends", then that's a signal that the criterion is hiding an assumption. Restate it.</p>
<h3 id="heading-reading-the-tasks">Reading the Tasks</h3>
<p>The tasks are ordered and self-contained. Each task produces a verifiable change. Before you hand any task to an agent, check two things:</p>
<ol>
<li><p>Does the task have all the context it needs? If a task says "follow the existing auth pattern" and you haven't pointed the agent at your auth code, it will guess.</p>
</li>
<li><p>Does the acceptance criteria match what you'd actually test? If the criteria are vague, tighten them before the agent sees the task.</p>
</li>
</ol>
<h2 id="heading-how-to-hand-the-spec-to-your-agent">How to Hand the Spec to Your Agent</h2>
<p>The spec is context, not a prompt. When you start an agent session for a task, include the relevant spec sections alongside the task description.</p>
<p>For Task 1 from the example above, your agent session might open like this:</p>
<pre><code class="language-plaintext">Context:
- This is a federated knowledge base built on Cloudflare Workers, D1, and Vectorize
- Sessions are stored in ~/.claude/projects/ as .jsonl files
- The API runs at https://&lt;your-worker&gt;.workers.dev
 
Spec:
[paste the SPEC and PLAN sections]
 
Task:
[paste Task 1]
</code></pre>
<p>The context block is just an example. Replace it with your own project's tech stack, file locations, and API URL. The point is to give the agent the same context a new team member would need on day one.</p>
<p>The agent now has requirements, architecture context, and a single scoped task with binary acceptance criteria. It cannot guess the deduplication key incorrectly because the spec already resolved that assumption. It cannot skip error handling because the acceptance criteria explicitly require it.</p>
<p>This is the workflow the spec is designed for. The spec doesn't replace the agent. Rather, it removes the decisions from the agent's hands and puts them in yours, before the work starts.</p>
<h3 id="heading-saving-the-spec-for-later">Saving the Spec for Later</h3>
<p>If you want to move toward Spec-Anchored development – where the spec lives in the repository – save the output to a <code>specs/</code> directory in your project:</p>
<pre><code class="language-bash"># Create specs directory
mkdir -p specs
 
# Save your spec
# Paste the output into specs/cli-capture.md
</code></pre>
<p>When requirements change, update the spec and re-prompt the agent to realign the implementation. The spec becomes the source of truth, not the code comments.</p>
<h2 id="heading-where-to-go-next">Where to Go Next</h2>
<p>Try it on your next feature before you write a line of code. The assumptions it flags will tell you something about your feature you hadn't consciously decided yet – and correcting the HIGH-impact ones before you hand anything to an agent is the whole point. Skipping that step is the same as prompting directly.</p>
<p>If your project is growing, move toward Spec-Anchored. Save specs in your repository under <code>specs/</code>. When a new contributor joins or an agent starts a session cold, the specs give them the decisions that got made without requiring them to reverse-engineer the code.</p>
<p>The strongest ongoing challenge to this workflow is Gabriella Gonzalez's argument that detailed specs become code. If your specs are getting implementation-specific, you've crossed a line. Pull back to decisions – "only authenticated users can trigger this" – and leave implementation to the agent. The spec's job is to name what the agent would have guessed wrong, not to write the feature in prose.</p>
<p>The Agent Skills standard now works across Claude Code, GitHub Copilot, Cursor, and Gemini CLI. The spec-writer repo is at <a href="https://github.com/dannwaneri/spec-writer">github.com/dannwaneri/spec-writer</a>.</p>
<p>The irony of spending 64% of a Claude budget building a token-efficiency tool is real. But the spec surfaced four decisions on a twelve-word prompt. The fourth one – the deduplication key assumption – would have produced a bug that worked perfectly until a session got renamed.</p>
<p>That's not a hallucination. That's the agent being exactly as helpful as the prompt allowed.</p>
<p>The spec is how you raise the ceiling on what "helpful" means.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Production-Ready Flutter CI/CD Pipeline with GitHub Actions: Quality Gates, Environments, and Store Deployment ]]>
                </title>
                <description>
                    <![CDATA[ Mobile application development has evolved over the years. The processes, structure, and syntax we use has changed, as well as the quality and flexibility of the apps we build. One of the major improv ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-production-ready-flutter-ci-cd-pipeline-with-github-actions-quality-gates-environments-and-store-deployment/</link>
                <guid isPermaLink="false">69bb2e078c55d6eefb6c2e8d</guid>
                
                    <category>
                        <![CDATA[ ci-cd ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ github-actions ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ github copilot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CI/CD pipelines ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Wed, 18 Mar 2026 22:58:15 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8c9d9384-ff02-47d7-aa69-42db2ebae247.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Mobile application development has evolved over the years. The processes, structure, and syntax we use has changed, as well as the quality and flexibility of the apps we build.</p>
<p>One of the major improvements has been a properly automated CI/CD pipeline flow that gives us seamless automation, continuous integration, and continuous deployment.</p>
<p>In this article, I'll break down how you can automate and build a production ready CI/CD pipeline for your Flutter application using GitHub Actions.</p>
<p>Note that there are other ways to do this, like with Codemagic (built specifically for Flutter apps – which I'll cover in a subsequent tutorial), but in this article we'll focus on GitHub Actions instead.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-the-typical-workflow">The Typical Workflow</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-pipeline-architecture">Pipeline Architecture</a></p>
</li>
<li><p><a href="#heading-writing-the-workflows">Writing the Workflows</a></p>
<ul>
<li><p><a href="#heading-the-helper-scripts">The Helper Scripts</a></p>
<ul>
<li><p><a href="#heading-script-1-generateconfigsh">generate_config.sh</a></p>
</li>
<li><p><a href="#heading-script-2-qualitygatesh">quality_gate.sh</a></p>
</li>
<li><p><a href="#heading-script-3-uploadsymbolssh-sentry">upload_symbols.sh</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-workflow-1-prchecksyml">PR Quality Gate (pr_checks.yml)</a></p>
</li>
<li><p><a href="#heading-workflow-2-androidyml">Android CI/CD Pipeline (android.yml)</a></p>
</li>
<li><p><a href="#heading-workflow-3-iosyml">iOS CI/CD Pipeline (ios.yml)</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-secrets-and-configuration-reference">Secrets and Configuration Reference</a></p>
</li>
<li><p><a href="#heading-end-to-end-flow">End-to-End Flow</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-the-typical-workflow">The Typical Workflow</h2>
<p>First, let's define the common approach to deploying production-ready Flutter apps.</p>
<p>The development team does their work on local, pushes to the repository for merge or review, and eventually runs <code>flutter build apk</code> or <code>flutter build appbundle</code> to generate the apk file. This then gets shared with the QA team manually, or deployed to Firebase app distribution for testing. If it's a production move, the app bundle is submitted to the Google Play store for review and then deployed.</p>
<p>This process is often fully manual with no automated checks, validation, or control over quality, speed, and seamlessness. Manually shipping a Flutter app starts out relatively simply, but can quickly and quietly turn into a liability. You run <code>flutter build</code>, switch configs, sign the build, upload it somewhere, and hope you didn’t mix up staging keys with production ones.</p>
<p>As teams grow and release updates more and more quickly, these manual steps become real risks. A skipped quality check, a missing keystore, or an incorrect base URL deployed to production can cost hours of debugging or worse – it can affect your users.</p>
<p>Automating this process fully involves some high level configuration and predefined scripting. It completely takes control of the deployment process from the moment the developer raised a PR into the common or base branch (for example, the <code>develop</code> branch).</p>
<p>This automated process takes care of everything that needs to be done – provided it has been predefined, properly scripted, and aligns with the use case of the team.</p>
<h3 id="heading-what-well-do-here">What we'll do here:</h3>
<p>In this tutorial, we'll build a production-grade CI/CD pipeline for a Flutter app using GitHub Actions. The pipeline automates the entire lifecycle: pull-request quality checks, environment-specific configuration injection, Android and iOS builds, Firebase App Distribution for testers, Sentry symbol uploads, and final deployment to the Play Store and App Store.</p>
<p>By the end, every release – from a developer opening a PR to the final build landing in users' hands – will be fully automated, with no one touching a terminal.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ol>
<li><p>A Flutter app with working Android and iOS builds</p>
</li>
<li><p>Basic familiarity with <a href="https://www.freecodecamp.org/news/automate-cicd-with-github-actions-streamline-workflow/">GitHub Actions</a> (workflows and jobs)</p>
</li>
<li><p>A Firebase project with App Distribution enabled</p>
</li>
<li><p>A Sentry project for error tracking</p>
</li>
<li><p>A Google Play Console app already created</p>
</li>
<li><p>An Apple Developer account with App Store Connect access</p>
</li>
<li><p>Fastlane configured for your iOS project</p>
</li>
<li><p>Basic Bash knowledge (I’ll explain the important parts)</p>
</li>
</ol>
<h2 id="heading-pipeline-architecture">Pipeline Architecture</h2>
<p>In this guide, we'll be building a CI/CD pipeline with very precise instructions and use cases. These use cases determine the way your pipeline is built.</p>
<p>For this tutorial, we'll use this use case:</p>
<p>I want to automate the workflow on my development team based on the following criteria:</p>
<ol>
<li><p>When a developer on the team raises a PR into the common working branch <code>develop</code> in most cases), a workflow is triggered to run quality checks on the code. It only allows the merge to happen if all checks (like tests coverage, quality checks, and static analysis) pass.</p>
</li>
<li><p>Code that's moving from the develop branch to the staging branch goes through another workflow that injects staging configurations/secret keys, does all the necessary checks, and distributes the application for testing on Firebase App Distribution for android as well as Testflight for iOS.</p>
</li>
<li><p>Code that's moving from the staging to the production branch goes through the production level workflow which involves apk secured signing, production configuration injection, running tests to ensure nothing breaks, Sentry analysis for monitoring, and submission to App Store Connect as well as Google Play Console.</p>
</li>
</ol>
<p>These are our predefined conditions which help with the construction of our workflows.</p>
<h2 id="heading-writing-the-workflows">Writing the Workflows</h2>
<p>We'll split this pipeline into three GitHub Actions workflows.</p>
<p>We'll also be taking it a notch higher by creating three helper .sh scripts for a cleaner and more maintainable workflow.</p>
<p>In your project root, create two folders:</p>
<ol>
<li><p>.github/</p>
</li>
<li><p>scripts.</p>
</li>
</ol>
<p>The <strong>.github/</strong> folder will hold the workflows we'll be creating for each use case, while the <strong>scripts/</strong> folder will hold the helper scripts that we can easily call in our CLI or in the workflows directly.</p>
<p>After this, we'll create three workflow .yaml files:</p>
<ol>
<li><p>pr_checks.yaml</p>
</li>
<li><p>android.yaml</p>
</li>
<li><p>ios.yaml</p>
</li>
</ol>
<p>Also in the scripts folder, let's create three .sh files:</p>
<ol>
<li><p>generate_config.sh</p>
</li>
<li><p>quality_checks.sh</p>
</li>
<li><p>upload_symbols.sh</p>
</li>
</ol>
<pre><code class="language-yaml">.github/
  workflows/
    pr_checks.yml
    android.yml
    ios.yml

scripts/
  generate_config.sh
  quality_checks.sh
  upload_symbols.sh
</code></pre>
<p>This workflow architecture ensures that a push to <code>develop</code> automatically produces a tester build. Also, merging to <code>production</code> ships directly to the stores without manual commands or config changes.</p>
<p>The scripts live outside the YAML on purpose. This lets you run the same logic locally.</p>
<h3 id="heading-the-helper-scripts">The Helper Scripts</h3>
<p>The scripts form the backbone of the pipeline. Each one has a single responsibility and is reused across workflows.</p>
<p>Instead of cramming logic into YAML, we'll move it into <strong>reusable scripts</strong>. This keeps workflows clean and lets you run the same logic locally. Let's go through each one now.</p>
<h3 id="heading-script-1-generateconfigsh">Script #1: <code>generate_config.sh</code></h3>
<p>Injecting secrets safely is one of the hardest CI/CD problems in mobile apps.</p>
<p>The strategy:</p>
<ul>
<li><p>Commit a Dart template file with placeholders</p>
</li>
<li><p>Replace placeholders at build time using secrets from GitHub Actions</p>
</li>
<li><p>Never commit real credentials</p>
</li>
</ul>
<pre><code class="language-yaml">#!/usr/bin/env bash
set -euo pipefail


ENV_NAME=${1:-}
BASE_URL=${2:-}
ENCRYPTION_KEY=${3:-}

TEMPLATE="lib/core/env/env_ci.dart"
OUT="lib/core/env/env_ci.g.dart"

if [ -z "\(ENV_NAME" ] || [ -z "\)BASE_URL" ] || [ -z "$ENCRYPTION_KEY" ]; then
  echo "Usage: $0 &lt;env-name&gt; &lt;base-url&gt; &lt;encryption-key&gt;"
  exit 2
fi

sed -e "s|&lt;&lt;BASE_URL&gt;&gt;|$BASE_URL|g" \
    -e "s|&lt;&lt;ENCRYPTION_KEY&gt;&gt;|$ENCRYPTION_KEY|g" \
    -e "s|&lt;&lt;ENV_NAME&gt;&gt;|$ENV_NAME|g" \
    "\(TEMPLATE" &gt; "\)OUT"

echo "Generated config for $ENV_NAME"
</code></pre>
<p>This script is responsible for injecting environment-specific configuration into the Flutter app at build time, without ever committing secrets to source control.</p>
<p>Let’s walk through it carefully.</p>
<h4 id="heading-1-shebang-choosing-the-shell">1. Shebang: Choosing the Shell</h4>
<pre><code class="language-yaml">#!/usr/bin/env bash
</code></pre>
<p>This line tells the system to execute the script using <strong>Bash</strong>, regardless of where Bash is installed on the machine.</p>
<p>Using <code>/usr/bin/env bash</code> instead of <code>/bin/bash</code> makes the script more portable across local machines, GitHub Actions runners, and Docker containers.</p>
<h4 id="heading-2-fail-fast-fail-loud">2. Fail Fast, Fail Loud</h4>
<pre><code class="language-yaml">set -euo pipefail
</code></pre>
<p>This is one of the most important lines in the script.</p>
<p>It enables three strict Bash modes:</p>
<ul>
<li><p><code>-e</code>: Exit immediately if any command fails</p>
</li>
<li><p><code>-u</code>: Exit if an undefined variable is used</p>
</li>
<li><p><code>-o pipefail</code>: Fail if any command in a pipeline fails, not just the last one</p>
</li>
</ul>
<p>This matters in CI because silent failures are dangerous, partial config generation can break production builds, and CI should stop immediately when something is wrong.</p>
<p>This line ensures that no broken config ever makes it into a build.</p>
<h4 id="heading-3-reading-input-arguments">3. Reading Input Arguments</h4>
<pre><code class="language-yaml">
ENV_NAME=${1:-}
BASE_URL=${2:-}
ENCRYPTION_KEY=${3:-}
</code></pre>
<p>These lines read <strong>positional arguments</strong> passed to the script:</p>
<ul>
<li><p><code>$1</code>: Environment name (<code>dev</code>, <code>staging</code>, <code>production</code>)</p>
</li>
<li><p><code>$2</code>: API base URL</p>
</li>
<li><p><code>$3</code>: Encryption or API key</p>
</li>
</ul>
<p>The <code>${1:-}</code> syntax means:</p>
<p><em>“If the argument is missing, default to an empty string instead of crashing.”</em></p>
<p>This works hand-in-hand with <code>set -u</code> , we control the failure explicitly instead of letting Bash explode unexpectedly.</p>
<h4 id="heading-4-defining-input-and-output-files">4. Defining Input and Output Files</h4>
<pre><code class="language-yaml">TEMPLATE="lib/core/env/env_ci.dart"
OUT="lib/core/env/env_ci.g.dart"
</code></pre>
<p>Here we define two files:</p>
<ul>
<li><p><strong>Template file (</strong><code>env_ci.dart</code><strong>)</strong></p>
<ul>
<li><p>Contains placeholder values like <code>&lt;&lt;BASE_URL&gt;&gt;</code></p>
</li>
<li><p>Safe to commit to Git</p>
</li>
</ul>
</li>
<li><p><strong>Generated file (</strong><code>env_ci.g.dart</code><strong>)</strong></p>
<ul>
<li><p>Contains real environment values</p>
</li>
<li><p>Must be ignored by Git (<code>.gitignore</code>)</p>
</li>
</ul>
</li>
</ul>
<p>At the heart of this approach are two Dart files with very different responsibilities. They may look similar, but they play completely different roles in the system.</p>
<h4 id="heading-envcidart"><code>env.ci.dart</code>:</h4>
<pre><code class="language-java">// lib/core/env/env_ci.dart

class EnvConfig {
  static const String baseUrl = '&lt;&lt;BASE_URL&gt;&gt;';
  static const String encryptionKey = '&lt;&lt;ENCRYPTION_KEY&gt;&gt;';
  static const String environment = '&lt;&lt;ENV_NAME&gt;&gt;';
}
</code></pre>
<p>This file is <strong>safe</strong>, <strong>static</strong>, and <strong>version-controlled</strong>. It contains placeholders, not real values.</p>
<p>Some of its key characteristics are:</p>
<ul>
<li><p>Contains no real secrets</p>
</li>
<li><p>Uses obvious placeholders (<code>&lt;&lt;BASE_URL&gt;&gt;</code>, etc.)</p>
</li>
<li><p>Safe to commit to Git</p>
</li>
<li><p>Reviewed like normal source code</p>
</li>
<li><p>Serves as the single source of truth for required config fields</p>
</li>
</ul>
<p>Think of this file as a contract:</p>
<p><em>“These are the configuration values the app expects at runtime.”</em></p>
<h4 id="heading-envcigdart"><code>env.ci.g.dart</code>:</h4>
<p>This file is created at <strong>build time</strong> by <code>generate_config.sh</code>. After substitution, it looks like this:</p>
<pre><code class="language-java">// lib/core/env/env_ci.g.dart
// GENERATED FILE — DO NOT COMMIT

class EnvConfig {
  static const String baseUrl = 'https://staging.api.example.com';
  static const String encryptionKey = 'sk_live_xxxxx';
  static const String environment = 'staging';
}
</code></pre>
<p>Key characteristics:</p>
<ul>
<li><p>Contains real environment values</p>
</li>
<li><p>Generated dynamically in CI</p>
</li>
<li><p>Differs per environment (dev / staging / production)</p>
</li>
<li><p>Must <strong>never</strong> be committed to source control</p>
</li>
</ul>
<p>This file exists only on a developer’s machine (if generated locally), inside the CI runner during a build. Once the job finishes, it disappears.</p>
<h4 id="heading-gitignore"><code>.gitignore</code>:</h4>
<p>To guarantee the generated file never leaks, it must be ignored:</p>
<h4 id="heading-why-this-separation-is-critical">Why This Separation Is Critical</h4>
<p>This design solves several hard problems at once.</p>
<p><strong>Security:</strong></p>
<ul>
<li><p>Secrets live <strong>only</strong> in GitHub Actions secrets</p>
</li>
<li><p>They never appear in the repository</p>
</li>
<li><p>They never appear in PRs</p>
</li>
<li><p>They never appear in Git history</p>
</li>
</ul>
<p><strong>Environment Isolation:</strong></p>
<p>Each environment gets its own generated config:</p>
<ul>
<li><p><code>develop</code>: dev API</p>
</li>
<li><p><code>staging</code>: staging API</p>
</li>
<li><p><code>production</code>: production API</p>
</li>
</ul>
<p>The same codebase behaves differently <strong>without branching logic in Dart</strong>.</p>
<p><strong>Deterministic Builds:</strong></p>
<p>Every build is fully reproducible, fully automated, and explicit about which environment it targets.</p>
<p>There are no “it worked locally” scenarios.</p>
<h4 id="heading-5-validating-required-arguments">5. Validating Required Arguments</h4>
<pre><code class="language-java">if [ -z "\(ENV_NAME" ] || [ -z "\)BASE_URL" ] || [ -z "$ENCRYPTION_KEY" ]; then
  echo "Usage: $0 &lt;env-name&gt; &lt;base-url&gt; &lt;encryption-key&gt;"
  exit 2
fi
</code></pre>
<p>This block enforces correct usage.</p>
<ul>
<li><p><code>-z</code> checks whether a variable is empty</p>
</li>
<li><p>If any required argument is missing:</p>
<ul>
<li><p>A helpful usage message is printed</p>
</li>
<li><p>The script exits with a non-zero status code</p>
</li>
</ul>
</li>
<li><p><code>0</code>: success</p>
</li>
<li><p><code>1+</code>: failure</p>
</li>
<li><p><code>2</code> conventionally means incorrect usage</p>
</li>
</ul>
<p>In CI, this immediately fails the job and prevents an invalid build.</p>
<h4 id="heading-6-injecting-environment-values">6. Injecting Environment Values</h4>
<pre><code class="language-java">sed -e "s|&lt;&lt;BASE_URL&gt;&gt;|$BASE_URL|g" \
    -e "s|&lt;&lt;ENCRYPTION_KEY&gt;&gt;|$ENCRYPTION_KEY|g" \
    -e "s|&lt;&lt;ENV_NAME&gt;&gt;|$ENV_NAME|g" \
    "\(TEMPLATE" &gt; "\)OUT"
</code></pre>
<p>This is the heart of the script.</p>
<p>What’s happening here:</p>
<ol>
<li><p><code>sed</code> performs <strong>stream editing</strong>: it reads text, transforms it, and outputs the result</p>
</li>
<li><p>Each <code>-e</code> flag defines a replacement rule:</p>
<ul>
<li><p>Replace <code>&lt;&lt;BASE_URL&gt;&gt;</code> with the actual API URL</p>
</li>
<li><p>Replace <code>&lt;&lt;ENCRYPTION_KEY&gt;&gt;</code> with the real key</p>
</li>
<li><p>Replace <code>&lt;&lt;ENV_NAME&gt;&gt;</code> with the environment label</p>
</li>
</ul>
</li>
<li><p>The transformed output is written to <code>env_ci.g.dart</code></p>
</li>
</ol>
<p>This entire operation happens <strong>at build time</strong>:</p>
<ul>
<li><p>No secrets are committed</p>
</li>
<li><p>No secrets are logged</p>
</li>
<li><p>No secrets persist beyond the CI run</p>
</li>
</ul>
<h4 id="heading-7-success-feedback">7. Success Feedback</h4>
<pre><code class="language-java">echo "Generated config for $ENV_NAME"
</code></pre>
<p>This line provides a clear success signal in CI logs.</p>
<p>It answers three important questions instantly:</p>
<ul>
<li><p>Did the script run?</p>
</li>
<li><p>Did it finish successfully?</p>
</li>
<li><p>Which environment was generated?</p>
</li>
</ul>
<p>In long CI logs, these small confirmations matter.</p>
<p>Alright, now let's move on to the second script.</p>
<h3 id="heading-script-2-qualitygatesh">Script #2: <code>quality_gate.sh</code></h3>
<p>This script defines what <em>“good code”</em> means for your team.</p>
<pre><code class="language-yaml">#!/usr/bin/env bash
set -euo pipefail

echo "Running quality checks"

dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test --no-pub --coverage

if command -v dart_code_metrics &gt;/dev/null 2&gt;&amp;1; then
  dart_code_metrics analyze lib --reporter=console || true
fi

echo "Quality checks passed"
</code></pre>
<p>Lets break down this script bit by bit.</p>
<h4 id="heading-1-start-amp-end-log-markers">1. Start &amp; End Log Markers</h4>
<pre><code class="language-yaml">echo "Running quality checks"
...
echo "Quality checks passed"
</code></pre>
<p>These two lines act as <strong>visual boundaries</strong> in CI logs.</p>
<p>In large pipelines (especially when Android and iOS jobs run in parallel), logs can be very noisy. Clear markers:</p>
<ul>
<li><p>Help developers quickly find the quality phase</p>
</li>
<li><p>Make debugging faster</p>
</li>
<li><p>Confirm that the script completed successfully</p>
</li>
</ul>
<p>The final success message only prints if <strong>everything above it passed</strong>, because <code>set -e</code> would have terminated the script earlier on failure.</p>
<p>So this line effectively means: All quality gates passed. Safe to proceed.</p>
<h4 id="heading-2-running-the-test-suite">2. Running the Test Suite</h4>
<pre><code class="language-yaml">flutter test --no-pub --coverage
</code></pre>
<p>This line executes your entire Flutter test suite.</p>
<p>Let’s break it down carefully.</p>
<p>1. <code>flutter test</code></p>
<p>This runs unit tests, widget tests, and any test under the <code>test/</code> directory. If <strong>any test fails</strong>, the command exits with a non-zero status code.</p>
<p>Because we enabled <code>set -e</code> earlier, that immediately stops the script and fails the CI job.</p>
<p>2. <code>--coverage</code></p>
<p>This flag generates a coverage report at:</p>
<pre><code class="language-yaml">coverage/lcov.info
</code></pre>
<p>This file can later be uploaded to Codecov, used to enforce minimum coverage thresholds, and tracked over time for quality improvement.</p>
<p>Even if you’re not enforcing coverage yet, generating it now future-proofs your pipeline.</p>
<h4 id="heading-3-optional-code-metrics">3. Optional Code Metrics</h4>
<pre><code class="language-yaml">if command -v dart_code_metrics &gt;/dev/null 2&gt;&amp;1; then
  dart_code_metrics analyze lib --reporter=console || true
fi
</code></pre>
<p>This block is intentionally designed to be optional and non-blocking.</p>
<p><strong>Step 1 – Check If the Tool Exists:</strong></p>
<pre><code class="language-yaml">command -v dart_code_metrics &gt;/dev/null 2&gt;&amp;1
</code></pre>
<p>This checks whether <code>dart_code_metrics</code> is installed.</p>
<ul>
<li><p>If installed, proceed</p>
</li>
<li><p>If not installed, skip silently</p>
</li>
</ul>
<p>The redirection:</p>
<ul>
<li><p><code>&gt;/dev/null</code> hides normal output</p>
</li>
<li><p><code>2&gt;&amp;1</code> hides errors</p>
</li>
</ul>
<p>This makes the script portable:</p>
<ul>
<li><p>Developers without the tool can still run the script</p>
</li>
<li><p>CI can enforce it if configured</p>
</li>
</ul>
<p><strong>Step 2 – Run Metrics (Soft Enforcement):</strong></p>
<pre><code class="language-yaml">dart_code_metrics analyze lib --reporter=console || true
</code></pre>
<p>This analyzes the <code>lib/</code> directory and prints results in the console.</p>
<p>The important part is:</p>
<pre><code class="language-yaml">|| true
</code></pre>
<p>Because we enabled <code>set -e</code>, any failing command would normally stop the script.</p>
<p>Adding <code>|| true</code> overrides that behavior:</p>
<ul>
<li><p>If metrics report issues,</p>
</li>
<li><p>The script continues,</p>
</li>
<li><p>CI does not fail.</p>
</li>
</ul>
<p>Why design it this way? Because metrics are often gradual improvements, technical debt indicators, or advisory rather than blocking.</p>
<p>You can later remove <code>|| true</code> to make metrics mandatory.</p>
<h4 id="heading-4-final-success-message"><strong>4. Final Success Message</strong></h4>
<pre><code class="language-yaml">echo "✅ Quality checks passed"
</code></pre>
<p>This line only executes if formatting passed, static analysis passed, and tests passed.</p>
<p>If you see this in CI logs, it means the branch has successfully cleared the quality gate. It’s your automated approval before deployment steps begin.</p>
<h4 id="heading-what-this-script-guarantees">What This Script Guarantees</h4>
<p>With this in place, every branch must satisfy:</p>
<ul>
<li><p>Clean formatting</p>
</li>
<li><p>No analyzer errors</p>
</li>
<li><p>Passing tests</p>
</li>
<li><p>(Optional) Healthy metrics</p>
</li>
</ul>
<p>That’s how you move from <strong>“We try to maintain quality”</strong> to <strong>“Quality is enforced automatically.”</strong></p>
<p>Alright, on to the third script.</p>
<h3 id="heading-script-3-uploadsymbolssh-sentry"><strong>Script #3:</strong> <code>upload_symbols.sh</code> <strong>(Sentry)</strong></h3>
<p>This script is responsible for uploading <strong>obfuscation debug symbols</strong> to Sentry so production crashes remain readable.</p>
<pre><code class="language-yaml">#!/usr/bin/env bash
set -euo pipefail

RELEASE=${1:-}

[ -z "$RELEASE" ] &amp;&amp; exit 2

if ! command -v sentry-cli &gt;/dev/null 2&gt;&amp;1; then
  exit 0
fi

sentry-cli releases new "$RELEASE" || true

sentry-cli upload-dif build/symbols || true

sentry-cli releases finalize "$RELEASE" || true

echo "✅ Symbols uploaded for release $RELEASE"
</code></pre>
<p>Let's go through it step by step.</p>
<h4 id="heading-1-reading-the-release-identifier">1. Reading the Release Identifier</h4>
<pre><code class="language-yaml">RELEASE=${1:-}
</code></pre>
<p>This reads the first positional argument passed to the script.</p>
<p>When you call the script in CI, it typically looks like:</p>
<pre><code class="language-yaml">./scripts/upload_symbols.sh $(git rev-parse --short HEAD)
</code></pre>
<p>So <code>$1</code> becomes the short Git commit SHA.</p>
<p>Using <code>${1:-}</code> ensures:</p>
<ul>
<li><p>If no argument is passed, the variable becomes an empty string</p>
</li>
<li><p>The script does not crash due to <code>set -u</code></p>
</li>
</ul>
<p>This release value ties the uploaded symbols, deployed build, and crash reports all to the exact same commit. This linkage is critical for production debugging.</p>
<h4 id="heading-2-validating-the-release-argument">2. Validating the Release Argument</h4>
<pre><code class="language-yaml">[ -z "$RELEASE" ] &amp;&amp; exit 2
</code></pre>
<p>This is a compact validation check.</p>
<ul>
<li><p><code>-z</code> checks whether the string is empty</p>
</li>
<li><p>If it is empty → exit with status code 2</p>
</li>
</ul>
<p>Conventionally:</p>
<ul>
<li><p><code>0</code> = success</p>
</li>
<li><p><code>1+</code> = failure</p>
</li>
<li><p><code>2</code> = incorrect usage</p>
</li>
</ul>
<p>This prevents symbol uploads from running without a release identifier, which would break traceability in Sentry.</p>
<h4 id="heading-3-checking-if-sentry-cli-exists">3. Checking If <code>sentry-cli</code> Exists</h4>
<pre><code class="language-yaml">if ! command -v sentry-cli &gt;/dev/null 2&gt;&amp;1; then
  exit 0
fi
</code></pre>
<p>This block checks whether the <code>sentry-cli</code> tool is available in the environment.</p>
<p>What’s happening:</p>
<ul>
<li><p><code>command -v sentry-cli</code> checks if it exists</p>
</li>
<li><p><code>&gt;/dev/null 2&gt;&amp;1</code> suppresses all output</p>
</li>
<li><p><code>!</code> negates the condition</p>
</li>
</ul>
<p>So this reads as: <em>"If</em> <code>sentry-cli</code> <em>is NOT installed, exit successfully."</em></p>
<p>Why exit with <code>0</code> instead of failing?</p>
<p>Because not every environment needs symbol uploads. Also, dev builds may not install Sentry, and you don’t want CI to fail just because Sentry isn’t configured.</p>
<p>This makes symbol uploading <strong>environment-aware</strong> and <strong>optional</strong>.</p>
<p>Production environments can install <code>sentry-cli</code>, while dev environments skip it cleanly.</p>
<h4 id="heading-4-creating-a-new-release-in-sentry">4. Creating a New Release in Sentry</h4>
<pre><code class="language-yaml">sentry-cli releases new "$RELEASE" || true
</code></pre>
<p>This tells Sentry: “A new release exists with this version identifier.”</p>
<p>Even if the release already exists, the script continues because of:</p>
<pre><code class="language-yaml">|| true
</code></pre>
<p>This prevents the build from failing if:</p>
<ul>
<li><p>The release was already created</p>
</li>
<li><p>The command returns a non-critical error</p>
</li>
</ul>
<p>The goal is resilience, not strict enforcement.</p>
<h4 id="heading-5-uploading-debug-information-files-difs">5. Uploading Debug Information Files (DIFs)</h4>
<pre><code class="language-yaml">sentry-cli upload-dif build/symbols || true
</code></pre>
<p>This is the core step.</p>
<p><code>build/symbols</code> is generated when you build Flutter with:</p>
<pre><code class="language-yaml">--obfuscate --split-debug-info=build/symbols
</code></pre>
<p>When you obfuscate Flutter builds:</p>
<ul>
<li><p>Method names are renamed</p>
</li>
<li><p>Stack traces become unreadable</p>
</li>
</ul>
<p>The symbol files allow Sentry to reverse-map obfuscated stack traces and show readable crash reports.</p>
<p>Without this step, production crashes look like:</p>
<pre><code class="language-yaml">a.b.c.d (Unknown Source)
</code></pre>
<p>With this step, you get:</p>
<pre><code class="language-yaml">AuthRepository.login()
</code></pre>
<p>Again, <code>|| true</code> ensures the build doesn’t fail if:</p>
<ul>
<li><p>The directory doesn’t exist</p>
</li>
<li><p>No symbols were generated</p>
</li>
<li><p>Upload encounters a transient issue</p>
</li>
</ul>
<p>Symbol uploads should not block deployment.</p>
<h4 id="heading-6-finalizing-the-release">6. Finalizing the Release</h4>
<pre><code class="language-yaml">sentry-cli releases finalize "$RELEASE" || true
</code></pre>
<p>This marks the release as complete in Sentry.</p>
<p>Finalizing signals:</p>
<ul>
<li><p>The release is deployed</p>
</li>
<li><p>It can begin aggregating crash reports</p>
</li>
<li><p>It’s ready for production monitoring</p>
</li>
</ul>
<p>Like the previous steps, this is soft-failed with <code>|| true</code> to keep CI robust.</p>
<h4 id="heading-what-this-script-guarantees">What This Script Guarantees</h4>
<p>When everything is configured correctly:</p>
<ol>
<li><p>Production build is obfuscated</p>
</li>
<li><p>Debug symbols are generated</p>
</li>
<li><p>Symbols are uploaded to Sentry</p>
</li>
<li><p>Crashes map back to real source code</p>
</li>
<li><p>Release version matches commit SHA</p>
</li>
</ol>
<p>That’s production-grade crash observability.</p>
<p>Now that we've gone through the three helper scripts we've created to optimize and enhance this process, lets now dive into the three workflow .yaml files we're going to create.</p>
<h2 id="heading-workflow-1-prchecksyml">Workflow #1: <code>PR_CHECKS.YML</code></h2>
<p>This workflow will be designed to help ensure a certain standard is met once a PR is raised into a certain common or base branch. This will ensure that all quality checks in the incoming code pass before allowing any merge into the base branch.</p>
<p>This is basically a gate that verifies the quality of the code that's about to be merged into the base branch. If your pipeline allows unverified code into your base branch, then your CI becomes decorative, not protective.</p>
<p>Lets break down what's actually needed during every PR Check.</p>
<h3 id="heading-1-dependency-integrity">1. Dependency Integrity</h3>
<p>For Flutter apps, where we manage dependencies with the <strong>pub get</strong> command, it's important to make sure that the integrity of all dependencies are confirmed – up to date as well as compatible.</p>
<p>Every PR should begin with:</p>
<pre><code class="language-yaml">flutter pub get
</code></pre>
<p>This ensures:</p>
<ul>
<li><p><code>pubspec.yaml</code> is valid</p>
</li>
<li><p>Dependency constraints are consistent</p>
</li>
<li><p>Lockfiles are not broken</p>
</li>
<li><p>The project is buildable in a clean environment</p>
</li>
</ul>
<p>If this fails, the branch is not deployable.</p>
<h3 id="heading-2-static-analysis">2. Static Analysis</h3>
<p>This ensures code quality and architecture integrity. Static analysis helps prevent common issues like forgotten await, dead code, null safety violations, async misuse, and so on.</p>
<p>Most production bugs aren't business logic errors – they're structural carelessness. Static analysis helps enforce consistency automatically, so code reviews focus on intent, not linting.</p>
<pre><code class="language-yaml">flutter analyze --fatal-infos --fatal-warnings
</code></pre>
<h3 id="heading-3-formatting">3. Formatting</h3>
<p>This command ensures that your code is properly formatted based on your organization's coding standard and policies.</p>
<pre><code class="language-yaml">dart format --output=none --set-exit-if-changed .
</code></pre>
<h3 id="heading-4-tests">4. Tests</h3>
<p>This runs the unit, widget and business logic tests to ensure quality and avoid regression leaks, silent behavior changes and feature drift.</p>
<pre><code class="language-yaml">flutter test --coverage
</code></pre>
<h3 id="heading-5-test-coverage-enforcement">5. Test Coverage Enforcement</h3>
<p>Ideally, running tests is not enough. Your workflow should also enforce a minimum threshold:</p>
<pre><code class="language-yaml">if [ \((lcov --summary coverage/lcov.info | grep lines | awk '{print \)2}' | sed 's/%//') -lt 70 ]; then
  echo "Coverage too low"
  exit 1
fi
</code></pre>
<p>The command above ensures that a minimum test coverage of 70% is met, with this quality becomes measurable.</p>
<p>The five commands above must be checked (at least) for a <strong>quality gate</strong> to guarantee code quality, security, and integrity.</p>
<p>Now here is the full <strong>pr_checks.yml</strong> file:</p>
<pre><code class="language-yaml">name: PR Quality Gate

on:
  pull_request:
    branches: develop
    types: [opened, synchronize, reopened, ready_for_review]

jobs:
  pr-checks:
    name: Run quality checks on this pull request
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup Java
        uses: actions/setup-java@v1
        with:
          java-version: "12.x"

      - name: Setup Flutter
        uses: subosito/flutter-action@v1
        with:
          channel: "stable"

      - name: Install dependencies
        run: flutter pub get

      - name: Run quality checks
        run: ./scripts/quality_checks.sh

      - name: Notify Team (Success)
        if: success()
        run: |
          echo "PR Quality Checks PASSED"
          echo "PR: ${{ github.event.pull_request.html_url }}"
          echo "Branch: \({{ github.head_ref }} → \){{ github.base_ref }}"
          echo "By: @${{ github.actor }}"
          echo "Team notification: @foluwaseyi-dev @olabodegbolu"

      - name: Notify Team (Failure)
        if: failure()
        run: |
          echo "PR Quality Checks FAILED"
          echo "PR: ${{ github.event.pull_request.html_url }}"
          echo "Branch: \({{ github.head_ref }} → \){{ github.base_ref }}"
          echo "By: @${{ github.actor }}"
          echo "Please fix the issues before requesting review 🔧"
          echo "Team notification: @foluwaseyi-dev @olabodegbolu"
</code></pre>
<p>Every time a developer opens (or updates) a pull request targeting the <code>develop</code> branch, this workflow kicks in automatically. Think of it as a bouncer at the door: no code gets through without passing inspection first.</p>
<h3 id="heading-what-triggers-it">What Triggers it?</h3>
<p>The workflow fires on four events: when a PR is <code>opened</code>, <code>synchronized</code> (new commits pushed), <code>reopened</code>, or marked <code>ready_for_review</code>. So drafts won't trigger it – only PRs that are actually ready to be looked at.</p>
<h3 id="heading-what-does-it-actually-do">What Does it Actually Do?</h3>
<p>It spins up a fresh Ubuntu machine and runs five steps in sequence:</p>
<ol>
<li><p><strong>Checkout</strong>: pulls down the branch's code</p>
</li>
<li><p><strong>Setup Java 12</strong>: installs the JDK (likely a dependency for some tooling or build process)</p>
</li>
<li><p><strong>Setup Flutter (stable channel)</strong>: this is a Flutter project, so it grabs the stable Flutter SDK</p>
</li>
<li><p><strong>Install dependencies</strong>: runs <code>flutter pub get</code> to pull all Dart/Flutter packages</p>
</li>
<li><p><strong>Run quality checks</strong>: executes the helper shell script (<code>./scripts/quality_checks.sh</code>) that we created which runs linting, tests, formatting checks, or all of the above</p>
</li>
</ol>
<h3 id="heading-the-notification-layer">The Notification Layer</h3>
<p>After the checks run, the workflow reports the verdict and it's context-aware:</p>
<ul>
<li><p><strong>If everything passes</strong>, it logs a success message with the PR URL, branch info, and the person who opened it</p>
</li>
<li><p><strong>If something fails</strong>, it logs a failure message and nudges the author to fix issues before requesting a review</p>
</li>
</ul>
<p>Both outcomes tag <code>@foluwaseyi-dev</code> and <code>@olabodegbolu</code> – the two team members responsible for staying in the loop.</p>
<p>This workflow enforces a "fix it before you merge it" culture. No one can merge broken code into <code>develop</code> without the team knowing about it.</p>
<h2 id="heading-workflow-2-androidyml">Workflow #2: Android.yml</h2>
<p>It's a better practice to split your workflows based on platform. This helps you properly manage the instructions regarding each platform. This is the reason behind keeping the Android workflow separate.</p>
<p>Unlike <code>PR _Checks</code>, this workflow presumes that all checks for quality and standards have been done and the code that runs this workflow already meets the required standards.</p>
<p>Based on our predefined use case, let's create a workflow to handle test deployments when merged to develop or staging, and production level activities when merged to production.</p>
<pre><code class="language-yaml">name: Android Build &amp; Release

on:
  push:
    branches:
      - develop
      - staging
      - production

jobs:
  android:
    runs-on: ubuntu-latest
    env:
      FLUTTER_VERSION: 'stable'

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '11'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}

      - name: Install dependencies
        run: flutter pub get

      - name: Determine environment
        id: env
        run: |
          echo "branch=\({GITHUB_REF##*/}" &gt;&gt; \)GITHUB_OUTPUT
          if [ "${GITHUB_REF##*/}" = "develop" ]; then
            echo "ENV=dev" &gt;&gt; $GITHUB_OUTPUT
          elif [ "${GITHUB_REF##*/}" = "staging" ]; then
            echo "ENV=staging" &gt;&gt; $GITHUB_OUTPUT
          else
            echo "ENV=production" &gt;&gt; $GITHUB_OUTPUT
          fi

      # Dev uses hardcoded values no secrets needed
      - name: Generate config (dev)
        if: steps.env.outputs.ENV == 'dev'
        run: ./scripts/generate_config.sh dev "https://dev.api.example.com" "dev_dummy_key"

      # Staging and production inject real secrets
      - name: Generate config (staging/production)
        if: steps.env.outputs.ENV != 'dev'
        run: |
          if [ "${{ steps.env.outputs.ENV }}" = "staging" ]; then
            ./scripts/generate_config.sh staging \
              "${{ secrets.STAGING_BASE_URL }}" \
              "${{ secrets.STAGING_API_KEY }}"
          else
            ./scripts/generate_config.sh production \
              "${{ secrets.PROD_BASE_URL }}" \
              "${{ secrets.PROD_API_KEY }}"
          fi

      # Keystore is only needed for signed builds (staging &amp; production)
      - name: Restore Keystore
        if: steps.env.outputs.ENV != 'dev'
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode &gt; android/app/upload-keystore.jks

      # Production builds are obfuscated + split debug info for Play Store
      - name: Build artifact
        run: |
          if [ "${{ steps.env.outputs.ENV }}" = "production" ]; then
            flutter build appbundle --release \
              --obfuscate \
              --split-debug-info=build/symbols
          else
            flutter build appbundle --release
          fi

      # Dev and staging go to Firebase App Distribution for internal testing
      - name: Upload to Firebase App Distribution
        if: steps.env.outputs.ENV == 'dev' || steps.env.outputs.ENV == 'staging'
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          FIREBASE_ANDROID_APP_ID: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
          FIREBASE_GROUPS: ${{ secrets.FIREBASE_GROUPS }}
        run: |
          firebase appdistribution:distribute \
            build/app/outputs/bundle/release/app-release.aab \
            --app "$FIREBASE_ANDROID_APP_ID" \
            --groups "$FIREBASE_GROUPS" \
            --token "$FIREBASE_TOKEN"

      # Only production goes to the Play Store
      - name: Upload to Play Store
        if: steps.env.outputs.ENV == 'production'
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
          packageName: com.your.package
          releaseFiles: build/app/outputs/bundle/release/app-release.aab
          track: production

      - name: Notify Team (Success)
        if: success()
        run: |
          echo "Android Build &amp; Release PASSED"
          echo "Environment: ${{ steps.env.outputs.ENV }}"
          echo "Branch: ${{ steps.env.outputs.branch }}"
          echo "By: @${{ github.actor }}"
          echo "Commit: ${{ github.sha }}"

      - name: Notify Team (Failure)
        if: failure()
        run: |
          echo "Android Build &amp; Release FAILED"
          echo "Environment: ${{ steps.env.outputs.ENV }}"
          echo "Branch: ${{ steps.env.outputs.branch }}"
          echo "By: @${{ github.actor }}"
          echo "Commit: ${{ github.sha }}"
          echo "Check the logs and fix the issue before retrying"
</code></pre>
<p>This workflow ensures that whenever code lands on the <strong>develop, staging or production</strong> branch, this action is triggered on a fresh Ubuntu machine.</p>
<p>This is triggered by a simple push to any of the tracked branches, no manual intervention needed.</p>
<p>Let's walk through it piece by piece.</p>
<h3 id="heading-1-the-setup-phase">1. The Setup Phase</h3>
<p>Before any Flutter-specific work happens, the workflow lays the foundation:</p>
<ol>
<li><p><strong>Checkout</strong>: grabs the latest code from the branch that triggered the run (using the more modern <code>actions/checkout@v3</code>).</p>
</li>
<li><p><strong>Java 11 via Temurin</strong>: this is an upgrade from the first workflow we created. Instead of a generic <code>setup-java@v1</code>, this uses the <code>temurin</code> distribution which is the Eclipse's open-source JDK build. It's the current industry standard for Android toolchains.</p>
</li>
<li><p><strong>Flutter (stable)</strong>: this pulls the stable Flutter SDK, version pinned via an environment variable (<code>FLUTTER_VERSION: 'stable'</code>) defined at the job level.</p>
</li>
<li><p><strong>Install dependencies</strong>: this ensures we run <code>flutter pub get</code> to pull all packages</p>
</li>
</ol>
<h3 id="heading-2-environment-detection">2. Environment Detection</h3>
<p>This is where it gets interesting. This workflow also checks and determines the environment which will help us define the next set of instructions to run.</p>
<p>This command reads the branch name from <strong>GITHUB REF</strong> and maps it to its environment label which we already created in one of our helper scripts.</p>
<ul>
<li><p>develop → ENV=dev</p>
</li>
<li><p>staging → ENV=staging</p>
</li>
<li><p>production → ENV=production</p>
</li>
</ul>
<p>It strips the branch name from the full ref path using <code>\({GITHUB_REF##*/}</code>, then writes both the branch name and the resolved <code>ENV</code> value to <code>\)GITHUB_OUTPUT</code>, making them available as named outputs (<code>steps.env.outputs.ENV</code>) for every subsequent step.</p>
<p>This means the rest of the pipeline can branch its behaviour based on which environment it's building for, different API keys, different signing configs, different targets – whatever the app needs.</p>
<h3 id="heading-3-config-injection">3. Config Injection</h3>
<p>With the environment resolved, the next step is injecting the right configuration into the app. This is where the <code>generate_config.sh</code> script we built earlier gets called directly from the workflow.</p>
<p>For the <code>dev</code> environment, hardcoded placeholder values are used. No real secrets are needed, since this build is only meant for internal developer testing:</p>
<pre><code class="language-yaml">- name: Generate config (dev)
  if: steps.env.outputs.ENV == 'dev'
  run: ./scripts/generate_config.sh dev "https://dev.api.example.com" "dev_dummy_key"
</code></pre>
<p>For staging and production, however, real secrets are pulled from GitHub Actions secrets and passed directly into the script:</p>
<pre><code class="language-yaml">- name: Generate config (staging/production)
  if: steps.env.outputs.ENV != 'dev'
  run: |
    if [ "${{ steps.env.outputs.ENV }}" = "staging" ]; then
      ./scripts/generate_config.sh staging \
        "${{ secrets.STAGING_BASE_URL }}" \
        "${{ secrets.STAGING_API_KEY }}"
    else
      ./scripts/generate_config.sh production \
        "${{ secrets.PROD_BASE_URL }}" \
        "${{ secrets.PROD_API_KEY }}"
    fi
</code></pre>
<p>Notice that these two steps use an <code>if</code> condition to make them mutually exclusive. Only one will ever run per job. This keeps the pipeline clean: no complicated branching logic inside the script itself, just a clear decision at the workflow level.</p>
<h3 id="heading-4-keystore-restoration">4. Keystore Restoration</h3>
<p>Android requires signed builds for distribution. The signing keystore file cannot be committed to the repository for obvious security reasons, so it's stored as a Base64-encoded GitHub secret and decoded at build time.</p>
<pre><code class="language-yaml">- name: Restore Keystore
  if: steps.env.outputs.ENV != 'dev'
  run: |
    echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode &gt; android/app/upload-keystore.jks
</code></pre>
<p>This step is skipped entirely for the <code>dev</code> environment because dev builds are unsigned debug artifacts meant purely for internal testing on Firebase App Distribution. Only staging and production builds need to be properly signed.</p>
<p>To encode your keystore file as a Base64 string for storing in GitHub secrets, you have to run this locally:</p>
<pre><code class="language-yaml">base64 -i upload-keystore.jks | pbcopy
</code></pre>
<p>This copies the encoded string to your clipboard, which you can then paste directly into your GitHub repository secrets.</p>
<h3 id="heading-5-building-the-artifact">5. Building the Artifact</h3>
<p>With the environment configured and the keystore in place, the workflow builds the app bundle:</p>
<pre><code class="language-yaml">- name: Build artifact
  run: |
    if [ "${{ steps.env.outputs.ENV }}" = "production" ]; then
      flutter build appbundle --release \
        --obfuscate \
        --split-debug-info=build/symbols
    else
      flutter build appbundle --release
    fi
</code></pre>
<p>There's a deliberate difference between how production and non-production builds are compiled.</p>
<p>For production:</p>
<ul>
<li><p><code>--obfuscate</code> renames method and class names in the compiled output, making it significantly harder to reverse engineer the app</p>
</li>
<li><p><code>--split-debug-info=build/symbols</code> extracts the debug symbols into a separate directory at <code>build/symbols</code></p>
</li>
</ul>
<p>These symbols are what <code>upload_symbols.sh</code> later ships to Sentry, so obfuscated crash reports remain readable in your monitoring dashboard.</p>
<p>For dev and staging, neither flag is used. This keeps build times faster and makes local debugging easier since stack traces remain human-readable.</p>
<h3 id="heading-6-distributing-to-firebase-app-distribution">6. Distributing to Firebase App Distribution</h3>
<p>Once the app bundle is built, dev and staging builds are uploaded to Firebase App Distribution so testers can install them immediately:</p>
<pre><code class="language-yaml">- name: Upload to Firebase App Distribution
  if: steps.env.outputs.ENV == 'dev' || steps.env.outputs.ENV == 'staging'
  env:
    FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
    FIREBASE_ANDROID_APP_ID: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
    FIREBASE_GROUPS: ${{ secrets.FIREBASE_GROUPS }}
  run: |
    firebase appdistribution:distribute \
      build/app/outputs/bundle/release/app-release.aab \
      --app "$FIREBASE_ANDROID_APP_ID" \
      --groups "$FIREBASE_GROUPS" \
      --token "$FIREBASE_TOKEN"
</code></pre>
<p>Three secrets power this step:</p>
<ul>
<li><p><code>FIREBASE_TOKEN</code>: the authentication token generated from <code>firebase login:ci</code></p>
</li>
<li><p><code>FIREBASE_ANDROID_APP_ID</code>: the app identifier from the Firebase console</p>
</li>
<li><p><code>FIREBASE_GROUPS</code>: the tester group(s) that should receive the build notification</p>
</li>
</ul>
<p>Once this step completes, every tester in the specified groups receives an email with a direct download link. No one needs to manually share an APK file over Slack or email.</p>
<h3 id="heading-7-deploying-to-the-play-store">7. Deploying to the Play Store</h3>
<p>Production builds skip Firebase entirely and goes straight to the Google Play Store:</p>
<pre><code class="language-yaml">- name: Upload to Play Store
  if: steps.env.outputs.ENV == 'production'
  uses: r0adkll/upload-google-play@v1
  with:
    serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
    packageName: com.your.package
    releaseFiles: build/app/outputs/bundle/release/app-release.aab
    track: production
</code></pre>
<p>This uses the <code>r0adkll/upload-google-play</code> GitHub Action, which handles the Google Play API interaction under the hood. The only requirements are:</p>
<ul>
<li><p>A Google Play service account with the correct permissions, stored as a JSON secret</p>
</li>
<li><p>The correct package name matching what is registered in your Play Console</p>
</li>
<li><p>The <code>track</code> set to <code>production</code> (you can also use <code>internal</code>, <code>alpha</code>, or <code>beta</code> depending on your release strategy)</p>
</li>
</ul>
<p>Replace <code>com.your.package</code> with your actual application ID (the same one defined in your <code>build.gradle</code> file).</p>
<h3 id="heading-8-the-notification-layer">8. The Notification Layer</h3>
<p>Just like the PR checks workflow, this workflow reports its outcome clearly:</p>
<pre><code class="language-yaml">- name: Notify Team (Success)
  if: success()
  run: |
    echo "Android Build &amp; Release PASSED"
    echo "Environment: ${{ steps.env.outputs.ENV }}"
    echo "Branch: ${{ steps.env.outputs.branch }}"
    echo "By: @${{ github.actor }}"
    echo "Commit: ${{ github.sha }}"

- name: Notify Team (Failure)
  if: failure()
  run: |
    echo "Android Build &amp; Release FAILED"
    echo "Environment: ${{ steps.env.outputs.ENV }}"
    echo "Branch: ${{ steps.env.outputs.branch }}"
    echo "By: @${{ github.actor }}"
    echo "Commit: ${{ github.sha }}"
    echo "Check the logs and fix the issue before retrying 🔧"
</code></pre>
<p>The success notification includes the environment, branch, actor, and shares everything needed to trace exactly what was deployed and who triggered it.</p>
<p>The failure notification includes the same context, with a clear call to action.</p>
<h2 id="heading-workflow-3-iosyml">Workflow #3: iOS.yml</h2>
<p>iOS CI/CD is more complex than Android by nature. This is because Apple's signing requirements involve certificates, provisioning profiles, and entitlements that all need to be in the right place before Xcode will produce a valid archive.</p>
<p>Fastlane helps us handles all of that complexity, and the workflow simply calls into it.</p>
<p>Here is the full <code>ios.yml</code>:</p>
<pre><code class="language-yaml">name: iOS Build &amp; Release

on:
  push:
    branches:
      - develop
      - staging
      - production

jobs:
  ios:
    runs-on: macos-latest
    env:
      FLUTTER_VERSION: 'stable'

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}

      - name: Install dependencies
        run: flutter pub get

      - name: Determine environment
        id: env
        run: |
          echo "branch=\({GITHUB_REF##*/}" &gt;&gt; \)GITHUB_OUTPUT
          if [ "${GITHUB_REF##*/}" = "develop" ]; then
            echo "ENV=dev" &gt;&gt; $GITHUB_OUTPUT
          elif [ "${GITHUB_REF##*/}" = "staging" ]; then
            echo "ENV=staging" &gt;&gt; $GITHUB_OUTPUT
          else
            echo "ENV=production" &gt;&gt; $GITHUB_OUTPUT
          fi

      - name: Generate config (dev)
        if: steps.env.outputs.ENV == 'dev'
        run: ./scripts/generate_config.sh dev "https://dev.api.example.com" "dev_dummy_key"

      - name: Generate config (staging/production)
        if: steps.env.outputs.ENV != 'dev'
        run: |
          if [ "${{ steps.env.outputs.ENV }}" = "staging" ]; then
            ./scripts/generate_config.sh staging \
              "${{ secrets.STAGING_BASE_URL }}" \
              "${{ secrets.STAGING_API_KEY }}"
          else
            ./scripts/generate_config.sh production \
              "${{ secrets.PROD_BASE_URL }}" \
              "${{ secrets.PROD_API_KEY }}"
          fi

      - name: Install Fastlane
        run: |
          cd ios
          gem install bundler
          bundle install

      - name: Import signing certificate
        if: steps.env.outputs.ENV != 'dev'
        run: |
          echo "${{ secrets.IOS_CERTIFICATE_BASE64 }}" | base64 --decode &gt; ios/cert.p12
          security create-keychain -p "" build.keychain
          security import ios/cert.p12 -k build.keychain -P "${{ secrets.IOS_CERTIFICATE_PASSWORD }}" -T /usr/bin/codesign
          security list-keychains -s build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "" build.keychain
          security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain

      - name: Install provisioning profile
        if: steps.env.outputs.ENV != 'dev'
        run: |
          echo "${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}" | base64 --decode &gt; profile.mobileprovision
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/

      - name: Build iOS (dev)
        if: steps.env.outputs.ENV == 'dev'
        run: flutter build ios --release --no-codesign

      - name: Build &amp; distribute to TestFlight (staging)
        if: steps.env.outputs.ENV == 'staging'
        env:
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
        run: |
          cd ios
          bundle exec fastlane beta

      - name: Build &amp; release to App Store (production)
        if: steps.env.outputs.ENV == 'production'
        env:
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
        run: |
          cd ios
          bundle exec fastlane release

      - name: Upload Sentry symbols (production only)
        if: steps.env.outputs.ENV == 'production'
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: ./scripts/upload_symbols.sh $(git rev-parse --short HEAD)

      - name: Notify Team (Success)
        if: success()
        run: |
          echo "iOS Build &amp; Release PASSED"
          echo "Environment: ${{ steps.env.outputs.ENV }}"
          echo "Branch: ${{ steps.env.outputs.branch }}"
          echo "By: @${{ github.actor }}"
          echo "Commit: ${{ github.sha }}"

      - name: Notify Team (Failure)
        if: failure()
        run: |
          echo "iOS Build &amp; Release FAILED"
          echo "Environment: ${{ steps.env.outputs.ENV }}"
          echo "Branch: ${{ steps.env.outputs.branch }}"
          echo "By: @${{ github.actor }}"
          echo "Commit: ${{ github.sha }}"
          echo "Check the logs and fix the issue before retrying 🔧"
</code></pre>
<p>Let's walk through what is different about this workflow compared to that of android.</p>
<h3 id="heading-1-macos-runner">1. MacOS Runner</h3>
<pre><code class="language-yaml">runs-on: macos-latest
</code></pre>
<p>This is the major difference.</p>
<p>iOS builds require Xcode, which only runs on macOS. GitHub Actions provides hosted macOS runners, but they are significantly more expensive in terms of compute minutes than Ubuntu runners. Just keep that in mind when thinking about build frequency.</p>
<p>No Java setup is needed here. Flutter on iOS compiles through Xcode directly, so the toolchain requirements are different.</p>
<h3 id="heading-2-installing-fastlane">2. Installing Fastlane</h3>
<pre><code class="language-yaml">- name: Install Fastlane
  run: |
    cd ios
    gem install bundler
    bundle install
</code></pre>
<p>Fastlane is a Ruby-based automation tool that handles certificate management, building, and uploading to TestFlight and the App Store.</p>
<p>This step navigates into the <code>ios/</code> directory and installs Fastlane along with all its dependencies as defined in the project's <code>Gemfile</code>.</p>
<p>Your <code>ios/Gemfile</code> should look something like this:</p>
<pre><code class="language-ruby">source "https://rubygems.org"

gem "fastlane"
</code></pre>
<p>And your <code>ios/fastlane/Fastfile</code> should define at minimum two lanes: one for staging (TestFlight) and one for production (App Store):</p>
<pre><code class="language-ruby">default_platform(:ios)

platform :ios do
  lane :beta do
    build_app(scheme: "Runner", export_method: "app-store")
    upload_to_testflight(skip_waiting_for_build_processing: true)
  end

  lane :release do
    build_app(scheme: "Runner", export_method: "app-store")
    upload_to_app_store(force: true, skip_screenshots: true, skip_metadata: true)
  end
end
</code></pre>
<h3 id="heading-3-certificate-and-provisioning-profile-setup">3. Certificate and Provisioning Profile Setup</h3>
<p>This is the step that trips most teams up the first time. Apple's code signing requires two things to be present on the machine:</p>
<ol>
<li><p>The signing certificate (a <code>.p12</code> file)</p>
</li>
<li><p>The provisioning profile</p>
</li>
</ol>
<p>Both are stored as Base64-encoded GitHub secrets and restored at build time.</p>
<pre><code class="language-yaml">- name: Import signing certificate
  if: steps.env.outputs.ENV != 'dev'
  run: |
    echo "${{ secrets.IOS_CERTIFICATE_BASE64 }}" | base64 --decode &gt; ios/cert.p12
    security create-keychain -p "" build.keychain
    security import ios/cert.p12 -k build.keychain -P "${{ secrets.IOS_CERTIFICATE_PASSWORD }}" -T /usr/bin/codesign
    security list-keychains -s build.keychain
    security default-keychain -s build.keychain
    security unlock-keychain -p "" build.keychain
    security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
</code></pre>
<p>Breaking this down step by step:</p>
<ul>
<li><p>Decodes the Base64 certificate and write it to <code>cert.p12</code></p>
</li>
<li><p>Creates a temporary keychain called <code>build.keychain</code> with an empty password</p>
</li>
<li><p>Imports the certificate into that keychain, granting codesign access</p>
</li>
<li><p>Sets it as the default keychain so Xcode finds it automatically</p>
</li>
<li><p>Unlocks the keychain so it can be used non-interactively</p>
</li>
<li><p>Sets partition list to allow access without repeated prompts</p>
</li>
</ul>
<p>The provisioning profile step is simpler:</p>
<pre><code class="language-yaml">- name: Install provisioning profile
  if: steps.env.outputs.ENV != 'dev'
  run: |
    echo "${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}" | base64 --decode &gt; profile.mobileprovision
    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
    cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
</code></pre>
<p>It decodes the profile and copies it into the exact directory where Xcode expects to find provisioning profiles on any macOS system.</p>
<p>To encode your certificate and profile locally, you can run these:</p>
<pre><code class="language-bash">base64 -i Certificates.p12 | pbcopy   # for the certificate
base64 -i YourApp.mobileprovision | pbcopy   # for the provisioning profile
</code></pre>
<h3 id="heading-4-building-for-each-environment">4. Building for Each Environment</h3>
<p>Dev builds skip signing entirely. They're built without code signing just to verify the project compiles correctly on a clean machine:</p>
<pre><code class="language-yaml">- name: Build iOS (dev)
  if: steps.env.outputs.ENV == 'dev'
  run: flutter build ios --release --no-codesign
</code></pre>
<p>Staging builds go through Fastlane's <code>beta</code> lane, which builds and uploads to TestFlight. Production builds go through Fastlane's <code>release</code> lane, which submits directly to App Store Connect.</p>
<p>Both staging and production steps consume the same three App Store Connect API key secrets: the key ID, the issuer ID, and the key content itself.</p>
<p>Fastlane uses these to authenticate with Apple's API without requiring a manual Apple ID login.</p>
<h3 id="heading-5-sentry-symbol-upload">5. Sentry Symbol Upload</h3>
<p>On production iOS builds, the <code>upload_symbols.sh</code> script runs after the build completes, passing the current short commit SHA as the release identifier:</p>
<pre><code class="language-yaml">- name: Upload Sentry symbols (production only)
  if: steps.env.outputs.ENV == 'production'
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
    SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
    SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
  run: ./scripts/upload_symbols.sh $(git rev-parse --short HEAD)
</code></pre>
<p>This is the same script explained earlier in the helper scripts section. It creates a Sentry release, uploads the debug information files, and finalizes the release. Any production crash from this point forward will map back to real, readable source code in your Sentry dashboard.</p>
<h2 id="heading-secrets-and-configuration-reference">Secrets and Configuration Reference</h2>
<p>For this entire pipeline to work, you need to configure the following secrets in your GitHub repository. Go to <strong>Settings → Secrets and variables → Actions → New repository secret</strong> to add each one.</p>
<p><strong>Shared (used across environments):</strong></p>
<table>
<thead>
<tr>
<th>Secret</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>FIREBASE_TOKEN</code></td>
<td>Generated via <code>firebase login:ci</code> on your local machine</td>
</tr>
<tr>
<td><code>FIREBASE_ANDROID_APP_ID</code></td>
<td>Android app ID from your Firebase console</td>
</tr>
<tr>
<td><code>FIREBASE_GROUPS</code></td>
<td>Comma-separated tester group names in Firebase</td>
</tr>
<tr>
<td><code>SENTRY_AUTH_TOKEN</code></td>
<td>Auth token from your Sentry account settings</td>
</tr>
<tr>
<td><code>SENTRY_ORG</code></td>
<td>Your Sentry organization slug</td>
</tr>
<tr>
<td><code>SENTRY_PROJECT</code></td>
<td>Your Sentry project slug</td>
</tr>
</tbody></table>
<p><strong>Staging:</strong></p>
<table>
<thead>
<tr>
<th>Secret</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>STAGING_BASE_URL</code></td>
<td>Your staging API base URL</td>
</tr>
<tr>
<td><code>STAGING_API_KEY</code></td>
<td>Your staging API or encryption key</td>
</tr>
</tbody></table>
<p><strong>Production:</strong></p>
<table>
<thead>
<tr>
<th>Secret</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>PROD_BASE_URL</code></td>
<td>Your production API base URL</td>
</tr>
<tr>
<td><code>PROD_API_KEY</code></td>
<td>Your production API or encryption key</td>
</tr>
</tbody></table>
<p><strong>Android:</strong></p>
<table>
<thead>
<tr>
<th>Secret</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>ANDROID_KEYSTORE_BASE64</code></td>
<td>Base64-encoded <code>.jks</code> keystore file</td>
</tr>
<tr>
<td><code>GOOGLE_PLAY_SERVICE_ACCOUNT_JSON</code></td>
<td>Full JSON content of your Play Console service account</td>
</tr>
</tbody></table>
<p><strong>iOS:</strong></p>
<table>
<thead>
<tr>
<th>Secret</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>IOS_CERTIFICATE_BASE64</code></td>
<td>Base64-encoded <code>.p12</code> signing certificate</td>
</tr>
<tr>
<td><code>IOS_CERTIFICATE_PASSWORD</code></td>
<td>Password protecting the <code>.p12</code> file</td>
</tr>
<tr>
<td><code>IOS_PROVISIONING_PROFILE_BASE64</code></td>
<td>Base64-encoded <code>.mobileprovision</code> file</td>
</tr>
<tr>
<td><code>APP_STORE_CONNECT_API_KEY_ID</code></td>
<td>Key ID from App Store Connect → Users &amp; Access → Keys</td>
</tr>
<tr>
<td><code>APP_STORE_CONNECT_API_ISSUER_ID</code></td>
<td>Issuer ID from the same App Store Connect page</td>
</tr>
<tr>
<td><code>APP_STORE_CONNECT_API_KEY_CONTENT</code></td>
<td>The full content of the downloaded <code>.p8</code> key file</td>
</tr>
</tbody></table>
<p>None of these values should ever appear in your codebase. If any secret is accidentally committed, rotate it immediately.</p>
<h2 id="heading-end-to-end-flow">End-to-End Flow</h2>
<p>With all three workflows in place, here is exactly what happens from the moment a developer opens a pull request to the moment a user receives an update:</p>
<h3 id="heading-1-developer-opens-a-pr-into-develop">1. Developer Opens a PR into <code>develop</code></h3>
<p>The <code>pr_checks.yml</code> workflow fires. It runs formatting checks, static analysis, and the full test suite. If anything fails, the PR cannot be merged and the team is notified immediately. The developer fixes the issues and pushes again, which triggers a fresh run.</p>
<h3 id="heading-2-pr-is-approved-and-merged-into-develop">2. PR is Approved and Merged into <code>develop</code></h3>
<p>The <code>android.yml</code> and <code>ios.yml</code> workflows both fire on the push event. They detect the environment as <code>dev</code>, inject placeholder config, build unsigned artifacts, and upload them to Firebase App Distribution. Testers receive an email and can install the build on their devices within minutes – no one shared a file manually.</p>
<h3 id="heading-3-develop-is-merged-into-staging">3. <code>develop</code> is Merged into <code>staging</code></h3>
<p>Both platform workflows fire again. This time the environment resolves to <code>staging</code>. Real secrets are injected, builds are properly signed, and the artifacts go to Firebase App Distribution (Android) and TestFlight (iOS). QA begins testing the staging build against the staging API.</p>
<h3 id="heading-4-staging-is-merged-into-production">4. <code>staging</code> is merged into <code>production</code></h3>
<p>Both workflows fire one final time. Production secrets are injected, builds are obfuscated and signed, debug symbols are uploaded to Sentry, and the final artifacts are submitted to the Google Play Store and App Store Connect. The release goes live on Apple and Google's review timelines with no further human intervention required.</p>
<p>From that first PR to a production submission, not a single command was run manually.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building this pipeline is an upfront investment that pays off from the very first release cycle. What used to be a sequence of error-prone manual steps building locally, signing, uploading, switching configs, and hoping nothing was mixed up is now a fully automated, auditable, and repeatable process that runs the moment code moves between branches.</p>
<p>The architecture we built here does more than just automate builds. The PR quality gate enforces team standards consistently, so code review becomes a conversation about intent rather than a hunt for formatting issues. The environment-aware config injection eliminates an entire class of production incidents where staging keys made it into a live release. The Sentry symbol upload means your team can debug production crashes with full source visibility even from an obfuscated binary.</p>
<p>Every piece of this pipeline also runs locally. The helper scripts in the <code>scripts/</code> folder are plain Bash so you can call them from your terminal the same way CI calls them. This eliminates the frustrating cycle of pushing a commit just to test a pipeline change.</p>
<p>As your team grows, this foundation scales with you. You can extend the <code>pr_checks.yml</code> to enforce code coverage thresholds, add a performance benchmarking job, or introduce a dedicated security scanning step. You can extend the platform workflows to support multiple flavors, multiple Firebase projects, or staged rollouts on the Play Store. The architecture stays the same – you're just adding new steps to an already working system.</p>
<p>This ensures that standards are met, code quality remains high, you have a proper team structure, clear process and automated post development activities are in place – and at the end of the day, you'll have an optimized engineering approach that will help your team in so many ways.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Key Metrics That Can Make or Break Your Startup ]]>
                </title>
                <description>
                    <![CDATA[ If you’ve built something worth pitching – something more than a fancy hobby with a login screen – you need to know your numbers. Not "I’ll get back to you" know them, know them like you know your co-founder's coffee order. I have seen too many found... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/key-metrics-that-can-make-or-break-your-startup/</link>
                <guid isPermaLink="false">6894eea1ba8a1138e41767a1</guid>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[ finance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Founder ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ideas ]]>
                    </category>
                
                    <category>
                        <![CDATA[ business ]]>
                    </category>
                
                    <category>
                        <![CDATA[ beginner ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Business and Finance  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ metrics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Product Management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #reporting ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ycombinator ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aditya Vikram Kashyap ]]>
                </dc:creator>
                <pubDate>Thu, 07 Aug 2025 18:21:21 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754590848364/2e68c07e-d5d8-4da7-bc41-3798c991bfbc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you’ve built something worth pitching – something more than a fancy hobby with a login screen – you need to know your numbers. Not "I’ll get back to you" know them, know them like you know your co-founder's coffee order.</p>
<p>I have seen too many founders who are smart, legit, and ambitious get ghosted by investors simply because they couldn't walk through their unit economics.</p>
<p>It's not personal. It's math.</p>
<p>So here it is: Numbers that will either carry your pitch or quietly kill it, explained by someone who has sat through them time and time again, with examples, and no fluff.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-burn-rate-how-fast-are-you-lighting-your-cash-on-fire">1. Burn Rate: How Fast Are You Lighting Your Cash on Fire?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-cash-runway-how-long-before-you-run-out-of-cash">2. Cash Runway: How Long Before You Run Out of Cash?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-cac-customer-acquisition-cost-how-much-does-it-cost-to-convince-someone-to-pay-you">3. CAC (Customer Acquisition Cost): How Much Does it Cost to Convince Someone to Pay You?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-customer-lifetime-value-ltv-how-much-is-one-customer-worth-over-time">4. Customer Lifetime Value (LTV): How Much is One Customer Worth Over Time?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-gross-profit-margin-what-do-you-actually-keep-after-delivering-your-service-or-product">5. Gross Profit Margin: What Do You Actually Keep After Delivering Your Service or Product?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-monthly-annual-recurring-revenue-mrr-arr">6. Monthly / Annual Recurring Revenue (MRR / ARR)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-7-churn-rate-how-fast-are-your-users-leaving">7. Churn Rate: How Fast Are Your Users Leaving</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-8-payback-period-how-long-before-you-recover-your-cac">8. Payback Period: How Long Before You Recover Your CAC?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-9-earnings-before-interest-taxes-depreciation-and-amortization-ebitda">9. Earnings Before Interest, Taxes, Depreciation, and Amortization (EBITDA)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-10-valuation-whats-your-company-worth-and-what-supports-that-number">10. Valuation: What’s Your Company Worth – and What Supports that Number?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-real-talk-before-you-close-that-tab">Real Talk Before You Close That Tab</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-real-experience">Real Experience</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-this-matters">Why This Matters</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion-and-final-thoughts">Conclusion and Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-1-burn-rate-how-fast-are-you-lighting-your-cash-on-fire">1. Burn Rate: How Fast Are You Lighting Your Cash on Fire?</h2>
<p>Burn rate is the speed at which a startup is spending its cash. Basically, how fast are you consuming your venture capital to cover over overhead until you generate positive cash flow from operations? It’s a measure of negative cash flow.</p>
<p>If you’re spending $80K a month to keep the lights on (payroll, AWS, your workspace snacks, and so on), that’s how much cash you’re burning each month. But many startups calculate two different burn rates: gross burn (how much cash you’re spending, ignoring any revenue), and net burn (monthly operating experiences minus any cash you take in each month). Net burn basically measures how fast your cash is shrinking, and it’s often what investors care more about.</p>
<p>Real talk: investors want to know when the plane runs out of fuel before they board. Thats what this metric helps them understand – how fast you’re going through the money you have.</p>
<p>At some point, if a company has a high burn rate, it has to reduce structural costs by cutting expenditures on labor, rent, marketing, and/or capital equipment. The burn rate is an important metric for any company, but it's particularly important for startups that aren't yet generating revenue. It tells managers and investors how fast the company is spending its capital</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=BpS3shZI35A">Watch this video</a> to understand more about Burn Rate.</p>
<h2 id="heading-2-cash-runway-how-long-before-you-run-out-of-cash">2. Cash Runway: How Long Before You Run Out of Cash?</h2>
<p>Cash runway tells how long a startup can continue to operate at a certain burn rate until they are out of cash. For startups without revenue, you can calculate this by dividing available cash by total monthly expenses. Available cash is defined as the funds that are accessible now or can be accessed at a later time relatively quickly to pay for expenses.</p>
<p>When you’re making this calculation, it’s important to not include any anticipated fundraising and other uncertain sources of capital.</p>
<p>Actively managing cash runway is crucial for startup survival and growth. With a significant percentage of startups failing due to cash shortages, founders need to closely monitor their cash burn rate and runway.</p>
<p>The length of runway needed varies based on factors including the startup’s stage, industry, and milestones. In tighter venture capital markets, startups should plan for longer runways and consider strategies such as increasing revenue, reducing expenses, or raising additional capital. Regularly updating financial models and understanding metrics like the burn multiple can help you make informed decisions to extend your runway and align your growth ambitions with financial stability.</p>
<p>While it’s a simple calculation at face value, a cash runway analysis is nuanced and unique to every startup and can be impacted by a multitude of circumstances.</p>
<p>To calculate this, you just divide your total cash reserves by the amount you’re spending each month. Say you’ve got $250K in the bank and you’re spending $50K/month: 250/50 = 5. So you’ve got 5 months. Not 6. Not “it depends” – 5. That’s your runway.</p>
<p>Investors ask “If we don’t fund you, how long do you survive?” If you don’t know that answer, you're not fundraising – you’re freelancing with hope.</p>
<p><a target="_blank" href="https://youtu.be/vtaMwtQgFGE?si=-Hcf_h_LxKYChdBa">Here is a video</a> that explains cash runway with real world examples and the thought process behind it.</p>
<p>And <a target="_blank" href="https://www.jpmorgan.com/insights/business-planning/does-your-startup-have-enough-runway-to-survive">here’s an article</a> from JP Morgan breaking down cash runway, its importance, and what can you to to maximize it.</p>
<h3 id="heading-burn-rate-vs-runway">Burn Rate vs Runway</h3>
<p>So, let’s just make this super clear: burn rate is simply how much you spend each month to run your operation – that is, your negative cash flow. Runway is how many months there are left before your bank balance reaches zero.</p>
<p>So again, why do these numbers matter?</p>
<p>Because burn rate tells you how quickly you need to find more revenue or funding. Runway tells investors whether you are going to still be around by the time they finish their due diligence.</p>
<p>They are not just numbers. They are your survival clock.</p>
<p>Smart founders utilize these metrics to:</p>
<ul>
<li><p>Trim the fat without cutting muscle – know what to focus on and what to let go</p>
</li>
<li><p>Forecast hiring/fundraising deadlines – know the process and prep for it. Numbers don’t line but they sure can get you ghosted.</p>
</li>
<li><p>Assure investors you’re not going to come knocking again in 90 days – establishing credibility is key, make an investor realize its not just a hobby, you mean business.</p>
</li>
</ul>
<p>The goal: Extend runway without stalling momentum. Keep the plane in the air, while building a bigger engine.</p>
<h2 id="heading-3-cac-customer-acquisition-cost-how-much-does-it-cost-to-convince-someone-to-pay-you">3. CAC (Customer Acquisition Cost): How Much Does it Cost to Convince Someone to Pay You?</h2>
<p>Cost of acquisition refers to the entire cost that a business incurs to obtain a new client or asset. This includes the purchase price, shipping, installation, and marketing costs for the asset acquired. CAC takes into account the total expenditure on all marketing, advertising, and sales for the period, which you then divide by the number of new customers for the period.</p>
<p>In this case, all the upfront costs incurred to purchase a business asset, including equipment or inventory, are part of the cost of acquisition. Cost of acquisition includes:</p>
<ul>
<li><p>Purchase price of the item</p>
</li>
<li><p>Costs to ship it to its point of use</p>
</li>
<li><p>Costs to install the item</p>
</li>
<li><p>Costs to get it up and running (in the case of equipment) or ready for sale (in the case of inventory) condition</p>
</li>
<li><p>Marketing sales teams salaries</p>
</li>
<li><p>All sales and consulting marketing expenses geared to get new consumers should all be included</p>
</li>
</ul>
<p><strong>Formula:</strong><br>CAC = (Total Marketing + Sales Expenses) / Number of New Customers Acquired</p>
<p>Say you spent $10K last month across paid ads, content creation, outbound campaigns, and sales team costs. You onboarded 100 new customers, so your CAC = $100.</p>
<p>But is that good?</p>
<p>It depends on:</p>
<ul>
<li><p>Your pricing model (one-time vs. subscription)</p>
</li>
<li><p>Your margin (how much of that sale do you actually keep?)</p>
</li>
<li><p>Your customer retention (how long do they stick around?)</p>
</li>
</ul>
<p>If you’re selling a $20 product once, a $100 CAC is a non-starter. But if that customer brings in $50/month for 12 months, you’ve got a solid return.</p>
<p><strong>Watch for red flags:</strong></p>
<ul>
<li><p>CAC is rising but revenue isn’t</p>
</li>
<li><p>You’re overly reliant on paid ads (especially if organic/referral is flat)</p>
</li>
<li><p>You don’t know CAC by channel (averages hide leaks)</p>
</li>
</ul>
<p>A healthy CAC is one that pays itself back quickly and can be improved over time as you optimize funnels and messaging</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=KFJ3ip30QPM">Here is a video</a> that breaks down CAC for you.</p>
<h2 id="heading-4-customer-lifetime-value-ltv-how-much-is-one-customer-worth-over-time">4. Customer Lifetime Value (LTV): How Much is One Customer Worth Over Time?</h2>
<p>Customer Lifetime Value is the average monetary value of each customer to your business. LTV takes into account how much a unique customer is expected to spend with your business. It’s an important metric so you know how much new customers are worth to your business over their lifespan as a customer.</p>
<p>Let’s say you charge $25/month. The average customer sticks around 12 months.<br>LTV = $300.</p>
<p>In this case, if your CAC is $80? You’re in the green. But if it’s $350? You’re basically paying people to hang out (and losing money on them).</p>
<p>Now, let’s connect this to CAC.</p>
<p>Say your CAC is $80. You’re doing fine – your LTV is ~4x CAC. That’s what investors want to see.</p>
<p>Rule of thumb: you want your LTV to be at least 3x your CAC. A 1:1 ratio means you’re barely breaking even, before operational costs and the math stops working at scale. So if you can hit a 3:1 ratio, great – and based off my experience, your business will be much more appealing if it’s closer to 5:1.</p>
<p>And keep in mind that different models can have different thresholds. For example, a SaaS company with low churn can afford higher CACs, while an e-commerce platform might need faster payback. And marketplaces and freemium models may have lower LTV per user, but they can often more easily offset it with volume.</p>
<p>If you don’t know your LTV or can’t defend it with data, it becomes hard to justify spend – and easy for investors to walk.</p>
<p>If you want to know more, <a target="_blank" href="https://www.youtube.com/watch?v=vA1YX8963ts">this video</a> walks you through the basics.</p>
<p>And <a target="_blank" href="https://www.youtube.com/watch?v=773zBQVPx_Q">here’s a video</a> that beautifully explains the CAC and LTV relationship.</p>
<h2 id="heading-5-gross-profit-margin-what-do-you-actually-keep-after-delivering-your-service-or-product">5. Gross Profit Margin: What Do You Actually Keep After Delivering Your Service or Product?</h2>
<p>Gross profit margin shows the amount of money a business collects after it pays for all its expenses. It’s usually calculated as a percentage of sales. This specific metric is also referred to as the gross margin ratio.</p>
<p>Companies use gross margin as a measure of how production costs relate to revenue. If a company's gross margin falls because it is making less revenue, it may try to cut labor costs, find cheaper suppliers of materials, or increase prices to increase revenue.</p>
<p>Gross profit margins can also allow a business to measure how efficient a company is, or compare two very differently sized companies that share a common revenue stream or product</p>
<p>If you sell a subscription for $50/month and it costs you $10/month to host, maintain, and support it, your gross margin is 80%.</p>
<ul>
<li><p>Good: SaaS companies often hit 70–90%.</p>
</li>
<li><p>Bad: If you're below 30%, your "scalable" business will collapse under weight.</p>
</li>
</ul>
<p>Want to know the conceptual math behind this metric and how it differs from Profit Margin? <a target="_blank" href="https://www.youtube.com/watch?v=9xAMe0QBFhU&amp;t=45s">Here is a fantastic video</a> that easily breaks it down.</p>
<h2 id="heading-6-monthly-annual-recurring-revenue-mrr-arr">6. Monthly / Annual Recurring Revenue (MRR / ARR)</h2>
<p>Annual recurring revenue (ARR) is revenue a company expects to see from its product and service offerings, calculated over the course of a year. Companies that sell annual subscriptions like using ARR as a sales metric to track what they anticipate making in a year.</p>
<p>ARR tends to be used if companies sell a product or service in the software as a service (SaaS) space, but it can also be useful in terms of streaming services, cell phone bills, and (almost) anything else with a predictable, recurring charge.</p>
<p>ARR is calculated annually, whereas monthly recurring revenue (MRR) is calculated monthly. MRR is useful in that it shows what’s happening on a month-to-month basis. For example, if you change your price in April, you can see the immediate effects of that change in May. MRR also helps track fluctuations in revenue based on outside factors like holiday shopping seasons and economic conditions.</p>
<p>In a nutshell, Monthly / Annual Recurring Revenue = predictable income.</p>
<p>If you’re pulling $20K/month in subscriptions, that’s $240K ARR. Simple.</p>
<p>What investors care about:</p>
<ul>
<li><ul>
<li><p>Is it growing?</p>
<ul>
<li><p>How fast?</p>
</li>
<li><p>And how stable is it?</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><a target="_blank" href="https://youtu.be/qwo7WFWusO4?si=pCelTr2slb2OLuw9">Here is a founder breaking down the metric</a> and explaining the relationship between MRR/ARR.</p>
<h2 id="heading-7-churn-rate-how-fast-are-your-users-leaving">7. Churn Rate: How Fast Are Your Users Leaving</h2>
<p>The churn rate, also known as attrition rate, represents the rate at which a customer stops doing business with a company. Customer churn is typically expressed as the percentage of service subscribers that discontinue their service subscriptions within a time frame. Churn can also be expressed as the rate at which employees leave their jobs in a given time.</p>
<p>In order for a business to grow its number of clients, its growth rate (which takes into account new customers) must be higher than its churn rate.</p>
<p>The benefit of calculating a churn rate is that it can clarify how well a business is retaining its customers, which is a measure of the quality of service the business is providing and the usefulness of that service.</p>
<p>When a business can see its churn rate increasing from period to period, this suggests that a critical aspect of how it is running the business might be problematic or flawed.</p>
<p>It could be the result of:</p>
<ul>
<li><p>A faulty product(s)</p>
</li>
<li><p>Bad customer service</p>
</li>
<li><p>Costs exceed utility to customers</p>
</li>
</ul>
<p>And so on.</p>
<p>The churn rate will indicate to a business that it needs to learn why its customers are leaving, and where it needs to adjust its business. It’s more expensive to attract new customers than it is to retain them, so reducing the churn rate can save a business resources in the future.</p>
<p>Real talk: Say you had 500 users at the start of the month, and you lost 50 by the end of the month. That’s 10% churn – which is high! Annualize that and…ouch. You're not growing. You're replacing.</p>
<p>Make sure you fix this before you fundraise. Or at least explain why churn’s high and what you’re doing to plug the holes.</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=Jlg5J_Mpq7g">Here is a video</a> that beautifully explains Churn Rate.</p>
<h2 id="heading-8-payback-period-how-long-before-you-recover-your-cac">8. Payback Period: How Long Before You Recover Your CAC?</h2>
<p>The payback period is a popular tool for determining investment return. People invest money for the purpose of getting it back and generating a positive return on the money they invested. The shorter the payback period, the more beneficial the investment will be.</p>
<p>The payback period does not factor in the time value of money. You can determine it simply by counting the number of years until the principal paid in is returned.</p>
<p>This metric measures how quickly your customer pays you back for the cost of acquiring them. The payback period doesn’t take into account the total profitability of an investment. It’s just concerned with paying the investment back.</p>
<p>There are two common interpretations:</p>
<ol>
<li><p><strong>Customer-Level Payback:</strong> If your CAC is $250 and your customer pays $50/month, it’ll take 5 months to recover the acquisition cost.</p>
</li>
<li><p><strong>Investment-Level Payback:</strong> You spend $100,000 on a new sales hire, tool stack, or feature. You want to know how long it takes for that investment to generate $100,000 in profit.</p>
</li>
</ol>
<p>Both use the same principle: the shorter the payback period, the less cash you need to float your growth.</p>
<p>If you want a target, aim for 6 months for customer-level payback. Closer to 3-6 is ideal. Long payback periods mean you need deep pockets – or exceptional retention – to stay afloat.</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=KbtTk2azIjY">Here’s a video</a> where you can learn more.</p>
<h2 id="heading-9-earnings-before-interest-taxes-depreciation-and-amortization-ebitda">9. Earnings Before Interest, Taxes, Depreciation, and Amortization (EBITDA)</h2>
<p>EBITDA stands for Earning Before Interest, Taxes, Depreciation, and Amortization. You can think of this as just your company's operating profit if you wanted a very rudimentary way of referring to it.</p>
<p>It’s not flashy. It’s not fun. But it tells investors: “Here’s what we really make once the accounting fog clears.” and “Are we generating real profits from our actual operations?”</p>
<p>EBITDA is what investors look at because it is the best way of comparing apples to apples when considering startups. EBITDA can provide investors a measure of your operational health.</p>
<p>A negative EBITDA for an early stage company isn't going to raise any eyebrows. Just don’t act surprised when someone brings it up. You need to do that math before the pitch. But, if you have a growing early stage company that's moving from negative EBITDA to positive? Now your getting into grown folks business.</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=DH901SrBv9Q">Here’s a video</a> that explains the basics of EBITDA.</p>
<p>And <a target="_blank" href="https://www.youtube.com/watch?v=D58oCe_7BBM">here’s another video</a> that explains how investors look at EBITDA and its value in determining your business’s worth.</p>
<h2 id="heading-10-valuation-whats-your-company-worth-and-what-supports-that-number">10. Valuation: What’s Your Company Worth – and What Supports that Number?</h2>
<p>Valuation is a focused exercise that determines the value of an asset, investment, or company. So, how much your company’s worth. And the goal is typically to determine whether that value is a fair value.</p>
<p>Valuations can be conducted in one of two ways:</p>
<ul>
<li><p>an absolute valuation, which evaluates a company on its own merits and entirely independently of other factors/companies, or</p>
</li>
<li><p>a relative valuation, which evaluates the company relative to other similar firms, or assets, in the same sector or industry. This determines if the company, or asset, is worth that much relative to others.</p>
</li>
</ul>
<p>Depending on how the analysis and conclusions are reached, there are a variety of methods and techniques used to develop valuations. And as you’d expect, there’s often significant variability between outputs (or valuations) based on the inputs and context.</p>
<p>While valuations are predominantly quantitatively driven, there’s often a significant subjective influence that come from the assumptions and estimates made along the way. Valuations are also subject to developing situations and events outside of the analysis or the control of the analyst – for example, earnings reports or material news, or economic news – that can result in a change to a valuation stance.</p>
<p>If you’re pre-revenue and you’re saying $30M because a friend raised at that, please stop. Their experience likely has nothing to do with yours.</p>
<p>Valuation = traction + market comps + revenue + momentum + team.</p>
<p>Valuation isn’t just about what you want – it’s about what you can defend.</p>
<p>Startups are typically valued using:</p>
<ul>
<li><p><strong>Comparable Analysis (Comps):</strong> What similar companies are worth</p>
</li>
<li><p><strong>Discounted Cash Flow (DCF):</strong> Projecting future cash and discounting it back</p>
</li>
<li><p><strong>Revenue Multiples:</strong> Often 5x–10x for SaaS, but varies wildly</p>
</li>
<li><p><strong>Precedent Transactions:</strong> What investors paid in past rounds for similar startups</p>
</li>
</ul>
<p>But that’s the math.</p>
<p>Here’s the messy truth: <strong>Valuation = Traction + Team + TAM (total addressable market) + Timing + Storytelling.</strong></p>
<p>Hard factors:</p>
<ul>
<li><p>MRR/ARR</p>
</li>
<li><p>Growth rate</p>
</li>
<li><p>Churn</p>
</li>
<li><p>CAC:LTV</p>
</li>
<li><p>Gross margins</p>
</li>
</ul>
<p>Soft factors:</p>
<ul>
<li><p>Founding team’s track record</p>
</li>
<li><p>Market momentum</p>
</li>
<li><p>Hype or scarcity</p>
</li>
</ul>
<p>Don’t inflate. Don’t anchor to your friend’s raise. Know your comps. And show why <em>your</em> model is defensible, not just desirable. Inflated numbers make investors run. They don’t correct you – they just ghost you.</p>
<p>There are numerous books written on valuation and each technique could be its own PhD. But my role here is to give you a sneak peak into the metrics.</p>
<p>Here’s a <a target="_blank" href="https://www.youtube.com/watch?v=T3Ud5WQCrzQ">basic video on valuation</a> if you’re interested in a deeper dive.</p>
<p>And <a target="_blank" href="https://www.youtube.com/watch?v=znmQ7oMiQrM&amp;list=PLUkh9m2BorqnKWu0g5ZUps_CbQ-JGtbI9">here’s a more detailed video course</a> outlining different forms of valuation. Professor Damodaran from New York University is considered to be one of the aces and thought leaders when it comes to valuation. In this video course he explains stepwise and beautifully so you can understand and explore the fascinating world of valuations.</p>
<h2 id="heading-real-talk-before-you-close-that-tab">Real Talk Before You Close That Tab</h2>
<h3 id="heading-real-experience">Real Experience</h3>
<p>I met a founder once – early days, rough product, but you could tell he actually cared. He wasn't trying to look good. No buzzwords. No "disrupt" talk. Just someone trying to solve something annoying and important.</p>
<p>He walked into the room with a twinkle. Not swagger – just that gentle intensity. We were leaning in.</p>
<p>Then, in the middle of the pitch, someone asked, "So what's your monthly burn?" And I swear to you, he said, "Umm... I think my co-founder has that. I haven't looked in a while."</p>
<p>That was it.</p>
<p>No freak out. No awkward pause. Just... a cluck. Like a window closing in the background.</p>
<p>The product? Still smart. But the moment? Gone.</p>
<p>Nobody was mad. Nobody laughed. We even said thank you. But nobody followed up.</p>
<p>Why? Because it didn't feel like a business. It felt like a maybe.</p>
<h3 id="heading-why-this-matters">Why This Matters</h3>
<p>I’ve seen so many versions of that same scene play out. It’s never about charisma. It’s not even about the idea, half the time.</p>
<p>It’s about whether the person asking for money actually knows what they’re building. Not the dream, the mechanics. The guts, nuts and bolds of the business. The ugly Excel math nobody brags about on Twitter.</p>
<p>Unfortunately, no simple pitch deck will do that part for you. No co-founder can answer those questions on your behalf.</p>
<p>If it’s your vision, own the math. If it’s your company, learn the cost of keeping it alive.</p>
<p>The rest? The logos, the taglines, the “go-to-market” plans?.... All of that’s just packaging.</p>
<p>And you don’t have to be perfect either. You just have to be in it. Eyes open. Numbers in your head.<br>Because if you’re asking people to believe in what you’re building, you’d better believe in the scaffolding holding it up.</p>
<p>So yeah, know your CAC. Your LTV. Your margins. Your churn. Not to check some box on an investor’s sheet, but to prove to yourself and the investor that the thing you’re spending your life on…has legs. That it can stand. And run.</p>
<p>And maybe, someday, outlast you. Maybe!</p>
<h2 id="heading-conclusion-and-final-thoughts"><strong>Conclusion and Final Thoughts</strong></h2>
<p>I hope this was helpful to you, especially if you’re a founder or aspiring founder trying to build the next big thing. While there a many more ratios and concepts, these are the crux of them.</p>
<p>A lot of other complex ratios and valuations are either built using these metrics or refer them in some way. And each of these metrics could be an article of its own. But I wanted to give you my top 10 run down so that you could get a head start. Numbers are very much a part of the ideation stage itself, and omitting them from your strategy could prove to be a fatal mistake.</p>
<p>I’ll leave you with <a target="_blank" href="https://youtu.be/Pg72m3CjuK4?si=GtIFdvC5WzbKna79">one last video</a> on How to Start a Start Up with Michael Seibel (Reddit, YC, Twitch) that I hope you find valuable. It lays out, in a crash course format, the mindset of a founder who has been there and done that. The fun fact is that a lot of the themes he speaks of tie in to the metrics here, directly or indirectly.</p>
<p>I hope this gives you a perspective of being on the other side, evaluating your hard work and passion, and I hope it sets you up for success in your next Investor Review.</p>
<p>I look forward to your thoughts, comments, and feedback. If this was helpful, engaging, and informative, do share it – you never know who may need it, or could benefit from it. I wish you all the very best in your funding rounds.</p>
<p>Until then, keep learning, unlearning, and relearning, folks.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Beginner Developer's Guide to Kanban ]]>
                </title>
                <description>
                    <![CDATA[ First, a confession: When I was learning to code, my “workflow” was a mess. Sticky notes. Google Docs. Random Trello boards I never checked again. And a to-do list that somehow never got any shorter. Then I joined a real team. Suddenly, I was introdu... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/a-beginner-developers-guide-to-kanban/</link>
                <guid isPermaLink="false">68815e6054ad71fa4b3b7bb6</guid>
                
                    <category>
                        <![CDATA[ agile ]]>
                    </category>
                
                    <category>
                        <![CDATA[ agile methodology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Agile Software Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Career ]]>
                    </category>
                
                    <category>
                        <![CDATA[ interview ]]>
                    </category>
                
                    <category>
                        <![CDATA[ kanban ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kanban boards ]]>
                    </category>
                
                    <category>
                        <![CDATA[ project management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Product Management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Product Design ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Developer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ workflow ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aditya Vikram Kashyap ]]>
                </dc:creator>
                <pubDate>Wed, 23 Jul 2025 22:12:48 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753300952223/508231c9-f0bc-4aa8-9c97-5ad4157891b9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>First, a confession<strong>:</strong> When I was learning to code, my “workflow” was a mess. Sticky notes. Google Docs. Random Trello boards I never checked again. And a to-do list that somehow never got any shorter.</p>
<p>Then I joined a real team.</p>
<p>Suddenly, I was introduced to this thing called <strong>Kanban</strong> – and I realized I’d been treating software like a solo art project, not a process.</p>
<p>If that sounds familiar, you’re in the right place.</p>
<p>This guide will walk you through <strong>how Kanban actually works</strong>, how developers use it to track and prioritize work, and how it can help you stay sane when juggling bugs, features, and real-world deadlines.</p>
<p>Without further delay, lets get into it.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-so-what-is-kanban">So… What Is Kanban?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-classic-kanban-board-three-simple-columns">The Classic Kanban Board: Three Simple Columns</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-developers-use-kanban-in-real-life">How Developers Use Kanban in Real Life</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-kanban-vs-scrum-whats-the-difference">Kanban vs Scrum: What’s the Difference?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-so-which-one-should-you-use-scrum-or-kanban">So which one should you use Scrum or Kanban?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-tools-do-teams-use-for-kanban">What Tools Do Teams Use for Kanban?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-kanban-to-manage-your-own-coding-projects">How to Use Kanban to Manage Your Own Coding Projects</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts-why-kanban-isnt-just-a-board">Final Thoughts: Why Kanban Isn’t Just a Board</a></p>
</li>
</ul>
<h2 id="heading-so-what-is-kanban">So… What Is Kanban?</h2>
<p>At its core, Kanban is a <strong>visual way to manage work</strong>. It helps teams (or team members) see:</p>
<ul>
<li><p>What needs to get done</p>
</li>
<li><p>What’s in progress</p>
</li>
<li><p>What’s finished</p>
</li>
<li><p>Where things are getting stuck</p>
</li>
</ul>
<p>The concept comes from lean manufacturing, but in tech, it’s often used in Agile teams that need flexibility without the structure of Scrum sprints.</p>
<p>Think of Kanban like a whiteboard that tells a story. Not just what’s done, but how work flows.</p>
<h2 id="heading-the-classic-kanban-board-three-simple-columns">The Classic Kanban Board: Three Simple Columns</h2>
<p>So what exactly is a Kanban board? At its core, it’s a visual representation of your workflow – a board that shows all the work your team (or you, solo warrior) are juggling, and where each task stands.</p>
<p>It can be physical, like an actual whiteboard with sticky notes that move from one column to the next. Or digital, using tools like Trello, Jira, GitHub Projects, or Notion. The key is that it’s visual and up-to-date. You can walk into a room or open a tab and instantly understand: What’s being worked on? What’s ready to go? Where are things stuck?</p>
<p>It’s like having your brain on a wall, but organized. And slightly less chaotic.</p>
<p>The beauty of Kanban is how dead simple it is to get started. At minimum, your board has three columns:</p>
<table><tbody><tr><td><p><strong>&nbsp;To Do</strong></p></td><td><p><strong>In Progress</strong></p></td><td><p><strong>Done</strong></p></td></tr></tbody></table>

<p>Each task – or <strong>card</strong> – moves from left to right as it gets worked on.</p>
<p>Let’s say your team is building a blog platform. Your Kanban board might have cards like:</p>
<ul>
<li><p>“Create signup form”</p>
</li>
<li><p>“Fix image upload bug”</p>
</li>
<li><p>“Deploy staging build”</p>
</li>
</ul>
<p>Now, while Kanban is flexible, it can absolutely be taken too far.</p>
<p>I’ve seen boards with more columns than a Greek temple: “Needs Review,” “Pending Client Feedback,” “QA Rework Round 2,” “Blocked but Still Hopeful,” “In Existential Limbo,” and so on. Every card had six tags, three owners, two checklists, and one migraine.</p>
<p>The lesson? Don’t turn your board into a bureaucratic jungle.</p>
<p>You don’t need to account for every edge case. Start simple: “To Do,” “In Progress,” “Review,” “Done.” These basic stages cover most workflows. If you discover a real need for something more – like a dedicated “QA” column or “Blocked” column – add it intentionally, not because you feel like your board needs to look fancy.</p>
<p>Remember: A Kanban board should be helpful, not overwhelming. If you spend more time managing the board than doing the work on it… it’s doing the opposite of what it’s meant to do.</p>
<h2 id="heading-how-developers-use-kanban-in-real-life">How Developers Use Kanban in Real Life</h2>
<p>Here’s how you might interact with a Kanban board on a dev team:</p>
<ol>
<li><p>You pick up a card from “To Do” – let’s say, “Add dark mode toggle.”</p>
</li>
<li><p>You move it to “In Progress.”</p>
</li>
<li><p>When it’s ready for review, you might move it to a temporary “Review” or “Testing” column.</p>
</li>
<li><p>Once it’s merged, tested, and deployed, you move it to “Done.”</p>
</li>
<li><p>You smile, drink some coffee, and grab the next card.</p>
</li>
</ol>
<p>That’s it. But over time, this process helps the whole team:</p>
<ul>
<li><p>Spot bottlenecks</p>
</li>
<li><p>Prevent duplicate work</p>
</li>
<li><p>Reduce context switching</p>
</li>
<li><p>Keep everyone aligned</p>
</li>
</ul>
<h3 id="heading-whats-a-wip-limit-and-why-should-you-care">What’s a WIP Limit — And Why Should You Care?</h3>
<p>WIP = <strong>Work In Progress</strong>. This is the most important concept to keep us in check.</p>
<p>One of Kanban’s key principles is <strong>limiting how many things you’re working on at once</strong>. Because guess what? Multitasking kills momentum.</p>
<p>A typical WIP limit might look like:</p>
<ul>
<li><p>No more than 2–3 cards per person in “In Progress” Again this is best practice, but folks do pick up a lot and then they end up being the bottleneck.</p>
</li>
<li><p>No more than 5 tasks waiting on QA.</p>
</li>
</ul>
<p>Why? Because when everything’s urgent, nothing gets done. WIP limits force you to finish one thing before you start more – and that’s how real velocity happens.</p>
<p>If there are more than 5 tasks in the “To Do” column, the team doesn’t take up new ones. Instead, everyone chips in to see how they can help unclog the bottleneck. A bottleneck is your worst enemy in Kanban, and you want to resolve it so items move smoothly on time and on target.</p>
<p><a target="_blank" href="https://youtu.be/R8dYLbJiTUE?si=Hh00XXI4_1urv4Mp">Here’s a video</a> recapping key concepts.</p>
<h2 id="heading-kanban-vs-scrum-whats-the-difference"><strong>Kanban vs Scrum: What’s the Difference?</strong></h2>
<p>You’ve probably heard Scrum and Kanban mentioned in the same breath – and both are popular Agile frameworks. But they’re not interchangeable.</p>
<p>Scrum is structured, with roles like Product Owner and Scrum Master, and work gets organized into time-boxed sprints. It’s perfect for teams that benefit from rhythm and rituals – like sprint planning, daily standups, and retrospectives.</p>
<p>Kanban, on the other hand, is a little looser. No official roles, no set sprint timelines. Work flows continuously, and change can happen anytime. It’s perfect for teams who need more flexibility and fewer ceremonies.</p>
<p>So how do they compare in practice? Let’s break it down:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Key Differentiating Factors</strong></td><td><strong>Scrum</strong></td><td><strong>Kanban</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Time-based</td><td>Yes – 1–2 week sprints</td><td>No – continuous flow</td></tr>
<tr>
<td>Roles</td><td>PO, SM, Developers</td><td>No specific roles required</td></tr>
<tr>
<td>Planning</td><td>Sprint planning, retros, and so on</td><td>On-demand, just-in-time</td></tr>
<tr>
<td>Cadence</td><td>Fixed sprint cycle</td><td>Flexible, ongoing</td></tr>
<tr>
<td>Use case</td><td>Complex, structured teams</td><td>Continuous delivery teams</td></tr>
</tbody>
</table>
</div><p><strong>Bottom line:</strong></p>
<ul>
<li><p>Scrum is a scheduled loop. Kanban is a living flow.</p>
</li>
<li><p>One’s a playbook. The other’s a status window.</p>
</li>
</ul>
<p><a target="_blank" href="https://youtu.be/F5QIqFEDv2k?si=jvNoAiHmrv_iq-Lx">Here’s a video</a> on the main differences between Scrum and Kanban you can watch if you want more detail.</p>
<h2 id="heading-so-which-one-should-you-use-scrum-or-kanban"><strong>So which one should you use Scrum or Kanban?</strong></h2>
<p>So… which one should you use?</p>
<p>It really depends on your team, your product, and your pain points.</p>
<p>✔️ If you’re working on a brand-new product where requirements shift a lot, and your team thrives with structure and routines – Scrum is likely the better fit. Sprints give you a sense of pacing, and ceremonies help ensure alignment.</p>
<p>✔️ If you’re managing ongoing work like bug triage, tech debt, infrastructure tasks, or anything that’s more “whenever it comes in” than “we need to ship this in two weeks” – Kanban gives you flexibility and visibility without the overhead.</p>
<p>And yes, there’s such a thing as <strong>Scrumban</strong> – a hybrid approach where teams use visual boards and WIP limits from Kanban, but keep some of Scrum’s structure like standups and retros. It’s like Agile tapas: you get the flavors that work best for your appetite.</p>
<p><a target="_blank" href="https://youtu.be/kiI3IweyAeQ?si=M1mtS5HCCcGcT78J">Here is a detailed video</a> that’'ll teach you more about how Scrumban works in practice.</p>
<p>Watch the Scrumban video only when you are familiar and comfortable with both Scrum and Kanban – otherwise, you might get confused from the cross-pollination of ideas and frameworks.</p>
<p>I personally have never seen a Scrumban implementation thats scaled well – too many folks trying too many things and none of them work. But thats just based on my experience – it may work for you and your team. I’ll let you be the judge.</p>
<h2 id="heading-what-tools-do-teams-use-for-kanban"><strong>What Tools Do Teams Use for Kanban?</strong></h2>
<p>You’ve probably seen (or used) one already:</p>
<ul>
<li><p><strong>Trello</strong> – Simple and great for solo or small teams</p>
</li>
<li><p><strong>Jira</strong> – Enterprise-level, customizable workflows</p>
</li>
<li><p><strong>GitHub Projects</strong> – Lightweight but powerful for devs</p>
</li>
<li><p><strong>ClickUp / Asana / Notion</strong> – Integrated with docs/tasks</p>
</li>
</ul>
<p>Kanban isn’t tied to any one tool – you can use an app, a browser tab, or a whiteboard and a pack of sticky notes from the office supply closet. What matters is how you use it. But let’s walk through some of the most common tools and what they offer in a Kanban context:</p>
<h3 id="heading-trello">🟩 <strong>Trello</strong></h3>
<p>Trello is probably the easiest way to start with Kanban. It gives you a simple digital board with columns and cards you can drag and drop. It’s great for devs or small teams who don’t need tons of automation – just a clean place to track work visually.</p>
<h3 id="heading-jira">🟨 <strong>Jira</strong></h3>
<p>Jira is a heavyweight – and while it’s built for Scrum, it also supports robust Kanban boards. You can define custom workflows, use built-in reports like cumulative flow diagrams, enforce WIP limits, and manage team velocity. Ideal for large teams that need traceability, integrations, and permissions.</p>
<h3 id="heading-github-projects">🟦 <strong>GitHub Projects</strong></h3>
<p>If your code lives in GitHub, GitHub Projects is a clean way to stay close to your codebase. It lets you create Kanban-style boards with issues and pull requests as cards, so you’re never toggling between tools just to track what’s in progress.</p>
<h3 id="heading-clickup-asana-notion">🟧 <strong>ClickUp / Asana / Notion</strong></h3>
<p>These are all-in-one productivity platforms. They combine Kanban boards with documentation, team chat, calendars, and reporting. If your team needs more than just “move card left to right,” these tools let you manage projects, meetings, notes, and workflows in one place.</p>
<h3 id="heading-whiteboard-sticky-notes">🟪 <strong>Whiteboard + Sticky Notes</strong></h3>
<p>Don’t underestimate the analog approach. It’s fast. It’s visible. It’s tactile. Physically moving a task from “Doing” to “Done” gives you a sense of progress no digital tool can match. And when something’s blocked? Slap a red sticky on it and call it a day.</p>
<p>Bottom line: The best tool is the one your team will <em>actually</em> use. Fancy doesn’t beat consistent. And the actual tool doesn’t matter as much as the <strong>discipline</strong> your team has to actually use it.</p>
<h2 id="heading-how-to-use-kanban-to-manage-your-own-coding-projects"><strong>How to Use Kanban to Manage Your Own Coding Projects</strong></h2>
<p>Even if you're not on a team yet, Kanban is great for your own workflow. Here’s how you can use it to help yourself out:</p>
<ol>
<li><p>Create a basic 3-column board (To Do, In Progress, Done)</p>
</li>
<li><p>Write out every task, big or small</p>
</li>
<li><p>Set a WIP limit (for example, no more than 2 tasks at once)</p>
</li>
<li><p>Update it daily. Make it a ritual.</p>
</li>
<li><p>Review your flow weekly – What got stuck? What moved fast?</p>
</li>
</ol>
<p> Example:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>To-Do</strong></td><td><strong>In Progress</strong></td><td><strong>Done</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Fix CSS Layout</td><td>Add blog search bar</td><td>Set up Netlify</td></tr>
<tr>
<td>Write README</td><td></td><td>Deploy v1</td></tr>
</tbody>
</table>
</div><p>You’ll be shocked how much clearer your thinking gets when you can <em>see</em> your work. It’s simple but super powerful to visualize your work it in this way.</p>
<h2 id="heading-final-thoughts-why-kanban-isnt-just-a-board"><strong>Final Thoughts: Why Kanban Isn’t Just a Board</strong></h2>
<p>Kanban isn’t just a tool – it’s a mindset.</p>
<p>It helps you focus. It helps your team collaborate. And it gives everyone – even non-technical folks – visibility into what’s going on.</p>
<p>If you’re learning to code and want to feel more confident working with others, <strong>learning Kanban is low-effort, high-impact</strong>.</p>
<p>So don’t wait until your first job. Start using it now – and show up to that standup with confidence.</p>
<p>I hope this small 101 Guide to Kanban was helpful to you all. My sole purpose to write this was to help beginner developers understand Kanban as a practical workflow system – especially for those transitioning from solo coding to collaborative, real-world development environments. It aims to demystify the methodology in a casual, beginner-friendly tone while still offering actionable guidance.</p>
<p>I hope you enjoyed my beginners guide to Kanban.</p>
<p>Until next time, keep Learning, Unlearning and Relearning, folks….</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Beginner Developer's Guide to Scrum ]]>
                </title>
                <description>
                    <![CDATA[ Let me guess: you’re learning to code…alone. You’ve been grinding through tutorials. You've built a portfolio site, maybe deployed a few projects on GitHub. And now you're trying to land a job or join a team. Then the interviews start. Suddenly, peop... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/a-beginner-developers-guide-to-scrum/</link>
                <guid isPermaLink="false">68813c7579e092b166d373b6</guid>
                
                    <category>
                        <![CDATA[ Scrum ]]>
                    </category>
                
                    <category>
                        <![CDATA[ agile development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ project management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Developer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ interview ]]>
                    </category>
                
                    <category>
                        <![CDATA[ guide ]]>
                    </category>
                
                    <category>
                        <![CDATA[ education ]]>
                    </category>
                
                    <category>
                        <![CDATA[ learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Product Management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Data Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Career ]]>
                    </category>
                
                    <category>
                        <![CDATA[ workflow ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aditya Vikram Kashyap ]]>
                </dc:creator>
                <pubDate>Wed, 23 Jul 2025 19:48:05 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753300058064/7046dd6c-1d9e-4f06-9ca1-65b3bb7eec83.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Let me guess: you’re learning to code…alone.</p>
<p>You’ve been grinding through tutorials. You've built a portfolio site, maybe deployed a few projects on GitHub. And now you're trying to land a job or join a team.</p>
<p>Then the interviews start.</p>
<p>Suddenly, people ask:</p>
<ul>
<li><p>"Are you familiar with Agile?"</p>
</li>
<li><p>"Have you worked in a Scrum environment?"</p>
</li>
<li><p>"What’s your experience with sprints?"</p>
</li>
</ul>
<p>Cue the imposter syndrome. Because no one teaches this stuff in JavaScript 101.</p>
<p>This guide is for you.</p>
<p>I’ll help make the Scrum process – a very common way developers work together – <em>make actual sense</em>. I’ll walk you through the basics, but also tell you what developers actually <em>do</em>, how standups feel when you're new, and what’s expected of you when you’re no longer coding in a vacuum.</p>
<p>Let’s break it down.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-even-is-scrum">What Even Is Scrum?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-three-roles-in-scrum-and-who-does-what">The Three Roles in Scrum (and Who Does What)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-scrum-rhythm-what-a-sprint-actually-looks-like">The Scrum Rhythm: What a Sprint Actually Looks Like</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-who-attends-the-ceremonies">Who attends the Ceremonies:</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-standups-where-you-talk-like-a-human-not-a-robot">Standups: Where You Talk Like a Human, Not a Robot</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sprint-planning">Sprint Planning</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-whats-a-user-story-and-why-does-it-sound-like-a-childrens-book">What’s a User Story and Why Does It Sound Like a Children’s Book?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-counts-as-done-definition-of-done-and-why-its-important">What Counts as “Done”? Definition of Done and Why It’s Important</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-demos-retros-and-saying-the-hard-things">Demos, Retros, and Saying the Hard Things</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tools-you-might-encounter">Tools You Might Encounter</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-if-youre-preparing-for-a-job-heres-what-you-can-do">If You’re Preparing for a Job, Here’s What You Can Do</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-what-even-is-scrum"><strong>What Even Is Scrum?</strong></h2>
<p>Scrum is not a tool. It’s not a software. It’s not some elite thing only PMs care about.</p>
<p>It’s a lightweight framework that helps software teams build things incrementally, together, in short focused cycles called sprints.</p>
<p>Scrum is used by everyone from FAANG teams to indie dev shops because it helps:</p>
<ul>
<li><p>Keep teams aligned</p>
</li>
<li><p>Deliver working software fast</p>
</li>
<li><p>Course-correct often</p>
</li>
<li><p>Spot problems early (before they go nuclear)</p>
</li>
</ul>
<p>It’s the opposite of the old-school “build for a year and pray it works” model.</p>
<h2 id="heading-the-three-roles-in-scrum-and-who-does-what"><strong>The Three Roles in Scrum (and Who Does What)</strong></h2>
<p>Scrum officially defines three roles. Here's what that means in practice:</p>
<h3 id="heading-1-product-owner-po"><strong>1. Product Owner (PO)</strong></h3>
<p>Think: Vision-holder. They decide <em>what</em> the team builds and <em>why</em>. A product owner:</p>
<ul>
<li><p>Writes user stories (think of these as feature requests written from a user’s point of view)</p>
</li>
<li><p>Prioritizes the work</p>
</li>
<li><p>Clarifies what success looks like</p>
</li>
<li><p>Says “yes” or “not yet” to features</p>
</li>
</ul>
<h3 id="heading-2-scrum-master-sm"><strong>2. Scrum Master (SM)</strong></h3>
<p>Think: Air-traffic controller meets therapist. They make sure the process works. The are master Facilitators, like between Dev and PO’s. A Scrum Master:</p>
<ul>
<li><p>Facilitates meetings</p>
</li>
<li><p>Removes blockers (“Your AWS access is stuck? I’ll escalate it.”)</p>
</li>
<li><p>Coaches the team on Scrum practices</p>
</li>
<li><p>Doesn’t manage people – manages <em>flow</em></p>
</li>
</ul>
<h3 id="heading-3-developers-you"><strong>3. Developers (YOU!)</strong></h3>
<p>Think: Builders. You write code, test it, ship it, fix it, and improve it. You also:</p>
<ul>
<li><p>Break down stories into tasks</p>
</li>
<li><p>Pick up work from the team board (like Jira or Trello)</p>
</li>
<li><p>Communicate progress</p>
</li>
<li><p>Demo what you’ve built at the end of the sprint</p>
</li>
</ul>
<p>You might also work with designers, testers, or DevOps folks – but within Scrum, you’re all “developers” building a product together.</p>
<h2 id="heading-the-scrum-rhythm-what-a-sprint-actually-looks-like"><strong>The Scrum Rhythm: What a Sprint Actually Looks Like</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752809790048/253fd92b-1ebe-4f3e-bfbc-48719676dc82.png" alt="253fd92b-1ebe-4f3e-bfbc-48719676dc82" class="image--center mx-auto" width="900" height="530" loading="lazy"></p>
<p>Image Source: <a target="_blank" href="https://www.invensislearning.com/blog/what-are-scrum-ceremonies/">https://www.invensislearning.com/blog/what-are-scrum-ceremonies/</a></p>
<h3 id="heading-understanding-the-scrum-cycle"><strong>Understanding the Scrum Cycle</strong></h3>
<p>So, what does it <em>actually</em> look like when a team uses Scrum to build software?</p>
<p>Let’s walk through a full sprint – not just the buzzwords, but what really happens when a group of humans tries to plan, build, review, and improve together. Think of this as your backstage pass to the rhythm of modern teamwork.</p>
<h3 id="heading-step-1-build-and-refine-the-product-backlog">📦 Step 1: Build and Refine the Product Backlog</h3>
<p>Before any coding starts, the team needs to agree on <em>what</em> they might build – not just this week, but in the near future too.</p>
<p>That’s where the <strong>Product Backlog</strong> comes in. This is a big, running list of everything the product might need – features, bug fixes, improvements, ideas, and maybe a few wild dreams. It’s like the wishlist for the product, but more organized (ideally).</p>
<p>The Product Owner is responsible for maintaining and prioritizing this list. They decide what’s most important to work on based on customer needs, business goals, and feedback.</p>
<p>But the PO doesn’t do this in isolation. Enter the <strong>Backlog Refinement meeting</strong>.</p>
<p>In these sessions, the Scrum Team – that’s the PO, the Scrum Master (SM), and the Developers – come together to:</p>
<ul>
<li><p><strong>Review</strong> the most important upcoming items</p>
</li>
<li><p><strong>Clarify</strong> any vague or confusing parts of each task</p>
</li>
<li><p><strong>Break big items</strong> down into smaller, buildable pieces called <strong>user stories</strong></p>
</li>
<li><p><strong>Estimate effort</strong> (how much time or complexity is involved for each story)</p>
</li>
</ul>
<p>This meeting makes sure the team isn’t caught off guard in the sprint – that they understand the work ahead and can actually start sprinting when the time comes.</p>
<h3 id="heading-step-2-sprint-planning-what-are-we-building-this-time">🧭 Step 2: Sprint Planning – What Are We Building This Time?</h3>
<p>Now that we’ve got a solid backlog, it’s time to pick what to build <em>right now</em>.</p>
<p>At the start of each sprint (which typically lasts 1 to 4 weeks), the team holds a <strong>Sprint Planning meeting</strong>. This meeting sets the stage for the entire sprint – it’s like the huddle before the big game.</p>
<p>In Sprint Planning, the team:</p>
<ul>
<li><p>Reviews the top items from the backlog</p>
</li>
<li><p>Discusses what can realistically be completed based on their availability and capacity</p>
</li>
<li><p>Chooses a handful of these stories to commit to</p>
</li>
<li><p><strong>Defines a Sprint Goal</strong> – a simple statement that captures the purpose of this sprint</p>
</li>
</ul>
<p>For example, the Sprint Goal might be:<br>🎯 <em>“Allow users to reset their passwords.”</em></p>
<p>Every user story chosen should contribute to that goal. The collection of these stories becomes the <strong>Sprint Backlog</strong> – basically, the to-do list for the sprint.</p>
<p>So when we say:</p>
<p>“The team selects an ordered list of user stories to comprise the Sprint Backlog for the next sprint, which will be achievable to satisfy the Sprint Goal...”</p>
<p>We’re really just saying:<br>👉 <em>“Pick a realistic number of important tasks that, if completed, will help us hit our target for the sprint.”</em></p>
<p>Not too vague. Not too ambitious. Just achievable and focused.</p>
<h3 id="heading-step-3-daily-standups-stay-in-sync">☀️ Step 3: Daily Standups – Stay in Sync</h3>
<p>Now the sprint is underway! But how does everyone stay aligned and avoid working in silos?</p>
<p>That’s where the <strong>Daily Standup</strong> comes in. Every day – usually in the morning – the team has a quick check-in (about 15 minutes) where each person answers three questions:</p>
<ol>
<li><p><strong>What did I do yesterday?</strong></p>
</li>
<li><p><strong>What am I working on today?</strong></p>
</li>
<li><p><strong>Is anything blocking me?</strong> (that is, am I stuck?)</p>
</li>
</ol>
<p>Example:</p>
<p>“Yesterday I set up the login API integration. Today I’ll work on the UI validation. I’m blocked on getting access to the staging database — may need help.”</p>
<p>These standups keep the team in sync and surface blockers early so they can be addressed quickly. They’re not about micromanaging or showing off. They’re about visibility and support.</p>
<h3 id="heading-whats-a-sprint-burndown-chart">📉 What’s a Sprint Burndown Chart?</h3>
<p>You might hear your team mention a “burndown chart.” No, this isn’t about things going down in flames (hopefully).</p>
<p>A <strong>Sprint Burndown Chart</strong> is a graph that shows how much work is left in the sprint – day by day.</p>
<ul>
<li><p>The <strong>y-axis</strong> is the amount of work remaining (often measured in story points or tasks)</p>
</li>
<li><p>The <strong>x-axis</strong> is the number of days left in the sprint</p>
</li>
</ul>
<p>The line should ideally trend downward as work gets completed – hence “burning down.” If it flattens out or slopes up, that’s a red flag that the team might be stuck, behind schedule, or not updating the board.</p>
<p>Think of it as a visual heartbeat of the sprint. You can learn more via a practical example <a target="_blank" href="https://youtu.be/2K84aZn9AY8?si=tS8oMGxVD0CYtnlw">in this video</a>.</p>
<h3 id="heading-step-4-sprint-review-show-what-youve-built">🖥️ Step 4: Sprint Review – Show What You’ve Built</h3>
<p>At the end of the sprint, the team holds a <strong>Sprint Review</strong> (also called a “demo”). This is where you show what was actually built during the sprint.</p>
<ul>
<li><p>The <strong>Developers</strong> demo working features – live, not just screenshots</p>
</li>
<li><p>The <strong>Product Owner</strong> reviews whether the Sprint Goal was achieved</p>
</li>
<li><p>Stakeholders may ask questions, give feedback, or suggest tweaks</p>
</li>
</ul>
<p>This meeting isn’t just for show – it’s a feedback loop. It helps the team validate that what they built is useful, usable, and meets expectations. If changes are needed, those get added to the backlog for future sprints.</p>
<h3 id="heading-step-5-sprint-retrospective-look-back-to-move-forward">🔍 Step 5: Sprint Retrospective – Look Back to Move Forward</h3>
<p>Once the review is done, the team shifts focus from <em>what</em> they built to <em>how</em> they worked together.</p>
<p>Enter the <strong>Sprint Retrospective</strong> – a meeting to reflect on the process, not the product.</p>
<p>The team discusses:</p>
<ul>
<li><p>✅ What went well</p>
</li>
<li><p>❌ What didn’t go so well</p>
</li>
<li><p>🔁 What could be improved next time</p>
</li>
</ul>
<p>This isn’t about pointing fingers. It’s about learning, adapting, and continuously improving how the team collaborates.</p>
<p>The <strong>Scrum Master</strong> often facilitates this meeting and helps turn feedback into action items for the next sprint. For example:</p>
<p>“We underestimated testing time. Next sprint, let’s budget for QA earlier.”</p>
<p>The best teams take retros seriously. Why? Because even if your code is perfect, your <em>process</em> needs tuning too – and small process changes often lead to big gains.</p>
<h3 id="heading-scrum-is-a-loop">♻️ Scrum Is a Loop</h3>
<p>Here’s the rhythm:</p>
<ol>
<li><p>Plan the sprint</p>
</li>
<li><p>Check in daily</p>
</li>
<li><p>Build and demo the product</p>
</li>
<li><p>Reflect and improve</p>
</li>
</ol>
<p>Then do it all over again – with slightly better coordination and slightly more trust each time.</p>
<p>It’s not about being fast. It’s about being intentional, consistent, and collaborative.</p>
<h3 id="heading-example-sprint">Example Sprint</h3>
<p>Let’s say, for example, that your team does 4-week sprints. (Keep in mind that Sprints can differ by team, nature of product, release cycles, and so on.)</p>
<p>Here’s the rough beat:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Week</strong></td><td><strong>What Happens (Sprint Ceremonies)</strong></td><td><strong>Your Role</strong></td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td><strong>Sprint Planning</strong></td><td>Help estimate effort, pick what to build</td></tr>
<tr>
<td>1-4</td><td><strong>Daily Stand ups</strong> (15 mins)</td><td>Share what you’re doing &amp; any blockers</td></tr>
<tr>
<td>1-3</td><td><strong>Development Time</strong></td><td>Code, test, commit, fix, push, repeat</td></tr>
<tr>
<td>3.5-4</td><td><strong>Sprint Review</strong></td><td>Demo what you built</td></tr>
<tr>
<td>4</td><td><strong>Sprint Retrospective</strong></td><td>Reflect on how the sprint went as a team</td></tr>
</tbody>
</table>
</div><p>Scrum works in <strong>loops</strong>. Every 2-4 weeks (depending on your cadence and sprint cycle), your team should have working, demo-able software to show for it – even if it’s small.</p>
<p>And no, it’s not about “speed.” It’s about consistency, communication, and collaboration.</p>
<h2 id="heading-who-attends-the-ceremonies"><strong>Who attends the Ceremonies:</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Ceremony</strong></td><td><strong>Who Attends</strong></td><td><strong>Why They’re There</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Sprint Planning</strong></td><td>Product Owner (PO), Scrum Master (SM), Development Team</td><td>To define what will be delivered and how the work will be accomplished</td></tr>
<tr>
<td><strong>Daily Standup</strong></td><td>Development Team, Scrum Master (optional), PO (optional)</td><td>To sync on progress, share blockers, and coordinate efforts</td></tr>
<tr>
<td><strong>Sprint Review</strong></td><td>Development Team, Scrum Master, Product Owner, Stakeholders</td><td>To demo the work, get feedback, and assess if goals were met</td></tr>
<tr>
<td><strong>Sprint Retrospective</strong></td><td>Development Team, Scrum Master, Product Owner (optional)</td><td>To reflect on the process, identify what worked/what didn’t, and improve the next sprint</td></tr>
<tr>
<td><strong>Backlog Refinement</strong></td><td>Product Owner, Development Team, Scrum Master (optional)</td><td>To clarify upcoming stories, estimate work, and prepare for future sprint planning</td></tr>
</tbody>
</table>
</div><p>Now lets dive deeper and understand practically how each of these ceremonies work:</p>
<h2 id="heading-standups-where-you-talk-like-a-human-not-a-robot"><strong>Standups: Where You Talk Like a Human, Not a Robot</strong></h2>
<p>So how does the team actually stay connected day to day? That’s where standups come in.</p>
<p>Every morning, your team meets briefly – usually on Zoom or in a circle – and you answer 3 questions:</p>
<ol>
<li><p>What did I work on yesterday?</p>
</li>
<li><p>What will I work on today?</p>
</li>
<li><p>What’s blocking me? Any impediments?</p>
</li>
</ol>
<p>Example:</p>
<p>"Yesterday I cleaned up the signup validation logic. Today I’m working on the email verification flow. I’m stuck on SendGrid config – might need help setting up credentials."</p>
<p>It’s not about impressing anyone. It’s about keeping everyone in sync. Some days you’ll say, “I spent the whole day debugging a CSS bug that turned out to be a semicolon.” That’s okay.</p>
<p>How does it work?</p>
<p>The Scrum Master gathers everyone in a huddle room, the PO and Dev Team included, and opens the the Standup. They are the facilitator of the ceremony. Everyone gets a chance to answer the 3 questions above (usually about 2-5 minutes each). It’s not a full report – it’s quick. When one person is done, they pass it on to someone else.</p>
<p>This ensures there is team cohesion and transparency.</p>
<p><a target="_blank" href="https://youtu.be/q_R9wQY4G5I?si=W1AcvcLXB-mnUM1f">Here is a video example of a standup</a>.</p>
<h2 id="heading-sprint-planning"><strong>Sprint Planning</strong></h2>
<p>The goal of the planning meeting is to answer the questions “What are we going to work on, and how are we going to do it?” It is critical for the team to have a shared goal and a shared commitment to this goal before beginning this ceremony.</p>
<p>Participants should:</p>
<ul>
<li><p>Measure growth</p>
</li>
<li><p>Sync with the Scrum Master</p>
</li>
<li><p>Sync with the Product Owner</p>
</li>
</ul>
<p>Sprint planning happens just before the sprint starts, and usually lasts for an hour or two. In this meeting, the team goes over a collection of <strong>user stories</strong> and discuss, plan, measure, and prioritize. This is where they decide what is going to be in scope for their upcoming sprint cycle.</p>
<p>The Product Owner will have a prioritized view of things in the backlog. They work with the team on each object or customer experience. Together, as a group they go through and make calculations, deciding to what they can commit.</p>
<h2 id="heading-whats-a-user-story-and-why-does-it-sound-like-a-childrens-book"><strong>What’s a User Story and Why Does It Sound Like a Children’s Book?</strong></h2>
<p>So you might be wondering: how do you know what to work on? What to build? So much work, so little time? Thats where <strong>user stories</strong> come in.</p>
<p>In Scrum, teams don’t just write vague tasks like “code the login.” Instead, they write user stories – short, human-centered feature descriptions that describe what the user needs, why they need it, and what success looks like.</p>
<p>Here’s an example:</p>
<p><em>As a user, I want to be able to reset my password, so I can access my account if I forget it.</em></p>
<p>User stories are the scaffolding of teamwork. They’re written with empathy, not just efficiency. And each one comes with <strong>acceptance criteria</strong> – a checklist that clarifies what “done” actually means:</p>
<ul>
<li><p>A “Forgot Password” link is visible</p>
</li>
<li><p>Clicking it shows a form</p>
</li>
<li><p>An email gets sent with a reset link</p>
</li>
</ul>
<p>Once a story is agreed upon, developers break it down into tasks, like “build form,” “hook into backend,” or “handle email validation.” It’s collaborative, not prescriptive. And user stories have priority so you know what’s the most important and what’s the least.</p>
<p>A helpful rule of thumb many teams use is the <a target="_blank" href="https://medium.com/@nic/writing-user-stories-with-gherkin-dda63461b1d2"><strong>Gherkin</strong>-style "Given–When–Then"</a> format:</p>
<ul>
<li><p><strong>Given</strong> some initial context</p>
</li>
<li><p><strong>When</strong> an event occurs</p>
</li>
<li><p><strong>Then</strong> a specific outcome should happen</p>
</li>
</ul>
<p>This ensures that everyone – devs, testers, and product owners – shares the same understanding of behavior and expectations.</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=7hoGqhb6qAs">Here is a great video example</a> thats outlines how to draft effective and powerful user stories.</p>
<h2 id="heading-what-counts-as-done-definition-of-done-and-why-its-important"><strong>What Counts as “Done”? Definition of Done and Why It’s Important</strong></h2>
<p>Now you might be wondering – how do I know when a task is done and can be closed out?</p>
<p>The <strong>Definition of Done</strong> is a type of documentation in the form of a <strong>team agreement</strong>. The Definition of Done identifies the conditions that need to be achieved in order for the product to be considered done (as in <strong>potentially shippable</strong>).</p>
<p>This is how we know that we "did the thing right". Meaning, we built the correct level of quality into the product. The Definition of Done is not the same as the acceptance criteria, which are written by the product owner to help us know we did the "right thing".</p>
<p>Every team has a Definition of Done – it’s not just “I pushed code.” It could mean:</p>
<ul>
<li><p>Code is written</p>
</li>
<li><p>Reviewed by a peer</p>
</li>
<li><p>Merged into main</p>
</li>
<li><p>Tested on staging</p>
</li>
<li><p>Possibly deployed</p>
</li>
</ul>
<p>This clarity keeps teams honest and accountable. No “it works on my machine” energy here. The DoD sets a quality bar. It prevents ambiguity, rework, and “it works on my machine” moments. When every card on the board passes the same finish line, teams move faster – and trust each other more.</p>
<p>Everyone should know what done is in a team. Either its Done as per DoD standards or its not.</p>
<p><a target="_blank" href="https://youtu.be/pYOJyQoBT3U?si=nVygkQQx79NaAOo4">Here is a beautiful video</a> highlighting the impotence of DoD.</p>
<h2 id="heading-demos-retros-and-saying-the-hard-things"><strong>Demos, Retros, and Saying the Hard Things</strong></h2>
<p>Once you’ve built the product, then comes demos (showcasing your work) and retros (analysis as a team on what when well and what areas to improve on).</p>
<p>In the retro, everyone’s encouraged to speak up:</p>
<ul>
<li><p>What went well?</p>
</li>
<li><p>What didn’t?</p>
</li>
<li><p>What should we try next time?</p>
</li>
</ul>
<p>Example:</p>
<p>“We missed a lot of stories because we didn’t account for testing time. Maybe we buffer next sprint with fewer tasks.”</p>
<p>The goal is not to blame – it’s to <em>improve</em>. Over time, this feedback loop becomes gold. The Scrum Master usually facilitates, collects feedback (via tools like Parabol, Miro, or sticky notes), and helps turn insights into actionable experiments for the next sprint.</p>
<p>Over time, retros become the heartbeat of team evolution.</p>
<p><a target="_blank" href="https://youtu.be/5eu1HotNmWs?si=1DZaSmztB6rHyawj">Here is a video</a> highlighting the importance of a Retro and Sprint Review.</p>
<h3 id="heading-why-retrospection-matters-more-than-you-think">🧠 Why Retrospection Matters More Than You Think</h3>
<p>The Sprint Retrospective is more than just another meeting. It’s a mirror for your team – a safe, structured space to pause, reflect, and improve together.</p>
<p>You discuss:</p>
<p>✅ what went well</p>
<p>❌ what did not go well</p>
<p>🔁 what could we do better next time</p>
<p>Great teams don't just deliver great software, they continually deliver better ways of working.</p>
<p>This is why many experienced Scrum practitioners consider the retro to be the most important event in Scrum. Code is deployed once, but process improvements grow exponentially, sprint after sprint.</p>
<h2 id="heading-tools-you-might-encounter"><strong>Tools You Might Encounter</strong></h2>
<p>Scrum doesn’t require software, but real-world teams use a variety of tools:</p>
<ul>
<li><p><strong>Jira</strong> – Tracks sprints, issues, velocity</p>
</li>
<li><p><strong>Trello</strong> – Simple board, good for small teams</p>
</li>
<li><p><strong>Slack</strong> – Where standups often happen if async</p>
</li>
<li><p><strong>Notion / Confluence</strong> – Docs, retros, notes</p>
</li>
<li><p><strong>GitHub Projects</strong> – Lightweight planning for devs</p>
</li>
</ul>
<p>Don’t worry if you’re not fluent in these yet. They’re tools – you’ll learn them on the job.</p>
<h2 id="heading-if-youre-preparing-for-a-job-heres-what-you-can-do"><strong>If You’re Preparing for a Job, Here’s What You Can Do</strong></h2>
<ul>
<li><p>✍️ Practice writing user stories from your side projects</p>
</li>
<li><p>🧪 Run a mini-sprint: Plan your weekend project, set goals, and “review” it at the end</p>
</li>
<li><p>🤝 Contribute to an open-source project that uses Scrum or Agile workflows</p>
</li>
<li><p>🧾 Write about what you learned – maybe as a tutorial (<em>hint hint</em>)</p>
</li>
</ul>
<h2 id="heading-final-thoughts"><strong>Final Thoughts</strong></h2>
<p>So to recap, Scrum is a simple yet powerful way for teams to work together, stay organized, and deliver results quickly. It runs in short cycles called <strong>sprints</strong>, where the team plans what to do, checks in daily, shows their progress at the end, and reflects on how to improve.</p>
<p>The four key ceremonies – <strong>Sprint Planning</strong>, <strong>Daily Scrum</strong>, <strong>Sprint Review</strong>, and <strong>Sprint Retrospective</strong> – help keep everyone aligned and focused. With clear roles and regular feedback, Scrum makes it easier to handle changes, solve problems early, and continuously get better as a team.</p>
<p>But scrum isn’t a magic spell. It’s just a way for humans to build complex things – together – without falling apart.</p>
<p>You don’t need to be a Scrum Master. You don’t need a certification. But if you understand how sprints work, what’s expected of you, and how to show up to meetings with clarity and candor, you’re 10 steps ahead of most.</p>
<p>Scrum helps teams talk, plan, build, and learn. And now? You can too.</p>
<p>If you liked this, please do share. You never know who it might help out.</p>
<p>Until then…keep learning, unlearning, and relearning!!!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Stop Chasing Productivity and Start Moving with Purpose ]]>
                </title>
                <description>
                    <![CDATA[ Most people know how to set goals. And many understand that developing good habits is key to reaching those goals. Yet, despite this knowledge, it's still easy to feel stuck – or worse, in motion with ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-set-goals-using-the-path-framework/</link>
                <guid isPermaLink="false">68654987cd4d0c286c5cc497</guid>
                
                    <category>
                        <![CDATA[ goals ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ habits ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-improvement  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Career development  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Brandon Wozniewicz ]]>
                </dc:creator>
                <pubDate>Wed, 02 Jul 2025 15:00:23 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750868906416/7cba189f-0c53-4beb-a0d6-cbb034fc73bb.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most people know how to set goals. And many understand that developing good habits is key to reaching those goals. Yet, despite this knowledge, it's still easy to feel stuck – or worse, in motion without direction.</p>
<p>We chase productivity without asking a deeper question: <em>Are my actions actually moving me toward who I want to become?</em> Being busy feels good. It's a quick hit of dopamine every time we check something off our to-do list – even if that list has nothing to do with our actual goals.</p>
<p>In my own pursuit of reaching my goals, I began developing a simple framework to help me align my daily actions with the person I wanted to be. This isn't a radically new concept here. It is more like a refined mental model that’s built from things you've probably heard and read before. But it’s designed to fit together in a clear, usable way.</p>
<p>The goal is to help bridge the gap between big-picture intentions and everyday behavior. I call it <strong>PATH</strong>.</p>
<p>It stands for:</p>
<ul>
<li><p><strong>Purpose</strong> – your <em>why</em></p>
</li>
<li><p><strong>Aspirations</strong> – the vision of who you want to become</p>
</li>
<li><p><strong>Targets</strong> – clear, measurable goals</p>
</li>
<li><p><strong>Habits</strong> – the consistent actions that support those goals</p>
</li>
</ul>
<p>This is the framework I use to stay oriented. Not just to be productive, but to be <em>intentional</em>. I still get off track, but I find my way back a bit quicker.</p>
<p>In this article, you’ll learn how to apply the PATH framework to clarify your direction, set meaningful goals, and build habits that stick.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ol>
<li><p><a href="#heading-busy-doesnt-mean-effective">Busy Doesn't Mean Effective</a></p>
</li>
<li><p><a href="#heading-the-power-of-frameworks">The Power of Frameworks</a></p>
</li>
<li><p><a href="#heading-a-framework-for-reaching-your-goals">A Framework For Reaching Your Goals</a></p>
</li>
<li><p><a href="#heading-complexity-and-difficulty-are-not-synonyms">Complexity and Difficulty Are Not Synonyms</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ol>
<h2 id="heading-busy-doesnt-mean-effective">Busy Doesn't Mean Effective</h2>
<p>Being busy can feel like progress. Sometimes it is – you're moving toward your goals, checking boxes, staying active. But other times, it's just noise. A way to avoid sitting still. A way to avoid asking the harder question: <em>Is this making a meaningful difference?</em></p>
<p>Sometimes, we chase productivity not because we're clear, but because, in fact, we're <em>uncertain</em>. And doing something – anything – feels better than acknowledging we might be lost.</p>
<p>On the other hand, you can have clear goals and develop effective habits. But if someone asked you why you picked those goals, would you have an answer? Why do you want to lose ten pounds? Why do you want to start that company? What are you really aiming for?</p>
<p>Here's a thought experiment: Imagine your funeral. You get to pick one person to give your eulogy – and they can only say one sentence about you. What do you hope they'll say?</p>
<p>This is a personal question. And I know my answer very clearly. Do you know yours?</p>
<h2 id="heading-the-power-of-frameworks">The Power of Frameworks</h2>
<blockquote>
<p>You do not rise to the level of your goals. You fall to the level of your systems.</p>
<p>— James Clear</p>
</blockquote>
<p>In programming, a framework is predefined code that helps developers build software more efficiently and effectively. It abstracts away the implementation details of common problems – things you <em>could</em> handle yourself, but probably don't need to. The tradeoff? You give up some flexibility in exchange for speed. In other words, frameworks help you solve your problem faster by <em>deciding a few things for you</em>.</p>
<p>Developers lean on frameworks like React or .NET because they help us focus on solving meaningful problems, rather than wrestling with boilerplate code. PATH works the same way: it abstracts the structure so you can spend more time executing on what matters.</p>
<p>When it comes to setting goals, it's no different. It's easy to get lost in these implementation details and end up overcomplicating the process. We want the perfect strategy or the ideal plan. But in reality, progress almost always beats perfection. Initially, we don't need the most elegant system – we need one that we can follow.</p>
<p>That's where frameworks shine. They reduce friction and help you stop tinkering with the <em>how</em> and start taking action on the <em>what</em>. They free up mental energy, allowing you to focus on actually solving the problem.</p>
<p>This isn't new. Every great tool – whether it's a budgeting app, a productivity system, or a JavaScript library – obscures just enough complexity to help you move towards a solution faster.</p>
<h2 id="heading-a-framework-for-reaching-your-goals">A Framework For Reaching Your Goals</h2>
<p>PATH is a framework I've used for years to align my actions with my desired future self. Most goal-setting systems start with habits or outcomes, but PATH begins a bit further back: your purpose.</p>
<h3 id="heading-p-is-for-purpose"><strong>P is for Purpose</strong></h3>
<p>Purpose is the core of who you are and what matters most. It is your mission statement, no more than a sentence or two. It should be <em>you.</em> A strong purpose will help you get back on track when you veer off course.</p>
<h3 id="heading-a-is-for-aspirations"><strong>A is for Aspirations</strong></h3>
<p>Once you've decided on your purpose, the next step is to dream a little. This is the fun part.</p>
<p>Where do you want to be in the next 5 to 10 years? Try to picture a typical day of your ideal future – not necessarily what you own, but what you experience and do.</p>
<ul>
<li><p>Where do you live?</p>
</li>
<li><p>What time do you wake up?</p>
</li>
<li><p>What do you do during the day?</p>
</li>
<li><p>Who are you with?</p>
</li>
</ul>
<p>Here is an example:</p>
<p><em>I live by a beautiful lake in the mountains. I get up around 7 am, go for a long run, and then eat a healthy breakfast. I spend my day writing. Evenings are slow, spent laughing on the porch with friends and family.</em></p>
<p>Now, let's break that down into aspirations – broad themes that define the kind of life you're building. From that vision, here are three I see:</p>
<ul>
<li><p>Be in good shape</p>
</li>
<li><p>Be a writer</p>
</li>
<li><p>Spend quality time with friends and family</p>
</li>
</ul>
<p>These are your <strong>aspirations</strong> – the "A" in PATH. And they're <em>not</em> meant to be specific or time-bound. Think of them as a compass, not a destination. Their job is to give you something to aim at over and over again.</p>
<h3 id="heading-t-is-for-targets"><strong>T is for Targets</strong></h3>
<p>Targets are the specific goals we aim to achieve.</p>
<p>Each aspiration should have one or more targets – clear, time-bound goals that move you in the direction of your aspiration.</p>
<p>These targets should follow the SMART framework:</p>
<ul>
<li><p>Specific</p>
</li>
<li><p>Measurable</p>
</li>
<li><p>Attainable</p>
</li>
<li><p>Realistic</p>
</li>
<li><p>Time-bound</p>
</li>
</ul>
<p>For example, if you aspire to become a software developer, a target might be:</p>
<p><em>Contribute one fix or update to an open-source project by the end of the month.</em></p>
<p>If aspirations are our compass, targets are the destination. They let you measure progress and know when you've arrived.</p>
<h3 id="heading-h-is-for-habits"><strong>H is for Habits</strong></h3>
<p>Habits are where the rubber meets the road.</p>
<p>Every target should have one or more supporting habits – things you do consistently that move you towards your goal. While they will generally be daily or weekly routines, one-off tasks are also acceptable. The idea is these are things that <strong>move the needle.</strong></p>
<p>For example, if your target is to run a 5k event, a habit might be:</p>
<p><em>Run 3 miles every other day.</em></p>
<p>Or, if you're just starting, you may want to create a one-off task such as:</p>
<p><em>Map out a running route.</em></p>
<p>Again, these habits (or tasks) are <strong>needle-movers</strong>. Following them – <em>actually</em> following them – should almost guarantee that you reach your targets.</p>
<p>When it comes to building a habit, the key is to start small, sometimes <em>really</em> small. In the beginning, the goal isn’t performance, it’s momentum. And while momentum can be hard to start, once it’s moving, it’s even harder to stop.</p>
<p>If you’re trying to build a writing habit, begin with two sentences. If you want to exercise more, start by putting on your workout clothes. Or just drive to the gym. The point is to make starting frictionless.</p>
<p>Consistency also depends on cues. Every habit needs a reliable trigger – something that prompts you to take action. It might be a specific time of day, walking in the door after work, or finishing your morning coffee. Over time, that cue becomes automatic, and so does the habit.</p>
<h2 id="heading-complexity-and-difficulty-are-not-synonyms">Complexity and Difficulty Are Not Synonyms</h2>
<p>There's a moment in the boxing film <em>Bleed for This</em> where the main character is asked, "What's the biggest lie you've ever been told?" He answers:</p>
<p><em>"It's not that simple."</em></p>
<p>The interviewer tries to clarify – as if she believes the main character doesn't understand the question – but he cuts her off and essentially says:</p>
<p><em>"No, that's the lie. It</em> <em><strong>is</strong></em> <em>that simple."</em></p>
<p>That line stuck with me, because we often treat life like it requires a perfect plan before we can begin. However, sometimes the hardest part is simply deciding.</p>
<p>Don't confuse something <em>hard</em> with something that's <em>complex</em>. In my experience, the most meaningful things in life are often simple yet challenging.</p>
<p>Knowing what to do isn't the challenge. It's doing it – over and over again, especially when it's boring, inconvenient, or the outcome is uncertain. That's why clarity matters, and frameworks can help.</p>
<p>There's the person who spends their entire life looking for an elevator. And then there's the person who starts climbing the stairs.</p>
<p>Figure out who you want to be, and filter your goals and actions through that lens.</p>
<p>I'd bet almost anyone reading this could name a few things that would take less than an hour a day that would completely change their life over time. The difference – the <em>only</em> difference –&nbsp;is that the person who gets clear on what they want and then actually does those things every day...gets there.</p>
<p>At the end of the day, the goal isn't to be productive. It's to be <em>intentional</em>. To align your daily actions with your goals and ensure those goals are pushing you toward the person you want to become. That's the idea behind PATH – a framework for being intentional.</p>
<h2 id="heading-summary"><strong>Summary</strong></h2>
<p>If you remember nothing else, here’s the heart of PATH:</p>
<ul>
<li><p><strong>Purpose</strong> – Define your <em>why</em>. A short, personal mission statement that keeps you grounded.</p>
</li>
<li><p><strong>Aspirations</strong> – Paint a picture of who you want to become. Think 5–10 years out, focused on experience, not possessions.</p>
</li>
<li><p><strong>Targets</strong> – Turn aspirations into SMART goals with clear deadlines. These give your vision structure.</p>
</li>
<li><p><strong>Habits</strong> – Identify consistent, meaningful actions that drive progress toward your targets – daily, weekly, or even one-off tasks.</p>
</li>
</ul>
<p>I’ve created a free Notion template to help you get started with the PATH framework.<br>→ <a href="https://www.notion.com/templates/path-goal-tracker-essentials">PATH Goal Tracker Essentials</a></p>
<p>There’s also an upgraded version available for those who want habit tracking, progress calculations, and more.<br>→ <a href="https://www.notion.com/templates/path-goal-tracker-plus">PATH Goal Tracker Plus</a></p>
<p>Found this helpful? I write about practical automation, productivity systems, and building smarter workflows — without the jargon. Visit me at <a href="http://brandonwoz.com">brandonwoz.com</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ What is Technical Debt and How Do You Manage it? ]]>
                </title>
                <description>
                    <![CDATA[ You’ve probably heard someone say, “We’ll fix it later.” Maybe you’ve said it yourself. In the rush to launch a feature, meet a deadline, or impress a client, you take a shortcut. The code works – for now. The design passes – for now. But over time, ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/what-is-technical-debt-and-how-do-you-manage-it/</link>
                <guid isPermaLink="false">681e31e668aa6f9f0c37bb12</guid>
                
                    <category>
                        <![CDATA[ engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Product Management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 09 May 2025 16:48:38 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746809303458/b4635ddc-d909-427a-9cc1-9b9f56ae1d41.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>You’ve probably heard someone say, “We’ll fix it later.”</p>
<p>Maybe you’ve said it yourself.</p>
<p>In the rush to launch a feature, meet a deadline, or impress a client, you take a shortcut. The code works – for now. The design passes – for now.</p>
<p>But over time, these choices pile up. They slow you down. They make every change harder. That’s technical debt.</p>
<p>Technical debt is a quiet, creeping cost. It doesn’t show up in metrics like churn or conversion rate. But it eats away at your product’s quality, your team’s velocity, and your ability to innovate.</p>
<p>You don’t notice it right away. Then, suddenly, everything feels slower. Nothing is simple anymore.</p>
<p>One bug fix breaks two new things. Engineers groan when they touch certain parts of the code. That’s when you realize you’re in debt.</p>
<p>Let’s talk about what technical debt really is, how it forms, and how you can deal with it before it kills your product.</p>
<h2 id="heading-what-is-technical-debt"><strong>What Is Technical Debt?</strong></h2>
<p>Think of technical debt like financial debt. When you borrow money, you get something now – at the cost of interest later.</p>
<p>In software, it’s the same. You make a quick decision to save time now, knowing it might cost you more effort down the line.</p>
<p>There’s nothing inherently wrong with that. Sometimes, taking on debt is smart. You might need to ship fast to test an idea or respond to the market.</p>
<p>But if you keep borrowing and never repay, the interest builds. And technical debt doesn’t just grow – it compounds. The longer you leave it, the worse it gets.</p>
<p>You don’t just end up paying more later. You slow down your whole team.</p>
<p>Every new feature takes longer. Bugs multiply. Morale drops. And eventually, your product feels fragile. That’s when the real damage begins.</p>
<h3 id="heading-types-of-technical-debt"><strong>Types of Technical Debt</strong></h3>
<p>Not all debt is the same. Some is like a short-term loan – you know you’re taking it and why. Other debt is like a bad mortgage – no one even knows it’s there until things break.</p>
<p>Here are the most common types:</p>
<ul>
<li><p><strong>Intentional debt</strong> — You cut corners to hit a deadline but log it and plan to fix it. This can be healthy if managed well.</p>
</li>
<li><p><strong>Unintentional debt</strong> — You didn’t realise the shortcut was harmful. Often happens with new tech or unclear requirements.</p>
</li>
<li><p><strong>Environmental debt</strong> — Your tools, libraries, or frameworks get outdated. Even if your code is clean, it sits on crumbling infrastructure.</p>
</li>
<li><p><strong>Process debt</strong> — The way you build software becomes inefficient. Poor handoffs, unclear documentation, or weak testing pipelines all contribute.</p>
</li>
</ul>
<p>Recognising which type you’re dealing with helps you prioritise. Not all debt needs immediate repayment. But all of it needs attention.</p>
<h2 id="heading-how-technical-debt-happens"><strong>How Technical Debt Happens</strong></h2>
<p>As you can probably imagine, technical debt shows up in many ways.</p>
<p>Sometimes it’s deliberate. You make a tradeoff. You know it’s a shortcut, and you plan to clean it up later. That’s manageable – if you actually clean it up.</p>
<p>But most technical debt isn’t planned. It sneaks in through decisions that feel small in the moment. A rushed feature. A new hire not trained on the codebase. A spec that changes mid-sprint. Over time, these small cracks become deep fractures.</p>
<p>Here’s a simple example:</p>
<pre><code class="lang-plaintext">// Temporary workaround for product discounts
function applyDiscount(price, productType) {
  if (productType === 'electronics') {
    return price * 0.9; // 10% off
  } else if (productType === 'clothing') {
    return price * 0.8; // 20% off
  } else if (productType === 'books') {
    return price * 0.95; // 5% off
  } else {
    return price;
  }
}
</code></pre>
<p>This started as a quick fix. But over time, new product types get added with more exceptions.</p>
<p>Soon, you will have twenty <code>if-else</code> branches. It’s fragile. Every change risks breaking something. That’s technical debt.</p>
<p>The worst part? You might not even notice until a year later, when a bug in that logic takes hours to trace. You wonder, “How did this get so messy?” The answer: <strong>one shortcut at a time.</strong></p>
<p>A better long-term approach in the above example would be a configuration-driven system or a discount rules engine.</p>
<pre><code class="lang-plaintext">// Config-driven discount logic
const discountRates = {
  electronics: 0.10,
  clothing: 0.20,
  books: 0.05
};
</code></pre>
<pre><code class="lang-plaintext">function applyDiscount(price, productType) {
  const discount = discountRates[productType] || 0;
  return price * (1 - discount);
}
</code></pre>
<h2 id="heading-why-technical-debt-can-be-dangerous"><strong>Why Technical Debt Can Be Dangerous</strong></h2>
<p>Technical debt slows you down. That’s its most visible cost.</p>
<p>A feature that should take a day now takes a week. Simple changes break unrelated things. Your team spends more time fixing than building.</p>
<p>But the real danger goes deeper. Technical debt makes you afraid to touch your code.</p>
<p>Engineers stop refactoring because “it’s too risky.” You start saying no to new ideas because the system can’t handle them. The product becomes rigid. You stop innovating.</p>
<p>It also hurts your team. Developers don’t like working in messy codebases. It leads to burnout. New hires struggle to onboard.</p>
<p>Your best engineers spend their time firefighting instead of creating. Eventually, people leave. And your debt remains.</p>
<h2 id="heading-how-to-manage-technical-debt"><strong>How to Manage Technical Debt</strong></h2>
<p>You can’t eliminate all technical debt. But you can manage it.</p>
<p>First, treat it like real debt. Track it. Prioritize it. Make regular payments.</p>
<p>Start by writing it down. Every time someone takes a shortcut, log it. You don’t need a fancy tool – a shared doc or Jira tag works fine. Just make it visible.</p>
<p>Next, build time into your workflow to pay it off. Use 10–20% of each sprint to refactor or improve the codebase. Don’t wait for a rewrite. Small, steady work adds up.</p>
<p>Code reviews help too. Encourage your team to ask: “Is this a shortcut?” If yes, make a conscious choice. Leave a clear comment. Note the tradeoff. Now it’s not a hidden cost – it’s a known one.</p>
<p>And when you do pay off debt, celebrate it. Make it part of your culture. The same way you’d celebrate shipping a feature, acknowledge when your team makes the codebase better. That builds pride and ownership.</p>
<h3 id="heading-knowing-when-to-refactor"><strong>Knowing When to Refactor</strong></h3>
<p>You can’t fix all the debt at once. So how do you choose?</p>
<p>Look for signs of pain. If a file breaks every sprint, fix it. If one part of the system takes days to test, improve it. If new hires always get stuck in one module, clean it up.</p>
<p>Focus on the code you touch often. There’s no point polishing a dead feature. But if something is part of your core flow, invest in it.</p>
<p>Also, listen to your team. Engineers know where the pain lives. If someone says, “This part scares me,” take that seriously. Fear in the codebase is a red flag.</p>
<h2 id="heading-when-debt-becomes-fatal"><strong>When Debt Becomes Fatal</strong></h2>
<p>Sometimes, the debt gets so bad that small fixes won’t save you. The system collapses under its own weight. Everything feels slow. Nothing is safe to change. That’s when teams start talking about rewrites.</p>
<p>But rewrites are risky. They take time. They often miss hidden business logic. And they can carry old debt into new code if not done carefully.</p>
<p>If you must rewrite, do it incrementally. Replace modules one at a time. Add tests. Migrate data with care.</p>
<p>And don’t forget why the old system failed. If you don’t fix the culture, the new system will rot too.</p>
<h2 id="heading-final-thoughts"><strong>Final Thoughts</strong></h2>
<p>Technical debt isn’t evil. It’s part of building software. But like financial debt, it needs discipline. You can’t just ignore it and hope it goes away.</p>
<p>Great products aren’t just well-designed. They’re well-maintained. The teams behind them care about quality — not just in what users see, but in what engineers live with every day.</p>
<p>So the next time you say, “We’ll fix it later,” ask yourself: will you? Or are you just borrowing against the future?</p>
<p>Hope you enjoyed this article. <a target="_blank" href="https://blog.manishshivanandhan.com/"><strong>Join my newsletter</strong></a> for similar articles and <a target="_blank" href="https://www.linkedin.com/in/manishmshiva/"><strong>connect with me on Linkedin</strong>.</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Automate CI/CD with GitHub Actions and Streamline Your Workflow ]]>
                </title>
                <description>
                    <![CDATA[ CI/CD stands for Continuous Integration and Continuous Delivery. It is a system or set of processes and methodologies that help developers quickly update codebases and deploy applications. The Continuous Integration (CI) part of CI/CD means that deve... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/automate-cicd-with-github-actions-streamline-workflow/</link>
                <guid isPermaLink="false">67fd2db6c741f3f1aec5ecd2</guid>
                
                    <category>
                        <![CDATA[ github-actions ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ workflow ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chidiadi Anyanwu ]]>
                </dc:creator>
                <pubDate>Mon, 14 Apr 2025 15:45:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744638276204/5cf04403-6bf0-4bf1-b9d3-89722bd90425.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>CI/CD stands for Continuous Integration and Continuous Delivery. It is a system or set of processes and methodologies that help developers quickly update codebases and deploy applications.</p>
<p>The Continuous Integration (CI) part of CI/CD means that developers can always integrate or merge their changes into the shared repository without breaking anything. Continuous Delivery, on the other hand, means that the code changes are automatically prepared for release after testing and validation.</p>
<p>CI/CD primarily involves various stages like building, testing, staging and deployment.</p>
<ul>
<li><p><strong>Build phase:</strong> This is where the code and its dependencies are compiled into a single executable. This is the first phase of Continuous Integration, and is triggered by an event like pushing code to the repository.</p>
</li>
<li><p><strong>Test phase:</strong> Here, the built artifacts are tested to be sure that the code runs as expected.</p>
</li>
<li><p><strong>Staging:</strong> Here, the application is run in a production-like environment so as to be sure it is production ready.</p>
</li>
<li><p><strong>Deployment:</strong> Here, the application is automatically deployed to the end-users.</p>
</li>
</ul>
<p>In this article, I’m going to explain how GitHub Actions works. I’ll also talk about basic GitHub Actions concepts, and then we’ll use it to build an example CI/CD pipeline.</p>
<h2 id="heading-what-is-github-actions">What is GitHub Actions?</h2>
<p>GitHub Actions is a service or feature of the GitHub platform that lets developers create their own CI/CD workflows directly on GitHub. It runs jobs on containers hosted by GitHub. The tasks are executed as defined in a YAML file called a workflow. This workflow file has to live on the <em>.github/workflows</em> folder on the repository for it to work.</p>
<h2 id="heading-basic-github-actions-concepts">Basic GitHub Actions Concepts</h2>
<p>GitHub Actions consists of events, jobs, tasks, runners, workflows, and various other features. Here is a brief explanation of the main concepts:</p>
<p><strong>Events:</strong> An event is basically something that happened. With GitHub, an event can be a push (when you push your code to the repository), a pull request, or even a cron job. These events trigger the CI/CD process.</p>
<p><strong>Tasks:</strong> When you use CI/CD, you want to be able to trigger an activity that should be done automatically. That activity is known as a task or step in GitHub. It could be building your code or testing it or deploying it.</p>
<p>Each of those tasks has to be defined by commands. A GitHub Actions task usually consists of the name, and the instructions on what to do in the form of a command which starts with  <code>- run:</code> or an Action which starts with <code>- uses:</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">code</span>
    <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">Node.js</span>
    <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v3</span>
    <span class="hljs-attr">with:</span>
      <span class="hljs-attr">node-version:</span> <span class="hljs-number">16</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">test</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">project</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run</span> <span class="hljs-string">build</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">"Deploy step goes here"</span>
</code></pre>
<p><strong>Runner:</strong> A GitHub runner is a server that runs your tasks. It executes what is defined in your GitHub workflow. You can use your own runners or you can use the GitHub runners.</p>
<p><strong>Job:</strong> A job is a collection of steps that are being executed on the same runner. Jobs are defined in a file called the workflow.</p>
<p><strong>Workflow:</strong> The GitHub workflow is a series of jobs defined in a YAML file, that are triggered upon an event. The events do not trigger individual tasks. They can only trigger workflows. Then the tasks in the jobs of the workflow are executed.</p>
<p><strong>Contexts:</strong> These provide a way to access information about workflows, jobs, and environments in GitHub. They are accessed with the expression <code>$${{ &lt;context&gt; }}</code>. Examples include <code>github</code>, <code>env</code>, <code>vars</code>, and <code>secrets</code>. The <code>github</code> context is used to access information about the workflow. For example:</p>
<pre><code class="lang-yaml"><span class="hljs-string">$${{github.repository}}</span> <span class="hljs-comment"># should tell the name of the repository</span>

<span class="hljs-string">$${{github.actor}}</span>  <span class="hljs-comment"># should tell the username of user that initially triggered the workflow</span>
</code></pre>
<p><strong>Secrets:</strong> This is used to store and access sensitive information that’s used by, and is available to, the workflow. Secrets are redacted when printed to the log. An example is $${{secrets.GITHUB_TOKEN}}.</p>
<h2 id="heading-how-to-build-a-simple-cicd-pipeline">How to Build a Simple CI/CD Pipeline</h2>
<p>Here, we’re going to build an example workflow to deploy a simple HTML and CSS website to GitHub Pages. Follow the steps below:</p>
<ol>
<li><p>Go to the sample code in my repository and fork it from <a target="_blank" href="https://github.com/chidiadi01/StaticPage-Starter">here</a>.</p>
</li>
<li><p>Go to the settings tab in the GitHub repository:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744220928970/d2f62ae0-49be-4770-b931-59e5bc28e20e.png" alt="Settings tab" class="image--center mx-auto" width="908" height="39" loading="lazy"></p>
<ol start="3">
<li><p>Go to the Pages settings:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744220974335/4aeac1df-be0d-493d-98d3-fb9ea4d48ca0.png" alt="Pages settings menu" class="image--center mx-auto" width="347" height="480" loading="lazy"></p>
</li>
<li><p>Set the deployment source to the <code>main</code> branch:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744290941501/365d8b5d-1265-42be-9de4-d3a07b984736.png" alt="Setting deployment source to main branch in GitHub pages" class="image--center mx-auto" width="858" height="369" loading="lazy"></p>
</li>
<li><p>Go to the General Actions settings and scroll down to the bottom:</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744221468786/d94fe477-65a3-49dc-85ea-6ab8cb2f9c63.png" alt="Find General Actions setting" class="image--center mx-auto" width="355" height="568" loading="lazy"></p>
<p> At the bottom, set the Workflow permissions to read and write:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744221589612/4f5b1cf2-8343-4da8-ad61-21ba76319ffc.png" alt="Set workflow permissions to read and write" class="image--center mx-auto" width="898" height="394" loading="lazy"></p>
<ol start="7">
<li><p>In the GitHub repository, you can clone it to your PC or press the fullstop (.) on your keyboard to open GitHub Codespaces, the online version of VS Code.</p>
</li>
<li><p>Go to the sidebar and click on create a new file:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744292745424/a3ca0a1e-13cf-4182-8425-9c3500e01e3d.png" alt="Creating new file" class="image--center mx-auto" width="924" height="394" loading="lazy"></p>
</li>
<li><p>Create a workflows folder and file. You can call it <code>deploy.yaml</code>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744292931628/300665a5-d517-46b1-9b30-21dd4bc228a6.png" alt="Creating a workflows folder and file named deploy.yaml" class="image--center mx-auto" width="335" height="215" loading="lazy"></p>
</li>
<li><p>Copy this code into the file:</p>
</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">Static</span> <span class="hljs-string">HTML</span> <span class="hljs-string">and</span> <span class="hljs-string">CSS</span> <span class="hljs-string">to</span> <span class="hljs-string">GitHub</span> <span class="hljs-string">Pages</span>

<span class="hljs-comment"># Trigger the workflow on push to the main branch</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
<span class="hljs-comment"># Define what operating system the job should run on</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">deploy:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">permissions:</span>
      <span class="hljs-attr">contents:</span> <span class="hljs-string">write</span>

    <span class="hljs-attr">steps:</span>
    <span class="hljs-comment"># Step 1: Checkout the repository</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">Code</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>

    <span class="hljs-comment"># Step 2: Check the files that have been checked out</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Display</span> <span class="hljs-string">files</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">ls</span>

    <span class="hljs-comment"># Step 3: Deploy to GitHub Pages</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">peaceiris/actions-gh-pages@v4</span>
      <span class="hljs-attr">with:</span>
        <span class="hljs-attr">github_token:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">publish_dir:</span> <span class="hljs-string">./</span> <span class="hljs-comment"># The HTML and CSS files lie in the root directory, hence that should be the publish directory</span>
</code></pre>
<ol start="11">
<li>Commit the code. You should see the job running when you go back to the repo:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744292376966/93d6078c-02c3-41f4-a4b5-09639522bbbe.png" alt="Running job" class="image--center mx-auto" width="904" height="53" loading="lazy"></p>
<p>When you’re done, go back to the home page of the repository and click on the Deployments section. There, you will see the GitHub Pages link to the deployment:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744293345646/e523a2b9-73a6-4ecf-9df8-99e03b457ad1.png" alt="GitHub Pages link" class="image--center mx-auto" width="516" height="338" loading="lazy"></p>
<p>When you’re done, your repository should look like <a target="_blank" href="https://github.com/chidiadi01/StaticPage-Final">this</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, you learned about how the CI/CD process works. We also covered the basic concepts of GitHub Actions. Finally, we created an example CI/CD pipeline with GitHub Actions. If you enjoyed this article, share it with others. You can also reach me on <a target="_blank" href="https://linkedin.com/in/chidiadi-anyanwu">LinkedIn</a> or <a target="_blank" href="https://x.com/chidiadi01">X</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Secure Ways to Access DeepSeek Using Third-Party Apps ]]>
                </title>
                <description>
                    <![CDATA[ AI-powered coding assistants have changed the way developers write software. They help automate repetitive tasks, catch errors early, and speed up development. But not all AI coding tools are built with security in mind. One of the most promising fre... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/secure-ways-to-access-deepseek-using-third-party-apps/</link>
                <guid isPermaLink="false">67b746282e12c8160896e071</guid>
                
                    <category>
                        <![CDATA[ Deepseek ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #ChatGPT #AI #MachineLearning #CodingAssistance #Programming #ArtificialIntelligence #CodeGeneration #SoftwareDevelopment #Technology #AIAssistance #DeveloperTools #NaturalLanguageProcessing #OpenAI #CodingTips #Innovation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwadamisi Samuel ]]>
                </dc:creator>
                <pubDate>Thu, 20 Feb 2025 15:11:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740064259806/efd0af33-1df9-415e-8944-6311e75391d0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>AI-powered coding assistants have changed the way developers write software. They help automate repetitive tasks, catch errors early, and speed up development. But not all AI coding tools are built with security in mind.</p>
<p>One of the most promising free AI coding assistants is DeepSeek. It’s been hailed as a game changer and has a reasoning model on par with or even better than OpenAI o1. It offers advanced code suggestions and supports multiple programming languages.</p>
<p>But here’s the problem—do you know what happens to your code after you enter it? Many free AI models operate as black boxes, with little transparency about how they handle user data. This raises serious concerns about code privacy, intellectual property security, and compliance with industry regulations.</p>
<p>For developers working on proprietary software or handling sensitive data, these risks aren’t just theoretical—they can lead to source code leaks, compliance violations, or even unauthorized AI training on your data.</p>
<p>That’s why third-party AI coding assistants are becoming the preferred choice. Unlike free tools, these alternatives offer better data protection, stronger security measures, and compliance-friendly environments without sacrificing performance.</p>
<p>In this article, we’ll break down how free AI coding assistants like DeepSeek pose security risks and why third-party alternatives provide a safer, more reliable solution for developers at all levels. I’ll also share with you my personal choices and discuss their features and strengths.</p>
<h2 id="heading-benefits-of-deepseek">Benefits of DeepSeek</h2>
<p>AI coding assistants have improved a lot recently, and DeepSeek-R1 is one of the most advanced free tools available. It goes beyond simple autocomplete, offering intelligent code suggestions, multi-language support, and AI-powered debugging—all for free.</p>
<p>Its model leverages technologies like Retrieval-Augmented Generation (RAG) to achieve context-awareness and it uses reinforcement learning to approach tasks with reasoning rather than simply predicting the most likely outcome and shows the user it’s chain of thought and process by which it came to a conclusion or carried out the task given to it.</p>
<h3 id="heading-what-makes-deepseek-stand-out">What Makes DeepSeek Stand Out?</h3>
<ul>
<li><p>DeepSeek understands context and intent, generating full functions instead of just completing words or lines. It also incorporates reinforcement learning to give better and more accurate responses, much like human reasoning. This makes the code it outputs more accurate.</p>
</li>
<li><p>Deepseek has ‘chain of thought” capabilities and it shows users it’s reasoning process while completing tasks or user inputs.</p>
</li>
<li><p>DeepSeek has support for multiple languages, and works seamlessly across Python, JavaScript, Go, Rust, and more.</p>
</li>
<li><p>DeepSeek is also very fast, giving you near-instant recommendations and helping you write code faster without breaking your flow.</p>
</li>
<li><p>DeepSeek is also a helpful debugger, as it not only suggests code but can also identify errors and recommend fixes. It can achieve this because the model is context-aware.</p>
</li>
<li><p>Finally, it’s free. Unlike some AI tools locked behind paywalls, DeepSeek provides all these features at no cost.</p>
</li>
</ul>
<p>With these capabilities, it’s no surprise that DeepSeek is gaining traction among developers looking for a free AI-powered coding assistant reported as the <a target="_blank" href="https://www.yahoo.com/tech/deepseek-hits-no-1-apples-023959452.html">Most downloaded app within Apple’s Top Free App category</a>. But does free always mean safe?</p>
<h2 id="heading-the-security-risks-of-free-ai-models-like-deepseek">The Security Risks of Free AI Models Like DeepSeek</h2>
<p>For all its benefits, DeepSeek—like most free AI coding assistants—comes with security risks that can’t be ignored.</p>
<p>First of all, do you know what happens to your code when you input it? DeepSeek doesn’t clearly disclose whether it stores or analyzes user input, raising concerns about data privacy and proprietary code exposure.</p>
<p>During a call with members of her subscription training, Heather Murray, an AI consultant for major corporations and the U.K. government who serves on the ISO committee for AI security, expressed concerns about DeepSeek’s policies regarding user data:</p>
<blockquote>
<p>“It keeps your data as long as it wants to, and even after users leave the app, it doesn’t delete their data. It’s going to hang on to that. That is a massive worry. All of that data is then transmitted and stored on servers in China. So that removes user data from under U.S., U.K. or European law — moving it under Chinese law, which is very, very different.”</p>
</blockquote>
<p>There are also potential intellectual property risks to think about. If DeepSeek retains input data, could fragments of your code appear in suggestions to other users? This is a real risk with AI models trained on user data. This means that any proprietary code you submit could become a suggestion for competitors using the same platform.</p>
<p>DeepSeek also hasn’t implemented any enterprise security standards. Unlike paid enterprise-grade AI solutions, DeepSeek does not provide guarantees of compliance with security frameworks like GDPR, SOC 2, or HIPAA.</p>
<p>Finally, there’s no guaranteed data isolation. Enterprise AI solutions often provide private model deployments or air-gapped environments, while free AI tools rely on centralized cloud processing, increasing exposure risks. This means your code is sent to external servers, increasing the risk of leaks or unauthorized access.</p>
<p>These risks don’t mean DeepSeek isn’t useful—but they do mean developers working on proprietary projects, enterprise applications, or sensitive data should think twice before relying on it alone.</p>
<h2 id="heading-secure-alternatives-to-free-ai-coding-assistants">Secure Alternatives to Free AI Coding Assistants</h2>
<p>If you rely on free AI coding assistants, you’ll need to balance performance, usability, and security. While free models like DeepSeek offer powerful code suggestions, they lack key security features required for professional and enterprise use.</p>
<p>Third-party AI tools provide a safer alternative, ensuring data privacy, enterprise compliance, and secure code processing.</p>
<p>So you might wonder: how do these third party apps achieve this higher level of security? Well, they do it by hosting the service on their local/personal servers which are under US and EU regulations and data protection laws. This is unlike the free DeepSeek API, which may route queries through shared infrastructure, which comes with unspecified backdoor access.</p>
<p>Here are two confirmed DeepSeek-powered AI assistants that address the security limitations of free models:</p>
<h3 id="heading-1-qodogen"><strong>1. QodoGen</strong></h3>
<p>QodoGen has a secure DeepSeek-powered AI assistant. It’s built for security-conscious developers and enterprises who want the advantages of DeepSeek without exposing sensitive code.</p>
<p>Some of its features are:</p>
<ul>
<li><p>Built-in security: Unlike the free DeepSeek model, QodoGen does not store, log, or use code for training.</p>
</li>
<li><p>Enterprise-grade protection: It’s designed to meet data privacy regulations like GDPR, SOC 2, and HIPAA.</p>
</li>
<li><p>Seamless IDE integration: QodoGen provides the same intuitive, real-time assistance as DeepSeek but with enhanced security controls. It’s available on your favorite IDEs like VSCode and JetBrains IDE.</p>
</li>
<li><p>Optional Data Sharing: Allows organizations and developers to fine-tune security settings and host the model on-premises or in a private cloud. It also provides users with the choice to Opt Out of data sharing with it’s servers all together( Go to the extension's settings and check the box <code>Opt out of sharing my data with Qodo</code>.)</p>
</li>
<li><p>Custom AI model options: It also has other AI models you can choose from to suit your personal or organization’s needs like OpenAI o1, GPT-4, Claude Sonnet 3.5, Gemini 2.0 flash and so on.</p>
</li>
<li><p>Guaranteed data isolation: QodoGen provides private model deployments / air-gapped environments which prevent unauthorized data collection.</p>
</li>
</ul>
<h3 id="heading-example-of-deepseek-r1-showing-chain-of-thought-on-qodogen">Example of Deepseek-R1 showing chain of thought on QodoGen</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739948742414/ea062034-6fcb-4ab3-93e3-cbc3f519369a.webp" alt="ea062034-6fcb-4ab3-93e3-cbc3f519369a" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-how-to-get-qodogen">How to get QodoGen</h3>
<p>To get QodoGen set up, visit its page on <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Codium.codium">Visual Studio Marketplace</a> to install it.</p>
<p>Alternatively, follow these steps to install the plugin from within VSCode:</p>
<ol>
<li><p>In VSCode, open the Extensions menu. You can do this by either:</p>
<ul>
<li><p>Clicking the Extensions icon in the Activity Bar on the left side.</p>
</li>
<li><p>Using the keyboard shortcut: <code>Cmd+Shift+X</code> for macOS. <code>Ctrl+Shift+X</code> for Windows and Linux.</p>
</li>
</ul>
</li>
<li><p>Type <strong>Qodo Gen</strong> in the search bar.</p>
</li>
<li><p>Click the Install button.</p>
</li>
<li><p>Sign Up with your email.</p>
</li>
</ol>
<p>To login to the Teams plan, after you have received an invitation, login using the email to which the invitation was sent. Team plans start at $15/month, while Enterprise solutions start at $45/month.</p>
<p>QodoGen also has a free plan for individual developers. <a target="_blank" href="https://www.qodo.ai/pricing/">You can check out their pricing here</a>.</p>
<h3 id="heading-2-perplexity-ai"><strong>2. Perplexity AI</strong></h3>
<p>Perplexity AI integrates DeepSeek-R1 while implementing security safeguards to minimize data risks. It originally served as a search engine for researchers, so it comes with cited sources for the output it generates. This means you can check, confirm, and further research any output it generates.</p>
<p>Some of its key features are:</p>
<ul>
<li><p>Data hosting security: User queries are processed in US/EU data centers, reducing risks associated with external data exposure.</p>
</li>
<li><p>Privacy-focused AI responses: Unlike free DeepSeek, Perplexity AI’s infrastructure prevents unauthorized data collection.</p>
</li>
<li><p>Available via web platform: Developers can access AI-assisted coding with DeepSeek’s capabilities without having to download the any application.</p>
</li>
<li><p>Gives you sources along with the output it generates, so you can be rest assured that the output is accurate (which you can confirm for yourself).</p>
</li>
<li><p>It has a free plan for individual developers, as well as a Teams subscription (starting at $20/month) and Enterprise subscription (with customizable plans). <a target="_blank" href="https://www.wheelhouse.com/products/perplexity-ai/pricing">You can check out their pricing here</a>.</p>
</li>
</ul>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXebbFPOo2iiGADXDNZSFrpRTwnaFC9ejCynScRQ9meDecqXlAqUmC6Lt20TecAHpU3LcJltaOvMzo_jzK0rqV3WEwln7eBj3mQubVoF38jfBahNwqvRPYW3TQDPjO-TdhqUNf8gag?key=J4IEKT06wkZslHq1SUCJyk0g" alt="AD_4nXebbFPOo2iiGADXDNZSFrpRTwnaFC9ejCynScRQ9meDecqXlAqUmC6Lt20TecAHpU3LcJltaOvMzo_jzK0rqV3WEwln7eBj3mQubVoF38jfBahNwqvRPYW3TQDPjO-TdhqUNf8gag?key=J4IEKT06wkZslHq1SUCJyk0g" width="600" height="400" loading="lazy"></p>
<h3 id="heading-example-of-deepseek-r1-showing-chain-of-thought-on-perplexcity-ai">Example of Deepseek-R1 showing chain of thought on Perplexcity AI</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739949029656/94879bc2-b10d-4d98-9364-14017f5728b9.png" alt="94879bc2-b10d-4d98-9364-14017f5728b9" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The above screenshot shows how it produced the output step by step. You can also see the option to read further with the dropdown buttons with sources for confirmation and resources for further study.</p>
<h2 id="heading-how-to-choose-the-right-ai-coding-assistant">How to Choose the Right AI Coding Assistant</h2>
<p>With the growing number of AI coding assistants available, selecting the right one comes down to security, data privacy, and usability. Whether you’re an individual developer or part of an enterprise team, your AI assistant should offer:</p>
<ul>
<li><p>Strong security measures</p>
</li>
<li><p>Enterprise-ready compliance</p>
</li>
<li><p>High-quality AI models</p>
</li>
<li><p>Seamless accessibility</p>
</li>
</ul>
<p>Both QodoGen and Perplexity AI have demonstrated a commitment to these principles. By integrating DeepSeek-R1, they provide cutting-edge AI assistance while maintaining security and ease of access for developers.</p>
<h3 id="heading-my-recommendation-qodogen-for-developers-perplexity-for-quick-access">My Recommendation: QodoGen for Developers, Perplexity for Quick Access</h3>
<p>For developers, teams, and enterprises, <code>QodoGen</code> is the best choice. It’s designed specifically for developers, integrating directly into popular IDEs like VS Code and JetBrains IDE. This ensures a smooth, secure, and efficient coding experience without data privacy concerns.</p>
<p>QodoGen delivers the power of DeepSeek-R1 and other top AI models while keeping your code protected within your development environment.</p>
<p>For individuals looking for an easy, web-based solution without installing an extension, <code>Perplexity AI</code> offers a good alternative. Without the need for installations, it provides AI-assisted coding in a browser, making it an accessible option if you prefer quick and hassle-free access to DeepSeek-R1 and don’t need deep IDE integration.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI coding assistants have become essential tools for developers, but not all solutions prioritize security and data privacy. While free models like DeepSeek offer impressive capabilities, they come with potential risks that developers and enterprises cannot afford to overlook.</p>
<p>Third-party AI coding assistants provide a secure and reliable alternative, ensuring that code remains private while still delivering state-of-the-art AI performance. Among them, QodoGen and Perplexity AI stand out by integrating DeepSeek-R1 into their platforms before others while maintaining a strong commitment to security and accessibility.</p>
<p>For developers, teams, and enterprises, QodoGen is the ideal choice, offering a seamless IDE experience with enterprise-grade security. Meanwhile, if you prefer quick, web-based access, Perplexity AI provides a hassle-free way to leverage DeepSeek-R1 without installation.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
