step.3.txt -- b3ubot U1.2: first real adapter (P1.M1) -- the
               `claude-code` adapter (d1 RESOLVED), driving the LOCAL
               Claude Code client headlessly via `claude --bare -p`.
               Requires $ANTHROPIC_API_KEY explicitly (a correction to
               d12's "rides the operator's interactive subscription"
               framing -- see ground truth below); auto-skips honestly
               when unavailable, never falls back to an ambient
               session. Tool execution stays OFF (tool_schema is still
               INERT, C-2-D from U1.1) -- the narrowest safe invocation.

Step:     3  -- promote UoW **U1.2** (end_to_end.md §3, P1.M1) to
                execution depth: a REAL, network-touching adapter that
                satisfies U1.1's contract -- the trinity's sole
                internet-communicating component (design.md §2.9), and
                the first place B-4 (credential hygiene) and B-5
                (egress -- foreshadowed, not yet enforced) actually
                matter, not just get named.
Parent:   end_to_end.md §3 U1.2 ("First real adapter (d1 RESOLVED:
                claude-code)"; objective = "the claude-code adapter
                driving the LOCAL Claude Code client... B-4 key/
                credential hygiene from the first line"; done-when =
                "the same contract suite passes against the live
                adapter (auto-skip when no Claude Code client is
                available)") + design.md §2.4 (adapter interface) +
                §2.9 (the local trinity -- this adapter's driven
                process is the ONLY component that touches the
                internet) + §2.12 (layout: adapters/claude/).
UoW:      U1.2 -- "First real adapter (claude-code)". Depends on U1.1
                (done, step.2, 4cbcad0). Sole blocker of U1.3 (the
                one-round-trip CLI) and U1.4 (P1 gates + retro).
Date:     2026-07-18  (SKELETON -- not executed. Execute on explicit request.)
Origin:   User directive 2026-07-18: "execute step for U1.2" -- this
                skeleton is drafted and executed in the same pass, at
                the operator's request, following the rhythm already
                established for step.1/step.2.

## (0) Status

SKELETON. Nothing built. ⚠ This step is the FIRST one in either
repo's history that spawns a REAL external process talking to a real
AI provider -- real cost, real account usage. Ground truth below
(researched via the Claude Code docs before writing any code) informs
a design that only ever executes that live path when the operator has
explicitly configured `$ANTHROPIC_API_KEY`; this dev environment does
NOT have that key set, so the live leg auto-skips HONESTLY in this
session -- itself a real proof of the done-when's "auto-skip when no
Claude Code client is available" clause, achieved without spending
anything. No live invocation is attempted in this session regardless
of whether the skip fires (see Q-3-B).

