step.7.diff.txt -- narrative retro for step.7 (U1.5: human-relay
adapter)

Executed: 2026-07-20, same session as the skeleton (no separate
skeleton-then-execute split this time -- code, tests, docs, and this
step file were built together per the operator's explicit direction).
Companion artifact: step.7.gdiff.txt (automated, scripts/step_gdiff).
This is the first step in either repo adding a THIRD PAL adapter, and
the first one that needs no AI-vendor integration, no key, no
subprocess, no network at all -- the whole "adapter" is a formatted
string and a blocking read.

## Plan vs actual

```
+---+----------------------------------+------------------------------------------+
| # | Planned (§)                      | Actual                                   |
+---+----------------------------------+------------------------------------------+
| 1 | §2.1: HumanRelayAdapter.run()    | Held exactly as spec'd -- the ONE real   |
|   | yields exactly one DONE Response | design choice that turned out to be      |
|   | wrapping the pasted-back text    | load-bearing beyond the adapter itself.  |
|   |                                  | Found live while reading envelope.py's   |
|   |                                  | own Response docstring ("DONE -- the run |
|   |                                  | has concluded, no new content") and      |
|   |                                  | app/cli.py's propose_ask() (hard-        |
|   |                                  | requires a TEXT-kind response): this     |
|   |                                  | adapter's honest single-event shape is   |
|   |                                  | narrower than what two existing          |
|   |                                  | consumers assume. Resolved by extending  |
|   |                                  | tests/test_pal_contract.py's             |
|   |                                  | test_text_response (accept a content-    |
|   |                                  | bearing terminal DONE as an alternative  |
|   |                                  | to a distinct TEXT response, both        |
|   |                                  | branches asserting real content) and by  |
|   |                                  | deliberately NOT wiring --provider human |
|   |                                  | into app/cli.py's ask subcommand --      |
|   |                                  | wiring it in without also generalizing   |
|   |                                  | propose_ask() would make every           |
|   |                                  | invocation fail loudly, which is worse   |
|   |                                  | than the flag simply not existing yet.   |
|   |                                  | See §1/§5 Q-7-B/Q-7-C of step.7.txt.     |
| 2 | §2.5: README mentions the human  | Done as a CODE EXAMPLE against the PAL   |
|   | adapter "in Getting started      | directly (HumanRelayAdapter() + .run()), |
|   | and/or The local trinity"        | not a --provider human CLI example --    |
|   |                                  | the CLI flag genuinely does not exist    |
|   |                                  | (finding #1), so a CLI usage example     |
|   |                                  | would have been fiction. Both sections   |
|   |                                  | got a mention, as planned; the SHAPE of  |
|   |                                  | the Getting-started mention changed from |
|   |                                  | "a --provider human example alongside    |
|   |                                  | mock/claude-code" to "a short PAL-level  |
|   |                                  | snippet, explicitly labeled as not CLI-  |
|   |                                  | wired, with the reason stated inline."   |
+---+----------------------------------+------------------------------------------+
```

Every §5 open question resolved exactly as LEANed: Q-7-A (plain
injectable callables, matching stdlib `print`/`input` signatures),
Q-7-B (exactly one DONE response, per the operator's spec, handled by
honest contract extension rather than adapter-shape bending), Q-7-C
(no CLI wiring this step, gap reported in three places: step.7.txt,
design.md d13, this file).

## Gate evidence

```
+------+---------------------------------------------------------------------+
| G1   | pytest -v: the `human` param runs all 7 shared contract test        |
|      | functions -- 4 pass (capabilities shape, text response via the      |
|      | terminal-DONE branch, abort-unknown-is-idle, abort-finished-is-     |
|      | noop), 3 honestly skip (tool_calls/multi_part_stream on             |
|      | capabilities()["tools"/"streaming"]=False; abort_running on         |
|      | SUPPORTS_INTERLEAVED_ABORT=False) -- zero real interactivity        |
|      | anywhere (grep-checked: no bare `input`/`print` call site in the    |
|      | registration or in tests/test_human_adapter.py).                    |
| G2   | grep -rn -e mock -e claude -e human -e openai app/pal/ -> empty      |
|      | (this step touches no file under app/pal/ at all -- exit 1,         |
|      | confirmed live).                                                     |
| G3   | grep -n "provider.*human\|human.*provider" app/cli.py -> empty;      |
|      | `git diff --stat app/cli.py` against this step's start -> empty      |
|      | (the file is byte-identical, confirmed live). The gap is recorded   |
|      | in step.7.txt §1/§5, design.md §5 d13, and this file.                |
| G4   | tests/test_pal_contract.py's top docstring gained a third,           |
|      | numbered finding (matching items 1/2's existing style) explaining   |
|      | WHY test_text_response now branches on whether a TEXT-kind          |
|      | response exists.                                                     |
| G5   | Full suite: 52 passed, 11 skipped -- up from the pre-step baseline   |
|      | of 38 passed / 8 skipped, confirmed by running the suite before      |
|      | any edit and again after. Delta is exactly +7 contract-suite         |
|      | instances (4 pass + 3 skip, the human param) + 10 new dedicated      |
|      | tests, zero regressions in the existing 46. make smoke unaffected    |
|      | (SMOKE PASS, unchanged output).                                      |
| G6   | b3ubot porcelain clean after the work commit (confirmed via git      |
|      | status before committing); retro pair via step_gdiff.                |
+------+---------------------------------------------------------------------+
```

## Findings for later steps

1. **A "third adapter" is a genuinely different stress test than a
   "second real adapter."** step.3's own finding #2 observed that a
   contract suite built against exactly one implementation can only
   prove itself mock-agnostic, not adapter-agnostic. This step shows
   the corollary: even a suite exercised against TWO real, differently
   -shaped implementations (fixture-scripted, subprocess-JSON) can
   still encode an assumption that happens to hold for both of them
   by coincidence -- here, "there is always a distinct TEXT response
   before DONE" held for mock (scripted) and claude-code (JSON-blob-
   then-terminator) for unrelated reasons, and only broke once a
   THIRD, structurally different execution model (single synchronous
   event) joined. Worth remembering for U6.1's second REAL automated
   adapter and beyond: expect each new adapter, real or not, to have
   a nonzero chance of surfacing one more test that was universal-by-
   coincidence, not universal-by-design.
