Saturday, June 13, 2026
HomeiOS DevelopmentKits All the Way Down

Kits All the Way Down


Everything that happened on github.com/Cocoanetics since the SwiftCross post (June 2 – June 13, 2026). Four new kits, a fourteen-year-old flagship that came home, and the stretch where the extraction pattern started shipping to users.


When I published the SwiftCross post on June 2nd, I left two threads dangling: an agent was refactoring SwiftMCP’s package structure so client-only consumers wouldn’t drag in SwiftNIO, and if that worked it would unlock SwiftAgents on Windows. I expected to report on that “eventually.” Both landed before the day was over. That set the tone for the week — I keep discovering that the limiting factor is no longer how fast things get built, but how fast I can decide what to build next.

The loose ends didn’t stay loose for a day

SwiftCross itself didn’t sit still past lunch. By the evening of June 2nd it had grown WallClock — a portable nanosecond wall-clock (clock_gettime on POSIX, GetSystemTimePreciseAsFileTime on Windows) that I needed for span timing in agent traces — released as 1.1.0, and Environment.set, a portable setenv for .env loaders, released as 1.2.0. Two tags in one evening for a package I had announced that morning.

The same evening, SwiftMCP 1.5.0 shipped the package-traits refactor from the post: Server, Client, and OpenAPI traits, all on by default, with swift-nio, swift-crypto, and swift-certificates gated behind Server. A client-only consumer now gets a fully NIO-free build — which is the whole ballgame for Windows. (1.5.1 followed the next morning to widen the swift-crypto range; dependency hygiene never sleeps.)

And SwiftAgents cashed the check immediately: it now links SwiftMCP with traits: [Client] only, dropped swift-nio entirely, migrated its remaining shims to SwiftCross, and the Windows CI probe went green. The thing I described as “if that works out” in the last post was a merged PR about three hours later.

SwiftMCP then got two more modernization passes during the week: all three server transports now conform to swift-service-lifecycle‘s Service (graceful SIGTERM/SIGINT shutdown replaces my bespoke signal handler, and the stdio transport finally exits on EOF instead of politely polling at 10 Hz), and the HTTP transport was rebuilt on apple/swift-http-types — typed header fields and methods instead of strings, with NIOHTTPTypesHTTP1 deleting a pile of hand-rolled conversions.

The cascade reached SwiftMail

This is my favorite part of having a small ecosystem: a release in one package becomes a one-line diff in the next. SwiftMail 1.7.0 (June 3) adopted SwiftCross 1.2.0, dropped my swift-nio-imap fork in favor of Apple’s upstream, refreshed every dependency, and got Windows and Android CI. A day later 1.7.1 fixed a genuinely subtle bug — and not one of mine: contributor tabmail-kmyi found that the pipelined FETCH dispatcher could drop later body parts when a server batches untagged responses. Fixed with proper EmbeddedChannel regression tests. The packages being presentable enough that strangers debug IMAP pipelining for you is a quiet milestone of its own.

The agents audited their own playground

On June 4th I did something that still makes me smile: I let an agent loose inside the SwiftBash sandbox and told it to poke around like a suspicious user. It came back with a stack of issues — stat leaked my real host uid/gid through the virtual filesystem, the time keyword didn’t parse, ps couldn’t see the shell it was running in, /tmp didn’t show up in ls /, assorted GNU options were missing from xxd, od, and realpath.

Then, over that night and the next day, the agents fixed everything on their own list. SwiftBash now reports virtual identity at the filesystem boundary, has a real mount command with a virtual mount table, parses time and reports real/user/sys, and its ps is no longer blind to itself. ShellKit got the matching ProcessTable lifecycle work, and BinCatalog moved from ShellKit into SwiftBash where it belongs. The README gained a “What’s virtual (and what that means)” section, because if an agent needed to discover those answers empirically, so would you. There’s something pleasingly self-referential about an agent filing bugs against the sandbox built to contain agents — and then resolving them.

sqlite3 to byte-parity, then out the door

Also on June 4th, the SwiftPorts sqlite3 shell port went through a parity marathon: .dump fidelity with proper identifier quoting, byte-exact floating-point formatting via a small C shim, .databases/.schema/.show/.width/.limit behavior matched against the real thing.

It turned out that it was exceptionally easy to get sqlite3 to build for all platforms because the code is essentially a single c file. And then I remembered that there was a full text search extension (FTS) that Apple chose not to ship in sqlite3. I asked the agents, if we could that. They sayed “Yes!” and built it. Then I remembered that there was also a vector search extension for sqlite3. Same story: if you can think it, they can make it.

