# m4t.tf: Full Content Archive This file contains the published blog content from m4t.tf. ## Failing cheaply - URL: https://m4t.tf/posts/20260921-1-failing-cheaply - Published: 2026-09-21 - Description: Ordering your checks by how expensive it is to ignore them. - Tags: dx, tools, git, programming A two-minute test suite that tells you a variable is misspelled has cost you two minutes to learn something that was true in the first millisecond. The same mistake found in production costs a great deal more. So I order my checks by that price, and there are four places they can run. - **Commit message**: free, and it happens while you are still thinking about the change. - **Pre-commit**: fast, and only on the files you staged. - **Pre-push**: seconds rather than milliseconds, but still before anyone else sees it. - **CI**: the expensive one, and the only place a check is guaranteed to have run at all. ## commitlint The `commit-msg` hook is one line: ```sh commitlint --edit "$1" ``` It enforces [conventional commits](https://conventionalcommits.org/). A prefix like `fix:` or `feat:` is a claim about intent that a diff cannot make. The diff knows what changed. Only the author knows whether it was a bug fix or a new idea, and six months later that distinction is most of what makes a history readable. You start from the shared conventional-commits preset and override the rules you disagree with. The default body length limit is 100 characters, for example, which is not much room to explain a trade-off. I raised mine to 200 after hitting the limit with my own commits. The heavier option is commitizen, or semantic-release, both of which sit on commit conventions and use them to generate changelogs and cut releases. If you already have a release pipeline, that is worth having. I wanted the grammar enforced and nothing else, and commitlint is a rule set and a hook. ## One engine for most of it Most of these checks run through [Vite+](https://viteplus.dev), one CLI (`vp`) wrapping Vite, Vitest, Rolldown, Oxlint and Oxfmt. It is not tied to a framework, so the same setup works for React, Vue, Svelte or plain TypeScript. - `vp check` formats with Oxfmt, lints with Oxlint, and type checks. - `vp test` runs Vitest. - `vp staged` runs the relevant check against staged files on pre-commit. Everything lives in one config file: import grouping, a ban on `./` and `../` imports in favour of path aliases, and class sorting if you write utility CSS. The alias rule is worth having on its own. It is a decision you would otherwise re-argue in every code review. ESLint and Prettier were the baseline I left behind. Oxlint is [50 to 100 times faster than ESLint](https://github.com/oxc-project/bench-linter) on Oxc's own benchmarks, and its type-aware engine, tsgolint, runs 12 to 18 times faster than `typescript-eslint` across large TypeScript codebases. Oxfmt is [up to 30 times faster than Prettier](https://voidzero.dev/posts/announcing-oxfmt-alpha) and stays compatible with its output. On a small project the seconds do not amount to much. The config surface does. One file replaces an ESLint config, a Prettier config, two ignore files, and the plugin wiring between them. Biome is the alternative I would take seriously. It is one Rust tool doing both jobs, which solves the same problem. I stayed with Oxc because Vite+ pulls build, test, lint, format and the staged-file workflow under one CLI and one config, all sharing a parser. The cost of that bet is a younger plugin ecosystem. JavaScript plugins give back a chunk of the speed as well: one benchmark drops from roughly 16 times faster than ESLint to 6 times faster once plugins load. Even with a couple loaded, the margin is still wide. ## fallow On pre-push, before anything leaves your machine: ```sh vpx fallow ``` [fallow](https://docs.fallow.tools) is codebase intelligence: dead code, duplication, complexity and styling, returning a verdict instead of a wall of warnings. It finds the things that accumulate when code is written faster than it is read. An export nothing imports. A package still sitting in `package.json`. The same helper written twice in two files that never met. A small config file silences false positives: path aliases that only resolve at build time, component internals generated by tooling, and dependencies used implicitly rather than imported. Every dead-code tool needs this. Budget for it. The tools it replaces are knip, jscpd and dependency-cruiser. That is three configs to write and three sets of output to read, and fallow covers the same ground in one Rust binary. On fallow's benchmark set it analyses fastify in 64ms where knip takes 205ms, and preact in 74ms against knip's 2.01s. Knip is faster on some Astro and TypeScript projects, and jscpd is still faster at raw duplication scanning. Breadth settled it, along with one thing the others lack. fallow ships an MCP server, so an agent can run the same analysis mid-session, not only at push time. ## Two tools for specific stacks The next two tools assume the stack they were built for. The pattern still generalises when the tools do not. ## Keeping the design system honest Components get edited by hand, copied between files, and adjusted one style at a time. That is how a design system erodes. If you use Tailwind with shadcn components, [`@shadcn/lint`](https://ui.shadcn.com/docs/lint) runs inside the same Oxlint pass and enforces the vocabulary: no arbitrary values, no inline styles, no raw colours, no unknown classes, no restyling published components, and static class names that can be extracted. The directory holding copied primitives gets overrides, since those break some of the rules by design. On a different design system, the equivalent is a plugin that knows your token vocabulary. Stylelint was the obvious other choice, but its rules are about CSS syntax. The question worth answering is whether a class belongs to this system or is a one-off that will read as inconsistency in a month. ## react-doctor react-doctor runs on pre-push and as a GitHub Action on every pull request. It checks accessibility, performance, security and correctness, including the rules around the React Compiler. Its own description is blunt: _"Your agent writes bad React. This catches it."_ On a React codebase it earns its place immediately. On anything else, skip it. You can configure it to ignore the unused dependency rule for your manifest, because several packages get picked up by a framework rather than imported by name. Without that exception the report is noise on the first run. The official `eslint-plugin-react-hooks` covers the rules of hooks and exhaustive dependencies, which are the two that matter most, and react-doctor includes them. I added it for everything else that is easy to get wrong in a component and hard to catch in review. ## The rest of the hooks Not every hook is a linter. Git LFS handles large binary assets across checkout, commit, merge and push, which starts to matter when a repository holds video or high-resolution images. Whatever else your project needs a hook for belongs at the cheapest point it can run. ## The same tools, for agents The fallow MCP server is the part I keep coming back to. During a session, an agent can run the same audit and fix what it finds before writing a commit: an export it just orphaned, a helper it duplicated, a dependency it stopped using. That is where the tooling earns its keep. The agent and I get the same signal, from the same tool, at the same moment. ## CI as the backstop Local hooks can be skipped, and a fresh clone has no memory of what you were supposed to run. So the same checks run again where nobody can forget them. - One job on every push to the main branch: run the checks and build. - One job on pull requests, reporting its findings back on the pull request itself. - One job on version tags: build, deploy, then smoke test what you deployed. The smoke test is the cheapest check here and the only one that proves the thing you deployed is actually up. | Check | Runs at | Catches | Applies to | Replaced | | -------------- | -------------- | ------------------------------------------ | ------------------------- | ------------------------- | | commitlint | commit-msg | Commit message format | any repo | Reviewing history by hand | | Oxfmt + Oxlint | pre-commit, CI | Formatting, lint, types, imports | any web app | Prettier + ESLint | | fallow | pre-push, MCP | Dead code, duplication, complexity | JavaScript and TypeScript | Knip + jscpd | | shadcn lint | pre-commit, CI | Design system drift | Tailwind + shadcn | Stylelint | | react-doctor | pre-push, PRs | React accessibility, performance, security | React only | eslint-plugin-react-hooks | | Smoke test | deploy | Downtime | any deployment | Hoping | ## What is left The pipeline rules out the boring mistakes while they are still cheap. Nothing more than that, really. The interesting ones are still yours to make. --- ## LLMs are tactical programmers - URL: https://m4t.tf/posts/20260908-1-llms-are-tactical-programmers - Published: 2026-09-08 - Description: LLMs are very good at making changes. They are less good at deciding which changes should exist in the first place. - Tags: llms, programming AI coding agents are very good at making changes. Give one a clear ticket and it will search the codebase, edit the files, run the tests, and continue until everything is green. The result often works. That does not necessarily mean it was the right change. It is useful to classify programming into two categories: tactical and strategic. Tactical programming gets the current task finished. Strategic programming makes the next task easier. In my experience, LLMs are much better at the former than the latter. ## Tactical programming Tactical programming focuses on the immediate problem in front of you. A user needs a new form field, so you add the field. An API needs validation, so you add a check. A test fails, so you change the implementation until it passes. None of this is inherently bad. Most software work is tactical. Features need to ship and bugs need fixing. The problem is what happens when the tactical solution becomes permanent. Suppose three screens need to validate an email address. A tactical solution adds the same check to all three screens: ```ts if (!email.includes("@")) { setError("Enter a valid email address"); } ``` It works. The tests pass. The feature is done. Then the validation rules change. You now need to find every copy and update them all. Maybe one gets missed. Maybe each screen has drifted slightly. Maybe the same rule now exists in the frontend, backend, and database. The original change was successful, but the system is worse. ## Strategic programming Strategic programming asks a different question: > What change would make this and future changes easier? In the email example, the answer might be a shared validation function: ```ts const result = validateEmail(email); ``` Or it might be a schema shared between the frontend and backend. Or it might be no new abstraction at all, because the three screens are unrelated and the validation rules are unlikely to change. That last possibility matters. Strategic programming does not mean adding an abstraction every time code looks duplicated. It means understanding the trade-off and choosing deliberately. Sometimes the strategic change is a new module. Sometimes it is moving a decision to a better boundary. Sometimes it is deleting an abstraction that no longer helps. ## Where LLMs are good LLMs are excellent tactical programmers. They are good at: - Following an explicit plan - Matching the conventions of an existing codebase - Making repetitive changes across many files - Translating a design into code - Fixing compiler errors - Fixing failing tests - Filling in familiar patterns This is why coding agents can feel so productive. Once you know what you want, they can turn the idea into working code very quickly. The faster feedback loop helps too. The agent writes something, runs the tests, reads the error, and tries again. It is very good at navigating a local loop where success is clearly defined. ## Where they struggle Strategic programming is harder because the specification is rarely written down. The ticket might say: > Add support for soft-deleting notes. It probably does not say: - Whether deletion belongs in the repository or service layer - Whether all resources should eventually use the same mechanism - Whether the existing database abstraction is already too complicated - Which parts of the API are public by accident - What the team expects this system to look like in a year An agent can inspect the codebase, but it cannot automatically know which awkward patterns are temporary, which are deliberate, and which everyone has simply learned to tolerate. It also tends to preserve local patterns. If the codebase contains duplicated logic, the agent will often copy that logic. If every feature creates a new service class, it will probably create another service class. The result is locally consistent but globally worse. This is the dangerous kind of AI-generated code. Not code that obviously fails, but code that looks reasonable while increasing coupling, duplication, and future maintenance cost. ## The missing context Strategic decisions depend on context that is difficult to include in a prompt. You need to know: - Why the current architecture looks the way it does - Which parts of the system are likely to change - Which interfaces other teams depend on - What the operational constraints are - Which complexity is intentional - What the team is willing to maintain This context often lives in old pull requests, conversations, incidents, and the heads of the people who built the system. An LLM does not own the consequences of an architectural decision. It will not be maintaining the code six months later. It does not feel the cost of adding another special case. That responsibility still belongs to the programmer. ## How I use coding agents I do not think this means LLMs should be kept away from design work. They can be useful strategic collaborators, but they need to be asked different questions. Before asking an agent to implement a feature, ask it to: - Inspect the existing patterns - Identify the boundaries involved - Propose multiple approaches - Explain what each approach makes easier or harder - Find existing duplication - Describe the likely impact of each option I also want the agent to answer a simple question: > What will be harder after we make this change? That question is often more useful than asking what files need to be edited. The first implementation should be treated as a proposal, not as the design. Tests can tell you whether the code works. They cannot tell you whether the abstraction is in the right place. ## A way to find the gaps This is where [`grill-me`](./20260716-1-grill-me-skill) helps. Before implementation, it interviews you one question at a time, uncovering assumptions, missing decisions, and edge cases that a feature ticket leaves unstated. It does not replace strategic judgment. It helps expose the gaps so you can resolve them before handing the plan to a tactical agent. Read the [full post on the grill-me skill](./20260716-1-grill-me-skill) for the details. ## Make the next change cheaper The best review question I know is: > Did this change make the next change cheaper? It does not have to make every future change cheaper. That is impossible. But it should be clear what trade-off was made. Did we reduce the number of places that know this rule? Did we create a real abstraction, or just hide duplicated code behind a function? What code can now be deleted? What happens when this feature becomes twice as large? LLMs are not incapable of answering these questions. They are simply not naturally optimised for them. Most prompts ask for a feature. Most feedback checks whether the feature works. Both encourage tactical programming. The current sweet spot is to provide the strategy yourself and let the agent handle the execution. The agent can search, edit, test, and iterate. You decide where the behaviour belongs, which trade-offs are acceptable, and whether the system is better than it was before. The code working is the beginning of the review, not the end. --- ## Make AI writing sound human with the humanize skill - URL: https://m4t.tf/posts/20260908-2-humanize-skill - Published: 2026-09-08 - Description: The humanize skill removes language that performs importance while preserving meaning, voice, and useful detail. - Tags: llms, writing, tools, skills AI-generated writing often sounds polished in the wrong way. It explains what it is about before getting to the point. It adds a summary after the conclusion. It reaches for words such as "utilise" and "commence" when "use" and "start" would do. Every paragraph arrives with the same careful rhythm. The `humanize` skill is a small set of instructions for fixing that. It lives at `https://github.com/krake747/skills/humanize/SKILL.md` and applies whenever I ask an agent to write or edit prose. > Good humanization removes language that performs importance while preserving meaning and voice. ## Five passes ### Strip Remove filler, throat-clearing, chatbot courtesy, self-reference, and unnecessary decoration. Instead of: > In this piece, we will take a look at why this matters. Write: > This matters because... The first sentence talks about the writing. The second talks about the subject. ### Swap Replace inflated words with their plain equivalents. The skill gives examples such as: - `commence` becomes `start` - `ascertain` becomes `find out` - `additionally` becomes `also` - `numerous` becomes `many` Formal vocabulary is sometimes the right choice. Question words that add status but no meaning. ### Rebuild Some sentences sound artificial because of their shape, not their vocabulary. The skill calls out reversal frames such as "It is not X, it is Y", forced lists of three, fake ranges, self-interview questions, and dense clauses that make the reader backtrack. State the point. Use the real number of items. Split a sentence when it contains too many ideas. ### Shape Human prose has rhythm. Five sentences of similar length, each beginning with "However" or "Moreover", can sound assembled even when every sentence is grammatical. Vary sentence lengths, strengthen endings, and begin paragraphs with their own claims rather than repeated cue words. Short sentences help. Then a longer one can take its time when the idea needs room. ### Guard The final pass protects meaning. Numbers, names, dates, quotes, units, negations, domain terms, and meaningful hedges must survive. Code and quoted examples are exempt. A phrase such as "Use pnpm, not npm" is not inflated language. It is an instruction. Tension matters too. A serious ending is not automatically a sermon. If the final line carries the conflict or the important fact, it should stay. ## The smallest useful edit The skill is deliberately conservative. Fix the span, not the paragraph. If one sentence is bloated, change that sentence. If nothing is broken, change nothing. That matters because humanising prose can easily become another kind of performance. A text can be stripped so aggressively that it loses personality, precision, or the writer's actual voice. Human writing also needs voice. Have a position. Name the concrete thing. Read the sentence aloud. Vary the rhythm. Say "I" when it fits. Leave a little mess. Perfect structure is its own tell. ## What it is not It does not turn every piece of writing into casual conversation, remove every formal word, flatten technical language, rewrite a story's structure, or invent a stronger argument. It fixes sentences, not stories. The skill leaves the meaning alone and keeps the language from getting in its way. ## Appendix: Why plain English works English has Germanic and Latinate vocabulary because of its history. That gives us pairs such as `ask` and `question`, `help` and `assist`, and `begin` and `commence`. These words are not always exact synonyms. Germanic words often feel shorter, more familiar, and more concrete. French and Latinate words can feel more formal, abstract, or institutional. Germanic words are not automatically better than Latinate ones. Use the clearest, most precise word. When a Latinate word adds no useful meaning, a shorter and more familiar word will often help. Humanising prose means choosing the word that says exactly what the writer means. --- ## Relentless interviews for your plans, with the grill-me skill - URL: https://m4t.tf/posts/20260716-1-grill-me-skill - Published: 2026-07-16 - Description: Stress-test your plans and designs before writing code, with a Socratic agent that grills you one question at a time. - Tags: llms, tools, programming Every plan looks solid on paper. It's only when you start building that the hidden assumptions surface - the ambiguous edge case, the dependency you didn't know existed, the decision you deferred that now blocks everything. [`mattpocock/skills`](https://github.com/mattpocock/skills) is a collection of engineering productivity skills for AI coding agents. The `grill-me` skill is designed to surface those holes _before_ you write code. ## What it is `grill-me` runs a "relentless interview" on your plan or design. You describe what you want to build, and the agent walks down every branch of your decision tree, one question at a time. It's a thin wrapper that delegates to the `grilling` skill, whose prompt instructs the agent to: > Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. ## The rules Three constraints keep the grilling productive: **One question at a time.** Asking multiple questions at once is bewildering. The agent asks one, waits for your answer, then proceeds. This forces real consideration of each tradeoff instead of skimming past a list. **Look up facts, don't ask.** If the answer lives in the codebase, the agent finds it rather than asking you. What middleware exists, what patterns are already established, what config values are in use - the agent reads the code so you don't have to explain context it can discover. **Decisions are yours.** The agent provides a recommended answer for each question, but you make the call. You stay in control of the design. ## Why one question at a time A human reviewer can list ten concerns in a single message. An LLM can vomit up thirty questions in one prompt. Neither is useful with the normal impulse being to answer superficially and move on. `grilling` is deliberately capped at one question. The result is a gradually enriched context window. ## The family: grill-with-docs If you want artifacts alongside the interrogation, there's `grill-with-docs`. It runs the same session but also writes ADRs capturing each decision and a glossary of domain terms that emerged during the conversation. Use `grill-me` for a quick sanity check. Use `grill-with-docs` when you need a written record - handoff, onboarding, or audit trail. ## When to use it - **Architecture decisions** - The agent forces you to articulate tradeoffs before choosing. - **API design** - Every endpoint, status code, and auth boundary gets examined. - **Refactoring plans** - "What depends on this?" is answered by the codebase; "what's the migration strategy?" is answered by you. - **Cross-team handoffs** - Run a grilling session, then share the output as a spec. The key insight: you want to make mistakes when they're cheap. A wrong turn in a conversation costs five minutes. A wrong turn in code costs a rewrite. ## The result But once you have sufficiently grilled the question and have your enriched context window, what then? You can turn it into a specification by using the `to-spec` skills from the same skills repository. This transforms the long conversation into an actionable set of tasks that you - or an agent - can work on, safe in the knowledge you have done your best to identify edge cases. ## Try it Load `grill-me` into any agent platform that supports skill loading (OpenCode, Claude Code, etc.) and say "grill me on [your plan]". The agent will start interviewing you. [github.com/mattpocock/skills](https://github.com/mattpocock/skills) --- ## Plan, iterate, then implement with OpenCode - URL: https://m4t.tf/posts/20260630-1-plan-iterate-implement-with-opencode - Published: 2026-06-30 - Description: A three-phase loop that gets better code out of AI coding agents. - Tags: llms, tools, projects, programming The default way to use an AI coding agent is to type a prompt and watch it write code. It's fast, but it's not perfect. Sometimes the code is clean and fits your codebase. Sometimes it's a tangle of wrong abstractions that takes longer to undo than to rewrite. OpenCode has a better way with its two modes: Plan and Build. Switching between them gives you a three-phase loop that catches bad decisions before they become bad code. ## The two modes Hit Tab. That's it. You toggle between Plan mode and Build mode with a single key. A small indicator in the corner shows which mode you're in. Plan mode is read-only. The agent can read files, search the codebase, and propose an approach. \*It cannot write a single line. Build mode is the agent's default. It reads, writes, runs commands - the full toolbox. The loop is: plan in one mode, implement in the other. \*_Sometimes when you aren't looking, the agent will try to obey a suggestion as an imperative. I have seen it try to invoke a Python script to concatenate text to a file as a form of editing, even though it was in plan mode._ ## Phase 1: Plan You describe the outcome you want. No implementation details. > When a user deletes a note, flag it as soft-deleted in the database. Then create a screen that lists recently deleted notes, with options to restore or permanently delete each one. OpenCode responds with a concrete plan: which files change, what the migration looks like, which components get created, how state flows. It surfaces questions it cannot answer from the codebase alone. It asks. You answer. This back-and-forth happens before any code is written. ## Phase 2: Iterate The first plan is rarely the right one. You push back. > I don't want a separate deleted_notes table. Add a deleted_at nullable timestamp to the existing notes table. Follow the pagination pattern from @components/ArchiveList.tsx. You can use `@` to fuzzy-search for files and point the agent at existing patterns it should match. Each round of feedback tightens the plan. You iterate until the approach is something you would approve in a code review. ## Phase 3: Implement Hit Tab to switch to Build mode. > Looks good. Go ahead. The agent works through the plan sequentially. Migration first, then the backend, then the frontend, then tests. You watch each change land and commit as you go. If a step goes sideways, `/undo` that change, refine the ambiguous part of the plan, and resume. You keep the context and the parts that worked. No need to start over. ## Why this works LLMs are eager. Without a plan phase they will jump to implementation, picking patterns that might not match your conventions or missing cross-cutting concerns. Plan mode forces the agent to think before it writes. The iteration phase catches mistakes while they are still cheap - during the conversation rather than in code. By the time you switch to Build mode, the agent has a contract to execute against. When it makes a wrong turn, you both know what right looks like. ## A real example ``` [Tab → Plan mode] > We need to add rate limiting to all routes in @packages/api. > Use the middleware pattern from @packages/api/src/middleware/cache.ts. > Default to 100 requests per minute per IP, configurable per-route. OpenCode: Here's my proposed plan... [details, questions about status codes and auth] > Return 429. Authenticated users get 1000/hr, anonymous 100/hr. > Store in Redis — client at @packages/api/src/lib/redis.ts. OpenCode: Updated — using Redis sliding window. Ready when you are. [Tab → Build mode] > Ship it. ``` Four minutes of conversation, then the agent implements to spec. ## A few things that help Use `@` references liberally. Every file reference anchors the agent to real code. The more context you give, the less it hallucinates. Run `/init` on new projects. It generates an `AGENTS.md` that captures your conventions. Commit it. The agent reads it automatically. Use `/share` to send the plan to a teammate for a sanity check before implementing. Lighter than a design doc, more concrete than a Slack thread. Treat the agent like a junior developer. Specificity is kindness. "Implement JWT refresh rotation" is vague. "When the access token expires, use the refresh token to get a new pair, rotating the refresh token each time. The existing handling is in @lib/auth.ts" is actionable. ## Check it out OpenCode is open source with 160K+ stars on GitHub. It runs in the terminal, desktop, or IDE, supports any LLM provider, and works with free-tier models. The Plan mode costs one keystroke and saves you from cleaning up after an over-eager agent. [github.com/anomalyco/opencode](https://github.com/anomalyco/opencode) --- ## Orchestrating AI coding agents with Sandcastle - URL: https://m4t.tf/posts/20260626-1-orchestrating-ai-coding-agents-with-sandcastle - Published: 2026-06-26 - Description: Sandcastle is a TypeScript library and CLI for orchestrating AI coding agents across 6 providers and 4 sandbox runtimes. - Tags: llms, tools, projects, programming AI coding agents are powerful, but they're also synchronous and limited by the human they converse with. You open a chat, type a prompt, watch code stream in, review diffs, copy-paste, commit. Every interaction is a conversation you have to sit through. What if you could treat an agent like a function call instead? ```ts const result = await run({ agent: "claude", sandbox: "docker", prompt: "Add input validation to the API layer", }); ``` That's what Sandcastle does. ## What is Sandcastle? Sandcastle is a TypeScript library and CLI for orchestrating AI coding agents built by Matt Pocock. It plugs into multiple providers - Claude Code, Codex, Cursor, OpenCode, Copilot, etc - and it can run in different sandboxes - Docker, Podman, etc. One API, many backends. Behind the scenes, Sandcastle manages the full lifecycle: spin up a sandbox, create a git worktree, invoke the agent, collect commits, and merge results back. You don't wrangle Docker commands or git plumbing. You just call `run()`. ## The Core API `run()` is the main event. You tell it which agent to use, where to run it, and what to do. It handles the rest. ```ts import { run } from "@ai-hero/sandcastle"; const result = await run({ agent: "claude", sandbox: "docker", prompt: "Refactor the auth module to use JWT", }); console.log(result.commits); ``` Need to work interactively? `interactive()` launches the agent in TUI mode inside the sandbox. Need more control? `createSandbox()` gives you a long-lived sandbox for multi-step workflows. `createWorktree()` creates an independent git worktree separate from any sandbox. Three branch strategies give you flexibility: - **`head`** - agent writes directly to the working directory - **`branch`** - commits land on a named branch, survives re-runs - **`merge-to-head`** - agent works on a temp branch, changes merged back automatically ## Provider Abstraction Different agents, different CLIs, different session formats; Sandcastle normalises all of them behind a single interface and handles all of this so your orchestration code doesn't have to. You can swap from Claude Code to Codex by changing one string: ```ts const result = await run({ agent: "codex", // was "claude" sandbox: "podman", // was "docker" prompt: "Fix the failing tests", }); ``` The same goes for sandboxes. Docker for local development, Vercel Firecracker for production isolation, Daytona for remote environments. Your orchestration script stays the same. ## Multi-Agent Workflows One agent is useful. Several agents working in parallel is where things get interesting. Sandcastle ships with 5 scaffold templates that demonstrate multi-agent patterns. The most powerful is `parallel-planner`: 1. An **orchestrator agent** analyses the work and splits it into tasks 2. **Parallel agents** each pick a task and work on separate git branches 3. A **reviewer agent** checks each branch 4. A **merger agent** brings everything together This isn't theoretical. Sandcastle uses this exact pattern - its own `.sandcastle/run.ts` orchestrates parallel agents to develop Sandcastle itself in CI. ## Conclusion AI coding agents are incredibly capable, but they've been stuck in a chat-first paradigm. Sandcastle treats them as programmable units - you compose them, parallelise them, and run them AFK. It's the most comprehensive open-source take on agent orchestration I've seen. `sandcastle run` is all you need to get started. --- ## How I Tailored Sandcastle for Ogle - URL: https://m4t.tf/posts/20260626-2-how-i-tailored-sandcastle-for-ogle - Published: 2026-06-26 - Description: Taking Sandcastle's parallel-planner-with-review template and tailoring it to a Go project's specific needs - custom Docker image, phase-specific models, Go-specific prompts, and the scrivener documentation audit. - Tags: llms, tools, projects, programming In my last post I introduced Sandcastle, the agent orchestration library from Matt Pocock. Today I want to show how I actually use it on a real project called Ogle, a Docker Compose TUI written in Go. Sandcastle ships with scaffold templates. The `parallel-planner-with-review` template provides a powerful workflow out of the box for a TypeScript project. Tailoring prompts, infrastructure, and models to a Go project is where my time went. ## The Pipeline Ogle's Sandcastle workflow runs a 3-phase loop, up to 10 iterations, so newly unblocked issues get picked up after each round of merges: 1. **Plan** - A pro model (`deepseek-v4-pro`) reads open issues tagged `Sandcastle`, builds a dependency graph, and outputs a `` JSON listing unblocked issues with branch names in `sandcastle/issue-{id}-{slug}` format. 2. **Execute + Scrivener + Review** - Each issue gets a dedicated sandbox on its own branch. The implementer runs first (flash model, up to 100 iterations). If it produces commits, a scrivener agent audits documentation, then a reviewer refines the code. All issue pipelines run concurrently via `Promise.allSettled` - one failure doesn't cancel the others. 3. **Merge** - A single agent merges all completed branches into the base branch, resolves conflicts, runs tests, and closes issues via `gh`. Edge cases are handled too: if an implementer makes no changes (already completed), the branch is skipped and the issue closed. If the scrivener fails (network issue, `gh` auth), the review still runs - it's non-fatal. ## Infrastructure Sandcastle's Docker sandbox is the default provider. Ogle needed more inside the container: Go 1.26.3, the GitHub CLI, and the OpenCode CLI. The Dockerfile adds all of those plus UID/GID alignment so bind-mounted files share ownership with the host user. ```dockerfile FROM node:22-bookworm RUN apt-get update && apt-get install -y git curl jq ca-certificates RUN curl -fsSL https://cli.github.com/packages/... \ && apt-get install -y gh ARG AGENT_UID=1000 ARG AGENT_GID=1000 RUN groupmod -g $AGENT_GID node && usermod -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node RUN curl -fsSL https://go.dev/dl/go1.26.3.linux-amd64.tar.gz | tar -C /usr/local -xz RUN npm install -g opencode-ai@latest USER ${AGENT_UID}:${AGENT_GID} ENV GOPATH="/home/agent/go" \ PATH="/usr/local/go/bin:/home/agent/go/bin:/home/agent/.local/bin:$PATH" WORKDIR /home/agent ENTRYPOINT ["sleep", "infinity"] ``` Sandcastle hooks run before the agent starts inside the sandbox. Ogle's hooks install dependencies and regenerate mocks: ```ts const hooks = { sandbox: { onSandboxReady: [{ command: "npm install && go mod tidy && make generate" }], }, }; ``` The `copyToWorktree` option avoids a full `npm install` from scratch each time - it copies `node_modules` from the host into the worktree before the sandbox starts, and the hook handles platform-specific binaries. ## Phase-Specific Models Not one model to rule them all. Each phase uses a different OpenCode model: | Phase | Model | Rationale | | ----------- | ------------------- | -------------------------------------------------------------------- | | Planner | `deepseek-v4-pro` | Deeper reasoning for dependency analysis | | Implementer | `deepseek-v4-flash` | Faster iterations, `OPENCODE_PERMISSION=allow` for autonomous writes | | Reviewer | `mimo-v2.5-pro` | Quality judgment for code review | | Merger | `deepseek-v4-flash` | Simple merge operations, doesn't need the pro model | The implementer is configured with `OPENCODE_PERMISSION=allow` so it can write files without asking permission each time. ## Prompt Design Prompt quality matters more than orchestration plumbing. Here's how each phase's prompt is structured. **Plan prompt** uses a `!` shell expression to pull open issues directly: ``` !`gh issue list --state open --label Sandcastle --json number,title,body,labels,comments` ``` The agent analyzes blocking dependencies between issues and outputs a `` JSON block. Only unblocked issues are included. **Implement prompt** drives the RGR cycle with Go-specific conventions baked in: - RED: write one test (black-box, table-driven, `t.Parallel()`) - GREEN: write the implementation - REFACTOR: clean up - `make lint && make test` before every commit - Commit messages end with `[RALPH]` so human-authored and agent-authored commits are distinguishable **Review prompt** loads `@.sandcastle/CODING_STANDARDS.md` which encodes Ogle's Go conventions: import order, error wrapping (`fmt.Errorf("…: %w", err)`), no package-level globals except in `cmd/root.go`, `log/slog` instead of `log`, and mockery-generated mocks. **Merge prompt** merges each completed branch in sequence, resolves conflicts intelligently, runs `make lint && make test` after each merge, and closes the corresponding GitHub issue via `gh issue close`. ## The Scrivener Phase This is the non-standard addition. After an implementer finishes but before the review, Ogle runs a scrivener agent that audits project documentation against the code changes on that branch. It follows the scrivener skill methodology - 7 audit categories: STALE, GAP, CONFLICT, AMBIGUOUS, STRUCTURAL, POLISH, UNWIELDY. If it finds discrepancies, it either appends a report to an existing documentation issue or creates a new one tagged `scrivener-discovered`. The skip logic prevents circularity: if the current issue was _itself_ created by a previous scrivener run, the scrivener phase is skipped entirely. No one wants an infinite loop of "documentation audit found a docs issue → create the issue → next iteration audits that issue too." ## Conclusion Sandcastle's `parallel-planner-with-review` template gave me a solid foundation. The real work was tailoring it to Ogle's specific needs: a custom Docker image with Go tooling, phase-specific model selection, detailed prompts encoding Go conventions, and the scrivener documentation audit phase. Hundreds of execution logs, tens of branches, and countless `[RALPH]` commits later, the pipeline runs entirely AFK - set it up, walk away, and come back to merged, reviewed, documented changes. Documentation is now a particularly bothersome issue that I no longer need to worry about. The templates give you the skeleton. The prompts, the models, and the infrastructure decisions are what make it work for your project. ### The data I introduced Sandcastle to the project with commit `f398f5e`, and v0.3.0 is the current release of Ogle. | Category | Commits | | --------------------------- | ------- | | Sandcastle (marked [RALPH]) | 107 | | Merge | 67 | | Human | 82 | I find this acceptable. Given that I can implement many bite-sized GitHub issues in parallel, fixing a few misimplemented issues myself is still an increase in productivity. Having used GPT 5.4, Sonnet 4.6, and Opus 4.6, I find DeepSeek v4 Flash to be a sufficient model that costs pennies per feature. But with a stronger model like GPT or Sonnet, I have no doubt the code quality issues would be reduced further. Overall, this is a great method to increase output without sacrificing code quality. --- ## Fractal renders on JWildfire - URL: https://m4t.tf/posts/20260623-1-fractal-renders-on-jwildfire - Published: 2026-06-23 - Description: JWildfire, a free tool for making generative art. - Tags: art, tools Nowadays when you hear generative art, the mind goes to generative AI and the controversy around it - but it has not always been this way. JWildfire is a free, open-source fractal flame editor. Fractal flames are those organic, swirling images that look like a cross between a nebula and a bundle of flowers. You have probably seen them on wallpaper sites and assumed they took hours in Photoshop. They did not. Most were made in JWildfire. ## How it works Open the app, hit the random flame button, and something beautiful appears. You tweak a few sliders, adjust the colours, and iterate. It feels more like sculpting than programming. There are countless variations which are different mathematical formulas that shape the fractal. Each one produces a completely different aesthetic. You do not need to understand the maths to use them. ## What it can do Almost every parameter can be animated over time too.
If the interface is still overwhelming, there is a dice-roll button to generate images with random parameters, and it usually produces something that looks good. ## The catch It runs on Java, so it is not the fastest thing around - especially without full GPU rendering support. Rendering a high-resolution image on the CPU takes patience. You can download it from [jwildfire.overwhale.com](https://jwildfire.overwhale.com) and use it however you like. --- ## Ogle at logs - URL: https://m4t.tf/posts/20260618-1-ogle-at-logs - Published: 2026-06-18 - Description: A terminal dashboard for Docker Compose projects. - Tags: docker, golang, programming, projects, tools, tui If you work with Docker Compose regularly, you know the dance. One terminal running `docker compose ps` to check if things are alive. Another tailing logs. A third for the occasional restart when something inevitably misbehaves. Multiply that across a few projects and you spend more time managing terminals than actually developing. I built ogle to fix that. ## What it does Ogle is a terminal user interface for Docker Compose. Point it at a directory with a `compose.yaml` and you get a full-screen dashboard: live log streams, service states with colour coding, and controls to stop, start, restart, or rebuild any service. ```sh ogle # auto-discovers compose files in cwd ogle -f path/to/compose.yaml # explicit path ``` No config files or setup steps. It scans for known compose filenames, parses the file, and shows you what it finds. If there are multiple valid compose files, you get a picker. If there are none, it watches the directory until one appears. ## Under the hood Ogle is written in Go using the Charm stack, Bubble Tea for the UI framework, Lipgloss for styling, and Bubbles for reusable components like viewports and accordions. Logs stream directly from the Docker Engine API over the Unix socket, so there's no dependency on the `docker` CLI for log tailing. Service actions (stop, start, restart, rebuild) do shell out to `docker compose` since that's the canonical way to run those operations. It handles the messy bits too. If Docker becomes unreachable, ogle retries automatically with a grace period rather than crashing. If the compose file gets deleted while you're looking at it, it enters a watching state and picks things back up when the file returns. Log streams use exponential backoff on errors and stop gracefully when a container disappears. ## Theming It includes eight built-in themes: Solarized, Catppuccin variants, and light and dark defaults. You can also drop a YAML file in `~/.ogle/themes/` to customise over 40 colour properties. It persists your settings to `~/.ogle/config.yaml` so they survive restarts. ## Check it out Ogle is open source under AGPL-3.0. The source is on GitHub at [ma-tf/ogle](https://github.com/ma-tf/ogle), with pre-built binaries for Linux, macOS, and Windows via GoReleaser. If you install from source, it's just: ```sh go install github.com/ma-tf/ogle@latest ``` If you live in `docker compose` all day, give it a try. It won't replace your editor, but it might save you a few terminal tabs. --- ## Yet another blog - URL: https://m4t.tf/posts/20260615-1-why-i-built-yab - Published: 2026-06-15 - Description: The story behind making this blog. - Tags: astro, programming, projects, typescript After using Astro at work and learning about static site generators in my spare time, I created this blog. It was a change of pace from the usual web development I am used to. --- ## Lazy commit messages - URL: https://m4t.tf/posts/20260615-2-lazy-commit-messages - Published: 2026-06-15 - Description: I wrote an agent skill to reduce hassle. - Tags: git, llms, programming, projects, skills, tools Writing git commit messages is an important and incredibly difficult aspect of programming. After making a large number of changes, I don't always want to go back over everything I've changed. Naturally, IDEs allow you to generate messages inline, but even this is not perfect. They do not consistently generate messages in the same format, and configuring agent instructions does not always work. Good commit messages require context you lose the moment you move on to the next thing. You know why you made the change, the diff knows what changed, but translating that into a message that makes sense to someone else - or to you in three weeks - takes effort. ## A better way I wanted something that could look at the staged changes and write a reasonable message automatically. Enter the _commit_msg_ skill. It reads `git diff --cached`, generates a [conventional commit](https://conventionalcommits.org/) message, and prints it. I still review it and hit enter. The agent just does the first pass I wasn't going to do myself. The skill itself is a short markdown file with a clear pipeline: ```md ## Steps 1. Read staged diff 2. Write conventional commit message (see format below) 3. Execute `.git/hooks/commit-msg` against the message if the hook exists (read-only — safe to run) 4. Print it — don't stage, amend, or commit ``` No hidden complexity. The agent reads the diff, writes a message, checks it against your linting rules, and shows you. Step 4 is the important one - it doesn't commit for you. On the topic of linting: step 3 will run your .git/hooks/commit-msg hook against the generated message if one exists. This is read-only; it doesn't modify anything. But it means if your project uses [commitlint](https://commitlint.js.org/) or a similar tool, the agent respects those rules. ## Check it out It is a small tool that does one thing. It won't revolutionise your workflow, but if you've ever stared at a blank commit message box after a long session, it's worth a look. The source is on GitHub at [ma-tf/skills](https://github.com/ma-tf/skills), and you can install it with: ```sh npx skills add ma-tf/skills ``` ---