2. **A design requirement that is correct for the adapter can still be
   incompatible with an existing CONSUMER of that adapter.** The
   "exactly one DONE response" shape is not a bug -- it is the honest
   representation of what a human paste-back actually is. But
   `propose_ask()` was written against exactly two adapters that both
   happen to produce a TEXT-then-DONE shape, and silently assumed that
   was universal. The fix here was NOT to bend the new adapter to fit
   the old consumer (that would have meant fabricating a TEXT response
   with no real distinct content behind it) -- it was to leave the
   mismatch visible: no CLI wiring, a documented reason in three
   places. Generalizing `propose_ask()` to accept content-bearing DONE
   responses is real, scoped, follow-up work, not a quiet side effect
   of this step.
3. **Injectable I/O is a reusable offline-testability pattern beyond
   "network vs mock fixtures."** U1.1's mock adapter proved the
   pattern for AVOIDING a network call (scripted fixtures instead of a
   real API). This step proves the same discipline generalizes to
   AVOIDING a blocking terminal read/write (injected functions instead
   of real `input`/`print`) -- the same "testable offline, zero cost"
   property, achieved by substituting a different kind of real-world
   dependency. Worth applying the same lens to any future adapter that
   blocks on something other than a network socket.

## Numbers

```
+---------------------------------------+----------------------+
| new adapters registered                | 1 (human, total now  |
|                                         | 3: mock/claude-code/ |
|                                         | human)               |
| new files                              | 4 (adapters/human/   |
|                                         | __init__.py+adapter. |
|                                         | py, tests/test_human |
|                                         | _adapter.py,         |
|                                         | steps/step.7.txt)    |
| new tests                              | 17 (10 adapter-      |
|                                         | internal + 7 contract|
|                                         | -suite instances: 4  |
|                                         | pass, 3 honest skip) |
| files edited                           | 5 (test_pal_contract |
|                                         | .py, design.md,      |
|                                         | end_to_end.md,       |
|                                         | README.md, this      |
|                                         | file's own step.7.txt|
|                                         | backfill)            |
| files deliberately NOT touched         | 1 (app/cli.py -- G3) |
| suite before -> after                  | 38p/8s -> 52p/11s    |
| regressions                            | 0                    |
+---------------------------------------+----------------------+
```

U1.5 done -- additive to P1.M1, does not block or get blocked by U1.4
(P1 gates + retro, still todo). Ledger updated in the work commit per
the §12 execution contract; commit hash of that same commit backfilled
into both end_to_end.md §11 and this file's own §7 in the immediately
following gdiff commit (mechanically necessary: `scripts/step_gdiff`
requires steps/step.7.txt to already be committed before it can
compute a range against it, so the hash could not be known at the
moment the work commit's own tree was written).

End of retro.
