step.17.txt -- b3ubot U3.4: Ratify gate + amendment loop
               (human-in-the-loop, the load-bearing gate). design.md
               §4's SKELETON --human ratify--> RATIFIED arrow -- the
               FIRST of the workflow's two load-bearing human gates
               ("a skeleton never executes unratified -- LEANs are
               presented for override exactly as the b3u steps present
               them") -- as code, plus the destructive-op approval
               mechanism U3.2's refuse-stubs named as their next
               action. Stops here -- does NOT continue into U3.5 or
               any later P3 UoW.

Step:     17
Parent:   end_to_end.md §5 U3.4 bullet verbatim ("Ratify gate +
          amendment loop (human-in-the-loop, the load-bearing
          gate)"). design.md §4 (the ratify arrow, the amendments
          cycle, the (discard) branch, and the closing "Human gates
          are load-bearing and few" paragraph) is what this step
          implements directly; §2.3's ledger-ops list names
          `ratify(skeleton, amendments) -> active UoW (human gate)`
          as the second op; §2.7's "Destructive operations ...
          require explicit human approval" is the approval
          mechanism's governing sentence. app/orchestrator/ (U3.1/
          U3.3) is the package this grows; app/tools/ (U3.2) is where
          the approval mechanism lands; app/ledger/parser.py+
          emitter.py (U2.1/U2.2) are the load/present/stamp
          machinery.
UoW:      U3.4
Date:     2026-07-21
Origin:   Operator directive 2026-07-21 (same dispatch shape as
          step.14/15/16's): draft AND execute steps/step.17.txt for
          U3.4 in a single pass (the step.7/9/10/11/14/15/16
          convention, NOT a skeleton-split), with the SAME explicit
          scope boundary -- stop after U3.4, do not continue into
          U3.5 or any later UoW; the operator is directing P3 one UoW
          at a time with strict per-UoW checkpoints (a prior incident
          of ambiguous scope expansion makes this boundary HARD).

## (0) Status

EXECUTED in this pass. Not a skeleton-then-later-execute split --
app/orchestrator/ratify.py, the RatifyError addition, the
app/tools/approval.py mechanism with delete_file's and
git_delete_branch's real approved paths, the `b3ubot ratify` CLI
subcommand, tests/test_orchestrator_ratify.py, the approval tests
grown into tests/test_tools{,_git}.py, the design.md / end_to_end.md /
docs/tool_surface.md edits, and this step file were all built
together, per the operator's explicit "draft AND execute (single
pass)" direction. Per the operator's explicit scope boundary (Origin,
above), this session STOPS after U3.4 -- U3.5 (EXECUTING loop /
scheduler) is the next UoW but is NOT started here: a RATIFIED
skeleton just sits there; nothing executes it.

