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.
13 KiB
13 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w3-git-mapping | Wave 3: the git mapping (exec grammar + alkgit dispatch) | completed |
|
moderate | medium | phase | implementation |
Description
Land the git branch of the spine (ADR-006 decision 2, exec-only), which is the alkforge primary purpose: the full exec grammar replaces the wave-2 skeleton probe, and the git row becomes a real dispatch to the published alkgit seam. Everything POC-3 stage A proved, re-landed as contract.
- The full grammar (replaces
dispatch::classify_exec, the wave-2 interpretation's named replacement; its 7 probe unit rows migrate):git-{upload,receive}-pack '<path>'handles quoted/unquoted paths; strip the quotes and the leading/(POC-3 stage A rows: what real OpenSSH git sends); unknown services (git-send-pack,git-upload-archive) → decline-with-reason, the honest surface; service-only (no repo) → decline-with-reason (POC-3 §A: SSH exec must carry the repo); per-length limits consumed at last, withLimits::max_exec_command_lenenforced before any parse or allocation (decline-first, alloc-never; the wave-2 carried-not-enforced reservation). Decide the wire shapes per decline class (open/reply vocabulary, whether "unknown service" and "not git at all" share a reason shape) and record. - Repo path → repo_id, no existence oracle: the parse validates
via
alkgit::validate_repo_id(1–2 segments, charset, ≤ 100 bytes) and hands the id through, alkssh never resolving; the collapsed generic denial before any protocol byte isserve_duplex_resolved'sSubstrateError::AccessDeniedposture, contract-pinned here: over the SSH channel, no pack/ref byte precedes the denial band (the POC-3 §A.5 byte-order evidence, now a test row). Decide the.git-suffix normalization against the alkgit registry contract (what vanilla git sends vsvalidate_repo_id's grammar) and record. - The dispatch: the git row becomes real, and the taken channel
(the store-and-take handoff lands here first; the accepted
channel is stored at the open,
channels.remove(&id)→into_stream()in the spawned serving task; the session task reuses the store) →alkgit::serve_duplex_resolved(repo_id, identity, service, duplex, limits, registry, refs, packgen, ingest, push_options_negotiated, pipeline), namely the serve-config'sgitseam handle (w3-seam-config), the resolved identity riding theidentityparam. Process-mode dialect: alkgit speaks first (the V0 advertisement, no service prefix), and nothing is alkssh-side. Verify the dialect holds over the channel duplex with a byte-level row. - Exit-status both directions (POC-3 §A.5): clean serve end →
exit-status(0)... decide the code mapping (SubstrateErroroutcomes → the code; POC evidence: denied → the client observedExitStatus{1}; vanilla git exits 0 on success), sent before close, from the spawned task via the clonedSession::handle()(the wave-2 mechanics reused). Record the decision. - Identity: the parked wave-2 deferred item discharges here, and
the resolved identity (the bridge's
auth_succeededride-along) reaches the seam, so assert it live (the authorize-path/witness row the harness can observe; decide the cheapest row) so the visible-surface invariant is observable through the git row. - Contract rows through the harness, per build, with the
tests/dispatch_core.rsskeleton rows upgraded: git-only serves the git row through the doubles (clone/push semantics over the in-mem composition), declines plain exec/pty/shell (parse-then-fail, no resource alloc); session-only still declines git-parseable exec; none-build still declines at open; under--all-featuresthe routing holds. The declined rows keep their wire shapes (channel_failure, channel usable / serving-path failure+close per class). - The real-tool smoke: one
git clone(2.43+) through the harness against hand-rolled in-mem alkgit doubles (alkgit'sMem*are test-private, so alkssh hand-rolls the minimal quartet peralkgit/src/backend.rs:164-254's noop shapes; never sibling-checkout paths, never copies of the doubles' code). Decide the tooling posture (require git in the environment vs conditional-skip, skipped-with-reason at most, never silently green) and record.
Acceptance Criteria
- The full grammar replaces the probe (quoted/unquoted, unknown-
service vs non-git decline rows, service-only decline, the
max_exec_command_lenpre-parse cap); the probe's unit rows migrate/extend - The git row dispatches to
serve_duplex_resolvedover the channel duplex; process-mode dialect verified byte-level; declined routes and theAccessDenied-before-protocol-bytes order pinned by contract rows - The store-and-take handoff lands (channel stored at open, taken in the serving task), structured so the session task reuses it
- The exit-status mapping (clean end / terminal error) decided + recorded; status before close from the spawned task
- The identity ride asserted live through the seam (the parked wave-2 item discharged here)
- The decline-matrix rows per build green through the harness (git-only / session-only / none / all-features), each with its contract stamp; the real-tool clone smoke row
cargo test,cargo test --features git --test <git rows>,cargo test --all-features, clippy ×2 (-D warnings), fmt,cargo doc --no-depspass
References
- docs/architecture/mapping.md (the git channel-type row; §Session- channel dispatch and its decline-first-alloc-never rule; §the structural constraint)
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md (decision 2, exec-only; decision 4, spawn-and-return)
- docs/architecture/decisions/004-auth-dispositions.md (the identity ride), docs/architecture/auth.md §Server direction (step 4)
- docs/research/poc-3-mapping-bakeoff.md §Stage A (the grammar rows, the denial byte-order, the exit evidence, the spawn rule)
- docs/research/mapping-table.md (the validated research ancestor)
- alkgit 0.1.0:
serve_duplex_resolved(src/duplex.rs:1412),SubstrateError(src/substrate.rs:116),validate_repo_id(src/registry.rs:132), the trait shapes (src/backend.rs:164-254)
Notes
To be filled at implementation time.
Decisions recorded (implementation, 2026-10-28)
- Course correction to
w3-seam-config:GitSeamgainslimits: alkgit::Limits, sinceserve_duplex_resolvedrequires the alkgit ADR-009 session budget, which the landed handle had no field for. alkgit's own contract shapes this ("constructed by the door/adapter from assembler config"), so the budget rides the seam (assembly-supplied,Copy), not a crate default. First seam consumer; landing it here, recorded on the field and in the config docs. - The grammar (
dispatch.rs, the probe's rows migrated): exact serving-service token + single separating space + path-shaped argument (the wave-2 probe's pinned verdicts preserved verbatim:git-upload-pack reland double-space arguments are not git); quoted shape is fully'-wrapped (unterminated → pipe, the probe's non-path-shape verdict); leading/stripped after quotes; one trailing.gitsuffix stripped (the vanilla-clone idiom; the alkgit registry grammar excludes., so the door normalizes the idiom rather than asking registries for dotted keys; an empty remainder (/.git) is kept and the git row's validation declines it, never a silent emptying). Validation:alkgit::validate_repo_idat the git row (git_map.rs, feature-gated, while the parse core stays feature-free); a shape-invalid id declinesinvalid-repo-idalkssh-side; a shape-valid id is handed through untouched and alkssh never resolves (resolution, authorize, and the collapsed generic denial live at the seam). Unknown services (git-send-pack,git-upload-archive) and service-only (including an empty argument) route toExecRoute::Declined(build-independent, off the pipe row, per POC-3 §2's honest-surface verdict. - Wire shapes per decline class (recorded): SSH request replies
carry no text, so the reason lives only in the event vocabulary;
the git declinables (
unknown-service/service-only/invalid-repo-id/exec-oversize) share the request-level reply shape with parse-then-fail:channel_failure, channel alive (POC-3's decline shape, POC-1 §5 posture); the reply vocabulary is the decision ("unknown service" vs "not git at all" differ in nothing observable on the wire, only the event). Serving-path declines (the row owned the channel) keep failure+close:seam-unconfigured(feature on, seam unset, the unset-declines contract; keeps the wave-2 decline-matrix rows' shape in builds without a configured seam), plus the unreachable-in-practice guardsno-identityandchannel-not-held. The serve's own denials are not alkssh declines: unknown/ungranted repo flows to alkgit, whose collapsed generic denial writesERR access denied: <repo>as the first byte on the channel (pinned byte-level intests/git_mapping.rs), then exit-status(1), then close. - Exit mapping (POC-3 §A.5, decided):
Ok(())→ exit-status(0) (vanilla git exits 0 on success; the clean-end wire row exercises the flush-only request'sOk(None)→Ok(())shape); everySubstrateError→ exit-status(1):AccessDeniedper the POC's wire evidence,Io/Protocolthe same (the service ran and failed), the machinery-misuse variants defensively 1 (they never arise from the serving entry).255never fabricated (OpenSSH's could-not-start semantic this door never claims). Status before close, from the spawned task via the cloned handle; a failed report send is the vocabulary's onewarn(the wave-2 stub mechanics). A serveErris not a decline event, as the substrate already mapped its own error onto the wire. - Store-and-take handoff:
SessionHandler.channels(plainstdHashMap, single-writer, with every mutation on the packet loop's task). Stored at the confirmed open (accept then insert); taken by the git row (remove→into_stream()in the spawned serving task); released bychannel_close(the cap is released there too: one hook, both budgets). The session rows still hold until their wave-3 tasks take them. A routed exec answerschannel_successinline (the POC-3 §A.5 shape, where the request routed and the serving outcome reports through exit-status). - Identity (
w2-auth-bridge's parked ride-along, discharged): the resolved identity is cloned into the serving task and passed onserve_duplex_resolved's identity param. The cheapest live witness row: alkgit'sauthorizeevaluation, giving two otherwise-identical rows (private repo granted to the riding id → the advertisement flows; the same repo ungranted → the denial band + exit 1); no direct identity introspection needed, and the deny row doubles as the authorize-path witness.Nonecan never occur post-auth and declines serving-path. - Push semantics through the doubles: the packless CAS path
(alkgit ADR-020 §1) is the pack-free receive-pack row, with commands +
flush (caps NUL-attached on the first command line,
report-statusrequested), no pack; the wire layer applies viaGitRefs::apply_updatesand writes the report (unpack ok/ok <ref>). The duplex discipline drains the raw tail packless, which is why the row had to signal client-side EOF after the report flush (observed, the real client-eof shape) and also why vanilla delete-pushes end cleanly.GitPackIngest::preparenever fires packless, as asserted. - Per-build matrix: unchanged rows in
tests/dispatch_core.rs(their observables hold, since the config-driven decline keeps the failure-then-close shape in seam-less builds; unknown services keep failure-then-alive), docs/stamps updated for the new decision set; the git row's serving + grammar-decline rows live in the newtests/git_mapping.rs(empty module in non-git builds, compile-time feature isolation).tests/observe.rs's git-row event row updated to theseam-unconfiguredreason. - Real-tool smoke posture (recorded): the clone smoke (a real
git cloneof an unborn-HEAD repo over a TCP-mounted adapter, on the same loopback server stack, the accept loop as the assembly's mount) requires git ≥ 2.43 and ssh in the environment; missing/older tooling is skipped with a printed reason (skipped-with-reason at most, never silently green). The empty-repo clone exercises the full chain (exec parse → dispatch → the seam advertisement → ls-refs → exit-status(0)) with zero pack bytes: the unborn-HEAD line is what real git 2.43's clone consumed (POC-3's OQ-19 rider shape), so the hand-rolled doubles stay noop-faithful. - Verification:
cargo test(default build),cargo test --features git --test git_mapping,cargo test --features session,cargo test --all-features,cargo clippy --all-targets --all- features -- -D warnings×2 (default + all-features),cargo fmt --check, andcargo doc --no-deps: all green.