<?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>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>
    <item>
      <title>The PHP 8.5 deprecation sweep: from 34 warnings to zero</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 11 May 2026 18:21:52 +0000</pubDate>
      <link>https://dev.to/jonesrussell/the-php-85-deprecation-sweep-from-34-warnings-to-zero-1077</link>
      <guid>https://dev.to/jonesrussell/the-php-85-deprecation-sweep-from-34-warnings-to-zero-1077</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;Second 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;. The first post covered the floor-bump mechanics. This one is about what 8.5 surfaced when the floor moved and how the sweep cleared it.&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;, work package WP02. 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;. The CHANGELOG entry for &lt;a href="https://github.com/waaseyaa/waaseyaa/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;alpha.176&lt;/a&gt; records the verification: 34 PHPUnit deprecations to 0, across 7,497 tests.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Before WP02 ran, the test suite was emitting 34 deprecation warnings against 8.5. That number does not measure how broken the codebase was. It measures how dense the test corpus is. A weaker test suite would have surfaced fewer warnings because fewer code paths run during CI.&lt;/p&gt;

&lt;p&gt;PHPUnit's deprecation handling matters here. Waaseyaa runs PHPUnit in strict mode where deprecation messages are captured and counted per test, not just printed. The mission's exit criterion was zero deprecations on the matrix that already ran 18,118 assertions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three categories, twenty-nine sites
&lt;/h2&gt;

&lt;p&gt;The 34 warnings collapsed into three deprecation patterns once you grouped them. Twenty-nine distinct call sites across the monorepo.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;Reflection*::setAccessible()&lt;/code&gt; — 22 sites
&lt;/h3&gt;

&lt;p&gt;The biggest category. &lt;code&gt;ReflectionMethod::setAccessible()&lt;/code&gt; and &lt;code&gt;ReflectionProperty::setAccessible()&lt;/code&gt; were marked as no-ops in PHP 8.1 (private members became reflectively accessible by default) and deprecated outright in 8.5.&lt;/p&gt;

&lt;p&gt;Twenty-two call sites across seven test files. Packages: &lt;code&gt;entity&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;ssr&lt;/code&gt;, &lt;code&gt;foundation&lt;/code&gt;, &lt;code&gt;entity-storage&lt;/code&gt;, and one integration test under &lt;code&gt;tests/Integration/Phase13/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every site looked roughly like this:&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;$reflection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReflectionMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'privateMethod'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$reflection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAccessible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$reflection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$arg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was deletion. The line that called &lt;code&gt;setAccessible(true)&lt;/code&gt; was removed. The line that called &lt;code&gt;invoke()&lt;/code&gt; worked unchanged. No behavior change at runtime. The reflection access was already implicit.&lt;/p&gt;

&lt;p&gt;This is the cleanest kind of deprecation removal you can do: the warning was telling you the line was already useless.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;$http_response_header&lt;/code&gt; — 1 site
&lt;/h3&gt;

&lt;p&gt;A single site in &lt;code&gt;packages/http-client/src/StreamHttpClient.php&lt;/code&gt;. The magic global variable &lt;code&gt;$http_response_header&lt;/code&gt; was deprecated in favor of the explicit function &lt;code&gt;http_get_last_response_headers()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before:&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;$headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$http_response_header&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&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;$headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;http_get_last_response_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same null-coalesce default. Explicit function call instead of a side-effect global. Easier to read, easier to mock, easier to grep for.&lt;/p&gt;

&lt;p&gt;This is the kind of language cleanup that arrives one site at a time when the language standardizes a long-running pattern. PHP's magic globals are slowly being retired across the major versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;curl_close()&lt;/code&gt; — 6 sites
&lt;/h3&gt;

&lt;p&gt;Six call sites of &lt;code&gt;curl_close()&lt;/code&gt;, deprecated in 8.5 because libcurl has treated it as a no-op since version 7.20.0. PHP held onto the function for years for compatibility. 8.5 finally removes it.&lt;/p&gt;

&lt;p&gt;Files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;packages/ai-agent/src/Provider/AnthropicProvider.php&lt;/code&gt; — 3 sites&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/ai-agent/src/Provider/OpenAiCompatibleProvider.php&lt;/code&gt; — 2 sites&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/mercure/src/MercurePublisher.php&lt;/code&gt; — 1 site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All six were paired with &lt;code&gt;curl_exec()&lt;/code&gt; calls in HTTP request flows. Removed without replacement. The cURL handle is collected by GC when it goes out of scope.&lt;/p&gt;

