@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/pick.ts CHANGED
@@ -63,9 +63,11 @@ import type { PickAnswer } from "./wire.ts";
63
63
  * command,
64
64
  * defineEvent,
65
65
  * fail,
66
+ * isReady,
66
67
  * jr2Setup,
67
68
  * paramsOf,
68
69
  * workspace,
70
+ * worktree,
69
71
  * type Workspaced,
70
72
  * } from "@jr2/orchestrator";
71
73
  *
@@ -83,6 +85,8 @@ import type { PickAnswer } from "./wire.ts";
83
85
  *
84
86
  * type Input = Workspaced<{}, "app">;
85
87
  *
88
+ * const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
89
+ *
86
90
  * const body = jr2Setup({
87
91
  * types: {} as { context: Input; input: Input },
88
92
  * events: [runTest, finished],
@@ -91,6 +95,7 @@ import type { PickAnswer } from "./wire.ts";
91
95
  * model: "anthropic/claude-haiku-5-5",
92
96
  * instructions: "You are a careful engineer.",
93
97
  * }),
98
+ * cut: worktree(),
94
99
  * test: command(["node", "--test", arg("file")]),
95
100
  * fail: fail(),
96
101
  * },
@@ -98,12 +103,22 @@ import type { PickAnswer } from "./wire.ts";
98
103
  * id: "fix-tests",
99
104
  * context: ({ input }) => input,
100
105
  * on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
101
- * initial: "coding",
106
+ * initial: "cutting",
102
107
  * states: {
108
+ * cutting: {
109
+ * invoke: {
110
+ * src: "cut",
111
+ * input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
112
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
113
+ * },
114
+ * },
103
115
  * coding: {
104
116
  * invoke: {
105
117
  * src: "coder",
106
- * input: { prompt: "Make the tests pass. Run the tests of one module at a time." },
118
+ * input: ({ context }) => ({
119
+ * prompt: "Make the tests pass. Run the tests of one module at a time.",
120
+ * cwd: fixTree(context),
121
+ * }),
107
122
  * },
108
123
  * on: { finished: "done" },
109
124
  * initial: "ready",
@@ -113,13 +128,17 @@ import type { PickAnswer } from "./wire.ts";
113
128
  * invoke: {
114
129
  * src: "test",
115
130
  * // A plain mapper: the Machine makes the file name from the pick's `module`.
116
- * input: ({ event }) => ({ file: `test/${paramsOf(event, runTest).module}.test.js` }),
131
+ * input: ({ context, event }) => ({
132
+ * file: `test/${paramsOf(event, runTest).module}.test.js`,
133
+ * worktree: fixTree(context),
134
+ * }),
117
135
  * onDone: "ready",
118
136
  * },
119
137
  * },
120
138
  * },
121
139
  * },
122
140
  * done: { type: "final" },
141
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
123
142
  * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
124
143
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
125
144
  * },
@@ -127,7 +146,6 @@ import type { PickAnswer } from "./wire.ts";
127
146
  *
128
147
  * export const machine = workspace(body, {
129
148
  * repos: { app: { url: "https://github.com/you/app.git" } },
130
- * spec: () => ({ branch: "fix-tests" }),
131
149
  * });
