@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
@@ -9,21 +9,24 @@ Function, in `@jr2/orchestrator/src/http-effect.ts`. It is an export of `@jr2/or
9
9
 
10
10
  ```ts
11
11
  function file<S extends z.ZodType | undefined = undefined>(path: FromInput<S, string>, options: {
12
- slot?: string;
13
12
  segments?: FromInput<S, readonly string[]>;
14
13
  contentType?: string;
15
14
  } = {}): FileBody<S>
16
15
  ```
17
16
 
18
17
  Make a Worktree file the body of an `http` Effect: `body: file("dist/report.pdf")`. The Effect
19
- then runs in the Sandbox Effector of the Workspace whose body invokes it. That Effector opens the
20
- file in the Worktree and sends it. In a Workspace with no Repo Slot, the path is relative to
18
+ then runs in the Sandbox Effector of the Workspace whose body invokes it. Under a Workspace with
19
+ Repo Slots, the invoke's input names the Worktree: `input: { worktree: "/work/app/fix" }`. It is a
20
+ Worktree that a cut of this run made. That Effector opens the file in that Worktree and sends it.
21
+ In a Workspace with no Repo Slot, the input names no `worktree`, and the path is relative to
21
22
  `/work`, the folder that its steps share. Use it with `PUT`, `POST` or `PATCH`. `path` and
22
23
  `segments` can be functions of the input of the Effect.
23
24
 
24
- Some paths and files give the output `refused`, and then nothing is sent. The path has a symlink
25
- in it, has `..`, or is absolute. Or the file is a FIFO, a directory, or a file with more than one
26
- link.
25
+ Some inputs, paths and files give the output `refused`, and then nothing is sent. The `worktree`
26
+ has no cut in this Sandbox: `no cut made <path> in this Sandbox: cut it with worktree(); nothing
27
+ was sent`. Or the input names a `worktree` in a Workspace with no Repo Slot. Or the path has a
28
+ symlink in it, has `..`, or is absolute. Or the file is a FIFO, a directory, or a file with more
29
+ than one link.
27
30
 
28
31
  It refuses, when you call it:
29
32
  - a path that is not a string or a function: `file(path): the path is a string, or a function of
@@ -43,16 +46,17 @@ And at `jr2 up`, an Effect that no `workspace()` invokes: `workflow "<workflow>"
43
46
  there is no Worktree to read; invoke it from a workspace()'s body, or send a json or text body
44
47
  (ADR-0068)`.
45
48
 
49
+ And when it starts, under a Workspace with Repo Slots, an input with no `worktree`: ``http
50
+ "<slot>" reads a Worktree file, under a Workspace with Repo Slots, and its input names no
51
+ `worktree` ``.
52
+
46
53
  **Parameters**
47
54
 
48
- - `path` ([`FromInput`](type-frominput.md)): The file, relative to the Worktree of the Repo Slot, or to `/work` in a Workspace
49
- with no Repo Slot: a string, or a function of the input of the Effect that gives one.
55
+ - `path` ([`FromInput`](type-frominput.md)): The file, relative to the Worktree that `worktree` of the input names, or to
56
+ `/work` in a Workspace with no Repo Slot: a string, or a function of the input of the Effect
57
+ that gives one.
50
58
  - `options`: Optional. Each field says what it does.
51
59
 
52
- - `slot` (optional): `string`
53
-
54
- The Repo Slot whose Worktree holds the file. Write it only when the Workspace has more than
55
- one.
56
60
  - `segments` (optional): `FromInput<S, readonly string[]>`
57
61
 
58
62
  Path segments that jr2 adds to the end of the url, as `path` does in `request` for a json or
@@ -73,17 +77,22 @@ import {
73
77
  fail,
74
78
  file,
75
79
  http,
80
+ isReady,
76
81
  jr2Setup,
77
82
  secret,
78
83
  workspace,
84
+ worktree,
79
85
  type Workspaced,
80
86
  } from "@jr2/orchestrator";
81
87
 
82
88
  type Input = Workspaced<{}, "app">;
83
89
 