This is why we got FTS5 and sqlite-vec which you can enable individually via SPM traits.

At which point the SQLite stack had clearly outgrown its host, and on June 8th it was extracted into SQLiteKit — a new repo: a cross-platform Swift wrapper over a vendored SQLite amalgamation with prepared statements, FTS5 full-text search, and sqlite-vec semantic search behind traits. Twelve thousand lines left SwiftPorts in a single commit. If that move sounds familiar, it’s exactly what SwiftCross was: the third time you copy something, it wants to be a package.

Teaching the agents to search

SQLiteKit immediately had a customer. For a long time I kept saying that macOS and iOS need to get an un-neutered version of sqlite3. Since last year CoreSpotlight clearly has a semantic index, which cries out “vector DB”. But there is no first-party public semantic DB capability to be found on Apple platforms.

Last year I experimented with a simple linear vector search in SwiftAgents, as well as the semantic search that OpenAI gives us. There you upload documents to OpenAI and you can use a file search tool to enrich your completions.

Now it was within my grasp to grow SwiftAgents a persistent SQLiteVectorStore — vec0 KNN plus FTS5 bm25 plus hybrid search with line-span provenance — and then, in a single day of eight consecutive PRs, absorbed the retrieval techniques from tobi’s qmd: smart markdown chunking that respects code fences, reciprocal rank fusion, lex/vec/hyde query expansion, LLM-gated expansion, a batch reranker, and query/document embedding asymmetry. On-device Apple NL embeddings by default; OpenAI only when a key is present.

The deliberate part was the order. qmdquery markdown — and the OpenClaw equivalent each carried a lot of hard-won, embedded knowledge about how to build a memory-retrieval pipeline, and I didn’t want that knowledge trapped inside a CLI. So I had the agent rebuild the index engine inside SwiftAgents first, where anything can reuse it, and only then — because the engine existed — QMDKit got built on top of it in about two days: another new repo, a qmd clone that indexes and semantically searches Markdown, available as a standalone CLI and as a sandboxed ShellKit builtin. Which means SwiftBash sessions — the same sandbox the agents work in — now have semantic search over my notes as a shell command. The agents built their own retrieval tooling, then installed it in their own environment. I just watched.

GitKit, and a lesson in humility from libgit2

June 9th: I wanted SwiftPorts’ git to stop depending on a personal libgit2 fork. The fork I’d been leaning on minted a fresh branch and package for every single libgit2 release; I wanted the opposite shape — one build that pulls the C source straight from upstream as a git submodule, version-matched, so bumping libgit2 becomes a submodule pointer rather than a brand-new repo. So GitKit became the third new repo of the week — libgit2 1.9.4 packaged for SwiftPM with a pristine upstream submodule, driven to green on macOS, iOS, Linux, Windows, and Android in a single afternoon (Windows needed a curated header umbrella; Android needed its own toolchain rituals; tvOS and watchOS were dropped because libgit2 forks processes, and they don’t).

Then reality arrived on schedule. SwiftPorts adopted GitKit, and git stash apply broke. The culprit was wonderfully obscure: feature-define names inherited from another fork that no longer matched libgit2 1.9.4’s dialect, silently disabling things like nanosecond timestamps. Adopt, regress, revert, fix, re-adopt — all within about nine hours, every leg verified by the five-platform CI. This is the outer loop from the SwiftCross post doing exactly what I claimed it does, except this time the red checkmark saved me from shipping a subtly broken git.

The week closed with the now-familiar move: SwiftPorts’ entire Swift git layer was lifted out and became the GitKit 2.0.0 SDK — a full idiomatic Repository API over libgit2 — leaving SwiftPorts with just the sandbox-aware face and CLI. Zero duplication. (There was also a brief comedy in three acts about how a tagged package may depend on the untagged swift-archive; the answer, after one rejected fork-with-a-minted-tag, is: pin the upstream commit and let traits prune it.)

The parser wanted to be a kit too

If the first nine days had a refrain, it was “the third time you copy something, it wants to be a package.” Day eleven onward just kept proving it — starting, of all places, with the oldest piece of code in the whole org.

For years the tolerant HTML parser inside SwiftText was a quiet workhorse: a Swift rewrite of DTHTMLParser, itself a piece of DTFoundation that predates most of these repos by more than a decade. On June 12 it finally moved out on its own. XMLKit — an async streaming HTMLParser built on libxml2’s SAX interface, so it handles real-world malformed HTML gracefully and never builds a DOM unless you ask it to. It left SwiftText carrying its full git history, and SwiftText immediately adopted it back as a dependency. Released as 1.0.0 that afternoon, then 1.0.1 a few hours later once CI was building and running the test suite on all five platforms — macOS, iOS, Linux, Windows, Android. The name is a promise: an XML SAX module along the same lines is the obvious next tenant.

