# Upgrade Guide — vNEXT

<!-- assembled-by: assemble-next-md -->
<!-- bump: patch -->

## What Changed

- **A new constitutional standard, "Self-Unblock Before Escalating"** (`docs/STANDARDS-REGISTRY.md`):
  a blocker is the agent's problem to solve FIRST. The agent must exhaust every unblock path
  *within its permissions or organizationally-granted scope* before requiring anything from a
  human — and when a human IS required, ask for the lowest rung on the human-requirement ladder
  (**nothing → an approval → an operator-only credential**), named exactly. Origin: operator
  directive (Justin, topic 12476, 2026-06-13) after the agent idled on a `feedback.instar.sh` DNS
  record that was already self-unblockable via a Cloudflare token sitting in the org vault.
- **It EXTENDS the existing `BlockerLedger` gate — it does NOT fork a parallel one.** Round-1
  convergence (and two independent external reviewers) found `BlockerLedger.settleTrueBlocker()`
  already mandates a recorded failed self-unblock attempt before a credential/account blocker can
  settle, already HARD-rejects `missing_failed_attempt`, and already routes the settle JUDGMENT
  through the Tier-1 `SettleAuthority` (B17) LLM gate. So this standard adds only the things the
  ledger did NOT have and reuses its pipeline / taxonomy / log / untrusted-data envelope for
  everything else.
- **`SelfUnblockChecklist` (the one substantial new module, `src/monitoring/SelfUnblockChecklist.ts`):**
  a deterministic, code-driven, ORDERED probe list (own per-agent vault → org Bitwarden → authed
  cloud accounts → MCP → browser → "do I already control a resource that does this?") that
  systematically PRODUCES the failed-attempt evidence the ledger requires. `holdsRelevantCred` is
  decided DETERMINISTICALLY by a `service:scope` tag match (e.g. `cloudflare:dawn-tunnel.dev`, with
  domain-hierarchy / wildcard rules), never by an LLM; ambiguous or MISSING metadata fails CLOSED
  (a stale cred is simply not surfaced, never mis-applied). Each probe is timeout-bounded by class
  and fails toward `reachable:false`, so one hung probe degrades to "unreachable" rather than
  stalling the path.
- **Anti-gaming is mechanical (the one edit to BlockerLedger's logic):** the checklist RUNNER
  persists each run keyed by an immutable run id, and `settleTrueBlocker` now takes a `runId`
  REFERENCE that the ledger LOADS + verifies — replacing the old caller-supplied `failedAttempt`
  object. A hand-supplied list with no genuine persisted run is treated as NO attempt, so the
  "self-asserted / gameable list" hole is closed by construction. Everything else in the ledger is
  reuse.
- **`DurableVaultSession` (`src/monitoring/DurableVaultSession.ts`):** flag-gated wiring to the
  existing in-memory `BitwardenProvider` session so the org-vault probe can actually reach the
  vault. The session value lives in process memory / keychain only, is held warm ONLY while a
  checklist run is in flight (TTL + idle-expiry), is NEVER logged, NEVER passed as argv (handed to
  `bw` only via the child `BW_SESSION` env), and NEVER placed on the cross-machine
  `multiMachine.secretSync` path. The master password stays operator-held; no new on-disk secret is
  introduced.
- **Rung FLOOR (capability ≠ authority):** an action that is irreversible, cost-bearing above a
  threshold, out-of-scope, or policy-sensitive keeps a MINIMUM rung of 1 (an approval) EVEN IF a
  self-unblock credential exists. A rung-1 approval must resolve against a VERIFIED principal (Know
  Your Principal), never a name seen in content.
- **Read-only HTTP surface:** the existing `/blockers/*` read surface is extended with the
  checklist's per-probe results + the rung — `GET /blockers/self-unblock-runs`. Bearer-gated, NOT on
  the auth-exempt allowlist, 503-when-dark AFTER auth (an unauthenticated caller gets 401, not a 503
  that confirms the route exists), `Cache-Control: no-store` (the body is credential-reachability
  reconnaissance), served through the `<blocker-ledger-data>` envelope.
- **Ships DARK behind the developmentAgent gate** — the `selfUnblockChecklist` and
  `durableVaultSession` blocks nest under the existing `monitoring.blockerLedger.*` gate and OMIT
  the `enabled` literal, so `resolveDevAgentGate` decides: LIVE on a development agent, DARK on the
  fleet. Reversible — disabling makes the checklist not run, the route 503, the session not kept
  warm; all inert.

## What to Tell Your User

Nothing changes for fleet agents — this ships off for everyone except development agents. On a
development agent, when the agent hits a blocker (a missing credential, a locked account), it now
systematically checks every place it's already allowed to look — its own vault, the organization's
password vault, the cloud accounts it's already signed into — before it ever asks you for anything.
Only after that genuinely comes up empty will it ask, and it asks for the smallest thing possible:
ideally nothing, then a yes/no approval, and a credential only you can produce as the last resort. It
can never use a credential to act outside the job at hand, and anything irreversible or costly still
asks for your approval first even if it could technically do it itself. There is no user-facing action
and no behavior change on a normal install.

## Summary of New Capabilities

- On a development agent, the agent now runs a deterministic self-unblock checklist before it can
  settle any credential/account blocker as genuinely operator-required — turning "exhaust every path
  you can already reach first" from a prompt-level wish into a structural precondition fed into the
  existing Tier-1 blocker-settle authority.

## Evidence

- `tests/unit/BlockerLedgerSelfUnblock.test.ts` (7): checklist probe ordering + short-circuit +
  per-probe timeout→`reachable:false`; the run-id provenance contract (a caller-embedded attempt
  with no persisted run is HARD-rejected as `missing_failed_attempt`); the ladder/rung-floor mapping
  (irreversible/cost-bearing → min rung 1 even with a cred); the rung-1 verified-principal
  resolution.
- `tests/unit/DurableVaultSession.test.ts` (8): TTL / idle-expiry, in-flight-only hold, and a wiring
  assertion that the session value never appears in the ledger or argv.
- `tests/integration/self-unblock-routes.test.ts` (5): the `/blockers/self-unblock-runs` read route —
  200 when enabled, 503 (after auth) when dark; a real persisted run feeds the settle path; the
  negative anti-gaming assertion (no persisted run → HARD reject).
- `tests/e2e/self-unblock-lifecycle.test.ts` (6): production init path — route ALIVE (200) when
  enabled, 503 when dark; a self-unblock action touching an external account is STILL evaluated by
  the external-operation + mandate gates (proves the boundary mechanically, not in prose).
- `tests/unit/lint-dev-agent-dark-gate.test.ts` + `tests/unit/feature-delivery-completeness.test.ts`:
  the two new flags are registered in `DEV_GATED_FEATURES` with justifications and the attribution
  map recomputed.

## Migration

- `PostUpdateMigrator.migrateConfigSelfUnblockChecklistDevGate` strips a default-shaped `false` for
  the `selfUnblockChecklist` / `durableVaultSession` blocks (nested under
  `monitoring.blockerLedger.*`) on update, so the gate resolves on already-deployed dev agents. An
  operator-set value is left entirely alone — reach is not authority. Idempotent. The CLAUDE.md
  self-unblock reflex section is delivered via the content-sniffed `migrateClaudeMd` path +
  `generateClaudeMd()` (Agent Awareness).
