Files
alktty/docs/reviews/003-prepublish-review.md
T

30 KiB
Raw Blame History

status, last_updated, resolved, reviewed_artifacts, tool, reviewer, base_commit, resolutions
status last_updated resolved reviewed_artifacts tool reviewer base_commit resolutions
open 2026-09-05 1, 4, 6, 7, 12, 13, 16 (P14 closed)
src/lib.rs
src/wire.rs
src/control.rs
src/negotiation.rs
src/backend.rs
src/adapter.rs
src/session.rs
src/channels.rs
src/local/mod.rs
src/local/backend.rs
src/local/pty.rs
src/local/pipe.rs
tests/common/mod.rs
tests/negotiation.rs
tests/pipe.rs
tests/pty.rs
Cargo.toml
AGENTS.md
docs/architecture/README.md
docs/plans/project-setup.md
two subagent reviews (code review + packaging/docs vs alkcall 0.4.1) + manual verification of every finding against source + empirical deadlock reproduction prepublish review for v0.1.0 (subagent-assisted) 918af40 (review
P1 + P13 resolved 2026-09-05 (87c52e5) — see the Session-1 Resolution section
P12 + P6 + P7 + P4 + P16 resolved 2026-09-05 — see the Session-2 Resolution section

Code Review #003 — Prepublish Review (v0.1.0)

Purpose

Prepublish sanity check for the first crates.io release. Everything before this point was reviewed while the crate was unpublished, so every API-shape and packaging mistake was still free. This review is the last pass under that property: it looks for (a) correctness bugs, (b) semver-visible surface decisions that are cheap now and expensive per-consumer later, and (c) packaging/docs parity with alkcall 0.4.1 (the org's published-crate reference).

Two subagent reviews were run against this tree — a code review (conventions, wire format, concurrency, API surface, test gaps) and a packaging/docs review (Cargo.toml parity with alkcall, package-list inspection, docs consistency, MSRV). Every M and L finding below was then manually re-verified against source by the publisher before being recorded here; the P1 deadlock was additionally reproduced empirically (twice, independently).

Verification Baseline

All verification run on the reviewed tree (918af40, before this review's own commit):

  • cargo test: 95 lib tests pass (default, wasm-clean build).
  • cargo test --all-features: 138 tests pass (119 lib + 5 negotiation + 6 pipe + 8 pty). Zero failures.
  • cargo clippy --all-targets -- -D warnings: clean.
  • cargo clippy --target wasm32-unknown-unknown -- -D warnings: clean (also cargo check --target wasm32-unknown-unknown).
  • cargo fmt --check: clean.
  • cargo doc --no-deps: 0 warnings.
  • cargo publish --dry-run --allow-dirty: OK — packages 42 files, 671.0KiB (180.7KiB compressed), no warnings. (One packaged file is wrong — see P2.)
  • alkcall pinned at 0.4.1 (Cargo.lock); alkcall = "0.4.0" in Cargo.toml; MSRV 1.85 backed by lockfile pins (idna_adapter 1.2.0, icu 2.0.x/1.5.x) — no post-1.85 syntax found in src/ or tests/.
  • CJK scan (git ls-files | xargs grep -lP "[\p{Han}]"): zero matches in tracked files.

Findings

P1. [M — blocks publish] pump_session deadlocks when the backend produces ≥63 stdout chunks before exit

Files: src/adapter.rs:428-490 (pump_session)

Problem: the stdout/stderr pumps and the exit_code future are joined (tokio::join!, adapter.rs:465-475) before the client-facing drainer loop starts (adapter.rs:480-486). All three producers share one bounded channel (mpsc::channel::<Chunk>(64), adapter.rs:437). Once 64 chunks are queued, pump_stdout's writer_tx.send(...).await (adapter.rs:516) parks — and because the drainer hasn't started, the channel never drains. pump_stdout can't finish, so the sentinel send (adapter.rs:520) can't happen, exit_code may still resolve but join! never completes (it waits for the stdout pump handle), the exit chunk is never enqueued, and the drainer never starts. Circular wait; the session hangs with the client actively reading.

Reproduced empirically (mock backend streaming N chunks through the real drive_session over a duplex transport, client reading with a 10 s timeout):

  • N=62 → OK: 62 data chunks + stdout sentinel + exit chunk = 64, exactly fills the channel before the join completes.
  • N=63 → deadlock: client times out having received 0 chunks.

Any real session (cat a file, ls -R) exceeds 64 chunks instantly — the first published consumer would hit this on day one.

Fix direction (preserves the exit-chunk-is-last invariant, ADR-005): start the drainer before the join — spawn a task owning writer_rx and client_write that loops recv()ChunkWriter::write_chunk (breaking on write error, as today); run the pumps/exit join exactly as now; send_exit_chunk after the join (same ordering — single mpsc FIFO, so the exit chunk is still last); then drop(writer_tx) so the drainer sees channel close and finishes. The W: AsyncWrite + Send + Unpin + 'static bound already permits moving the writer into a task. Add a >64-chunk regression test with the fix (see P13).


P2. [M] AGENTS.md ships in the published crate; exclude list has two dead entries

Files: Cargo.toml:11

Problem: alkcall's exclude contains "AGENTS.md" (alkcall/Cargo.toml:12); alktty's does not. Verified in the package list: alktty-0.1.0/AGENTS.md ships. Also:

  • "Cargo.lock" in exclude is a dead entry — modern cargo force-includes the lockfile; it ships in both crates regardless (alkcall doesn't exclude it). Remove the entry or keep as documentation.
  • "docs/research/" targets a directory that doesn't exist (harmless, but dead).
  • docs/plans/project-setup.md ships, including its /workspace/@alkdev/... absolute paths. alkcall ships only docs/architecture/*. Consider excluding docs/plans/.

Fix: add "AGENTS.md" (and decide on docs/plans/) to exclude; drop or keep the dead entries deliberately.


P3. [M] Stale agent- and reader-facing docs contradict the tree

Files: AGENTS.md:253-257,307-310, docs/architecture/README.md:21-22,28-37,138-142

Problem: three stale spots, one of them actively harmful:

  • AGENTS.md:310 says alkcall "is v0.1.x … Pin alkcall = "0.1.1"" while Cargo.toml pins alkcall = "0.4.0" (lock: 0.4.1). An agent following AGENTS.md would downgrade the dependency. Same doc's ADR mapping stops at 007→008 and omits ADR-009 (landed in 37ae07a).
  • AGENTS.md:253-254 and docs/architecture/README.md:140-142 say "Phase 5 … is not yet done" — Phase 5 landed 2026-08-17 per docs/plans/project-setup.md:5, and the integration tests exist and pass (19 tests).
  • docs/architecture/README.md:28-37 ADR table stops at 008 and the preamble says "renumbered into alktty's ADR range (001..008)" — the directory contains 009, which overview.md and tty-adapter.md already reference.

Fix: update AGENTS.md (alkcall guidance, phase status, ADR range 001..009) and the architecture README (ADR-009 row, phase status). Related N-level staleness in the plan doc is folded into P16.


P4. [M] No README (and no readme field)

Files: Cargo.toml ([package]), repo root

Problem: no README.md and no readme = "README.md" field; alkcall has both. cargo publish --dry-run does not warn, so nothing catches it at publish time — the crates.io page will be bare and the missing field is a (trivially additive, but avoidable) post-publish change.

Fix: write the README mirroring alkcall's structure — tagline + positioning paragraph, ## Quick start with per-role compilable examples (producer: backends map + TtyAdapter as ProtocolHandler, or channels::register_openable; consumer: TtySession::connect_direct / open_via_channels), ## Architecture (producer/consumer framing per AGENTS.md convention 7; wasm target + local feature note), ## Documentation (architecture docs, docs.rs), ## License. Add the readme field. Do this after P5/P6 so the examples show the final surface.


P5. [L] Backend lookup runs before the scope gate

Files: src/adapter.rs:300-312 (validate_and_allocate)

Problem: backends.get(&req.backend) (adapter.rs:300) is checked before has_scope(identity, TTY_OPEN_SCOPE) (adapter.rs:309). An unscoped caller gets unknown_backend for registered names and forbidden otherwise — a differential that enumerates registered backend names ("local", and every future alknet-docker/alknet-ssh registration) to any authenticated-but-unscoped identity.

Fix: move the scope check above the lookup (arguably above the carriage/cmd checks too). Trivial; check whether any test asserts unknown_backend with an unscoped identity and update accordingly.


P6. [L] Wire write paths accept invalid chunks

Files: src/wire.rs:255-306 (write_chunk, write_stdin, write_ctrl_in_json, write_ctrl_out_json)

Problem: the read path validates (stream_type > 4 → error, length > MAX_CHUNK_LEN → error, wire.rs:200-212), but the write paths validate nothing: they accept stream_type > 4, payloads larger than MAX_CHUNK_LEN, and compute chunk.bytes.len() as u32 — silently truncating a ≥2³² payload and corrupting the peer's framing. A bad Chunk from a caller (or a future backend bug) corrupts the peer's stream instead of failing locally.

Fix: return RawError (e.g. InvalidStreamType/ChunkTooLarge reused) when stream_type > 4 or len > MAX_CHUNK_LEN. Additive pre-consumers; aligns write-side with read-side invariants. Also decide the empty-payload inconsistency while touching this: write_chunk/ write_stdin skip the payload when empty but write_ctrl_* always writes it (wire.rs:261 vs 290/303) — harmless on the wire (length 0 ≡ skip) but pick one shape.


P7. [L] ChunkReader: calling read_chunk() after a manual peek_stream_type() silently corrupts framing

Files: src/wire.rs:166-185

Problem: read_chunk() internally calls peek_stream_type() then read_chunk_after_peek() (wire.rs:167-168). A caller who has already peeked and then calls read_chunk() (instead of read_chunk_after_peek()) consumes a second header byte — the peeked byte is gone — and the stream desynchronizes with no error. The docs steer callers correctly, but the misuse is one wrong call away and silent.

Fix: track peeked state in the reader (a bool; read_chunk asserts/uses it) or debug_assert!(!self.peeked) in read_chunk. Small, pre-consumers, and it makes the misuse loud instead of corrupt.


P8. [L] StdinSink::poll_shutdown returns Pending without registering a waker

Files: src/local/pty.rs:339-357

Problem: on a full stdin command channel, try_send(StdinCmd::Eof)FullPoll::Pending — but _cx is ignored, so no waker is registered. The input pump task may never be woken: a stdin blast that fills the channel immediately followed by EOF never delivers the EOF to the child, whose exit_code then never resolves. Same hang shape as P1, lower trigger probability.

Fix: mirror poll_write's inflight-future pattern in the same file (send an async send and poll it), so backpressure wakes correctly.


P9. [L] pty.rs uses .lock().expect("… poisoned") — violates the poisoned-lock convention

Files: src/local/pty.rs:134,175,184,433,474

Problem: five non-test sites use .expect() on lock acquisition. AGENTS.md convention 2 mandates unwrap_or_else(|e| e.into_inner()) so a panic in one operation does not cascade. (All other .expect() sites in src/local/ are under #[cfg(test)] — verified: local/backend.rs:83, pty.rs:546, pipe.rs:302 are the test-module boundaries.)

Fix: mechanical replacement at the five sites.


P10. [L] pty.rs panics on thread-spawn failure in library code

Files: src/local/pty.rs:465,504,523

Problem: thread::Builder::spawn(...).expect("spawn pty-reader/-writer/-waiter") panics on thread-spawn failure (fd/thread exhaustion) — a panic in library code, and the no-panics convention's spirit. allocate_pty already returns Result, so the error path exists.

Fix: map the three sites to TtyError (e.g. Backend / AllocFailed with the io error message).


P11. [L] recv_stdout doc says the stream ends at the sentinel; the implementation delivers it as an empty item

Files: src/session.rs:436-453 (doc vs unfold)

Problem: the doc states the stream ends when the server's stdout reaches EOF (the zero-length sentinel). The unfold ends only when the read pump terminates — the sentinel arrives as an empty Bytes item the consumer must filter. The crate's own tests do exactly that (session.rs:918, 1160), i.e. the implementation is the de-facto contract and the doc is wrong — or the doc is the intended contract and the unfold should terminate on the empty stdout item.

Fix: pick one before consumers codify the accident. Terminating the stream on the empty-stdout item matches the documented contract (and the sentinel's meaning: "drained") and is the better shape; the doc fix is the fallback.


P12. [L] No crate-root re-exports of the primary types

Files: src/lib.rs:49-58

Problem: the crate root exports modules only; every consumer writes alktty::adapter::TtyAdapter, alktty::session::TtySession, alktty::backend::{TtyBackend, TtyError, TtyHandle}. Ergonomics surface decided implicitly by omission. alkcall re-exports its core types (alkcall::core) — the org pattern favors a curated root.

Fix: add root re-exports (pub use of the primary types — and decide whether wire/control/negotiation constants like STREAM_STDOUT get root aliases). Purely additive; do it before the README (P4) so examples use the short paths.


P13. [L] Test gaps on the riskiest paths

Files: src/wire.rs (tests), tests/

Problem: no test drives >64 chunks through pump_session (the P1 regression — the single most important missing test in the crate); no MAX_CHUNK_LEN exact-boundary test (the suite covers over-limit only); no test for the server sending stream_type 0/3 toward the consumer's read pump (the pump's discard arm); and TtyAdapter::handle's accept_bi loop is untested (needs a transport harness).

Fix: the P1 fix lands with its backpressure regression test; add the boundary/pump-discard cases to wire.rs/session.rs unit tests. The accept-loop harness can wait for the first transport-level consumer.


P14. [N] register_openable returns Result<(), String>

Files: src/channels.rs:108

Problem: an untyped error locks String into the public shape. However, alkcall's own register_openable/register_on have the identical Result<(), String> shape (alkcall src/channels/operations.rs:65,405) — alktty is consistent with its upstream, and a typed error here ahead of alkcall's would be a divergence.

Disposition: closed as consistent-with-upstream. Revisit only if alkcall types its return first; match whatever alkcall does.


P15. [N] Minor polish batch

Verified, low-stakes items; batch into one commit when convenient:

  • src/backend.rs:253-254 — doc typo: "alktty's local the reference implementation" (missing "module is").
  • Cargo.toml:50tokio-stream is redundant as a dev-dependency (the main dep is genuinely used at adapter.rs:73; dev-deps inherit it).
  • src/adapter.rs:362NegotiationError::Io(_) arm returns silently while the catch-all arm logs debug!; add a log for parity.
  • src/session.rs:191-193connect_direct flattens alkcall's StreamError into io::Error::new(ConnectionReset, string), losing the type. Consider a TtySessionError variant pre-consumers (additive now, breaking later — same logic as R5 in review #002).
  • src/session.rs:228params.clone() exists only for the fail-fast parse; serde_json::from_value can consume via Deserialize::deserialize(&params)… or restructure to avoid the clone. Trivial.
  • src/adapter.rs:133set_identity result discarded with let _; log on failure or assert in debug.
  • src/adapter.rs:478drop(input_pump) detaches the client→backend pump; it lingers until the client disconnects after session end. Aborting at session end is tighter.
  • src/negotiation.rs:263-271 — an "error" key in fields would overwrite the error code in error_response_bytes; no caller passes one today. One-line guard or doc note.
  • src/adapter.rs:146-150, src/session.rs:451 — two trivial inline comments (split-idiom explanation; "Already taken") — the carve-out is for non-obvious correctness constraints; promote or remove.
  • docs/plans/project-setup.md:530-531 — test counts stale (says 80+19; actual 119 lib + 19 integration = 138 under --all-features); front-matter last_updated dates in overview.md/tty-adapter.md predate the ADR-009 edits.

P16. [N] No CHANGELOG.md

Files: repo root

Problem: alkcall ships a Keep-a-Changelog-format CHANGELOG.md (# Changelog preamble, ## [X.Y.Z] - YYYY-MM-DD sections with ### Added/Changed/Fixed, link definitions mapping versions to git tags). alktty has none, so v0.1.0's entry will be written retroactively from the log.

Fix: start CHANGELOG.md with the full [0.1.0] history (the crate's commits are the source), including the link definition for the v0.1.0 tag — and note alkcall's own 0.4.x headings currently lack theirs; don't replicate that nit.


Examined and closed

  • RawError/NegotiationError lack #[non_exhaustive] — matches the recorded policy from review #002's R5 resolution: those enums mirror fixed wire semantics and stay exhaustive deliberately (in-crate matchers keep exhaustiveness checking); only the extension-point enums (TtyError, TtySessionError) are #[non_exhaustive]. Both are. No change.
  • register_openable untyped error — see P14; consistent with alkcall's shape. Closed.
  • "Phase 7" references — all are the legitimate alknet-Phase-7 provenance note inside ADR-001 (control-channel split amendment), matching AGENTS.md's instruction. Not stale.
  • MSRV 1.85 — no let-chains, no edition-2024 syntax, no post-1.85 std APIs in src//tests/; lockfile pins back the claim. Clean.
  • unsafe — exactly the four documented libc::kill sites in src/local/ (pty.rs:157,162, pipe.rs:167,176). Convention 14 holds.
  • local-feature isolation — zero portable_pty/tokio::process/ std::thread/libc code outside src/local/ (doc mentions only); wasm check + clippy pass. Convention 9 holds.

What's Good

  • The verification baseline is genuinely clean — 138 tests, both clippy targets, fmt, doc, publish dry-run, wasm build all pass on the reviewed tree.
  • The reader side of the wire format is correct. Header bounds checks, bounded allocation (≤16 MiB before vec!), ConnectionClosed vs Io distinction, the 0x00 framing-disambiguation invariant, and the BAST-drift guard on enum ordering all match ADR-001/006.
  • The cancellation/kill contract holds. Both kill guards (LocalExitFuture, PipeExitFuture) disarm on resolve and kill on cancel (ADR-006), verified by the cancel-cleanup integration tests; TtySession::wait's watch pattern has no lost wakeup; Drop aborts the read pump.
  • The packaging is one file away from alkcall parity. Every other [package] field matches (edition, rust-version, license + both files, keywords at the 5 limit, categories, [lib] name), and the publish dry-run is otherwise warning-free.
  • The docs structure is sound. The ADR set is complete on disk (001009, including the reversal pair 007/008), the "Phase 7" provenance is handled per convention, and the port-provenance alknet-tty mentions all correctly describe history.

Remediation Plan

ID Finding Fix Effort Risk Status
P1 pump_session deadlock ≥63 chunks concurrent drainer + regression test small medium (core pump) resolved (87c52e5)
P2 AGENTS.md ships; dead exclude entries fix exclude trivial none open
P3 stale AGENTS.md + architecture README refresh phase/ADR/alkcall text trivial none open
P4 no README + readme field write README (after P5/P6/P12) medium none resolved (Session 2)
P5 scope gate after backend lookup reorder checks trivial low (test updates) open
P6 write paths accept invalid chunks validate on write; unify empty-payload shape small low resolved (Session 2)
P7 read-after-peek framing corruption peeked-state guard small none resolved (Session 2)
P8 poll_shutdown missing waker inflight-future pattern small low open
P9 poisoned-lock .expect() × 5 into_inner() trivial none open
P10 thread-spawn .expect() × 3 map to TtyError trivial none open
P11 recv_stdout sentinel contract terminate stream on sentinel (or fix doc) small low (semver-visible) open
P12 no crate-root re-exports pub use primary types trivial none resolved (Session 2)
P13 test gaps backpressure (>64 chunks) + boundary tests small none resolved (87c52e5; accept-loop harness deferred)
P14 Result<(), String> on register_openable closed — alkcall parity none none closed
P15 minor polish batch one batched commit small none open
P16 no CHANGELOG.md start changelog with [0.1.0] small none resolved (Session 2)

Suggested Session Breakdown

Natural break points, cheapest-dependency order first:

  1. P1 + P13 — the deadlock and its regression test are one unit (the test proves the fix). Highest value; do first.
  2. P12 + P6 + P7 — the API-shape batch (root re-exports, write validation, peek guard). Then P4 (README uses the final surface) and P16 (changelog) in the same or next session.
  3. P8 + P9 + P10 — the local robustness batch, all in pty.rs.
  4. P5 + P11 — producer-hygiene batch (scope reorder + sentinel contract decision).
  5. P2 + P3 + P15 — packaging/docs closeout; then final cargo publish --dry-run and tag.

Session-1 Resolution (P1 + P13, 2026-09-05)

P1 and P13 were resolved together (the regression test is part of the fix). Commit: 87c52e5.

  • P1 fix: pump_session now spawns the drainer (drain_chunks, a new function owning writer_rx + client_write) before the pumps+exit join; after the join, send_exit_chunk enqueues as before, then drop(writer_tx) lets the drainer observe channel close and finish. The single FIFO preserves exit-chunk-is-last (ADR-005); W's existing 'static + Send + Unpin bounds permitted moving the writer into the task. The doc comment on pump_session was updated to describe the concurrent-drainer shape and why it is load-bearing.
  • P1 regression test (adapter::tests::backpressure_more_chunks_than_channel_capacity): drives 80 chunks (> the 64-slot channel) through a real drive_session via the TestBackend harness, asserts in-order delivery of all 80 + sentinel + exit, with a 10 s per-read timeout so a regression fails the test instead of hanging it (the test body was validated against the bug: with the old order reinstated, it fails with "chunk read must not stall (P1 regression: deadlock): Elapsed" — the producer is spawned so the test's own sends cannot park first).
  • P13 boundary test (wire::tests::chunk_at_exact_max_len_boundary): a payload of exactly MAX_CHUNK_LEN round-trips (the limit is inclusive); the existing chunk_too_large covers over-limit.
  • P13 discard test (session::tests::read_pump_ignores_client_to_server_stream_types_from_server): stream_type 0 and 3 chunks arriving from the server (mid-stream — a leading 0x00 is the negotiation-error marker, ADR-052 §5, so stream_type 0 cannot be first) are ignored without desynchronizing framing, losing stdout data, or blocking the exit. The remaining P13 item (the TtyAdapter::handle accept-loop harness) stays deferred to the first transport-level consumer, per the review.

Verification on the resolution tree: cargo test 98 lib (was 95) / cargo test --all-features 141 (was 138); clippy (all-targets + wasm32), fmt, wasm check, doc — all clean.

Session-2 Resolution (P12 + P6 + P7 + P4 + P16, 2026-09-05)

The API-shape batch (P12/P6/P7) plus the docs that use the final surface (P4/P16), per the review's suggested session breakdown item 2. Commit: c8e4610.

  • P12 — crate-root re-exports (src/lib.rs): pub use of the primary types — TtyAdapter, drive_session, drive_session_pre_negotiated, TTY_OPEN_SCOPE (adapter); TtyBackend, TtyHandle, TtyParams, TerminalParams, TtyControl, TtyControlHandle, TtyError, BoxFuture (backend); TtySession, TtySessionError (session); Chunk, ChunkReader, ChunkWriter, RawError and the five STREAM_* constants + CHUNK_HEADER_LEN + MAX_CHUNK_LEN (wire); ControlMessage and signal_from_name — the latter #[cfg(unix)]-gated to match its definition (caught by the wasm check); NegotiateRequest, TerminalParamsWire, NegotiationError, NegotiationReader, NegotiationWriter, error_response_bytes (negotiation); register_openable, tty_open_spec, OP_TTY_OPEN, TTY_ALPN (channels); LocalTtyBackend under the local feature. Module paths remain the full surface; the lib.rs doc comment gained a "Crate-root surface" section explaining the split.
  • P6 — write-path validation (src/wire.rs): all four ChunkWriter methods validate via a shared validate_header (stream_type ≤ 4, length ≤ MAX_CHUNK_LEN) before touching the transport, reusing RawError::InvalidStreamType/ChunkTooLarge. The length is validated as u64 before the u32 cast, so an oversized payload fails instead of wrapping past the check — validating the truncated value would have reintroduced the exact bug the validation exists to catch. Empty-payload shape unified: one write_validated helper emits length == 0 + no payload bytes for all four paths (the old write_ctrl_* always-write shape is gone; wire-equivalent). Tests: reject-invalid-stream-type (nothing written), reject-oversized (writer + all three helpers), and exact-boundary acceptance.
  • P7 — peek-state guard (src/wire.rs, src/session.rs): ChunkReader tracks peeked state. read_chunk() after peek_stream_type() completes the peeked chunk instead of consuming a second header byte; a second peek is idempotent; read_chunk_after_peek debug-asserts a peek happened. This let the session's read pump drop its manual first_byte_peeked threading — read_chunk() handles both orders. Tests: peek-then-read_chunk delivers two well-framed chunks (the old shape silently read the peeked byte's length field from the payload), and peek idempotence.
  • P4 — README + readme field: README.md mirrors alkcall's structure — tagline + positioning, ## Quick start with per-role examples (producer: backends map + TtyAdapter as ProtocolHandler; producer-via-channels: register_openable; consumer: TtySession::connect_direct with the full typed-method flow; consumer-via-channels: open_via_channels), ## Architecture (producer/consumer table per convention 7, wire-format summary, wasm + local note), ## Documentation, ## License. Examples use the P12 root paths. readme = "README.md" added to [package]; the dry-run package list confirms it ships.
  • P16 — CHANGELOG.md: Keep-a-Changelog format with a full [0.1.0] entry written from the commit log (wire format, backend trait, local backend, adapter, channels integration, session consumer, control messages, plus the Session-1/2 fixes folded into their features), with the [0.1.0] release-tag link definition (alkcall's missing-link nit not replicated). ADR references use the actual on-disk numbering (exit code = ADR-004, cancel-cleanup = ADR-005, framing = ADR-006, channels = ADR-008/009).

Verification on the resolution tree: cargo test 104 lib (was 98) / cargo test --all-features 128 lib + 19 integration = 147 (was 141); clippy (all-targets + wasm32) -D warnings, fmt, wasm check, doc (0 warnings), cargo publish --dry-run --allow-dirty (44 files, 702.8KiB) — all clean.

Notes

  • Line numbers refer to the tree at 918af40 (review base).
  • The P1 reproduction probe was a scratch crate at /tmp/opencode/alktty-deadlock-probe (mock TtyBackend emitting N chunks through real drive_session over tokio::io::duplex; N=62 passes, N=63 hangs) — ephemeral by design; the P13 regression test makes it permanent.
  • The two subagent reviews overlapped on findings P1/P5/P6/P7/P9/P10/ P12 and the #[non_exhaustive] question; the overlapping findings agreed, and every one was re-verified against source before being recorded. The packaging agent's file list came from cargo package --list --allow-dirty, not inference.
  • This review deliberately does not re-open decisions recorded in review #002 (R3's per-connection-registry constraint, R5's #[non_exhaustive] policy) — they are cited where relevant.

Cross-references

  • Review #001 (docs/reviews/001-code-review.md) — first full review; its M/L/N ID scheme is why this review uses P1…P16 (prepublish).
  • Review #002 (docs/reviews/002-post-session-review.md) — R5's resolution records the #[non_exhaustive] policy this review applied in "Examined and closed".
  • alkcall 0.4.1 (/workspace/@alkdev/alkcall) — packaging/README/ CHANGELOG reference for P2/P4/P16; src/channels/operations.rs for P14.
  • ADR-001 (wire format) and ADR-005 (exit-code chunk) — the invariants P1's fix must preserve.