&lt;p&gt;A second tautological PHPStan warning was cleaned up in the same area while we were there. Worth noting because deprecation sweeps are a good moment to fix the things you keep walking past.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not get removed
&lt;/h2&gt;

&lt;p&gt;WP02 was a deprecation sweep, not a code cleanup. The bar for removal was "PHP 8.5 marks it deprecated" or "PHPStan reports it tautological in the file we are already editing." Nothing else.&lt;/p&gt;

&lt;p&gt;That bar matters. It is tempting during an upgrade to fold in unrelated cleanup. The reason not to is that the diff stops being readable. A reviewer looking at WP02 should be able to read it as "deprecation removals" with no surprises. Adding "and we also renamed this variable while we were there" makes every line of the diff a question.&lt;/p&gt;

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

&lt;p&gt;The exit criterion was numeric and binary. Run the full suite. Count deprecations. The number must be zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Locked by full PHPUnit (7497 tests / 18118 assertions / 0 deprecations / 2 expected skips)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line in the CHANGELOG is the work package's signature. Two expected skips are environment-dependent tests that always skip in CI (Redis, Mercure broker variants). Zero deprecations across 7,497 tests is dense enough coverage that a future deprecation drift would surface immediately.&lt;/p&gt;

&lt;p&gt;If you are running a similar sweep on your own codebase, the methodology is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move the PHP floor.&lt;/li&gt;
&lt;li&gt;Run the full test suite. Capture deprecation output.&lt;/li&gt;
&lt;li&gt;Group warnings by the deprecation key (&lt;code&gt;E_USER_DEPRECATED&lt;/code&gt; message, function name, or class).&lt;/li&gt;
&lt;li&gt;For each group, write a one-line removal pattern and apply it across all sites in one commit per group.&lt;/li&gt;
&lt;li&gt;Rerun. The number is zero or it is not done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason to group by deprecation pattern (not by file) is that each group has the same fix. Mixing them in one commit makes the diff hard to read and impossible to revert selectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;Post 3 in the series covers the features 8.5 introduced that we deliberately did not adopt. Property hooks. The pipe operator. Broader &lt;code&gt;array_find()&lt;/code&gt; adoption beyond the two confirmed sites. The reasoning is that restraint is part of the upgrade, not the absence of one.&lt;/p&gt;

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

</description>
      <category>php</category>
      <category>waaseyaa</category>
      <category>monorepo</category>
      <category>testing</category>
    </item>
    <item>
      <title>Spec Kitty mission lifecycle: a domain modeling pass through Giiken</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 11 May 2026 16:24:24 +0000</pubDate>
      <link>https://dev.to/jonesrussell/spec-kitty-mission-lifecycle-a-domain-modeling-pass-through-giiken-3j4k</link>
      <guid>https://dev.to/jonesrussell/spec-kitty-mission-lifecycle-a-domain-modeling-pass-through-giiken-3j4k</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;A lot of agent frameworks promise "end to end" workflows. Most of them stop at "generate a plan and hope." Spec Kitty is different. It runs a real mission through a state machine, with artifacts on disk and gates between phases. This post walks one of those missions, &lt;code&gt;giiken-domain-modeling-01KR2HKT&lt;/code&gt;, from spec to merge.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; Giiken is the community knowledge service built on Waaseyaa. The mission did discovery and docs for its domain model. Real commit: &lt;a href="https://github.com/waaseyaa/giiken/commit/5b2328bf330b73bc1d999999bcc7cae02e2b1b6f" rel="noopener noreferrer"&gt;&lt;code&gt;waaseyaa/giiken@5b2328b&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What "a mission" actually is
&lt;/h2&gt;

