@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
package/src/open-body.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  type UnknownActorLogic,
25
25
  } from "xstate";
26
26
  import { isAgent, type AgentDefinition, type WorkspaceAccess } from "./agent.ts";
27
+ import { ambientHandlesFor } from "./ambient.ts";
27
28
  import {
28
29
  admitChart,
29
30
  chartDigest,
@@ -31,8 +32,10 @@ import {
31
32
  ChartRefusal,
32
33
  GENERIC_ACTIONS,
33
34
  pathSegments,
35
+ READY_GUARD,
34
36
  resolveSource,
35
37
  resolveReferences,
38
+ slotFolderFault,
36
39
  type AdmittedChart,
37
40
  type ChartAction,
38
41
  type ChartGuard,
@@ -43,11 +46,12 @@ import {
43
46
  type ChartTransition,
44
47
  type ChartTransitions,
45
48
  } from "./chart.ts";
46
- import { inputKeysOf, isEffect, type Duration } from "./effect.ts";
49
+ import { inputShapeOf, isEffect, type Duration, type InputShape } from "./effect.ts";
47
50
  import { fail, FAILURE_GUARD, failureGuard } from "./fail.ts";
48
51
  import { agent } from "./harness-client.ts";
49
52
  import { SENT_BY } from "./party.ts";
50
- import { wrapperBodyOf } from "./parts.ts";
53
+ import { composesSandbox, sandboxPartsOf, wrapperBodyOf } from "./parts.ts";
54
+ import { open } from "./open.ts";
51
55
  import { actorPath, boundRun } from "./registration.ts";
52
56
  import { COMPILED_CHART, jr2Setup } from "./setup.ts";
53
57
  import { attachBoundMachine, boundMachineFor } from "./vocabulary.ts";
@@ -573,9 +577,12 @@ export function admitBodyChart(
573
577
  text: unknown,
574
578
  decl: OpenBodyDeclaration,
575
579
  lent: LentParts,
576
- options: { inputKeys?: readonly string[] } = {},
580
+ options: { inputKeys?: readonly string[]; repoSlots?: readonly string[] } = {},
577
581
  ): AdmittedChart {
578
- const admitted = admitChart(text, decl, lent, options.inputKeys ? { inputKeys: options.inputKeys } : {});
582
+ const admitted = admitChart(text, decl, lent, {
583
+ ...(options.inputKeys ? { inputKeys: options.inputKeys } : {}),
584
+ ...(options.repoSlots ? { repoSlots: options.repoSlots } : {}),
585
+ });
579
586
  try {
580
587
  compileChart(admitted, lent, decl);
581
588
  } catch (err) {
@@ -627,11 +634,23 @@ export function admitOpenBodyChart(
627
634
  const logic = (inner.implementations.actors as Record<string, unknown>)[slot];
628
635
  if (!isOpenBody(logic)) throw new Error(`machine "${machine.id}" has no Open body "${slot}"`);
629
636
  const decl = openBodyDeclarationOf(logic);
637
+ const repoSlots = repoSlotsAround(machine);
630
638
  return admitBodyChart(text, decl, lentIn(inner.implementations as Implementations, decl), {
631
639
  ...(inputKeys ? { inputKeys } : {}),
640
+ ...(repoSlots ? { repoSlots } : {}),
632
641
  });
633
642
  }
634
643
 
644
+ /** The Repo Slots of the `workspace()` around a Chart wrapper, where the door can see one: the
645
+ * wrapper IS the `workspace()`. Undefined: an Open map, or a Workspace that a parent state invokes,
646
+ * whose slots this door cannot read. Then the Open body's own door reads them off the running
647
+ * Workspace, and a Turn's or an Effect's start refuses what no door could see. */
648
+ export function repoSlotsAround(machine: AnyStateMachine): readonly string[] | undefined {
649
+ if (!composesSandbox(machine)) return undefined;
650
+ const { repos } = sandboxPartsOf(machine);
651
+ return repos === open ? undefined : Object.keys(repos);
652
+ }
653
+
635
654
  /**
636
655
  * Admit a Chart field of a Chart wrapper's door (ADR-0070), the door a run's start runs before the
637
656
  * run exists. The door cannot tell which Open body a field feeds, so the Chart is admitted against
@@ -648,7 +667,10 @@ export function admitWrapperChart(machine: AnyStateMachine, text: unknown): Admi
648
667
  for (const { slot, logic } of bodies) {
649
668
  const decl = openBodyDeclarationOf(logic);
650
669
  try {
651
- return admitBodyChart(text, decl, lentIn(inner.implementations as Implementations, decl));
670
+ const repoSlots = repoSlotsAround(machine);
671
+ return admitBodyChart(text, decl, lentIn(inner.implementations as Implementations, decl), {
672
+ ...(repoSlots ? { repoSlots } : {}),
673
+ });
652
674
  } catch (err) {
653
675
  if (!(err instanceof ChartRefusal)) throw err;
654
676
  report.push(...err.report.map((line) => (bodies.length > 1 ? `body "${slot}": ${line}` : line)));
@@ -675,7 +697,7 @@ const scopeOf = (args: Args) => ({
675
697
  input: chartInputOf((args.context?.[CHART_INPUT_KEY] as Record<string, unknown> | undefined) ?? {}),
676
698
  });
677
699
 
678
- /** An invoke's error whose message jr2 wrote for a Chart to read: a lent Effect's input fault. */
700
+ /** An invoke's error whose message jr2 wrote for a Chart to read: a fault in an input the door could not see. */
679
701
  class ChartInvokeFault extends Error {}
680
702
 
681
703
  /**
@@ -839,6 +861,10 @@ function transitionsOf(ts: ChartTransitions): unknown {
839
861
  return Array.isArray(ts) ? ts.map(transitionOf) : transitionOf(ts);
840
862
  }
841
863
 
864
+ /** `jr2.ready`: is this cut's output `ready`? On an `onDone`. */
865
+ const readyGuard = ({ event }: { event: unknown }): boolean =>
866
+ (event as { output?: { outcome?: unknown } } | undefined)?.output?.outcome === "ready";
867
+
842
868
  const mapValues = <T, U>(o: Record<string, T>, fn: (v: T) => U): Record<string, U> =>
843
869
  Object.fromEntries(Object.entries(o).map(([k, v]) => [k, fn(v)]));
844
870
 
@@ -847,15 +873,17 @@ const mapValues = <T, U>(o: Record<string, T>, fn: (v: T) => U): Record<string,
847
873
  * resolved object again at invoke time"), or undefined when it is. The door checked the literal
848
874
  * parts; a reference may still resolve to a value that is no string.
849
875
  */
850
- function effectInputFault(keys: readonly string[], value: unknown): string | undefined {
876
+ function effectInputFault(shape: InputShape, value: unknown): string | undefined {
851
877
  const given = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : undefined;
878
+ const known = [...shape.required, ...shape.optional, ...(shape.oneOf ?? [])];
852
879
  const ok =
853
880
  given !== undefined &&
854
- given.length === keys.length &&
855
- keys.every((k) => typeof (value as Record<string, unknown>)[k] === "string");
881
+ shape.required.every((k) => given.includes(k)) &&
882
+ given.every((k) => known.includes(k) && typeof (value as Record<string, unknown>)[k] === "string") &&
883
+ (shape.oneOf === undefined || shape.oneOf.filter((k) => given.includes(k)).length === 1);
856
884
  return ok
857
885
  ? undefined
858
- : `its input resolved to ${JSON.stringify(value)}; it takes exactly ${keys.join(", ") || "nothing"}, each a string (ADR-0070)`;
886
+ : `its input resolved to ${JSON.stringify(value)}; it takes ${known.join(", ") || "nothing"}, each a string (ADR-0070)`;
859
887
  }
860
888
 
861
889
  const checkedEffects = new WeakMap<object, UnknownActorLogic>();
@@ -867,13 +895,15 @@ const checkedEffects = new WeakMap<object, UnknownActorLogic>();
867
895
  */
868
896
  function checkedEffect(slot: string, logic: UnknownActorLogic): UnknownActorLogic {
869
897
  if (!isEffect(logic)) return logic;
870
- const keys = inputKeysOf(logic.effect);
871
- if (keys === undefined) return logic;
898
+ const shape = inputShapeOf(logic.effect);
899
+ if (shape === undefined) return logic;
872
900
  const hit = checkedEffects.get(logic);
873
901
  if (hit) return hit;
874
902
  const checked = Object.create(logic) as UnknownActorLogic;
875
903
  checked.getInitialSnapshot = (scope, input) => {
876
- const fault = effectInputFault(keys, input);
904
+ // An Effect that takes nothing, invoked with no input, is a whole input: the door let it through.
905
+ const fault =
906
+ input === undefined && shape.required.length === 0 && !shape.oneOf ? undefined : effectInputFault(shape, input);
877
907
  if (fault === undefined) return logic.getInitialSnapshot(scope, input);
878
908
  return {
879
909
  status: "error",
@@ -885,9 +915,38 @@ function checkedEffect(slot: string, logic: UnknownActorLogic): UnknownActorLogi
885
915
  return checked;
886
916
  }
887
917
 
888
- /** The lent actors as a Chart invokes them: each Effect checked at its start (see {@link checkedEffect}). */
918
+ const checkedAgents = new WeakMap<object, UnknownActorLogic>();
919
+
920
+ /**
921
+ * An Agent as a Chart invokes it: the Agent itself, whose start refuses a resolved `cwd` that is a
922
+ * Repo Slot's folder. A reference hides that value from the door. The check is in `start`, because
923
+ * the Workspace's handles are registered at a start, and a Chart's first invoke resolves before
924
+ * that. The refusal is THAT invoke's error, as in {@link checkedEffect}.
925
+ */
926
+ function checkedAgent(slot: string, logic: UnknownActorLogic): UnknownActorLogic {
927
+ const hit = checkedAgents.get(logic);
928
+ if (hit) return hit;
929
+ const checked = Object.create(logic) as UnknownActorLogic;
930
+ checked.start = (snapshot, scope) => {
931
+ const cwd = ((snapshot as { input?: unknown }).input as { cwd?: unknown } | undefined)?.cwd;
932
+ const reads = (logic as { definition?: { workspace?: unknown } }).definition?.workspace !== "none";
933
+ const repos = reads && typeof cwd === "string" ? ambientHandlesFor(scope.self)?.repos : undefined;
934
+ const fault = repos ? slotFolderFault(cwd as string, Object.keys(repos)) : undefined;
935
+ if (fault !== undefined) throw new ChartInvokeFault(`the Agent "${slot}": ${fault}`);
936
+ logic.start?.(snapshot, scope);
937
+ };
938
+ checkedAgents.set(logic, checked);
939
+ return checked;
940
+ }
941
+
942
+ /** The lent actors as a Chart invokes them: each Effect and Agent checked at its start. */
889
943
  function chartActors(actors: Record<string, UnknownActorLogic>): Record<string, UnknownActorLogic> {
890
- return Object.fromEntries(Object.entries(actors).map(([slot, logic]) => [slot, checkedEffect(slot, logic)]));
944
+ return Object.fromEntries(
945
+ Object.entries(actors).map(([slot, logic]) => [
946
+ slot,
947
+ isAgent(logic) ? checkedAgent(slot, logic) : checkedEffect(slot, logic),
948
+ ]),
949
+ );
891
950
  }
892
951
 
893
952
  /** An Agent Turn's input with `prompt` and `cwd` as strings: a whole placeholder may resolve to
@@ -964,7 +1023,7 @@ export type CompileOptions = OpenBodyOptions & {
964
1023
  /**
965
1024
  * Build the Machine a Chart describes (ADR-0070), with `jr2Setup` over: the lent actors by their
966
1025
  * slot names, `agent(def)` for each Agent the Chart mints, the kit's `gate`, and `fail()` where the
967
- * body lends it; the lent guards, `jr2.sentBy` and `jr2.failure`; the lent actions and the four
1026
+ * body lends it; the lent guards, `jr2.sentBy`, `jr2.failure` and `jr2.ready`; the lent actions and the four
968
1027
  * generic ones; the lent delays. Every reference resolves at invoke time, so the Machine holds the
969
1028
  * text's shape and no run's data. The Chart's
970
1029
  * input is the context key {@link CHART_INPUT_KEY}; a done Chart outputs `{ outcome: "done",
@@ -984,7 +1043,7 @@ export function compileChart(
984
1043
  const mint = options.mintAgent ?? ((_slot: string, definition: AgentDefinition) => agent(definition));
985
1044
  const minted = mapValues(admitted.agents, (def) => def);
986
1045
  const actors: Record<string, UnknownActorLogic> = chartActors(lent.actors);
987
- for (const [slot, def] of Object.entries(minted)) actors[slot] = mint(slot, def);
1046
+ for (const [slot, def] of Object.entries(minted)) actors[slot] = checkedAgent(slot, mint(slot, def));
988
1047
  // The kit's Part that fails the run, where the body lends it (ADR-0073): no slot of the wrapper's.
989
1048
  if (decl.lends.fail === true) actors.fail = fail();
990
1049
  // The slots an invoke runs an Agent Turn in: the lent Agents and the Chart's own.
@@ -1018,7 +1077,11 @@ export function compileChart(
1018
1077
  events: admitted.events,
1019
1078
  actors,
1020
1079
  // `jr2.failure` is in every Chart, as `jr2.sentBy` is: a predicate, with no capability (ADR-0073).
1021
- guards: { ...(lent.guards as Record<string, unknown>), [FAILURE_GUARD]: failureGuard } as never,
1080
+ guards: {
1081
+ ...(lent.guards as Record<string, unknown>),
1082
+ [FAILURE_GUARD]: failureGuard,
1083
+ [READY_GUARD]: readyGuard,
1084
+ } as never,
1022
1085
  actions: { ...(lent.actions as Record<string, unknown>), ...GENERIC } as never,
1023
1086
  delays: lent.delays as never,
1024
1087
  }).createMachine(config as never) as unknown as AnyStateMachine;
@@ -1382,7 +1445,10 @@ export function openBodyLogic(decl: OpenBodyDeclaration, options: OpenBodyOption
1382
1445
  /** Admit and compile a Chart text, and record the Machine the actor runs. Throws ChartRefusal. */
1383
1446
  const bind = (self: AnyActorRef, text: unknown, inputKeys: readonly string[]) => {
1384
1447
  const lent = liveLentOf(self, logic);
1385
- const admitted = admitChart(text, decl, lent, { inputKeys });
1448
+ // A Chart that comes after the Workspace started finds its slots here. A Chart in the step that
1449
+ // starts the Workspace comes before them, and the wrapper's door or the Turn's start saw it.
1450
+ const repos = ambientHandlesFor(self)?.repos;
1451
+ const admitted = admitChart(text, decl, lent, { inputKeys, ...(repos ? { repoSlots: Object.keys(repos) } : {}) });
1386
1452
  const src = (self as { src?: unknown }).src;
1387
1453
  const machine = compiledFor(admitted, lent, decl, {
1388
1454
  ...options,
package/src/open.ts CHANGED
@@ -24,7 +24,18 @@
24
24
  * @example
25
25
  * ```ts
26
26
  * import { z } from "zod";
27
- * import { agent, customize, defineEvent, fail, jr2Setup, open, workspace } from "@jr2/orchestrator";
27
+ * import {
28
+ * agent,
29
+ * customize,
30
+ * defineEvent,
31
+ * fail,
32
+ * isReady,
33
+ * jr2Setup,
34
+ * open,
35
+ * workspace,
36
+ * worktree,
37
+ * type Workspaced,
38
+ * } from "@jr2/orchestrator";
28
39
  *
29
40
  * const finish = defineEvent({
30
41
  * name: "finish",
@@ -32,19 +43,40 @@
32
43
  * input: z.object({}),
33
44
  * });
34
45
  *
46
+ * type Input = Workspaced<{}, "app">;
47
+ *
48
+ * const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
49
+ *
35
50
  * const body = jr2Setup({
51
+ * types: {} as { context: Input; input: Input },
36
52
  * events: [finish],
37
- * actors: { coder: agent({ model: open, instructions: "You fix the failing test." }), fail: fail() },
53
+ * actors: {
54
+ * coder: agent({ model: open, instructions: "You fix the failing test." }),
55
+ * cut: worktree(),
56
+ * fail: fail(),
57
+ * },
38
58
  * }).createMachine({
39
59
  * id: "fix",
60
+ * context: ({ input }) => input,
40
61
  * on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
41
- * initial: "coding",
62
+ * initial: "cutting",
42
63
  * states: {
64
+ * cutting: {
65
+ * invoke: {
66
+ * src: "cut",
67
+ * input: ({ context }) => ({ path: fixTree(context), branch: "jr2/fix" }),
68
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
69
+ * },
70
+ * },
43
71
  * coding: {
44
- * invoke: { src: "coder", input: { prompt: "Make `node --test` pass." } },
72
+ * invoke: {
73
+ * src: "coder",
74
+ * input: ({ context }) => ({ prompt: "Make `node --test` pass.", cwd: fixTree(context) }),
75
+ * },
45
76
  * on: { finish: "done" },
46
77
  * },
47
78
  * done: { type: "final" },
79
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
48
80
  * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
49
81
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
50
82
  * },
@@ -53,7 +85,6 @@
53
85
  * // What a package exports: the repository and the model are Open.
54
86
  * export const fix = workspace(body, {
55
87
  * repos: { app: open },
56
- * spec: () => ({ branch: "jr2/fix" }),
57
88
  * });
58
89
  *
59
90
  * // What an Instance's `workflows/fix.ts` exports: the same Machine, its Open parts bound.
package/src/parts.ts CHANGED
@@ -56,7 +56,9 @@ import { isImageContext } from "./images.ts";
56
56
  import { isOpenBody, openBodyDeclarationOf, type OpenBodyDeclaration } from "./open-body.ts";
57
57
  import { open } from "./open.ts";
58
58
  import { repoIdentity } from "./repo-identity.ts";
59
+ import { refuseUnroutedOutcome } from "./effect-checks.ts";
59
60
  import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
61
+ import { refuseSlotActiveTwice } from "./party.ts";
60
62
  import { refuseLostOutput } from "./run-output.ts";
61
63
  import { userSeatOf, type Size, type UserContainer } from "./size.ts";
62
64
 
@@ -75,9 +77,9 @@ import { userSeatOf, type Size, type UserContainer } from "./size.ts";
75
77
  export { open } from "./open.ts";
76
78
 
77
79
  /**
78
- * The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch
79
- * or commit that the branch Worktree starts from; absent, the Repo's default branch. Where only a
80
- * url is needed, a string is enough.
80
+ * The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch,
81
+ * the tag or the commit that the clone stays on, and that a new branch starts from; absent, the
82
+ * Repo's default branch. Where only a url is needed, a string is enough.
81
83
  *
82
84
  * `workspace()` and `customize()` refuse:
83
85
  * - an empty `url`: ``<where>: Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot
@@ -102,7 +104,6 @@ export { open } from "./open.ts";
102
104
  *
103
105
  * export const machine = workspace(body, {
104
106
  * repos: { app },
105
- * spec: () => ({ branch: "jr2/hotfix" }),
106
107
  * });
107
108
  * ```
108
109
  *
@@ -113,8 +114,8 @@ export { open } from "./open.ts";
113
114
  export type Binding = {
114
115
  /** The Repo's git url. */
115
116
  url: string;
116
- /** The branch or commit that the branch Worktree starts from. Default: the Repo's default
117
- * branch. */
117
+ /** The branch, tag or commit that the clone stays on, and that a new branch starts from. Default:
118
+ * the Repo's default branch. */
118
119
  ref?: string;
119
120
  };
120
121
 
@@ -130,10 +131,11 @@ export type Binding = {
130
131
  * - a function of the run input, `({ input }) => url` or a {@link Binding}: each run chooses. The
131
132
  * url must match an entry of `git.credentials` in `jr2.config.ts`, or the run cannot use it.
132
133
  *
133
- * The key of the slot is a directory name under `/work`. It starts with a letter, and
134
- * `workspace()` and `customize()` refuse a key that does not: ``<where>: Repo Slot key "<key>" is
135
- * not a directory name — a slot becomes `/work/<slot>` and its order matters, so it starts with a
136
- * letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).`` They refuse a value of another
134
+ * The key of the slot is a directory name under `/work`. It starts with a letter, so never with
135
+ * `.`: `/work/.jr2/` is reserved for jr2. `workspace()` and `customize()` refuse a key that does
136
+ * not: ``<where>: Repo Slot key "<key>" is not a directory name — a slot becomes `/work/<slot>`,
137
+ * `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter and matches
138
+ * /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).`` They refuse a value of another
137
139
  * shape: ``<where>: Repo Slot "<slot>" is <what> — a slot is `open`, a url, `{ url, ref? }`, or a
138
140
  * mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
139
141
  *
@@ -163,7 +165,6 @@ export type Binding = {
163
165
  * docs: "https://github.com/you/docs.git",
164
166
  * config: open,
165
167
  * },
166
- * spec: ({ input }) => ({ branch: `deploy/${input.service}` }),
167
168
  * });
168
169
  * ```
169
170
  *
@@ -195,12 +196,13 @@ export function repoSlotState(value: RepoSlot<any>): RepoSlotState {
195
196
  * carry — and to what a prompt can name without quoting. It starts with a LETTER: the handles
196
197
  * keep declaration order, which a Machine may give a meaning to (`task`: the first slot is the
197
198
  * one the coder edits), and JS puts an integer-like key such as `"1"` ahead of every other key in
198
- * `Object.keys`, wherever the author wrote it. */
199
+ * `Object.keys`, wherever the author wrote it. So a key never starts with `.`, and `/work/.jr2/`
200
+ * stays jr2's (ADR-0075). */
199
201
  export const REPO_SLOT_KEY = /^[A-Za-z][A-Za-z0-9._-]*$/;
200
202
 
201
203
  /**
202
- * Refuse a malformed slot value BY NAME, at build time — the same derives-from-a-typo class the
203
- * spec guard catches for the branch, one build earlier. A bound url is also parsed here: an
204
+ * Refuse a malformed slot value BY NAME, at build time — a derives-from-a-typo fault, one build
205
+ * earlier. A bound url is also parsed here: an
204
206
  * identity the walk cannot derive (a relative path, an unknown scheme) is refused where the
205
207
  * author wrote it rather than at the converge that walks it. `where` names the caller
206
208
  * (`workspace()`, `customize()`).
@@ -209,8 +211,8 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
209
211
  if (!REPO_SLOT_KEY.test(slot)) {
210
212
  throw new Error(
211
213
  `${where}: Repo Slot key ${JSON.stringify(slot)} is not a directory name — a slot becomes ` +
212
- "`/work/<slot>` and its order matters, so it starts with a letter and matches " +
213
- "/^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).",
214
+ "`/work/<slot>`, `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter " +
215
+ "and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).",
214
216
  );
215
217
  }
216
218
  if (value === open || typeof value === "function") return;
@@ -242,9 +244,8 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
242
244
  * to a concrete ref on the port's side, which is what keeps the Machine cluster-agnostic. The
243
245
  * Repos are Slots, each bound, open, or per-run.
244
246
  *
245
- * They are options rather than `WorkspaceSpec` fields because they are STATIC: `jr2 up` must find
246
- * them by walking the Machine, and a per-run spec is a function of run input that no walk can
247
- * evaluate.
247
+ * They are static options because `jr2 up` must find them by walking the Machine, and no walk can
248
+ * evaluate a function of run input.
248
249
  */
249
250
  export type SandboxParts = {
250
251
  /** The Sandbox Image. Absent → the Instance's `images/default`, then the stock Harness. */
@@ -258,6 +259,8 @@ export type SandboxParts = {
258
259
  /** The Size the Machine states (ADR-0060) — the `workspace()`'s, with any `customize()` layered
259
260
  * over it. Absent → the Instance's `sandbox.resources`, then the kit default. */
260
261
  resources?: Size;
262
+ /** The pod's work group (ADR-0005). Absent: the port's default. */
263
+ workGroup?: number;
261
264
  /** The Held secrets this Workspace's pod gets beside its Agents' model keys (ADR-0059): names of
262
265
  * the `secrets` list. Naming is placement — no other pod gets a name for being listed here. */
263
266
  secrets?: string[];
@@ -561,14 +564,15 @@ export type CarriedEffect = {
561
564
  (| {
562
565
  kind: "command";
563
566
  argv: string[];
564
- slot?: string;
565
567
  secrets?: string[];
566
568
  pin?: true;
567
569
  open?: Array<"argv" | "secret">;
570
+ /** A `worktree()`: its `expect`. A report prints it in place of the kit program's argv. */
571
+ cut?: { expect?: "new" | "existing" };
568
572
  }
569
573
  /** An http Effect (ADR-0068): its method, its fixed target, and the secrets it names — what
570
574
  * places each secret, and what its Effector's config allows. `file`: its body is a Worktree
571
- * file (ADR-0067), so it runs in its Workspace's Sandbox Effector, on Repo Slot `slot` when named.
575
+ * file (ADR-0067), so it runs in its Workspace's Sandbox Effector.
572
576
  * An Open url has no target, and names only the secrets its headers and auth name. */
573
577
  | {
574
578
  kind: "http";
@@ -577,10 +581,9 @@ export type CarriedEffect = {
577
581
  secrets: string[];
578
582
  idempotent: boolean;
579
583
  file?: true;
580
- slot?: string;
581
584
  open?: Array<"url" | "secret">;
582
585
  }
583
- /** A push (ADR-0067, ADR-0051): the Workspace's branch to its Repo Slot's Binding. It names no
586
+ /** A push (ADR-0067, ADR-0075): a cut Worktree's branch to its Repo Slot's Binding. It names no
584
587
  * secret: `jr2 up` resolves its credential through `git.credentials`. */
585
588
  | { kind: "push"; slot?: string; sign: boolean; squash: boolean; force: boolean; pin: boolean; secrets: string[] }
586
589
  /** A dispatch (ADR-0071): the Workflow it starts a run of, as `jr2 run` names it, whether it
@@ -808,6 +811,9 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
808
811
  refuseInlineParts(machine);
809
812
  refuseLostOutput(machine);
810
813
  refuseUnroutedAgentFault(machine);
814
+ refuseUnroutedOutcome(machine);
815
+ // Here too, for a Machine built with xstate's own setup: the Turns' identity is the runtime's.
816
+ refuseSlotActiveTwice(machine);
811
817
  const parts = sandboxPartsOf(machine);
812
818
  const user = userSeatOf(parts.user);
813
819
  collectImage(parts.image);
@@ -968,7 +974,6 @@ export function carriedEffectOf(
968
974
  idempotent: parts.idempotent,
969
975
  timeoutMs: parts.timeoutMs,
970
976
  ...(parts.file ? { file: true as const } : {}),
971
- ...(parts.file?.slot !== undefined ? { slot: parts.file.slot } : {}),
972
977
  };
973
978
  }
974
979
  if (parts.kind === "push") {
@@ -988,7 +993,7 @@ export function carriedEffectOf(
988
993
  ...(slot !== undefined ? { slot } : {}),
989
994
  };
990
995
  }
991
- const { argv, timeoutMs, slot, secrets, pin } = parts;
996
+ const { argv, timeoutMs, secrets, pin } = parts;
992
997
  return {
993
998
  name,
994
999
  path,
@@ -997,10 +1002,10 @@ export function carriedEffectOf(
997
1002
  placement,
998
1003
  argv: argvText(argv),
999
1004
  timeoutMs,
1000
- ...(slot !== undefined ? { slot } : {}),
1001
1005
  ...(secrets?.length ? { secrets: [...secrets] } : {}),
1002
1006
  ...(pin ? { pin } : {}),
1003
1007
  ...(openParts.length ? { open: openParts as Array<"argv" | "secret"> } : {}),
1008
+ ...(parts.cut ? { cut: { ...parts.cut } } : {}),
1004
1009
  };
1005
1010
  }
1006
1011
 
@@ -1039,6 +1044,9 @@ export type WorkspacePod = {
1039
1044
  * credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
1040
1045
  * their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
1041
1046
  effects: CarriedEffect[];
1047
+ /** The slot of each `worktree()` below the `workspace()`, down to the next `workspace()`. A cut runs
1048
+ * in the Harness container, so `effects` does not hold it. Absent when there is none. */
1049
+ cuts?: string[];
1042
1050
  /** The `workspace()`'s `effector: { image }`, as written. Absent → the Sandbox Image. */
1043
1051
  effector?: { image: string };
1044
1052
  /** The `workspace()`'s own `image`, as written — its Sandbox Image. Absent → `images/default`,
@@ -1067,6 +1075,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1067
1075
  models: Set<string>,
1068
1076
  on: Set<AnyStateMachine>,
1069
1077
  effects: Map<string, CarriedEffect>,
1078
+ cuts: Set<string>,
1070
1079
  path: OpenSlot["path"],
1071
1080
  route: PlacementRoute,
1072
1081
  ): void => {
@@ -1074,7 +1083,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1074
1083
  on.add(machine);
1075
1084
  const body = wrapperBodyOf(machine);
1076
1085
  const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
1077
- if (!composesSandbox(child)) modelsIn(child, models, on, effects, at, to);
1086
+ if (!composesSandbox(child)) modelsIn(child, models, on, effects, cuts, at, to);
1078
1087
  };
1079
1088
  for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
1080
1089
  if (isAgent(logic)) {
@@ -1093,6 +1102,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1093
1102
  if (isEffect(logic)) {
1094
1103
  const effect = carriedEffectOf(name, path, route, logic.effect);
1095
1104
  if (effect.placement === "sandbox") effects.set(JSON.stringify([route, name]), effect);
1105
+ if (logic.effect.kind === "command" && logic.effect.cut) cuts.add(name);
1096
1106
  continue;
1097
1107
  }
1098
1108
  const child = asMachine(logic);
@@ -1112,7 +1122,8 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1112
1122
  if (composesSandbox(machine)) {
1113
1123
  const models = new Set<string>();
1114
1124
  const effects = new Map<string, CarriedEffect>();
1115
- modelsIn(machine, models, new Set(), effects, path, route);
1125
+ const cuts = new Set<string>();
1126
+ modelsIn(machine, models, new Set(), effects, cuts, path, route);
1116
1127
  const parts = sandboxPartsOf(machine);
1117
1128
  const bindings: Record<string, string | undefined> = {};
1118
1129
  if (parts.repos !== open) {
@@ -1127,6 +1138,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1127
1138
  secrets: [...(parts.secrets ?? [])],
1128
1139
  models: [...models].sort(),
1129
1140
  effects: [...effects.values()],
1141
+ ...(cuts.size ? { cuts: [...cuts] } : {}),
1130
1142
  ...(parts.effector ? { effector: { ...parts.effector } } : {}),
1131
1143
  ...(parts.image !== undefined ? { image: parts.image } : {}),
1132
1144
  bindings,
package/src/party.ts CHANGED
@@ -422,6 +422,39 @@ function orthogonal(a: Node, b: Node): boolean {
422
422
  /** Can the two states be active at the same time? */
423
423
  const together = (a: Node, b: Node): boolean => isAncestorOrSelf(a, b) || isAncestorOrSelf(b, a) || orthogonal(a, b);
424
424
 
425
+ /**
426
+ * Refuse one Agent slot invoked by two states that can be active together (ADR-0015): in two
427
+ * parallel regions, one state inside the other, or twice by one state. A slot is one Agent of this
428
+ * Machine: its Turns share one sender, so `sentBy` cannot tell them apart and one pick matches both
429
+ * writings, and they share one conversation, so `continue` cannot hold two. Two Agents at once are
430
+ * two slots (one definition can fill both), a child Machine in each region, or a Pool (ADR-0075).
431
+ */
432
+ export function refuseSlotActiveTwice(machine: AnyStateMachine): void {
433
+ const bySlot = new Map<string, Node[]>();
434
+ for (const node of allNodes(machine))
435
+ for (const p of partiesAt(node, machine))
436
+ if (p.party === "agent") bySlot.set(p.id, [...(bySlot.get(p.id) ?? []), node]);
437
+ for (const [slot, nodes] of bySlot) {
438
+ for (let i = 0; i < nodes.length; i++) {
439
+ for (let j = i + 1; j < nodes.length; j++) {
440
+ const [a, b] = [nodes[i]!, nodes[j]!];
441
+ if (!together(a, b)) continue;
442
+ const how =
443
+ a === b
444
+ ? `state "${a.id}" invokes it twice`
445
+ : `"${a.id}" and "${b.id}" invoke it and can be active together ` +
446
+ (orthogonal(a, b) ? "(parallel regions)" : "(one inside the other)");
447
+ throw new Error(
448
+ `machine "${machine.id}": Agent "${slot}" would run two Turns at once: ${how}. A slot is one Agent, with ` +
449
+ "one sender and one conversation, so `sentBy` cannot tell its two Turns apart and `continue` cannot " +
450
+ "hold both. For two Agents at once, use two slots (one definition can fill both), a child Machine in " +
451
+ "each region, or a Pool (ADR-0075)",
452
+ );
453
+ }
454
+ }
455
+ }
456
+ }
457
+
425
458
  /** One writing: one `on` key on one state, with its candidates (an array writing has several). */
426
459
  type Writing = { node: Node; name: string; candidates: Transition[] };
427
460