@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
@@ -31,7 +31,7 @@ The imports:
31
31
  ```ts
32
32
  import { fromPromise } from "xstate";
33
33
  import { z } from "zod";
34
- import { customize, defineEvent, pool, source } from "@jr2/orchestrator";
34
+ import { customize, defineEvent, pool, source, type PoolNextInput } from "@jr2/orchestrator";
35
35
  import { task } from "@jr2/machines";
36
36
  ```
37
37
 
@@ -59,13 +59,9 @@ type Issue = { number: number; title: string; body: string };
59
59
  // because the request has no token.
60
60
  const ISSUES = "https://api.github.com/repos/you/app/issues?labels=jr2&state=open&per_page=100";
61
61
 
62
- // The issues that this Source gave to a worker. A worker has each one now, or it finished it.
63
- // The set is in the memory of the Orchestrator, and a restart empties it.
64
- const claimed = new Set<number>();
65
-
66
62
  // The Source: give the next open issue that no worker has had.
67
63
  const issues = source<Issue>({
68
- next: fromPromise<Issue | null, { active: string[] }>(async ({ input }) => {
64
+ next: fromPromise<Issue | null, PoolNextInput>(async ({ input }) => {
69
65
  const res = await fetch(ISSUES, { headers: { accept: "application/vnd.github+json" } }).catch(() => undefined);
70
66
  if (!res?.ok) {
71
67
  // While workers run, look again later. With no worker, stop the run with the error.
@@ -78,12 +74,11 @@ const issues = source<Issue>({
78
74
  body: string | null;
79
75
  pull_request?: unknown;
80
76
  }>;
81
- // The issues API also lists pull requests. Skip them, and skip each issue that a worker has now.
82
- const issue = open.find(
83
- (i) => i.pull_request === undefined && !claimed.has(i.number) && !input.active.includes(`issue-${i.number}`),
84
- );
77
+ // The issues API also lists pull requests. Skip them, and skip each issue that a worker has now
78
+ // or that a worker finished.
79
+ const taken = [...input.active, ...input.settled];
80
+ const issue = open.find((i) => i.pull_request === undefined && !taken.includes(`issue-${i.number}`));
85
81
  if (!issue) return null;
86
- claimed.add(issue.number);
87
82
  return { number: issue.number, title: issue.title, body: issue.body ?? "" };
88
83
  }),
89
84
  // Send this event to the `source` Gate when you add the label to an issue.
@@ -129,10 +124,10 @@ The file has five parts:
129
124
  When no issue is ready, it gives `null`. The request has no token, so the Repo must be public. Without a token, GitHub
130
125
  allows 60 requests in each hour. The example reads only the first page of issues: at most 100. When the request fails
131
126
  while workers run, `next` gives `null` in place of the error, so that the failure does not stop the run.
132
- - **The record of the Source.** `next` keeps each issue that it gives in the `Set` `claimed`, and skips the issues in
133
- `active`. The `Set` is in the memory of the [Orchestrator](../reference/glossary.md#orchestrator), and each run of the
134
- Workflow uses the same `Set`. A restart of the Orchestrator empties it. Then `next` can give a finished issue again,
135
- but never an issue that a worker of this run has now.
127
+ - **The record of the Source.** The Source keeps no record of its own. `next` skips each issue in `active` and in
128
+ `settled`. The Pool holds both, so they stay across a restart of the
129
+ [Orchestrator](../reference/glossary.md#orchestrator). Each run of the Workflow has its own lists, so a new run can
130
+ give an issue that is still open.
136
131
  - **The worker.** `fixer` is `task` with its two [Open](../reference/glossary.md#open) parts bound: the Repo and the
137
132
  model of the coder. [Bind a shipped Machine's Open parts](bind-open-parts.md) tells more.
138
133
  - **The run input.** `door` is the run input of the Pool. `cap` reads `workers` from it. A worker never gets it.
@@ -149,15 +144,21 @@ jr2 up
149
144
  …
150
145
  jr2 up — instance "my-instance" → context kind-jr2 / namespace my-instance
151
146
  typecheck: tsc --noEmit (the instance's own compiler)
147
+ …
148
+ effects: workflow "issues" — what it runs outside itself (ADR-0067)
149
+ cut: worktree — 600s, in the Harness
150
+ …
152
151
  failures: workflow "issues" — the states that fail the run (ADR-0073)
153
152
  issues.failing: invokes fail
153
+ task.noBranch: invokes fail
154
+ task.notCut: invokes fail
154
155
  task.lost: invokes fail
155
156
  …
156
157
  seats: workflow "issues" — who may send what, per state (ADR-0015)
157
158
  task.working agent:coder: [finish]
158
159
  task.review gate: [approve, request_changes]
159
160
  …
160
- agents: coder, triager (carried by 4 workflow machine(s))
161
+ agents: coder, triager (carried by 5 workflow machine(s))
161
162
  …
162
163
  sandbox nodes: jr2-control-plane
163
164
  …
@@ -286,8 +287,8 @@ In the JSON, `context.items` holds the output of each worker that finished, by i
286
287
  workers register the same Gate or [Agent](../reference/glossary.md#agent), the run stops with the status `error`, and
287
288
  its `fault` is `registration address "<address>" is already live (two concurrent registrations share an id)`. For
288
289
  `task`, `<address>` is `gate/<run id>/<item id>.body.review` or `agent/<run id>/<item id>.body/coder`.
289
- - `next` gets only `active`: the ids of the items that workers have now. To not give a finished item again, the Source
290
- must keep the items that it gave.
290
+ - `next` gets `active`, the ids of the items that workers have now, and `settled`, the ids of the finished and failed
291
+ items. It also gets `input`, the input of the Pool. To not give a finished item again, skip each id in `settled`.
291
292
  - When a worker fails, for example when the Sandbox of a `task` is lost, the Pool keeps the Failure in `failures` and
292
293
  goes on. Only the Sandbox of that worker is deleted. The other workers, and the work in their Sandboxes, stay. A
293
294
  defect in a worker is different: the run stops with the status `error`.
@@ -19,7 +19,7 @@ the file as `workflows/review.ts` in your [Instance](../reference/glossary.md#in
19
19
  ```ts
20
20
  import { z } from "zod";
21
21
  import { assign } from "xstate";
22
- import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
22
+ import { agent, defineEvent, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
23
23
 
24
24
  const approve = defineEvent({
25
25
  name: "approve",
@@ -42,6 +42,10 @@ const runInput = z.object({ branch: z.string(), sha: z.string(), model: z.string
42
42
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
43
43
  type Context = BodyInput & { notes: string; summary: string };
44
44
 
45
+ // Two Worktrees of one clone: the coder's branch, and the commit that the reviewer reads.
46
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/work` as const;
47
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
48
+
45
49
  const body = jr2Setup({
46
50
  types: {} as { input: BodyInput; context: Context },
47
51
  events: [approve, requestChanges, finished],
@@ -64,21 +68,38 @@ const body = jr2Setup({
64
68
  workspace: "none",
65
69
  thinkingLevel: "low",
66
70
  }),
71
+ cut: worktree(),
67
72
  fail: fail(),
68
73
  },
69
74
  }).createMachine({
70
75
  id: "review",
71
76
  context: ({ input }) => ({ ...input, notes: "", summary: "" }),
72
77
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
73
- initial: "reviewing",
78
+ initial: "cutting",
74
79
  states: {
80
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
81
+ cutting: {
82
+ invoke: {
83
+ src: "cut",
84
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
85
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "detaching" }, { target: "notCut" }],
86
+ },
87
+ },
88
+ // The second cut has no branch: the Worktree holds the commit, detached.
89
+ detaching: {
90
+ invoke: {
91
+ src: "cut",
92
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.sha }),
93
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
94
+ },
95
+ },
75
96
  reviewing: {
76
97
  invoke: {
77
98
  src: "reviewer",
78
99
  input: ({ context }) => ({
79
100
  prompt: `Review commit ${context.sha}.`,
80
- // The review Worktree holds the commit of `reviewSha`, detached from the branch.
81
- cwd: context.workspace.review?.app ?? context.workspace.repos.app,
101
+ // The review Worktree holds the commit, detached from the branch.
102
+ cwd: reviewTree(context),
82
103
  // A Dial: this Turn thinks harder than the default of the model.
83
104
  thinkingLevel: "high",
84
105
  }),
@@ -93,7 +114,7 @@ const body = jr2Setup({
93
114
  src: "coder",
94
115
  input: ({ context }) => ({
95
116
  prompt: `Change the code on the branch as the review asks:\n\n${context.notes}`,
96
- cwd: context.workspace.repos.app,
117
+ cwd: coderTree(context),
97
118
  // Each Turn of the coder continues the same conversation.
98
119
  continue: true,
99
120
  // A Dial: the model of this Turn, from the run input. Absent, the model of the definition.
@@ -115,6 +136,7 @@ const body = jr2Setup({
115
136
  },
116
137
  },
117
138
  done: { type: "final" },
139
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
118
140
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
119
141
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
120
142
  },
@@ -123,7 +145,6 @@ const body = jr2Setup({
123
145
  export const machine = workspace(body, {
124
146
  input: runInput,
125
147
  repos: { app: "https://github.com/<you>/app.git" },
126
- spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
127
148
  });
128
149
  ```
129
150
 
@@ -132,7 +153,7 @@ export const machine = workspace(body, {
132
153
  ```ts
133
154
  import { z } from "zod";
134
155
  import { assign } from "xstate";
135
- import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
156
+ import { agent, defineEvent, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
136
157
 
137
158
  const approve = defineEvent({
138
159
  name: "approve",
@@ -155,6 +176,10 @@ const runInput = z.object({ branch: z.string(), sha: z.string(), model: z.string
155
176
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
156
177
  type Context = BodyInput & { notes: string; summary: string };
157
178
 
179
+ // Two Worktrees of one clone: the coder's branch, and the commit that the reviewer reads.
180
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/work` as const;
181
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
182
+
158
183
  const body = jr2Setup({
159
184
  types: {} as { input: BodyInput; context: Context },
160
185
  events: [approve, requestChanges, finished],
@@ -177,21 +202,38 @@ const body = jr2Setup({
177
202
  workspace: "none",
178
203
  thinkingLevel: "low",
179
204
  }),
205
+ cut: worktree(),
180
206
  fail: fail(),
181
207
  },
182
208
  }).createMachine({
183
209
  id: "review",
184
210
  context: ({ input }) => ({ ...input, notes: "", summary: "" }),
185
211
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
186
- initial: "reviewing",
212
+ initial: "cutting",
187
213
  states: {
214
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
215
+ cutting: {
216
+ invoke: {
217
+ src: "cut",
218
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
219
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "detaching" }, { target: "notCut" }],
220
+ },
221
+ },
222
+ // The second cut has no branch: the Worktree holds the commit, detached.
223
+ detaching: {
224
+ invoke: {
225
+ src: "cut",
226
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.sha }),
227
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
228
+ },
229
+ },
188
230
  reviewing: {
189
231
  invoke: {
190
232
  src: "reviewer",
191
233
  input: ({ context }) => ({
192
234
  prompt: `Review commit ${context.sha}.`,
193
- // The review Worktree holds the commit of `reviewSha`, detached from the branch.
194
- cwd: context.workspace.review?.app ?? context.workspace.repos.app,
235
+ // The review Worktree holds the commit, detached from the branch.
236
+ cwd: reviewTree(context),
195
237
  // A Dial: this Turn thinks harder than the default of the model.
196
238
  thinkingLevel: "high",
197
239
  }),
@@ -206,7 +248,7 @@ const body = jr2Setup({
206
248
  src: "coder",
207
249
  input: ({ context }) => ({
208
250
  prompt: `Change the code on the branch as the review asks:\n\n${context.notes}`,
209
- cwd: context.workspace.repos.app,
251
+ cwd: coderTree(context),
210
252
  // Each Turn of the coder continues the same conversation.
211
253
  continue: true,
212
254
  // A Dial: the model of this Turn, from the run input. Absent, the model of the definition.
@@ -228,6 +270,7 @@ const body = jr2Setup({
228
270
  },
229
271
  },
230
272
  done: { type: "final" },
273
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
231
274
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
232
275
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
233
276
  },
@@ -236,7 +279,6 @@ const body = jr2Setup({
236
279
  export const machine = workspace(body, {
237
280
  input: runInput,
238
281
  repos: { app: "https://github.com/<you>/app.git" },
239
- spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
240
282
  });
241
283
  ```
242
284
 
@@ -259,13 +301,13 @@ prompt and picks from its [Menu](../reference/glossary.md#menu). You can invoke
259
301
  the example does, or in a [Machine](../reference/glossary.md#machine) with no Workspace.
260
302
 
261
303
  A `"read"` Agent keeps bash, so a command can still change a file or the branch. To keep a reviewer away from the
262
- branch, give its Turn the review [Worktree](../reference/glossary.md#worktree):
304
+ branch, give its Turn a detached [Worktree](../reference/glossary.md#worktree):
263
305
 
264
- 1. In the `spec` of the `workspace()`, set `reviewSha` to the commit to review.
265
- 2. In the Turn, set `cwd` to `context.workspace.review?.<slot>`.
306
+ 1. Cut the Worktree with [`worktree()`](../reference/authoring-surface/worktree.md), with `{ path, detach }`.
307
+ 2. In the Turn, set `cwd` to that path.
266
308
 
267
- The review Worktree is `/work/<slot>/<branch>-review`, with each `/` in the branch changed to `-`. It holds the commit
268
- of `reviewSha`, detached from the branch. In the example, `<slot>` is `app`, and the commit comes from the run input.
309
+ The Worktree holds the commit, with no branch. In the example, the path is `/work/app/review`, and the commit comes from
310
+ the run input. [Cut a Worktree](cut-a-worktree.md) tells more.
269
311
 
270
312
  The access is a part of the identity of the Agent. Only the definition sets it. A Turn cannot change it.
271
313
 
@@ -290,7 +332,7 @@ Run [`jr2 up`](../reference/cli.md#up) to converge the new Workflow:
290
332
  jr2 up
291
333
  ```
292
334
 
293
- `jr2 up` prints no line that is special to this Workflow. The `seats:` lines show the picks of the three Agents.
335
+ The `effects:` lines show the cut. The `seats:` lines show the picks of the three Agents.
294
336
 
295
337
  To give a model to the coder's Turns, add `model` to the run input:
296
338
 
@@ -337,9 +379,11 @@ task, also on a Turn that continues. In the example, each `fixing` prompt gives
337
379
  - jr2 refuses a Turn whose `input` has a key that a Turn does not have, such as `instructions` or `workspace`. Only the
338
380
  definition sets those two. `<keys>` is each key that is not correct, in backticks, with a comma between them:
339
381
  ``agent "<slot>": the Turn's input carries <keys>, which a Turn has no use for — its input is its Frame (`prompt`, `cwd`), its Dials (`model`, `thinkingLevel`), and `continue` (ADR-0057)``
340
- - Under a Workspace with more than one [Repo Slot](../reference/glossary.md#repo-slot), jr2 refuses a Turn of an Agent
341
- with Working tools that sets no `cwd`:
342
- ``turn <id>: agent "<slot>" works under a Workspace carrying more than one Repo Slot (<slots>) and its Turn framed no `cwd` — say which Worktree it works in (`cwd: context.workspace.repos.<slot>`, one of those), because the kit gives no slot a meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057)``
382
+ - Under a Workspace with [Repo Slots](../reference/glossary.md#repo-slot), jr2 refuses a Turn of an Agent with Working
383
+ tools that sets no `cwd`:
384
+ ``turn <id>: agent "<slot>" works under a Workspace with Repo Slots (<slots>) and its Turn framed no `cwd`: say where it works, a Worktree, or `cwd: "/work"` above them (ADR-0057)``
385
+ - `workspace()` refuses a body with an invoke that does not say where it works, when it can read the input:
386
+ ``workspace(): state "<state>": "<slot>" names no `<key>`, and a Workspace with Repo Slots says where each Turn and Effect works: a Worktree, or `cwd: "/work"` for a Turn (ADR-0057)``
343
387
  - The Harness refuses a `cwd` that is not an absolute path. The run gets `agent.fault`, with this reason:
344
388
  ``harness admission failed (400): agent "<slot>": the Turn's Frame carries a `cwd` that is not an absolute path — the Frame says WHERE this Turn works (ADR-0057), and a Turn rooted anywhere else would work in the wrong directory silently``
345
389
  - The Harness refuses a model that is not in its catalog and not a model of the custom provider. The run gets
@@ -359,7 +403,8 @@ task, also on a Turn that continues. In the example, each `fixing` prompt gives
359
403
  ## Read next
360
404
 
361
405
  - [Agents and Turns](../concepts/agents-and-turns.md): the definition, the Frame, the Dials and `continue`.
362
- - [Workspaces](../concepts/workspaces.md): the Worktrees and the review Worktree.
406
+ - [Workspaces](../concepts/workspaces.md): the Worktrees.
407
+ - [Cut a Worktree](cut-a-worktree.md): a detached Worktree for a reviewer.
363
408
  - [The Menu](../concepts/the-menu.md): the picks of an Agent.
364
409
  - [Bind a shipped Machine's Open parts](bind-open-parts.md): bind an Open model.
365
410
  - Reference: [`agent`](../reference/authoring-surface/agent.md),
@@ -14,26 +14,39 @@ it on a node, so the Sandbox always has it. Write it in the `resources` option o
14
14
 
15
15
  ```ts
16
16
  import { z } from "zod";
17
- import { command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
17
+ import { command, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
18
18
 
19
19
  const runInput = z.object({ branch: z.string() });
20
20
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
21
21
 
22
+ // The Worktree that the tests run in.
23
+ const testTree = (context: BodyInput) => `${context.workspace.repos.app}/test` as const;
24
+
22
25
  const body = jr2Setup({
23
- types: {} as { input: BodyInput },
26
+ types: {} as { input: BodyInput; context: BodyInput },
24
27
  actors: {
28
+ cut: worktree(),
25
29
  // JR2_CPUS holds the cpu limit of the container. Playwright does not read that limit.
26
30
  e2e: command(["sh", "-c", "npx playwright test --workers=$JR2_CPUS"], { timeout: "30m" }),
27
31
  fail: fail(),
28
32
  },
29
33
  }).createMachine({
30
34
  id: "e2e",
35
+ context: ({ input }) => input,
31
36
  on: { "workspace.lost": ".lost" },
32
- initial: "testing",
37
+ initial: "cutting",
33
38
  states: {
39
+ cutting: {
40
+ invoke: {
41
+ src: "cut",
42
+ input: ({ context }) => ({ path: testTree(context), branch: context.branch }),
43
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
44
+ },
45
+ },
34
46
  testing: {
35
47
  invoke: {
36
48
  src: "e2e",
49
+ input: ({ context }) => ({ worktree: testTree(context) }),
37
50
  onDone: [
38
51
  {
39
52
  guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
@@ -46,6 +59,7 @@ const body = jr2Setup({
46
59
  passed: { type: "final" },
47
60
  // Not final states: each one fails the run, and the run ends `failed`.
48
61
  failed: { invoke: { src: "fail", input: { reason: "the Playwright tests did not pass" } } },
62
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
49
63
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost" } } },
50
64
  },
51
65
  });
@@ -55,7 +69,6 @@ export const machine = workspace(body, {
55
69
  // The Size of the whole Sandbox: 4 GiB of memory and 2 cpus. jr2 reserves all of it on a node.
56
70
  resources: { limits: { memory: "4Gi", cpu: "2" } },
57
71
  repos: { app: "https://github.com/you/app.git" },
58
- spec: ({ input }) => ({ branch: input.branch }),
59
72
  });
60
73
  ```
61
74
 
@@ -160,8 +173,8 @@ The Harness can miss a fast increase. Then the kernel stops the whole container,
160
173
  memory limit (OOMKilled, limit <memory>): the Workspace's processes passed the Harness container's memory limit and the kernel killed the container, the conversation with it. Use fewer workers, or give the Workspace a larger Size (ADR-0060, ADR-0061).
161
174
  ```
162
175
 
163
- The next Agent in the Workspace gets a notice of the kill. When you see either text again and again, state a larger
164
- Size.
176
+ Each conversation that was in the Workspace at the kill gets a notice of it one time, and so does the next Turn of the
177
+ Agent whose Turn the kill cut. When you see either text again and again, state a larger Size.
165
178
 
166
179
  ## Rules
167
180
 
@@ -190,6 +190,12 @@ custom provider, with `harness.provider` and `apiKey`. The guide
190
190
  - A credential that signs each request, such as `AWS_ACCESS_KEY_ID`, cannot be a Held secret. `jr2 up` warns when
191
191
  `harness.env` sets one. Send the calls through a gateway that holds the credential, and hold the key of the gateway:
192
192
  `warning: harness.env sets <NAME>: a signing credential cannot be held (a Stand-in cannot be swapped into a signature), so the Agent can read it — route the provider through a gateway that holds it, and hold the gateway's key instead (ADR-0059)`.
193
+ - The Harness process reads the file that a credential-file name in `harness.env` names, such as `AWS_CONFIG_FILE` or
194
+ `GOOGLE_APPLICATION_CREDENTIALS`. `jr2 up` refuses a path in a folder that the Agent's code writes, or a path that is
195
+ not absolute:
196
+ `harness.env[<i>] sets <NAME> to "<path>", which is in <folder>, a folder the Agent's code writes: the Harness process reads that file, and code that writes it acts outside the Agent's confinement (ADR-0076). Point it at a file of the image that the Sandbox's user cannot write.`
197
+ For another path, it warns:
198
+ `warning: harness.env sets <NAME>: the Harness process reads "<path>", so the Sandbox's user must not be able to write it — a folder of the image that the user owns is one the Agent's code writes (ADR-0076)`.
193
199
  - The Harness refuses a model that is not in the catalog when it admits the Turn:
194
200
  `harness admission failed (400): agent "<agent>": model "<provider>/<modelId>" resolves to nothing — not in pi's catalog, and no custom provider "<provider>" is configured (ADR-0018)`.
195
201
 
@@ -79,19 +79,24 @@ jr2 status a2d8
79
79
 
80
80
  The status is a [`RunStatus`](../reference/http-api.md#type-runstatus), with the open Gates of the run:
81
81
 
82
- | Field | What it holds |
83
- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
84
- | `status` | `active` while the run continues. `done`, `failed`, `error` or `cancelled` when it ended. `drifted` when jr2 did not continue it. |
85
- | `value` | The state of the root Machine. |
86
- | `context` | The context of the root Machine. |
87
- | `children` | The Machines that the run invokes now, such as its Workspaces, each with its state. |
88
- | `waiting` | Each Workspace that waits for capacity: what it waits for, since when, and the words of the scheduler or the quota. |
89
- | `held` | Each [outstanding](../reference/glossary.md#outstanding) pick of an Agent: the Agent, the pick, and since when. |
90
- | `fault` | The error, when the status is `error`. |
91
- | `output` | The output of the Machine, when the run ended. For `failed`, the Failure: `{ reason, detail? }`. |
92
- | `reason` | Why jr2 set the status, when the Machine did not set it, such as `drifted`. |
93
- | `assembly` | The hash of the [Assembly](../reference/glossary.md#assembly) that the run started under, and holds. |
94
- | `gates` | Each open Gate. [Answer a Gate](answer-a-gate.md) explains its fields. |
82
+ | Field | What it holds |
83
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `status` | `active` while the run continues. `done`, `failed`, `error` or `cancelled` when it ended. `drifted` when jr2 did not continue it. |
85
+ | `value` | The state of the root Machine. |
86
+ | `context` | The context of the root Machine. |
87
+ | `children` | The Machines that the run invokes now, such as its Workspaces, each with its state. |
88
+ | `waiting` | Each Workspace that waits for capacity: what it waits for, since when, and the words of the scheduler or the quota. |
89
+ | `held` | Each [outstanding](../reference/glossary.md#outstanding) pick of an Agent: the Agent, the pick, and since when. |
90
+ | `fault` | The error, when the status is `error`. |
91
+ | `output` | The output of the Machine, when the run ended. For `failed`, the Failure: `{ reason, detail? }`. |
92
+ | `reason` | Why jr2 set the status, when the Machine did not set it, such as `drifted`. |
93
+ | `assembly` | The hash of the [Assembly](../reference/glossary.md#assembly) that the run started under, and holds. |
94
+ | `gates` | Each open Gate. [Answer a Gate](answer-a-gate.md) explains its fields. |
95
+ | `worktrees` | Each [Worktree](../reference/glossary.md#worktree) that the run cut: its `slot`, its `path`, and its `branch`, which is `null` when the Worktree is detached. |
96
+
97
+ For a run that cut a Worktree, `jr2 status` also prints one line on stderr for each cut:
98
+ `worktree: <path> (<slot>), branch <branch>`, or `worktree: <path> (<slot>), detached`. jr2 reads each line from the
99
+ record of the cut, so these lines do not change when an Agent changes the branch in the Worktree.
95
100
 
96
101
  [Durable runs](../concepts/durable-runs.md) tells how long jr2 keeps a run that ended.
97
102
 
@@ -241,19 +246,37 @@ For example, this is the log of a run of the `task` Workflow of
241
246
 
242
247
  ```text
243
248
  jr2 harness serving on :8080
249
+ [run] [status] running · body: cutting
250
+ [run] [status] running · body: cutting
244
251
  [run] [status] running · body: working
245
252
  [run] [status] running · body: working
246
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] [prompt] In docs/intro/repo, run `node bin/greet.js`. Then finish, with what it printed.
247
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder]
248
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] Work in /work/target/jr2-task-19b49b58-2ea4-4d79-b100-49aad6e12013, on branch jr2/task-19b49b58-2ea4-4d79-b100-49aad6e12013 — your tools are rooted there, so a relative path lands inside it. Commit what you do there, then call finish.
249
- …
250
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] [thinking] The user wants me to run `node bin/greet.js` inside `docs/intro/repo` directory. Let me navigate there and run it.
251
- …
252
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] [bash] cd docs/intro/repo && node bin/greet.js
253
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] [thinking] The script printed "hello, world". I should now finish with this summary.
254
- …
255
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] [finish] {"summary":"Ran `node bin/greet.js` in docs/intro/repo, which printed: hello, world"}
256
- [coder] [19b49b58-2ea4-4d79-b100-49aad6e12013/body/coder] [finish answer] Delivered "finish" to the workflow (delivery afe6340b-fe10-4819-b545-8ebd1b9a3964). The workflow consumed it and moved on: your turn is over.
253
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [prompt] In docs/intro/repo, run `node bin/greet.js`. Then finish, with what it printed.
254
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
255
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] Work in /work/target/task, on branch jr2/task-84f810c8-dda7-4577-a57e-4a09a4e7f507 — your tools are rooted there, so a relative path lands inside it. Commit what you do there, then call finish.
256
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
257
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] <jr2-turn>
258
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] Working directory: /work/target/task
259
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] Allowed now: `mcp__jr2__finish`.
260
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] </jr2-turn>
261
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [thinking] The user wants me to:
262
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] 1. Run `node bin/greet.js` from the `docs/intro/repo` directory within the worktree
263
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] 2. Report what it printed
264
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] 3. Commit my work and call finish
265
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
266
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] Let me first run the command.
267
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
268
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [text]
269
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
270
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
271
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [bash] cd /work/target/task && node docs/intro/repo/bin/greet.js
272
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [thinking] The command printed "hello, world". Now I need to finish with that information.
273
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
274
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [text]
275
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
276
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder]
277
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [finish] {"summary":"Ran `node bin/greet.js` from docs/intro/repo. It printed: hello, world"}
278
+ [coder] [9501dd2f-95ba-479b-b384-307c736a8aad/body/coder] [finish answer] Delivered "finish" to the workflow (delivery 1b61fa16-50b7-4202-b7ed-b46a6ead5930). The workflow consumed it and moved on: your turn is over.
279
+ [run] [status] running · body: review
257
280
  [run] [status] running · body: review
258
281
  ```
259
282
 
@@ -290,7 +290,7 @@ file whose name starts with `_`, or a `.d.ts` file. Thus put the code that two W
290
290
  not an empty one. The run ends `error`, and its fault is:
291
291
  `fail: the input must be { reason: string; detail?: unknown } with a reason that is not empty (got <value>) — invoke: { src: "fail", input: { reason: "the tests failed" } } (ADR-0073)`.
292
292
  - `workspace()` refuses a body that declares a door. Declare the door with the `input` option of `workspace()`:
293
- ``workspace(): the body "<id>" declares its own run input, which nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` handles, so the body's schema is not the door. Move it to the wrapper: workspace(body, { input, spec }) (ADR-0033).``
293
+ ``workspace(): the body "<id>" declares its own run input, which nothing will ever serve or enforce — the wrapper feeds the body the run input PLUS the injected `workspace` handles, so the body's schema is not the door. Move it to the wrapper: workspace(body, { input }) (ADR-0033).``
294
294
  - `jr2 up` prints each refusal of `defineEvent`, `createMachine` and `workspace()` as `error: <refusal>`, and exits with
295
295
  `1`. It changes nothing in the cluster.
296
296
  - The Orchestrator refuses a start whose input does not agree with the door, with the status code `400`: