From 6b5329bd7502831d0b7d8339f7a5408abaf3d4ea Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Tue, 7 Jul 2026 12:34:48 +0100 Subject: [PATCH 1/8] moralistic programming --- .../2026/07-07-moralistic-programming.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/priv/blog/engineering/2026/07-07-moralistic-programming.md diff --git a/app/priv/blog/engineering/2026/07-07-moralistic-programming.md b/app/priv/blog/engineering/2026/07-07-moralistic-programming.md new file mode 100644 index 0000000..3a5161b --- /dev/null +++ b/app/priv/blog/engineering/2026/07-07-moralistic-programming.md @@ -0,0 +1,51 @@ +%{ + title: "Replace Moralistic Programming with Situational Awareness", + author: "Willem van den Ende", + tags: ~w(craft, situational-awareness), + description: "", + published: true +} +--- + +I prefer to start where I am, or where we are, and figure things out from there. That requires Situational Awareness, being able to make sense, together, of what we are facing, where we want to go and what we have at our disposal. + +This is contextual. Which is annoying, because it makes communicating online about our lived experience difficult when there are new-ish tools (like coding agents with LLMs) are available. What works in one context for one team, may not work for another, for reasons that may become clear in hindsight, or not. + +One of the things I didn't like around the early Software Craft movement (especially the american instatiation of it) was what I now call "moralistic programming". "You are not a good developer if ". + +I'm seeing some of the same tendencies that were around in the early software craft movement repeated in agentic engineering. +Instead of "you're not a professional software developer if you don't write tests" we have "you're not a professional software developer if you write code by hand". + +I enjoy both writing tests and wrangling (and developing) coding agents, and am always looking for better ways. When I briefly had a job in 1999, in the job interview Danny Greefhorst, and the late Gert Florijn, who would become my mentor, asked what I thought about Object Orientation. I remember saying something like "I like it, and use it, until something better comes along". They hired me anyway. + +I like to put "knobs to 11" and figure out the extremes. So that I know where the limits of my tools and my thinking are. Preferably together with other people. + +This brief post is the adaptation of a comment on [a LinkedIn post by Machiel Keizer-Groeneveld](https://www.linkedin.com/posts/machielkeizergroeneveld_llms-are-confident-and-that-trips-me-up-share-7478794175946309632-HIOY/) + +Machiel: + +>Because nothing is categorically solved, we don't need new words for software development, AI does not 'change everything' even though we are discovering where and how LLMs can assist our software dev practice. The downsides are also showing themselves clearly: LLMs have the wrong built-in intent, they either lack domain knowledge or context rot ignores it, causality is an obvious weakness, they do not evolve their understanding, ask too few questions, they are too confident and apt design requires real intelligence, not plausibility at scale. + +>LLM or not, we need software that is reliable, transparent, extensible and capturing domain intent + +As ever, we are finding better ways of delivering value (often, but not necessarily, with software) by doing it, and helping others do it. + +Looking for something in my notes, I found John Cutler quoting Cat Hicks: + +>What would it look like if we could ask, "how do I make this the absolute best environment for complex problem-solving" instead of "how do I take away all these annoying hard problems." The stuff we want to accomplish in the world is hard. But can be joyful to accomplish. + +Further reading +--- + +Gojko Adzic recent post on how to use [Commanders' Intent](https://www.votito.com/methods/commanders-intent-statement/?tag=li2608) for feature development. + +I like to combine Commanders Intent with Backbriefing, which is well described in the late Stephen Bungay's [The Art of Action](https://www.hachette.co.uk/titles/stephen-bungay/the-art-of-action/9781529376968/) + +Afterword +---- + +I haven't written about this, but I have once shipped a system with very few tests (and no, this was not written in an "if it compiles it works" language, but PHP). It wasn't the optimal way to do it, but we managed to deliver value faster in other ways that were visible and achievable for the team. Focus and zero-defects worked without tests, but slower. Automating other manual work shrunk the release cycle more than tests would have - they would have added value after that, but then the project was over. Did I miss tests? Yes. Did I tell anyone about it? Well, maybe Rob and Marc at [QWAN](https://www.qwan.eu). And I told you now. + +Everyone I worked with wanted to achieve a good outcome, and that is what counts. We can always find better ways. + + From 716bfb4b12c3d1fdbd54f2c37e639328efbbea56 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Tue, 7 Jul 2026 13:18:04 +0100 Subject: [PATCH 2/8] Run tests when resetting pi, so we do not forget to run tests --- .pi/extensions/run_make_test.ts | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .pi/extensions/run_make_test.ts diff --git a/.pi/extensions/run_make_test.ts b/.pi/extensions/run_make_test.ts new file mode 100644 index 0000000..7983ba6 --- /dev/null +++ b/.pi/extensions/run_make_test.ts @@ -0,0 +1,37 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +export default function (pi: ExtensionAPI) { + pi.on("session_start", async (_event, ctx) => { + try { + const { stdout, stderr, code } = await pi.exec("make", ["test"]); + const output = stdout + stderr; + + if (code === 0) { + ctx.ui.notify("make test passed", "info"); + } else { + ctx.ui.notify(`make test failed (exit ${code})`, "error"); + } + + // Also send the output as a message so it's visible in the session + pi.sendMessage({ + customType: "run-make-test", + content: [ + { + type: "text", + text: `Here's the result of running \`make test\`:\n\n\`\`\`\n${output}\`\`\``, + }, + ], + display: true, + }, { deliverAs: "followUp" }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + pi.sendMessage({ + customType: "run-make-test", + content: [ + { type: "text", text: `Error running make test: ${message}` }, + ], + display: true, + }, { deliverAs: "followUp" }); + } + }); +} From c8864c5b3b0b4a7e64321ba131649bc6fd4b5eb4 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Tue, 7 Jul 2026 13:33:50 +0100 Subject: [PATCH 3/8] fixed test - clicking on line instead of parts in svg --- app/test/firehose_web/live/microprints_live_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/test/firehose_web/live/microprints_live_test.exs b/app/test/firehose_web/live/microprints_live_test.exs index d42ec78..6c9723b 100644 --- a/app/test/firehose_web/live/microprints_live_test.exs +++ b/app/test/firehose_web/live/microprints_live_test.exs @@ -90,9 +90,9 @@ defmodule FirehoseWeb.MicroprintsLiveTest do assert html =~ ~s(phx-value-path="#{file_a}") assert html =~ "Collapse" - # Highlight a line in file B via direct event (avoids brittle DOM selector) - view - |> render_click("highlight_line", %{"line" => "1", "path" => file_b}) + # Highlight a line in file B by dispatching the highlight_line event + # (microprint renders multiple rect segments per line, element() can't pick one) + render_click(view, "highlight_line", %{"line" => "1", "path" => file_b}) # file A should be collapsed when highlighting a different file html = render(view) From 10c02f5c6a8beb06fa03116e090963e186efb3dd Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Tue, 7 Jul 2026 13:47:57 +0100 Subject: [PATCH 4/8] add description --- app/priv/blog/engineering/2026/07-07-moralistic-programming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/priv/blog/engineering/2026/07-07-moralistic-programming.md b/app/priv/blog/engineering/2026/07-07-moralistic-programming.md index 3a5161b..597769d 100644 --- a/app/priv/blog/engineering/2026/07-07-moralistic-programming.md +++ b/app/priv/blog/engineering/2026/07-07-moralistic-programming.md @@ -2,7 +2,7 @@ title: "Replace Moralistic Programming with Situational Awareness", author: "Willem van den Ende", tags: ~w(craft, situational-awareness), - description: "", + description: "I don't like what I now call Moralistic Programming. 'You are not a good developer if '. I see some of that resurfacing in discussions around coding agents. I value Situational Awareness over best practices and fingerpointing..", published: true } --- From 9238ecb7f315b569c2da78a83e12a12d87337e75 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 8 Jul 2026 14:22:09 +0100 Subject: [PATCH 5/8] add serve target to Makefile --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bc1f486..6888d38 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,10 @@ test: format: @make -C app format +# Serve the app +serve: + @cd app && mix phx.server + # Run Playwright browser-based tests (requires running server) # Set PLAYWRIGHT_BASE_URL to point at the running Phoenix app playwright: @@ -49,4 +53,4 @@ playwright-docker-start: # Stop Docker Chrome container playwright-docker-stop: - @make -C app playwright-docker-stop \ No newline at end of file + @make -C app playwright-docker-stop From 855b6cb5901ecd338b0119af04d50ff6b27261ee Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 8 Jul 2026 16:44:41 +0100 Subject: [PATCH 6/8] Add RSS subscribe links with icon across blog pages, footer, and head - Add <.rss_icon> inline SVG component in core_components.ex - Inject RSS tags into on blog pages for auto-discovery - Show RSS icon next to blog title on index and tag pages - Show RSS icon on post pages next to back-link - Add Subscribe section in footer with links to both feeds - Wire feed URL through blog controller (index, show, tag actions) 174 tests pass, credo clean. --- .../components/core_components.ex | 25 ++++++++++++++++++ .../components/layouts/app.html.heex | 26 +++++++++++++++++++ .../components/layouts/root.html.heex | 3 +++ .../controllers/blog_controller.ex | 25 +++++++++++++++--- .../controllers/blog_html/index.html.heex | 13 +++++++++- .../controllers/blog_html/show.html.heex | 13 +++++++++- .../controllers/blog_html/tag.html.heex | 13 +++++++++- 7 files changed, 111 insertions(+), 7 deletions(-) diff --git a/app/lib/firehose_web/components/core_components.ex b/app/lib/firehose_web/components/core_components.ex index dcb60ad..18cf8f5 100644 --- a/app/lib/firehose_web/components/core_components.ex +++ b/app/lib/firehose_web/components/core_components.ex @@ -420,6 +420,31 @@ defmodule FirehoseWeb.CoreComponents do """ end + @doc """ + Renders an RSS feed icon (inline SVG). + + ## Examples + + <.rss_icon class="size-5" /> + """ + attr :class, :string, default: "size-4" + attr :rest, :global + + def rss_icon(assigns) do + ~H""" + + + + + """ + end + ## JS Commands def show(js \\ %JS{}, selector) do diff --git a/app/lib/firehose_web/components/layouts/app.html.heex b/app/lib/firehose_web/components/layouts/app.html.heex index 5d99688..d787052 100644 --- a/app/lib/firehose_web/components/layouts/app.html.heex +++ b/app/lib/firehose_web/components/layouts/app.html.heex @@ -39,4 +39,30 @@ + + <.flash_group flash={@flash} /> diff --git a/app/lib/firehose_web/components/layouts/root.html.heex b/app/lib/firehose_web/components/layouts/root.html.heex index 4147954..74b7879 100644 --- a/app/lib/firehose_web/components/layouts/root.html.heex +++ b/app/lib/firehose_web/components/layouts/root.html.heex @@ -41,6 +41,9 @@ <% end %> <% end %> + <%= for feed <- get_content_for(assigns, :feed_links) do %> + + <% end %> {@inner_content} diff --git a/app/lib/firehose_web/controllers/blog_controller.ex b/app/lib/firehose_web/controllers/blog_controller.ex index 43cab46..72fa372 100644 --- a/app/lib/firehose_web/controllers/blog_controller.ex +++ b/app/lib/firehose_web/controllers/blog_controller.ex @@ -9,9 +9,11 @@ defmodule FirehoseWeb.BlogController do result = blog.paginate(page) meta = Blogex.SEO.meta_tags_for_blog(blog, FirehoseWeb.Endpoint.url()) + rss_url = rss_feed_url(blog) conn |> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta) + |> FirehoseWeb.Layouts.put_content_for(:feed_links, %{rss_url: rss_url, title: blog.title()}) |> render(:index, page_title: blog.title(), blog_title: blog.title(), @@ -19,7 +21,8 @@ defmodule FirehoseWeb.BlogController do posts: result.entries, base_path: blog.base_path(), page: result.page, - total_pages: result.total_pages + total_pages: result.total_pages, + rss_feed_url: rss_url ) end @@ -27,31 +30,40 @@ defmodule FirehoseWeb.BlogController do blog = conn.assigns.blog post = blog.get_post!(slug) visibility = Blogex.Post.visibility(post) + rss_url = rss_feed_url(blog) meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog) conn |> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta) + |> FirehoseWeb.Layouts.put_content_for(:feed_links, %{rss_url: rss_url, title: blog.title()}) |> render(:show, page_title: post.title, post: post, meta: meta, base_path: blog.base_path(), visibility: visibility, - authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user) + authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user), + rss_feed_url: rss_url ) end def tag(conn, %{"tag" => tag}) do blog = conn.assigns.blog posts = blog.posts_by_tag(tag) + rss_url = rss_feed_url(blog) - render(conn, :tag, + rss_link = %{rss_url: rss_url, title: blog.title()} + + conn + |> FirehoseWeb.Layouts.put_content_for(:feed_links, rss_link) + |> render(:tag, page_title: "#{blog.title()} — #{tag}", blog_title: blog.title(), tag: tag, posts: posts, - base_path: blog.base_path() + base_path: blog.base_path(), + rss_feed_url: rss_url ) end @@ -77,4 +89,9 @@ defmodule FirehoseWeb.BlogController do _ -> 1 end end + + defp rss_feed_url(blog) do + base_id = blog.base_path() |> String.trim_leading("/blog/") + FirehoseWeb.Endpoint.url() <> "/api/blog/#{base_id}/feed.xml" + end end diff --git a/app/lib/firehose_web/controllers/blog_html/index.html.heex b/app/lib/firehose_web/controllers/blog_html/index.html.heex index c96f07a..7a7c119 100644 --- a/app/lib/firehose_web/controllers/blog_html/index.html.heex +++ b/app/lib/firehose_web/controllers/blog_html/index.html.heex @@ -1,6 +1,17 @@
-

{@blog_title}

+

{@blog_description}

diff --git a/app/lib/firehose_web/controllers/blog_html/show.html.heex b/app/lib/firehose_web/controllers/blog_html/show.html.heex index 8f4313b..8cf0f19 100644 --- a/app/lib/firehose_web/controllers/blog_html/show.html.heex +++ b/app/lib/firehose_web/controllers/blog_html/show.html.heex @@ -1,5 +1,16 @@
- ← Back to posts + <%= if @authenticated and @visibility == :draft do %>
-

{@blog_title}

+

Posts tagged "{@tag}"

From e3857104984b3986add37b22c0b6f102b446fc58 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 8 Jul 2026 16:47:58 +0100 Subject: [PATCH 7/8] Swap to classic RSS waveguide icon for better recognizability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the broadcast-style SVG icon with the traditional RSS symbol (dot + two concentric arcs) — the standard feed icon used across browsers and feed readers. --- app/lib/firehose_web/components/core_components.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/lib/firehose_web/components/core_components.ex b/app/lib/firehose_web/components/core_components.ex index 18cf8f5..d0be6c1 100644 --- a/app/lib/firehose_web/components/core_components.ex +++ b/app/lib/firehose_web/components/core_components.ex @@ -439,8 +439,7 @@ defmodule FirehoseWeb.CoreComponents do class={[@class]} {@rest} > - - + """ end From 990af18ef8b4e6beedf2e59128205093574e28be Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 8 Jul 2026 16:49:30 +0100 Subject: [PATCH 8/8] =?UTF-8?q?v0.2.0=20=E2=80=94=20RSS=20subscribe=20link?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add release note for v0.2.0 - Bump version from 0.1.0 to 0.2.0 --- app/mix.exs | 2 +- app/priv/blog/release-notes/2026/07-08-v0-2-0.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 app/priv/blog/release-notes/2026/07-08-v0-2-0.md diff --git a/app/mix.exs b/app/mix.exs index a04e3e8..89cfadf 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -4,7 +4,7 @@ defmodule Firehose.MixProject do def project do [ app: :firehose, - version: "0.1.0", + version: "0.2.0", elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, diff --git a/app/priv/blog/release-notes/2026/07-08-v0-2-0.md b/app/priv/blog/release-notes/2026/07-08-v0-2-0.md new file mode 100644 index 0000000..8d87958 --- /dev/null +++ b/app/priv/blog/release-notes/2026/07-08-v0-2-0.md @@ -0,0 +1,16 @@ +%{ + title: "v0.2.0 — RSS Subscribe Links", + author: "Willem van den Ende", + tags: ~w(release), + description: "RSS feed subscribe links with classic waveguide icons across blog pages, post pages, and site footer." +} +--- + +RSS feeds have always been available at `/api/blog/engineering/feed.xml` and `/api/blog/releases/feed.xml`, but they were never advertised. This release adds subscribe links throughout the site. + +## What's new + +- RSS `` tags in `` on all blog pages for browser/feed reader auto-discovery +- Orange RSS icon next to blog title on index pages +- RSS icon on each post page +- Subscribe section in the footer with links to both feeds \ No newline at end of file