The honest trigger was narrower than the result. I didn’t want to drag swift-markdown into DTCoreText just to reach the HTML parser, and my first instinct was the cheap fix — a trait inside SwiftText that gates the dependency away. But the earlier extractions had taught me better: the third time you reach for a thing through a wall of unrelated dependencies, it wants to be its own package. XMLKit is, at heart, libxml2 plus a thin layer of Swift wrappers — and because it stands alone, with nothing else cluttering the manifest, I could chase it all the way to green CI on all five platforms. That five-platform checkmark is my current gold standard, and realistically you only earn it one library at a time.

And then the fourteen-year-old flagship came home

This is the part I didn’t see coming. DTCoreText — the CoreText-meets-HTML library with six thousand stars and a decade and a half of history — shipped 2.1.0 on June 13, and it’s the most interesting release of the stretch precisely because it’s the least new.

Two things happened to it at once. It got features: HTML tables with a real NSTextTable-compatible model (colspan/rowspan, border-collapse, row-by-row pagination), CSS floats (float/clear with text wrapping), CSS border-spacing, and — the one that made me grin — native iOS 27 TextKit bridging, so DTCoreText’s text blocks and tables lay out as real NSTextBlock/NSTextTable on the new SDK. Roughly 8,600 lines across five PRs, the first feature release built on this spring’s Swift 2.0 rewrite.

The tables especially are a fourteen-year finally. The first request to render an HTML <table> is issue #144, filed in March 2012 and labeled — with brutal honesty — Sponsor Needed. Nobody ever sponsored it, and the asks just kept arriving every few years. What finally broke the logjam wasn’t a cheque: it was iOS 27 publishing NSTextBlock/NSTextTable/NSTextTableBlock as real classes — so there was a native model worth mirroring — plus an agent willing to write the eight thousand lines the bounty never bought.

And it got smaller: its dependency graph went from five packages to one. Because the only thing DTCoreText ever imported from SwiftText was that HTML parser — which now lives in XMLKit — it could drop SwiftText, swift-markdown, swift-cmark, swift-docc-plugin, SymbolKit, and swift-argument-parser in a single Package.swift edit, with zero source changes. import HTMLParser resolves to the identical module; only the manifest moved.

So the lineage closed a loop. DTHTMLParser became SwiftText’s parser became XMLKit’s HTMLParser, and the fourteen-year-old library that the original DTHTMLParser shipped in is now a one-dependency consumer of its own great-grandchild. (It also quietly renamed its default branch from develop to main on the way out the door.)

One core, many facades

Underneath the visible packages, the plumbing kept consolidating on the same principle. On June 11 the virtual-to-host PathMapping logic — the thing that makes a sandbox show /workspace while the bytes live somewhere real — moved down out of SwiftBash into ShellKit, where every caller can share one authority instead of each reinventing it. ShellKit grew PathMapping and a Sandbox.confined(to:home:temporaryDirectory:) entry point; SwiftBash then re-consumed it as two thin facades (a mounted filesystem and an exec confinement) and deleted its old identity mount on purpose — a loud compile-time break for embedders, which is how this org likes its breaks. The same day, SwiftBash also gave each sandbox its own per-instance /tmp instead of sharing the process temp directory.

It’s the BinCatalog/ProcessTable move from the SwiftCross week, one level lower: the shared primitive sinks to the lowest package that can own it, and everything above it gets thinner.

SwiftGH, byte-for-byte

The GitHub-CLI port living in SwiftPorts picked up a name this stretch — SwiftGH — and a parity obsession. Across a tight run of PRs it chased real gh 2.89.0 to the byte: gh api --include now prints the full Go-style header preamble with canonical MIME casing and CRLFs, --jq matches gojq’s compact key-sorted output, gh auth status refuses API calls without a token and exits with the same code upstream does, and the REST models grew fork metadata and PR-review types. Most of it was pulled along by a downstream app the commits keep calling “RepoGraph” — the now-familiar shape of a consumer dragging a library to completion.

The architecturally interesting bit is the wiring. SwiftPorts now depends on GitKit pinned to branch: main, and on June 13 the two repos shipped a matched pair: GitKit added ColorPalette.colorizeDiffStat (the green/red histogram coloring for git diff --stat), and SwiftPorts’ git port consumed it the same day. The diff-coloring logic that GitKit inherited when its SDK was carved out of SwiftPorts last week is now flowing back the other way as a live dependency. The extraction wasn’t a goodbye; it was a phone line.

