<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Russell Jones</title>
    <description>The latest articles on DEV Community by Russell Jones (@jonesrussell).</description>
    <link>https://dev.to/jonesrussell</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F136661%2Fd812786d-8ef0-4b08-9421-35be6f99b174.png</url>
      <title>DEV Community: Russell Jones</title>
      <link>https://dev.to/jonesrussell</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jonesrussell"/>
    <language>en</language>
    <item>
      <title>A beginner's guide to Git worktrees: What they are, why they matter, and how to use them without breaking anything</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Thu, 16 Jul 2026 20:47:15 +0000</pubDate>
      <link>https://dev.to/jonesrussell/a-beginners-guide-to-git-worktrees-what-they-are-why-they-matter-and-how-to-use-them-without-3hb3</link>
      <guid>https://dev.to/jonesrussell/a-beginners-guide-to-git-worktrees-what-they-are-why-they-matter-and-how-to-use-them-without-3hb3</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;If you have ever needed to work on two branches at the same time, Git worktrees can save you a lot of friction. This post covers what worktrees are, why they exist, and how you can use them safely as a beginner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You have &lt;a href="https://git-scm.com/" rel="noopener noreferrer"&gt;Git&lt;/a&gt; installed&lt;/li&gt;
&lt;li&gt;You can run commands in your terminal&lt;/li&gt;
&lt;li&gt;You already have a local repository&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is a Git Worktree?
&lt;/h2&gt;

&lt;p&gt;A Git worktree is an extra working folder connected to the same repository history.&lt;br&gt;&lt;br&gt;
You can think of it as another checkout of your project, without making another full clone.&lt;/p&gt;

&lt;p&gt;Your main folder still exists. A worktree gives you a second folder where a different branch can be checked out at the same time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Worktrees Exist
&lt;/h2&gt;

&lt;p&gt;Git worktrees solve a practical problem. You may be in the middle of feature work, then need to fix a bug on another branch right away.&lt;/p&gt;

&lt;p&gt;Without worktrees, you usually do one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stash or commit unfinished work, then switch branches&lt;/li&gt;
&lt;li&gt;Open a second full clone of the same repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both options work, but both add overhead. Worktrees give you a cleaner path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Normal Clone vs Branch Checkout vs Worktree
&lt;/h2&gt;

&lt;p&gt;Here is the simple difference:&lt;/p&gt;
&lt;h3&gt;
  
  
  Normal clone
&lt;/h3&gt;

&lt;p&gt;A clone is a separate copy of a repository with its own &lt;code&gt;.git&lt;/code&gt; directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/example/project.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You use this when you need the repository on your machine for the first time. It is fully independent from other clones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Branch checkout
&lt;/h3&gt;

&lt;p&gt;A branch checkout changes which branch is active in your current folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout feature/foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fast, but only one branch can be active in that folder at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worktree
&lt;/h3&gt;

&lt;p&gt;A worktree creates another folder tied to the same repository, usually on a different branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../feature-foo feature/foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have two folders open at once: your main folder and &lt;code&gt;../feature-foo&lt;/code&gt;. Each can point at a different branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Benefits of Worktrees
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Multiple branches checked out at once
&lt;/h3&gt;

&lt;p&gt;You can keep your main branch open in one folder and your feature branch in another.&lt;br&gt;&lt;br&gt;
No constant branch switching.&lt;/p&gt;
&lt;h3&gt;
  
  
  2) Isolated environments for experiments
&lt;/h3&gt;

&lt;p&gt;You can test risky changes in one worktree without touching the working state in another folder.&lt;/p&gt;
&lt;h3&gt;
  
  
  3) No need for multiple full clones
&lt;/h3&gt;

&lt;p&gt;Worktrees share repository data, so you avoid duplicate clones for everyday branch work.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Safe Beginner Workflow
&lt;/h2&gt;

&lt;p&gt;This is a clean workflow you can use right away.&lt;/p&gt;
&lt;h3&gt;
  
  
  1) Create a worktree
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../feature-foo feature/foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This command creates a new folder named &lt;code&gt;../feature-foo&lt;/code&gt; and checks out &lt;code&gt;feature/foo&lt;/code&gt; there.&lt;br&gt;&lt;br&gt;
If &lt;code&gt;feature/foo&lt;/code&gt; does not exist yet, create it first with &lt;code&gt;git branch feature/foo&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2) Switch into it
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../feature-foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now every Git command runs inside that worktree folder.&lt;br&gt;&lt;br&gt;
Before you edit files, confirm where you are with &lt;code&gt;pwd&lt;/code&gt; and &lt;code&gt;git branch --show-current&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  3) Commit from it
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add first pass of feature foo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These commits belong to the branch checked out in that worktree.&lt;br&gt;&lt;br&gt;
You do not need to return to your original folder to commit.&lt;/p&gt;
&lt;h3&gt;
  
  
  4) Remove it safely
&lt;/h3&gt;

&lt;p&gt;First leave the worktree folder, then remove it with Git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../your-main-repo
git worktree remove ../feature-foo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Git to unregister the worktree and remove the directory safely.&lt;br&gt;&lt;br&gt;
Always prefer this over deleting the folder manually.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to See Your Current Worktrees
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This shows every registered worktree path and branch.&lt;br&gt;&lt;br&gt;
Use this often, especially when you are learning.&lt;/p&gt;
&lt;h2&gt;
  
  
  Common Beginner Mistakes
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Deleting the directory without removing the worktree
&lt;/h3&gt;

&lt;p&gt;If you run &lt;code&gt;rm -rf&lt;/code&gt; on a worktree folder first, Git can keep stale metadata.&lt;br&gt;&lt;br&gt;
Remove worktrees with &lt;code&gt;git worktree remove &amp;lt;path&amp;gt;&lt;/code&gt; whenever possible.&lt;/p&gt;
&lt;h3&gt;
  
  
  Forgetting which worktree you are in
&lt;/h3&gt;

&lt;p&gt;It is easy to commit to the wrong branch when two folders look similar.&lt;br&gt;&lt;br&gt;
Check &lt;code&gt;pwd&lt;/code&gt; and &lt;code&gt;git branch --show-current&lt;/code&gt; before making changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Trying to check out the same branch twice
&lt;/h3&gt;

&lt;p&gt;Git does not allow the same branch to be active in two worktrees at once.&lt;br&gt;&lt;br&gt;
Create a new branch if you need a second experimental space.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Simple Mental Model for &lt;code&gt;.git/worktrees&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Your main repository still has the real Git database.&lt;br&gt;&lt;br&gt;
Inside it, &lt;code&gt;.git/worktrees&lt;/code&gt; stores small records that point to each extra working folder.&lt;/p&gt;

&lt;p&gt;Think of it like a clipboard that says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which extra folders exist&lt;/li&gt;
&lt;li&gt;Which branch each one uses&lt;/li&gt;
&lt;li&gt;Whether Git still expects them to be present&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why manual deletion can confuse Git. The clipboard still has an entry, even if the folder is gone.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Clean Up Orphaned Worktrees
&lt;/h2&gt;

&lt;p&gt;Sometimes a worktree folder gets deleted outside Git.&lt;br&gt;&lt;br&gt;
You can clean this up safely in a few steps.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: List what Git thinks exists
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Look for paths that no longer exist on disk.&lt;br&gt;&lt;br&gt;
Those are likely orphans.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Prune stale metadata
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This removes stale worktree entries that no longer point to valid folders.&lt;br&gt;&lt;br&gt;
It is a safe maintenance command for this situation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Verify cleanup
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run the list command again to confirm orphan entries are gone.&lt;br&gt;&lt;br&gt;
If everything looks clean, you are done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verify It Works
&lt;/h2&gt;

&lt;p&gt;Run this quick check whenever you start using worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
git branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;
&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These three commands tell you what worktrees exist, which branch is active, and which folder you are in.&lt;br&gt;&lt;br&gt;
That simple habit prevents most beginner mistakes.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>git</category>
      <category>gitworktree</category>
      <category>versioncontrol</category>
      <category>beginnerguide</category>
    </item>
    <item>
      <title>The hackathon Anthropic didn't expect</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Thu, 16 Jul 2026 20:47:11 +0000</pubDate>
      <link>https://dev.to/jonesrussell/the-hackathon-anthropic-didnt-expect-2hp1</link>
      <guid>https://dev.to/jonesrussell/the-hackathon-anthropic-didnt-expect-2hp1</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.platformer.news/boris-cherny-interview-ai-jobs/" rel="noopener noreferrer"&gt;Casey Newton interviewed Boris Cherny&lt;/a&gt;, the creator of Claude Code, for Platformer last week. Most of the coverage pulled the headline-friendly quote that coding is "solved" and moved on. I want to point at a smaller moment in the same interview that I think matters more. This post is about that moment, and three things I've watched up close that line up with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hackathon Anthropic didn't expect
&lt;/h2&gt;

&lt;p&gt;Newton asks Cherny about the AI divide. The worry, in shorthand, is that the people who already have power will use these tools to get more of it. The early data on who benefits from new technology usually goes that way. So who's actually getting the most out of Claude Code?&lt;/p&gt;

&lt;p&gt;Cherny's answer surprised Newton, and it surprised me when I read it. He talks about a recent Anthropic hackathon for the Opus 4.7 release, and says the people who won were largely not professional engineers. "There was an electrician, a doctor, a carpenter who used it to build an app." Same pattern at the 4.6 hackathon. He calls it a continuous surprise. The people who get the most value out of Claude Code, he says, are not the people he'd expect.&lt;/p&gt;

&lt;p&gt;That's a small line in a long interview. It's the line I haven't stopped thinking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions, not one
&lt;/h2&gt;

&lt;p&gt;The AI-jobs discourse is mostly fighting over one question: will engineers be replaced. Cherny's answer to that question, if you read the whole interview, is actually mild. He thinks the title "software engineer" probably changes ("builder" is the word he uses), the role expands, and the number of people writing code with the help of agents goes up roughly a hundred-fold. That's not an extinction story. It's a transformation story, with a hiring pull, not a layoff cliff.&lt;/p&gt;

&lt;p&gt;But that's not the only question we should be asking. The other question is the one Newton was getting at: when the syntax gate falls, who walks through?&lt;/p&gt;

&lt;p&gt;The hackathon detail is an early answer. Not "the engineers got faster." An electrician, a doctor, and a carpenter built apps that won. The default story we tell about new technology — the people in the room get more powerful first — doesn't fit. The people in the room didn't even win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I've watched up close
&lt;/h2&gt;

