fux 0.2.0

Agent-native terminal workspace
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# Prompt: build zor and fux together

Paste the section below into a coding-agent session opened in a directory containing two sibling
Git repositories: this checkout at `fux/`, with its gitignored `references/` trees present, and an
empty, already `git init`-ed `zor/`. It was audited on 3 Sep 2026 against the published koh 0.12.0
release (`5e8620b633d7ab2770d18610c8ccc47313693150`), herdr
0.8.2 (`94f6d9c`), zellij main (`af38660`), and vt100 0.16.2.

The prompt deliberately resolves implementation gaps that the design documents leave open. In
particular, it records the exact koh API and prerequisite revision, makes named workspaces
implementable, and closes passthrough, scrollback-access, detach, and downstream-test seams found
in the audit.

---

Build two programs side by side: **zor**, the agent-state wrapper, in `zor/`, and **fux**, the
agent workspace, in `fux/`.

Before changing anything, read all tracked files in `fux/`, then read these reference surfaces in
full: `fux/references/koh/{README.md,CHANGELOG.md,docs/ARCHITECTURE.md}`, the public traits and
functions named below in koh's source, herdr's layout and detection sources cited by the designs,
and zellij's grid tests cited by the designs. Read any `AGENTS.md` that governs a file before
editing it. Do not read generated targets, nested Git metadata, duplicated documentation versions,
translations, binaries, or fixtures unrelated to the cited behaviour.

`fux/docs/design.md`, `fux/docs/wrapper-design.md`, and `fux/docs/wrapper-prompt.md` are the product
specification. This prompt is the execution specification and wins where it explicitly resolves a
gap or contradiction. Otherwise, `wrapper-design.md` wins for zor and `design.md` wins for fux.
Copy `fux/docs/wrapper-design.md` byte-for-byte to `zor/DESIGN.md` in Phase 0. When this prompt
chooses something an earlier document left open or described differently, append a short dated
note under that document's *Open questions*; do not silently rewrite the design.

The programs share only the `OSC 7877` report contract. fux depends on zor as a library with
default features off, which must compile only the OSC data model/parser. The binaries otherwise
remain independent.

## Working rules and completion gates

- Start by recording `git status --short`, the branch, and the three reference revisions. Preserve
  pre-existing changes. Never commit `fux/references/`, build output, captured secrets, runtime
  descriptors, socket files, or generated keys.
- Use Rust 2024, `rust-version = "1.91"`, MIT, `dead_code = "deny"`, and `unsafe_code = "forbid"`
  in fux. zor permits unsafe only in `src/platform/{linux,macos}.rs`, with
  `unsafe_op_in_unsafe_fn = "deny"` and a `// SAFETY:` explanation on every block. Deny clippy's
  `unwrap_used`, `expect_used`, `panic`, `unreachable`, `todo`, `unimplemented`,
  `indexing_slicing`, and `string_slice` outside tests.
- Keep load-bearing dependencies exact: koh `=0.12.0`, iroh `=1.0.0` where fux names iroh types,
  vt100 `=0.16.2`, portable-pty `=0.9.0`, ratatui-core `=0.1.0`, ratatui-widgets `=0.3.0`, and zor
  `=0.1.1`. Use compatible current releases for ordinary utility crates and commit both lockfiles.
  Every direct dependency must have a one-line purpose comment in `Cargo.toml`.
- fux uses koh with `default-features = false, features = ["backend-termina"]`. It has no Cargo
  features. zor has one feature, `cli`, enabled by default; its binary and every non-OSC module and
  dependency are gated by it. Declare the fux dependency as
  `zor = { version = "=0.1.1", path = "../zor", default-features = false }`, so packaging strips
  the development path after zor 0.1.1 exists on crates.io.
- Other expected fux dependencies are clap derive, serde, serde_json, toml, tokio with only the
  features actually used, tokio-util for reaper/task cancellation, nix for safe signal handling,
  anyhow, tracing, base64 for OSC 52, and proptest as a dev dependency. Do not assume koh depends
  on ratatui—it does not.
- Study reference code for invariants, edge cases, constants, and test oracles. Do not copy Rust,
  TOML, schemas, prose, or fixtures from herdr or zellij. Use koh through the public API at the
  pinned v0.12.0 revision; do not add further local patches or vendor it.
