step.7.txt -- b3ubot U1.5: the human-relay adapter (P1.M1) -- a THIRD
               first-class PAL implementation, `adapters/human/`, where
               the developer manually relays request context to an AI
               coding tool running in a SEPARATE session and pastes its
               response back. Validates the whole Adaptive Programming
               workflow (skeleton -> ratify -> execute -> verify ->
               retro) without any AI-tool-specific automated adapter,
               and decouples the workflow's validation from any single
               vendor's API timing (design.md §5 d13).

Step:     7  -- promote UoW **U1.5** (end_to_end.md §3, P1.M1; not
                originally planned -- new architectural insight,
                operator directive 2026-07-20) to execution depth: a
                third `Adapter` implementation that needs no AI-vendor
                integration at all, proving the orchestrator/PAL/oracle
                machinery is genuinely provider-agnostic one adapter
                earlier than U6.1's planned second REAL (automated)
                adapter.
Parent:   end_to_end.md §3 U1.5 (new bullet, this step) + design.md
                §2.4 (adapter interface, "The mock provider is a
                first-class adapter..." bullet, now joined by a third)
                + §5 d13 (new decision, this step) + §2.12 (layout:
                adapters/human/).
UoW:      U1.5 -- "Human-relay adapter (manual, decouples workflow
                from any AI tool)". Depends on U1.1 (done, step.2,
                4cbcad0) for the Adapter ABC/envelope; sibling to U1.2
                (claude-code, done, step.3, 96afa52) and U1.4 (P1
                gates + retro, still todo -- U1.5 does not block or
                get blocked by U1.4; it is additive to P1.M1's adapter
                roster, not on U1.4's critical path).
Date:     2026-07-20  (drafted and executed in the same pass, at the
                operator's explicit request -- the rhythm already
                established for step.1/step.3.)
Origin:   User directive 2026-07-20: before any AI-tool-specific
                automated adapter is built, the whole Adaptive
                Programming workflow can be validated with the human
                developer standing in as the relay between b3ubot and
                an AI coding tool running in a SEPARATE terminal (e.g.
                Claude Code in Shell 1, b3ubot in Shell 2). The
                developer manually copies b3ubot's request context
                into Shell 1 and pastes the AI's response back into
                Shell 2.

## (0) Status

EXECUTED in this pass. Not a pure skeleton-then-later-execute split
like step.3 -- code, tests, docs, and this step file were built
together per the operator's direction for this session.

## (1) Ground truth probed at drafting

    READ    app/pal/adapter.py (the `Adapter` ABC): `capabilities()`,
            `run(request) -> Iterator[Response]`, `abort(run_id)`.
            `SUPPORTS_INTERLEAVED_ABORT`'s own docstring already names
            the exact case this adapter is: "a synchronous single-
            process design with no concurrent-abort checkpoint" --
            written for ClaudeCodeAdapter (step.3) but applies letter-
            for-letter to a human blocked on `input()`.
    READ    app/pal/envelope.py: `Request` (context/tool_schema/
            budget/policy, the latter two still INERT per U1.1's
            C-2-D) and `Response` (`kind` discriminates TEXT/
            TOOL_CALLS/DONE; the docstring states "DONE -- the run has
            concluded, no new content").
    READ    adapters/mock/adapter.py + adapters/claude/adapter.py (the
            two sibling patterns to mirror): both yield a distinct
            TEXT response before a final DONE; both track per-run-id
            status in a dict for abort() idempotency; ClaudeCodeAdapter
            sets `SUPPORTS_INTERLEAVED_ABORT = False` with an inline
            reason, the direct precedent for this adapter's own flag.
    READ    tests/test_pal_contract.py: the shared suite's own
            docstring already documents TWO prior findings from adding
            a second real adapter (a test that turned out to be mock-
            specific, and a capability flag that turned out to
            duplicate an existing contract field) -- explicit
            precedent for "the contract evolves honestly when a new
            adapter reveals a wrongly-universal assumption," which
            this step needed (see finding below).
    READ    app/cli.py's `propose_ask()`: hard-requires at least one
            TEXT-kind response (`if not text_responses: raise
            RuntimeError(...)`) to compute a diff and record egress.
    FOUND   (a real, load-bearing design tension, not a corner case):
            the operator's design for this adapter -- `run()` writes
            the formatted request via an injectable output function,
            blocks on an injectable input function, and yields EXACTLY
            ONE `Response` with `kind == ResponseKind.DONE` wrapping
            the raw pasted-back text -- is narrower than both (a)
            envelope.py's own Response docstring ("DONE... no new
            content") and (b) `test_pal_contract.py`'s
            `test_text_response`, which asserted at least one
            TEXT-kind response exists, and (c) `app/cli.py`'s
            `propose_ask()`, which hard-requires the same. A human
            paste-back genuinely has no separate "content chunk" to
            distinguish from "the run concluded" -- unlike a streaming
            or single-blob-then-terminator provider, there is exactly
            one event. Resolved by extending (not hacking) the
            contract test to accept either shape honestly (see §2.4
            below), and by NOT wiring `--provider human` into the
            `ask` CLI subcommand this step -- wiring it in without
            also changing `propose_ask()` would make every invocation
            fail with that same RuntimeError, which is worse than not
            exposing the flag. Reported here and in step.7.diff.txt
            rather than silently working around it.
    MISSING adapters/human/ (design.md's layout convention, §2.12,
            does not name it yet -- added this step); HumanRelayAdapter
            itself; the contract-suite registration; a dedicated unit
            test file; design.md's third-adapter bullet (§2.4) and
            new decision (§5 d13, next free id after d12); end_to_end.md's
            U1.5 bullet (§3) and ledger row (§11); README mention.

## (2) What U1.5 delivers

  2.1 **adapters/human/adapter.py**: `HumanRelayAdapter(Adapter)`.
      `__init__(self, *, output_fn=print, input_fn=input)` --
      constructor-injected I/O, the ONE design choice that gives this
      adapter the same offline/zero-network/zero-keys testability
      MockAdapter gets from fixtures (design.md §2.4). `_format_request()`
      renders a clearly delimited, human-readable block: a banner, the
      role-tagged context entries (`[role]\ncontent`, mirroring
      ClaudeCodeAdapter's `_flatten_context` shape), and the still-INERT
      `tool_schema`/`budget`/`policy` fields shown for visibility only
      when non-empty. `capabilities()`: `tools=False`, `streaming=False`
      (both honest -- manual text relay, no native tool-calling, no
      token stream), `context_window=None` (no real limit enforced BY
      THIS ADAPTER -- the human decides what fits; `None` states
      "unbounded/unknown" rather than fabricating a number), `cost_model`
      an explicit `{"tracked": False, "reason": ...}` marker (b3ubot
      makes no API call here; the AI tool's cost is the operator's own,
      external to this adapter's accounting -- consistent with d12's
      BYO-key framing). `run(request)`: generates a run_id, writes the
      formatted block via `output_fn`, blocks on `input_fn` for the
      pasted-back text, yields exactly one `Response(kind=DONE,
      text=<pasted text>, provider_meta={"provider": "human",
      "run_id": ...})`. `abort(run_id)`: same IDLE/RUNNING/DONE/ABORTED
      status-table shape as the sibling adapters, idempotent, never
      raises (the RUNNING branch is unreachable in today's strictly
      synchronous call shape, kept for symmetry and defensiveness).
  2.2 **SUPPORTS_INTERLEAVED_ABORT = False**, with an inline comment
      citing the Adapter ABC's own docstring reasoning verbatim (no
      concurrent-abort checkpoint exists while blocked on `input_fn`).
  2.3 **Contract suite wiring** (tests/test_pal_contract.py, extended
      not rewritten): `human` registered via one `pytest.param` entry,
      constructed with a captured-output list and a canned-string
      input function -- zero real interactivity. `test_text_response`
      EXTENDED (the genuine finding from §1): accepts a distinct
      TEXT-kind response (mock/claude-code's shape) OR a content-
      bearing terminal DONE (human's shape), asserting real content +
      run_id either way -- documented in the file's own docstring
      as a third contract-evolution finding, matching the file's
      established precedent for this kind of honest widening.
  2.4 **tests/test_human_adapter.py** (new, adapter-internal, not part
      of the shared suite): `_format_request` actually contains the
      request's role-tagged content (not a placeholder) and the INERT
      fields when present/absent; `run()` calls `output_fn` exactly
      once with a block containing the real context, calls `input_fn`
      with a prompt string, and wraps the injected return value into
      exactly one DONE `Response` with the right `provider_meta`;
      `capabilities()`'s honesty (tools/streaming False, context_window
      None, cost_model untracked); `abort()`'s idempotent/never-raises
      contract on both an unknown run_id and a completed one.
  2.5 **Documentation** (same commit as the code): design.md §2.4 gains
      a third-adapter bullet; design.md §5 gains d13 (RESOLVED,
      2026-07-20, framed as a development/validation aid, NOT a
      replacement for d1's claude-code adapter); end_to_end.md §3
      gains a U1.5 objective/done-when bullet in P1.M1; end_to_end.md
      §11 gains the U1.5 ledger row (done, step.7, commit hash filled
      in after committing per §12's discipline); README.md's "Getting
      started" and "The local trinity" sections mention the human
      adapter as the manual-relay alternative -- WITHOUT a working
      `--provider human` CLI example, per the §1 finding (`ask`'s
      `propose_ask()` cannot consume this adapter's single-DONE shape
      without its own change, out of scope this step).
  2.6 **Closure**: end_to_end.md §11 U1.5 -> done; memory update;
      retro pair via step_gdiff.

EXPLICITLY OUT OF SCOPE (each belongs to a later UoW, or is a
deliberate non-goal this step):
  - Wiring `--provider human` into `app/cli.py`'s `ask` subcommand --
    blocked on `propose_ask()`'s hard TEXT-response requirement, a
    genuine mismatch this step reports rather than papers over.  -> later,
                                                                      if ever
  - Changing `propose_ask()` itself to accept a content-bearing DONE
    response (would affect mock/claude-code's contract too) -- a
    separate, deliberately-considered design change, not a quiet
    side effect of adding a third adapter.                          -> later
  - Any real terminal/TTY interaction in tests -- `output_fn`/
    `input_fn` are ALWAYS injected in every test this step adds.     -> never
    (by design)
  - Multi-turn conversational state, streaming, or tool execution
    for this adapter -- `capabilities()` honestly reports all three
    unsupported; widening any of them is a new, separately-considered
    step.                                                            -> later,
                                                                         if ever

## (3) Verification gates

    G1  DONE-WHEN (end_to_end.md, this step's own bullet): the SAME
        contract suite (tests/test_pal_contract.py) exercises `human`
        alongside `mock` and `claude-code`, with ZERO real
        interactivity (verified: no bare `input`/`print` call site in
        the human registration or in tests/test_human_adapter.py --
        grep-checked).
    G2  NO PROVIDER NAME OUTSIDE adapters/ (U1.1's G1 precedent,
        re-run): `grep -rn -e mock -e claude -e human -e openai
        app/pal/` stays empty -- this step touches no file under
        app/pal/ at all.
    G3  THE MISMATCH IS REPORTED, NOT HACKED: `--provider human` does
        NOT appear in `app/cli.py`'s `--provider` choices (grep-
        verified); the reason is recorded in this file (§1), design.md
        §5 d13, and step.7.diff.txt -- not silently worked around by,
        e.g., faking a TEXT-kind response that contradicts the
        adapter's own documented "exactly one DONE" design.
    G4  CONTRACT EVOLUTION IS DOCUMENTED, NOT SILENT: the
        `test_text_response` change carries an inline comment
        explaining WHY (mirrors the file's own existing docstring
        precedent for two prior findings), and the file's top
        docstring gets a third numbered finding.
    G5  NO REGRESSION: the pre-existing 38 passed / 8 skipped baseline
        (mock + claude-code legs, all adapter-internal suites) stays
        fully green; `make smoke` unaffected (this step touches no
        file smoke.sh depends on).
    G6  Hygiene: b3ubot porcelain clean after each commit; retro via
        step_gdiff.

## (4) LOCKs

    C-7-A  b3ubot porcelain clean after each commit.
    C-7-B  INJECTED I/O ONLY IN TESTS: every test this step adds
           constructs `HumanRelayAdapter` with explicit `output_fn`/
           `input_fn` overrides -- never relies on the `print`/`input`
           defaults (which would block real test runs on a TTY).
    C-7-C  NO SILENT CLI WIRING: `--provider human` is not added to
           `app/cli.py` until `propose_ask()`'s TEXT-response
           requirement is itself reconciled with this adapter's
           single-DONE shape -- a deliberate, separately-considered
           change, not a quiet side effect of this step (G3).
    C-7-D  CONTRACT CHANGES ARE EXPLAINED IN-FILE: any edit to
           tests/test_pal_contract.py's shared assertions carries an
           inline comment naming which adapter's shape motivated it
           (G4) -- matches this file's own established discipline.
    C-7-E  standing locks inherited: B-1 (moot, no CCS surface here),
           B-3 (no disclosure/no push), B-4 (moot, no credential of
           any kind touches this adapter), B-5 (the formatted request
           block IS everything a human is asked to relay externally --
           no egress policy gate exists yet, P7's job, same posture as
           step.3's finding), B-6 (this step's suite is T0/T1 --
           deterministic, gates; no AI review tier involved).

## (5) Open questions

    Q-7-A  Injectable I/O signature: `output_fn: Callable[[str], None]
           = print` / `input_fn: Callable[[str], str] = input` (LEAN --
           both defaults are real stdlib callables with matching
           signatures, so the adapter is usable interactively with
           ZERO configuration, and swappable with zero-argument
           overrides for tests) vs a richer callback protocol (e.g. an
           object with separate `write()`/`read()` methods). LEAN:
           plain callables; revisit only if a real interactive session
           reveals the single-string contract is insufficient.
    Q-7-B  Response shape: exactly one DONE response wrapping the text
           (LEAN, operator's explicit design -- a human paste-back has
           no genuine intermediate chunk to report, so a fabricated
           TEXT-then-DONE pair would be dishonest content duplication)
           vs a TEXT response followed by an empty DONE (mirrors mock/
           claude-code exactly, but invents a distinction that does
           not exist in this adapter's actual execution model, and
           would make the "exactly one Response" requirement moot).
           LEAN: exactly one DONE response, per spec; the resulting
           contract-suite and CLI mismatches are handled by honest
           extension (test) and honest non-wiring (CLI), not by
           bending the adapter's shape to fit consumers built for a
           different model. Revisit if/when `propose_ask()` itself is
           generalized.
    Q-7-C  `--provider human` CLI wiring: build it now with a `- since
           this adapter cannot satisfy `propose_ask()`'s TEXT-response
           requirement (LEAN -- do not wire this step, see C-7-C) vs
           changing `propose_ask()` to accept a content-bearing DONE
           as well (would touch the mock/claude-code path too, a
           larger, separately-considered change). LEAN: do not wire;
           report the gap (this file, design.md d13, step.7.diff.txt);
           whoever picks up "make `ask --provider human` real" should
           decide `propose_ask()`'s generalization deliberately, not
           as a side effect of adding a fourth adapter.

## (6) Acceptance

    - [x] adapters/human/__init__.py, adapters/human/adapter.py:
          HumanRelayAdapter -- capabilities(), run() (injectable I/O,
          exactly one DONE response), abort() (2.1/2.2, C-7-B).
    - [x] tests/test_pal_contract.py: `human` registered with fake I/O;
          `test_text_response` extended with an in-file-documented,
          adapter-shape-driven branch (2.3, G1/G3/G4/C-7-D).
    - [x] tests/test_human_adapter.py: format/output/input/capabilities/
          abort coverage, zero real interactivity throughout (2.4,
          C-7-B).
    - [x] design.md §2.4 third-adapter bullet + §5 d13 (2.5).
    - [x] end_to_end.md §3 U1.5 bullet + §11 ledger row (2.5).
    - [x] README.md: human adapter mentioned in Getting started / The
          local trinity, WITHOUT a working `--provider human` CLI
          example (2.5, G3).
    - [x] `app/cli.py` NOT touched -- `--provider human` deliberately
          absent (G3, C-7-C).
    - [x] Full suite green: 52 passed, 11 skipped (up from the 38
          passed / 8 skipped baseline -- +7 contract-suite instances
          for the human param (4 pass, 3 honest capability/flag skips)
          + 10 new adapter-internal unit tests, zero regressions) (G5).
    - [x] G2/G3/G6 grep/review-verified.
    - [x] Closure: end_to_end §11 U1.5 -> done; retro pair via
          step_gdiff (2.6, G6).

## (7) Hash backfill

    WORK commit (adapter + tests + docs + this file + ledger):  18b1495
    GDIFF commit (step.7.gdiff.txt via scripts/step_gdiff):     (HEAD)

End of step.
