ADLC
Toolkit

context-handoff

Absolute context-band signals, session-terminal mutation deny (D1–D3), and the operator CLI for write/resume/bypass/repair/unlock — F3 continuity handoff.

context-handoff

ADLC phase: P4 continuity (F3) · Library + CLI: absolute context bands, session-terminal mutation deny (D1–D3), and operator handoff commands.

Status since 1.11.1 (#966): no harness evaluates the session-terminal deny (D1-D3) by default. The Claude Code and Codex plugins ship without the hook entry; pi and OpenCode run the gate only with ADLC_CONTEXT_ROT_HANDOFF_ENABLED=1. The band thresholds are still consumed by build-gate, and the adlc handoff CLI is unaffected.

The ADLC lifecycle, with P4 highlighted. Read the theory for P4

What it defends against

Long sessions rot: depth, transcript size, and context-window fill climb past the point where the agent still holds the plot. context-handoff freezes the absolute band thresholds and the session-terminal deny contracts that later harness adapters and build-gate will share — so no local copy of the numbers can drift, and a deny written once cannot silently clear when a marker file disappears.

Slice 2 adds the operator/host CLI (adlc handoff …) that adapters and humans call to write finals, resume/consume denies, bypass, repair binds, and unlock. Harness adapters and build-gate migration land in later slices.

Absolute bands

Signals are OR-joined across available kinds (pct, depth, bytes). Missing kinds are ignored; present-but-malformed or out-of-domain values fail closed as hard. Floor is never subtracted from band comparators (no floor-delta bands).

Bandpctdepthbytes
warn≥ 50≥ 20≥ 128 KiB
handoff≥ 60≥ 30≥ 192 KiB
hard≥ 80≥ 40≥ 256 KiB

Advisory nags (nagSuppression) may be suppressed near hard so deny/handoff owns the signal; that path never affects mutation deny.

Deny lifecycle (D1–D3)

CodeMeaning
D1Deny active for the denier session — mutations blocked.
D2Deny sticky across re-entry for that session until a verified consume.
D3Other open denies in the store also block mutations for non-denier sessions.

After a verified other-session consume, that record leaves D3 (status=consumed); the denier remains D2. Marker vanish / store-unavailable paths fail closed unless an unbound operator bypass clears the recoverable codes.

CLI (adlc handoff)

Dry-run by default. Pass --write to mutate. Mutating commands require ADLC_MANIFEST_KEY and append durable evidence to .adlc/manifest.jsonl — missing key exits 1 (never silent success).

adlc handoff write --session <id> [--ticket <id>] [--host <h>] [--content-hash <h>] [--write] [--json]
adlc handoff resume --session <consumer> --deny-session <denier> [--write] [--json]
adlc handoff bypass --session <id> [--unbound-reason <text>] [--write] [--json]
adlc handoff repair --session <id> --ticket <id> --content-hash <h> [--write] [--json]
adlc handoff unlock --session <id> --pid <n> --started-at <iso> --host <h> --nonce <n> [--write] [--json]
CommandRole
writeFinal checkpoint + ensure deny marker / .adlc/.deny-store
resumeOther-session consume (same-session exits 2); writes resume-auth cache
bypassBound (session only) vs unbound (--unbound-reason) one-shot grant
repairPrivileged host bind of an existing open deny ticket_id+content_hash
unlockReclaim lock only when the PID is dead, the lock is this host's, and all fields match

repair never creates a deny: a missing or already-consumed marker exits 1, so the command that relaxes a deny cannot arm one. resume makes the manifest entry durable before it flips the record (outcome: authorized attests the authorization, not the persisted consume), and rolls the resume-auth cache back if anything fails — a failed resume leaves the deny open. The bypass grant printed on stdout is scoped to the calling adapter invocation; the durable proof is the context-handoff-bypass manifest entry. An explicitly empty --unbound-reason exits 1 rather than handing back the bound grant that cannot clear what the operator asked to override.

bypass --write exits 1 when the resolved ledger has neither a .adlc/handoffs directory nor a surviving .adlc/.deny-store sentinel (the D0 state, repairable only by an unbound bypass) — run from the repo root (an operator outside the hook may instead pass --dir <repo>/.adlc; the hook's recovery grammar does not accept --dir) — so a grant is never reported as recorded under a tree the enforcing hook does not read; a dry run prints the resolved ledger path.

write is the refresh path, so it rebinds an open marker onto the final it writes; a refresh that would unbind one (dropping --ticket) or refresh a consumed one exits 1 instead of leaving a final and a deny that disagree. Every command that mutates a marker — write, resume, repair — runs under that session's .lock and exits 2 when a live session on this host holds it, so one deny authorizes exactly one successor. If evidence cannot be appended, the files written in that run are rolled back: un-evidenced state is un-auditable state. A hard-killed process leaves its lock behind for unlock to reclaim.

Artifact paths (under the repo root; --dir names the .adlc directory, and its final path segment must be .adlc so artifacts and evidence stay together):

  • Final: .adlc/handoffs/finals/<session_id>.json
  • Deny: .adlc/handoffs/denies/<session_id>.json + .adlc/.deny-store
  • Resume-auth: .adlc/handoffs/<session_id>.resume-auth.json
  • Lock: .adlc/handoffs/<session_id>.lock

Library usage

import { WARN_PCT, HANDOFF_PCT, HARD_PCT } from '@adlc/context-handoff/lib/thresholds.mjs';
import { evaluateBands, nagSuppression } from '@adlc/context-handoff/lib/bands.mjs';
import { evaluateMutationGate } from '@adlc/context-handoff/lib/mutation-gate.mjs';
import { ensureDenyMarker, loadDenyRecords } from '@adlc/context-handoff/lib/deny-marker.mjs';

const bands = evaluateBands({ pct: 65, depth: 12 });
// → { warn: true, handoff: true, hard: false }
node --test packages/context-handoff/test/*.test.mjs

Binding design: docs/specs/context-rot-handoff.md and the package README.

Go deeper

packages/context-handoff

On this page