&lt;p&gt;Here's what I'm seeing in three different places.&lt;/p&gt;

&lt;p&gt;I just finished my Grade 12 coursework as an adult learner through Sagamok Anishnawbek's Lifelong Learning Centre. AI was in the loop for a lot of it. I'm not the demographic anyone meant when they said "AI for developers." I'm a forty-something who came back to finish his OSSD, and the tools worked for me anyway. I finished the work that for years had been hard to finish. That's one shape.&lt;/p&gt;

&lt;p&gt;I started &lt;a href="https://www.change.org/p/rainbow-district-school-board-give-students-a-clear-fair-ai-policy-before-next-semester" rel="noopener noreferrer"&gt;a petition&lt;/a&gt; asking the Rainbow District School Board to publish a real public AI policy before next semester. Students across the board are using these tools every day. The adults setting the rules haven't caught up. Partway through my own coursework, I was pulled aside and questioned about my AI use because there was no policy to point at. The petition is about that, but it's also about the kids: they're walking through a door before anyone has written a rule about it. That's another shape.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://oiatc.ca" rel="noopener noreferrer"&gt;Anokii&lt;/a&gt;, the embedded chat on the OIATC site, on top of my own framework. Anokii isn't being built for engineers. It's being built so a member of a community can find a community-specific resource and get an answer that cites where it came from. Per-community variants. A relevance gate so it stays quiet when it shouldn't speak. A topic-confidence gate on the citations. When the tool is reachable, and built with the people who'll use it in mind, the people who reach for it are not the obvious ones. That's a third shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Three different angles. An adult learner finishing high school. A petition asking for a fair rule. A chat that surfaces community resources without pretending to be smarter than it is.&lt;/p&gt;

&lt;p&gt;None of these are "engineers replaced." All of them are "someone got access to a tool that wasn't available before, and the question is whether the systems around them caught up."&lt;/p&gt;

&lt;p&gt;Cherny's hackathon detail isn't an outlier. It's the leading edge of what happens everywhere when the syntax-and-credentials gate falls. People who were never in the room start showing up. The electrician builds the app. The adult learner finishes the coursework. The community member finds the resource they needed and gets a real citation instead of an authoritative-sounding guess.&lt;/p&gt;

&lt;p&gt;The energy we spend arguing about whether engineers will exist in five years is energy we are not spending on what the hackathon detail is actually telling us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the energy belongs
&lt;/h2&gt;

&lt;p&gt;Three places I'd want it to go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schools need policy now, not after the next semester.&lt;/strong&gt; The Rainbow District petition is one example. Every board with students using these tools is facing the same problem. The students are already there. The policy can't be retroactive forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communities deserve tools built for them, not demos pointed at them.&lt;/strong&gt; Anokii is the version of that I'm running, and the way to tell whether a community tool is real is whether it cites its sources, whether it knows when to stay quiet, and whether it gets better when the community pushes back on it. Demos do not pass any of those tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For engineers and builders and whatever else we get called next:&lt;/strong&gt; stop pretending the threat is your job disappearing. The threat, if there is one, is that the tools become most useful to the people who already had power. Cherny's data so far says that is not what's happening. Don't make it happen. (If you want the engineer-facing half of this argument, I wrote &lt;a href="https://jonesrussell.github.io/blog/from-vibe-coded-to-shippable/" rel="noopener noreferrer"&gt;the playbook for taking a vibe-coded prototype to something a stranger can run&lt;/a&gt; earlier this week. This post is the other half.)&lt;/p&gt;

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

&lt;p&gt;The people getting the most out of these tools right now are not the people the discourse expects. The electrician winning the hackathon, the adult learner finishing the diploma, the community member finding the resource. None of them are the story we're telling. All of them are the story we should be telling.&lt;/p&gt;

&lt;p&gt;That should change how we talk about this.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>aiassisteddev</category>
      <category>equity</category>
      <category>community</category>
      <category>policy</category>
    </item>
    <item>
      <title>From vibe-coded to shippable: a playbook</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Thu, 16 Jul 2026 20:46:37 +0000</pubDate>
      <link>https://dev.to/jonesrussell/from-vibe-coded-to-shippable-a-playbook-j8i</link>
      <guid>https://dev.to/jonesrussell/from-vibe-coded-to-shippable-a-playbook-j8i</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;There's a lot of energy right now spent bashing vibe coding. I think most of it is aimed at the wrong target. The MVP you stand up with AI doesn't have to generalize. That's not what it was for. The interesting craft is what you do &lt;em&gt;next&lt;/em&gt; to take that prototype from "it works on my laptop" to something a stranger can run, debug, and trust. That's a craft worth respecting. This post is the playbook I'm currently running on a real public repo, with six specific moves and an artifact for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong fight
&lt;/h2&gt;

&lt;p&gt;The bashing usually goes like this: "Look at this AI-generated MVP, look at how brittle it is, look at how much it cost in tokens to get it half-right." Sure. Now compare it to the alternative: an empty directory and a developer who hasn't started. The point of the prototype isn't to be production code. The point of the prototype is to find out whether the idea works at all. To answer a question. To make something move on a screen that wasn't moving yesterday.&lt;/p&gt;

&lt;p&gt;Once it moves, you have new information. You know which parts are load-bearing. You know which gotchas bit you. You know which assumptions held up. You're now in a much better position to do the second pass, and the second pass is where the craft lives. The craft is the set of moves you make to take a working prototype to a state where someone else can run it, debug it, and trust it.&lt;/p&gt;

&lt;p&gt;Those moves are not mysterious. They have names. They are the rest of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repo on the bench
&lt;/h2&gt;

&lt;p&gt;The worked example is the OIATC application, a public PHP repo I build alongside my own modern PHP framework, &lt;a href="https://github.com/waaseyaa/framework" rel="noopener noreferrer"&gt;Waaseyaa&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/waaseyaa/oiatc-waaseyaa" rel="noopener noreferrer"&gt;https://github.com/waaseyaa/oiatc-waaseyaa&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It powers &lt;a href="https://oiatc.ca" rel="noopener noreferrer"&gt;oiatc.ca&lt;/a&gt;. The marquee feature inside it is Anokii, an embedded AI chat that grounds its answers in the site's own community-resource pages and cites them. Anokii started as a vibe-coded chat experiment. It's now a RAG pipeline with a relevance gate, per-community variants, anonymous query-gap logging, and a topic-confidence gate on citations. Every step below points at a specific artifact in that repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Milestone the roadmap
&lt;/h2&gt;

&lt;p&gt;The first thing that turned the Anokii work from "a directory of chat hacks" into "a project" was naming the phases. Phase 1 got a chat working at all. Phase 2 introduced RAG grounding, split into stages: Stage 1 was the &lt;code&gt;doc_chunk&lt;/code&gt; entity and the &lt;code&gt;app:ingest-docs&lt;/code&gt; CLI that fills it; Stage 2 was the keyword-RAG retrieval over those chunks.&lt;/p&gt;

&lt;p&gt;Branches in the repo carry the phase names. &lt;code&gt;feat/doc-chunk-ingestion&lt;/code&gt; landed Stage 1. &lt;code&gt;feat/keyword-rag-chat&lt;/code&gt; landed the MVP retrieval. &lt;code&gt;feat/sagamok-resources&lt;/code&gt; and &lt;code&gt;feat/data-sovereignty-and-masthead&lt;/code&gt; carried sibling work. Each branch closes with a merge commit that documents what shipped, and only after that does the next phase begin.&lt;/p&gt;

&lt;p&gt;Three phases, a handful of branches, every one of them with a concrete definition of done. That's enough structure to know what to work on next, and just as importantly, what &lt;em&gt;not&lt;/em&gt; to work on right now.&lt;/p&gt;

&lt;p&gt;You don't need a wiki or a project board. You need branch names that announce intent and merge commits that close the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Write the runbook, including the gotchas
&lt;/h2&gt;

&lt;p&gt;The repo's &lt;a href="https://github.com/waaseyaa/oiatc-waaseyaa/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;CLAUDE.md&lt;/a&gt; is the runbook. It opens with a Strategy folder pointer: a separate workspace outside the repo tracks every live page on oiatc.ca against its canonical Twig source, last-updated date, and analytics. The CLAUDE.md says, plainly, that if the two disagree, the repo wins.&lt;/p&gt;

&lt;p&gt;Below that pointer it documents the architecture (&lt;code&gt;Access/&lt;/code&gt;, &lt;code&gt;Controller/&lt;/code&gt;, &lt;code&gt;Domain/&lt;/code&gt;, &lt;code&gt;Entity/&lt;/code&gt;, &lt;code&gt;Provider/&lt;/code&gt;, &lt;code&gt;Support/&lt;/code&gt;), the ServiceProvider DI methods with full signatures (&lt;code&gt;singleton&lt;/code&gt;, &lt;code&gt;bind&lt;/code&gt;, &lt;code&gt;resolve&lt;/code&gt;, &lt;code&gt;tag&lt;/code&gt;, &lt;code&gt;entityType&lt;/code&gt;), the queue Job pattern with &lt;code&gt;tries&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;retryAfter&lt;/code&gt;, and the frontend template families (site shell vs. longform documents vs. news). An Operations section, added in commit &lt;code&gt;896ec8f&lt;/code&gt;, covers deploy, Raspberry Pi access, and secrets.&lt;/p&gt;

&lt;p&gt;The runbook is the operating manual you wish someone had handed you. The OIATC one is dense because it earned every line. The mistakes that produced those lines aren't generic; they're specific to this app, this framework version, this deployment target. That's exactly what makes them worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Pin the world
&lt;/h2&gt;

&lt;p&gt;Vibe-coded prototypes love &lt;code&gt;composer install&lt;/code&gt; and &lt;code&gt;git clone main&lt;/code&gt;. That works on Tuesday. It breaks on Friday when upstream cuts a release. The OIATC repo pins the world three different ways.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;composer.lock&lt;/code&gt; is committed and treated as the source of truth. Lock-file drift gets its own entry in the upstream-notes (entry 003 walks through a drift caused by a post-hash &lt;code&gt;php: &amp;gt;=8.5&lt;/code&gt; constraint and the fix). Second, the deploy contract is a &lt;code&gt;docker compose run&lt;/code&gt; that calls &lt;code&gt;bin/waaseyaa db:init&lt;/code&gt; before bringing the app up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; oiatc-app bin/waaseyaa db:init
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db:init&lt;/code&gt; is idempotent. Fresh volume gets migrations. Current schema is a no-op. Safe to invoke on every deploy. That's what makes a deploy step a contract instead of a tradition.&lt;/p&gt;