- Tests have behavioural sentence names and cite the relevant design heading or acceptance id in
  their first-line comment. Timing logic uses an injected clock. Real-session tests use koh's
  loopback endpoint helpers and bounded 10-second deadlines; short polling sleeps are allowed in
  I/O integration tests, not in pure timing tests. Every spawned process, task, socket, endpoint,
  and temporary directory has cleanup that also runs on failure.
- Parallel work is allowed only after Phase 0. Give each worker one repository or disjoint files,
  never the same worktree. Do not let one phase depend on uncommitted changes owned by another.
  At every phase gate, merge completed work, run both crates' format, clippy, and test checks, then
  commit each changed repository separately with the phase in the subject. Do not make empty
  commits merely to keep the repositories' commit counts aligned.
- Never leave `todo!()`, `unimplemented!()`, placeholder success, ignored failing tests, or a
  silently weakened requirement. If an external prerequisite is unavailable, complete everything
  independent of it and list the exact blocked evidence and command in the final report.

## Phase 0: prove the seams and freeze the contract

This phase is sequential. Do not begin the larger modules until its tests pass.

1. Verify that `fux/references/koh` is tag `v0.12.0` at
   `5e8620b633d7ab2770d18610c8ccc47313693150`. Read the exact signatures of
   `SyncState`, `SessionHost`, `ChangeSignal`, `HostProvider`, `SharedHost`, `Hosts`, `serve_with`,
   `ClientState`, `ClientTerminal`, `ClientIoTasks`, `spawn_client_io`, `connect_with`, `KohBackend`,
   `ScreenView`, `Pty` (including owned group shutdown), and `ServerTerminal` (including bounded
   scrollback access). Add a short `docs/implementation-notes.md` in each new project recording
   public-API constraints that later phases must not rediscover.
2. Create zor's crate and copy its `DESIGN.md`. Its always-built library exports only
   `zor::osc::{AgentId, Report, State, Flags, format, parse}`. `parse(&[u8])` accepts koh's joined callback
   payload (`b"7877;state=..."`) and, as a convenience, a complete OSC frame ending in ST or BEL.
   `state` and decimal `seq` are required; `agent` is required except for `none`. Unknown keys are
   ignored. Duplicate contract keys, unknown states or flags, invalid UTF-8 identifiers, bad percent
   escapes, overlong decoded messages, missing required keys, and trailing bytes after a terminator
   are rejected. Formatting always uses ST, a deterministic key order, percent-encodes `message`,
   and caps its decoded UTF-8 form at 128 bytes without splitting a code point.
3. Test OSC format/parse round trips for every state and flag combination, malformed inputs, and
   unknown keys. Feed formatted bytes through `vt100::Parser::new_with_callbacks`; join the
   callback params with `;` and prove the same report parses. Fuzz or proptest `parse` for totality.
4. Replace fux's placeholder with a binary skeleton and its final dependency policy. Reproduce the
   *Surface* from `design.md` in fux's own clap types. `id`, `key`, and the temporary terminal-screen
   form of `connect` translate into koh's config types and call `run_id`, `keycmd::run`, and
   `connect`; no koh clap type is available because `cli` is disabled. Commands not implemented in
   this phase return an explicit non-zero `NotImplemented` error naming their phase. Prove
   `zor::osc::parse` is available with a downstream compile test, then inspect
   `cargo tree -e normal --no-default-features` in zor to prove CLI-only dependencies are absent.
5. Commit both repositories. From here Phase Z and Phases F1-F5 may proceed in parallel; Phase I
   joins them.

## Phase Z: build zor

Follow sections 1-9 of `fux/docs/wrapper-prompt.md`, in order, with the following audited
clarifications. These override conflicting wording there.

- vt100 0.16.2 exposes no parser-ground-state query and cannot grow its configured scrollback
  limit. Give `screen` a separate, streaming ECMA-48 boundary tracker that persists across chunks
  and covers ESC, CSI, OSC, DCS, SOS, PM, APC, ST, BEL-terminated OSC, CAN/SUB cancellation, and
  their C1 forms. Test every split point of every sequence class. Configure a documented bounded
  scrollback limit at construction (at least the supported maximum terminal row count); resize the
  screen without pretending the limit grew.
