Cover the remaining role-carrying events with test_sink assertions:
session.started (ctx-held role), session.resumed and session.ended
(session-held role), turn.interrupted and turn.failed. Correct the
mcp_factory race comment (the loser fires a second plain connected and
overwrites the winner's map entry, not a reconnect) and the role_extras
doc (derived roles fall through to the session-held role).
- McpFactory::acquire now reads the live probe and the reconnect flag
under one lock section so a racing first connect cannot be
misreported as COYOTE_MCP_RECONNECT=true; the residual window across
the spawn await (no singleflight) is documented. try_get_active,
now unused, is removed.
- All three RAG sync funnels (init, init_with_config,
refresh_document_paths) fire rag.sync.completed only after a
successful save(), matching the documented contract; a save error
routes through rag.sync.failed with COYOTE_ERROR before propagating.
- New funnel-level tests pin init_with_config's started-then-completed
ordering, the failed event on a save error, and the failed event on
the sync Err arm that a user abort surfaces through.
- Register five new events (26 -> 31): rag.sync.started/completed/failed
and mcp.server.connected/failed, whitelist-gated for agent global hooks
like every non-escalation event
- Bracket every real RAG document sync with rag.sync.* via pre-resolved
RagSyncHooks snapshots carried into the build funnels; cache hits, pure
loads, and attaches fire nothing, and a user abort mid-sync counts as a
failure
- Fire mcp.server.connected only on real spawns in McpFactory::acquire
(reusing a live server is silent), marking respawns of a previously
live key with COYOTE_MCP_RECONNECT; mcp.server.failed carries
COYOTE_ERROR and COYOTE_MCP_AUTH_REQUIRED on auth failures. Boot-time
registry connects deliberately fire nothing
- Carry COYOTE_ROLE on session.* and turn.* events, resolved at fire
time so mid-session role switches and temp roles are reflected
The advisory flock in run_checks.py uses one system-wide lock path
(~/.cache/coyote/review-stack.lock). Concurrent runs of the timeout/deadline
tests contend on it: the losers sleep in the bounded retry loop, the total
deadline (computed before lock acquisition) expires, and commands record
'skipped: deadline' instead of the expected timeout/kill behavior.
- run_checks.py: ADVERSARY_STACK_LOCK_PATH env override relocates the lock
file (isolation seam, documented alongside the other env seams); every
other lock semantic (default path, bounded wait, proceed-unlocked warning,
fd held for process lifetime, skip when no commands) is unchanged.
- run_checks.py: guard the POSIX-only fcntl import — windows-latest CI has
python3 and runs these tests, so the unconditional import crashed the
script there; on non-POSIX the lock is now skipped with a stderr WARN,
matching the script's documented non-POSIX limitations.
- tests: the shared adversary_script_command helper points every spawned
script at its own throwaway lock path, isolating the run_checks tests from
each other and from any real review running on the same machine.
- tests: adversary_run_checks_stack_lock_path_is_env_overridable pins the
override (lock file appears at the overridden path across two sequential
runs; no lock file is created when no commands are declared).
cmd opens '>>' redirection targets write-exclusive, so when the STARTED
and terminal hook children appended to one shared log concurrently, the
loser of the open race hit a sharing violation and silently dropped its
marker line -- a permanent loss that surfaced as an intermittent
Windows-CI-only failure of the macro-bracketing suite (the engine's exit
drain was verified sound: spawn intent is registered synchronously at
fire time and covers macro-context fires). One file per event makes
writer overlap impossible while keeping every exact-count assertion
byte-identical.
Hook processes are fire-and-forget: the binary's exit drain acknowledges
spawns only, so the STARTED writer can land after the terminal one. The
macro-bracketing tests waited only for the terminal marker and then
asserted STARTED's count immediately, flaking on Windows CI and loaded
Linux runners. The bounded wait now covers every expected marker before
the exact-count and absence assertions run; the probe script's scenario K
comment is corrected to state the spawn-ack-only contract it already
waits around.
The fixture's hook commands were sh-only: single-quoted redirect targets
are literal under cmd /C, so the marker log never received content and
all six tests timed out on windows-latest. Build the marker command
per-platform — parenthesized `(echo MARKER)>> "log"` on Windows to
avoid cmd's trailing-space echo gotcha — and move the interpolated
paths into single-quoted YAML scalars so Windows backslashes are not
read as double-quoted-scalar escapes. Also replace the cleanup call
that a wait timeout skipped with a drop guard, so failed runs no
longer leak temp dirs.
Windows links the process main thread with a ~1 MiB stack (unix defaults
to 8 MiB). Coyote's root async future is a compiler-generated state
machine whose debug-build poll frames blow through 1 MiB the moment
block_on polls it, so every invocation of the binary on Windows died at
startup with STATUS_STACK_OVERFLOW (0xC00000FD) before doing any work.
Replace #[tokio::main] with an explicit bootstrap: main() spawns a
dedicated thread (named "main", 16 MiB stack) that builds the same
multi-thread enable_all runtime and block_on's the async body, then joins
and propagates the Result unchanged; panics are re-raised on the real
main thread via resume_unwind without re-invoking the panic hook, so
panic output and exit codes stay identical. Runtime worker threads get
the same 16 MiB headroom since spawned tasks embed the same class of
giant state machine.
The bootstrap is unconditional rather than cfg(windows)-gated: the unix
arrangement is behaviorally identical (same runtime flavor and driver
set — tokio's ctrl_c signal driver is thread-agnostic), and one shared
code path removes the entire small-main-stack class everywhere instead
of special-casing one OS. Verified on Linux with the full test suite
(inspection_flags 12/12) and the hooks usage probe end-to-end, including
the terminal ctrl-c -> agent.interrupted scenario.
On NTFS the fixture self-collides: .builtin-manifest auto-generates the
8.3 short name BUILTI~1, so writing that fixture file opens the manifest
through its alias and clobbers it -- the exact threat the guard models.
Gate the fixture-based test to unix, and add a windows counterpart that
proves the collision where 8.3 generation is enabled and asserts the
filter invariant (alias-shaped entries are neither removal candidates
nor claimed) either way.