@jr2/orchestrator 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/docs/agents-block.md +2 -2
  2. package/docs/concepts/agents-and-turns.md +56 -20
  3. package/docs/concepts/architecture.md +30 -17
  4. package/docs/concepts/charts.md +6 -3
  5. package/docs/concepts/converge.md +3 -0
  6. package/docs/concepts/effects.md +86 -31
  7. package/docs/concepts/machines-and-workflows.md +9 -0
  8. package/docs/concepts/pools-and-sources.md +6 -6
  9. package/docs/concepts/secrets-and-trust.md +126 -42
  10. package/docs/concepts/the-menu.md +3 -0
  11. package/docs/concepts/workspaces.md +129 -51
  12. package/docs/get-started/an-agent-in-a-sandbox.md +21 -19
  13. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  14. package/docs/get-started/install.md +4 -0
  15. package/docs/guides/add-tools-to-the-sandbox.md +17 -4
  16. package/docs/guides/bind-open-parts.md +5 -4
  17. package/docs/guides/cut-a-worktree.md +560 -0
  18. package/docs/guides/fix-a-failed-converge.md +1 -1
  19. package/docs/guides/let-an-agent-write-a-chart.md +5 -1
  20. package/docs/guides/look-inside-a-sandbox.md +18 -15
  21. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  22. package/docs/guides/push-and-sign-a-branch.md +67 -21
  23. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  24. package/docs/guides/run-one-worker-per-item.md +19 -18
  25. package/docs/guides/set-an-agents-access-and-dials.md +67 -22
  26. package/docs/guides/size-a-workspace.md +19 -6
  27. package/docs/guides/use-a-hosted-provider.md +6 -0
  28. package/docs/guides/watch-a-run.md +47 -24
  29. package/docs/guides/write-a-workflow.md +1 -1
  30. package/docs/llms.txt +33 -12
  31. package/docs/reference/authoring-surface/agent.md +34 -11
  32. package/docs/reference/authoring-surface/answer.md +19 -3
  33. package/docs/reference/authoring-surface/arg.md +27 -5
  34. package/docs/reference/authoring-surface/branchname.md +15 -0
  35. package/docs/reference/authoring-surface/command.md +36 -21
  36. package/docs/reference/authoring-surface/fail.md +17 -4
  37. package/docs/reference/authoring-surface/file.md +36 -16
  38. package/docs/reference/authoring-surface/http.md +108 -8
  39. package/docs/reference/authoring-surface/isready.md +14 -0
  40. package/docs/reference/authoring-surface/jr2setup.md +1 -1
  41. package/docs/reference/authoring-surface/open.md +36 -5
  42. package/docs/reference/authoring-surface/paramsof.md +22 -4
  43. package/docs/reference/authoring-surface/pool.md +4 -4
  44. package/docs/reference/authoring-surface/push.md +30 -12
  45. package/docs/reference/authoring-surface/readyof.md +21 -0
  46. package/docs/reference/authoring-surface/secret.md +25 -4
  47. package/docs/reference/authoring-surface/source.md +3 -3
  48. package/docs/reference/authoring-surface/type-agentdefinition.md +2 -2
  49. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  50. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  51. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  52. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  53. package/docs/reference/authoring-surface/type-binding.md +5 -6
  54. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  55. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  56. package/docs/reference/authoring-surface/type-commandoutput.md +22 -4
  57. package/docs/reference/authoring-surface/type-customize.md +4 -2
  58. package/docs/reference/authoring-surface/type-dispatchoutput.md +8 -1
  59. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  60. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  61. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  62. package/docs/reference/authoring-surface/type-hostinjectedinput.md +2 -4
  63. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  64. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  65. package/docs/reference/authoring-surface/type-httpoutput.md +7 -1
  66. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  67. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  68. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  69. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  70. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  71. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  72. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  73. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  74. package/docs/reference/authoring-surface/type-poolspec.md +2 -2
  75. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  76. package/docs/reference/authoring-surface/type-pushoutput.md +32 -7
  77. package/docs/reference/authoring-surface/type-reposlot.md +5 -5
  78. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  79. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  80. package/docs/reference/authoring-surface/type-sourcespec.md +10 -10
  81. package/docs/reference/authoring-surface/type-workspaced.md +29 -8
  82. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  83. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  84. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  85. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  86. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  87. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  88. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  89. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  90. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  91. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  92. package/docs/reference/authoring-surface/workspace.md +65 -37
  93. package/docs/reference/authoring-surface/worktree.md +71 -0
  94. package/docs/reference/authoring-surface.md +30 -10
  95. package/docs/reference/cli.md +1 -1
  96. package/docs/reference/config.md +13 -4
  97. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  98. package/docs/reference/decisions/0002-duplex-streaming-actor.md +17 -10
  99. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  100. package/docs/reference/decisions/0005-sandbox-pod-composition.md +29 -19
  101. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  102. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +30 -30
  103. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +35 -17
  104. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +8 -2
  105. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  106. package/docs/reference/decisions/0017-pool-over-a-source-port.md +6 -4
  107. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +21 -9
  108. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +2 -1
  109. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +8 -9
  110. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +62 -65
  111. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +9 -1
  112. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +13 -12
  113. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +4 -1
  114. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +18 -19
  115. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +8 -8
  116. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +4 -4
  117. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  118. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  119. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  120. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +24 -24
  121. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +5 -3
  122. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +110 -67
  123. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +101 -74
  124. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +5 -6
  125. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +2 -0
  126. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +181 -153
  127. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +74 -63
  128. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +31 -20
  129. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  130. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  131. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  132. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  133. package/docs/reference/decisions.md +3 -0
  134. package/docs/reference/glossary.md +51 -12
  135. package/docs/reference/http-api.md +45 -7
  136. package/docs/reference/machines.md +0 -1
  137. package/docs/why.md +5 -4
  138. package/package.json +3 -3
  139. package/src/actor.ts +161 -35
  140. package/src/agent.ts +6 -6
  141. package/src/ambient.ts +3 -5
  142. package/src/chart.ts +186 -19
  143. package/src/charts.ts +9 -2
  144. package/src/config.ts +16 -6
  145. package/src/custodian.lua +17 -5
  146. package/src/custodian.ts +251 -77
  147. package/src/customize.ts +11 -3
  148. package/src/effect-checks.ts +50 -3
  149. package/src/effect.ts +343 -127
  150. package/src/effector-wire.ts +6 -3
  151. package/src/fail.ts +17 -4
  152. package/src/harness-client.ts +74 -31
  153. package/src/harness-env.ts +33 -0
  154. package/src/held-secrets.ts +125 -16
  155. package/src/holds.ts +3 -0
  156. package/src/http-effect.ts +88 -32
  157. package/src/http.ts +56 -17
  158. package/src/index.ts +15 -1
  159. package/src/internal.ts +1 -0
  160. package/src/kube-client.ts +6 -3
  161. package/src/machine-doc.ts +3 -4
  162. package/src/model-keys.ts +24 -0
  163. package/src/names.ts +19 -6
  164. package/src/notices.ts +443 -54
  165. package/src/open-body.ts +85 -19
  166. package/src/open.ts +36 -5
  167. package/src/parts.ts +40 -28
  168. package/src/party.ts +33 -0
  169. package/src/pick.ts +41 -7
  170. package/src/pool.ts +44 -23
  171. package/src/registration.ts +53 -11
  172. package/src/repo-identity.ts +7 -3
  173. package/src/repos.ts +20 -2
  174. package/src/run-host.ts +106 -20
  175. package/src/sandbox-effect.ts +217 -66
  176. package/src/sandbox-kube.ts +63 -16
  177. package/src/secret-placement.ts +9 -2
  178. package/src/setup.ts +86 -6
  179. package/src/tokens.ts +107 -32
  180. package/src/vocabulary.ts +23 -4
  181. package/src/wire.ts +10 -10
  182. package/src/workspace.ts +264 -253
  183. package/src/worktree.ts +249 -0
  184. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  185. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