- There is exactly one ordered output owner. The PTY reader only sends chunks to the main loop. For
  each chunk, the loop writes and flushes the child's bytes to stdout first, then parses a copy,
  then emits queued zor bytes only if the boundary tracker is in ground state. A reader thread and
  an emitter must never race to stdout: a mutex alone cannot prevent a later child chunk from
  overtaking parsing of the previous one and placing an injected OSC inside a control string.
- Title `prefix` and `replace` modes do not delete or edit bytes already emitted. They pass the
  child's title OSC through, then append zor's title OSC at the next safe boundary. `never` adds no
  title bytes. Restore the last original title on clean or signal-driven exit if zor changed it.
- Cache the detection window while `Screen` is mutably available; do not design an immutable
  `ScreenView` method that secretly calls `vt100::Screen::set_scrollback(&mut self)`. Restore the
  live viewport before returning from the cache refresh.
- In 0.1.0, a child-emitted OSC 7877 is passed through and logged under `--debug`; fux may consume it
  directly. zor does not reinterpret or resequence it. OSC 21337 is likewise observation-only until
  its provenance and schema are verified. Apply reports in fux in byte/ring arrival order; `seq` is
  diagnostic and deduplicates identical consecutive reports, not a cross-emitter trust boundary.
- If `ZOR_PID` is already present, avoid a second emulator/PTY layer: run the requested command as
  a transparent child and propagate its exit. Otherwise set `ZOR_PID` for the child. Cover both
  paths. Forward termination, hangup, window-change, and job-control signals to the child process
  group; restore terminal modes and titles on every exit path. Return the child's exit code, using
  the shell convention for signal death.
- Section 6 extends the Phase 0 OSC module; it does not replace its public types or parser. The
  single implementation stays at `src/osc.rs`; `emit` calls it rather than creating a second
  `emit/osc.rs`. The `cli` feature gates clap, the binary, platform, pty, screen, rules, the
  hysteresis machine, title, and event sinks.
- The original layering sentence saying `state/` imports nothing conflicts with its prescribed
  `Verdict`, `State`, and `Flags` inputs. Keep wire-neutral `AgentId`, `State`, `Flags`, and `Report` in the
  always-built `osc` module. The CLI-gated hysteresis module may import those types and otherwise
  imports no zor module; give it a small local `Observation` value so it never imports `rules`.
  The main loop converts a rule verdict into an observation. Enforce this corrected dependency
  direction in the layering check.
- If Claude Code cannot be run to capture genuine panes, ship `rules/claude.toml.draft`, keep it out
  of the bundle, and make `zor agents` honestly report no bundled Claude rules. Synthetic fixtures
  may test the engine but may not be presented as captured evidence.

Run zor's no-default-features build after every section so fux's dependency stays minimal.

## Phase F1: synced state and layout

Implement `fux/src/state/` without I/O.

- Use typed `PaneId`, `TabId`, and arena `NodeId` values. A tab owns `name`, an arena BSP
  `LayoutTree`, its own focused pane, and optional zoomed pane; the workspace owns `active_tab`.
  Pane ids are workspace-global. A popup references a pane id plus bounded dimensions and stacking
  order. This per-tab focus removes the invalid single-focus state implied by the earlier sketch.
- Define a serializable fux-owned cell: bounded UTF-8 grapheme contents, explicit
  blank/wide-leading/wide-continuation kind, and a fux-owned style/color representation covering
  vt100's foreground, background, bold, dim, italic, underline, and inverse fields. Do not attempt
  to serialize `vt100::Cell`; do not reduce contents to `char`, because combining sequences occupy
  one vt100 cell. Define serializable cursor and pane-mode types and lossless conversions from
  vt100 0.16.2.
- `PaneView` carries rows, columns, row-major cells, cursor state, modes, title, agent state, and
  viewport offset. `WorkspaceState` also carries popups, named status segments, the selected window
  title, base64 clipboard payload, monotonic aggregate bell count, per-connection `echo_ack`, final
  workspace exit code, and generation. All strings, collection counts, dimensions, and total cells
  have explicit bounds. Rendering malformed-but-decodable state must fall back safely, never index
  or allocate from unchecked peer values.
- `LayoutTree` is an arena of `Node::Leaf` and `Node::Split`; a split stores its axis,
  `NonZeroU16` ratio, and first and second child ids. Use a root and free-list or compaction
  strategy. Define the fixed-point ratio scale and legal range. Implement split, close, swap,
  resize, geometry, and directional neighbour lookup.
  Closing promotes the sibling into the parent slot; focus chooses the nearest leaf in the close
  direction, then the promoted sibling. Neighbours maximize perpendicular overlap, minimize
  directional distance, then use stable layout order. Every operation validates reachability,
  uniqueness, acyclicity, child ids, focus, zoom, and ratios.
- `WorkspaceDiff` explicitly represents removed panes as well as new/full panes. For same-sized
  existing panes it carries maximal contiguous changed-cell runs and independent deltas for every
  scalar pane field. It carries tabs/layout and small workspace metadata as replacement values only
  when changed. `apply` must exactly invert `diff_from` and preserve all unchanged fields.
- Choose `RECV_DECODE_LIMIT` and `RECEIVE_BUDGET_UNITS` from documented maximums, including cell
  text and metadata bytes rather than counting only cells. Maintain a verified cached resource-unit
  total so `resource_units()` is O(1), as koh's trait requires; update it on every mutation and
  diff application. Tests recompute it deeply and compare.

Tests: arbitrary state edits satisfy koh's round-trip law; arbitrary diffs and malformed topology
never panic; removing a pane, changing only modes, changing only agent flags, and changing metadata
round trip; close and tab switch emit no cell runs for unchanged surviving panes; random layout
operations preserve invariants; and independently written geometry cases cover herdr's behavioural
oracle. Drive `Transport<UserInput, WorkspaceState>` through koh's `SimHarness` under loss. Do not
call koh's `sim::run_generic_session`: it is fixed to koh's `GridState`, not fux's state.

## Phase F2: host, panes, and input routing

Implement `fux/src/host/` around one `WorkspaceHost` implementing koh 0.12's exact
`SessionHost<State = WorkspaceState>` methods: `snapshot`, `input`, `resize`,
`stamp_echo_ack`, `application_cursor`, `alive`, `attach_notify`, `client_detached`, `kill`, and
`shutdown`.

- Make ownership explicit. Each pane runtime owns one koh `Pty` and `ServerTerminal`; its drain
  task owns the output receiver. Start drain tasks from `attach_notify`, after a `ChangeSignal`
  exists, and immediately when panes are added later. Shared mutable pane data uses a narrowly held,
  poison-recovering lock or messages; never block while holding koh's outer session lock.
- Spawn configured commands through `zor --title never -- ...` when the configured zor executable
  passes an executable probe; otherwise log once and spawn bare. Do not treat the zor library path
  as proof that its binary is installed.
- For every PTY chunk, call `ServerTerminal::process`, drain `take_host_replies()` back into that
  same PTY (required for DA/DSR/DECRQM), drain `take_unhandled_oscs()` in arrival order through
  `zor::osc::parse`, refresh the pane snapshot, update title/clipboard/bell ledgers, set dirty, and
  pulse `ChangeSignal`. Poll `try_wait`; retain the real per-pane exit status for events and derive
  the final workspace exit status only when the workspace itself is killed or has no panes by the
  documented policy.
- Use koh 0.12's public `ServerTerminal::with_scrollback_screen` callback to inspect a temporary
  scrollback position without cloning the full configured history. Clamp rows and output bytes
  before materializing capture text; the callback must restore the live viewport even if capture
  panics. Do not add a replay parser or raw-output log: one authoritative emulator per pane is
  sufficient.
- The router is streaming across input chunks. Outside bracketed paste, the configured prefix opens
  one-key command mode. Inside paste, bytes are verbatim. Unknown/incomplete escape sequences are
  forwarded losslessly. Buffer only a prefix that may still become a recognized sequence, and use
  an injected, bounded ambiguity timeout so a lone Escape or partial sequence cannot stall input.
  Handle SGR mouse globally: focus on click; use Shift or copy mode for local selection; otherwise
  translate coordinates to the target pane and re-encode for an application that requested mouse
  input. Wheel scrolls fux history only when the pane application has not claimed the event. Test
  sequences split at every byte boundary.
- Resize uses the most recent client size, with an explicit deterministic tie rule. Fit the active
  tab and popups, subtract borders/status before resizing PTYs, and handle rectangles too small for
  koh's 2x2 minimum without underflow. `client_detached` removes that client's remembered viewport.
