chore: add .gitignore and AGENTS.md; fix implementation-specialist conventions

The scaffold was missing basic repo hygiene, which led to drift: the
implementation-specialist agent's Project Conventions section was a
verbatim copy from alkcall (referencing OperationEnv, EventEnvelope,
Capabilities, alktype as a dep, call.aborted, vendored core types —
none of which exist in alktty), so spawned agents got alkcall
conventions injected.

- .gitignore: target/, node_modules/, .worktrees/ (matches alkcall/alktype)
- AGENTS.md: alktty-specific operating instructions — Git Workflow,
  14 Project Conventions (5-byte chunk header wire format, TtyBackend
  trait one-way door, local feature isolation / WASM invariant,
  BAST-as-doc-not-dep, 3-arg/4-arg ownership shapes, alknet ADR→alktty
  ADR renumbering plan), Verification Commands (incl. wasm checks),
  Architecture Context (phase status, ADR index, port origin)
- .opencode/agents/implementation-specialist.md: replace stale alkcall
  Project Conventions section with alktty-appropriate rules that mirror
  AGENTS.md

Verification: cargo test (80 passed), cargo fmt --check, cargo build
--target wasm32-unknown-unknown all clean. No source changes.
This commit is contained in:
2026-08-17 10:32:21 +00:00
parent e1610c2825
commit 712e7ae071
3 changed files with 390 additions and 49 deletions
+3
View File
@@ -0,0 +1,3 @@
target/
node_modules/
.worktrees/
+95 -49
View File
@@ -210,55 +210,101 @@ This is especially important for complex tasks that span many file operations.
Read `AGENTS.md` at project root for full details. Key rules: Read `AGENTS.md` at project root for full details. Key rules:
1. **No comments in code** — Per project convention. Doc comments (`///`, `//!`) 1. **No comments in code** — Per project convention. Doc comments (`///`, `//!`)
are fine and expected on public API. are fine and expected on public API. Inline `//` comments only when the user
2. **Error handling** — `thiserror` for library error types. No panics in asks or when a non-obvious safety/correctness constraint would otherwise be
library code. No `unwrap()` or `expect()` outside tests. For poisoned missed (e.g., "zero-length stdin is the EOF sentinel — the codec does not
`RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in one special-case it; the adapter interprets `length == 0` chunks").
operation does not cascade to other operations. 2. **Error handling** — `thiserror` for library error types (`TtyError`,
3. **`tokio` is the async runtime** — all I/O is async. Use `tokio::sync` `NegotiateError`, `WireError`; `HandlerError`/`StreamError` come from
primitives (`oneshot`, `mpsc`) for request correlation and subscription alkcall::core). No panics in library code. No `unwrap()` or `expect()`
channels; `parking_lot` for short-held internal locks (`PendingRequestMap`). outside tests. For poisoned `RwLock`/`Mutex`, use
4. **No secret material on the wire** — `call.requested`/`call.responded` `unwrap_or_else(|e| e.into_inner())`.
payloads and `OperationContext.metadata` carry no private keys, API keys, or 3. **`tokio` is the async runtime** — all I/O is async. The adapter's
decrypted credentials. Outbound credentials flow through `Capabilities` per-session pumps, the channels integration, and `TtySession` are all async.
injected at the assembly layer → `HandlerRegistration.capabilities` → Use `tokio::sync` primitives (`oneshot`, `mpsc`) for request/exit
`OperationContext.capabilities` → handler. correlation and stdout/stderr streams. The PTY bridge in `src/local/pty.rs`
5. **No-env-vars invariant** — no handler reads outbound credentials from any is the one exception: three dedicated std threads feeding tokio mpsc/oneshot
source other than `OperationContext.capabilities`. This is a spec-level channels (the `portable_pty` API is blocking) — behind the `local` feature.
invariant, not a runtime convention. 4. **WASM target is load-bearing** — the default crate (no `local` feature)
6. **`OperationEnv` must remain a trait** — the trait-based design enables MUST compile to `wasm32-unknown-unknown`. This is what makes the downstream
registry layering (session overlays, connection overlays, peer-keyed TS/Python adapter story work. `Cargo.toml` uses
composition). Do not make it concrete or hardcode the global registry. `tokio = { default-features = false, features = ["rt", "sync", "io-util", "macros"] }`
7. **Wire formats are stable** — `EventEnvelope` (`{ type, id, payload }` + (the wasm-clean subset alkcall uses); `local` adds `tokio/process` +
length-prefixed JSON framing) and the channels 8-byte chunk header `tokio/rt-multi-thread`. **Do NOT use `features = ["full"]`** — it pulls in
(`[channel_id:u32 BE][length:u32 BE][payload]`) are one-way doors. New event `signal`/`fs`/`net` which break `wasm32-unknown-unknown`. The adapter's
types may be added; existing shapes must not change. `tokio::spawn` is fine on wasm; the `local` module's std threads,
8. **Producer/consumer, not server/client** — both sides of a call or channels `tokio::process::Command`, and `libc::kill` are the non-wasm parts, and
connection can initiate. Use "producer"/"consumer" or "accept side"/"connect they're feature-gated.
side," not "server"/"client." 5. **Wire formats are stable** — two one-way doors:
9. **Vendored core types** — `Connection`, `ProtocolHandler`, `BiStream`, - **5-byte chunk header** (`[stream_type: u8][length: u32 BE][payload]`) —
`BidiStreamSource`, `AuthContext`, `IdentityProvider`, `Identity`, the raw chunk codec in `wire.rs`. Five stream types: `STREAM_STDIN=0`,
`AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`, `STREAM_STDOUT=1`, `STREAM_STDERR=2`, `STREAM_CTRL_IN=3`,
`StreamError` live in this crate. Do not add a separate `alkcore` dependency. `STREAM_CTRL_OUT=4`. Zero-length data chunks are sentinels (zero-length
Keep them lean (no TLS, no transport coupling, no endpoint/accept-loop). stdin = EOF from client; zero-length stdout = "drained" from server);
10. **`alktype` dependency** — use `alktype` for binary layout (channels chunk control chunks are never zero-length. Changing the header, the
header, future binary payload schemas) and JSON payload schema validation stream-type values, or the sentinel semantics breaks all peers.
(`OperationSpec`'s `input_schema`/`output_schema`). Do not roll your own. - **Negotiation frame** (4-byte BE length prefix + UTF-8 JSON
11. **Feature flags** — transports may be feature-gated if the need arises. The `NegotiateRequest` body) — self-contained, not reused from alkcall's
base crate should compile lean (no `quinn`, no `iroh` unless the feature is `EventEnvelope` framing. The `NegotiateRequest` JSON shape is
on). Verify both `cargo test` (default) and `cargo test --all-features` pass wire-stable once consumers exist.
if features are added. Per ADR-093: TTY always uses its 5-byte format, even inside channels. The
12. **Abort cascades to descendants** — `call.aborted` for a parent cascades to channels layer strips its 8-byte header transparently — the same `wire.rs`
all non-terminal descendants. Default `abort-dependents`; code runs in both direct (`alk/tty`) and channels (`alk/channels`) modes;
`continue-running` opt-in. The composing handler decides the child's policy, only the `BiStream` source differs.
not the wire caller. 6. **`TtyBackend` trait shape is a one-way door** — the trait in
13. **Peer authorization via `AccessControl`** — a remote peer's call is `src/backend.rs` is the inversion point between the wire-format adapter and
authorized by `AccessControl::check(peer_identity)`. No `remote_safe` flag, the backend crates (alktty's own `local` feature module, future
no `trusted_peer` bypass. `Visibility::Internal` ops are never wire-callable. `alknet-docker`, `alknet-ssh`). alktty defines the trait; the backends
14. **Naming conventions** — Rust standard: `snake_case` for functions/variables/ implement it. The adapter holds a `HashMap<String, Arc<dyn TtyBackend>>`
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. keyed by the negotiation frame's `backend` string; backends produce
15. **Module structure** — one module per file under `src/`, re-exported from `TtyHandle`s, they do not write to the wire. `TtyError` is
`src/lib.rs`. Public API surface is `lib.rs` re-exports. `#[non_exhaustive]` so new variants are additive.
7. **Producer/consumer, not server/client** — both sides of a `alk/tty` or
`alk/channels` connection can initiate. Use "producer"/"consumer" or
"accept side"/"connect side," not "server"/"client."
8. **`local` feature isolation** — the `local` module is the only non-wasm
part of the crate. The shared/producer/consumer modules (`wire`, `control`,
`negotiation`, `backend`, `adapter`, `channels`, `session`) MUST NOT import
from `crate::local` or reference any `local`-only type (`LocalTtyBackend`,
`portable_pty`, `tokio::process::Command`). The local backend is injected at
the assembly layer, not wired in by the adapter. A regression where a
non-`local` module pulls in `portable_pty`, `tokio::process`,
`std::thread`, or `libc` (outside the existing `cfg(unix)` calls in
`control.rs`/`pipe.rs`) silently breaks the downstream TS/Python adapter
story.
9. **Module structure** — one module per file under `src/`, re-exported from
`src/lib.rs`. Producer half: `src/adapter.rs` + `src/channels.rs`. Consumer
half: `src/session.rs`. Shared: `src/wire.rs`, `src/control.rs`,
`src/negotiation.rs`, `src/backend.rs`. The `src/local/` module is behind
the `local` feature and never imported from the shared/producer/consumer
modules — only re-exported from `lib.rs` under `#[cfg(feature = "local")]`.
10. **BAST document, not alktype dependency** — `docs/architecture/tty-bast.md`
(Phase 4, not yet written) will be the machine-readable spec for the
`alk/tty` wire format, conforming to the BAST meta-schema at
`https://alk.dev/bast/v1/schema`. alktty does **not** depend on alktype;
the hand-rolled `ChunkReader`/`ChunkWriter` in `wire.rs` is the runtime
codec, the BAST document is the human-readable contract. If runtime
validation against the BAST becomes desirable later, alktype becomes an
optional dep — do not roll your own offset map or validator for complex
formats.
11. **Feature flags** — the only feature is `local` (default = `[]`). `local`
is inherently non-wasm; enabling it on wasm is a build error by design.
Verify both `cargo test` (default) and `cargo test --all-features` pass.
Do not add a feature flag that pulls non-wasm deps into the default crate.
12. **Access control** — the direct-ALPN path keeps the ad-hoc scope check
(`has_scope(identity, TTY_OPEN_SCOPE)`) and optionally consults
`OwnershipProvider` (`provider.owns(id_ref, kind, &id, "tty")` — the 4-arg
shape). The channels path gets both for free via
`ChannelCore::register_openable`, which wires `AccessControl` into the
operation spec. `OwnershipStore::record(&self, identity, resource_type,
resource_id)` is the 3-arg shape (alkcall dropped the old `action` arg).
13. **Naming conventions** — Rust standard: `snake_case` for functions/variables/
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for
constants (`STREAM_STDIN`, `TTY_OPEN_SCOPE`, `CHUNK_HEADER_LEN`).
14. **No `unsafe`** — the crate has zero `unsafe` blocks. The PTY bridge's
`libc::kill(-pgid, sig)` and `libc::kill(pid, sig)` calls are safe `libc`
crate APIs (not `unsafe` blocks in this crate). Do not introduce `unsafe`
for performance.
## Key Principles ## Key Principles
+292
View File
@@ -0,0 +1,292 @@
# AGENTS.md
Operating instructions for opencode agents working in this repo. opencode
auto-loads this file as instructions, overriding the built-in defaults for
this project. Custom agents in `.opencode/agents/` inherit these rules
unless their own prompts say otherwise.
## Git Workflow
**Commit and push when reasonable.** When a change is complete and
verified (build + lint + tests pass), commit and push to `origin/main`
without asking. This overrides the built-in default of "only commit when
explicitly asked."
The workflow:
1. Make the change
2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check`, `cargo doc --no-deps` if docs changed
3. Inspect `git status` and `git diff` before staging — stage only the
intended files, never secrets
4. Write a concise commit message matching the repo style (see `git log
--oneline -10`). For multi-point changes, use a summary line plus a
body with bullet points and a verification block.
5. `git push origin main`
6. Report the commit hash and the verification summary
Exceptions — **do not** commit or push without asking:
- The change is exploratory / speculative (you're not sure the user wants
it kept)
- The user is actively reviewing the diff and may ask for changes
- The change touches the wire format or the `TtyBackend` trait shape
(one-way doors — see "Wire formats are stable" and "`TtyBackend` trait
shape is a one-way door" below; the 5-byte chunk header, the
negotiation-frame layout, the `NegotiateRequest` JSON shape, and the
trait signature are wire-stable contracts once consumers exist)
- You'd be force-pushing, amending a published commit, creating an empty
commit, or skipping hooks
Never commit secrets, keys, or credentials. If a commit fails or hooks
reject it, fix the issue and create a new commit — do not amend the
failed one.
Git identity is preconfigured (`glm-5.2 <glm-5.2@alk.dev>`). Do not
change `git config`, skip hooks, or use `git commit -i`.
## Project Conventions (Rust / TTY protocol crate)
This is the `alk/tty` terminal-session protocol crate — a
producer/consumer protocol crate on top of alkcall channels. It folds
the old `alknet-tty` (wire/adapter/backend-trait half) and
`alknet-tty-local` (`portable_pty` + `tokio::process` half) from the
alknet mono-repo into a single crate with a `local` feature. The
conventions below apply to all work in `src/` and `tests/`. They mirror
`.opencode/agents/implementation-specialist.md` §Project Conventions
and are repeated here so they apply to every session, not just spawned
implementation agents.
1. **No comments in code** unless the user explicitly asks. This is a
project-wide convention. Doc comments (`///`, `//!`) are fine and
expected on public API. Inline `//` comments only when the user asks
or when a non-obvious safety/correctness constraint would otherwise be
missed (e.g., "zero-length stdin is the EOF sentinel — the codec does
not special-case it; the adapter interprets `length == 0` chunks").
2. **Error handling** — `thiserror` for library error types
(`TtyError`, `NegotiateError`, `WireError`, `HandlerError`/`StreamError`
come from alkcall::core). No panics in library code. No `unwrap()` or
`expect()` outside tests. If you reach for `unwrap`, the error path
wasn't specified — stop and decide what should actually happen. For
poisoned `RwLock`/`Mutex`, use
`unwrap_or_else(|e| e.into_inner())` so a panic in one operation does
not cascade to other operations.
3. **`tokio` is the async runtime** — all I/O is async. The adapter's
per-session pumps, the channels integration, and `TtySession` are all
async. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for
request/exit correlation and stdout/stderr streams. The PTY bridge in
`src/local/pty.rs` is the one exception: it uses three dedicated std
threads feeding tokio mpsc/oneshot channels (the `portable_pty` API
is blocking) — and it's behind the `local` feature.
4. **WASM target is load-bearing** — the default crate (no `local`
feature) MUST compile to `wasm32-unknown-unknown`. This is what makes
the downstream TS/Python adapter story work (a wasm-compiled alktty
is the protocol layer for a sandboxed adapter). `Cargo.toml` uses
`tokio = { default-features = false, features = ["rt", "sync",
"io-util", "macros"] }` (the wasm-clean subset alkcall uses) and
`local` adds `tokio/process` + `tokio/rt-multi-thread`. **Do NOT use
`features = ["full"]`** — it pulls in `signal`/`fs`/`net` which break
`wasm32-unknown-unknown`. The adapter's `tokio::spawn` for per-session
pumps is fine on wasm (the wasm tokio runtime supports `spawn`); the
`local` module's std threads, `tokio::process::Command`, and
`libc::kill` are the non-wasm parts, and they're feature-gated.
`libc` stays under `cfg(unix)` (it's already there for
`signal_from_name` and the pipe-mode `kill` path — neither runs on
wasm because the only callers are in `local`).
5. **Wire formats are stable** — two wire formats live in this crate,
both one-way doors:
- **5-byte chunk header** (`[stream_type: u8][length: u32 BE]
[payload]`) — the raw chunk codec in `wire.rs`. Five stream types:
`STREAM_STDIN=0`, `STREAM_STDOUT=1`, `STREAM_STDERR=2`,
`STREAM_CTRL_IN=3`, `STREAM_CTRL_OUT=4`. Zero-length data chunks are
sentinels (zero-length stdin = EOF from client; zero-length stdout
= "drained" from server); control chunks are never zero-length.
Changing the header, the stream-type values, or the sentinel
semantics breaks all peers. See ADR-052 (ported as alktty ADR-001).
- **Negotiation frame** (4-byte BE length prefix + UTF-8 JSON
`NegotiateRequest` body) — self-contained per ADR-057 (ported as
alktty ADR-008), not reused from alkcall's `EventEnvelope` framing
(the payload is `NegotiateRequest`, not `EventEnvelope`; alkcall's
`FrameFramedReader` is hardcoded to deserialize `EventEnvelope`).
The `NegotiateRequest` JSON shape is wire-stable once consumers
exist.
Per ADR-093 (ported as alktty ADR-007): TTY always uses its 5-byte
format, even inside channels. The channels layer strips its 8-byte
header and hands TTY the payload transparently — the same `wire.rs`
code runs in both direct (`alk/tty`) and channels (`alk/channels`)
modes; only the `BiStream` source differs.
6. **`TtyBackend` trait shape is a one-way door** — the trait
(`src/backend.rs`) is the inversion point between the wire-format
adapter and the backend crates (alktty's own `local` feature module,
future `alknet-docker`, `alknet-ssh`). alktty defines the trait; the
backends implement it. Changing the trait shape after backends exist
is a rewrite across crates. The adapter holds a
`HashMap<String, Arc<dyn TtyBackend>>` keyed by the negotiation
frame's `backend` string and pumps the `TtyHandle` fields
bidirectionally; backends produce handles, they do not write to the
wire. `TtyError` is `#[non_exhaustive]` so new variants are additive
(two-way-door extension within the one-way trait shape). See ADR-053
(ported as alktty ADR-002).
7. **Producer/consumer, not server/client** — inherited from alkcall.
Both sides of a `alk/tty` or `alk/channels` connection can initiate.
A producer exposes TTY (direct ALPN via `TtyAdapter::ProtocolHandler`,
or via `channels::register_openable`); a consumer opens a session
(`TtySession::connect_direct` / `open_via_channels`). Both sides can
be both simultaneously — connection direction (who opened it) is
independent of TTY direction (who's the shell, who's the client).
Avoid "server" and "client" framing in docs and API names; use
"producer" and "consumer," or "accept side" / "connect side" for the
connection-establishment half specifically.
8. **Module structure** — one module per file under `src/`, re-exported
from `src/lib.rs`. Public API surface is `lib.rs` re-exports. The
crate has three concerns:
- **Producer half** — `src/adapter.rs` (`TtyAdapter` + `drive_session`
for direct `alk/tty`) and `src/channels.rs` (`register_openable`
helper + `TtyOpenHandler` for the `alk/channels` multiplexed path).
- **Consumer half** — `src/session.rs` (`TtySession` typed client
with `connect_direct` and `open_via_channels` constructors).
- **Shared** — `src/wire.rs` (chunk codec), `src/control.rs`
(control-message enum + `signal_from_name`), `src/negotiation.rs`
(negotiation frame + `NegotiateRequest`), `src/backend.rs`
(`TtyBackend` trait, `TtyHandle`, `TtyParams`, `TtyError`).
The `src/local/` module (the folded `alknet-tty-local`) is behind the
`local` feature and never imported from the shared/producer/consumer
modules — only re-exported from `lib.rs` under
`#[cfg(feature = "local")]`.
9. **`local` feature isolation** — the `local` module is the only
non-wasm part of the crate. The shared/producer/consumer modules
(`wire`, `control`, `negotiation`, `backend`, `adapter`, `channels`,
`session`) MUST NOT import from `crate::local` or reference any
`local`-only type (`LocalTtyBackend`, `portable_pty`,
`tokio::process::Command`). The producer's `TtyAdapter` takes a
`HashMap<String, Arc<dyn TtyBackend>>` — the local backend is
injected at the assembly layer, not wired in by the adapter. A
regression where a non-`local` module pulls in `portable_pty`,
`tokio::process`, `std::thread`, or `libc` (outside the existing
`cfg(unix)` calls in `control.rs`/`pipe.rs`) silently breaks the
downstream TS/Python adapter story.
10. **BAST document for the wire format** — `docs/architecture/tty-bast.md`
(Phase 4, not yet written) will be the machine-readable spec for the
`alk/tty` wire format, conforming to the BAST meta-schema at
`https://alk.dev/bast/v1/schema`. BAST is plain JSON — no
dependency required to author or consume it. alktty does **not**
depend on alktype; the hand-rolled `ChunkReader`/`ChunkWriter` in
`wire.rs` is the runtime codec, the BAST document is the
human-readable contract that describes what those types round-trip.
If runtime validation against the BAST becomes desirable later,
alktype becomes an optional dep and the BAST document is already
there to feed it. Do not roll your own offset map or validator for
complex formats — use alktype (as an optional dep) when that
surfaces.
11. **Access control** — the producer's direct-ALPN path keeps the
existing ad-hoc scope check (`has_scope(identity, TTY_OPEN_SCOPE)`)
for the scope gate and optionally consults `OwnershipProvider` for
the resource-ownership check (`provider.owns(id_ref, kind, &id,
"tty")` — the 4-arg shape alkcall adopted). The channels path gets
both for free via `ChannelCore::register_openable`, which wires
`AccessControl` into the operation spec — the registry runs the ACL
before the wrapper, so the `OpenHandler` only needs to validate
params and spawn the protocol. Terminal sessions are resources per
ADR-050 (ported as alktty ADR-003); `OwnershipStore::record(&self,
identity, resource_type, resource_id)` is the 3-arg shape (alkcall
dropped the old `action` arg).
12. **Feature flags** — the only feature is `local` (default = `[]`).
`local` is inherently non-wasm (`portable-pty` + `tokio::process`
need a real OS); enabling `local` on wasm is a build error by
design. Verify both `cargo test` (default) and
`cargo test --all-features` pass. Do not add a feature flag that
pulls non-wasm deps into the default crate.
13. **Naming** — Rust standard: `snake_case` for functions/variables/
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for
constants (`STREAM_STDIN`, `TTY_OPEN_SCOPE`, `CHUNK_HEADER_LEN`).
14. **No `unsafe`** — the crate has zero `unsafe` blocks. The PTY
bridge's `libc::kill(-pgid, sig)` and `libc::kill(pid, sig)` calls
are safe `libc` crate APIs (not `unsafe` blocks in this crate);
bounds-checked slice access via `get(..)`/`ok_or_else` is the
pattern. Do not introduce `unsafe` for performance.
## Verification Commands
Run these before committing. All must pass.
```bash
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps
cargo test --all-features
cargo check --target wasm32-unknown-unknown # the default crate stays wasm-clean
cargo clippy --target wasm32-unknown-unknown -- -D warnings
cargo publish --dry-run --allow-dirty # before a release
```
The wasm check is the structural guard for the "default crate is
wasm-clean" invariant — run it whenever a non-`local` module changes.
`cargo test --all-features` exercises the `local` backend (PTY mode is
`#[cfg(unix)]`; pipe mode is cross-platform).
## Architecture Context
- `docs/plans/project-setup.md` — the current plan (phases 05). Phase 0
(scaffold hygiene), Phase 1 (port core types), Phase 2 (channels
integration + `TtySession`), and Phase 3 (`local` backend) are landed.
Phase 4 (architecture docs + BAST schema + renumbered ADRs) and
Phase 5 (tests, including integration tests in `tests/` at the crate
root) are not yet done.
- `docs/architecture/` does not exist yet — it's created in Phase 4.
The ADRs referenced below (alknet ADR-052, 053, 054, 055, 056, 057,
077, 093) will be ported and renumbered into alktty's ADR range
(001..008) at that time. Until then, the alknet originals at
`/workspace/@alkdev/alknet/docs/architecture/decisions/` are the
authoritative source — read them before non-trivial changes to the
wire format, trait, or adapter.
- Key ADRs that inform this crate's design (alknet numbers → planned
alktty numbers):
- ADR-052 → 001 — two-carriage wire format (JSON negotiation + raw
chunks); the 5-byte chunk header
- ADR-053 → 002 — `TtyBackend` trait (the inversion point between
wire-format adapter and backend crates)
- ADR-050 → 003 — dynamic resource ownership for runtime-spawned
terminal sessions
- ADR-054 → 004 — single crate with `local` feature (resolves the
alknet cyclic-dep workaround; the local backend is folded in)
- ADR-055 → 005 — exit-code reporting (`{"type":"exit","code":N}` on
`ctrl_out`; `code: -1` on wait-failure)
- ADR-056 → 006 — control-message split (`ctrl_in` client→server,
`ctrl_out` server→client; the bidirectionality fix)
- ADR-093 → 007 — TTY always uses its 5-byte format inside channels
(reverses ADR-077; channels strips its 8-byte header transparently)
- ADR-057 → 008 — negotiation framing is self-contained (not reused
from alkcall's `EventEnvelope` framing)
- If a TODO references a "Phase 7" note or a design direction that an
ADR has since decided against, the TODO is stale — remove it and
align with the ADR. Do not implement the rejected design.
- The crate was ported from `/workspace/@alkdev/alknet/crates/
alknet-tty/src/` (wire, control, negotiation, backend, adapter) and
`/workspace/@alkdev/alknet/crates/alknet-tty-local/src/` (local
backend, pty, pipe). The source architecture docs at
`/workspace/@alkdev/alknet/docs/architecture/crates/tty/` are the
port origin for the Phase 4 spec docs.
- alkcall (`/workspace/@alkdev/alkcall`) is the upstream dependency.
All types formerly in `alknet-core` (`Connection`, `ProtocolHandler`,
`BiStream`, `BidiStreamSource`, `AuthContext`, `Identity`,
`IdentityProvider`, `AccessControl`, `OwnershipProvider`,
`OwnershipStore`, `InMemoryOwnershipStore`, `HandlerError`,
`StreamError`) come from `alkcall::core`. alkcall is v0.1.x —
breaking changes are expected at this major-zero stage; this is the
first real consumer, so we find and fix issues upstream rather than
working around them. Pin `alkcall = "0.1.1"` and bump deliberately.