&lt;p&gt;A Spec Kitty mission is a directory under &lt;code&gt;kitty-specs/&lt;/code&gt;, named with a slug and an ULID. After this mission landed, that directory looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kitty-specs/giiken-domain-modeling-01KR2HKT/
  spec.md
  plan.md
  research.md
  data-model.md
  meta.json
  status.json
  status.events.jsonl
  mission-events.jsonl
  checklists/
    requirements.md
  research/
    evidence-log.csv
    source-register.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a generated artifact dump. Each file has a role in the state machine. &lt;code&gt;spec.md&lt;/code&gt; is the contract. &lt;code&gt;plan.md&lt;/code&gt; is the chosen approach. &lt;code&gt;research.md&lt;/code&gt; plus the CSVs are the evidence trail. &lt;code&gt;status.json&lt;/code&gt; and the two &lt;code&gt;.jsonl&lt;/code&gt; files are the lane state and the audit log. The checklist is a hard gate, not a suggestion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phases
&lt;/h2&gt;

&lt;p&gt;The mission moved through these phases. Each one writes an artifact and emits a status event.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Specify.&lt;/strong&gt; Compile a &lt;code&gt;spec.md&lt;/code&gt; from the mission brief. Requirements get checklisted. Ambiguity gets surfaced before code touches the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; Choose an approach in &lt;code&gt;plan.md&lt;/code&gt;. Inputs from spec. Output is the shape of the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks.&lt;/strong&gt; Break the plan into work packages (WPs). Each WP is independent enough to assign and review on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement.&lt;/strong&gt; Each WP runs through implement and review until approved. State transitions go through the orchestrator, not by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; Per WP, against the spec. Reviewers can reject with structured feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge.&lt;/strong&gt; Once every WP is approved, the mission squash-merges and the events log records the terminal state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing that makes this different from a long prompt is that every transition is gated. You can't move a WP to &lt;code&gt;approved&lt;/code&gt; without a passing review. You can't merge with WPs still in flight. The agent is constrained to the shape of the state machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this mission actually produced
&lt;/h2&gt;

&lt;p&gt;Beyond the kitty-specs directory, the merge commit added two architecture documents to the Giiken repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docs/architecture/domain-model.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs/architecture/lifecycle.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the implementation work in WP01 and WP02 left the data model migration-aligned, with PHPUnit and Vitest both green at merge time. Thirteen files in one squash commit, all traceable back to the spec.&lt;/p&gt;

&lt;p&gt;The point is the trail. A reader six months from now can open &lt;code&gt;kitty-specs/giiken-domain-modeling-01KR2HKT/&lt;/code&gt; and see: what was asked for, what was chosen, what evidence informed it, what got built, and which checks passed. That is a working memory you can hand to the next agent or the next human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than the output
&lt;/h2&gt;

&lt;p&gt;The output of this mission is fine. Useful, even. But the output is replaceable. The trail is not.&lt;/p&gt;

&lt;p&gt;If you have been around agent workflows for any length of time, you know the failure mode: an AI session ends, the context evaporates, and the next session has to reconstruct everything from the code. Spec Kitty inverts that. The mission directory &lt;strong&gt;is&lt;/strong&gt; the persistent context. The next agent picks up the spec and the checklist, not a chat log.&lt;/p&gt;

&lt;p&gt;That is the lifecycle proof: not "an agent shipped code," but "an agent moved through a structured workflow that another agent or human can audit, resume, or extend."&lt;/p&gt;

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

&lt;p&gt;If you want to see one of these missions in your own repo, the easiest path is to install Spec Kitty and run &lt;code&gt;spec-kitty next --agent &amp;lt;name&amp;gt;&lt;/code&gt; on a small scope. Pick something with a clear question, not a vague refactor. Discovery missions like this one are a good first try.&lt;/p&gt;

&lt;p&gt;The commit for the mission described here is &lt;a href="https://github.com/waaseyaa/giiken/commit/5b2328b" rel="noopener noreferrer"&gt;&lt;code&gt;waaseyaa/giiken@5b2328b&lt;/code&gt;&lt;/a&gt;. The full mission directory is in that repo at &lt;code&gt;kitty-specs/giiken-domain-modeling-01KR2HKT/&lt;/code&gt;.&lt;/p&gt;

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

