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.