Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
11 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w3-decline-tracing | Wave 3: decline-path tracing (the wave-2 gate's booked finding) | completed |
|
narrow | low | component | implementation |
Description
Land the wave-2 review gate's booked finding: observability on the
decline paths. tracing rides the manifest per the ADR-002 toolkit but
so far nothing in src/ emits, so a request-level decline's
"reason" is today only the routing decision documented per build. The real serving
paths land in this wave's mapping tasks, where serve/decline tracing
matters; therefore the event vocabulary lands first and the
mappings inherit it.
- The family-convention check first: before choosing the event
shape, read the family's established tracing posture (alktty /
alkgit / alktunnels / alkcall
src/: levels, event/field naming, what gets emitted on error and decline paths) and adopt it (no alkssh-private dialect); record what was read and chosen. - The event vocabulary, with the deciding context on every decline being row + feature build + channel id (plus the decline class: open-level vs request-level vs serving-path, and where applicable the failure shape carried). The mapping tasks then emit at their sites.
- Instrument what exists now: the open-level declines
(unfeatured / never-feature / cap-exceeded), the request-level
capability declines (
subsystem/env/x11/agent, the unfeatured-parse routes), the seam-stub serving declines, and the serve-path skeleton (spawned path started / ended, failure), with the stubs' events carrying into the mapping tasks' replaced paths. - Safety posture (conventions 6, OQ-03): no secret material ever enters an event, never the username; identity/fingerprint exposure on decline events is decided here against the family posture and recorded (auth.md: log exposure is deployment policy, and the crate's events must be safe-by-default at every level).
A single small cross-cutting pass over the landed handler/dispatch sites with the vocabulary, so the three mapping branches never land inconsistent events.
Acceptance Criteria
- The family tracing posture read and recorded; the event
vocabulary established (row + build + channel + decline class)
and used at every decline site in
src/ - Serve-path skeleton events in place (start/ended/failure) so the mapping tasks' spawned paths inherit shaped rows
- The safety posture recorded (no secrets, no username; what identity detail may appear decided)
- A test pins the event presence at the decline sites (or the doc-test convention the family uses, decided and recorded)
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --check,cargo doc --no-depspass
References
- docs/plans/implementation.md §Wave 2 (the booked finding, review-wave-2 §Notes)
- docs/architecture/mapping.md §Wire decline reasons (the two decline classes), §Decline matrix
- docs/architecture/auth.md §Dispositions (log exposure posture), docs/architecture/decisions/002-russh-pin.md (the toolkit)
- sibling
src/tracing usage, the convention this task reads and adopts
Notes
The family posture read (2026-10-28)
Read before choosing the event shape (the task's first bullet), across
the family's src/:
- alktty (
adapter.rs,session.rs,channels.rs,local/pipe.rs,local/pty.rs): freetracing::{debug, warn}imports; message-last lowercase sentences with a component prefix ("tty: backend stdin write failed: {e}"); interpolation in messages, no spans. - alktunnels (
producer.rs,consumer.rs,params.rs): the same free-fn posture, plus structured fields where identity exists (listen = …,error = %e,reason = …); levelsdebugfor structural observations,warnfor recoverable failures,errorfor abnormal pump terminations. - alkcall (
protocol/*,channels/*): the widest use, with the same fields convention (channel_id,request_id,error = %e), component-prefixed messages ("channel client: …","open wrapper: …"),debug/warnonly. - alkhttp (the closest structural analog):
warn/errorwitherror = %e("channels session ended"). - alkgit: no tracing at all at this writing.
Never observed: info!/trace! events, subscriber setup in library
code, macro or span wrappers. Adopted verbatim in src/observe.rs:
free tracing fn calls, message-last lowercase with the
"decline: …" / "serving path …" phrasing, flat per-event fields.
The event vocabulary (landed in src/observe.rs)
feature_build, the compile-time feature set as a stable string ("none","git","session+git", …; features join in the ordersession, git, tunnel, sftp), so a captured event is self-describing per build;sftpships in the string even though it gates no route (it is part of what the assembly deployed).row, the greppable wire row: the channel type for open-level ("session","direct-tcpip","x11", …), the request name for request-level ("pty_request","tcpip_forward", …), the handler hook name for auth ("auth_publickey","auth_none", …).decline_class,"open"|"request"|"serving"|"auth": the two wire decline classes (mapping.md§Wire decline reasons), plus the serving-path class (this wave the seam stub's decline is the serving path; the mappings emit the same class from their real spawned paths) and the auth class (the ADR-004 disposition declines, added by this task: the auth hooks are decline sites too, andauth.md's dispositions are the crate's auth-decline contract).reason, the decline cause mirroring the architecture's own classifications:"unfeatured","never-feature"(ADR-003 §5),"cap-exceeded","connect-failed"(tunnel stub / ADR-006 decision 1),"seam-stub"(session-row stubs),"unimplemented"(the subsystem rows under OQ-13 and the wave-3 listen forms),"declined-by-design"(env, OQ-09), and on the auth class"method-unimplemented"/"cert-unsupported"/"non-ed25519"/"fingerprint-unresolved".channel_id, the wire channel id (u32), on every decline of a channel (notably: the event's id is russh's server-side id, becauseChannelIdis not publicly constructible, so the helpers take the raw u32 viaChannelId::number()); the auth class and the global-Rlisten forms carry none (a separatedecline_global_requesthelper, since global requests have no channel).exec_route, only onexec_request-row events: the parse route ("git"|"pipe"), so a git row's decline is distinguishable from a pipe row's in combined builds. ANoneroute field is not recorded at all bytracing(discovered by the unit rows; recorded in the module docs). Themessagefield rides first (theevent!macro's shape, the unit rows pinning the field order).
Levels: every decline and the lifecycle's started/ended emit at
debug, since a decline is the door's normal, honest posture
(vanilla
clients probe unfeatured rows routinely), matching the family's
structural-observation level. Only [serving_path_failed] is warn
(a serving path that could not complete its wire reporting is a real
failure, which is the family's recoverable-failure level).
The serving-path skeleton
src/serve.rs stays the mechanics only (spawn + exit-report); the
lifecycle events hang off the spawned task's body in
handler.rs's seam_stub_decline: started (the spawned task's first
action, so "started" is the task's honest start, not the hook's spawn
call) → the serving-path decline (class serving, reason seam-stub)
→ ended, or failed (warn, with the russh protocol error) when a
report send could not complete. The wave-3 mapping branches
(w3-git-mapping, w3-tty-session, w3-tunnel-direct) inherit the
shaped rows by emitting the same helpers from their real serving tasks.
The safety posture (conventions 6 and OQ-03, recorded)
No secret material ever enters an event; never the username;
the fingerprint stays out too. Structurally enforced, not recalled:
the vocabulary helpers take only &'static str values and the wire
channel id, leaving no parameter a username, command string, env
name/value,
repo path, key, or fingerprint could ride. The reason codes carry
the decline's cause class, and the deciding context is the row +
build + channel, deliberately not the identity behind the wire
attempt. This is the recording of auth.md §Dispositions' standing
rule at the crate level: log exposure is deployment policy, so the
crate's events are safe-by-default at every level, with the
fingerprint
(and any identity detail) is a deployer-side computation over its own
logs, never crate-emitted. If a future decline site needs
request-argument detail (e.g. the git grammar's decline reasons in
w3-git-mapping), it must add an explicitly-vocabulary'd field and
record its safety reasoning, since dynamic strings do not ride
src/observe.rs by default. The wire rows pin the posture by
scanning every captured event: no username string, no SHA256:
fingerprint, no user*-named field ever appears.
The test convention (decided + recorded)
The family has no event-capture precedent to adopt: alktunnels'
tracing-subscriber dev-dep drives fmt output, not assertions. The
convention these rows pin: a hand-rolled minimal
tracing::Subscriber capturing events into a shared buffer,
installed once as the global default (tests/observe.rs), because
the server side runs on spawned tasks, so a thread-local
with_default capture cannot see them, and a global capture needs no
new dependency. Two levels of pinning:
- Helper-level (
src/observe.rsunit rows, thread-localwith_default): exact field sets and order (message first), theexec_routepresence semantics, the no-channel auth shape, the lifecycle messages, thewarnfailure arm, and the cfg-derivedfeature_buildstring. - Wire-level (
tests/observe.rs, the loopback harness drives the real wire; capture installed up front via the mount helpers, since a lazily-installed global loses the earliest emissions): presence by field-matching at every decline site across the builds: x11 open,-Rlisten (global), the fingerprint-unresolved and auth_none dispositions (driven on fresh mounts, since a second auth exchange on an already-authenticated session is not a supported russh surface, as the probe discovered), the unfeatured session/pipe/pty rows, the git seam-stub's full lifecycle, the tunnel stub'sconnect-failed, the cap-exceeded row (zero-cap limits must ride the server config (serve_config), not just the client'sLimits), and the username/fingerprint safety scan. Build-specific rows follow thedispatch_core.rsper-build cfg convention and were verified in the default,git,session,tunnel, and--all-featuresbuilds.
Verification (all green)
cargo test default build 110 rows across 7 binaries + doctests ·
cargo test --all-features ✓ · cargo test --features git/session/tunnel
✓ (each build runs its decline rows) ·
cargo clippy --all-targets -- -D warnings ✓ ·
cargo clippy --all-features --all-targets -- -D warnings ✓ ·
cargo fmt --check ✓ · cargo doc --no-deps 0 warnings ·
taskgraph validate ✓.