Skip to content

Blog

To the Moon

Over the past year, our team here at MRS had been working on solutions to two seemingly unrelated problems.

First, some of our projects had grown from just one or two repositories in the beginning to sprawling groups of repos that posed a real challenge for keeping project admin and management best practices in sync. For example, when we began implementing pre-commit hooks as a standard part of projects, we had to copy the same code to each repo.

Sometimes we were able to resolve this with other means. For example, we had implemented an automatic issue triaging bot in many projects with some custom JS code that ran in CI. A few months ago, we took its core concepts and created our simple custom issue bot, which we then imported into each project. Now, the ROI for implementing new features for that bot is much higher because they are automatically applied to all projects instead of having to be ported from repo to repo.

However, many times these sorts of DevOps centralizations take a significant amount of development, and the structure of our team prevented us from dedicating lots of resources to such initiatives. Besides, many administrative tasks, such as configuring GitLab branch rules for each repo, cannot be efficiently centralized.

Second, we began to implement AI into more areas of our development workflow, from patching small bugs to designing and implementing large features to automating code review. This exposed an issue: AI agents frequently did not have all of the necessary context in one repo for implementing or reviewing code because other parts of the system were in different repos.

Additionally, as you doubtless have discovered, the world of AI is moving VERY quickly. Standard practices and tooling change practically every month. For example, when AGENTS.md started to become standard as a tool for giving project-level context to agents, we of course wanted to add one to each project we were actively developing. However, in practice, this meant creating and curating AGENTS.md in each of the many repos. The same thing happened with opencode skills and then standard agent skills: we wanted to add our custom-built skills to each project, but this required quite a few MRs.

Our solution to these two problems has been to migrate our polyrepo projects to monorepos.

Frankly, this has revolutionized our per-repo workflows. GitLab/GitHub admin is simpler. Internal innovations are easily applied to multiple projects. The code changes for a feature that affects more than one part of the project (e.g. new password regex rules made in the frontend and backend for redundancy) can be applied simultaneously, in one MR, instead of requiring developers to create two MRs.

Clearly there are benefits to the monorepo approach, but there are also some drawbacks. For example, consider how you might design the CI pipeline for a monorepo. If every project had its own CI suite when it was in its own repo, how would those be merged together? (Stay tuned for a deeper discussion on this in a later post…) If every project has its suite of common development commands, managed in a justfile, how will those work together in the monorepo?

Additionally, now that we have this monorepo, how will we implement such potential advantages as build caching? If App A depends on Lib A, and Lib A has already been compiled, we don’t want to recompile it every time we build App A. Speaking of which, what about dependency management? How will we define the dependency surface for App A? How will we keep track of that dependency graph once it’s been implemented?

Recently, we came upon a tool called moon that solves all of these new problems.

moon is built to help teams efficiently and effectively manage monorepos. There are other tools out there for this, such as nx and turborepo (which we would encourage you to try for yourself), but we chose moon because of its toolchain-agnostic nature and simplicity of setup. Where other tools are optimized for JS/TS-focused repos, moon is fully equipped to help with projects of all types. We are using it for Flutter mobile apps, embedded Qt/C++ apps and libraries, Go tools, and, yes, JS projects.

The impetus for adding moon to our monorepo workflow first came from a need to scale our per-project justfiles. For example, if each project has a deps recipe for installing dependencies, then how do we run those efficiently? While just does support invoking recipes in subdirectories, this doesn’t scale well. How do I quickly install dependencies for a bunch of projects? How do I install all dependencies for all projects without knowing the locations of every single one?

moon solves this problem by scoping tasks (their name for recipes) by project, the list of which is defined at the top of the monorepo.

You go from this:

Terminal window
just app1/deps app2/deps libs/lib1/deps libs/lib3/deps tools/deps

To this:

Terminal window
moon run :deps

Each project can define its own deps task…or you can define a common deps task for certain projects. If a monorepo has 6 Dart packages, all of which use the same set of static analysis tasks, you could define .moon/tasks/dart-lib.yml like this:

# These tasks are inherited by Dart libraries.
inheritedBy:
layer: library
language: dart
tasks:
deps:
command: dart pub get --enforce-lockfile
check:
script: |
dart fix "$projectRoot"
dart format --output json --set-exit-if-changed "$projectRoot"
fix:
script: |
dart fix --apply "$projectRoot"
dart format "$projectRoot"

This completely solves the problem of duplicating tasks between lots of repos; now, those tasks are defined in one place. You can run them locally, in pre-commit hooks, or in CI. Plus, moon automatically generates a project dependency graph and full list of tasks, so that you can see all the available tasks with one moon tasks command.

