The sceneLa escena
Three agents, one mainTres agentes, un solo main
1. Three agents, one fileTres agentes, un fichero
Three agents get three small tasks in the same repository. Two of them, A and B, are asked to reword the same line of notes.txt. The third, C, only adds a line at the end. They finish within moments of each other.
Tres agentes reciben tres encargos pequeños en el mismo repositorio. A dos de ellos, A y B, les toca reescribir la misma línea de notes.txt. El tercero, C, solo añade una línea al final. Terminan casi a la vez.
- agent-a · notes.txt
- alpha + alpha, from a - agent-b · notes.txt
- alpha + alpha, from b - agent-c · notes.txt
gamma + delta, from c
2. In git, they step on each otherEn git, se pisan
Put them in one checkout and they trip over its single index:
Si trabajan en la misma copia, chocan con el índice, porque solo hay uno:
fatal: Unable to create '…/.git/index.lock': File exists.
Another git process seems to be running in this repository, e.g. …
So each one gets a clone of its own, and the fight moves to the push. C gets in first. A is turned away:
Así que cada uno se lleva su propio clon, y la pelea pasa al push. C llega primero. Al agente A le rechazan el suyo:
! [rejected] main -> main (fetch first)
error: failed to push some refs to '…/remote.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. …
A fetches, rebases and pushes again. B is turned away too: its rebase runs into A's edit, it fixes the conflict and pushes again, and if anyone got in meanwhile, it is turned away once more. Every agent that loses the race runs the same lap: fetch, rebase, push, again. The more agents, the more laps; we counted them in Measured against git. And much of what comes back is text written for people.
A hace fetch, rebase y vuelve a hacer push. Con B pasa lo mismo, pero al hacer rebase choca con el cambio de A: arregla el conflicto, vuelve a empujar y, si alguien ha entrado entretanto, otra vez rechazado. Todo agente que pierde la carrera da la misma vuelta: fetch, rebase, push, y vuelta a empezar. Cuantos más agentes, más vueltas; las contamos en «Medido frente a git». Y buena parte de lo que devuelve git es texto pensado para personas.
3. The same three agents, in braidLos mismos tres, en braid
Braid gives each agent a workspace of its own over one shared store. There is no index to fight over and no staging area: an agent commits what it changed, and lands it. All three commit before anyone lands; then they land as they finish.
Braid da a cada agente su propio espacio de trabajo sobre un único almacén compartido. No hay índice por el que pelearse ni área de preparación: el agente hace commit de lo que ha cambiado y lo aterriza. Los tres hacen commit antes de que aterrice nadie, y luego aterrizan según van terminando.
C is first. Nothing has landed since it started, so its change goes straight in:
C es el primero. No ha aterrizado nada desde que empezó, así que su cambio entra directo:
[agent-c] $ braid land
Landed e39b1668831b on main (fast-forward)
A is one changeset behind. It does not fetch and it does not retry: in one short step, braid rebases A's change onto C's and merges the one file both touched.
A va un changeset por detrás. Ni hace fetch ni reintenta: en un solo paso corto, braid le hace rebase sobre el cambio de C y fusiona el único fichero que han tocado los dos.
[agent-a] $ braid land
Landed on main as e0c85ddea85b, rebased onto e39b1668831b (1 path merged)
Updated 1 path in the working copy
4. Main did not movemain no se movió
B changed the same line as A. That is a real conflict, and no tool should guess its way through one. Braid stops and leaves main where it was:
B cambió la misma línea que A. Eso es un conflicto de verdad, y ahí ninguna herramienta debería ir a ciegas. Braid se para y deja main donde estaba:
[agent-b] $ braid land
65bb04180402 conflicts with main at e0c85ddea85b; main did not move:
CONTENT notes.txt (1 region)
Run `braid update` to bring the conflicts into the working copy.
It exits with status 2. A command line braid cannot parse exits with 2 as well, so to be sure an agent asks for --json: only a conflict prints "outcome": "conflicts", and the conflict itself comes back as data. Each side is named from B's point of view: base is the common ancestor, branch is what main holds now, workspace is B's own change. Lines count from 0 with the end left out, and next says what to run:
Sale con código 2. Un comando que braid no sabe interpretar también sale con 2, así que, para ir sobre seguro, el agente pide --json: solo un conflicto imprime "outcome": "conflicts", y el propio conflicto llega como datos. Cada lado se nombra desde el punto de vista de B: base es el ancestro común, branch lo que tiene main ahora y workspace su propio cambio. Las líneas se cuentan desde 0 y el final del tramo no se incluye; next le dice qué ejecutar:
[agent-b] $ braid land --json
{
…
"conflicts": [{
…
"hunks": [{
"base": {
"end": 2, "start": 1,
"text": "alpha\n" },
"branch": {
"end": 2, "start": 1,
"text": "alpha, from a\n" },
"workspace": {
"end": 2, "start": 1,
"text": "alpha, from b\n" }
}],
"kind": "CONTENT",
"path": "notes.txt",
…
}],
…
"next": "braid update",
"operation": null,
"outcome": "conflicts",
…
}
B runs braid update. Its copy gets the markers, with C's line already merged below them:
B ejecuta braid update. Su copia recibe las marcas de conflicto, con la línea de C ya fusionada debajo:
[agent-b] $ braid update
65bb04180402 conflicts with e0c85ddea85b; the working copy has the merge so far:
CONTENT notes.txt (1 region)
Fix the files, `braid resolve` the other conflicts, then `braid commit`.
Updated 1 path in the working copy
title
<<<<<<< main (e0c85ddea85b)
alpha, from a
=======
alpha, from b
>>>>>>> 65bb04180402 (b: alpha)
beta
gamma
delta, from c
Until the markers are gone, braid status says why a commit would be refused:
Mientras queden marcas, braid status explica por qué no aceptaría el commit:
[agent-b] $ braid status
…
Not ready to commit:
- conflict markers remain in "notes.txt": edit them out
B writes one line that keeps both edits and commits without a new message, so the change keeps its own. Then it lands:
B escribe una línea que recoge las dos ediciones y hace commit sin mensaje nuevo, así que el cambio conserva el suyo. Después aterriza:
[agent-b] $ braid commit
Committed 4938acdd0794 (1 path)
Rebase finished at 4938acdd0794; `braid land` when ready
[agent-b] $ braid land
Landed 4938acdd0794 on main (fast-forward)
Start the three landings at the same moment and the order changes from run to run. The shape does not: one goes straight in, one is rebased, one gets the conflict.
Si lanzas los tres aterrizajes a la vez, el orden cambia de una ejecución a otra, pero la forma no: uno entra directo, a otro se le hace rebase y otro se lleva el conflicto.
5. Everyone ends with the same file, and git sees all of itTodos acaban con el mismo fichero, y git lo ve todo
A and C run braid update and get the same file as B. Each line names the agent that last changed it, even through a rebase:
A y C hacen braid update y les llega el mismo fichero que tiene B. Cada línea dice qué agente la cambió por última vez, también tras un rebase:
$ braid blame main notes.txt
f161e3fa5539 seeder … 1 title
4938acdd0794 agent-b … 2 alpha, from a and b
f161e3fa5539 seeder … 3 beta
f161e3fa5539 seeder … 4 gamma
e39b1668831b agent-c … 5 delta, from c
The op log says who did what, and how:
El registro de operaciones cuenta quién hizo qué, y cómo:
$ braid op log -n 3
#4 agent-b land workspace b on main: fast-forward 4938acdd0794 -> 4938acdd0794 (0 paths merged)
main: e0c85ddea85b -> 4938acdd0794
#3 agent-a land workspace a on main: rebased a984ef34e1c9 -> e0c85ddea85b (1 path merged)
main: e39b1668831b -> e0c85ddea85b
#2 agent-c land workspace c on main: fast-forward e39b1668831b -> e39b1668831b (0 paths merged)
main: f161e3fa5539 -> e39b1668831b
And git keeps working. One command pushes main to a git remote as a fast-forward, with a lease; history there is never rewritten. Each changeset arrives as one git commit that names its agent in an agent header:
Y git sigue funcionando. Un solo comando sube main a un remoto de git como fast-forward y con lease: la historia del remoto nunca se reescribe. Cada changeset llega como un commit de git que nombra a su agente en una cabecera agent:
$ braid git sync ../remote.git
Pushed main (4938acdd0794) to main as be3c44e7383b.
$ git -C ../remote.git cat-file commit main
…
agent agent-b
rebased-from 65bb04180402…
…
This scene is one of braid's tests, crates/cli/tests/three_agents.rs, which checks each landing, the conflict, the file all three end with, the op log and the push. CI runs it on every push to main and every pull request. Ids change on every run, because every signature carries its time.
Esta escena es uno de los tests de braid, crates/cli/tests/three_agents.rs, que comprueba cada aterrizaje, el conflicto, el fichero con el que acaban los tres, el registro de operaciones y el push. La integración continua lo ejecuta en cada push a main y en cada pull request. Los ids cambian en cada ejecución, porque cada firma lleva su hora.
Run it yourselfRepítela tú
# with braid on your PATH (see Quick start)
export BRAID_AUTHOR="Ada Lovelace <ada@example.org>"
cd "$(mktemp -d)"
braid init repo
printf 'title\nalpha\nbeta\ngamma\n' > seed.txt
(cd repo && BRAID_AGENT=seeder braid change create --base none \
--set notes.txt=@../seed.txt -m "Seed the notes" --land main)
for n in a b c; do (cd repo && braid workspace add $n ../$n); done
printf 'title\nalpha, from a\nbeta\ngamma\n' > a/notes.txt
printf 'title\nalpha, from b\nbeta\ngamma\n' > b/notes.txt
printf 'title\nalpha\nbeta\ngamma\ndelta, from c\n' > c/notes.txt
(cd a && BRAID_AGENT=agent-a braid commit -m "a: alpha")
(cd b && BRAID_AGENT=agent-b braid commit -m "b: alpha")
(cd c && BRAID_AGENT=agent-c braid commit -m "c: delta")
(cd c && BRAID_AGENT=agent-c braid land) # fast-forward
(cd a && BRAID_AGENT=agent-a braid land) # rebased, 1 path merged
(cd b && BRAID_AGENT=agent-b braid land) # exit status 2: main did not move
(cd b && BRAID_AGENT=agent-b braid update) # markers in b/notes.txt
printf 'title\nalpha, from a and b\nbeta\ngamma\ndelta, from c\n' > b/notes.txt
(cd b && BRAID_AGENT=agent-b braid commit) # keeps its message
(cd b && BRAID_AGENT=agent-b braid land) # fast-forward
(cd a && BRAID_AGENT=agent-a braid update) # a gets the same file as b
(cd c && BRAID_AGENT=agent-c braid update) # and so does c
(cd repo && braid op log -n 3)
(cd repo && braid blame main notes.txt)
git init --quiet --bare remote.git
(cd repo && braid git sync ../remote.git)