- `stamp_echo_ack` changes only the snapshot passed to it; frame numbers never enter shared host
  state. `snapshot` is bounded and clones only dirty pane views where practical. `kill` is
  best-effort and nonblocking; `shutdown` closes every pane and joins owned tasks.
- In this phase, foreground `fux serve` hosts one default workspace with
  `Hosts::new().with(FUX_ALPN, SharedHost::new(...))`; `FUX_ALPN` is `b"fux/1"`. It may also register
  `TERMINAL_ALPN` with `PtyHosts` for plain-koh fallback. At least one explicit remote allow id is
  required. The local daemon's automatic identity and multi-workspace endpoint policy arrive in F4.

Tests: router tables and chunk boundaries; DA/DSR replies reach a real child; a real child OSC
updates agent state; consecutive duplicate reports behave as documented; bell and clipboard
accounting survives pane close; every PTY receives safe geometry; scrollback requests clamp safely;
and a fake host round-trips over a real loopback connection using `Hosts::serve_connection` plus
`run_client`.

## Phase F3: client and compositor

Implement `fux/src/client/` after F1; it may proceed in parallel with F2.

- Implement koh's exact `ClientState` surface, including `echo_ack()`. `window()` returns fux's
  title, clipboard, and aggregate bell ledger; `input_modes()` mirrors application keypad,
  application cursor, and bracketed paste from the focused pane while enabling the outer SGR mouse
  mode fux's router needs. `predict_target()` returns the focused live pane as a
  `koh::predict::ScreenView`; return `None` in copy/scroll mode or when focus/topology is invalid.
- Implement `ClientTerminal<WorkspaceState>` with a fux-owned wrapper around
  `koh::client::backend::DefaultBackend`. koh's `BackendTerminal` implements only
  `TerminalScreen`, its `OutOfBand` ledger is private, and its `CaptureBackend` is `pub(crate)`;
  none is reusable downstream. Reproduce the small ledger in fux using public `KohBackend`
  methods and `InputModes::{formatted,diff}`: enter/leave raw and alternate-screen modes, sanitize
  and bound titles, make OSC 52 opt-in and validate base64, coalesce bells, reset modes on drop, and
  invalidate after suspend/resume. Define a fux-local capture backend for tests.
- Composite a ratatui `Buffer` at the real terminal size. Lay out the active tab inside the area
  left after the tab/status row, paint pane cells with continuations skipped, translate the koh
  prediction `Overlay` from focused-pane coordinates into that pane's content rectangle, draw
  borders and focus, then popups, then status. Treat koh's `status: Option<&str>` as a temporary
  connection banner that overrides or augments fux's status without changing synced state. Diff
  buffers and emit only changed cells through `KohBackend`, in a synchronized-output frame.
- Call the actual koh 0.12 API:
  `connect_with(config, FUX_ALPN, make_terminal, input_rx, resize_rx)`. Obtain the public raw-stdin
  and SIGWINCH channels with `koh::client::spawn_client_io`; retain its `ClientIoTasks` owner for
  the session and always await `ClientIoTasks::shutdown` afterward. Koh's higher-level CLI adapters
  remain disabled/private, but these cancellation-aware producers are part of koh 0.12.0's public API.
- Detach is client-side. Before `input_rx`, a tiny stateful preprocessor recognizes configured
  `prefix d` outside bracketed paste and emits koh's client escape `0x1e, b'.'`; the host cannot
  detach one viewer because `SessionHost::input` has no `ClientId`. On named-manager attachments,
  `prefix s` is also consumed locally: shut down the current connection, fetch the bounded manager
  list, select through `/dev/tty`, and reconnect. Other prefix commands pass to the host unchanged.
  Test configured prefixes, every chunk split, and switching between two named workspaces.
- Koh's predictor sees bytes before the host router. Confirm with a latency test that prefix/mouse
  control bytes open a tentative epoch and do not visibly corrupt the focused pane; if they do,
  disable prediction for those modes and record the limitation rather than accepting a ghost glyph.

Snapshot tests cover two panes/tabs at small and large sizes, zero/tiny areas, combining and wide
cells at the right border, prediction overlay offset, popup stacking, connection banners,
out-of-band deltas, suspend/resume, and malformed state. They use fux's capture backend.

## Phase F4: local daemon and named workspaces

Implement `fux/src/{local,manager}.rs` after F2 and F3. Resolve the koh provider-selection gap as
follows; do not attempt a first-SSP-frame selector (`HostProvider` never sees one) or an ALPN suffix
on one fixed `Hosts` value (the client ALPN is `&'static` and the registry is fixed).

- The daemon is one process with **one iroh endpoint per named workspace**, all using the static
  `FUX_ALPN`. A workspace name is a local alias for that endpoint; `fux connect <endpoint-id>` needs
  no name and attaches to exactly one workspace. Persist a distinct server key per workspace under
  `$XDG_STATE_HOME/fux/keys/`, and write a runtime descriptor containing name, pid, a per-daemon
  random instance nonce, endpoint id, and IPv4 loopback socket address. This is the dated
  Open-questions note to add to `design.md`.
- The daemon binds endpoints with koh's public `bind_endpoint*_alpns`, retains the returned
  `iroh::Endpoint` (hence fux's exact direct iroh dependency), and serves authorized completed
  handshakes through `Hosts::serve_connection`. Check the peer id against the union of the daemon's
  explicit allowlist and fux's persistent local-client id **before** that call; enforce connection
  limits with a semaphore. Retain a clone of `PtyHosts` and run its public TTL reaper when the
  terminal fallback is enabled.
- Use a global manager Unix socket at `$XDG_RUNTIME_DIR/fux/manager.sock` and per-workspace control
  sockets. Create the runtime directory as 0700 and sockets as 0600; reject unsafe workspace names,
  symlinks, stale descriptors, pid reuse, oversized frames, and non-socket collisions. Binding the
  manager socket elects the single daemon, so simultaneous first clients cannot spawn two servers.
- Bare `fux [name]` connects to the manager, starts `fux serve --daemon` only when the manager is
  absent, requests create-or-find, then calls `connect_with` using the descriptor's endpoint id,
  direct address, and `FUX_ALPN`. No-name opens the only workspace, creates `default` when there are
  none, or shows the picker when there are several. Use bounded startup/retry deadlines and report
  stale-state recovery.
- Killing one workspace closes its endpoint and panes but not the daemon. After replying to the
  request that killed the last workspace, the daemon removes runtime files and exits. Give a
  just-started daemon a bounded grace period in which to receive the initial create request.

Tests start two named workspaces in one daemon, prove state isolation and distinct endpoint ids,
attach two viewers to one, detach/kill clients and reattach with state intact, race two first
clients, recover stale descriptors, reject unsafe permissions/names, and prove the daemon exits
only after the last workspace is killed. Use private temporary XDG roots.

## Phase F5: control socket, configuration, hooks, and notifications

Implement `fux/src/control/` and `fux/src/config.rs`.

- Freeze serde request, reply, and event enums for every command/event in `design.md` before wiring
  CLI parsing. Newline-delimited JSON frames are UTF-8, bounded in bytes, and each carries an id;
  streamed events carry the subscription request's id. Unknown commands/fields get structured
  errors without closing the connection. Bound capture size, status text, argv/env, subscriber
  queues, and event rate; a slow subscriber drops `pane.output` nudges before state transitions.
- `fux ctl` and all top-level verbs—`new`, `split`, `focus`, `zoom`, `kill`, `resize`, `send-keys`,
  `capture`, `list`, `tab`, `workspace`, `set-status`, `popup`, and `subscribe`—are one thin client
  over the socket. Define escape decoding for `send-keys` once and test invalid escapes. Replies
  distinguish accepted, completed, and failed operations.
- Load `$XDG_CONFIG_HOME/fux/config.toml` with a deny-unknown-fields schema: prefix, bindings,
  default command, zor path, clipboard policy, notification policy, history/resource limits,
  remote allow ids, and hooks. Validate before replacing live config. External bindings receive
  `FUX_PANE`, `FUX_SOCKET`, and `FUX_CWD`; scrub fux/koh secrets. Hooks use bounded exponential
  restart backoff, reset after a healthy interval, and terminate with the workspace.
- Emit events after the authoritative mutation, in order. The notifier fires once on entering
  blocked and on working/blocked to idle—not on initial idle, heartbeats, duplicate OSCs, or replay.
  A `visible_blocker` may affect presentation but is not required for the alert. Use `notify-send`
  only with a display, `terminal-notifier` then `osascript` on macOS,
  and `termux-notification` on Termux. Avoid duplicate local alerts: the host owns notifications for
  locally hosted workspaces; an attaching client owns them only for remote workspaces/Termux, under
  config. Run commands detached with stdio closed and reap them.

Tests exercise every command against a running workspace; verify reply/event ordering and
backpressure; feed a real zor-formatted OSC through a PTY and observe `pane.opened`, `agent.state`,
and `pane.closed`; verify env scrubbing; restart/stop hooks with a fake clock and command; and run
notification policy against a fake executable.

## Phase I: cross-repository integration and release readiness

1. Build the actual zor binary. Point fux at it through an explicit test-only `ZOR_BIN`; never
   assume another repository's `target/debug` layout. Run a child that emits working then idle OSC
   7877 and assert control events and status snapshots. Separately run a captured/synthetic zor rule
   fixture through zor itself so the integration test covers generated, not only passed-through,
   reports.
2. Finish copy mode over F2's bounded cloned scrollback view: keyboard and Shift-drag selection,
   wrapped-line text extraction, and OSC 52 via `WorkspaceState.clipboard`. Test zellij-derived
   behaviours with independently written cases: a wide glyph that fits/does not fit the margin, combining cells,
   wrapped selection, and resize while scrolled. State explicitly that copy/scroll viewport is
   shared between viewers in v1 because koh supplies `ClientId` to resize/detach but not input; add
   the dated design note.
3. Write useful `README.md` and `CHANGELOG.md` files for both crates; do not merely paste a Surface
   section. Document security boundaries, runtime paths, remote authorization, workspace endpoint
   ids, history limits, bare-pane fallback, OSC spoofability by the pane process, detach keys,
   platform support, and every known limitation.
4. Add CI independently to both repositories. zor: fmt, clippy with/default without features,
   tests on Linux and macOS, layering check, and package. fux: fmt, clippy, tests on Linux and macOS,
   an `aarch64-linux-android` check, package, and a cross-repo job that checks out zor as `../zor`,
   builds it, and sets `ZOR_BIN` for integration tests. Pin actions by commit. Do not claim Android
   runtime coverage from a cross-check.
5. Run in each repository:

   ```sh
   cargo fmt --all --check
   cargo clippy --all-targets --all-features --locked -- -D warnings
   cargo test --all-features --locked
   cargo doc --no-deps --all-features --locked
   cargo package --locked
   ```

   Also run `cargo check --no-default-features --locked` in zor, the fux Android check, all
   cross-repository integration tests, and any repository-specific layering/security checks.
   `cargo publish --dry-run` for zor comes first. A verified fux package cannot be built after Cargo
   strips development paths until crates.io resolves both `koh =0.12.0` and `zor =0.1.1`: before
   then, attempt the command and record the exact unresolved prerequisites. `cargo package
   --no-verify --locked` also resolves registry dependencies and is not an archive-inspection
   workaround. Do not call either attempt a successful package gate. Rerun full `cargo package`
   and `cargo publish --dry-run` for fux after both releases are published.
6. Perform a fresh full-diff review in each repository for correctness, regressions, unsafe edges,
   unbounded input/allocation, task/process leaks, terminal restoration, path/socket attacks,
   flaky timing, and missing tests. Validate findings against current code, fix confirmed issues,
   rerun affected checks, and repeat for any new high-severity finding.

## Deliverables

- Both repositories build, lint, and test on this machine; every ignored test is listed with the
  environment it needs. No required check may be described as green if it was skipped, unavailable,
  pending, or failed for an unrelated reason.
- A phase-by-phase commit list for each repository and final `git status --short` output.
- A closing summary per crate: deviations and dated design notes, resolved open questions, exact
  verification commands/results, independent-review findings and dispositions, supported
  platforms, and remaining risks.
- A separate **Human evidence still required** list: genuine Claude Code captures for any draft
  rules, OSC 7877 collision checks on Terminal.app/iTerm2/kitty/alacritty/wezterm/Termux/tmux,
  provenance/schema of OSC 21337, crates.io ownership/publication, real Android runtime testing,
  and any remote-relay test not run locally.
- Do not publish, tag, push, open a PR, or mutate issues/reviews unless the operator separately asks.