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 "" --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.