&lt;p&gt;Third, the app lives on a specific &lt;code&gt;waaseyaa/framework&lt;/code&gt; alpha version (currently alpha.188). Upgrades happen as deliberate events with their own branch and their own entry in the upstream-notes, not as quiet drift. When upstream cuts a release that breaks something, you have a working baseline to compare against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Name the graveyard
&lt;/h2&gt;

&lt;p&gt;The repo has a literal graveyard directory: &lt;a href="https://github.com/waaseyaa/oiatc-waaseyaa/tree/main/docs/archive" rel="noopener noreferrer"&gt;&lt;code&gt;docs/archive/2026-04-20-cut-pages/&lt;/code&gt;&lt;/a&gt;. It holds Twig templates and design notes for pages that were on oiatc.ca and got cut. Not deleted from git history. Not pretended to never have existed. Filed under a date and a reason so the next person can read what we tried and why we stopped.&lt;/p&gt;

&lt;p&gt;This pattern shows up in the codebase too. The keyword-RAG retrieval merge commit literally calls itself "Path B." There was a Path A. Path A's notes are still around. Anyone looking at the current Path B implementation can see the alternative that was considered and the trade-offs that drove the decision.&lt;/p&gt;

&lt;p&gt;Naming the graveyard is one of the cheapest, most under-used moves in software. Most repos don't do it because it feels embarrassing. The OIATC repo does it because each cut page and each abandoned path represents a hypothesis tested. The next person who has a similar hypothesis deserves to see the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5. Recon before you build
&lt;/h2&gt;

&lt;p&gt;The single most useful artifact in the repo is &lt;a href="https://github.com/waaseyaa/oiatc-waaseyaa/blob/main/docs/waaseyaa-upstream-notes.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/waaseyaa-upstream-notes.md&lt;/code&gt;&lt;/a&gt;. It's a running log of framework quirks, bugs, breakages, and missing pieces hit while building on an alpha release of &lt;code&gt;waaseyaa/framework&lt;/code&gt;. Each entry uses a fixed format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## NNN — short title&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Date / version:**&lt;/span&gt; YYYY-MM-DD · waaseyaa/framework alpha.NNN
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Doing:**&lt;/span&gt; what we were doing when we hit it
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Symptom:**&lt;/span&gt; the observable problem
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Workaround:**&lt;/span&gt; what we did to get unblocked
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Likely upstream fix:**&lt;/span&gt; the proper change in waaseyaa/framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 16+ entries. Stale &lt;code&gt;VERSION&lt;/code&gt; files. Ambiguous class resolution between the metapackage and split mirrors. Lock-file drift after a platform requirement bump. Hard &lt;code&gt;ext-sodium&lt;/code&gt; dependency via the OIDC stack. Each one is recon for the framework itself.&lt;/p&gt;

&lt;p&gt;The point of the upstream-notes is not to complain. The point is to keep app-level hacks out of the consumer code. Every entry is a decision: do we patch around it here, do we file the upstream fix now, do we wait until the next alpha. Without the log, those decisions get re-litigated every time someone hits the same wall.&lt;/p&gt;

&lt;p&gt;This is also the move I think AI tooling makes most useful. When you hit a quirk, write the entry first. The structured shape forces you to articulate what you actually saw, what you're guessing, and what would fix it upstream. That writing is exactly the input the model needs to either help you work around it cleanly or propose an upstream change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6. Layer specs on what's next
&lt;/h2&gt;

&lt;p&gt;The hardest current work on Anokii doesn't get vibe-coded. The topic-confidence gate that decides whether a citation is worth showing (&lt;code&gt;65de562&lt;/code&gt;), the relevance gate that ensures only genuinely relevant passages are cited (&lt;code&gt;8359e37&lt;/code&gt;), the climate-companion variant for the Massey Solar resource cluster (&lt;code&gt;c8956f6&lt;/code&gt;), the shared relational-graph instance per community (&lt;code&gt;928191d&lt;/code&gt;) — all of those run through &lt;a href="https://github.com/waaseyaa/oiatc-waaseyaa/tree/main/docs/superpowers" rel="noopener noreferrer"&gt;&lt;code&gt;docs/superpowers/specs&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;docs/superpowers/plans&lt;/code&gt;. Each change has a spec articulating the intent, a plan decomposing the work, and reviewable PRs landing the implementation.&lt;/p&gt;

&lt;p&gt;Why specs and not just commits: a relevance gate that filters citations is the kind of thing where a hallucinated implementation looks fine until a user gets a confidently-wrong answer. The spec gives you a contract for what the gate is supposed to do. The plan gives reviewers something to evaluate against. The implementation has a referenceable target.&lt;/p&gt;

&lt;p&gt;This is the handoff move. Vibe coding got the chat answering. The runbook captured the patterns. The pinned world made the deploys reproducible. The graveyard remembered the dead ends. The upstream-notes captured what the framework still owes us. And now spec-driven work takes over for the parts that are too big or too risky to vibe through. Each layer earned the right to the next layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like together
&lt;/h2&gt;

&lt;p&gt;The OIATC repo is not a clean codebase. It's a layered one. Some Anokii code still reads like the experiment it started as, because it still works and the cost of replacing it is higher than the cost of keeping it. The newer pieces — the keyword-RAG retrieval, the relevance gate, the topic-confidence gate — were built with specs and reviewed PRs because each of them is a place where a quiet bug becomes a confidently-wrong answer to a real person looking for real community resources.&lt;/p&gt;

&lt;p&gt;Vibe coding was the first move. The six moves above are the second, third, fourth, fifth, sixth, and seventh. None of them are mysterious. None of them are expensive. They are the boring craft that turns a working prototype into something a stranger can run, debug, and trust.&lt;/p&gt;

&lt;p&gt;If you've been bashing vibe coding, you're aiming at the start of a process and ignoring the rest of it. If you've been vibe-coding without any of the rest, you're going to keep losing days to the same gotchas, and the next person who touches your prototype is going to lose them too.&lt;/p&gt;

&lt;p&gt;There's no shame in vibe coding. There's a lot of value in what comes next.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>aiassisteddev</category>
      <category>prototyping</category>
      <category>specdriven</category>
    </item>
    <item>
      <title>AI keeps speccing my projects on pre-AI timelines</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Thu, 16 Jul 2026 20:46:33 +0000</pubDate>
      <link>https://dev.to/jonesrussell/ai-keeps-speccing-my-projects-on-pre-ai-timelines-3205</link>
      <guid>https://dev.to/jonesrussell/ai-keeps-speccing-my-projects-on-pre-ai-timelines-3205</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;You sit down to spec a new feature with &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; or whichever assistant. You describe the surface, the constraints, the slices you want it broken into. The plan comes back: "5 work packages, 6 to 8 weeks." You shipped a 5-WP mission in a day yesterday. This post is about that gap and why it eats more time than it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Spec mode is where it shows up worst. You ask for a plan. The plan arrives with timelines attached. Those timelines are calibrated to a world that ended around 2023. A "2-week task" inside a modern plan is often something you can ship the same afternoon if the scope is honest.&lt;/p&gt;

&lt;p&gt;When you push back, the assistant does not back down quickly. It cites "industry norms," talks about "code review cycles," reminds you about testing time and edge cases. All of which are real concerns. None of which describe what actually happens when you and an AI co-author the work at full session-pace.&lt;/p&gt;

&lt;p&gt;You end up in a loop. You spec. It estimates long. You explain that the work is being done with AI. It hedges. You re-spec. You ship in a fraction of the estimate. Next time you do this same dance again, because the assistant has no persistent sense that you ship faster than the baseline it was trained on.&lt;/p&gt;

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

&lt;p&gt;Almost every public dataset that taught these models how long things take pre-dates widespread AI-assisted development. Stack Overflow threads from 2019 about how long a Laravel package takes to build. Engineering manager blog posts from 2021 about ticket-pointing conventions. Git histories from teams that did not have Claude on the keyboard. The model sees those signals and projects.&lt;/p&gt;

&lt;p&gt;The reasoning models are smart enough to know AI assistance changes velocity. They will say so if you ask them directly. The problem is they do not apply that knowledge to estimation by default. You have to invoke it explicitly, every time, and even then they hedge in case you turn out to be slower than you said.&lt;/p&gt;

&lt;p&gt;It is the same failure mode as asking an assistant in early 2024 whether you should use React 18 features. It knows React 19 exists. It does not always update its working assumptions to match.&lt;/p&gt;

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

&lt;p&gt;Two things.&lt;/p&gt;

&lt;p&gt;First, time. Every spec session has a recalibration tax. You say two weeks; it says six. You push; it says four. You re-explain; it says three. You ship in two days. Multiply across a year of speccing and you are spending real hours arguing with an estimator that is wrong in one direction.&lt;/p&gt;

&lt;p&gt;Second, drift. When you accept a long estimate to move on, the WP breakdown that gets generated is shaped by that estimate. Work packages get padded with imagined complexity to justify the duration. Tasks get split that did not need splitting. You end up implementing a plan designed for a slower world and discovering halfway through that half the WPs collapse into one.&lt;/p&gt;

&lt;p&gt;The downstream version of this is worse: an assistant reviewing your PR will sometimes flag "is the scope of this change too large for a single WP?" when the change is fine and the WP boundary was the artifact of a stale estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;The fixes are all small and all annoying because you have to repeat them.&lt;/p&gt;

&lt;p&gt;State the velocity assumption in your spec prompt explicitly. "This is being done with AI assistance. Estimate WPs in hours, not days, and assume each WP fits inside a single focused session." This is not subtle and it works. You can put it in your project's spec template so you do not have to remember.&lt;/p&gt;

&lt;p&gt;Reject the estimate during planning, not after. If the plan comes back with multi-week WPs, push back before generating the WP breakdown. The breakdown is downstream of the duration assumption; if you let it materialize, you inherit its shape.&lt;/p&gt;

&lt;p&gt;Cite recent evidence. "I shipped a 5-WP mission yesterday in a day. Use that as the baseline, not pre-2024 industry norms." The model responds to concrete recent counter-evidence more than to abstract argument.&lt;/p&gt;

&lt;p&gt;Strip duration estimates from the plan entirely when you can. Half the time you do not need them and they only exist because the spec template asked for them. A plan that says "5 WPs, dependency-ordered" is more honest than a plan that says "5 WPs, 6-8 weeks." You are not running a Gantt chart. You are sequencing work.&lt;/p&gt;

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

