@jr2/machines 0.4.1 → 0.5.1

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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/task.ts +143 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr2/machines",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "The Machines the jr2 kit ships: packaged xstate Machines that leave their parts Open for a composer to bind.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -35,14 +35,14 @@
35
35
  "peerDependencies": {
36
36
  "xstate": "^5.18.0",
37
37
  "zod": "^4.4.3",
38
- "@jr2/orchestrator": "0.4.1"
38
+ "@jr2/orchestrator": "0.5.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^26.0.1",
42
42
  "typescript": "^5.6.0",
43
43
  "xstate": "^5.18.0",
44
44
  "zod": "^4.4.3",
45
- "@jr2/orchestrator": "0.4.1"
45
+ "@jr2/orchestrator": "0.5.1"
46
46
  },
47
47
  "files": [
48
48
  "src"
package/src/task.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  // target: { url: "https://github.com/you/repo.git" },
18
18
  // reference: { url: "https://github.com/you/lib.git" }, // optional: more checkouts to read
19
19
  // },
20
- // agents: { coder: { model: "anthropic/claude-sonnet-4-6" } },
20
+ // agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
21
21
  // });
22
22
  //
23
23
  // `jr2 up` walks the registered Machines and refuses either part unbound, printing exactly that
@@ -36,6 +36,7 @@ import { z } from "zod";
36
36
  import {
37
37
  agent,
38
38
  defineEvent,
39
+ fail,
39
40
  jr2Setup,
40
41
  open,
41
42
  workspace,
@@ -65,27 +66,74 @@ const THINKING_LEVELS = [
65
66
  ] as const satisfies readonly ThinkingLevel[];
66
67
 
67
68
  const door = z.object({
69
+ /** What to do, in prose. Required. The coder's first Turn is this prompt, with the Worktree it
70
+ * works in. */
68
71
  prompt: z.string().describe("What to do, in prose. The coder's first turn is this, framed with the worktree."),
72
+ /** The branch to cut and commit on. Default: `jr2/task-<instance id>`, where `<instance id>` is
73
+ * the seed Instance ID of the run (`instanceId` in `jr2 status`). It is not the run id. Two runs
74
+ * never use the same branch. Required when `task` runs inside another Machine, such as a
75
+ * `pool()`: there is no Instance ID to name the branch after. */
69
76
  branch: z
70
77
  .string()
71
78
  .optional()
72
- .describe("The branch to cut and commit on. Default: jr2/task-<run id>, so concurrent runs never collide."),
79
+ .describe(
80
+ "The branch to cut and commit on. Default: jr2/task-<instance id>, where <instance id> is the seed " +
81
+ "Instance ID of the run (instanceId in jr2 status). It is not the run id.",
82
+ ),
73
83
  // The two Dials (ADR-0018) as a PER-RUN escape hatch, not a default (ADR-0054): the bound
74
84
  // definition is what `jr2 up` preflights, and a door value overrides it for this one run's Turns.
75
85
  // Identity — instructions, workspace access — is deliberately absent: an invocation that rewrote
76
86
  // those would make the Agent's name a lie.
87
+ /** The coder's model for this run only, as `"<provider>/<modelId>"`. Default: the model of the
88
+ * coder Agent. */
77
89
  model: z.string().optional().describe('Override the coder\'s model for this run only, as "<provider>/<modelId>".'),
90
+ /** The coder's thinking level for this run only: `off`, `minimal`, `low`, `medium`, `high` or
91
+ * `xhigh`. Default: the thinking level of the coder Agent. */
78
92
  thinkingLevel: z
79
93
  .enum(THINKING_LEVELS)
80
94
  .optional()
81
95
  .describe("Override the coder's reasoning effort for this run only."),
82
96
  });
83
97
 
84
- /** What a caller sends. */
98
+ /**
99
+ * The run input of `task`: what a caller sends to start a run, for example with
100
+ * `jr2 run <workflow> --input '{"prompt":"…"}'`.
101
+ *
102
+ * `prompt` is required. `branch`, `model` and `thinkingLevel` are optional. Each field has its own
103
+ * doc.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import type { TaskInput } from "@jr2/machines";
108
+ *
109
+ * export const input: TaskInput = {
110
+ * prompt: "Make the failing test in test/greet.test.ts pass.",
111
+ * branch: "fix/greet",
112
+ * thinkingLevel: "high",
113
+ * };
114
+ * ```
115
+ *
116
+ * @see @jr2/orchestrator/docs/reference/machines.md
117
+ */
85
118
  export type TaskInput = z.infer<typeof door>;
86
119
 
87
- /** What a run of `task` settles as — approved by the human, or lost with its Workspace. */
88
- export type TaskOutput = { outcome: "approved"; branch: string } | { outcome: "lost" };
120
+ /**
121
+ * The output of a run of `task` that ends `done`: `{ branch }`, the branch of the work that the
122
+ * human approved. A run whose Workspace is lost has no output of this type: it fails, and the run
123
+ * ends `failed` with the reason `the Workspace was lost` (ADR-0073).
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * import type { TaskOutput } from "@jr2/machines";
128
+ *
129
+ * export function describe(output: TaskOutput): string {
130
+ * return `approved on ${output.branch}`;
131
+ * }
132
+ * ```
133
+ *
134
+ * @see @jr2/orchestrator/docs/reference/machines.md
135
+ */
136
+ export type TaskOutput = { branch: string };
89
137
 
90
138
  // ---------------------------------------------------------------------------------------------
91
139
  // Vocabulary (ADR-0011/0015), scoped to this Machine alone: the agent event becomes the coder
@@ -141,6 +189,31 @@ type BodyContext = BodyInput & {
141
189
  notes?: string;
142
190
  };
143
191
 
192
+ /**
193
+ * The Machine inside `task`: the coder's loop and the `review` Gate, with no `workspace()` around
194
+ * it. Use `task` to run it. Use `body` only to put the same loop in a `workspace()` of your own.
195
+ * For example, your `workspace()` can have Repo Slots or a run input that differ from those of
196
+ * `task`. Its input is the run input plus the `workspace` handles, and it works in the first Repo
197
+ * Slot of those handles.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * import { customize, workspace } from "@jr2/orchestrator";
202
+ * import { body, type TaskInput } from "@jr2/machines";
203
+ *
204
+ * const fixed = workspace(body, {
205
+ * repos: { app: { url: "https://github.com/you/app.git" } },
206
+ * spec: ({ input }: { input: TaskInput }) => ({ branch: input.branch ?? "jr2/fix" }),
207
+ * });
208
+ *
209
+ * export const machine = customize(fixed, {
210
+ * agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
211
+ * });
212
+ * ```
213
+ *
214
+ * @see @jr2/orchestrator/docs/reference/machines.md
215
+ * @see @jr2/orchestrator/docs/concepts/workspaces.md
216
+ */
144
217
  export const body = jr2Setup({
145
218
  types: {} as { context: BodyContext; input: BodyInput; output: TaskOutput },
146
219
  events: [finish, approve, requestChanges],
@@ -165,6 +238,8 @@ export const body = jr2Setup({
165
238
  - You MUST end your turn by calling \`finish\` exactly once, with a summary of what you did. An
166
239
  uncalled tool parks the whole workflow waiting for you.`,
167
240
  }),
241
+ // The Failure (ADR-0073): the `lost` state invokes it, and the run ends `failed`.
242
+ fail: fail(),
168
243
  },
169
244
  }).createMachine({
170
245
  id: "task",
@@ -172,7 +247,7 @@ export const body = jr2Setup({
172
247
  initial: "working",
173
248
 
174
249
  // Our Sandbox is gone — reaped, or replaced after an eviction (ADR-0021). The pod-local clone and
175
- // every unpushed commit went with it, so settle as lost rather than pretend we can resume.
250
+ // every unpushed commit went with it, so fail the run rather than pretend we can resume.
176
251
  on: { "workspace.lost": { target: ".lost" } },
177
252
 
178
253
  states: {
@@ -257,13 +332,23 @@ export const body = jr2Setup({
257
332
  // the pod, and anything still unpushed in it goes too.
258
333
  done: {
259
334
  type: "final",
260
- output: ({ context }): TaskOutput => ({ outcome: "approved", branch: context.workspace.branch }),
335
+ output: ({ context }): TaskOutput => ({ branch: context.workspace.branch }),
336
+ },
337
+ // Not a final state: a final state means `done`, and this work did not succeed. The Failure
338
+ // goes up through the wrapper, and the run ends `failed` (ADR-0073). A parent catches it with
339
+ // an `onError` that `isFailure` guards.
340
+ lost: {
341
+ invoke: {
342
+ src: "fail",
343
+ input: {
344
+ reason: "the Workspace was lost",
345
+ detail: { lost: "the Sandbox is gone, with every commit that was not pushed" },
346
+ },
347
+ },
261
348
  },
262
- lost: { type: "final", output: { outcome: "lost" } satisfies TaskOutput },
263
349
  },
264
350
 
265
- // The body's output is whichever final state settled it — the wrapper forwards it verbatim, so
266
- // `jr2 status` on a finished run says which of the two things happened.
351
+ // The body's output is the output of its one final state, `done`; the wrapper forwards it.
267
352
  output: ({ event }) => (event as { output?: TaskOutput }).output as TaskOutput,
268
353
  });
269
354
 
@@ -271,6 +356,51 @@ export const body = jr2Setup({
271
356
  // The wrapper is this Machine's ROOT, so its `input` is the run's door (ADR-0033) and both mappers
272
357
  // are typed by it — nothing here restates a shape.
273
358
 
359
+ /**
360
+ * A Machine in which a coder Agent works on one prompt in a Workspace until a human approves the
361
+ * work. The coder works in a Sandbox and commits on a branch. Then the run waits at the `review`
362
+ * Gate. The human answers `request_changes` with `notes`, and the coder continues the same
363
+ * conversation, or `approve`, and the run ends. A run has no limit on the number of rounds.
364
+ *
365
+ * `task` does not push. While the run waits at the Gate, the Sandbox stays. You can exec into it
366
+ * and read the branch. To keep the work, push it yourself. `approve` removes the Sandbox, and
367
+ * every commit that was not pushed goes with it. If the Sandbox is lost, `task` fails: the run
368
+ * ends `failed` with the reason `the Workspace was lost`. A parent Machine catches that Failure
369
+ * with an `onError` that `isFailure` guards.
370
+ *
371
+ * `task` leaves two parts Open, so you bind them with `customize()` where you register it:
372
+ *
373
+ * - `repos`: the whole Repo Slot map. Name every slot, and at least one. The first slot is the
374
+ * Repo that the coder works in. The coder can read the others.
375
+ * - `agents.coder.model`: the coder's model, as `"<provider>/<modelId>"`. You can also change the
376
+ * coder's other fields, such as `instructions` or `thinkingLevel`.
377
+ *
378
+ * `jr2 up` refuses a Workflow that leaves either part Open, and prints the `customize()` line
379
+ * that binds it. You can also give `task` an `image`, a `user` or a Size (`resources`). Without an
380
+ * image, it uses the Instance's `images/default`, or jr2's Harness image when there is none.
381
+ * Without a Size, it uses the Instance's `sandbox.resources`, or the kit's Size when it has none.
382
+ * Inside another Machine, `task` needs `branch` in its input, or it refuses:
383
+ * ``task: no `branch` on the door and no Instance ID to derive one from``.
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * import { customize } from "@jr2/orchestrator";
388
+ * import { task } from "@jr2/machines";
389
+ *
390
+ * export const machine = customize(task, {
391
+ * repos: {
392
+ * app: { url: "https://github.com/you/app.git" },
393
+ * lib: { url: "https://github.com/you/lib.git" },
394
+ * },
395
+ * agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
396
+ * });
397
+ * ```
398
+ *
399
+ * @see @jr2/orchestrator/docs/reference/machines.md
400
+ * @see @jr2/orchestrator/docs/get-started/an-agent-in-a-sandbox.md
401
+ * @see @jr2/orchestrator/docs/guides/bind-open-parts.md
402
+ * @see @jr2/orchestrator/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
403
+ */
274
404
  export const task = workspace(body, {
275
405
  input: door,
276
406
  // The Repo Slots, OPEN as a map (ADR-0051/0054): the consumer names every slot and every Repo.
@@ -283,7 +413,7 @@ export const task = workspace(body, {
283
413
  });
284
414
 
285
415
  /**
286
- * The branch this run works on: the door's, or `jr2/task-<run id>` (ADR-0054).
416
+ * The branch this run works on: the door's, or `jr2/task-<instance id>` (ADR-0054).
287
417
  *
288
418
  * The id is the run's seed Instance ID, which `RunHost.start` injects beside the door on the ROOT
289
419
  * machine's input ({@link HostInjectedInput}) and `jr2 status` reports. It is not door material —
@@ -300,8 +430,8 @@ function branchOf(input: TaskInput): string {
300
430
  const { instanceId } = input as TaskInput & Partial<HostInjectedInput>;
301
431
  if (!instanceId) {
302
432
  throw new Error(
303
- "task: no `branch` on the door and no run id to derive one from — `jr2/task-<run id>` needs the " +
304
- "id the host injects beside the door of the ROOT machine (ADR-0033), and this run of `task` " +
433
+ "task: no `branch` on the door and no Instance ID to derive one from — `jr2/task-<instance id>` needs " +
434
+ "the id the host injects beside the door of the ROOT machine (ADR-0033), and this run of `task` " +
305
435
  "was started by a parent Machine instead. Pass `branch` in the run input.",
306
436
  );
307
437
  }