step.2.txt -- b3ubot U1.1: envelope + adapter contract + mock provider
               (P1.M1) -- design.md §2.4 as code: the request/response
               envelope, the capabilities()/run()/abort() adapter
               interface, and the REAL mock adapter (fixture-scripted,
               zero network, zero keys) that replaces U0.4's throwaway
               "hello" stub. Everything U1.2's real claude-code adapter
               will plug into the SAME contract suite this step builds.

Step:     2  -- promote UoW **U1.1** (end_to_end.md §3, P1.M1) to
                execution depth: the provider abstraction layer's
                CONTRACT, realized as code and locked in place by a
                reusable, adapter-agnostic pytest suite -- before any
                real (network-touching) provider exists to tempt the
                contract into being shaped around one provider's quirks.
Parent:   end_to_end.md §3 U1.1 ("Envelope + adapter contract + mock
                provider"; objective = "design §2.4 as code"; done-when
                = "pytest exercises the whole contract against the
                mock; no provider name appears outside adapters/") +
                design.md §2.4 (the PAL contract, quoted in full below)
                + §2.11 ("PAL ⟂ workflow: no provider name outside
                adapters + ProviderRun rows") + §1.2 ("honest interims:
                partial mechanisms are named at creation... never
                silently deferred").
UoW:      U1.1 -- "Envelope + adapter contract + mock provider".
                Depends on U0.4 (done, step.1, 4dd2b4b). Sole blocker
                of U1.2 (the first REAL adapter, claude-code) and U1.3
                (the one-round-trip CLI).
Date:     2026-07-18  (SKELETON -- not executed. Execute on explicit request.)
Origin:   User directive 2026-07-18: "populate step file for U1.1".

## (0) Status

SKELETON. Nothing built. Larger than step.1 (U0.4 was pure plumbing;
this is the engine's first real architectural surface), but still a
single, offline-testable slice: no network, no orchestrator, no CLI --
just the contract and one adapter that satisfies it.

## (1) Ground truth probed at drafting

    EXISTS (the contract to implement, design.md §2.4, quoted in full):

        request:  { role-tagged context, tool schema, budget, policy }
        response: { text | tool_calls[] | done, usage, provider_meta }

        adapter interface (every provider implements exactly this):
          capabilities() -> { tools?, streaming?, context_window,
                              modalities, cost_model }
          run(request)   -> response stream
          abort(run_id)

        "Adapters are thin: translate the envelope to the provider's
        native API and back. No adapter contains workflow logic."
        "The mock provider is a first-class adapter... the whole
        engine must run and be tested with zero network and zero
        keys." "Provider identity appears in exactly one place: the
        ProviderRun record, for audit and cost accounting. The
        developer-facing surfaces say 'b3ubot'."
    EXISTS (U0.4's throwaway stub, step.1): adapters/mock/hello.py --
            a hardcoded `hello() -> str`, explicitly commented "U1.1
            REPLACES this file's contents wholesale." This step does
            exactly that; adapters/mock/ as a PATH is not new, its
            CONTENTS are.
    EXISTS (§3's ProviderRun entity): "one PAL invocation: provider
            id, usage, tool trail, egress digest (THE audit row --
            B-4/B-5)" -- but §3's five-layer data-model pipeline is
            Track B (P4), not built. CONSEQUENCE: this step's
            `provider_meta` / usage fields are plain in-memory data on
            the response object -- there is no SQLite-backed
            ProviderRun row to write yet. Naming this explicitly (not
            silently skipping persistence) is the design's own "honest
            interims" rule (§1.2) in its first real test.
    EXISTS (the reuse requirement, U1.2's own end_to_end.md text):
            "the SAME contract suite passes against the live adapter
            (auto-skip when no Claude Code client is available)." This
            is a real architecture constraint ON THIS STEP: the test
            suite built here must be adapter-agnostic/parametrized
            from the start, not a one-off hardcoded against the mock,
            or U1.2 has to refactor it under time pressure instead of
            just registering a second adapter.
    EXISTS (d3, LEANING Python; §1.2/§2.11 minimalism): no validation
            boundary exists yet (no HTTP layer, no untrusted external
            input -- d2 leans CLI-first and the CLI itself is U1.3) --
            nothing in this step's scope justifies a new dependency
            like pydantic; U0.4's requirements.txt is still pytest-only.
    MISSING: app/ (design.md §2.12's engine-code home) does not exist
            yet -- this step creates it, scoped to exactly the PAL
            (app/pal/), nothing else (no orchestrator, no ledger, no
            oracle harness -- those are P2/P3/§2.5's jobs). tests/
            (§2.12) does not exist yet either. The envelope classes,
            the adapter interface, the real mock adapter, the fixture
            set, and the contract suite itself are all net-new.

## (2) What U1.1 delivers

  2.1 **The envelope** (app/pal/envelope.py): `Request` and `Response`
      dataclasses matching §2.4's shape exactly -- `Request(context,
      tool_schema, budget, policy)`, `Response(kind, text, tool_calls,
      usage, provider_meta)` where `kind` discriminates
      text/tool_calls/done (Q-2-B: stdlib dataclasses, no new
      dependency). `policy` and `tool_schema` are defined at their
      FULL eventual shape but are INERT this step -- no enforcement
      exists yet (P3's tool surface, P7's egress hardening) -- and the
      code says so at the point of definition (C-2-D), not by silent
      omission.
  2.2 **The adapter interface** (app/pal/adapter.py): an ABC (Q-2-A)
      with exactly `capabilities()`, `run(request)`, `abort(run_id)`
      as abstract methods -- a subclass that skips one fails at
      INSTANTIATION, not at first call, catching an incomplete future
      adapter (U1.2's claude-code, U6.1's openai) at import time.
  2.3 **The real mock adapter** (adapters/mock/adapter.py, replacing
      hello.py -- Q-1-D from step.1 pays off here, zero relocation):
      implements the ABC against fixture-scripted responses (Q-2-C:
      plain Python dicts/dataclasses in a fixtures module -- no
      serialization round-trip, nothing crosses a process boundary
      yet). Fixture coverage: a plain text response, a tool_calls
      response, a "done" terminal, `usage`/`provider_meta` populated
      (`provider_meta` names "mock" -- the ONE place provider identity
      is allowed to appear outside adapters/ itself, per §2.4), and an
      `abort()` on a running / not-yet-started / already-finished run
      (Q-2-E: no-op + a returned status enum, since P3's orchestrator
      will need idempotent-abort semantics eventually regardless).
  2.4 **The contract test suite** (tests/pal_contract.py): a
      pytest-parametrized module (Q-2-D: a fixture yielding a
      registered `(name, adapter_instance)` list) that exercises
      EVERY adapter registered against it -- shape conformance for
      `capabilities()`, the full `run()` response taxonomy, `abort()`
      idempotency -- currently registering exactly the mock adapter.
      U1.2 adds ONE registration line, not a new test file.
  2.5 **The "no provider name outside adapters/" gate** (new, matching
      step.1's G6 cppcc-invocation-count precedent): a grep-based
      structural check that `app/pal/*` never contains the literal
      strings "mock"/"claude"/"openai" -- the code-level enforcement
      of §2.11's rule, cheap to run, catches the leak on the day it
      would happen rather than in a later review.
  2.6 **Closure**: end_to_end.md §11 U1.1 -> done; design.md untouched
      (this step implements existing design, changes none of it --
      the honest-interims notes for `policy`/`tool_schema` live in
      CODE comments, not a design.md rewrite); memory. Retro pair via
      scripts/step_gdiff (confirmed portable at step.1).

EXPLICITLY OUT OF SCOPE (each belongs to a later UoW):
  - Any adapter that touches a network or a credential (claude-code,
    openai)                                                          -> U1.2, U6.1
  - The one-round-trip CLI / tool surface's minimal core             -> U1.3
  - The orchestrator state machine (INTENT/SKELETON/.../CLOSED)      -> P3
  - Real enforcement of `policy` (egress) or `tool_schema` (gated
    tool execution) -- this step defines their SHAPE only            -> P3/P7
  - The ledger engine, step-file parser/emitter                      -> P2
  - The ProviderRun SQLite-backed entity / any persistence at all    -> P4
  - Capability-negotiation LOGIC beyond the mock returning a fixed,
    honest `capabilities()` result (degrading based on it is the
    orchestrator's job, P3)                                          -> P3
  - T2-T4 oracle tiers (live gates, CCS round-trip, AI review) --
    this step's suite is entirely T0/T1 (static + unit)              -> later

## (3) Verification gates

    G1  DONE-WHEN (end_to_end.md verbatim): pytest (tests/pal_contract.py)
        exercises the whole contract against the mock adapter, green;
        `grep -rn -e mock -e claude -e openai app/pal/` is empty (no
        provider name appears outside adapters/).
    G2  CONTRACT SUITE IS ADAPTER-AGNOSTIC, VERIFIED BY DESIGN: the
        registration mechanism (Q-2-D) is inspected/dry-run to confirm
        adding a second adapter needs exactly one new registration
        entry, zero test-body edits -- checked by a throwaway SECOND
        mock instance registered under a different name inside the
        test file during BUILD, then removed before commit (proving
        the mechanism without prematurely building U1.2).
    G3  ZERO NETWORK / ZERO KEYS: the entire contract suite run makes
        no network call and consults no environment variable (grep
        the mock adapter + fixtures for `os.environ`/`socket`/`http`
        -- empty; extends step.1's C-1-D discipline from the smoke
        script into real library code).
    G4  MOCK FIXTURE COVERAGE NAMED: text response, tool_calls
        response, done terminal, usage+provider_meta populated,
        abort() on all three run states (idle/running/finished) --
        each individually asserted in the suite, not folded into one
        vague "it works" test.
    G5  ADAPTERS STAY THIN (G6's step.1 precedent, generalized):
        adapters/mock/adapter.py imports nothing from outside
        `app/pal/` + the stdlib -- no orchestrator, no ledger, no
        tool-surface reference (none exist yet, but the import graph
        itself is the regression guard for when they do).
    G6  HONEST INTERIMS NAMED IN CODE: `policy` and `tool_schema`
        fields carry an inline comment at their definition site
        stating they are inert pending P3/P7 -- grep-verified present,
        not just asserted in this skeleton's prose.
    G7  Hygiene: b3ubot porcelain clean after each commit; `make test`
        (from step.1's Makefile) picks up tests/pal_contract.py with
        zero new configuration; retro via step_gdiff.

## (4) LOCKs

    C-2-A  b3ubot porcelain clean after each commit.
    C-2-B  ADAPTERS STAY THIN: no adapter -- mock now, any real one
           later -- may contain orchestrator/workflow logic (design.md
           §2.4's own rule; G5 is its regression guard).
    C-2-C  NO PROVIDER NAME LEAKAGE: provider identity lives ONLY
           inside adapters/ modules and the `provider_meta` response
           field's runtime VALUE -- never as a literal string anywhere
           in `app/pal/` logic (§2.11; G1/G5 enforce it).
    C-2-D  HONEST INTERIMS: `policy` and `tool_schema` are defined at
           their full eventual shape but are INERT this step -- stated
           in code comments at the definition site (G6), never
           silently implied as "already enforced."
    C-2-E  standing locks inherited: B-1 (moot -- this step touches no
           CCS surface at all), B-3 (no disclosure / no push), B-4
           (provider credentials env-only -- foreshadowed by C-2-D's
           `policy` note, not yet exercised since the mock needs no
           credential), B-6 (deterministic oracles gate -- this step's
           entire suite IS a T0/T1 oracle, the ledger's first one).

## (5) Open questions

    Q-2-A  Interface realization: an ABC with `capabilities()/run()/
           abort()` as abstract methods (LEAN -- explicit contract,
           fails LOUDLY at instantiation if a future adapter is
           incomplete, matches §2.4's own "every provider implements
           exactly this" wording) vs `typing.Protocol` (structural
           typing, no forced inheritance, arguably more Pythonic for
           swappable components). LEAN: ABC -- the fail-loud-at-import
           property is worth more here than Protocol's flexibility,
           given there will only ever be a handful of adapters, not a
           large duck-typed ecosystem.
    Q-2-B  Envelope implementation: stdlib `dataclasses` (LEAN -- zero
           new dependency, matches step.1's minimal-requirements
           discipline; no validation boundary exists yet to justify
           more) vs `pydantic` (real validation + serialization, but
           for a boundary -- untrusted external JSON -- that doesn't
           exist until a much later CLI/web surface). LEAN:
           dataclasses; revisit if/when U1.3's CLI or a later web
           surface needs to parse untrusted input into this shape.
    Q-2-C  Fixture format: plain Python dicts/dataclasses defined
           directly in a fixtures module (LEAN -- simplest, type-
           checked by the same envelope classes at import time, no
           serialization round-trip since nothing crosses a process
           boundary yet) vs JSON fixture files (more "data-driven,"
           but adds a parse step with no current consumer for the
           JSON form). LEAN: Python fixtures module; JSON fixtures are
           worth revisiting if/when a non-Python tool ever needs to
           read the same scripted responses (none does today).
    Q-2-D  Contract-suite reuse mechanism (the piece U1.2 depends on
           directly): a pytest fixture yielding a registered list of
           `(name, adapter_instance)` pairs, consumed via
           `pytest.mark.parametrize` (LEAN -- idiomatic pytest, each
           adapter's own module just appends one registration, zero
           test-body changes) vs a shared base test class each
           adapter's suite subclasses (more OOP, less pytest-
           idiomatic, harder to see all adapters' results in one `-v`
           run). LEAN: parametrized fixture registry -- G2 verifies
           this mechanically during BUILD.
    Q-2-E  `abort()` semantics with no orchestrator yet to define
           calling conventions: no-op + a returned status enum
           (IDLE/RUNNING/DONE/ABORTED) covering idle/running/finished
           (LEAN -- P3's orchestrator will need SOME idempotent-abort
           behavior regardless; inventing a small, honest version now
           is cheaper than guessing blind later without ANY contract
           to test against) vs leaving `abort()` unimplemented/raising
           `NotImplementedError` until P3 defines the real semantics
           (defers a real decision, but the mock adapter then can't
           satisfy its OWN interface, which is worse). LEAN: no-op +
           status enum, named explicitly as provisional (P3 may widen
           it, never narrow it without a version note).

## (6) Acceptance

    - [x] app/pal/envelope.py: Request/Response dataclasses per §2.4
          exactly, `policy`/`tool_schema` commented as inert (2.1,
          C-2-D, G6).
    - [x] app/pal/adapter.py: the ABC per Q-2-A (2.2).
    - [x] adapters/mock/adapter.py: replaces hello.py's role entirely
          (hello.py itself untouched, confirmed by full smoke.sh
          replay), implements the ABC, fixture-driven (2.3, G4).
    - [x] tests/test_pal_contract.py (renamed from the skeleton's
          pal_contract.py -- pytest discovery requires the test_
          prefix, see step.2.diff.txt finding #2): parametrized
          contract suite, mock registered, 8/8 green (2.4, G1). G2
          dry-run (a throwaway second adapter, 8 -> 16 tests, zero
          test-body edits) demonstrated then reverted before commit.
    - [x] G3/G5 verified: zero real network/env import (one grep
          false-positive on the English word "requests", confirmed
          harmless by reading the actual import list); adapters/mock/
          adapter.py's import graph confined to app/pal/ + stdlib +
          adapters.mock.fixtures.
    - [x] G1's grep confirms zero provider-name leakage in app/pal/ --
          ONE real hit found (an illustrative "mock" in a docstring),
          fixed with a generic placeholder, re-verified clean.
    - [x] `make test` (step.1's Makefile, unmodified) picks up the new
          suite with zero new configuration (G7); full rm -rf .venv
          work && make venv && make smoke && make test replay green.
    - [x] Closure: end_to_end §11 U1.1 -> done + memory; retro pair via
          step_gdiff (2.6, G7).

## (7) Hash backfill

    SKELETON commit (this file + ledger row U1.1 -> active):    234e360
    BUILD commit (envelope + adapter ABC + mock + contract suite): 4cbcad0
    RETRO commit (retro pair + ledger done):                     (HEAD)

End of skeleton.