&lt;p&gt;This will probably get worse before it gets better. Training data for "how long does this take" lags real-world velocity by a couple of years. Right now the gap is roughly 2023 vs 2026, which is about a 3-5x estimation error in many domains. As AI-assisted development accelerates, that gap widens, and the public-corpus signal stays stuck in the past.&lt;/p&gt;

&lt;p&gt;The assistants that handle this well will be ones that calibrate against the user's recent shipping pace, not against the corpus average. That requires per-user telemetry the current tools do not have, or explicit user-state that they do not yet persist between sessions. Until then, the workaround is the four-step dance above.&lt;/p&gt;

&lt;p&gt;The honest version of the complaint is not "AI is bad at estimates." It is "AI is estimating on a baseline that the existence of AI itself made obsolete." Worth naming so you can stop arguing with it and just override the defaults.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>vibecoding</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Bimaaji: agent-safe mutations for Waaseyaa</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Thu, 02 Jul 2026 06:43:30 +0000</pubDate>
      <link>https://dev.to/jonesrussell/bimaaji-agent-safe-mutations-for-waaseyaa-3enn</link>
      <guid>https://dev.to/jonesrussell/bimaaji-agent-safe-mutations-for-waaseyaa-3enn</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;If you let an AI agent modify your application, the agent needs more than a text editor. Raw &lt;code&gt;str_replace&lt;/code&gt; on a PHP file passes a lot of tests and still breaks things an hour later in production, because the tool has no idea what the file actually represents. Bimaaji is the &lt;a href="https://github.com/waaseyaa/framework" rel="noopener noreferrer"&gt;Waaseyaa&lt;/a&gt; package that gives agents a structured path from "I want to add a field to this entity" to a reviewable patch that a community's sovereignty rules have already vetted. This post walks through what shipped in &lt;code&gt;waaseyaa/bimaaji&lt;/code&gt; and why each piece exists.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; familiarity with Waaseyaa's package layout, PHP 8.4+, and the idea that an application has more state than the filesystem (routes, entities, introspection metadata).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why not just let the agent edit files
&lt;/h2&gt;

&lt;p&gt;The failure mode you want to avoid: an agent reads a prompt like "add a &lt;code&gt;published_at&lt;/code&gt; field to the &lt;code&gt;Post&lt;/code&gt; entity," does a reasonable-looking edit to &lt;code&gt;Post.php&lt;/code&gt;, and leaves the rest of the app inconsistent. The migration is missing. The JSON:API resource doesn't expose the field. The admin panel still doesn't know it exists. The sovereignty profile that was supposed to block the change on a local-only deployment never got consulted.&lt;/p&gt;

&lt;p&gt;Each of those is a different subsystem. A good agent can write a correct edit to any one of them. What a filesystem-level tool cannot do is ensure the edit is &lt;em&gt;coordinated&lt;/em&gt; across all of them and is &lt;em&gt;allowed&lt;/em&gt; under the community's posture.&lt;/p&gt;

&lt;p&gt;Bimaaji separates that problem into three stages: introspect, propose, patch.&lt;/p&gt;

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

&lt;p&gt;The package description (from &lt;code&gt;packages/bimaaji/composer.json&lt;/code&gt;) spells it out: &lt;em&gt;application graph introspection and agent-safe mutation for Waaseyaa.&lt;/em&gt; The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Introspection → ApplicationGraph → MutationRequest → Validator → PatchGenerator → PatchSet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent reads the graph, submits a structured mutation request, a validator checks it against sovereignty rules, and the patch generator returns reviewable diffs. Nothing touches the filesystem until a human (or a higher-level workflow) accepts the &lt;code&gt;PatchSet&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introspection: what the agent reads first
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/Introspection/&lt;/code&gt; holds a provider for every surface the agent might need context on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AdminIntrospectionProvider&lt;/code&gt; — what's exposed to the admin panel&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EntityIntrospectionProvider&lt;/code&gt; — entity definitions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JsonApiIntrospectionProvider&lt;/code&gt; — public API shape&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PublicSurfaceProvider&lt;/code&gt; — what's reachable from outside&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RoutingIntrospectionProvider&lt;/code&gt; — route table&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SovereigntyIntrospectionProvider&lt;/code&gt; — the community's deployment posture and rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one implements &lt;code&gt;GraphSectionProviderInterface&lt;/code&gt; and contributes a &lt;code&gt;GraphSection&lt;/code&gt; to the &lt;code&gt;ApplicationGraph&lt;/code&gt;. The point is that the agent never reads source files to understand the app. It reads the graph. That is the canonical view.&lt;/p&gt;

&lt;p&gt;This matters because it means an agent's understanding of your app is a data structure you control, not whatever the agent's context window happened to pick up from grep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task DSL
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/Dsl/&lt;/code&gt; is the entry point for agents. &lt;code&gt;TaskParser&lt;/code&gt; parses a structured task definition into &lt;code&gt;TaskDefinition&lt;/code&gt; objects. &lt;code&gt;TaskPipeline&lt;/code&gt; runs them, producing a &lt;code&gt;TaskPipelineResult&lt;/code&gt;. The DSL describes &lt;em&gt;what&lt;/em&gt; to change (add a field, add an entity type, add a route stub, add a test skeleton), not &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That separation is the whole point. An agent says "add field &lt;code&gt;published_at: datetime&lt;/code&gt; to entity &lt;code&gt;Post&lt;/code&gt;." Bimaaji decides how that compiles into a PHP edit, a migration stub, an admin surface update, and a JSON:API resource change. The agent is not writing PHP. It's writing a task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutation: the reviewable proposal
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/Mutation/&lt;/code&gt; turns a parsed task into a &lt;code&gt;MutationRequest&lt;/code&gt;, runs it through &lt;code&gt;MutationValidator&lt;/code&gt;, and returns a &lt;code&gt;MutationResult&lt;/code&gt;. The validator is where the sovereignty guardrails plug in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/Policy/SovereigntyGuardrails.php&lt;/code&gt; and &lt;code&gt;GuardrailRule&lt;/code&gt; hold the rules. The model: a community declares a &lt;code&gt;SovereigntyProfile&lt;/code&gt; (local, hybrid, cloud) on their Waaseyaa deployment. Certain mutations are allowed under certain profiles and not others. A local-only community might forbid any mutation that adds outbound network dependencies. A cloud-hosted community might allow them but require a specific audit annotation. The guardrails are declarative, matrixed per profile, and they &lt;em&gt;stop the mutation at the proposal stage&lt;/em&gt;, not after the patch has already rewritten files.&lt;/p&gt;

&lt;p&gt;This is where Waaseyaa's sovereignty story gets teeth. Community control over AI-driven changes is not a policy document. It's a validator in the mutation path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patching: AST, not strings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;src/Patch/PatchGenerator.php&lt;/code&gt; takes a validated &lt;code&gt;MutationRequest&lt;/code&gt; and produces a &lt;code&gt;PatchSet&lt;/code&gt; of &lt;code&gt;PatchEntry&lt;/code&gt; objects. For PHP files, it uses &lt;a href="https://github.com/nikic/PHP-Parser" rel="noopener noreferrer"&gt;nikic/php-parser&lt;/a&gt; via &lt;code&gt;PhpFileBuilder&lt;/code&gt; to round-trip through an AST. That means the patch is syntactically valid by construction. You cannot generate a patch that breaks parsing because the patch itself is a parsed tree that gets printed back out.&lt;/p&gt;

&lt;p&gt;For non-PHP files, the generator falls back to constrained operations with risk flags. Anything that can't be AST-verified is surfaced as unsafe and requires an explicit opt-in. That's the right default. Agents should fail loudly on anything they can't guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  The integration test
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tests/Integration/FullPipelineTest.php&lt;/code&gt; runs the whole flow: introspect an app, submit a task through the DSL, validate against guardrails, generate a patch, assert the patch is well-formed. It's the check that all five subsystems (Graph, Dsl, Mutation, Policy, Patch) still agree on the contracts between them. When any one of them changes, that test catches the drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in the bigger picture
&lt;/h2&gt;

&lt;p&gt;Bimaaji is the seam where Waaseyaa's AI tooling meets Waaseyaa's community governance. The whole Waaseyaa thesis is that the software communities run should answer to the community, not the other way around. Sovereignty profiles are the policy expression of that. Bimaaji is the enforcement point for anything an AI agent wants to do to the app.&lt;/p&gt;

&lt;p&gt;The package is at &lt;a href="https://github.com/waaseyaa/framework/tree/main/packages/bimaaji" rel="noopener noreferrer"&gt;waaseyaa/framework packages/bimaaji&lt;/a&gt;. The README is still a scaffold note; the code has moved past that. If you want to read one thing, start with &lt;code&gt;tests/Integration/FullPipelineTest.php&lt;/code&gt; — it's the shortest honest tour of what the pipeline does end to end.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>waaseyaa</category>
      <category>aiagents</category>
      <category>php</category>
      <category>sovereignty</category>
    </item>
    <item>
      <title>The ingest side of a sovereign language platform</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Thu, 25 Jun 2026 18:39:42 +0000</pubDate>
      <link>https://dev.to/jonesrussell/the-ingest-side-of-a-sovereign-language-platform-3m6i</link>
      <guid>https://dev.to/jonesrussell/the-ingest-side-of-a-sovereign-language-platform-3m6i</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;I just shipped the ingest side of &lt;a href="https://minoo.live" rel="noopener noreferrer"&gt;Minoo&lt;/a&gt;, the Anishinaabemowin language platform I am building. The short version: an Elder posts a video of himself holding a whiteboard with a word on it, and that teaching becomes a published, searchable lesson, with a human reviewing every step and the community owning the whole stack. This post walks through how it actually works and the decisions underneath it.&lt;/p&gt;

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

&lt;p&gt;Anishinaabemowin teaching happens constantly, but it is scattered. Elders share words on Facebook, in notebooks, in classrooms, and almost none of it flows into anything a learner can search tonight. The few deep digital resources that do exist are owned by institutions, not by the communities whose language they hold.&lt;/p&gt;

&lt;p&gt;So I set two goals that have to be true at the same time. Turn everyday teaching into structured, reusable data. And keep that data under community control end to end. Not control as a policy promise bolted on afterward, but control built into where the data lives, who can read it, and who can change it.&lt;/p&gt;

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