@@ -111,8 +111,14 @@ can differ when your Assistant used other names. Other Workflows of the Instance
111
111
  …
112
112
  typecheck: tsc --noEmit (the instance's own compiler)
113
113
  effects: workflow "fix" — what it runs outside itself (ADR-0067)
114
+ cut: worktree — 600s, in the Harness
114
115
  tests: command ["node","--test","docs/site/tutorial/greet/test/*.test.js"] — 300s, in the Harness
115
116
  publish: push — 300s, in the Sandbox Effector
117
+ …
118
+ failures: workflow "fix" — the states that fail the run (ADR-0073)
119
+ fix.notCut: invokes fail
120
+ fix.lost: invokes fail
121
+ …
116
122
  seats: workflow "fix" — who may send what, per state (ADR-0015)
117
123
  fix.working agent:coder: [finish]
118
124
  fix.review gate: [approve, request_changes]
@@ -126,8 +132,9 @@ git.credentials "*": push Secret jr2-git-push-684888c0 — pushes over https
126
132
 
127
133
  Read these lines as a first check:
128
134
 
129
- - `effects` shows two Effects: a command and a push. A Workflow that pushes with no
135
+ - `effects` shows three Effects: the cut, a command and a push. A Workflow that pushes with no
130
136
  [Gate](../reference/glossary.md#gate) before the push shows the same lines. The graph shows where the push is.
137
+ - `failures` shows the two states that end the run as `failed`.
131
138
  - `seats` shows that the coder can send only `finish`. Only the Gate in `review` can send `approve` and
132
139
  `request_changes`.
133
140
  - The `warning` line is about git over ssh. The `jr2-git-ssh` Secret is for an ssh key, and it does not exist. Your fork
@@ -199,7 +206,7 @@ jr2 run fix --input '{"prompt":"In docs/site/tutorial/greet, one test fails. Fix
199
206
  ```text
200
207
  …
201
208
  → context kind-jr2 / namespace my-instance
202
- run b22cef63-1523-45f3-9c51-36c54b79ce36 (fix) — attached
209
+ run 790ad78a-db4e-4b09-a248-ab62da088cde (fix) — attached
203
210
  → active "placing"
204
211
  → active "provisioning"
205
212
  → active "attaching"
@@ -221,12 +228,14 @@ jr2 status <run id>
221
228
 
222
229
  [`jr2 status`](../reference/cli.md#status) prints the status of the run as one line of JSON. Its `gates` list holds the
223
230
  Gate of `review`. Its `gate` is `body.review`. Give that value to `--gate`. The `meta` of the Gate gives the coder's
224
- `summary`, the `branch` and the `worktree`:
231
+ `summary`, the `branch` and the `worktree`. After the JSON, `jr2 status` prints one line for each Worktree that the run
232
+ cut:
225
233
 
226
234
  ```text
227
235
  …
228
236
  → context kind-jr2 / namespace my-instance
229
- {…,"gates":[{"gate":"body.review",…,"meta":{"summary":"Fixed the \"trims the name\" test failure in docs/site/tutorial/greet/src/greet.js by adding .trim() to the name parsing. The name \" ada \" was not being trimmed, causing the greeting to include extra whitespace. Changed `args.find((a) => !a.startsWith(\"-\")) ?? \"world\"` to `(args.find((a) => !a.startsWith(\"-\")) ?? \"world\").trim()`.","branch":"jr2/fix-336ae477-2a46-49c2-b142-b81e705bb3f8","worktree":"/work/target/jr2-fix-336ae477-2a46-49c2-b142-b81e705bb3f8"}}]}
237
+ {…,"gates":[{"gate":"body.review",…,"meta":{"summary":"Fixed the \"trims the name\" test failure in docs/site/tutorial/greet. The bug was that the `greet` function didn't trim whitespace from the name argument. Added `.trim()` to the name extraction so that `greet([\" ada \"])` correctly returns `\"hello, ada\"` instead of `\"hello, ada \"`.","branch":"jr2/fix-a78c793e-607a-4b33-9784-ccf2cfb6f395","worktree":"/work/target/fix"}}]}
238
+ worktree: /work/target/fix (target), branch jr2/fix-a78c793e-607a-4b33-9784-ccf2cfb6f395
230
239
  ```
231
240
 
232
241
  The `summary` is in the words of the model, so your summary is different.
@@ -242,7 +251,7 @@ jr2 send <run id> --gate body.review --event approve
242
251
  ```text
243
252
  …
244
253
  → context kind-jr2 / namespace my-instance
245
- delivered approve to gate "body.review" on b22cef63-1523-45f3-9c51-36c54b79ce36
254
+ delivered approve to gate "body.review" on 790ad78a-db4e-4b09-a248-ab62da088cde
246
255
  ```
247
256
 
248
257
  The run goes to `pushing`, and the push Effect pushes the branch. Then the run ends, and jr2 removes the
@@ -253,7 +262,7 @@ status as one line of JSON:
253
262
  …
254
263
  → active "teardown"
255
264
  → done "done"
256
- {"runId":"b22cef63-1523-45f3-9c51-36c54b79ce36","workflow":"fix",…,"output":{"outcome":"pushed","branch":"jr2/fix-336ae477-2a46-49c2-b142-b81e705bb3f8","commit":"03f8800ec105c98f612bd2c55687bb92db80357a"}}
265
+ {"runId":"790ad78a-db4e-4b09-a248-ab62da088cde","workflow":"fix",…,"output":{"outcome":"pushed","branch":"jr2/fix-a78c793e-607a-4b33-9784-ccf2cfb6f395","commit":"ee7f5d8084db32c3610efb5c9b889a710ef30f70"},"worktrees":[{"slot":"target","path":"/work/target/fix","branch":"jr2/fix-a78c793e-607a-4b33-9784-ccf2cfb6f395"}]}
257
266
  ```
258
267
 
259
268
  In that JSON, `context.output` and `output` are the output of `fix`. In this result, it is
@@ -289,9 +298,11 @@ import {
289
298
  command,
290
299
  defineEvent,
291
300
  fail,
301
+ isReady,
292
302
  jr2Setup,
293
303
  push,
294
304
  workspace,
305
+ worktree,
295
306
  type CommandOutput,
296
307
  type HostInjectedInput,
297
308
  type PushOutput,
@@ -339,6 +350,15 @@ type Context = BodyInput & {
339
350
  // The output of a run that ends `done`. A lost Sandbox fails the run, with no output of this type.
340
351
  type Output = { outcome: "pushed"; branch: string; commit: string };
341
352
 
353
+ // The Worktree of the coder. The Machine names the folder, and each Effect that works there names it.
354
+ function fixTree(context: BodyInput) {
355
+ return `${context.workspace.repos.target}/fix` as const;
356
+ }
357
+ // One branch for each run, from its seed Instance ID.
358
+ function branchOf(context: BodyInput): string {
359
+ return `jr2/fix-${context.instanceId}`;
360
+ }
361
+
342
362
  // Two guards. A guard with a name shows that name on its arrow in the Console's graph.
343
363
  function testsPassed({ event }: { event: { output: CommandOutput } }): boolean {
344
364
  return event.output.outcome === "exited" && event.output.code === 0;
@@ -362,7 +382,9 @@ const body = jr2Setup({
362
382
  - Make the smallest change that fixes the bug.
363
383
  - End your turn with exactly one call to \`finish\`.`,
364
384
  }),
365
- // A program in the Harness container of the Sandbox, in the Worktree of `target`. Node expands the glob itself.
385
+ // Cut a Worktree of `target`: the Workspace only clones.
386
+ cut: worktree(),
387
+ // A program in the Harness container of the Sandbox, in the Worktree that its input names. Node expands the glob itself.
366
388
  tests: command(["node", "--test", "docs/site/tutorial/greet/test/*.test.js"], { timeout: "5m" }),
367
389
  // Push the tip of the coder's branch to the Repo of `target`, with the token of `git.credentials`.
368
390
  publish: push(),
@@ -372,16 +394,24 @@ const body = jr2Setup({
372
394
  }).createMachine({
373
395
  id: "fix",
374
396
  context: ({ input }) => ({ ...input, fresh: true }),
375
- initial: "working",
397
+ initial: "cutting",
376
398
  // The Sandbox is gone, and every commit that was not pushed went with it.
377
399
  on: { "workspace.lost": { target: ".lost" } },
378
400
  states: {
401
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
402
+ cutting: {
403
+ invoke: {
404
+ src: "cut",
405
+ input: ({ context }) => ({ path: fixTree(context), branch: branchOf(context) }),
406
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "working" }, { target: "notCut" }],
407
+ },
408
+ },
379
409
  working: {
380
410
  invoke: {
381
411
  src: "coder",
382
412
  input: ({ context }) => ({
383
413
  prompt: promptOf(context),
384
- cwd: context.workspace.repos.target,
414
+ cwd: fixTree(context),
385
415
  // Each Turn continues the coder's one conversation, so the coder remembers its work.
386
416
  continue: true,
387
417
  }),
@@ -406,6 +436,7 @@ const body = jr2Setup({
406
436
  testing: {
407
437
  invoke: {
408
438
  src: "tests",
439
+ input: ({ context }) => ({ worktree: fixTree(context) }),
409
440
  onDone: [
410
441
  { guard: testsPassed, target: "review" },
411
442
  {
@@ -423,8 +454,8 @@ const body = jr2Setup({
423
454
  meta: {
424
455
  ...(context.summary !== undefined ? { summary: context.summary } : {}),
425
456
  ...(context.reason !== undefined ? { reason: context.reason } : {}),
426
- branch: context.workspace.branch,
427
- worktree: context.workspace.repos.target,
457
+ branch: branchOf(context),
458
+ worktree: fixTree(context),
428
459
  },
429
460
  }),
430
461
  },
@@ -442,6 +473,7 @@ const body = jr2Setup({
442
473
  pushing: {
443
474
  invoke: {
444
475
  src: "publish",
476
+ input: ({ context }) => ({ worktree: fixTree(context) }),
445
477
  onDone: [
446
478
  {
447
479
  guard: pushed,
@@ -457,11 +489,12 @@ const body = jr2Setup({
457
489
  type: "final",
458
490
  output: ({ context }): Output => ({
459
491
  outcome: "pushed",
460
- branch: context.workspace.branch,
492
+ branch: branchOf(context),
461
493
  commit: context.commit ?? "",
462
494
  }),
463
495
  },
464
- // Not a final state: the run ends `failed`, and jr2 deletes the Sandbox.
496
+ // Not final states: the run ends `failed`, and jr2 deletes the Sandbox.
497
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
465
498
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost, with every commit that was not pushed" } } },
466
499
  },
467
500
  // The output of the run. xstate reads only this root `output` as the output of the Machine. It
@@ -476,14 +509,14 @@ function promptOf(context: Context): string {
476
509
  const reason = context.reason ? `\n\nThe last attempt ended: ${context.reason}` : "";
477
510
  return (
478
511
  `${context.prompt}${reason}${feedback ? `\n\n${feedback}` : ""}\n\n` +
479
- `Work in ${context.workspace.repos.target}, on the branch ${context.workspace.branch}. ` +
512
+ `Work in ${fixTree(context)}, on the branch ${branchOf(context)}. ` +
480
513
  "Commit your fix there, then call finish."
481
514
  );
482
515
  }
483
516
 
484
517
  // What the coder reads when the tests fail: the end of the output of `node --test`.
485
518
  function testFeedback(output: CommandOutput): string {
486
- const text = output.outcome === "unknown" ? output.reason : output.tail;
519
+ const text = "tail" in output ? output.tail : output.reason;
487
520
  return (
488
521
  `The tests do not pass (${output.outcome}). The end of their output:\n${text}\n\n` +
489
522
  "Fix the code, commit, then call finish."
@@ -503,8 +536,6 @@ function pushFailure(output: PushOutput): string {
503
536
  export const machine = workspace(body, {
504
537
  input: door,
505
538
  repos: { target: { url: "https://github.com/<you>/jr2.git", ref: "v<version>" } },
506
- // One branch for each run, from its seed Instance ID: `input.instanceId`.
507
- spec: ({ input }) => ({ branch: `jr2/fix-${input.instanceId}` }),
508
539
  });
509
540
  ```
510
541
 
@@ -518,9 +549,11 @@ import {
518
549
  command,
519
550
  defineEvent,
520
551
  fail,
552
+ isReady,
521
553
  jr2Setup,
522
554
  push,
523
555
  workspace,
556
+ worktree,
524
557
  type CommandOutput,
525
558
  type HostInjectedInput,
526
559
  type PushOutput,
@@ -568,6 +601,15 @@ type Context = BodyInput & {
568
601
  // The output of a run that ends `done`. A lost Sandbox fails the run, with no output of this type.
569
602
  type Output = { outcome: "pushed"; branch: string; commit: string };
570
603
 
604
+ // The Worktree of the coder. The Machine names the folder, and each Effect that works there names it.
605
+ function fixTree(context: BodyInput) {
606
+ return `${context.workspace.repos.target}/fix` as const;
607
+ }
608
+ // One branch for each run, from its seed Instance ID.
609
+ function branchOf(context: BodyInput): string {
610
+ return `jr2/fix-${context.instanceId}`;
611
+ }
612
+
571
613
  // Two guards. A guard with a name shows that name on its arrow in the Console's graph.
572
614
  function testsPassed({ event }: { event: { output: CommandOutput } }): boolean {
573
615
  return event.output.outcome === "exited" && event.output.code === 0;
@@ -591,7 +633,9 @@ const body = jr2Setup({
591
633
  - Make the smallest change that fixes the bug.
592
634
  - End your turn with exactly one call to \`finish\`.`,
593
635
  }),
594
- // A program in the Harness container of the Sandbox, in the Worktree of `target`. Node expands the glob itself.
636
+ // Cut a Worktree of `target`: the Workspace only clones.
637
+ cut: worktree(),
638
+ // A program in the Harness container of the Sandbox, in the Worktree that its input names. Node expands the glob itself.
595
639
  tests: command(["node", "--test", "docs/site/tutorial/greet/test/*.test.js"], { timeout: "5m" }),
596
640
  // Push the tip of the coder's branch to the Repo of `target`, with the token of `git.credentials`.
597
641
  publish: push(),
@@ -601,16 +645,24 @@ const body = jr2Setup({
601
645
  }).createMachine({
602
646
  id: "fix",
603
647
  context: ({ input }) => ({ ...input, fresh: true }),
604
- initial: "working",
648
+ initial: "cutting",
605
649
  // The Sandbox is gone, and every commit that was not pushed went with it.
606
650
  on: { "workspace.lost": { target: ".lost" } },
607
651
  states: {
652
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
653
+ cutting: {
654
+ invoke: {
655
+ src: "cut",
656
+ input: ({ context }) => ({ path: fixTree(context), branch: branchOf(context) }),
657
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "working" }, { target: "notCut" }],
658
+ },
659
+ },
608
660
  working: {
609
661
  invoke: {
610
662
  src: "coder",
611
663
  input: ({ context }) => ({
612
664
  prompt: promptOf(context),
613
- cwd: context.workspace.repos.target,
665
+ cwd: fixTree(context),
614
666
  // Each Turn continues the coder's one conversation, so the coder remembers its work.
615
667
  continue: true,
616
668
  }),
@@ -635,6 +687,7 @@ const body = jr2Setup({
635
687
  testing: {
636
688
  invoke: {
637
689
  src: "tests",
690
+ input: ({ context }) => ({ worktree: fixTree(context) }),
638
691
  onDone: [
639
692
  { guard: testsPassed, target: "review" },
640
693
  {
@@ -652,8 +705,8 @@ const body = jr2Setup({
652
705
  meta: {
653
706
  ...(context.summary !== undefined ? { summary: context.summary } : {}),
654
707
  ...(context.reason !== undefined ? { reason: context.reason } : {}),
655
- branch: context.workspace.branch,
656
- worktree: context.workspace.repos.target,
708
+ branch: branchOf(context),
709
+ worktree: fixTree(context),
657
710
  },
658
711
  }),
659
712
  },
@@ -671,6 +724,7 @@ const body = jr2Setup({
671
724
  pushing: {
672
725
  invoke: {
673
726
  src: "publish",
727
+ input: ({ context }) => ({ worktree: fixTree(context) }),
674
728
  onDone: [
675
729
  {
676
730
  guard: pushed,
@@ -686,11 +740,12 @@ const body = jr2Setup({
686
740
  type: "final",
687
741
  output: ({ context }): Output => ({
688
742
  outcome: "pushed",
689
- branch: context.workspace.branch,
743
+ branch: branchOf(context),
690
744
  commit: context.commit ?? "",
691
745
  }),
692
746
  },
693
- // Not a final state: the run ends `failed`, and jr2 deletes the Sandbox.
747
+ // Not final states: the run ends `failed`, and jr2 deletes the Sandbox.
748
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
694
749
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost, with every commit that was not pushed" } } },
695
750
  },
696
751
  // The output of the run. xstate reads only this root `output` as the output of the Machine. It
@@ -705,14 +760,14 @@ function promptOf(context: Context): string {
705
760
  const reason = context.reason ? `\n\nThe last attempt ended: ${context.reason}` : "";
706
761
  return (
707
762
  `${context.prompt}${reason}${feedback ? `\n\n${feedback}` : ""}\n\n` +
708
- `Work in ${context.workspace.repos.target}, on the branch ${context.workspace.branch}. ` +
763
+ `Work in ${fixTree(context)}, on the branch ${branchOf(context)}. ` +
709
764
  "Commit your fix there, then call finish."
710
765
  );
711
766
  }
712
767
 
713
768
  // What the coder reads when the tests fail: the end of the output of `node --test`.
714
769
  function testFeedback(output: CommandOutput): string {
715
- const text = output.outcome === "unknown" ? output.reason : output.tail;
770
+ const text = "tail" in output ? output.tail : output.reason;
716
771
  return (
717
772
  `The tests do not pass (${output.outcome}). The end of their output:\n${text}\n\n` +
718
773
  "Fix the code, commit, then call finish."
@@ -732,8 +787,6 @@ function pushFailure(output: PushOutput): string {
732
787
  export const machine = workspace(body, {
733
788
  input: door,
734
789
  repos: { target: { url: "https://github.com/<you>/jr2.git", ref: "v<version>" } },
735
- // One branch for each run, from its seed Instance ID: `input.instanceId`.
736
- spec: ({ input }) => ({ branch: `jr2/fix-${input.instanceId}` }),
737
790
  });
738
791
  ```
739
792
 
@@ -765,16 +818,19 @@ const requestChanges = defineEvent({
765
818
  });
766
819
  ```
767
820
 
768
- **The Effects.** `tests` and `publish` are actor slots, as the coder is. A state runs one when it invokes it.
769
- [`command()`](../reference/authoring-surface/command.md) takes a list of arguments, not a line of shell text. It runs in
770
- the [Worktree](../reference/glossary.md#worktree) of the one [Repo Slot](../reference/glossary.md#repo-slot). A command
771
- runs in the root of the Worktree, so `tests` names the test files by their path from that root.
772
- [`push()`](../reference/authoring-surface/push.md) pushes the tip of the coder's branch.
821
+ **The Effects.** `cut`, `tests` and `publish` are actor slots, as the coder is. A state runs one when it invokes it.
822
+ [`worktree()`](../reference/authoring-surface/worktree.md) cuts a [Worktree](../reference/glossary.md#worktree) of the
823
+ [Repo Slot](../reference/glossary.md#repo-slot). [`command()`](../reference/authoring-surface/command.md) takes a list
824
+ of arguments, not a line of shell text. Its input names the Worktree. A command runs in the root of the Worktree, so
825
+ `tests` names the test files by their path from that root. [`push()`](../reference/authoring-surface/push.md) pushes the
826
+ tip of the coder's branch. Its input names the Worktree too, and jr2 reads the branch from the cut.
773
827
  [`fail()`](../reference/authoring-surface/fail.md) is a Part, not an Effect. The `lost` state invokes it to end the run
774
828
  `failed`, because a final state means `done`.
775
829
 
776
830
  ```ts
777
- // A program in the Harness container of the Sandbox, in the Worktree of `target`. Node expands the glob itself.
831
+ // Cut a Worktree of `target`: the Workspace only clones.
832
+ cut: worktree(),
833
+ // A program in the Harness container of the Sandbox, in the Worktree that its input names. Node expands the glob itself.
778
834
  tests: command(["node", "--test", "docs/site/tutorial/greet/test/*.test.js"], { timeout: "5m" }),
779
835
  // Push the tip of the coder's branch to the Repo of `target`, with the token of `git.credentials`.
780
836
  publish: push(),
@@ -782,6 +838,21 @@ publish: push(),
782
838
  fail: fail(),
783
839
  ```
784
840
 
841
+ **The cut.** The Workspace only clones. `cutting` is the first state, and it makes the coder's Worktree. The path is
842
+ `/work/target/fix`, and the branch has the Instance ID of the run in it. See
843
+ [Cut a Worktree](../guides/cut-a-worktree.md).
844
+
845
+ ```ts
846
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
847
+ cutting: {
848
+ invoke: {
849
+ src: "cut",
850
+ input: ({ context }) => ({ path: fixTree(context), branch: branchOf(context) }),
851
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "working" }, { target: "notCut" }],
852
+ },
853
+ },
854
+ ```
855
+
785
856
  **The coder's state.** `working` invokes the coder. Each [Turn](../reference/glossary.md#turn) gets a prompt and the
786
857
  folder to work in. `continue: true` keeps one conversation, so the coder remembers its last Turn. `finish` is the
787
858
  coder's pick: the run goes to `testing`. [`agent.fault`](../reference/mechanism-events.md#agent-fault) is an event from
@@ -793,7 +864,7 @@ working: {
793
864
  src: "coder",
794
865
  input: ({ context }) => ({
795
866
  prompt: promptOf(context),
796
- cwd: context.workspace.repos.target,
867
+ cwd: fixTree(context),
797
868
  // Each Turn continues the coder's one conversation, so the coder remembers its work.
798
869
  continue: true,
799
870
  }),
@@ -825,6 +896,7 @@ catches every other outcome. See [`CommandOutput`](../reference/authoring-surfac
825
896
  testing: {
826
897
  invoke: {
827
898
  src: "tests",
899
+ input: ({ context }) => ({ worktree: fixTree(context) }),
828
900
  onDone: [
829
901
  { guard: testsPassed, target: "review" },
830
902
  {
@@ -849,8 +921,8 @@ review: {
849
921
  meta: {
850
922
  ...(context.summary !== undefined ? { summary: context.summary } : {}),
851
923
  ...(context.reason !== undefined ? { reason: context.reason } : {}),
852
- branch: context.workspace.branch,
853
- worktree: context.workspace.repos.target,
924
+ branch: branchOf(context),
925
+ worktree: fixTree(context),
854
926
  },
855
927
  }),
856
928
  },
@@ -875,6 +947,7 @@ commits in it. See [`PushOutput`](../reference/authoring-surface/type-pushoutput
875
947
  pushing: {
876
948
  invoke: {
877
949
  src: "publish",
950
+ input: ({ context }) => ({ worktree: fixTree(context) }),
878
951
  onDone: [
879
952
  {
880
953
  guard: pushed,
@@ -890,15 +963,13 @@ pushing: {
890
963
 
891
964
  **The Workspace.** [`workspace()`](../reference/authoring-surface/workspace.md) runs the body in a Sandbox. `input` is
892
965
  the schema of the run input that `jr2 run --input` sends. `repos` binds the Repo Slot `target` to your fork, at the
893
- release tag. `spec` gives the branch. The input of `spec` also holds `instanceId`, an id that jr2 makes for each run, so
966
+ release tag. A new branch starts from that tag. The body also gets `instanceId`, an id that jr2 makes for each run, so
894
967
  each run gets a branch of its own.
895
968
 
896
969
  ```ts
897
970
  export const machine = workspace(body, {
898
971
  input: door,
899
972
  repos: { target: { url: "https://github.com/<you>/jr2.git", ref: "v<version>" } },
900
- // One branch for each run, from its seed Instance ID: `input.instanceId`.
901
- spec: ({ input }) => ({ branch: `jr2/fix-${input.instanceId}` }),
902
973
  });
903
974
  ```
904
975
 
@@ -20,6 +20,10 @@ This is the first page of the tutorial. You need a computer with these tools:
20
20
  kind makes the cluster from Docker containers. `jr2 up` builds an image with Docker and loads it into the kind cluster.
21
21
  kubectl is the tool that `jr2 up`, `jr2 run` and the other commands use to speak to the cluster.
22
22
 
23
+ The kind cluster uses the Linux kernel of Docker: the kernel of your computer, or on macOS and Windows, the kernel of
24
+ the virtual machine of Docker. jr2 needs Landlock in that kernel: Linux 6.2 or later, with `landlock` in its list of
25
+ security modules. `jr2 up` refuses to converge without Landlock.
26
+
23
27
  ## Check Node.js
24
28
 
25
29
  Run:
@@ -137,26 +137,39 @@ RUN pip install --no-cache-dir pytest ruff
137
137
 
138
138
  ```ts
139
139
  import { z } from "zod";
140
- import { command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
140
+ import { command, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
141
141
 
142
142
  const runInput = z.object({ branch: z.string() });
143
143
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
144
144
 
145
+ // The Worktree that the tests run in.
146
+ const testTree = (context: BodyInput) => `${context.workspace.repos.app}/test` as const;
147
+
145
148
  const body = jr2Setup({
146
- types: {} as { input: BodyInput },
149
+ types: {} as { input: BodyInput; context: BodyInput },
147
150
  actors: {
151
+ cut: worktree(),
148
152
  // python and pytest come from the Sandbox Image in ./python.
149
153
  tests: command(["python", "-m", "pytest"], { timeout: "15m" }),
150
154
  fail: fail(),
151
155
  },
152
156
  }).createMachine({
153
157
  id: "pytest",
158
+ context: ({ input }) => input,
154
159
  on: { "workspace.lost": ".lost" },
155
- initial: "testing",
160
+ initial: "cutting",
156
161
  states: {
162
+ cutting: {
163
+ invoke: {
164
+ src: "cut",
165
+ input: ({ context }) => ({ path: testTree(context), branch: context.branch }),
166
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
167
+ },
168
+ },
157
169
  testing: {
158
170
  invoke: {
159
171
  src: "tests",
172
+ input: ({ context }) => ({ worktree: testTree(context) }),
160
173
  onDone: [
161
174
  {
162
175
  guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
@@ -169,6 +182,7 @@ const body = jr2Setup({
169
182
  passed: { type: "final" },
170
183
  // Not final states: each one fails the run, and the run ends `failed`.
171
184
  failed: { invoke: { src: "fail", input: { reason: "pytest did not pass" } } },
185
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
172
186
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost" } } },
173
187
  },
174
188
  });
@@ -178,7 +192,6 @@ export const machine = workspace(body, {
178
192
  // The docker context in the folder beside this file. jr2 up builds it.
179
193
  image: import.meta.resolve("./python"),
180
194
  repos: { app: "https://github.com/you/app.git" },
181
- spec: ({ input }) => ({ branch: input.branch }),
182
195
  });
183
196
  ```
184
197
 
@@ -121,9 +121,10 @@ jr2 run fix-app --input '{"prompt":"Make the failing test in test/greet.test.ts
121
121
  The key of each Repo Slot is the name of the slot, and the directory `/work/<slot>` in the
122
122
  [Sandbox](../reference/glossary.md#sandbox). The value is one of these:
123
123
 
124
- - a url: `"https://github.com/<you>/handbook.git"`. The [Worktree](../reference/glossary.md#worktree) starts from the
125
- default branch of the Repo.
126
- - a [Binding](../reference/glossary.md#binding): `{ url, ref }`. The Worktree starts from `ref`, a branch or a commit.
124
+ - a url: `"https://github.com/<you>/handbook.git"`. The clone stays on the default branch of the Repo, and a new
125
+ [Worktree](../reference/glossary.md#worktree) branch starts from it.
126
+ - a [Binding](../reference/glossary.md#binding): `{ url, ref }`. The clone stays on `ref`, a branch, a tag or a commit,
127
+ and a new branch starts from it.
127
128
  - a function of the run input: `({ input }) => url`, or `({ input }) => ({ url, ref })`. Each run chooses its Repo. The
128
129
  url must agree with an entry of [`git.credentials`](../reference/config.md#git-credentials) in `jr2.config.ts`.
129
130
  - `open`, to leave the slot Open for the Instance that registers your Machine.
@@ -191,7 +192,7 @@ gives the values, and [Run a command or call a URL with an Effect](run-a-command
191
192
  - `customize()` refuses an Open map that you bind with no slot:
192
193
  ``customize(): machine "<id>" declares its Repo Slots open as a map — name at least one: `repos: { app: "https://…" }` (ADR-0051).``
193
194
  - `customize()` refuses a Repo Slot key that is not a directory name. The key becomes `/work/<slot>`:
194
- ``customize(): Repo Slot key "<slot>" is not a directory name — a slot becomes `/work/<slot>` and its order matters, so it starts with a letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).``
195
+ ``customize(): Repo Slot key "<slot>" is not a directory name — a slot becomes `/work/<slot>`, `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).``
195
196
  - `customize()` refuses an empty url:
196
197
  ``customize(): Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot is `open`, a url, `{ url, ref? }`, or a mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
197
198
  For an empty `ref`, the text says `bound with an empty ref`.