It has been almost half a year since my last update on clice. Taking advantage of a rare stretch of free time over the May Day holiday, I’d like to write down our recent progress along with some personal thoughts. The topics are somewhat scattered, though they might be interconnected in a few ways.

structured concurrency

From January to March, I spent all my time cleaning up and refactoring, so development on clice itself barely moved forward. What was I cleaning up and refactoring? As mentioned in Design and Implementation of a New C++ Language Server, we wrote our server logic using a “thin wrapper” around libuv with C++20 coroutines. And by “thin wrapper,” I really mean thin—it pretty much just wrapped libuv’s callbacks in C++20 coroutines, which was little more than a syntactic change. All of the initial server logic was written on top of this wrapper, and before long I realized it wasn’t all that different from writing against the raw libuv API. I constantly spent hours debugging bugs in the async logic. The wrapper had very loose lifetime management for coroutines: simple RAII was responsible for destroying the coroutine frame, and whenever a task had a dynamic lifetime that couldn’t be directly expressed via RAII, we would detach it and leave the caller that captured the task to manage it manually. That sounds fine on paper—by analogy with pointers, it’s like managing task lifetimes with unique_ptr most of the time, while falling back to manual release and delete for dynamic cases.

And indeed, for simple cases, writing code this way works well enough. For complex requirements, however, code written with this approach quickly turns into unmaintainable spaghetti. And what clice ran right into is the hardest problem in async programming: cancellation. I consider cancellation the single most critical requirement for clice’s async code. Why? Imagine an extreme case: a user pastes 100,000 lines of code by mistake, immediately deletes it all, and types out just a few lines by hand. For a language server, we only care about the latest text content; that 100,000-line edit is completely obsolete and can be discarded. For a C++ compiler frontend, parsing 100,000 lines of code takes tens of seconds or even several minutes. If we can’t cancel stale tasks, they will keep burning CPU cycles in the background even with a thread pool. The reason clangd sometimes freezes and stops responding is precisely that these outdated compilation tasks aren’t canceled in time, saturating the compilation thread pool and leaving you no choice but to manually restart the server to kill off those pointless background tasks.

In fact, for language servers targeting languages with simple syntax and smaller codebases, simply dropping the results of previous tasks is good enough, with no special handling needed. They don’t need this kind of optimization because parsing is so fast that letting an old task spin for a bit doesn’t hurt. C++ syntax and semantics, however, are far too complex, and parsing in the compiler frontend alone takes a massive amount of time.

And that’s still the relatively simple case: canceling a single task with no dependencies. Traditional C++ parsing operates on a single TU as its basic unit, which fits this model. However, C++20 modules introduce compilation dependencies—a module’s compilation dependencies form a directed acyclic graph. How should canceling stale tasks behave then? For instance, if I write import A, the server starts compiling A’s dependencies (assuming A in turn depends on many other modules). Before A finishes compiling, the user changes the line to import B—how should we handle that? Cascading task cancellation like this is already a headache on its own, not to mention all the intricate and tedious manual lifetime management involved.

The above is about internal async task management within the server itself. In fact, the LSP specification also explicitly defines a $/cancelRequest request for canceling stale requests, but due to the complexity of async task cancellation, most language servers don’t support it.

I spent a long time thinking through and solving this problem. The detailed thought process deserves a dedicated post of its own. Async cancellation is a classic hard problem across programming languages, and different languages take wildly different approaches. For the sake of brevity, I’ll just cover the final solution here. I redesigned an async framework with cancellation as a first-class citizen built on structured concurrency, where all async tasks are wired together into a single async graph. Canceling any node in the graph triggers a cascading cancellation across the entire async graph. For C++20 modules, the async graph directly mirrors the compilation dependency graph. See the diagram below:

What does it mean for cancellation to be a first-class citizen? It means task takes three template parameters <T, E, C>: T is the normal return value, E represents the error return channel, and C indicates that the task was canceled. Because our project does not use exceptions, errors are still propagated explicitly—much like std::expected—whereas cancellation is propagated implicitly. Separating errors from cancellation is the core idea behind structured concurrency. The lifetimes of task instances also follow a strict parent-child hierarchy defined by the graph, solving both lifetime management and cancellation in one stroke. The async graph shown above is a natural byproduct of structured concurrency: at any time and from anywhere in the code, we can dump the async graph for debugging and visually inspect async issues. That, perhaps, is the real beauty of structured concurrency!

kotatsu

To make maintenance and updates easier, I extracted the infrastructure components from clice into a standalone library called kotatsu. The structured concurrency framework mentioned above lives in the async module of this library. Compared to clice itself, this standalone library has much more thorough testing and a far more comprehensive CI matrix, covering combinations of different operating systems, compilers, and build options. Only when the infra layer is rock-solid can we reliably build higher-level logic on top of it. Beyond the async module, I subsequently migrated several other modules over, such as automatic codec serialization/deserialization, the testing framework, the IPC server, and more. The codebase of clice itself is now very lean and consists almost entirely of “business logic,” while the entire infra layer has been moved into kotatsu, redesigned from the ground up, and backed by comprehensive test coverage.

kotatsu has grown into a very comprehensive library, encompassing an async framework, I/O, IPC, HTTP, static reflection, serialization, command-line parsing, and a unit testing framework. It meets every need for writing a language server, and in fact pretty much covers the foundational building blocks needed for general-purpose software development. It is also used as a shared foundation across other projects under clice-io—for example, catter also depends on this common infrastructure. That’s another interesting project, developed mainly by @fogsong233 and @Seele-Official, which complements clice itself; I’ll write a post introducing it when I get the chance.

Looking at it this way, it seems we’ve spent an awful lot of time reinventing the wheel—and that’s certainly true. Objectively speaking, though, we had no choice but to build these wheels ourselves, because the C++ ecosystem simply lacks ergonomic equivalents. It’s a real shame. By contrast, the Rust ecosystem has all of these common needs covered out of the box, which is why so many emerging language servers are written in Rust. People often say C++’s advantage over Rust is its mature ecosystem and long history. From where I stand, however, that’s not really the case—or at least, it requires a more nuanced discussion. Plenty of massive projects are written in C++, such as Chromium, LLVM, Torch, QT, and UE. They have long histories, rich ecosystems, and in most cases, decade-long backing from major corporations—it’s indeed unlikely that a younger language like Rust will surpass C++ in those domains anytime soon. But the real question is: are these really usable ecosystems? Those massive projects are all highly self-contained and poorly modularized; as a developer, I can’t easily pull out and reuse their internal components. For example, the C++ community doesn’t even have a decent glob library. So we ended up rolling our own wheels driven by our practical needs, and I think we’ve done a solid job with modularization. Once the APIs stabilize in the future, we’ll consider cutting official releases for other developers to use.

Note that we didn’t reinvent everything from scratch. In reality, kotatsu still builds on top of many existing libraries in the C++ ecosystem, such as simdjson, toml++, flatbuffer, cpptrace, and libuv. The entire library revolves around just two core abstractions—the new async framework and static reflection—and wraps the APIs of existing libraries around them to make everything ergonomic to use.

clice

Thanks to the brand-new async framework, I could finally pursue a much bolder design: a multi-process architecture! I’ve had this idea for a long time, but our earlier async infrastructure was too rudimentary, so I had to shelve it. The new framework paid off our technical debt and opened up whole new possibilities—it has been a huge win across the board.

Why use a multi-process architecture? clangd has two long-standing pain points: crashes and memory leaks. And 99% of clangd’s memory leaks and crashes come from Clang itself, since clangd uses the Clang frontend to parse C++ code, build the AST, and provide language services. You might wonder: why would Clang crash so often? Doesn’t that sound unreasonable? With all the code out there, I barely ever see Clang crash when compiling my own projects. That’s true—on valid C++ code, Clang rarely crashes (unless you’re pushing heavy template metaprogramming or unstable bleeding-edge features). The problem is that a language server constantly has to deal with incomplete and broken code. For example, compile_commands.json is often misconfigured, causing Clang to miss header files and emit a flood of compilation errors while still trying to continue parsing. When a compiler like Clang encounters an error, it performs error recovery as best it can to keep going and report as many diagnostics as possible in a single pass. However, this error recovery is heuristic—basically educated guesswork—and the vast majority of Clang’s own test suite consists of complete, well-formed code, with very few test cases covering the half-written code states produced during live editing in a language server. Put all of these factors together, and it’s no surprise that Clang occasionally crashes, falls into infinite loops, or leaks memory when running inside a language server.

Because clangd uses a multi-threaded architecture, there is nothing it can do when this happens except restart. In truth, this is also part of C++’s own technical debt. Rust-analyzer also uses a multi-threaded architecture with heuristic error recovery, and its parser can likewise hit corner cases and panic, yet its server process doesn’t crash as a result. That’s because Rust-analyzer wraps each request handler in std::panic::catch_unwind at the request dispatch layer; when the parser or another internal component panics, the panic is caught and turned into an error response, while the process continues serving subsequent requests normally. Rust’s panic can be safely caught by catch_unwind, much like exceptions in C++. In our case, however, not only does Clang compile without exceptions, but bugs like out-of-bounds array accesses in C++ are immediate undefined behavior—typically manifesting as a segmentation fault (SIGSEGV)—with no reliable mechanism for in-process recovery. So the only way to solve this problem once and for all is a multi-process architecture, offloading all compilation tasks to child processes.

That said, while spawning child processes is very fast on Unix systems, it is notoriously slow on Windows—for small files, spawning a process can take longer than the compilation itself. To mitigate this, we use a process pool where each worker process executes multiple compilation tasks (since crashes are still relatively rare events), similar to how Chromium manages its multi-process model. Depending on the nature of the task, we also distinguish between stateless workers and stateful workers to handle different kinds of compilation workloads. With this setup, the only remaining overhead from going multi-process is IPC communication. Because the payloads we send over IPC are tiny, that overhead is negligible—just a few milliseconds—whereas a typical compilation task takes tens of milliseconds and remains the actual bottleneck. Compared to the massive stability gains of a multi-process architecture, that small overhead is well worth it.

On top of building the new multi-process architecture, I also implemented the following features:

  • A fast include-graph scanner capable of scanning up to 20,000 files/s. Compared to traditional header dependency scanning, it skips preprocessing, which means it may over-approximate and include a few extra results. The upside is that we avoid scanning headers repeatedly and can cache scan results, achieving a 100x speedup. For a language server, an approximate include graph is more than sufficient and still provides plenty of valuable information, such as inferring compilation commands for header files from include relationships. In this scenario, speed matters far more than exact precision.
  • C++20 named module support—specifically, module dependency scanning, recursive module building, circular dependency detection, global module name completion, semantic highlighting, and go-to-definition for modules.
  • Compilation context support, allowing any file to switch between different compilation contexts. For source files with multiple compilation commands, you can pick which one acts as the active context. For header files, in addition to choosing among different compilation commands, you can also select a source file as the host context; clice will automatically synthesize the missing preamble from that source file ahead of the header. This naturally provides full support for non-self-contained headers (non self contained).

