@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
@@ -10,23 +10,18 @@ Type, in `@jr2/orchestrator/src/workspace.ts`. The signature of an export, or a
10
10
  ```ts
11
11
  type PermissiveWorkspaceOptions<TSlots extends string, TInput = unknown> = SandboxOptions<TSlots, TInput> & {
12
12
  input?: never;
13
- spec: (args: { input: TInput }) => WorkspaceSpec;
14
13
  };
15
14
  ```
16
15
 
17
- The options of a `workspace()` with no `input`: it starts with any input. The `input` of `spec`
18
- is then `unknown`, and `spec` must check it before it reads a field.
16
+ The options of a `workspace()` with no `input`: it starts with any input. The `input` of a
17
+ per-run Repo Slot is then `unknown`, and the mapper must check it before it reads a field.
19
18
 
20
- Use it for a Workspace that a Pool or another Machine starts. Give the parameter of `spec` a
21
- type to say what that input is: `spec: ({ input }: { input: Item }) => …`. That also types the
22
- input of the Workspace. For a Workspace that a caller starts, declare `input`.
19
+ Use it for a Workspace that a Pool or another Machine starts. The body's `input` type says what
20
+ that input is, and that also types the input of the Workspace. For a Workspace that a caller
21
+ starts, declare `input`.
23
22
 
24
23
  **Fields**: each field of [`SandboxOptions`](type-sandboxoptions.md), and these:
25
24
 
26
25
  - `input` (optional): `never`
27
26
 
28
27
  Not present: a `workspace()` without `input` starts with any input.