For more references:

One of the benefits of moon that we weren’t looking for at first but soon found to be extremely useful is its automatic task caching. When you run a task, moon will cache the results and output locally, hashed by the state of the project at the time of the run. Then, if you do not change any relevant files before running the task again, the cached results will be used; if relevant files did change, the task will be fully run again.

The caching behavior of moon is extremely configurable; you can tell it exactly what file changes should trigger a rerun, what files should be treated as build artifacts, what other tasks should be treated as dependencies to force a rerun, and so on.

This caching behavior actually has a very nice application in CI environments. The cache is stored in .moon/cache, which means that you can configure your CI to store that directory in the runner cache and then use it in other jobs. At MRS, we had done this on a lesser level with techniques like NPM and Go caches, but moon provides another level by effectively caching entire jobs and their outputs.

For more information:

Another great feature of moon is its ability to run tasks only when relevant files changed, as determined by the VCS. This concept is similar to how cache hits and misses are determined, but it has an added benefit when running a bunch of tasks at once.

By tracking which tasks have been “affected” by changed files, moon can avoid running a task at all if no input files have changed. You can run something like moon run :check --affected and it will run the check task for only those projects which have been modified.

This is extremely useful in monorepos for things like Git hooks and CI. For example, if every project has a check job defined, I can define a simple pre-commit hook that runs moon run :check to make sure all static checks run before commit. I can do the same thing in CI—but this isn’t efficient in the slightest, nor is it good practice. I want my CI runs to be scoped to the project I changed. If I am working a new feature for app1, then I don’t want or need to run the entire unit test suites (task test, for example) for app2 and lib1. So, I use moon run :check :test --affected in my CI pipeline, and presto! All the static checks and unit tests suites for the project(s) I’m working on get ran without any extra configuration.

In fact, this is such a core use case of moon that they have a dedicated moon ci command with extra options.

For more information:

Monorepos are a great way to address the problems that come with having many repos for one project. They are especially well-suited for today’s increasingly AI-focused software development cycle.

However, monorepos pose unique challenges, and we have found moon to be the tool for resolving them. It does a great job of preserving the things we love about just in smaller repos while providing advanced features for effectively managing and developing in a monorepo.

TO THE MOON!! 🌑 🌘 🌗 🌖 🌕

Docs-Driven Infrastructure

Established infrastructure can often become a black box. You don’t know how it was set up, and there is no documentation. The person who set it up isn’t around to answer your questions or has forgotten the details because it’s been five years. How can you set up new infastructure with good documentation to go along with it?

Deployment scripts or infrastructure as code are a good way to aim for reproducible infrastructure1, but often you are setting up specific infrastructure that only needs to be stood up once. What you really need is a simple guide describing the steps.

Instead of asking your coding agent “How do I set up X?”, give a prompt of the form “Write a README explaining how to set up X”. Then follow the steps in the new documentation. If you run into something incorrect or confusing, revise the documentation and try again. In the end, you should have a guide that accurately describes how to do it, provable by the new infrastructure that you just set up using the guide.

Not only do you end up with a helpful setup guide for the new infrastructure, you also end up with accurate documentation for guiding yourself or other team members when you need to replicate or maintain the infrastructure in the future. It’s a great way to kill two birds with one stone.

  1. Personally, I’m a big fan of Nix flakes.

Guiding Your Agents

AGENTS.md files are a great way to persist long-term “memory” for your coding agents. However, it’s easy for them to become bloated and/or outdated. A recent study has shown this is actually worse than having no AGENTS.md at all.

There is no need to list your tech stack, file structure, or list of just recipes in your AGENTS.md. This is redundant and can be harmful to your agents’ effectiveness when this information becomes outdated. Modern coding agents can quickly figure out the essential details of your codebase for themselves. If you put these details in your AGENTS.md file, you are wasting context space and putting yourself at a high risk of documentation rot - when you update your tech stack or file structure, will you remember to also update AGENTS.md?

In our experience, the best usage of AGENTS.md is for recording guidelines that help your coding agents stay on the right path. Think of it as building some guardrails for your agents. When your agents make mistakes, be sure to update the guidelines to help point them in the right direction for future work.

We’ve found that the following three-tier system works well for organizing agent guidelines:

  • Always Do (no asking)
    • A list of things the agent is allowed to do (and must do) without asking.
  • Ask First (pause for approval)
    • A list of things the agent should pause and ask for permission for.
  • Never Do (hard stop)
    • A list of things the agent should never do.

Sometimes you need to record facts that don’t fit well into the three-tier guidelines system. For these, it works well to add a Long Term Memory section to your AGENTS.md. Be careful to prune this list frequently. Often your agent will want to add items here that provide no value for future situations.

It’s easy for your AGENTS.md to become polluted with information related to specific workflows or tasks (for example, running automated tests or accessing GitLab issues). Agent Skills are a much better fit for this. Whenever possible, move non-essential information from AGENTS.md to your skill files.

In general, be very strict about what is in your AGENTS.md file. It takes up valuable context space and biases every coding agent session. Be sure that your guidelines provide positive benefits with useful guardrails rather than redundant or outdated documentation.

Just Is Just Great

We work on software projects across a variety of programming languages and build systems. Some of them provide an easy way to run development commands (for example, npm run), and others do not.

You need some way to answer the questions “How do I set this up?” and “How do I run this?”. You need some form of documentation of common commands.

It’s easy to write this documentation once and then forget to update it as your project grows.

We needed a system that we can use on any project, no matter the language or build system. Something that would keep the development commands and related documentation in sync.

just is just great because it solves several problems at once while keeping things simple. You only need one simple file and one simple command. just is a command runner that reads all your common project commands (called recipes) from a single file. You can also add explanation comments above each recipe and just will automatically include them in the help output.

For any of our active projects, a new developer can run just to get a list of the available recipes. Our core set of recipes includes:

  • default - to list recipes
  • deps - to install dependencies
  • setup - to run all required commands to set up a local development environment

just automatically runs the first recipe of your file if you run just without any arguments. We take advantage of this by always putting the default recipe at the top, and have it list the available recipes with just --list.

Many of our projects also have a test recipe for running the automated test suite. You’ll also often see at least one of dev, up, run-mobile, or run-docs for running the given project in the local development environment. We use lint and format recipes to trigger the correct linter and formatter for the project’s programming language.

Here is a minimal example that demonstrates all of our core recipes, along with a dev recipe. This is actually the current justfile from our docs-template repository.

# List available recipes
default:
@just --list
# Install dependencies
deps:
npm i
# Set up development environment
setup: deps
pre-commit install
# Run in development environment
dev:
npm run dev

The comments above each recipe are automatically included in the output of just --list:

Terminal window
Available recipes:
default # List available recipes
deps # Install dependencies
dev # Run in development environment
setup # Set up development environment

Why we’re adding a justfile to each of our active projects

Section titled “Why we’re adding a justfile to each of our active projects”

Having a justfile in every active project means that new developers can get started quickly in a project they’ve never worked in before. The “muscle memory” of just to list available recipes and just setup to set up the local environment makes things very convenient.

We can onboard developers to projects quicker, and we can spend more time focused on writing code rather than internal developer documentation.

2025 in Review

In 2024 and early 2025, we faced the departure of four well-regarded colleagues. It was a lot to absorb, and we started out 2025 with a good amount of uncertainty. There were fewer engineers per project and less review bandwidth to go around. We also lost a lot of accumulated expertise. How would we establish ourselves moving forward?

We did not want to compromise on the quality or consistency of our output, so we focused on leverage. For us, that meant using standardization and automated tooling to amplify our work. We also started an exciting open source initiative, which acted as a big morale boost.

a smooth and consistent path is easier to walk than a rough and chaotic one

Standardization was a big focus of 2025. Each project did project management differently, and there was no clarity for what to use for new projects. We also had a generally low bar for documentation across most of our projects.

In 2025, we made great progress in standardizing our project management practices. We have developed a set of best practices through trial-and-error across different projects the past few years, but they had only been shared through ad-hoc communication between engineers. It was time to document the best practices and apply them consistently, so every active project could benefit from a more efficient workflow. This improved our workflow on existing projects and made it much easier to get started with new projects. Before, there was uncertainty on how to configure each setting for a new project, leading to decision fatigue and wasted time. Now we have a pre-set package of settings we have discussed, tested, and know work well for us, allowing us to configure a new repo within minutes.

Of course, every project has its own unique constraints, requirements, and team. We don’t have a rigid system that we treat as law. Instead, we have developed a set of general project management best practices including things like: how to label issues, how to merge changes, how to handle branching, and how to handle deployments.

To make getting a new repository off the ground very easy, we created internal checklists for both GitLab and GitHub. These cover things like: branch protection rules, repository settings that we change from the defaults, and setting up integrations like the code review bot. We also created a couple helpful repository templates for our GitHub projects. One focused on docs repositories - docs-template, and one for any new open source repository - basic-template.