90
+ const reportTree = (context: Input) => `${context.workspace.repos.app}/report` as const;
91
+
84
92
  const body = jr2Setup({
85
93
  types: {} as { context: Input; input: Input },
86
94
  actors: {
95
+ cut: worktree(),
87
96
  build: command(["npm", "run", "report"]),
88
97
  upload: http({
89
98
  method: "PUT",
@@ -100,18 +109,29 @@ const body = jr2Setup({
100
109
  id: "report",
101
110
  context: ({ input }) => input,
102
111
  on: { "workspace.lost": ".lost" },
103
- initial: "building",
112
+ initial: "cutting",
104
113
  states: {
105
- building: { invoke: { src: "build", onDone: "uploading" } },
106
- uploading: { invoke: { src: "upload", onDone: "done" } },
114
+ cutting: {
115
+ invoke: {
116
+ src: "cut",
117
+ input: ({ context }) => ({ path: reportTree(context), branch: "report" }),
118
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "building" }, { target: "notCut" }],
119
+ },
120
+ },
121
+ building: {
122
+ invoke: { src: "build", input: ({ context }) => ({ worktree: reportTree(context) }), onDone: "uploading" },
123
+ },
124
+ uploading: {
125
+ invoke: { src: "upload", input: ({ context }) => ({ worktree: reportTree(context) }), onDone: "done" },
126
+ },
107
127
  done: { type: "final" },
128
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
108
129
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
109
130
  },
110
131
  });
111
132
 
112
133
  export const machine = workspace(body, {
113
134
  repos: { app: { url: "https://github.com/you/app.git" } },
114
- spec: () => ({ branch: "report" }),
115
135
  });
116
136
  ```
117
137
 
@@ -7,6 +7,8 @@ description: "Make an `http` Effect: a call to a URL, with Effect secrets that t
7
7
 
8
8
  Function, in `@jr2/orchestrator/src/http-effect.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Effects.
9
9
 
10
+ ## Overload 1
11
+
10
12
  ```ts
11
13
  function http<
12
14
  S extends z.ZodType | undefined = undefined,
@@ -14,8 +16,8 @@ function http<
14
16
  const A extends HttpDefinition<S>["auth"] = undefined,
15
17
  const H extends HttpDefinition<S>["headers"] = undefined,
16
18
  >(
17
- def: HttpDefinition<S> & { url: U; auth?: A; headers?: H },
18
- ): EffectLogic<HttpInput<S>, HttpOutput> & OpenOf<HttpOpenParts<U, A, H>>
19
+ def: HttpDefinition<S> & { url: U; auth?: A; headers?: H; body: FileBody<S> },
20
+ ): EffectLogic<HttpFileInput<S>, HttpOutput> & OpenOf<HttpOpenParts<U, A, H>>
19
21
  ```
20
22
 
21
23
  Make an `http` Effect: a call to a URL, with Effect secrets that the Agent never reads. Put it
@@ -23,8 +25,9 @@ in `actors`, and invoke it by its key: `actors: { comment: http({ ... }) }`, the
23
25
  src: "comment" }`.
24
26
 
25
27
  It runs in the Instance Effector, which `jr2 up` deploys when a Machine has an `http` Effect.
26
- With `body: file(...)`, it runs in the Sandbox Effector of the Workspace whose body invokes it.
27
- The Effector makes the call. The Orchestrator holds no secret.
28
+ With `body: file(...)`, it runs in the Sandbox Effector of the Workspace whose body invokes it,
29
+ and under Repo Slots its input names the Worktree in `worktree`. The Effector makes the call. The
30
+ Orchestrator holds no secret.
28
31
 
29
32
  Every outcome is output (`HttpOutput`): `response`, `unreachable`, `refused` or `unknown`. A 4xx
30
33
  or 5xx status is output too. jr2 sends the call at most one time, unless you mark it
@@ -91,12 +94,15 @@ customize() share: actors: { <name>: http(...) }, then invoke: { src: "<name>" }
91
94
  A function that makes the part of the request that changes for each run, from the input:
92
95
  `path` segments, and a `json` or `text` body. It cannot change the host, a header or an
93
96
  Effect secret. Do not write it beside `body`.
94
- - `body` (optional): `FileBody<S>`
97
+ - `body`: `FileBody<S>`
95
98
 
96
- A Worktree file as the body: `file(path, { slot?, segments?, contentType? })`. The Effect then
97
- runs in the Sandbox Effector of its Workspace. Do not write `request` beside it: the `segments`
99
+ A Worktree file as the body: `file(path, { segments?, contentType? })`. The Effect then
100
+ runs in the Sandbox Effector of its Workspace. Under a Workspace with Repo Slots, the invoke's
101
+ input names a Worktree that a cut of this run made, in `worktree`; with none, it refuses when
102
+ it starts: ``http "<slot>" reads a Worktree file, under a Workspace with Repo Slots, and its
103
+ input names no `worktree` ``. Do not write `request` beside it: the `segments`
98
104
  of `file` are the path. It refuses another body: ``http({ body }): a body is file(path, {
99
- slot?, segments?, contentType? }) — json and text come from `request` ``.
105
+ segments?, contentType? }) — json and text come from `request` ``.
100
106
  - `timeout` (optional): [`Duration`](type-duration.md)
101
107
 
102
108
  How long jr2 waits for the answer: a number and a unit, such as `"30s"` or `"2m"`. Default
@@ -157,3 +163,97 @@ export const machine = jr2Setup({
157
163
  - [Secrets and trust](../../concepts/secrets-and-trust.md)
158
164
  - [ADR-0068: An Effector holds an Effect's secrets and signs its wire](../decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)
159
165
  - [ADR-0067: An Effect is an actor slot, and runs at most once unless marked idempotent](../decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)
166
+
167
+ ## Overload 2
168
+
169
+ ```ts
170
+ function http<
171
+ S extends z.ZodType | undefined = undefined,
172
+ const U extends HttpDefinition<S>["url"] = string,
173
+ const A extends HttpDefinition<S>["auth"] = undefined,
174
+ const H extends HttpDefinition<S>["headers"] = undefined,
175
+ >(
176
+ def: HttpDefinition<S> & { url: U; auth?: A; headers?: H },
177
+ ): EffectLogic<HttpInput<S>, HttpOutput> & OpenOf<HttpOpenParts<U, A, H>>
178
+ ```
179
+
180
+ **Parameters**
181
+
182
+ - `def`
183
+
184
+ - `method`: [`HttpMethod`](type-httpmethod.md)
185
+
186
+ The HTTP method: `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` or `OPTIONS`. It refuses
187
+ another: `http({ method: "<method>" }): the method is one of GET, HEAD, POST, PUT, PATCH,
188
+ DELETE, OPTIONS`.
189
+ - `url`: `string | Text | typeof open`
190
+
191
+ The url: `http://` or `https://`, then a literal host. No input, param or Effect secret can
192
+ change where the call goes. An Effect secret can be only in the path:
193
+ `` text`https://api.telegram.org/bot${secret("tg")}/sendMessage` ``.
194
+
195
+ Or `open`: a packaged Machine that cannot know the host leaves the url Open, and the composer
196
+ binds it one time with `customize(machine, { effects: { <slot>: { url } } })`.
197
+
198
+ It refuses:
199
+ - a scheme or host that is not literal: `` http({ url }): the url is an http:// or https:// url
200
+ whose scheme and host are literal — a secret may sit only in its path, after a "/":
201
+ text`https://api.example.com/bot${secret("tg")}/send` (ADR-0068) ``
202
+ - an Effect secret after `?` or `#`: `` http({ url }): secret("<name>") sits in the url's query
203
+ or fragment — a secret may sit only in its path; send it in `auth` or a header instead:
204
+ headers: { "x-api-key": text`${secret("...")}` } (ADR-0068) ``
205
+ - a host that is not a host: `http({ url }): "<scheme>://<host>" is not a host`
206
+ - a user name or password in the url: ``http({ url }): the url carries userinfo — use `auth` ``
207
+ - `headers` (optional): `Readonly<Record<string, string | Text>>`
208
+
209
+ Headers that are the same for each run. Each name is lowercase. A value is a string, or a
210
+ `text` with an Effect secret.
211
+
212
+ It refuses:
213
+ - a name that is not lowercase, or that frames or routes the request (`host`,
214
+ `content-length`, `transfer-encoding`, `connection`, `upgrade`, `te`, `trailer`,
215
+ `keep-alive`, `proxy-*`): `http({ headers }): "<name>" is not a header an Effect sets — a
216
+ lowercase name that does not frame or route the request (ADR-0068)`
217
+ - an `authorization` header beside `auth`: ``http({ headers }): "authorization" is set by
218
+ `auth` — write one of them``
219
+ - `auth` (optional): `{ readonly bearer: Secret; } | { readonly basic: { user: Text; password: Text; }; } | Text`
220
+
221
+ The `Authorization` header: `bearer(secret("x"))`, `basic(user, password)`, or a `text` that
222
+ is the whole value.
223
+ - `input` (optional): `S`
224
+
225
+ The zod schema of the input of the Effect: what the `input` of the `invoke`, or `fromPick`,
226
+ gives it. jr2 parses the input with it before the call. When it starts, input that does not
227
+ agree with it makes the Effect fail (its `onError` route): `http "<slot>": its input does not
228
+ match the definition's input schema — <path>: <message>`, one `<path>: <message>` for each
229
+ problem, joined by `; `.
230
+ - `request` (optional): `(input: S extends z.ZodType ? z.infer<S> : void) => HttpRequestParts`
231
+
232
+ A function that makes the part of the request that changes for each run, from the input:
233
+ `path` segments, and a `json` or `text` body. It cannot change the host, a header or an
234
+ Effect secret. Do not write it beside `body`.
235
+ - `body` (optional): `FileBody<S>`
236
+
237
+ A Worktree file as the body: `file(path, { segments?, contentType? })`. The Effect then
238
+ runs in the Sandbox Effector of its Workspace. Under a Workspace with Repo Slots, the invoke's
239
+ input names a Worktree that a cut of this run made, in `worktree`; with none, it refuses when
240
+ it starts: ``http "<slot>" reads a Worktree file, under a Workspace with Repo Slots, and its
241
+ input names no `worktree` ``. Do not write `request` beside it: the `segments`
242
+ of `file` are the path. It refuses another body: ``http({ body }): a body is file(path, {
243
+ segments?, contentType? }) — json and text come from `request` ``.
244
+ - `timeout` (optional): [`Duration`](type-duration.md)
245
+
246
+ How long jr2 waits for the answer: a number and a unit, such as `"30s"` or `"2m"`. Default
247
+ `"30s"`. With no answer in that time, the output is `unknown`. It refuses a value that is not
248
+ a positive duration: `http: timeout "<timeout>" is not a positive duration (like "30s",
249
+ "2m")`.
250
+ - `maxBody` (optional): `number`
251
+
252
+ The largest number of bytes of a text response body that the output keeps. Default 65536.
253
+ When jr2 cuts the body, `truncated` is `true`. It refuses a value that is not a positive whole
254
+ number: `http({ maxBody }): a positive number of bytes`.
255
+ - `idempotent` (optional): `boolean`
256
+
257
+ `true` when it is safe to send the call two times: it has its own idempotency key, or it only
258
+ reads. Default `false`. After an Orchestrator restart, jr2 cannot always tell if the call went
259
+ out. Then it sends an idempotent call again, and gives any other call the output `unknown`.
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: isReady
3
+ description: Is it `ready`?
4
+ ---
5
+
6
+ # `isReady`
7
+
8
+ Constant, in `@jr2/orchestrator/src/worktree.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Effects.
9
+
10
+ ```ts
11
+ const isReady: <O extends { outcome: string; }>(output: O) => output is Extract<O, { outcome: "ready"; }>
12
+ ```
13
+
14
+ Is it `ready`? For a guard. A Chart's form is the guard `jr2.ready`.
@@ -60,7 +60,7 @@ function jr2Setup<
60
60
  ): SetupReturn<
61
61
  TContext,
62
62
  WorkflowEvent<TDefs>,
63
- MergedActors<TActors>,
63
+ MergedActors<BindActors<TActors, TInput, TContext>>,
64
64
  {},
65
65
  TActions,
66
66
  TGuards & SentByGuards,
@@ -28,7 +28,18 @@ Open part.
28
28
 
29
29
  ```ts
30
30
  import { z } from "zod";
31
- import { agent, customize, defineEvent, fail, jr2Setup, open, workspace } from "@jr2/orchestrator";
31
+ import {
32
+ agent,
33
+ customize,
34
+ defineEvent,
35
+ fail,
36
+ isReady,
37
+ jr2Setup,
38
+ open,
39
+ workspace,
40
+ worktree,
41
+ type Workspaced,
42
+ } from "@jr2/orchestrator";
32
43
 
33
44
  const finish = defineEvent({
34
45
  name: "finish",
@@ -36,19 +47,40 @@ const finish = defineEvent({
36
47
  input: z.object({}),
37
48
  });
38
49
 
50
+ type Input = Workspaced<{}, "app">;
51
+
52
+ const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
53
+
39
54
  const body = jr2Setup({
55
+ types: {} as { context: Input; input: Input },
40
56
  events: [finish],
41
- actors: { coder: agent({ model: open, instructions: "You fix the failing test." }), fail: fail() },
57
+ actors: {
58
+ coder: agent({ model: open, instructions: "You fix the failing test." }),
59
+ cut: worktree(),
60
+ fail: fail(),
61
+ },
42
62
  }).createMachine({
43
63
  id: "fix",
64
+ context: ({ input }) => input,
44
65
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
45
- initial: "coding",
66
+ initial: "cutting",
46
67
  states: {
68
+ cutting: {
69
+ invoke: {
70
+ src: "cut",
71
+ input: ({ context }) => ({ path: fixTree(context), branch: "jr2/fix" }),
72
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
73
+ },
74
+ },
47
75
  coding: {
48
- invoke: { src: "coder", input: { prompt: "Make `node --test` pass." } },
76
+ invoke: {
77
+ src: "coder",
78
+ input: ({ context }) => ({ prompt: "Make `node --test` pass.", cwd: fixTree(context) }),
79
+ },
49
80
  on: { finish: "done" },
50
81
  },
51
82
  done: { type: "final" },
83
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
52
84
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
53
85
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
54
86
  },
@@ -57,7 +89,6 @@ const body = jr2Setup({
57
89
  // What a package exports: the repository and the model are Open.
58
90
  export const fix = workspace(body, {
59
91
  repos: { app: open },
60
- spec: () => ({ branch: "jr2/fix" }),
61
92
  });
62
93
 
63
94
  // What an Instance's `workflows/fix.ts` exports: the same Machine, its Open parts bound.
@@ -50,9 +50,11 @@ import {
50
50
  command,
51
51
  defineEvent,
52
52
  fail,
53
+ isReady,
53
54
  jr2Setup,
54
55
  paramsOf,
55
56
  workspace,
57
+ worktree,
56
58
  type Workspaced,
57
59
  } from "@jr2/orchestrator";
58
60
 
@@ -70,6 +72,8 @@ const finished = defineEvent({
70
72
 
71
73
  type Input = Workspaced<{}, "app">;
72
74
 
75
+ const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
76
+
73
77
  const body = jr2Setup({
74
78
  types: {} as { context: Input; input: Input },
75
79
  events: [runTest, finished],
@@ -78,6 +82,7 @@ const body = jr2Setup({
78
82
  model: "anthropic/claude-haiku-5-5",
79
83
  instructions: "You are a careful engineer.",
80
84
  }),
85
+ cut: worktree(),
81
86
  test: command(["node", "--test", arg("file")]),
82
87
  fail: fail(),
83
88
  },
@@ -85,12 +90,22 @@ const body = jr2Setup({
85
90
  id: "fix-tests",
86
91
  context: ({ input }) => input,
87
92
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
88
- initial: "coding",
93
+ initial: "cutting",
89
94
  states: {
95
+ cutting: {
96
+ invoke: {
97
+ src: "cut",
98
+ input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
99
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
100
+ },
101
+ },
90
102
  coding: {
91
103
  invoke: {
92
104
  src: "coder",
93
- input: { prompt: "Make the tests pass. Run the tests of one module at a time." },
105
+ input: ({ context }) => ({
106
+ prompt: "Make the tests pass. Run the tests of one module at a time.",
107
+ cwd: fixTree(context),
108
+ }),
94
109
  },
95
110
  on: { finished: "done" },
96
111
  initial: "ready",
@@ -100,13 +115,17 @@ const body = jr2Setup({
100
115
  invoke: {
101
116
  src: "test",
102
117
  // A plain mapper: the Machine makes the file name from the pick's `module`.
103
- input: ({ event }) => ({ file: `test/${paramsOf(event, runTest).module}.test.js` }),
118
+ input: ({ context, event }) => ({
119
+ file: `test/${paramsOf(event, runTest).module}.test.js`,
120
+ worktree: fixTree(context),
121
+ }),
104
122
  onDone: "ready",
105
123
  },
106
124
  },
107
125
  },
108
126
  },
109
127
  done: { type: "final" },
128
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
110
129
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
111
130
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
112
131
  },
@@ -114,7 +133,6 @@ const body = jr2Setup({
114
133
 
115
134
  export const machine = workspace(body, {
116
135
  repos: { app: { url: "https://github.com/you/app.git" } },
117
- spec: () => ({ branch: "fix-tests" }),
118
136
  });
119
137
  ```
120
138
 
@@ -51,7 +51,7 @@ its own.
51
51
  The run input of the Pool, as a zod object: what a caller sends to start a run. `cap` and
52
52
  `itemInput` read it, typed by it. A worker never gets it: each worker gets its item. Default:
53
53
  none, and then their `input` is `unknown`.
54
- - `source`: `SourceSpec<T>`
54
+ - `source`: `SourceSpec<T, TInput> | SourceSpec<T>`
55
55
 
56
56
  Where the items come from. Make it with [`source`](source.md).
57
57
  - `itemId`: `(item: T) => string`
@@ -83,7 +83,7 @@ its own.
83
83
 
84
84
  ```ts
85
85
  import { fromPromise } from "xstate";
86
- import { customize, pool, source } from "@jr2/orchestrator";
86
+ import { customize, pool, source, type PoolNextInput } from "@jr2/orchestrator";
87
87
  import { task } from "@jr2/machines";
88
88
 
89
89
  type Issue = { id: string; title: string };
@@ -94,7 +94,7 @@ const backlog: Issue[] = [
94
94
  ];
95
95
 
96
96
  const issues = source<Issue>({
97
- next: fromPromise(async ({ input }: { input: { active: string[] } }) => {
97
+ next: fromPromise(async ({ input }: { input: PoolNextInput }) => {
98
98
  const issue = backlog.find((i) => !input.active.includes(i.id)) ?? null;
99
99
  if (issue) backlog.splice(backlog.indexOf(issue), 1);
100
100
  return issue;
@@ -151,7 +151,7 @@ an example.
151
151
  The run input of the Pool, as a zod object: what a caller sends to start a run. `cap` and
152
152
  `itemInput` read it, typed by it. A worker never gets it: each worker gets its item. Default:
153
153
  none, and then their `input` is `unknown`.
154
- - `source`: `SourceSpec<T>`
154
+ - `source`: `SourceSpec<T, TInput> | SourceSpec<T>`
155
155
 
156
156
  Where the items come from. Make it with [`source`](source.md).
157
157
  - `itemId`: `(item: T) => string`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: push
3
- description: "Make a `push` Effect: it pushes the branch of the Workspace to the Binding of its Repo Slot."
3
+ description: "Make a `push` Effect: it pushes the branch of a Worktree to the Binding of its Repo Slot."
4
4
  ---
5
5
 
6
6
  # `push`
@@ -8,17 +8,16 @@ description: "Make a `push` Effect: it pushes the branch of the Workspace to the
8
8
  Function, in `@jr2/orchestrator/src/sandbox-effect.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Effects.
9
9
 
10
10
  ```ts
11
- function push<
12
- const O extends PushOptions,
13
- >(
14
- options?: O,
15
- ): EffectLogic<PushInput<O> | void, PushOutput>
11
+ function push<const O extends PushOptions>(options?: O): EffectLogic<PushInput<O>, PushOutput>
16
12
  ```
17
13
 
18
- Make a `push` Effect: it pushes the branch of the Workspace to the Binding of its Repo Slot.
14
+ Make a `push` Effect: it pushes the branch of a Worktree to the Binding of its Repo Slot.
19
15
  Put it in `actors`, and invoke it from the body of a `workspace()`: `actors: { publish: push({
20
16
  sign: true }) }`.
21
17
 
18
+ Its input names the Worktree: `{ worktree }`. It has no branch. jr2 reads the branch from the
19
+ `worktree()` cut of that path, so an Agent that moves `HEAD` does not change what jr2 pushes.
20
+
22
21
  It runs in the Sandbox Effector of the Workspace, with the push credential of the
23
22
  `git.credentials` entry that the Binding matches. The Agent never reads that credential. It
24
23
  pushes one commit: the tip of the Agent's branch when the Effect starts, or, with `pin`, the
@@ -26,8 +25,10 @@ commit that the input names. After an Orchestrator restart, jr2 pushes only that
26
25
  again. With `sign` or `squash`, the commit on the remote is not the Agent's own. Then jr2 moves
27
26
  the Agent's branch to it.
28
27
 
29
- Every outcome is output (`PushOutput`): `pushed`, `rejected`, `diverged` or `unknown`. Each
30
- field of [`PushOptions`](type-pushoptions.md) tells what it refuses. When it starts, it also refuses a push
28
+ Every outcome is output (`PushOutput`): `pushed`, `rejected`, `diverged` or `unknown`. A
29
+ `worktree` that no cut made in this Sandbox, a Worktree of another Repo Slot, and a detached
30
+ Worktree are `rejected` at `stage` `start`, and nothing is pushed. Each field of
31
+ [`PushOptions`](type-pushoptions.md) tells what it refuses. When it starts, it also refuses a push
31
32
  outside a Workspace: `push "<slot>" runs in the Sandbox Effector of a Workspace, and its state is
32
33
  not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`.
33
34
 
@@ -45,9 +46,11 @@ import {
45
46
  agent,
46
47
  defineEvent,
47
48
  fail,
49
+ isReady,
48
50
  jr2Setup,
49
51
  push,
50
52
  workspace,
53
+ worktree,
51
54
  type Workspaced,
52
55
  } from "@jr2/orchestrator";
53
56
 
@@ -59,6 +62,8 @@ const committed = defineEvent({
59
62
 
60
63
  type Input = Workspaced<{}, "app">;
61
64
 
65
+ const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
66
+
62
67
  const body = jr2Setup({
63
68
  types: {} as { context: Input; input: Input },
64
69
  events: [committed],
@@ -67,6 +72,7 @@ const body = jr2Setup({
67
72
  model: "anthropic/claude-haiku-5-5",
68
73
  instructions: "You are a careful engineer.",
69
74
  }),
75
+ cut: worktree(),
70
76
  publish: push({ sign: true }),
71
77
  fail: fail(),
72
78
  },
@@ -74,10 +80,20 @@ const body = jr2Setup({
74
80
  id: "fix-test",
75
81
  context: ({ input }) => input,
76
82
  on: { "workspace.lost": ".failed", "agent.fault": ".failed" },
77
- initial: "coding",
83
+ initial: "cutting",
78
84
  states: {
85
+ cutting: {
86
+ invoke: {
87
+ src: "cut",
88
+ input: ({ context }) => ({ path: fixTree(context), branch: "fix-test" }),
89
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
90
+ },
91
+ },
79
92
  coding: {
80
- invoke: { src: "coder", input: { prompt: "Fix the failing test, and commit the fix." } },
93
+ invoke: {
94
+ src: "coder",
95
+ input: ({ context }) => ({ prompt: "Fix the failing test, and commit the fix.", cwd: fixTree(context) }),
96
+ },
81
97
  // The Agent moves the Machine on with a pick. An Agent's actor never ends, so `onDone`
82
98
  // never fires.
83
99
  on: { committed: "publishing" },
@@ -85,6 +101,8 @@ const body = jr2Setup({
85
101
  publishing: {
86
102
  invoke: {
87
103
  src: "publish",
104
+ // jr2 reads the branch from the cut of this Worktree.
105
+ input: ({ context }) => ({ worktree: fixTree(context) }),
88
106
  onDone: [
89
107
  { guard: ({ event }) => event.output.outcome === "pushed", target: "done" },
90
108
  { target: "failed" },
@@ -92,6 +110,7 @@ const body = jr2Setup({
92
110
  },
93
111
  },
94
112
  done: { type: "final" },
113
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
95
114
  // Not a final state: the run ends `failed`, and the Workspace deletes the Sandbox.
96
115
  failed: { invoke: { src: "fail", input: { reason: "the fix was not pushed" } } },
97
116
  },
@@ -99,7 +118,6 @@ const body = jr2Setup({
99
118
 
100
119
  export const machine = workspace(body, {
101
120
  repos: { app: { url: "https://github.com/you/app.git" } },
102
- spec: () => ({ branch: "fix-test" }),
103
121
  });
104
122
  ```
105
123
 
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: readyOf
3
+ description: The `ready` output of an `onDone` arm that `isReady` guards.
4
+ ---
5
+
6
+ # `readyOf`
7
+
8
+ Function, in `@jr2/orchestrator/src/worktree.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Effects.
9
+
10
+ ```ts
11
+ function readyOf<O extends { outcome: string }>(output: O): Extract<O, { outcome: "ready" }>
12
+ ```
13
+
14
+ The `ready` output of an `onDone` arm that `isReady` guards. xstate carries no guard's narrowing
15
+ to its actions, so an `assign` reads the output through this.
16
+
17
+ **Parameters**
18
+
19
+ - `output`
20
+
21
+ - `outcome`: `string`
@@ -56,13 +56,25 @@ And at `jr2 up`:
56
56
  **Example**
57
57
 
58
58
  ```ts
59
- import { command, fail, jr2Setup, secret, workspace, type Workspaced } from "@jr2/orchestrator";
59
+ import {
60
+ command,
61
+ fail,
62
+ isReady,
63
+ jr2Setup,
64
+ secret,
65
+ workspace,
66
+ worktree,
67
+ type Workspaced,
68
+ } from "@jr2/orchestrator";
60
69
 
61
70
  type Input = Workspaced<{}, "app">;
62
71
 
72
+ const deployTree = (context: Input) => `${context.workspace.repos.app}/deploy` as const;
73
+
63
74
  const body = jr2Setup({
64
75
  types: {} as { context: Input; input: Input },
65
76
  actors: {
77
+ cut: worktree(),
66
78
  // The command reads the file at $KUBECONFIG. The Agent never reads the secret.
67
79
  deploy: command(["kubectl", "apply", "-f", "k8s/"], {
68
80
  env: { KUBECONFIG: secret("kube").path },
@@ -73,17 +85,26 @@ const body = jr2Setup({
73
85
  id: "deploy",
74
86
  context: ({ input }) => input,
75
87
  on: { "workspace.lost": ".lost" },
76
- initial: "deploying",
88
+ initial: "cutting",
77
89
  states: {
78
- deploying: { invoke: { src: "deploy", onDone: "done" } },
90
+ cutting: {
91
+ invoke: {
92
+ src: "cut",
93
+ input: ({ context }) => ({ path: deployTree(context), branch: "deploy" }),
94
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "deploying" }, { target: "notCut" }],
95
+ },
96
+ },
97
+ deploying: {
98
+ invoke: { src: "deploy", input: ({ context }) => ({ worktree: deployTree(context) }), onDone: "done" },
99
+ },
79
100
  done: { type: "final" },
101
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
80
102
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
81
103
  },
82
104
  });
83
105
 
84
106
  export const machine = workspace(body, {
85
107
  repos: { app: { url: "https://github.com/you/app.git" } },
86
- spec: () => ({ branch: "deploy" }),
87
108
  });
88
109
  ```
89
110