## (1) Ground truth probed at drafting

    EXISTS (headless invocation, verified against the official Claude
            Code docs, not assumed): `claude --bare -p "<prompt>"
            --output-format json` runs one-shot, non-interactively,
            and exits. `--bare` skips auto-discovery of hooks/skills/
            MCP servers/CLAUDE.md (reproducibility -- this adapter
            must not silently pick up whatever hooks happen to exist
            in the invoking machine's config). `--output-format json`
            returns a structured blob: `result` (text), `session_id`,
            `total_cost_usd`, usage fields.
    FOUND  (a real correction to design.md d12's framing): in `--bare`
            mode, authentication comes from `$ANTHROPIC_API_KEY`
            ONLY -- NOT the interactive OAuth/keychain session an
            operator's normal `claude` login uses. d12's own text
            ("the trinity's Claude Code client rides the OPERATOR'S
            subscription -- fine for development") assumed the
            SUBSCRIPTION-riding shape; the SAFE, reproducible,
            automatable invocation mode (`--bare`) does not support
            that at all. The alternative -- non-bare `-p`, which WOULD
            ride the ambient interactive session -- was deliberately
            NOT tested live this step (Q-3-B): it also auto-discovers
            hooks/CLAUDE.md/MCP servers from wherever it's invoked,
            which is a real, unbounded surface for an automated caller
            to inherit unpredictably, and -- more pointedly -- running
            it from WITHIN a live interactive Claude Code session (the
            one executing this very step) risks spending the
            operator's real account usage on an experiment, which is
            exactly the class of action this project's own standing
            discipline says to pause and confirm before taking, not
            silently try. This step's adapter therefore REQUIRES
            `$ANTHROPIC_API_KEY` explicitly (B-4's own "environment-
            only" wording, extended to be the SOLE auth path) and
            never falls back to an ambient session. d12 needs a
            design.md correction note at closure (2.7).
    FOUND  (tool restriction): `--allowedTools ""` (empty allowlist)
            plus `--bare` is the narrowest documented invocation --
            no file/shell/network tool access for the spawned process.
            This step's adapter hardcodes this; there is no
            "unrestricted" mode exposed. Direct consequence: with all
            tools disabled, the live Claude Code process CANNOT emit a
            genuine TOOL_CALLS response, matching (not contradicting)
            U1.1's own C-2-D lock that `tool_schema` is INERT until
            P3 wires real tool execution.
    FOUND  (no zero-cost live pre-check exists): the docs name no
            "ping" endpoint; the only honest, zero-cost availability
            check is LOCAL: is the `claude` binary on PATH (`shutil.
            which`, confirmed present in this dev environment) AND is
            `$ANTHROPIC_API_KEY` set and non-empty (confirmed ABSENT
            in this dev environment, checked directly, value never
            printed). Both together gate `is_available()`; the adapter
            NEVER shells out just to check.
    FOUND  (abort() has a real architectural limit here, not just a
            provisional stub): U1.1's MockAdapter is a generator that
            yields per fixture item, giving a natural checkpoint for a
            synchronous test to call abort() mid-stream. This
            adapter's single live call blocks inside one subprocess
            wait; a caller on the SAME thread has no opportunity to
            call abort() while `run()` is blocked. Using `Popen` +
            `.communicate(timeout=...)` (rather than the simpler
            `subprocess.run`) at least gives a REAL process handle
            `abort()` can `.terminate()` from a DIFFERENT thread/
            process -- P3's orchestrator (async/threaded scheduling)
            is where that path gets exercised for real; the single-
            threaded contract suite cannot exercise it today, and
            should say so rather than fake a pass (Q-3-C).
    FOUND  (SIGTERM exit behavior, per docs): a terminated Claude Code
            process aborts its turn, tears down its own process tree,
            and exits 143 -- this adapter treats exit 143 as the
            ABORTED signature specifically (distinct from any other
            non-zero exit, which is a genuine run error).
    EXISTS (an alternative: the claude-agent-sdk): a Python SDK exists
            (in-process, no subprocess). NOT used this step (Q-3-D) --
            it is a new dependency this codebase's minimalism
            discipline (Q-1-C/Q-2-B) has no current justification for;
            the CLI subprocess path needs zero new pip package.
    MISSING: adapters/claude/ (design.md §2.12's named layout does not
            exist yet); the ClaudeCodeAdapter class; the availability
            check; the JSON-response parser (no REAL captured
            response exists to test against -- this session has no
            live key -- so the parser fixture is built from the
            DOCUMENTED shape and flagged for re-validation against a
            real capture once a key exists, Q-3-E); the contract
            suite's skipif wiring; the two adapter-capability flags
            (SUPPORTS_TOOL_CALLS, SUPPORTS_INTERLEAVED_ABORT) that
            U1.1's ABC needs added so the shared suite can honestly
            skip what THIS adapter's safe configuration structurally
            cannot do, without hardcoding a provider-name string
            inside tests/test_pal_contract.py's test bodies (which
            would itself be a small, ironic violation of "no provider
            name outside adapters/" -- capability flags keep the
            skip reason principled, not name-matched).

## (2) What U1.2 delivers

  2.1 **Capability flags on the ABC** (app/pal/adapter.py, a small
      extension of U1.1's file): `SUPPORTS_TOOL_CALLS: bool = True`
      and `SUPPORTS_INTERLEAVED_ABORT: bool = True` as class defaults
      -- MockAdapter needs no change (both stay True, matching its
      actual behavior); ClaudeCodeAdapter overrides both to False,
      each with an inline reason.
  2.2 **app/pal/errors.py** (new, shared -- any real adapter can
      raise these): `AdapterError`, `AdapterUnavailableError` (raised
      by `run()` if called while unavailable -- defensive, "fail
      loud, name the fix"), `AdapterRunError` (non-zero exit / bad
      JSON / any run-time failure, message capped and never includes
      the API key -- B-4).
  2.3 **adapters/claude/adapter.py**: `ClaudeCodeAdapter(Adapter)`.
      `is_available() -> (bool, str)`: binary-on-PATH AND
      `$ANTHROPIC_API_KEY` set, zero subprocess spawned to check.
      `capabilities()`: `tools=False` (hardcoded off this step),
      `streaming=False` (this adapter uses single-blob JSON, not
      `stream-json`, honestly stated), `context_window` a named,
      commented best-known constant (not queried live -- an honest
      interim), `cost_model` states pricing is Anthropic's published
      rate, not locally computed (mirrors b3u's C-21-B "never compute
      money locally" convention). `run(request)`: flattens
      `request.context` into one role-tagged prompt string (headless
      `-p` takes one string; genuine multi-turn state does not exist
      in this adapter yet, same statelessness as MockAdapter),
      `Popen`s `claude --bare -p <prompt> --output-format json
      --allowedTools ""` with a MINIMAL subprocess environment
      (`PATH`/`HOME`/`ANTHROPIC_API_KEY` only -- nothing else from the
      parent environment leaks in, B-4/B-5 in spirit), a bounded
      timeout, parses the JSON blob into Response object(s) tagged
      TEXT then DONE (never TOOL_CALLS, per 2.1's flag), raises
      `AdapterUnavailableError`/`AdapterRunError` on the documented
      failure paths. `abort(run_id)`: `.terminate()`s the tracked
      `Popen` handle if RUNNING, same IDLE/RUNNING/DONE/ABORTED
      status-table shape as MockAdapter for interface consistency.
  2.4 **Contract suite wiring** (tests/test_pal_contract.py, extended
      not rewritten): claude-code registered via ONE
      `pytest.param(..., marks=pytest.mark.skipif(not available,
      reason=...))` entry (Q-2-D's mechanism proven live by this real
      second adapter, not just a throwaway dry-run this time). The
      two tests that need `SUPPORTS_TOOL_CALLS`/
      `SUPPORTS_INTERLEAVED_ABORT` gates get a short, capability-
      flag-driven `pytest.skip()` guard at their top -- zero other
      test bodies touched.
  2.5 **Adapter-internal unit tests** (tests/test_claude_code_adapter.py,
      new -- NOT part of the shared contract suite, this adapter's
      own logic): context-flattening, `is_available()`'s two-
      condition logic (binary present/absent x key present/absent,
      all 4 branches, zero live invocation in any of them), and JSON-
      response parsing against a fixture literal BUILT FROM THE
      DOCUMENTED SHAPE (explicitly commented: not a captured real
      response, this session had no live key -- Q-3-E names the
      follow-up).
  2.6 **B-4 hygiene, concretely**: grep-based regression guard (G6)
      that no test, fixture, or source file in this step ever prints,
      logs, or asserts-the-VALUE-of `$ANTHROPIC_API_KEY` -- only its
      presence/absence.
  2.7 **Closure**: end_to_end.md §11 U1.2 -> done; design.md d12
      gains a short correction note (this step found `--bare` mode
      needs an explicit API key, not the ambient subscription --
      recorded where d12 lives, not silently overwritten); memory.
      Retro pair via step_gdiff.

EXPLICITLY OUT OF SCOPE (each belongs to a later UoW, or is
permanently out per this step's own safety posture):
  - Any invocation that RIDES the operator's ambient interactive
    session (non-`--bare`) -- deliberately not built or tested this
    step (Q-3-B); if ever wanted, it is a NEW, separately-considered
    capability, not a fallback path this adapter silently takes.  -> never
    (unless explicitly reopened)
  - Real tool execution / a non-empty `--allowedTools` mode -- gated
    on P3's tool surface existing at all.                          -> P3
  - `--output-format stream-json` / genuine streaming responses --
    this step's `capabilities()` honestly reports `streaming=False`
    for what THIS adapter actually does today.                     -> later
  - Multi-turn conversational state across `run()` calls -- headless
    `-p` is one-shot; real state needs the orchestrator (P3).       -> P3
  - The claude-agent-sdk alternative (Q-3-D) -- CLI subprocess only,
    this step.                                                      -> later,
                                                                        if ever
  - A REAL captured JSON response fixture (Q-3-E) -- this session
    has no live key to capture one from.                            -> next
                                                                        session
                                                                        with a
                                                                        key
  - The one-round-trip CLI, orchestrator, ledger engine             -> U1.3+

## (3) Verification gates

    G1  DONE-WHEN (end_to_end.md verbatim): the SAME contract suite
        (tests/test_pal_contract.py) exercises claude-code alongside
        mock; the claude-code parametrization SKIPS (not fails, not
        errors) in an environment lacking `$ANTHROPIC_API_KEY`,
        verified by actually running the suite in exactly that
        (this dev box's real) condition.
    G2  NO PROVIDER NAME OUTSIDE adapters/ (U1.1's G1 precedent,
        re-run): `grep -rn -e mock -e claude -e openai app/pal/` stays
        empty after this step's edits to app/pal/adapter.py and the
        new app/pal/errors.py.
    G3  CAPABILITY-FLAG SKIPS ARE PRINCIPLED, NOT NAME-MATCHED: the
        two gated contract tests key off `adapter.SUPPORTS_TOOL_CALLS`
        / `adapter.SUPPORTS_INTERLEAVED_ABORT`, never an `if name ==
        "claude-code"` string check inside tests/test_pal_contract.py.
    G4  ZERO LIVE INVOCATION THIS SESSION, PROVEN NOT ASSUMED: no
        subprocess named `claude` is ever spawned during this step's
        BUILD or verification (checked -- no process-launch call
        succeeds past the `is_available()` gate given the confirmed-
        absent API key); the live-path CODE is reviewed and unit-
        tested (2.5) but never executed end-to-end this session.
    G5  B-4 HYGIENE: `$ANTHROPIC_API_KEY`'s VALUE is never printed,
        logged, or embedded in an assertion anywhere this step
        touches -- only its presence/absence (G6's grep).
    G6  MINIMAL SUBPROCESS ENVIRONMENT: adapters/claude/adapter.py's
        `Popen` call passes an explicitly-built env dict containing
        only PATH/HOME/ANTHROPIC_API_KEY -- never a bare inherited
        `os.environ` passthrough (grep-verified: no bare `env=None`
        / `env=os.environ` call site).
    G7  NO REGRESSION: the 8 existing U1.1 contract tests (mock leg)
        stay green, byte-identical in behavior; step.1's `make smoke`
        still passes unmodified; `make test` picks up the new adapter
        unit tests with zero new pytest configuration.
    G8  Hygiene: b3ubot porcelain clean after each commit; retro via
        step_gdiff.

## (4) LOCKs

    C-3-A  b3ubot porcelain clean after each commit.
    C-3-B  NO AMBIENT-SESSION FALLBACK: this adapter NEVER invokes
           Claude Code in a mode that could ride an already-
           authenticated interactive session -- `$ANTHROPIC_API_KEY`
           is the ONLY accepted credential path, checked explicitly,
           never silently inferred from CLI login state.
    C-3-C  TOOLS STAY OFF: `--allowedTools ""` is hardcoded, not
           configurable via any parameter this step exposes -- widening
           it is P3's job, done deliberately when the tool surface
           (§2.7) exists to gate it, never as a quiet default change
           here.
    C-3-D  MINIMAL SUBPROCESS ENVIRONMENT (G6's regression guard):
           PATH/HOME/ANTHROPIC_API_KEY only, nothing else from the
           parent process's environment reaches the child.
    C-3-E  CAPABILITY FLAGS OVER NAME CHECKS: any test needing to
           behave differently per adapter reads a capability flag on
           the adapter instance, never a literal provider-name string
           comparison outside adapters/ itself (G3).
    C-3-F  standing locks inherited: B-1 (moot, no CCS surface here),
           B-3 (no disclosure/push), B-4 (this step's whole design is
           its first real enforcement point), B-5 (foreshadowed --
           the flattened prompt IS everything this adapter sends
           externally; no egress POLICY gate exists yet, P7's job, but
           the prompt's full content is exactly what design.md's
           ProviderRun trail will need to record once P4 exists), B-6
           (this step's suite is T0/T1 -- deterministic, gates; no AI
           review tier exists yet).

## (5) Open questions

    Q-3-A  Prompt flattening shape: role-tagged plain-text lines
           (`[system]\n...\n[user]\n...`, LEAN -- simplest, matches
           headless `-p`'s single-string input, human-readable if
           logged for debugging) vs a more elaborate structured
           encoding (e.g. XML-ish tags). LEAN: plain role-tagged
           lines; revisit only if a real live run shows Claude Code
           parsing it worse than expected (untestable without a key
           this session).
    Q-3-B  Ambient-session invocation (non-`--bare`, riding the
           operator's interactive login): explicitly NOT built or
           tested this step (LEAN -- real, unbounded risk: spends the
           operator's real account usage AND auto-discovers whatever
           hooks/CLAUDE.md/MCP config exists at the invocation cwd,
           neither of which this step can respinsibly own without a
           much larger design) vs building it as a second, opt-in mode
           now. LEAN: never build it silently; if the operator wants
           subscription-riding invocation later, that is its own,
           separately-considered step with its own safety review, not
           a flag added quietly here.
    Q-3-C  Interleaved abort() testing: name the limitation honestly
           via `SUPPORTS_INTERLEAVED_ABORT = False` + a skipped
           contract test (LEAN -- matches this project's "honest
           interims, never silently deferred" discipline exactly; a
           fake pass would be worse than an honest skip) vs building a
           thread-based test harness to actually exercise `Popen`-
           level `.terminate()` this step (real engineering, but for a
           path P3's real orchestrator will need ANYWAY once it
           exists, so building a THROWAWAY threading harness now
           mostly duplicates future work). LEAN: honest skip now;
           P3 is where this gets a real test, using real orchestrator
           concurrency instead of a bespoke test-only thread.
    Q-3-D  claude-agent-sdk vs CLI subprocess: CLI subprocess (LEAN --
           zero new dependency, matches every prior minimalism
           decision this repo has made) vs the Python Agent SDK
           (in-process, likely more robust long-term, but a new
           dependency with its own version/compat surface this step
           has no pressing need to take on). LEAN: CLI subprocess;
           revisit if subprocess fragility (parsing, timeouts,
           zombie processes) becomes a real, observed problem once
           this adapter is exercised live.
    Q-3-E  JSON-response fixture provenance: built from the
           DOCUMENTED output shape (LEAN this session -- no live key
           available to capture a real response; a documented-shape
           fixture, clearly labeled as such, is still real coverage
           of the parsing LOGIC, just not proof the live shape
           matches exactly) vs deferring the parser's unit tests
           entirely until a real capture exists (leaves the parsing
           code completely untested this step, worse than a labeled
           approximation). LEAN: documented-shape fixture now,
           explicitly flagged in a code comment for re-validation
           against a REAL captured response the first time this
           adapter actually runs live (whoever configures the key
           first should diff the real shape against this fixture and
           fix any mismatch before trusting the parser).

## (6) Acceptance

    - [x] app/pal/adapter.py: SUPPORTS_INTERLEAVED_ABORT class default
          added (2.1, G3). SUPPORTS_TOOL_CALLS DROPPED mid-build --
          redundant with the already-contractual capabilities()
          ["tools"] field; see step.3.diff.txt finding #1.
    - [x] app/pal/errors.py: AdapterError/AdapterUnavailableError/
          AdapterRunError (2.2).
    - [x] adapters/claude/adapter.py: ClaudeCodeAdapter -- is_available(),
          capabilities(), run() (Popen, --bare, --allowedTools "",
          minimal env, timeout, TEXT/DONE-only parsing), abort()
          (2.3, C-3-B/C/D, G4/G5/G6).
    - [x] tests/test_pal_contract.py: claude-code registered with a
          skipif marker; test_tool_calls_response/test_multi_part_stream
          gated on capabilities()["tools"]/["streaming"] (not a new
          flag); test_abort_running_run gated on
          SUPPORTS_INTERLEAVED_ABORT (2.4, G1, G3).
    - [x] tests/test_mock_adapter.py (new, not in the original plan):
          test_unrecognized_request_fails_loud MOVED here -- found
          building the second adapter that it was never a universal
          contract property, only MockAdapter's own fixture-routing
          behavior (step.3.diff.txt finding #2).
    - [x] tests/test_claude_code_adapter.py: context-flattening,
          all 4 is_available() branches, JSON-parsing against the
          documented-shape fixture (2.5, Q-3-E noted in-file).
    - [x] G1 run for real in this dev environment: claude-code
          parametrization SKIPS with the exact reason "$ANTHROPIC_API_KEY
          is not set..." (pytest -v -rs output captured); mock leg
          unaffected, 7/7 still green (plus 10 new adapter-internal +
          mock-specific unit tests, all green).
    - [x] G2/G3/G5/G6 grep-verified.
    - [x] G7: full rm -rf .venv work && make venv/smoke/test replay
          clean, zero regression (17 passed, 7 honestly skipped).
    - [x] Closure: end_to_end §11 U1.2 -> done + design.md d12
          correction note + memory; retro pair via step_gdiff (2.7, G8).

## (7) Hash backfill

    SKELETON commit (this file + ledger row U1.2 -> active):    effe53f
    BUILD commit (adapter + errors + tests, live-skip verified): 96afa52
    RETRO commit (retro pair + ledger done):                     (HEAD)

End of skeleton.