We open sourced the issue-bot, our internal tool for helping us manage issues by reminding us of things like missing labels or incorrectly formatted issue titles. Currently, it only is available for GitLab, but in the future we would like to add support for GitHub. We are also interested in adding support for GitLab Statuses (replacing our current practice of using scoped status labels) and finding interesting use cases for GitLab Custom Fields.

Going forward, I’m excited to experiment with using the Specture System for some of our projects. Specture is based on some basic strategies I’ve found to work well with AI agents after some experimentation in the past few months. The basic idea is a spec-driven approach, where designs are documented in the git repo in simple markdown files, rather than scattered across GitLab or GitHub issue descriptions and comments. It seems like it will work best for projects with very small teams and a lot of AI agent usage, which is a good match for many of our projects.

Another area of focus for us in standardization was in raising the bar with our documentation practices. We embraced the Astro framework early on. We found it to be a great way to write documentation in Markdown and quickly deploy a static site.

Our current strategy is to have a docs directory in each new software project’s repository for storing the Astro docs for that project. We’ve found that it works really well to have the documentation for a project in the same repository as the code. The goal is that every time a pull request includes an important update or new feature, that same pull request would include the corresponding documentation update.

We also created GitHub repositories dedicated to documentation for several of our hardware products (for example, neuraplex.dev and mconn.mrs-electronics.dev). Our docs-template repo serves as the starting point for these repositories.

We deployed mrs-electronics.dev as the home for our public-facing developer content. We use subdomains for the docs of our different projects, for example: qt.mrs-electronics.dev and mconn.mrs-electronics.dev.

In 2025, we made a lot of good progress in standardizing and establishing our documentation practices within the software development team. However, we still have a lot of work to do. Establishing documentation is one thing, but keeping it up to date is another. We also hope to share some of the things we’ve learned about writing and deploying good documentation with others at MRS outside our team.

shortens feedback loops, so developers move faster with confidence

From the start, we saw the need to scale our team’s capabilities. Automated tooling is a powerful tool for leverage. We can automate the tedious and time-consuming tasks so we can focus on creative and high-impact work.

The following three sections cover three different feedback loops commonly encountered in software development: integration, local development, and implementation. We’ve found interesting ways to apply automated tooling in all three loops to shorten the cycle for each.

It’s important to reduce the feedback loop for the integration cycle. You can have developers producing all kinds of great code, but if you don’t have a good system for reviewing and merging new code quickly and efficiently things will quickly get backed up.

Early in the year, we did some team training on Docker and containerization. This discussion led to much more widespread usage of Docker containers and CI/CD pipelines across our projects. Our CI/CD pipelines protect us from all kinds of mistakes. We run linters, formatting checks, and automated test suites on every commit to most of our active projects.

Another place that CI/CD can have a big impact in is deployment processes. Our web projects and docs sites have automated pipelines for every commit to main, and we have pipelines that automate the time-consuming process of building APK and AAB files for each new release in our Android projects.

We also started work on our Code Review Bot. This runs on each new pull request for most of our projects. It allows us to shorten the code review feedback loop - a human review might not be available for several hours or days, but the code review bot can give basic feedback within a few minutes. It’s not perfect, and we have a lot of ideas for how to improve it in 2026, but it caught many silly mistakes for us in 2025.

High quality tooling for local development is essential for rapid iteration. We don’t want to have to rely on manual human checks for all our work. It is much quicker to have automated tooling that can check our work before each commit and push.

One essential piece that we’ve begun introducing to all our projects is just. It allows us to have a self-documenting place for configuring all the common commands for a project. This is very useful for enabling new developers to get started quickly with a project - they list the just recipes and find what they need.

Most of our projects have a lint recipe and format recipe in their justfile. These basic tools are essential for developing consistent code as a team. There is no reason to argue about code formatting - just use what your formatter produces.

It is also very convenient if developers don’t have to remember to run the linter and formatter themselves. We have found the pre-commit framework invaluable for configuring Git hooks. It can run the linter and formatter on staged files for every new commit, and also check for things like trailing whitespace and unwanted large files.

As a learning experiment, we started using Go for a few projects (time-tracker and mrs-sdk-manager being two notable examples). We found the superb built-in tooling to be a breath of fresh air compared to what we are used to in older languages like C++ or Python. Go has a built-in formatter, testing framework, and package manager, which makes it very easy for an inexperienced developer to get started with new projects without getting bogged down in a complicated ecosystem.