## (1) Ground truth probed at drafting (outputs -> named writers)

    READ    design.md §4 in full. The diagram's amendments arrow is
            RATIFIED -> SKELETON (post-ratification), NOT a SKELETON
            self-loop -- confirmed against state_machine.py's own
            docstring, which traced the ASCII arrow ends explicitly
            in U3.1. The closing paragraph names ratify as the first
            of exactly two load-bearing human gates and fixes the
            LEAN-presentation contract this step implements.
    READ    app/orchestrator/state_machine.py (U3.1): the ACTUAL 8
            states / 9 edges. The three edges this gate needs all
            already exist -- ("SKELETON","RATIFIED") whose rationale
            string names U3.4 by name, ("SKELETON","DISCARDED"), and
            ("RATIFIED","SKELETON") for the post-ratification
            amendment. CHECKED FINDING (2.2): a pre-ratification
            decline needs NO edge at all -- the machine SUFFICED, no
            state-machine change was made, and no self-loop was
            invented.
    READ    app/orchestrator/persistence.py + drafting.py: the
            engine record's `step` field is set on the INTENT ->
            SKELETON move (drafting.py's advance_state call), so the
            ratify gate loads the skeleton FROM THE RECORD -- the
            same record advances SKELETON -> RATIFIED/DISCARDED; no
            second bookkeeping location. errors.py's house style
            (named subclasses, message names defect + fix) is what
            RatifyError follows.
    READ    this repo's own steps/step.8.txt §(5) (the richest real
            LEAN example -- Q-8-A/Q-8-B, LEANING markers, section
            title SUFFIXED "-- BOTH must be confirmed before
            execution") and adapters/mock/fixtures.py's _skeleton()
            §(5) (what U3.3 actually drafts). Both shapes drove the
            extraction design AND its tests -- step.8.txt itself is a
            test fixture (title-prefix match + Q-marker split proven
            against a real corpus file, not only synthetic text).
    READ    app/ledger/parser.py: section titles past §(0) are
            deliberately NOT schema-rigid (U2.1's corpus survey), so
            the open-questions section is found by title PREFIX;
            Section.raw/body must be edited coherently for the
            emitter (the step.16 _stamp_probe precedent, reused for
            the ratification stamp with a post-stamp re-parse guard).
    READ    app/tools/workspace.py + gitops.py + errors.py + trail.py
            (U3.2): the four refuse-stubs and their exact messages
            ("wait for U3.4"); logged_call's refused/error/ok
            classification; the two stub-message-pinning tests
            (test_tools.py match="U3.4", test_tools_git.py
            match="U3.4"/"no---force"/"human approval") that would --
            correctly -- break when the messages change, and were
            updated honestly (2.6).
    READ    app/cli.py in full: the ask/draft subcommand structure the
            new `ratify` sits beside, the Q-8-B non-TTY refusal
            pattern, `--yes` on ask as the honest-bypass precedent,
            and draft's "U3.4 gate -- not built yet" print (now
            updated to point at the real command).
    READ    tests/conftest.py: hermetic per-test $B3UBOT_HOME --
            engine-state/trail writes in this step's tests sandboxed
            for free; its C-7-B note said "the interactive approval
            flow is U3.4's" and was updated to the delivered state.
    CONFIRMED `git log --oneline -30` for the 2-commit phrasing
            (step.16's precedent, 3f8d0ba/f74b817).
    CONFIRMED baseline: full suite 402 passed / 11 skipped before any
            edit (matches step.16's reported final).
    CONFIRMED live smoke before committing: mock draft into a
            scratch workspace, then `b3ubot ratify <ws> UT.1 --yes`
            -> summary + LEAN presented, "UT.1: SKELETON -> RATIFIED",
            ENGINE RATIFICATION RECORD in §(0) on disk, engine record
            history [INTENT, SKELETON, RATIFIED].

## (2) What U3.4 delivers

  2.1 **app/orchestrator/ratify.py** (new; the package's per-concern
      peer of drafting.py): `ratify_skeleton(workspace_root, uow, *,
      output_fn, input_fn, assume_yes=, home=)` -- the full gate arc:
      state legality FIRST (validate_transition(state, "RATIFIED")
      against the persisted record BEFORE anything is read or
      presented -- step.16 retro's carried lesson; an illegal ratify
      costs zero reads and leaves zero presentation), skeleton loaded
      from the record's own `step` field via the trail-recorded tool
      surface and parsed by app/ledger/parser.py, summary + LEANs
      presented, ONE decision collected: ratify / amend / discard.
      Plus extract_leans(), present_skeleton(), and the Lean/
      LeanExtraction/RatifyResult dataclasses; RatifyError in
      errors.py; all re-exported through app/orchestrator/__init__.
  2.2 **The amendment loop's V0 state shape (decision, justified,
      CHECKED against the real machine):** a pre-ratification decline
      causes NO state transition -- the skeleton STAYS in SKELETON
      while its file is amended (hand-edited) and ratify is re-run.
      Worked out against state_machine.py's actual 9 edges: no
      SKELETON self-loop exists and none is needed; design §4's
      SKELETON <-> amendments cycle before ratification is FILE-level
      (the text changes, the state does not -- exactly this repo's
      own step.8 history). The diagram's RATIFIED -> SKELETON edge is
      the POST-ratification variant; it exists in the machine and
      stays undriven in V0 (drivable work for U3.5, when RATIFIED
      dwell time becomes real). Existing edges SUFFICED -- U3.4
      needed no state-machine change: a checked finding, not an
      assumption.
  2.3 **Where amendments are recorded (decision, justified):**
      `steps/<step>.amendments.txt` BESIDE the skeleton -- append-
      only, timestamped entries, read+written through the trail-
      recorded tool surface (C-15-B: the decline outcome is
      auditable). Beside the skeleton and NOT in the engine-state
      record because the notes' consumer is the NEXT DRAFTER (human
      hand-edit or a future PAL re-draft): workspace content that
      must live where the skeleton lives; the engine-state record is
      engine-private, per-machine, and schema'd as a state history --
      prose does not belong in it. An empty-notes decline refuses
      (RatifyError): a decline must say what to amend; abandoning is
      discard's job.
  2.4 **LEAN presentation + per-LEAN confirm-or-override (decision,
      justified):** extract_leans() finds the first section whose
      title STARTS WITH "Open questions" (the corpus suffixes titles
      -- step.8.txt's own is suffixed; U2.1 deliberately left title
      vocabulary non-rigid) and splits its body on lines whose first
      token is a Q-marker (`Q-8-A` style). That token split is the
      WHOLE parser -- each question's block (LEAN lines included) is
      presented verbatim; NO LEANING/LEAN prose sub-parsing (the
      fragile micro-parser the dispatch warned against). Honest
      fallbacks: a section with no recognizable markers = ONE unsplit
      block judged once; no section at all = an explicit "no
      open-questions section -- no LEANs to present" line, and
      ratification proceeds on the top-level decision alone. Under
      ratify, each LEAN is confirmed or overridden ONE BY ONE ("
      presented for override"); an override note is REQUIRED and --
      the load-bearing call -- ANY override turns the decision into
      an amendment (2.3's file, kind "LEAN OVERRIDES"): a skeleton is
      ratified AS ITS TEXT STANDS, and text that no longer says what
      the human decided goes back for amendment instead of being
      ratified over.
  2.5 **The ratification stamp (the step.16 engine-stamp pattern,
      reused):** on ratify, the engine appends its ENGINE RATIFICATION
      RECORD into §(0) -- timestamp + the per-LEAN outcome lines
      (mechanical facts, engine-written, never prose-trusted) --
      editing Section.raw AND .body coherently, re-parsing as the
      guard (failure named as a ratify.py bug), writing via
      tools.write_file, THEN advance_state(uow, "RATIFIED"). Discard
      is double-confirmed (DISCARDED is terminal) and leaves the
      skeleton file untouched on disk -- a discarded draft is
      history, not garbage; an unconfirmed discard aborts with
      nothing changed.
  2.6 **The destructive-op approval mechanism (app/tools/approval.py;
      what got a REAL approved path vs what still refuses, exactly):**
      ApprovalCallback = injectable `(operation, consequence) ->
      bool`; interactive_approval(output_fn=print, input_fn=input) is
      the real human-facing factory (EOF = decline; destructive
      default NO). `delete_file` and `git_delete_branch` gained REAL
      approved paths: approve=None (every pre-U3.4 call site,
      signature-compatible) refuses exactly as U3.2's stubs did with
      the next action now naming the mechanism instead of "wait for
      U3.4"; a decline refuses with the human-DECLINED message (a
      DIFFERENT audit fact from "nobody was asked"); a grant actually
      executes (unlink / `git branch -D`), with the trail entry's
      args carrying "approved": true -- approved-and-executed is
      distinguishable on the trail by inspection. Scope checks run
      BEFORE the callback (C-4-B: approval never widens scope; proven
      by a spy test). `git_push` and `git_rewrite_history` STILL
      refuse unconditionally and take NO callback parameter at all
      (no fake flows -- proven at the signature level by a test),
      each for its own narrower named reason: push needs a
      remote/refspec POLICY (which remote is legitimate is an egress
      decision -- B-5-adjacent -- nobody has designed; approval alone
      cannot substitute, and a bare-repo fixture would only prove
      plumbing, not policy); rewrite names a FAMILY with no argument
      saying which rewrite of what range (nothing executable to
      approve) and design §2.6's "never rewrites history it did not
      create" is unenforceable without a commit-provenance record
      (Track B).
  2.7 **CLI: top-level `b3ubot ratify`** (draft's sibling -- the gate
      is an orchestrator op with engine-state effects): presents via
      print, collects via input -- the ONLY place real I/O enters;
      ratify_skeleton itself has NO default I/O (output_fn/input_fn
      are required parameters -- stricter than propose_ask's optional
      adapter seam, because this function's whole point is
      interaction). Q-8-B non-TTY refusal UNLESS `--yes`: --yes means
      "ratify as-is, every LEAN confirmed as leaned" -- a decision
      expressible without input (never calls input_fn under
      assume_yes, proven by a must-not-be-called test), so an
      unattended --yes run is honest, the `ask --yes` rule; amend and
      discard have NO bypass (their content needs actual typing).
      draft's stale "U3.4 gate -- not built yet" print now points at
      the real command. Presentation-layer error handling only;
      ratify_skeleton stays pure and raises.
  2.8 **tests/test_orchestrator_ratify.py** (25 new): LEAN extraction
      (drafted-fixture shape; two-marker split; title-prefix match
      against the REAL steps/step.8.txt; unsplittable section as one
      block; no-section honest-empty); ratify happy path (stamp in
      §(0), history INTENT/SKELETON/RATIFIED, presentation carries
      the LEAN, read+write on the trail); --yes with a
      must-not-be-called input_fn; no-LEAN skeleton ratifies with an
      honest presentation line; two LEANs prompted in order; override
      becomes an amendment (state stays SKELETON, skeleton file
      byte-untouched, note attributed to its Q); amend records notes
      (file created beside the skeleton, trail-visible), second amend
      APPENDS, empty-notes refuses, and the full loop
      decline->hand-edit->re-ratify lands RATIFIED; discard confirmed
      = terminal (second gate run refuses), unconfirmed = aborted;
      illegal ratify refuses BEFORE anything (zero reads, zero
      presentation -- spy-asserted); unknown UoW / step-less record /
      missing skeleton file / EOF-before-decision all refuse with
      nothing changed; CLI non-TTY refusal, CLI --yes unattended
      success, CLI error presentation. Plus 14 approval tests grown
      into tests/test_tools.py (+10: declined / approved-deletes /
      approved-missing-file / callback-never-consulted-out-of-scope /
      trail-distinguishes-three-outcomes / interactive_approval y,
      4x not-yes, EOF) and tests/test_tools_git.py (+4 net:
      branch-delete declined / approved / current-branch-GitError /
      push+rewrite-take-no-callback signature test; the U3.2
      stub-message pins UPDATED honestly, named in each file's
      docstring).
  2.9 **Docs (same commit):** end_to_end.md §5 U3.4 row -> DONE in
      U3.1-U3.3's row style; §11 ledger THROUGH the ledger CLI
      (transition U3.4 active --step step.17; close U3.4 PENDING;
      hash backfilled in the gdiff commit -- the step.13-16 shape).
      design.md §2.3 delivered-state paragraph (ratify now the second
      delivered op) + §2.7 delivered-state paragraph (approval
      realized; the push/rewrite named reasons). docs/tool_surface.md
      header + delivered-state note. Every app/tools docstring that
      named U3.4 as pending updated (workspace.py, gitops.py,
      errors.py, __init__.py) + tests/conftest.py's C-7-B note +
      app/orchestrator/__init__.py. No new docs/*.md file -- this
      file is the writeup.

EXPLICITLY OUT OF SCOPE (named here per the operator's brief, not
silently built -- and NOT continued into after this step, per the
operator's explicit stop-here scope boundary):
  - U3.5 EXECUTING loop / scheduler -- a RATIFIED skeleton just SITS
    there; nothing executes it; the compiler (U3.1) is untouched;
    the RATIFIED -> SKELETON edge stays undriven.               -> next
  - U3.6 VERIFYING / repair budgets (d9 stays OPEN).            -> later
  - U3.7 RETRO closure / U3.8 M1 oracle.                        -> later
  - Automatic PAL re-draft on amendment notes -- the dispatch's LEAN,
    taken (2.2/Q-17-A): V0 records amendments; re-drafting is a
    manual hand-edit (draft_skeleton correctly refuses
    SKELETON -> SKELETON, so a PAL re-draft-under-amendments is a
    genuinely NEW flow with its own egress surface).            -> later
  - Track B / P4.                                               -> later

## (3) Verification gates

    G1  tests/test_orchestrator_ratify.py 25/25 PASS live: all three
        decision paths with injected I/O; state-record assertions
        after each; LEAN extraction against the drafted fixture AND
        the real steps/step.8.txt AND a no-open-questions skeleton
        (honest "no LEANs", no crash).
    G2  Amendment loop proven end to end live: decline-with-notes ->
        steps/step.1.amendments.txt beside the skeleton, state stays
        SKELETON, skeleton byte-untouched; second decline APPENDS;
        hand-edit -> re-ratify lands RATIFIED. LEAN override = the
        same amendment path (never a stamp over disagreeing text).
    G3  Illegal ratify refuses BEFORE anything happens: INTENT-state
        record -> IllegalStateTransitionError with zero read_file
        trail entries and zero presentation lines; RATIFIED/
        DISCARDED-state records refuse likewise (terminal case
        message asserted).
    G4  Approval mechanism proven both ways live: delete_file and
        git_delete_branch approved paths ACTUALLY execute (file
        unlinked / branch gone) with "approved": true on the trail;
        declined and no-callback paths refuse with distinct messages,
        both trail-recorded "refused"; the callback is never
        consulted on an out-of-scope path (spy); push/rewrite refuse
        even in an approval-mechanism world and take no callback
        (signature-asserted).
    G5  Live CLI smoke (not just tests): mock draft into a scratch
        workspace + isolated $B3UBOT_HOME, then `b3ubot ratify <ws>
        UT.1 --yes` -> summary + LEAN presented, "decision: ratified",
        ENGINE RATIFICATION RECORD visible in §(0) on disk, engine
        record history [INTENT, SKELETON, RATIFIED]. Confirmed live
        before committing.
    G6  Full suite before this step's edits: 402 passed / 11 skipped
        (confirmed live, matching step.16's reported final). After:
        441p/11s pre-step-file; 443p/11s once this step.17.txt lands
        in the corpus (+25 ratify tests, +14 tools-approval tests,
        +2 self-inclusive corpus pickups -- the steps 10-16 growth
        pattern). Zero regressions; the ONLY edited tests are the 2+2
        U3.2 stub-message pins whose pinned messages this step
        legitimately changed (named in their file docstrings), plus
        conftest.py's docstring.
    G7  Scope boundary held: no scheduler/EXECUTING code anywhere; no
        RATIFIED -> EXECUTING call site added; drafting.py and
        compiler.py byte-untouched (git diff confirms); the only
        U3.5+ mentions are scope annotations; this session stops
        after U3.4.
    G8  §11 ledger row updated THROUGH the existing `ledger` CLI
        (transition -> active --step step.17, close -> done/PENDING),
        against the REAL end_to_end.md -- dogfooding, the
        step.13/14/15/16 precedent.
    G9  b3ubot porcelain clean after each commit; retro pair via the
        SHARED v3_claude/scripts/step_gdiff (steps/step.17.gdiff.txt);
        exactly 2 commits (work + gdiff-with-hash-backfill-folded-in),
        the step.7-16 precedent.

## (4) LOCKs

    C-17-A  b3ubot porcelain clean after each commit.
    C-17-B  A SKELETON IS RATIFIED AS ITS TEXT STANDS: any LEAN
            override is an amendment, never a ratification-with-
            side-notes; the ratification stamp records only confirmed
            LEANs. The engine stamps mechanical facts (timestamp,
            per-LEAN outcomes) -- never solicited prose (C-16-B's
            pattern carried forward).
    C-17-C  PRE-RATIFICATION DECLINE IS NOT A STATE TRANSITION: the
            skeleton stays in SKELETON; amendments are file-level
            (steps/<step>.amendments.txt). Nothing may invent a
            SKELETON self-loop edge; the RATIFIED -> SKELETON edge
            stays reserved for the post-ratification amendment
            (U3.5+).
    C-17-D  APPROVAL NEVER WIDENS SCOPE AND NEVER DEFAULTS ON: scope
            checks (C-4-B) run before the callback is consulted;
            approve=None refuses; EOF/anything-but-yes declines;
            git_push/git_rewrite_history take NO callback until their
            named preconditions (remote policy; concrete verb +
            provenance record) exist -- no fake flows.
    C-17-E  ALL GATE I/O IS INJECTABLE (C-7-B lineage): ratify_skeleton
            requires output_fn/input_fn; app/tools/approval.py's
            interactive_approval takes injectable I/O; app/cli.py is
            the ONLY module passing real print/input, behind the
            Q-8-B non-TTY refusal; --yes is the one honest bypass
            (ratify-as-is only).
    C-17-F  NO SILENT SCOPE CREEP: U3.5-U3.8 and the automatic PAL
            re-draft named out of scope in (2) and confirmed not
            started (G7); the operator's explicit "stop after U3.4"
            direction is honored.
    C-17-G  standing locks inherited: B-3 (no disclosure/no push --
            and git_push refuses regardless); B-6 (deterministic
            pytest oracles gate this step); B-4 (zero keys -- mock/
            injected-I/O only); B-5 (served: the ratify gate itself
            sends NOTHING to any provider -- zero egress records in
            this step's flows); C-15-B (every tool call this gate
            makes, including the amendments write and the approved
            deletes, is on the trail).

## (5) Open questions

    Q-17-A  Should amendment notes trigger an automatic PAL re-draft
            (feed skeleton + notes back through the PAL, overwrite in
            place)? LEAN: not yet -- taken as the dispatch offered,
            and CHECKED sharper: draft_skeleton() correctly refuses a
            UoW already in SKELETON, so this is a genuinely NEW flow
            (its context assembly sends the whole failed skeleton to
            a provider -- an egress-surface decision, B-5) that
            deserves its own named step alongside U3.5+'s loop work;
            V0's manual hand-edit closes the loop honestly meanwhile.
    Q-17-B  Should the ratify gate also serve a RATIFIED-state UoW
            (the diagram's post-ratification RATIFIED -> SKELETON
            amendment)? LEAN: no for V0 -- RATIFIED dwell time is
            zero until U3.5 gives EXECUTING an entry point; drive
            that edge when there is a real moment between ratify and
            execute for a human to regret in (and a real revocation
            semantics question: does the §(0) stamp get counter-
            stamped?).
    Q-17-C  Should git_push's approved path arrive as a policy key
            (e.g. a `push_remotes` allowlist in app/config/config.txt,
            the shell_allowlist precedent) + the approval callback?
            LEAN: yes as the SHAPE when it arrives, but not before a
            real workspace needs push -- U3.5's evidence loop works
            entirely on the local branch; adding remote egress before
            anything needs it would be surface without a consumer.

## (6) Acceptance

    - [x] app/orchestrator/ratify.py: legality-first gate, summary +
          LEAN presentation, ratify/amend/discard through the SAME
          persisted record (2.1, G1, G3).
    - [x] Amendment loop V0: no state change, append-only
          steps/<step>.amendments.txt beside the skeleton,
          decline->hand-edit->re-ratify proven (2.2, 2.3, G2, C-17-C).
    - [x] LEAN extraction: title-prefix + Q-marker split, real-corpus
          fixture (step.8.txt), one-block and no-section honest
          fallbacks; per-LEAN confirm-or-override with override ->
          amendment (2.4, G1, C-17-B).
    - [x] Ratification stamp in §(0) with re-parse guard; discard
          double-confirmed, terminal, file untouched (2.5, G1).
    - [x] Approval mechanism: delete_file + git_delete_branch real
          approved paths (executed + trail "approved": true);
          declined/no-callback refuse distinctly; push/rewrite still
          refuse for narrower named reasons, no callback param
          (2.6, G4, C-17-D).
    - [x] `b3ubot ratify` CLI beside draft: non-TTY refusal unless
          --yes; --yes = ratify-as-is only; all I/O injectable in the
          library (2.7, G5, C-17-E).
    - [x] 25 + 14 new tests, zero regressions beyond the honestly
          updated stub-message pins (402p/11s -> 441p/11s -> 443p/11s
          with this file) (2.8, G6).
    - [x] design.md §2.3/§2.7 delivered-state notes; docs/
          tool_surface.md; every stale "U3.4 pending" docstring;
          end_to_end.md §5 row + §11 ledger via the `ledger` CLI
          (2.9, G8).
    - [x] Scope boundary held: U3.5-U3.8 + auto-re-draft not started
          (G7, C-17-F) -- this session stops after U3.4.
    - [x] Retro pair: this file + step.17.diff.txt + step.17.gdiff.txt
          via the SHARED scripts/step_gdiff (G9).
    - [x] Exactly 2 commits (work + gdiff-with-backfill), not 3 (G9).

## (7) Hash backfill
WORK commit (app/orchestrator/ratify.py + errors/__init__ + app/tools/approval.py + workspace/gitops/errors/__init__ + cli + tests + design.md/end_to_end.md/docs/tool_surface.md + this file):  8e14637
GDIFF commit (step.17.gdiff.txt via scripts/step_gdiff):  (HEAD)

End of step.