132
150
  * ```
133
151
  *
@@ -640,8 +658,10 @@ export const isAnswer = (action: unknown): boolean =>
640
658
  * command,
641
659
  * defineEvent,
642
660
  * fail,
661
+ * isReady,
643
662
  * jr2Setup,
644
663
  * workspace,
664
+ * worktree,
645
665
  * type Workspaced,
646
666
  * } from "@jr2/orchestrator";
647
667
  *
@@ -659,6 +679,8 @@ export const isAnswer = (action: unknown): boolean =>
659
679
  *
660
680
  * type Input = Workspaced<{}, "app">;
661
681
  *
682
+ * const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
683
+ *
662
684
  * const body = jr2Setup({
663
685
  * types: {} as { context: Input; input: Input },
664
686
  * events: [runTests, finished],
@@ -667,6 +689,7 @@ export const isAnswer = (action: unknown): boolean =>
667
689
  * model: "anthropic/claude-haiku-5-5",
668
690
  * instructions: "You are a careful engineer.",
669
691
  * }),
692
+ * cut: worktree(),
670
693
  * tests: command(["npm", "test"]),
671
694
  * fail: fail(),
672
695
  * },
@@ -674,10 +697,20 @@ export const isAnswer = (action: unknown): boolean =>
674
697
  * id: "fix-tests",
675
698
  * context: ({ input }) => input,
676
699
  * on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
677
- * initial: "coding",
700
+ * initial: "cutting",
678
701
  * states: {
702
+ * cutting: {
703
+ * invoke: {
704
+ * src: "cut",
705
+ * input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
706
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
707
+ * },
708
+ * },
679
709
  * coding: {
680
- * invoke: { src: "coder", input: { prompt: "Make the tests pass." } },
710
+ * invoke: {
711
+ * src: "coder",
712
+ * input: ({ context }) => ({ prompt: "Make the tests pass.", cwd: fixTree(context) }),
713
+ * },
681
714
  * on: { finished: "done" },
682
715
  * initial: "ready",
683
716
  * states: {
@@ -685,6 +718,7 @@ export const isAnswer = (action: unknown): boolean =>
685
718
  * testing: {
686
719
  * invoke: {
687
720
  * src: "tests",
721
+ * input: ({ context }) => ({ worktree: fixTree(context) }),
688
722
  * onDone: {
689
723
  * target: "ready",
690
724
  * // The Agent reads this string, not the whole output of the command.
@@ -699,6 +733,7 @@ export const isAnswer = (action: unknown): boolean =>
699
733
  * },
700
734
  * },
701
735
  * done: { type: "final" },
736
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
702
737
  * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
703
738
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
704
739
  * },
@@ -706,7 +741,6 @@ export const isAnswer = (action: unknown): boolean =>
706
741
  *
707
742
  * export const machine = workspace(body, {
708
743
  * repos: { app: { url: "https://github.com/you/app.git" } },
709
- * spec: () => ({ branch: "fix-tests" }),
710
744
  * });
711
745
  * ```
712
746
  *
package/src/pool.ts CHANGED
@@ -38,6 +38,7 @@ import type { EventDef } from "@jr2/agent-protocol";
38
38
  import { fail, failurePayload, isFailure, type FailureInput } from "./fail.ts";
39
39
  import { gate } from "./gate.ts";
40
40
  import { attachWrapperBody, type JR2Wrapper, type WrapperActors } from "./parts.ts";
41
+ import { parsedNestedDoor } from "./vocabulary.ts";
41
42
  import { attachInputSchema, attachVocabulary } from "./vocabulary.ts";
42
43
 
43
44
  // The rich shape's `open` is the one bit that separates DRAINED from DEADLOCKED when the pool goes
@@ -50,22 +51,27 @@ import { attachInputSchema, attachVocabulary } from "./vocabulary.ts";
50
51
  */
51
52
  export type SourceNextResult<T> = T | null | { item: T | null; open: number };
52
53
 
54
+ /** The input of a Source's `next`: the ids of the items that workers have now, the ids of the items
55
+ * that settled, and the Pool's input. So a Source over a list that a Machine holds needs no state
56
+ * of its own. */
57
+ export type PoolNextInput<TInput = unknown> = { active: string[]; settled: string[]; input: TInput };
58
+
53
59
  // The Source port (ADR-0017).
54
60
  /** Where a Pool gets its work items: the Source. Make one with {@link source}. */
