Skip to content

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-instance has the model of your tab, and kind-jr2 is 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.

  1. Open https://github.com/snapwich/jr2.
  2. Click Fork.
  3. Clear Copy the main branch only, so that the fork gets the release tags.
  4. 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.

  1. Open https://github.com/<you>/jr2/tags.
  2. Find the tag v<version>, such as v0.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:

  1. Click your profile picture, then Settings.
  2. Click Developer settings, then Personal access tokens, then Fine-grained tokens.
  3. Click Generate new token.
  4. In Repository access, select Only select repositories.
  5. Select <you>/jr2.
  6. In Permissions, set Contents to Read and write.
  7. Click Generate token.
  8. 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:

sh
npm install --save-exact @jr2/machines@<version>
text

added 1 package, and audited 15 packages in 266ms

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Your 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:

ts
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" } },
});
  • target is the name of the Repo Slot. The coder works in the first Repo Slot.
  • ref is 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:

sh
jr2 up
text
…
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 ready

task 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:

sh
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."}'
text
…
→ 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:

sh
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:

text
…"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.

  • summary is what the coder says that it did, in the words of the model. Your summary is different.
  • branch is the branch that the coder commits on.
  • worktree is 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:

sh
kubectl -n my-instance get sandboxes -l jr2.dev/run=<run id>
text
NAME                      PHASE   ENDPOINT                                              AGE
ws-a7a8fc26-x-38-oakg23   Ready   http://ws-a7a8fc26-x-38-oakg23.my-instance.svc:8080   5m10s

ws-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:

sh
kubectl -n my-instance exec -it <sandbox> -c harness -- bash

In the Sandbox, go to the fixture in the Worktree. Then run its tests. <worktree> is the worktree that you copied:

sh
cd <worktree>/docs/site/tutorial/greet
node --test
text
✔ 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.318206

Your 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:

sh
git log --oneline v<version>..HEAD
text
1a84393 Fix: trim the name before using it in greet output

Each 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:

sh
exit

Ask 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:

sh
jr2 send <run id> --gate body.review --event request_changes --input '{"notes":"Also add a test that trims the name when you pass --shout."}'
text
…
→ context kind-jr2 / namespace my-instance
delivered request_changes to gate "body.review" on a7a8fc26-851f-4459-9efb-845389acfda9

The 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:

sh
kubectl -n my-instance exec -it <sandbox> -c harness -- bash

In the Sandbox, go to the Worktree. Then push the branch. <branch> is the branch that you copied:

sh
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.

text
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-b153caf70f16

origin 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:

sh
exit

Approve 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:

sh
jr2 send <run id> --gate body.review --event approve
text
…
→ context kind-jr2 / namespace my-instance
delivered approve to gate "body.review" on a7a8fc26-851f-4459-9efb-845389acfda9

In the first terminal, jr2 run prints the end of the run, then the final status as one line of JSON, and stops:

text
…
  → 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:

sh
kubectl -n my-instance get sandboxes -l jr2.dev/run=<run id>
text
No resources found in my-instance namespace.

What happened ​

  • task is 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.
  • task left its Repo Slots and the model of its coder Open. customize() bound them where you registered the Workflow, and jr2 up refuses a Workflow with an Open part. See Composition and ADR-0054.
  • The review Gate kept the Sandbox while the run waited. request_changes sent the coder back to the same conversation. approve ended the run, and the end of the Workspace removed the Sandbox. See Gates and ADR-0012.
  • You used kubectl exec to go into the Sandbox. The guide Look inside a Sandbox gives the same commands.
  • You pushed by hand, because task does not push. On the next page, a Machine pushes the branch with an Effect.

Next ​

Have your Assistant write a Machine

The docs of Kit version 0.5.1. Each page is also at its .md URL.