Most stories about AI-assisted coding are about web or phone apps, where the whole world of the program lives inside the computer that runs it. Control software is different. Half of the system is a motor, an encoder, a limit switch connected to a controller at the far end of a network cable that has its own opinions about timing. The software has to handle everything that can go wrong in a messy physical world.

This is an account of building that kind of software with an AI: motion control software for the Thirty Meter Telescope’s alignment system, taken from a first question in December 2025 to a component verified on real hardware, with the full instrument control layer above it running against a simulator, by the end of July 2026. One engineer, part time, and Claude, in what became a tech lead and junior developer relationship.

That relationship was not designed up front. It was learned one session at a time and written down by the AI in a project memory that it maintains and I review. The memory, and how things got into it, is the core of this article. I have been struck by both the AI’s ability to do great work and its capacity for really poor judgment. If you take one thing away from this account, make it this: read every word of the code the AI produces (and the comments!), and review its memory regularly. If you are weighing a large AI-assisted project, I want you to come away with a method you could adopt, and a clear view of where it strains.

1. Astronomical motion control

The Thirty Meter Telescope will have nearly five hundred hexagonal mirror segments that must work together as a single reflecting surface. The Alignment and Phasing System (APS) is the instrument that measures and corrects that alignment, and like any optical instrument it is full of mechanisms: stages that move, wheels that select filters and masks, and a mirror that rotates to follow the sky.

Galil motion controllers drive those mechanisms. Each controller runs several motors and executes embedded programs on parallel threads inside the controller. Between the controller and the rest of the observatory software sits a Hardware Control Daemon, or HCD. It accepts requests like “move this stage,” configures and calls the right embedded program, and reports what the hardware is actually doing. Keeping the motor-specific details in the embedded layer makes the HCD a reusable component.

The Galil motion control solution. Assemblies command a standardized HCD through the observatory framework; the HCD talks to the controller over TCP, where customized embedded programs carry out the motion. (From the Software Design Description.)
Figure 1. The Galil motion control solution. Assemblies command a standardized HCD through the observatory framework; the HCD talks to the controller over TCP, where customized embedded programs carry out the motion. (From the Software Design Description.)

I had completed the HCD design and its interfaces, which had just passed an external final design review before I decided to start this AI development experiment. I had planned out user stories, but hadn’t yet learned to think in terms of context and continuity. I began with a question, “Can you even help me with this?”, and learned the way engineers usually do: by making mistakes and finding solutions.

I am very proud of the final product. It is the full realization of my design, only better, because with Claude the stretch goals were very attainable: the advanced simulator, the engineering user interface (Figure 2), and the motion assemblies I built along with the HCD.

The engineering display, shown here with eight simulated axes. Motor status appears at the top, digital output buttons in the middle, and reported position histories below.
Figure 2. The engineering display, shown here with eight simulated axes. Motor status appears at the top, digital output buttons in the middle, and reported position histories below.

The hardware was a four-axis controller in my home office and the eight-axis Stimulus Test Bench (STB) in the lab, reached over VPN and remote desktop. The STB contains actual servo and stepper motor stages from the opto-mechanical design. The HCD has been driving the seven motors on the STB with months of stable uptime, and it has been exercised by an extensive unit test suite and by full stack load testing.

2. The shape of the effort

I worked on this part time, roughly twenty hours a week. Between December 15, 2025 and July 29, 2026 the record shows seventy-seven active days spread over thirty weeks. Most goals took a working day, sometimes two: implement tracking, fix a homing timeout, add travel limits.

The rhythm of part-time work. Weekly messages, tool calls, active days, and Claude Opus releases across the whole project. These are activity counts, not a measure of productivity.
Figure 3. The rhythm of part-time work. Weekly messages, tool calls, active days, and Claude Opus releases across the whole project. These are activity counts, not a measure of productivity.

A typical session starts with the project’s current state loaded. I describe the goal. We discuss the design and I give direction. Claude writes the code. I compile it, paste back any errors, run the tests, and paste the logs. If the work touches hardware, I test it on the small controller in my office or on the eight-axis test bench in the lab.

