Sprint 18 deferred: what a blocked sprint got done anyway
Sprint 18 was Desktop CDN completeness: macOS installers on R2, an automated publish, a smoke test of Check for updates across a version bump. Not one of its four tasks is done, and the reason is not technical. The first task is "restore GitHub Actions billing", and every other task waits behind it. Jobs do not fail — they never start:
The job was not started because recent account payments have failed or your spending limit needs to be increased.
So the sprint stays open and honest in the roadmap, marked planned — deferred, and this post records what happened in that window instead. A blocked sprint is still a sprint that cost calendar time; pretending otherwise is how roadmaps start lying.
Connecting a chat assistant stopped being guesswork
Desktop had a Start MCP button that connected nothing. Chat clients spawn their own MCP process over stdio from their own config file, so the button was, at best, a boot check — and the setup itself lived in the docs as one Cursor-shaped JSON block full of C:/path/to/... placeholders.
Desktop now generates the config from the running install: real Node path, real server entry, the connected project seeded into MERONQ_PROJECTS. One snippet per client in that client's own format — Claude Desktop, Claude Code (claude mcp add …), Cursor, VS Code (which wants servers, not mcpServers), Codex CLI (TOML), Windsurf, plus a generic fallback. Each card resolves the client's config file for your OS and tells you whether Meronq is already in it.
Connecting several assistants is the expected path, not the exception: they all read the same local memory. Permissions stay opt-in — the runtime, file-write and network flags are absent from the snippet unless you tick the box, because a paste should not silently grant build and network access (ADR-0017).
The handshake stopped eating the session it bootstraps
Handshake returned the entire CEM graph plus the full project index on every call — roughly 27k tokens, spent to explain a project to an agent that then had less room to work on it. It now defaults to detail=brief: a vitality-ranked entity focus, no evidence, about 5k tokens. detail=full still exists for the rare case that needs everything, and durable evidence never left SQLite — memory_search reaches it on demand.
That change forced an uncomfortable question: was the cheaper handshake also a worse one? Nothing in the repository could answer it. A metric that rewards a bigger dump will always recommend dumping more.
ADR-0018 answers it by measuring understanding against context cost, so the two are priced together rather than one at a time. It also sharpened an older weakness: the PUC control arm banned Meronq's MCP tools in the prompt but left .meronq/local/memory.db on disk, which is a weaker control than its name suggested.
Releases stopped depending on CI
With Actions unable to start jobs, publishing moved to the machine. pnpm desktop:release mirrors the publish workflow end to end — build, sign, upload to R2, write latest.json — with secrets read once from a gitignored .env.release instead of being retyped per release. Two guards are built in: it refuses to republish an existing version, because a rebuild under the same number is invisible to installs that already have it, and it restores createUpdaterArtifacts: false even when the build fails, because leaving it on breaks the verify workflow that has no signing key.
Desktop 0.1.1 went out that way.
macOS CI moved to Codemagic
Rather than wait for the billing question, macOS builds moved to a second CI. Two workflows, mirroring the Actions ones step for step so the two cannot drift: an unsigned verify build on main, and a signed publish on desktop-v* tags that ends in the same publish-to-r2.mjs. Windows is deliberately not duplicated — Actions keeps it, and it builds fine locally (MSI + NSIS in about four minutes, with the packaged sidecar answering /health in 203 ms).
The first publish run is a good example of why CI configs deserve preflight checks. It compiled for three minutes, bundled the .app, the .dmg and the updater pack, and then died on signing:
failed to decode secret key: incorrect updater private key password: failed to fill whole buffer
The message blames the password. It was not the password. Reproduced against the real key, each failure mode says something different: a wrong password says "Wrong password for that key", a truncated key says "Invalid input", a stray newline fails base64 decoding — and only the public key produces "failed to fill whole buffer". meronq.key.pub is valid base64 that decodes to too few bytes, so it slips past every shallow validation and then blames the wrong thing.
The publish workflow now rejects that value by name before compiling anything, and proves key and password together with a one-second tauri signer sign probe. Both messages are in the ops troubleshooting table, with the tell: the private key is 348 characters, the public one is 152 and identical to the pubkey already committed in tauri.conf.json.
What stays open
Sprint 18 keeps its four tasks. macOS installers on the CDN, automated publish, the updater smoke test across a version bump, and the optional custom domain all wait — now on a Codemagic run rather than an Actions one, and on a signing variable holding the right file.
Principle
A blocker outside the repository is still a fact about the project. Record it where the plan lives, keep the sprint open, and spend the window on work that does not depend on it.