An Agent in a Sandbox
At the end of this page, a coder Agent has fixed a bug in your fork of jr2. The Agent works in a Sandbox: a pod of its own, with a git checkout of your fork. You go into the Sandbox and read the work. You ask for one more change, push the branch by hand, and approve the work.
Before you start
- Do A model decides first. Your Instance
my-instancehas the model of your tab, andkind-jr2is the current kubectl context. - You need a GitHub account.
- Open two terminals in
my-instance. The first terminal shows the run. The second terminal sends commands to it.
Fork jr2
The Agent works on a Repo that you can push to, so you fork jr2. Your fork holds the tutorial fixture, docs/site/tutorial/greet/. It is a small Node CLI with one bug, and its test trims the name fails.
- Open
https://github.com/snapwich/jr2. - Click Fork.
- Clear Copy the main branch only, so that the fork gets the release tags.
- Click Create fork.
Make sure that the fork has the release tag of your Kit. <you> is your GitHub user name. <version> is the Kit version: the version of @jr2/orchestrator in package.json, such as 0.4.1.
- Open
https://github.com/<you>/jr2/tags. - Find the tag
v<version>, such asv0.4.1.
If the page shows no tags, delete the fork. Fork again with Copy the main branch only cleared.
Make a token for the fork
The Agent cannot push. You push its branch by hand, so you need a token for your fork. On GitHub, make a fine-grained personal access token:
- Click your profile picture, then Settings.
- Click Developer settings, then Personal access tokens, then Fine-grained tokens.
- Click Generate new token.
- In Repository access, select Only select repositories.
- Select
<you>/jr2. - In Permissions, set Contents to Read and write.
- Click Generate token.
- Copy the token.
Keep the token in a safe place. You use it on this page and on the next page.
Install @jr2/machines
The Kit ships Machines in the package @jr2/machines. Install it at the Kit version:
npm install --save-exact @jr2/machines@<version>
added 1 package, and audited 15 packages in 266ms
3 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilitiesYour numbers can be different.
Write the Workflow
The shipped Machine task gives one prompt to a coder Agent in a Workspace. Then the run waits at a Gate named review, until you approve the work. task puts the review Gate in its body, so the id of the Gate is body.review.
task cannot know your Repo, and it cannot pay for your model. Thus it leaves two parts Open: its Repo Slots and the model of its coder. You bind the two with customize().
Make the file workflows/task.ts. Replace <you> and <version> in it:
import { customize } from "@jr2/orchestrator";
import { task } from "@jr2/machines";
// `task` leaves two parts Open: its Repo Slots and the model of its coder. This file binds the two.
// The file name gives the Workflow its name: task.
export const machine = customize(task, {
// One Repo Slot, target: your fork, from the release tag of the Kit that you installed.
// Replace <you> with your GitHub user name. Replace <version> with the Kit version.
repos: { target: { url: "https://github.com/<you>/jr2.git", ref: "v<version>" } },
// The model that the coder Agent uses.
agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
});targetis the name of the Repo Slot. The coder works in the first Repo Slot.refis the release tag. The branch of the Agent starts from it, so each reader of a release starts from the same fixture.- The file name gives the Workflow its name:
task.
Converge the Instance
jr2 up converges the Instance with the new Workflow:
jr2 up…
seats: workflow "task" — who may send what, per state (ADR-0015)
task.working agent:coder: [finish]
task.review gate: [approve, request_changes]
…
sandbox image "default": building jr2-sandbox-my-instance-default:6389bdf72d50-amd64
no USER declared — the pod's uid-1000 fallback applies
kind load → cluster "jr2"
…
repos: 1 bound Repo(s) — the Orchestrator creates their Repo resources at boot and at each publish (ADR-0069); the cache agent clones on first need (`jr2 status` reports sync state)
converged — `jr2 run <workflow>` when readytask is the first Workflow of the Instance that has a Workspace. Thus this jr2 up also builds the Sandbox Image from images/default/Dockerfile, and loads it into the kind cluster. If you did not bind an Open part, jr2 up stops and prints the customize() line that binds it.
Start the run
In the first terminal, start a run of task with jr2 run. The prompt tells the Agent what to fix and how to check the fix:
jr2 run task --input '{"prompt":"In docs/site/tutorial/greet, one test fails. Fix the bug in docs/site/tutorial/greet/src/greet.js. To check your fix, run node --test in docs/site/tutorial/greet. Do not change files outside that folder. Do not run the tests of the repo root."}'…
→ context kind-jr2 / namespace my-instance
run a7a8fc26-851f-4459-9efb-845389acfda9 (task) — attached
→ active "placing"
→ active "provisioning"
→ active "attaching"
→ active "running"
…The line run <run id> (task) — attached gives the run id. Your run id is different. As on the last page, the first line of the output, → .env:, shows as …. Keep this terminal open. The run does not end until you approve the work.
Wait for the review Gate
The Sandbox starts, and the coder works. When the coder finishes, the run waits at the review Gate. In the second terminal, read the status of the run with jr2 status. <run id> is the run id, or its first 4 characters or more:
jr2 status <run id>The status is one line of JSON. While the coder works, gates is []. When the run waits at review, gates holds the Gate. Its gate is body.review, and the line ends with its meta:
…"gate":"body.review",…,"meta":{"summary":"Fixed the \"trims the name\" test failure in docs/site/tutorial/greet. The bug was that the name wasn't being trimmed before constructing the greeting output. Added `.trim()` to the name extraction in src/greet.js, changing `args.find(...) ?? \"world\"` to `(args.find(...) ?? \"world\").trim()`. All 6 tests now pass.","branch":"jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16","worktree":"/work/target/jr2-task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16"}}]}Run the command again until gates holds the Gate.
summaryis what the coder says that it did, in the words of the model. Your summary is different.branchis the branch that the coder commits on.worktreeis the folder in the Sandbox that holds the branch. It is the Worktree.
Copy branch and worktree from your output. The part after jr2/task-, here d8f59a0c-9ea1-43fc-8d0c-b153caf70f16, is the Instance ID of the run. jr2 status prints it as instanceId. It is not the run id. Do not make the branch name from the run id. With the Instance token, the drawer of the Console shows the same meta.
If meta has a reason and no summary, the Turn of the coder failed. Send request_changes, as in Ask for a change, and the coder tries again.
Find the Sandbox
The label jr2.dev/run on the Sandbox holds the full run id. A prefix does not work here. Copy the full run id from the line run <run id> (task) — attached in the first terminal:
kubectl -n my-instance get sandboxes -l jr2.dev/run=<run id>NAME PHASE ENDPOINT AGE
ws-a7a8fc26-x-38-oakg23 Ready http://ws-a7a8fc26-x-38-oakg23.my-instance.svc:8080 5m10sws-a7a8fc26-x-38-oakg23 is the name of the Sandbox. Your Sandbox has a different name. It starts with ws- and the first 8 characters of the run id. The pod of the Sandbox has the same name. The commands below write it as <sandbox>.
Look inside the Sandbox
Go into the harness container of the pod. The Agent's tools run in this container:
kubectl -n my-instance exec -it <sandbox> -c harness -- bashIn the Sandbox, go to the fixture in the Worktree. Then run its tests. <worktree> is the worktree that you copied:
cd <worktree>/docs/site/tutorial/greet
node --test✔ greets the world by default (1.172165ms)
✔ greets a name (0.138615ms)
✔ shouts (0.117168ms)
✔ trims the name (0.067037ms)
✔ prints help (0.642445ms)
✔ refuses an unknown flag (0.281559ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 55.318206Your times are different. Read the commits of the coder. The branch starts at the release tag, so the commits after the tag are the commits of the coder:
git log --oneline v<version>..HEAD1a84393 Fix: trim the name before using it in greet outputEach line is one commit. The commit ids and the messages come from the model. The author of each commit is jr2 Agent <agent@jr2.invalid>, the default git author that jr2 gives a Sandbox. Then leave the Sandbox:
exitAsk for a change
In the second terminal, send request_changes to the review Gate with jr2 send. --gate takes the gate value from jr2 status. Put your notes in the input:
jr2 send <run id> --gate body.review --event request_changes --input '{"notes":"Also add a test that trims the name when you pass --shout."}'…
→ context kind-jr2 / namespace my-instance
delivered request_changes to gate "body.review" on a7a8fc26-851f-4459-9efb-845389acfda9The coder continues the same conversation, with your notes as its next prompt. The Sandbox and the branch stay. Run jr2 status <run id> again until gates holds the review Gate. The new summary tells what the coder changed.
Push the branch
task does not push, and the Agent has no credential to push with. While the run waits at review, you push the branch from the Sandbox. Go into the Sandbox again:
kubectl -n my-instance exec -it <sandbox> -c harness -- bashIn the Sandbox, go to the Worktree. Then push the branch. <branch> is the branch that you copied:
cd <worktree>
git push origin <branch>git push asks for a user name and a password. Give <you> as the user name. Give the token as the password.
Username for 'https://github.com': <you>
Password for 'https://<you>@github.com':
…
To https://github.com/<you>/jr2.git
* [new branch] jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16 -> jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16origin fetches through the Repo cache of the node, and pushes to your fork. Open your fork on GitHub to see the branch. Then leave the Sandbox:
exitApprove the work
Caution
approve ends the run and removes the Sandbox. A commit that you did not push is lost.
In the second terminal, send approve to the review Gate. approve takes no input:
jr2 send <run id> --gate body.review --event approve…
→ context kind-jr2 / namespace my-instance
delivered approve to gate "body.review" on a7a8fc26-851f-4459-9efb-845389acfda9In the first terminal, jr2 run prints the end of the run, then the final status as one line of JSON, and stops:
…
→ active "teardown"
→ done "done"
{"runId":"a7a8fc26-851f-4459-9efb-845389acfda9","workflow":"task",…,"output":{"branch":"jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16"}}Make sure that the Sandbox is gone
Use the full run id again:
kubectl -n my-instance get sandboxes -l jr2.dev/run=<run id>No resources found in my-instance namespace.What happened
taskis a Machine in a Workspace. The run made a Sandbox for the Workspace and cloned your fork into it. It cut the branch from the release tag, in a Worktree in/work/target/. See Workspaces.taskleft its Repo Slots and the model of its coder Open.customize()bound them where you registered the Workflow, andjr2 uprefuses a Workflow with an Open part. See Composition and ADR-0054.- The
reviewGate kept the Sandbox while the run waited.request_changessent the coder back to the same conversation.approveended the run, and the end of the Workspace removed the Sandbox. See Gates and ADR-0012. - You used
kubectl execto go into the Sandbox. The guide Look inside a Sandbox gives the same commands. - You pushed by hand, because
taskdoes not push. On the next page, a Machine pushes the branch with an Effect.