When something fails, the process we worked toward is diagnosis before repair: gather the evidence, prove the cause, change the code, and write a test that fails without the change. At the end of the session, Claude updates the project memory, and I review the code in detail before committing.

The tools changed along the way. At first I uploaded a zip of the code at the start of each session and copied changes between the chat and my working files. From July 8, Claude could edit files directly in a folder on my computer, which was a big improvement.

This was the smooth, efficient workflow that evolved through the project. It represents a combination of my learning how to manage context and Claude learning how I like to work and anticipating me.

Things didn’t start so smoothly.

3. How the working relationship was learned

3.1 Fifteen days in one chat

The first conversation ran from December 15 to February 11, fifteen working days in a single chat window. It felt like the natural way to work, since everything we had said was right there. It wasn’t working. Claude was losing track of its own earlier work in the same conversation, and I was correcting the same mistakes repeatedly. A very long conversation is not a memory. It is a haystack the model has to search, and it does not search all of it.

On February 11 the chat became inaccessible after a product update. A new conversation could recover fragments but not the working files. I uploaded the code and design document again and asked how we could keep context from one day to the next over several more months. Claude proposed two things: a Project, so the design documents stay pinned, and a living project-state document it would update at the end of every session and read at the start of the next. The first version existed five minutes later.

3.2 What we started with

That first state document held status, architecture, next steps, technical lessons, and one process rule, which is still the first line of the project’s memory: “Code is the source of truth: always check the actual source, not documentation or memory of what was implemented.”

My first mentor in controls software taught me that decades ago. On operational systems, problems can be fixed without updating documentation, and the only way to know what a system actually does is to read the “source code.” It’s the first thing I teach every junior engineer, and Claude had just given me fresh reasons to teach it again.

The rest of our working practices weren’t written down yet. They were things I did in conversation: asking for a design discussion first, stopping a speculative fix, refusing a workaround that hid the real problem. In the beginning I had to do all of that fresh every session.

3.3 What the memory learned

Every version of the state document was written by Claude, fifty-four of them in the chat era alone. I have never edited one by hand. I read each one at the end of the session and correct it in conversation, the way a lead reviews a junior’s write-up. That review is not optional. A document an AI writes about its own work drifts toward tidiness unless someone who was there reads every word.

Read in order, the versions show the relationship forming. On March 18 Claude began recording its own recurring failures: applying a fix to a stale file, editing a file it had not read, trusting documentation over source. In the April 6 version, written after a week in which I had repeatedly stopped it from fixing things it had not diagnosed, process rules appear for the first time, written as observations about me:

Root cause before solution: Angelic explicitly stops implementation when solutions are based on speculation rather than evidence. Diagnosis must precede fixes.

No workarounds that mask real problems: Angelic prefers understanding and documenting root causes over optimistic UI hacks or timing-based patches.

On April 13 these got their own “Working Practices” section. Through May and June the list kept growing in that register: what Angelic does, what Angelic wants, what went wrong and should not again. It was a junior’s notes on a new lead.

3.4 The working agreements as they stand

In July, when the memory was restructured (section 5), those observations were compressed into a short section stated as agreements between us:

Working agreements. Design discussion before implementation; Angelic decides scope, naming, and architecture; Claude proposes and implements. Minimal targeted changes; no workarounds that mask root causes. Architecture decisions get an in-repo ADR. Empirical verification: hardware and simulator results drive decisions; Angelic runs the build and pastes logs. Delivery: Claude edits straight into the connected working tree; Angelic reviews the diff and owns all check-ins.

Nothing in it was new. Every clause had been in the document for months as a description of me. Alongside it sits a list of verified learnings, one-line rules each tagged with the session that produced it. The five stories that follow are where some of those lines came from.

4. Five stories

4.1 Nine green tests over a placeholder

As the engineer, I want the HCD to poll the controller for status, so that everything above it sees what the hardware is doing.

From the first message, the division of labor was settled: it writes, I run. By early February I believed the status monitor, which asks the controller what it is doing ten times a second, was at least partly working. Claude’s description said so, and nine passing tests appeared to confirm it.

