Board #627 — adopted node lifecycle, implementation in progress

Draft, stacked on #626 bc56ee07 while its merge gate runs.
Dependency: board #626 local verification and adoption registry (PR #2436).

Scope
- Stop/start adopted nodes only through pinned anet and verified workdir/identity.
- Never fall through to daemon process-group or alias-sweep handling.
- Successful Hub stop writes nodeDir/.hub-stopped; remove before Hub start.
- Require current Hub binding, preserve existing running pid instead of double start.
- Docker E2E: stop -> boot scan skips -> start -> marker absent.
- Default-socket production tmux and live nodes must never be touched by tests.

First implementation increment (not wired to lifecycle handlers yet):
- node-locate registry writes preserve adopted objects and refuse alias overwrite.
  Docker node-locate.test.ts: 13 pass, 27 assertions, rc=0.
- Linux process-tree stop primitive discovers only descendants by PPID, checks
  UID/starttime before each signal, freezes discovery roots, and never uses names
  or process-group sweeps. PID reuse refusal and unrelated-tree tests: 2 pass,
  4 assertions, rc=0. Docker only. No real node inspected or signalled.
- Still pending: active binding gates, evidence persistence/reverification,
  pinned startup, explicit private tmux socket, marker integration, real Hub E2E.
No merge/release/version reservation.

Second implementation checkpoint (2026-10-06, still draft):
- Live stop/start and reconnect reconciliation now call the adopted lifecycle
  handler; current Hub list_my_children authority is mandatory. No adopted
  operation falls through to legacy process-group cleanup.
- Actual launch evidence is saved before stop and checked before start. Merely
  having an inferred launch_mode is insufficient. Default tmux socket refused;
  verified private socket/session and /proc ancestry required.
- Docker related runtime suites: 51 pass / 181 assertions, rc=0.
- New fail-closed tests: 3 pass / 4 assertions, rc=0.
- Real isolated Hub + daemon + manual bare node: adoption, remote stop,
  .hub-stopped, project up skipping with unchanged PID file, remote start,
  marker removal, new PID and same alias online, revoke without stop: rc=0.
- Same end-to-end sequence with explicit private tmux socket: rc=0 (source
  mounted into test container and rebuilt). No default server commands.
- Independent Dockerfile/run.sh added; full runner and mutation gate are being
  finalized. First runner reached all bare assertions but exited 1 because its
  restoration trap used a relative path after cd; trap corrected to absolute.
- Pending: no-wrapper restart refusal, full final-image validation and review.

