hermes-hq

Activity

Stars and mentions · Last 30 days, Oct 9 – Nov 7

GitHub stars13
Mentions405
09182736Oct 9Oct 23Nov 7
UTC days · repositories hermes-hq owns2 repositories

Top repositories

hermes-hq’s repositories ranked by new forks, as on trending.

Loading

Contributes to

Repositories outside hermes-hq’s account where hermes-hq opened or reviewed pull requests in the past 365 days.

Who noticed hermes-hq

Everyone who starred or forked hermes-hq’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

412posts
Nov 87 posts
  1. Exhale: A Breathing Guide That Reads Your Breath While Claude Code Works This is a submission for the Presage SmartSpectra SDK Challenge. You hand Claude Code a task. It says "Elucidating…" and the spinner starts. For the next two minutes you watch it. Shoulders up, jaw set, breath shallow, eyes locked on a terminal that isn't asking you for anything. That waiting time is the most common moment in AI-assisted coding, and it's dead time. Exhale turns it into a minute of slow breathing, with a twist: the horizon on screen is your own breathing, read live from an ordinary webcam by the Presage SmartSpectra SDK. No wearable, no chest strap. !Left: me during a live session. Right: the Exhale card, its horizon drawn from my chest movement, 83% in sync with the guide What I Built Exhale is a small desktop app that listens for your coding agent. It's built on Electron and tested on macOS; the SDK also ships Windows and Linux runtimes. 1. You send Claude Code a prompt. 2. If the run lasts longer than 10 seconds, a small card rises in the corner of your screen. It's a sky at dusk. 3. A sun climbs while you breathe in and sinks while you breathe out: 4 seconds in, 6 out, six breaths a minute. 4. The horizon the sun sinks behind is your own breathing, from the SDK's chest waveform. 5. When the agent finishes, the camera turns off and the card shows a short recap. !The recap card: agent ran 1 min 27s, 30 s with the guide, breathing 6 to 7.4 a minute Short runs never turn the camera on. In daily use you don't click anything; the first time, the card asks before it uses the camera. It also works with any long command through exhale run -- npm test. Demo {% youtube psJDRBy0cMk %} Every reading in the video comes from one live session with the real SDK. In that run Claude worked for 87 seconds, the guide ran for 77, my breathing settled at 6 to 7.4 breaths a minute, and I was in sync with the guide for 30 of the 51 seconds Exhale could score (59%; scoring starts once the breathing waveform arrives). The narrator voice is AI-generated; the footage and the numbers are not. Code gabrielanhaia/exhale: setup in the README, plus a Docker setup with a simulated sensor so you can try the whole flow without a camera or an API key. The repo has 34 unit and integration tests and 8 Playwright tests that drive the real Electron app. How It Responds to Human Signals Exhale requests four metrics from the SDK and nothing else: | Signal | What Exhale does with it | |---|---| | CHEST_BREATHING waveform | Draws the horizon, and scores how closely you follow the guide | | BREATHING_RATE | Breaths per minute in the recap | | BLINKING | Suggests a 20-second look-away when you stop blinking | | TALKING | Pauses the guide while you speak | Following the guide. Every second, Exhale takes the last 20 seconds of your chest waveform, resamples it to 10 Hz and compares its shape with the guide curve. The score is the best Pearson correlation over a lag of 0 to 2 seconds, because people follow a visual cue a little late. It uses the absolute value, since the SDK's trace is in arbitrary units and Exhale doesn't assume whether an inhale goes up or down. At 60% or above, the card's text turns gold and the sun's glow widens. Blinks. Rising edges in the blink stream give a rolling blinks-per-minute over 60 seconds. Below 8 a minute, after at least 45 seconds of face time, the card suggests looking at something far away. At most once every five minutes. Talking. The guide pauses only when most of the last 1.5 seconds of talking detections are confident ones, so a flicker of the mouth doesn't stop it. The plumbing. - Claude Code hooks (UserPromptSubmit, Stop, SessionEnd) POST to a local server on 127.0.0.1. The hook skips runs where Claude Code reports the session as unattended (CLAUDE_CODE_SESSION_ATTENDED=0). I checked that a headless claude -p run sets it, so scheduled jobs don't turn the camera on. - Each run gets a fresh child process that owns the SDK, with useCamera() and explicit capture settings (720p at 30 fps). - The child streams small frames over IPC to a session state machine (idle → pending → active → summary). The UI renders from Server-Sent Events. The SDK hands over protobuf packets. This is the function in src/engine/sdk-engine.js that turns each one into the small frame the rest of the app understands: function toFrame(m) { const b = m.breathing || {}; const f = m.face || {}; const sec = (x) => tsToSec(x.timestamp); return { trace: (b.upperTrace || []).map((x) => ({ t: sec(x), v: x.value, })), rate: (b.rate || []).map((x) => ({ t: sec(x), value: x.value, stable: !!x.stable, confidence: x.confidence, })), blinks: (f.blinking || []).map((x) => ({ t: sec(x), detected: !!x.detected, })), talking: (f.talking || []).map((x) => ({ t: sec(x), detected: !!x.detected, stable: !!x.stable, })), }; } Four things the SDK taught me These came up while testing on real hardware (Node SDK 3.4.0, macOS on Apple Silicon). 1. A second sdk.on('metrics') replaced the first. I added a debug logger as another metrics listener, and the handler that fed the UI went quiet. No error, just a flat horizon. Keep one handler per event and do everything in it. There's now a regression test for this. 2. The metric streams start at different times. In my logs, blink detections arrived from the first packet and the breathing waveform about 20 seconds later; the breathing rate needs a 30-second window on top of that. The card says "Reading your breathing…" until the waveform shows up. toFrame keeps the SDK's timestamps; the session then lines each batch up by when it arrived, keeping the spacing between samples, instead of assuming every stream shares one clock. 3. Frame rate matters more than resolution. My 4K webcam, in a fairly dim room, kept falling under the frame rate the SDK needs, and the SDK said so through its validation status. During one of those sessions the native pipeline also stopped with an OpenCV assertion about mismatched image sizes, and the whole app went down with it. Now each run's SDK lives in its own process: a crash ends that run with a clear message, and the app stays up. An iPhone through Continuity Camera, in the same room, gave no frame-rate warnings. 4. The talking detector is sensitive. During my sessions the guide kept pausing while I wasn't speaking; small mouth movements were enough to trigger it. That's why the rule above needs a sustained, confident signal. Prize Categories - Best Thoughtful Real-World Use: it fits a moment developers already have dozens of times a day, asks before it uses the camera, runs only during that moment, stores nothing by default, and never pretends to be a medical tool. - Most Creative or Delightful Use: the breathing signal isn't shown as a number on a chart. It becomes the landscape the guide's sun sets behind. Consent and Data Handling - Consent. The first time a long run starts, the card explains what will happen and asks before the camera is used. Until you click "Turn on camera", it stays off. You can withdraw consent from the Exhale window at any time, which turns the camera off immediately. On macOS the system camera prompt appears once as well. - When the camera is on. Only while an agent run is in progress and the card is showing. The card says "camera on" and the menu-bar item shows a dot. "Turn off camera" stops it at once. When the run ends, the camera is released. Unattended runs never trigger it. - Where video goes. Frames go from the camera into the SmartSpectra SDK inside a process Exhale starts for that run. Presage's README says the SDK computes the vitals on the device. Exhale never writes video or images to disk and never sends them anywhere. - What leaves the computer. In my SDK logs, the SDK contacts Presage to check the API key, verify usage and fetch its models. Its optional aggregate telemetry is described in Presage's telemetry docs. Exhale switches it off (enableTelemetry: false). Exhale has no analytics of its own and doesn't use the SDK's optional LLM Insights. - What is stored, and for how long. By default one file, ~/.exhale/settings.json: when you gave consent, your history preference and your chosen camera. History is opt-in. When it's on, each run adds one summary line to ~/.exhale/history.jsonl (duration, percent with the guide, start and end breathing rate, look-away reminders). No waveforms, no video. Both files are readable only by your user. They stay until you delete them: "Delete all Exhale data" removes both, and switching history off deletes the history file. - Sharing. None. The local server listens on 127.0.0.1 only and rejects requests from web pages (custom header plus Origin check) and from non-loopback host names (DNS-rebinding guard). - Not medical. Exhale is a breathing exercise and a screen-break reminder. It doesn't diagnose, treat or monitor any condition, and it isn't meant for health decisions. It never runs covertly: the camera works only after consent, only during your own agent runs, with a visible indicator. Agent Session I built Exhale with Claude Code, in one long session inside Hermes IDE. I'm not embedding the transcript: it contains local machine paths and personal details that I won't publish. The repo history shows how it came together. --- If you spend your days waiting on agents too, try it for a week and tell me what your recap says. If you build with agents and want the patterns behind long-running tool loops, my AI Agents Pocket Guide covers them. - Book: AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs - Also by me: Thinking in Go (2-book series): Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub, an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub ![AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs](amazon.com)

    22 reactions · 3 comments
  2. River Enqueues Jobs in Your Postgres Transaction. Keep It Out of Your Go Domain - Book: Hexagonal Architecture in Go - Also by me: Pocket Guides for Developers — the 8-book series - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- A customer places an order. Your handler inserts the row, commits, then pushes a "send confirmation" job to Redis. The Redis call times out. The order exists, the email never goes out, and nothing retries it because nothing knows the job was supposed to exist. Swap the order of operations and you get the opposite bug: the job lands first, a worker grabs it, and the order it points to is still uncommitted, or rolled back for good. River's own docs tell this story with a user signup: the worker fetched the job from Redis before the Postgres transaction committed, and "the worker could not yet see that row when it queried for it." Their fix is the reason to pick River at all. The queue lives in the same Postgres database as your data, so the job can commit in the same transaction as the order. That fix comes with a cost. The obvious way to use it puts pgx and River inside your use case. The version below keeps both out. InsertTx is the feature, and the trap River is a Go job queue backed by Postgres. Its Client is generic over the transaction type (type Client[TTx any] struct), and with the pgx v5 driver that type is pgx.Tx. The method that matters is InsertTx. Its doc comment in client.go spells out the guarantee: "the job will not be worked until the transaction has committed, and if the transaction rolls back, so too is the inserted job." Here is how that tends to end up in a codebase: func (uc PlaceOrder) Execute(ctx context.Context, o Order) error { tx, err := uc.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) // ... INSERT INTO orders using tx ... _, err = uc.river.InsertTx(ctx, tx, ConfirmArgs{ID: o.ID}, nil) if err != nil { return err } return tx.Commit(ctx) } It works, and it's atomic. It also means your application layer imports github.com/jackc/pgx/v5 and github.com/riverqueue/river. Testing PlaceOrder now needs a Postgres instance. Swapping the queue later means editing every use case that enqueues. The business rule ("placing an order schedules a confirmation") is tangled with the plumbing that makes it atomic. The goal: keep the atomicity, move the plumbing to the edge. !A hexagonal core with plug-shaped ports on its outer ring, and a stream outside carrying small crates into one port The use case talks to ports. River and pgx live outside the ring. The ports: a scheduler that knows nothing about Postgres Start with the domain. It has no imports beyond the standard library: package order import "errors" var ErrInvalid = errors.New("order: invalid") type Order struct { ID string Email string Total int64 // cents } func New(id, email string, total int64) (Order, error) { if id == "" || email == "" || total <= 0 { return Order{}, ErrInvalid } return Order{ID: id, Email: email, Total: total}, nil } The application layer declares what it needs. Four ports: a unit of work, a repository, a job scheduler, and a mailer. package app import ( "context" "example.com/shop/internal/order" ) type UnitOfWork interface { Do(ctx context.Context, fn func(context.Context) error) error } type Orders interface { Save(ctx context.Context, o order.Order) error ByID(ctx context.Context, id string) (order.Order, error) } type JobScheduler interface { ScheduleConfirmation(ctx context.Context, id string) error } type Mailer interface { SendConfirmation(ctx context.Context, o order.Order) error } JobScheduler says what should happen later, in domain words. No job kinds, no InsertOpts, no queue names. The use cases read like the business rule: package app import ( "context" "example.com/shop/internal/order" ) type PlaceOrder struct { UoW UnitOfWork Orders Orders Jobs JobScheduler } func (uc PlaceOrder) Execute( ctx context.Context, id, email string, total int64, ) error { o, err := order.New(id, email, total) if err != nil { return err } return uc.UoW.Do(ctx, func(ctx context.Context) error { if err := uc.Orders.Save(ctx, o); err != nil { return err } return uc.Jobs.ScheduleConfirmation(ctx, o.ID) }) } type SendConfirmation struct { Orders Orders Mailer Mailer } func (uc SendConfirmation) Execute( ctx context.Context, orderID string, ) error { o, err := uc.Orders.ByID(ctx, orderID) if err != nil { return err } return uc.Mailer.SendConfirmation(ctx, o) } PlaceOrder states that saving and scheduling belong to one unit of work. It doesn't know what a unit of work is made of. The adapter: where the transaction has to bend Here's the one place the abstraction can't stay clean. InsertTx needs the exact pgx.Tx the repository wrote with. The port doesn't mention a transaction, so something has to carry it from the unit of work to the River adapter. This version carries it in context.Context, behind an unexported key. Only code that imports the postgres adapter can get it back out, and the app layer never imports it: package postgres import ( "context" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type txKey struct{} // UnitOfWork runs fn inside one pgx transaction. type UnitOfWork struct{ Pool *pgxpool.Pool } func (u UnitOfWork) Do( ctx context.Context, fn func(context.Context) error, ) error { if _, ok := TxFrom(ctx); ok { return fn(ctx) // nested call joins the outer tx } return pgx.BeginFunc(ctx, u.Pool, func(tx pgx.Tx) error { return fn(context.WithValue(ctx, txKey{}, tx)) }) } // TxFrom is for adapters. The app layer never calls it. func TxFrom(ctx context.Context) (pgx.Tx, bool) { tx, ok := ctx.Value(txKey{}).(pgx.Tx) return tx, ok } pgx.BeginFunc commits if fn returns nil and rolls back if it returns an error, so the use case never sees Commit or Rollback. The repository picks the transaction when one is present and falls back to the pool otherwise: package postgres import ( "context" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" "example.com/shop/internal/order" ) type querier interface { Exec(context.Context, string, ...any) ( pgconn.CommandTag, error) QueryRow(context.Context, string, ...any) pgx.Row } type Orders struct{ Pool *pgxpool.Pool } func (r Orders) q(ctx context.Context) querier { if tx, ok := TxFrom(ctx); ok { return tx } return r.Pool } func (r Orders) Save(ctx context.Context, o order.Order) error { _, err := r.q(ctx).Exec(ctx, `INSERT INTO orders (id, email, total_cents) VALUES ($1, $2, $3)`, o.ID, o.Email, o.Total) return err } func (r Orders) ByID( ctx context.Context, id string, ) (order.Order, error) { var o order.Order err := r.q(ctx).QueryRow(ctx, `SELECT id, email, total_cents FROM orders WHERE id = $1`, id).Scan(&o.ID, &o.Email, &o.Total) return o, err } And the River adapter implements JobScheduler by pulling the same transaction: package riverq import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/riverqueue/river" "example.com/shop/internal/adapter/postgres" ) type ConfirmationArgs struct { OrderID string `json:"order_id"` } func (ConfirmationArgs) Kind() string { return "order_confirmation" } var ErrNoTx = errors.New("riverq: no transaction in context") type Scheduler struct { Client *river.Client[pgx.Tx] } func (s Scheduler) ScheduleConfirmation( ctx context.Context, orderID string, ) error { tx, ok := postgres.TxFrom(ctx) if !ok { return ErrNoTx } _, err := s.Client.InsertTx(ctx, tx, ConfirmationArgs{OrderID: orderID}, nil) return err } Two decisions in there are worth defending. The scheduler fails when there's no transaction instead of falling back to client.Insert. A silent fallback would bring back the exact gap you adopted River to close, and you'd only find out from a missing email. If a caller outside a unit of work ever needs to enqueue, give it a separate method that says so in its name. The riverq package imports postgres. That's adapter-to-adapter coupling, and it's fine: both sit in the outer ring, and both exist because you chose Postgres. The application layer still compiles without either of them. Carrying a transaction in a context hides a dependency, and some Go developers reject it for that reason. The other shape that works is a unit of work that hands fn a bundle of tx-bound ports (fn func(Ports) error), where Ports holds an Orders and a JobScheduler already bound to the transaction. It's more explicit and costs a constructor per bundle. Either way the bend stays in the adapters. !A single sealed envelope holding two cards, an order receipt and a job ticket, closed with one wax seal One transaction, two rows: the order and the job commit or roll back together. Workers are driving adapters A River worker is another way into your application, like an HTTP handler. So treat it like one: unpack the input and call a use case. package riverq import ( "context" "github.com/riverqueue/river" "example.com/shop/internal/app" ) type ConfirmationWorker struct { river.WorkerDefaults[ConfirmationArgs] UC app.SendConfirmation } func (w *ConfirmationWorker) Work( ctx context.Context, job *river.Job[ConfirmationArgs], ) error { return w.UC.Execute(ctx, job.Args.OrderID) } The HTTP side calls the same SendConfirmation use case for a "resend confirmation" button, using the method patterns and PathValue that Go 1.22 added to net/http: package httpapi import ( "encoding/json" "net/http" "example.com/shop/internal/app" ) type Handler struct { Place app.PlaceOrder Resend app.SendConfirmation } func (h Handler) Routes() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("POST /orders", h.place) mux.HandleFunc("POST /orders/{id}/confirmation", h.resend) return mux } type placeReq struct { ID string `json:"id"` Email string `json:"email"` TotalCents int64 `json:"total_cents"` } func (h Handler) place(w http.ResponseWriter, r *http.Request) { var req placeReq err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } err = h.Place.Execute( r.Context(), req.ID, req.Email, req.TotalCents) if err != nil { http.Error(w, "could not place order", 500) return } w.WriteHeader(http.StatusCreated) } func (h Handler) resend(w http.ResponseWriter, r *http.Request) { err := h.Resend.Execute(r.Context(), r.PathValue("id")) if err != nil { http.Error(w, "could not resend", 500) return } w.WriteHeader(http.StatusAccepted) } When the worker returns an error, River retries it. Per the retry docs, the default is 25 total attempts with a backoff of "attempts ^ 4 + rand(±10%)". The use case doesn't know any of that, which is correct: retry policy is a property of how the work was triggered. Because a job can run more than once, make the mailer idempotent, for example with a provider idempotency key or a sent_at check. JobCompleteTx won't help with email, since an email can't roll back. It's for workers whose database writes must commit with the job's completion. Wiring happens in main, the only file that sees every package. The order matters only because PlaceOrder needs the client: package main import ( "context" "log" "net/http" "os" "github.com/jackc/pgx/v5/pgxpool" "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver/riverpgxv5" "example.com/shop/internal/adapter/httpapi" "example.com/shop/internal/adapter/postgres" "example.com/shop/internal/adapter/riverq" "example.com/shop/internal/app" "example.com/shop/internal/order" ) type logMailer struct{} func (logMailer) SendConfirmation( _ context.Context, o order.Order, ) error { log.Printf("confirmation for %s", o.ID) return nil } logMailer stands in for a real mailer. The rest of main builds the adapters and hands them to the use cases: func main() { ctx := context.Background() pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { log.Fatal(err) } defer pool.Close() orders := postgres.Orders{Pool: pool} send := app.SendConfirmation{ Orders: orders, Mailer: logMailer{}, } workers := river.NewWorkers() river.AddWorker(workers, &riverq.ConfirmationWorker{UC: send}) client, err := river.NewClient(riverpgxv5.New(pool), &river.Config{ Queues: map[string]river.QueueConfig{ river.QueueDefault: {MaxWorkers: 10}, }, Workers: workers, }) if err != nil { log.Fatal(err) } if err := client.Start(ctx); err != nil { log.Fatal(err) } h := httpapi.Handler{ Place: app.PlaceOrder{ UoW: postgres.UnitOfWork{Pool: pool}, Orders: orders, Jobs: riverq.Scheduler{Client: client}, }, Resend: send, } log.Fatal(http.ListenAndServe(":8080", h.Routes())) } Before it runs you need River's tables (river migrate-up --database-url "$DATABASE_URL", per the getting started guide) and your own orders table with id text PRIMARY KEY, email text NOT NULL and total_cents bigint NOT NULL. For production, add signal handling and call client.Stop, which River's source documents as a graceful shutdown that "waits for any fetched or in-progress jobs to complete before exiting." Testing: a fake for the rule, one real test for the atomicity The payoff of the port shows up in the use-case tests. No database, no River: package app_test import ( "context" "errors" "testing" "example.com/shop/internal/app" "example.com/shop/internal/order" ) type fakeUoW struct{} func (fakeUoW) Do( ctx context.Context, fn func(context.Context) error, ) error { return fn(ctx) } type memOrders struct { saved map[string]order.Order saveErr error } func (m *memOrders) Save(_ context.Context, o order.Order) error { if m.saveErr != nil { return m.saveErr } if m.saved == nil { m.saved = map[string]order.Order{} } m.saved[o.ID] = o return nil } func (m *memOrders) ByID( _ context.Context, id string, ) (order.Order, error) { return m.saved[id], nil } type memJobs struct{ scheduled []string } func (m *memJobs) ScheduleConfirmation( _ context.Context, id string, ) error { m.scheduled = append(m.scheduled, id) return nil } Three fakes, none of them aware of transactions. The tests sit in the same file: func TestPlaceOrderSchedulesConfirmation(t *testing.T) { jobs := &memJobs{} uc := app.PlaceOrder{ UoW: fakeUoW{}, Orders: &memOrders{}, Jobs: jobs, } err := uc.Execute(context.Background(), "ord_1", "alice@example.com", 4200) if err != nil { t.Fatal(err) } if len(jobs.scheduled) != 1 || jobs.scheduled[0] != "ord_1" { t.Fatalf("scheduled = %v", jobs.scheduled) } } func TestPlaceOrderSkipsJobWhenSaveFails(t *testing.T) { boom := errors.New("boom") jobs := &memJobs{} uc := app.PlaceOrder{ UoW: fakeUoW{}, Orders: &memOrders{saveErr: boom}, Jobs: jobs, } err := uc.Execute(context.Background(), "ord_2", "alice@example.com", 4200) if !errors.Is(err, boom) || len(jobs.scheduled) != 0 { t.Fatalf("err = %v, scheduled = %v", err, jobs.scheduled) } } These tests prove the rule: a placed order schedules a confirmation, and a failed save doesn't. They can't prove the job and the order share a transaction, because the fake unit of work has no transaction. That claim belongs to the adapter, so test it there against a real database with rivertest, whose RequireInsertedTx fails the test when no job of that kind was inserted on the given transaction: package riverq_test import ( "context" "errors" "testing" "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver/riverpgxv5" "github.com/riverqueue/river/rivertest" "example.com/shop/internal/adapter/postgres" "example.com/shop/internal/adapter/riverq" ) func TestSchedulerUsesUoWTransaction(t *testing.T) { ctx := context.Background() pool := testPool(t) // your helper: a migrated test DB workers := river.NewWorkers() river.AddWorker(workers, &riverq.ConfirmationWorker{}) client, err := river.NewClient(riverpgxv5.New(pool), &river.Config{Workers: workers}) if err != nil { t.Fatal(err) } uow := postgres.UnitOfWork{Pool: pool} sched := riverq.Scheduler{Client: client} errUndo := errors.New("roll back on purpose") err = uow.Do(ctx, func(ctx context.Context) error { err := sched.ScheduleConfirmation(ctx, "ord_9") if err != nil { return err } tx, _ := postgres.TxFrom(ctx) rivertest.RequireInsertedTx[*riverpgxv5.Driver]( ctx, t, tx, &riverq.ConfirmationArgs{}, nil) return errUndo }) if !errors.Is(err, errUndo) { t.Fatalf("err = %v", err) } } The client here never calls Start. River's Config docs say Queues "may be omitted for a program that's only queueing jobs rather than working them", and registering the worker lets the client check that the job kind has a worker. Returning errUndo rolls the transaction back, so the test leaves no rows behind. River appears in three non-test files: the scheduler, the worker, and main (plus the adapter's integration test). app imports order and the standard library. If you move to another Postgres-backed queue, you rewrite riverq and the wiring. If you move to a queue outside Postgres, the port stays the same, but you lose InsertTx and need an outbox table to get the atomicity back. app doesn't change either way. --- If this was useful The transaction is where hexagonal Go codebases tend to leak, and this post is one instance of that. Hexagonal Architecture in Go works through that problem at book length: where ports end, how adapters share infrastructure without pulling it inward, how persistence and units of work fit the layout, and how to test each ring on its own. If River (or any Postgres-backed queue) is about to land in your codebase, it's the book I'd want you to have read first. ![Thinking in Go — the 2-book series on Go programming and hexagonal architecture](amazon.de)

    6 reactions · 1 comment
  3. 同时开几个 agent,哪个等你批、哪个跑完了? Hermes 把 Claude Code、Codex、Copilot 收进一个控制台,每任务独立分支和 worktree,等你的进收件箱。 适合多 agent 并行干活、切窗口切到晕的人。 你同时跑过最多几个 agent? #AI编程 #Agent工具 @grok hermes-hq/hermes-ide

    2 likes · 178 views
  4. Your Postgres Get-or-Create Has a Race. Postgres 19's ON CONFLICT DO SELECT Closes It - Book: Database Playbook - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- Two requests to tag an article postgres arrive a few milliseconds apart. Both look up the tag. Neither finds it. Both insert it. One gets an id back. The other gets SQLSTATE 23505, unique_violation, and your API returns a 500 for a request that did nothing wrong. Get-or-create is the most boring query in your codebase, and the obvious ways to write it on PostgreSQL 18 are all wrong somewhere. Some race. Some return nothing. One writes to the table every time you read from it. On 12 February 2026, commit 88327092ff added INSERT ... ON CONFLICT DO SELECT, and it is in PostgreSQL 19 RC 1, released 15 October 2026. GA is planned for 19 November 2026, barring delays. Until then it is a release candidate, so don't plan a production migration around it yet. Everything below uses the default READ COMMITTED isolation level and one table. Open two psql sessions, A and B, and follow along. CREATE TABLE tags ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE ); SELECT, then INSERT: the race you write first This is the version most code starts with. Look, and insert if missing. -- Session A BEGIN; SELECT id FROM tags WHERE name = 'postgres'; -- no rows -- Session B BEGIN; SELECT id FROM tags WHERE name = 'postgres'; -- no rows -- Session A INSERT INTO tags (name) VALUES ('postgres') RETURNING id; -- Session B INSERT INTO tags (name) VALUES ('postgres') RETURNING id; -- blocks: A holds an uncommitted entry in the unique index -- Session A COMMIT; -- Session B now fails with unique_violation (23505) The BEGIN does not save you. A SELECT that finds nothing locks nothing, so the transaction has no row to protect. A 2020 write-up on concurrency-safe upserts reaches the same verdict: wrapping the read and the write in a transaction "is not going to help in any way here." The unique constraint keeps the data correct. Your code is what breaks. DO NOTHING RETURNING goes quiet on conflict ON CONFLICT has been around since 9.5 and fixes the error. It also takes away the row you wanted. INSERT INTO tags (name) VALUES ('postgres') ON CONFLICT (name) DO NOTHING RETURNING id; When the tag is new, you get its id. When it already exists, you get zero rows. The PostgreSQL 18 INSERT docs are explicit: "Only rows that were successfully inserted or updated will be returned." A skipped row is neither. A 2020 pgsql-hackers thread that proposed a SELECT conflict action put it plainly: "in case of conflict no value is returned." So you add a second statement, a SELECT by name. That is two round trips, and if a DELETE lands between them the SELECT comes back empty as well. Correct code ends up wrapping both in a retry loop. DO UPDATE writes a row so it can read one The common workaround is a no-op update, because DO UPDATE always returns a row. INSERT INTO tags (name) VALUES ('postgres') ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id; It works. It also turns every "get" into a write, and Postgres does not skip a write because the values match. The trigger functions docs describe the "normal behavior which always performs a physical row update regardless of whether or not the data has changed." Each of those writes costs you something: - Every lookup leaves a dead tuple behind. "An UPDATE or DELETE of a row does not immediately remove the old version of the row," per the vacuuming docs, so VACUUM ends up cleaning after your reads. - Changes to table and index files are logged to WAL first, so replicas and backups carry the no-op writes too. Unless the update qualifies as a heap-only tuple (HOT) update, the new row version also needs new index entries. - With DO UPDATE, row-level BEFORE UPDATE triggers can run, and statement-level UPDATE triggers run whether or not any row was updated. An updated_at trigger now bumps the timestamp on every lookup. - The INSERT docs note that "all rows will be locked when the ON CONFLICT DO UPDATE action is taken." Two transactions fetching the same hot tag queue behind each other until the first one commits. Laurenz Albe's write-up at CYBERTEC lists the same two headline costs: a dead row for VACUUM and, without HOT, an update to every index "even though all data stay the same." The CTE + UNION trick misses a row it waited for The cleverer pre-19 answer does it in one statement: try the insert in a data-modifying CTE, and union in a lookup for the case where it was skipped. WITH ins AS ( INSERT INTO tags (name) VALUES ('mvcc') ON CONFLICT (name) DO NOTHING RETURNING id ) SELECT id FROM ins UNION ALL SELECT id FROM tags WHERE name = 'mvcc' LIMIT 1; No write on the conflict path, one round trip. Now race it. -- Session A BEGIN; INSERT INTO tags (name) VALUES ('mvcc'); -- Session B: run the CTE query above -- blocks on A's uncommitted index entry -- Session A COMMIT; -- Session B returns zero rows Two documented rules combine here. The PostgreSQL 18 concurrency docs say that in READ COMMITTED, DO NOTHING "may have insertion not proceed for a row due to the outcome of another transaction whose effects are not visible to the INSERT snapshot." And the WITH docs say all sub-statements "are executed with the same snapshot." B's snapshot was taken before A committed. The insert waits for A, sees the conflict, and skips. The fallback SELECT reads from that same older snapshot, where the row does not exist yet. Both branches come back empty, and you are back to a retry loop. Postgres 19's DO SELECT, and what to ship before GA The new conflict action returns the existing row and writes nothing. INSERT INTO tags (name) VALUES ('postgres') ON CONFLICT (name) DO SELECT RETURNING id; Run the same two-session race against it. The PostgreSQL 19 concurrency docs say that if the conflict comes from a transaction not yet visible to the INSERT, "the command will wait for that transaction to commit or roll back, then return the conflicting row if it was committed (even though that row was not visible when the command started)." That is the exact case the CTE trick gets wrong. In READ COMMITTED, each row is "guaranteed to either insert or return the conflicting row." Nothing is modified on the select path. The 19 INSERT docs say "the new values will be identical to the old values, since no modification takes place." So you get no new row version, no dead tuple for VACUUM, and no row-level UPDATE triggers. You also get no row lock, unless you ask for one: INSERT INTO tags (name) VALUES ('postgres') ON CONFLICT (name) DO SELECT FOR UPDATE RETURNING id, (old.id IS NULL) AS created; FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE and FOR KEY SHARE lock the conflicting row, for when you are about to modify it in the same transaction. The created column uses the old/new aliases in RETURNING that arrived in PostgreSQL 18. The 19 docs say old values can be non-NULL when the proposed row conflicts with an existing one. A freshly inserted row has no old version, so old.id IS NULL means this call created the tag. The rules, from the same INSERT page: - RETURNING is mandatory, and so is a conflict target such as (name). - An optional WHERE filters which existing rows come back. With FOR UPDATE/SHARE, rows that fail the WHERE are still locked. - Only unique indexes and NOT DEFERRABLE constraints can be arbiters. Exclusion constraints are not supported. - FOR UPDATE/SHARE needs UPDATE privilege on at least one column. Client libraries are starting to pick it up. Sequel 5.108.0, the Ruby toolkit, added :select and :select_where options to insert_conflict for PostgreSQL 19+. As of today PostgreSQL 19 is a release candidate. The RC 1 announcement says it "will be mostly identical to the initial release," plans RC 2 for 12 November and GA for 19 November 2026. The release notes still read "Release date: 2026-??-??". Test against RC 1. Do not run it in production, and do not assume the feature is on your managed Postgres until your provider ships 19. Until then, put the retry loop in a function. In PL/pgSQL, a VOLATILE function (the default) takes a fresh snapshot for each query, so the second pass of the loop sees the row a concurrent transaction just committed. CREATE FUNCTION tag_id(p_name text) RETURNS bigint LANGUAGE plpgsql AS $$ DECLARE v_id bigint; BEGIN LOOP SELECT id INTO v_id FROM tags WHERE name = p_name; IF FOUND THEN RETURN v_id; END IF; INSERT INTO tags (name) VALUES (p_name) ON CONFLICT (name) DO NOTHING RETURNING id INTO v_id; IF FOUND THEN RETURN v_id; END IF; END LOOP; END $$; When you move to 19, the whole function collapses into the three-line DO SELECT statement at the start of this section. --- If this was useful Get-or-create is the kind of query that looks finished in review and fails under load, and the fix depends on how your store handles snapshots, locks and row versions. The Database Playbook is about that layer: choosing a store for the workload you have, and knowing which of its guarantees your code is leaning on. If this post had you rereading your upsert helpers, that is the book I wrote for it. ![Database Playbook: Choosing the Right Store for Every System You Build](amazon.com)

    5 reactions · 1 comment
  5. Type a pipe() That Checks Every Step: Variadic Tuples, infer and One Recursive Type - Book: The TypeScript Type System - Also by me: The TypeScript Library — the 5-book collection - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- Open Effect's Function.ts and count the signatures of pipe. There are 20: one for a bare value, then one more for every extra function, up to 19. RxJS 7's standalone pipe types nine functions and then falls back to a catch-all signature that returns UnaryFunction<T, unknown>. Hand it a tenth function and the output type is gone. Those libraries know exactly what they are doing. When TypeScript 4.0 shipped variadic tuple types, the release notes called this pattern "death by a thousand overloads", and overloads are still how the big libraries type pipe. The reason is that a single generic signature that checks every step is harder to write than it looks, and it gives some things up. You're going to write that single signature anyway: about 40 lines of types, checked by tsc --noEmit, and then the four places it breaks so you know when overloads win. Step 1: read the ends with infer Start with four small helpers. In and Out pull the parameter and return type out of a one-argument function. First and Last pull the first and last element out of a tuple. // pipe.ts export type Fn = (arg: any) => any; type In<F> = F extends (arg: infer A) => any ? A : never; type Out<F> = F extends (arg: any) => infer R ? R : never; type First<T extends unknown[]> = T extends [infer F, ...unknown[]] ? F : never; type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never; Last matches with a leading rest element, [...unknown[], infer L], which TypeScript has accepted since 4.2. With these you can write the obvious pipe type: the input of the first function, the output of the last. type NaivePipe<Fns extends Fn[]> = (input: In<First<Fns>>) => Out<Last<Fns>>; It reads the ends and never looks at the middle. Give it trim (string to string), len (string to number) and shout (string to string) and it reports (input: string) => string, even though shout would receive a number at runtime. A pipe type that only reads the ends is an as cast with better marketing. !A pipeline of joined segments where only the first and last joints are inspected and a mismatched joint in the middle goes unnoticed Reading only the first input and the last output leaves every joint in the middle unchecked. Step 2: walk the list with one recursive type The real check has to visit every adjacent pair. Recursive conditional types (TypeScript 4.1) let a type refer to itself in its own branches, so you peel one function off the front, compare its input with the value flowing in, and recurse on the rest. type PipeError<Step, Got, Want> = { readonly error: "pipe(): a step got the wrong input"; readonly step: Step; readonly got: Got; readonly want: Want; }; type Check< Fns extends Fn[], Input, Cur, Done extends unknown[] = [] > = Fns extends [infer F, ...infer Rest extends Fn[]] ? [Cur] extends [In<F>] ? Check<Rest, Input, Out<F>, [...Done, F]> : PipeError<[...Done, F]["length"], Cur, In<F>> : (input: Input) => Cur; export type Pipe<Fns extends [Fn, ...Fn[]]> = Check< Fns, In<First<Fns>>, In<First<Fns>> >; Four details carry the weight here. infer Rest extends Fn[] puts a constraint on the inferred rest, a feature from TypeScript 4.7. Without it, Rest can't be passed back into Check, whose first parameter demands Fn[]. [Cur] extends [In<F>] wraps both sides in brackets on purpose. A bare Cur extends ... on a type parameter is distributive: if a step returns string | number, the check would split into one branch per member and hand you a union of a function and an error. The brackets compare the whole union at once. Done collects the functions already visited. Its only job is to number the failing step: [...Done, F]["length"] is the 1-based position of F as a number literal type. The recursive call is the entire branch result. Nothing wraps it, and that makes Check tail-recursive, which matters when chains get long. The runtime part is short: export function pipe<Fns extends [Fn, ...Fn[]]>( ...fns: Fns ): Pipe<Fns> { const run = (input: unknown) => fns.reduce((acc: unknown, fn) => fn(acc), input); return run as unknown as Pipe<Fns>; } Fns extends [Fn, ...Fn[]] on a rest parameter makes TypeScript infer the arguments as a tuple, one element per function, which is what Check walks. The double cast is the honest part: the body can't prove a deferred conditional type, so you assert once at the boundary and callers get the checked type. The overloaded libraries pay the same toll in their own way; Effect's implementation signature is pipe(a: unknown, ...args: Array<any>): unknown. Step 3: prove it with @ts-expect-error A type you can't test is a type you will break by accident. // @ts-expect-error (TypeScript 3.9) suppresses the error on the next line, and if that line has no error, tsc reports Unused '@ts-expect-error' directive. That turns every expected failure into an assertion. // pipe.types.ts import { pipe } from "./pipe"; type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; type Expect<T extends true> = T; const trim = (s: string) => s.trim(); const len = (s: string) => s.length; const isLong = (n: number) => n > 10; const shout = (s: string) => s.toUpperCase(); const ok = pipe(trim, len, isLong); export type OkIsTyped = Expect< Equal<typeof ok, (input: string) => boolean> >; // @ts-expect-error the input must be a string ok(42); const broken = pipe(trim, len, shout); // @ts-expect-error step 3 wants a string, gets a number broken(" hello "); // @ts-expect-error a broken pipe is not a function const f: (s: string) => string = broken; // @ts-expect-error pipe() needs at least one step pipe(); Equal compares two types strictly, so a result that drifts to any or to a wider type fails it; Expect fails to compile when handed false. A minimal tsconfig.json next to the two files: { "compilerOptions": { "strict": true, "noEmit": true, "target": "es2022", "module": "esnext", "moduleResolution": "bundler" }, "include": ["pipe.ts", "pipe.types.ts"] } Install the compiler with npm i -D typescript, then run npx tsc. The config needs TypeScript 5.0 or newer for "moduleResolution": "bundler"; the types themselves need 4.7. If any expectation stops holding, either an Expect line or an unused directive fails the build, locally or in CI. Where the recursive version breaks Look again at the broken test. The pipe(trim, len, shout) line compiles. The type of broken is PipeError<3, number, string>, and the error only appears where you call it or assign it to a function type. Hovering broken tells you the step, what it got and what it wanted, which beats a silent unknown, but the red squiggle sits on the wrong line. Overloads don't have this problem: each step is its own parameter, so a mismatch is an argument error on the exact function that's wrong. There is a variant that rewrites the rest parameter to the expected tuple so the error moves onto the bad argument; ecyrbe's write-up shows one. I haven't run it, and a commenter there reports a gap in its first-to-second step check, so test it the same way before you trust it. Generic steps lose their type parameter. Take const wrap = <T>(x: T): T[] => [x]. When In and Out match a generic signature against infer, the type parameter doesn't survive; TypeScript issue #38964 shows ReturnType of <T = string>() => T coming out as unknown. So pipe(trim, wrap, join), with join taking string[], fails with got: unknown[]. The fix is an instantiation expression (4.7): pass wrap<string> as the step and the chain checks. Overloads get this for free, because each step is matched against (a: A) => B with A already known, and since TypeScript 3.4 a composer can even propagate a generic step's type parameters into the result. Inline lambdas aren't typed from the previous step. In Effect, pipe(" hi ", (s) => s.trim(), (s) => s.length) types each s from the step before it, because every overload names the intermediate types as separate type parameters. In ...fns: Fns, nothing ties the second slot's parameter to the first slot's return, so an unannotated (s) => s.length has no way to learn that s is a string. The best it gets is the any from the Fn constraint, and an any step checks nothing. Annotate every inline step, or write the steps as named functions. Long chains hit the depth limit, depending on how you wrote the recursion. The TypeScript PR that added tail-recursion elimination lets tail-recursive conditional types loop 1,000 times before giving up, and lowered the general limit for the rest to 100. Check is tail-recursive, so a 30-step pipe is nowhere near the ceiling. Rewrite it so the recursive call sits inside something else, a tuple spread or a union, and you are back under the general limit, where the failure is the familiar Type instantiation is excessively deep and possibly infinite from the 4.5 release notes. Even well under the limit, the 4.1 notes warn that recursive types can increase type-checking time, so keep the step types themselves simple. When overloads are still the better choice !A tall stack of near-identical cards on one side and a single looping arrow passing through a row of function blocks on the other Overloads write one signature per chain length. The recursive type writes one signature that walks the list. The rule that falls out of the four breaks: choose by who writes the steps. If callers write steps inline, as lambdas, or pass generic helpers like map and filter, use overloads. That is the RxJS and Effect situation, where most steps are operators with type parameters and most callbacks are unannotated. Contextual typing per step and per-argument errors are worth more than an unlimited chain length, and very few real pipelines need more than the 19 functions Effect covers. If callers pass named, already-typed, non-generic functions, use the recursive type. Validation chains, middleware lists and data transforms assembled from a registry all fit: the steps already carry their own types, chains can be longer than any overload stack, and the PipeError type names the failing step. Ship it with the @ts-expect-error file so the next refactor can't quietly break the check. --- If this was useful The recursive Check type is a small version of a pattern that keeps coming back: walk a tuple with infer, carry state in an accumulator, keep the recursion in tail position. The TypeScript Type System goes deep on that toolkit: generics, mapped and conditional types, infer, template literals, branded types. If you already write TypeScript daily, that's the one to read next; if you're still getting comfortable with narrowing and modules, start with TypeScript Essentials. - TypeScript Essentials — types, narrowing, modules, async, daily-driver tooling - The TypeScript Type System — generics, mapped/conditional types, infer, template literals, branded types - Kotlin and Java to TypeScript — JVM developers moving to TypeScript - PHP to TypeScript — PHP 8+ developers moving to TypeScript - TypeScript in Production — tsconfig, build tools, monorepos, library authoring, dual ESM/CJS, JSR Coming from Kotlin, Java or PHP? The two bridge books replace the first two for you. Shipping TypeScript at work, with build tooling and published packages? TypeScript in Production covers that layer. All five books ship in ebook, paperback, and hardcover. ![The TypeScript Library — the 5-book collection](xgabriel.com)

    5 reactions · 1 comment
  6. Claude Took Anthropic From 2,500 to 30,000 Merges a Day. Then It Moved Off Git - Book: AI Agents Pocket Guide - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- In May 2026, Anthropic merged around 2,500 code changes a day. By September it was merging more than 30,000 a day, according to the company's engineering blog. Anthropic names the cause itself. Claude's ability to write code, the post says, is "what put us in this situation in the first place." It also names where things gave way first: "The bottleneck, now, is all of the developer infrastructure built for human scale: source control, builds, and CI." On October 13, Anthropic published what it did about it. A team of seven built Scion, an in-house version control system, plus a code review tool called Antique, and fully migrated the monorepo off git in five months. The post drew a busy Hacker News thread within hours. What broke at 30,000 merges a day The scale numbers in Anthropic's write-up explain why. "The history of our main branch alone is almost 100 GB, and a compressed working copy is over 25 GB." Anthropic runs "on the order of 25 million code checkouts per day for CI." Then the line that ends the argument: "Fetched naively, that would be over 600 PB of git traffic per day." Anthropic's diagnosis is short: "Git assumes that clients work offline and need a complete copy of the repository." That design is why git is pleasant on a laptop. It is also why every CI job that wants one directory still has to negotiate with history it will never read. Anthropic had been absorbing that load with a fleet of git mirrors, which it now calls "our previous git mirroring infrastructure." !A classic branching version-control graph drawn as a tree, overwhelmed by a dense swarm of small orange commit nodes pouring in from robotic hands, with a narrow pipe at the base straining to carry them When agents write the code, the repository's plumbing becomes the slow part. How Scion breaks git's assumptions Scion starts from the opposite premise, which Anthropic states in four words: "Clients are always online." The rest of the design follows from that: - The server owns history, blame, commits, merges and rebases. The client keeps a bounded local cache and the working files. - Checkouts are sparse by default. Scion's protocol "is centered around sparse checkouts, which only contain the directories the client asks for." Some jobs skip the checkout entirely: "a CI job that needs the diff between a commit and its merge base, for example, can ask the server for it." - Fetches go through tiers: the scn client talks to a per-node scion-daemon, which talks to regional scion-cache servers, which talk to scion-server instances backed by Spanner or cloud storage. Anthropic reports over 99% cache hits at each tier. - Branches are mostly gone. Scion "discourages refs (pointers to commits, such as branches and tags)," except for the main ref and release branches, because shared global state is what stops scaling. The results Anthropic reports: an infrastructure footprint "200x smaller than our previous git mirroring infrastructure," checkouts for CI and research jobs that take "seconds rather than minutes," and "a 25% CPU capacity win" for CI alone. These are Anthropic's own numbers on its own systems, and Anthropic hasn't released anything that would let outsiders reproduce them. None of this is new as a category. Anthropic itself points to Google's Piper and Meta's Sapling as earlier server-centric systems. What is new is the reason: Anthropic built one because its own coding model outran its tooling. The process changes matter more than the VCS The part most teams can learn from has nothing to do with storage. Start with the merge queue. "A proper merge queue, for example, doesn't make much sense at agent scale," Anthropic writes. It moved the safety net later instead: "we've invested in post-merge and pre-deploy automated testing." Review changed too. "Our team members still review every change request for the system," but "they now examine the intent of the change as opposed to reading every line of code." Anthropic does not claim the model got everything right. "Agents will sometimes violate core system constraints to satisfy a pattern." One adversarial review "was overly defensive about trusting another internal service," and Claude piled on extra validation to satisfy it. The upside was large too. The team handed an early internal snapshot of Claude Mythos Preview the whole ingestion pipeline to rewrite, and the new version, per Anthropic, "consumed 20x less memory and was 4x faster." Every engineer who worked on the system would now rate Claude as better at writing code than themselves, the post adds. !Abstract illustration of a request travelling down a tiered stack of cache layers, from a small client node through a middle cache ring to a large central server cylinder, with most requests stopping early at the upper tiers Scion's fetch path: client, node daemon, regional cache, server. Most requests never reach the bottom. Should you leave git? Anthropic says no Anthropic is blunt about this. "Scion is by no means a suitable replacement for git for general use." The concepts "really only make sense for truly enormous monorepos." Anthropic has not announced a public release, and it describes its internal systems as highly bespoke. What it asks for instead is better git infrastructure: "Git platforms can provide comprehensive server-side RPCs like Scion," and "A git server doesn't have to work the same way as a git client does." Two of Scion's ideas already exist in stock git at a smaller scale. A partial clone skips file contents until you need them, and sparse checkout only materialises the directories you name. Both are flags on git clone. For a CI job that touches one service: git clone --filter=blob:none --sparse \ https://github.com/your-org/your-repo.git cd your-repo git sparse-checkout set services/billing It won't get you anywhere near Anthropic's numbers, but the instinct is the same: stop shipping the whole repository to a job that reads one folder. My reading of the post: most teams will not hit 30,000 merges a day, but plenty will hit the moment when agents open changes faster than CI and review can absorb them. That is the moment to look at your merge queue and your review rules, long before you look at your VCS. Several agents on one repo, at laptop scale I build Hermes IDE, so weigh this section with that in mind. Hermes is not affiliated with Anthropic. At Anthropic's volume, branches are shared global state worth avoiding. On one machine with three agents, the problem is smaller and different: two agents in one checkout overwrite each other. In Hermes, ⌘N (Ctrl+Shift+N on Windows and Linux) starts each task on its own hermes/<name> branch in its own git worktree, with the task as the agent's first prompt. Claude Code, Codex, Antigravity CLI and other command-line agents each run in a real terminal. The review side is closer to what Anthropic describes. The Review Desk (⌘G) shows an agent's changes by turn or by file, with risk flags, and sends a line comment back to the agent that wrote it. Reviewing one turn at a time makes it easier to judge the intent behind a change. When you are done, Land commits, opens a pull request or merges locally, and can be undone. How many agent-written changes a day can your CI take before it becomes the queue? Tell me in the comments. --- If this was useful Hermes IDE is free, source-available (BSL 1.1), and runs on macOS, Windows and Linux. Download it and give two agents the same ticket on separate branches, or star it on GitHub if keeping agents apart in one repo matters to you. If you build agents yourself, the AI Agents Pocket Guide covers the patterns behind agents that write and review code, like the ones in Anthropic's post. ![AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs](amazon.com)

    7 reactions

Achievements

Awards earned by the repositories hermes-hq owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.