All of the initial ideas I had envisioned are now implemented, which is very exciting progress. Still, I’m holding off on cutting a new release right now. Why? Because the code is currently at the “it works on my machine” stage—it runs great locally for me, but I know for a fact there are rough edges that aren’t handled properly yet and might break in other people’s environments. If I were to publish a release now, a lot of people might try it out only to find it doesn’t work on their machines. On one hand, that leaves a poor first impression; on the other, it would generate a wave of issues that I’d have to spend time replying to and triaging. Since these are already known issues to me, that would just waste everyone’s time and energy—and I don’t have the bandwidth to field all those issues right now anyway, so I don’t think releasing prematurely is the right move. Over the coming weeks, I plan to steadily iron out these corner cases and invite a few friends to help test things out incrementally. Once the dust settles and all the obvious issues I know about are resolved, I’ll look into releasing version 1.0; the issues reported at that point will be much more actionable.

I’m not sure whether this strategy is the right one. When I talked about this with a friend earlier, his philosophy was to ship an MVP as quickly as possible and let users teach you how to improve it. Looking back, I’ve pretty much done the exact opposite. It’s hard to say who’s right and who’s wrong—perhaps both approaches have their merits and trade-offs. If you’re really interested in trying clice right now, I recommend cloning the main branch and building it locally yourself. As I mentioned previously in Building an Elegant Cross-Platform C++ Development and Build Workflow, we now use pixi to manage our development environment, which gives us great reproducibility and makes building a one-command affair in theory. A while ago, I also added cross-compilation support along the way, so we now support all six combinations of Windows, Linux, and macOS across both x64 and arm64.

agent

Looking back at the past six months, one topic that’s impossible to ignore is agentic coding. Half a year ago, the general consensus still seemed to be that programmers wouldn’t easily be replaced by AI—but with the explosion of Claude Code, everything seemed to change overnight. Suddenly, anxiety was everywhere.

It started around January when, on the recommendation of folks in a group chat, I gave codex + gpt-5.3-codex a try. My initial impression was decent—it was handy for writing tests and the like, though I still had to spend a lot of effort iterating over multiple rounds to get the exact results I wanted, so it felt somewhat underwhelming. When I returned to work after the Lunar New Year, my company provided Cursor for daily work, so I started experimenting with opus 4.6 as a coding assistant on the job. I was immediately blown away. Once I got hooked on opus, the first thing on my mind after clocking out was how to get my own access to opus for working on clice. At first, I bought tokens through third-party API relays, but those relays were way too expensive to sustain heavy usage. An official Claude subscription turned out to be the most cost-effective route by far. After getting two accounts banned, my third account finally survived for the long haul. I subscribed to the 20x plan, and later picked up a second account on the 5x plan to top up my quota, finally achieving “opus freedom.”

Most of the work I described earlier was done by me laying out the plan and handing it off to opus to execute—these days, I barely write any code by hand. That might raise some eyebrows, since the relentless hype around AI has left many developers with the impression that AI-generated code is pure garbage, a stereotype pushed to the extreme by the recent OpenClaw drama. Before long, the internet split into two camps: one arguing that AI-generated code is terrible and we should stick to human intervention or even artisanal, hand-crafted coding; the other arguing that code quality doesn’t matter at all and shipping a product that makes money like OpenClaw is the only thing that counts. This debate isn’t actually new. Long before coding agents came along, people were already arguing endlessly over whether to “agilely” ship an MVP first and refactor the technical debt later, or invest in thorough upfront design and high code quality even if it meant moving slower. The exact same debate plays out everywhere in software engineering—programming language choices, premature optimization, Java vs. C++, you name it. Perhaps this is just old wine in a new bottle, with agents becoming the new vessel for the same old argument. And the answer to that question is just as simple as it has always been: it depends.

Here is my take on it:

First, we need to assess what agents can do objectively and break the stereotype that AI can only churn out garbage code. Take today’s frontier model opus4.6 as an example: as long as you can articulate your intent reasonably clearly, it does a great job generating code. On fundamental coding practices—code structure, error handling, adherence to style guidelines—it is rock solid, and frankly better than most human programmers. Where it still has significant room for improvement is in scenarios that demand complex context: architectural design, code reuse, reasoning about problems that span multiple parts of a codebase, and so on. Anywhere an agent needs to connect the dots across several disparate contexts to arrive at a solution, current models still struggle. That’s not hard to understand: models today have limited context windows and typically read code using a heuristic of guessing keywords + grep, so missing relevant context scattered elsewhere in the codebase is completely natural—to say nothing of the implicit context that only exists inside your own head. Context is all you need. That’s why we see a constant flood of new tools and techniques centered around agent context every single day; of course, most of them aren’t particularly useful, and this space remains wide open for exploration. Right now, the simplest and most effective approach is still keeping a human in the loop: manually feeding the missing context to the agent yourself, especially when working on long-lived codebases.

Second, we have to admit that programming isn’t always fun—it’s also full of trivial and boring work. I didn’t start learning to code until college, and as a non-CS major I was completely self-taught, driven early on purely by the joy of writing code. Back then, I’d be thrilled to write a JSON parser from scratch, and learning how to write a recursive descent parser would make my day. If you asked me to write one by hand today, though, I probably wouldn’t have the appetite or patience for it. On one hand, it takes a lot of time; on the other, the community already has plenty of mature implementations, so hand-rolling yet another one doesn’t mean much.

  • I’d define this category as tasks I could certainly complete myself given enough time, but given my limited energy, there’s simply no need for me to do so by hand. While building clice, I ran into plenty of requirements like this: URI parsing, glob matching, Markdown rendering and generation, and so forth. To me, writing this kind of code is trivial and boring; other languages have had mature, off-the-shelf libraries for them for ages, and it’s only because of the quirks of the C++ ecosystem that we can’t easily pull one in. Using an agent for tasks like these shouldn’t be controversial at all—and agents excel at them, because the required context is tiny, the specification is crystal clear, and virtually nothing goes wrong.
  • Another kind of boring work is debugging. Because clice makes heavy use of modern C++ features, hitting compiler bugs in newly implemented language features is par for the course. The catch is: how do you tell whether a bug is in your own code or in the compiler? Naturally, that takes a ton of effort spent debugging and narrowing things down—and there’s nothing glamorous about it; in practice, it’s just grinding out a minimal reproducer, setting breakpoints, adding log statements, and testing hypotheses bit by bit. I’ve already used opus to investigate and isolate at least four compiler bugs across MSVC, Clang, and GCC. You can imagine how much of my own time would have gone down the drain if I had chased down and debugged all of those by hand.
  • The last category is what we might call CRUD logic. What do I mean by CRUD here? Essentially boilerplate code, like loading configuration files in clice. Strictly speaking, though, most of clice’s own codebase is now CRUD—because, as mentioned earlier, I factored all the lower-level infrastructure out into kotatsu. What remains in the clice layer is almost entirely “business logic,” just set within the domain of a language server. The logic itself isn’t inherently complicated; it’s just a niche domain where agents lack built-in context. As long as I supply that domain context, the agent handles it remarkably well.

At this point, if you were initially strongly resistant to agentic coding, hopefully this perspective makes it much more palatable. We have to acknowledge that once you’ve gained enough experience, repeating the same kinds of implementation work yields very low—and steadily diminishing—returns. In a domain you already know inside and out, developing with an agent will undoubtedly give your productivity a massive boost. Of course, doing so means giving up some degree of certainty: you are no longer intimately familiar with every single line of code. But that situation is universal in software engineering—even without agents, the exact same thing happens whenever you collaborate with other humans. In a module owned by a teammate, do you know every single line of code by heart? Obviously not. You might argue that you trust code written by a human more, which brings us right back to the question of agent capability we started with: trust shouldn’t be based on stereotypes, and human-written code isn’t necessarily any better thought out.