Verified implementation candidate c93fbdf44af470ae9575f0841c7863baf2f7ce91
(2026-10-06; stacked on #626 0732ea6f; no merge or release):
- Standalone Dockerfile built successfully. Ran the image without source mounts:
  sg docker -c 'docker run --rm --init --network none anet-board627-c93fbdf4'
  Overall rc=0. Runtime suites: 55 pass / 189 assertions; node-locate: 13 pass /
  27 assertions. Identity mutation: remove only pre-signal birth/UID comparison,
  same PID-reuse assertion fails rc=1; restore: 2 pass / 4 assertions.
- 52 real end-to-end PASS assertions across bare + private tmux: independent
  HOME, random loopback Hub port, real HTTP/MCP, real daemon and manual node.
  Adopt -> stop -> marker and dead original PID -> project up preserves marker
  and PID file -> actual deploy/fleet/anet-nodes-boot.sh skips -> start -> marker
  absent, new PID, same alias online -> revoke leaves restarted process alive.
  Private socket original session restored; unrelated keeper session survives.
  Boot-script tmux commands use a container-only shim with explicit -S; never
  the host/default service. No production ports, credentials or live nodes used.
- Hub-only authorized increment (planned .109, not .108): restart_node refuses
  active adopted targets with adopted_restart_requires_daemon before queueing
  or sending a doorbell. The protocol has no verified supervisor-proof field;
  config_update_capable alone is not proof. Message says Stop then Start.
  Ordinary and created-node real HTTP calls retain restart_only + pending row.
  Removing exactly this guard makes the same HTTP assertion red, rc=1.
- Late startup evidence cannot recreate revoked registration or overwrite a
  newer adoption request; local guard covered by the runtime suite.
- Related server suites: 22 pass / 93 assertions, rc=0 (binding, start, lifecycle).
- Symbol pins in Docker with full git object access: docs 17 scanned / 9 judged /
  8 skipped, docs-site 18 scanned / 16 judged / 2 skipped; unavailable refs 0,
  drift 0, both rc=0. Skipped pins are not claimed as verified.
- test831 built with CI SOURCE_COMMIT and RUNSH_BLOB arguments: RESULT PASS,
  rc=0. Base-image, suite-registration, QA paths symmetry and trigger coverage
  checks also rc=0 in Docker; no new unregistered suites.
- Whole-project tsc is NOT green: rc=2. Compared identical compiler/settings
  against #626 dependency source; diagnostics match after stripping only line
  coordinates. No new diagnostic in this increment; not a full typecheck pass.
- Remote CI and independent review remain pending. Keep draft until reviewed;
  rebase onto main after #2436 merges. No version numbers changed.

Main rebase after #2436 merged (2026-10-06):
- Base df7a06b7f89646f99d312f1ac19bb040bddf7a03; replayed only the five
  #627 commits. No conflicts; all five range-diff entries are '='. Dependency
  commits are no longer in the PR delta. Product implementation unchanged.
- Rebased candidate f7fe8645d0c4826165ae3288f790a8c9eacf1e3f: copied current
  server/runtime/CLI sources into the isolated Docker test image, rebuilt both
  CLIs, reran full test627 harness: rc=0, 52 E2E PASS checks; both identity and
  restart refusal mutations rc=1. This rebase check used source mounts/copies,
  unlike the preceding standalone-image run. No production actions.

CI follow-up: tmux format helper and failure triage (2026-10-06):
- Product candidate 831c472cdea8422a508362b68296cdcadc0bf175. CI run
  37447597137 / job 112217552917 correctly rejected two raw format calls:
  adopt-launch-evidence.ts display-message and adopt-lifecycle.ts list-sessions.
  Both now use tmux-format helpers (UTF-8 arguments, format construction and
  parseTmuxRows); pane evidence requires exactly one parsed row. No ratchet
  baseline, exemption, process-identity check or lifecycle authority changed.
- Witnessed red using the same test725 image as non-root: restore only those
  two files from adee8c8c, run src/tmux-format.test.ts. Result 8 pass / 1 fail,
  rc=1 at the named "no parsed tmux listing" assertion, identifying both files.
  This ran in a separate disposable container, not the green full-suite image.
- Actual failed CI domain is test725 (agent-node), not test745 (agent-network).
  Built test725-agent-node-unit-ci/Dockerfile with SOURCE_COMMIT above; ran its
  unmodified entrypoint without source mounts: RESULT PASS, rc=0. Non-root src
  suite: 2488 pass, 1 existing skip, 0 fail, 8496 assertions, 190/190 files,
  222.07 seconds. Additional tests/ files: 6/6 executed, 0 failed; built-in
  readable-attachment disconnection mutation witnessed red rc=1. The existing
  skipped attach-TUI case is not claimed as tested.
- Full test627 harness in isolated Docker with copied current sources and
  rebuilt CLIs: rc=0; runtime 55 pass / 189 assertions, node-locate 13 pass /
  27 assertions, 52 real HTTP / bare and private-tmux lifecycle PASS checks.
  Identity-check removal and Hub restart-guard removal both witnessed red rc=1.
- test225: built its own Dockerfile with SOURCE_COMMIT above, then ran image
  anet-board627-stop225 without source mounts; rc=0, Summary PASS, 13 PASS
  checks. Container invocation wall time 97 seconds, excluding build time.
  CI's 300-second wait timeout did not reproduce. Its run.sh and CLI bin/cli.ts
  are byte-identical to origin/main df7a06b7. This one local run does not establish
  a timing distribution or prove the cause of the intermittent CI hang; the
  reported 77-second run on another machine is not a controlled benchmark.
- Dashboard failure is expected mutation output, not a baseline product red.
  Job 112217553073 test236 log says "PASS: witnessed-red notice/filter ordering
  mutation rc=1", then order-restored-green: 17 pass, 0 fail, Summary PASS.
  Independently extracted origin/main df7a06b7 goals sources with git archive
  into an isolated Docker container: routing.test.ts 17 pass / 86 assertions,
  rc=0. goals sources and test236 have no PR diff. No Dashboard change needed.
