diff --git a/_astro_nano.png b/_astro_nano.png
deleted file mode 100644
index 9e7d5d2..0000000
Binary files a/_astro_nano.png and /dev/null differ
diff --git a/_deploy_netlify.svg b/_deploy_netlify.svg
deleted file mode 100644
index 28837b6..0000000
--- a/_deploy_netlify.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
diff --git a/_deploy_vercel.svg b/_deploy_vercel.svg
deleted file mode 100644
index e2d3a0d..0000000
--- a/_deploy_vercel.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/_lighthouse.png b/_lighthouse.png
deleted file mode 100644
index 0695a06..0000000
Binary files a/_lighthouse.png and /dev/null differ
diff --git a/public/Logo-144.jpeg b/public/Logo-144.jpeg
new file mode 100644
index 0000000..aba9e59
Binary files /dev/null and b/public/Logo-144.jpeg differ
diff --git a/public/Logo.jpeg b/public/Logo.jpeg
new file mode 100644
index 0000000..b16a87e
Binary files /dev/null and b/public/Logo.jpeg differ
diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
new file mode 100644
index 0000000..75407a6
Binary files /dev/null and b/public/android-chrome-192x192.png differ
diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png
new file mode 100644
index 0000000..3a610a8
Binary files /dev/null and b/public/android-chrome-512x512.png differ
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..fe77295
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/astro-nano.png b/public/astro-nano.png
deleted file mode 100644
index 9e7d5d2..0000000
Binary files a/public/astro-nano.png and /dev/null differ
diff --git a/public/astro-sphere.jpg b/public/astro-sphere.jpg
deleted file mode 100644
index ba48e97..0000000
Binary files a/public/astro-sphere.jpg and /dev/null differ
diff --git a/public/deploy_netlify.svg b/public/deploy_netlify.svg
deleted file mode 100644
index 28837b6..0000000
--- a/public/deploy_netlify.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
diff --git a/public/deploy_vercel.svg b/public/deploy_vercel.svg
deleted file mode 100644
index e2d3a0d..0000000
--- a/public/deploy_vercel.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
new file mode 100644
index 0000000..f9bf9e7
Binary files /dev/null and b/public/favicon-16x16.png differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
new file mode 100644
index 0000000..080e056
Binary files /dev/null and b/public/favicon-32x32.png differ
diff --git a/public/favicon-48x48.png b/public/favicon-48x48.png
new file mode 100644
index 0000000..97d64c8
Binary files /dev/null and b/public/favicon-48x48.png differ
diff --git a/public/lighthouse.png b/public/lighthouse.png
deleted file mode 100644
index 0695a06..0000000
Binary files a/public/lighthouse.png and /dev/null differ
diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png
new file mode 100644
index 0000000..7e728ce
Binary files /dev/null and b/public/mstile-150x150.png differ
diff --git a/public/patrick.webp b/public/patrick.webp
deleted file mode 100644
index aadbe09..0000000
Binary files a/public/patrick.webp and /dev/null differ
diff --git a/public/rss-image.jpg b/public/rss-image.jpg
new file mode 100644
index 0000000..04cd144
Binary files /dev/null and b/public/rss-image.jpg differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..e8671c5
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "Dispatches",
+ "short_name": "Dispatches",
+ "description": "Technical writing on topics of personal interest.",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone",
+ "start_url": "/"
+}
diff --git a/src/components/Head.astro b/src/components/Head.astro
index 8aff2dc..3d615eb 100644
--- a/src/components/Head.astro
+++ b/src/components/Head.astro
@@ -24,7 +24,7 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
-const { title, description, image = "/nano.png", ogData } = Astro.props;
+const { title, description, image = "/Logo.jpeg", ogData } = Astro.props;
// Create default OG data if not provided
const defaultOgData: OpenGraphData = ogData || {
@@ -37,7 +37,7 @@ const defaultOgData: OpenGraphData = ogData || {
title,
theme: "dark",
backgroundImage: "gradient",
- logo: `${Astro.site}favicon-light.svg`
+ logo: `${Astro.site}Logo-144.jpeg`
}),
twitter: {
card: "summary_large_image"
@@ -48,9 +48,17 @@ const defaultOgData: OpenGraphData = ogData || {
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/consts.ts b/src/consts.ts
index e4e4ce1..3ffdcef 100644
--- a/src/consts.ts
+++ b/src/consts.ts
@@ -10,7 +10,7 @@ export const SITE: Site = {
export const HOME: Metadata = {
TITLE: "Dispatches",
- DESCRIPTION: "Astro Nano is a minimal and lightweight blog and portfolio.",
+ DESCRIPTION: "Technical writing on topics of personal interest.",
};
export const BLOG: Metadata = {
diff --git a/src/content/briefs/objective-c/equivalent-objects-function.md b/src/content/briefs/objective-c/equivalent-objects-function.md
new file mode 100644
index 0000000..50886e6
--- /dev/null
+++ b/src/content/briefs/objective-c/equivalent-objects-function.md
@@ -0,0 +1,124 @@
+---
+title: "You (Often) Want `EquivalentObjects`, Not `isEqual:`"
+cardTitle: "`EquivalentObjects` vs `isEqual:`"
+description: "`nil`-Messaging Lays a Subtle Trap for `isEqual:`"
+date: "2025-08-22"
+---
+
+A quirky aspect of Objective-C is that sending messages to `nil` is *valid*:
+
+- it's a no-op, not a crash
+- the return value is the "all-zero" value for the return type[^1]
+
+This is *very* different from most other languages:
+
+- modern languages[^2] tend to make "calling a method on `nil`" impossible via compile-time checks.
+- older languages[^3] tend to make "calling a method on `nil`" a crash.
+
+[^1]: In Swift terminology, it's as-if `bar.someMethod()` were getting implicitly-rewritten to `bar?.someMethod() ?? 0` (loosely-speaking).
+
+[^2]: Swift, Rust, Kotlin, and so on.
+
+[^3]: C++, Java, and so on.
+
+The focus of this brief is a specific pitfall that arises from this behavior:
+
+- in Objective-C, you check semantic equality via `isEqual:` (e.g. `[foo isEqual:bar]`)
+- `isEqual:` returns `NO` when sent to `nil`, which probably isn't what you want
+
+In other words, the truth table you *want* looks like this:
+
+| | `nil` | X | Y |
+|---|---|---|---|
+| `nil` | `YES` | `NO` | `NO` |
+| X | `NO` | `YES` | `NO` |
+| Y | `NO` | `NO` | `YES` |
+
+...but the truth table you *get* from `[foo isEqual:bar]` looks like this:
+
+| | `nil` | X | Y |
+|---|---|---|---|
+| `nil` | **`NO`** | `NO` | `NO` |
+| X | `NO` | `YES` | `NO` |
+| Y | `NO` | `NO` | `YES` |
+
+This can lead to subtle bugs like in the code below:
+
+```objective-c
+// ProjectDescriptor.h
+@interface ProjectDescriptor : NSObject
+
+@property(nonatomic, nonnull, copy, readonly) NSString *projectName;
+@property(nonatomic, nullable, copy, readonly) NSURL *repositoryURL;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithProjectName:(NSString *)projectName repositoryURL:(NSURL *)repositoryURL NS_DESIGNATED_INITIALIZER;
+
+// dedicated equality-checker that we will also call from within `isEqual:`
+- (BOOL)isEqualToProjectDescriptor:(ProjectDescriptor *)other;
+
+@end
+```
+
+```objective-c
+// ProjectDescriptor.m
+@implementation ProjectDescriptor
+
+- (BOOL)isEqualToProjectDescriptor:(ProjectDescriptor *)other {
+ return [self.projectName isEqual:other.projectName] && [self.repositoryURL isEqual:other.repositoryURL];
+}
+
+@end
+```
+
+```objective-c
+// ProjectDescriptorTests.m
+@import XCTest;
+
+@interface ProjectDescriptorTests : XCTestCase
+
+@end
+
+@implementation ProjectDescriptorTests
+
+- (void)testEquality {
+ ProjectDescriptor *foo = [[ProjectDescriptor alloc] initWithProjectName:@"Foo" repositoryURL:nil];
+ ProjectDescriptor *bar = [[ProjectDescriptor alloc] initWithProjectName:@"Foo" repositoryURL:nil];
+ XCTAssertTrue([foo isEqualToProjectDescriptor:bar]);
+}
+
+@end
+```
+
+Can you spot why `testEquality` will fail?
+
+If not, here's the explanation: `isEqualToProjectDescriptor:` when `repositoryURL` is `nil`, then `[foo.repositoryURL isEqual:bar.repositoryURL]` will *always* return `NO`, *even if* `bar.repositoryURL` is also `nil`—that's just `nil`-messaging at work!
+
+Thankfully, in this case knowing is ~half~ about ~80%~ of the battle: once you're aware of this trap, it's easy to avoid via consistent use of a C-style wrapper function.
+
+The way I usually write it looks like this:
+
+```objective-c
+static inline
+BOOL EquivalentObjects(id _Nullable lhs, id _Nullable rhs) {
+ return lhs == rhs || [lhs isEqual:rhs];
+}
+```
+
+As such a simple function, there's not much to say about it beyond administrivia:
+
+- it's not a bad idea to put a namespace prefix on it
+- it's also not a bad idea to give it a hard-to-use Swift name (e.g. `NS_SWIFT_NAME(__dont_use_EquivalentObjects(_:_:)`)
+- you can *consider* replacing it with a macro, but I wouldn't—it's trickier than it looks!
+- you want to make sure it's not visible-from/used-within your public headers[^5]
+
+[^5]: Mainly to avoid any possible chance of ambiguity / name conflicts / duplicate symbols / other shenanigans.
+
+That's about it!
+
+Once you have that function defined, the rest is just being consistent with using it instead of `isEqual:`.
+My personal strategy for it is:
+
+- use it unconditionally for *all* equality checks *inside* methods like `isEqual:` (and type-specific helpers like `isEqualToProjectDescriptor:`)
+- use it *when appropriate* for checks impacting control flow (it's often more legible to include explict `!= nil` checks in those cases, however)
+
diff --git a/src/content/briefs/swift-warts/definition-order-matters-in-package.md b/src/content/briefs/swift-warts/definition-order-matters-in-package.md
new file mode 100644
index 0000000..1a95fe5
--- /dev/null
+++ b/src/content/briefs/swift-warts/definition-order-matters-in-package.md
@@ -0,0 +1,14 @@
+---
+title: "Definition Order Matters in Swift Packages"
+description: "You *Can* Use Helpers in `Package.swift`, but only if they are defined before use."
+date: "2025-08-22"
+---
+
+Short and sweet: in larger projects it can be helpful to define helper functions within your `Package.swift` file, but there's a catch: you *must* define them before their use site.
+
+What makes this a wart is the following:
+
+- your `Package.swift` file will "compile" ok (you just get bizarre errors)
+- it's different from the rest of Swift, which imposes no such ordering restriction
+
+For this one there isn't really a fix—it's just something to be aware of.
diff --git a/src/content/projects/agentic-navigation-guide/index.md b/src/content/projects/agentic-navigation-guide/index.md
index fa3f8be..008ab11 100644
--- a/src/content/projects/agentic-navigation-guide/index.md
+++ b/src/content/projects/agentic-navigation-guide/index.md
@@ -1,75 +1,137 @@
---
-title: "Astro Sphere"
-description: "Portfolio and blog build with astro."
-date: "Mar 18 2024"
+title: "Agentic Navigation Guide"
+cardTitle: "Keep your CLAUDE.md content accurate."
+description: "Keep your CLAUDE.md content accurate."
+date: "August 2, 2025"
+repoURL: "https://github.com/plx/agentic-navigation-guide/"
draft: true
---
-
+The [`agentic-navigation-guide` project](https://github.com/plx/agentic-navigation-guide/) is a small CLI tool primarily for *maintaining* "navigation guides" for use in memory files like `CLAUDE.md`.
+It's also the result of a deliberate, successful experiment in "pure vibe coding"—see [development notes](#development-notes) for discussion on that process.
-Astro Sphere is a static, minimalist, lightweight, lightning fast portfolio and blog theme based on my personal website.
+## Overview
-It is primarily Astro, Tailwind and Typescript, with a very small amount of SolidJS for stateful components.
+What's a "navigation guide", you ask?
-## 🚀 Deploy your own
+Easy: it's a list of files and folders, with optional comments, that looks about like this:
-
+```md
+
+- astro.config.js # sitewide astro configuration
+- justfile # local development commands
+- public/ # static resources, installed to site root
+- scripts/ # (project-specific) custom scripts
+ - validate-links.js # link validation script
+- src/ # source code
+ - lib/ # utility libraries
+ - components/ # UI components (in .astro files)
+ - content/ # content collections (blog, briefs, projects)
+ - briefs/ # categorized short notes, one folder per category
+ - blog/ # longer-form articles (one folder per post)
+ - projects/ # project pages (one folder per project)
+ - layouts/ # page layouts
+ - pages/ # routes and pages
+ - index.astro # site root
+ - styles/ # global styles
+
+```
-## 📋 Features
+Maybe *someday* this will be a fancy "standard" with an RFC, a domain name, and all that.
+For now, though, it's just a thing I've been using when working with Claude Code in larger repositories.
-- ✅ 100/100 Lighthouse performance
-- ✅ Responsive
-- ✅ Accessible
-- ✅ SEO-friendly
-- ✅ Typesafe
-- ✅ Minimal style
-- ✅ Light/Dark Theme
-- ✅ Animated UI
-- ✅ Tailwind styling
-- ✅ Auto generated sitemap
-- ✅ Auto generated RSS Feed
-- ✅ Markdown support
-- ✅ MDX Support (components in your markdown)
-- ✅ Searchable content (posts and projects)
+The *tool*'s primary job is to check the guide against the state of the file-system:
-## 💯 Lighthouse score
-
+- it parses the content of the `` tag
+- it checks if each listed path actually exists
+- if the guide mentions non-existant files, it reports useful errors
-## 🕊️ Lightweight
-All pages under 100kb (including fonts)
+You can use it yourself (e.g. as a pre-commit hook, etc.), if you like.
+You can also set it up as a "hook" for Claude Code, in which case:
-## ⚡︎ Fast
-Rendered in ~40ms on localhost
+- it tells Claude when the navigation guide has become inaccurate
+- it gives Claude enough help he can probably fix it himself
-## 📄 Configuration
+You should still check his work, of course, but it's a good start.
-The blog posts on the demo serve as the documentation and configuration.
+## Development Notes
-## 💻 Commands
+As noted above, I deliberately implemented this purely via vibe coding: I'd *review* Claude's output and offer feedback, but intentionally had Claude write *all the code*.
-All commands are run from the root of the project, from a terminal:
+### Development Process: Primary Implementation
-Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc
+For the *initial* implementation, I used a specification-driven workflow:
-| Command | Action |
-| :------------------------ | :----------------------------------------------- |
-| `npm install` | Installs dependencies |
-| `npm run dev` | Starts local dev server at `localhost:4321` |
-| `npm run sync` | Generates TypeScript types for all Astro modules.|
-| `npm run build` | Build your production site to `./dist/` |
-| `npm run preview` | Preview your build locally, before deploying |
-| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
-| `npm run astro -- --help` | Get help using the Astro CLI |
-| `npm run lint` | Run ESLint |
-| `npm run lint:fix` | Auto-fix ESLint issues |
+1. I hand-wrote [a detailed specification document](https://github.com/plx/agentic-navigation-guide/blob/main/Specification.md)
+2. In *plan mode*, I had Opus generate a high-level roadmap with distinct *phases* (and iterated a bit until it was satisfactory)
+3. I asked Claude to implement "phase 1" (and just "phase 1")
+4. I had Claude write a `ContinuingMission.md` file that:
+ - described the work done so far
+ - described the work remaining
+ - described the immediate "next steps" for the next session
+4. I then entered a loop like this:
+ - start a fresh session
+ - have Claude copy the `ContinuingMission.md` file into a `missions/` folder in the repo (and rename with a timestamp, to make it unique)
+ - have Claude read the `ContinuingMission.md` file and take on the next task
+ - review the results, offer feedback, and keep Claude iterating until he finished the task
+ - have Claude *rewrite* `ContinuingMission.md` to once again:
+ - describe the work done so far
+ - describe the work remaining
+ - describe the immediate "next steps" for the next session
+5. I kept repeating that loop until the initial pass on the project was complete
-## 🏛️ License
+Since this was my first pure vibe-coding experiment, I iteratively improved my workflow as I went:
-MIT
+- initially, I manually copied the `ContinuingMission.md` file and manually typed out the start-of-session and end-of-session prompts
+- eventually, I setup slash commands for the start-of-session and end-of-session prompts (and had the prompts include the backup-file operation)
+
+This approach isn't as robust as the fancier "project management" workflows I'd studied prior to this experiment, but it proved effective for this project.
+For future projects I'd consider a fancier workflow, but think there's definitely a complexity spike when you go from "all task information is in a single file" to "we have per-task files"—having everything in a single file sidesteps the challenge of updating future tasks in response to discoveries during the current task.
+
+I had given a little thought to what I'd need to do if Claude went haywire during a step, but never had to test those ideas—things basically just "worked out."
+
+### Development Process: Subsequent Refinements
+
+After initial development was done, I used a simpler workflow:
+
+- start a new session
+- ask Claude to do what I needed done
+- review the results, and either keep, revise, or reset and try again
+
+This was actually where I found Claude Code to be particularly helpful, because I could offload some tedious research tasks to him, too:
+
+- with Claude: "how do I publish this crate? OK, great, please do that."
+- without: google for how to publish, fiddle with this or that, and so on
+
+Experiences like this make me think the commonly-made point that you don't learn when using agents is true, but not the full story: there's a very real cognitive burden to having to learn necessary-but-trivial things, and it's nice to *have the option to be selective*. Longer-term, this points to increasing returns on meta-skills like "learning *when* to learn" (...now that learning is increasingly optional).
+
+### Development Experience: Faster, Not Easier
+
+If you measure time-to-completion, I'd estimate I finished this 7x faster than I would have doing the work myself (including tool-writing time, deepening my knowledge of Rust, and so on). If I wrote Rust on a regular basis I'd guess the speedup factor would be closer to 4x, but that's more-speculative.
+
+So, purely for speed, this was a big win.
+
+If you measure cognitive effort, the picture's a bit more nuanced: the cognitive effort *was* reduced, but it was also *compressed* and *front-loaded*—writing that psecification document was *a lot* of work!
+
+Ordinarily, I use a process like this:
+
+- think it through *enough* to feel confident about my general approach
+- begin with the bits for which I feel most confident
+- use the insight gained from implementing the "easy bits" to help think through the "harder bits"
+
+Generally this works well for me, and has the benefit of alternating between mentally-taxing "thinking" and comparatively-easy "coding".
+
+For this project, however, writing the spec required that I do essentially all the thinking-through at the beginning (and without the benefit of learning from the implementation). This changed the overall process to something more like:
+
+- mentally-taxing: thinking it through *fully* before writing any code
+- chill-and-easy: supervising Claude through the implementation
+
+I don't have a deeper conclusion or grand theory about this, but it's interesting.
+
+### Development Process: Claude Is Pretty Resourceful
+
+One minor surprise was how "resourceful" Claude turned out to be with this project.
+
+The particular thing that stuck out was that Claude figured out how to write unit tests that created temporary files and then checked "agentic navigation guides" against them. This approach makes a lot of sense, and it was a little uncanny seeing Claude reach for it on his own.
+
+It rhymes with another case from a project I haven't (yet) written up: I'd asked Claude to match the behavior of a system with existing python and javascript implementations, and watched as it autonomously decided to write dozens of little "tester" programs to identify exactly how those other implementations handled each edge case. Agentic assistants remain very jagged intelligences, but at least for Claude the "learn by conducting experiments" capability seems very well-developed.
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 461a775..d2b9f6d 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -38,9 +38,6 @@ const ogData = getHomeOGData(
-
- Hi! 👋🏻
-
@@ -112,7 +109,7 @@ const ogData = getHomeOGData(
- If you want to get in touch, please do!
+ Here's how to get in touch: