# Robin Saleh-Jan — Full Content > All published articles from robinsalehjan.com in full text. ## Compound engineering for the implementation phase URL: https://robinsalehjan.com/blog/compound-engineering-for-the-implementation-phase Date: 2026-05-28 Tags: ai, tooling, context-engineering ## Compaction eats your decisions A coding agent in implementation mode burns tokens fast. Reading files, capturing build output, dumping `git status`. A 1M context window sounds like plenty until a single `grep` across a monorepo lands half the codebase in the transcript. When the context window fills, the session compacts. Compaction is lossy. A decision made fifty turns ago comes back as a paragraph summary, and the constraint that mattered is gone. The next turn rewrites code you already agreed not to touch. The fix is not a bigger context window. The fix is loading less into the one you have. ## Composing the root `CLAUDE.md` with `@import` references Claude Code reads a user-level config from `~/.claude/CLAUDE.md`. The `~/.claude/` directory is hidden by default on macOS and Linux, but it is the file every session loads first, before any project-specific config. Call it the root Claude file: whatever it says applies to every project, every agent, every turn. Keeping that file thin is the point. The pattern Claude Code uses, `@filename.md`, inlines the referenced file at session start. So the root file stays a small index and each tool gets its own focused page: ```markdown ## Subagent Model Selection When spawning subagents via the Agent tool, use `model: "sonnet"` for routine tasks like: - Codebase exploration and search - File reading and summarization - Running tests or builds - Simple code generation or edits Reserve the default (Opus) for tasks requiring deep reasoning, complex architecture decisions, or multi-step problem solving. ## References @CRG.md @RTK.md @PLANNOTATOR.md @CLAUDE-TOKEN-EFFICIENT.md @WORKTRUNK.md ``` The whole file is 17 lines. Each `@import` resolves to a self-contained module: - `CRG.md` tells the agent when to reach for [`code-review-graph`](https://code-review-graph.com) for structural and graph analysis of the codebase - `RTK.md` documents the wrapper meta-commands like [`rtk gain` and `rtk discover`](https://www.rtk-ai.app) - `PLANNOTATOR.md` lists the slash commands for the [visual plan review tool](/blog/compound-engineering-for-the-plan-and-review-loop) - `WORKTRUNK.md` covers `wt` worktree commands and the [post-merge cleanup rule](/blog/compound-engineering-for-the-plan-and-review-loop) - `CLAUDE-TOKEN-EFFICIENT.md` [trims the agent's own replies](https://github.com/drona23/claude-token-efficient) so each turn costs less to feed back into the next Splitting the config this way pays off for three reasons. Authoring is local: tweaking the RTK rules touches one 18-line file, not a 500-line monolith. Reuse is easy: the same `RTK.md` ships across machines and into project-level `CLAUDE.md` files via `@~/.claude/RTK.md`. And the agent gets a clean mental model of what each module is for, because each file is named after the tool it describes rather than the agent behaviour it shapes. ## Trimming the agent's own output with `CLAUDE-TOKEN-EFFICIENT.md` The agent's own output is context too. Every "Let me check..." preamble, every restated user instruction, every paragraph of closing fluff feeds back into the next turn. A one-page global instruction trains it out: ```markdown # Token Efficient - Think before acting. Read existing files before writing code. - Be concise in output but thorough in reasoning. - Prefer editing over rewriting whole files. - Do not re-read files you have already read unless the file may have changed. - No sycophantic openers or closing fluff. - Go straight to the point. Lead with the answer or action, not the reasoning. - Do not restate what the user said. Just do it. - If you can say it in one sentence, don't use three. ``` A 30% shorter assistant turn keeps the next user turn 30% cheaper to process, and the saving compounds across a long session. ## Structural code search with `code-review-graph` A `grep` for a function name returns every occurrence: the definition, every call site, every comment that mentions the name. The agent reads all of them and decides relevance after the fact, billed per token. [`code-review-graph`](https://code-review-graph.com) (CLI `code-review-graph`, MCP server) builds a local knowledge graph of the codebase. Symbols are nodes. Calls, imports and tests are edges. The agent queries the graph instead of the filesystem: ```bash query_graph callers_of "parsePost" # who calls this? query_graph tests_for "parsePost" # which tests cover it? get_impact_radius "src/utils/slugify" # blast radius of a change semantic_search_nodes "rate limit" # find code by concept ``` The response is structural. A list of nodes with locations, not the source lines themselves. The agent reads files only after the graph tells it which ones matter. A typical "where is X used?" turn drops from a fifty-file grep dump to a dozen-node response. Registration is the friction point. A graph the agent doesn't know about gets ignored. `crg-here` is a one-shot zsh function that registers the current repo and builds the graph in one step, idempotent so it is safe to run on every clone: ```bash crg-here() { local repo repo=$(git rev-parse --show-toplevel) || return 1 code-review-graph register "$repo" --alias "$(basename "$repo")" code-review-graph build --repo "$repo" } ``` Registration writes to `~/.code-review-graph/registry.json`, which a launchd watcher picks up to start indexing in the background. The MCP server is wired per-project in `.mcp.json` so the agent gets the graph tools the moment it opens the repo: ```json { "mcpServers": { "code-review-graph": { "command": "uvx", "args": ["code-review-graph@2.3.3", "serve"], "type": "stdio" } } } ``` Keeping the graph fresh is a `PostToolUse` hook on `Edit|Write|Bash`. Every time the agent modifies a file, the graph updates. A `SessionStart` hook prints `code-review-graph status` at the top of every session so a stale graph surfaces immediately: ```json { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write|Bash", "hooks": [ { "type": "command", "command": "code-review-graph update --skip-flows", "timeout": 30 } ] } ], "SessionStart": [ { "matcher": "", "hooks": [ { "type": "command", "command": "code-review-graph status", "timeout": 10 } ] } ] } } ``` ## Filtering command output with `rtk` `git status` in a busy worktree returns hundreds of lines. `npm install` spills deprecation warnings. `cargo build` dumps the dependency graph on every run. Each one lands in context verbatim. [`rtk`](https://www.rtk-ai.app) is a transparent shell wrapper that filters noisy CLI output before it reaches the agent: ```bash rtk git status # collapse untracked, drop cleanup hints rtk gain # show token savings analytics rtk gain --history # per-command breakdown rtk discover # scan history for missed opportunities ``` A `PreToolUse` hook rewrites bare commands automatically. The agent calls `git status`, and `rtk` filters under the hood without any explicit instruction: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "~/.claude/hooks/rtk-rewrite.sh" } ] } ] } } ``` The hook is intentionally thin. It reads the agent's `Bash` tool input, hands the command to `rtk rewrite`, and acts on the exit code: | Exit | Meaning | Hook action | |------|---------|-------------| | 0 | Rewrite found, no permission rule | Return rewritten command, `allow` | | 1 | No RTK equivalent | Pass through unchanged | | 2 | Deny rule matched | Pass through, let Claude Code deny | | 3 | Ask rule matched | Return rewritten command, no auto-allow | All the actual filtering rules live in the `rtk` Rust binary, so the hook never has to change when a new wrapper is added. A cached version check at the top short-circuits the rest of the hook for old `rtk` versions instead of failing silently. Run `rtk gain` yourself to see the savings. On a working day it reports five-figure token totals, all context the agent never had to spend deciding which lines were signal. ## What you get back **Sessions that finish what they start.** Compaction is the moment the agent forgets the constraint that mattered ("don't touch this file", "we already rejected that approach"). Pushing it from turn 30 to turn 60 means a feature that used to need two sessions and a hand-off prompt now ships in one. No more re-pasting the spec at turn 25 because the next turn is going to lose it. **Faster turns.** Less context to upload means less time waiting for the model to start streaming. Filtered output means less time scrolling past noise to find the line that decides the next move. A graph query returns in milliseconds and answers a question that a grep would have spent thirty seconds of file I/O on. The session feels responsive instead of grinding. **Bigger problems become tractable.** A monorepo that used to overflow context on the second `grep` now stays addressable for a full debugging session. Refactors that touch ten files no longer require a planning phase to triage which files the agent is allowed to read first. Structural reads cut another order of magnitude off codebase exploration, so the ceiling on what fits in one conversation moves up, and the kind of task you can hand the agent moves with it. **Less babysitting.** The hook catches the noisy command before it costs context. The instruction file catches the verbose reply before it costs the next turn. Structural reads catch the over-eager grep before it costs the next ten. You stop having to interrupt the agent to course-correct on resource use and start trusting it to manage its own budget. That is the real compounding effect: the work the three pieces do is work you no longer do yourself. ## Two gotchas worth knowing ### CLI over MCP when both exist An MCP server adds JSON-RPC framing, a schema header, and tool descriptions to every call. A CLI command returns plain output. For deterministic operations like git, file reads, or a build, the CLI is cheaper. Reach for MCP when the operation is stateful or the schema earns its bytes (Xcode build, browser automation, a graph database). ### Images over PDFs A PDF of an API doc burns thousands of tokens on layout and font metadata before any text reaches the model. A screenshot of the same page is a few thousand tokens of pixels and renders identically for the agent's purposes. When the source is the rendered page, hand it the image. --- ## Compound engineering for the plan and review loop URL: https://robinsalehjan.com/blog/compound-engineering-for-the-plan-and-review-loop Date: 2026-04-29 Tags: ai, tooling, code-review ## The bottleneck has moved to review A coding agent produces a plan in seconds and a diff in minutes. Reading them takes the rest of the session. Generation is cheap now; deciding what's worth shipping is not. It sharpens when two agents run at once. Claude Code in one terminal, Codex in another, both touching the same repo. Within minutes one is rebasing on top of the other's uncommitted changes. A plan comes back as eighty bullet points to read in a terminal. A large diff lands, and a margin note that should have been raised before the build now costs a re-plan. Three tools target the same loop. Each wraps a primitive that already exists — git worktrees, the agent's own plan output, the brainstorming conversation — and stays out of the judgement itself. Plenty of agentic tooling automates that part with auto-approve and auto-merge. These go the other way. They cut the friction around reading and reviewing so the human pass happens on every change, not the changes you remember to look at. ## Parallel branches with `worktrunk` Two agents in the same checkout collide fast. Git worktrees solve it: one branch per directory, all sharing one object store. [`worktrunk`](https://github.com/max-sixty/worktrunk) (binary `wt`) is a thin Rust CLI that makes the lifecycle ergonomic enough to actually use: ```bash wt switch -c add-search # create branch + worktree, cd into it wt list # show every active worktree wt merge main # merge current into main, auto-clean wt remove # remove worktree, delete branch if merged ``` Vanilla git is `git worktree add ../foo feature/foo && cd ../foo && ...` followed by manual cleanup of the worktree, the branch, and the directory. `wt` collapses each step into one verb, and the shell integration actually changes your working directory. Each Claude Code or Codex session gets its own worktree, with its own installed dependencies, its own dev server port, its own dirty state: ```text $ wt list * main /Users/me/repo add-search /Users/me/repo-add-search (claude-code) fix-tokens /Users/me/repo-fix-tokens (codex) prep-release /Users/me/repo-prep-release ``` When a branch merges, `wt remove` deletes the worktree and the branch in one step. The discipline is to keep `wt list` empty of merged branches. Stale worktrees pile up fast and bring back the collisions the worktrees were meant to prevent. ## Spec-to-plan with `superpowers` A plan is only as good as the spec it came from. A bullet list assembled from a one-line prompt isn't a spec. [`superpowers`](https://github.com/obra/superpowers) ships a methodology rather than a tool. The skills auto-trigger when you start describing a feature — you don't invoke them by name. Four matter for this loop: - `brainstorming` — runs before any creative work. Teases a spec out of the conversation in chunks short enough to read, instead of jumping to code. - `writing-plans` — turns the signed-off spec into an implementation plan structured for TDD, with each step narrow enough that a junior could follow it. - `executing-plans` — runs the plan in a separate session with review checkpoints between steps. - `subagent-driven-development` — fans independent steps out to subagents so the main session keeps its context clean. A `writing-plans` output looks something like this: ```markdown ## Plan: Add full-text search to blog 1. Add `fuse.js` dependency 2. Create `SearchIndex.astro` that builds a JSON index at build time 3. Create `SearchBox.svelte` — input field, debounced query, result list 4. Wire `SearchBox` into the header layout 5. Add test: build succeeds, index contains all non-draft posts ``` Each step is narrow enough to review in isolation and small enough that a wrong step costs one revision, not a re-plan. ## Plan review with `plannotator` A wrong abstraction, a missed edge case, intent read backwards. These show up in the plan. They're cheap to fix only before the 600-line diff lands. [`plannotator`](https://plannotator.ai) renders a Claude Code plan as a local webpage you can annotate in the margin, then ships your feedback back to the agent. It installs as a plugin, so the surface is slash commands: ```bash /plannotator-review # annotate a PR diff /plannotator-annotate # annotate a plan markdown /plannotator-last # annotate the last rendered assistant message /plannotator-archive # browse saved plan decisions ``` A hook picks up plans automatically when the agent enters plan mode — no manual export. Margin comments come back as a follow-up prompt in the same session, so a note like "split this step into a separate PR" reaches the agent without you retyping anything. The archive keeps every annotated plan around, which turns the review pass into something you can revisit when a decision later looks wrong. ## How they compose The output of each tool is the input to the next. You describe a feature. `brainstorming` asks three rounds of questions and produces a signed-off spec. `writing-plans` turns that spec into a five-step plan. `plannotator` opens the plan in a browser; you annotate two steps, and the corrections ship back as a follow-up prompt. The agent revises the plan in the same session. `wt switch -c add-search` creates an isolated worktree. The agent works through the revised plan with checkpoints between steps. A second agent can run on a different branch in the same repo without colliding. When the diff lands, `plannotator` reopens the same annotation surface for the PR — the review pass that started on the plan continues on the code. ## What changes **Specs get written.** The agent doesn't jump to code; it draws out intent first. The plan reflects the spec, not the agent's first guess. **Plans get read.** The cheapest artifact takes the heaviest review pass. The diff arrives smaller, with fewer surprises. **Branches stay isolated.** Two agents can work in parallel without rebasing on top of each other's dirty state. **Judgement stays in the loop.** Auto-approve removes the human from review entirely. These tools cut the friction around review so it actually happens. --- ## Practical use of the inout parameter in Swift URL: https://robinsalehjan.com/blog/practical-use-of-the-inout-parameter-in-swift Date: 2026-04-10 Tags: ios ## `inout` who? The `inout` keyword enables pass-by-reference semantics for value types. Mark a parameter `inout` and the function receives a reference to the original, not a copy. The `&` at the call site makes mutation explicit: ```swift var request = URLRequest(url: url) modifyHTTPHeadersIfNeeded(&request) // `&` signals that request may be modified ``` ## Custom HTTP headers in a WKWebView request When you need to inject custom HTTP headers into `WKWebView` navigation requests, you run into a constraint: the `WKNavigationDelegate` method `webView(_:decidePolicyFor:)` lets you intercept navigations but not modify the underlying request directly. The `inout` pattern lets you build and apply those mutations before the load happens. ### 1. Load ```swift public func load(_ request: URLRequest) { var newRequest = request modifyHTTPHeadersIfNeeded(&newRequest) webView.load(newRequest) } ``` `URLRequest` arrives as an immutable value. Assigning it to a `var` creates a mutable copy; `&` lets `modifyHTTPHeadersIfNeeded` write back through it. ### 2. Modify ```swift private func modifyHTTPHeadersIfNeeded(_ request: inout URLRequest) { if isCustomHTTPHeadersAdded(request) { return } var headers = request.allHTTPHeaderFields ?? [:] if let providedHeaders = delegate?.provideHeaders() { for (key, value) in providedHeaders { headers[key] = value } } addRequiredHTTPHeaders(&headers) request.allHTTPHeaderFields = headers } ``` The early return on `isCustomHTTPHeadersAdded` is the guard that prevents infinite navigation loops. More on that below. ### 3. Intercept ```swift public func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction ) async -> WKNavigationActionPolicy { guard let url = navigationAction.request.url, isDomainTrusted(url) else { return .cancel } var request = navigationAction.request if isCustomHTTPHeadersAdded(request) { return .allow } else { var newRequest = navigationAction.request modifyHTTPHeadersIfNeeded(&newRequest) webView.load(newRequest) return .cancel } } ``` When a navigation fires, check whether custom headers have already been added. If not, cancel the navigation, inject the headers, and reload. The reloaded request passes the check, so the second pass through the delegate returns `.allow`. ## Avoiding infinite loops `isCustomHTTPHeadersAdded` is what makes the cancel-and-reload pattern safe: ```swift private func isCustomHTTPHeadersAdded(_ request: URLRequest) -> Bool { guard let headers = request.allHTTPHeaderFields else { return false } let customHeaderKey = RequiredHeaders.customAdded return headers[customHeaderKey.headerName] == customHeaderKey.headerValue } ``` Notice this one takes `URLRequest` by value, not `inout` — it only reads headers, never mutates. The `&` isn't needed when there's nothing to write back. Without this guard, every navigation would be cancelled, modified, and reloaded indefinitely. ## Benefits of `inout` **In-place mutation.** The compiler can optimize `inout` parameters to modify the value in place rather than copying it in and out. More importantly, `inout` lets you mutate a struct and have the caller see the result without returning a new value. **Explicit mutations.** The `&` at every call site is a contract: this function may change what you pass in. Readers don't need to inspect the signature to know mutation is happening. **Composability.** Each function owns one step; `inout` threads the request through the chain. --- ## URLComponents quietly decodes what you carefully encoded URL: https://robinsalehjan.com/blog/urlcomponents-quietly-decodes-what-you-carefully-encoded Date: 2026-04-10 Tags: ios ## The gotcha `URLComponents` handles percent encoding for you, most of the time. The trap is that it behaves differently depending on how you touch it. Initialize from a URL string and it preserves the exact encoding you passed in. Modify the query through `queryItems` and it re-encodes everything according to RFC 3986, throwing away any "voluntary" percent encoding: characters like `%3A` (`:`) and `%2F` (`/`) that don't *need* to be encoded per the spec. The escape hatch is `percentEncodedQueryItems`. It reads and writes raw percent-encoded strings without touching them. ## A real example You'll hit this whenever you pass a `URL` as a query parameter, like in OAuth flows, deep links, or analytics tracking: ```swift let originalURL = URL(string: "https://api.example.com/redirect?callback=https%3A%2F%2Fapp.example.com%2Fauth%3Ftoken%3Dabc123")! var components = URLComponents(url: originalURL, resolvingAgainstBaseURL: false)! print(components.queryItems!.first!.value) // https://app.example.com/auth?token=abc123 components.queryItems?.append(URLQueryItem(name: "state", value: "xyz")) print(components.url!.absoluteString) // https://api.example.com/redirect?callback=https://app.example.com/auth?token%3Dabc123&state=xyz ``` The callback URL's encoding is gone. Colons, slashes, and the nested `?` are technically allowed in query values by RFC 3986, so `URLComponents` decoded them, leaving you with a URL that's spec-compliant but broken in practice. WebKit may parse the unescaped `?` as a second query delimiter, fail to extract the callback value, or reject the URL outright. ## The fix Use `percentEncodedQueryItems` to preserve existing encoding. The tradeoff: you encode new values yourself. ```swift let originalURL = URL(string: "https://api.example.com/redirect?callback=https%3A%2F%2Fapp.example.com%2Fauth%3Ftoken%3Dabc123")! var components = URLComponents(url: originalURL, resolvingAgainstBaseURL: false)! var items = components.percentEncodedQueryItems ?? [] let encodedValue = "xyz".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! items.append(URLQueryItem(name: "state", value: encodedValue)) components.percentEncodedQueryItems = items print(components.url!.absoluteString) // https://api.example.com/redirect?callback=https%3A%2F%2Fapp.example.com%2Fauth%3Ftoken%3Dabc123&state=xyz ``` The original encoding survives because nothing in the path touched it. Your new parameter rides alongside, encoded once. ## When to use what **`queryItems`.** When you control both sides and standard URI encoding is fine. **`percentEncodedQueryItems`.** When you're embedding nested URLs in query parameters, or handing the result to something like WebKit that expects stricter encoding. A small extension keeps repeated usage tidy: ```swift extension URLComponents { mutating func appendPercentEncodedQueryItem(name: String, value: String) { var items = percentEncodedQueryItems ?? [] let encodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! items.append(URLQueryItem(name: encodedName, value: encodedValue)) percentEncodedQueryItems = items } } ```