step.8.txt -- b3ubot U1.6: wire `--provider human` into the `ask` CLI --
               reconcile `propose_ask()`'s hard TEXT-response
               requirement with the human-relay adapter's single-DONE
               response shape (step.7's reported, deliberately
               unresolved gap; design.md §5 d13; Q-7-C).

Step:     8  -- promote UoW **U1.6** (end_to_end.md §3, P1.M1) to
                execution depth: make `adapters/human/` reachable from
                `app/cli.py`'s `ask` subcommand, which today hard-fails
                against it (step.7 §1 finding, §5 Q-7-C).
Parent:   step.7.txt §1/§5 (Q-7-B, Q-7-C) + design.md §5 d13 + app/cli.py
                `propose_ask()`/`_make_adapter()`.
UoW:      U1.6 -- "Wire `--provider human` into `ask` (reconcile
                propose_ask()'s TEXT-response requirement)". Depends on
                U1.5 (done, step.7, 18b1495). Sibling to U1.4 (P1 gates
                + retro, still todo -- independent, does not block or
                get blocked by U1.4).
Date:     2026-07-20  (SKELETON -- not executed. Execute on explicit
                request -- operator directive 2026-07-20: create the
                step text file FIRST, operator reviews it, THEN
                explicitly requests execution; a deliberate sequencing
                change from step.7's "drafted and executed in the same
                pass.")
Origin:   Direct continuation of step.7's own reported gap (step.7.txt
                §1, §2.5, §4 C-7-C, §5 Q-7-C): `--provider human` was
                deliberately left out of `app/cli.py` because
                `propose_ask()` hard-raises `RuntimeError` when no
                `ResponseKind.TEXT` response exists, and
                `HumanRelayAdapter.run()` yields exactly one `DONE`
                response by design (Q-7-B, already resolved: that
                shape is correct and should not be bent to fake a TEXT
                chunk that doesn't exist in a human paste-back).

## (0) Status

SKELETON ONLY. No code, test, or doc changes beyond this file and the
end_to_end.md ledger/§3 bookkeeping below. Per operator directive
2026-07-20: this step is not executed until explicitly requested after
review of this file.

## (1) Ground truth probed at drafting

    READ    app/cli.py L31-L41 (`_make_adapter`): raises `ValueError`
            for any provider name not in `{mock, claude-code}` --
            `human` is unreachable today, confirming step.7 G3's
            grep-verified claim still holds.
    READ    app/cli.py L71-L113 (`propose_ask`): L92 filters
            `responses` down to `ResponseKind.TEXT` only; L93-94
            hard-raises `RuntimeError(f"{provider}: no TEXT response
            produced for this request")` when that list is empty; L95
            takes `text_responses[0].text`; L96 takes
            `text_responses[0].provider_meta.get("run_id")`. A
            `HumanRelayAdapter` run today produces zero TEXT-kind
            responses (by design, Q-7-B) -- `propose_ask(...,
            provider="human")` would raise 100% of the time if the
            adapter were merely registered without this change.
    READ    app/cli.py L126-L177 (`run_ask` / argparse wiring): L161
            `ask.add_argument("--provider", choices=["mock",
            "claude-code"], default="mock")` -- confirms the CLI-level
            gate step.7 deliberately did not cross.
    READ    app/pal/envelope.py (`Response`/`ResponseKind` docstrings,
            re-read from step.7): DONE is documented as "no new
            content" -- `propose_ask()`'s TEXT-only filter is a
            faithful reading of that contract as it stands for
            mock/claude-code, NOT a bug; widening it to also accept a
            content-bearing terminal DONE is a genuine, considered
            contract change (step.7 Q-7-B/Q-7-C), not a bugfix.
    READ    steps/step.7.txt §1, §2.5, §4 (C-7-C), §5 (Q-7-B, Q-7-C) --
            the full prior reasoning this step continues from; not
            re-litigated here except where this step's own findings
            add to it.
    CONFIRMED (grep) no other call site depends on
            `propose_ask()`'s TEXT-only assumption besides `run_ask()`
            (app/cli.py L139) and the two `test_cli.py` cases that
            exercise `propose_ask` directly against `mock`.

## (2) What U1.6 delivers (PROPOSED -- confirm at execution time)

  2.1 **The reconciliation itself -- THE decision this step exists to
      make** (see Q-8-A): two live options, both fully worked out in
      step.7's own §5, neither implemented yet:

      OPTION A -- generalize `propose_ask()` (LEANING, carried
      forward from step.7 Q-7-B/Q-7-C's own lean): accept EITHER a
      distinct `TEXT`-kind response (mock/claude-code's existing
      shape) OR a content-bearing terminal `DONE` response (human's
      shape) as "the proposed text," mirroring EXACTLY the extension
      already made to `test_pal_contract.py::test_text_response` in
      step.7 §2.3 -- so the CLI's acceptance rule would match the
      contract suite's own already-widened rule instead of lagging
      behind it. Touches `propose_ask()` only (L92-96); does not
      change any adapter.

      OPTION B -- bend `HumanRelayAdapter` to emit a TEXT response
      followed by an empty DONE, mirroring mock/claude-code's shape
      exactly. REJECTED going into this step (Q-7-B already resolved
      against it: "a fabricated TEXT-then-DONE pair would be
      dishonest content duplication" -- a human paste-back has no
      real intermediate chunk to report). Listed here only so the
      operator's review has both options in front of them, not
      because this step plans to implement it.

      This step's plan is OPTION A. Flagged as the step's single open
      design question (Q-8-A) rather than silently assumed, because
      step.7 Q-7-C was explicit that "whoever picks up this should
      decide propose_ask()'s generalization deliberately, not as a
      side effect."

  2.2 **`app/cli.py` changes** (if Option A confirmed):
      - `propose_ask()`: replace the TEXT-only filter with one that
        also accepts a terminal DONE carrying non-empty `text` --
        exact matching logic mirrors `test_pal_contract.py`'s step.7
        extension (read it first, do not reinvent the predicate).
      - `_make_adapter()`: add `"human"` to the known-provider map.
      - `ask.add_argument("--provider", choices=[...])`: add
        `"human"`.
      - Behavior when `--provider human` is used non-interactively
        (e.g. stdin is not a TTY, or CI runs it by accident) is an
        open sub-question -- see Q-8-B. NOT assumed to be "just works
        because `input()` blocks forever"; that is a real footgun for
        an unattended `ask` invocation and needs an explicit answer,
        not a silent default.

  2.3 **Tests** (extend, mirror step.7's own discipline of extending
      rather than rewriting):
      - `tests/test_cli.py`: a new case exercising `propose_ask(...,
        provider="human")` with an injected `HumanRelayAdapter` (fake
        I/O, C-7-B's discipline carried forward) confirming the
        proposed diff is computed correctly from the DONE-carried
        text and that `run_id`/egress-trail fields populate exactly
        as they do for mock/claude-code.
      - `tests/test_pal_contract.py`: no change expected (step.7
        already widened `test_text_response`); if this step's
        `propose_ask()` change reveals the contract-suite extension
        was itself incomplete, that is a NEW finding to report
        in-file (G4-style), not a silent second widening.
  2.4 **Documentation** (same commit as the code, once executed):
      README.md's "Getting started" gains a working `--provider human`
      example (step.7 explicitly left this out, §2.5); design.md §5
      d13 gets a short addendum noting the CLI is now wired, with a
      pointer to this step; end_to_end.md §3 U1.6 bullet and §11
      ledger row move from `active`/no-commit to `done`/step.8/commit
      hash.
  2.5 **Closure**: end_to_end.md §11 U1.6 -> done; retro pair
      (step.8.diff.txt narrative + step.8.gdiff.txt via
      scripts/step_gdiff) written at execution time, not now.

EXPLICITLY OUT OF SCOPE:
  - Any change to `HumanRelayAdapter` itself (Option B, rejected).    -> never,
                                                                          per Q-7-B
  - Widening `propose_ask()`'s acceptance rule beyond "TEXT or
    content-bearing terminal DONE" (e.g. accepting TOOL_CALLS as a
    proposal source) -- a different, unrelated question.              -> later,
                                                                           if ever
  - Any real terminal/TTY interaction in tests -- injected I/O only,
    carried forward from step.7's C-7-B.                              -> never
    (by design)
  - Resolving Q-8-B's non-interactive-footgun question by fiat in
    this skeleton -- it is recorded as open and answered at execution
    time, not guessed at now.                                         -> execution
                                                                           time

## (3) Verification gates (PLANNED -- not yet run)

    G1  DONE-WHEN: `python -m app.cli ask <ws> <file> "<intent>"
        --provider human` no longer raises `RuntimeError` for a
        well-formed injected paste-back, and DOES raise a clear,
        distinct error (not the generic TEXT-response one) for an
        empty paste-back -- so the reconciliation doesn't silently
        accept empty content either.
    G2  NO PROVIDER NAME OUTSIDE adapters/ (U1.1's G1 precedent,
        re-run): `_make_adapter()`'s provider-name literals in
        app/cli.py are the documented, pre-existing exception to this
        rule (mock/claude-code already appear there) -- adding
        "human" to the same list is consistent with existing practice,
        not a new violation. Re-grep to confirm no OTHER file gained a
        provider-name literal.
    G3  NO REGRESSION: step.7's 52 passed / 11 skipped baseline stays
        green; the new `test_cli.py` case is additive.
    G4  THE DECISION IS RECORDED, NOT IMPLICIT: whichever way Q-8-A
        and Q-8-B resolve (confirmed by the operator before or at
        execution), this file and design.md d13's addendum name the
        decision and the reason explicitly -- matching step.7's own
        G3/G4 discipline.
    G5  Hygiene: b3ubot porcelain clean after each commit; retro via
        step_gdiff.

## (4) LOCKs

    C-8-A  b3ubot porcelain clean after each commit.
    C-8-B  INJECTED I/O ONLY IN TESTS (carried forward from C-7-B):
           the new `test_cli.py` case never relies on real `input()`/
           `print()`.
    C-8-C  NO SILENT SCOPE CREEP: this step touches `propose_ask()`'s
           acceptance rule and the CLI's provider list ONLY -- not
           `HumanRelayAdapter`, not `test_pal_contract.py`'s already-
           settled step.7 extension, unless a genuinely new finding
           forces it (G4).
    C-8-D  standing locks inherited: B-1/B-3/B-4/B-5/B-6, same posture
           as step.7 §4 C-7-E (no CCS surface, no disclosure/push, no
           credential, egress-policy gate still P7's job, T0/T1 suite
           only).

## (5) Open questions -- BOTH must be confirmed before execution

    Q-8-A  Reconciliation approach: Option A, generalize
           `propose_ask()` to accept a content-bearing terminal DONE
           (LEANING -- carries forward step.7 Q-7-B/Q-7-C's own lean;
           touches one function, mirrors the contract suite's already-
           settled shape) vs Option B, bend the adapter to fake a
           TEXT-then-DONE pair (REJECTED going in, per Q-7-B's own
           reasoning -- listed for completeness, not as a live
           choice). Operator confirms Option A (or overrides) before
           execution.
    Q-8-B  Non-interactive footgun: what should `ask --provider human`
           do if stdin is not attached to a real TTY (e.g. invoked
           from a script, CI, or by accident)? Candidates: (i) let it
           block on `input()` forever -- honest but a real hang
           hazard; (ii) detect a non-TTY stdin up front and refuse
           loudly with a named next action (the b3u launcher's
           "refuse with a named next action, no --force" posture,
           already this repo's house style per design.md §2.7); (iii)
           add a `--timeout` knob. LEANING (ii) -- refuse loudly on a
           detected non-TTY, no new flag needed -- but genuinely open;
           no default is assumed by this skeleton.

## (6) Acceptance (CLOSED -- executed 2026-07-20)

    - [x] Q-8-A and Q-8-B confirmed by the operator (explicitly, before
          or at the start of execution).
    - [x] app/cli.py: `propose_ask()` reconciliation, `_make_adapter()`
          + `--provider` choices gain `"human"` (2.2).
    - [x] tests/test_cli.py: new `provider="human"` case, injected I/O
          only (2.3, C-8-B) -- landed as 3 cases (well-formed
          paste-back, empty-paste-back distinct error, Q-8-B non-TTY
          refusal); required a new `adapter=` injection seam on
          `propose_ask()` (genuine finding, see step.8.diff.txt #1).
    - [x] README.md, design.md §5 d13 addendum (2.4).
    - [x] end_to_end.md §3 U1.6 bullet + §11 ledger row -> done, commit
          hash filled in (2.4).
    - [x] Full suite green, additive only, zero regressions (G3): 52p/
          11s -> 55p/11s.
    - [x] Closure: retro pair via step_gdiff (2.5, G5).

## (7) Hash backfill

    SKELETON commit (this file + end_to_end.md §3/§11, U1.6 -> active):
        9216667
    BUILD commit (app/cli.py + tests/test_cli.py + README/design.md/
        end_to_end.md, all gates green):                    8c58f8d
    RETRO commit (retro pair + acceptance/hashes backfilled +
        end_to_end.md §11 Commit cell filled in):            (HEAD)
    GDIFF commit (step.8.gdiff.txt via scripts/step_gdiff):  (not yet)

End of skeleton.