29
- - `spec`: `(args: { input: TInput; }) => WorkspaceSpec`
30
-
31
- A function from the input to the Workspace's [`WorkspaceSpec`](type-workspacespec.md). Its `input` is
32
- `unknown` unless you give the parameter a type: `spec: ({ input }: { input: Item }) => …`.
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: PoolNextInput
3
+ description: "The input of a Source's `next`: the ids of the items that workers have now, the ids of the items that settled, and the Pool's input."
4
+ ---
5
+
6
+ # `PoolNextInput`
7
+
8
+ Type, in `@jr2/orchestrator/src/pool.ts`. It is an export of `@jr2/orchestrator`, on the [Authoring surface](../authoring-surface.md) page, in Composition.
9
+
10
+ ```ts
11
+ type PoolNextInput<TInput = unknown> = { active: string[]; settled: string[]; input: TInput };
12
+ ```
13
+
14
+ The input of a Source's `next`: the ids of the items that workers have now, the ids of the items
15
+ that settled, and the Pool's input. So a Source over a list that a Machine holds needs no state
16
+ of its own.
17
+
18
+ **Fields**
19
+
20
+ - `active`: `string[]`
21
+ - `settled`: `string[]`
22
+ - `input`: `TInput`
@@ -11,7 +11,7 @@ Type, in `@jr2/orchestrator/src/pool.ts`. The signature of an export, or a type
11
11
  type PoolSpec<T, TInput = unknown> = {
12
12
  id?: string;
13
13
  input?: z.ZodObject;
14
- source: SourceSpec<T>;
14
+ source: SourceSpec<T, TInput> | SourceSpec<T>;
15
15
  itemId: (item: T) => string;
16
16
  cap?: number | ((args: { input: TInput }) => number);
17
17
  itemInput?: (item: T, args: { input: TInput }) => unknown;
@@ -33,7 +33,7 @@ each worker gets.
33
33
  The run input of the Pool, as a zod object: what a caller sends to start a run. `cap` and
34
34
  `itemInput` read it, typed by it. A worker never gets it: each worker gets its item. Default:
35
35
  none, and then their `input` is `unknown`.
36
- - `source`: `SourceSpec<T>`
36
+ - `source`: `SourceSpec<T, TInput> | SourceSpec<T>`
37
37
 
38
38
  Where the items come from. Make it with [`source`](source.md).
39
39
  - `itemId`: `(item: T) => string`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: PushInput
3
- description: "The input of a `push` Effect: `{ sha }` with `pin`, and `{ message }` with `squash`."
3
+ description: "The input of a `push` Effect: the Worktree that a cut made, `{ sha }` with `pin`, and `{ message }` with `squash`."
4
4
  ---
5
5
 
6
6
  # `PushInput`
@@ -8,8 +8,27 @@ description: "The input of a `push` Effect: `{ sha }` with `pin`, and `{ message
8
8
  Type, in `@jr2/orchestrator/src/sandbox-effect.ts`. The signature of an export, or a type that it names, names this type. The [Authoring surface](../authoring-surface.md) page lists each export. This type is not an export of `@jr2/orchestrator`, so a workflow module does not import it by name.
9
9
 
10
10
  ```ts
11
- type PushInput<O extends PushOptions> = (O["pin"] extends true ? { sha: string } : unknown) &
11
+ type PushInput<O extends PushOptions> = {
12
+ worktree: O extends { slot: infer S extends string } ? `/work/${S}/${string}` : AnyWorktreePath;
13
+ branch?: never;
14
+ to?: never;
15
+ cwd?: never;
16
+ path?: never;
17
+ } & (O["pin"] extends true ? { sha: string } : unknown) &
12
18
  (O["squash"] extends true ? { message: string } : unknown);
13
19
  ```
14
20
 
15
- The input of a `push` Effect: `{ sha }` with `pin`, and `{ message }` with `squash`.
21
+ The input of a `push` Effect: the Worktree that a cut made, `{ sha }` with `pin`, and
22
+ `{ message }` with `squash`. It names no branch: jr2 reads it from the cut's record, so no word of
23
+ a Chart reaches a remote branch. The other keys are refused by name, because each is a key of a
24
+ cut or a Turn, and a mapper's excess key is no compile error otherwise.
25
+
26
+ **Fields**
27
+
28
+ - `worktree`: ``O extends { slot: infer S extends string; } ? `/work/${S}/${string}` : AnyWorktreePath``
29
+
30
+ The Worktree to push: `event.output.path` of its cut, of the push's Repo Slot.
31
+ - `branch` (optional): `never`
32
+ - `to` (optional): `never`
33
+ - `cwd` (optional): `never`
34
+ - `path` (optional): `never`
@@ -10,7 +10,8 @@ Type, in `@jr2/orchestrator/src/sandbox-effect.ts`. It is an export of `@jr2/orc
10
10
  ```ts
11
11
  type PushOutput =
12
12
  | Exclude<PushWireOutput, { outcome: "pushed" }>
13
- | (Omit<Extract<PushWireOutput, { outcome: "pushed" }>, "bundle"> & { sync: SyncState; syncReason?: string });
13
+ | (Omit<Extract<PushWireOutput, { outcome: "pushed" }>, "bundle"> & { sync: SyncState; syncReason?: string })
14
+ | LaterOutcome;
14
15
  ```
15
16
 
16
17
  The output of a `push` Effect: what the `onDone` of its state gets as `event.output`. Every
@@ -40,6 +41,8 @@ Beside `pushed`, `sync` tells what jr2 did to the Agent's branch after the push:
40
41
 
41
42
  With `busy` or `not-synced`, `syncReason` tells why, and jr2 tries again before the next push.
42
43
 
44
+ The union is open (`LaterOutcome`): give the rest a last arm with no guard.
45
+
43
46
  **Forms**: it is one of these objects.
44
47
 
45
48
  - Form 1: `outcome`, `stage`, `reason`, `detail`
@@ -70,13 +73,28 @@ With `busy` or `not-synced`, `syncReason` tells why, and jr2 tries again before
70
73
  - `squashed` (optional): `"signed" | "unsigned"`
71
74
  - `sync`: [`SyncState`](type-syncstate.md)
72
75
  - `syncReason` (optional): `string`
76
+ - Form 5: `outcome`, `reason`
77
+
78
+ - `outcome`: `"(a later outcome)"`
79
+ - `reason` (optional): `string`
73
80
 
74
81
  **Example**
75
82
 
76
83
  ```ts
77
- import { fail, jr2Setup, push, type PushOutput, workspace, type Workspaced } from "@jr2/orchestrator";
84
+ import {
85
+ fail,
86
+ isReady,
87
+ jr2Setup,
88
+ push,
89
+ type PushOutput,
90
+ workspace,
91
+ worktree,
92
+ type Workspaced,
93
+ } from "@jr2/orchestrator";
94
+
95
+ type Input = Workspaced<{ branch: string; sha: string }, "app">;
78
96
 
79
- type Input = Workspaced<{ sha: string }, "app">;
97
+ const releaseTree = (context: Input) => `${context.workspace.repos.app}/release` as const;
80
98
 
81
99
  // A push that did not happen (`rejected` or `diverged`) is a different problem from one jr2
82
100
  // cannot see (`unknown`).
@@ -85,17 +103,24 @@ const notPushed = (output: PushOutput): boolean =>
85
103
 
86
104
  const body = jr2Setup({
87
105
  types: {} as { context: Input; input: Input },
88
- actors: { publish: push({ pin: true }), fail: fail() },
106
+ actors: { cut: worktree({ expect: "existing" }), publish: push({ pin: true }), fail: fail() },
89
107
  }).createMachine({
90
108
  id: "release",
91
109
  context: ({ input }) => input,
92
110
  on: { "workspace.lost": ".lost" },
93
- initial: "publishing",
111
+ initial: "cutting",
94
112
  states: {
113
+ cutting: {
114
+ invoke: {
115
+ src: "cut",
116
+ input: ({ context }) => ({ path: releaseTree(context), branch: context.branch }),
117
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "publishing" }, { target: "notCut" }],
118
+ },
119
+ },
95
120
  publishing: {
96
121
  invoke: {
97
122
  src: "publish",
98
- input: ({ context }) => ({ sha: context.sha }),
123
+ input: ({ context }) => ({ worktree: releaseTree(context), sha: context.sha }),
99
124
  onDone: [
100
125
  { guard: ({ event }) => event.output.outcome === "pushed", target: "published" },
101
126
  { guard: ({ event }) => notPushed(event.output), target: "notPushed" },
@@ -107,13 +132,13 @@ const body = jr2Setup({
107
132
  // Not final states: each one fails the run, with its own reason.
108
133
  notPushed: { invoke: { src: "fail", input: { reason: "the remote refused the push" } } },
109
134
  unknown: { invoke: { src: "fail", input: { reason: "jr2 cannot tell if the push happened" } } },
135
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
110
136
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
111
137
  },
112
138
  });
113
139
 
114
140
  export const machine = workspace(body, {
115
141
  repos: { app: { url: "https://github.com/you/app.git" } },
116
- spec: () => ({ branch: "release" }),
117
142
  });
118
143
  ```
119
144
 
@@ -24,10 +24,11 @@ The value of one Repo Slot in `workspace({ repos })`. One of:
24
24
  - a function of the run input, `({ input }) => url` or a [`Binding`](type-binding.md): each run chooses. The
25
25
  url must match an entry of `git.credentials` in `jr2.config.ts`, or the run cannot use it.
26
26
 
27
- The key of the slot is a directory name under `/work`. It starts with a letter, and
28
- `workspace()` and `customize()` refuse a key that does not: ``<where>: Repo Slot key "<key>" is
29
- not a directory name — a slot becomes `/work/<slot>` and its order matters, so it starts with a
30
- letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).`` They refuse a value of another
27
+ The key of the slot is a directory name under `/work`. It starts with a letter, so never with
28
+ `.`: `/work/.jr2/` is reserved for jr2. `workspace()` and `customize()` refuse a key that does
29
+ not: ``<where>: Repo Slot key "<key>" is not a directory name — a slot becomes `/work/<slot>`,
30
+ `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter and matches
31
+ /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).`` They refuse a value of another
31
32
  shape: ``<where>: Repo Slot "<slot>" is <what> — a slot is `open`, a url, `{ url, ref? }`, or a
32
33
  mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
33
34
 
@@ -58,7 +59,6 @@ export const deploy = workspace(body, {
58
59
  docs: "https://github.com/you/docs.git",
59
60
  config: open,
60
61
  },
61
- spec: ({ input }) => ({ branch: `deploy/${input.service}` }),
62
62
  });
63
63
  ```
64
64
 
@@ -10,6 +10,7 @@ Type, in `@jr2/orchestrator/src/workspace.ts`. The signature of an export, or a
10
10
  ```ts
11
11
  type SandboxOptions<TSlots extends string, TInput = unknown> = {
12
12
  image?: string;
13
+ workGroup?: number;
13
14
  user?: string | UserContainer;
14
15
  resources?: Size;
15
16
  repos: Record<TSlots, RepoSlot<TInput>> | typeof open;
@@ -30,6 +31,9 @@ clones. They are the same for each run.
30
31
  (`import.meta.resolve("./image")`), which `jr2 up` builds, or a registry ref. Its floor is
31
32
  glibc and git; jr2 adds its own runtime at `/opt/jr2`. Default: the Instance's
32
33
  `images/default`, then jr2's Harness image.
34
+ - `workGroup` (optional): `number`
35
+
36
+ The group id that owns the files in `/work`. Default: 2000. A non-negative integer.
33
37
  - `user` (optional): `string | UserContainer`
34
38
 
35
39
  The User Container: a third container in the Sandbox, for a service that must run beside the
@@ -52,8 +56,8 @@ clones. They are the same for each run.
52
56
  choice. The name is the key of `workspace.repos` in the body and the directory under `/work`.
53
57
  At least one slot. Leave `repos` out for a Workspace with no Repo Slot (see
54
58
  [`NoSlotWorkspaceOptions`](type-noslotworkspaceoptions.md)). Each value is one of:
55
- - a url, or `{ url, ref }`, where `ref` is the branch or commit that the Worktree starts from
56
- (default: the Repo's default branch);
59
+ - a url, or `{ url, ref }`, where `ref` is the branch, the tag or the commit that the clone
60
+ stays on, and that a new branch starts from (default: the Repo's default branch);
57
61
  - `open`, for the Instance that registers the Machine to bind with `customize()`;
58
62
  - a function of the run input, `({ input }) => url` or `{ url, ref }`, when each run chooses.
59
63
 
@@ -63,8 +67,11 @@ clones. They are the same for each run.
63
67
 
64
68
  The Held secrets that this Sandbox can use: names from the `secrets` list in `jr2.config.ts`.
65
69
  The Sandbox's Custodian holds each value and puts it into requests to the hosts of that Held
66
- secret. The Agent sees only a Stand-in. You need not list the model key of an Agent that runs
67
- here: its provider claims it. Each name once.
70
+ secret. Listing a name also gives its Stand-in to the Agent's programs — each `bash` call, test,
71
+ build and `command` — in their env under that name, so code in `/work` can spend it ([ADR-0076](../decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)).
72
+ You need not list the model key of an Agent that runs here: its provider claims it, and the
73
+ Harness gives it to the model calls alone. List the model key only when code in `/work` must
74
+ call the model itself: such a call is outside the Turn's usage record. Each name once.
68
75
  - `effector` (optional): `object`
69
76
 
70
77
  The image of the Sandbox Effector. This container runs three kinds of Effect for this
@@ -9,9 +9,7 @@ Type, in `@jr2/orchestrator/src/workspace.ts`. The signature of an export, or a
9
9
 
10
10
  ```ts
11
11
  type SlotHandles<TSlots extends string> = {
12
- repos: Record<TSlots, string>;
13
- branch: string;
14
- review?: Partial<Record<TSlots, string>>;
12
+ repos: { [K in TSlots]: `/work/${K}` };
15
13
  };
16
14
  ```
17
15
 
@@ -19,14 +17,8 @@ The handles of a Workspace that declares Repo Slots. @internal
19
17
 
20
18
  **Fields**
21
19
 
22
- - `repos`: `Record<TSlots, string>`
20
+ - `repos`: ``{ [K in TSlots]: `/work/${K}`; }``
23
21
 
24
- The absolute path of each Repo Slot's Worktree, `/work/<slot>/<branch>`, in the order that
25
- `repos` declares the slots. Give one to an Agent's Turn as its `cwd`.
26
- - `branch`: `string`
27
-
28
- The branch that the Workspace works on: the `branch` of the spec.
29
- - `review` (optional): `Partial<Record<TSlots, string>>`
30
-
31
- The path of each Repo Slot's review Worktree. Present only when the spec has `reviewSha`.
32
- Give one to a reviewer's Turn as its `cwd`.
22
+ The folder of each Repo Slot, `/work/<slot>`, in the order that `repos` declares the slots.
23
+ `${handle}/default` is the clone, on the Binding's base. Any other Worktree is one the Machine
24
+ cut with `worktree()`.
@@ -8,12 +8,12 @@ description: "Where a Pool gets its work items: the Source."
8
8
  Type, in `@jr2/orchestrator/src/pool.ts`. The signature of an export, or a type that it names, names this type. The [Authoring surface](../authoring-surface.md) page lists each export. This type is not an export of `@jr2/orchestrator`, so a workflow module does not import it by name.
9
9
 
10
10
  ```ts
11
- type SourceSpec<T> = {
11
+ type SourceSpec<T, TInput = unknown> = {
12
12
  next:
13
- | PromiseActorLogic<SourceNextResult<T>, { active: string[] }>
14
- | PromiseActorLogic<{ item: T | null; open: number }, { active: string[] }>
15
- | PromiseActorLogic<T | null, { active: string[] }>
16
- | PromiseActorLogic<null, { active: string[] }>;
13
+ | PromiseActorLogic<SourceNextResult<T>, PoolNextInput<TInput>>
14
+ | PromiseActorLogic<{ item: T | null; open: number }, PoolNextInput<TInput>>
15
+ | PromiseActorLogic<T | null, PoolNextInput<TInput>>
16
+ | PromiseActorLogic<null, PoolNextInput<TInput>>;
17
17
  wake?: EventDef;
18
18
  pollEvery?: number;
19
19
  };
@@ -23,13 +23,13 @@ Where a Pool gets its work items: the Source. Make one with [`source`](source.md
23
23
 
24
24
  **Fields**
25
25
 
26
- - `next`: `PromiseActorLogic<SourceNextResult<T>, { active: string[]; }> | PromiseActorLogic<{ item: T | null; open: number; }, { active: string[]; }> | PromiseActorLogic<T | null, { active: string[]; }> | PromiseActorLogic<null, { active: string[]; }>`
26
+ - `next`: `PromiseActorLogic<SourceNextResult<T>, PoolNextInput<TInput>> | PromiseActorLogic<{ item: T | null; open: number; }, PoolNextInput<TInput>> | PromiseActorLogic<T | null, PoolNextInput<TInput>> | PromiseActorLogic<null, PoolNextInput<TInput>>`
27
27
 
28
28
  Claim the next ready item. Write it as an xstate promise actor,
29
- `fromPromise(async ({ input }) => …)`. Its input is `{ active }`: the ids of the items that
30
- workers have now, which it must not give again. It must also not give again an item that a
31
- worker finished. It resolves to a [`SourceNextResult`](type-sourcenextresult.md). The Pool calls it when the run
32
- starts, after each claim, and when a worker settles. While workers run and no item is ready, it
29
+ `fromPromise(async ({ input }) => …)`. Its input is [`PoolNextInput`](type-poolnextinput.md): `active`, the ids of
30
+ the items that workers have now, `settled`, the ids of the items that finished or failed, and
31
+ `input`, the Pool's input. It must not give again an item in `active` or `settled`. It resolves
32
+ to a [`SourceNextResult`](type-sourcenextresult.md). The Pool calls it when the run starts, after each claim, and when a worker settles. While workers run and no item is ready, it
33
33
  also calls it on `wake` and every `pollEvery`.
34
34
  - `wake` (optional): [`EventDef`](type-eventdef.md)
35
35
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Workspaced
3
- description: "The input of a `workspace()` body: the run input, plus `workspace`, the handles that the Workspace adds (`workspace.repos`, `workspace.branch`, `workspace.review`)."
3
+ description: "The input of a `workspace()` body: the run input, plus `workspace`, the handles that the Workspace adds (`workspace.repos`, the folder of each Repo Slot)."
4
4
  ---
5
5
 
6
6
  # `Workspaced`
@@ -14,7 +14,7 @@ type Workspaced<TInput, TSlots extends string> = WithoutEmptyIndex<TInput> & {
14
14
  ```
15
15
 
16
16
  The input of a `workspace()` body: the run input, plus `workspace`, the handles that the
17
- Workspace adds (`workspace.repos`, `workspace.branch`, `workspace.review`). Use it as the type of
17
+ Workspace adds (`workspace.repos`, the folder of each Repo Slot). Use it as the type of
18
18
  the body's input.
19
19
 
20
20
  The first argument is the run input: the `input` schema of the `workspace()`. The second is the
@@ -36,16 +36,29 @@ Its fields are the fields of the run input, and `workspace`. The run input can h
36
36
 
37
37
  ```ts
38
38
  import { z } from "zod";
39
- import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
39
+ import {
40
+ agent,
41
+ defineEvent,
42
+ fail,
43
+ isReady,
44
+ jr2Setup,
45
+ workspace,
46
+ worktree,
47
+ type Workspaced,
48
+ } from "@jr2/orchestrator";
40
49
 
41
50
  const done = defineEvent({
42
51
  name: "done",
43
52
  description: "Call this when REVIEW.md is written.",
44
53
  input: z.object({}),
45
54
  });
46
- const runInput = z.object({ branch: z.string(), sha: z.string() });
55
+ const runInput = z.object({ sha: z.string() });
47
56
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
48
57
 
58
+ // A handle is the slot's folder: `/work/app`. A helper returns `as const`, so that the compiler
59
+ // sees the slot.
60
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
61
+
49
62
  const body = jr2Setup({
50
63
  types: {} as { input: BodyInput; context: BodyInput },
51
64
  events: [done],
@@ -55,25 +68,34 @@ const body = jr2Setup({
55
68
  instructions: "You review one commit and write REVIEW.md.",
56
69
  workspace: "read",
57
70
  }),
71
+ cut: worktree(),
58
72
  fail: fail(),
59
73
  },
60
74
  }).createMachine({
61
75
  id: "review",
62
76
  context: ({ input }) => input,
63
77
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
64
- initial: "reviewing",
78
+ initial: "cutting",
65
79
  states: {
80
+ cutting: {
81
+ invoke: {
82
+ src: "cut",
83
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.sha }),
84
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
85
+ },
86
+ },
66
87
  reviewing: {
67
88
  invoke: {
68
89
  src: "reviewer",
69
90
  input: ({ context }) => ({
70
- prompt: `Review commit ${context.sha} on ${context.workspace.branch}.`,
71
- cwd: context.workspace.review?.app ?? context.workspace.repos.app,
91
+ prompt: `Review commit ${context.sha}.`,
92
+ cwd: reviewTree(context),
72
93
  }),
73
94
  },
74
95
  on: { done: "finished" },
75
96
  },
76
97
  finished: { type: "final" },
98
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
77
99
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
78
100
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
79
101
  },
@@ -82,7 +104,6 @@ const body = jr2Setup({
82
104
  export const machine = workspace(body, {
83
105
  input: runInput,
84
106
  repos: { app: "https://github.com/you/app.git" },
85
- spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
86
107
  });
87
108
  ```
88
109
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: WorkspaceHandles
3
- description: "What the body of a `workspace()` gets as `input.workspace`: the path of each Worktree, and the branch."
3
+ description: "What the body of a `workspace()` gets as `input.workspace`: the folder of each Repo Slot."
4
4
  ---
5
5
 
6
6
  # `WorkspaceHandles`
@@ -13,11 +13,10 @@ type WorkspaceHandles<TSlots extends string> = [TSlots] extends [never]
13
13
  : SlotHandles<TSlots>;
14
14
  ```
15
15
 
16
- What the body of a `workspace()` gets as `input.workspace`: the path of each Worktree, and the
17
- branch. A path is correct in each Instance that registers the Machine.
16
+ What the body of a `workspace()` gets as `input.workspace`: the folder of each Repo Slot. A path
17
+ is correct in each Instance that registers the Machine.
18
18
 
19
19
  `TSlots` is the names of the Repo Slots that the body reads. `workspace()` refuses a body that
20
20
  names a slot that the Workspace does not have. Write `string` for a body that names no slot and
21
- reads the Worktrees in the order of `repos`, as under `repos: open`. Write `never` for a
22
- Workspace with no Repo Slot: its handles are `{ repos: {} }`, with no branch, and its Agents work
23
- in `/work`.
21
+ reads the folders in the order of `repos`, as under `repos: open`. Write `never` for a
22
+ Workspace with no Repo Slot: its handles are `{ repos: {} }`, and its Agents work in `/work`.
@@ -13,7 +13,6 @@ type WorkspaceOptions<TSchema extends z.ZodObject, TSlots extends string> = Sand
13
13
  z.infer<TSchema>
14
14
  > & {
15
15
  input: TSchema;
16
- spec: (args: { input: WithoutEmptyIndex<z.infer<TSchema>> & HostInjectedInput }) => WorkspaceSpec;
17
16
  };
18
17
  ```
19
18
 
@@ -24,16 +23,5 @@ The options of a `workspace()` that declares the input that starts a run.
24
23
  - `input`: `TSchema`
25
24
 
26
25
  The input that starts a run, as a `z.object`. jr2 checks each start against it. It also types
27
- the `input` of `spec` and of a per-run Repo Slot, and the body must accept it plus the
26
+ the `input` of a per-run Repo Slot, and the body must accept it plus the
28
27
  `workspace` handles. Write it here, not on the body.
29
- - `spec`: `(args: { input: WithoutEmptyIndex<z.infer<TSchema>> & HostInjectedInput; }) => WorkspaceSpec`
30
-
31
- A function from the run input to the Workspace's [`WorkspaceSpec`](type-workspacespec.md): the `branch` it works
32
- on, and optionally `reviewSha` and `workGroup`. jr2 calls it when the run enters the
33
- Workspace.
34
-
35
- The input of `spec` also holds `instanceId`, the seed Instance ID of the run (see
36
- [`HostInjectedInput`](type-hostinjectedinput.md)). Use it to give each run a branch of its own:
37
- ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``. Only a root `workspace()` gets
38
- it from jr2. A `workspace()` that a different Machine invokes gets only the input that its
39
- parent gives it, so that parent must give `instanceId` when `spec` reads it.
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: WorktreeFound
3
+ description: "What a cut found: `held`, a cut made this Worktree earlier, so what is in it is earlier work; `local`, a local branch; `remote`, `origin/<branch>`, now tracked; `none`, a new branch with no upstream; `commit`, a detached cut."
4
+ ---
5
+
6
+ # `WorktreeFound`
7
+
8
+ Type, 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
+ type WorktreeFound = "held" | "local" | "remote" | "none" | "commit" | (string & {});
12
+ ```
13
+
14
+ What a cut found: `held`, a cut made this Worktree earlier, so what is in it is earlier work;
15
+ `local`, a local branch; `remote`, `origin/<branch>`, now tracked; `none`, a new branch with no
16
+ upstream; `commit`, a detached cut. Open: a later value does not break an author's code.
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: WorktreeInput
3
+ description: "The input of a `worktree()`: a branch Worktree, or a detached one."
4
+ ---
5
+
6
+ # `WorktreeInput`
7
+
8
+ Type, 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
+ type WorktreeInput =
12
+ | {
13
+ path: AnyWorktreePath;
14
+ branch: string;
15
+ from?: string;
16
+ detach?: never;
17
+ expect?: never;
18
+ }
19
+ | {
20
+ path: AnyWorktreePath;
21
+ detach: string;
22
+ branch?: never;
23
+ from?: never;
24
+ expect?: never;
25
+ };
26
+ ```
27
+
28
+ The input of a `worktree()`: a branch Worktree, or a detached one.
29
+
30
+ **Forms**: it is one of these objects.
31
+
32
+ - Form 1: `path`, `branch`, `from`, `detach`, `expect`
33
+
34
+ - `path`: [`AnyWorktreePath`](type-anyworktreepath.md)
35
+
36
+ The root of the Worktree: `${context.workspace.repos.app}/<name>`, a constant word.
37
+ - `branch`: `string`
38
+
39
+ The branch. It resolves as a local branch, then `origin/<branch>` (tracked), then a new
40
+ branch.
41
+ - `from` (optional): `string`
42
+
43
+ Where a new branch starts. Default: the Binding's base. On a branch that exists, it must
44
+ be that branch's tip, or the cut is `refused`.
45
+ - `detach` (optional): `never`
46
+ - `expect` (optional): `never`
47
+ - Form 2: `path`, `detach`, `branch`, `from`, `expect`
48
+
49
+ - `path`: [`AnyWorktreePath`](type-anyworktreepath.md)
50
+ - `detach`: `string`
51
+
52
+ The commit to check out, with no branch. The Worktree is forced to it and cleaned on each
53
+ cut. Ignored files stay, so that a reviewer can run the tests.
54
+ - `branch` (optional): `never`
55
+ - `from` (optional): `never`
56
+ - `expect` (optional): `never`
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: WorktreeKey
3
+ description: A type that the Authoring surface names.
4
+ ---
5
+
6
+ # `WorktreeKey`
7
+
8
+ Type, in `@jr2/orchestrator/src/effect.ts`. It is type plumbing: it computes a type from the types of a Machine or of its parts, and a workflow module does not write it. The signature of an export, or a type that it names, names this type, so its declaration is here. The [Authoring surface](../authoring-surface.md) page lists each export.
9
+
10
+ ```ts
11
+ type WorktreeKey<Opt> = [Opt] extends [{ readonly env: object }]
12
+ ? { worktree: AnyWorktreePath }
13
+ : { worktree?: AnyWorktreePath };
14
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ title: WorktreeOptions
3
+ description: The options of `worktree()`.
4
+ ---
5
+
6
+ # `WorktreeOptions`
7
+
8
+ Type, 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
+ type WorktreeOptions = {
12
+ expect?: "new" | "existing";
13
+ timeout?: Duration;
14
+ };
15
+ ```
16
+
17
+ The options of `worktree()`.
18
+
19
+ **Fields**
20
+
21
+ - `expect` (optional): `"new" | "existing"`
22
+
23
+ `"new"`: refuse a branch that exists, local or on the remote. For a name that a planner or a
24
+ door chose for new work: a collision is a refusal, not someone else's branch. `"existing"`:
25
+ refuse a branch that does not exist, and make nothing. For a run that must continue a branch.
26
+ A Chart cannot change it, and a cut lent to a Chart must state it.
27
+ - `timeout` (optional): [`Duration`](type-duration.md)
28
+
29
+ How long the fetch and the cut can take. Default `"10m"`.
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: WorktreeOutput
3
+ description: The output of a `worktree()`.
4
+ ---
5
+
6
+ # `WorktreeOutput`
7
+
8
+ Type, 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
+ type WorktreeOutput<S extends string = string> = WorktreeReady<S> | WorktreeRefused | Unknown | LaterOutcome;
12
+ ```
13
+
14
+ The output of a `worktree()`. `unknown`: its state exited while it ran, or the cut did not end.
15
+
16
+ **Forms**: it is one of these objects.
17
+
18
+ - Form 1: `outcome`, `reason`
19
+
20
+ - `outcome`: `"unknown"`
21
+ - `reason`: `string`
22
+ - Form 2: `outcome`, `slot`, `path`, `branch`, `head`, `found`, `stale`
23
+
24
+ - `outcome`: `"ready"`
25
+ - `slot`: `S`
26
+ - `path`: `WorktreePath<S>`
27
+ - `branch`: `string | null`
28
+
29
+ The branch of the cut, from git and the cut's record, never from the folder or HEAD. `null`
30
+ when the Worktree is detached.
31
+ - `head`: `string`
32
+
33
+ The tip of `branch` (of the commit, when detached). Never HEAD.
34
+ - `found`: [`WorktreeFound`](type-worktreefound.md)
35
+ - `stale` (optional): `string`
36
+
37
+ [ADR-0053](../decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md)'s words when the remote did not answer. The cut used what the clone holds.
38
+ - Form 3: `outcome`, `reason`
39
+
40
+ - `outcome`: `"refused"`
41
+ - `reason`: `string`
42
+ - Form 4: `outcome`, `reason`
43
+
44
+ - `outcome`: `"(a later outcome)"`
45
+ - `reason` (optional): `string`