On February 10, while chasing a startup hang, I asked: “Do any of our tests actually verify that the status monitor starts polling? Looking back, I realize that when we tested this, I never saw simulator activity.”

Claude inspected the code. The timer fired every hundred milliseconds. Where the request to the controller should have been, there was a TODO. Nine green tests had been exercising a loop that never asked the hardware anything. The same afternoon, Claude told me a download routine was unimplemented when we had already written it and tested it on hardware. The forgetting cut both ways: finished work went missing, and unfinished work looked finished, with tests to match.

From then on I read every change line by line, and one question recurred for the rest of the project: does this test exercise the feature, or only the software’s bookkeeping?

What went into the memory. “Code is the source of truth,” the one process rule in the first version the next day.

4.2 The display must never lie

As the engineer, I want the engineering page to show what the controller is doing, so that I can trust it on the bench.

The engineering page has a panel of digital output buttons. A click sends a command, the controller reports its new state on the next status poll, and the page updates. On March 23 the buttons started going unresponsive: I could click through several outputs and watch each change, and then for a while none would respond.

Claude’s first theory was display lag. Its second was a bug in the page’s own state handling. Then, without confirming either, it implemented an “optimistic” update: flip the button the instant it is clicked and let the controller’s reported state overwrite it whenever that arrives. It is a common technique in web applications and it makes them feel responsive.

“No. I absolutely do not want to fake the output in the interface. No no no no no.”

For anyone who has not worked around hardware, my reaction may seem out of proportion. The engineering page is the window into the controller. An operator looks at that panel to know whether an output is energized. If it shows what was requested instead of what the controller reported, an output that failed to switch looks switched, and the failure I was chasing would disappear from view. I have spent a career around interlocks and shutters. A display that reports intent instead of state is the kind of thing you build systems to prevent.

The logs showed that when the buttons went dead, no command was being sent at all, so this was not a display problem.

“If you have a root cause, present it for discussion. Do not ever skip past it and present crude workarounds that might mask a real problem. Do not decide for me what is too complicated to address immediately.”

Claude reverted the change and diagnosed. The command handler blocked for up to five seconds on each button press while it waited for the controller’s reply, so clicks arriving in that window queued and were lost. That was a real design problem, not a five-minute fix, so I had it recorded as an open item and addressed it later as its own piece of work.

What went into the memory. The two April 6 lines quoted in section 3.3, and on April 13: “Angelic decides what is too complicated to address immediately; Claude should not make that judgment unilaterally.” The display requirement later became a rule of its own: “the display shows state, never intent.”

4.3 Meeting the hardware

As the engineer, I want the HCD to bring up the real test bench, so that I can trust it with real mechanisms.

The simulator got us to the door. Hardware opened it. Through April and May the HCD met the real controllers, and the hardware exposed assumptions no software test had challenged.

Downloading a program to the controller failed intermittently. Claude proposed retries, pauses, and changes in pacing. Each sounded plausible from the software side, and none established the cause. Two kinds of evidence settled it. The controller’s reference manual specified an eighty-character line limit, and longer lines were being silently dropped. Then, because I wanted the real protocol and not only the documented one (“My instinct is to figure out what the real protocol is”), we instrumented the connection and watched the bytes. That is how we found that the end of a download is acknowledged with two characters, not one. The manual said what the controller was supposed to do; the trace showed what it did. We needed both.

What went into the memory. The April 13 version had it as an observation: “Angelic runs manual tests on the controller, checks hardware command output, and cross-references the Galil manual before accepting any explanation.” By May it was a rule: “Read the reference documentation before theorizing.”

4.4 Prevent it, don’t recover from it

As the engineer, I want every mechanism on a controller to move independently and simultaneously without intermittent faults, because reliability is critical for observatory operations. (So that I don’t get woken up in the middle of the night to investigate).

By July the HCD could drive individual mechanisms reliably. The question was whether it could drive all of them at once, so we built a load generator that commands every assembly at the same time and ran it against the simulated system. Every so often a motor would stay “homing” after its homing program had finished.

On July 6 at 9:46 in the morning I pasted the revealing logs and the current code. At 9:55 Claude’s reasoning, which the tooling of that era recorded, reached the correct diagnosis: “I found the bug: thread 1 is reused for HomeC while HomeB is still running, clobbering B’s thread mapping and leaving it orphaned in Homing state.” The controller had reported a thread free, and the software handed it to another motor before accounting for the previous program finishing. In the very next thought: “have RegisterThread detect when it’s about to clobber an existing entry for a different axis, and synthesize a completion event for the old axis.” By the time the reply was written, that was “Fix 1 (primary).”

At 10:10 I wrote: “I would love to find a solution that prevents the clobber in the first place, rather than recovering from it.”

Two minutes later Claude laid out the rule the HCD runs on today: never assign a thread to a new command until the software has seen and accounted for the previous one finishing. Hardware-free is necessary but not sufficient.

Those fifteen minutes are the clearest example in the record of why the lead matters. The diagnosis was right. The instinct, having found a corruption, was to repair its effects. Sometimes recovery code is necessary, but here it would have meant cleanup paths around a bookkeeping error that did not need to happen, and every cleanup path is another place for the next bug to hide. Years of control systems work is what let me recognize that fork in the road.

The sequel carries the bigger lesson. The same boundary, between the component that watches the controller and the component that tracks what each thread is doing, kept producing races. We patched four. When a fifth turned up in mid-July, I called a halt: a seam that needs that many fixes does not have bugs in it, it has a design flaw. We wrote a decision record and rebuilt the boundary so that only one component may decide what a controller thread has done.

All 271 unit tests passed. Then we ran forty seconds of every mechanism stopping and starting together and audited the full debug logs line by line. The load test found a race no unit test could have shown: status scans delivered up to 1.4 seconds late were being matched against threads that had since been released and reassigned. The fix was to make every observation carry the time it was taken. The log audit found three more defects, including one introduced by an earlier patch on that same seam, which had quietly wasted a controller thread ever since. The final runs were clean: zero misattributions across more than five hundred completions.

What went into the memory. “Prevention over recovery.” “Repeated fixes in the same seam mean a design flaw; redesign, with a decision record, rather than patch again.” “Observations must carry their observation time.” And the definition of done the document still carries: “Unit tests plus one clean load run is not done; load test plus forensic log audit is.”

4.5 Guards that hide failures

As the engineer, I want a broken feature to tell me it’s broken, not quietly disappear.

On July 27 we found that the engineering page had carried a position chart since it was first built, and it had never worked. Nobody knew, because nothing ever said it was broken. Two independent faults each hid it. The history recorder restarted its half-second timer every time the four-times-a-second status update arrived, so it never fired. And the charting library needed a companion library the page never loaded, so it failed silently on startup. Neither fault was ever reported, because the page checked whether the charting library was available and quietly left the plot out when it was not. That check had been written as graceful degradation for an observatory network with no internet access. What it did was turn a hard wiring error into silence, on every network, for the life of the feature.

Two weeks later the same pattern turned up in the HCD itself. The status read for wheel positions was eighty-two characters long on an eight-axis controller. It was the eighty-character limit from section 4.3, striking again. The read failed on every poll, and because wheel position had been treated as optional telemetry, the failure was logged at debug level and swallowed. The wheel position stayed “unknown” for as long as the HCD ran.

Both were the same mistake: a guard meant to tolerate a missing dependency hid a broken one. For a dependency you control, a missing piece is a defect, and it should fail loudly. The libraries are now bundled with the page, and optional reads that fail now warn once instead of disappearing.

What went into the memory. “A graceful-degradation guard can hide a hard wiring error indefinitely; prefer failing loudly for dependencies you control.”

5. When the memory needed a redesign

The memory developed a familiar problem: we kept adding to it until its structure stopped working. By mid-June the living document was four hundred kilobytes. Without anyone deciding it, Claude began writing separate state files for individual sessions and leaving the main one alone. By early July there were sixteen across two workstreams, each tidy, together a set of snapshots that a later session could mistake for current truth.