</description>
      <category>speckitty</category>
      <category>giiken</category>
      <category>waaseyaa</category>
      <category>ai</category>
    </item>
    <item>
      <title>Hugo blog shortcodes: adding a visual component system to PaperMod</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Wed, 08 Apr 2026 20:41:09 +0000</pubDate>
      <link>https://dev.to/jonesrussell/hugo-blog-shortcodes-adding-a-visual-component-system-to-papermod-3i3l</link>
      <guid>https://dev.to/jonesrussell/hugo-blog-shortcodes-adding-a-visual-component-system-to-papermod-3i3l</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/adityatelange/hugo-PaperMod" rel="noopener noreferrer"&gt;PaperMod&lt;/a&gt; is a clean, fast &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; theme. What it doesn't give you out of the box is a component library: no callouts, no numbered steps, no before/after comparisons. If you write tutorials or technical posts, you end up compensating with blockquotes and bold text where purpose-built components would serve the reader better.&lt;/p&gt;

&lt;p&gt;This post covers all six shortcodes, the CSS behind them, and how to add the same components to your own PaperMod blog. All of it came together in a single &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;Six shortcodes, one CSS file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;callout&lt;/strong&gt;: highlighted aside with five severity types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;steps / step&lt;/strong&gt;: auto-numbered procedure blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pullquote&lt;/strong&gt;: large-format quote for emphasis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stats / stat&lt;/strong&gt;: side-by-side metric tiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compare / before / after&lt;/strong&gt;: side-by-side comparison panels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cta&lt;/strong&gt;: call-to-action box with a button&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All styles hook into PaperMod's CSS variables (&lt;code&gt;--primary&lt;/code&gt;, &lt;code&gt;--entry&lt;/code&gt;, &lt;code&gt;--border&lt;/code&gt;, etc.), so they adapt to dark and light mode automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  File locations
&lt;/h2&gt;

&lt;p&gt;Hugo resolves shortcodes from &lt;code&gt;layouts/shortcodes/&lt;/code&gt;. Create one &lt;code&gt;.html&lt;/code&gt; file per shortcode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;layouts/shortcodes/
  callout.html
  steps.html
  step.html
  pullquote.html
  stats.html
  stat.html
  compare.html
  before.html
  after.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSS goes in &lt;code&gt;assets/css/extended/&lt;/code&gt;. PaperMod loads everything in that directory automatically; no import statements needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortcodes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Callout
&lt;/h3&gt;

