@awebai/oats 0.40.2 → 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.
@@ -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 live window of that name
59
- in another session, such as a `pi-agents` window after the default moved, does
60
- not block it. Session commands target each home's exact recorded window, so
61
- the two never mix. The receipt records the
62
- session, window and socket. The spawn result prints the attach command.
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 on the
103
- recorded tmux session; a `--no-launch` home starts on the default tmux
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; a missing window or a lost tmux server is recreated.
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`).
@@ -284,14 +284,25 @@ scaling by call count.
284
284
 
285
285
  Locally, run the suites your change affects, plus `validate` and `check`; run
286
286
  `smoke:tarball` when you change the smoke script or packaging. Pull-request CI
287
- runs the full suite (sharded), `check`, `validate`, `pack:check` and the smoke
288
- test, and is the gate. Tests use local bare repositories and fakes; none
289
- contacts GitHub, aweb, Jira or Linear.
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.
290
291
 
291
292
  Desktop's standalone tests (`cd packages/desktop && npm ci && npm test`) must
292
- load with only Desktop dependencies installed. Cross-package tests that import
293
- both the kernel and Desktop belong under root `test/`; install dependencies at
294
- the root and in `packages/desktop` before running those tests. The schedule
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
295
306
  round-trip case is `node --test test/desktop-schedule-roundtrip.integration.mjs`.
296
307
  The root runner includes this file with its existing Desktop dependency group.
297
308
  With only root dependencies installed, it omits both Desktop suites and this
@@ -316,3 +327,23 @@ which fails with `ENOTEMPTY` (awebai/oats#451). So:
316
327
  it runs there.
317
328
 
318
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`.
@@ -0,0 +1,238 @@
1
+ # OATS 0.41.0
2
+
3
+ ## Changed
4
+
5
+ - **The framework workspace has a separate maintainer soul.** The new
6
+ `oats-maintainer` soul is the OATS maintainer: direction, the roadmap,
7
+ architecture coherence, routing work to experts, review, merge and releases.
8
+ It composes `oats.maintainer` and carries the OATS-only `/oats-pr-review` and
9
+ `/git-tag-release` skills. `oats-expert` stays an expert, the generalist that
10
+ leads cross-area work, coordinates the domain experts and reports to
11
+ `oats-maintainer`. The other souls send direction, contract decisions and
12
+ merges to `oats-maintainer`, and read its knowledge node
13
+ (`oats/oats-maintainer`, which holds what the `oats-expert` node held).
14
+ - The `oats` knowledge store's main must declare the `oats-maintainer`
15
+ node before this reaches a deployment: souls that read or own it fail
16
+ their knowledge check with `E_CONFIG unresolved node:
17
+ oats/oats-maintainer` against a base without it.
18
+ - Operators: `oats sync`, then spawn `oats-maintainer` for maintainer work.
19
+ Running instances keep what they were spawned with.
20
+ - **Desktop terminals keep text readable (#602).** Desktop terminals now keep
21
+ text at a contrast of at least 4.5:1 with its background, so coloured text written for
22
+ another palette (a grey meant for a dark terminal, shown in White) stays
23
+ readable. The built-in palettes' colours on the terminal background are
24
+ unchanged; text on a coloured background may be drawn lighter or darker than
25
+ the program asked.
26
+ - **Desktop can match this computer's theme (#602).** A fourth theme choice,
27
+ **This computer**, shows Desktop's chrome and every terminal in the theme of
28
+ the computer that runs Desktop. On an Omarchy computer that is the current
29
+ Omarchy theme, followed when it changes, with no restart. Elsewhere it is
30
+ Dark or White by the system's appearance. Text colours taken from the host
31
+ are adjusted where needed to stay at least 4.5:1 on Desktop's surfaces; a
32
+ terminal's 16 colours are the host's own. If the host's theme cannot be
33
+ read, Desktop shows Dark or White and says so. A theme you chose does not
34
+ change: choose **Theme: This computer** in the command palette, or cycle to
35
+ it with the theme button.
36
+ - On an Omarchy computer Desktop reads the theme's colours file at start and
37
+ each time a window gets focus, whichever theme is chosen. It opens the file
38
+ without blocking and reads it only if it is a regular file, so a colours
39
+ file replaced by a named pipe, for example, cannot hang it (#612).
40
+ - On Linux, This computer is the default: a Desktop where no theme was ever
41
+ chosen starts in This computer (the computer's Omarchy theme, or the
42
+ system's light or dark appearance where there is none) instead of White.
43
+ That includes an existing install where the theme was never changed. If
44
+ you chose White at any time, it stays White. Choose another theme with the
45
+ theme button or the palette, and it stays. On other platforms the default
46
+ is still White.
47
+ - **The Desktop accepts OATS CLIs `>=0.25.8 <0.42.0`**, so it runs against
48
+ this release's kernel; the Desktop 0.40.x refuses a 0.41 CLI.
49
+ - **Desktop's Remove has one choice, the worktree.** Remove no longer offers
50
+ to delete a branch: retirement leaves branches to the operator, and the
51
+ kernel refuses the flag. The dialog's one choice is **Also delete the
52
+ worktree**, and it says "Remove never deletes a branch." A home left by a
53
+ failed spawn that still owes its branch stays incomplete while that branch
54
+ exists, and Remove can no longer complete it: check the branch and delete it
55
+ with Git, then remove the home again.
56
+ - **New sessions run on an OATS-owned tmux server (#602).** The server is
57
+ `tmux -L oats`, so tools that restyle the default tmux server no longer
58
+ reach agent terminals. Running instances stay where they are. Plain
59
+ `tmux ls` and `tmux attach` no longer show new sessions: use `oats session
60
+ attach` or the `attach:` line that spawn prints. An existing instance moves
61
+ only when its window is recreated: a start after its window is gone or its
62
+ tmux server cannot be reached now opens on the OATS server instead of the
63
+ recorded socket. To move one deliberately, stop it, close its window and
64
+ start it: see
65
+ [Existing instances](../execution-targets.md#existing-instances).
66
+ - Operators: after upgrading, and after a reboot, start the `oats` server
67
+ from your own shell before anything else does: `tmux -L oats new-session
68
+ -d -s <session> -n hq` (`<session>` is the deployment's session name,
69
+ `oats-agents` by default). A tmux server keeps the environment of whoever
70
+ starts it: without this step the first Desktop-run or scheduled spawn
71
+ decides the ambient environment of later panes, and a spawn or start run
72
+ from inside an instance whose own tmux server cannot be read is refused
73
+ ([the environment of the server and its panes](../execution-targets.md#the-servers-start-environment)).
74
+ - Nothing of an instance's environment reaches a server or a pane through
75
+ the calls that create an agents' session or window: an instance creates
76
+ the session with a reduced copy of the environment of the server its
77
+ home records (a variable whose value holds a `$`, a line break or a
78
+ character tmux prints encoded is not carried), and a window with only
79
+ `PATH` and the locale names tmux's client needs
80
+ (`LANG`, `LC_ALL`, `LC_CTYPE`), which tmux does not hand on. Every
81
+ creator's environment goes without the names the kernel sets
82
+ (`OATS_INSTANCE`, `OATS_ROOT`, `PI_AGENTS_ROOT`, launch references and
83
+ the others the docs list). A server that already runs keeps the
84
+ environment it has: OATS does not certify where that came from.
85
+ - On each agent window it creates, OATS resets `window-style`,
86
+ `window-active-style` and `cursor-colour` to `default`, and removes
87
+ `COLORFGBG` from the pane, so the pane shows the colours of the terminal
88
+ that views it. It does not reset `pane-colours`: a palette set in your
89
+ tmux configuration still reaches agent panes
90
+ ([what OATS sets](../execution-targets.md#the-oats-tmux-server)).
91
+ - OATS sets nothing server-global on any server when it creates a session
92
+ or a window. `window-size latest` and `aggressive-resize on`, which it
93
+ used to set globally on the server it created its session on, are now set
94
+ on the windows it creates.
95
+ - The spawn result's `attach` is `tmux -S <socket> attach -t <session>` for
96
+ a launched spawn and `oats session attach --home <home>` for
97
+ `--no-launch`; a routed spawn (`--server`) prints `oats session attach
98
+ --server <id> --instance <instance>`. The JSON shapes are unchanged.
99
+ - The kernel no longer exports `launchEnvTmuxFlags` (it had no consumer).
100
+
101
+ ## Upgrade
102
+
103
+ **Retirement no longer deletes branches.** What to do:
104
+
105
+ - **`oats retire --delete-branch` is refused** with `E_BAD_ARGS` before
106
+ anything happens: no child or session is stopped, no hook runs, nothing is
107
+ written. Retire without it; the branch is left in the repository. Inspect it
108
+ there and delete it with Git if it is no longer wanted. Scripts that pass the
109
+ flag must drop it. A self-retire an older OATS recorded with the flag is
110
+ completed with the branch and the worktree left, and its `retired` event says
111
+ so.
112
+ - **A home a failed spawn left that still owes its branch** stays incomplete
113
+ while that branch exists, with the item `the branch the failed spawn created
114
+ is left: OATS does not delete it. Inspect it and delete it with Git if it is
115
+ not wanted, then retry`. The item names no branch; the CLI prints it on the
116
+ next line (the receipt's `retention.recordedBranch`, or the `branch` in the
117
+ retained home's `instance.json` when the worktree step did not run). While
118
+ Git cannot show the branch gone, the item is `git branch <b>: could not
119
+ verify whether it still exists (…)`. Delete the branch with Git, then retire
120
+ again.
121
+ - **A worktree whose branch has no commit yet:** `oats retire
122
+ --discard-worktree`, or a retire that has work to copy, on such a worktree
123
+ refuses with `E_WORK_INSPECTION_FAILED` and removes nothing. Make a commit,
124
+ switch the worktree to a branch that has one, or remove the worktree with
125
+ Git; then retire again.
126
+ - **A worktree Git refuses to read without your own `safe.directory`**
127
+ (typically one owned by another user) now refuses a retire that has work to
128
+ preserve or that removes the worktree, with `E_WORK_INSPECTION_FAILED`. A
129
+ plain retire of such a worktree with nothing to preserve retains it under a
130
+ `detached-unknown` name, without its branch in the receipt. Retire as the
131
+ owner, or with Git's ownership check satisfied for that path.
132
+
133
+ ## Fixed
134
+
135
+ - **A retire keeps what only the worktree has.** The recovery a retire writes
136
+ of a worktree is proven against the commit the worktree has checked out, and
137
+ is checked out at that commit on the worktree's own branch (detached, when
138
+ the branch's name is not one OATS carries). A clean worktree whose HEAD is
139
+ detached at a commit no ref of the repository reaches is preserved in a
140
+ recovery before `--discard-worktree` removes it, also when a retire hook made
141
+ that commit. HEAD is read again immediately before the worktree is removed:
142
+ if it moved since the retire's last inspection, the worktree is not removed
143
+ and the retire stops with `E_WORK_PRESERVATION_FAILED`; the home and the
144
+ worktree are kept, and so is any recovery the retire wrote.
145
+ - **A stopped instance's harness mark is readable in Solarized (#602).** In
146
+ the Desktop's roster, the harness mark of a stopped instance was drawn at
147
+ 4.0:1 in Solarized, under the 4.5:1 the rest of the app keeps. It is now
148
+ 4.7:1. The mark's block is slightly lighter in White and Solarized and
149
+ slightly darker in Dark.
150
+ - **Review threads reach an agent whose session is not on the default tmux
151
+ server (#602).** Sending a pull request's review threads to an agent's
152
+ terminal from the Desktop now uses the tmux socket the instance records, as
153
+ the Desktop's terminals do. Before, the paste always went to the default tmux
154
+ server: for an instance that records another socket it answered that the
155
+ paste failed, or it could go into a window of the same name on the default
156
+ server. An instance that records no socket is pasted to as before.
157
+ - **The Desktop names a retire refused at inspection.** When `oats retire`
158
+ refused a local instance with `E_WORK_INSPECTION_FAILED`, the Remove dialog
159
+ said "The lifecycle CLI is unavailable.", although the CLI had answered. It
160
+ now says: "Retirement was refused: the instance's home or work could not be
161
+ inspected. The home is kept; its session may already have been stopped, and
162
+ earlier retirement steps may have run." The outcome is still reported as
163
+ unknown, because the kernel can refuse after it stopped the children and the
164
+ session and ran the retire hooks. For an instance on a server the same
165
+ sentence replaces that headline, above the host's own message in Details.
166
+ For a local refusal the CLI's own message, which names the path and the
167
+ remedy, is in Details too (next entry). Part of
168
+ [#601](https://github.com/awebai/oats/issues/601).
169
+ - **The Desktop shows the kernel's own message when a local Stop or Remove
170
+ fails.** The dialog said only its own sentence for the error code; the
171
+ message the CLI answered with was dropped. It is now in the dialog's
172
+ **Details**, as the code and the message, under that sentence, as it already
173
+ was for an instance on a server. It is shown only when the installed CLI
174
+ answered with an error and the Desktop has a sentence for its code: a
175
+ timeout, an unreadable answer or an unknown code shows none. Closes
176
+ [#601](https://github.com/awebai/oats/issues/601).
177
+ - **The Desktop shows a kernel or host message as one plain line.** In the
178
+ Details of a refusal (Stop and Remove, the Git panel, Activity, readiness),
179
+ line feeds, the Unicode line and paragraph separators and tabs are collapsed
180
+ into single spaces. The C1 control characters, the bidirectional embedding,
181
+ override and isolate characters, the zero-width space, the word joiner, the
182
+ byte order mark and tag characters are each replaced by U+FFFD, the
183
+ replacement character. A message that holds a carriage return, DEL or
184
+ another ASCII control character other than tab and line feed (a message
185
+ with Windows line endings, for example), or that looks like a credential,
186
+ is withheld whole as `[Detail withheld]`, as before. A long message is cut
187
+ at 2048 characters. For readiness, the message is no longer in the "Couldn't
188
+ refresh" line's tooltip, which keeps the Desktop's sentence and the error
189
+ code; it stays in Details. In the Git panel a failed read's message, which
190
+ could span several lines, is now one line.
191
+ - **A tmux server started from the Desktop gets your environment, not the
192
+ Desktop's (#602).** A tmux server started from the Desktop (by a spawn or a
193
+ start, or by opening a terminal) no longer carries the Desktop's own runtime
194
+ settings. They made Electron programs started from an agent's pane run as
195
+ Node, and on the AppImage they pointed library and data paths into the
196
+ Desktop's mount, which exists only while that Desktop runs. Every program
197
+ the Desktop starts (the `oats` CLI, `tmux`, `gh`, the login shell that
198
+ resolves `PATH`) now gets your environment without them. One change in a
199
+ diagnostic: `probePath` in the backend's `GET /api/cli` answer no longer
200
+ lists the AppImage's own entries. A tmux server that was already running
201
+ keeps the environment it was started with (#616). Known limits are in the
202
+ Desktop README.
203
+ - **The Desktop builds a view's failure placeholder as text.** A tab whose
204
+ view fails to load or to mount shows the same words, built without markup.
205
+ - **`oats retire` no longer refuses a launched instance only because its
206
+ tmux socket file is missing.** After a reboot that cleared tmux's socket
207
+ directory, tmux answers `error connecting to <socket> (No such file or
208
+ directory)` for the socket the instance's session was recorded on, and
209
+ retire refused with `E_RUNTIME_QUIESCE_FAILED`. Retire now goes on when the
210
+ recorded server cannot be reached because its socket file is missing and no
211
+ process on the host works in the instance's home (any process whose working
212
+ directory is in the home, not only the harness). A tmux server keeps running
213
+ when its socket file is removed, so when a process still works in the home,
214
+ or the process scan (`lsof`) cannot run or does not complete, retire refuses
215
+ as before and the message says which. Such a server recreates its socket
216
+ when its process is sent `SIGUSR1`. The check needs `lsof`: on a host
217
+ without it, a retire whose recorded socket file is missing is still refused,
218
+ and the message says that `lsof` is missing; install it, then retire. The
219
+ scan counts only when `lsof` completed: one that was cut off (a timeout, too
220
+ much output, a signal) is no longer read as "no process", here, for a home
221
+ without its session receipt and for a home opened in Herdr; those retires
222
+ are refused. So is one whose listing names no process at all, which a
223
+ completed scan never prints. A window that is still running, or a server
224
+ that cannot be read for another reason, is refused as before.
225
+ [#620](https://github.com/awebai/oats/issues/620).
226
+
227
+ ## Removed
228
+
229
+ - **Three unused routes of the Desktop's local server.**
230
+ `GET /api/session/<instance>` (a capture of the instance's terminal),
231
+ `POST /api/keys/<instance>` (keys or a paste typed into it) and
232
+ `POST /api/interrupt/<instance>` (Ctrl-C) are gone. Nothing in the Desktop
233
+ used them: its terminals attach to the instance's session directly. They
234
+ ran `tmux` without the socket an instance records, so for an instance on
235
+ its own tmux server they would have reached another one. Each path now
236
+ answers as any unknown route does (`404`, `not found`).
237
+ `GET /api/chat/<instance>` is unchanged. Closes
238
+ [#609](https://github.com/awebai/oats/issues/609).
package/docs/servers.md CHANGED
@@ -277,7 +277,10 @@ with `--server`; the remote default applies. `session attach --print` shows
277
277
  the ssh command without running it. A server without the `session` commands
278
278
  (before 0.22.2) is refused with the tmux command to attach there directly,
279
279
  naming the session and window its roster records for the instance (else
280
- `pi-agents`, that kernel's default).
280
+ `pi-agents`, that kernel's default), and the tmux socket when the roster
281
+ records one. The remote command is one quoted word either way: `ssh -t
282
+ <host> 'tmux attach -t <target>'`, or `ssh -t <host> 'tmux -S <socket>
283
+ attach -t <target>'`.
281
284
 
282
285
  ## The roster and harvest
283
286