On July 6 I asked for a better solution: “I’ve noticed that the project state doc has become so unwieldy that you’ve started to create a new project state each session rather than keep the core state doc current.” The answer was to separate current state from history. The state document would be rewritten after each session under a length budget: status, working agreements, one-line learnings, open items, pointers to detail. A session history would accumulate one bounded entry per session, never edited afterward. That compression is where the observations about me became the working agreements in section 3.4.

Compression also preserved a stale task from the spring. In late July a session spent an hour on work that had already been done twice, on a premise disproven in May. Rules survive compression. To-do items and technical detail go stale silently, and a short statement can look authoritative long after it has become wrong.

On August 11 I reviewed the whole record and set out four principles: a stored archive is required so that retrospective analysis is possible; the goal is curation, not just compression; limits on what is kept are the wrong control; and the project memory is an asset that needs periodic review and correction. On August 13 we adopted the design that followed, in three tiers:

  • Current state, always loaded, with a length budget that triggers moving older detail down rather than deleting it.
  • Session history, read when the current summary is not enough.
  • An indexed archive of every outgoing state document and every transcript, with an audit every ten sessions that checks current claims against the code.

Design decisions with more than a few lines of rationale go into decision records in the repository, next to the code they govern. Claude maintains all of it under written instructions, and I review it. Deciding where information belongs and whether it is still true matters as much as keeping it short.

6. Did it get better?

My impression was that the work improved as we went. Later in the project Claude would go looking for the root cause on its own, adding logging, reading the code or the manual before theorizing, and I stepped in far less often. An impression is not evidence, so I had Claude analyze the full conversation record.

When I interrupt, I often write “I stopped you because…”. In February and March there were twenty such messages in twenty active days. In April and May, four in twenty-three. In June and July, none in twenty-one. Most concerned a wrong solution, skipped diagnosis, or lost context. Corrections such as “we already implemented that” also peaked in March and nearly disappeared after May: none in June, one in July. I still said “no,” but increasingly to make a design decision rather than to stop a bad fix.

Claude’s side moved the other way. Unprompted statements that it would read the code or check the evidence before proposing a solution rose from almost none early on to one in six of its messages in June. On June 15, given a log and a zip file, it wrote: “Before I theorize, let me read the actual error-detection code.”

The trend was not a guarantee. On August 20, on the controller in my home office, stopping a jog from the engineering page tripped the spontaneous-motion detector, the check that raises an error when an axis moves while the software believes it is idle. The diagnosis was sound. The stop command marked the axis idle between status scans while a snapshot taken mid-jog, still reporting motion, was on its way. The detector did its job on stale data.

The proposed fix was to blind the detector for two seconds after every stop. It would have worked, in the sense that the error would go away. It would also have disabled a safety check at exactly the moment a mechanism is most likely to do something unexpected. I rejected it. Instead, the stop now waits for a status scan newer than the stop itself to confirm the axis has stopped, checks once more, and only then reports completion; if confirmation does not come within three seconds, the axis stays marked as moving and the command returns an error. The cost is that the engineering page waits for up to one scan. “I’m also not happy about making the HMI wait on the moving flag to update .. but I think it is better.” Observe the hardware; don’t declare its state.

The fix was verified on the controller in my home office, and the detector, which until then had no unit tests at all, got four, including a replay of the exact stale-snapshot sequence. By August the rules were in the memory and the AI cited them. It still reached for the cheap fix first, and it still took someone who had spent a career around interlocks to see that the cheap fix was a hole in a safety check.

The limits matter as much as the trend. The memory and the models changed over the same months, and the record does not identify the model used in each session. June’s work differed from March’s. Saying “I’ll check” is not proof of checking, and narrow phrase counts miss interventions phrased other ways. Still, two independent signs point the same way: my explicit interruptions declined while Claude’s stated approach increasingly reflected the written rules. How much credit belongs to the memory, I cannot say.

7. How the lead talks

I also wanted to know whether the way I write to the AI matters. Through July, the record contains 1,814 of my messages of three words or more once pasted logs are removed; the floor drops the “ok” and “go ahead” replies that say nothing about how instructions are written. The median is 33 words. About a quarter contain a question, a fifth include a tentative proposal such as “my instinct is,” and a quarter cite evidence I had just produced. About one in twenty is an explicit override. The bare directive with no reason and no question is effectively absent. That is how I talk to a junior engineer, and it connects to the memory in three ways.