&lt;p&gt;The source material is real. Steven Bennett, an Elder from Sagamok, posts short videos holding up a whiteboard with one word, the Anishinaabemowin on top and the English gloss below. The pipeline turns one of those into a lesson in four stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingest.&lt;/strong&gt; A reel comes in, by upload or through a URL importer backed by a swappable media-fetcher interface. The system pulls a keyframe and the audio, stages the media, and creates a draft tagged with its community provenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vision.&lt;/strong&gt; The keyframe goes to a vision model through the framework's provider abstraction, which returns a small JSON object: the Ojibwe and the English read straight off the whiteboard. Today that provider is Claude vision. The binding is swappable by config, and the sovereign-stack goal is a local model before any public beta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transcribe, Curate, Publish.&lt;/strong&gt; Each of these is a human gate, not an automated hop. The model drafts, a person confirms. Curate promotes the entry into the dictionary. Publish puts it on the live site inside a lesson. Nothing reaches the public without a human pass.&lt;/p&gt;

&lt;p&gt;The design principle is that the model is an assistant that fills a draft, never an authority that publishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language tags: BCP 47, three layers
&lt;/h2&gt;

&lt;p&gt;One of the core decisions was how to tag the language so it can federate across the 21 Robinson Huron Treaty nations without flattening their dialects into one another. I use &lt;a href="https://www.rfc-editor.org/info/bcp47" rel="noopener noreferrer"&gt;BCP 47&lt;/a&gt; with three layers and a fallback chain.&lt;/p&gt;

&lt;p&gt;There is the macrolanguage, &lt;code&gt;oj&lt;/code&gt;, always displayed with the autonym Anishinaabemowin rather than the ISO exonyms. There is an optional dialect layer in the middle. And there is community provenance as a private-use subtag, for example &lt;code&gt;oj-x-sagamok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Translation memory keys on the full tag, never on a dialect-only code, so each community keeps its own granularity. Dialect groupings (Nishnaabemwin spans two ISO codes) are derived from the community tag, not stored as the source of truth. A tag like &lt;code&gt;oj-x-sagamok&lt;/code&gt; resolves &lt;code&gt;oj-x-sagamok&lt;/code&gt; to &lt;code&gt;oj&lt;/code&gt; to &lt;code&gt;en&lt;/code&gt;, which needed a small fix to the framework's i18n fallback chain so it would resolve private-use subtags at all. That fix shipped upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The translation side
&lt;/h2&gt;

&lt;p&gt;Alongside transcription there is a translation memory exposed at &lt;code&gt;/api/lang&lt;/code&gt;: exact match first, then fuzzy, then log the gap when there is no entry yet, so the backlog fills itself as it gets used.&lt;/p&gt;

&lt;p&gt;To seed it with real demand instead of guesses, I crawled the public English interface strings off the 21 RHT nation websites and ranked them by how many sites each one appears on. The result is a demand-ordered list of the words communities actually put on their own sites, things like Governance, Education, Membership, Chief and Council, a few hundred of them, waiting on speaker-verified translations. That ranked list is the backlog, highest demand first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sovereign by construction
&lt;/h2&gt;

&lt;p&gt;The part I care about most: this runs on infrastructure the community controls. The app is a PHP service built on the &lt;a href="https://github.com/waaseyaa/framework" rel="noopener noreferrer"&gt;Waaseyaa framework&lt;/a&gt;, in Docker behind Caddy, on a Raspberry Pi the community runs, not on someone else's cloud. The corpus stays local. The AI provider is swappable by config. The model assists, it does not own the language, the data, or the hosting.&lt;/p&gt;

&lt;p&gt;That boundary shows up in the API too. The public &lt;code&gt;/api/lang&lt;/code&gt; surface is read-only and validated, returning a 422 on a malformed tag rather than guessing. The admin pipeline and the corpus behind it are staff-gated. Reading is open. The language itself is governed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is honest about it
&lt;/h2&gt;

&lt;p&gt;Build-in-public should include the rough parts. Pulling video off Facebook is login-walled, so reliable ingest is upload-first for now. The vision provider is a hosted model, which is the interim and not the destination. And "published in the admin" has to actually mean "visible on the public site," which is exactly the kind of seam you only find by walking the whole pipeline on camera. Finding those is the point of demoing it for real.&lt;/p&gt;

&lt;p&gt;The language has been taught this way for a long time, one word at a time, by people willing to stand in front of a camera and share it. The software's only job is to catch those teachings and hand them back to the community in a form a learner can use, without taking ownership of them along the way.&lt;/p&gt;

&lt;p&gt;Watch the walkthrough: &lt;a href="https://youtu.be/zfx7CHs_Ec0" rel="noopener noreferrer"&gt;youtu.be/zfx7CHs_Ec0&lt;/a&gt;. The framework is open source at &lt;a href="https://github.com/waaseyaa/framework" rel="noopener noreferrer"&gt;github.com/waaseyaa/framework&lt;/a&gt; and on &lt;a href="https://packagist.org/packages/waaseyaa/framework" rel="noopener noreferrer"&gt;Packagist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>waaseyaa</category>
      <category>languagetech</category>
      <category>aiagents</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>One URL, two readers: serving HTML to people and Markdown to agents</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 22 Jun 2026 22:04:37 +0000</pubDate>
      <link>https://dev.to/jonesrussell/one-url-two-readers-serving-html-to-people-and-markdown-to-agents-11l2</link>
      <guid>https://dev.to/jonesrussell/one-url-two-readers-serving-html-to-people-and-markdown-to-agents-11l2</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;The web has two kinds of readers now: people and agents. Most stacks make you build a second system to serve the second one, a separate API with its own routes, auth, and serializers. This post shows the approach &lt;a href="https://github.com/waaseyaa/framework" rel="noopener noreferrer"&gt;Waaseyaa&lt;/a&gt; takes instead: one URL serves a human a web page and an AI agent clean Markdown, decided by HTTP content negotiation. It covers the content type you define, the negotiation that picks the format, and the agent-facing routes that come along for free.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Familiarity with HTTP &lt;code&gt;Accept&lt;/code&gt; headers and basic PHP. Waaseyaa is an early-alpha PHP framework, so treat the specifics as a moving target.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Define the content once
&lt;/h2&gt;

&lt;p&gt;You describe the shape of your content one time. In Waaseyaa that is a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waaseyaa make:content-type story &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"title:string,body:text,source_url:string"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That scaffolds a &lt;code&gt;story&lt;/code&gt; content type with three fields. Then you add an entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waaseyaa entity:create story &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"The Five Totems"&lt;/span&gt; &lt;span class="nt"&gt;--field&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You never write a controller, a route, or a serializer for any of this. The type is the only thing you author. Everything that follows is the framework reading that one definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  One URL, negotiated by Accept
&lt;/h2&gt;

&lt;p&gt;The same canonical path, &lt;code&gt;/{type}/{id}&lt;/code&gt;, serves both audiences. What comes back depends on the request's &lt;code&gt;Accept&lt;/code&gt; header. A browser sends &lt;code&gt;text/html&lt;/code&gt; and gets a rendered page. An agent that asks for &lt;code&gt;text/markdown&lt;/code&gt; gets Markdown. The decision lives in &lt;code&gt;MediaTypeAcceptNegotiator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Waaseyaa\Foundation\Http\ContentNegotiation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MediaTypeAcceptNegotiator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;HTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'text/html'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;string&lt;/span&gt; &lt;span class="no"&gt;MARKDOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'text/markdown'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;negotiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$acceptHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$supported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$default&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Ranks the Accept entries (RFC 7231) and returns the best supported match.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The negotiator parses the &lt;code&gt;Accept&lt;/code&gt; header by quality value and returns the most specific supported media type. The human path and the agent path converge on one URL, so there is no &lt;code&gt;/api/story/123&lt;/code&gt; shadow of &lt;code&gt;/story/123&lt;/code&gt; to keep in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  A human toggle for the same switch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Accept&lt;/code&gt; headers are invisible in a browser, so there is also an explicit query override. The negotiator recognizes it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;resolveQueryOverride&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$supported&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;\array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'raw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MARKDOWN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;\is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'md'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'markdown'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MARKDOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'html'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append &lt;code&gt;?raw&lt;/code&gt; or &lt;code&gt;?format=md&lt;/code&gt; to any content URL and you see exactly what an agent sees. That makes the agent-facing output something you can eyeball in a browser, not a black box you have to script against to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching two formats at one address
&lt;/h2&gt;

&lt;p&gt;Serving two representations from one URL has a well-known hazard: a shared cache can hand the HTML variant to an agent or the Markdown to a browser. &lt;code&gt;SsrPageHandler&lt;/code&gt; guards against that by varying the cache on the negotiated type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$mediaType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;negotiateMediaType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$httpRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ...render either Markdown or HTML based on $mediaType...&lt;/span&gt;

&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Vary'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Accept'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Vary: Accept&lt;/code&gt; header tells every cache in the chain that the response depends on the request's &lt;code&gt;Accept&lt;/code&gt; header, so the Markdown and HTML variants never cross-contaminate. One URL, two cache entries, no leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent-facing routes you get for free
&lt;/h2&gt;

&lt;p&gt;Because the framework already knows which content types are public, it can publish the discovery surface agents and crawlers expect without you wiring anything. &lt;code&gt;SeoPublicController&lt;/code&gt; exposes three zero-config routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;robotsTxt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;   &lt;span class="c1"&gt;// /robots.txt&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;sitemapXml&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;   &lt;span class="c1"&gt;// /sitemap.xml&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;llmsTxt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;      &lt;span class="c1"&gt;// /llms.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/llms.txt&lt;/code&gt; is the emerging convention for telling language models what a site contains and where to look. Here it is generated from the same content-type metadata that drives everything else, alongside schema.org JSON-LD injected into the page head. Your content becomes legible to an AI assistant the moment it is published, without a second pipeline.&lt;/p&gt;

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

&lt;p&gt;As more of the web gets read through AI assistants, the content you publish is increasingly consumed by something that does not render HTML. The common answer is to stand up a parallel API: more routes, more auth surface, more drift between what people see and what machines see. Negotiating on one URL collapses that back into a single source of truth. You define the content once, and the same address answers both readers correctly.&lt;/p&gt;

&lt;p&gt;It is still alpha, and the write side has rougher edges than the read side. But the read path holds the thesis: one URL, two readers, no second system.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>waaseyaa</category>
      <category>contentnegotiation</category>
      <category>aiagents</category>
      <category>php</category>
    </item>
    <item>
      <title>AI slop and the content treadmill every developer is on</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 22 Jun 2026 22:04:32 +0000</pubDate>
      <link>https://dev.to/jonesrussell/ai-slop-and-the-content-treadmill-every-developer-is-on-4he5</link>
      <guid>https://dev.to/jonesrussell/ai-slop-and-the-content-treadmill-every-developer-is-on-4he5</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;I built a machine that turns my git commits into social media posts. This post is about why I did that, what it costs, and whether any of us can share our work anymore without contributing to the flood of AI slop.&lt;/p&gt;

