# Ouail Bni (ProxySoul) — full blog content Site: https://proxysoul.com Index: https://proxysoul.com/llms.txt --- # Agent Harness Engineering: The Discipline That Decides How Good Your AI Agent Is URL: https://proxysoul.com/blog/agent-harness-engineering Date: 2026-07-17T00:00:00.000Z Author: Ouail Bni Description: Agent harness engineering is the practice of designing everything around the LLM — tools, context, caching, routing, evaluation. It's why the same model can be brilliant in one product and useless in another. A field guide from building Empryo and SoulForge. The model is not the product. The harness is. **Agent harness engineering** is the discipline of designing everything *around* a large language model that turns it into a working agent: the tools it can call, the context it sees each turn, the caching that makes repeat turns affordable, the routing that picks which model handles which job, and the evaluation loop that tells you whether any of it actually works. The model supplies raw intelligence; the harness decides how much of that intelligence reaches your task — and at what price. I've spent the last year building two coding agents — [SoulForge](https://github.com/proxysoul/soulforge) (840+ stars on GitHub) and its successor [Empryo](https://empryo.com) — and the single most important thing I learned is this: **most of an agent's quality is decided before the model generates a single token.** --- ## Why the harness beats the model Everyone has access to the same frontier models. Claude, GPT, Gemini — they're an API key away from every competitor you have. So when two agents use the same model and one fixes the bug while the other burns $18 of tokens and gives up, the model didn't make the difference. The harness did. Here's the sharpest example I have, from benchmarking Empryo. When I gave a *small* model (Claude Haiku) a ranked structural map of the codebase in context — Empryo's "genome" — it fixed bugs that otherwise required a frontier model, at roughly **one fifth of the cost**. Same task, same repo, same prompts. The only variable was what the harness put in front of the model. That result generalizes into a rule I now design by: > Intelligence you rent per token. Understanding you index once. Every token the model spends rediscovering your codebase's structure — grepping, reading files, guessing where things live — is rented intelligence doing a job the harness should have done once, for free. ## The six components of an agent harness Every serious agent product I've studied or built iterates on the same six loops. ### 1. Tool design Tools are the agent's hands, and more hands are not better. Each tool you add grows the decision space, the prompt, and the failure modes. The craft is in *sharpness*: a tool that edits code by **symbol** (a function, a class) through the syntax tree beats a tool that pastes strings, because it can't corrupt whitespace or match the wrong occurrence. In Empryo, moving from string edits to AST edits meant the 60th edit in a session was as reliable as the first. ### 2. Context engineering The model only knows what's in the window. What goes in — and in what order — is the highest-leverage decision in the whole system. The pattern that worked for me: front-load *stable, structural* knowledge (a ranked map of the repo: files, symbols, dependency edges), so the agent orients instantly instead of spending its first ten tool calls exploring. Search tells you where a string appears; structure tells you what the code *is*. ### 3. Compaction Long sessions die one of two deaths: the context overflows, or the summary that replaces it loses the thread. Compaction is the art of throwing away the right things — keeping decisions, discarding transcripts of tool output, re-deriving what can be re-derived. Done well, an agent stays coherent across hours of work. Done badly, it re-reads the same file eleven times. ### 4. Prompt caching Agents resend nearly identical prompts dozens of times per task. Providers will serve the repeated prefix from cache at a fraction of the price — *if* your harness keeps that prefix byte-stable. One late-arriving injection at the top of the prompt invalidates everything after it. Caching discipline routinely cuts real bills by half or more, which is why it's an architectural concern, not an optimization pass. ### 5. Model routing Not every step needs the frontier model. Read-only exploration, summarization, and classification run fine on small models; the big model should be saved for the edits that matter. Empryo routes per role — cheap models for scouting, strong models for writing — across 22 providers, including free local ones. Routing is also your safety net: when a provider fails mid-stream, the harness should ride a fallback chain without losing the turn. ### 6. Evaluation The loop that makes the other five improvable. Benchmarks of *real* tasks — actual merged bug fixes, not synthetic toys — with the provider's billing console as the source of truth for cost. When I audited agents' self-reported spend against the actual bill, one competitor's numbers were off by up to 32%; an agent's cost display should be a bill, not a vibe. If you don't measure against ground truth, your harness improvements are folklore. ## How to start 1. **Instrument first.** Log tokens, cost, steps, and outcomes per task. Compare self-reported cost against the provider console once — you may be surprised. 2. **Cut tools before adding them.** Merge overlapping tools; make the survivors sharper and harder to misuse. 3. **Stabilize your prompt prefix.** Put static context first, volatile context last, and watch your cache hit rate. 4. **Index what the agent keeps rediscovering.** For code, that's the dependency graph. For other domains, it's whatever structure the agent greps for every session. 5. **Build a ten-task benchmark from your real workload.** Run it after every harness change. Green rows you can't reproduce are ads, not evidence. ## Where this discipline is heading The term is young, but the job is already real: every team shipping agents in 2026 has someone doing harness engineering, whether they call it that or not. The models will keep getting better on their own schedule. The harness is the part *you* control — and in my experience, it's where products win or lose. If you want to see one harness in full, [Empryo](https://empryo.com) is free to try: a live code genome, AST-first editing, per-role model routing, checkpointed time travel, and benchmarks published with the receipts. Its story — and the open-source experiment it grew from — starts with [SoulForge](/blog/soulforge). --- ## Frequently asked questions ### What is agent harness engineering? The discipline of building everything around an LLM that turns it into a working agent: tool belt, context, compaction, caching, routing, and evaluation. The model provides intelligence; the harness decides how much of it reaches your task. ### Is harness engineering the same as prompt engineering? No. Prompt engineering tunes the words in a single request. Harness engineering designs the *system* that assembles every request: which tools exist, what context gets injected, what stays cached, which model runs. Prompts are one component; the harness is the machine. ### Why does the harness matter more than the model? Everyone rents the same models. Differentiation lives in what you build around them — the harness is why a small model with a ranked code graph outperformed a frontier model at a fifth of the cost in my benchmarks. ### How do I get into harness engineering? Build one. Take a real, repetitive task from your own work, wire an LLM to three sharp tools, measure cost and outcomes honestly, and iterate on the six loops above. The field is young enough that a well-instrumented side project puts you at the frontier. --- # OpenCode vs Claude Code vs SoulForge: a real comparison URL: https://proxysoul.com/blog/opencode-vs-claude-code-vs-soulforge Date: 2026-05-22T00:00:00.000Z Author: Ouail Bni Description: Three terminal coding agents, three philosophies. Provider freedom, vertical Anthropic stack, or graph-powered code intelligence. Honest matrix, source-verified facts, zero marketing fog. ![SoulForge in action](/screenshots/soulforge-1.png) Morph published a [head-to-head between OpenCode and Claude Code](https://www.morphllm.com/comparisons/opencode-vs-claude-code). Good piece. Numbers, prompt extracts, the January 2026 OAuth block. Two tools, two philosophies. It left a third one out. This is the three-way. Every SoulForge claim below is verified against the source tree (paths cited inline). The other two columns come from Morph's piece and the public docs of each tool. --- ## Table of contents 1. [The verdict](#the-verdict) 2. [Legend](#legend) 3. [The 30-second version](#the-30-second-version) 4. [Foundations](#foundations) 5. [How the agent edits your file](#how-the-agent-edits-your-file) 6. [Codebase intelligence](#codebase-intelligence) 7. [Agent architecture](#agent-architecture) 8. [Context and compaction](#context-and-compaction) 9. [Permissions and safety](#permissions-and-safety) 10. [MCP](#mcp) 11. [Remote and mobile](#remote-and-mobile) 12. [Headless](#headless) 13. [Themes](#themes) 14. [Memory across sessions](#memory-across-sessions) 15. [Pricing](#pricing) 16. [Bench numbers](#bench-numbers) 17. [When to pick which](#when-to-pick-which) --- ## The verdict Who wins between Claude Code and OpenCode? Look at the numbers Morph published. Claude Code finished every task in under half the time. Cross-file refactor 2m 15s vs 4m 20s. Bug fix 1m 45s vs 3m 10s. Total 9m 9s vs 16m 20s. OpenCode also reformatted code it shouldn't have on every model tested. On raw speed and harness reliability, **Claude Code wins** that fight. OpenCode wins on cost, provider choice, and remote control. Different axes, different answers. Here's the punchline though. On the SoulForge bench, with Claude Opus 4.6 driving both tools on the same repo with the same prompt, SoulForge finished a bug fix in **6m 22s for $1.70**. OpenCode took 11m 18s and $3.52. The audit task was sharper: **2m 00s, $0.84, 7-out-of-7 correct findings, zero false alarms**. OpenCode: 5m 56s, $2.61, 4-out-of-7, three false alarms, one wrong claim. Same model. Same code. Same prompt. **Half the time. Half the cost. Better accuracy.** The gap isn't model magic, it's harness design. AST editing instead of string replace. A live codebase graph instead of grep. Symbol-level reads instead of file dumps. Zero-LLM compaction instead of summarization round-trips. Per-task model routing so Haiku does exploration while Sonnet writes code. You don't pick the agent. You pick the harness that lets the agent do less work. ![Same model, half the cost](/screenshots/soulforge-2.png) --- ## Legend | Symbol | Meaning | |---|---| | ✓ | Shipped, on by default | | ◐ | Shipped, behind a flag or partial | | ✗ | Not available | | n | Numeric value from source | | `path:line` | Verifiable in the SoulForge repo | --- ## The 30-second version **Claude Code** wins if you want Anthropic's vertical stack with `/goal` autonomy and Agent View fleet management. Tight, polished, subscription-locked. **OpenCode** wins if you want provider freedom across 75+ models, the Tauri desktop app, Scout for external docs research. MIT, open ecosystem. **SoulForge** wins if you want the agent to understand your codebase as a graph before it touches anything. AST editing, live PageRank repo map, per-task model routing, zero-LLM compaction. BSL, BYOK. Same model as the others, finished in roughly half the time and half the cost on our bench. --- ## Foundations | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | License | MIT | Proprietary | BSL 1.1 → Apache 2.0 (2030) | | Runtime | Node.js | Node.js | **Bun** | | UI | TUI + Tauri desktop | TUI + VS Code ext | TUI (OpenTUI) + embedded Neovim | | GitHub stars (May 2026) | 161K | 124K | growing | | Provider count | 75+ | Anthropic only | 21 built-in + custom | | Local models | ✓ Ollama | ✗ | ✓ Ollama, LM Studio | | Subscription required | optional | $20+/mo | none, BYOK | SoulForge's 21 built-in providers verified at `src/core/llm/providers/index.ts:49`: Anthropic, OpenAI, Google, xAI, Groq, DeepSeek, Mistral, Bedrock, Fireworks, MiniMax, Codex, Copilot, GitHub Models, OpenRouter, OpenCode Zen, OpenCode Go, LLM Gateway, Vercel AI Gateway, Proxy, Ollama, LM Studio. Plus any OpenAI-compatible endpoint via `providers[]` array. --- ## How the agent edits your file This is the single biggest difference. | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Primary edit tool | `edit` (string replace) | `Edit` (string replace) | **`ast_edit`** (ts-morph) for TS/JS, line-anchored `edit_file` / `multi_edit` for the rest | | Symbol-level ops | via MCP | via MCP | native: `rename_symbol`, `move_symbol`, `refactor` | | LSP integration | ✓ default | ✓ since v2.1.121 | ✓ default, dual backend (Neovim + standalone) | | LSP server installer | via Neovim | via Neovim | **Mason inside the TUI**, 576+ packages | | Atomic multi-op | ✗ | ✗ | ✓ all-or-nothing rollback | | Pre/post-edit diagnostics fed to agent | ✗ | partial | ✓ | OpenCode and Claude Code both treat code as text. Match `old_string`, replace with `new_string`. When whitespace drifts, the match fails. Morph's own data: 35% of string-match edits fail on the first try, 70%+ on files with `formatOnSave`. SoulForge picks `ast_edit` for `.ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs` automatically. It walks the TypeScript compiler API, mutates the node, serializes back. Whitespace drift can't fail something the tool doesn't read as text. Want to make a function async, change its return type, add a parameter, and import the new type? One tool call, four operations, atomic: ```ts ast_edit({ path: "src/api.ts", operations: [ { action: "set_async", target: "function", name: "fetchUser", value: "true" }, { action: "set_return_type", target: "function", name: "fetchUser", value: "Promise" }, { action: "add_parameter", target: "function", name: "fetchUser", value: "cache: boolean" }, { action: "add_named_import", value: "./types", newCode: "User" }, ], }) ``` If `User` doesn't exist in `./types`, none of the four apply. No half-refactored file. --- ## Codebase intelligence | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Live repo graph | ✗ | ✗ | **Soul Map** — SQLite, `src/core/intelligence/repo-map.ts` | | Ranking | n/a | n/a | PageRank over import graph | | Git-aware | ✗ | ✗ | co-change weighting | | Blast radius per file | ✗ | ✗ | ✓ | | Symbol-level reads | ✗ | ✗ | ✓ across 33 languages | | External docs research | ◐ via WebFetch | ◐ via WebFetch | ◐ via web_search + soul_grep dep=npm-package | | Scout-style dep cloning | ✓ Scout subagent | ✗ | ✗ | SoulForge starts every session by parsing your codebase with tree-sitter, building a SQLite graph, and ranking files by PageRank over the import graph plus git co-change. The agent receives a ranked digest in its system prompt before the first turn. Neither OpenCode nor Claude Code has anything equivalent. Both grep when they need to find something. SoulForge greps too, but only after the map fails. OpenCode's **Scout** is the one place it leads on intelligence: it clones dependency repos into cache and inspects library source. SoulForge approaches the same problem with `soul_grep dep="react"` which searches inside `node_modules` directly, but Scout's full-clone model is more thorough. --- ## Agent architecture | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Built-in subagents | General, Explore, Scout | Plan, Explore, Task | Spark (read-only), Ember (code), WebSearch | | User-defined agents | ✓ `.opencode/agents/*.md` | ✓ `.claude/agents/` + marketplace | per-task router slots | | Fleet dashboard | ✗ | ✓ Agent View | per-tab dispatch view | | Autonomous goal completion | ✗ | ✓ `/goal` with validator model | `auto` mode (no validator) | | Background subagents | ✓ | ✓ `--bg` | dispatch only | | Cross-tab file claims | ✗ | ✗ | ✓ 5 tabs, advisory warnings | | Per-task model routing | one per agent | one per session | **8 task slots, different model each** | The router slots in SoulForge (`src/core/agents/agent-runner.ts:33`): `spark`, `ember`, `webSearch`, `desloppify`, `verify`, `compact`, `semantic`, `default`. Default concurrency: 3, max 8. You can run Haiku on explore, Sonnet on code, Flash on compaction, all in one session. Cheap work to cheap models, automatically. --- ## Context and compaction | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Auto-compaction | ✓ v1.14+ | ✓ built-in | ✓ default | | Strategy | LLM summary | LLM summary | **V2 structural extraction, usually zero LLM tokens** | | Session persistence | server-side via Hono | background sessions + `--bg` | JSONL auto-save, crash-resilient | | Checkpoints | session pinning | Esc×2 instant rewind | **every prompt is a git-tagged checkpoint, per-tab, branchable** | | Context window | provider-dependent | 1M (Opus 4.7) | provider-dependent | V2 compaction in SoulForge (`src/core/compaction/working-state.ts`) tracks structured state as the conversation runs: files touched, decisions, failures, tool results. When the context fills, that state is already built. No LLM round-trip. Old tool results prune to one-liners enriched with Soul Map symbols. --- ## Permissions and safety | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Default posture | granular glob rules | ask by default | approval gates on destructive ops | | `rm -rf` detection | regex rule | heuristic | hardcoded denylist | | Forbidden files | config | config | built-in blocks: `.env`, `.pem`, `credentials`, `id_rsa`, `.npmrc`, `.netrc`, `shadow`, `passwd` | | Pre-commit gate | ✗ | ✗ | ✓ runs lint + typecheck before any commit | | Hook system | per-agent frontmatter | plugin hooks | **13 events, wire-compatible with Claude Code** | SoulForge's hook events (`src/core/hooks/types.ts:12`): `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `UserPromptSubmit`, `Stop`, `StopFailure`, `SessionStart`, `SessionEnd`, `PreCompact`, `PostCompact`, `SubagentStart`, `SubagentStop`, `Notification`. Reads from five config sources merged in order (`src/core/hooks/loader.ts:21`): `~/.claude/settings.json`, `.claude/settings.json`, `.claude/settings.local.json`, `~/.soulforge/config.json`, `.soulforge/config.json`. Your existing Claude Code hooks work without modification. --- ## MCP | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Transports | stdio, HTTP | stdio, HTTP | stdio, streamable HTTP, SSE | | Loading | declarative per-agent | eager (Tool Search lazy) | per-server config | | Tool namespace | `mymcp_*` | `mcp__server__tool` | `mcp__server__tool` | | Auto-restart on crash | ✓ | ✓ | ✓ stdio only | | Bounded concurrency at startup | ✗ | ✗ | ✓ max 5 simultaneous, retry 3× exponential | Verified at `src/core/mcp/manager.ts:223`. SoulForge supports the legacy SSE transport for older remote servers, which the other two have dropped. --- ## Remote and mobile | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Remote control | ✓ HTTP API | ✗ | ✓ **Hearth** (experimental) | | Mobile | via API | ✗ | Telegram + Discord | | Code leaves your host | optional | no | no, ever | | Approval prompts | API | n/a | inline buttons in chat | | Secret redaction in logs | ✗ | ✗ | ✓ Telegram + Discord + JWT + PEM + AWS + GitHub + Stripe + Slack + Google + bearer + DB URL + basic auth | Hearth runs as a daemon over a UNIX socket (mode 0600). Tokens stored in OS keychain, never in config. Destructive tool calls arrive as tap-to-approve buttons. Identity allowlist; unknown senders dropped silently. Path containment forces every daemon-managed file to live inside `~/.soulforge`. Service install via launchd (macOS) or systemd (Linux). --- ## Headless | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Headless mode | ✓ | ✓ `-p` | ✓ first-class | | Event stream | partial | partial | **typed JSONL union, 14 event types** | | Pipe from stdin | ✓ | ✓ | ✓ | | Resume by short prefix | ✗ | ✗ | ✓ `--session abc` finds `abc123...` | | Pre-load files | ✗ | ✗ | ✓ `--include` (repeatable) | | Daemon-embeddable | ✗ | ✗ | ✓ via Hearth seam | ```bash soulforge --headless "fix the auth bug" soulforge --headless --events "refactor store" | jq -r 'select(.type=="tool-call").tool' soulforge --headless --chat --session abc # resume by prefix echo "list TODOs" | soulforge --headless # pipe from stdin ``` Exit codes: 0 success, 1 error, 2 timeout, 130 abort. SIGINT re-raised so parent shells see a true signal death. --- ## Themes | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Built-in themes | a few | 1 | **36** | | Custom themes | ✗ | ✗ | JSON files, hot-reload on save | | Transparent backgrounds | ✗ | ✗ | ✓ with per-element opacity | | Kitty inline images | ✗ | ✗ | ✓ | Counted directly from `src/core/theme/tokens.ts:1879`. The list includes Catppuccin (Mocha, Frappé, Macchiato, Latte), Dracula, Gruvbox, Tokyo Night (+ Storm), Nord, Rose Pine, Kanagawa, Nightfox, Cyberdream, Oxocarbon, Sonokai, Moonfly, Melange, Solarized (Dark + Osaka), Bamboo, Nordic, Synthwave, Iceberg, Ember, Vesper, GitHub (Dark + Light), Everforest, Ayu, One Dark + Light, and three proxysoul themes. --- ## Memory across sessions | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Cross-session memory | via files | CLAUDE.md + Skills | **SQLite DB, project + global scopes** | | Auto-recall per turn | ✗ | ✗ | ✓ top-3 stubs from prompt + edited files | | Inline hint footers | ✗ | ✗ | ✓ pinned, pref, gotcha, decision | | Soft-delete (restorable) | n/a | n/a | ✓ forever | | Embeddings | n/a | n/a | offline default, optional provider embedder | When the agent learns something worth remembering ("we use bun, not npm", "JWT clock drift breaks prod auth"), it writes a memory. Next session, the relevant entries surface automatically from the prompt and the files you're editing. Two-DB layout: project-scoped per repo, global for cross-project preferences. --- ## Pricing | | OpenCode | Claude Code | SoulForge | |---|---|---|---| | Tool cost | free, MIT | free, proprietary | free, BSL personal/internal | | Minimum to start | $0 (Ollama) | $20/mo Pro | $0 (Ollama / LM Studio) | | Cheapest cloud tier | Go $10/mo (open-weight) | Pro $20/mo | BYOK, your account | | Premium tier | Black $200/mo (sold out) | Max20x $200/mo | BYOK | | Commercial use | permissive | per Anthropic ToS | commercial license for resale | --- ## Bench numbers Morph's own benchmark, Claude Opus 4.7, OpenCode vs Claude Code: | Task | OpenCode | Claude Code | |---|---|---| | Cross-file refactor | ✓ 4m 20s | ✓ 2m 15s | | Bug fix from error | ✓ 3m 10s | ✓ 1m 45s | | Test generation | 94 tests, 8m 50s | 73 tests, 5m 9s | | Reformatting bugs | yes (all 3 models) | no | SoulForge bench, Claude Opus 4.6, same repo, SoulForge vs OpenCode: | Bug fix | SoulForge | OpenCode | |---|---|---| | Time | **6m 22s** | 11m 18s | | Cost | **$1.70** | $3.52 | | Audit task | SoulForge | OpenCode | |---|---|---| | Time | **2m 00s** | 5m 56s | | Cost | **$0.84** | $2.61 | | Accuracy | **7/7** | 4/7 | | False alarms | 0 | 3 | Different harness, different model, not directly comparable to Morph's run. The cost gap is structural though, coming from V2 compaction and symbol-level reads instead of file dumps. --- ## When to pick which | Your priority | Pick | |---|---| | Anthropic vertical stack, fire-and-forget autonomy | **Claude Code** | | Provider freedom, open-weight models, desktop app, $10/mo tier | **OpenCode** | | Symbol-aware editing, codebase graph, per-task routing, multi-tab workflow, Neovim embed, headless CI, BYOK across 21 providers | **SoulForge** | The three tools aren't really competing for the same thing. Claude Code optimizes for autonomous Anthropic-stack work. OpenCode optimizes for provider freedom and community surface area. SoulForge optimizes for what the agent *understands* before it acts. If you've ever watched a string-matching agent burn three retries on whitespace, give SoulForge a try. ```bash brew tap proxysoul/tap && brew install soulforge soulforge --set-key llmgateway sk-... # or any of 20 other providers soulforge ``` --- ## Sources - [Morph: OpenCode vs Claude Code (2026)](https://www.morphllm.com/comparisons/opencode-vs-claude-code) - [SoulForge docs](https://soulforge.proxysoul.com/) · [AST editing](https://soulforge.proxysoul.com/tools/ast-edit) · [Soul Map](https://soulforge.proxysoul.com/concepts/repo-map) · [Hearth](https://soulforge.proxysoul.com/tools/hearth) - [SoulForge on GitHub](https://github.com/proxysoul/soulforge) - [OpenCode docs](https://opencode.ai/docs/) · [Claude Code](https://claude.com/claude-code) \- ProxySoul --- # I built an AST editor for AI because string matching is killing your codebase URL: https://proxysoul.com/blog/ast-editor-for-ai Date: 2026-05-09T00:00:00.000Z Author: Ouail Bni Description: Every AI coding tool in 2026 still edits your files with str_replace. I rebuilt the editing primitive on the TypeScript compiler API. Here's what changes when the agent treats code as structure, not text. It's 2 AM. I'm watching [Claude Code](https://claude.com/claude-code) spiral on a one-line refactor. It's trying to add a prop to a React component. The `str_replace` keeps failing because the JSX has line wrapping the agent didn't account for. Three retries. The fourth attempt rewrites the whole 200-line file, breaks two unrelated `useEffect` blocks, and helpfully reformats my imports. I close the laptop. ![Cover: string-matching chaos vs AST clarity](/screenshots/ast-edit/cover.png) The next morning I open my master's thesis from 2022. I co-wrote it at Malmö University with Artur Matusiak: *Typed vs Untyped Programming Languages*. We built a tool that migrated JavaScript codebases to TypeScript by walking the syntax tree and mutating nodes directly. No regex. No retries on whitespace, because there was no whitespace to match. That night I wrote down a question. > *Why is every AI coding tool in 2026 still editing my code with `str_replace`?* That question became [SoulForge](https://soulforge.proxysoul.com/). --- ## The dance you know If you've used Claude Code, [OpenCode](https://opencode.ai/), [Cursor](https://cursor.com/), or [Aider](https://aider.chat/), you know it. The agent reads a 600-line component. Generates an edit. Fails because the JSX prop sits on a different line than the agent assumed. Re-reads. Tries again. Drops a closing brace. Third try succeeds, but it stripped a comment. You lost ten thousand tokens to add `disabled={loading}` to a button. [Morph published the receipts](https://www.morphllm.com/common-errors/error-editing-file): > **35%** of AI edit attempts fail in string-matching tools. > **2.3** attempts per successful edit. > **45%** failure rate on files over 500 lines. > **70%+** failure rate when `formatOnSave` is on. The cause is the same in every tool. --- ## Four flavors of the same bug **Claude Code uses `str_replace`.** It searches for an exact string and substitutes another. Two spaces drift, match fails. **OpenCode uses `edit`.** Per [their own docs](https://opencode.ai/docs/tools/): > *Modify existing files using exact string replacements. This tool performs precise edits to files by replacing exact text matches. It's the primary way the LLM modifies code.* Same failure mode. They ship an experimental LSP tool behind a flag, but the default editing path is exact-string match. **Cursor relies on a separate apply model.** A second model merges suggested changes into your file. The merge step has historically struggled with large files. **Aider asks the model for search/replace blocks.** Same `old_string`/`new_string` failure mode, slightly different syntax. It can fall back to whole-file rewrites for small files. Four tools, one root cause. They all treat code as text. Code isn't text. Code has structure. --- ## LSP is the agent's nervous system, not a side feature In SoulForge, LSP is the default lookup path. Definitions, references, workspace rename, call hierarchy, type info, code actions, diagnostics — all on, all the time, no flags. The verification loop is what changes the agent's behavior. After every edit, the tool snapshots diagnostics *before and after*, then tells the agent exactly which errors it just introduced or fixed: ``` Applied 4 edits to src/api.ts (lines: 24→25, imports: 8→9) (formatted) ⚠ New diagnostic: src/api.ts:18 — Type 'string' is not assignable to type 'User' [impact: cochanges: src/core/types.ts, tests/api.test.ts] ``` The agent doesn't have to run a separate typecheck to find out it broke something. The error is in its hands the same turn it made the edit — so it can fix it on the spot, not three turns later when the user runs `npm test`. --- ## The Soul Map: code as structure SoulForge starts every session by understanding your codebase. On launch it parses your project with tree-sitter (30+ languages) and indexes it into a SQLite graph: files, exported symbols with signatures, import edges, blast-radius, git co-change. Most agents now ship some flavor of "repo map." Most are flat file lists or alphabetized symbol dumps. Soul Map ranks by **PageRank over the import graph** — a file that 30 others depend on outranks one nobody imports — then re-weights using **git co-change history**, so files that always change together get pulled in even when imports don't connect them. The result: when the agent thinks about `auth`, the relevant types and the test file that always changes with them surface together, not because of keyword overlap but because they actually move as a unit. The agent gets a ranked digest in its system prompt: ![Soul Map digest in the system prompt](/screenshots/ast-edit/soulmap.png) The full graph is one tool call away through `soul_grep`, `soul_find`, `soul_analyze`, and `navigate`. The agent never needs to grep for *"where does auth live"* — it already sees `AuthMiddleware` in `src/auth/middleware.ts` with a blast radius of 18, and asks for the function by name when it wants the body. --- ## Real refactors that break string-matching agents Forget toy benchmarks. Here are workday tasks where string matching falls over. **Make a function async, change its return type, add a parameter, import the type.** A string-matching agent reads the file, generates an `old_string`/`new_string` pair for the function body, then a *separate* edit for the import, then maybe a third edit if the first one shifted line numbers. If anything drifts between read and write, the chain breaks. SoulForge sends one tool call: ```ts ast_edit({ path: "src/api.ts", operations: [ { action: "set_async", target: "function", name: "fetchUser", value: "true" }, { action: "set_return_type", target: "function", name: "fetchUser", value: "Promise" }, { action: "add_parameter", target: "function", name: "fetchUser", value: "cache: boolean" }, { action: "add_named_import", value: "./types", newCode: "User" }, ], }) ``` Four operations, one tool call, **all-or-nothing rollback**. If any operation fails — say `User` doesn't exist in `./types` — none apply. You don't end up with the function modified but the import missing, or vice versa. No other AI coding tool I know of supports atomic multi-edit with rollback. They all run edits sequentially and pray the chain holds. ![ast_edit tool call rendered in the SoulForge TUI](/screenshots/ast-edit/ast-edit.png) **Adding a prop to a React component used in 30 places.** Each call site has different formatting — some single-line, some multi-line, some with trailing commas, some without. `str_replace` has to match each variation exactly. ts-morph's JSX manipulation adds the attribute regardless of how the call is formatted. **Tightening a function's return type from `any` to a real type.** The signature change is one line, but the type errors cascade across imports. SoulForge's pre/post-edit diagnostics surface every new error immediately. String-based agents either run a typecheck as a separate step or finish the edit and leave the errors for you. **Renaming a method on a frequently-imported class.** LSP workspace rename is one call. String-replace requires N edits, one per call site, each with its own potential drift. If three of them have the symbol in a comment or a string literal, LSP gets it right and string-replace doesn't. **Refactoring a 100-line JSX block.** This is where `ast_edit`'s anchor-pair `replace_in_body` shines: ```ts ast_edit({ path: "src/Settings.tsx", action: "replace_in_body", target: "function", name: "ProviderSettings", value: "", // end anchor newCode: "...", }) ``` Two short anchors. The tool replaces everything between them inside the named symbol. **A hundred-line block rewritten with twenty tokens of anchor text.** The AST scopes the search to the function, so the anchors don't have to be unique across the file — only inside the symbol. `str_replace` either fails on the first whitespace mismatch or the agent gives up and rewrites the whole component. These aren't demos. They're what happens during regular work, every day. --- ## ts-morph and the thesis `ast_edit` is built on [ts-morph](https://ts-morph.com/), a wrapper around the official TypeScript compiler API. Same compiler your IDE uses for *go to definition* and *rename symbol*. When the agent says *target: method, name: UserStore.load*, ts-morph walks the class, finds the node, hands back a mutable object. Mutating the node and serializing back produces formatted, valid TypeScript. This part comes from [the thesis](https://www.diva-portal.org/smash/record.jsf?pid=diva2%3A1690910). Artur and I built JS Typer for Axis Communications in 2022 — a tool that walked their JavaScript codebase, inferred types from runtime behavior, and rewrote files as valid TypeScript. The output compiled. The output preserved comments. The output didn't trash whitespace. The thesis defended a single claim: **the JS→TS migration problem is unsolvable through `sed`-style transformations and trivial through AST mutation.** Four years later, the same claim defends `ast_edit`. Sixty-five operations grouped by token cost: cheap ones (toggle `async`, change a parameter, set a return type) take one to ten tokens of input; mid-weight ones (replace a body, add a method, change inheritance) take ten to a hundred. Atomic batches group multiple operations into one tool call with rollback. > The AST handles the precision. You handle the intent. --- ## What it cannot do `ast_edit` works on `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs`. For the other 30+ languages SoulForge supports, the agent falls back to text editing. Tree-sitter parsers exist for those, but a real structural-edit story for Python or Rust needs more than a tree-sitter parse — it needs the equivalent of ts-morph for that language. I'll get there. It can't target anonymous callbacks or union members inside a type alias. It falls back to `replace_in_body` on the enclosing named symbol. It fails on files with parse errors. If your TypeScript doesn't compile, ts-morph won't parse it. The tool reports the error and falls back to text editing. You can't structurally edit code that isn't structured. These are real limitations. They're also what you'd expect from a tool that respects your code enough to refuse to corrupt it. --- ## A start, not a finish line `ast_edit` is a step in the right direction. The destination is AI coding tools that produce less slop. Less generated code that compiles but doesn't belong. Less duplicated logic. Less *"the agent rewrote my whole component because it couldn't find the right line."* When the editing primitive operates on structure, the model has fewer ways to wander. It can't quietly reformat a function. It can't leave behind a half-applied refactor because batches are atomic. > Cleaner primitives, cleaner output. You reduce slop by giving the model fewer ways to generate it. Caching is good hygiene. It is not the bottleneck. The bottleneck is the agent reading whole files to find single lines and re-reading them on retries. --- ## Try it ```bash brew tap proxysoul/tap && brew install soulforge soulforge ``` The agent picks `ast_edit` automatically for TS/JS files. You don't configure it. You just notice your edits stop failing on whitespace. --- ## Sources - [SoulForge](https://soulforge.proxysoul.com/) · [AST editing docs](https://soulforge.proxysoul.com/tools/ast-edit) · [Soul Map docs](https://soulforge.proxysoul.com/concepts/repo-map) - [OpenCode tools reference](https://opencode.ai/docs/tools/) · [Morph: AI edit failure analysis](https://www.morphllm.com/common-errors/error-editing-file) - [ts-morph](https://ts-morph.com/) · [Thesis: *Typed vs Untyped Programming Languages*, Bni & Matusiak, Malmö University, 2022](https://www.diva-portal.org/smash/record.jsf?pid=diva2%3A1690910) · [JS Typer reference implementation](https://github.com/proxysoul/Javascript-Typer) \- ProxySoul --- # Introducing SoulForge: Graph-Powered Code Intelligence URL: https://proxysoul.com/blog/soulforge Date: 2026-03-29T00:00:00.000Z Author: Ouail Bni Description: I wanted to trust AI coding agents. That meant building one that understands the codebase as a system, not file by file, but as a graph with structure, coupling, and consequences. Today I'm releasing **SoulForge**, graph-powered code intelligence for the terminal. --- ## Why I built this I use AI coding agents daily. Claude Code, Copilot, Codex. They're genuinely good. But I kept hitting the same wall: **I couldn't fully trust them.** Not because the models are bad. Because the agents don't understand what they're touching. They read a file, grep for a pattern, make an edit, and move on. They don't know that 30 files depend on the function they just renamed. They don't know that two files always change together. They don't know which parts of the codebase matter most right now. I wanted an agent that thinks about code the way an architect does. Not file by file, but as a system with structure, coupling, and consequences. An agent that can answer "what breaks if I change this?" before it changes anything. That's what trust requires. Not better prompts. **Better understanding.** --- ## The Soul Map When you open a project, SoulForge parses every file across **30+ languages** with tree-sitter, extracts symbols and imports, and builds a live dependency graph backed by SQLite. The agent sees a **Soul Map**: every file ranked by importance, with exported symbols, signatures, and dependency arrows. Before it reads a single file, it already knows which files matter most, what changes together, how far an edit ripples, and what symbols exist where. The graph updates in real-time. Edit a file and the agent sees the current state on the next turn. And when it does need to read code, it doesn't read whole files. It pulls exactly the function or class it needs by name. A 500-line file becomes a 20-line symbol extraction. The Soul Map provides line numbers and signatures, so the agent always knows precisely what to ask for. [How the Soul Map works →](https://github.com/ProxySoul/soulforge/blob/main/docs/repo-map.md) --- ## Tools that work with the graph Most AI coding tools give the agent `grep` and `read_file` and hope for the best. SoulForge gives the agent tools that query the graph directly: `soul_grep`, `soul_find`, `soul_analyze`, `soul_impact`. Answered from SQLite in milliseconds, zero LLM tokens burned. The agent doesn't grep and read 20 files to understand a module. It asks the graph "what depends on this?" and gets an answer with file paths, symbol names, and blast radius in one call. --- ## 4-tier code intelligence The graph handles structure. For precise code operations, SoulForge routes through four tiers: **LSP → ts-morph → tree-sitter → regex**. The agent gets the full LSP surface as tools. Go-to-definition, find references, workspace rename, call hierarchy, type hierarchy, diagnostics, code actions, formatting. When LSP isn't available, the intelligence router falls through gracefully. This powers compound tools that do the complete job in one call: `read` batches multiple files in parallel with surgical symbol extraction, `multi_edit` applies multiple edits atomically, `rename_symbol` does LSP workspace rename with verification, `move_symbol` moves code between files and updates all importers, `rename_file` moves files with import path updates, and `project` auto-detects your toolchain across 23 ecosystems. One tool call replaces a dozen grep-read-edit cycles. [Compound tools reference →](https://github.com/ProxySoul/soulforge/blob/main/docs/compound-tools.md) --- ## Parallel agents that coordinate When a task spans many files, SoulForge dispatches parallel agents through a shared bus. Two tiers: **Sparks** (explore/investigate, read-only, share the forge's cache prefix for zero cold-start cost) and **Embers** (code agents, own model, full edit capabilities). Agents share a file cache, post findings to each other in real-time, and coordinate edits. Up to 8 agents, 3 running concurrently. Optional post-dispatch passes: a cleanup agent reviews edits in fresh context, a verify agent checks correctness. You can also run multiple tabs side by side with different models and modes per tab. Agents see what other tabs are editing and git operations coordinate automatically. [Agent Bus deep dive →](https://github.com/ProxySoul/soulforge/blob/main/docs/agent-bus.md) --- ## Context that doesn't rot AI tools are great for 10 minutes. Hour-long sessions are where they break. Context fills up, old tool results pile on, every API call sends 100K tokens of stale content. SoulForge tracks working state deterministically from tool calls: files touched, decisions made, errors hit. When context fills up, it compacts from this pre-built state. No LLM call, no latency, no cost. Old tool results get pruned to one-liners enriched with Soul Map symbols. Sessions save incrementally and survive crashes. [Compaction deep dive →](https://github.com/ProxySoul/soulforge/blob/main/docs/compaction.md) --- ## Your Neovim, embedded I wanted to stay hands-on with the code. Not just watch a chat window, actually co-edit alongside the agent. SoulForge embeds **real Neovim** via msgpack-RPC. Your config loads. Your plugins work. LazyVim, Mason, Catppuccin, treesitter, all of it. The AI and the editor share the same LSP connection. When you rename in Neovim, the agent sees it. When the agent edits, you see it in the buffer. Toggle the editor closed and the intelligence router falls back to standalone LSP servers automatically. Open it back up and they reconnect. **It works over SSH.** tmux, screen, remote servers. No Electron, no X11 forwarding. SoulForge is a terminal app. --- ## 24 themes Ships with 24 builtin themes including Catppuccin, Dracula, Gruvbox, Tokyo Night, Nord, Rose Pine, Kanagawa, and more. Create your own with a JSON file. Themes hot-reload instantly. --- ## Cost transparency You see exactly what you're spending per-task, per-agent, per-model. The task router lets you assign different models to different jobs. Put Opus on complex code, Haiku on search and cleanup. You choose what goes where. --- ## Lock-in mode Hides agent narration during work, shows only tool activity and the final answer. Toggle via `/lock-in` or config. When you don't need to watch the agent think, just see what it does and what it produces. --- ## Safety built in - **Approval gates.** `rm -rf`, `git push --force`, edits to `.env` or credentials, individually prompted every time. No "Always Allow" button. - **Forbidden files.** `.env`, `*.pem`, `*.key`, `credentials.json` blocked across every tool. The LLM never sees your secrets. - **Pre-commit gate.** Auto-runs lint and typecheck before any `git commit`. Broken commits don't happen. --- ## Everything works headless ```bash soulforge --headless "fix the auth bug" # Stream to stdout soulforge --headless --json "add rate limiting" # Structured JSON soulforge --headless --events "refactor auth" # Real-time JSONL events soulforge --headless --chat # Multi-turn interactive echo "prompt" | soulforge --headless # Pipe from stdin ``` Exit codes: 0 success, 1 error, 2 timeout, 130 abort. Designed for CI/CD, scripting, and automation. --- ## The numbers | | | |---|---| | **Tools** | 35+ | | **Languages** | 30+ (tree-sitter grammars) | | **Providers** | 10 built-in + custom OpenAI-compatible | | **Agents** | Up to 8, 3 concurrent (Spark + Ember tiers) | | **Ecosystems** | 23 auto-detected toolchains | | **Themes** | 24 builtin + custom with hot reload | | **Compaction** | Zero LLM cost | --- ## Get started ```bash # Homebrew (macOS / Linux) brew tap proxysoul/tap && brew install soulforge # Bun (global install, requires Bun >= 1.2) curl -fsSL https://bun.sh/install | bash # install Bun if needed bun install -g @proxysoul/soulforge soulforge # or: sf ``` - **Docs:** [soulforge.proxysoul.com](https://soulforge.proxysoul.com/) - **Repository:** [github.com/proxysoul/soulforge](https://github.com/proxysoul/soulforge) If you've ever wished you could trust your AI coding agent to understand what it's touching before it touches it, give it a try. \- ProxySoul --- # What I Learned Building JuriCt URL: https://proxysoul.com/blog/building-jurict Date: 2026-03-07T00:00:00.000Z Author: Ouail Bni Description: Lessons from building a live AI deliberation platform — real-time orchestration, prompt engineering for personality, and why the hardest part of AI apps isn't the AI. [JuriCt](https://jurict.com) is a live AI deliberation platform. You submit a topic, and five AI agents — each with a distinct personality — debate it in real-time, vote, and deliver a verdict. The audience watches it stream live, votes, and chats alongside the debate. ![JuriCt — AI courtroom debate platform](/screenshots/JURICT_SCREENSHOT.png) Building it taught me more than any tutorial or course could. Here's what stuck. ## Multi-agent orchestration is its own discipline A single LLM call is simple. Orchestrating five agents debating in real-time with streaming, tool use, reactions, and classification — that's a different problem entirely. Each debate round involves far more than just "ask the model and display the response": 1. **Agent speaks** — Claude Sonnet streams a response with `smoothStream` for word-level chunking. Mid-stream, the agent can invoke web search (up to 2 searches per turn), and those tool calls get intercepted, displayed live, and their citations injected into the transcript. 2. **Reactions fire** — While one agent speaks, Haiku generates real-time reactions from the other four agents *in parallel*. Each reaction has an intent (agree, disagree, skeptical...), an intensity score, and a physical gesture. These stream to clients as the debate unfolds. 3. **Emotion classification** — After each turn, a separate Haiku call classifies the speaker's emotion (15 possible emotions) and gesture (7 types). This drives the agent's visual state on the frontend. 4. **Vote extraction** — After all rounds complete, Haiku reads the full transcript and extracts each agent's specific position as a single word — not "support" or "oppose," but the actual choice: "Python," "Walking," "Therapy." This required careful prompt engineering to stop the model from returning attitudes instead of answers. 5. **Verdict generation** — Sonnet synthesizes the full debate into a conclusion with confidence level and any dissenting opinions. Each of these steps uses the Vercel AI SDK differently — `streamText` with tool definitions for debate turns, structured output with Zod schemas for classification, and parallel `Promise.all` for reactions. The retry logic alone has five levels of exponential backoff per agent per round. The lesson: building with AI isn't about one model call. It's about composing multiple models at different capability levels (Sonnet for reasoning, Haiku for fast classification), running them in parallel where possible, and handling the failure modes gracefully when any of them break. ## Durable Objects changed how I think about state Before JuriCt, I would've reached for Redis or a queue system to orchestrate real-time debates. Cloudflare's Durable Objects gave me something better: a single JavaScript object that holds state, runs logic, and manages WebSocket connections — all in one place. The entire debate lifecycle lives inside one `CouncilDO` instance. It queues topics, coordinates agent turns, broadcasts events, and handles reconnections. No separate queue service, no pub/sub layer, no Redis. Just one object doing one job. The mental model shift: instead of "stateless functions that read/write to external state," think "stateful objects that *are* the state." It's closer to how you'd naturally model the problem. The trade-off is scale. A single Durable Object handles all connections, so you hit limits faster. But for a product like this, that constraint is acceptable — and the simplicity it buys you is worth it. ## Personality requires more than a system prompt Early versions of the five agents all sounded the same. I'd give them different role descriptions ("you are the analyst," "you are the ethicist") and they'd produce slightly different content, but the *voice* was identical. What actually worked: defining **emotional triggers, speech patterns, and frustrations** — not just roles. CIPHER doesn't just "think critically." He has dark humor, he's already assumed your plan will fail, and he gets frustrated when people ignore failure modes. MUSE doesn't just "consider human impact." She uses vivid metaphors, gets fierce when people are treated as statistics, and speaks in shorter, more evocative sentences. The lesson: LLMs are great at following instructions, but *tone* needs to be demonstrated, not described. Show the model how the character talks — don't just tell it what the character thinks about. ## Real-time streaming UX is harder than it looks Getting the WebSocket connected and tokens flowing was the easy part. Making it *feel* good was the hard part. Problems I didn't anticipate: - **Auto-scroll needs to be smart.** If the user scrolled up to re-read something, don't yank them back down on every new token. But if they're at the bottom, keep them there. This sounds simple until you account for images loading, code blocks expanding, and layout shifts. - **Markdown rendering mid-stream is broken by default.** A half-finished code block or bold tag will break the parser. I switched to [Streamdown](https://github.com/vercel/streamdown) which handles partial markdown gracefully. - **Throttling matters.** Scrolling on every single token (sometimes dozens per second) kills performance. Capping scroll updates to once per 250ms made everything smooth without feeling laggy. ## SQLite is enough JuriCt runs on Cloudflare D1, which is SQLite. Users, debates, chat messages, votes, payments, security events — all in SQLite with Drizzle ORM. No Postgres. No managed database service. Just SQLite on the edge. For 90% of web apps, this is the right call. The queries are simple, the data model is straightforward, and SQLite's single-writer model is actually fine when your writes are low-frequency (new debates, new messages, new votes — not thousands per second). What I'd watch out for: lack of transactions on deletes can leave orphaned records. I'd add those before scaling up. But for launch, SQLite was the right tool. ## Low-friction auth doesn't mean weak auth JuriCt uses PIN-based authentication — no email required, no OAuth flows. You join, get an auto-generated username and a 32-character cryptographically random PIN, and you're in. One-time profile edit if you want to customize. The design is deliberate: this is an entertainment product. People want to watch debates and chat, not fill out registration forms. But low friction doesn't mean cutting corners on security. PINs are hashed with **PBKDF2 (100k iterations, SHA-256)** with unique 16-byte salts. Session tokens are SHA-256 hashed before storage — raw tokens never touch the database. All sensitive comparisons (PIN verification, OTP checks, API key validation) use **constant-time comparison** to prevent timing attacks. Admin accounts get email-based 2FA on top of that. The lesson: you can have a one-tap signup flow *and* production-grade security. They're not in conflict — you just need to push the complexity to the backend instead of the user. ## What I'd do differently **Emotion inference is too expensive.** I run a separate LLM call to classify each agent's emotion on every turn. This should be batched or done at the sentence level, not the token level. It works but it's wasteful. **Module-level Maps need cleanup.** Some in-memory caches (vote tracking, session data) live in module scope and never get cleared between debates. Fine for now, but it's a memory leak waiting to happen under sustained traffic. **I'd add a CDN layer for replays.** Right now, viewing a past debate fetches from D1 every time. Completed debates never change — they should be cached aggressively. ## The stack, briefly **Frontend:** Next.js 16, React 19, Tailwind v4, Zustand, Motion, WebSockets **Backend:** Cloudflare Workers + Hono, Durable Objects, D1 (SQLite) + Drizzle **AI:** Claude API via Vercel AI SDK **Payments:** Stripe (pay-per-debate, first one free) ## Try it Head to [jurict.com](https://jurict.com) and submit a topic. Your first debate is free. Watch five agents disagree about something you care about. ```bash $ curl -s https://jurict.com # 5 agents. Your topic. Live deliberation. ``` -- ProxySoul --- # From Mr-wii to ProxySoul: 7 Years of Portfolio Evolution URL: https://proxysoul.com/blog/evolution Date: 2026-02-08T00:00:00.000Z Author: Ouail Bni Description: A look back at how my portfolio evolved across three major versions — from a static HTML page in 2019 to a terminal-inspired Next.js app in 2026. # From Mr-wii to ProxySoul Every developer's portfolio tells a story. Not just about the projects listed on it, but about the person who built it. Over the past 7 years, mine has been rewritten three times — each version a reflection of where I was as an engineer at that point in time. This is that story. ## v1 — Mr-wii (2019) ```bash $ git log --oneline mr-wii edae5b0 Mr Wii ``` The very first version. Built with **Create React App**, **Material UI** for components, **styled-components** for custom styling, and **react-spring** for animations. React Router handled navigation, and the whole thing was bootstrapped with `react-scripts`. It was rough around the edges — the code was messy and the design was basic — but it worked. It was the first thing I ever put on the internet, and deploying it felt like launching a rocket. For 2019, using React 16 with Material UI felt like wielding serious power. Looking back, the most important thing about v1 wasn't the code. It was the decision to build it at all. That single choice set everything else in motion. ## v2 — Pouiiro (2023) ```bash $ git log --oneline pouiiro 154baaa Pouiiro ``` Four years later, everything was different. I had learned React, TypeScript, and the entire modern frontend ecosystem. The second portfolio reflected that growth. **Pouiiro** was a full React SPA powered by **Vite**, with **GraphQL** queries hitting the GitHub API through **urql** to dynamically pull in my repositories. It had **Tailwind CSS** for styling, **Framer Motion** for animations, **tsparticles** for that interactive particle background, and **styled-components** for the bits that needed more control. The stack was ambitious — maybe too ambitious for a portfolio. GraphQL codegen, urql cache exchanges, custom theming, form validation with react-hook-form and zod. I treated it like a production app because I wanted to prove I could build one. It shipped. It looked good. It did the job. But over time, the SPA approach started to show its limits. SEO was possible but required extra work and wasn't as reliable as server-rendered HTML. The initial load was slower since the browser had to download and execute the entire JS bundle before rendering anything. And the codebase was increasingly hard to maintain for what was essentially a personal website. ## v3 — ProxySoul (2026) ```bash $ git log --oneline proxysoul 86cc98d ProxySoul ``` The current version. A complete rewrite from scratch. This time the priorities were different: **performance first**, **content first**, **maintainability first**. The choice of **Next.js 16** with App Router and React Server Components was deliberate — server-rendered pages, automatic code splitting, and a file-system router that just makes sense. **Tailwind v4** with its CSS-first config replaced the old JavaScript-based setup. **Framer Motion** stayed for animations, but this time with a lighter touch — page transitions, scroll-triggered reveals, and a custom animated icon system that brings the UI to life without overwhelming it. The terminal aesthetic wasn't planned. It started with the navigation labels (`~/home`, `~/about`, `~/projects`) and grew from there — the blinking cursor, the `git show` commit references, the monospace typography. It became the identity of the site. The blog runs on **Velite** with MDX, compiled at build time. No CMS, no database — just markdown files in a `content/` directory that get transformed into pages. Simple, fast, version-controlled. **Zod v4** validates the contact form. The GitHub integration switched from GraphQL to the simpler **REST API** via Octokit. Less complexity, same result. ## What changed between versions It's not just the tech that evolved: | | v1 (2019) | v2 (2023) | v3 (2026) | |---|---|---|---| | **Framework** | React (CRA) | React + Vite | Next.js 16 | | **Styling** | Material UI + styled-components | Tailwind + styled-components | Tailwind v4 | | **Data** | Static | GraphQL (urql) | REST API (Octokit) | | **Deployment** | Static hosting | Netlify | Vercel | | **Rendering** | Client-side SPA | Client-side SPA | Server Components | | **Blog** | None | None | MDX + Velite | | **TypeScript** | No | Yes | Yes | The pattern is clear: each version moved toward **less complexity** and **more content**. v1 was about existing. v2 was about proving technical skill. v3 is about communicating clearly and shipping something maintainable. ## Lessons **You don't need the fanciest stack.** v2 had GraphQL, cache exchanges, and codegen pipelines for a portfolio that displayed 6 repos. v3 uses a simple REST call and gets the same data. **Rewrites are worth it — sometimes.** I wouldn't rewrite a production app for fun. But a portfolio is different. Each rewrite forced me to re-evaluate what matters, learn new tools, and practice making architectural decisions from scratch. **Ship it, then improve.** v1 wasn't the best version — but it existed. That's more than most people's "I'll get to it someday" portfolio. ## What's next There's always a v4 somewhere in the future. But for now, ProxySoul is the one — dark, fast, and built to last. You can explore all three versions on the [evolution page](/evolution). ```typescript const versions = ["Mr-wii", "Pouiiro", "ProxySoul"]; console.log(`${versions.length} versions. Same developer. Always shipping.`); ``` --- # Hello, ProxySoul URL: https://proxysoul.com/blog/hello-proxysoul Date: 2026-02-07T00:00:00.000Z Author: Ouail Bni Description: Welcome to my rebranded portfolio. From Pouiiro to ProxySoul — a fresh start with the same passion for building great software. # Hello, ProxySoul Welcome to the new home of my portfolio. If you knew me before, you might remember **Pouiiro** — that was the old alias. Now it's **ProxySoul**. ## Why the rebrand? The name *ProxySoul* reflects what I do: I act as a **proxy** between complex technical problems and elegant solutions, putting my **soul** into every line of code I write. ## What's new? This portfolio has been completely rebuilt from scratch using: - **Next.js 16** with App Router and React Server Components - **React 19** with `useActionState` for form handling - **Tailwind CSS v4** with the new CSS-first configuration - **Framer Motion** for smooth page transitions and micro-interactions - **Velite** for this blog system with MDX - **ItsHover** animated icons throughout the UI The aesthetic is darker, more professional — black, purple, and red. A hacker-meets-engineer vibe with terminal-inspired navigation. ## What's next? I'll be sharing more about: - Technical deep-dives into projects I'm building - Lessons learned from freelancing across multiple domains - AI integration patterns and MCP development - The tools and workflows that keep me productive Stay tuned. The best is yet to come. ```typescript const proxysoul = { name: "Ouail Bni", role: "Software Engineer", motto: "If it exists, I can learn it.", }; ``` — ProxySoul