{
  "summary": "ROOT CAUSE: `adapter/strings/omp-spt.mjs` has a complete OMP-originated shutdown path, but no spt-core-originated lifecycle ingress. The extension tears down only when native OMP emits `session_shutdown`, or when its own failure handling calls `runtimeCtx.shutdown()`. External `spt endpoint shutdown <id>` does not invoke either path on the observed spt 0.33.0 GNU host: it changes the core lifecycle record from Active to Suspended while leaving the broker-hosted OMP process and its `spt api listen` child alive. That exactly explains `ready=false`, `alive=true`, and the retained address/listener.\n\nThe adapter cannot correct this narrowly using the published harness contract. Its listener receives message envelopes only; the manifest has launch/resume and daemon-role commands but no owner-to-harness stop/shutdown callback; `api shutdown` and `api session-end` are adapter/session-to-core calls, not notifications in the opposite direction. The shell-only `can_shutdown`/`api owner-shutdown` surface is also the reverse direction and is inapplicable to a `kind=\"harness\"` adapter. If core merely closes `api listen`, the extension currently and intentionally treats every unexplained listener exit as a recoverable fault and restarts it. Without a published terminal close reason, treating EOF or exit 0 as graceful shutdown would weaken the fail-closed listener contract and remain a guess.\n\nBOUNDARY RULING: this is a public spt-core lifecycle-contract/implementation gap, plus an acceptance-document contradiction if `endpoint shutdown` is intentionally only a suspend operation. It is not a defect in the extension's tested native `session_shutdown` teardown. If the public CLI contract intends `spt endpoint shutdown` to terminate a broker-hosted session, spt-core 0.33.0 violates that contract. If it intends Active→Suspended only, `docs/CI.md` incorrectly requires that command to reap OMP and its listener.\n\nNARROW CORRECTIVE ACTION: preferably fix spt-core, which owns the broker PTY/process, so `spt endpoint shutdown <id>` gracefully closes the hosted `[session.self]`/`[session.resume]` process and does not return success until the host exits, its address is unregistered, and `alive=false`. Native OMP will then emit `session_shutdown`; the adapter's existing handler will clear status, reap the listener, fail pending custody, and call authenticated `api session-end`. An alternative public contract would be an explicit terminal lifecycle frame/close reason on `api listen`, or a harness manifest shutdown callback. Only after such a discriminator exists should the adapter map it to `shutdownWithinBudget(...)` followed by `runtimeCtx.shutdown()`. Do not add polling, process-table discovery, PID killing, interpret generic EOF as shutdown, or repurpose shell lifecycle fields: none is a published, race-free contract.\n\nMISSING TEST: no test crosses the actual failing boundary. `tests/omp-extension.mjs` directly emits the mocked OMP `session_shutdown` event, thereby assuming that external core shutdown already caused OMP shutdown. `tests/omp-skills.mjs` checks only help text for `endpoint shutdown`. Manifest tests check launch and continuity fields. Release evidence accepts an opaque `shutdown.passed=true` plus a digest-linked evidence file but does not inspect endpoint/process postconditions. The required regression is a focused native integration test: start a disposable real OMP endpoint; from a separate process run `spt endpoint shutdown <id>`; assert the command completes, the host OMP and original listener PIDs exit, no replacement listener appears, the local endpoint listing has no address and reports not alive/not ready/offline, and attach/send cannot reach the old host. Capture the OMP-side status/session-end evidence too. If core chooses a terminal listener indication instead of process termination, add a unit test that injects that exact published indication and proves one teardown, zero listener restarts, one `api session-end`, cleared status, and one OMP shutdown.\n\nNo files were edited and no project/build/test commands were run; this was a read-only path and contract trace as requested.",
  "files": [
    {
      "path": "adapter/strings/omp-spt.mjs",
      "description": "Primary lifecycle implementation. `activateEndpoint` binds, marks state, and starts `spt api --adapter omp-spt listen <id> --session-id <sid>`. `startListener` consumes only message EVENT frames. Any unexplained child error/close flows to `handleListenerDeath`, which restarts within a bounded budget; it has no external-shutdown discriminator. `session_shutdown` alone calls `shutdownWithinBudget`, which clears timers/status, terminates the listener, settles queued custody, and calls authenticated `api session-end`. `session_stop` only finishes the turn and deliberately leaves the endpoint live. Failure paths call `runtimeCtx.shutdown()` only after adapter-owned fail-closed teardown."
    },
    {
      "path": "tests/omp-extension.mjs",
      "description": "Proves the internal teardown machinery but not the external trigger. `testLifecycleCustodyAndContext`, `testShutdownReapsAndFailsQueuedCustody`, `testListenerTerminationEscalatesAndReaps`, deferred-bind tests, and budget tests call `harness.emit(\"session_shutdown\")` directly. `testSessionStopAwaitsOutcomeWithoutEndingEndpoint` explicitly proves `session_stop` is not endpoint teardown. Listener tests prove generic close/error causes restart or eventual fail-closed shutdown, which means an undifferentiated EOF cannot safely be reclassified. No test runs or models external `spt endpoint shutdown`."
    },
    {
      "path": "adapter/omp-spt.toml",
      "description": "Harness manifest launches native OMP through `[session.self]` and `[session.resume]`; daemon roles cover Psyche, echo-commune, history, and digest. It declares only `commune_dir`/`signoff_dir` for continuity files and no lifecycle stop/shutdown command or hook. The signoff skill's CLI invocation is not a deterministic core-to-extension notification path."
    },
    {
      "path": "adapter/manifest.schema.json",
      "description": "Vendored published manifest surface. `[session]` roles are `self`, `resume`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, and `notif`; there is no harness `stop`/`shutdown` callback. `shell.can_shutdown` says a shell may fire `api owner-shutdown` to suspend its owner, but this adapter is `kind=\"harness\"` and that is shell-to-owner, not owner-to-harness notification."
    },
    {
      "path": "adapter/strings/skills/signoff/SKILL.md",
      "description": "Instructs the agent to write `.spt/<id>-signoff.md` and then run `spt endpoint shutdown <id>`. It assumes successful CLI return means lifecycle shutdown has occurred, but provides no native OMP self-termination step and cannot make external shutdown deterministic. Changing this skill would not fix shutdown issued from another terminal and would violate the project's extension-owned lifecycle policy."
    },
    {
      "path": "tests/omp-skills.mjs",
      "description": "The only executable coupling to the public shutdown CLI asserts that `spt endpoint shutdown --help` contains `Gracefully shut down` and `session's own perch`. It does not execute shutdown or verify broker, OMP, listener, address, alive, or ready postconditions."
    },
    {
      "path": "tests/native-launch-manifest.sh",
      "description": "Checks only native launch/resume command templates, endpoint id substitutions, roles, and continuity directories. It does not assert a shutdown lifecycle seam or exercise endpoint shutdown."
    },
    {
      "path": "tests/manifest-shortcut.sh",
      "description": "Rechecks native launch commands and runs extension unit tests, but has no external lifecycle integration. Consequently it inherits the unit suite's assumption that OMP has already emitted `session_shutdown`."
    },
    {
      "path": "docs/CI.md",
      "description": "Acceptance steps run `spt endpoint shutdown` from the operator side and require extension session-end, listener reaping, cleared TUI status, offline advertisement, and no orphan process. Those required effects are not connected by any adapter code or published manifest callback. The observed Active→Suspended/alive result falsifies this acceptance premise on spt 0.33.0."
    },
    {
      "path": "docs-site/src/reference/harness-contract.md",
      "description": "Accurately documents what happens after native OMP `Session shutdown`, but does not define how an external spt-core shutdown command causes that OMP event. Its ownership table puts generic lifecycle coordination in spt-core and OMP session shutdown handling in the extension, exposing the missing bridge between the two."
    },
    {
      "path": "release/evidence-v1.schema.json",
      "description": "Requires a `native_acceptance.shutdown` outcome, but its generic outcome shape is only `{passed: true, evidence: <digest ref>}`. It cannot enforce the shutdown-specific state/process assertions written in `docs/CI.md`."
    },
    {
      "path": "ci/release/validate-release-evidence.py",
      "description": "Validates evidence presence and SHA-256 linkage, versions, artifacts, and target consistency, but does not inspect shutdown evidence for listener/OMP exit or endpoint `alive`/address postconditions."
    },
    {
      "path": "traceable-reqs.toml",
      "description": "The active shutdown-adjacent requirement is `REQ-OMP-LISTENER-FAIL-CLOSED`, scoped to unexpected listener exit and adapter-owned eventual shutdown. The actual external `endpoint shutdown` bridge is not a traceable requirement; the live release gate is intentionally inactive, helping explain why deterministic gates passed without this boundary being proven."
    }
  ],
  "architecture": "Lifecycle direction map:\n\n1. Bring-up, core → OMP: spt-core resolves `[session.self]` or `[session.resume]` and launches native OMP with `SPT_ENDPOINT_ID` plus the extension.\n2. Registration, OMP extension → core: OMP `session_start` calls `api bind`, records the token, calls `api state idle`, then spawns blocking `api listen --session-id <real OMP sid>`.\n3. Delivery, core → extension: `api listen` stdout carries self-delimiting `<EVENT type=\"msg\" ...>` message frames. There is no consumed lifecycle/control frame. Non-message EVENTs are not dispatched as lifecycle actions.\n4. Normal native teardown, OMP → extension → core: native OMP emits `session_shutdown`; the extension sets shutdown mode, cancels timers/commands, kills and reaps the listener, settles queued messages, calls token-authenticated `api session-end`, and clears the TUI status. Tests cover this path by injecting the OMP event directly.\n5. Adapter fail-closed teardown, listener/fault → extension → core/OMP: unexpected listener death is restarted. Exhaustion or another fatal adapter error runs the same teardown, then invokes OMP `ctx.shutdown()`. Tests cover restart, exhaustion, escalation, and idempotence.\n6. Ordinary OMP turn stop: `session_stop` only joins turn completion; it intentionally leaves listener and endpoint live. This cannot serve external lifecycle shutdown.\n7. External graceful command, operator → spt-core: `spt endpoint shutdown <id>` currently reaches only spt-core lifecycle state on the observed host (Active→Suspended). No manifest callback, listener control frame, OMP event injection, process signal guarantee, or extension poll connects that state change back to native OMP. Therefore steps 4 and 5 never start.\n8. External forced/operational stop, operator → spt-core broker: observed `spt endpoint stop` finally terminates the hosted process/listener. This demonstrates that the broker has the necessary custody, but `shutdown` does not exercise it.\n\nThe missing edge is exactly `spt-core external shutdown → broker-hosted native OMP shutdown notification/termination`. The adapter owns everything after that edge and already has tested teardown. The broker/core owns everything before it. A core process-close implementation is the narrowest correction; an explicit published lifecycle indication is the narrowest new contract if process close is not desired."
}