First, the memory is written from the conversation. When I give a reason, the reason ends up in the memory, and a rule with a reason generalizes to cases I never named. The April 6 line is a preference with a rationale because on March 23 I explained myself rather than only saying no.

Second, leading rather than dictating makes the AI state the conclusion in its own output, in its own vocabulary, which is the vocabulary a later session reads as its own. On July 6 I did not say “use a reservation gate.” I asked for prevention, and Claude produced the rule and wrote it down.

Third, overrides work because they are rare. One in twenty is rare enough that each one lands. The “No no no no no” of March 23 stands out because nothing else in the record for weeks is in that register.

This is one operator, with no counterfactual, and the AI mirrors register, so some of its reasoned tone is reflection rather than learning. But it is what I would tell any new tech lead about a human junior: explain your reasoning, let them reach the conclusion, and save the flat no for when it is needed.

8. What it cost

None of this was free, and the cost is the part that is easiest to leave out.

The memory is a system, and like any system it needed maintenance. It bloated, it fragmented, it had to be redesigned twice, and one stale line that survived a compression cost us an hour redoing work that was already done. A record that is wrong is worse than no record, because it gets read with the same confidence as a right one. The ongoing overhead is ordinary and it never stops: rewrite the state document at the end of the session, add one history entry, audit the archive against the code every tenth session, and read all of it.

The bigger cost is review. Since February 10, when nine passing tests turned out to be exercising nothing, I have read every line of every change. That did not stop as the work improved, and it is the one part of the job that cannot be delegated to the thing being reviewed.

9. What transfers

Read the “what went into the memory” notes in order and the shape of the project is there. February: one rule, mine. April: the AI writing down what I do, in the third person, the way a junior takes notes on a lead. July: those notes compressed into agreements in the first person plural. The working relationship was learned by the AI from repeated correction and recorded in a memory about how to work with me. That memory is the real artifact of the project, and it transfers. The next project can start with it on day one.

Some of it is specific to Galil controllers. Most of it applies anywhere you need to tell a plausible explanation from a demonstrated result:

  • Code is the source of truth. Read the actual implementation.
  • Establish the root cause before fixing it, and do not hide it with a workaround.
  • Check what a passing test actually proves.
  • The display shows state, never intent.
  • Prevent where you can, and redesign a boundary that keeps needing repairs.
  • Fail loudly for dependencies you control.
  • For anything concurrent, unit tests plus one clean run is not done.
  • Let the AI maintain the memory, and read every word.

One of those rules reaches well past the engineering page. The display must show state, not intent, and so must everything else we rely on: the nine green tests reported a status monitor that was intended rather than built, and a memory is worth keeping only if it records what is true now, not what was once planned.

Something transferred in the other direction too, and it’s the reason I would do this again. My background is in C and EPICS control software. I’ve learned Scala and functional programming for TMT, but I’m still slow at them. Claude bridged that gap and let my control-systems experience reach into frameworks where I could not have been this productive on my own. The simulator was an especially good fit. Claude had the HCD’s expectations right in front of it and could build the matching controller behavior quickly. The engineering interface, a flexible, good-looking web page in frameworks I had never touched, is something I simply would not have built at all.

The judgment behind stopping the misleading display didn’t transfer nearly as easily. It came from decades around hardware and from being the person on call when a control system did the wrong thing in the middle of the night. I had to bring it to every session, and for the first few months I had to bring it fresh each time, because nothing I said survived to the next day until the AI started writing it down. As the implementation got faster, my decisions became what the schedule waited on. Read that as a staffing statement, not a footnote: done this way, AI-assisted development does not replace the domain expert. It makes the expert’s judgment the scarce resource.

The improvement in our work has tangled causes: the written rules, a growing pile of examples, better models, and me enforcing the rules over and over. I’d like to give the credit to the memory. I can’t prove how much of it the memory deserves.

What I can say is that writing the lessons down gave us something we could inspect, correct, and carry forward. The next project can start with standards that took this one months to work out, and it will still need someone who can recognize when they haven’t been met.