55
- export type SourceSpec<T> = {
61
+ export type SourceSpec<T, TInput = unknown> = {
56
62
  // A union of the accepted output shapes because xstate's actor-logic type is invariant in its
57
63
  // output — a `fromPromise` inferred as `T | null` must still assign.
58
64
  /** Claim the next ready item. Write it as an xstate promise actor,
59
- * `fromPromise(async ({ input }) => …)`. Its input is `{ active }`: the ids of the items that
60
- * workers have now, which it must not give again. It must also not give again an item that a
61
- * worker finished. It resolves to a {@link SourceNextResult}. The Pool calls it when the run
62
- * starts, after each claim, and when a worker settles. While workers run and no item is ready, it
65
+ * `fromPromise(async ({ input }) => …)`. Its input is {@link PoolNextInput}: `active`, the ids of
66
+ * the items that workers have now, `settled`, the ids of the items that finished or failed, and
67
+ * `input`, the Pool's input. It must not give again an item in `active` or `settled`. It resolves
68
+ * to a {@link SourceNextResult}. 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
63
69
  * also calls it on `wake` and every `pollEvery`. */
64
70
  next:
65
- | PromiseActorLogic<SourceNextResult<T>, { active: string[] }>
66
- | PromiseActorLogic<{ item: T | null; open: number }, { active: string[] }>
67
- | PromiseActorLogic<T | null, { active: string[] }>
68
- | PromiseActorLogic<null, { active: string[] }>;
71
+ | PromiseActorLogic<SourceNextResult<T>, PoolNextInput<TInput>>
72
+ | PromiseActorLogic<{ item: T | null; open: number }, PoolNextInput<TInput>>
73
+ | PromiseActorLogic<T | null, PoolNextInput<TInput>>
74
+ | PromiseActorLogic<null, PoolNextInput<TInput>>;
69
75
  // The webhook / `jr2 send` push seam. The def IS the pool machine's vocabulary (ADR-0011).
70
76
  /** An external event, made with `defineEvent`, that tells the Pool that new work is ready, for
71
77
  * example from a webhook or `jr2 send`. While workers run and no item is ready, it makes the Pool
@@ -91,7 +97,7 @@ export type SourceSpec<T> = {
91
97
  * @example
92
98
  * ```ts
93
99
  * import { fromPromise } from "xstate";
94
- * import { source } from "@jr2/orchestrator";
100
+ * import { source, type PoolNextInput } from "@jr2/orchestrator";
95
101
  *
96
102
  * type Issue = { id: string; title: string };
97
103
  *
@@ -101,7 +107,7 @@ export type SourceSpec<T> = {
101
107
  * ];
102
108
  *
103
109
  * export const issues = source<Issue>({
104
- * next: fromPromise(async ({ input }: { input: { active: string[] } }) => {
110
+ * next: fromPromise(async ({ input }: { input: PoolNextInput }) => {
105
111
  * const issue = backlog.find((i) => !input.active.includes(i.id)) ?? null;
106
112
  * if (issue) backlog.splice(backlog.indexOf(issue), 1);
107
113
  * return issue;
@@ -114,7 +120,7 @@ export type SourceSpec<T> = {
114
120
  * @see docs/concepts/pools-and-sources.md
115
121
  * @see docs/reference/decisions/0017-pool-over-a-source-port.md
116
122
  */
117
- export function source<T>(spec: SourceSpec<T>): SourceSpec<T> {
123
+ export function source<T, TInput = unknown>(spec: SourceSpec<T, TInput>): SourceSpec<T, TInput> {
118
124
  return spec;
119
125
  }
120
126
 
@@ -143,8 +149,10 @@ export type PoolSpec<T, TInput = unknown> = {
143
149
  * `itemInput` read it, typed by it. A worker never gets it: each worker gets its item. Default:
144
150
  * none, and then their `input` is `unknown`. */
145
151
  input?: z.ZodObject;
152
+ // `SourceSpec<T>` too: xstate's actor logic is invariant in its input, so a `next` that does not
153
+ // read the Pool's input must still assign under a typed door.
146
154
  /** Where the items come from. Make it with {@link source}. */
147
- source: SourceSpec<T>;
155
+ source: SourceSpec<T, TInput> | SourceSpec<T>;
148
156
  // Stable child identity: the spawn id, the active-set entry, the outcome key.
149
157
  /** The id of an item. The Pool uses it as the id of the item's worker and in the `active` list
150
158
  * that `next` gets. It is also the key of that worker's output in `items`. Give each item an id
@@ -250,7 +258,7 @@ function normalizeClaim(output: unknown): Claim {
250
258
  * @example
251
259
  * ```ts
252
260
  * import { fromPromise } from "xstate";
253
- * import { customize, pool, source } from "@jr2/orchestrator";
261
+ * import { customize, pool, source, type PoolNextInput } from "@jr2/orchestrator";
254
262
  * import { task } from "@jr2/machines";
255
263
  *
256
264
  * type Issue = { id: string; title: string };
@@ -261,7 +269,7 @@ function normalizeClaim(output: unknown): Claim {
261
269
  * ];
262
270
  *
263
271
  * const issues = source<Issue>({
264
- * next: fromPromise(async ({ input }: { input: { active: string[] } }) => {
272
+ * next: fromPromise(async ({ input }: { input: PoolNextInput }) => {
265
273
  * const issue = backlog.find((i) => !input.active.includes(i.id)) ?? null;
266
274
  * if (issue) backlog.splice(backlog.indexOf(issue), 1);
267
275
  * return issue;
@@ -325,13 +333,22 @@ export function pool(worker: AnyStateMachine, spec: PoolSpec<any, any>): AnyStat
325
333
  actors: { worker, next: spec.source.next, gate, fail: fail() },
326
334
  }).createMachine({
327
335
  id: spec.id ?? "pool",
328
- context: ({ input }): PoolCtx => ({
329
- runInput: (input ?? {}) as Record<string, unknown>,
330
- cap: typeof spec.cap === "function" ? spec.cap({ input }) : (spec.cap ?? 1),
331
- active: [],
332
- items: {},
333
- failures: {},
334
- }),
336
+ context: ({ input, self }): PoolCtx => {
337
+ // RunHost parsed a root run's door. A parent's input is parsed here, and a value that is
338
+ // wrong starts no worker (ADR-0033). The handles and `instanceId` are no caller's to send.
339
+ const sent = (input ?? {}) as Record<string, unknown>;
340
+ const runInput =
341
+ spec.input && self._parent
342
+ ? parsedNestedDoor(spec.input, sent, ["workspace", "instanceId"], "pool input invalid, before any worker")
343
+ : sent;
344
+ return {
345
+ runInput,
346
+ cap: typeof spec.cap === "function" ? spec.cap({ input: runInput as never }) : (spec.cap ?? 1),
347
+ active: [],
348
+ items: {},
349
+ failures: {},
350
+ };
351
+ },
335
352
  // The wake seam: a standing gate for the whole run's life (root invokes stop only at final).
336
353
  // The invoke id IS the gate's name (the id derives from the actor path — ADR-0011):
337
354
  // `source` when the pool is the root, `<path>.source` nested — so two nested
@@ -386,7 +403,11 @@ export function pool(worker: AnyStateMachine, spec: PoolSpec<any, any>): AnyStat
386
403
  invoke: {
387
404
  id: "discover",
388
405
  src: "next",
389
- input: ({ context }) => ({ active: (context as PoolCtx).active }),
406
+ input: ({ context }) => ({
407
+ active: (context as PoolCtx).active,
408
+ settled: [...Object.keys((context as PoolCtx).items), ...Object.keys((context as PoolCtx).failures)],
409
+ input: (context as PoolCtx).runInput,
410
+ }),
390
411
  onDone: [
391
412
  {
392
413
  // Claimed: record it, spawn its worker (TOP-LEVEL spawnChild — the visualize
@@ -30,7 +30,7 @@ import { SENDER_KEY, type EventDef, type Sender } from "@jr2/agent-protocol";
30
30
  import type { AgentAdmission } from "./actor.ts";
31
31
  import type { EffectMarker, EffectRow } from "./effect.ts";
32
32
  import type { EffectorPlacement, EffectorTarget } from "./http-effect.ts";
33
- import type { NoticeScope, TakenNotices } from "./notices.ts";
33
+ import type { NoticeScope, RaiseOptions, TakenNotices } from "./notices.ts";
34
34
  import type { AnswerSeat } from "./pick.ts";
35
35
  import type { Notice, PickAnswer } from "./wire.ts";
36
36
  import type { SandboxPort } from "./workspace.ts";
@@ -82,6 +82,20 @@ export type Registration = {
82
82
  * Harness runs on the host, in no Sandbox at all): those are the Instance token's business.
83
83
  */
84
84
  sandbox?: string;
85
+ /**
86
+ * An Agent's: its SLOT (ADR-0076) — the actor path of its Machine and its agent name, which is its
87
+ * continued conversation's Instance ID without the run id ({@link continuedIid}). The Harness
88
+ * states it on every control request, from its own record of the conversation, and the
89
+ * Orchestrator speaks here only for that slot. Two Agents with one key in two child Machines are
90
+ * two slots. Absent on gates.
91
+ */
92
+ slot?: string;
93
+ /** An Agent's: the step boundary's read of its conversation's pending notices (ADR-0062), each
94
+ * reserved for this Turn until {@link noticesHeard} or the Turn's end releases it. */
95
+ boundaryNotices?: () => Promise<Array<{ id: string; notice: Notice }>>;
96
+ /** An Agent's: the Harness appended these step-boundary notices — they are heard, ledgered in one
97
+ * save. Answers how many were this Turn's reservations. */
98
+ noticesHeard?: (ids: readonly string[]) => number;
85
99
  /** Close over the invoking state's `sendBack`; delivery lands where the actor was invoked. */
86
100
  deliver: (event: DeliveredEvent) => void;
87
101
  /** An Agent's: the Harness read this surface (`GET /agents/:iid/surface`), so it has the Turn in
@@ -313,9 +327,15 @@ export function actorPath(ref: AnyActorRef): string[] {
313
327
  * Turn adds a random suffix, a burned conversation's successor its epoch), and the Agent's Actor
314
328
  * names the conversation it burns with it.
315
329
  */
316
- export function continuedIid(machine: AnyActorRef | undefined, runId: string, agentName: string): string {
330
+ /** An Agent's slot (ADR-0076): its continued conversation's Instance ID without the run id —
331
+ * `<machine actor path>/<agent>`. What the Harness states and the registration records. */
332
+ export function agentSlot(machine: AnyActorRef | undefined, agentName: string): string {
317
333
  const path = (machine ? actorPath(machine).join(".") : "") || "root";
318
- return `${runId}/${path}/${agentName}`;
334
+ return `${path}/${agentName}`;
335
+ }
336
+
337
+ export function continuedIid(machine: AnyActorRef | undefined, runId: string, agentName: string): string {
338
+ return `${runId}/${agentSlot(machine, agentName)}`;
319
339
  }
320
340
 
321
341
  /** The address of a run's gate: gate ids are run-scoped by construction (ADR-0011). */
@@ -488,9 +508,18 @@ export type RunBinding = {
488
508
  * Record an Agent invocation's durable admission in the host ledger (ADR-0016): persisted
489
509
  * beside the snapshot in the same RunBlob save, keyed by iid (globally unique, so the map is
490
510
  * flat). Optional so a bare unit-test binding can omit it — then admissions simply are not
491
- * durable.
511
+ * durable. `delivered` are the notices the admission carried, delivered by this save in `taker`'s
512
+ * conversation — the Instance ID the admission rides (ADR-0062). `member`: the Workspace the
513
+ * conversation was admitted in, its slot and whether it continues — so it is in the audience of
514
+ * every kill there from now on, in the same save.
492
515
  */
493
- recordAdmission?: (instanceId: string, admission: AgentAdmission, delivered?: readonly string[]) => void;
516
+ recordAdmission?: (
517
+ instanceId: string,
518
+ admission: AgentAdmission,
519
+ delivered?: readonly string[],
520
+ taker?: string,
521
+ member?: { workspace: string; slot: string; continued: boolean },
522
+ ) => void;
494
523
  /**
495
524
  * The admission ledger's READ half (ADR-0016): the last admission ledgered under this iid. A
496
525
  * continued Turn asks it whether its conversation was ever admitted — only then can the Harness
@@ -521,14 +550,27 @@ export type RunBinding = {
521
550
  * `older` names earlier events this one tells the same as, marked raised with it (`NoticeLedger`).
522
551
  * A burned conversation's notice is raised by `bumpEpoch` itself, in the same save.
523
552
  */
524
- raiseNotice?: (notice: Notice, to: string, source?: string, older?: readonly string[]) => void;
553
+ raiseNotice?: (notice: Notice, to: string, source?: string, older?: readonly string[], opts?: RaiseOptions) => void;
525
554
  /** Put back a notice an admission carried that no model heard, WITH its source (`NoticeLedger.restore`):
526
- * a raise would drop it, its source being seen. Persisted at the write. */
527
- restoreNotice?: (notice: Notice, to: string, source?: string) => void;
528
- /** Reserve the pending notices in one admission's scope. They are delivered by the
529
- * `recordAdmission` that ledgers it (its `delivered` ids), or released if the admission fails. */
555
+ * a raise would drop it, its source being seen. A Workspace notice is pending again for `taker`,
556
+ * the conversation that did not hear it. Persisted at the write. */
557
+ restoreNotice?: (notice: Notice, to: string, source?: string, taker?: string) => void;
558
+ /** Reserve the pending notices in one admission's scope, for its conversation (`scope.taker`).
559
+ * They are delivered by the `recordAdmission` that ledgers it (its `delivered` ids and `taker`),
560
+ * or released for that conversation if the admission fails. */
530
561
  takeNotices?: (scope: NoticeScope) => TakenNotices;
531
- releaseNotices?: (ids: readonly string[]) => void;
562
+ releaseNotices?: (ids: readonly string[], taker?: string) => void;
563
+ /** What `takeNotices` would reserve in this scope, reserving nothing (`NoticeLedger.peek`). */
564
+ peekNotices?: (scope: NoticeScope) => Array<{ id: string; notice: Notice; source?: string }>;
565
+ /** The Harness acknowledged a step boundary's notices (ADR-0062): each that `taker` reserved is
566
+ * heard there, persisted at the write. Answers how many were. */
567
+ acknowledgeNotices?: (ids: readonly string[], taker: string) => number;
568
+ /** The conversations each kill's `bash` answer told (ADR-0062, D4 rule 3), off the pod's list:
569
+ * they leave that kill's audience. Persisted at the write when anything changed. */
570
+ noticesTold?: (told: ReadonlyArray<{ source: string; takers: readonly string[] }>) => void;
571
+ /** A Turn of these conversations in `workspace` ended: a fresh one leaves every audience there,
572
+ * a continued one stays for its next Turn (ADR-0062). Persisted at the write. */
573
+ noticeTurnEnded?: (workspace: string, takers: readonly string[]) => void;
532
574
  /**
533
575
  * The pick the Turn now at this address has outstanding, or answered and not yet read (ADR-0024):
534
576
  * what the Agent's Actor asks when its Harness restarted under a Turn — a Turn whose pick is still
@@ -18,7 +18,8 @@ export type RepoIdentity = {
18
18
  * absolute path for a local repository. Path case is KEPT: hosts are case-insensitive, paths on
19
19
  * most forges are not. */
20
20
  identity: string;
21
- /** `<slug>-<sha256(identity)[:8]>` — a DNS-1123 label of at most 49 characters. */
21
+ /** `<slug>-<sha256(identity)[:32]>` — a DNS-1123 label of at most 49 characters: a slug of at
22
+ * most 16 characters and 128 bits of the hash, so no one finds two identities with one key. */
22
23
  key: string;
23
24
  scheme: "https" | "http" | "ssh" | "git" | "local";
24
25
  };
@@ -139,7 +140,10 @@ function keyOf(identity: string): string {
139
140
  .toLowerCase()
140
141
  .replace(/[^a-z0-9]+/g, "-")
141
142
  .replace(/^-+|-+$/g, "")
142
- .slice(0, 40)
143
+ .slice(0, 16)
143
144
  .replace(/-+$/, "") || "repo";
144
- return `${slug}-${createHash("sha256").update(identity).digest("hex").slice(0, 8)}`;
145
+ // 128 bits (ADR-0051): the key names the cache that a Sandbox mounts, and a per-run url is the
146
+ // Agent's text. With 32 bits a search found a url of another repository with the same key in
147
+ // seconds, and that url mounted the other repository's cache.
148
+ return `${slug}-${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`;
145
149
  }
package/src/repos.ts CHANGED
@@ -192,10 +192,28 @@ export function kubeRepos(opts: KubeReposOptions): RepoResources {
192
192
  return item;
193
193
  };
194
194
 
195
+ /** The resource that stands under `repo.key`, when it is `repo`'s identity; else a throw that names
196
+ * both urls (ADR-0051). The key is collision-resistant, so another identity under one key is a
197
+ * defect or an attack, and no statement of this port writes a resource of another identity: not
198
+ * its url, not its `secretRef`, not its clock. */
199
+ const sameIdentity = async (repo: { url: string; identity: string; key: string }): Promise<RepoItem> => {
200
+ const item = await get(repo.key);
201
+ const stands = item.metadata?.annotations?.[ANNOTATION_REPO_IDENTITY];
202
+ if (stands !== repo.identity) {
203
+ throw new Error(
204
+ `Repo "${repo.key}" stands for ${stands === undefined ? "no stated identity" : `"${stands}"`} ` +
205
+ `(url ${item.spec?.url ?? "unknown"}), not for "${repo.identity}" (url ${repo.url}): one key, two ` +
206
+ "repositories, so jr2 changes nothing on it (ADR-0051). Delete that Repo if it is stale.",
207
+ );
208
+ }
209
+ return item;
210
+ };
211
+
195
212
  return {
196
213
  async bind(repo) {
197
214
  const { secretRef, cr } = await resourceFor({ ...repo, bound: true });
198
215
  if (await create(cr)) return;
216
+ await sameIdentity(repo);
199
217
  // Already there from an earlier deploy: ONE merge patch says what the Machine resolves now —
200
218
  // url, credential, and the label `jr2 gc` honors. `secretRef: null` clears a credential the
201
219
  // config no longer names, so a dropped entry does not linger. The clock is not touched: it
@@ -216,12 +234,12 @@ export function kubeRepos(opts: KubeReposOptions): RepoResources {
216
234
  // Already there — the boot's statement, or an earlier run's. The eviction clock moves: the
217
235
  // run attached it now. The url stays as stated, whatever spelling this run brought.
218
236
  const clock = { metadata: { annotations: { [ANNOTATION_REPO_LAST_ATTACHED]: attached } } };
237
+ const standing = await sameIdentity(repo);
219
238
  // A Machine's slot binds it: the boot is its writer, and restates the credential at the next
220
- // deploy. Read nothing.
239
+ // deploy.
221
240
  if (repo.bound) return patch(repo.key, clock);
222
241
  // This run's slot is per-run, but another registered Machine may bind the identity — the
223
242
  // label says so, and then the boot is still the writer.
224
- const standing = await get(repo.key);
225
243
  if (standing.metadata?.labels?.[LABEL_REPO_BOUND] === "true") return patch(repo.key, clock);
226
244
  // Nothing binds it: no boot ever restates it, so this attach does — the credential resolved
227
245
  // against the url that STANDS, since the scheme picks the Secret's kind (config.ts) and the