&lt;p&gt;Let me be honest about my own setup first.&lt;/p&gt;

&lt;h2&gt;
  
  
  I automated my own content pipeline
&lt;/h2&gt;

&lt;p&gt;Every day a script scans my repositories for recent commits. It groups them by theme, scores each group on how postable it looks, and files a queue item. I review the queue, pick the good ones, and a second tool drafts a blog post or a short update. A third tool rewrites that draft three times, once for &lt;a href="https://bsky.app/" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;, once for &lt;a href="https://www.linkedin.com/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, once for Facebook, and pushes all three into a scheduler.&lt;/p&gt;

&lt;p&gt;Four stages: mine, curate, produce, distribute. A human, me, sits in the middle of two of them. The rest runs on its own.&lt;/p&gt;

&lt;p&gt;I am not proud of all of it. I am also not going to pretend I would keep up without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The treadmill has a lot of belts
&lt;/h2&gt;

&lt;p&gt;Here is the part nobody mentions when you start sharing your work. It is not one post. It is one idea, reshaped for every platform's algorithm, because each one punishes you for treating it like the others.&lt;/p&gt;

&lt;p&gt;Bluesky wants one or two sentences and a link, under 300 characters, or it reads as spam.&lt;/p&gt;

&lt;p&gt;LinkedIn wants 1,200 to 1,800 characters with the hook in the first two lines, because everything after "see more" is invisible to people who never click.&lt;/p&gt;

&lt;p&gt;Facebook barely shows text posts to anyone who does not already follow you, so you end up writing for an audience that is already yours.&lt;/p&gt;

&lt;p&gt;X wanted something else again, before I disconnected it.&lt;/p&gt;

&lt;p&gt;Same idea. Four rewrites. Four character budgets. Four hashtag policies. Four mental models of an algorithm I do not control and cannot see. And that is before you reach Mastodon, Threads, Reddit, a newsletter, &lt;a href="https://dev.to/"&gt;dev.to&lt;/a&gt;, and whatever launched this quarter.&lt;/p&gt;

&lt;p&gt;I am a developer. I want to share what I built and have a few of the right people see it. Instead I am a one-person content team optimizing for five recommendation engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the slop actually comes from
&lt;/h2&gt;

&lt;p&gt;We talk about AI slop like it is a content problem. Low-effort articles, generated images, summaries of summaries. But the slop is a symptom. The disease is the incentive.&lt;/p&gt;

&lt;p&gt;When the only way to be seen is to feed five algorithms every day, nobody can do that by hand and still ship code. So we automate. And automated, optimized, competent, voiceless content is exactly what slop is.&lt;/p&gt;

&lt;p&gt;My pipeline is good. The posts are accurate, they link to real commits, they read fine. That is the problem. "Reads fine" at infinite scale is the slop. I am not flooding the feed with garbage. I am flooding it with volume. Past a certain point those are the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think the way out looks like
&lt;/h2&gt;

&lt;p&gt;I do not have this solved. I have a few moves that feel less bad than the alternative, and I would genuinely like your read on them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write once, syndicate from a canonical source.&lt;/strong&gt; The blog post is the real thing. Everything else points back to it. The social copy is a doorway, not the room. One piece of writing holds my actual voice, instead of five disposable ones competing to be the loudest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automate distribution, not authorship.&lt;/strong&gt; I let machines handle scheduling and reformatting. I do not let them decide what is worth saying. The human stays on the idea and the voice. The robot does the cross-posting. The moment the robot picks the topic, it is slop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cover fewer platforms, on purpose.&lt;/strong&gt; You do not have to be everywhere. Owning one channel you control, an RSS feed, a newsletter, your own domain, beats renting attention on five you do not. I would rather have 200 readers who chose me than 5,000 impressions an algorithm rented me for an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat the reader as the tiebreaker.&lt;/strong&gt; When the algorithm and the human want different things, pick the human. It performs worse this quarter. It is the only thing that compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclose the assistance.&lt;/strong&gt; If a tool helped me write something, saying so costs me nothing and keeps me honest about what I am putting into the feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A question, not a conclusion
&lt;/h2&gt;

&lt;p&gt;Here is what I keep getting stuck on. If I stop, I lose to the people who do not. If we all keep going, the feeds become unreadable and none of us win either. That is a coordination problem, and I cannot solve it from inside my own pipeline.&lt;/p&gt;

&lt;p&gt;So I am asking you. How do you share your work without turning into a content factory? Have you cut platforms and survived it? Did owning your own channel actually work, or is it a slow fade into obscurity? Is disclosing AI assistance signal, or just more noise?&lt;/p&gt;

&lt;p&gt;I will read every reply. Not the algorithm. Me.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>socialmedia</category>
      <category>content</category>
    </item>
    <item>
      <title>Agent-friendly JSON output for PHP CI tools</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Sun, 24 May 2026 19:33:07 +0000</pubDate>
      <link>https://dev.to/jonesrussell/agent-friendly-json-output-for-php-ci-tools-2720</link>
      <guid>https://dev.to/jonesrussell/agent-friendly-json-output-for-php-ci-tools-2720</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;When an AI agent runs your test suite or a CI gate during an implement-or-review loop, the verbose stdout gets piped straight back into its context window. A full &lt;a href="https://phpunit.de/" rel="noopener noreferrer"&gt;PHPUnit&lt;/a&gt; run on the &lt;a href="https://github.com/waaseyaa/framework" rel="noopener noreferrer"&gt;Waaseyaa framework&lt;/a&gt; monorepo is around 12,000 lines. &lt;code&gt;bin/check-package-layers&lt;/code&gt; is about 600. Per iteration, per gate. The token cost is real, and it compounds across review cycles. This post walks through &lt;code&gt;waaseyaa/agent-output&lt;/code&gt;, a Layer 0 package that shrinks that output to a single NDJSON line for agents while leaving human terminal output completely unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agent context windows hate CI output
&lt;/h2&gt;

&lt;p&gt;The pattern shows up the moment you let an agent drive your test loop. The agent runs &lt;code&gt;composer test&lt;/code&gt;. PHPUnit emits its banner, then a dot per test, then a footer summary, then optionally a slow-test report. None of that helps the agent. It needs three things: did the run pass, what failed, where. Everything else is noise that displaces real signal.&lt;/p&gt;

