@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
@@ -0,0 +1,560 @@
1
+ ---
2
+ title: Cut a Worktree
3
+ description: Make a Worktree with worktree(), choose what a cut refuses, and remove a Worktree.
4
+ ---
5
+
6
+ # Cut a Worktree
7
+
8
+ Make each [Worktree](../reference/glossary.md#worktree) that your [Machine](../reference/glossary.md#machine) needs,
9
+ with the [`worktree()`](../reference/authoring-surface/worktree.md) [Effect](../reference/glossary.md#effect).
10
+
11
+ A [Workspace](../reference/glossary.md#workspace) only clones each [Repo](../reference/glossary.md#repo). The body of
12
+ the Workspace does a [Cut](../reference/glossary.md#cut) of each Worktree in a state of its own. In this
13
+ [Workflow](../reference/glossary.md#workflow), a coder [Agent](../reference/glossary.md#agent) works on a new branch. A
14
+ reviewer reads the coder's commit in a second Worktree, with no branch. Then the Machine removes the review Worktree,
15
+ and pushes the commit that the reviewer read. Save the file as `workflows/fix-a-bug.ts` in your
16
+ [Instance](../reference/glossary.md#instance):
17
+
18
+ **Anthropic**
19
+
20
+ ```ts
21
+ import { assign } from "xstate";
22
+ import { z } from "zod";
23
+ import {
24
+ agent,
25
+ arg,
26
+ branchName,
27
+ command,
28
+ defineEvent,
29
+ fail,
30
+ isReady,
31
+ jr2Setup,
32
+ push,
33
+ readyOf,
34
+ workspace,
35
+ worktree,
36
+ type Workspaced,
37
+ } from "@jr2/orchestrator";
38
+
39
+ const finished = defineEvent({
40
+ name: "finished",
41
+ description: "Call this when you committed the fix.",
42
+ input: z.object({}),
43
+ });
44
+ const pass = defineEvent({ name: "pass", description: "Call this when the commit is correct.", input: z.object({}) });
45
+ const revise = defineEvent({
46
+ name: "revise",
47
+ description: "Call this when the commit is not correct. Say what to change.",
48
+ input: z.object({ notes: z.string() }),
49
+ });
50
+
51
+ // The run input. `branchName` refuses a name that the cut and the push refuse.
52
+ const runInput = z.object({ prompt: z.string(), branch: branchName });
53
+ type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
54
+ type Context = BodyInput & { reviewed?: string };
55
+
56
+ // Your Machine names each folder. So it knows each path before the cut.
57
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/fix` as const;
58
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
59
+ const cloneTree = (context: BodyInput) => `${context.workspace.repos.app}/default` as const;
60
+
61
+ const body = jr2Setup({
62
+ types: {} as { input: BodyInput; context: Context },
63
+ events: [finished, pass, revise],
64
+ actors: {
65
+ coder: agent({ model: "anthropic/claude-haiku-5-5", instructions: "You fix the bug. You commit the fix." }),
66
+ reviewer: agent({
67
+ model: "anthropic/claude-haiku-5-5",
68
+ workspace: "read",
69
+ instructions: "You review one commit. You change nothing.",
70
+ }),
71
+ // Each run gets a new branch: the cut refuses a branch that exists.
72
+ cut: worktree({ expect: "new" }),
73
+ // No `--force`: git refuses a Worktree with changes, and the output holds its exit code.
74
+ // The state below goes on with either result: a Worktree that stays does no harm here.
75
+ remove: command(["git", "worktree", "remove", arg("path")], { timeout: "1m" }),
76
+ publish: push({ pin: true }),
77
+ fail: fail(),
78
+ },
79
+ }).createMachine({
80
+ id: "fix-a-bug",
81
+ context: ({ input }) => input,
82
+ on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
83
+ initial: "cutting",
84
+ states: {
85
+ cutting: {
86
+ invoke: {
87
+ src: "cut",
88
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
89
+ // The last arm has no guard: it takes `refused`, `unknown` and each later outcome.
90
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
91
+ },
92
+ },
93
+ coding: {
94
+ invoke: {
95
+ src: "coder",
96
+ input: ({ context }) => ({ prompt: context.prompt, cwd: coderTree(context), continue: true }),
97
+ },
98
+ on: { finished: "reading" },
99
+ },
100
+ // A cut of the same path reads the Worktree. `head` is the tip of the branch, whatever HEAD the coder left.
101
+ reading: {
102
+ invoke: {
103
+ src: "cut",
104
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
105
+ onDone: [
106
+ {
107
+ guard: ({ event }) => isReady(event.output),
108
+ target: "detaching",
109
+ actions: assign({ reviewed: ({ event }) => readyOf(event.output).head }),
110
+ },
111
+ { target: "notCut" },
112
+ ],
113
+ },
114
+ },
115
+ // Each round forces this folder to the commit, and cleans it.
116
+ detaching: {
117
+ invoke: {
118
+ src: "cut",
119
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.reviewed! }),
120
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
121
+ },
122
+ },
123
+ reviewing: {
124
+ invoke: {
125
+ src: "reviewer",
126
+ input: ({ context }) => ({ prompt: `Review ${context.reviewed}.`, cwd: reviewTree(context) }),
127
+ },
128
+ on: {
129
+ pass: "removing",
130
+ revise: {
131
+ target: "coding",
132
+ actions: assign({ prompt: ({ event }) => `Change the fix as the review asks:\n\n${event.notes}` }),
133
+ },
134
+ },
135
+ },
136
+ // The clone is not the Worktree that goes, so the command runs there.
137
+ removing: {
138
+ invoke: {
139
+ src: "remove",
140
+ input: ({ context }) => ({ path: reviewTree(context), worktree: cloneTree(context) }),
141
+ onDone: "publishing",
142
+ },
143
+ },
144
+ // The input names the Worktree and the commit. jr2 reads the branch from the cut of that path.
145
+ publishing: {
146
+ invoke: {
147
+ src: "publish",
148
+ input: ({ context }) => ({ worktree: coderTree(context), sha: context.reviewed! }),
149
+ onDone: [{ guard: ({ event }) => event.output.outcome === "pushed", target: "done" }, { target: "notPushed" }],
150
+ },
151
+ },
152
+ done: { type: "final" },
153
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
154
+ notPushed: { invoke: { src: "fail", input: { reason: "the push did not succeed" } } },
155
+ failed: { invoke: { src: "fail", input: { reason: "an Agent's Turn could not go on" } } },
156
+ lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
157
+ },
158
+ });
159
+
160
+ export const machine = workspace(body, {
161
+ input: runInput,
162
+ repos: { app: "https://github.com/<you>/app.git" },
163
+ });
164
+ ```
165
+
166
+ **Local**
167
+
168
+ ```ts
169
+ import { assign } from "xstate";
170
+ import { z } from "zod";
171
+ import {
172
+ agent,
173
+ arg,
174
+ branchName,
175
+ command,
176
+ defineEvent,
177
+ fail,
178
+ isReady,
179
+ jr2Setup,
180
+ push,
181
+ readyOf,
182
+ workspace,
183
+ worktree,
184
+ type Workspaced,
185
+ } from "@jr2/orchestrator";
186
+
187
+ const finished = defineEvent({
188
+ name: "finished",
189
+ description: "Call this when you committed the fix.",
190
+ input: z.object({}),
191
+ });
192
+ const pass = defineEvent({ name: "pass", description: "Call this when the commit is correct.", input: z.object({}) });
193
+ const revise = defineEvent({
194
+ name: "revise",
195
+ description: "Call this when the commit is not correct. Say what to change.",
196
+ input: z.object({ notes: z.string() }),
197
+ });
198
+
199
+ // The run input. `branchName` refuses a name that the cut and the push refuse.
200
+ const runInput = z.object({ prompt: z.string(), branch: branchName });
201
+ type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
202
+ type Context = BodyInput & { reviewed?: string };
203
+
204
+ // Your Machine names each folder. So it knows each path before the cut.
205
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/fix` as const;
206
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
207
+ const cloneTree = (context: BodyInput) => `${context.workspace.repos.app}/default` as const;
208
+
209
+ const body = jr2Setup({
210
+ types: {} as { input: BodyInput; context: Context },
211
+ events: [finished, pass, revise],
212
+ actors: {
213
+ coder: agent({ model: "local/qwen3.6-35b-a3b", instructions: "You fix the bug. You commit the fix." }),
214
+ reviewer: agent({
215
+ model: "local/qwen3.6-35b-a3b",
216
+ workspace: "read",
217
+ instructions: "You review one commit. You change nothing.",
218
+ }),
219
+ // Each run gets a new branch: the cut refuses a branch that exists.
220
+ cut: worktree({ expect: "new" }),
221
+ // No `--force`: git refuses a Worktree with changes, and the output holds its exit code.
222
+ // The state below goes on with either result: a Worktree that stays does no harm here.
223
+ remove: command(["git", "worktree", "remove", arg("path")], { timeout: "1m" }),
224
+ publish: push({ pin: true }),
225
+ fail: fail(),
226
+ },
227
+ }).createMachine({
228
+ id: "fix-a-bug",
229
+ context: ({ input }) => input,
230
+ on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
231
+ initial: "cutting",
232
+ states: {
233
+ cutting: {
234
+ invoke: {
235
+ src: "cut",
236
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
237
+ // The last arm has no guard: it takes `refused`, `unknown` and each later outcome.
238
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
239
+ },
240
+ },
241
+ coding: {
242
+ invoke: {
243
+ src: "coder",
244
+ input: ({ context }) => ({ prompt: context.prompt, cwd: coderTree(context), continue: true }),
245
+ },
246
+ on: { finished: "reading" },
247
+ },
248
+ // A cut of the same path reads the Worktree. `head` is the tip of the branch, whatever HEAD the coder left.
249
+ reading: {
250
+ invoke: {
251
+ src: "cut",
252
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
253
+ onDone: [
254
+ {
255
+ guard: ({ event }) => isReady(event.output),
256
+ target: "detaching",
257
+ actions: assign({ reviewed: ({ event }) => readyOf(event.output).head }),
258
+ },
259
+ { target: "notCut" },
260
+ ],
261
+ },
262
+ },
263
+ // Each round forces this folder to the commit, and cleans it.
264
+ detaching: {
265
+ invoke: {
266
+ src: "cut",
267
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.reviewed! }),
268
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
269
+ },
270
+ },
271
+ reviewing: {
272
+ invoke: {
273
+ src: "reviewer",
274
+ input: ({ context }) => ({ prompt: `Review ${context.reviewed}.`, cwd: reviewTree(context) }),
275
+ },
276
+ on: {
277
+ pass: "removing",
278
+ revise: {
279
+ target: "coding",
280
+ actions: assign({ prompt: ({ event }) => `Change the fix as the review asks:\n\n${event.notes}` }),
281
+ },
282
+ },
283
+ },
284
+ // The clone is not the Worktree that goes, so the command runs there.
285
+ removing: {
286
+ invoke: {
287
+ src: "remove",
288
+ input: ({ context }) => ({ path: reviewTree(context), worktree: cloneTree(context) }),
289
+ onDone: "publishing",
290
+ },
291
+ },
292
+ // The input names the Worktree and the commit. jr2 reads the branch from the cut of that path.
293
+ publishing: {
294
+ invoke: {
295
+ src: "publish",
296
+ input: ({ context }) => ({ worktree: coderTree(context), sha: context.reviewed! }),
297
+ onDone: [{ guard: ({ event }) => event.output.outcome === "pushed", target: "done" }, { target: "notPushed" }],
298
+ },
299
+ },
300
+ done: { type: "final" },
301
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
302
+ notPushed: { invoke: { src: "fail", input: { reason: "the push did not succeed" } } },
303
+ failed: { invoke: { src: "fail", input: { reason: "an Agent's Turn could not go on" } } },
304
+ lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
305
+ },
306
+ });
307
+
308
+ export const machine = workspace(body, {
309
+ input: runInput,
310
+ repos: { app: "https://github.com/<you>/app.git" },
311
+ });
312
+ ```
313
+
314
+ Write your GitHub user name in place of `<you>`. The Agents use the model of the tab.
315
+ [Push and sign a branch](push-and-sign-a-branch.md) tells how to give the push a credential.
316
+
317
+ ## Name the path
318
+
319
+ The path of a Worktree is exactly `/work/<slot>/<name>`. `<slot>` is a [Repo Slot](../reference/glossary.md#repo-slot).
320
+ `<name>` is one folder name that you choose. It is not `default`, and it starts with neither `.` nor `-`. It does not
321
+ change with the name of the branch.
322
+
323
+ Make the path from `context.workspace.repos`, and add `as const`. Then the compiler can read the slot, and it refuses a
324
+ slot that the Workspace does not have:
325
+
326
+ ```ts
327
+ // Your Machine names each folder. So it knows each path before the cut.
328
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/fix` as const;
329
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
330
+ const cloneTree = (context: BodyInput) => `${context.workspace.repos.app}/default` as const;
331
+ ```
332
+
333
+ The check of the compiler is a help. It sees only a literal path under `jr2Setup`. jr2 also checks each path when the
334
+ run gets to it.
335
+
336
+ ## Cut a branch
337
+
338
+ Invoke the Effect with `{ path, branch }`. `branch` is required, unless you give `detach`. The Machine names the branch,
339
+ so jr2 never reads it from the folder or from `HEAD`.
340
+
341
+ ```ts
342
+ cutting: {
343
+ invoke: {
344
+ src: "cut",
345
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
346
+ // The last arm has no guard: it takes `refused`, `unknown` and each later outcome.
347
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
348
+ },
349
+ },
350
+ ```
351
+
352
+ The cut fetches, then finds the branch in this order:
353
+
354
+ 1. A local branch.
355
+ 2. `origin/<branch>`. The new local branch tracks it.
356
+ 3. A new branch, with no upstream. It starts from `from`, or from the base of the
357
+ [Binding](../reference/glossary.md#binding) when the input has no `from`.
358
+
359
+ The output is `ready`, `refused` or `unknown`. Each Effect outcome is open, so give the last arm of `onDone` no guard.
360
+ [`isReady`](../reference/authoring-surface/isready.md) is the guard for `ready`.
361
+
362
+ | The field of `ready` | What it holds |
363
+ | -------------------- | ---------------------------------------------------------------------------- |
364
+ | `slot`, `path` | The Repo Slot and the path of the Worktree. |
365
+ | `branch` | The branch of the cut, or `null` for a detached cut. |
366
+ | `head` | The tip of the branch, or the commit of a detached cut. It is never `HEAD`. |
367
+ | `found` | How the cut found it: `local`, `remote`, `none`, `held` or `commit`. |
368
+ | `stale` | The warning of git when the fetch failed. The cut used what the clone holds. |
369
+
370
+ `held` means that a cut made this path before, so the Worktree holds earlier work. No fetch happens, and the cut does
371
+ not check `expect` again. Thus a cut of the same path also reads a Worktree:
372
+
373
+ ```ts
374
+ // A cut of the same path reads the Worktree. `head` is the tip of the branch, whatever HEAD the coder left.
375
+ reading: {
376
+ invoke: {
377
+ src: "cut",
378
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
379
+ onDone: [
380
+ {
381
+ guard: ({ event }) => isReady(event.output),
382
+ target: "detaching",
383
+ actions: assign({ reviewed: ({ event }) => readyOf(event.output).head }),
384
+ },
385
+ { target: "notCut" },
386
+ ],
387
+ },
388
+ },
389
+ ```
390
+
391
+ [`readyOf`](../reference/authoring-surface/readyof.md) gives the `ready` output to an action, because xstate does not
392
+ carry the guard to the action.
393
+
394
+ ## Choose what a cut refuses
395
+
396
+ `expect` says if the branch can exist. It is an option of `worktree()`, so a run cannot change it:
397
+
398
+ - `worktree()`: use a local branch, then a branch of the remote, then make a new branch.
399
+ - `worktree({ expect: "new" })`: refuse a branch that exists, local or on the remote. Use it for a name that a planner
400
+ or a run input chooses for new work. A name that collides is then a refusal, and not the branch of a different person.
401
+ - `worktree({ expect: "existing" })`: refuse a branch that does not exist, and make nothing. Use it when a run must
402
+ continue a branch that exists.
403
+
404
+ The example gives the coder a new branch for each run:
405
+
406
+ ```ts
407
+ // Each run gets a new branch: the cut refuses a branch that exists.
408
+ cut: worktree({ expect: "new" }),
409
+ ```
410
+
411
+ A pick param or a run input that names a branch can use the schema
412
+ [`branchName`](../reference/authoring-surface/branchname.md). It is the rule of the cut and of the push. Thus jr2
413
+ refuses a bad name at the [Menu](../reference/glossary.md#menu) or at the door, before the cut.
414
+
415
+ ## Read a commit with no branch
416
+
417
+ Invoke the Effect with `{ path, detach }` to hold one commit, with no branch. The example gives this Worktree to a
418
+ reviewer. Each cut forces the folder to the commit, and cleans it. Ignored files stay, so that a reviewer can run the
419
+ tests:
420
+
421
+ ```ts
422
+ // Each round forces this folder to the commit, and cleans it.
423
+ detaching: {
424
+ invoke: {
425
+ src: "cut",
426
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.reviewed! }),
427
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
428
+ },
429
+ },
430
+ ```
431
+
432
+ `expect` applies only to a branch. A slot with `expect` can thus also make a detached cut, as `cut` does here. A cut
433
+ that you lend to a [Chart](../reference/glossary.md#chart) must state `expect`, so a Chart uses such a slot too.
434
+
435
+ ## Give the Worktree to each step
436
+
437
+ Each step names its Worktree by its path:
438
+
439
+ | The step | The key of its input |
440
+ | --------------------------------------- | -------------------- |
441
+ | A [Turn](../reference/glossary.md#turn) | `cwd` |
442
+ | A `command` | `worktree` |
443
+ | A `push` | `worktree` |
444
+ | An `http` Effect with a file | `worktree` |
445
+
446
+ Each key is required under Repo Slots. A Turn that wants the folder above the Repo Slots states `cwd: "/work"`. To read
447
+ a slot that has no Worktree, use `/work/<slot>/default`. A Workspace with no Repo Slot, and an Agent with
448
+ `workspace: "none"`, need no `cwd`.
449
+
450
+ A Turn of a Working Agent that names a path where no Worktree exists does not start. The
451
+ [Harness](../reference/glossary.md#harness) refuses it, and the Machine gets `agent.fault` with
452
+ ``the Turn's `cwd` <path> does not exist: cut the Worktree first``.
453
+
454
+ A `push` has no branch in its input. jr2 finds the cut of that path, and reads the branch there. An Agent can move
455
+ `HEAD`, and it cannot change that record:
456
+
457
+ ```ts
458
+ // The input names the Worktree and the commit. jr2 reads the branch from the cut of that path.
459
+ publishing: {
460
+ invoke: {
461
+ src: "publish",
462
+ input: ({ context }) => ({ worktree: coderTree(context), sha: context.reviewed! }),
463
+ onDone: [{ guard: ({ event }) => event.output.outcome === "pushed", target: "done" }, { target: "notPushed" }],
464
+ },
465
+ },
466
+ ```
467
+
468
+ ## Run more than one coder
469
+
470
+ The author chooses how many Agents work in a Workspace. Agents in one Worktree share git, as people in one checkout do.
471
+ Give each coder that needs its own branch a Worktree of its own, with its own path and its own branch. Each Worktree
472
+ uses the one clone. To merge the branches, cut a Worktree for the merge target, and run `git merge` in a `command`
473
+ there. No fetch is necessary, because each branch is in the clone. Then push once.
474
+
475
+ Cuts of one clone wait for each other, so they do not collide.
476
+
477
+ ## Remove a Worktree
478
+
479
+ jr2 has no Effect that removes a Worktree. To remove one, run `git worktree remove` in a `command`:
480
+
481
+ ```ts
482
+ // No `--force`: git refuses a Worktree with changes, and the output holds its exit code.
483
+ // The state below goes on with either result: a Worktree that stays does no harm here.
484
+ remove: command(["git", "worktree", "remove", arg("path")], { timeout: "1m" }),
485
+ ```
486
+
487
+ Invoke the `command` in a different Worktree. The clone is always there, so the example names it:
488
+
489
+ ```ts
490
+ // The clone is not the Worktree that goes, so the command runs there.
491
+ removing: {
492
+ invoke: {
493
+ src: "remove",
494
+ input: ({ context }) => ({ path: reviewTree(context), worktree: cloneTree(context) }),
495
+ onDone: "publishing",
496
+ },
497
+ },
498
+ ```
499
+
500
+ - The branch stays. Only the folder goes.
501
+ - A Worktree with changes or with untracked files stays too, and git exits with a code that is not 0. Route on `exited`
502
+ to see it, or add `--force` to the argv to drop those changes. Ignored files do not stop the removal.
503
+ - jr2 keeps its record of the cut. A cut of the same path and the same branch makes the folder again. Its `found` is
504
+ `local`. A detached cut makes the folder again too.
505
+ - jr2 deletes each Worktree with the [Sandbox](../reference/glossary.md#sandbox). Thus a Machine needs this recipe only
506
+ to free the disk during a run.
507
+
508
+ ## Rules
509
+
510
+ These settle as `refused`, and nothing changes. Route them to your last arm:
511
+
512
+ - a path that is not a Worktree path: `"<path>" is not a Worktree path: /work/<slot>/<name>, with one segment in <name>`
513
+ - the path of the clone: `"<path>" is the clone: it stays as the attach left it; cut a sibling`
514
+ - a name that starts with `.` or `-`: `"<path>": a Worktree's name starts with neither . nor -`
515
+ - a slot that the Workspace does not have:
516
+ `"<path>" names Repo Slot "<slot>", which its Workspace does not carry (<slots>)`
517
+ - no branch and no commit, or both: ``a cut names a branch, or a commit in `detach` ``
518
+ - `from` on a detached cut: ``a detached cut takes no `from` ``
519
+ - `expect: "new"` on a branch that exists: `<branch> exists, and this cut makes only a new branch`
520
+ - `expect: "existing"` on a branch that does not exist: `<branch> does not exist, and this cut makes nothing`
521
+ - a re-cut that does not agree with the first cut: `<path> was cut for <branch>, not <other>`,
522
+ `<path> was cut detached, not for <branch>`, or `<path> was cut from <from>, not <other>`
523
+ - a folder that no cut made: `<path> is a folder and no Worktree: a cut removes nothing it did not make`
524
+ - a detached cut of a Worktree that no cut made:
525
+ `<path> is a Worktree no cut made, and a detached cut forces only its own`
526
+ - a commit that the clone does not hold, also after a fetch: `no commit "<sha>" in this clone`
527
+ - a branch that `default/` or a different Worktree holds: `<path> holds <branch>: read it there, or cut another branch`
528
+ - a name that the clone does not hold, after a fetch that failed:
529
+ `<branch> is not in this clone, and the remote did not answer, so it may exist there: <warning>`
530
+ - a `from` on a branch that exists:
531
+ ``<branch> exists, and it is not at `from` "<from>": `from` starts a new branch and moves no branch``
532
+
533
+ A name that git or the push refuses also settles `refused`. The `reason` says why, for example
534
+ `"<name>" is not a branch name`.
535
+
536
+ These are not `refused`, because no word of a run can cause them:
537
+
538
+ - `createMachine` refuses an Effect's `onDone` with a guard on its last arm:
539
+ `machine "<id>": state "<state>" (Effect "<slot>") has an onDone with no unguarded last arm — … (ADR-0075)`
540
+ - `jr2 up` refuses a `worktree()` in a Workspace with no Repo Slot:
541
+ `workflow "<workflow>": worktree "<slot>" runs in Workspace of workflow "<workflow>", which has no Repo Slot: a cut makes a Worktree of a Repo Slot, and there is none — declare one: repos: { app: "<url>" } (ADR-0075)`
542
+ - when a `worktree()` starts, it refuses the same Workspace:
543
+ `worktree "<slot>" runs under a Workspace with no Repo Slot: a cut makes a Worktree of a Repo Slot, and there is none — declare one: repos: { app: "<url>" } (ADR-0075)`
544
+ - a Repo Slot name that starts with `.`, because `/work/.jr2/` is reserved for jr2. See
545
+ [Bind a shipped Machine's Open parts](bind-open-parts.md).
546
+
547
+ ## Read next
548
+
549
+ - [Workspaces](../concepts/workspaces.md): the clone, the folders, and what names a Worktree.
550
+ - [Effects](../concepts/effects.md): where a cut runs, and what a restart does to it.
551
+ - [Push and sign a branch](push-and-sign-a-branch.md): the `push` Effect.
552
+ - [Set an Agent's access and Dials](set-an-agents-access-and-dials.md): a reviewer in a detached Worktree.
553
+ - [Let an Agent write a Chart](let-an-agent-write-a-chart.md): a cut that you lend to a Chart.
554
+ - Reference: [`worktree`](../reference/authoring-surface/worktree.md),
555
+ [`WorktreeOptions`](../reference/authoring-surface/type-worktreeoptions.md),
556
+ [`WorktreeInput`](../reference/authoring-surface/type-worktreeinput.md),
557
+ [`WorktreeOutput`](../reference/authoring-surface/type-worktreeoutput.md) and
558
+ [`branchName`](../reference/authoring-surface/branchname.md).
559
+ - [ADR-0075](../reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md): a Machine cuts a
560
+ Worktree, and the Workspace only clones.
@@ -308,7 +308,7 @@ refusals of a push and of the [Effector](../reference/glossary.md#effector) imag
308
308
 
309
309
  The check has a limit. The check pod has labels of its own, so a NetworkPolicy that selects the Harness pods by label
310
310
  does not select the check pod. When such a policy closes the egress of the Harness pods, the check can pass while a
311
- Turn fails.
311
+ Turn fails. The egress policy of jr2, which closes the cloud metadata addresses, selects the check pod too.
312
312
 
313
313
  A check can fail when the model works, for example a model that loads for longer than the check waits. When you know
314
314
  that the models work, run `jr2 up --skip-provider-preflight`. No check pod runs, and the converge goes on. `jr2 up`
@@ -200,11 +200,15 @@ it. Then the Chart ends, and the run ends with the status `done`.
200
200
  - The door refuses a Chart, and gives one line for each problem. Each line starts with the path in the Chart. For
201
201
  example:
202
202
  - `<path>: "<name>" is not a lent actor or an Agent of this Chart; lent: <names>; kit: <gate, fail or none>; minted: <names>`
203
- - `<path>: "<name>" is not a lent guard; lent guards: <names> (and jr2.sentBy with params { id }, and jr2.failure)`
203
+ - `<path>: "<name>" is not a lent guard; lent guards: <names> (and jr2.sentBy with params { id }, jr2.failure and jr2.ready)`
204
204
  - `<path>.type: "<name>" is not a generic or a lent action; generic: set, append, increment, clear; lent: <names>`
205
205
  - ``<path>: <reference> is supplied by nothing: it is not in the initial `context`, and no `set`, `append` or `increment` writes it``,
206
206
  then what a write supplies
207
207
  - `<path>: "<reference>" is not a reference: it starts at context, event or input (a reference is a path, never an expression)`
208
+ - ``<path>: the lent cut "<slot>" does not state `expect`, so a Chart cannot use it; <fix>``
209
+ - ``<path>: the lent cut "<slot>" states `expect: "existing"` beside the lent force push "<slot>", and together they can force-push any existing branch; <fix>``
210
+ - ``<path>.cwd: "<cwd>" is a Repo Slot's folder, not a Worktree; `cwd` is a Worktree that a cut made, "/work/<slot>/<name>", or "/work/<slot>/default" to read, or "/work"``
211
+ - ``<path>: <agent> works under a Workspace with Repo Slots, and a Turn there says where: `cwd` is a Worktree, or "/work"``
208
212
  - The door then compiles the Chart as the Open body does. A line of this last check starts with `(chart):`, and names a
209
213
  state by its [xstate](https://xstate.js.org) id (for example `body.writing`), not by its path in the Chart.
210
214
  - A refusal is output, not an error: the Open body ends with `{ outcome: "refused", report }`. A Chart wrapper that
@@ -47,10 +47,12 @@ ws-a1322024-x-2-h8pgwk Ready http://ws-a1322024-x-2-h8pgwk.my-instance.svc:8
47
47
  `Lost` when the pod is gone, and a `Lost` Sandbox does not get a new pod. A delete removes the Sandbox at once, so you
48
48
  seldom see the phase `Terminating`.
49
49
  - When the run has no Sandbox now, `kubectl` prints `No resources found in <namespace> namespace.`
50
+ - [`jr2 status <run id>`](../reference/cli.md#status) prints each [Worktree](../reference/glossary.md#worktree) that the
51
+ run cut, with its path and its branch, in `worktrees` and in one `worktree:` line for each cut. Give this command on
52
+ your computer, not in the Sandbox.
50
53
  - When a [Workflow](../reference/glossary.md#workflow) puts `branch` and `worktree` in the `meta` of a
51
54
  [Gate](../reference/glossary.md#gate), as the `review` Gate of [`task`](../reference/machines.md#task) does,
52
- [`jr2 status <run id>`](../reference/cli.md#status) prints them in `gates`. Give this command on your computer, not in
53
- the Sandbox. Then you can copy the Worktree from the status.
55
+ `jr2 status <run id>` prints them in `gates` too.
54
56
 
55
57
  ## Open a shell
56
58
 
@@ -64,8 +66,8 @@ starts from `node:24-slim`, which has `bash`. If your Sandbox Image has no `bash
64
66
  ## Find the Worktree
65
67
 
66
68
  Each [Repo Slot](../reference/glossary.md#repo-slot) of the Workspace has a folder in `/work`. In it, `default` is a
67
- clone of the [Repo](../reference/glossary.md#repo), and the [Worktree](../reference/glossary.md#worktree) of the branch
68
- is beside it. To show the folders of the Repo Slots, use this command:
69
+ clone of the [Repo](../reference/glossary.md#repo), and each [Worktree](../reference/glossary.md#worktree) that the
70
+ Machine cut is beside it. To show the folders of the Repo Slots, use this command:
69
71
 
70
72
  ```sh
71
73
  ls /work
@@ -80,20 +82,20 @@ git -C /work/<Repo Slot>/default worktree list
80
82
  For the slot `target` of a run of `task`, the output is:
81
83
 
82
84
  ```text
83
- /work/target/default 1e40403 [main]
84
- /work/target/jr2-task-0cbdb607-48dc-4eab-9469-f8e41f2b0025 1e40403 [jr2/task-0cbdb607-48dc-4eab-9469-f8e41f2b0025]
85
+ /work/target/default 7f258f6 (detached HEAD)
86
+ /work/target/task e467788 [jr2/task-1f34eef3-5749-4d7c-a16b-488c56ec88cd]
85
87
  ```
86
88
 
87
- The first line is the clone, on the default branch of the Repo. The second line is the Worktree of the branch. Your
88
- commits and your instance id are different.
89
+ The first line is the clone, on the base of the Repo Slot. Here the base is a tag, so the clone is detached. On a
90
+ branch, the line shows the branch. The second line is the Worktree that `task` cut. Your commits and your instance id
91
+ are different.
89
92
 
90
- - In the commands below, `<branch>` is the name of the branch, and `<worktree>` is the folder of its Worktree. The
91
- folder is the name of the branch, with each `/` changed to `-`. For example, the branch `jr2/task-<instance id>` has
92
- the Worktree `/work/target/jr2-task-<instance id>`.
93
- - A Workspace that has a commit to review has one more Worktree, `/work/<Repo Slot>/<worktree>-review`. It holds that
94
- commit, with no branch.
95
- - The attach makes the folders and the Worktrees after the Sandbox is `Ready`. Until then, `/work` has no folder for the
96
- Repo Slot.
93
+ - In the commands below, `<branch>` is the name of the branch, and `<worktree>` is the name of the folder of its
94
+ Worktree. Your Machine chooses each one. The name of the folder does not come from the branch. `task` names the folder
95
+ `task`.
96
+ - A detached Worktree, such as the Worktree of a reviewer, shows `(detached HEAD)` in place of a branch.
97
+ - The attach makes the folders and the clones after the Sandbox is `Ready`. Until then, `/work` has no folder for the
98
+ Repo Slot. The Machine cuts each Worktree in a state of its body. See [Cut a Worktree](cut-a-worktree.md).
97
99
 
98
100
  ## Read the branch
99
101
 
@@ -168,6 +170,7 @@ A [Machine](../reference/glossary.md#machine) can also push the branch with an
168
170
  ## Read next
169
171
 
170
172
  - [Workspaces](../concepts/workspaces.md)
173
+ - [Cut a Worktree](cut-a-worktree.md)
171
174
  - [ADR-0004](../reference/decisions/0004-worktree-provisioning-model.md): the clone and the Worktree.
172
175
  - [ADR-0005](../reference/decisions/0005-sandbox-pod-composition.md): the containers of a Sandbox.
173
176
  - [ADR-0012](../reference/decisions/0012-workspace-wrapper-machine.md): when a Workspace deletes its Sandbox.