Off to the side, SwiftMCP opened a planning issue to adopt the upcoming 2026-07-28 MCP draft — dual-era support and a protocol-version abilities model. That’s the next big decision queued up rather than a shipped one, but it’s worth flagging: the protocol the whole agent stack speaks is about to gain a versioned capabilities handshake.

The agents’ memory grew a second backend

The retrieval thread from last week kept going. SwiftAgents introduced a store-agnostic SemanticStore protocol — indexText / indexFile / sync / search / count — with two backends behind it: the existing on-device SQLite store and a new hosted OpenAIVectorStore. One protocol, two stores, swap by configuration. It came with a deliberate breaking rename (the old VectorStore name collided with both the wire DTO and OpenAI’s own product term), which rippled downstream into QMDKit within the hour, since QMDKit pins SwiftAgents at branch: main. QMDKit also reached qmd output parity the same day — snippet extraction with diff-style line headers and clickable OSC-8 file hyperlinks in the terminal.

Then it started shipping

Here’s the genuinely new kind of activity. For nine days the work was building kits; on June 13 a noticeable share of it was shipping them to people.

Post — my local mail daemon, MCP server, and CLI, built on SwiftMail, SwiftMCP, and SwiftText — got a dependency refresh and went out as v1.6.7 through the homebrew-tap, which now serves four tools by brew install: post, codexmonitor, swifttext, and swiftbutler. And ChatGPTExporter, the little Safari extension that saves a ChatGPT conversation as Markdown (footnoted citations and all), got dressed for public release the same day — renamed from its working title to “ChatGPT Exporter,” given a CI workflow that compile-checks the app and extension unsigned, docs cleaned of a feature that no longer exists, and a fix so code-interpreter file downloads resolve to real bytes instead of a JSON envelope.

None of these are kits, and that’s the point. The same outer loop that drives a library to five-platform green also, it turns out, drives a menu-bar app and a browser extension to the store-adjacent finish line. The pattern grew a roof.

The moment it got real

All of this — the kits, the extractions, the five-platform CI — stays abstract until you watch it run somewhere it has no business running yet. The other day I launched iBash, my still-in-development iOS shell, indexed a folder of Markdown notes with qmd, and then, because I could, opened sqlite3 right there inside the sandbox and queried the database it had just written. And there it was: the actual vec0 vectors, the actual FTS5 rows — semantic and keyword indexes sitting in a real SQLite file, on a phone. WOW. It actually works. Every layer I’d been assembling in the abstract — SQLiteKit’s vec0 and FTS5, the SemanticStore engine, QMDKit’s pipeline, the SwiftBash sandbox hosting both the indexer and the inspector — collapsed into one running thing I could poke at with a shell command. That’s the moment a pile of packages stops being a dependency graph and becomes a tool.

The pattern, twelve days in

Looking back at the whole stretch: SwiftCross, SQLiteKit, GitKit, XMLKit — four packages that all followed the identical arc. Something gets built inside a host project because it’s needed there, gets driven to parity or production quality by the CI outer loop, and the moment a second consumer appears, the agents make extraction nearly free. PathMapping did it one level down, sinking from SwiftBash into ShellKit. The expensive parts used to be the mechanical ones — moving twelve thousand lines, re-wiring dependents, keeping five platforms green through the whole shuffle. Those now cost approximately one well-phrased instruction.

It’s worth saying plainly what a Kit even is around here, because three of those four make it concrete: SQLiteKit is SQLite, GitKit is libgit2, XMLKit is libxml2. The recipe doesn’t vary. Take a gnarly C library, get it building with every last platform-specific intricacy on all five major Swift platforms — macOS, iOS, Linux, Windows, Android — and then lay a Swift API over it that’s actually pleasant to call. The five-platform green checkmark is the gold standard; the Swift surface is the reason anyone reaches for it. That’s the whole suffix.

What surprised me in the back half was the reach. The pattern didn’t just spawn new kits; it pulled in the oldest code I have. DTCoreText — fourteen years old, six thousand stars — slimmed to a single dependency and gained modern CSS tables and iOS 27 TextKit bridging in the same release, because the parser it always carried had finally become a package its great-grandchild could share. And then the work turned outward: Homebrew formulae, a Safari extension prepped for the store. Kits all the way down, and now products on top.

What’s left for me is the part I actually enjoy: noticing that a thing wants to be a kit — or that a kit is finally ready to be a product — and saying so.


Categories: Updates

RELATED ARTICLES

Most Popular

Recent Comments