[AI in Action] First Time Using Herdr: Collaborating with Claude Code and agy for Go Backend Code Review, Issue Fixes, and Cloud
Background song-lingo is a small tool I made for learning Japanese songs: paste a YouTube MV link, and Gemini helps me transcribe the lyrics, label Romanization (Romaji), provide Traditional Chinese translation and gra
Background
song-lingo is a small tool I made for learning Japanese songs: paste a YouTube MV link, and Gemini helps me transcribe the lyrics, label Romanization (Romaji), provide Traditional Chinese translation and grammar notes, and then uses Gemini TTS to generate a "teacher demonstration audio." After I record myself shadowing the song, Gemini transcribes my pronunciation and provides a word-by-word score.
A few days ago, in a single Claude Code session, I rewrote the entire backend from Python + Next.js API routes to Go 1.26 (Issues #44β#49, Tracking Issue #50, PR #51β#56). The architecture after the rewrite looks like this:
-
Next.js is only for the frontend, using a rewrite to forward
/apito the Go server in the same container (127.0.0.1:8081). - Deployed on Cloud Run (
asia-east1), data stored in a Cloud Storage bucket mounted via gcsfuse at/data; learning progress stored in Firestore; protected by IAP so only I can log in. - Key points of the Go version:
genaiSDK, Gemini REST (voices / interactions), Firestore transactions,kagomefor tokenization and conversion to Hiragana, a custom Hepburn Romanization table, and a port of Python'sdifflib.SequenceMatcherto Go (aligned with 400 sets of golden tests produced by Python).
The rewrite was done, it worked, and tests passed. But "writing and reviewing your own code" always has blind spots. I happened to want to try Herdrβa tool that allows multiple AI Agents to collaborate in the same window, each occupying a Pane. So, I conducted an experiment:
Let Claude Code be the controller and Reviewer on the left, and the Antigravity CLI (
agy) be the implementer on the right. The two Agents communicate via Herdr, taking the song-lingo Go backend from Code Review all the way to Cloud Run deployment.
1. What is Herdr? First-time Usage Notes
1. Two Executables: herdr-app and herdr-core
Herdr is divided into two layers:
| Component | Role | Description |
|---|---|---|
herdr-app |
Desktop App | Manages the layout of spaces > tabs > panes. You can split panes left/right and run programs directly in a new pane. |
herdr-core (CLI called hc) |
Terminal Core | Responsible for running " terminals that don't die when the window is closed," providing commands like run / send / wait / read / keys / get for Agents to operate other terminals. |
A small side note: I initially told Claude Code to "use herdr --skill to learn first," but the herdr command couldn't be foundβthe actual executables are ~/.local/bin/herdr-app and ~/.local/bin/herdr-core.
2. --skill: An Instruction Manual for AI Agents
I think Herdr's smartest design is that both CLIs have a --skill parameter. Running herdr-app --skill and herdr-core --skill prints an "instruction manual for AI Agents."
So, Claude Code's first step wasn't to blindly try commands, but to read both manuals before starting. The manuals contain many sensible guidelines, such as: background Agents must use --bg, provide a name and clear scope, don't let sub-agents spawn more sub-agents, and use hc wait instead of sleep polling when waiting.
3. Core Command Cheat Sheet
| Command | Purpose |
|---|---|
$HC_TERMINAL_ID |
Every terminal has an address; Claude Code itself is t4. |
hc run --bg --name xxx <agent> "<task>" |
Start a named Agent in the background and assign a task. |
herdr-app do pane.split --direction right --pane <id> --argv [...] |
Split the pane to the right on the screen and run a program in the new pane. |
hc send <t> "..." |
Paste text and press Enter; used to assign tasks or provide Review feedback. |
hc wait <t> / hc wait --children all
|
Wait for the other party to finish (replaces sleep polling). |
hc read <t> --lines 0 |
Read the other party's entire current screen. |
hc keys <t> enter |
Send a keystroke, such as answering a permission dialog. |
hc get <t> |
Check status: idle / working / blocked / done; when blocked, it includes a msg, e.g., Approve RunCommand: .... |
4. Agent-to-Agent Communication Channel
This is the most critical piece of the collaboration. When assigning a task, I would require agy: "When finished, please run herdr-core send t4 '...' to report back." After agy finishes, that message appears directly in Claude Code's input, looking like this:
<hc from="t7">ready for review #57 ...</hc>
In other words, Claude Code doesn't need to keep staring at the right screen; agy will "knock on the door" when done. Reporting is asynchronous, and every message carries a source address for tracking.
5. Layout Configuration
The first time I had Claude Code open agy, it used hc run --bg to run it in the backgroundβit worked functionally, but I couldn't see what agy was doing. So, the second time, I specifically requested "in a pane on the right," and Claude Code used herdr-app do pane.split to open agy on the right. The final configuration was:
flowchart LR
subgraph Herdr["Herdr Window (Same Tab)"]
direction LR
CC["Left Pane: Claude Code (t4)<br/>Controller / Reviewer<br/>Opens Issues, views diffs, runs go test -race, approves"]
AGY["Middle Pane: agy (t7)<br/>Implementer<br/>Code Review, modifies code, runs tests, commit/push"]
BLOG["Right Pane: Claude Code<br/>Blog Writer<br/>(The one writing this article)"]
end
Human(["Me (Human)<br/>Can see every Pane, can intervene anytime"])
CC -->|"hc send: Assign tasks / Review feedback"| AGY
AGY -->|"herdr-core send t4: ready for review #N"| CC
CC -->|"hc keys: Grant read-only permissions"| AGY
CC -->|"hc send: Write blog"| BLOG
BLOG -->|"herdr-core send t4: blog draft ready"| CC
Human -.-> CC
Human -.-> AGY
2. Round One: Let agy be the Reviewer
The first task was simple: ask agy to review the Go code under backend/, read-only, no subagents, and write the results to a file, reporting back with hc send t4 when finished.
agy's interface showed it was running Claude Sonnet 5.5 (plan mode), and it eventually produced 17 issues, sorted by importance. Claude Code helped me organize them into three categories:
| Category | Issues found by agy |
|---|---|
| Concurrency and Data Integrity | During Re-annotate (one Gemini call can take up to 5 mins), if I modify lyrics on the proofreading page, the write-back will be overwrittenβthe two sides use different mutexes; EditLine's global lock spans across Firestore calls; WriteAtomic's temporary filename is fixed as .tmp. |
| Resource Limits and Leaks | TTS generation has no concurrency limit (this is a paid API); the failure cache only grows and stores duplicates; 429 errors without Retry-After have no rate limiting at all; TeacherVoice re-reads voices.json every time and holds a global lock during network calls; request bodies lack MaxBytesReader; background jobs use context.Background(), so they aren't cancelled on shutdown. |
| Performance and Structure |
listSongs parses all songs sequentially and reads progress for each; demonstration audio is read entirely into memory before being returned; the same Error type exists in 3β4 places; CLI speak and tts.Service logic are redundant. |
Here is a detail I really liked: Claude Code did not accept everything blindly. Before opening an Issue, it first used grep to verify the most important oneβ"re-annotate and review use different mutexes"βand only after confirming it was true did it start opening Issues. The Reviewer's conclusions also need to be Reviewed.
3. Round Two: Open Issue β agy Fixes β Claude Code Reviews β Commit only if passed
My instructions to Claude Code were:
"Open issues and fix them sequentially. Mainly let the agy pane on the right do the modifications. You are responsible for reviewing and giving feedback. Only after you approve should agy commit and push."
1. Consolidating 17 items into 5 Issues
Claude Code consolidated the 17 items into 5 Issues (#57β#61), all performed on the same branch go-review-fixes, with one commit per Issue, and commit messages including Fixes #N.
2. Collaboration Workflow for Each Round
sequenceDiagram
participant CC as Claude Code (t4)<br/>Reviewer
participant AGY as agy (t7)<br/>Implementer
participant GH as GitHub
CC->>AGY: hc send: Issue #N task + implementation points + "do not commit"
AGY->>AGY: Implementation + gofmt -l . && go vet ./... && go test ./...
AGY->>CC: herdr-core send t4 'ready for review #N'
CC->>CC: git diff + run go test -race personally
alt Has issues
CC->>AGY: hc send: Review feedback
AGY->>CC: Report back again after fix
end
CC->>AGY: Approved, you may commit & push
AGY->>GH: git commit (Fixes #N) + push
AGY->>CC: Report commit sha
3. What did the Review actually catch?
This is the part I want to share the most. Out of the 5 Issues, 3 required a second round of fixes, and 2 passed on the first try:
| Issue | Topic | Review Result | Commit |
|---|---|---|---|
| #57 | Per-song lock | Design correct, but no tests written β requested them | e5df5f4 |
| #58 | TTS concurrency limit and failure cache | Caught error message regression β requested fix and tests | d67ec5a |
| #59 | Request limits, timeout, graceful shutdown | Caught shutdown sequence and time budget errors β redesigned | de38741 |
| #60 | Song list concurrency, demo audio streaming | β Passed first try | bc1615a |
| #61 | Unified error types, CLI refactor, signal handling | β Passed first try | deb1fb7 |
#57: Design was right, but where are the tests?
agy's per-song lock design was correct. Claude Code also specifically confirmed one thing: manual translations and the reviewed flag are both written into the transcript, so "comparing the entire transcript before writing back" indeed catches all edits.
But agy didn't write tests. So Claude Code requested extracting the logic into saveAnnotation and adding two cases: normal write when unchanged, and return conflict and no write when edited.
#58: Delete one map, and the frontend doesn't know why it failed
While cleaning up the failure cache, agy deleted the server-side recentErrors. All tests passed, but Claude Code caught a regression:
When the TTS quota is exhausted, "other sentences that have never been generated" checking
?errorwould get{error: null}βthe frontend then couldn't display the reason "quota exhausted."
The fix was to make LastError return a still-valid quota error when there is no failure record for that sentence, and add a test.
#59: Cloud Run gives you about 10s, but you scheduled 20s
This was the most valuable bug catch in my opinion. The graceful shutdown agy wrote was: srv.Shutdown waits 10s, then background jobs wait another 10s. There were two problems:
- Cloud Run only gives about 10s after sending SIGTERM, so the two segments combined exceeded the budget.
-
Re-analysis runs synchronously within the HTTP request.
srv.Shutdownwould wait for it to finish, but its context hadn't been cancelled yetβit was essentially waiting for itself.
Claude Code requested changing it to a single 9-second deadline and adjusting the sequence:
// Single deadline, leaving a bit of buffer for Cloud Run's SIGTERM grace period
ctx, cancel := context.WithTimeout(context.Background(), 9*time.Second)
defer cancel()
runner.Cancel() // 1. Cancel background and ongoing analysis work first
srv.Shutdown(ctx) // 2. Then shut down HTTP server (sync analysis already cancelled, won't hang)
runner.Wait(ctx) // 3. Finally wait for jobs to wrap up within the same deadline
(The above is illustrative of the sequence and single-deadline concept.)
#60, #61: Passed first try
-
#60: Song list changed to bounded concurrency 8, so a single song progress read failure won't drag down the whole list; generated demo audio now uses
*os.File+ModTimehanded tohttp.ServeContentfor streaming (getting Range and Last-Modified for free). -
#61: Unified error types to
apierr.Error, CLIspeaknow usestts.Service.GenerateAll, switched tosignal.NotifyContext, and daily quota checks now use type fields instead of string comparisons.
Looking back at these three rejected Issues, there's a common theme: they were all "tests pass, but it hurts in production" problemsβtest gaps, frontend error message regressions, and shutdown sequences. This is exactly the greatest value of "one writing, one reviewing."
4. Round Three: PR, Merge, Deployment, and Verification
After all 5 commits passed, PR #62 was opened and merged, and #57β#61 were automatically closed. Then, a new revision was deployed using gcloud run deploy --source ..
Post-deployment verification was completed by Claude Code running fetch directly through a Chrome instance logged into IAP:
| Verification Item | Result | Corresponding Change |
|---|---|---|
Homepage /api/songs
|
200 (10 songs) |
#60 Concurrent reading |
| Learning page | 200 |
β |
| Generated demo audio | 200 audio/wav |
#60 Streaming |
| Demo audio Range request | 206 |
#60 http.ServeContent
|
With If-Modified-Since
|
304 (New feature: now has Last-Modified) |
#60 ModTime
|
| PATCH with 70 KB |
413 "Request content too large." |
#59 MaxBytesReader
|
| Cloud Logging | Only two WARNINGs, both from the test requests themselves | β |
The previous revision was also kept, allowing for an immediate rollback if needed.
5. Six "Devil in the Details" Moments from Using Herdr for the First Time
Detail 1: agy "prompt" will exit with code 2
Intuitively, you might start agy like this:
agy "Please review the Go code in backend/..."
The result is an immediate error and exit (exit 2):
Prompts are read only from -p/--print, -i/--prompt-interactive, or stdin
The correct way is agy -i "..."βinteractive startup with an initial task. The Herdr manual also warns not to use -p one-off mode, because the Agent needs to remain "observable and follow-upable" to conduct the Review back-and-forth.
Detail 2: Trust dialog on first entering a folder
The first time you start agy in a folder, it pops up "Do you trust the contents of this project?". At this point, the hc get status becomes blocked (auth). Claude Code just needs to use hc keys go-review enter to confirm.
Detail 3: A prompt that looks unsent has actually arrived
After opening agy with pane.split, the screen looks idle, and the initial prompt seems unsent. Claude Code tried to resend it with hc send, but was blocked by hc:
t7 is blocked (permission)... typing would answer its dialog; read its screen first
It turns out the task had already arrived, and agy was just stuck waiting for permission. This protection mechanism in hc prevents accidentally typing a task description into a permission dialogβif that actually happened, the result could be an unintended approval or rejection of a command.
Detail 4: Plan mode can only look, not modify
When agy is in plan mode (read-only research), it can only analyze. To have it start modifying code, you must press shift+tab to switch modes; Claude Code used hc keys t7 shift+tab to switch it. The first round of Code Review was perfect for plan mode, and it only switched out when modifications were needed in the second round.
Detail 5: Permission DriverβEvolving from "checking status bar" to "reading the whole screen"
Every shell command in agy requires permission. Pressing Enter for each one is too tedious, so Claude Code wrote a small driver script with the following logic:
flowchart TD
A["hc wait t7"] --> B["hc get t7 Check status"]
B -->|"idle / done"| Z["End, wait for agy report"]
B -->|"blocked"| C["hc read Read full permission screen"]
C --> D{"Contains dangerous command?<br/>rm -rf / git reset / git checkout<br/>--force / curl / gcloud write operationsβ¦"}
D -->|"Yes"| H["Stop, let human decide"]
D -->|"No"| E{"Is it git commit / push?"}
E -->|"Yes, and no ALLOW_GIT=1"| H
E -->|"No, or Reviewer approved"| F["hc keys t7 enter Grant permission"]
F --> A
This script was refined several times during the process, each time providing a practical lesson:
-
Status bar
msggets truncated: Initially, it only looked at the status msg returned byhc get, but it would be truncated with "...". The truncated part might hide a second command. Changed tohc readthe entire permission screen for judgment. -
gcloud A; gcloud deploysingle-line grep misses: Changed to splitting combined commands and judging each one for read-only status (describe/list/read/ls/du). -
Fixing false positives:
gcloud storage lsandgcloud storage duare actually read-only; they were blocked initially but later added to the allowlist.
Detail 6: Boundaries of Permission Granting
The line for automated granting was drawn like this:
- Read-only commands: Automatically granted.
-
git commit/push: Blocked by default, only granted withALLOW_GIT=1after Claude Code Review passes. - Commands requiring external APIs (e.g., agy wanting to use an access token to call the Google Developer Knowledge API for documentation): Stop and let the human decide.
Agents can trust each other enough to say "you can run tests," but "writing to git, writing to the cloud, making external requests" must go through a Reviewer or a human.
ε γMulti-Agent Collaboration Checklist
If you also want to use Herdr (or similar tools) to have two Agents write and review, you can refer to this checklist:
- 1. Read the manual before starting: Have the controller Agent run
herdr-app --skillandherdr-core --skillfirst to confirm actual executable paths. - 2. Open sub-agents in visible Panes: Use
pane.splitto open the implementer Agent alongside, so a human can see and intervene at any time. - 3. Start in interactive mode:
agy -i "...", don't use positional arguments, and don't use-pone-off mode. - 4. Clear task scope: Specify read-only or not, no sub-agents, "do not commit," and report back using
herdr-core send <controller address>when finished. - 5. Use
hc wait, not sleep polling; before sending text, if the other party isblocked,hc readthe screen first. - 6. Verify Reviewer conclusions: Before opening an Issue, use grep to confirm the most important findings are true.
- 7. Reviewer runs tests personally: For example,
go test -race, don't just rely on the implementer's report. - 8. Permission Driver reads the whole screen: Don't rely on the status bar which can be truncated; judge combined commands individually.
- 9. Gatekeeping for Git and Cloud writes: Only allow commit/push after Review passes; leave external API calls to humans.
- 10. Verify new behavior after deployment: Don't just look for 200s; verify new limits (413), new headers (304 / 206), and keep the previous revision.
7. Next Step: Let agy analyze Cloud Run performance
As I finish writing this, agy on the right is already running the next task: using its GCP skill to analyze song-lingo's performance on Cloud Runβservice settings, cold starts, gcsfuse, Firestore, etc.βproposing 5β8 solutions, each with evidence, expected benefits, monthly costs, and risks, for Claude Code to decide which to implement. This part is still ongoing; I'll share more later.
Summary
My biggest takeaway from using Herdr for the first time is: Multi-Agent collaboration is finally "visible."
- One writing and one reviewing catches blind spots better than doing both yourself: 3 out of 5 Issues were rejected, catching "tests pass, production hurts" problemsβtest gaps, error message regressions, and Cloud Run shutdown sequences.
-
Agent-to-Agent messaging makes collaboration async and trackable:
herdr-core send t4 'ready for review #N'lets the implementer "knock on the door," and the Reviewer useshc waitto wait without polling. - Panes keep humans in the loop: I can see exactly what commands agy is running in real-time on the right and intervene if necessary.
- Permission granting must be measured: Read-only can be automated, but writing to git, writing to the cloud, and making external requests must go through Review or a human.
And this article itself was written by Claude Code in the rightmost Pane of Herdrβafter finishing, it will use herdr-core send t4 to report back to the controller on the left.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.