DEV Community

Evan Lin for Google Developer Experts

Posted on Originally published at evanlin.com on

[AI in Action] First Time Using Herdr: Collaborating with Claude Code and agy for Go Backend Code Review, Issue Fixes, and Cloud

Herdr: Claude Code on the left assigning tasks, Antigravity (agy) on the right executing

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 /api to 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: genai SDK, Gemini REST (voices / interactions), Firestore transactions, kagome for tokenization and conversion to Hiragana, a custom Hepburn Romanization table, and a port of Python's difflib.SequenceMatcher to 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ?error would 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:

  1. Cloud Run only gives about 10s after sending SIGTERM, so the two segments combined exceeded the budget.
  2. Re-analysis runs synchronously within the HTTP request. srv.Shutdown would 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
Enter fullscreen mode Exit fullscreen mode

(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 + ModTime handed to http.ServeContent for streaming (getting Range and Last-Modified for free).
  • #61: Unified error types to apierr.Error, CLI speak now uses tts.Service.GenerateAll, switched to signal.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/..."
Enter fullscreen mode Exit fullscreen mode

The result is an immediate error and exit (exit 2):

Prompts are read only from -p/--print, -i/--prompt-interactive, or stdin
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This script was refined several times during the process, each time providing a practical lesson:

  1. Status bar msg gets truncated: Initially, it only looked at the status msg returned by hc get, but it would be truncated with "...". The truncated part might hide a second command. Changed to hc read the entire permission screen for judgment.
  2. gcloud A; gcloud deploy single-line grep misses: Changed to splitting combined commands and judging each one for read-only status (describe / list / read / ls / du).
  3. Fixing false positives: gcloud storage ls and gcloud storage du are 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 with ALLOW_GIT=1 after 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 --skill and herdr-core --skill first to confirm actual executable paths.
  • 2. Open sub-agents in visible Panes: Use pane.split to 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 -p one-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 is blocked, hc read the 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."

  1. 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.
  2. 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 uses hc wait to wait without polling.
  3. 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.
  4. 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.

Top comments (0)