@jr2/machines 0.5.1 → 0.7.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.
- package/package.json +3 -3
- package/src/task.ts +48 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jr2/machines",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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.
|
|
38
|
+
"@jr2/orchestrator": "0.7.0"
|
|
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.
|
|
45
|
+
"@jr2/orchestrator": "0.7.0"
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"src"
|
package/src/task.ts
CHANGED
|
@@ -40,15 +40,18 @@ import {
|
|
|
40
40
|
jr2Setup,
|
|
41
41
|
open,
|
|
42
42
|
workspace,
|
|
43
|
+
worktree,
|
|
44
|
+
branchName,
|
|
43
45
|
type HostInjectedInput,
|
|
44
46
|
type ThinkingLevel,
|
|
45
47
|
type Workspaced,
|
|
48
|
+
type WorktreePath,
|
|
46
49
|
} from "@jr2/orchestrator";
|
|
47
50
|
|
|
48
51
|
// ---------------------------------------------------------------------------------------------
|
|
49
52
|
// The door (ADR-0033): what a caller sends to start a run, declared on the `workspace()` that is
|
|
50
53
|
// this Machine's root. One source of truth — the types below derive from it, the Console generates
|
|
51
|
-
// its start form from it, and it types the
|
|
54
|
+
// its start form from it, and it types the body's input.
|
|
52
55
|
//
|
|
53
56
|
// No `repo` field, deliberately (ADR-0054): a packaged door cannot enumerate the Instance's
|
|
54
57
|
// repositories, and "a Workflow is a name" reads best when the name means "a prompt against THIS
|
|
@@ -73,8 +76,7 @@ const door = z.object({
|
|
|
73
76
|
* the seed Instance ID of the run (`instanceId` in `jr2 status`). It is not the run id. Two runs
|
|
74
77
|
* never use the same branch. Required when `task` runs inside another Machine, such as a
|
|
75
78
|
* `pool()`: there is no Instance ID to name the branch after. */
|
|
76
|
-
branch:
|
|
77
|
-
.string()
|
|
79
|
+
branch: branchName
|
|
78
80
|
.optional()
|
|
79
81
|
.describe(
|
|
80
82
|
"The branch to cut and commit on. Default: jr2/task-<instance id>, where <instance id> is the seed " +
|
|
@@ -172,6 +174,9 @@ const requestChanges = defineEvent({
|
|
|
172
174
|
type BodyInput = Workspaced<TaskInput & HostInjectedInput, string>;
|
|
173
175
|
|
|
174
176
|
type BodyContext = BodyInput & {
|
|
177
|
+
/** The branch of this run: the door's, or `jr2/task-<instance id>`. Undefined only under a parent
|
|
178
|
+
* Machine with no `branch` in its input. */
|
|
179
|
+
branch: string | undefined;
|
|
175
180
|
/** Turns COMPLETED on the conversation the next Turn will land on. Zero means that Turn is its
|
|
176
181
|
* first, so the prompt has to carry the whole task and the worktree framing again — which is
|
|
177
182
|
* exactly what a post-fault Turn needs and a continued one does not. It counts for the PROMPT
|
|
@@ -203,7 +208,6 @@ type BodyContext = BodyInput & {
|
|
|
203
208
|
*
|
|
204
209
|
* const fixed = workspace(body, {
|
|
205
210
|
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
206
|
-
* spec: ({ input }: { input: TaskInput }) => ({ branch: input.branch ?? "jr2/fix" }),
|
|
207
211
|
* });
|
|
208
212
|
*
|
|
209
213
|
* export const machine = customize(fixed, {
|
|
@@ -238,27 +242,43 @@ export const body = jr2Setup({
|
|
|
238
242
|
- You MUST end your turn by calling \`finish\` exactly once, with a summary of what you did. An
|
|
239
243
|
uncalled tool parks the whole workflow waiting for you.`,
|
|
240
244
|
}),
|
|
245
|
+
cut: worktree(),
|
|
241
246
|
// The Failure (ADR-0073): the `lost` state invokes it, and the run ends `failed`.
|
|
242
247
|
fail: fail(),
|
|
243
248
|
},
|
|
244
249
|
}).createMachine({
|
|
245
250
|
id: "task",
|
|
246
|
-
context: ({ input }) => ({ ...input, turns: 0 }),
|
|
247
|
-
initial: "
|
|
251
|
+
context: ({ input }) => ({ ...input, branch: branchOf(input), turns: 0 }),
|
|
252
|
+
initial: "starting",
|
|
248
253
|
|
|
249
254
|
// Our Sandbox is gone — reaped, or replaced after an eviction (ADR-0021). The pod-local clone and
|
|
250
255
|
// every unpushed commit went with it, so fail the run rather than pretend we can resume.
|
|
251
256
|
on: { "workspace.lost": { target: ".lost" } },
|
|
252
257
|
|
|
253
258
|
states: {
|
|
259
|
+
// A bad name is the door's (`branchName`): it costs no pod, under a parent also, because
|
|
260
|
+
// `place` holds an invocation's input to the wrapper's door. A name that NO ONE gave cannot be a
|
|
261
|
+
// door rule: the host adds `instanceId` after the door. So this one line stays, and it costs a
|
|
262
|
+
// pod: a Failure, and not a defect: under a pool(), one item fails, and the run goes on.
|
|
263
|
+
starting: {
|
|
264
|
+
always: [{ guard: ({ context }) => context.branch === undefined, target: "noBranch" }, { target: "cutting" }],
|
|
265
|
+
},
|
|
266
|
+
// The first slot is the coder's. A slot after it needs no cut: `<slot>/default` is on its base.
|
|
267
|
+
cutting: {
|
|
268
|
+
invoke: {
|
|
269
|
+
src: "cut",
|
|
270
|
+
input: ({ context }) => ({ path: worktreeOf(context).path, branch: context.branch! }),
|
|
271
|
+
onDone: [{ guard: ({ event }) => event.output.outcome === "ready", target: "working" }, { target: "notCut" }],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
254
274
|
working: {
|
|
255
275
|
invoke: {
|
|
256
276
|
src: "coder",
|
|
257
277
|
input: ({ context }) => ({
|
|
258
278
|
// This Turn's FRAME (ADR-0057): what it is about, and where it works. The `cwd` is
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
279
|
+
// required under Repo Slots (ADR-0075): "the first slot is the one the coder edits" is this
|
|
280
|
+
// Machine's convention (`worktreeOf`), and a consumer who binds a second slot must not
|
|
281
|
+
// change where the coder works.
|
|
262
282
|
prompt: coderPrompt(context),
|
|
263
283
|
cwd: worktreeOf(context).path,
|
|
264
284
|
// One human steering one Agent wants the Agent to remember what it did (ADR-0054), so
|
|
@@ -310,9 +330,9 @@ export const body = jr2Setup({
|
|
|
310
330
|
meta: {
|
|
311
331
|
...(context.summary !== undefined ? { summary: context.summary } : {}),
|
|
312
332
|
...(context.reason !== undefined ? { reason: context.reason } : {}),
|
|
313
|
-
// The branch the
|
|
333
|
+
// The branch the cut made and the Worktree the coder committed in — what
|
|
314
334
|
// an `exec` needs, and what a push would name.
|
|
315
|
-
branch: context.
|
|
335
|
+
branch: context.branch!,
|
|
316
336
|
worktree: worktreeOf(context).path,
|
|
317
337
|
},
|
|
318
338
|
}),
|
|
@@ -332,11 +352,18 @@ export const body = jr2Setup({
|
|
|
332
352
|
// the pod, and anything still unpushed in it goes too.
|
|
333
353
|
done: {
|
|
334
354
|
type: "final",
|
|
335
|
-
output: ({ context }): TaskOutput => ({ branch: context.
|
|
355
|
+
output: ({ context }): TaskOutput => ({ branch: context.branch! }),
|
|
336
356
|
},
|
|
337
357
|
// Not a final state: a final state means `done`, and this work did not succeed. The Failure
|
|
338
358
|
// goes up through the wrapper, and the run ends `failed` (ADR-0073). A parent catches it with
|
|
339
359
|
// an `onError` that `isFailure` guards.
|
|
360
|
+
noBranch: {
|
|
361
|
+
invoke: {
|
|
362
|
+
src: "fail",
|
|
363
|
+
input: { reason: "task: no `branch` on the door and no Instance ID to derive one from" },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
notCut: { invoke: { src: "fail", input: { reason: "task: the Worktree was not cut" } } },
|
|
340
367
|
lost: {
|
|
341
368
|
invoke: {
|
|
342
369
|
src: "fail",
|
|
@@ -409,7 +436,6 @@ export const task = workspace(body, {
|
|
|
409
436
|
// the coder is told about and may read. A named
|
|
410
437
|
// `target: open` would have said "one repository", which is not what the body knows.
|
|
411
438
|
repos: open,
|
|
412
|
-
spec: ({ input }) => ({ branch: branchOf(input) }),
|
|
413
439
|
});
|
|
414
440
|
|
|
415
441
|
/**
|
|
@@ -421,21 +447,12 @@ export const task = workspace(body, {
|
|
|
421
447
|
* carry it and reading it takes a cast that says why.
|
|
422
448
|
*
|
|
423
449
|
* A `task` composed UNDER another Machine is fed by its parent rather than by the host, so the
|
|
424
|
-
* field is absent there and there is no per-run id to name a branch after.
|
|
425
|
-
* before any pod exists: the alternative is a constant branch name, which two concurrent workers
|
|
426
|
-
* would both cut and neither would own.
|
|
450
|
+
* field is absent there and there is no per-run id to name a branch after: undefined.
|
|
427
451
|
*/
|
|
428
|
-
function branchOf(input: TaskInput): string {
|
|
452
|
+
function branchOf(input: TaskInput): string | undefined {
|
|
429
453
|
if (input.branch) return input.branch;
|
|
430
454
|
const { instanceId } = input as TaskInput & Partial<HostInjectedInput>;
|
|
431
|
-
|
|
432
|
-
throw new Error(
|
|
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` " +
|
|
435
|
-
"was started by a parent Machine instead. Pass `branch` in the run input.",
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
return `jr2/task-${instanceId}`;
|
|
455
|
+
return instanceId ? `jr2/task-${instanceId}` : undefined;
|
|
439
456
|
}
|
|
440
457
|
|
|
441
458
|
/**
|
|
@@ -455,11 +472,12 @@ function coderPrompt(context: BodyContext): string {
|
|
|
455
472
|
if (context.turns > 0) return notes.trimStart() || "Continue.";
|
|
456
473
|
const fault = memoryKill(context.reason);
|
|
457
474
|
const ended = fault ? `\n\nThe last attempt ended: ${fault}` : "";
|
|
458
|
-
const {
|
|
475
|
+
const { repos } = context.workspace;
|
|
476
|
+
const { branch } = context;
|
|
459
477
|
const worktree = worktreeOf(context);
|
|
460
478
|
const beside = Object.entries(repos)
|
|
461
479
|
.filter(([slot]) => slot !== worktree.slot)
|
|
462
|
-
.map(([slot, path]) => `\n- ${slot}: ${path}`)
|
|
480
|
+
.map(([slot, path]) => `\n- ${slot}: ${path}/default`)
|
|
463
481
|
.join("");
|
|
464
482
|
return (
|
|
465
483
|
`${context.prompt}${ended}${notes}\n\n` +
|
|
@@ -487,7 +505,7 @@ function memoryKill(reason: string | undefined): string | undefined {
|
|
|
487
505
|
* (ADR-0051) — so it is decided here, in one place, and stated on the line that registers the
|
|
488
506
|
* Machine. The wrapper refuses a map with no slot, so the first always exists.
|
|
489
507
|
*/
|
|
490
|
-
function worktreeOf(context: Pick<BodyContext, "workspace">): { slot: string; path: string } {
|
|
491
|
-
const [slot,
|
|
492
|
-
return { slot, path };
|
|
508
|
+
function worktreeOf(context: Pick<BodyContext, "workspace">): { slot: string; path: WorktreePath<string> } {
|
|
509
|
+
const [slot, folder] = Object.entries(context.workspace.repos)[0]!;
|
|
510
|
+
return { slot, path: `${folder}/task` };
|
|
493
511
|
}
|