So how do we increase our confidence in code written by an agent? The simplest answer is writing tests—but agents can also cut corners when writing tests, so what do we do about that? There is no silver bullet, but the main practices I rely on are:

  • Use the strongest model available—this is the single most important factor. Right now, the top-tier models are GPT5.5 and Opus4.6.
  • Introduce an agent team where multiple agents take on distinct roles, with dedicated review and acceptance agents conducting multiple rounds of inspection.
  • Bring in more deterministic static analysis tools, defining explicit metrics and custom rules to feed deterministic diagnostics back to the agent.
  • Manually review the generated code and provide targeted feedback for revisions.
  • Introduce broader test coverage—such as smoke tests modeled on real user workflows, fuzz testing, and so on—that is not written by the agent itself.

There are plenty of similar strategies, and they all boil down to the same simple core idea: countering non-determinism with deterministic rules, and giving the agent the most actionable feedback possible. Personally, I find these straightforward practices very effective. The agent space seems to churn out some shiny “new thing” almost every day, fueling a lot of FOMO and anxiety, but under the hood it all comes back to these same fundamentals. I don’t spend hours every day chasing the latest agent trends; I’d much rather use these simple, proven techniques to get my actual coding done faster. Ironically, many of the loudest evangelists of agentic coding workflows haven’t actually written much code themselves and lack real-world software engineering experience, which makes the whole discourse a bit awkward.

As you can see, even though I’ve delegated most of the concrete coding tasks to agents, I still keep a tight grip on clice’s overall design, architecture, and code reviews. A common refrain these days is that “taste” is what matters most in the agent era—but what actually is taste? To me, taste means knowing when to build your own wheel, when to reuse existing code, and how the architecture of a piece of software should be shaped. All of these questions deserve serious thought. An agent can write the code for you, but it shouldn’t do the thinking for you; rather than saying taste has become more important, I’d say maintaining the habit of critical thinking is what truly matters. What agents have really given me is the ability to iterate and experiment at a much higher velocity. When designing the codec module in kotatsu, I scrapped and redesigned it from scratch at least five times; each iteration sparked new ideas and insights, while the agent handled validating their feasibility—something I could never have imagined doing on my own in the past.

Finally, let me show off a little tool I vibe-coded for myself—in fact, the vibe-coding tools I use every day are now vibe-coded by me too. It’s a lightweight multi-task agent orchestrator that makes it easy to run an agent team for cross-review. I can open its web UI directly inside VSCode, and I also rented a cloud server to forward it to my phone so I can check in on the go. It’s been super convenient in daily use.

agent native

After going all-in on agentic coding, a critical question naturally arises: do language servers even have a point anymore? In the foreseeable future, fewer and fewer people will be writing code by hand—so what is the point of clice? When I started working on clice two years ago, I never could have predicted that I’d be asking myself this question today, or at least not this soon. Pouring thousands of hours of blood, sweat, and tears into a project only to watch a paradigm shift threaten to make all that effort obsolete overnight is definitely not a pleasant feeling.

The good news is that there is a silver lining here. As I mentioned earlier, the way AI agents currently read code is a simple greedy strategy: guessing a few keywords and running grep to pull in surrounding context. That approach is universal enough to work across any language, but it is far from the most efficient. In C++ especially, thanks to function overloading and templates, a plain text grep for an identifier often pulls in mountains of completely unrelated matches—all of which is pure noise to an AI. A language server, on the other hand, can provide an agent with exact semantic information and precise symbol resolution. Yet notice one telling fact: Claude Code has supported using clangd via MCP for quite a while now, and almost nobody uses it. Why is that? Because it simply doesn’t work well in practice—if it did, people would already be using it everywhere.

Why doesn’t it work well? At the end of the day, Claude Code’s clangd MCP integration is just a thin wrapper around an existing language server. And the LSP protocol was fundamentally designed for humans working inside an editor. Human developers edit slowly, react slowly, and rely heavily on visual feedback—which is why LSP revolves around autocompletion, mouse-driven go-to-definition, full diagnostic rendering, and the like. The human workflow is all about slow output and careful incremental verification: low throughput, high precision. Agents, by contrast, are characterized by rapid output, frequent edits, and batch operations (Batch)—a workload fundamentally at odds with the design assumptions of LSP. Moreover, a typical agent workflow involves spinning up multiple git worktrees in parallel, a scenario clangd was never optimized for, resulting in redundant indexing across worktrees and exploding memory usage.

What if we designed a brand-new protocol specifically tailored for agents and optimized the server around their access patterns?

  • From a performance standpoint: optimize specifically for batch operations. Support batched symbol lookups and batched file modifications in a single request to avoid rebuilding symbol indices on every edit, and share index caches across multiple worktrees/workspaces.
  • From an interaction standpoint: move to a fully asynchronous, pull-based interactive workflow where the language server exposes rich query interfaces and lets the agent actively pull only the details it needs. For example, a single C++ compilation error can easily span tens of thousands of lines; we can collapse most of the verbose details by default to show only a high-level summary, letting the agent selectively expand the specific parts it cares about. Give the agent ample room to make informed decisions by exposing project-wide structural information such as call graphs and include relationships, and go even further by providing semantic code analysis and refactoring tools so agents can refactor and delete code much more efficiently.

Of course, these are still empirical observations drawn from my own day-to-day experience working with agents and inspecting opus’s reasoning traces. Whether these ideas actually pan out in practice—and how much of a difference they make—still needs further validation. Recently, I added a daemon mode to clice so it can run persistently in the background, allowing an agent to invoke lightweight clice CLI query processes that connect to the daemon to inspect project information—such as querying a file’s compilation command, looking up symbols, or fetching call graphs. You could call this our first step toward becoming truly agent-native: at the very least, querying semantic project information straight from the command line now works out of the box. Next, I plan to have agents use clice while developing clice itself—dogfooding it to further validate both its effectiveness and its practicality.

Note that at its core, clice is a real-time compilation system, and LSP is merely one consumer of that system. Because the response formats suitable for an agent differ quite a bit from LSP, I added a dedicated agentic protocol directly on top of this core system specifically for agents.

conclusion

That’s pretty much everything I wanted to share. To wrap up, let me lay out our long-term goals for clice—consider it a roadmap for my future self:

  • A stable and reliable C++ language server: This remains our original core goal—addressing the many pain points of existing C++ language servers through architectural innovation, while polishing quality-of-life features to make everyday C++ editing much more ergonomic. At this point, the core architecture is largely in place; what remains is mostly polishing and hardening.
  • A flagship showcase for modern C++: We want clice to exemplify modern C++ best practices across the board. So far, it’s shaping up nicely: a ground-up infrastructure library in kotatsu, automatic serialization code generation powered by static reflection, and reproducible cross-platform package management and cross-compilation via pixi. Down the road, we also plan to migrate the entire codebase to C++20 Modules—after all, we are building first-class module support ourselves.
  • An agent-native language server tailored to agent workflows, equipping AI with precise semantic information instead of leaving it to guess keywords + grep.

Progress has been solid so far, though there is still plenty of hard work ahead.

On a personal note about the past six months: last year I started an internship working on AI Infra. It was a field I had no prior experience with, and I’m still learning the ropes every day. AI Infra spans a massive breadth of topics and demands a huge time commitment, which means I can only push clice forward on weekends and in my spare time. Being short on time is definitely a challenge since human energy is finite, though leaning on agents to assist with development has gone a long way toward easing that bottleneck.

I’m about two months away from graduating now, and I’m aiming to get clice ready for its first official release before graduation. Once I graduate and start working full-time, my day job will inevitably demand even more of my focus, leaving even less time for clice. After all, this isn’t a full-time job and it has no external funding—it’s purely a passion project built in my spare time. All I can do is give it my best shot, let the chips fall where they may, and make sure I don’t burn out along the way.

Thank you for following the project. If you’re interested, feel free to join our QQ group at https://qm.qq.com/q/tSD3D81fpu. Thanks for reading!