@awebai/oats 0.40.1 → 0.41.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/bin/oats.mjs +45 -18
- package/docs/capabilities.md +44 -3
- package/docs/desktop-cli-api.md +147 -23
- package/docs/desktop.md +5 -4
- package/docs/execution-targets.md +342 -38
- package/docs/implementation.md +85 -11
- package/docs/oats-local.schema.json +2 -2
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +5 -5
- package/docs/release-lane.md +8 -4
- package/docs/release-notes/v0.40.2.md +120 -0
- package/docs/release-notes/v0.41.0.md +238 -0
- package/docs/schedules.md +40 -11
- package/docs/servers.md +7 -2
- package/docs/souls-and-instances.md +32 -6
- package/docs/workspaces.md +1 -1
- package/lib/core.mjs +579 -177
- package/lib/dir-lock.mjs +7 -4
- package/lib/instance-events.mjs +130 -46
- package/lib/instance-git.mjs +113 -4
- package/lib/instance-lifecycle.mjs +3 -2
- package/lib/packages.mjs +1 -1
- package/lib/resolve.mjs +1 -1
- package/lib/schedule-command-child.mjs +39 -9
- package/lib/schedule.mjs +57 -28
- package/lib/servers.mjs +23 -9
- package/lib/session-input.mjs +42 -47
- package/package-catalog.json +1 -1
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +1 -1
|
@@ -55,11 +55,311 @@ inspect`. A recorded server that cannot be read gives `running: null` with
|
|
|
55
55
|
`runtimeState: "unreachable"`. The environment variables are read when the spawn runs, and `oats
|
|
56
56
|
inspect --json` reports the session a new spawn would open in as `session`
|
|
57
57
|
(`{tmuxSession}`). A spawn refuses an instance name that is a live window in the
|
|
58
|
-
session it would open in (`E_INSTANCE_NAME_TAKEN`); a
|
|
59
|
-
in another session, such as a `pi-agents` window
|
|
60
|
-
|
|
61
|
-
the two never mix. The
|
|
62
|
-
session, window and socket. The spawn result prints the
|
|
58
|
+
session it would open in, on the OATS tmux server (`E_INSTANCE_NAME_TAKEN`); a
|
|
59
|
+
live window of that name in another session, such as a `pi-agents` window
|
|
60
|
+
after the default moved, or on another tmux server, does not block it. Session
|
|
61
|
+
commands target each home's exact recorded window, so the two never mix. The
|
|
62
|
+
receipt records the session, window and socket. The spawn result prints the
|
|
63
|
+
attach command.
|
|
64
|
+
|
|
65
|
+
<a id="the-oats-tmux-server"></a>
|
|
66
|
+
#### The OATS tmux server
|
|
67
|
+
|
|
68
|
+
OATS creates sessions on a tmux server of its own, named `oats`
|
|
69
|
+
(`tmux -L oats`), not on your default tmux server, where earlier kernels
|
|
70
|
+
created them. A tool that restyles the
|
|
71
|
+
default server (a theme switcher, a script that runs `tmux set -g …` or
|
|
72
|
+
`tmux kill-server`) therefore no longer reaches agent terminals.
|
|
73
|
+
|
|
74
|
+
- **Where it is.** The server's socket is
|
|
75
|
+
`${TMUX_TMPDIR:-/tmp}/tmux-<uid>/oats`. The instance records the absolute
|
|
76
|
+
socket path it was created on, in `instance.json` and in the receipt, and
|
|
77
|
+
every later command uses that recorded path, never the name. OATS does not
|
|
78
|
+
compute the socket path and does not use `TMUX` to find a server: tmux
|
|
79
|
+
resolves the name once, when the session is ensured, from the
|
|
80
|
+
`TMUX_TMPDIR` of the process that runs OATS, which OATS passes on unchanged
|
|
81
|
+
when it replaces the environment for that call.
|
|
82
|
+
- **Your configuration loads.** The server starts as any tmux server does, so
|
|
83
|
+
your `~/.tmux.conf` (key bindings, status line, options) applies to it.
|
|
84
|
+
- **See what runs there:** `tmux -L oats ls`. Plain `tmux ls` and a bare
|
|
85
|
+
`tmux attach` show your default server, so they do not show the sessions
|
|
86
|
+
OATS creates now.
|
|
87
|
+
- **Attach:** `oats session attach --home <home>`. By hand, copy the
|
|
88
|
+
`attach:` line that spawn prints: `tmux -S <socket> attach -t <session>`,
|
|
89
|
+
with the recorded socket. `tmux -L oats attach -t <session>` is a
|
|
90
|
+
convenience that holds only in an environment with the same `TMUX_TMPDIR`
|
|
91
|
+
as the one that created the session.
|
|
92
|
+
- **`TMUX_TMPDIR`.** Two environments with different values (a login shell,
|
|
93
|
+
and a process started by a service manager or launchd, a schedule runner or
|
|
94
|
+
a GUI launch) each get their own `oats` server. Every instance stays
|
|
95
|
+
reachable, because its absolute socket is recorded.
|
|
96
|
+
- **A limit.** A tool that runs inside a pane of the OATS server has `TMUX`
|
|
97
|
+
pointing at that server and still reaches it.
|
|
98
|
+
|
|
99
|
+
**What OATS sets, and at which scope.** OATS sets nothing server-global on any
|
|
100
|
+
server when it creates a session or a window, and nothing at all on a server
|
|
101
|
+
other than the one the window is on.
|
|
102
|
+
|
|
103
|
+
| On | OATS sets | Scope |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| each agent window it creates | `window-style default`, `window-active-style default`, `cursor-colour default` | that window (`set-option -w`), by window id |
|
|
106
|
+
| each agent window, and the `hq` window of a session it creates | `window-size latest`, `aggressive-resize on` | that window |
|
|
107
|
+
| the pane of each window it launches a harness in, and a pane it respawns in place | no `COLORFGBG` in the pane's environment | that pane's command |
|
|
108
|
+
|
|
109
|
+
So an agent pane takes the colours of the terminal that views it, whatever
|
|
110
|
+
the server's own defaults say, and on tmux 3.1 or later the window follows
|
|
111
|
+
the size of the viewer that used it last. A window that is restarted in place
|
|
112
|
+
keeps the options it has. A session OATS did not create, and the server's global
|
|
113
|
+
options and environment, are left exactly as they were.
|
|
114
|
+
|
|
115
|
+
- `pane-colours` (the palette) is not reset: a local reset did not neutralise
|
|
116
|
+
inherited array entries, so OATS leaves the palette alone. A configuration
|
|
117
|
+
that sets `pane-colours` globally therefore reaches agent panes. It comes
|
|
118
|
+
from your own tmux configuration, which OATS loads on purpose: remove the
|
|
119
|
+
setting there, or unset it on the running server with `tmux -L oats
|
|
120
|
+
set-option -gu pane-colours`.
|
|
121
|
+
- The oldest supported tmux stays 3.0. On tmux 3.0 and 3.0a `window-size
|
|
122
|
+
latest` is not available and is skipped, as before this change;
|
|
123
|
+
`cursor-colour` is skipped below 3.3; no error in either case. On 3.0 and
|
|
124
|
+
3.0a the window therefore keeps the sizing that server gives it, not the
|
|
125
|
+
latest viewer's.
|
|
126
|
+
- The two sizing commands are each attempted on their own, and a refused one
|
|
127
|
+
is ignored: the launch goes on. Nothing else is tolerated: a refused
|
|
128
|
+
`window-style` or `window-active-style` is a launch failure (the spawn is
|
|
129
|
+
rolled back and fails; a start fails with `E_SESSION_START_FAILED`). An
|
|
130
|
+
earlier kernel failed a start whose recorded tmux server could not be
|
|
131
|
+
reached when a sizing command was refused; this one does not.
|
|
132
|
+
|
|
133
|
+
<a id="the-servers-start-environment"></a>
|
|
134
|
+
#### The environment of the server and its panes
|
|
135
|
+
|
|
136
|
+
A tmux server keeps the environment of the process that started it, as its
|
|
137
|
+
global environment. A pane gets that, plus its session's environment (the
|
|
138
|
+
variables tmux's `update-environment` names, taken from whoever created the
|
|
139
|
+
session), plus what the launch command sets (`OATS_INSTANCE`,
|
|
140
|
+
`OATS_INSTANCE_HOME`, the capabilities' and the launch configuration's
|
|
141
|
+
variables). One variable is different: **a pane's `PATH` is the `PATH` of the
|
|
142
|
+
process that creates its window**, that is of each `oats spawn` or `oats
|
|
143
|
+
session start`, with the home's own `oats` shim put first by the launch
|
|
144
|
+
command. So:
|
|
145
|
+
|
|
146
|
+
- `PATH` follows the process that runs each spawn or start, not whoever
|
|
147
|
+
started the server.
|
|
148
|
+
- The rest of the ambient environment follows the server's first creator,
|
|
149
|
+
and a server that already runs keeps what it started with. Creating a
|
|
150
|
+
session or a window on it changes nothing global.
|
|
151
|
+
|
|
152
|
+
Who starts the `oats` server, after an install, a reboot or its last session
|
|
153
|
+
ending, decides that ambient environment:
|
|
154
|
+
|
|
155
|
+
| Started by | The server's environment |
|
|
156
|
+
|---|---|
|
|
157
|
+
| an operator's shell | that shell's |
|
|
158
|
+
| the Desktop | your environment as the Desktop got it, without what the Desktop or its packaging added to its own, and with the login-shell `PATH` it puts in front ([desktop.md](desktop.md)) |
|
|
159
|
+
| a schedule runner or a trigger | the service's |
|
|
160
|
+
| an OATS instance that creates an agents' session | the global environment of the tmux server that instance's home records, reduced as described below; not the instance's own |
|
|
161
|
+
|
|
162
|
+
The first creator that succeeds determines it; when two start it at the same
|
|
163
|
+
moment, tmux starts one server and nothing says which of the two it is.
|
|
164
|
+
|
|
165
|
+
**What OATS removes, for every creator.** The names the kernel itself sets
|
|
166
|
+
never go into the environment OATS creates an agents' session or window
|
|
167
|
+
with: `COLORFGBG`, `TMUX`, `TMUX_PANE`; the launch identity and roots
|
|
168
|
+
(`OATS_INSTANCE`, `OATS_INSTANCE_HOME`, `OATS_HOME`, `OATS_AGENT`,
|
|
169
|
+
`OATS_SOUL`, `OATS_SOUL_ID`, `OATS_ROOT`, `OATS_CONTEXT`, `OATS_WORKSPACE`,
|
|
170
|
+
`OATS_EVENT`, `OATS_SETTINGS`, `OATS_SETTINGS_ORIGINS`, `OATS_CLI_BIN`,
|
|
171
|
+
`PI_AGENT_INSTANCE`, `PI_AGENT_HOME`, `PI_AGENTS_ROOT`); what a hook, an
|
|
172
|
+
operation, a retire or a trigger is given (`OATS_CAPABILITY`, `OATS_LAYER`,
|
|
173
|
+
`OATS_LEVEL`, `OATS_META`, `OATS_DEPLOYMENT`, `OATS_RESOLUTION`,
|
|
174
|
+
`OATS_OPERATION`, `OATS_REPO`, `OATS_BRANCH`, `OATS_WORK`, `OATS_KIND`,
|
|
175
|
+
`OATS_TASK`, `OATS_HARNESS`, `OATS_PREVIOUS_HARNESS`, `OATS_RUNTIME`,
|
|
176
|
+
`OATS_PREVIOUS_RUNTIME`, `OATS_LAUNCH_PREVIEW`, `OATS_RETIRE_INTENT`,
|
|
177
|
+
`OATS_TRIGGER_EVENT_FILE`, `OATS_TEAM_NAME`, `OATS_TEAM_SCOPE`,
|
|
178
|
+
`OATS_TEAM_ID`, `OATS_TEAM_LABEL`, `OATS_TEAM_LABELS`, `OATS_TEAMS`,
|
|
179
|
+
`OATS_TEAMS_SOURCE`, `OATS_DEFAULT_TEAM`, `OATS_DEFAULT_TEAM_ID`,
|
|
180
|
+
`OATS_DEFAULT_TEAM_FROM`, `OATS_WORKSPACE_NAME`, `OATS_WORKSPACE_KEY`); and
|
|
181
|
+
every launch reference (`OATS_LAUNCH_REF_<NAME>`) with the `<NAME>` it stands
|
|
182
|
+
for. Other `OATS_` variables you export (`OATS_HOME_DIR`,
|
|
183
|
+
`OATS_TMUX_SESSION`) are yours and stay. An agent's plain `oats` finds its
|
|
184
|
+
deployment from its home.
|
|
185
|
+
|
|
186
|
+
**An instance creates an agents' session or window without its own
|
|
187
|
+
environment.** Nothing of an instance's environment reaches a server or a
|
|
188
|
+
pane through the calls that create an agents' session or window. A harness
|
|
189
|
+
puts its own variables, credentials and identity into the environment of
|
|
190
|
+
what it runs. A process is inside an instance when `OATS_INSTANCE_HOME` names an instance
|
|
191
|
+
home, or its working directory is inside one. That covers an agent that
|
|
192
|
+
spawns or starts another instance, a capability hook that spawns or starts
|
|
193
|
+
(a hook runs with its instance's identity, also when a person ran the command
|
|
194
|
+
that triggered it), and `oats schedule run-now` typed inside an instance.
|
|
195
|
+
|
|
196
|
+
- When such a process has to create the tmux session, OATS reads the global
|
|
197
|
+
environment of the server its home records (`tmux -S <recorded socket>
|
|
198
|
+
show-environment -g -s`, the endpoint of the home's receipt, checked
|
|
199
|
+
against `instance.json` as every session command checks it) and creates
|
|
200
|
+
the session with that. What the new session gets is a reduction of that
|
|
201
|
+
environment, not an exact copy. It is read strictly and never run by a
|
|
202
|
+
shell; text that cannot be read to its end is a failed read. Not carried
|
|
203
|
+
from the recorded server:
|
|
204
|
+
- a hidden or removed variable;
|
|
205
|
+
- a variable whose value holds a line break;
|
|
206
|
+
- a variable whose value holds a `$`: tmux versions print it differently,
|
|
207
|
+
so OATS does not guess what the value was;
|
|
208
|
+
- on tmux 3.4 and 3.5, a variable whose value holds a control character or
|
|
209
|
+
a byte that is not UTF-8, which those versions print encoded. Other
|
|
210
|
+
versions print the line as it is: there a control character other than
|
|
211
|
+
a line break is carried exactly, and a byte that is not UTF-8 fails the
|
|
212
|
+
read.
|
|
213
|
+
|
|
214
|
+
Such a variable is absent in the panes of the new server unless the pane's
|
|
215
|
+
own shell start-up files set it; it is never filled in from the instance's
|
|
216
|
+
own environment. When the variable left out is one that decides which
|
|
217
|
+
configuration a tmux server loads or which programs it runs (`HOME`,
|
|
218
|
+
`XDG_CONFIG_HOME`, `PATH`, `SHELL`), the creation is refused instead, with
|
|
219
|
+
the same remedy as below.
|
|
220
|
+
- When it creates a window in a session that exists, the tmux client that
|
|
221
|
+
creates it runs with `PATH`, without any instance's `oats` shim directory,
|
|
222
|
+
and with `LANG`, `LC_ALL` and `LC_CTYPE`, which that client needs to start
|
|
223
|
+
on a host whose only UTF-8 locale is the one they name. tmux hands a pane
|
|
224
|
+
the `PATH` of that client and nothing else of it: the three locale names
|
|
225
|
+
stay with the client, and the pane, the session and the server keep the
|
|
226
|
+
values they had. Nothing else of the instance travels. When the instance
|
|
227
|
+
has no `PATH` to give (none is set, or only `oats` shim directories were
|
|
228
|
+
in it), none is passed: the pane then takes no `PATH` from the client. A
|
|
229
|
+
restart that reuses the pane the home already has runs its tmux client
|
|
230
|
+
with the same environment; where and whether the pane is reused does not
|
|
231
|
+
change.
|
|
232
|
+
- **It is refused** (`E_RUNTIME_ENDPOINT_UNKNOWN`; `oats spawn` reports it
|
|
233
|
+
as `E_SPAWN_FAILED` with the same message) when the session does not exist
|
|
234
|
+
on the `oats` server and there is no source to read: the home records no
|
|
235
|
+
tmux server (it was never launched), its receipt cannot be used, or its
|
|
236
|
+
recorded server cannot be reached or read. A process that carries an
|
|
237
|
+
instance's identity (`OATS_INSTANCE`, `OATS_INSTANCE_HOME`, `OATS_HOME`,
|
|
238
|
+
`PI_AGENT_INSTANCE` or `PI_AGENT_HOME`) with no home to be found is refused
|
|
239
|
+
the same way. Nothing falls back to the caller's environment, to another
|
|
240
|
+
server or to a built-in list.
|
|
241
|
+
- **When the refusal comes.** In `oats spawn`: before any scaffold, work
|
|
242
|
+
tree, identity or hook. In `oats session start`: after the start's
|
|
243
|
+
preflights and its planning, and before the real run of preview-aware
|
|
244
|
+
launch hooks, a stop and any write of the home's launch state (its record,
|
|
245
|
+
its receipt, a pending start). A launch hook that does not declare
|
|
246
|
+
`launchPreview` has already run by then, and a warning it returned is
|
|
247
|
+
already a `launch-warning` event of the home, as before any other late
|
|
248
|
+
refusal of a start (`E_SESSION_RUNNING`, `E_LAUNCH_ENV_MISSING`). Nothing
|
|
249
|
+
undoes what that hook did, and there is nothing to clean up: a launch hook
|
|
250
|
+
may do idempotent provider registration on a real start and no more
|
|
251
|
+
([capabilities.md](capabilities.md)), so the next start repeats it
|
|
252
|
+
harmlessly. Both launch hooks OATS ships are preview-aware. If the
|
|
253
|
+
session disappears between that check and the creation, the refusal comes
|
|
254
|
+
at the creation, and the spawn is rolled back as any failed launch is.
|
|
255
|
+
- **The remedy** is to create the session from your own shell, outside every
|
|
256
|
+
instance home, as below. The session name matters: it must be the
|
|
257
|
+
deployment's (the refusal names it); another deployment's session on the
|
|
258
|
+
same server does not help. Once the session exists, the instance spawns
|
|
259
|
+
and starts there without reading anything.
|
|
260
|
+
- The recorded server's environment is an existing baseline, chosen because
|
|
261
|
+
it is what a window that instance opened on that server got. It is not
|
|
262
|
+
proof that it holds no old identity or credential, and this is not a
|
|
263
|
+
promise that secrets are isolated between instances that run as the same
|
|
264
|
+
user on one tmux server.
|
|
265
|
+
- A server that already runs keeps its baseline. OATS creates sessions and
|
|
266
|
+
windows on it and does not certify where that baseline came from or that
|
|
267
|
+
it is clean: someone who started a server named `oats` by other means
|
|
268
|
+
decided it.
|
|
269
|
+
- These rules are about the calls that create an agents' session or window,
|
|
270
|
+
nothing wider. `oats session attach` creates a temporary viewer session on
|
|
271
|
+
the server the home records, with the attaching process's own environment
|
|
272
|
+
([#623](https://github.com/awebai/oats/issues/623)): tmux imports the
|
|
273
|
+
`update-environment` names into that temporary session, and if that
|
|
274
|
+
server exits between the viewer's check and its creation, the attaching
|
|
275
|
+
process is the one that starts it.
|
|
276
|
+
- Which server is reached is decided by the process that creates, never by
|
|
277
|
+
the environment it passes: its own `tmux`, its own `TMUX_TMPDIR`, and the
|
|
278
|
+
socket the lookup returned when the server runs. Its own `tmux` is the one
|
|
279
|
+
its own `PATH` finds, and the session is created by that program's full
|
|
280
|
+
path. A process whose `PATH` holds no tmux cannot read the server at all:
|
|
281
|
+
it is refused (`E_RUNTIME_ENDPOINT_UNKNOWN`) whether or not the session
|
|
282
|
+
exists, and told to run the command with a `PATH` that holds tmux. A
|
|
283
|
+
process whose `PATH` is not set is refused only when it has to create the
|
|
284
|
+
session (tmux used to be found by the system's default search then); with
|
|
285
|
+
the session already there, it is not refused. What the server process
|
|
286
|
+
gets (`HOME` and so which configuration loads, `PATH`, everything else)
|
|
287
|
+
comes from the passed environment alone. The global environment of the
|
|
288
|
+
server the session is created on is never read for this and never changed;
|
|
289
|
+
the one that is read is the recorded server's, as above.
|
|
290
|
+
|
|
291
|
+
A process that neither sign identifies as an instance is treated as any
|
|
292
|
+
other creator: its environment, without the names above, is what it passes.
|
|
293
|
+
|
|
294
|
+
By hand (`<session>` is the deployment's session name: `oats-agents` unless
|
|
295
|
+
`session.tmuxSession` or `OATS_TMUX_SESSION` says otherwise):
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
tmux -L oats new-session -d -s <session> -n hq # start it from your own shell, before anything else does
|
|
299
|
+
tmux -L oats show-environment -g # what environment the server has
|
|
300
|
+
tmux -L oats show-environment -g LANG # one variable
|
|
301
|
+
tmux -L oats set-environment -g LANG "$LANG" # repair a running server: windows created from now on inherit it
|
|
302
|
+
tmux -L oats kill-server # replace it: ends EVERY session on that server
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- Start it yourself after an upgrade or a reboot, before agents, the Desktop
|
|
306
|
+
or a schedule run. OATS finds a session you created and uses it: it looks
|
|
307
|
+
the session up by its exact name and assumes nothing about its `hq`
|
|
308
|
+
window.
|
|
309
|
+
- `set-environment -g` is how a running server is repaired: it ends nothing,
|
|
310
|
+
and panes that already run keep the environment they have. A pane's `PATH`
|
|
311
|
+
is always that of the process that runs the spawn or the start, so setting
|
|
312
|
+
`PATH` on the server changes nothing for OATS panes.
|
|
313
|
+
- `kill-server` ends every session on that server, every running agent
|
|
314
|
+
included. It is for before agents are started, not for repairing a running
|
|
315
|
+
host. The next start takes the environment of whoever starts it.
|
|
316
|
+
- `-L oats` reaches the server of the environment you type it in
|
|
317
|
+
(`TMUX_TMPDIR`). For the server an instance is on, use the socket from its
|
|
318
|
+
row in `oats status --json`: `tmux -S <socket> …`.
|
|
319
|
+
- A tmux server whose socket file is missing or does not answer reads as not
|
|
320
|
+
reachable, which is not proof that it exited: status, start and stop read
|
|
321
|
+
that state as stopped
|
|
322
|
+
([#624](https://github.com/awebai/oats/issues/624)). That is an existing
|
|
323
|
+
limit of every tmux server OATS uses, not of the `oats` server in
|
|
324
|
+
particular.
|
|
325
|
+
|
|
326
|
+
<a id="existing-instances"></a>
|
|
327
|
+
#### Existing instances
|
|
328
|
+
|
|
329
|
+
Upgrading moves nothing. An instance launched by an earlier kernel stays on the server
|
|
330
|
+
it recorded (usually your default one), and status, inspect, input, attach,
|
|
331
|
+
stop and retire keep acting on that recorded socket. While a deployment has
|
|
332
|
+
both, look at both servers: `tmux ls` and `tmux -L oats ls`.
|
|
333
|
+
|
|
334
|
+
- A restart in place stays on the recorded server: a live harness that is
|
|
335
|
+
restarted, a fallback shell or a dead pane is reused where it is, and the
|
|
336
|
+
record does not change.
|
|
337
|
+
- **Changed:** a start that has to create the window again, because the
|
|
338
|
+
recorded window is gone or the recorded server cannot be reached, creates
|
|
339
|
+
it on the OATS server. Earlier kernels recreated it on the recorded socket.
|
|
340
|
+
The start records the new socket in `instance.json` and the receipt, and
|
|
341
|
+
says so: one line in its `warnings`, naming the instance, the old socket
|
|
342
|
+
and the new one, and the same text as a `launch-warning` instance event.
|
|
343
|
+
- A home that was never launched (`--no-launch`) starts on the OATS server,
|
|
344
|
+
with no warning.
|
|
345
|
+
|
|
346
|
+
To move a running instance deliberately:
|
|
347
|
+
|
|
348
|
+
```sh
|
|
349
|
+
oats instance stop <instance> --plan # then run the `apply with:` line it prints
|
|
350
|
+
tmux -S <recorded socket> kill-window -t '=<session>:=<instance>'
|
|
351
|
+
oats session start --home <home>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The socket and the session are the `tmux.socket` and `tmux.session` of the
|
|
355
|
+
instance's row in `oats status --json`. The stop leaves a fallback shell in the
|
|
356
|
+
window, which a start would reuse in place; closing the window is what makes
|
|
357
|
+
the start create a new one.
|
|
358
|
+
|
|
359
|
+
Do not end the agents' session by hand (`tmux kill-session`): that ends every
|
|
360
|
+
other instance whose window is in that session, not only the one you are
|
|
361
|
+
moving. And a window that a viewer still links would live on in that viewer,
|
|
362
|
+
so the next start would create a second window for the same home.
|
|
63
363
|
|
|
64
364
|
### Herdr (removed in 0.31.0)
|
|
65
365
|
|
|
@@ -99,16 +399,19 @@ saved route that records `herdrPath` still loads; the field is ignored.
|
|
|
99
399
|
### Start and restart
|
|
100
400
|
|
|
101
401
|
`session start` keeps the instance's identity, work tree and notes. It runs no
|
|
102
|
-
spawn hooks and creates no new home. It runs the recorded launch recipe
|
|
103
|
-
recorded tmux session
|
|
104
|
-
server
|
|
402
|
+
spawn hooks and creates no new home. It runs the recorded launch recipe in the
|
|
403
|
+
recorded tmux session. A window that is still there is reused on its recorded
|
|
404
|
+
server; a window that has to be created, a `--no-launch` home's first one
|
|
405
|
+
included, opens on [the OATS tmux server](#the-oats-tmux-server).
|
|
105
406
|
|
|
106
407
|
- `--model`, `--launch-config <name>|none`, `--harness` and `--yolo` /
|
|
107
408
|
`--no-yolo` re-resolve the recipe against the home's recorded context and
|
|
108
409
|
run every check before anything starts. `--model` replaces the recorded model
|
|
109
410
|
for this and later starts.
|
|
110
411
|
- A live harness is refused (`E_SESSION_RUNNING`). A fallback shell or a dead
|
|
111
|
-
pane is reused in place
|
|
412
|
+
pane is reused in place. A missing window, or one whose tmux server cannot
|
|
413
|
+
be reached, is created again on the OATS tmux server, and the start warns when that is
|
|
414
|
+
not the server the home recorded ([Existing instances](#existing-instances)).
|
|
112
415
|
- A state that cannot be established is refused (`E_SESSION_UNKNOWN`). Two
|
|
113
416
|
starts of one home serialize (`E_SESSION_START_BUSY`). A home being retired
|
|
114
417
|
is refused (`E_INSTANCE_RETIRING`).
|
|
@@ -143,35 +446,36 @@ oats session attach --home /abs/home
|
|
|
143
446
|
live claim that the instance needs input from a human, `{since, producer,
|
|
144
447
|
reason, message}`, or `null`; it is non-null only for a running harness
|
|
145
448
|
(docs/desktop-cli-api.md, "Waiting on you").
|
|
146
|
-
- **input**
|
|
147
|
-
NUL)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
449
|
+
- **input** sends UTF-8 text (stdin or `--text-file`, at most 256 KiB, no
|
|
450
|
+
NUL) as exactly one bracketed paste followed by exactly one Enter. The text
|
|
451
|
+
is never run by a shell. Existing endpoint authority, fallback-shell,
|
|
452
|
+
stopped-session and split-window checks still refuse before input.
|
|
453
|
+
|
|
454
|
+
Before Enter, a size-based floor (200 ms plus 3 ms per KiB) and read-only
|
|
455
|
+
settling polls share one **monotonic 2-second observation budget**, starting
|
|
456
|
+
immediately after the paste command returns, before buffer cleanup. Sleeps
|
|
457
|
+
and capture timeouts are clipped to the remaining budget; a failed capture
|
|
458
|
+
or exhausted budget ends settling and proceeds to the single Enter. After
|
|
459
|
+
Enter, at most two read-only looks share a separate 1-second observation
|
|
460
|
+
budget. These deadlines do not bound the original paste/key commands, buffer
|
|
461
|
+
cleanup or OS scheduling. They authorize no further keys.
|
|
462
|
+
|
|
463
|
+
Successful terminal commands return `submitted: true` and `verified`, with
|
|
464
|
+
no `reason`. `verified: true` means only that the bounded display comparison
|
|
465
|
+
saw a changed look; `false` means unchanged, unreadable or exhausted
|
|
466
|
+
observation. The comparison retains its whitespace and resize/reflow
|
|
467
|
+
handling. Display movement can be unrelated output, a spinner or a dialog;
|
|
468
|
+
an unchanged display is not proof that no effects occurred or that a draft
|
|
469
|
+
is pending. **Neither value authorizes retry or proves model acceptance.**
|
|
470
|
+
|
|
471
|
+
A failed paste or key command remains `E_SESSION_INPUT_FAILED`; an
|
|
472
|
+
observational failure does not turn successful terminal operations into a
|
|
473
|
+
refusal. Command errors may themselves be uncertain after partial effects.
|
|
474
|
+
This transport offers no exactly-once guarantee and does not guarantee that
|
|
475
|
+
a busy pane accepts the input. Wake schedules retain their existing rule:
|
|
476
|
+
any nonthrowing input answer is recorded as delivered, meaning terminal
|
|
477
|
+
operations, not model processing. Broker delivery/ack policy and harness
|
|
478
|
+
acceptance evidence remain separate contracts.
|
|
175
479
|
- **attach** is interactive and takes no `--json`. It opens a temporary tmux
|
|
176
480
|
session linked to the agent's window alone.
|
|
177
481
|
Closing the viewer leaves the agent running.
|
package/docs/implementation.md
CHANGED
|
@@ -65,11 +65,54 @@ published to npm. Its developer docs are in
|
|
|
65
65
|
| `harness-trust.mjs` | reading (never writing) Claude's and Codex's folder trust for a launch |
|
|
66
66
|
|
|
67
67
|
The schedule registry stores explicit concurrency caps, leaving the schedule
|
|
68
|
-
cap absent for its effective default of five.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
cap absent for its effective default of five. `readRegistry()` is a pure,
|
|
69
|
+
bounded atomic-file snapshot: it takes no lock and writes nothing, projecting
|
|
70
|
+
legacy implicit one to absent and `capsVersion: 2` only in memory. Its no-follow
|
|
71
|
+
regular-file descriptor accepts an atomic rename between stat and open, so it
|
|
72
|
+
returns a complete old or new snapshot; other bounded readers retain their
|
|
73
|
+
identity check. Invalid present schedule or trigger caps are refused.
|
|
74
|
+
Registration, unregistration and cap updates reread under `registry.lock` and
|
|
75
|
+
compare against the raw snapshot, so even unchanged membership persists the
|
|
76
|
+
migration. A successful write removing legacy one emits a stderr notice naming
|
|
77
|
+
the one-slot restoration command. `writeRegistry()` remains a raw atomic writer;
|
|
78
|
+
read-modify-write callers must supply the lock and reread. Later explicit one
|
|
79
|
+
stays explicit, including one reintroduced by an older writer: the stored
|
|
80
|
+
marker cannot distinguish its provenance. The trigger cap stays independent.
|
|
81
|
+
|
|
82
|
+
The shared mkdir lock retries every occupied-directory window within the
|
|
83
|
+
caller's retry deadline, including owner-file publication and removal. It never
|
|
84
|
+
removes a contender's lock or steals from a live, dead or unreadable owner;
|
|
85
|
+
persistent contention ends in the caller's existing busy error.
|
|
86
|
+
|
|
87
|
+
Spawn compensation returns its diagnostic and a structural uncertainty flag;
|
|
88
|
+
only an incomplete result marks the original error's `details.unconfirmed`.
|
|
89
|
+
CLI wrappers preserve that field, and the operation runner promotes a provider's
|
|
90
|
+
literal true marker while retaining the nested envelope. Message-based consumers
|
|
91
|
+
remain during the additive producer migration; do not replace stage evidence
|
|
92
|
+
with a substring check or infer uncertainty from a retained home alone.
|
|
93
|
+
|
|
94
|
+
The schedule child supervisor installs SIGINT/SIGTERM/SIGHUP handlers before
|
|
95
|
+
launch and removes them on settlement. All catchable shutdown signals share
|
|
96
|
+
its idempotent TERM/KILL path; the first stop cause is retained. The private
|
|
97
|
+
receipt records interruption independently of both the first stop cause and
|
|
98
|
+
the direct child's observed exit, so a signal during overflow cleanup still
|
|
99
|
+
keeps an otherwise valid envelope unconfirmed. Group probes
|
|
100
|
+
start at leader exit; an observed-empty group is permanently excluded from
|
|
101
|
+
later probes/signals. Polling cannot eliminate the gap before observation or
|
|
102
|
+
prove away PID reuse. Escaped sessions are outside the owned group, and
|
|
103
|
+
SIGKILL/OOM or unrecoverable supervisor death cannot be cleaned up by handlers;
|
|
104
|
+
a missing receipt supplies no child-exit evidence and cannot release a slot.
|
|
105
|
+
|
|
106
|
+
Session input keeps terminal operations separate from display observation.
|
|
107
|
+
The pre-Enter budget starts at paste completion, before best-effort buffer
|
|
108
|
+
cleanup; post-Enter observation starts after the single key command returns.
|
|
109
|
+
Both use a monotonic clock, clip sleeps and read-only capture subprocess
|
|
110
|
+
budgets, and stop observing on failure or exhaustion. Capture subprocesses use
|
|
111
|
+
SIGKILL on timeout so an ignored TERM cannot extend a probe; terminal command
|
|
112
|
+
timeouts/errors are unchanged. Screen comparison can set only the observational
|
|
113
|
+
`verified` boolean, never send another key or report terminal failure. Tests
|
|
114
|
+
use inert command runners and clocks; they do not qualify broker delivery or
|
|
115
|
+
harness acceptance.
|
|
73
116
|
|
|
74
117
|
The kernel is runtime-neutral: nothing in `lib/` depends on a harness or on
|
|
75
118
|
a provider. Provider behaviour lives in capabilities; the kernel supplies
|
|
@@ -241,14 +284,25 @@ scaling by call count.
|
|
|
241
284
|
|
|
242
285
|
Locally, run the suites your change affects, plus `validate` and `check`; run
|
|
243
286
|
`smoke:tarball` when you change the smoke script or packaging. Pull-request CI
|
|
244
|
-
runs the full suite (sharded),
|
|
245
|
-
|
|
246
|
-
|
|
287
|
+
runs the full suite (sharded), the Desktop suite again with only Desktop
|
|
288
|
+
dependencies installed, `check`, `validate`, `pack:check` and the smoke test,
|
|
289
|
+
and is the gate. Tests use local bare repositories and fakes; none contacts
|
|
290
|
+
GitHub, aweb, Jira or Linear.
|
|
247
291
|
|
|
248
292
|
Desktop's standalone tests (`cd packages/desktop && npm ci && npm test`) must
|
|
249
|
-
load with only Desktop dependencies installed.
|
|
250
|
-
|
|
251
|
-
the root
|
|
293
|
+
load with only Desktop dependencies installed. The release builds Desktop that
|
|
294
|
+
way: its `desktop-build` job installs and tests inside `packages/desktop` and
|
|
295
|
+
never installs the root. The sharded suite installs the root first, so a
|
|
296
|
+
Desktop test that reaches a root-only dependency passes there and would fail
|
|
297
|
+
only at a tag. Pull-request CI therefore has a `desktop-standalone` job that
|
|
298
|
+
runs the same `npm ci` and `npm test` inside `packages/desktop` with no root
|
|
299
|
+
install, and the gate requires it. `test/continuous-integration.test.mjs` pins
|
|
300
|
+
that job against `desktop-build`: change how one installs or tests Desktop and
|
|
301
|
+
the other changes with it.
|
|
302
|
+
|
|
303
|
+
Cross-package tests that import both the kernel and Desktop belong under root
|
|
304
|
+
`test/`; install dependencies at the root and in `packages/desktop` before
|
|
305
|
+
running those tests. The schedule
|
|
252
306
|
round-trip case is `node --test test/desktop-schedule-roundtrip.integration.mjs`.
|
|
253
307
|
The root runner includes this file with its existing Desktop dependency group.
|
|
254
308
|
With only root dependencies installed, it omits both Desktop suites and this
|
|
@@ -273,3 +327,23 @@ which fails with `ENOTEMPTY` (awebai/oats#451). So:
|
|
|
273
327
|
it runs there.
|
|
274
328
|
|
|
275
329
|
Never paper over such a race with a retry around the cleanup.
|
|
330
|
+
|
|
331
|
+
A test never reaches the operator's tmux. The kernel creates sessions on the
|
|
332
|
+
server named `oats` (`tmux -L oats`), which tmux resolves under `TMUX_TMPDIR`,
|
|
333
|
+
and acts on every existing session through its recorded socket
|
|
334
|
+
([execution-targets.md](execution-targets.md#the-oats-tmux-server)). So:
|
|
335
|
+
|
|
336
|
+
- A test that runs real tmux installs `isolateSessionEnvironment(base)`
|
|
337
|
+
(`test/helpers/host-fixture.mjs`). It owns a private, short `TMUX_TMPDIR`
|
|
338
|
+
(socket paths are limited to about 104 bytes) and puts a `tmux` wrapper
|
|
339
|
+
first on `PATH` that admits only `-L oats` and `-S` of a socket inside the
|
|
340
|
+
fixture, with no user configuration unless the test asks for it;
|
|
341
|
+
`oatsSocket()` is the socket `-L oats` resolves to there. Its restore
|
|
342
|
+
function kills that server, by socket.
|
|
343
|
+
- The shared fixture (`test/helpers/v2-deployment.mjs`) gives every command
|
|
344
|
+
it runs a private `TMUX_TMPDIR` too, so a test with a fake `tmux` on `PATH`
|
|
345
|
+
cannot reach a real server either. A fake answers what the kernel asks:
|
|
346
|
+
`list-sessions`, `new-session` (it prints `<socket>\t<window id>`),
|
|
347
|
+
`list-windows`, `new-window` (a window id) and `set-option`.
|
|
348
|
+
- A server a test starts is killed by its socket, never by name and never
|
|
349
|
+
with a bare `tmux kill-server`.
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"disabled": {
|
|
100
100
|
"description": "Workspace schedules this host does not run, by qualified id <member>/<id>, without a commit (`oats schedule disable <member>/<id>` writes it). Local schedules are enabled and disabled in oats-schedules.json.",
|
|
101
101
|
"type": "array", "uniqueItems": true,
|
|
102
|
-
"items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,
|
|
102
|
+
"items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,100}$" }
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
},
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"description": "The workspace triggers and schedules this host agrees to run, by qualified id <member>/<id>, or \"*\" for every one the workspace places on this host (0.30). A workspace automation runs only when its runsOn is host.name, its owner is this host's gh account AND trust admits it; absent or empty, none runs. A host fact: the committed workspace file refuses it. Local triggers and schedules need no trust.",
|
|
112
112
|
"oneOf": [
|
|
113
113
|
{ "const": "*" },
|
|
114
|
-
{ "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,
|
|
114
|
+
{ "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,100}$" } }
|
|
115
115
|
]
|
|
116
116
|
}
|
|
117
117
|
}
|
package/docs/official-catalog.md
CHANGED
|
@@ -9,7 +9,7 @@ or workspace membership alone does not make a package official.
|
|
|
9
9
|
|
|
10
10
|
| package | release | capabilities | package souls |
|
|
11
11
|
|---|---|---|---|
|
|
12
|
-
| `oats.framework` | `oats-framework/v1.6.
|
|
12
|
+
| `oats.framework` | `oats-framework/v1.6.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
|
|
13
13
|
| `oats.okf` | `v4.1.1` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
|
|
14
14
|
| `oats.aweb` | `v1.21.1` | `oats.aweb` (messaging) | |
|
|
15
15
|
| `oats.engineering` | `v1.8.1` | `oats.engineering-expert`, `oats.developer`, `oats.code-review`, `oats.maintainer` | `code-reviewer` |
|
package/docs/packages.md
CHANGED
|
@@ -51,7 +51,7 @@ packages:
|
|
|
51
51
|
- **Bare version** (`v4.1.1`, `4.1.1`, `1.0.0-rc.1`): the id is looked up in
|
|
52
52
|
the official catalog — `package-catalog.json` in the `oats` repo, or the file
|
|
53
53
|
named by `OATS_PACKAGE_CATALOG` — which supplies the repo url, the tag
|
|
54
|
-
convention (`v4.1.1` or `oats-framework/v1.6.
|
|
54
|
+
convention (`v4.1.1` or `oats-framework/v1.6.1`) and the payload path. An id
|
|
55
55
|
the catalog does not know is `E_PACKAGE_MISSING` ("use `git:<repo>@<ref>` for
|
|
56
56
|
a package outside the catalog"). The catalog is the reviewed official list
|
|
57
57
|
([official-catalog.md](official-catalog.md)) and the only way a
|
|
@@ -74,7 +74,7 @@ members:
|
|
|
74
74
|
- git:github.com/acme/agents
|
|
75
75
|
- git:github.com/acme/platform
|
|
76
76
|
packages:
|
|
77
|
-
oats.framework: v1.6.
|
|
77
|
+
oats.framework: v1.6.1
|
|
78
78
|
oats.okf: v4.1.1
|
|
79
79
|
oats.aweb: v1.21.1
|
|
80
80
|
teams:
|
|
@@ -333,11 +333,11 @@ A soul that names one of the package's capabilities with
|
|
|
333
333
|
"policy": "docs/official-catalog.md",
|
|
334
334
|
"packages": {
|
|
335
335
|
"oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.1.1", "path": "oats-package" },
|
|
336
|
-
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.6.
|
|
336
|
+
"oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.6.1", "path": "oats-package" }
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
`ref` carries the tag convention: a workspace's `oats.framework: v1.6.
|
|
342
|
-
resolves to tag `oats-framework/v1.6.
|
|
341
|
+
`ref` carries the tag convention: a workspace's `oats.framework: v1.6.1`
|
|
342
|
+
resolves to tag `oats-framework/v1.6.1`. Resolving through the catalog never
|
|
343
343
|
advances a lock by itself: `oats sync` does, and says so.
|
package/docs/release-lane.md
CHANGED
|
@@ -47,8 +47,8 @@ override; report the risk you accepted). It never touches the checkout it
|
|
|
47
47
|
runs from: it exports the SHA into a detached worktree under the system
|
|
48
48
|
temporary directory (recorded in `MANIFEST.json`, `--export <dir>` overrides)
|
|
49
49
|
and runs every build step there. The bumped manifests exist only in that
|
|
50
|
-
export; the version-bump commit
|
|
51
|
-
manual
|
|
50
|
+
export; the version-bump commit reaches `main` through the pull request the
|
|
51
|
+
workflow opens (or a manual one), merged by a maintainer.
|
|
52
52
|
|
|
53
53
|
`publish-npm` and `release-github` print their plan and refuse without
|
|
54
54
|
`--yes`. `tag` creates the local tag without `--yes` but pushes only with
|
|
@@ -117,8 +117,12 @@ Everything after `build` reads `MANIFEST.json` and the files already staged:
|
|
|
117
117
|
runs `release.yml`, whose steps are idempotent: it skips the live npm
|
|
118
118
|
versions, re-uploads the same assets, and attaches the attestations. That
|
|
119
119
|
later pass is the way to add provenance; nothing is republished.
|
|
120
|
-
- **The version-bump PR.** The workflow's final step
|
|
121
|
-
|
|
120
|
+
- **The version-bump PR.** The workflow's final step opens it and stops: the
|
|
121
|
+
run never merges into `main`. A maintainer reviews that the diff is the
|
|
122
|
+
version lines only and merges it. The PR is opened with the workflow token,
|
|
123
|
+
so no checks run on it by themselves; close and reopen it to run them, and
|
|
124
|
+
do not read missing checks as green. Open the PR by hand if the workflow
|
|
125
|
+
does not run.
|
|
122
126
|
- **Legs for hosts you do not have.** The Linux AppImage/DEB need a Linux
|
|
123
127
|
host; the lane says so and `stage` lists what is missing.
|
|
124
128
|
|