Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
80db87b
feat(shepherd-sdk): workspace + skeleton (BLEU-835)
brunota20 Jun 17, 2026
99141b6
feat(shepherd-sdk): extract shared helpers from M2 modules (BLEU-840)
brunota20 Jun 17, 2026
b93b93e
refactor(modules): consume shepherd-sdk helpers (BLEU-843)
brunota20 Jun 17, 2026
4d325a1
feat(shepherd-sdk-test): in-memory host mocks for module tests (BLEU-…
brunota20 Jun 17, 2026
a4f54c1
docs(shepherd-sdk): rustdoc polish + README + docs/sdk.md (BLEU-844)
brunota20 Jun 17, 2026
3686969
feat(examples): price-alert Chainlink oracle reader (BLEU-846)
brunota20 Jun 17, 2026
0bfe67c
feat(examples): balance-tracker example module (BLEU-847)
brunota20 Jun 17, 2026
a989327
docs(tutorial): first-module walkthrough (BLEU-848)
brunota20 Jun 17, 2026
4915813
chore: rust-idiomatic compliance pass across M3 + M2 modules
brunota20 Jun 17, 2026
83b1996
refactor(price-alert): port to Host trait + MockHost tests (BLEU-851)
brunota20 Jun 17, 2026
895b1b7
feat(examples): stop-loss module + tutorial as guided tour (BLEU-852)
brunota20 Jun 17, 2026
36b0e1c
refactor(twap-monitor): port to Host trait + MockHost tests (BLEU-854)
brunota20 Jun 17, 2026
cb9dc1e
refactor(ethflow-watcher): port to Host trait + MockHost tests (BLEU-…
brunota20 Jun 17, 2026
97ff7aa
chore(qa): workspace cargo fmt sweep + em-dash cleanup (COW-1063)
brunota20 Jun 17, 2026
955bbd1
docs(qa): COW-1063 sign-off matrix + architectural findings
brunota20 Jun 17, 2026
e271895
docs: resolve QA findings - ADR-0009 + doc 05 status callouts
brunota20 Jun 17, 2026
748f146
ci: build all production module .wasm targets via matrix (COW-1066)
brunota20 Jun 17, 2026
97d6885
ci: gate cargo doc warnings (-D warnings) + fix 3 broken intra-doc li…
brunota20 Jun 17, 2026
6171f4d
docs(shepherd-sdk): add 6 doctests covering Host trait + helper API (…
brunota20 Jun 17, 2026
769d88d
test(nexum-engine): supervisor integration tests for 5 production mod…
brunota20 Jun 17, 2026
fa8eb8f
docs(m2): testnet runbook + engine.m2.toml + `just run-m2` (validated…
brunota20 Jun 17, 2026
ab6d633
fix(event_loop): do not bail boot when block / log stream Vec is empty
brunota20 Jun 18, 2026
20c4933
docs(m3): testnet runbook + engine.m3.toml + `just run-m3` (validated…
brunota20 Jun 18, 2026
b92c200
docs(m3): testnet edge-case validation report - 5 scenarios run, all …
brunota20 Jun 18, 2026
663dc0b
fix(supervisor): mark module alive=false when init returns Err (COW-1…
brunota20 Jun 18, 2026
73c142c
review: address jeffersonBastos M3 epic feedback (PR #55)
brunota20 Jun 22, 2026
422646c
chore(rust-idiomatic): M3 compliance pass (filtered from M4/M5 compli…
brunota20 Jun 23, 2026
2cb4927
docs(deployment): operator runbook (BLEU-836) (#17)
brunota20 Jun 24, 2026
e4d0aa5
chore(shepherd-sdk): derive strum::IntoStaticStr + non_exhaustive on …
brunota20 Jun 25, 2026
c20cd08
chore(twap-monitor): derive strum::IntoStaticStr + non_exhaustive on …
brunota20 Jun 25, 2026
bc1e77d
fix(balance-tracker): replace Result<_, String> with typed AddressLis…
brunota20 Jun 25, 2026
4066993
feat(shepherd-sdk): consolidate AddressParse helper from balance-trac…
brunota20 Jun 25, 2026
1674cb8
chore: align module deps with workspace + add ModuleLimits to M3 tests
jean-neiverth Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,46 @@ jobs:
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo test --workspace --all-features --no-fail-fast

docs:
name: rustdoc
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: "-D warnings"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master 2026-03-27
with:
toolchain: nightly
targets: wasm32-wasip2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo doc --workspace --no-deps

build-module:
name: build example module
name: build ${{ matrix.module }} (wasm32-wasip2)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module:
- example
- twap-monitor
- ethflow-watcher
- price-alert
- balance-tracker
- stop-loss
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master 2026-03-27
with:
toolchain: nightly
targets: wasm32-wasip2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo build -p example --target wasm32-wasip2 --release
- run: cargo build -p ${{ matrix.module }} --target wasm32-wasip2 --release
- name: report wasm size
run: |
artifact_name=$(echo "${{ matrix.module }}" | tr '-' '_')
wasm_path="target/wasm32-wasip2/release/${artifact_name}.wasm"
if [ -f "$wasm_path" ]; then
size=$(wc -c < "$wasm_path")
echo "${{ matrix.module }} .wasm size: ${size} bytes"
fi
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
[workspace]
members = [
"crates/nexum-engine",
"crates/shepherd-sdk",
"crates/shepherd-sdk-test",
"modules/ethflow-watcher",
"modules/example",
"modules/examples/balance-tracker",
"modules/examples/price-alert",
"modules/examples/stop-loss",
"modules/twap-monitor",
]
resolver = "2"
Expand Down
22 changes: 19 additions & 3 deletions crates/nexum-engine/src/runtime/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! supervisor until a shutdown signal arrives.

use futures::StreamExt;
use futures::stream::{FuturesUnordered, select_all};
use futures::stream::{BoxStream, FuturesUnordered, select_all};
use tracing::{info, warn};

use crate::bindings::nexum;
Expand Down Expand Up @@ -93,8 +93,24 @@ pub async fn run(
log_streams: Vec<TaggedLogStream>,
shutdown: impl std::future::Future<Output = ()> + Send,
) {
let mut blocks = select_all(block_streams);
let mut logs = select_all(log_streams);
// `select_all` over an empty Vec yields `None` immediately, which
// would trip the "stream ended -> shut down" arm below before the
// first block / log ever flows. Engine configs that subscribe to
// only one event kind (e.g. all modules use `[[subscription]] kind
// = "block"`) are valid and must not be punished. Replace each
// empty side with `stream::pending()` so the corresponding select
// arm is never selected; the bail-on-None semantic still fires
// when a *non-empty* stream actually closes.
let mut blocks: BoxStream<'_, _> = if block_streams.is_empty() {
futures::stream::pending().boxed()
} else {
select_all(block_streams).boxed()
};
let mut logs: BoxStream<'_, _> = if log_streams.is_empty() {
futures::stream::pending().boxed()
} else {
select_all(log_streams).boxed()
};
let mut shutdown = Box::pin(shutdown);
loop {
tokio::select! {
Expand Down
44 changes: 31 additions & 13 deletions crates/nexum-engine/src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ impl Supervisor {
.with_context(|| format!("load module {}", entry.path.display()))?;
modules.push(loaded);
}
info!(count = modules.len(), "supervisor up");
let alive = modules.iter().filter(|m| m.alive).count();
info!(loaded = modules.len(), alive, "supervisor up");
Ok(Self { modules })
}

Expand Down Expand Up @@ -216,21 +217,38 @@ impl Supervisor {
} else {
loaded_manifest.config.clone()
};
match bindings
// Whether `init` returned `Ok(())`. When `init` returns
// `Err(HostError)` the module's strategy state (e.g. an
// `OnceLock<Settings>`) is left uninitialised. Existing M3
// example modules short-circuit on the missing state via
// `SETTINGS.get().is_none() -> return Ok(())`, but future
// modules without that guard could panic, and even with the
// guard each dispatch wastes fuel + an RPC subscription tick
// on a no-op. The `LoadedModule.alive` flag below is set from
// this result so the dispatcher skips the failed module
// without surfacing it to the dispatch fast-path. See
// COW-1070.
let init_succeeded = match bindings
.call_init(&mut store, &config)
.await
.map_err(Error::from)?
{
Ok(()) => info!(module = %module_namespace, "init succeeded"),
Err(e) => warn!(
module = %module_namespace,
domain = %e.domain,
kind = ?e.kind,
code = e.code,
message = %e.message,
"init failed",
),
}
Ok(()) => {
info!(module = %module_namespace, "init succeeded");
true
}
Err(e) => {
warn!(
module = %module_namespace,
domain = %e.domain,
kind = ?e.kind,
code = e.code,
message = %e.message,
"init failed - module loaded but marked dead; dispatcher will skip it",
);
false
}
};
// Refuel after init so the first on_event starts with a full budget.
store.set_fuel(limits_cfg.fuel())?;

Expand All @@ -252,7 +270,7 @@ impl Supervisor {
store,
subscriptions: loaded_manifest.manifest.subscriptions.clone(),
fuel_per_event: limits_cfg.fuel(),
alive: true,
alive: init_succeeded,
})
}

Expand Down
Loading