And now we get to the inner loop of software development. How do you take an idea from your brain to code? The past year or two has seen the rise of a brand-new way to convert ideas to code much faster - LLM-powered coding agents.

We’ve found coding agents to be helpful in many ways. They allow us to quickly prototype new ideas and explore new possibilities. Tedious refactors and writing boilerplate or glue code takes much less time. They also can be a great help in debugging tricky errors.

A great side benefit of embracing coding agents is that they thrive off of many of the same things as human developers - high quality documents, standardized development tooling, and good test coverage. When we invest in these things to help reduce the number of mistakes our agents make, we also make life better for ourselves. There is no excuse to have poor test coverage when a coding agent can quickly write you a bunch of test cases.

We tried out several different coding agents, adapting as new and better tools hit the market. Our first experiments were with Aider. It was a great introduction to having an agent with direct access to your local filesystem, but it was a bit tedious to have to manually introduce each new file to the agent. OpenCode was our next tool of choice. It is a great open source TUI for coding with LLMs. Tools like grep and bash commands really streamline the experience as compared to Aider. Amp is our current favorite. It likes to go through tokens quickly, but their ad-supported free mode allows a generous $10 of access per day. The main drawback is that it is proprietary and relies on their cloud servers, but it provides nice extras like shareable threads and workspaces. The main reason we like it is that it just seems to work. Amp seems to take the least amount of trial and error to get decent results.

OpenRouter was invaluable throughout the year. It provides an easy and effective way to access any model we want, based on the needs at hand. We used it for Aider, OpenCode, and our Code Review Bot. I like to think of the overall integration of LLMs through OpenRouter and coding agents like Aider or OpenCode as similar to a brain and a body. We can switch out the brain (the model requested from OpenRouter) based on what we need for the current task - a more expensive model like Claude Sonnet for a more challenging problem, and a cheaper model like Gemini Flash for simpler tasks. We can also switch out the body (the coding agent/LLM interface) as required - maybe OpenCode for implementing a new feature, and our Code Review Bot for reviewing the code.

It has been interesting to see how our coding habits adjust as we acclimate to using code agents regularly, and I’m sure we will continue to see lots of big improvements in the space in 2026, which will require further adjustments. One thing we have found very useful is to have a good AGENTS.md file in each repository. This is a good place to store LLM “memories”. After the coding agent makes a mistake, have it record the correct method of doing things in AGENTS.md. Most tools, including OpenCode and Amp, will automatically load AGENTS.md and this helps tune your agents in the correct way to operate on your projects.

a shared place for fixes and features

In 2025, we made our first steps into publishing open source software. Our open source projects provide us with a variety of benefits, including being a better way to distribute common shared code, and a good creative outlet for our developers.

Our biggest and most ambitious open source project so far is the MRS Qt SDK. We envision this to be the first of many SDKs, each focused on a different language and/or framework. The Qt SDK is targeted at the immediate need of our developers and our customers to have a solid foundation for new Qt applications for our embedded Linux hardware.

Our previous solution for shared code across different Qt projects was copy-and-paste between various repositories. This was far from ideal. Bug fixes and features would get introduced in one repository and never make it to other repositories. A centralized SDK should give us a single source of truth for Qt code optimized for working with our hardware. We can have a central place for applying bug fixes and new features, and then developers both internal and external can pull those improvements into their applications. It is a great way to reduce duplicate code across projects, and our improvements can have a larger impact as they multiply across projects.

We also started work on several helpful open source tools for improving our efficiency in our day-to-day work.

time-tracker is a simple app written in Go that provides both a CLI and TUI for quickly recording time entries. This is very useful for tracking the time we spend across all our different projects. We hope to introduce a web interface soon, which should make the app accessible from even more places.

bots is a collection of CI/CD tooling that we use across many of our projects. It currently consists of an Issue Management Bot and a Code Review Bot, and we plan to add more bots in the future to automate other parts of our software development process. Like the Qt SDK, the bots codebase is based on code that we had developed and copied-and-pasted across several projects. Having it put together in a central place with automatically built Docker images makes it much easier to maintain and distribute across projects. The bots are a great way to reduce time spent on tedious or time-consuming project management tasks, allowing our team to focus on high-impact work.

2025 was a year of big adjustments. We bore the grief of departing team members and faced uncertain prospects. We had to find creative ways to leverage the time and effort of our team to make an outsized impact. Standardization, automation, and shared open source codebases all helped to improve the effectiveness of our team, reduce inconsistencies between our projects, and shorten feedback loops. It was an exciting year of growth, and we look forward to finding more ways to continually improve our work in 2026!