@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.
@@ -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`).
@@ -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** submits UTF-8 text (stdin or `--text-file`, at most 256 KiB, no
147
- NUL) followed by Enter, as a bracketed paste. The text is never run by a
148
- shell. A fallback shell, a stopped session or a split
149
- tmux window is refused. The text is pasted once. Enter waits for the pane to
150
- settle (two identical captures, at least about 200 ms, longer for a larger
151
- paste, at most 2 s), and is judged by whether it changed the bottom 15 lines
152
- of the pane. The comparison is of bytes only, and the pane's text is never
153
- interpreted. Trailing spaces are ignored. When the pane was seen to change
154
- size (a resize, or a second client attaching), a reflow of the same content
155
- is not a change either: each side's content must already be on the other
156
- side's screen or in its history, so content that appeared or disappeared
157
- still counts. An Enter that changed nothing was swallowed, and is resent
158
- after a backoff, at most 3 Enters in total. The answer adds:
159
- - `submitted: true, verified: true`: an Enter was taken.
160
- - `submitted: false, verified: true, reason: "enter-not-taken"`: none of the
161
- 3 Enters changed the pane. The text stays in the agent's input box; it is
162
- not pasted again.
163
- - `submitted: true, verified: false`: a capture failed, so no further Enter
164
- was sent and the last one was not judged. As before, this means the
165
- terminal accepted the keys. When the pane cannot be read before the first
166
- resend, exactly one Enter was sent.
167
-
168
- `submitted` never means the agent processed the text. A pane that changes
169
- for another reason after Enter (a spinner, a clock, a human typing) reads as
170
- taken. A harness that shows no visible reaction to Enter receives up to two
171
- extra Enters; real harnesses (claude, codex, pi) redraw on submit. A call takes at most about 4 s plus its tmux calls. A failed paste or
172
- key send is `E_SESSION_INPUT_FAILED`, with no retry. Wake schedules and
173
- messaging capabilities use this command ([schedules.md](schedules.md)); a
174
- wake schedule records any answer as delivered.
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.
@@ -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. Reads migrate legacy stored one
69
- to absent once, under `registry.lock`, and record `capsVersion: 2`. Registration
70
- and cap updates use that same short lock; later explicit one stays explicit.
71
- `readRegistry()` returns stored choices; scheduling and status apply the default
72
- without writing it back. The trigger cap is independent and absent means no cap.
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), `check`, `validate`, `pack:check` and the smoke
245
- test, and is the gate. Tests use local bare repositories and fakes; none
246
- 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.
247
291
 
248
292
  Desktop's standalone tests (`cd packages/desktop && npm ci && npm test`) must
249
- load with only Desktop dependencies installed. Cross-package tests that import
250
- both the kernel and Desktop belong under root `test/`; install dependencies at
251
- 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
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,40}$" }
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,40}$" } }
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
  }
@@ -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.0` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
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.0`) and the payload path. An id
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.0
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.0", "path": "oats-package" }
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.0`
342
- resolves to tag `oats-framework/v1.6.0`. Resolving through the catalog never
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.
@@ -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 to `main` remains the workflow's job, or a
51
- manual PR.
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; open it by hand if the
121
- workflow does not run.
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