&lt;p&gt;A callout is a highlighted aside that draws the reader's attention. It accepts a &lt;code&gt;type&lt;/code&gt; parameter: &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;tip&lt;/code&gt;, &lt;code&gt;note&lt;/code&gt;, or &lt;code&gt;success&lt;/code&gt;. Defaults to &lt;code&gt;note&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template&lt;/strong&gt; (&lt;code&gt;layouts/shortcodes/callout.html&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{- $type := .Get "type" | default "note" -}}
{{- $emoji := dict "info" "💡" "warning" "⚠️" "tip" "✨" "note" "📝" "success" "✅" -}}
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"callout callout-{{ $type }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"callout-marker"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ index $emoji $type }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"callout-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner | markdownify }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{&amp;lt;/* callout type="warning" */&amp;gt;}}
Run `git stash` before switching branches or you will lose your changes.
{{&amp;lt;/* /callout */&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rendered:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;git stash&lt;/code&gt; before switching branches or you will lose your changes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;markdownify&lt;/code&gt; call means you can use inline markdown inside the body: backtick code, bold, links. All render correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps and step
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;steps&lt;/code&gt; shortcode wraps a sequence of &lt;code&gt;step&lt;/code&gt; shortcodes. Each &lt;code&gt;step&lt;/code&gt; takes a title as its first positional argument and auto-numbers itself via CSS counters. No JavaScript, no manual numbering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Templates&lt;/strong&gt; (&lt;code&gt;layouts/shortcodes/steps.html&lt;/code&gt; and &lt;code&gt;step.html&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- steps.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"steps"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- step.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step-badge"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Get 0 }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"step-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner | markdownify }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{&amp;lt;/* steps */&amp;gt;}}
{{&amp;lt;/* step "Create the shortcode file" */&amp;gt;}}
Add `layouts/shortcodes/callout.html` to your project.
{{&amp;lt;/* /step */&amp;gt;}}
{{&amp;lt;/* step "Add the CSS" */&amp;gt;}}
Create `assets/css/extended/components.css` with the component styles.
{{&amp;lt;/* /step */&amp;gt;}}
{{&amp;lt;/* /steps */&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rendered:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;layouts/shortcodes/callout.html&lt;/code&gt; to your project.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;assets/css/extended/components.css&lt;/code&gt; with the component styles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stats and stat
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;stats&lt;/code&gt; shortcode is a flex container for &lt;code&gt;stat&lt;/code&gt; tiles. Each &lt;code&gt;stat&lt;/code&gt; takes two positional arguments: the value and the label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Templates&lt;/strong&gt; (&lt;code&gt;layouts/shortcodes/stats.html&lt;/code&gt; and &lt;code&gt;stat.html&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- stats.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stats"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- stat.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stat-number"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Get 0 }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stat-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Get 1 }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{&amp;lt;/* stats */&amp;gt;}}
{{&amp;lt;/* stat "6" "shortcodes" */&amp;gt;}}
{{&amp;lt;/* stat "1" "CSS file" */&amp;gt;}}
{{&amp;lt;/* stat "0" "JS required" */&amp;gt;}}
{{&amp;lt;/* /stats */&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rendered:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tiles flex-wrap on small screens, so they stack gracefully on mobile without extra media query work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare, before, and after
&lt;/h3&gt;

&lt;p&gt;Three files work together: &lt;code&gt;compare.html&lt;/code&gt; wraps the pair, &lt;code&gt;before.html&lt;/code&gt; and &lt;code&gt;after.html&lt;/code&gt; render each panel. The before panel uses PaperMod's warning colour; after uses the success colour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Templates&lt;/strong&gt; (&lt;code&gt;compare.html&lt;/code&gt;, &lt;code&gt;before.html&lt;/code&gt;, &lt;code&gt;after.html&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- compare.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- before.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare-before"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare-marker"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;✕&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner | markdownify }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- after.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare-after"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare-marker"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;✓&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"compare-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner | markdownify }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{&amp;lt;/* compare */&amp;gt;}}
{{&amp;lt;/* before */&amp;gt;}}
Blockquote hacks repurposed as callouts.
{{&amp;lt;/* /before */&amp;gt;}}
{{&amp;lt;/* after */&amp;gt;}}
Purpose-built `callout` shortcode with five types.
{{&amp;lt;/* /after */&amp;gt;}}
{{&amp;lt;/* /compare */&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rendered:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Blockquote hacks repurposed as callouts.&lt;/p&gt;

&lt;p&gt;Purpose-built &lt;code&gt;callout&lt;/code&gt; shortcode with five types.&lt;/p&gt;

&lt;p&gt;On screens narrower than 600px the panels stack vertically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pullquote
&lt;/h3&gt;

&lt;p&gt;A pullquote is a styled blockquote for emphasis. Use it to surface a key insight or memorable line from the surrounding text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template&lt;/strong&gt; (&lt;code&gt;layouts/shortcodes/pullquote.html&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;blockquote&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pullquote"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ .Inner | markdownify }}
&lt;span class="nt"&gt;&amp;lt;/blockquote&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{&amp;lt;/* pullquote */&amp;gt;}}
Good writing tools get out of the way. Good components make the writing better.
{{&amp;lt;/* /pullquote */&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rendered:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Good writing tools get out of the way. Good components make the writing better.&lt;/p&gt;

&lt;h3&gt;
  
  
  CTA
&lt;/h3&gt;

&lt;p&gt;A call-to-action box with a centred button. Takes three named parameters: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;button&lt;/code&gt;, and &lt;code&gt;href&lt;/code&gt;. The inner body is optional copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template&lt;/strong&gt; (&lt;code&gt;layouts/shortcodes/cta.html&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{- $title := .Get "title" -}}
{{- $button := .Get "button" -}}
{{- $href := .Get "href" -}}
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ $title }}&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ .Inner | markdownify }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta-button"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ $href }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ $button }}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{&amp;lt;/* cta title="Try it yourself" button="View the source" href="https://github.com/jonesrussell/blog" */&amp;gt;}}
All six shortcodes and the CSS are in the repo.
{{&amp;lt;/* /cta */&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rendered:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All six shortcodes and the CSS are in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proving ground
&lt;/h2&gt;

&lt;p&gt;Before calling the system done, retrofit an existing post. I used Minoo Elders, replacing a flat numbered list with a &lt;code&gt;steps&lt;/code&gt; block and a closing paragraph with a &lt;code&gt;cta&lt;/code&gt;. If the shortcodes work in a real post with real content, they are ready.&lt;/p&gt;

&lt;p&gt;The retrofit caught a line-height edge case in the step badge CSS and confirmed the dark mode colours held in both themes. Worth the ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe coding the component system
&lt;/h2&gt;

&lt;p&gt;This system was built with &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; in one session. Describe the component you want, review the draft, push back on anything over-engineered. Nine files and the CSS came together without a lot of manual effort.&lt;/p&gt;

&lt;p&gt;The real gain is in the iteration loop: see a render, request a tweak, get updated CSS in thirty seconds. That speed is the whole point.&lt;/p&gt;

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

</description>
      <category>hugo</category>
      <category>claudecode</category>
      <category>papermod</category>
      <category>shortcodes</category>
    </item>
    <item>
      <title>Day One of the Content Pipeline: What Broke and What I Fixed</title>
      <dc:creator>Russell Jones</dc:creator>
      <pubDate>Mon, 06 Apr 2026 04:01:00 +0000</pubDate>
      <link>https://dev.to/jonesrussell/day-one-of-the-content-pipeline-what-broke-and-what-i-fixed-3nde</link>
      <guid>https://dev.to/jonesrussell/day-one-of-the-content-pipeline-what-broke-and-what-i-fixed-3nde</guid>
      <description>&lt;p&gt;Ahnii!&lt;/p&gt;

&lt;p&gt;Yesterday's post walked through &lt;a href="https://jonesrussell.github.io/blog/automated-content-pipeline-github-actions/" rel="noopener noreferrer"&gt;automating a content pipeline with GitHub Actions and Issues&lt;/a&gt;. The idea: a daily scheduled job scans recent commits and closed issues across several repos, filters out the noise, and opens what's left as GitHub issues labeled &lt;code&gt;stage:mined&lt;/code&gt;. One of those issues looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title: [content] feat: add SovereigntyProfile to Layer 0
Body:
  ## Source
  Commit `abc1234` in `waaseyaa/framework`
  ## Content Seed
  feat: add SovereigntyProfile to Layer 0
  ## Suggested Type
  text-post
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those issues are raw material. You curate them into drafts, produce the copy, and publish. That surfacing step is what the rest of this post calls &lt;em&gt;mining&lt;/em&gt;. This post is about what happened the first time I actually ran that pipeline. The short version: it works, but the first real run turned up three problems no amount of planning could have caught. Here are the three fixes and the meta-lesson underneath them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day One Output: 20 Issues, Too Much Noise
&lt;/h2&gt;

&lt;p&gt;The mining workflow fired on schedule and opened 20 &lt;code&gt;stage:mined&lt;/code&gt; issues overnight, pulled from three repos. Good news: the pipeline saw everything it was supposed to see. Bad news: "everything" is not the same as "a usable drafting queue." The first run had more noise than I expected, and it had noise the filter couldn't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Tighten the Mining Filter
&lt;/h2&gt;

&lt;p&gt;Even with the v1 noise filter, too many low-signal commits made it through. Things like &lt;code&gt;fix: align FileRepositoryInterface usage with Waaseyaa\Media\File contract&lt;/code&gt; matter for the codebase and are boring as standalone posts. The first fix was to extend the exclude regex in &lt;code&gt;content-mine.yml&lt;/code&gt;:&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="nv"&gt;COMMITS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;/commits?since=&lt;/span&gt;&lt;span class="nv"&gt;$SINCE&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;per_page=50"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.commit.message | test("^(Merge |chore|docs|fix typo|bump|update dep|Bump |fix:.*([Pp]hp[Ss]tan|namespace|alignment|placeholder|phpunit|mock|ignore|typo))"; "i") | not) | {sha: .sha[0:7], message: (.commit.message | split("\n") | .[0]), date: .commit.author.date}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new patterns (&lt;code&gt;phpstan&lt;/code&gt;, &lt;code&gt;namespace&lt;/code&gt;, &lt;code&gt;alignment&lt;/code&gt;, &lt;code&gt;placeholder&lt;/code&gt;, &lt;code&gt;phpunit&lt;/code&gt;, &lt;code&gt;mock&lt;/code&gt;, &lt;code&gt;ignore&lt;/code&gt;, &lt;code&gt;typo&lt;/code&gt;) catch categories of real work nobody wants to read about. A minimum message length of 25 characters cuts drive-by fixes. Fewer mined issues per run, and the ones that survive sit closer to "actually postable." That handled the mechanical noise. The next problem was harder because no regex could see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Merge-in-Curation
&lt;/h2&gt;

&lt;p&gt;Filters are a blunt instrument. They cannot tell that eight separate commits all belong to the same post. On day one, the &lt;a href="https://github.com/waaseyaa/giiken" rel="noopener noreferrer"&gt;Giiken&lt;/a&gt; project alone produced eight mined issues: scaffold, entity types, RBAC, ingestion, wiki schema, query layer, plus two support commits. Every one of them was a valid feature commit. Together they were one post. No filter was going to catch that. Only a human reading them side by side could say "these are a story."&lt;/p&gt;

&lt;p&gt;So curation got a new action: &lt;strong&gt;merge into target&lt;/strong&gt;. Instead of picking one winner and closing the rest, you pick a canonical issue, roll the seeds from the others into its body, and close the sources. The target ends up carrying a combined seed (the whole story), and the sub-issues get a &lt;code&gt;skipped&lt;/code&gt; label and a closed state.&lt;/p&gt;

&lt;p&gt;The curation skill now runs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;→ Approve (move to stage:curated)
→ Skip   (close with skipped label)
→ Merge  (pick target, combine seeds, close sources)
→ Edit   (adjust seed, type, or channels before approving)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running that over the 20 mined issues collapsed them to 4 curated posts: one about the pipeline itself, one about the Giiken project, one about a governance protocol suite in the framework, and one about a specific Symfony refactor. Signal up, count down. Two fixes done. The third was the embarrassing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: Put the Blog First
&lt;/h2&gt;

&lt;p&gt;The v1 production step went straight from a curated issue to Facebook, X, and LinkedIn copy. That read fine in the design doc. It fell apart the first time I tried to run it, because every one of those social posts had a placeholder where the URL should go. The URL had to point at a blog post. The blog post did not exist yet.&lt;/p&gt;

&lt;p&gt;So I rewrote the &lt;code&gt;/content-produce&lt;/code&gt; skill — a &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; workflow that turns queue issues into drafts. The new flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[stage:curated issue] --&amp;gt; B[Draft Hugo post&amp;lt;br/&amp;gt;draft: true]
    B --&amp;gt; C[Draft social copy&amp;lt;br/&amp;gt;docs/social/slug.md]
    C --&amp;gt; D[Commit both to blog repo]
    D --&amp;gt; E{Human review}
    E --&amp;gt;|Flip draft: false| F[GitHub Actions deploys]
    F --&amp;gt; G[/content-pipeline/]
    G --&amp;gt; H[Buffer API → X, LinkedIn, Facebook]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human controls publication. The skill commits drafts only and never flips &lt;code&gt;draft: false&lt;/code&gt;. Once I flip the flag and push, &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; deploys the post, and a separate &lt;code&gt;/content-pipeline&lt;/code&gt; skill handles the Buffer API for social distribution. Each step has one job. This post you're reading is the first one produced by the new flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Content Pipelines Need Continuous Refinement
&lt;/h2&gt;

&lt;p&gt;You cannot design a content pipeline in the abstract. You ship v1, run it against one day of real input, and watch it lie to you. Then you fix the specific lies. That loop is the work.&lt;/p&gt;

&lt;p&gt;Three days ago this pipeline did not exist. Two days ago it was a spec. Yesterday it shipped. Today it is already different. None of the three fixes in this post were things I could have known up front. They came from running the thing, staring at the output, and asking "what is this queue actually trying to tell me?"&lt;/p&gt;

&lt;p&gt;If you are building your own version of this, expect the same arc. Your v1 will have noise you cannot see yet. Your first curation session will reveal merges a filter could not find. And your production step will probably be backwards, because writing the fun part first (the tweets) is more tempting than writing the part that does the work (the blog post). The refinement is not a sign something went wrong. It is the point.&lt;/p&gt;

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

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>content</category>
      <category>claudecode</category>
    </item>
  </channel>
</rss>
