@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
@@ -8,10 +8,10 @@ description: How a Workflow gets a Sandbox with a git Worktree or a shared folde
8
8
  A [Workspace](../reference/glossary.md#workspace) is a [Machine](../reference/glossary.md#machine) that runs a body
9
9
  Machine in a [Sandbox](../reference/glossary.md#sandbox). The Sandbox is a pod of its own, with a folder, `/work`, that
10
10
  the Agents and Effects of the body share. When the Workspace names [Repos](../reference/glossary.md#repo), `/work` holds
11
- a git [Worktree](../reference/glossary.md#worktree) of each Repo on one branch. A Workspace that names no Repo has only
12
- the folder (see [A Workspace with no Repo](#a-workspace-with-no-repo)). You make a Workspace with
13
- [`workspace()`](../reference/authoring-surface/workspace.md). The Workspace makes the Sandbox when the run enters it,
14
- and deletes the Sandbox when the body reaches a final state.
11
+ a clone of each Repo. The body cuts each git [Worktree](../reference/glossary.md#worktree) that it needs. A Workspace
12
+ that names no Repo has only the folder (see [A Workspace with no Repo](#a-workspace-with-no-repo)). You make a Workspace
13
+ with [`workspace()`](../reference/authoring-surface/workspace.md). The Workspace makes the Sandbox when the run enters
14
+ it, and deletes the Sandbox when the body reaches a final state.
15
15
 
16
16
  ## Why Workspaces exist
17
17
 
@@ -27,14 +27,24 @@ Agent.
27
27
  ## A Workspace and its body
28
28
 
29
29
  This example is a [Workflow](../reference/glossary.md#workflow). A coder changes the `app` Repo, and reads the
30
- `handbook` Repo beside it. Then a person approves the change at a [Gate](../reference/glossary.md#gate), and the Machine
31
- pushes the branch:
30
+ `handbook` Repo beside it. The Machine first cuts the coder's Worktree. Then a person approves the change at a
31
+ [Gate](../reference/glossary.md#gate), and the Machine pushes the branch:
32
32
 
33
33
  **Anthropic**
34
34
 
35
35
  ```ts
36
36
  import { z } from "zod";
37
- import { agent, defineEvent, fail, jr2Setup, push, workspace, type Workspaced } from "@jr2/orchestrator";
37
+ import {
38
+ agent,
39
+ defineEvent,
40
+ fail,
41
+ isReady,
42
+ jr2Setup,
43
+ push,
44
+ workspace,
45
+ worktree,
46
+ type Workspaced,
47
+ } from "@jr2/orchestrator";
38
48
 
39
49
  const finished = defineEvent({
40
50
  name: "finished",
@@ -51,6 +61,10 @@ const approve = defineEvent({
51
61
  const runInput = z.object({ ticket: z.string(), prompt: z.string() });
52
62
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app" | "handbook">;
53
63
 
64
+ // The folder of the Worktree, and its branch. The Machine knows both before the cut.
65
+ const fixTree = (context: BodyInput) => `${context.workspace.repos.app}/fix` as const;
66
+ const branchOf = (context: BodyInput) => `docs/${context.ticket}`;
67
+
54
68
  const body = jr2Setup({
55
69
  types: {} as { context: BodyInput; input: BodyInput },
56
70
  events: [finished, approve],
@@ -59,6 +73,7 @@ const body = jr2Setup({
59
73
  model: "anthropic/claude-haiku-5-5",
60
74
  instructions: "You are a careful engineer. You commit your work on the branch.",
61
75
  }),
76
+ cut: worktree(),
62
77
  // Two Repo Slots, so the push names the slot that it pushes.
63
78
  publish: push({ slot: "app" }),
64
79
  fail: fail(),
@@ -66,17 +81,25 @@ const body = jr2Setup({
66
81
  }).createMachine({
67
82
  id: "docs-fix",
68
83
  context: ({ input }) => input,
69
- initial: "working",
84
+ initial: "cutting",
70
85
  // The Sandbox is gone, and its unpushed commits with it. This body fails the run.
71
86
  on: { "agent.fault": ".failed", "workspace.lost": ".lost" },
72
87
  states: {
88
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
89
+ cutting: {
90
+ invoke: {
91
+ src: "cut",
92
+ input: ({ context }) => ({ path: fixTree(context), branch: branchOf(context) }),
93
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "working" }, { target: "notCut" }],
94
+ },
95
+ },
73
96
  working: {
74
97
  invoke: {
75
98
  src: "coder",
76
99
  input: ({ context }) => ({
77
- prompt: `${context.prompt}\nRead the style rules in ${context.workspace.repos.handbook}.`,
78
- // The Agent works in the Worktree of the app slot.
79
- cwd: context.workspace.repos.app,
100
+ // A slot that is only read needs no cut: its `default` folder is on the Binding's base.
101
+ prompt: `${context.prompt}\nRead the style rules in ${context.workspace.repos.handbook}/default.`,
102
+ cwd: fixTree(context),
80
103
  }),
81
104
  },
82
105
  on: { finished: "review" },
@@ -86,7 +109,7 @@ const body = jr2Setup({
86
109
  invoke: {
87
110
  src: "gate",
88
111
  input: ({ context }) => ({
89
- meta: { branch: context.workspace.branch, worktree: context.workspace.repos.app },
112
+ meta: { branch: branchOf(context), worktree: fixTree(context) },
90
113
  }),
91
114
  },
92
115
  on: { approve: "publishing" },
@@ -96,11 +119,14 @@ const body = jr2Setup({
96
119
  publishing: {
97
120
  invoke: {
98
121
  src: "publish",
122
+ // The Worktree names what to push: jr2 reads its branch from the cut.
123
+ input: ({ context }) => ({ worktree: fixTree(context) }),
99
124
  onDone: [{ guard: ({ event }) => event.output.outcome === "pushed", target: "done" }, { target: "review" }],
100
125
  },
101
126
  },
102
127
  done: { type: "final" },
103
128
  // Not final states: a final state means `done`. Each one fails the run, and the run ends `failed`.
129
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
104
130
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost" } } },
105
131
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
106
132
  },
@@ -115,7 +141,6 @@ export const machine = workspace(body, {
115
141
  app: { url: "https://github.com/you/app.git", ref: "main" },
116
142
  handbook: "https://github.com/you/handbook.git",
117
143
  },
118
- spec: ({ input }) => ({ branch: `docs/${input.ticket}` }),
119
144
  });
120
145
  ```
121
146
 
@@ -123,7 +148,17 @@ export const machine = workspace(body, {
123
148
 
124
149
  ```ts
125
150
  import { z } from "zod";
126
- import { agent, defineEvent, fail, jr2Setup, push, workspace, type Workspaced } from "@jr2/orchestrator";
151
+ import {
152
+ agent,
153
+ defineEvent,
154
+ fail,
155
+ isReady,
156
+ jr2Setup,
157
+ push,
158
+ workspace,
159
+ worktree,
160
+ type Workspaced,
161
+ } from "@jr2/orchestrator";
127
162
 
128
163
  const finished = defineEvent({
129
164
  name: "finished",
@@ -140,6 +175,10 @@ const approve = defineEvent({
140
175
  const runInput = z.object({ ticket: z.string(), prompt: z.string() });
141
176
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app" | "handbook">;
142
177
 
178
+ // The folder of the Worktree, and its branch. The Machine knows both before the cut.
179
+ const fixTree = (context: BodyInput) => `${context.workspace.repos.app}/fix` as const;
180
+ const branchOf = (context: BodyInput) => `docs/${context.ticket}`;
181
+
143
182
  const body = jr2Setup({
144
183
  types: {} as { context: BodyInput; input: BodyInput },
145
184
  events: [finished, approve],
@@ -148,6 +187,7 @@ const body = jr2Setup({
148
187
  model: "local/qwen3.6-35b-a3b",
149
188
  instructions: "You are a careful engineer. You commit your work on the branch.",
150
189
  }),
190
+ cut: worktree(),
151
191
  // Two Repo Slots, so the push names the slot that it pushes.
152
192
  publish: push({ slot: "app" }),
153
193
  fail: fail(),
@@ -155,17 +195,25 @@ const body = jr2Setup({
155
195
  }).createMachine({
156
196
  id: "docs-fix",
157
197
  context: ({ input }) => input,
158
- initial: "working",
198
+ initial: "cutting",
159
199
  // The Sandbox is gone, and its unpushed commits with it. This body fails the run.
160
200
  on: { "agent.fault": ".failed", "workspace.lost": ".lost" },
161
201
  states: {
202
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
203
+ cutting: {
204
+ invoke: {
205
+ src: "cut",
206
+ input: ({ context }) => ({ path: fixTree(context), branch: branchOf(context) }),
207
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "working" }, { target: "notCut" }],
208
+ },
209
+ },
162
210
  working: {
163
211
  invoke: {
164
212
  src: "coder",
165
213
  input: ({ context }) => ({
166
- prompt: `${context.prompt}\nRead the style rules in ${context.workspace.repos.handbook}.`,
167
- // The Agent works in the Worktree of the app slot.
168
- cwd: context.workspace.repos.app,
214
+ // A slot that is only read needs no cut: its `default` folder is on the Binding's base.
215
+ prompt: `${context.prompt}\nRead the style rules in ${context.workspace.repos.handbook}/default.`,
216
+ cwd: fixTree(context),
169
217
  }),
170
218
  },
171
219
  on: { finished: "review" },
@@ -175,7 +223,7 @@ const body = jr2Setup({
175
223
  invoke: {
176
224
  src: "gate",
177
225
  input: ({ context }) => ({
178
- meta: { branch: context.workspace.branch, worktree: context.workspace.repos.app },
226
+ meta: { branch: branchOf(context), worktree: fixTree(context) },
179
227
  }),
180
228
  },
181
229
  on: { approve: "publishing" },
@@ -185,11 +233,14 @@ const body = jr2Setup({
185
233
  publishing: {
186
234
  invoke: {
187
235
  src: "publish",
236
+ // The Worktree names what to push: jr2 reads its branch from the cut.
237
+ input: ({ context }) => ({ worktree: fixTree(context) }),
188
238
  onDone: [{ guard: ({ event }) => event.output.outcome === "pushed", target: "done" }, { target: "review" }],
189
239
  },
190
240
  },
191
241
  done: { type: "final" },
192
242
  // Not final states: a final state means `done`. Each one fails the run, and the run ends `failed`.
243
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
193
244
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost" } } },
194
245
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
195
246
  },
@@ -204,28 +255,26 @@ export const machine = workspace(body, {
204
255
  app: { url: "https://github.com/you/app.git", ref: "main" },
205
256
  handbook: "https://github.com/you/handbook.git",
206
257
  },
207
- spec: ({ input }) => ({ branch: `docs/${input.ticket}` }),
208
258
  });
209
259
  ```
210
260
 
211
261
  [Agents and Turns](agents-and-turns.md) tells how to define an Agent.
212
262
 
213
263
  The options of `workspace()` say what the Sandbox is. [`jr2 up`](../reference/cli.md#up) reads them before a run starts.
214
- Only `spec` and a per-run [Repo Slot](../reference/glossary.md#repo-slot) read the run input:
264
+ Only a per-run [Repo Slot](../reference/glossary.md#repo-slot) reads the run input:
215
265
 
216
266
  - `input` is the schema of the run input: what [`jr2 run`](../reference/cli.md#run) gives with `--input`. Put it on the
217
267
  `workspace()`, not on the body.
218
268
  - `repos` gives the Repo Slots that the Sandbox clones. Leave it out for a Workspace with no Repo.
219
- - `spec` is a function from the run input to the branch. Its input also holds `instanceId` (see
220
- [`HostInjectedInput`](../reference/authoring-surface/type-hostinjectedinput.md)).
221
269
  - `resources` is the [Size](../reference/glossary.md#size). `image`, `user` and `effector` name the images of the pod's
222
270
  containers.
223
271
  - `secrets` names the [Held secrets](../reference/glossary.md#held-secret) that the Sandbox can use (see
224
272
  [Secrets and trust](secrets-and-trust.md)).
273
+ - `workGroup` is the group id that owns the files in `/work`. The default is 2000.
225
274
 
226
275
  The body gets the run input, plus `workspace`: the handles of the Workspace (see
227
- [`WorkspaceHandles`](../reference/authoring-surface/type-workspacehandles.md)). `workspace.repos` gives the absolute
228
- path of each Worktree, and `workspace.branch` gives the branch. Give a path to an Agent's
276
+ [`WorkspaceHandles`](../reference/authoring-surface/type-workspacehandles.md)). `workspace.repos` gives the folder of
277
+ each Repo Slot, `/work/<slot>`. This folder is not a Worktree. Give the path of a Worktree to an Agent's
229
278
  [Turn](../reference/glossary.md#turn) as its `cwd`, as the example does. Type the body's input with
230
279
  [`Workspaced`](../reference/authoring-surface/type-workspaced.md), also when the run input is `z.object({})`.
231
280
 
@@ -243,7 +292,8 @@ A Repo Slot is a key of the `repos` option. The key is your Machine's name for t
243
292
  Repo under `/work`. Each slot is one of three kinds:
244
293
 
245
294
  - **Bound**: the Machine gives the url, or a [Binding](../reference/glossary.md#binding) `{ url, ref }`. `ref` is the
246
- branch or the commit that the Worktree starts from. Without `ref`, the Worktree starts from the Repo's default branch.
295
+ branch, the tag or the commit that the clone stays on, and a new branch starts from it. Without `ref`, it is the
296
+ Repo's default branch.
247
297
  - **[Open](../reference/glossary.md#open)**: the Machine gives [`open`](../reference/authoring-surface/open.md). The
248
298
  [Instance](../reference/glossary.md#instance) that registers the Machine binds the slot with
249
299
  [`customize()`](../reference/authoring-surface/customize.md) (see [Composition](composition.md)).
@@ -251,7 +301,7 @@ Repo under `/work`. Each slot is one of three kinds:
251
301
 
252
302
  A Machine that names no slot writes `repos: open` for the whole map. Then the composer names each slot.
253
303
  `workspace.repos` keeps the slots in the order that `repos` declares them. jr2 gives no slot a special meaning. Your
254
- Machine says which Worktree each Agent works in.
304
+ Machine says where each Agent works.
255
305
 
256
306
  The cluster keeps one read-only cache of each Repo on each node. A Sandbox clones from that cache, and a fetch in the
257
307
  pod goes through it. [Architecture](architecture.md) tells about the Repo cache. Run
@@ -259,23 +309,44 @@ pod goes through it. [Architecture](architecture.md) tells about the Repo cache.
259
309
 
260
310
  ## The folders in the Sandbox
261
311
 
262
- Each Repo Slot gets a folder under `/work`. The clone of the Repo is `/work/<slot>/default/`. The Worktree of the branch
263
- is beside it, at `/work/<slot>/<branch>`. In the folder name, each `/` of the branch becomes `-`. For example, the
264
- branch `docs/T-12` of the slot `app` is at `/work/app/docs-T-12`. This is the
265
- [Project layout](../reference/glossary.md#project-layout).
312
+ Each Repo Slot gets a folder under `/work`. The clone of the Repo is `/work/<slot>/default/`. It stays on the base of
313
+ the Binding: a branch is attached, and a tag or a commit is detached. No work happens in `default/`. A slot that an
314
+ Agent only reads needs nothing more: the Agent reads `/work/<slot>/default`. This is the
315
+ [Project layout](../reference/glossary.md#project-layout). `/work/.jr2/` is reserved for jr2.
316
+
317
+ The Workspace only clones. Your Machine cuts each Worktree with the
318
+ [`worktree()`](../reference/authoring-surface/worktree.md) Effect, in a state of the body. You give the path, and the
319
+ branch or the commit:
320
+
321
+ - `{ path: "/work/app/fix", branch: "docs/123" }` cuts a branch Worktree. The cut finds a local branch, then
322
+ `origin/<branch>`, tracked, after a fetch. With neither, it makes a new branch from `from` or the base of the Binding.
323
+ - `{ path: "/work/app/review", detach: sha }` cuts a detached Worktree for a reviewer. Each cut forces it to the commit
324
+ and cleans it. Ignored files stay, so that the reviewer can run the tests.
325
+
326
+ The path is exactly `/work/<slot>/<name>`. The name is one segment that is not `default` and starts with neither `.` nor
327
+ `-`. So your Machine knows the path before the cut, and the folder name does not change with the branch name.
328
+
329
+ The output of a cut is `ready`, `refused` or `unknown`. A later outcome can come, so route the last arm of `onDone` with
330
+ no guard. A bad path or a bad branch name is `refused`, and nothing changes. A cut is idempotent: the same cut again
331
+ reads the same Worktree, and its `head` is the tip of the branch.
332
+
333
+ `worktree({ expect: "new" })` refuses a branch that exists. `worktree({ expect: "existing" })` refuses a branch that
334
+ does not exist, and makes nothing.
335
+
336
+ The author chooses how many Agents work in a Workspace. Agents in one Worktree share git, as people in one checkout do.
337
+ Each coder that needs its own branch gets its own Worktree of the one clone.
266
338
 
267
- `spec` returns a [`WorkspaceSpec`](../reference/authoring-surface/type-workspacespec.md). It gives the branch, and two
268
- optional fields:
339
+ Your Machine chooses the branch name. Give each run a different branch, so that two runs do not push to one branch. Make
340
+ it from a field of the run input, or from `instanceId` when the run input has no field that is different for each run.
341
+ Only a root `workspace()` gets `instanceId` from jr2. A `workspace()` that a different Machine invokes gets it only when
342
+ its parent gives it.
269
343
 
270
- - `reviewSha`: a commit for a reviewer. jr2 checks it out in a second Worktree, at `/work/<slot>/<branch>-review`. The
271
- body gets its paths as `workspace.review`.
272
- - `workGroup`: the group id that owns the files in `/work`. The default is 2000.
344
+ An Agent, a `command`, a `push`, and an `http` Effect with a file body each name their Worktree by its path: `cwd` for a
345
+ Turn, `worktree` for an Effect. A `push` has no branch in its input. jr2 reads the branch from the cut of that path. So
346
+ an Agent that moves `HEAD` does not change what is pushed.
273
347
 
274
- Your Machine chooses the branch name. jr2 makes the branch in each Repo Slot. Give each run a different branch, so that
275
- two runs do not push to one branch. Make it from a field of the run input, or from `instanceId` when the run input has
276
- no field that is different for each run: ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``. Only a root
277
- `workspace()` gets `instanceId` from jr2. A `workspace()` that a different Machine invokes gets it only when its parent
278
- gives it.
348
+ jr2 has no Effect that removes a Worktree. To remove one, run `git worktree remove` in a `command`.
349
+ [Cut a Worktree](../guides/cut-a-worktree.md) gives the procedure.
279
350
 
280
351
  ## A Workspace with no Repo
281
352
 
@@ -504,13 +575,13 @@ export const machine = workspace(body, { input: runInput });
504
575
 
505
576
  In a Workspace with no Repo:
506
577
 
507
- - Type the body's input as `Workspaced<RunInput, never>`. The handles are `{ repos: {} }`, with no branch.
578
+ - Type the body's input as `Workspaced<RunInput, never>`. The handles are `{ repos: {} }`.
508
579
  - An Agent's Turn that gives no `cwd` works in `/work`. The Agent's `workspace` access is the same as in each Workspace:
509
580
  `"write"` gives it `write`, `edit` and `bash`, and `"read"` gives it `bash` only.
510
581
  - A `command` with no Effect secret runs in `/work`.
511
582
  - An `http` Effect sends a file of `/work` with `file(path)`. The path is relative to `/work`.
512
- - `spec` is optional, and gives only `workGroup`.
513
- - A `push` needs a branch, and a `command` with an Effect secret runs on a commit. Thus `jr2 up` refuses both.
583
+ - A `worktree()` needs a Repo Slot, a `push` needs a branch, and a `command` with an Effect secret runs on a commit.
584
+ Thus `jr2 up` refuses all three.
514
585
 
515
586
  jr2 deletes `/work` with the Sandbox. Send what must stay before the body ends, as the example does with `upload`. A
516
587
  Lost Sandbox has lost all of its work, because no commit was pushed.
@@ -593,8 +664,7 @@ A Workspace goes through these states:
593
664
 
594
665
  1. `placing`: jr2 writes the Sandbox, and waits for a node.
595
666
  2. `provisioning`: the pod starts.
596
- 3. `attaching`: the Harness clones each Repo Slot and makes its Worktree. With no Repo Slot, this state has nothing to
597
- do.
667
+ 3. `attaching`: the Harness clones each Repo Slot. With no Repo Slot, this state has nothing to do.
598
668
  4. `running`: the body runs.
599
669
  5. `teardown`: the body is in a final state, and jr2 deletes the Sandbox.
600
670
  6. `failing`: the body invoked [`fail`](../reference/authoring-surface/fail.md), and jr2 deletes the Sandbox. Then the
@@ -643,29 +713,37 @@ the Harness container does not lose the Workspace: the pod and `/work` stay.
643
713
  - A per-run url must match an entry of [`git.credentials`](../reference/config.md#git-credentials). jr2 refuses the url
644
714
  before it makes the Sandbox when no entry matches it. Thus a run cannot send the cluster's credentials to a host that
645
715
  it chooses.
646
- - `spec` returns a `branch`. When a run enters the Workspace, it faults if `spec` gives no `branch`, or the branch
647
- `default`, because `default` is the folder of the clone. With no Repo Slot, it faults if `spec` gives a `branch` or a
648
- `reviewSha`.
649
- - With no Repo Slot, `jr2 up` refuses a `push`, and a `command` that names an Effect secret.
716
+ - `jr2 up` refuses a Repo Slot name that starts with `.`, because `/work/.jr2/` is reserved for jr2.
717
+ - A cut of the branch that `default/` holds is `refused`, and the reason names `/work/<slot>/default`.
718
+ - With no Repo Slot, `jr2 up` refuses a `worktree()`, a `push`, and a `command` that names an Effect secret.
650
719
  - A Size has only `limits.memory` and `limits.cpu`. jr2 refuses `requests` and each other key.
651
720
  - `jr2 up` refuses a Size that leaves the Harness container less than 256Mi of memory, or no cpu.
652
- - Under a Workspace with more than one Repo Slot, a Turn states its `cwd`, and a `command` and a `push` name their slot.
721
+ - Under a Workspace with Repo Slots, a Turn states its `cwd`, and a `command`, a `push` and an `http` Effect with a file
722
+ body state their `worktree`. A Turn that wants `/work` states `cwd: "/work"`. An Agent with `workspace: "none"` needs
723
+ no `cwd`.
724
+ - A `push`, a `command` that names an Effect secret, and an `http` Effect with a file body need a Worktree that a cut of
725
+ this run made.
726
+ - Under a Workspace with more than one Repo Slot, a `push` names its `slot`.
653
727
 
654
728
  The guides and the Reference give the refusal text of each rule.
655
729
 
656
730
  ## Read next
657
731
 
732
+ - [Cut a Worktree](../guides/cut-a-worktree.md)
658
733
  - [Look inside a Sandbox](../guides/look-inside-a-sandbox.md)
659
734
  - [Add tools to the Sandbox](../guides/add-tools-to-the-sandbox.md)
660
735
  - [Size a Workspace](../guides/size-a-workspace.md)
661
736
  - [Give the cluster git credentials](../guides/give-the-cluster-git-credentials.md)
662
737
  - [Effects](effects.md), [Composition](composition.md), [Architecture](architecture.md)
663
738
  - Reference: [`workspace`](../reference/authoring-surface/workspace.md),
739
+ [`worktree`](../reference/authoring-surface/worktree.md),
664
740
  [`RepoSlot`](../reference/authoring-surface/type-reposlot.md), [`Size`](../reference/authoring-surface/type-size.md),
665
741
  [`UserContainer`](../reference/authoring-surface/type-usercontainer.md),
666
742
  [`workspace.lost`](../reference/mechanism-events.md#workspace-lost), [`sandbox`](../reference/config.md#sandbox)
667
743
  - [ADR-0012](../reference/decisions/0012-workspace-wrapper-machine.md): `workspace()` wraps a body Machine and owns only
668
744
  Sandbox lifecycle
745
+ - [ADR-0075](../reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md): a Machine cuts a
746
+ Worktree, and the Workspace only clones
669
747
  - [ADR-0051](../reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md): a Repo is a slot
670
748
  on the `workspace()` and a cache on the node
671
749
  - [ADR-0060](../reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md): a Workspace states
@@ -161,7 +161,7 @@ jr2 run task --input '{"prompt":"In docs/site/tutorial/greet, one test fails. Fi
161
161
  ```text
162
162
  …
163
163
  → context kind-jr2 / namespace my-instance
164
- run a7a8fc26-851f-4459-9efb-845389acfda9 (task) — attached
164
+ run 290b4be9-1e24-46a2-8cb5-27fabb3bbd3c (task) — attached
165
165
  → active "placing"
166
166
  → active "provisioning"
167
167
  → active "attaching"
@@ -183,10 +183,12 @@ jr2 status <run id>
183
183
  ```
184
184
 
185
185
  The status is one line of JSON. While the coder works, `gates` is `[]`. When the run waits at `review`, `gates` holds
186
- the Gate. Its `gate` is `body.review`, and the line ends with its `meta`:
186
+ the Gate. Its `gate` is `body.review`, and the line ends with its `meta`. After the JSON, `jr2 status` prints one line
187
+ for each Worktree that the run cut:
187
188
 
188
189
  ```text
189
- …"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"}}]}
190
+ …"gate":"body.review",…,"meta":{"summary":"Fixed a bug in docs/site/tutorial/greet/src/greet.js where the name argument wasn't being trimmed. The test \"trims the name\" failed because input like \" ada \" produced \"hello, ada \" instead of \"hello, ada\". Added `.trim()` to the name extraction so whitespace is stripped before constructing the greeting. All 6 tests now pass.","branch":"jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd","worktree":"/work/target/task"}}]}
191
+ worktree: /work/target/task (target), branch jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd
190
192
  ```
191
193
 
192
194
  Run the command again until `gates` holds the Gate.
@@ -196,7 +198,7 @@ Run the command again until `gates` holds the Gate.
196
198
  - `worktree` is the folder in the Sandbox that holds the branch. It is
197
199
  [the Worktree](../reference/glossary.md#worktree).
198
200
 
199
- Copy `branch` and `worktree` from your output. The part after `jr2/task-`, here `d8f59a0c-9ea1-43fc-8d0c-b153caf70f16`,
201
+ Copy `branch` and `worktree` from your output. The part after `jr2/task-`, here `1f34eef3-5749-4d7c-a16b-488c56ec88cd`,
200
202
  is the [Instance ID](../reference/glossary.md#instance-id) of the run. `jr2 status` prints it as `instanceId`. It is not
201
203
  the run id. Do not make the branch name from the run id. With the Instance token, the drawer of the
202
204
  [Console](../reference/glossary.md#console) shows the same `meta`.
@@ -215,10 +217,10 @@ kubectl -n my-instance get sandboxes -l jr2.dev/run=<run id>
215
217
 
216
218
  ```text
217
219
  NAME PHASE ENDPOINT AGE
218
- ws-a7a8fc26-x-38-oakg23 Ready http://ws-a7a8fc26-x-38-oakg23.my-instance.svc:8080 5m10s
220
+ ws-290b4be9-x-0-19gh8ze Ready http://ws-290b4be9-x-0-19gh8ze.my-instance.svc:8080 5m25s
219
221
  ```
220
222
 
221
- `ws-a7a8fc26-x-38-oakg23` is the name of the Sandbox. Your Sandbox has a different name. It starts with `ws-` and the
223
+ `ws-290b4be9-x-0-19gh8ze` is the name of the Sandbox. Your Sandbox has a different name. It starts with `ws-` and the
222
224
  first 8 characters of the run id. The pod of the Sandbox has the same name. The commands below write it as `<sandbox>`.
223
225
 
224
226
  ## Look inside the Sandbox
@@ -237,12 +239,12 @@ node --test
237
239
  ```
238
240
 
239
241
  ```text
240
- ✔ greets the world by default (1.172165ms)
241
- ✔ greets a name (0.138615ms)
242
- ✔ shouts (0.117168ms)
243
- ✔ trims the name (0.067037ms)
244
- ✔ prints help (0.642445ms)
245
- ✔ refuses an unknown flag (0.281559ms)
242
+ ✔ greets the world by default (1.0597ms)
243
+ ✔ greets a name (0.162102ms)
244
+ ✔ shouts (0.08266ms)
245
+ ✔ trims the name (0.057171ms)
246
+ ✔ prints help (3.806196ms)
247
+ ✔ refuses an unknown flag (0.222015ms)
246
248
  ℹ tests 6
247
249
  ℹ suites 0
248
250
  ℹ pass 6
@@ -250,7 +252,7 @@ node --test
250
252
  ℹ cancelled 0
251
253
  ℹ skipped 0
252
254
  ℹ todo 0
253
- ℹ duration_ms 55.318206
255
+ ℹ duration_ms 53.312932
254
256
  ```
255
257
 
256
258
  Your times are different. Read the commits of the coder. The branch starts at the release tag, so the commits after the
@@ -261,7 +263,7 @@ git log --oneline v<version>..HEAD
261
263
  ```
262
264
 
263
265
  ```text
264
- 1a84393 Fix: trim the name before using it in greet output
266
+ e467788 fix: trim name in greet.js to handle leading/trailing whitespace
265
267
  ```
266
268
 
267
269
  Each line is one commit. The commit ids and the messages come from the model. The author of each commit is
@@ -283,7 +285,7 @@ jr2 send <run id> --gate body.review --event request_changes --input '{"notes":"
283
285
  ```text
284
286
  …
285
287
  → context kind-jr2 / namespace my-instance
286
- delivered request_changes to gate "body.review" on a7a8fc26-851f-4459-9efb-845389acfda9
288
+ delivered request_changes to gate "body.review" on 290b4be9-1e24-46a2-8cb5-27fabb3bbd3c
287
289
  ```
288
290
 
289
291
  The coder continues the same conversation, with your notes as its next prompt. The Sandbox and the branch stay. Run
@@ -312,7 +314,7 @@ Username for 'https://github.com': <you>
312
314
  Password for 'https://<you>@github.com':
313
315
  …
314
316
  To https://github.com/<you>/jr2.git
315
- * [new branch] jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16 -> jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16
317
+ * [new branch] jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd -> jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd
316
318
  ```
317
319
 
318
320
  `origin` fetches through the Repo cache of the node, and pushes to your fork. Open your fork on GitHub to see the
@@ -337,7 +339,7 @@ jr2 send <run id> --gate body.review --event approve
337
339
  ```text
338
340
  …
339
341
  → context kind-jr2 / namespace my-instance
340
- delivered approve to gate "body.review" on a7a8fc26-851f-4459-9efb-845389acfda9
342
+ delivered approve to gate "body.review" on 290b4be9-1e24-46a2-8cb5-27fabb3bbd3c
341
343
  ```
342
344
 
343
345
  In the first terminal, `jr2 run` prints the end of the run, then the final status as one line of JSON, and stops:
@@ -346,7 +348,7 @@ In the first terminal, `jr2 run` prints the end of the run, then the final statu
346
348
  …
347
349
  → active "teardown"
348
350
  → done "done"
349
- {"runId":"a7a8fc26-851f-4459-9efb-845389acfda9","workflow":"task",…,"output":{"branch":"jr2/task-d8f59a0c-9ea1-43fc-8d0c-b153caf70f16"}}
351
+ {"runId":"290b4be9-1e24-46a2-8cb5-27fabb3bbd3c","workflow":"task",…,"output":{"branch":"jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd"},"worktrees":[{"slot":"target","path":"/work/target/task","branch":"jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd"}]}
350
352
  ```
351
353
 
352
354
  ## Make sure that the Sandbox is gone
@@ -364,7 +366,7 @@ No resources found in my-instance namespace.
364
366
  ## What happened
365
367
 
366
368
  - `task` is a Machine in a Workspace. The run made a Sandbox for the Workspace and cloned your fork into it. It cut the
367
- branch from the release tag, in a Worktree in `/work/target/`. See [Workspaces](../concepts/workspaces.md).
369
+ branch from the release tag, in the Worktree `/work/target/task`. See [Workspaces](../concepts/workspaces.md).
368
370
  - `task` left its Repo Slots and the model of its coder Open. `customize()` bound them where you registered the
369
371
  Workflow, and `jr2 up` refuses a Workflow with an Open part. See [Composition](../concepts/composition.md) and
370
372
  [ADR-0054](../reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md).