&lt;p&gt;The same is true for &lt;code&gt;bin/check-package-layers&lt;/code&gt;, &lt;code&gt;bin/check-phpstan&lt;/code&gt;, &lt;code&gt;tools/drift-detector.sh&lt;/code&gt;, and friends. Each one is a CI gate that the agent already understands at the contract level. The full human-readable output exists to help a person scan and react. An agent does not need any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the package does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waaseyaa/agent-output&lt;/code&gt; is a single-purpose Layer 0 package (no &lt;code&gt;waaseyaa/*&lt;/code&gt; runtime deps, installable standalone). It does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detects an agent runtime&lt;/strong&gt; from a list of well-known env vars (&lt;code&gt;CLAUDE_CODE&lt;/code&gt;, &lt;code&gt;CURSOR_AGENT&lt;/code&gt;, and the rest), extensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provides a &lt;code&gt;FormatterInterface&lt;/code&gt;&lt;/strong&gt; and first-party formatters for PHPUnit, Pest, PHPStan, the &lt;code&gt;bin/check-*&lt;/code&gt; CI gates, and the drift detector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honors three activation triggers&lt;/strong&gt; per command: an &lt;code&gt;--output=json&lt;/code&gt; flag, a &lt;code&gt;WAASEYAA_OUTPUT=json&lt;/code&gt; env var, or auto-activation when an agent env var is set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When none of those triggers apply, the affected command emits exactly the human output it always did. No JSON fields leak, no exit codes change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to flip a tool into agent mode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/check-package-layers &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;span class="nv"&gt;WAASEYAA_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json bin/check-package-layers
&lt;span class="nv"&gt;CLAUDE_CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 bin/check-package-layers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is explicit per-invocation. The second sets it for the shell. The third is what happens automatically when Claude Code (or another supported agent) drives your terminal — you do not have to wire anything up; the auto-detection kicks in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage
&lt;/h2&gt;

&lt;p&gt;Here is the full set of tools the package now covers, taken verbatim from the package README:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Formatter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin/check-package-layers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output=json&lt;/code&gt; / env&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PackageLayersFormatter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin/check-dead-code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output=json&lt;/code&gt; / env&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeadCodeFormatter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin/check-getquery-bindings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output=json&lt;/code&gt; / env&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetQueryBindingsFormatter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin/check-composer-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output=json&lt;/code&gt; / env&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ComposerPolicyFormatter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin/check-phpstan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output=json&lt;/code&gt; / env&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PhpStanFormatter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/drift-detector.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--output=json&lt;/code&gt; / env&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DriftDetectorFormatter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vendor/bin/phpunit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WAASEYAA_OUTPUT=json&lt;/code&gt; (PHPUnit does not surface custom CLI flags)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PhpUnitFormatter&lt;/code&gt; via &lt;code&gt;AgentOutputPhpUnitExtension&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five &lt;code&gt;bin/check-*&lt;/code&gt; scripts, a drift detector, and PHPUnit. Each one emits an NDJSON envelope through a formatter dedicated to that tool's domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHPUnit is the awkward one
&lt;/h2&gt;

&lt;p&gt;PHPUnit's extension API does not surface custom CLI flags. There is no clean way to add &lt;code&gt;--output=json&lt;/code&gt; and have PHPUnit pass it to your extension. So the env var is the canonical trigger, and the package ships a PHPUnit 10 extension that registers six event subscribers (passed, failed, errored, marked-incomplete, skipped, execution-finished) over a shared run-state object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PhpUnitRunState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$passed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$skipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @var list&amp;lt;array{test: string, file: string, line: int, message: string}&amp;gt; */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That class lives in its own file rather than as an anonymous shape inside the extension, so PHPStan can type-check the field accesses without inferring &lt;code&gt;mixed&lt;/code&gt; through anonymous classes. A small thing, but it is the kind of detail that decides whether a package's own lint suite stays green.&lt;/p&gt;

&lt;p&gt;The extension itself is a no-op when &lt;code&gt;WAASEYAA_OUTPUT&lt;/code&gt; is not &lt;code&gt;json&lt;/code&gt; — zero overhead in human mode. When it is, the envelope is printed at &lt;code&gt;TestRunner\ExecutionFinished&lt;/code&gt; with a leading newline so it lands on its own trailing line. Agent consumers read the file line-by-line and parse the line that starts with &lt;code&gt;{"tool":"phpunit"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;WP06 of the mission was an empirical token-reduction smoke test against the original NFR. The headline result, measured on &lt;code&gt;packages/foundation/tests/Unit --no-coverage&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard PHPUnit output:&lt;/strong&gt; 2,209 bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent envelope (NDJSON line only):&lt;/strong&gt; 117 bytes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduction:&lt;/strong&gt; 94.70%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The threshold was ≥90%. The pattern delivers. And that number understates the savings on a full monorepo run, where the human output runs in the thousands of lines and the envelope stays a single line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use Laravel PAO?
&lt;/h2&gt;

&lt;p&gt;The pattern was lifted from Laravel PAO (released around May 2026), but the package is framework-native for two reasons. First, PAO does not cover the custom CI gates the Waaseyaa monorepo runs as hard gates (&lt;code&gt;bin/check-package-layers&lt;/code&gt; and the rest). Second, the formatters need to live alongside the gate scripts so the contract between script and envelope shape can evolve in the same PR — third-party packaging would have made that coupling awkward.&lt;/p&gt;

&lt;p&gt;The package is also a Layer 0 dependency, which means anyone outside the Waaseyaa monorepo can install just &lt;code&gt;waaseyaa/agent-output&lt;/code&gt; and reuse the formatter interface for their own tools. The detection logic and envelope contract travel; the bin/check-* wrappers stay in the framework where they belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in your own monorepo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require waaseyaa/agent-output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then either pass &lt;code&gt;--output=json&lt;/code&gt; to any supported script, set &lt;code&gt;WAASEYAA_OUTPUT=json&lt;/code&gt; in your shell, or run under an agent that sets &lt;code&gt;CLAUDE_CODE=1&lt;/code&gt;. For PHPUnit specifically, register the extension in &lt;code&gt;phpunit.xml.dist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;extensions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;bootstrap&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Waaseyaa\AgentOutput\Listener\AgentOutputPhpUnitExtension"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/extensions&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension self-disables when &lt;code&gt;WAASEYAA_OUTPUT&lt;/code&gt; is not set to &lt;code&gt;json&lt;/code&gt;, so registering it does not change human-mode output.&lt;/p&gt;

&lt;p&gt;For the full envelope schema, formatter contract, and a guide for writing third-party formatters, see &lt;code&gt;docs/specs/agent-output.md&lt;/code&gt; in the framework repo.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>php</category>
      <category>citools</category>
      <category>waaseyaa</category>
    </item>
    <item>
      <title>Spot the AI: can you tell which passage Claude wrote?</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Sat, 23 May 2026 21:11:45 +0000</pubDate>
      <link>https://dev.to/jonesrussell/spot-the-ai-can-you-tell-which-passage-claude-wrote-d25</link>
      <guid>https://dev.to/jonesrussell/spot-the-ai-can-you-tell-which-passage-claude-wrote-d25</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://spot-the-ai.oiatc.ca" rel="noopener noreferrer"&gt;Spot the AI&lt;/a&gt; is a small web game. You're shown two short passages, one written by a human author and one written by &lt;a href="https://www.anthropic.com/claude" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, and you pick which one is the AI. This post is a heads up that the game is live and an invitation to play a few rounds.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://spot-the-ai.oiatc.ca" rel="noopener noreferrer"&gt;spot-the-ai.oiatc.ca&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Read both passages.&lt;/li&gt;
&lt;li&gt;Pick the one you think Claude wrote.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the whole loop. No account, no setup.&lt;/p&gt;

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

&lt;p&gt;People talk a lot about AI writing without testing whether they can actually tell the difference. The game is a tiny way to test yourself before you make claims about what AI writing sounds like.&lt;/p&gt;

&lt;p&gt;It is also a small thing under the &lt;a href="https://oiatc.ca" rel="noopener noreferrer"&gt;OIATC&lt;/a&gt; umbrella, which is the broader push toward Indigenous-controlled AI tooling and infrastructure. Most of that work happens out of view. This one happens to be playable in a browser.&lt;/p&gt;

&lt;p&gt;Play a few rounds and see how you do.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>claude</category>
      <category>games</category>
    </item>
    <item>
      <title>Bumping a PHP monorepo to 8.5: the mechanics</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 11 May 2026 18:22:30 +0000</pubDate>
      <link>https://dev.to/jonesrussell/bumping-a-php-monorepo-to-85-the-mechanics-551d</link>
      <guid>https://dev.to/jonesrussell/bumping-a-php-monorepo-to-85-the-mechanics-551d</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;This is the first of three posts about taking Waaseyaa to PHP 8.5. This one is about the mechanics: how a coordinated version bump across a 67-package monorepo actually happens. The next two cover the deprecation sweep that came with it and the features we deliberately did not adopt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; Waaseyaa is the open-source PHP framework I have been writing about. Mission: &lt;code&gt;php-8-5-upgrade-01KR8DN2&lt;/code&gt;. Shipped as PR &lt;a href="https://github.com/waaseyaa/waaseyaa/pull/1406" rel="noopener noreferrer"&gt;#1406&lt;/a&gt;, merge commit &lt;a href="https://github.com/waaseyaa/waaseyaa/commit/e0f8cb570" rel="noopener noreferrer"&gt;&lt;code&gt;e0f8cb57&lt;/code&gt;&lt;/a&gt;. Released in alpha.176.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The starting state
&lt;/h2&gt;

&lt;p&gt;Before the bump, Waaseyaa required PHP 8.4. Sixty-six first-party &lt;code&gt;composer.json&lt;/code&gt; files, all aligned on &lt;code&gt;&amp;gt;=8.4&lt;/code&gt;. Plus a skeleton package, which is a template artifact and is kept at the lowest reasonable floor on purpose.&lt;/p&gt;

&lt;p&gt;CI ran a single PHP version. PHPStan was pinned to a matching &lt;code&gt;phpVersion&lt;/code&gt;. The floor was tight and consistent. That alignment is what makes a bump cheap. The expensive version of this story is the one where every package picks its own minimum and you have to negotiate sixty-six exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mission shape
&lt;/h2&gt;

&lt;p&gt;The mission split into five work packages plus a closing one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WP01.&lt;/strong&gt; Constraint bump, CI, Docker, lockfile, PHPStan pin, docs, governance charter touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WP02.&lt;/strong&gt; 8.5 deprecation sweep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WP03.&lt;/strong&gt; Adopt &lt;code&gt;#[\NoDiscard]&lt;/code&gt; on critical surfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WP04.&lt;/strong&gt; Targeted &lt;code&gt;array_find()&lt;/code&gt; adoption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WP05.&lt;/strong&gt; PHP-CS-Fixer migration rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WP06.&lt;/strong&gt; CHANGELOG and verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WP01 is the only one that touches the floor. Everything after is feature work that becomes available because the floor moved. Splitting it this way matters: if WP01 lands clean, the rest can land in any order without coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WP01 actually changed
&lt;/h2&gt;

&lt;p&gt;The mechanical surface of a floor bump is smaller than people expect. From the merge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;66 first-party &lt;code&gt;composer.json&lt;/code&gt; files&lt;/strong&gt; updated from &lt;code&gt;&amp;gt;=8.4&lt;/code&gt; to &lt;code&gt;&amp;gt;=8.5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 GitHub Actions workflows&lt;/strong&gt; repinned to &lt;code&gt;php-version: '8.5'&lt;/code&gt;: &lt;code&gt;ci.yml&lt;/code&gt;, &lt;code&gt;skeleton-smoke.yml&lt;/code&gt;, &lt;code&gt;release-cut.yml&lt;/code&gt;. Ten total occurrences of the string &lt;code&gt;'8.5'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;phpstan.neon&lt;/code&gt;&lt;/strong&gt; updated: &lt;code&gt;phpVersion: 80500&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lockfile&lt;/strong&gt; regenerated against 8.5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the bump. Everything else in the mission is downstream of those four artifacts moving in lockstep.&lt;/p&gt;

&lt;p&gt;The reason the surface is small is that Waaseyaa has hard gates that already enforce alignment. There is a &lt;code&gt;bin/check-composer-policy&lt;/code&gt; script that fails CI if any package drifts from the root constraint. There is a &lt;code&gt;bin/check-package-layers&lt;/code&gt; script that fails if the dependency direction inverts. There is a &lt;code&gt;tools/drift-detector.sh&lt;/code&gt; that fails if docs lag the code. The floor is one number defended in many places.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verification surface
&lt;/h2&gt;

&lt;p&gt;For a bump to be safe, every hard gate has to be green on the new floor. Waaseyaa's full gate list for this mission:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;composer phpstan&lt;/code&gt; (root level + package level)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vendor/bin/phpunit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;composer cs-check&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bin/check-composer-policy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bin/check-package-layers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bin/audit-dead-code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tools/drift-detector.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At merge time the test suite was 7,497 unit tests, 18,118 assertions, 0 deprecations, 2 expected skips. That is the number to trust. Not because tests prove a version is fine, but because the test corpus is dense enough that deprecation warnings would surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why split it into work packages at all
&lt;/h2&gt;

&lt;p&gt;This is the part worth paying attention to if you maintain a PHP monorepo. The actual diff for a version bump is small. You could do it in one PR with one commit. People do.&lt;/p&gt;

&lt;p&gt;The cost of doing it that way is that the diff conflates four different kinds of change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The floor moves (a policy change).&lt;/li&gt;
&lt;li&gt;Deprecations get removed (a behavior change).&lt;/li&gt;
&lt;li&gt;New features get adopted (a style change).&lt;/li&gt;
&lt;li&gt;New tooling gets wired (a configuration change).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When all four land in one squash commit, the next person to touch any of them cannot read the rationale. Six months later, someone reverts a &lt;code&gt;#[\NoDiscard]&lt;/code&gt; attribute thinking it is part of the floor bump, and now the floor bump cannot be reverted cleanly either.&lt;/p&gt;

&lt;p&gt;The work package structure makes each kind of change auditable on its own terms. WP02 is removable without affecting WP01. WP04 can be reverted without touching WP03. The mission directory is the persistent record of why each was done.&lt;/p&gt;

&lt;p&gt;That is the same point I made about the &lt;a href="https://jonesrussell.github.io/blog/spec-kitty-mission-lifecycle/" rel="noopener noreferrer"&gt;Spec Kitty mission lifecycle&lt;/a&gt; post: the output of any mission is replaceable. The trail is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the next posts cover
&lt;/h2&gt;

&lt;p&gt;Post 2 in this series digs into the deprecation sweep: what 8.5 surfaced, where it was hiding in the codebase, and how the sweep got from 34 warnings to 0.&lt;/p&gt;

&lt;p&gt;Post 3 covers the features we deliberately did not adopt. Property hooks. The pipe operator. Broader &lt;code&gt;array_find()&lt;/code&gt;. The argument is that restraint is part of the upgrade, not absent from it.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>php</category>
      <category>waaseyaa</category>
      <category>monorepo</category>
      <category>speckitty</category>
    </item>
    <item>
      <title>PHP 8.5 restraint: features we did not adopt</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 11 May 2026 18:21:56 +0000</pubDate>
      <link>https://dev.to/jonesrussell/php-85-restraint-features-we-did-not-adopt-568g</link>
      <guid>https://dev.to/jonesrussell/php-85-restraint-features-we-did-not-adopt-568g</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;Third in the &lt;a href="https://jonesrussell.github.io/blog/waaseyaa-php-version-bump-monorepo/" rel="noopener noreferrer"&gt;PHP 8.5 upgrade series&lt;/a&gt;. Post one was the floor-bump mechanics. &lt;a href="https://jonesrussell.github.io/blog/php-8-5-deprecation-sweep/" rel="noopener noreferrer"&gt;Post two&lt;/a&gt; was the deprecation sweep. This one is about what we deliberately did not adopt.&lt;/p&gt;

&lt;p&gt;Most upgrade write-ups read like a feature tour. Here is what is new, here is how to use it. They are useful and they are not the whole story. The other half of an upgrade is what you choose not to add. That choice is invisible in the diff and load-bearing in the codebase.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mission:&lt;/strong&gt; &lt;code&gt;php-8-5-upgrade-01KR8DN2&lt;/code&gt;, merge commit &lt;a href="https://github.com/waaseyaa/waaseyaa/commit/e0f8cb570" rel="noopener noreferrer"&gt;&lt;code&gt;e0f8cb57&lt;/code&gt;&lt;/a&gt;. Five work packages shipped. Property hooks were not in any of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Property hooks: not in scope
&lt;/h2&gt;

&lt;p&gt;PHP 8.4 introduced property hooks. Define &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;set&lt;/code&gt; on a property directly, eliminate the boilerplate getter and setter pair. Asymmetric visibility came in the same window. Lots of writeups called this the biggest PHP language change in years.&lt;/p&gt;

&lt;p&gt;Waaseyaa did not adopt either. The mission spec did not mention them. The plan did not list them as a non-goal. They simply were not part of the upgrade.&lt;/p&gt;

&lt;p&gt;If you grep the codebase for the patterns property hooks would replace, you will find traditional methods everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getClientId&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setClientId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$clientId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$clientId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring. Repetitive. Could be a property hook. Was not converted.&lt;/p&gt;

&lt;p&gt;The reason this is intentional rather than accidental: the mission was scoped to "raise the PHP requirement and fix what 8.5 surfaces, plus a focused 8.5 feature-adoption pass." Property hooks are an 8.4 feature, not an 8.5 feature. The line was drawn at the version being adopted.&lt;/p&gt;

&lt;p&gt;That line is the discipline. An upgrade pass is a window where adopting new patterns is cheap because everyone is reading the diff anyway. The temptation is to use the window for everything. The cost of using it for everything is that the diff conflates "we now require 8.5" with "we changed our property style." Two reverts deep, those become impossible to separate.&lt;/p&gt;

&lt;p&gt;Property hooks are not rejected. They are deferred. They get their own mission when the conversion is the work, not a side effect of something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipe operator: not used
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 introduced &lt;code&gt;|&amp;gt;&lt;/code&gt;, a pipe operator that lets you write &lt;code&gt;$x |&amp;gt; $fn1 |&amp;gt; $fn2&lt;/code&gt; instead of nested calls.&lt;/p&gt;

&lt;p&gt;Waaseyaa shipped 8.5 without using &lt;code&gt;|&amp;gt;&lt;/code&gt; anywhere. The plan considered it in WP04 alongside &lt;code&gt;array_first()&lt;/code&gt; and &lt;code&gt;array_find()&lt;/code&gt;. After the survey pass, no use sites were strong enough to take.&lt;/p&gt;

&lt;p&gt;The reason is that pipe shines when you have a multi-step transform that reads naturally as a chain. Waaseyaa's transforms are usually one-step (use a function), two-step (assign an intermediate), or many-step but heterogeneous (a builder pattern with named methods). The middle band where pipe wins is narrow.&lt;/p&gt;

&lt;p&gt;Adopting &lt;code&gt;|&amp;gt;&lt;/code&gt; at every two-step site for style would create a second idiom alongside the existing intermediate-variable style. Mixed idioms have a tax: every reader has to decide which style is in play before reading. That tax is paid every time the file is opened.&lt;/p&gt;

&lt;p&gt;So pipe stays unused until a real call site asks for it. Then it gets adopted in that one place. Not across the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;array_find()&lt;/code&gt;: two adoptions, five rejections
&lt;/h2&gt;

&lt;p&gt;The most interesting case. PHP 8.5 added &lt;code&gt;array_find()&lt;/code&gt; for "first matching element or null." The surface use case is exactly the foreach-and-return-first pattern that shows up in every codebase.&lt;/p&gt;

&lt;p&gt;WP04 surveyed seven candidate sites. Two were adopted. Five were rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two adoptions
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;packages/search/src/SearchResult.php::getFacet()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;facets&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$facet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$facet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$facet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;array_find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;facets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SearchFacet&lt;/span&gt; &lt;span class="nv"&gt;$facet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$facet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;packages/cli/src/Testing/CliTester.php::findOption()&lt;/code&gt; follows the same pattern. Three lines of foreach become one &lt;code&gt;array_find()&lt;/code&gt; with a typed predicate.&lt;/p&gt;

&lt;p&gt;Both sites win because the return type is &lt;code&gt;?SearchFacet&lt;/code&gt; or &lt;code&gt;?OptionDefinition&lt;/code&gt;. The null case is a real outcome the caller handles. &lt;code&gt;array_find&lt;/code&gt; returns null when nothing matches, and that lines up cleanly with the existing contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five rejections
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SqlEntityStorage&lt;/code&gt;, &lt;code&gt;AuthController&lt;/code&gt;, &lt;code&gt;EntityResolver&lt;/code&gt;, &lt;code&gt;JsonApiController&lt;/code&gt;, &lt;code&gt;DbalTransport&lt;/code&gt;. The mission notes give one rationale that covers all five:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;all rejected because the surrounding type contracts (&lt;code&gt;load()&lt;/code&gt; accepts &lt;code&gt;int|string&lt;/code&gt;, not null) require an explicit empty guard either way&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The point is subtle. &lt;code&gt;array_find()&lt;/code&gt; returning null is only a win if the caller wants null. If the caller's contract guarantees non-null (because the input was validated upstream, or because nullness is an error condition), then the foreach version is doing two things: searching and asserting. Replacing it with &lt;code&gt;array_find()&lt;/code&gt; keeps the search but loses the assertion. You end up writing an explicit guard right after the &lt;code&gt;array_find()&lt;/code&gt; call. The line count is the same. The intent is worse.&lt;/p&gt;

&lt;p&gt;The fastest way to spot this in your own codebase: read the immediate caller of the candidate site. If it does &lt;code&gt;throw&lt;/code&gt; or &lt;code&gt;assert&lt;/code&gt; on the result, do not adopt &lt;code&gt;array_find()&lt;/code&gt; there. The foreach is encoding more than iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was adopted, intentionally
&lt;/h2&gt;

&lt;p&gt;To be specific about what restraint does not mean: WP03 added &lt;code&gt;#[\NoDiscard]&lt;/code&gt; to sixteen API surfaces. Four allowed/forbidden/neutral factory methods on &lt;code&gt;AccessResult&lt;/code&gt;. Five repository interface methods that return loaded entities. Ten fluent-builder methods on &lt;code&gt;DBALSelect&lt;/code&gt; that return the modified builder.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#[\NoDiscard]&lt;/code&gt; is a semantic safety net. If a caller ignores a &lt;code&gt;find()&lt;/code&gt; return value they probably have a bug. The attribute makes the compiler say so. Adopting it on sixteen surfaces was a security-shaped decision, not a style one.&lt;/p&gt;

&lt;p&gt;WP05 also wired three mechanical PHP-CS-Fixer rules: &lt;code&gt;octal_notation&lt;/code&gt; (52 sites converted to &lt;code&gt;0o755&lt;/code&gt;), &lt;code&gt;new_expression_parentheses&lt;/code&gt; (58 chained-new conversions), and &lt;code&gt;heredoc_indentation&lt;/code&gt; (8 SQL and HTML heredocs reindented). Mechanical, fixer-driven, no judgement required per site. Easy to adopt at scale because the fixer makes the decision.&lt;/p&gt;

&lt;p&gt;The pattern across both: adoption is at its best when it is either a safety improvement on a critical surface, or a mechanical fixer rule that can be applied uniformly. Adoption is at its worst when it is a style change applied site by site by humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;An upgrade is a decision about what to add and what not to add. Both decisions live in the diff. The "did not adopt" decisions are invisible if you only read the merged code, which is why they are worth writing down somewhere.&lt;/p&gt;

&lt;p&gt;Mission directories are the place we write them down. The five-site rejection rationale for &lt;code&gt;array_find()&lt;/code&gt; is one line in WP04's notes. Six months from now, someone will look at &lt;code&gt;SqlEntityStorage::load()&lt;/code&gt; and think "why isn't this &lt;code&gt;array_find()&lt;/code&gt;?" The mission directory has the answer.&lt;/p&gt;

&lt;p&gt;If your team is doing a PHP 8.5 upgrade, the most useful thing you can write down is not the list of features you adopted. It is the list of features you considered and rejected, with one sentence each. That list is what makes the upgrade a position, not a checklist.&lt;/p&gt;

&lt;p&gt;Baamaapii&lt;/p&gt;

</description>
      <category>php</category>
      <category>waaseyaa</category>
      <category>monorepo</category>
      <category>design</category>
    </item>
  </channel>
</rss>
