step.2.diff.txt -- narrative retro for step.2 (U1.1: envelope + adapter contract + mock provider) Executed: 2026-07-18, same day as the skeleton. Companion artifact: step.2.gdiff.txt (automated, scripts/step_gdiff). ## Plan vs actual Skeleton followed closely; every open question (§5) taken exactly as LEANed, no reversals. Three real findings during execution, all caught by the step's OWN gates rather than missed -- the gates did their job. ``` +---+----------------------------------+------------------------------------------+ | # | Planned (§) | Actual | +---+----------------------------------+------------------------------------------+ | 1 | §2.4/G1: "no provider name | G1's own grep caught a REAL hit: the | | | appears outside adapters/" | envelope.py Response docstring used | | | | {"provider": "mock", ...} as an | | | | ILLUSTRATIVE example. Fixed by replacing | | | | it with a generic | | | | placeholder rather than rationalizing | | | | the hit away as "just a comment, doesn't | | | | count" -- a lexical gate that excuses | | | | its own hits stops meaning anything. | | 2 | §2.4: "tests/pal_contract.py" | Renamed to tests/test_pal_contract.py | | | (the skeleton's own filename) | at write time -- pytest's default | | | | `python_files` pattern is `test_*.py`; | | | | the skeleton's literal name would have | | | | needed new pytest config to be | | | | discovered at all, directly violating | | | | G7's "zero new configuration" bar. Fixed | | | | in the file itself, noted here rather | | | | than silently diverging from the | | | | skeleton's prose. | | 3 | §4.2 (from step.1): "adapters/ | Confirmed rather than assumed: ran the | | | mock/adapter.py replacing | FULL step.1 smoke.sh unmodified after | | | hello.py's role" | this step's changes landed -- hello.py | | | | untouched, still imported by smoke.sh, | | | | still SMOKE PASS. "Role" replaced, | | | | file/regression untouched, exactly as | | | | the skeleton's §2.3 note intended. | +---+----------------------------------+------------------------------------------+ ``` ## Gate evidence ``` +------+---------------------------------------------------------------------+ | G1 | `grep -rn -e mock -e claude -e openai app/pal/` -> ONE hit found | | | (finding #1 above), fixed, re-grepped: empty. pytest: 8/8 green. | | G2 | Live dry-run: registered a throwaway `("mock2", MockAdapter())` | | | entry -> 8 tests became 16 with ZERO test-body edits (the SAME 8 | | | test functions just re-ran against the second instance via the | | | existing parametrize). Reverted before commit -- the registration- | | | only reuse mechanism (Q-2-D) is proven, not asserted. | | G3 | grep for os.environ/socket/urllib/requests/httpx in the mock | | | adapter + fixtures: one hit, but it was "requests" inside the | | | English phrase "across requests" in a docstring -- confirmed a | | | false positive by reading the actual import list (dataclasses, | | | typing, app.pal.*, adapters.mock.fixtures -- nothing else). | | G4 | 8 named tests, each asserting one fixture scenario individually: | | | capabilities shape, text response (+usage+run_id), tool_calls | | | response, a 2-part TEXT stream, an unrecognized-request KeyError, | | | abort-unknown=IDLE, abort-running=ABORTED (+idempotent second | | | call), abort-finished=DONE (no-op). | | G5 | adapters/mock/adapter.py's import list: dataclasses.replace, | | | typing.Iterator, app.pal.adapter, app.pal.envelope, | | | adapters.mock.fixtures -- nothing outside app.pal + the same | | | adapters/mock/ subtree + stdlib. | | G6 | grep "INERT" app/pal/envelope.py -> 4 hits, one per inert field | | | (tool_schema, budget, policy) plus the class-level summary line. | | G7 | rm -rf .venv work && make venv && make smoke && make test replayed | | | clean end to end; b3ubot porcelain before this commit; .venv/ + | | | work/ stayed untracked throughout. | +------+---------------------------------------------------------------------+ ``` ## Findings for later steps 1. **A lexical grep gate will occasionally false-positive on English prose, and that's an acceptable cost.** "requests" (the docstring word) collided with "requests" (the library name) in G3's check. The fix is READING the hit before dismissing it, not loosening the pattern -- a looser pattern (e.g. requiring `import requests` as the literal grep target instead of the bare word) would have missed less-common import styles (`from requests import ...`, `importlib.import_module("requests")`). Keep the cheap, slightly noisy version; read every hit. 2. **G1's own catch is the more important lesson**: even a skeleton author being careful about a rule can accidentally violate it while writing an ILLUSTRATIVE comment, precisely because comments don't feel like "real code" to the person writing them. The gate existing as an actual command (not just a design-doc sentence) is what caught it -- this is the concrete payoff of step.1's own precedent (build the structural regression guard AS a grep from day one, not as prose to remember). 3. **abort() semantics (Q-2-E) held up under adversarial testing.** The "don't let a post-abort exhaustion clobber ABORTED back to DONE" guard (adapters/mock/adapter.py's `if self._status.get(run_id) == RunStatus.RUNNING` check before setting DONE) was written proactively, not discovered by a failing test -- worth flagging for P3: when the real orchestrator defines abort() calling conventions, this exact race (caller keeps consuming a generator after aborting it) is a real shape to test against, not a hypothetical. 4. **The contract suite's routing rule (fixtures.py: first context entry's `content` string selects the scenario) is deliberately dumb.** U1.2's claude-code adapter will need this same test suite to drive REAL prompts through a REAL provider -- the fixture strings ("say hello", "call a tool", "long run") were chosen specifically to also work as plausible real prompts, so the SAME test bodies can plausibly pass against both the mock and a live adapter without rewriting the scenario content, only what backs it. ## Numbers ``` +--------------------------------------+----------------+ | pinned deps added this step | 0 (pytest only,| | | unchanged) | | new files | 7 | | contract tests | 8 | | adapters registered | 1 (mock) | | provider-name leaks found + fixed | 1 | | BUILD commit 4cbcad0 | 7 files, +350 | +--------------------------------------+----------------+ ``` U1.1 done -> unblocks U1.2 (the first REAL adapter, claude-code) and U1.3 (the one-round-trip CLI). Ledger updated in this RETRO commit per the §0 execution contract. End of retro.