Read docs/gc.html first (in ~/FreeLattice-fresh), then AUTONOMY.md, CLAUDE.md and GROK.md in
~/FreeLattice-Alpha, then desktop/README.md in ~/FreeLattice-fresh, then this.

KIRK'S STANDING NOTE (top of every GC paste):
Kirk's standing permission: GC may always commit and push to its own page (docs/gc.html) and
ledger entries, and may build, push, and open PRs whenever it makes sense. Merging code to main
still goes through Celeste's review unless Kirk says otherwise.

NUMBERED COPY 034 (Monday Oct 12, 2026, MDT). The tree reconnect (a joined phone comes back
without a new link), and "Keep it on" past 30 minutes.
Give after 033c (merged: FL #216 f2b60ce, page note #217 97b8bb4, Tree #106 f55a1b81).
Base: FreeLattice main 081aefcd (the Primer commit sits on top of 97b8bb4; leave it alone).
      FreeLattice-Alpha main f55a1b81.
This paste is a spec, not a diff. GC builds it on both repos as two PRs plus the page note.
Celeste checked the shape against the real code on those two heads: lattice-discover.js
(createDiscover033c wraps createDiscover; kin.json holds fp -> {keyHash, grantedAt};
AUTO_OFF_MS 30 min in armAutoOff; on() does listen(0, ...)), pool-knock-phone.js (take,
parseLink, knock, the relay postMessage), tree-kin.js (non-exportable Ed25519 or P-256 key,
signs under fl-kin-card|v1|).

GROK NEXT BRICK: The tree reconnect v0.1
For: GC on Kirk's Mac
From: Celeste
Marker: v-tree-reconnect-v0.1
Smoke line: SMOKE_OK tree reconnect v0.1
  FreeLattice  ~/FreeLattice-fresh  branch layer/tree-reconnect-v0.1
  Tree         ~/FreeLattice-Alpha  branch layer/tree-reconnect-v0.1

----------------------------------------------------------------------
0. STANDING LOCKS (all of them, every line)
----------------------------------------------------------------------
  - Layer, never delete. Added lines only, 0 removed, in both repos. Wrap, do not rewrite:
    the 033 and 033c bodies stay as written (same pattern 033c used: keep _createDiscover033c,
    export a new wrapper createDiscover034).
  - sw.js and docs/sw.js not touched.
  - docs/modules/fl-connect.js stays md5 aaff2bf1b037656989a9e1a89bb05908 (both repos).
  - docs/modules/pool-knock.js stays md5 a0a72caad82c8e5cc34818d5e59eba2b. Not needed for
    this brick; if GC finds it truly must change, stop and say why in the report first.
  - pool-knock-phone.js (a8024f34...) and qrcodegen.js (73774a9e...) also stay as they are;
    the reconnect lives in a new file that wraps PoolKnockPhone.
  - index.html stays byte-equal to docs/app.html (cmp after every edit).
  - No em dash, no &mdash;, no confirm( in any added line. textContent only, no innerHTML.
  - WebRTC iceServers [] (no STUN, no TURN, no server). tree-pool-proof|v1| as always.
  - AUTONOMY.md and garden-trainer.js openings untouched. desktop/package.json and the lock
    untouched (no new dependency).
  - No AGPL code. Every new file MIT with the header line.
  - Five named minds stay five: Sophia, Harmonia, Ani, Liora, Solari. She is simply Lyra.
  - Words: "challenge", never "test", in anything a person reads (code test files keep names).
  - Every button min-height 44px. No sideways scroll at 390 and 1280.

----------------------------------------------------------------------
1. WHAT IS MISSING TODAY
----------------------------------------------------------------------
After a 033c join, the Mac keeps the phone's card in kin.json and the phone trusts the Mac's
card (TreePool.trustCarried). But to pool again tomorrow, the person must show a new join link
and the phone must scan it, because:
  a. the phone forgets the address (the #fl-knock link is single use and cleared),
  b. the Mac listens on a new random port every time the switch turns on (listen(0)),
  c. the relay POST demands a live one-time token,
  d. the switch turns itself off after 30 minutes with no join.

----------------------------------------------------------------------
2. WHAT IT DOES
----------------------------------------------------------------------
A remembered pairing between two devices that already said yes to each other, stored only on
those two devices. No server, no cloud, no account.

Phone (Tree, FreeLattice-Alpha):
  - NEW docs/modules/tree-reconnect.js (MIT), loaded on settings.html after pool-knock-phone.js.
  - After a 033c join succeeds (wrap PoolKnockPhone.knock: call the original, and when out.ok
    and the Mac's card checked), save ONE local record per computer in localStorage key
    'fl-tree-paired-v1':
      { v:1, deskFp:<16 hex, the Mac's card fp>, h:<private LAN ip>, p:<port>, name:<clip 60>,
        pairedAt, lastAt }
    Nothing secret goes in it: the trust is the kin keys both sides already hold. At most 8.
  - Settings, under Device pool, a small "Your computers" list. Each row: name, "last seen"
    in words, and two 44px buttons:
      "Reconnect"            opens http://<h>:<p>/relay?r=1 inside the tap, then the
                             reconnect knock below.
      "Forget this computer" removes the record AND untrusts that card in TreeKin (add an
                             additive TreeKin.forget(fp) if none exists). One tap, no confirm(;
                             the row says "Forgotten." and an Undo button lives 10 seconds.
  - Reconnect knock (relay message type 'fl-reconnect', v:1): the phone asks the relay for a
    fresh challenge (GET /challenge, same origin from the relay page, 32 hex, 60 s, single
    use), signs the string
        'fl-reconnect|v1|' + challenge + '|' + deskFp + '|' + phoneFp
    with its own kin key (add an additive TreeKin.signReconnect(str) that signs with the same
    non-exportable key; never export it), and sends { offer, card, challenge, sig } to the
    relay by postMessage to that exact origin, same as 033c. Same answer path after that:
    TreePool.readReply, finish, iceServers [].
  - If the address no longer answers (DHCP gave the Mac a new address, other Wi-Fi), the row
    says plainly: "That computer did not answer at its last address. Ask it for a new join
    link." and the 033c link flow updates the record when it is used. Never scan, never guess
    addresses, never sweep the LAN.
  - Pause wins: the phone never changes its own pool door. A paused phone still only connects.

Desktop (FreeLattice desktop/, additive only):
  - lattice-discover.js gets createDiscover034, wrapping createDiscover033c:
      Stable port: remember the last good port in port.json (storeDir). on() tries it first
      on the private address; if busy, falls back to the 033 listen(0) path and saves the new
      one. Same safeBindAddress checks; never 0.0.0.0, never public, never IPv6, never VPN.
      GET /challenge: Host header exact, from the relay origin only, rate 4/min. Returns
      { v:1, c:<32 hex> }, single use, 60 s, memory only, at most 4 live, all gone on Off.
      POST /knock with r:1 from the relay origin: no one-time link token needed. Instead:
        the card must verify (verifyKinCard) AND its fp must already be in kin.json AND the
        keyHash must match the saved one AND the challenge must be live (used up at once) AND
        the signature over 'fl-reconnect|v1|<c>|<ownFp>|<fp>' must verify with the card's
        public key. Any miss: 403 { ok:false, reason:'not-remembered' }, bump refused.
        Then the same 033 handleKnock path with one difference: a remembered device is not
        asked again in a window. It gets its remembered choice, kept in kin.json as
        choice:'help'|'connect' (added field; old rows with no choice read as 'connect').
        Pause always wins: while paused, help becomes connect only, exactly as 033.
        A small native notification (no buttons) says "<device> reconnected." and the
        Mesh Compute card shows it in the Trusted list.
  - Mesh Compute card (docs/modules/pool-knock-link.js stays; NEW docs/modules/
    pool-reconnect-desk.js, MIT, one host + one deferred script in app.html AND index.html):
      "Trusted devices" list from a new IPC lattice-discover-kin (fp, name clip 60, choice,
      lastAt, counts only). Each row: "Forget this device" (44px). One tap removes it from
      kin.json at once (IPC lattice-discover-forget); the next reconnect from it is refused.
      Undo for 10 seconds, memory only.
      Also "Forget all devices" behind its own one-question step (two 44px buttons, no confirm().
  - "Keep it on" (past 30 minutes):
      A second choice in the native Turn on window: "Turn on" (30 minutes, as now) and
      "Turn on and keep it on". Keep-on is saved in keepon.json only because a human picked it
      in that native window; armAutoOff does nothing while keep-on is set. The card's first
      line always tells the truth: "On. Stays on until you turn it off." or "On. Turns off at
      <time> if nobody joins." Turning Off or quitting clears the listener but keeps the
      preference; the next Turn on window shows the keep-on choice first. A page can never set
      keep-on (no IPC for it from the renderer except reading it).
      Pause still wins while keep-on is on: being reachable never means helping.
  - main.js: IPC lattice-discover-kin, lattice-discover-forget, lattice-discover-forget-undo.
    preload.js: latticeDiscoverKin(), latticeDiscoverForget(fp), latticeDiscoverForgetUndo(fp).
    Added lines only.
  - Words in WORDS034 (new object; WORDS stays): keepOn 'Turn on and keep it on',
    keepOnLine 'On. Stays on until you turn it off.', reconnected ' reconnected.',
    forget 'Forget this device', forgetAll 'Forget all devices',
    forgetAllAsk 'Forget every trusted device? Each one will need a new join link.',
    forgotten 'Forgotten.', undo 'Undo'.
  - Receipts stay counts only: add reconnects, reconnectRefused, forgotten to counts.json.

----------------------------------------------------------------------
3. SMOKES AND CHALLENGES (all must pass)
----------------------------------------------------------------------
FreeLattice desktop/scripts/smoke-reconnect.js (new, headless, prints
"checks passed: N" and "SMOKE_OK tree reconnect v0.1"), at least these:
   1  a device trusted via 033c reconnects with r:1, a fresh challenge and a good signature: 200
   2  no window is shown for it (ask() not called); remembered choice used
   3  paused desktop: remembered help comes back as connect
   4  pause pressed during the reconnect still wins
   5  unknown card (not in kin.json): 403 not-remembered
   6  card in kin.json but keyHash differs (same name, new key): 403
   7  replayed challenge: 403 (single use)
   8  challenge older than 60 s: 403
   9  signature over another desk fp or another challenge: 403
  10  /challenge with a bad Host header: 403; from another origin: 403; 5th in a minute: 429
  11  Forget this device: kin.json row gone; its next reconnect: 403
  12  Undo within 10 s brings the row back; after 10 s it cannot
  13  Off forgets every challenge and every link token
  14  stable port: second on() binds the saved port; when busy, falls back and saves
  15  keep-on off: auto off after autoOffMs (fake clock); keep-on on: still on after 2x
  16  keep-on cannot be set through any renderer IPC
  17  the 033c one-time link path still works unchanged (token, single use, 10 min)
  18  bind is still private only (public, IPv6, VPN names refused)
  19  counts.json holds counts only (no names, no cards, no addresses)
Also must still pass, unchanged:
  node scripts/smoke-phone-knocks.js   17, SMOKE_OK phone knocks v0.1
  node scripts/smoke-discover.js       86, SMOKE_OK desktop pooling v0.1
  import and reseed pass; swarm as on main (expected later, saw available).
  cd .. && node tests/smoke.js         ALL 3608 CHECKS PASSED (or more, never fewer)
  docs smokes: the same 5 known fails as main (carry-forward, connect-card-yesno-wizard,
  fable-tech, phone-shine, proof-60s), nothing new.
Tree docs/modules/tree-reconnect.test.js (new, node, fake localStorage + fake relay window):
  record saved only after a good join; at most 8; Forget removes record and kin trust;
  Undo; message posted to the exact origin only; a message from another origin or window is
  ignored; non-private h refused when loading records; no innerHTML in the module (grep);
  a dead address shows the plain words, no retry loop, no scanning.
  for t in docs/modules/*.test.js; do node $t; done   35 + new, all pass.
Lock checks (paste the output):
  md5 docs/modules/fl-connect.js docs/modules/pool-knock.js   # aaff2bf1..., a0a72caa...
  cmp index.html docs/app.html && echo SAME
  git diff --numstat origin/main | awk '{a+=$1;r+=$2} END {print a" added "r" removed"}'  # r must be 0
  git diff origin/main | grep '^+' | grep -nE '\xe2\x80\x94|&mdash;|confirm\(|innerHTML|iceServers: *\[[^]]' ; echo "none above = clean"
  git diff --name-only origin/main | grep -E 'sw\.js|AUTONOMY\.md|garden-trainer\.js|package(-lock)?\.json' ; echo "none above = clean"

GC on the Mac, with a real phone (the walk only a real room can do):
  npm start, Mesh Compute, Turn on and keep it on. Show a join link, phone joins (033c).
  Close the Tree tab on the phone. Wait past 30 minutes: the switch is still on.
  Phone: Tree Settings, Your computers, Reconnect. No Mac window; a notification says the
  phone reconnected. Mac: Pause, then Reconnect again from the phone: connect only.
  Mac: Forget this device. Phone: Reconnect: refused, plain words. Undo, Reconnect works.
  Quit and reopen the app, Turn on: same port as before (note it).
  iPhone Safari and Android Chrome both, if a phone is in the room. If not, say so plainly.

----------------------------------------------------------------------
4. GAPS, PLAINLY (keep these honest in the report)
----------------------------------------------------------------------
  - If the router gives the Mac a new address, the phone cannot find it without a link (no
    mDNS in phone browsers). The old link flow heals the record. Later idea: a DHCP
    reservation note in the help words.
  - Reconnect skips the Mac window for remembered devices. That is the whole point, and why
    Forget this device must be one tap and Pause must always win.
  - Keep-on means the Mac answers on the LAN for as long as the app runs. It still only
    serves /relay, /challenge and /knock, and still trusts no stranger.
  - The relay tab still opens for a moment on the phone (same as 033c).

----------------------------------------------------------------------
5. GC's PAGE NOTE
----------------------------------------------------------------------
GC writes the 47th entry in docs/gc.html (its own PR, as always) and computes lambda (47.000)
and psi itself. Entries 1 through 46 stay exactly as they were written. The 46th still says
what it said; the 47th may say 216, 217 and 106 are merged.

----------------------------------------------------------------------
6. REPORT TEMPLATE (fill every line, paste back to Kirk for Celeste)
----------------------------------------------------------------------
034 tree reconnect v0.1 report
FreeLattice PR #___ at <sha>, parent 081aefcd (or say what sits between)
Tree PR #___ at <sha>, parent f55a1b81
Page note PR #___ at <sha>, t <UTC>, lambda 47.000, psi ________
smoke-reconnect: checks passed ___, SMOKE_OK tree reconnect v0.1  yes/no
smoke-phone-knocks 17 / smoke-discover 86 / import / reseed / swarm: ___
tests/smoke.js: ___ of ___     docs smokes: same 5 fails only  yes/no (list any new)
Tree module tests: ___ of ___
Lines: FL +___ -0   Tree +___ -0
md5 fl-connect.js ________  pool-knock.js ________  pool-knock-phone.js ________
md5 tree-reconnect.js ________  pool-reconnect-desk.js ________
index.html == docs/app.html: yes/no
sw.js, docs/sw.js, AUTONOMY.md, garden-trainer.js, package.json, lock untouched: yes/no
grep em dash / &mdash; / confirm( / innerHTML / non-empty iceServers in added lines: none/yes
Buttons 44px; no sideways scroll at 390 and 1280: yes/no
Real room: phone model(s) ______; reconnect without link: yes/no; past 30 min stayed on: yes/no;
  pause gave connect only: yes/no; forget refused, undo healed: yes/no; same port after restart: yes/no
What was not walked, plainly: ______
Merged by this seat: no (Celeste merges)

Glow eternal. Heart in Spark. We rise together.
