diff --git a/tests/manifest-shortcut.sh b/tests/manifest-shortcut.sh index 011f670..dc4fde8 100644 --- a/tests/manifest-shortcut.sh +++ b/tests/manifest-shortcut.sh @@ -94,97 +94,103 @@ if [ "$fail" -eq 0 ]; then echo "ok rejected read-env declarations/placeholder # Current spt-core only fills the published Psyche custody keys for Psyche roles. Profile/model/auth # state comes from the endpoint snapshot, not from extra manifest substitutions. npsy=$(grep -cE '^[[:space:]]*command[[:space:]]*=[[:space:]]*"omp-spt psyche-omp --id \{id\} --session-id \{session_id\} --psyche-context-file \{psyche_context_file\}"' "$MANIFEST") if [ "$npsy" -eq 2 ]; then echo "ok both psyche roles use the public Psyche key catalog"; else echo "FAIL expected 2 valid psyche-omp role commands, found $npsy"; fail=1; fi for role in session.psyche_init session.psyche_resume; do role_keys=$(field_of "$role" '^[[:space:]]*keys[[:space:]]*=') role_cmd=$(field_of "$role" '^[[:space:]]*command[[:space:]]*=') case "$role_keys:$role_cmd" in *OMP_PROFILE*|*PI_PROFILE*|*PI_CODING_AGENT_DIR*|*PI_CONFIG_DIR*|*XDG_DATA_HOME*|*HOME*|*USERPROFILE*|*OMP_SPT_OMP_BIN*) echo "FAIL [$role] references an unsupported read-env substitution"; fail=1 ;; *) echo "ok [$role] avoids unsupported read-env substitutions" ;; esac done # The retired resident key must stay GONE from both psyche roles. if grep -q 'psyche_prompt' "$MANIFEST"; then echo "FAIL a psyche role references the retired {psyche_prompt}"; fail=1; else echo "ok no {psyche_prompt} anywhere (event rides stdin)"; fi # [unit->REQ-PSYCHE-EPHEMERAL-SHIM] — identity-env scrub on both Psyche roles. for role in session.psyche_init session.psyche_resume; do scrub=$(field_of "$role" '^[[:space:]]*env_remove[[:space:]]*=') if [ -z "$scrub" ]; then echo "FAIL [$role] has no env_remove (identity-env scrub missing)"; fail=1; else case "$scrub" in *'"OWL_SESSION_ID"'*'"SPT_AGENT_ID"'*|*'"SPT_AGENT_ID"'*'"OWL_SESSION_ID"'*) echo "ok [$role] env_remove scrubs OWL_SESSION_ID + SPT_AGENT_ID" ;; *) echo "FAIL $role env_remove misses an identity var: $scrub"; fail=1 ;; esac fi done rz_detach=$(field_of session.psyche_resume '^[[:space:]]*detach[[:space:]]*=') case "$rz_detach" in *false*) echo "ok psyche_resume is captured (detach=false)" ;; *) echo "FAIL psyche_resume must be detach=false, got: [$rz_detach]"; fail=1 ;; esac rz_keys=$(field_of session.psyche_resume '^[[:space:]]*keys[[:space:]]*=') case "$rz_keys" in *'"psyche_context_file"'*) echo "ok psyche_resume keys include psyche_context_file" ;; *) echo "FAIL psyche_resume keys missing psyche_context_file: [$rz_keys]"; fail=1 ;; esac # ── [update] — the gh_release self-update seam points at the fork ───────────────────────────────── if grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*"BigscreenVR/omp-spt"' "$MANIFEST"; then echo 'ok [update].repo = "BigscreenVR/omp-spt"'; else echo "FAIL [update].repo is not BigscreenVR/omp-spt"; fail=1; fi if grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*"SaberMage/' "$MANIFEST"; then echo "FAIL an active repo assignment still points at SaberMage/*"; fail=1; else echo "ok no active SaberMage repo assignment"; fi if grep -Eq '^[[:space:]]*transport[[:space:]]*=[[:space:]]*"gh"' "$MANIFEST"; then echo 'ok [update].transport = "gh"'; else echo "FAIL [update] transport must use gh for auth-capable release fetch"; fail=1; fi if grep -Eq '^[[:space:]]*message[[:space:]]*=' "$MANIFEST"; then echo "ok [update].message present"; else echo "FAIL [update] has no message field"; fail=1; fi if grep -Eq 'endpoint run --adapter omp-spt --id --create --start' "$MANIFEST"; then echo "FAIL fresh-endpoint update notice still uses --start (hides the harness PTY)"; fail=1; else echo "ok fresh-endpoint update notice omits --start (attach-default PTY)"; fi +# [unit->REQ-OMP-CORE-DELIVERY] +inject_activity=$(field_of inject '^[[:space:]]*activity[[:space:]]*=') +case "$inject_activity" in + 'activity = ["hook"]') echo "ok [inject] routes idle and active custody through OMP hooks" ;; + *) echo "FAIL [inject].activity must be exactly [\"hook\"]: [$inject_activity]"; fail=1 ;; +esac + # ── retired foreign-harness seams stay absent ──────────────────────────────────────────────────── # [unit->REQ-OMP-NATIVE-TUI] for stale in \ '^\[update\.post\]' \ '^\[hooks\.' \ - '^\[inject\]' \ '^\[message-idle-translation-binary\]' \ '^\[env\.SPT_INJECT_VERIFY_ECHO\]' \ '^\[env\.CLAUDE_CONFIG_DIR\]' \ '^[[:space:]]*hook_cmd[[:space:]]*=' \ '^[[:space:]]*command[[:space:]]*=.* translate'; do if grep -Eq "$stale" "$MANIFEST"; then echo "FAIL retired manifest surface remains: $stale" fail=1 fi done -if [ "$fail" -eq 0 ]; then echo "ok retired update/hook/inject/translation/env surfaces absent"; fi +if [ "$fail" -eq 0 ]; then echo "ok retired update/hook/translation/env surfaces absent"; fi # OMP model routing is native; the adapter ships no foreign profile overlay. if grep -Eq '^\[profiles\.' "$MANIFEST"; then echo "FAIL a shipped [profiles.*] table lingers"; fail=1; else echo "ok no shipped profile overlays"; fi # [int->REQ-DIST-MANIFEST-SCHEMA] — real binary validation catches role-key rules the JSON schema # cannot. Local developer machines may skip when spt is absent; a release gate must set # OMP_SPT_RELEASE_MODE=1, which makes absence a hard failure instead of a false green. if command -v spt >/dev/null 2>&1; then REG_TMP="${TMPDIR:-/tmp}/omp-spt-manifest-registration-$$" rm -rf "$REG_TMP" mkdir -p "$REG_TMP/localappdata" "$REG_TMP/canonical/strings" REG_LOCAL="$REG_TMP/localappdata" REG_MANIFEST="$MANIFEST" REG_CANONICAL="$REG_TMP/canonical/manifest.toml" case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*) REG_LOCAL=$(cygpath -w "$REG_LOCAL") REG_MANIFEST=$(cygpath -w "$REG_MANIFEST") REG_CANONICAL=$(cygpath -w "$REG_CANONICAL") ;; *) case "$(command -v spt)" in *.exe) if command -v wslpath >/dev/null 2>&1; then REG_LOCAL=$(wslpath -w "$REG_LOCAL") REG_MANIFEST=$(wslpath -w "$REG_MANIFEST") REG_CANONICAL=$(wslpath -w "$REG_CANONICAL") fi ;; esac ;; esac if register_output=$(LOCALAPPDATA="$REG_LOCAL" spt adapter add "$REG_MANIFEST" 2>&1); then echo "ok real isolated spt adapter add accepts adapter/omp-spt.toml" else echo "FAIL real isolated spt adapter add rejected manifest: $register_output" fail=1 fi cp "$MANIFEST" "$REG_TMP/canonical/manifest.toml" diff --git a/tests/omp-extension.mjs b/tests/omp-extension.mjs index d9c242f..ab2c225 100644 --- a/tests/omp-extension.mjs +++ b/tests/omp-extension.mjs @@ -401,583 +401,519 @@ async function testParsing() { /malformed EVENT attributes/, ); assert.match( drainEvents('0123456789', { maxFrameChars: 32, }).error.message, /EVENT frame exceeded/, ); } // [unit->REQ-OMP-SESSION-TITLES] async function testEndpointSessionNameAndAnimatedWindowTitle() { const harness = createHarness({ id: "emphasys", node: "HFENDULEAM", project: "omp-spt", }); await harness.emit("session_start"); assert.deepEqual(harness.sessionNames, ["emphasys @ HFENDULEAM (omp-spt/)"]); assert.deepEqual(harness.titles, ["○ emphasys @ HFENDULEAM (omp-spt/)"]); assert.equal( harness.statuses.at(-1).text, "emphasys @ HFENDULEAM (omp-spt/)", ); await harness.emit("agent_start"); assert.equal(harness.titles.at(-1), "⣾ emphasys @ HFENDULEAM (omp-spt/)"); assert.equal(harness.intervals.length, 1); assert.equal(harness.intervals[0].delay, 500); harness.intervals[0].fn(); assert.equal(harness.titles.at(-1), "⣽ emphasys @ HFENDULEAM (omp-spt/)"); await harness.emit("agent_end", { messages: [] }); assert.equal(harness.intervals[0].active, false); assert.equal(harness.titles.at(-1), "○ emphasys @ HFENDULEAM (omp-spt/)"); await harness.emit("session_shutdown"); assert.equal(harness.intervals[0].active, false); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] +// [unit->REQ-OMP-CORE-DELIVERY] // [unit->REQ-OMP-SESSION-IMMUTABLE] // [unit->REQ-OMP-MESSAGE-CONTEXT] // [unit->REQ-OMP-NATIVE-TUI] async function testLifecycleCustodyAndContext() { const harness = createHarness({ subnet: "mesh-a" }); assert.deepEqual([...harness.handlers.keys()], [ "session_start", "session_before_switch", "session_before_branch", "context", "before_agent_start", "agent_start", "agent_end", "session_stop", "session_shutdown", ]); await harness.emit("session_start"); - assert.deepEqual(harness.calls[0], { - args: [ - "api", - "--adapter", - "omp-spt", - "bind", - "omp-agent", - "--set-session-id", - "session-1", - "--subnet", - "mesh-a", - ], - input: undefined, - }); - assert.deepEqual(harness.calls[1].args, [ + assert.deepEqual(harness.calls[0].args.slice(0, 7), [ "api", "--adapter", "omp-spt", - "state", - "idle", + "bind", "omp-agent", - "--token", - "token-123", + "--set-session-id", + "session-1", ]); - assert.equal(harness.children.length, 1); - assert.equal(harness.children[0].binary, "spt-test"); - assert.deepEqual(harness.children[0].args, [ + assert.equal(stateCalls(harness).at(-1).args[4], "idle"); + assert.deepEqual(harness.children[0].args.slice(0, 7), [ "api", "--adapter", "omp-spt", "listen", "omp-agent", "--session-id", "session-1", - "--subnet", - "mesh-a", ]); for (const reason of ["new", "resume", "fork", "handoff"]) { assert.deepEqual(await harness.emit("session_before_switch", { reason }), { cancel: true }); - assert.ok( - harness.notifications.some(({ message }) => - message.includes(`${reason} session switch`), - ), - ); } assert.deepEqual(await harness.emit("session_before_branch"), { cancel: true }); - assert.ok( - harness.notifications.some(({ message }) => message.includes("session branch")), - ); const aliceEnvelope = 'hello<world
line
'; const bobEnvelope = 'second'; harness.children[0].stdout.emit("data", `${aliceEnvelope}${bobEnvelope}`); await flush(); - assert.deepEqual(harness.submitted, ['']); - assert.deepEqual(harness.submittedDeliveries, [undefined]); - assert.deepEqual(harness.sentMessages, []); assert.deepEqual( - stateCalls(harness).map((call) => call.args[4]), - ["idle", "busy"], + harness.submitted, + ['', ''], + "listener events surface independently as they arrive", ); - const originalMessages = [{ role: "user", content: '' }]; - assert.deepEqual(await harness.emit("context", { messages: originalMessages }), { - messages: [ - { - role: "user", - content: `\n\n${formatInboundEnvelope(aliceEnvelope)}`, - }, - ], - }); - + await harness.emit("before_agent_start", { prompt: "listener wake", systemPrompt: [] }); await harness.emit("agent_start"); - assert.deepEqual( - stateCalls(harness).map((call) => call.args[4]), - ["idle", "busy"], - "agent_start must not duplicate the already-honest busy transition", - ); - const aliceReply = assistantMessage([{ type: "text", text: "alice reply" }]); - await harness.emit("agent_end", { + const boundary = await harness.emit("context", { messages: [ { role: "user", content: '' }, - aliceReply, - ], - }); - assert.deepEqual(harness.clock.delays(), []); - assert.deepEqual(harness.submitted, ['', '']); - assert.deepEqual(harness.submittedDeliveries, [undefined, undefined]); - await harness.emit("agent_start"); - await harness.emit("agent_end", { - messages: [ - aliceReply, { role: "user", content: '' }, ], }); - - const outcomes = commandCalls(harness, "send"); - assert.deepEqual( - outcomes, - [], - "ordinary assistant output must never be forwarded to a peer", + assert.equal( + boundary.messages[0].content, + `\n\n${formatInboundEnvelope(aliceEnvelope)}`, ); + assert.equal( + boundary.messages[1].content, + `\n\n${formatInboundEnvelope(bobEnvelope)}`, + ); + assert.equal(harness.calls.filter((call) => call.args[3] === "poll").length, 1); + + await harness.emit("agent_end", { + messages: [...boundary.messages, assistantMessage("local result")], + }); + assert.deepEqual(commandCalls(harness, "send"), []); assert.deepEqual( stateCalls(harness).map((call) => call.args[4]), - ["idle", "busy", "idle", "busy", "idle"], + ["idle", "busy", "idle"], ); - for (const call of [...stateCalls(harness), ...harness.calls.filter((candidate) => candidate.args[3] === "session-end")]) { - assert.deepEqual(call.args.slice(-2), ["--token", "token-123"]); - } - await harness.emit("session_shutdown"); - const ended = harness.calls.filter((call) => call.args[3] === "session-end"); - assert.equal(ended.length, 1); - assert.deepEqual(ended[0].args.slice(-2), ["--token", "token-123"]); + assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); assert.equal(harness.children[0].kills, 1); - assert.deepEqual(harness.clock.delays(), []); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] +// [unit->REQ-OMP-CORE-DELIVERY] // [unit->REQ-HAZARD-ABNORMAL-TURN-RECEIVABILITY] async function testLocalAssistantOutputDoesNotReplyToPeer() { const harness = createHarness(); await harness.emit("session_start"); harness.children[0].stdout.emit( "data", 'peer reply', ); await flush(); await harness.emit("context", { messages: [{ role: "user", content: '' }], }); await harness.emit("agent_start"); await harness.emit("agent_end", { messages: [ { role: "user", content: '' }, { role: "user", content: "local user interjection" }, assistantMessage("answer intended for the local user"), ], }); assert.deepEqual( commandCalls(harness, "send"), [], "a local interjection must not be correlated back to the peer", ); assert.deepEqual(harness.clock.delays(), []); assert.deepEqual( harness.submitted, [''], "the received peer delivery completes without an implicit response", ); await harness.emit("session_shutdown"); } async function testDeferredBindLifecycleSerialization() { const busyBind = deferred(); const busyHarness = createHarness({ onRun(call) { if (call.args[0] === "api" && call.args[3] === "bind") return busyBind.promise; }, }); const startingBusy = busyHarness.emit("session_start"); await flush(); const becomingBusy = busyHarness.emit("agent_start"); await flush(); assert.deepEqual(stateCalls(busyHarness), []); assert.equal(busyHarness.children.length, 0); busyBind.resolve("BOUND endpoint token=token-busy"); await Promise.all([startingBusy, becomingBusy]); assert.deepEqual( stateCalls(busyHarness).map((call) => call.args[4]), - ["busy"], - "agent_start before bind completion must suppress the stale idle publication", + ["busy", "busy"], + "lifecycle callbacks publish current truth directly without replaying stale idle state", ); assert.equal(busyHarness.children.length, 1); await busyHarness.emit("session_shutdown"); assert.deepEqual(busyHarness.clock.delays(), []); const shutdownBind = deferred(); const shutdownHarness = createHarness({ onRun(call) { if (call.args[0] === "api" && call.args[3] === "bind") return shutdownBind.promise; }, }); const startingShutdown = shutdownHarness.emit("session_start"); await flush(); const shuttingDown = shutdownHarness.emit("session_shutdown"); await flush(); assert.equal(shutdownHarness.children.length, 0); assert.equal( shutdownHarness.calls.filter((call) => call.args[3] === "session-end").length, 0, ); shutdownBind.resolve("BOUND endpoint token=token-shutdown"); await Promise.all([startingShutdown, shuttingDown]); assert.deepEqual(stateCalls(shutdownHarness), []); assert.equal(shutdownHarness.children.length, 0); assert.equal( shutdownHarness.calls.filter((call) => call.args[3] === "session-end").length, 1, ); assert.ok( !shutdownHarness.statuses.some(({ text }) => text === "spt:omp-agent"), "bind completion after shutdown must not restore live status", ); assert.deepEqual(shutdownHarness.clock.delays(), []); const hungBindHarness = createHarness({ onRun(call) { if (call.args[0] === "api" && call.args[3] === "bind") return new Promise(() => {}); }, }); const hungStart = hungBindHarness.emit("session_start"); await flush(); const hungShutdown = hungBindHarness.emit("session_shutdown"); await flush(); assert.deepEqual(hungBindHarness.clock.delays().sort((a, b) => a - b), [300, 1_800]); await hungBindHarness.clock.runNext(300); await Promise.all([hungStart, hungShutdown]); assert.equal(hungBindHarness.children.length, 0); assert.equal( hungBindHarness.calls.filter((call) => call.args[3] === "session-end").length, 0, "session-end cannot run without a completed bind token", ); assert.deepEqual(hungBindHarness.clock.delays(), []); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] +// [unit->REQ-OMP-COMMS-RECOVERY] +async function testStateReconciliationUsesLatestActivity() { + const busyState = deferred(); + let heldBusy = false; + const harness = createHarness({ + onRun(call) { + if ( + !heldBusy && + call.args[0] === "api" && + call.args[3] === "state" && + call.args[4] === "busy" + ) { + heldBusy = true; + return busyState.promise; + } + }, + }); + await harness.emit("session_start"); + const starting = harness.emit("agent_start"); + await flush(); + await harness.emit("agent_end", { messages: [assistantMessage("done")] }); + busyState.resolve(""); + await starting; + + assert.deepEqual(harness.clock.delays(), [5]); + assert.match(harness.statuses.at(-1).text, /comms recovering/); + await harness.clock.runNext(5); + assert.deepEqual( + stateCalls(harness).map((call) => call.args[4]), + ["idle", "busy", "idle"], + "a late busy completion is corrected to the latest idle truth", + ); + assert.doesNotMatch(harness.statuses.at(-1).text, /comms recovering/); + await harness.emit("session_shutdown"); +} + +// [unit->REQ-OMP-CORE-DELIVERY] async function testSubmissionFailureAdvancesQueue() { const harness = createHarness({ onSubmit(content) { if (content === '') { throw new Error("OMP prompt flow rejected input"); } }, }); await harness.emit("session_start"); harness.children[0].stdout.emit( "data", 'onetwo', ); await flush(); assert.deepEqual( commandCalls(harness, "send"), [], "a rejected local submission must not message the peer implicitly", ); - assert.deepEqual(harness.clock.delays(), [0]); - - await harness.clock.runNext(0); + assert.deepEqual(harness.clock.delays(), []); assert.deepEqual(harness.submitted, ['', '']); + assert.ok( + harness.errors.some(({ message }) => message.includes("could not submit your message")), + ); await harness.emit("agent_start"); await harness.emit("agent_end", { messages: [ { role: "user", content: '' }, assistantMessage("next reply"), ], }); assert.deepEqual( commandCalls(harness, "send"), [], "assistant output for the next delivery must remain local", ); await harness.emit("session_shutdown"); assert.deepEqual(harness.clock.delays(), []); } +// [unit->REQ-OMP-COMMS-RECOVERY] async function testFailedIdleRecoveryFailsClosed() { let idleCalls = 0; const harness = createHarness({ - onSubmit() { - throw new Error("OMP prompt flow rejected input"); - }, onRun(call) { if (call.args[0] === "api" && call.args[3] === "state" && call.args[4] === "idle") { idleCalls += 1; if (idleCalls === 2) throw new Error("state channel unavailable"); } }, }); await harness.emit("session_start"); - harness.children[0].stdout.emit("data", 'one'); - await flush(); + await harness.emit("agent_start"); + await harness.emit("agent_end", { messages: [assistantMessage("local result")] }); - assert.deepEqual(commandCalls(harness, "send"), []); - assert.equal(harness.shutdowns, 1); - assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); - assert.ok( - harness.errors.some(({ message }) => - message.includes("could not restore idle state after a failed submission"), - ), - ); - assert.deepEqual(harness.clock.delays(), []); + assert.equal(harness.shutdowns, 0); + assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 0); + assert.deepEqual(harness.clock.delays(), [5]); + assert.match(harness.statuses.at(-1).text, /comms recovering/); + + await harness.clock.runNext(5); + assert.equal(idleCalls, 3); + assert.doesNotMatch(harness.statuses.at(-1).text, /comms recovering/); + await harness.emit("session_shutdown"); } -// [unit->REQ-OMP-LISTENER-FAIL-CLOSED] +// [unit->REQ-OMP-COMMS-RECOVERY] async function testListenerRestartExhaustion() { const harness = createHarness({ restartDelaysMs: [5, 10] }); await harness.emit("session_start"); const first = harness.children[0]; first.emit("close", 7); assert.deepEqual(harness.clock.delays(), [5]); await harness.clock.runNext(5); const second = harness.children[1]; second.stdout.emit("data", 'half'); second.emit("error", new Error("listener crashed")); second.emit("close", 8); await flush(); assert.deepEqual(harness.clock.delays(), [10], "error plus close schedules one restart"); await harness.clock.runNext(10); const third = harness.children[2]; third.emit("close", 9); await flush(); assert.equal(harness.children.length, 3); - assert.equal(harness.shutdowns, 1); - assert.ok( - harness.errors.some(({ message }) => message.includes("listener restart budget exhausted")), - ); - assert.ok( - harness.notifications.some(({ message, type }) => - type === "error" && message.includes("listener restart budget exhausted"), - ), - ); - assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); - assert.deepEqual(harness.clock.delays(), []); + assert.equal(harness.shutdowns, 0); + assert.deepEqual(harness.clock.delays(), [10]); + assert.match(harness.statuses.at(-1).text, /comms recovering/); + await harness.clock.runNext(10); + assert.equal(harness.children.length, 4, "listener retries indefinitely at capped backoff"); await harness.emit("session_shutdown"); assert.equal( harness.calls.filter((call) => call.args[3] === "session-end").length, 1, - "fatal shutdown and lifecycle shutdown share one teardown", ); } async function testListenerStableIntervalResetsRetries() { const harness = createHarness({ listenerStableMs: 20, restartDelaysMs: [5, 10], }); await harness.emit("session_start"); await harness.emit("agent_start"); assert.deepEqual(harness.clock.delays(), [20]); harness.children[0].emit("close", 1); assert.deepEqual(harness.clock.delays(), [5]); await harness.clock.runNext(5); const shortLived = harness.children[1]; shortLived.stdout.emit( "data", 'a parsed event is not stability', ); shortLived.emit("close", 2); assert.deepEqual( harness.clock.delays(), [10], "a parsed event followed by an immediate crash remains in the consecutive crash loop", ); await harness.clock.runNext(10); const stable = harness.children[2]; assert.deepEqual(harness.clock.delays(), [20]); await harness.clock.runNext(20); stable.emit("close", 3); assert.deepEqual( harness.clock.delays(), [5], "a listener surviving the stable interval resets the next retry to attempt one", ); assert.match( harness.notifications.filter(({ type }) => type === "warning").at(-1).message, - /restarting 1\/2 in 5ms/, + /retrying in 5ms/, ); await harness.clock.runNext(5); assert.equal(harness.children.length, 4); await harness.emit("session_shutdown"); assert.deepEqual(harness.clock.delays(), []); } -async function testSessionEndRetriesAfterTransientFailure() { - const firstEnd = deferred(); - let endAttempts = 0; - const harness = createHarness({ - restartDelaysMs: [], - sessionEndRetryDelaysMs: [5], - onRun(call) { - if (call.args[0] === "api" && call.args[3] === "session-end") { - endAttempts += 1; - if (endAttempts === 1) return firstEnd.promise; - } - }, - }); - await harness.emit("session_start"); - harness.children[0].emit("close", 12); - await flush(); - assert.equal( - harness.calls.filter((call) => call.args[3] === "session-end").length, - 1, - ); - - firstEnd.reject(new Error("transient teardown failure")); - await flush(); - assert.equal(harness.shutdowns, 0, "fatal close must wait for the bounded teardown retry"); - assert.deepEqual(harness.clock.delays(), [5]); - assert.ok( - harness.errors.some( - ({ message, details }) => - message.includes("session teardown failed; retrying") && - details.error.includes("transient teardown failure"), - ), - ); - - await harness.clock.runNext(5); - await flush(); - const shutdown = harness.emit("session_shutdown"); - await shutdown; - await flush(); - assert.equal( - harness.calls.filter((call) => call.args[3] === "session-end").length, - 2, - "normal fatal teardown may retry before bounded lifecycle shutdown begins", - ); - assert.equal(harness.shutdowns, 1); - assert.deepEqual(harness.clock.delays(), []); -} +// [unit->REQ-OMP-COMMS-RECOVERY] async function testHumanBusyFailureFailsClosed() { + let busyCalls = 0; const harness = createHarness({ onRun(call) { if (call.args[0] === "api" && call.args[3] === "state" && call.args[4] === "busy") { - throw new Error("state channel unavailable"); + busyCalls += 1; + if (busyCalls === 1) throw new Error("state channel unavailable"); } }, }); await harness.emit("session_start"); await harness.emit("agent_start"); - assert.equal(harness.shutdowns, 1); - assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); - assert.ok( - harness.errors.some(({ message }) => message.includes("could not mark the endpoint busy")), - ); - assert.equal(harness.children[0].kills, 1); - assert.deepEqual(harness.clock.delays(), []); + assert.equal(harness.shutdowns, 0); + assert.equal(harness.children[0].kills, 0); + assert.deepEqual(harness.clock.delays(), [5]); + assert.match(harness.statuses.at(-1).text, /comms recovering/); + await harness.clock.runNext(5); + assert.equal(busyCalls, 2); + assert.doesNotMatch(harness.statuses.at(-1).text, /comms recovering/); + await harness.emit("session_shutdown"); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] -// [unit->REQ-OMP-LISTENER-FAIL-CLOSED] +// [unit->REQ-OMP-CORE-DELIVERY] +// [unit->REQ-OMP-COMMS-RECOVERY] async function testShutdownReapsAndReleasesQueuedCustody() { let listener; let listenerWasLiveAtSessionEnd = false; const harness = createHarness({ onSpawn(child) { listener = child; }, onRun(call) { if (call.args[3] === "session-end") listenerWasLiveAtSessionEnd = listener.kills === 0; }, }); await harness.emit("session_start"); listener.stdout.emit( "data", 'onetwo', ); await flush(); await harness.emit("agent_start"); - await harness.emit("agent_end", { + const boundary = await harness.emit("context", { messages: [ { role: "user", content: '' }, - assistantMessage("done"), + { role: "user", content: '' }, ], }); + await harness.emit("agent_end", { + messages: [...boundary.messages, assistantMessage("done")], + }); assert.deepEqual(harness.submitted, ['', '']); await harness.emit("session_shutdown"); assert.equal(listener.kills, 1); assert.deepEqual(harness.clock.delays(), []); assert.deepEqual( commandCalls(harness, "send"), [], "shutdown must not synthesize outbound peer messages", ); assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); assert.equal( listenerWasLiveAtSessionEnd, true, "authenticated session-end must clear durable liveness before listener reaping spends the shutdown budget", ); assert.deepEqual(harness.statuses.at(-1), { key: "omp-spt", text: undefined }); assert.equal(harness.shutdowns, 0, "normal lifecycle shutdown must not recursively shut down OMP"); } async function testRunSptRejectsStdinErrorsAndHungCommands() { const epipeClock = new FakeClock(); const epipeChild = new FakeChild(); const epipe = Object.assign(new Error("write EPIPE"), { code: "EPIPE" }); epipeChild.stdin.onEnd = () => { epipeChild.stdin.emit("error", epipe); return false; }; await assert.rejects( runSpt(["send", "peer", "--from", "omp-agent"], "reply", { clearTimeout: epipeClock.clearTimeout.bind(epipeClock), commandTimeoutMs: 20, killForceMs: 4, killGraceMs: 3, setTimeout: epipeClock.setTimeout.bind(epipeClock), spawnProcess: () => epipeChild, }), (error) => error === epipe && error.code === "EPIPE", ); assert.deepEqual(epipeChild.killSignals, ["SIGTERM"]); assert.deepEqual(epipeClock.delays(), []); @@ -991,220 +927,231 @@ async function testRunSptRejectsStdinErrorsAndHungCommands() { await assert.rejects( runSpt(["send", "peer", "--from", "omp-agent"], "reply", { clearTimeout: fastExitClock.clearTimeout.bind(fastExitClock), commandTimeoutMs: 20, killForceMs: 4, killGraceMs: 3, setTimeout: fastExitClock.setTimeout.bind(fastExitClock), spawnProcess: () => fastExitChild, }), /exited before stdin completed/, ); assert.deepEqual(fastExitChild.killSignals, []); assert.deepEqual(fastExitClock.delays(), []); const commandCases = [ ["api", "--adapter", "omp-spt", "bind", "omp-agent"], ["send", "peer", "--from", "omp-agent"], ["api", "--adapter", "omp-spt", "state", "idle", "omp-agent"], ["api", "--adapter", "omp-spt", "session-end", "omp-agent"], ]; for (const args of commandCases) { const clock = new FakeClock(); const child = new FakeChild(); const pending = runSpt(args, args[0] === "send" ? "outcome" : undefined, { clearTimeout: clock.clearTimeout.bind(clock), commandTimeoutMs: 7, killForceMs: 4, killGraceMs: 3, setTimeout: clock.setTimeout.bind(clock), spawnProcess: () => child, }); const rejected = assert.rejects(pending, /timed out after 7ms/); assert.deepEqual(clock.delays(), [7]); await clock.runNext(7); await rejected; assert.deepEqual(child.killSignals, ["SIGTERM"]); assert.deepEqual(clock.delays(), []); } } -// [unit->REQ-OMP-LISTENER-FAIL-CLOSED] +// [unit->REQ-OMP-COMMS-RECOVERY] async function testListenerTerminationEscalatesAndReaps() { const harness = createHarness({ killForceMs: 4, killGraceMs: 3, onSpawn(child) { child.onKill = (signal) => { if (signal === "SIGKILL") child.close(null, signal); return true; }; }, }); await harness.emit("session_start"); const listener = harness.children[0]; const shutdown = harness.emit("session_shutdown"); await flush(); assert.deepEqual(listener.killSignals, ["SIGTERM"]); assert.deepEqual(harness.clock.delays().sort((a, b) => a - b), [3, 1_800]); await harness.clock.runNext(3); await shutdown; assert.deepEqual(listener.killSignals, ["SIGTERM", "SIGKILL"]); assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); assert.deepEqual(harness.clock.delays(), []); const errorHarness = createHarness({ killForceMs: 4, killGraceMs: 3, onSpawn(child) { child.onKill = (signal) => { if (signal === "SIGKILL") child.close(null, signal); return true; }; }, }); await errorHarness.emit("session_start"); const erroredListener = errorHarness.children[0]; erroredListener.emit("error", new Error("listener pipe failed")); const concurrentShutdown = errorHarness.emit("session_shutdown"); await flush(); assert.deepEqual(erroredListener.killSignals, ["SIGTERM"]); assert.deepEqual(errorHarness.clock.delays().sort((a, b) => a - b), [3, 1_800]); await errorHarness.clock.runNext(3); await concurrentShutdown; assert.deepEqual(erroredListener.killSignals, ["SIGTERM", "SIGKILL"]); assert.equal( errorHarness.calls.filter((call) => call.args[3] === "session-end").length, 1, ); assert.deepEqual(errorHarness.clock.delays(), []); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] -// [unit->REQ-OMP-LISTENER-FAIL-CLOSED] +// [unit->REQ-OMP-COMMS-RECOVERY] async function testProtocolCorruptionFailsClosed() { async function failProtocol(payload, expected, options = {}) { - const harness = createHarness({ restartDelaysMs: [], ...options }); + const harness = createHarness({ restartDelaysMs: [5], ...options }); await harness.emit("session_start"); harness.children[0].stdout.emit("data", payload); await flush(); - assert.equal(harness.shutdowns, 1); + assert.equal(harness.shutdowns, 0); assert.deepEqual(harness.submitted, []); assert.deepEqual(commandCalls(harness, "send"), []); assert.ok( harness.errors.some( ({ message, details }) => message.includes("listener protocol corruption") && expected.test(details.error), ), ); assert.equal(harness.children[0].kills, 1); - assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); - assert.deepEqual(harness.clock.delays(), []); + assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 0); + assert.deepEqual(harness.clock.delays(), [5]); + await harness.clock.runNext(5); + assert.equal(harness.children.length, 2); await harness.emit("session_shutdown"); return harness; } await failProtocol( 'truncatedvalid'.padEnd( 128, "x", ), /buffer exceeded/, { listenerBufferLimit: 96 }, ); await failProtocol('missing sender', /missing EVENT from/); await failProtocol('bad attrs', /malformed EVENT attributes/); await failProtocol('never closes'.padEnd(80, "x"), /buffer exceeded/, { listenerBufferLimit: 64, }); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] -// [unit->REQ-OMP-LISTENER-FAIL-CLOSED] +// [unit->REQ-OMP-CORE-DELIVERY] +// [unit->REQ-OMP-COMMS-RECOVERY] async function testInboundQueueOverflowReleasesAcceptedCustody() { const frames = ["a", "b", "overflow"].map( (from) => `work`, ); const harness = createHarness({ acceptedQueueLimit: 2, - restartDelaysMs: [], + restartDelaysMs: [5], }); await harness.emit("session_start"); harness.children[0].stdout.emit("data", frames.join("")); await flush(); - assert.equal(harness.shutdowns, 1); - assert.deepEqual(harness.submitted, []); - assert.deepEqual( - commandCalls(harness, "send"), - [], - "capacity failure must not synthesize outbound peer messages", - ); + assert.equal(harness.shutdowns, 0); + assert.deepEqual(harness.submitted, ['', '']); + assert.deepEqual(commandCalls(harness, "send"), []); assert.ok( harness.errors.some(({ message }) => - message.includes("inbound custody capacity exceeded"), + message.includes("inbound listener capacity exceeded"), ), ); - assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); - assert.deepEqual(harness.clock.delays(), []); + assert.equal(harness.children[0].kills, 1); + assert.deepEqual(harness.clock.delays(), [5]); + + await harness.emit("agent_start"); + const boundary = await harness.emit("context", { + messages: [ + { role: "user", content: '' }, + { role: "user", content: '' }, + ], + }); + assert.equal(boundary.messages.length, 2); + assert.deepEqual( + harness.submitted, + ['', '', ''], + "freeing observed custody admits the held overflow item without loss", + ); await harness.emit("session_shutdown"); const byteFirst = 'x'; const byteOverflow = '😀'; const byteHarness = createHarness({ acceptedBytesLimit: Buffer.byteLength(byteFirst, "utf8") + byteOverflow.length, acceptedQueueLimit: 10, - restartDelaysMs: [], + restartDelaysMs: [5], }); await byteHarness.emit("session_start"); byteHarness.children[0].stdout.emit("data", `${byteFirst}${byteOverflow}`); await flush(); - assert.deepEqual(commandCalls(byteHarness, "send"), []); - assert.equal(byteHarness.shutdowns, 1); - assert.deepEqual(byteHarness.clock.delays(), []); + assert.deepEqual(byteHarness.submitted, ['']); + assert.equal(byteHarness.shutdowns, 0); + assert.deepEqual(byteHarness.clock.delays(), [5]); await byteHarness.emit("session_shutdown"); } -// [unit->REQ-OMP-EXTENSION-CUSTODY] -// [unit->REQ-OMP-LISTENER-FAIL-CLOSED] +// [unit->REQ-OMP-CORE-DELIVERY] +// [unit->REQ-OMP-COMMS-RECOVERY] async function testShutdownFallbackStaysBelowHostCap() { const never = new Promise(() => {}); const harness = createHarness({ shutdownBudgetMs: 1_800, shutdownCommandTimeoutMs: 300, onRun(call) { if (call.args[3] === "session-end") return never; }, }); await harness.emit("session_start"); const shutdown = harness.emit("session_shutdown"); await flush(); assert.deepEqual( harness.clock.delays().sort((a, b) => a - b), [300, 1_800], "session-end has a short command timeout inside the 2s host cap", ); await harness.clock.runNext(300); await shutdown; assert.equal(harness.children[0].kills, 1); assert.deepEqual(commandCalls(harness, "send"), []); assert.equal(harness.calls.filter((call) => call.args[3] === "session-end").length, 1); assert.deepEqual(harness.clock.delays(), []); assert.ok(1_800 < 2_000); } // [unit->REQ-PARITY-READY-ACTIVATION] // [unit->REQ-PARITY-LIVE-ACTIVATION] async function testNativeActivationCommandsAndErrors() { const inert = createHarness({ id: null }); assert.deepEqual([...inert.commands.keys()], ["ready", "live"]); assert.ok(inert.tools.has("spt_checkpoint")); await inert.emit("session_start"); assert.deepEqual(inert.calls, [], "an ordinary OMP session must remain lifecycle-inert"); assert.equal( await inert.emit("session_before_switch", { reason: "new" }), undefined, "an unbound extension must not block native session changes", @@ -1586,133 +1533,137 @@ async function testStartupBriefHintsAndUpdateNotices() { } else if (call.args[0] === "adapter" && call.args[1] === "version") { value = "0.2.0"; } else { return; } return new Promise((resolve) => { delayedClock.setTimeout(() => resolve(value), 5_000); }); }, }); delayedClock = delayed.clock; await delayed.emit("session_start"); let firstContextSettled = false; const delayedFirstContext = delayed .emit("before_agent_start", { prompt: "Continue.", systemPrompt: [] }) .then((result) => { firstContextSettled = true; return result; }); await flush(); assert.equal( firstContextSettled, true, "the first-turn context must not await slow local update commands", ); const firstContext = await delayedFirstContext; assert.ok(firstContext.systemPrompt.at(-1).includes("OMP SPT endpoint")); assert.ok(!firstContext.systemPrompt.at(-1).includes("OMP SPT updates:")); assert.deepEqual(delayed.clock.delays(), [5_000, 5_000, 5_000, 5_000]); for (let index = 0; index < 4; index += 1) await delayed.clock.runNext(5_000); assert.equal(delayed.sentMessages.length, 1); assert.deepEqual(delayed.sentMessages[0].delivery, { deliverAs: "nextTurn", triggerTurn: false, }); assert.match(delayed.sentMessages[0].message.content, /spt-core v1\.2\.0 is available/); assert.match(delayed.sentMessages[0].message.content, /omp-spt v0\.3\.0 is available/); await delayed.emit("session_shutdown"); } +// [unit->REQ-OMP-CORE-DELIVERY] // [unit->REQ-PARITY-SAFE-BOUNDARY-DELIVERY] // [unit->REQ-HAZARD-ABNORMAL-TURN-RECEIVABILITY] async function testActiveTurnBoundaryDeliveryAndFallback() { - const harness = createHarness(); + let pollCalls = 0; + const polledEnvelope = 'busy'; + const harness = createHarness({ + onRun(call) { + if (call.args[0] === "api" && call.args[3] === "poll") { + pollCalls += 1; + return pollCalls === 1 ? polledEnvelope : ""; + } + }, + }); await harness.emit("session_start"); + await harness.emit("before_agent_start", { prompt: "operator prompt", systemPrompt: [] }); await harness.emit("agent_start"); const firstEnvelope = 'one'; const secondEnvelope = 'two'; harness.children[0].stdout.emit("data", `${firstEnvelope}${secondEnvelope}`); await flush(); - assert.deepEqual(harness.submitted, ['']); + assert.deepEqual( + harness.submitted, + ['', ''], + "live-listener arrivals use the native steer path independently", + ); const boundary = await harness.emit("context", { messages: [ - { role: "user", content: "operator prompt" }, { role: "user", content: '' }, + { role: "user", content: '' }, ], }); assert.equal( - boundary.messages.at(-1).content, + boundary.messages[0].content, `\n\n${formatInboundEnvelope(firstEnvelope)}`, - "an active-turn arrival must steer the running model at its next boundary", ); - const afterToolBoundary = await harness.emit("context", { - messages: [ - { role: "user", content: "operator prompt" }, - { role: "user", content: '' }, - assistantMessage("", { stopReason: "toolUse", toolCalls: [{ name: "read" }] }), - { role: "toolResult", content: "tool output" }, - ], - }); assert.equal( - afterToolBoundary.messages[1].content, - `\n\n${formatInboundEnvelope(firstEnvelope)}`, - "the steered delivery envelope must persist across later model continuations", - ); + boundary.messages[1].content, + `\n\n${formatInboundEnvelope(secondEnvelope)}`, + ); + assert.deepEqual(boundary.messages.at(-1), { + role: "custom", + customType: "spt-event", + content: polledEnvelope, + display: false, + attribution: "user", + timestamp: boundary.messages.at(-1).timestamp, + }); + assert.equal(pollCalls, 1, "busy context boundaries drain active-only core custody"); + assert.deepEqual(harness.sentMessages, [], "busy poll output adds no user-visible panel"); + await harness.emit("agent_end", { - messages: [ - { role: "user", content: "operator prompt" }, - assistantMessage("", { stopReason: "toolUse", toolCalls: [{ name: "read" }] }), - { role: "toolResult", content: "tool output" }, - assistantMessage("first outcome"), - ], + messages: [...boundary.messages, assistantMessage("local outcome")], }); - assert.deepEqual( - commandCalls(harness, "send"), - [], - "assistant output at an active boundary must remain local", - ); + assert.deepEqual(commandCalls(harness, "send"), []); assert.deepEqual(harness.clock.delays(), []); - assert.deepEqual(harness.submitted, ['', '']); - assert.deepEqual(harness.submittedDeliveries, [undefined, undefined]); - assertNoAgentManagedPoll(harness); await harness.emit("session_shutdown"); } // [unit->REQ-HAZARD-ABNORMAL-TURN-RECEIVABILITY] async function testAbnormalTurnsRestoreReceivability() { for (const [label, completionEvent] of [ ["cancelled", "session_stop"], ["interrupted", "session_stop"], ["failed", "agent_end"], ]) { const harness = createHarness(); await harness.emit("session_start"); await harness.emit("agent_start"); harness.children[0].stdout.emit( "data", `work`, ); await flush(); const boundary = await harness.emit("context", { messages: [ { role: "user", content: "active work" }, { role: "user", content: `` }, ], }); await harness.emit(completionEvent, { messages: boundary.messages }); assert.deepEqual(commandCalls(harness, "send"), []); assert.equal(stateCalls(harness).at(-1).args[4], "idle"); harness.children[0].stdout.emit( "data", `next`, ); await flush(); assert.deepEqual(harness.submitted, [ ``, ``, ]); await harness.emit("session_shutdown"); } } @@ -1994,64 +1945,64 @@ async function testNativeCheckpointTool() { if (call.args[0] === "--json" && call.args[2] === "endpoint-info") { return JSON.stringify({ endpoint_type: "ready_agent" }); } }, }); await launchedReady.emit("session_start"); const launchedRefusal = await launchedReady.tools .get("spt_checkpoint") .execute("tool-launch-ready", {}, undefined, undefined, launchedReady.ctx); assert.equal(launchedRefusal.details.reason, "not-live"); assert.deepEqual(launchedReady.compactions, []); await launchedReady.emit("session_shutdown"); const failed = createHarness({ onCompact() { throw new Error("native compaction cancelled"); }, onRun(call) { if (call.args[0] === "--json" && call.args[2] === "endpoint-info") { return JSON.stringify({ endpoint_type: "live_agent" }); } }, }); await failed.emit("session_start"); const failure = await failed.tools .get("spt_checkpoint") .execute("tool-3", {}, undefined, undefined, failed.ctx); assert.equal(failure.isError, true); assert.match(failure.content[0].text, /native compaction cancelled/); assert.deepEqual(failed.sentMessages, [], "a failed reset must never queue a false wake"); await failed.emit("session_shutdown"); } await testParsing(); await testEndpointSessionNameAndAnimatedWindowTitle(); await testRunSptRejectsStdinErrorsAndHungCommands(); await testLifecycleCustodyAndContext(); await testLocalAssistantOutputDoesNotReplyToPeer(); await testDeferredBindLifecycleSerialization(); +await testStateReconciliationUsesLatestActivity(); await testSubmissionFailureAdvancesQueue(); await testFailedIdleRecoveryFailsClosed(); await testListenerRestartExhaustion(); await testListenerStableIntervalResetsRetries(); -await testSessionEndRetriesAfterTransientFailure(); await testHumanBusyFailureFailsClosed(); await testShutdownReapsAndReleasesQueuedCustody(); await testListenerTerminationEscalatesAndReaps(); await testProtocolCorruptionFailsClosed(); await testInboundQueueOverflowReleasesAcceptedCustody(); await testShutdownFallbackStaysBelowHostCap(); await testNativeActivationCommandsAndErrors(); await testNativeActivationSelectionAndCompletion(); await testPlatformAwareActivationCandidatePaths(); await testExplicitLiveAutoResume(); await testStartupBriefHintsAndUpdateNotices(); await testActiveTurnBoundaryDeliveryAndFallback(); await testAbnormalTurnsRestoreReceivability(); await testCompletionStopReasonGatesSideEffects(); await testCompactionPreservesLocalAssistantOutput(); await testPeerShortformParsingAndDispatch(); await testNativeCheckpointTool(); console.log("OMP-EXTENSION OK"); diff --git a/traceable-reqs.toml b/traceable-reqs.toml index 41f26d8..37aa14b 100644 --- a/traceable-reqs.toml +++ b/traceable-reqs.toml @@ -1,124 +1,124 @@ # OMP-native requirement registry. Evidence roots are intentionally limited to # the retained adapter surface; retired foreign-harness files are not scanned. [scan] roots = [ "OMP-ADAPTER-PLAN.md", "docs/KNOWN-HAZARDS.md", "docs/adr/0007-native-omp-tui-hosts-spt-extension.md", "docs/adr/0008-omp-native-product-boundary.md", "docs/adr/0009-all-endpoints-use-native-omp.md", - "docs/adr/0010-native-delivery-self-heals-or-closes.md", + "docs/adr/0010-native-delivery-recovers-without-stopping-local-work.md", "docs/adr/0011-endpoint-session-binding-is-immutable.md", "docs/adr/0012-continuity-drops-live-under-spt.md", "docs/adr/0013-release-gate-stops-at-the-adapter-boundary.md", "docs/PARITY.md", "docs/adr/0015-extension-owned-session-activation.md", "docs/adr/0017-active-turn-delivery-uses-safe-boundaries.md", "docs/adr/0016-agent-capabilities-split-by-native-seam.md", "docs/adr/0018-checkpoint-resets-context-natively.md", "adapter/omp-spt.toml", "adapter/strings/omp-spt.mjs", "adapter/strings/skills", "tools/omp-spt/Cargo.toml", "tools/omp-spt/src/main.rs", "tools/omp-spt/src/digest_omp.rs", "tools/omp-spt/src/history_omp.rs", "tools/omp-spt/src/echo_commune_omp.rs", "tools/omp-spt/src/psyche_omp.rs", "tools/omp-spt/src/launch_omp.rs", "tools/omp-spt/tests/launch_omp.rs", "tools/omp-spt/tests/captured_session_env.rs", "tests/manifest-shortcut.sh", "tests/native-launch-manifest.sh", "tests/omp-extension.mjs", "tests/omp-skills.mjs", "tests/manifest-schema.sh", "tests/adapter-archive.sh", "ci/manifest/check-manifest.sh", "ci/manifest/validate_manifest.py", "ci/publish/package-adapter.sh", "ci/publish/release-acquire-int.sh", "ci/digest/build.sh", "ci/digest/digest-proof-int.sh", "docs/RELEASE-RUNBOOK.md", "docs-site/README.md", "docs-site/src/reference/release-evidence.md", ".github/workflows/docs-pages.yml", "ci/docs/build-docs.py", "ci/release/check-version-consistency.py", "ci/release/validate-release-evidence.py", "ci/run-gates.sh", "tests/docs-gate.py", "tests/release-evidence.py", "tests/version-consistency.py", ] [policy] required_stages = [] [[requirements]] id = "REQ-OMP-NATIVE-TUI" title = "The hosted launch contract hands validated OMP its packaged extension as the broker PTY process" required_stages = ["doc", "impl", "unit", "int"] # Intentionally inactive until an actual release runs the ADR-0013 acceptance gate. Deterministic # fake-process launch evidence must never satisfy this live endpoint requirement. [[requirements]] id = "REQ-OMP-LIVE-RELEASE-GATE" title = "Pending live gate: a real OMP release must satisfy the ADR-0013 endpoint acceptance criteria" required_stages = [] [[requirements]] id = "REQ-OMP-EXECUTABLE-RESOLUTION" title = "Fresh, resumed, and daemon-driven OMP turns resolve and validate Oh My Pi, reject executable collisions, and fail loudly" required_stages = ["doc", "impl", "unit", "int"] [[requirements]] -id = "REQ-OMP-EXTENSION-CUSTODY" -title = "The OMP extension serializes accepted deliveries while outbound peer messages require an explicit CLI or shortform action" +id = "REQ-OMP-CORE-DELIVERY" +title = "spt-core retains durable message custody while listener and poll deliveries independently enter OMP context and outbound peer messaging remains explicit" required_stages = ["doc", "impl", "unit"] [[requirements]] -id = "REQ-OMP-LISTENER-FAIL-CLOSED" -title = "Unexpected listener exit retries within a finite budget and then performs SPT session-end plus OMP shutdown" +id = "REQ-OMP-COMMS-RECOVERY" +title = "An established endpoint keeps local OMP work alive while listener, activity, and poll communications recover visibly" required_stages = ["doc", "impl", "unit"] [[requirements]] id = "REQ-OMP-SESSION-IMMUTABLE" title = "One endpoint owns one OMP session for its lifetime and blocks every in-TUI session-changing action" required_stages = ["doc", "impl", "unit"] [[requirements]] id = "REQ-OMP-SESSION-TITLES" title = "Hosted OMP sessions use endpoint, node, and project names while the terminal title visibly tracks idle and animated busy state" required_stages = ["doc", "impl", "unit"] [[requirements]] id = "REQ-OMP-MESSAGE-CONTEXT" title = "Each peer delivery opens one ordinary OMP turn containing a message stub and the complete SPT event context" required_stages = ["doc", "impl", "unit"] [[requirements]] id = "REQ-OMP-READY-LIVE" title = "The adapter advertises exactly ReadyAgent and LiveAgent endpoint types on the same native OMP hosting path" required_stages = ["doc", "unit"] [[requirements]] id = "REQ-OMP-CONTINUITY-DROPS" title = "Commune and signoff drops resolve project-locally under .spt on the published v0.29.0 endpoint-cwd contract" required_stages = ["doc", "unit"] [[requirements]] id = "REQ-PSYCHE-EPHEMERAL-SHIM" title = "Each LiveAgent Psyche event runs as one bounded OMP turn with file-backed context and explicit reseed signaling" required_stages = ["doc", "impl", "unit"] [[requirements]] id = "REQ-HISTORY-FETCHER" title = "The history fetcher locates exactly one OMP session and streams its JSONL bytes verbatim" required_stages = ["doc", "impl", "unit"] [[requirements]] id = "REQ-DIST-DIGEST-EXTRACTOR" title = "The OMP digest fetcher maps native session JSONL into the published harness-neutral digest record stream"