128 KiB
logos-scaffold dogfooding Scenarios
This document is the canonical dogfooding runbook for logos-scaffold.
Use it to evaluate the latest repository state, not as a dated findings report.
Earlier one-off dogfood notes are historical context only; future runs should start here.
Maintenance note: update this document whenever first-class commands, templates, supported workflows, or major user-facing behaviors are added, removed, or materially changed. If the product surface changes and this runbook does not, the runbook is wrong.
Agent Execution Directives
If you are an automated agent running this runbook, treat the following as binding. They exist because a previous run stopped at "this environment can't run a real sequencer" when, in fact, it could — the toolchain just had to be installed by hand.
- Do the real thing. End-to-end dogfooding means a real, running LEZ sequencer executing real transactions — not a stub, a mock, or a "validated by composition" shortcut. A stub (e.g. a scripted JSON-RPC server) is acceptable only as an addition to a real run, never a substitute. If you catch yourself writing "this environment can't…", stop and apply directive 3 before you believe it.
- Install every dependency yourself. The real path needs the risc0 toolchain (
r0vm), thelogos-blockchain-circuitsrelease, and the LEZ-builtsequencer_service(pluswalletfor transactions). None ship in a fresh container. Provision them per "Provisioning the Real LEZ Sequencer Toolchain" below. The container hascurl, network access to GitHub, and (usually) aGH_TOKENenv var — that is enough. - Push through difficulties; a failed first attempt is a routing problem, not a verdict. Expect the obvious installers to fail and route around them:
rzupdownloads over rustls and fails withInvalidCertificate(UnknownIssuer)behind a TLS-intercepting proxy. Fall back tocurl, which trusts the system CA, to fetch the release tarball directly.- Unauthenticated GitHub API calls hit rate limits; add
-H "Authorization: Bearer $GH_TOKEN". git pushand PR creation via the default proxy/MCP may403; push and open PRs withhttps://x-access-token:$GH_TOKEN@github.com/<owner>/<repo>.gitand the GitHub REST API. Do not report "impossible" until you have exhausted the curl + token fallbacks.
- Builds are long; background them so they survive turns. The sequencer build is ~6 min,
setup(wallet + spel) ~3 min. Launch them with a mechanism that outlives a single shell — the harness's background-run, or a Monitor with anuntilloop — becausenohup … &from a one-shot shell gets killed when the shell tears down. Wait on a sentinel (the built binary path, or anEXIT=marker you append), not a fixed sleep. - Leave the box provisioned. Once installed, the toolchain persists in the container cache and the
~/.risc0extensions dir, so later runs are fast. Reuse it rather than reinstalling.
Purpose and Audience
- Dogfooders: use this as a repeatable checklist when validating the latest scaffold DX.
- Contributors: use this document to decide which scenarios must be rerun for a given change.
This guide is intentionally scenario-oriented:
- It defines what to exercise.
- It defines what success looks like.
- It calls out the failures and caveats that are worth recording.
- It does not replace generated project READMEs or CLI help text.
Usage Model
The recommended dogfooding pattern is:
- Build the local scaffold binary from the repository under test.
- Create fresh generated projects in a scratch workspace outside the repo.
- Run project-level scenarios from inside the generated project root.
- Capture command, cwd, exit code, and a short output excerpt for each scenario.
- If the behavior differs from this runbook, update the runbook when the difference is intentional and file a bug when it is not.
For repo dogfooding, prefer the freshly built local binary over an already-installed global binary.
Scaffold now treats LEZ tooling as project-local state. For non-vendored projects, the shared cache layout is <cache_root>/repos/lez/<pin>/...; for vendored projects, LEZ lives under <project>/.scaffold/repos/lez. In both cases, the wallet binary under test is the LEZ-local build artifact at <lez>/target/release/wallet, invoked through logos-scaffold wallet ... rather than a wallet binary on PATH.
export REPO_ROOT=/absolute/path/to/logos-scaffold
export SCRATCH_ROOT=/absolute/path/to/dogfood-runs
cd "$REPO_ROOT"
cargo build
export SCAFFOLD_BIN="$REPO_ROOT/target/debug/logos-scaffold"
mkdir -p "$SCRATCH_ROOT"
You may replace "$SCAFFOLD_BIN" with logos-scaffold when the install path itself is part of what you are validating.
Execution Contexts
| Context | Purpose | Typical commands |
|---|---|---|
| Repo root | Build the latest CLI, inspect docs, validate help/version output, verify out-of-project errors | cargo build, "$SCAFFOLD_BIN" --help, "$SCAFFOLD_BIN" --version, "$SCAFFOLD_BIN" build (expect error) |
| Scratch workspace | Create fresh generated projects without polluting the repo; test advanced creation flags | "$SCAFFOLD_BIN" new dogfood-default, "$SCAFFOLD_BIN" new ... --template ... |
| Generated project root | Execute scaffold workflows and example runners against a fresh project | setup, localnet, build, deploy, wallet, doctor, report, cargo run --bin run_* |
| Test-node host | Drive isolated, short-lived integration-test sequencers from inside a project (or any directory via --project <root>); the RPC-scoped reads target a node URL directly |
test-node prepare, test-node start --json, test-node run -- <cmd>, test-node tx submit-and-wait --url ... |
Do not run project-scoped commands from the repository root unless the scenario is explicitly checking the "outside project" error path. test-node commands are the exception: they accept an explicit --project <root> and may be driven from outside a project directory.
Scaffold is also consumable as a Rust library (logos_scaffold::api): the same setup/localnet/wallet/deploy/doctor/report and test-node capabilities are exposed as typed functions returning typed results and categorized errors, so downstream tests and tooling can embed scaffold without shelling out to the CLI. Scenario A1 validates that surface.
Shared Preconditions
- Unix-like environment with
git,rustc,cargo,lsof,ps, andkill. - Docker or Podman available for guest builds.
logos-blockchain-circuitsrelease on disk when validating older projects without[circuits]: setLOGOS_BLOCKCHAIN_CIRCUITS=<path>(scaffold no longer consults~/.logos-blockchain-circuits/). New projects should carry a[circuits]table inscaffold.toml;setup,build,build idl,localnet, and test-node startup resolve and materialize the configured release instead of relying on ambient shell state.- No conflicting listener on the scaffold localnet port before
localnet start. - Network access available for setup/build flows that fetch dependencies.
- No preinstalled
walletbinary is required. If one exists onPATH, do not treat it as the runtime under test for scaffold wallet scenarios. - Optional but supported:
LOGOS_SCAFFOLD_WALLET_PASSWORDwhen validating password override behavior. - For
B-series (basecamp) scenarios: Nix with flakes enabled, plus a module project on disk whoseflake.nixexposes apackages.<system>.lgxoutput built withlogos-module-builder0.2.x (atictactoe-style project, or that repo'stemplates/minimal-module). Tutorial-era packages no longer install: scaffold's pinnedlgpmvalidates content hashes they don't carry.docs/basecamp-module-requirements.md(also reachable via"$SCAFFOLD_BIN" basecamp docs) is the canonical contract.
The lgs binary is a short alias for logos-scaffold produced by the same crate; "$SCAFFOLD_BIN" and lgs are interchangeable in the commands below.
Provisioning the Real LEZ Sequencer Toolchain
The T-series (and any real localnet / deploy / run validation) needs a real LEZ sequencer. A fresh container has none of the pieces; provision them once — they then live in the scaffold cache and ~/.risc0, and persist for later runs. This whole section was reverse-engineered from a real run; follow it rather than re-deriving it.
Assume "$P" is a generated project root (e.g. $SCRATCH_ROOT/dogfood-default) and GH_TOKEN is set.
1. Discover the risc0 version the pinned LEZ needs. scaffold's r0vm auto-detect requires an exact version match, read from the LEZ Cargo.lock:
LEZ=$("$SCAFFOLD_BIN" test-node pins --project "$P" --json | jq -r .lez_checkout)
grep -A1 'name = "risc0-zkvm"' "$LEZ/Cargo.lock" # e.g. version = "3.0.5"
2. Install r0vm at that version. Try rzup install r0vm <ver> first (curl -sSL https://risczero.com/install | bash installs rzup). In a TLS-intercepting environment rzup fails with InvalidCertificate(UnknownIssuer); fall back to curl for the release tarball (which bundles r0vm and cargo-risczero):
VER=3.0.5; TRIPLE=x86_64-unknown-linux-gnu # aarch64-apple-darwin on macOS
curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/cr.tgz \
"https://github.com/risc0/risc0/releases/download/v$VER/cargo-risczero-$TRIPLE.tgz"
mkdir -p /tmp/cr && tar xzf /tmp/cr.tgz -C /tmp/cr
EXT="$HOME/.risc0/extensions/v$VER-cargo-risczero-$TRIPLE" # scaffold's expected path
mkdir -p "$EXT" && cp /tmp/cr/r0vm "$EXT/r0vm" && chmod +x "$EXT/r0vm"
"$EXT/r0vm" --version # → risc0-r0vm 3.0.5
find_r0vm_path_for_lez looks at ~/.risc0/extensions/v<risc0-zkvm-version>-cargo-risczero-<arch>-<os>/r0vm; placing r0vm there is what wires it into a spawned sequencer (scaffold sets RISC0_SERVER_PATH from it). The sequencer runs with RISC0_DEV_MODE=1 (the test-node default), so r0vm executes guests without real proving — which is why no GPU/prover is needed.
2b. (Only for guest compilation — build, D-series/L-series.) Install the risc0 Rust toolchain. risc0-build resolves the guest toolchain through rzup's directory layout; without one installed, build fails with Risc Zero Rust toolchain not found. Try running rzup install rust. When rzup itself is unusable (same TLS issue as above), install it from the risc0/rust release assets — the directory name must follow the v<version>-rust-<triple> pattern for discovery, and no settings.toml is needed (the highest installed version wins). Pick the newest available tag: guest dependency floats (e.g. ruint) carry MSRVs that outrun older toolchains — 1.88.0 already fails with rustc 1.88.0-dev is not supported by the following packages on a fresh default-template project.
TCVER=1.91.1 # tag r0.1.91.1 — newest published as of 2026-07
curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/rust-tc.tar.gz \
"https://github.com/risc0/rust/releases/download/r0.$TCVER/rust-toolchain-$TRIPLE.tar.gz"
DEST="$HOME/.risc0/toolchains/v$TCVER-rust-$TRIPLE"
mkdir -p "$DEST" && tar xzf /tmp/rust-tc.tar.gz -C "$DEST"
"$DEST/bin/rustc" --version # → rustc 1.91.1-dev
The lez-framework template's guest additionally compiles C (the default template's guests do not), so the L-series build also needs the risc0 C++ toolchain; without it the guest build dies in cc-rs with failed to find tool "/no_risc0_cpp_toolchain_installed_run_rzup_install_cpp". Same rzup layout, date-based version (dir uses the semver form of the tag, e.g. tag 2024.01.05 → dir v2024.1.5). The release asset (and the directory inside the tarball) is platform-specific and does not follow $TRIPLE: pick riscv32im-linux-x86_64 on Linux x86_64 and riscv32im-osx-arm64 on macOS arm64:
CPPASSET=riscv32im-linux-x86_64 # riscv32im-osx-arm64 on macOS arm64
curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/cpp-tc.tar.xz \
"https://github.com/risc0/toolchain/releases/download/2024.01.05/$CPPASSET.tar.xz"
CPPDEST="$HOME/.risc0/toolchains/v2024.1.5-cpp-$TRIPLE"
mkdir -p /tmp/cpp-tc && tar xJf /tmp/cpp-tc.tar.xz -C /tmp/cpp-tc
mkdir -p "$CPPDEST" && mv /tmp/cpp-tc/"$CPPASSET"/* "$CPPDEST/"
ln -sfn "$CPPDEST" "$HOME/.risc0/cpp"
"$CPPDEST/bin/riscv32-unknown-elf-gcc" --version # → gcc 13.2.0
3. Build the real sequencer. test-node prepare downloads the circuits release (via curl, automatically) and builds sequencer_service. It is long (~6 min); run it, then confirm doctor is green:
"$SCAFFOLD_BIN" test-node prepare --project "$P" # → "test-node prerequisites ready"
"$SCAFFOLD_BIN" test-node doctor --project "$P" --json | jq .ok # → true (all checks pass)
4. (Only for real transactions — T4) build the wallet via setup. One gotcha distinguishes setup from test-node prepare: it uses cwd discovery, so it must run inside the project (no --project flag). Circuits need no manual provisioning here — projects with a [circuits] table (every freshly generated one) materialize the pinned release into .scaffold/circuits during setup automatically; set LOGOS_BLOCKCHAIN_CIRCUITS only to override with a local checkout (the env var wins when set):
( cd "$P" && "$SCAFFOLD_BIN" setup ) # builds wallet + spel, seeds the default wallet (~3 min) → "setup complete"
Sanity-check the provisioned toolchain before running the T-series:
"$EXT/r0vm" --version
ls "$LEZ/target/release/sequencer_service" # real sequencer (T1–T3)
ls "$LEZ/target/release/wallet" # real wallet (T4)
ls "$P"/.scaffold/circuits/pol/verification_key.json # project-local [circuits] install
If any of these is missing, do not "skip the real run" — go back and fix the step that produced it.
Scenario Index
| ID | Template | Level | Goal | Command surface |
|---|---|---|---|---|
| D1 | default |
Core | Fresh project creation and first-success bootstrap | new, create, setup, localnet start, build, deploy, wallet topup, wallet -- check-health |
| D2 | default |
Core | Localnet lifecycle visibility and doctor checks | localnet status, localnet logs, localnet stop, doctor, JSON variants |
| D3 | default |
Advanced | Deploy path variations and machine-readable single-program submission | deploy [program-name], deploy --program-path, deploy --program-path --json |
| D4 | default |
Core | Wallet management, default-address behavior, and passthrough UX | wallet list, wallet default set, wallet topup --dry-run, wallet topup, wallet -- ... |
| D5 | default |
Advanced | Diagnostics bundle and support artifact hygiene | report, report --out, report --tail |
| D6 | default |
Core | Example runner interaction and account state verification | cargo run --bin run_hello_world, cargo run --bin run_hello_world_with_move_function, wallet -- account get |
| D7 | default |
Core | One-step run pipeline and post-deploy hooks |
run, run --post-deploy, run --no-post-deploy, [run] config |
| L1 | lez-framework |
Core | Fresh LEZ project bootstrap to ready state | new --template lez-framework, setup, localnet start, doctor, build |
| L2 | lez-framework |
Core | LEZ IDL regeneration | build idl |
| L3 | lez-framework |
Advanced | LEZ client generation from current IDL | build client |
| L4 | lez-framework |
Core | LEZ deploy and counter interaction | deploy, cargo run --bin run_lez_counter |
| E1 | N/A | Core | CLI discoverability and error quality | --help, help, --version, unknown commands, out-of-project errors |
| E2 | N/A | Advanced | Project creation with advanced flags and invalid inputs | new --template, new --vendor-deps, new --cache-root |
| E3 | N/A | Core | AI skills materialized into generated and adopted projects | new, new --template lez-framework, init, init re-run |
| B1 | external module project | Core | Basecamp + lgpm setup and idempotent re-run | init, basecamp setup, basecamp doctor, basecamp docs |
| B2 | external module project | Core | Module capture, install, paths, and single-instance launch | basecamp modules, basecamp modules --show, basecamp install, basecamp paths, basecamp launch <profile> |
| B3 | external module project | Core | Two-instance p2p dogfooding | basecamp launch <profile> (parallel) |
| B4 | external module project | Advanced | Clean-slate and profile safety on relaunch | basecamp launch <profile> (×2), custom profile names |
| B5 | external module project | Advanced | Module artefact builds by variant | basecamp build, basecamp build-portable, --variant, --module |
| B6 | external module project | Advanced | Captured module run loop | basecamp run <module>, --host standalone |
| B7 | external module project | Core | Pin-set contract checks that need no basecamp app build | nix build of [repos.lgpm] + .#lgx, real lgpm install, resolved-binary check |
| A1 | N/A | Advanced | Public Rust API surface for embedding scaffold in tests/tooling | logos_scaffold::api::Project, cargo doc, doctests |
| T1 | default |
Advanced | Isolated test-node lifecycle and caller-project pins | test-node pins, test-node prepare, test-node doctor, test-node start, test-node status, test-node stop, test-node run |
| T2 | default |
Advanced | Typed RPC reads against a running test node | test-node tx submit-and-wait, test-node blocks head/range/wait, test-node clock read/wait-stable, test-node account get/batch-get, test-node proof get, test-node snapshot accounts |
| T3 | default |
Advanced | Caller-provided state seeding | test-node state schema, test-node state export, test-node state seed, test-node start --state |
| T4 | default |
Advanced | Real committed user transaction (test-node + wallet) | test-node start --port, wallet topup, test-node blocks wait, test-node tx wait |
Standing Validation Notes
- Project context matters. Many scaffold commands are meant to be run only inside a generated project root. Running them elsewhere should produce a clear error, not silent misbehavior.
- Localnet readiness, listener ownership, and wallet connectivity are high-value validation points. Record contradictions instead of smoothing over them.
- Machine-readable paths matter for tooling. Preserve
--jsonoutputs when a scenario includes them. reportis sanitized on a best-effort basis, not on an absolute guarantee. Always inspect the archive before sharing it.- When wallet behavior depends on an omitted address, verify whether the project default wallet was seeded and persisted as expected.
- Example runner programs (
cargo run --bin run_*) are the final proof that the scaffold pipeline works end-to-end. A successful deploy means nothing if the runner cannot interact with the deployed program.
D1. Default Template Bootstrap and First Success
Goal
Validate that the default template can be scaffolded from the latest repo and reach the documented first-success path.
Preconditions
cargo buildcompleted at the repo root."$SCAFFOLD_BIN"points to the freshly built binary.- Scratch workspace exists and is writable.
Commands / Actions
From the scratch workspace:
cd "$SCRATCH_ROOT"
"$SCAFFOLD_BIN" new dogfood-default
"$SCAFFOLD_BIN" create dogfood-default-create
cd dogfood-default
"$SCAFFOLD_BIN" setup
"$SCAFFOLD_BIN" localnet start
"$SCAFFOLD_BIN" build
"$SCAFFOLD_BIN" deploy
"$SCAFFOLD_BIN" wallet topup
"$SCAFFOLD_BIN" wallet -- check-health
Use new for the main runnable project and create as the lightweight alias-parity check in a separate directory. Both commands also accept --template, --vendor-deps, --lez-path, and --cache-root, but this scenario uses defaults only. See E2 for advanced flag coverage.
Expected Success Signals
- Project creation succeeds and prints the destination path, pinned LEZ commit, and cache root.
- Generated
scaffold.tomlincludes a[circuits]table. The default install dir is project-local (.scaffold/circuits), and the configured version/download template/install dir become the single source of truth for commands that needlogos-blockchain-circuits. setupcompletes after syncing LEZ to the configured pin, building bothsequencer_serviceandwalletinside the project's LEZ tree, and either seeding the default wallet or reporting that a default wallet is already configured. Both seeding paths are a PASS:default wallet seeded from preconfigured accountwhen the pinned LEZ debug config ships aninitial_accountsentry, anddefault wallet seeded by initializing wallet storage (config ships no preconfigured account)on LEZ v0.2.0, whose debug config ships none — theresetupruns the freshly builtwalletto create its persistent storage and adopts the firstPublic/account on a/-prefixed listing line, ignoringPublic/addresses on lines that are not/-prefixed — notably the wallet's ownPreconfigured …entries, which it prints above the stored accounts even when the config ships noinitial_accounts, and which a first-token scan would adopt instead of the account the wallet just created. Only if no/-prefixed line yields a usablePublic/address does it fall back to the firstPublic/token anywhere in the output; if the wallet ever stops/-prefixing stored accounts, that fallback starts adopting a preconfigured address, so a seeded address matching aPreconfiguredline rather than a/one is worth reporting. Either line is followed byAddress:andState file:. Onlywarning: could not seed default wallet automaticallyis a failure. With--prebuilt:sequencer_serviceis downloaded instead of built from source (falls back to source build if no artifact is published);walletis always built from source regardless of--prebuilt.localnet startreports a ready localnet rather than only a spawned PID.buildexits successfully after preparing the project workspace, resolving the configured circuits release, and — when the project has amethods/Cargo.toml(Risc0 guest crate excluded from the main workspace) — also printsBuilding guest methods...and produces guest.binfiles undertarget/riscv-guest/<methods-crate>/<guest-crate>/riscv32im-risc0-zkvm-elf/release/, the same pathsdeploysubmits from. The default template uses the workspacetarget/tree, notmethods/target/.deployprints a submission summary with zero failures when built binaries are present. Multi-program deploys are paced one program per sequencer block (Waiting for a new block past N before the next deployment ...between submissions): the pinned LEZ settles each block as a single bedrock inscription with a ~896 KiB payload cap and panics fatally when a block exceeds it, so batching several ~370 KiB deployment ELFs into one block kills the sequencer. Expect roughly oneblock_create_timeout(15s) of wait per additional program. Pacing fails closed: a stalled head or an unreadable post-submission baseline aborts the remaining submissions withdeploy pacing aborted ...and a non-zero exit rather than batching unpaced (re-rundeployfor the rest once the sequencer recovers, or raiseLOGOS_SCAFFOLD_DEPLOY_PACING_TIMEOUT_MSfor slow block intervals). A deploy that continues unpaced and crashes localnet mid-flow is a regression; equally, record it if the upstream cap is lifted and pacing becomes dead weight.wallet topupsucceeds without an explicit address because the project default wallet was seeded during setup.wallet -- check-healthsucceeds against the running localnet without requiring a globalwalletinstall or manualPATHchanges.- Generated
scaffold.tomlstores[wallet].home_dirbut does not carry a wallet binary override; wallet location is derived from the pinned LEZ checkout.
Failure Signals / Common Pitfalls
- Running
setup,build,deploy, or wallet commands outside the generated project root should fail with a project-scoped message. - A foreign listener or stale state on the localnet port is a real dogfooding finding; capture
localnet status, not just the final error. - If
wallet topupwithout an address says no destination is configured, record that as a regression in default-wallet seeding or persistence. On a v0.2.0 pin this is what a broken storage-initialization fallback looks like:setupprintswarning: could not seed default wallet automatically,.scaffold/state/wallet.statenever gains adefault_address=line, andrun's topup step fails. - If
setupor wallet commands depend onwalletbeing installed globally or onPATH, record that as a regression in the self-contained project model. - If
deployfails due to missing binaries after a successfulbuild, capture the exact missing path.
Evidence to Capture
- Scaffold creation output for both
newandcreate. setup,localnet start,build,deploy, and wallet command excerpts.- The generated project path and the exact binary path used for the run.
scaffold.tomlexcerpt showing[circuits], plusls .scaffold/circuitsor the configured install dir aftersetup/build.
Execution Notes
- Use fresh directories per run. Do not reuse an old generated project unless the scenario explicitly targets upgrade or persistence behavior.
- Keep the alias check isolated so a failure in
createdoes not contaminate the primary bootstrap project.
D2. Default Template Operational Health: Localnet and Doctor
Goal
Validate that localnet lifecycle commands and doctor diagnostics provide usable human and machine-readable state.
Preconditions
- A default-template project exists.
setuphas already completed for that project.
Commands / Actions
From the generated project root:
"$SCAFFOLD_BIN" localnet status
"$SCAFFOLD_BIN" localnet status --json
"$SCAFFOLD_BIN" doctor
"$SCAFFOLD_BIN" doctor --json
"$SCAFFOLD_BIN" localnet logs --tail 200
"$SCAFFOLD_BIN" localnet stop
"$SCAFFOLD_BIN" localnet status
If the scenario begins with localnet stopped, run "$SCAFFOLD_BIN" localnet start first and capture both the started and stopped states.
Expected Success Signals
- Human-readable
localnet statusclearly reports tracked PID, listener state, ownership, and readiness. localnet status --jsonreturns parseable JSON with at leasttracked_pid,listener_present,ownership, andready.doctorreturns actionable next steps rather than only raw failures.doctorvalidates the configured circuits install path, checks the top-levelVERSIONfile against[circuits].version, and warns when project config drifts from the LEZ pin's expected circuits release.doctor --jsonreturns parseable JSON with at leaststatus,summary,checks, andnext_steps.localnet logs --tail 200returns useful recent log lines when logs exist.localnet stopsucceeds cleanly and subsequent status reflects the stopped state.- The sequencer survives shell/tmux closure: after
localnet start, detaching the terminal or tmux session should not kill the sequencer. Verify withlocalnet statusfrom a new shell —running=trueconfirms daemon behavior.
Failure Signals / Common Pitfalls
- Contradictions between tracked PID, listener ownership, and readiness are high-value findings.
- Empty or unhelpful logs after a failed startup are worth recording. A sequencer that dies during startup — most often because it could not bind the port, which the pre-flight check can miss while a previous instance is still releasing the socket — exits before writing a line, so the tail reads
<no log output yet>. That is expected; what the failure must still carry is a next step. Bothlocalnet startfailures namelogos-scaffold localnet status(the command that identifies a foreign listener, stale state, or ownership) andlocalnet logs --tail 200; a start failure that ends at the empty tail with nowhere to go is the regression. - If
doctoromits next steps or machine-readable output becomes malformed, treat that as a DX regression.
Evidence to Capture
- Human-readable and JSON output for both
localnet statusanddoctor. - If
[circuits]is edited for the run, capture the matchingdoctorwarning/error and the configured install dir. - A short
localnet logsexcerpt. - Stop behavior and the post-stop status output.
Execution Notes
- Preserve raw JSON output exactly.
- If state is contradictory, do not silently restart localnet before capturing the failing state.
D3. Default Template Deploy Variants and JSON Output
Goal
Validate targeted deployment flows, including the machine-readable single-program submission path via --program-path.
Preconditions
- Default-template project has already completed
build. - Localnet is reachable.
- Guest binaries exist under the generated project's
target/riscv-guest/.../releasedirectory.
Commands / Actions
From the generated project root:
export EXAMPLE_PROGRAMS_BUILD_DIR="$PWD/target/riscv-guest/example_program_deployment_methods/example_program_deployment_programs/riscv32im-risc0-zkvm-elf/release"
"$SCAFFOLD_BIN" deploy hello_world
"$SCAFFOLD_BIN" deploy --program-path "$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin"
"$SCAFFOLD_BIN" deploy --program-path "$EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin" --json
"$SCAFFOLD_BIN" deploy nonexistent_program
Use a known default-template program name such as hello_world. If the generated project exposes a different set of programs in methods/guest/src/bin, record the discovered list.
Both deploy paths honor --json, with a different shape each. --program-path --json prints one flat object for the single submitted program; the discovery-based path (deploy or deploy <name>) wraps one such object per attempted program in {"deploys":[…]}. Either way --json is pure JSON on stdout — no command echoes, no summary block. This scenario validates that distinction. (Both shapes omit absent fields rather than emitting null: today the pinned LEZ exposes no transaction receipt for a deploy, so tx is always absent and consumers test has("tx") rather than branching on a guaranteed-null key. program_id is present whenever the vendored spel binary resolved it, and a failed entry carries error in its place.)
Expected Success Signals
deploy hello_worldreportsOK hello_world submittedand ends with a human-readable success summary.deploy --program-path ... --jsonprints a parseable JSON object with at leaststatusandprogram(plusprogram_idoncesetuphas builtspel).deploy <name> --jsonand baredeploy --jsonprint a parseable{"deploys":[…]}object whose entries carry the same fields.deploy --program-path ...without--jsonprints a human-readableOKline with the binary path.deploy nonexistent_programfails with an error listing the available discovered programs.
Failure Signals / Common Pitfalls
- If either
--jsonpath starts emitting a guaranteed-nulltxkey, or mixes command echoes and the human-readable summary into the JSON stream, record that as a machine-readability regression. - If localnet is unreachable, deploy should fail with a sequencer-unavailable hint instead of a vague wallet error.
- Unknown program names should report the available discovered programs.
- Missing binaries should point back to
logos-scaffold build.
Evidence to Capture
- One successful human-readable deploy excerpt from the discovery path.
- One successful JSON deploy output from the
--program-pathpath, and one{"deploys":[…]}output from the discovery path. - The error output for an unknown program name.
- Any failure-path excerpt for unreachable sequencer or missing binary when intentionally probed.
Execution Notes
- Record both JSON shapes: the flat object from
--program-pathand the{"deploys":[…]}wrapper from the discovery path. They are separate contracts and a change to one does not imply a change to the other. - When recording a custom
--program-path, preserve the absolute path used in the run log.
D4. Default Template Wallet Workflows and Passthrough
Goal
Validate wallet-focused scaffold behavior beyond the basic bootstrap path.
Preconditions
- Default-template project exists.
- Setup completed successfully.
- Localnet is running if you are validating non-dry-run topup or passthrough health checks.
Commands / Actions
From the generated project root:
"$SCAFFOLD_BIN" wallet list
"$SCAFFOLD_BIN" wallet list --long
"$SCAFFOLD_BIN" wallet default set Public/<account-id>
"$SCAFFOLD_BIN" wallet topup --dry-run
"$SCAFFOLD_BIN" wallet topup
"$SCAFFOLD_BIN" wallet -- account list
"$SCAFFOLD_BIN" wallet -- check-health
Use a real address from wallet list when explicitly validating wallet default set.
Expected Success Signals
wallet listandwallet list --longproxy wallet account enumeration from the project-scoped wallet home using the LEZ-local wallet binary.wallet default setaccepts either positional address or--addressand persists the normalized project default.wallet topup --dry-runrenders the underlying faucet claim command instead of mutating state.wallet topupwithout an explicit address uses the saved default wallet. On a LEZ v0.2.0 pin that default came fromsetup'sdefault wallet seeded by initializing wallet storage (config ships no preconfigured account)path rather than from a preconfigured account — both are a PASS; check that the address in.scaffold/state/wallet.stateis onewallet listreports.wallet -- ...preserves the project wallet environment while forwarding the raw wallet command to<lez>/target/release/wallet. That environment names the wallet home under bothNSSA_WALLET_HOME_DIR(LEZ up to v0.1.2) andLEE_WALLET_HOME_DIR(LEZ v0.2.0); verify with"$SCAFFOLD_BIN" wallet -- account listlisting the project's accounts and not an unrelated~/.lee/walletset.
Optional: validate LOGOS_SCAFFOLD_WALLET_PASSWORD override behavior by setting the env var to a non-default value and observing whether wallet commands honor it.
LOGOS_SCAFFOLD_WALLET_PASSWORD="custom-pw" "$SCAFFOLD_BIN" wallet topup --dry-run
Failure Signals / Common Pitfalls
- Invalid addresses should be rejected with an "Accepted formats" hint.
- If both positional address and
--addressare supplied together, that is a user error and should remain clearly reported. - Connectivity failures during topup should mention localnet/sequencer reachability rather than only raw wallet output.
- Passthrough flows require the literal
--; if the CLI starts accepting or mangling passthrough without it, record that change. - If wallet flows only succeed when
walletis separately installed onPATH, or if missing-binary errors point anywhere other than the LEZ-localtarget/release/wallet, record that as a regression. - Wallet commands that succeed but enumerate accounts the project never created point at the wallet home falling back to
~/.lee/wallet. The failure is silent, so do not trust exit code 0: scaffold itself only ever putswallet_config.jsoninto.scaffold/wallet, so ifls .scaffold/walletstill shows that file alone after a wallet command that should have created or opened account storage, the wallet CLI wrote its storage somewhere else (~/.lee/wallet).
Evidence to Capture
wallet listoutput with account identifiers redacted only if needed for sharing.wallet topup --dry-runoutput showing the rendered command.- One successful passthrough example, ideally
wallet -- check-healthorwallet -- account list. - If
LOGOS_SCAFFOLD_WALLET_PASSWORDoverride was tested, the dry-run output showing the password was or was not forwarded.
Execution Notes
- Do not let the shell consume the passthrough separator. Record the exact argv form you used.
- If you redact account IDs for public sharing, keep the unredacted originals in a local evidence log so repeated runs stay traceable.
D5. Default Template Diagnostics Bundle
Goal
Validate that scaffold support artifacts can be collected and inspected safely.
Preconditions
- Default-template project exists.
- The project has enough state to make the report meaningful, ideally after setup and at least one localnet or build action.
Commands / Actions
From the generated project root:
"$SCAFFOLD_BIN" report
"$SCAFFOLD_BIN" report --tail 200
"$SCAFFOLD_BIN" report --out "$PWD/artifacts/support-report.tar.gz"
Inspect the produced archive before sharing it:
find .scaffold/reports -maxdepth 1 -name '*.tar.gz' -print | sort
REPORT_ARCHIVE="$(find .scaffold/reports -maxdepth 1 -name '*.tar.gz' | sort | tail -n 1)"
tar -tzf "$REPORT_ARCHIVE" | sort
tar -tzf "$PWD/artifacts/support-report.tar.gz" | sort
Expected Success Signals
reportprints a completion message, archive path, and a warning to inspect files before sharing.- The default output lands under
.scaffold/reports/. - A custom
--outpath is honored. - The archive contains support files such as
README.txt,manifest.json,diagnostics/doctor.json,diagnostics/localnet-status.json, andsummaries/build-evidence.json.
Failure Signals / Common Pitfalls
- If raw wallet files under
.scaffold/wallet/appear in the archive, treat that as a severe regression. - If absolute local paths leak without scrubbing in human-facing report files, record it.
- If the archive is produced but the warning about manual inspection disappears, record it.
Evidence to Capture
- Report completion output.
- Archive path(s).
- A short file listing from the tarball.
Execution Notes
- Never attach the archive to an external system without first listing its contents.
- Keep the tar listing with the run evidence so redaction regressions can be compared across releases.
D6. Default Template Example Runner Interaction
Goal
Validate that deployed programs can actually be invoked via the generated example runner binaries and that account state changes are observable.
D1 validates the scaffold pipeline up to deploy and wallet health. This scenario validates the final step: running programs against the localnet and confirming observable state mutations.
Preconditions
- Default-template project exists with D1 completed (setup, build, deploy done).
- Localnet is running and
wallet -- check-healthsucceeds. - Create two fresh public accounts for this scenario — one per runner. The first program to write an account becomes its
program_owner, and the pinned LEZ rejects any later write to it by a different program withUnauthorizedDataModification(execution-check rejection visible only in the sequencer log; the runner still exits 0 because submission succeeded).
"$SCAFFOLD_BIN" wallet -- account new public # <account-id-a> for run_hello_world
"$SCAFFOLD_BIN" wallet -- account new public # <account-id-b> for run_hello_world_with_move_function
Capture each account ID from the output (format: Public/<base58>). The runners take the bare base58 portion; wallet account get requires the full Public/<base58> form (the bare id fails with Unsupported privacy kind).
Commands / Actions
From the generated project root:
export NSSA_WALLET_HOME_DIR="$PWD/.scaffold/wallet" LEE_WALLET_HOME_DIR="$PWD/.scaffold/wallet"
cargo run --bin run_hello_world -- <account-id-a>
"$SCAFFOLD_BIN" wallet -- account get --account-id Public/<account-id-a>
cargo run --bin run_hello_world_with_move_function -- write-public <account-id-b> "dogfood-test-message"
"$SCAFFOLD_BIN" wallet -- account get --account-id Public/<account-id-b>
The first runner (run_hello_world) submits a basic public transaction; once committed, account A's data decodes to Hola mundo! and its program_owner is the hello_world program. The second (run_hello_world_with_move_function write-public) writes a custom greeting string to account B, producing an observable data field change. Reads are eventually consistent with block production (default localnet block interval 15s) — poll account get until the write lands.
Expected Success Signals
- Both runners print
submitted transaction: tx_hash=...on success. - Both runners print a
verification hint:line pointing towallet account get. - After
run_hello_world, account A showsdata= hex(Hola mundo!) and a non-nullprogram_owner. - After
run_hello_world_with_move_function write-public, account B'sdatacontains the hex-encoded greeting string. - Runner exit code is 0.
Failure Signals / Common Pitfalls
- If a runner exits 0 but the account remains
Uninitialized, the transaction may have been submitted without effect. Record both the runner output and the account state — and check the sequencer log forfailed execution checklines: submission-level success does not imply execution-level success. - Pointing both runners at the same account is the known execution-rejection case (
UnauthorizedDataModification— see Preconditions), not a scaffold regression. - Panic output from a runner (e.g.,
unwrap()on wallet/sequencer errors) instead of a structured error is worth recording. - Invalid account ID format (not base58) should produce a clear parse error from the runner, not a panic.
- If localnet is down, runners should fail with a connection-refused error. Capture the exact error text.
Evidence to Capture
- Runner output including
statusandtx_hashfor at least one successful run. wallet account getoutput showing account state after interaction.- The exact account ID used (for traceability across repeated runs).
Execution Notes
- The wallet home must be exported for runners that initialize
WalletCore::from_env(). The scaffold wallet commands set it automatically, but directcargo rundoes not. Export both names: LEZ readsNSSA_WALLET_HOME_DIRup to v0.1.2 andLEE_WALLET_HOME_DIRfrom v0.2.0. The two half-exports fail differently: a v0.2.0 pin given onlyNSSA_WALLET_HOME_DIRfalls back to~/.lee/walletwith no error, so the runner reports an unknown or empty account instead of failing loudly; a pre-v0.2.0 pin given onlyLEE_WALLET_HOME_DIRhas no wallet home at all and fails inWalletCore::from_env(). Only the first shape is silent — treat an empty-looking wallet as the signal to check. - Use the fresh public account created in the preconditions rather than reusing accounts from other scenarios. This avoids confusion about pre-existing state.
- If additional runners are available (e.g.,
run_hello_world_private,run_hello_world_through_tail_call), exercising them is valuable but not required for this scenario.
D7. run Pipeline and Post-Deploy Hooks
Goal
Validate that lgs run collapses the build → IDL → localnet → topup → deploy chain into a single command, fires [run].post_deploy hooks with the documented environment, and that --post-deploy / --no-post-deploy flags override the configured hooks correctly.
Preconditions
- A default-template project exists at
$SCRATCH_ROOT/dogfood-defaultwithsetupalready complete. - No existing scaffold localnet running on the configured port (the scenario will start one). If one exists from a prior scenario, stop it first.
wallet topuphas worked at least once for this project (D1 or D4 covers this).
Commands / Actions
From the project root, exercise the bare pipeline:
"$SCAFFOLD_BIN" run
Then add a [run] section to scaffold.toml and re-run with hooks:
[run]
post_deploy = [
"echo 'sequencer:' $SEQUENCER_URL",
"echo 'idl:' $SCAFFOLD_IDL_DIR",
"echo 'project root:' $SCAFFOLD_PROJECT_ROOT",
"echo 'wallet home:' $NSSA_WALLET_HOME_DIR",
"echo 'wallet home (lez v0.2.0 name):' $LEE_WALLET_HOME_DIR",
"echo 'program id:' ${SCAFFOLD_PROGRAM_ID:-unavailable}",
"echo 'guest bin:' ${SCAFFOLD_GUEST_BIN:-unavailable}",
]
"$SCAFFOLD_BIN" run
"$SCAFFOLD_BIN" run --post-deploy "echo override" # one-shot override
"$SCAFFOLD_BIN" run --no-post-deploy # skip hooks
"$SCAFFOLD_BIN" run --post-deploy "x" --no-post-deploy # expect clap conflict error
Then add a profile that owns deployment itself (deploy = false) and run it:
[run.profiles.self-deploy]
deploy = false
post_deploy = ["echo 'deploy skipped:' $SCAFFOLD_DEPLOY_SKIPPED"]
"$SCAFFOLD_BIN" run --profile self-deploy # skips step 5, still fires hooks
Then add a profile that funds its own accounts (topup = false) and run it:
[run.profiles.self-fund]
topup = false
post_deploy = ["echo 'topup skipped:' $SCAFFOLD_TOPUP_SKIPPED"]
"$SCAFFOLD_BIN" run --profile self-fund # skips step 4, still deploys + fires hooks
# same hook without the profile: topup runs, so the variable reports 0
"$SCAFFOLD_BIN" run --post-deploy 'echo "topup skipped:" $SCAFFOLD_TOPUP_SKIPPED'
Expected Success Signals
- The first
run(no hooks configured) prints a numbered step header for each phase ([1/5] Building...through[5/5] Deploying...) and ends with a deployed-programs summary. - A second
runreuses the running localnet (localnet already running (sequencer pid=...)) instead of starting a new sequencer, and — when guest binaries, IDL, config, and sequencer are all unchanged — replaces[5/N] Deploying...with[5/N] Deploy skipped (guest binaries + IDL + config + sequencer unchanged; pass --reset ...). Post-deploy hooks still fire after a dedup-skipped deploy. Use--reset(or delete.scaffold/state/run_deploy.json) when the scenario needs to force a real re-deploy. - After adding the
[run]block,runreports[6/6] Running N post-deploy hook(s)and each hook prints a non-empty value for its env var.cwdfor each hook is the project root (verifiable with apwdhook). For a single-program project,$SCAFFOLD_PROGRAM_IDis the deployed program's risc0 image ID and$SCAFFOLD_GUEST_BINis the absolute path to the guest binary. --post-deploy "echo override"ignores[run].post_deployand runs only the override.--no-post-deployskips the post-deploy step entirely; the run prints the deployed-programs summary instead.--post-deploywith--no-post-deployerrors at clap parse time with acannot be used withmessage; exit code is non-zero.- A non-zero hook exit aborts the run with a clear
post-deploy hook exited with status Nmessage. - With
deploy = falsein the selected profile,run --profile self-deployprints[5/6] Deploy skipped (`deploy = false` in the run profile; ...)instead of[5/6] Deploying..., skips the program-hash/deploy work entirely, and still runs thepost_deployhooks. Each hook seesSCAFFOLD_DEPLOY_SKIPPED=1(hereecho 'deploy skipped:' $SCAFFOLD_DEPLOY_SKIPPEDprintsdeploy skipped: 1). - With
topup = falsein the selected profile,run --profile self-fundprints[4/6] Topup skipped (`topup = false` in the run profile; ...)instead of[4/6] Topping up wallet..., skips the wallet-topup call entirely (no destination-address requirement), and still runs deploy and thepost_deployhooks. Each hook seesSCAFFOLD_TOPUP_SKIPPED=1(hereecho 'topup skipped:' $SCAFFOLD_TOPUP_SKIPPEDprintstopup skipped: 1). The same hook run without--profile self-fundprintstopup skipped: 0— the variable is always set, never absent.
Failure Signals / Common Pitfalls
- A
runinvocation that restarts the sequencer when one is already running healthy is a regression in the localnet-reuse path. - Hooks running with
cwdsomewhere other than the project root, or missing any ofSEQUENCER_URL/NSSA_WALLET_HOME_DIR/LEE_WALLET_HOME_DIR/SCAFFOLD_PROJECT_ROOT/SCAFFOLD_IDL_DIR/SCAFFOLD_TOPUP_SKIPPED/SCAFFOLD_DEPLOY_SKIPPED, is a regression in the env contract. The last two are1/0and are always set, never absent — an unset one is itself the regression, since a hook cannot tell "scaffold did the step" from "this scaffold cannot report"; a value that disagrees with the step header printed in the same run (e.g.[4/6] Topup skipped …withSCAFFOLD_TOPUP_SKIPPED=0) is a worse one.NSSA_WALLET_HOME_DIRandLEE_WALLET_HOME_DIRmust both be set and must print the same path; one of them empty means hooks that exec the wallet binary silently target~/.lee/walleton one of the two LEZ pins. $SCAFFOLD_PROGRAM_IDunset after a successful deploy on a single-program project with a vendoredspelbinary is a regression. Hint:lgs setupbuilds the spel binary; if it's missing,program_id: unavailablewill also appear in the deploy summary.
Evidence to Capture
- Console output of the first
runshowing the step headers and the deployed-programs summary. - Output of
runafter the[run]block is added, showing the===> post_deploy[i/n]:markers and the resolved env values. - Output of
run --post-deploy "echo override"showing only the override hook fires. - Output of
run --no-post-deployshowing the deployed-programs summary instead of hooks. - Output of
run --profile self-deployshowing the[5/6] Deploy skipped (`deploy = false` ...)header and thepost_deployhook reportingdeploy skipped: 1. - Output of
run --profile self-fundshowing the[4/6] Topup skipped (`topup = false` ...)header followed by the deploy step and thepost_deployhook reportingtopup skipped: 1, plus the profile-less run of the same hook reportingtopup skipped: 0.
L1. LEZ Template Bootstrap
Goal
Validate that the LEZ template scaffolds and reaches a ready-to-build state.
Preconditions
- Latest scaffold binary has been built from the repo root.
- Scratch workspace exists.
Commands / Actions
From the scratch workspace:
cd "$SCRATCH_ROOT"
"$SCAFFOLD_BIN" new dogfood-lez --template lez-framework
cd dogfood-lez
ls -d idl crates/lez-client-gen methods/guest/src/bin src/bin
"$SCAFFOLD_BIN" setup
"$SCAFFOLD_BIN" localnet start
"$SCAFFOLD_BIN" doctor
"$SCAFFOLD_BIN" build
The ls step verifies that LEZ-specific directories were scaffolded before proceeding with the build pipeline.
Expected Success Signals
- Project creation succeeds with the LEZ template.
- The generated project contains
idl/,crates/lez-client-gen/,methods/guest/src/bin/lez_counter.rs, andsrc/bin/run_lez_counter.rs. setup,localnet start, anddoctorbehave the same way they do for the default template.buildsucceeds for the LEZ project workspace and also runs IDL generation and client generation automatically.
Failure Signals / Common Pitfalls
- If the generated project is missing LEZ-specific paths such as
idl/,crates/lez-client-gen/, ormethods/guest/src/bin/lez_counter.rs, record that immediately. - If LEZ bootstrap behavior diverges from the default template in setup/localnet/doctor flows, capture the difference explicitly.
- If
builddoes not automatically trigger IDL + client generation for the LEZ template, record that as a regression.
Evidence to Capture
- LEZ project creation output.
- Directory listing showing LEZ-specific scaffolded paths.
setup,localnet start,doctor, andbuildexcerpts.
Execution Notes
- Keep LEZ runs separate from default-template runs. The template-specific directories and follow-up commands are part of the validation.
L2. LEZ IDL Regeneration
Goal
Validate that LEZ projects can regenerate IDL from the current project source.
Preconditions
- LEZ project exists.
- The LEZ project build environment is working.
Commands / Actions
From the LEZ project root:
"$SCAFFOLD_BIN" build idl
find idl -maxdepth 1 -type f -name '*.json' | sort
Expected Success Signals
build idlwrites one or more JSON files underidl/.- Command output includes explicit
Wrote IDL ...lines. - The regenerated files are valid JSON and match the current program surface.
Failure Signals / Common Pitfalls
- If the command prints that IDL build is being skipped due to framework kind, the scenario is running in the wrong project.
- Missing IDL marker output or empty IDL generation is a real regression for the LEZ template.
Evidence to Capture
build idloutput.- Listing of generated files under
idl/. - If relevant, a diff between pre-existing and regenerated IDL.
Execution Notes
- Preserve the raw
Wrote IDL ...lines. They make it much easier to diagnose partial-generation failures.
L3. LEZ Client Generation
Goal
Validate that LEZ client bindings can be regenerated from the current IDL set.
Preconditions
- LEZ project exists.
build idlhas been run successfully, either directly or viabuild client.
Commands / Actions
From the LEZ project root:
"$SCAFFOLD_BIN" build client
find src/generated -type f | sort
Expected Success Signals
build clientreports that it is regenerating IDL before generating client code.- Client artifacts are written under
src/generated. - The generated files reflect the current contents of
idl/.
Failure Signals / Common Pitfalls
- If
build clientdoes not refresh IDL first, record that behavior change. - Missing
src/generatedoutput or missing generator crate paths are LEZ-specific regressions.
Evidence to Capture
build clientoutput.- Listing of files under
src/generated. - Any diff in generated client code when the scenario is rerun after a program change.
Execution Notes
- Treat generated client output as part of the scenario evidence, not as disposable noise.
- When the generator fails, capture the exact manifest path and working directory that were used.
L4. LEZ Template Deploy and Counter Interaction
Goal
Validate that the LEZ counter program can be deployed and that the generated runner binary can invoke init and increment subcommands against the running localnet.
Preconditions
- LEZ project exists with L1 completed (setup, build, localnet running).
wallet -- check-healthsucceeds.- At least one public account exists. If not:
"$SCAFFOLD_BIN" wallet -- account new public
Commands / Actions
From the LEZ project root:
"$SCAFFOLD_BIN" deploy
export NSSA_WALLET_HOME_DIR="$PWD/.scaffold/wallet" LEE_WALLET_HOME_DIR="$PWD/.scaffold/wallet"
export HOST_CC=cc HOST_CXX=c++
cargo run --bin run_lez_counter -- init --to <account-id>
cargo run --bin run_lez_counter -- increment --counter <account-id> --authority <account-id> --amount 5
HOST_CC/HOST_CXX matter for direct cargo run in lez-framework projects when the risc0 C++ toolchain is installed: the guest embed refingerprints under your shell env, risc0-build exports plain CC = riscv gcc, and the guest graph's host-side proc-macro deps (spel-framework-macros → … → ring) then compile host C with the riscv compiler and die on -m64. Scaffold's own build/IDL/client commands pin these automatically; direct cargo invocations need the export (CI's template-e2e does the same at the job level).
Expected Success Signals
deploysubmits thelez_counterprogram and prints a success summary.run_lez_counter initprints confirmation that the counter was initialized at the target account.run_lez_counter incrementprints confirmation of the increment operation.
Note: as of this writing, the LEZ counter runner contains TODO placeholders for actual transaction submission. If the runner only prints diagnostic messages without submitting transactions, record that as the current state. When transaction submission is implemented, update this scenario with account-state verification steps matching D6.
Failure Signals / Common Pitfalls
- If
deploycannot findlez_counterin the discovered program list, record the actual discovered list. - If the runner panics on wallet initialization, the name this pin reads is unset: either neither name was exported, or only
LEE_WALLET_HOME_DIRwas exported against a pre-v0.2.0 pin. If it instead starts against an empty wallet, the pin is v0.2.0 and onlyNSSA_WALLET_HOME_DIRwas exported — that wallet ignores the old name and falls back to~/.lee/walletwithout an error. - If the runner accepts the subcommand but does nothing (due to TODO stubs), record the output and note the gap.
Evidence to Capture
deployoutput for the LEZ project.run_lez_counter initandincrementoutput.- Whether the runner actually submitted transactions or only printed placeholder messages.
Execution Notes
- Both
NSSA_WALLET_HOME_DIR(LEZ up to v0.1.2) andLEE_WALLET_HOME_DIR(LEZ v0.2.0) must be exported for the runner. Scaffold wallet commands set both automatically, but directcargo rundoes not. - Keep LEZ interaction evidence separate from default-template interaction evidence.
E1. CLI Discoverability and Error Quality
Goal
Validate that the scaffold CLI provides consistent, non-destructive help and version output, useful error messages for unknown commands, and clear project-context errors when commands are run outside a generated project.
Preconditions
- Latest scaffold binary has been built from the repo root.
- A scratch workspace exists (for verifying that help flags do not create files).
Commands / Actions
From the repo root:
"$SCAFFOLD_BIN" --help
"$SCAFFOLD_BIN" --version
"$SCAFFOLD_BIN" help
"$SCAFFOLD_BIN" setup --help
"$SCAFFOLD_BIN" setup --wallet-install auto
"$SCAFFOLD_BIN" nonexistent-command
"$SCAFFOLD_BIN" build
"$SCAFFOLD_BIN" deploy
"$SCAFFOLD_BIN" doctor
"$SCAFFOLD_BIN" localnet status
"$SCAFFOLD_BIN" wallet list
From the scratch workspace (verify help flags do not mutate the filesystem):
cd "$SCRATCH_ROOT"
ls -la before_help_test > /dev/null 2>&1 || true
"$SCAFFOLD_BIN" create --help
"$SCAFFOLD_BIN" new --help
ls -la
Check that no new directories were created by the --help invocations.
Expected Success Signals
--helpprints a usage summary listing all top-level commands.--versionprints the version string and exits.helpprints the same top-level usage summary as--helpand exits successfully.setup --helpdocuments the setup workflow without a--wallet-installflag.- Legacy
setup --wallet-install autois rejected during argument parsing as an unknown argument. nonexistent-commandfails with an error and directs the user to--helpor an equivalent corrective hint.build,deploy,doctor,localnet status, andwallet listrun from outside a project fail with a message likeNot a logos-scaffold project ... Run logos-scaffold create <name>.create --helpandnew --helpdo not create directories or files in the current working directory.
Failure Signals / Common Pitfalls
- If
create --helpornew --helpcreates a directory named--help, that is a significant UX regression. Record it and the exact argv used. - If project-context errors are missing or unhelpful (e.g., a raw file-not-found instead of a scaffold-specific message), record the exact output.
- If some subcommands support
--helpand others do not, document the inconsistency. - If
setup --helpstill advertises--wallet-install, or the deprecated flag is silently accepted, record that as a command-surface regression.
Evidence to Capture
--helpoutput.--versionoutput.- Error output for unknown command and out-of-project commands.
- Directory listing before and after
create --help/new --helpto confirm no side effects.
Execution Notes
- Run the
create --helptest in an isolated temporary directory so any accidental file creation does not pollute the scratch workspace. - Do not interpret missing
--helpsupport on a subcommand as a blocker. Record it as a finding and move on.
E2. Project Creation with Advanced Flags
Goal
Validate that create/new handle the --template, --vendor-deps, --lez-path (legacy alias: --lssa-path), and --cache-root flags correctly, including error cases for invalid inputs.
Preconditions
- Latest scaffold binary has been built from the repo root.
- Scratch workspace exists and is writable.
Commands / Actions
From the scratch workspace:
cd "$SCRATCH_ROOT"
"$SCAFFOLD_BIN" new dogfood-invalid-template --template nonexistent-template
"$SCAFFOLD_BIN" new dogfood-lez-explicit --template lez-framework
ls -d dogfood-lez-explicit/idl dogfood-lez-explicit/crates/lez-client-gen
"$SCAFFOLD_BIN" new dogfood-vendor --vendor-deps
"$SCAFFOLD_BIN" new dogfood-cache --cache-root "$SCRATCH_ROOT/custom-cache"
find "$SCRATCH_ROOT/custom-cache/repos/lez" -maxdepth 2 -mindepth 1 -type d | sort
grep -n "^\[wallet\]\|^home_dir\|^binary" dogfood-cache/scaffold.toml
Expected Success Signals
- Invalid
--templatename fails with a clear error listing the available templates (default,lez-framework). --template lez-frameworkcreates a project with LEZ-specific structure (same as L1).--vendor-depsis accepted without error and creates a project that vendors the pinned LEZ repo under.scaffold/repos/lez.--cache-rootis honored and scaffold uses the specified directory for cache operations, with non-vendored LEZ clones isolated by pin under<cache-root>/repos/lez/<pin>/.- Generated
scaffold.tomlincludes[wallet].home_dirand does not include a deprecatedwallet.binaryfield.
Failure Signals / Common Pitfalls
- If an invalid template name silently falls back to
default, record that as a regression. - If
--vendor-depsor--cache-rootare silently ignored or produce an error, record the exact output. - If
--lez-pathis tested and the path does not exist, verify the error message points to the bad path. - If non-vendored cache reuse collapses different LEZ pins into a single shared
repos/lezcheckout, record that as a cache-isolation regression.
Evidence to Capture
- Error output for invalid
--template. - Creation output for
--template lez-frameworkwith directory listing. - Creation output for
--vendor-depsand--cache-rootif tested. - Directory listing proving the pin-isolated cache path.
scaffold.tomlexcerpt showing wallet home config without a wallet binary field.
Execution Notes
- Clean up the generated projects after this scenario to avoid consuming disk space with multiple scaffolded projects.
- The
--lez-pathflag is optional to test here because it requires a real LEZ checkout. Only probe it if one is available.
E3. AI Skills Materialized Into Every Project
Goal
Validate that lgs new and lgs init both drop the canonical AI skill set
into a generated project so that Claude Code, Cursor, and Codex pick them up
without manual configuration. Skills are version-controlled in the generated
project (no .gitignore exclusion).
Preconditions
- Latest scaffold binary built from the repo root (
"$SCAFFOLD_BIN"). - Scratch workspace exists.
Commands / Actions
From the scratch workspace:
cd "$SCRATCH_ROOT"
"$SCAFFOLD_BIN" new dogfood-skills-default
"$SCAFFOLD_BIN" new dogfood-skills-lez --template lez-framework
mkdir dogfood-skills-init && cd dogfood-skills-init
"$SCAFFOLD_BIN" init
shasum AGENTS.md .claude/skills/lgs-cli/SKILL.md .cursor/rules/lgs-cli.mdc
"$SCAFFOLD_BIN" init # re-init must succeed and not change skill content
shasum AGENTS.md .claude/skills/lgs-cli/SKILL.md .cursor/rules/lgs-cli.mdc
Inspect the generated layout in each of the three projects:
find dogfood-skills-default/.claude/skills dogfood-skills-default/.cursor/rules -type f | sort
find dogfood-skills-lez/.claude/skills dogfood-skills-lez/.cursor/rules -type f | sort
ls dogfood-skills-default/AGENTS.md dogfood-skills-lez/AGENTS.md dogfood-skills-init/AGENTS.md
Expected Success Signals
- Every generated project (default template, lez-framework template, and
init-adopted bare directory) contains exactly four.claude/skills/<name>/SKILL.mdfiles:lgs-cli,lez-template,lez-framework-template,basecamp. - The same four skills appear under
.cursor/rules/<name>.mdc. AGENTS.mdexists at every project root, lists all four skills with their descriptions, and links to.claude/skills/<name>/SKILL.md.- Re-running
initon an already-migrated project succeeds (no longer bails) and printsAI skills refreshed under .claude/skills/, .cursor/rules/, AGENTS.md.Theshasumoutput before and after a re-init is byte-identical for all three skill files. .claude/skills/<name>/SKILL.mdis byte-identical to the canonical source under<scaffold-repo>/skills/<name>/SKILL.md(rundiffif validating against a built-from-source binary)..cursor/rules/<name>.mdcfrontmatter containsdescription:andalwaysApply: false, and does not contain aname:field. The body after the closing---is identical to the SKILL.md body.- The generated
.gitignoredoes not exclude.claude/,.cursor/, orAGENTS.md.
Failure Signals / Common Pitfalls
- A skill missing from one of the three locations in any generated project is a regression — every project gets the same four-skill set per the v0.1 contract.
- A
.cursor/rules/<name>.mdcthat still carries thename:line from the source SKILL.md is a regression in the frontmatter rewrite. - A re-
initthat errors with "already at schema" is a stale build — that bail was removed when skill refresh became part of init's contract. - A re-
initthat mutates skill content without a corresponding canonical-source change is a regression in idempotency. - Skills appearing in
.gitignoreis a regression — they are version-controlled by design. - Hand-edited team skills under
.claude/skills/<other>/that get clobbered byinitare a regression —apply_skillsonly owns the four shipped names.
Evidence to Capture
- File listings under
.claude/skills/,.cursor/rules/, and the existence ofAGENTS.mdfor each of the three project flavors. - One
.cursor/rules/<name>.mdchead excerpt showing the rewritten frontmatter. AGENTS.mdexcerpt showing the four-row table.shasumpairs from the re-initidempotency check.
Execution Notes
- This scenario does not require
setup,localnet, or any network access — it validates only the materialization contract. - Pair with E2 when validating template-related changes; pair with B1 when validating
initbehavior alongside basecamp adoption.
B1. Basecamp Setup From a Module Project
Goal
Validate that a module project can fetch the pinned basecamp + lgpm binaries, seed the default profiles, preserve configurable profile schema, and re-run setup idempotently.
Preconditions
- Nix with flakes enabled — and unrestricted GitHub access for Nix specifically. This is a stricter requirement than the rest of this runbook and the usual reason a
B-series run stalls in an agent container, so check it before installing anything. Nix resolvesgithub:flake inputs overhttps://api.github.com/repos/…/commits/HEADandhttps://github.com/…/archive/<rev>.tar.gz; a proxy that allowlists GitHub per repository answers403on both, and the basecamp closure is large — itsflake.lockcarries ~10k nodes acrosslogos-co,NixOS/nixpkgsandoxalica/rust-overlay(it was ~250 at the v0.1.1 pin, so budget accordingly for a cold run). Neithergit cloneworking nornix --versionworking proves this — probe it directly withcurl -sS -o /dev/null -w '%{http_code}\n' https://api.github.com/repos/NixOS/nixpkgs/commits/HEAD(expect200) before starting. Rewriting the project's own input togit+https://does not help: the transitive inputs are already locked asgithub:inside each dependency's ownflake.lock. If the probe fails,B1–B6are out of reach in that environment and the honest result is to record the blocker — the scaffold-side surface that needs no Nix (basecamp --help,basecamp docs, the missing-Nix hint,basecamp doctor, out-of-project errors) is still worth exercising and reporting as partial coverage. - Latest scaffold binary built from the repo root (
"$SCAFFOLD_BIN"). - A module project on disk whose
flake.nixexposespackages.<system>.lgx, built withlogos-module-builder0.2.x (see"$SCAFFOLD_BIN" basecamp docs). Reachable as$MODULE_PROJECT. That repo'stemplates/minimal-moduleis the smallest one that satisfies the contract. - Optional but strongly recommended for a cold run: basecamp's own binary cache. The pinned flake declares
extra-substituters = https://cache.nix.logos.co/public, but scaffold invokes a plainnix build, and a flake-declared substituter is only honored for a trusted user who accepts it. Without it, a coldbasecamp setupbuilds a Qt-heavy closure from source. Add the substituter and its key to~/.config/nix/nix.conf(as atrusted-usersmember) before timing anything, and say which mode a reported duration was measured in. scaffold.tomlis present at the project root; if not, run"$SCAFFOLD_BIN" initonce.
Commands / Actions
From the module project root:
cd "$MODULE_PROJECT"
test -f scaffold.toml || "$SCAFFOLD_BIN" init
"$SCAFFOLD_BIN" basecamp --help
"$SCAFFOLD_BIN" basecamp docs | head
grep -n '^\[repos.basecamp.attr\]\|^\[basecamp.profiles' scaffold.toml || true
"$SCAFFOLD_BIN" basecamp setup
ls .scaffold/basecamp/profiles
"$SCAFFOLD_BIN" basecamp doctor
"$SCAFFOLD_BIN" basecamp doctor --json
"$SCAFFOLD_BIN" basecamp setup
Expected Success Signals
basecamp --helplistssetup,modules,install,launch,develop,build,build-portable,run,doctor,paths, anddocs.basecamp docsprints the canonical project-compatibility rules, including per-profileenv_file,runtime_dir,log_file, custom profile names, and per-platform[repos.basecamp.attr].- First
basecamp setupclones the pinned basecamp repo into a pin-isolated cache path, buildsbasecampandlgpmvia Nix, seeds.scaffold/basecamp/profiles/alice/and.scaffold/basecamp/profiles/bob/, and reports completion. - If
[repos.basecamp.attr]is a per-platform map, setup uses the current host's attr and preserves the map plus scalar fallback on serialize. basecamp doctorreports the basecamp + lgpm binaries as present and both profiles as seeded;--jsonreturns parseable JSON with the same checks. Immediately after a green firstsetup(beforebasecamp modules) that is four PASS rows —basecamp binary,lgpm binary,basecamp profile alice,basecamp profile bob. A doctor that summarizes0 PASSthere is the regression: it leaves the user with no confirmation thatsetupactually landed.basecamp doctorshows abasecamp pin setrow. On a project using scaffold's defaults it passes and names both pins. It warns only when one of the pair is at the default and the other is not — that split is what silently breaks module loading, since the app embeds the same package-manager library the CLI installs with. A project deliberately pinned away from both defaults passes with a note, not a warning.- On a fresh
setupagainst the default pin, the built basecamp carries its own bundled modules inside the nix output (result/modules,result/plugins) rather than pushing them into the profile — so a freshly seeded profile'smodules/holding only the project's own modules is correct. - Second
basecamp setupis idempotent: pin unchanged → no rebuild reported, exit 0. - All commands run only inside the project; running them from outside the project must fail with the existing scaffold "not a logos-scaffold project" message.
Failure Signals / Common Pitfalls
- Raw nix or
lgpmstack traces with no scaffold-side hint are a UX regression — the setup-missing path is supposed to be a single one-line hint. - A
setupre-run that rebuilds when the pin has not changed is a regression in idempotency. - Profile directories under
.scaffold/basecamp/profiles/missing after firstsetupis a fail. - If
basecampcommands write to the user's global~/.local/share/Logos/or~/Library/Application Support/Logos/, that is a severe regression — basecamp state is project-local under.scaffold/basecamp/. - If the basecamp binary lands on
PATH, that is a contract violation.
Evidence to Capture
basecamp --helpoutput.- First and second
basecamp setupoutput (to compare rebuild vs. no-rebuild). basecamp doctorandbasecamp doctor --jsonoutput.- Listing of
.scaffold/basecamp/profiles/. - Relevant
scaffold.tomlexcerpt for[repos.basecamp.attr]and[basecamp.profiles.*]when present.
Execution Notes
- Do not pollute the user's home; basecamp setup must stay under
<project>/.scaffold/basecamp/. If something writes outside that root, stop and capture it before continuing. - Pin-changed re-runs (rebuild path) are a separate validation; capture them when intentionally bumping the pin, not as part of this scenario.
B2. Module Capture, Install, and Single-Instance Launch
Goal
Validate the per-project source of truth for module identity ([modules] in scaffold.toml), the install pipeline that builds .lgx artefacts and loads them via lgpm, resolved profile paths, and a single-profile launch.
Preconditions
- B1 completed in the same project.
- Module project's
flake.nix(root or one or more sub-flakes) exposespackages.<system>.lgx. Sub-flake projects (e.g.,tictactoe-ui-cpp/,tictactoe-ui-qml/) are valid. - A graphical environment if you intend to actually drive the launched basecamp UI;
launchitself does not require X/Wayland to start, but interactive validation does.
Commands / Actions
From the module project root:
"$SCAFFOLD_BIN" basecamp modules
grep -n '^\[modules\.' scaffold.toml
"$SCAFFOLD_BIN" basecamp modules --show
"$SCAFFOLD_BIN" basecamp install
"$SCAFFOLD_BIN" basecamp install --print-output
"$SCAFFOLD_BIN" basecamp doctor
"$SCAFFOLD_BIN" basecamp paths alice
"$SCAFFOLD_BIN" basecamp paths alice --json
"$SCAFFOLD_BIN" basecamp launch alice
To validate custom profile schema, add one profile and inspect it before launch:
[basecamp.profiles.maker]
env_file = ".scaffold/basecamp/maker.env"
runtime_dir = "/tmp/lgs-maker"
log_file = ".scaffold/basecamp/profiles/maker/basecamp.log"
[basecamp.profiles.maker.env]
LOGOS_PROFILE_ROLE = "maker"
printf 'MAKER_ONLY=1\nLOGOS_PROFILE_ROLE=env-file\n' > .scaffold/basecamp/maker.env
"$SCAFFOLD_BIN" basecamp paths maker --json
"$SCAFFOLD_BIN" basecamp launch maker --log-file
If your project does not auto-discover correctly, capture explicit sources:
"$SCAFFOLD_BIN" basecamp modules --flake "./tictactoe#lgx" --flake "./tictactoe-ui-qml#lgx"
"$SCAFFOLD_BIN" basecamp modules --path /abs/path/to/prebuilt.lgx
Expected Success Signals
basecamp moduleseither auto-discovers project sub-flakes exposing.#lgxor accepts explicit--path/--flakesources and writes one[modules.<name>]sub-section per source intoscaffold.toml. The file remains human-editable; re-runs are byte-identical and never overwrite existing keys.- For each captured project source, scaffold also resolves declared
dependenciesand insertsrole = "dependency"entries unless the dep is already keyed, is a module basecamp bundles itself (capability_module,main_ui,package_downloader,package_manager,package_manager_ui; seeBASECAMP_PREINSTALLED_MODULESinsrc/constants.rsfor the authoritative list — basecamp 0.2.x installs these next to its own binary, so they never appear in a profile'smodules/), or is resolvable via the source's ownflake.lock/ the scaffold-default table. - An unresolvable dep fails fast with a targeted error naming the dep and the two user-side fixes (capture as a project source, or add
[modules.<name>]withrole = "dependency"); no silent drop. basecamp modules --showprints the captured set without mutating state.basecamp installbuilds each project source (sibling--override-inputrewrites apply forpath:../<sibling>inputs in multi-flake projects) and shells out tolgpmto install into bothaliceandbob. By default it logs to.scaffold/logs/<ts>-install.logand prints a one-line status;--print-output(orLOGOS_SCAFFOLD_PRINT_OUTPUT=1) streams nix output directly.basecamp doctorreports each profile's installed modules matching the captured set; drift between[modules]and on-disk profile state is flagged, not hidden. Drift is compared on the normalized flake ref:basecamp modulespersists in-project sources relatively (path:.#lgx) while discovery yieldspath:/abs/root#lgx, so a doctor that reportsbasecamp drift: uncapturedfor a source already present in[modules]is comparing raw strings and is a false positive.basecamp paths <profile> --jsonis pure path resolution: it emits parseable JSON for XDG config/data/cache, runtime dir, module/plugin dirs, launch state, log file, and env file without building or mutating anything.- Custom profile names launch like default profiles when they are a single safe path component;
env_fileis sourced before global/profile inline env,runtime_diris exported as bothTMPDIRandXDG_RUNTIME_DIR, and--log-fileoverrides the configuredlog_file. launchprepares the runtime dir before it scrubs or reinstalls anything, and refuses to use one that is a symlink, is not a directory, or is owned by another user; it creates it0700and tightens loose permissions on an existing one. The default sits in world-writable/tmpunder a name derived from the project path, so a local attacker can claim it first — and whatever lands there holds the modules'logos_token_*sockets. A launch that follows a pre-planted symlink, or that scrubs the profile before discovering the runtime dir is unusable, is the regression.- With no configured
runtime_dir, every profile still gets one:basecamp paths <profile> --jsonreportstmpdir==xdg_runtime_dir==/tmp/lgs-<project-hash>-<profile>(the hash scopes it to the project root, so two checkouts never share a temp root). This path is deliberately outside the project tree —launchleaves livelogos_token_*Unix sockets in it, andnix build path:<project-root>#lgxrefuses to copy a socket (file ... has an unsupported type). An in-project temp root therefore broke everybasecamp install/basecamp launchafter the first launch, and made concurrentalice/boblaunches fail against each other's live sockets. Atmpdirthat resolves under<project>/.scaffold/by default is that regression; so is any socket found byfind .scaffold -type safter a launch. basecamp launch alicekills any priorlogos_host/LogosBasecampdescendants for that profile, scrubs the profile's XDG dirs under.scaffold/basecamp/profiles/alice/, reinstalls each captured source for that profile, setsXDG_{CONFIG,DATA,CACHE}_HOMEplusLOGOS_PROFILE=aliceandLOGOS_USER_DIR, andexecs basecamp.
Failure Signals / Common Pitfalls
- A flake that exposes only
.#lgx-portableand not.#lgxmust fail explicitly with a hint pointing at--flake <ref>#lgx-portablefor opt-in. Silent fallback is a contract violation. - Re-running
basecamp modulesoverwriting an existing key is a regression — manual edits inscaffold.tomlmust win. - An unresolved transitive
logos-module-builderinput that fails without naming the missingfollowsis a regression. installsucceeding when a build orlgpm installstep actually failed is a fail; exit codes must be non-zero on any source failure.launch alicewith an empty[modules]must bail (rather than scrubbing the profile and leaving it empty).- Custom profile names that are empty, absolute,
.,.., separator-containing, or contain control characters must be rejected before any filesystem work. - An env file key containing control characters must fail before spawning basecamp.
- Sibling
--override-inputnot being applied at probe time would surface as a build that resolves the wrong sibling pin duringbasecamp modulesauto-discovery; record any such mismatch with the exact derived module names.
Evidence to Capture
scaffold.tomlexcerpt showing[modules.<name>]sub-sections withflake,role, and (for project sources) the in-project relative path used.basecamp modules --showoutput.basecamp installlog path under.scaffold/logs/plus the printed one-line status, or the--print-outputstream.basecamp doctoroutput post-install.basecamp paths <profile> --jsonoutput for both a default profile and one configured profile.- The first lines of
basecamp launch aliceshowing the kill → scrub → reinstall → exec sequence. - For log checks, the log path and first lines proving stdout/stderr were tee'd to file and terminal.
Execution Notes
basecamp modulesis the sole automated writer of[modules]. If the user manually edited an entry, do not re-runbasecamp modulesmid-scenario without recording the pre-edit state — manual entries are intentionally preserved.- Only
path:../<sibling>flake inputs are sibling-rewritten;path:./sub,github:, andgit+schemes pass through. If a project uses multi-line input declarations, the line-level parser may not detect them — record any sibling-override miss along with the offendingflake.nixexcerpt.
B3. Two-Instance P2P Dogfooding
Goal
Validate the canonical basecamp use case: two profiles running simultaneously on one machine and exercising p2p features (chat, delivery, storage) of the project's .lgx modules.
Preconditions
- B1 and B2 completed in the same project.
basecamp installhas captured at least one project source and produced a successful install for bothaliceandbob.- A graphical environment for both basecamp windows.
Commands / Actions
From two terminals, both rooted at the module project:
Terminal 1:
"$SCAFFOLD_BIN" basecamp launch alice
Terminal 2:
"$SCAFFOLD_BIN" basecamp launch bob
If the project defines custom profiles such as maker and taker, repeat the same two-terminal check with those names.
Within the running UIs, exercise whatever p2p surface the module exposes (chat exchange, delivery between peers, storage round-trip). Capture screenshots or short transcripts.
Expected Success Signals
- Both basecamp windows open against their own profile dirs under
.scaffold/basecamp/profiles/{alice,bob}/. - Custom profile pairs open against their own profile dirs under
.scaffold/basecamp/profiles/<profile>/and their configured runtime/log/env paths. - Each window shows the project's
.lgxmodules installed and ready. LOGOS_PROFILE=aliceandLOGOS_PROFILE=bobare visible in each respective process environment (helpful for debugging).- Every process environment carries an absolute
LOGOS_USER_DIRpointing at that profile's own module root (.scaffold/basecamp/profiles/<profile>/xdg-data/Logos/LogosBasecampon a portable stack,…/LogosBasecampDevon the dev stack) — set automatically bylaunchon every host and stack, no manual export needed. On the macOS portable stack an absoluteLOGOS_DATA_DIRaccompanies it: that is the 0.1.x name for the same override, kept so a project pinned to a 0.1.x basecamp behaves identically. Which one the app honors depends on the[repos.basecamp]pin;launchwrites both there, so the check is the same either way. - The two instances do not collide on Qt remote-objects or any non-module port. Do not go hunting for per-module port-override env vars in the process environment: the registry they would flow in through is empty in v1 (no module has published a name yet), so
launchexports none. A module-level port collision betweenaliceandbobis therefore still possible, and belongs to the owning module rather than to scaffold. - A p2p interaction triggered from
aliceis observable inbob(and vice versa) within the module's expected latency window.
Failure Signals / Common Pitfalls
- Two windows opening but sharing identity keys, profile state, or message history is a clean-slate / XDG-isolation regression.
- On macOS — either stack, since 0.2.x ships a runnable dev build there too — both windows showing only basecamp's bundled modules and none of the project's
.lgxmodules, while their logs report a base data directory under the shared~/Library/Application Support/Logos/LogosBasecamp[Dev], is the profile-collapse signature: the app is not reading the per-profile module root at all. Check thatLOGOS_USER_DIR(plusLOGOS_DATA_DIRon the portable stack) is present, absolute, and distinct per profile in each process environment. - A non-module port collision (Qt remote objects, etc.) is a real finding — file upstream against the affected component, do not patch around it inside scaffold.
- A module that hardcodes its port is a known gap pending an upstream fix on that module. Scaffold exports no override for it to honor (see the signal above), so capture the module name and the observed collision — not a missing env var.
- One window crashing while the other survives is recordable evidence; capture the crashing instance's logs from
.scaffold/basecamp/profiles/<name>/before relaunching. - Running
basecamp launch alicetwice in parallel is undefined in v1 — record the behavior if you trip it accidentally, but don't treat it as a supported scenario.
Evidence to Capture
- The exact two-terminal command sequence used.
- A short transcript or screenshot pair showing a p2p interaction propagating from one instance to the other.
- The env block of each running process.
launchrecords the basecamp PID in.scaffold/basecamp/profiles/<profile>/launch.stateaspid=<pid>; read the env with:- Linux:
tr '\0' '\n' < /proc/<pid>/environ | grep -E 'XDG_|LOGOS_' - macOS (no
/proc):ps eww -p <pid>— read theXDG_*/LOGOS_*assignments off the line directly rather than splitting on spaces, since the profile-collapse target (~/Library/Application Support/…) contains one and would be truncated atApplication.
- Linux:
- Any port-collision error text verbatim, with the module that owns the colliding port.
- If custom profiles are used,
basecamp paths <profile> --jsonfor each profile and the resolved log/runtime dirs.
Execution Notes
- Do not start
aliceandbobfrom the same shell with&backgrounding unless you also redirect their logs; use two terminals for clean log separation. - If the underlying module surface is not yet wired for p2p between profiles, record the gap and the module's TODO state rather than declaring B3 a pass.
B4. Clean-Slate Verification
Goal
Validate that basecamp launch <profile> scrubs profile state on every invocation and that profile/path safety guards bound all filesystem work to the project.
Preconditions
- B2 completed (alice has captured modules and at least one successful install).
Commands / Actions
From the module project root:
"$SCAFFOLD_BIN" basecamp launch alice # let it come up, then close it
"$SCAFFOLD_BIN" basecamp paths alice --json
ls .scaffold/basecamp/profiles/alice
mkdir -p .scaffold/basecamp/profiles/alice/.scaffold-xdg-data/scratch
echo "marker-$(date -u +%s)" > .scaffold/basecamp/profiles/alice/.scaffold-xdg-data/scratch/marker.txt
"$SCAFFOLD_BIN" basecamp launch alice # scrub-and-reinstall
test -e .scaffold/basecamp/profiles/alice/.scaffold-xdg-data/scratch/marker.txt && echo "REGRESSION: marker survived clean launch" || echo "OK: marker scrubbed"
"$SCAFFOLD_BIN" basecamp paths ../escape
Expected Success Signals
launch aliceremoves any user-introduced files under the alice profile XDG dirs and reinstalls each captured source beforeexecing basecamp.rm -rfonlaunchis bounded to<project>/.scaffold/basecamp/profiles/<profile>/. Never any path outside that root.- A
launchthat finds no modules in[modules]bails before scrubbing (the empty-install + scrubbed profile combination is the regression we're guarding against). basecamp pathsrejects the same unsafe profile names aslaunchand remains non-mutating for valid profiles.- A second
launch alicewhile the first is still running terminates the first, leaving no orphan behind. Check withpgrep -fl LogosBasecamp(orps -o comm=) before and after. The reported process name is never simply the filelaunchexeced, because both generations start through a Qt-env launcher script: 0.2.x's dev build reports.LogosBasecamp(bin/LogosBasecampwraps the hidden real binary), 0.1.x reportsLogosBasecampeven thoughlaunchexecsbin/logos-basecamp(that launcher execs its differently-named sibling), and the portable stacks reportLogosBasecampdirectly. All three are expected — a surviving process of any of those names after the second launch is the regression, and it is a silent one: the kill is skipped rather than failing loudly. module_data/and basecamp's ownlogs/under the profile's module root are gone after a relaunch, like every other child of that root. That is clean-slate working as designed, not data loss:basecamp paths <profile>names both directories so their lifetime is discoverable before a module puts anything there.
Failure Signals / Common Pitfalls
- The
marker.txtfile survivinglaunch aliceis a regression: clean-slate is the v1 contract. - A
launchscrubbing a path outside the profile's XDG dirs is a severe safety regression — capture the offending path and stop. - An empty
[modules]plus alaunchthat wipes the profile and leaves it empty is a real regression; the empty-modules bail must fire first. - A custom
runtime_diron macOS that makes<runtime_dir>/logos_token_<module>_<pid>exceed the 104-byte Unix socket path budget is a dogfooding finding; keep custom values short, preferably under/tmp. Note that a customruntime_diris resolved relative to the project root, so pointing it back inside the project re-creates the socket-in-the-flake-tree failure described in B2 — prefer an absolute path under/tmp. launchscrubsxdg-data,xdg-cache, and the legacy in-profilexdg-tmp. The last one matters for profiles first launched by an older scaffold, which left sockets under<profile>/xdg-tmp; without that scrub such a project can never build its own root flake again.
Evidence to Capture
- The marker write and the post-launch listing showing it was scrubbed.
- The exact path under which the marker was placed and the path basecamp scrubbed (verify they match the profile root).
- Any unexpected paths touched by
launchoutside.scaffold/basecamp/profiles/<profile>/. - The unsafe-profile rejection output from
basecamp paths ../escape.
Execution Notes
- Use a marker filename and timestamp you can search for after the fact; do not rely on visual inspection alone.
- Clean-slate state is project-local; never test scrub behavior against the user's global Logos directories.
B5. Module Artefact Builds by Variant
Goal
Validate that project sources captured under [modules] with role = "project" can be built against their #lgx and #lgx-portable flake outputs, that --module narrows the build, and that the old build-portable command remains a compatibility alias for the portable variant.
Preconditions
- B2 completed (project sources are captured and
basecamp installhas succeeded against.#lgx). - The same flakes expose
packages.<system>.lgxand, for portable checks,packages.<system>.lgx-portable.
Commands / Actions
From the module project root:
"$SCAFFOLD_BIN" basecamp build --variant all
"$SCAFFOLD_BIN" basecamp build --variant lgx --module <module-name>
"$SCAFFOLD_BIN" basecamp build --variant lgx-portable --module <module-name>
"$SCAFFOLD_BIN" basecamp build-portable
find .scaffold/basecamp -maxdepth 3 -type f -o -type l | sort
Expected Success Signals
basecamp build --variant allbuilds both.#lgxand.#lgx-portablefor eachrole = "project"entry in dependency order, then writes/symlinks outputs under.scaffold/basecamp/<variant-dir>/.--module <module-name>builds only that captured project module and fails clearly for an unknown module.build-portablebehaves likebasecamp build --variant lgx-portableand keeps the historical.scaffold/basecamp/portable/output directory.role = "dependency"entries are skipped by build commands; dependencies are runtime inputs provided by install/basecamp.- A flake that does not expose the requested variant fails with a targeted error naming the missing attribute, not a raw nix trace or silent fallback.
Failure Signals / Common Pitfalls
- Any requested variant that silently falls back to another variant is a contract violation.
- An empty, duplicated, unknown, or path-like variant value through the Rust API must be rejected or normalized before filesystem work.
- Building dependency entries (those with
role = "dependency") is wasted work and a behavior regression. - Out-of-order builds that ignore the dependency graph between project sources are a regression introduced by changes to ordering logic.
Evidence to Capture
basecamp buildandbasecamp build-portableoutput excerpts including the per-source build lines.- The directory listing of the produced artefacts under
.scaffold/. - For any failure, the exact missing flake attribute and the offending project source.
Execution Notes
- This scenario does not exercise the AppImage itself. Hand-loading into a basecamp AppImage is owned by the AppImage release, not by scaffold.
B6. Captured Module Run Loop
Goal
Validate that basecamp run launches a captured module from its flake for the local development loop, and that host selection is predictable.
Preconditions
- B2 completed and
[modules.<name>]contains at least onerole = "project"module captured from a flake, not from a prebuilt.lgxfile. - For standalone UI checks, the module flake exposes
apps.<system>.defaultor the attr named by[modules.<name>].standalone_app.
Commands / Actions
From the module project root:
"$SCAFFOLD_BIN" basecamp run <module-name> --host standalone
"$SCAFFOLD_BIN" basecamp run <module-name>
For one negative-path check, capture or hand-edit a module entry that points at a prebuilt .lgx path and run:
"$SCAFFOLD_BIN" basecamp run <path-captured-module>
Expected Success Signals
--host standaloneinvokesnix runfor the module flake's default app, or#<standalone_app>when that config key is set.- With no
--host, the run defaults tostandalone(the only host today). - A module captured as a prebuilt
.lgxpath is rejected with guidance to edit/remove the entry and capture a flake source;nix runis not attempted. - Running a module as a configured Basecamp peer (one-shot build + install + launch) is not yet available; use
basecamp installthenbasecamp launch <profile>. Tracked as follow-up work.
Failure Signals / Common Pitfalls
- Running a
.lgxpath source throughnix runis a regression; path captures are installable artefacts, not flake apps. - A remote flake ref without an explicit fragment must still receive the requested app/build attr when scaffold constructs the Nix command.
- An omitted
standalone_appmust not serialize back asstandalone_app = "".
Evidence to Capture
- Command output for standalone/default-host/basecamp-host paths.
- The
[modules.<name>]excerpt showingflake,role, optionalstandalone_app, and whether the source is a flake or.lgxpath. - Any rejected
.lgxpath-source error verbatim.
B7. Pin-Set Contract Checks Without a Basecamp App Build
Goal
Validate the half of the basecamp pin set that does not require building basecamp itself: that [repos.lgpm] builds, that the project's .lgx carries what that lgpm validates, that a real lgpm install succeeds with the exact flags scaffold passes, and that the launcher scaffold would exec is the one that actually starts.
This exists because B1's Nix precondition is the heaviest in this runbook and fails for a second reason beyond network policy: memory. Evaluating the basecamp 0.2.3 flake (~10k lock nodes) needs more RAM than a small container has, and the failure is a bare SIGKILL with no error text. B1 is then out of reach while most of the pin set is still verifiable — this scenario is what to run instead of reporting "no coverage".
Preconditions
- Nix with flakes enabled and the GitHub access
B1describes. - A module project built with
logos-module-builder0.2.x, reachable as$MODULE_PROJECT. - No basecamp build required. If you have one, prefer
B1–B6.
Commands / Actions
# Is this an eval-memory failure rather than a build failure?
# --dry-run only evaluates. If this is SIGKILLed, B1 is blocked on RAM.
cd "$MODULE_PROJECT" && nix build .#app --dry-run # only when diagnosing a B1 SIGKILL
# 1. The pinned lgpm builds, both stacks.
LGPM=$(grep -A2 '^\[repos.lgpm\]' scaffold.toml | sed -n 's/^pin = "\(.*\)"/\1/p')
nix build "github:logos-co/logos-package-manager/$LGPM#cli" --out-link /tmp/lgpm-dev
nix build "github:logos-co/logos-package-manager/$LGPM#cli-portable" --out-link /tmp/lgpm-portable
# 2. The project's .lgx builds and carries content hashes.
nix build .#lgx --out-link /tmp/mod-lgx
tar xzf /tmp/mod-lgx/*.lgx -C "$(mktemp -d)" && jq '.hashes.root, .name' <manifest.json
# 3. A real install with scaffold's exact argument shape.
mkdir -p /tmp/p/modules /tmp/p/plugins
/tmp/lgpm-dev/bin/lgpm --modules-dir /tmp/p/modules --ui-plugins-dir /tmp/p/plugins \
install --file /tmp/mod-lgx/*.lgx
# 4. Negative: the two failures `basecamp install` turns into hints.
# (a) strip `hashes` from manifest.json, repack, install -> missing-hash path
# (b) install the dev .lgx with the *portable* lgpm -> variant-mismatch path
/tmp/lgpm-portable/bin/lgpm --modules-dir /tmp/p/modules --ui-plugins-dir /tmp/p/plugins \
install --file /tmp/mod-lgx/*.lgx
# 5. If any basecamp `#app` output is on hand (store path or an old result link),
# check which entry point scaffold would exec, and that it actually starts.
ls "$APP"/bin
env -i HOME=/tmp PATH=/usr/bin:/bin QT_QPA_PLATFORM=offscreen "$APP"/bin/<entry> --version
Expected Success Signals
- Both
lgpmattrs build (or substitute) and exposebin/lgpm. - The
.lgxmanifest has a non-emptyhashes.root, and its variant directory is<host>-devfor#lgx. - The real install prints
Installed to: <modules-dir>and exits 0, creating<modules-dir>/<module_name>/.Warning: Package is unsignedis expected and not a failure — the default signature policy iswarn, and validation still runs underneath it. - Repacking matters:
tar czf out.lgx .produces./-prefixed members and lgpm rejects the package for an unrelated reason. Pack member names exactly as the original (tar czf out.lgx manifest.json variants) or the negative test proves nothing. - Hash-stripped install fails with
Package validation failed: Missing content hashes in manifest— the stringbasecamp installmaps to its rebuild hint. - Dev
.lgxunder the portablelgpmfails withPackage does not contain variant for platform: <host> (package provides: <host>-dev)— the string mapped to the stack-mismatch hint. - Not every
Package validation failed:is a hash problem. The same banner covers malformed manifests (e.g.Manifest: 'name' field is empty). A hint that answers those with "rebuild with newer tooling" is a regression — the raw stderr is the better message there. - The entry point scaffold resolves is a launcher, not a raw binary. Both generations ship a
/bin/shscript that exportsQT_PLUGIN_PATH/QML2_IMPORT_PATH/LD_LIBRARY_PATHand then execs the real binary, but they name it differently: 0.1.x usesbin/logos-basecamp(itsbin/LogosBasecampis the raw ELF), while 0.2.x has nobin/logos-basecampand makesbin/LogosBasecampthe launcher over a hiddenbin/.LogosBasecamp. Launching the raw binary dies at exec withlibQt6RemoteObjects.so.6: cannot open shared object file; the launcher reachesLogos Core started successfully!. That one-line difference is the whole check.
Failure Signals / Common Pitfalls
- A
SIGKILLwith a log ending mid-copying pathor right after anevaluation warning:is out-of-memory, not a broken pin. Confirm with--dry-runbefore filing anything against the pin. - An
lgpmpin that does not build at all means the pin set is wrong at the source — stop and fixDEFAULT_LGPM_PINbefore running anything else. - A
.lgxwith nohashes.rootmeans the module was built by tutorial-era tooling; that is the module's pin to fix, not scaffold's.
Evidence to Capture
- The two
lgpmbuild result paths and the.lgxmanifesthashes.root. - Full stdout/stderr of the successful install and of both negative installs.
ls <app>/binplus the launcher-vs-raw-binary startup comparison.
A1. Public Rust API Surface
Goal
Validate that the public logos_scaffold::api library surface exists, is documented, and lets a Rust consumer drive a scaffold project (open by explicit root, inspect paths, read localnet status, categorized errors) without shelling out to the CLI. The API is the library boundary the test-node integration features build on.
Preconditions
- Latest scaffold checkout at the repo root.
- A generated default-template project exists at
$SCRATCH_ROOT/dogfood-default(D1).
Commands / Actions
From the repo root, confirm the surface builds and documents cleanly:
cargo doc --no-deps
cargo test --doc
Then exercise the API from a throwaway consumer. Either add a dev-dependency on the local crate from a scratch crate, or drive it through a one-off integration test inside the repo:
use logos_scaffold::api::{LocalnetStartOptions, Project};
fn main() -> logos_scaffold::api::Result<()> {
// Explicit root — no cwd discovery.
let project = Project::open("/abs/path/to/dogfood-default")?;
println!("rpc = {}", project.localnet_rpc_url());
let paths = project.paths()?;
println!("sequencer = {}", paths.sequencer_binary.display());
println!("cache_root = {} (from {})", paths.cache_root.display(), paths.cache_root_source);
// Typed status — same model as `localnet status --json`.
let status = project.localnet_status();
println!("ready = {}", status.ready);
// Opening a non-project directory yields a categorized Config error.
if let Err(err) = Project::open("/tmp") {
println!("expected config error: {err}");
}
Ok(())
}
Expected Success Signals
cargo doc --no-depsbuilds rustdoc for theapimodule, andcargo test --docpasses theapidoctests (setup / localnet lifecycle / wallet topup / deploy / doctor / report / test-node examples).Project::open(root)loads a project from an explicit root with no dependency on the process working directory;Project::discover(dir)walks upward to findscaffold.toml.Project::paths()reports the resolved cache root (and which layer supplied it), pinned repo checkouts, vendored binary paths, wallet home, localnet state/log, and circuits dir — whether or not they exist yet.Project::localnet_status()returns the same typed model the CLI prints underlocalnet status --json.- Errors are categorized (
api::Error::{Config, MissingTool, RepoState, Process, Timeout, Transport, Command, Other}); a missing/unreadablescaffold.tomlis aConfigerror, and external-command failures carry a structuredCommandFailed(rendered command, exit code, captured output).
Failure Signals / Common Pitfalls
- A doctest failure in the
apimodule means a documented example drifted from the surface — fix the example or the doc, do not delete the test. - If
Project::openon a directory withoutscaffold.tomlreturns an uncategorized error (notError::Config), that is a regression in the error contract. - If an operation silently depends on the process cwd instead of the project root passed in, record it — explicit-root targeting is the core API guarantee.
Evidence to Capture
cargo doc/cargo test --docoutput lines for theapimodule.- The consumer program's output showing the resolved paths, status, and the categorized error.
Execution Notes
- This scenario validates the library boundary only; it does not require a running localnet. Pair it with T1–T3 when validating the
test-nodeAPI.
T1. Isolated Test-Node Lifecycle and Caller-Project Pins
Goal
Validate that test-node spins up isolated, short-lived sequencer instances (own port, config, database, logs, runtime dir) for integration tests, that the prerequisite/pin commands resolve the caller project's LEZ and circuits pins, and that lifecycle commands (start/status/stop/run) are clean and machine-readable.
Preconditions
- The real sequencer toolchain is provisioned (r0vm + circuits + built
sequencer_service) per "Provisioning the Real LEZ Sequencer Toolchain".test-node preparebuilds/fetches the sequencer and circuits on demand;setupis not required for T1–T3 (it is only needed for T4's wallet). - No requirement that the developer
localnetis running — test nodes are independent of it. - Run this against a real node. On a provisioned box,
test-node doctor --jsonreturns"ok": truewith every checkpass,test-node prepareends withtest-node prerequisites ready, andtest-node startyields a realpid/rpc_urlwhoseblock_idrises within seconds as the sequencer produces clock blocks. Do not substitute a stub for the node here.
Commands / Actions
From the generated project root:
"$SCAFFOLD_BIN" test-node pins
"$SCAFFOLD_BIN" test-node pins --json
"$SCAFFOLD_BIN" test-node doctor
"$SCAFFOLD_BIN" test-node prepare --json
"$SCAFFOLD_BIN" test-node start --json
# capture the node id (state_dir basename) and rpc_url from the JSON
"$SCAFFOLD_BIN" test-node status --node <node-id> --json
"$SCAFFOLD_BIN" test-node stop --node <node-id>
"$SCAFFOLD_BIN" test-node run --serial --block-create-timeout-ms 500 --retry-pending-blocks-timeout-ms 500 -- sh -c 'echo "rpc=$LGS_TEST_NODE_RPC_URL port=$LGS_TEST_NODE_PORT"'
The pin/prepare/doctor commands also accept --project <root> so they can be driven from outside the project directory.
Expected Success Signals
test-node pinsreports the LEZ source/ref, resolved commit, checkout path and ownership (managed_cachevscaller_provided), sequencer binary path, and circuits version/path — each annotated with its origin (cli_override->project_config->scaffold_default). For projects with[circuits], the reported circuits version matches the project config; startup should still materialize the configured circuits install before launching the node.test-node doctorreports pin drift, checkout presence/commit/cleanliness, sequencer binary, circuits release, and platform support as separate categorized checks; exits non-zero only when a real prerequisite is missing.test-node prepareresolves the project's pins, ensures the checkout + circuits, builds the standalone sequencer, and (with--json) reports the checkout, resolved commit, binary path, and circuits path.test-node start --jsonprints at leastrpc_url,pid,state_dir,config_path,log_path,genesis_block_id, and currentblock_height; the node runs on its own port under.scaffold/test-nodes/<id>/and does not touch the vendored LEZ checkout or the developer localnet.test-node start --port 0(the default) selects an unused localhost port;test-node status --node <id> --jsonreportshealthyand the servedrpc_url, exiting non-zero when unhealthy.test-node start/test-node runwith--block-create-timeout-msand--retry-pending-blocks-timeout-mspatch those sequencer config values as millisecond strings (for example,500msfor faster local tests) in the runtimesequencer_config.json; accepted values are 1 to 3,600,000 ms, and omitting them preserves the pinned debug config values. Values near or below the stable-read sample cadence can keepclock wait-stableand account-boundary reads from converging.test-node stop --node <id>terminates only that node and removes its runtime state (unless--preserve-work-dir).test-node run -- <cmd>starts a node, waits for health, exportsLGS_TEST_NODE_RPC_URL/LGS_TEST_NODE_PORT/LGS_TEST_NODE_STATE_DIR(and friends) to the child, forwards the child's exit status, and stops the node afterward;--serialcaps concurrent node creation at one and--parallel Nat N.
Failure Signals / Common Pitfalls
- A node that reuses the developer
localnetport, writes into the vendored LEZ checkout, or otherwise is not isolated is a contract violation. test-node startwith an explicit--portalready in use must fail fast with a port-conflict error, not hang.- If a caller-provided LEZ checkout (
[repos.lez].path, or a local dir via--lez-source) is reset/force-checked-out byprepare, that is a severe regression — caller checkouts are validated, never mutated. - A
runthat leaks the node (does not stop it) after the child exits, or does not forward the child's non-zero exit, is a regression. - Missing sequencer binary or circuits must produce a clear scaffold-side error pointing at
test-node prepare, not a raw panic.
Evidence to Capture
test-node pins --jsonandtest-node doctoroutput.- The circuits version from
test-node pins --json, and the configured install dir aftertest-node startor another command that materializes project circuits. test-node start --jsonoutput (the full connection record) and the.scaffold/test-nodes/<id>/listing.test-node status --jsonfor the running and stopped states.test-node runoutput showing the exportedLGS_TEST_NODE_*env reaching the child.
Execution Notes
- Test nodes are designed to be ephemeral; prefer fresh nodes per scenario and rely on
stop(or handleDropin the API) for teardown. Use--preserve-work-dironly when you need to inspect a node's database/logs after the fact. - Keep test-node runs independent of the
localnetscenarios (D1/D2): they are separate sequencer instances by design.
T2. Test-Node Typed RPC Reads: Transactions, Blocks, Clock, Accounts, Proofs
Goal
Validate that the test-node RPC subcommands give integration tests definitive, structured observations against a running node — terminal transaction outcomes, block/clock context for replay, and stable account/proof reads for parity assertions — instead of hand-rolled JSON-RPC scraping.
Preconditions
- A test node is running (T1): capture its
rpc_url(e.g.,export TN_URL=http://127.0.0.1:<port>). - A transaction file is available for the
txchecks. For a quick negative-path check, any base64 borsh blob works; for a committed-path check, use a transaction produced by an example runner or wallet against the same node.
Commands / Actions
Against the running node URL:
# Blocks and clock (no transaction needed)
"$SCAFFOLD_BIN" test-node blocks head --url "$TN_URL" --json
"$SCAFFOLD_BIN" test-node blocks range --url "$TN_URL" --from 1 --to 3 --json
"$SCAFFOLD_BIN" test-node blocks wait --url "$TN_URL" --after 1 --count 1 --json
"$SCAFFOLD_BIN" test-node clock read --url "$TN_URL" --json
"$SCAFFOLD_BIN" test-node clock wait-stable --url "$TN_URL" --samples 2 --json
# Transactions
"$SCAFFOLD_BIN" test-node tx submit-and-wait --url "$TN_URL" --file ./tx.b64 --encoding borsh-base64 --json
"$SCAFFOLD_BIN" test-node tx submit --url "$TN_URL" --file ./tx.b64 --json
"$SCAFFOLD_BIN" test-node tx wait --url "$TN_URL" --hash <tx-hash> --json
# Accounts and proofs (parity assertions)
"$SCAFFOLD_BIN" test-node account get --url "$TN_URL" --account-id <id> --json
"$SCAFFOLD_BIN" test-node account batch-get --url "$TN_URL" --account-id <id-a> --account-id <id-b> --json
"$SCAFFOLD_BIN" test-node proof get --url "$TN_URL" --commitment <hex-or-base58> --json
"$SCAFFOLD_BIN" test-node snapshot accounts --url "$TN_URL" --account-id <id> --output ./accounts-snapshot.json --json
Expected Success Signals
tx submit-and-wait --jsonemits exactly one terminal outcome object:committed(with the actual sequencerblock_idandtimestamp),rejected(phase=stateless|stateful, withreasonorobserved_after_block_id),timeout(last_observed_block_id),transport_error, orwire_mismatch; it exits non-zero for anything butcommitted. Transport failures are never reported as business rejections, and a stateful rejection follows an explicit multi-block observation rule (not a single sleep).tx submitreturns the node-assigned tx hash or a structured stateless rejection;tx waitobserves a previously submitted hash, honoring--after-blockwhen supplied.blocks head/blocks rangereport each block's id and timestamp and classify it explicitly: genesis (the only zero-transaction block — no clock tick to replay), clock-only (empty post-genesis blocks still advance clock state via the mandatory clock transaction), and blocks carrying user transactions, with per-tx hashes for public/deployment transactions.blocks waitreturns the requested number of blocks after the boundary.clock readreturns all three/LEZ/ClockProgramAccount/...accounts with decodedblock_id/timestamp;clock wait-stablereturns a snapshot only after consecutive identical samples (head + clock state), or a retryable timeout error.account getdistinguishespresent(with lossless base64 account bytes plus decoded balance/nonce/owner/data),missing(never written), anddecode_error; every read reports theblock_idit was scoped to.batch-getreads all accounts at one consistent block boundary.proof getdistinguishes a missing commitment (proof: null), an invalid commitment (local error before any RPC), and transport failures.snapshot accountswrites a block-consistent JSON snapshot to the--outputpath.
Against a real node specifically (this is the highest-value check — the client's hand-rolled borsh parsers must match genuine sequencer output, not just the unit-test fixtures): a freshly started node produces clock-only blocks where blocks head --json shows transaction_count: 1, the single tx has is_clock: true and kind: "public", and fully_parsed: true; clock read --json decodes real ClockAccountData where the /0000001 account's block_id tracks the head while /0000010 and /0000050 lag at their slower cadence; account get on a seeded account returns the exact seeded balance (see T3). Any mismatch here is a wire-format regression that the in-process stub would not catch.
Failure Signals / Common Pitfalls
- A
tx submit-and-waitthat collapses transport errors or timeouts into "rejected" (or that exits zero for a non-committed outcome) is a regression in the divergence-detection contract. - A block classification that marks genesis as having a clock transaction, or hides empty post-genesis (clock-only) blocks, breaks clock-sensitive replay.
- An account read that does not report its block boundary, or that races a clock block and returns inconsistent data instead of a stable result / structured retryable error, is a parity regression.
- A
proof getthat conflates "missing commitment" with "invalid commitment" or with a transport failure is a regression.
Evidence to Capture
- One
tx submit-and-wait --jsoncommitted object and (if probed) one non-committed outcome. blocks range --jsonoutput showing the genesis / clock-only / user-tx classification.clock read --jsonand aclock wait-stableresult.account get --jsonfor present and missing accounts, and aproof get --jsonfor present and missing commitments.
Execution Notes
- The RPC-scoped subcommands target a node URL, not a project directory, so they can run anywhere once
$TN_URLis known. - These are the typed equivalents of the
logos_scaffold::api::testnode::TestNodeClientmethods; when validating an API change, exercise both the CLI and the client.
T3. Test-Node Caller-Provided State Seeding
Goal
Validate that a test node can start from a caller-provided state snapshot — not only from empty localnet state — with validation up front and exact-state startup (no implicit wallets or default testnet accounts).
Preconditions
- T1 prerequisites met (sequencer binary + circuits available for the project).
- A running node (T1) if you intend to
state exportfrom it; otherwise a hand-written snapshot file suffices.
Commands / Actions
From the generated project root:
"$SCAFFOLD_BIN" test-node state schema --json
# Author a minimal snapshot (public account with a balance), then seed from it:
cat > ./seed.json <<'JSON'
{ "format": "lgs-state-snapshot/1",
"public_accounts": [ { "account_id": "<base58-account-id>", "balance": 5000 } ],
"private_accounts": [] }
JSON
"$SCAFFOLD_BIN" test-node state seed --input ./seed.json --output ./seeded-state --json
"$SCAFFOLD_BIN" test-node start --state ./seeded-state --json
# Optionally export public balances from a running node into a snapshot:
"$SCAFFOLD_BIN" test-node state export --url "$TN_URL" --account-id <id> --output ./exported.json --json
# Negative path: an unsupported format must be rejected with a categorized error.
echo '{"format":"bogus/1"}' > ./bad.json
"$SCAFFOLD_BIN" test-node state seed --input ./bad.json # expect format-mismatch error, non-zero exit
Expected Success Signals
state schemaidentifies the exact snapshot formats the project's pins accept (lgs-state-snapshot/1, thelgs-account-snapshot/1output ofsnapshot accounts, or a rocksdb state directory), the state format version (nssa-v03), the LEZ ref/commit, and the seedable account fields.state seedvalidates the snapshot before producing a state directory and reports the seed kind (configvsdatabase), the LEZ commit, the state format version, and the public/private account counts. Validation errors are distinguished: format mismatch, storage-schema mismatch (e.g. public-account data, which the genesis config cannot seed), LEZ pin mismatch, and account decode errors.test-node start --state ./seeded-statestarts from exactly the snapshot's accounts — the sequencer builds genesis state frominitial_public_accounts/initial_private_accountswith no implicit wallets, sample programs, or default testnet accounts. A database-seeded directory (a node's preservedrocksdb/) resumes from it verbatim.state exportwrites named public-account balances from a running node into anlgs-state-snapshot/1file (the pinned RPC exposes public balances only; full-fidelity state comes from a stopped node's database directory, which the command output notes).- Confirmed against a real node: seed an account at a distinctive balance (e.g. 4242),
start --state, thentest-node account get --account-id <id>returnsstate: presentwithbalance: 4242and the account exists at genesis — proving the sequencer built genesis from exactly the snapshot (no testnet defaults). This is the end-to-end proof that exact-state seeding works, not just that the file validated.
Failure Signals / Common Pitfalls
- A node started with
--statethat injects extra accounts (default testnet wallets, sample programs) beyond the snapshot is a regression — seeded startup must be exact. - A snapshot needing unsupported state (e.g. public-account data/nonce via the genesis config) that fails late inside the node instead of up front during
state seedvalidation is a regression. - A pin mismatch (snapshot's
lez_commitdiffers from the project's resolved pin) that is silently accepted is a regression. - An unknown snapshot format accepted instead of rejected with a
format mismatcherror is a regression.
Evidence to Capture
state schema --jsonoutput.state seed --jsonoutput showing the seed kind, account counts, and (for the negative path) the categorized error.test-node start --state ... --jsonoutput and atest-node account getagainst a seeded account confirming the exact seeded balance.
Execution Notes
- Pair this scenario with T2: after seeding and starting, use
account get/account batch-getto assert the seeded accounts match the snapshot exactly. - Database seeding (
--state <dir with rocksdb/>) is the full-fidelity path; the JSON snapshot path is balance-and-commitment level by design at the pinned revision.
T4. Real Committed User Transaction (Test-Node + Wallet)
Goal
The capstone end-to-end proof: a real wallet transaction, executed by a real test-node sequencer, observed as committed through the test-node client. This ties the test-node feature to the rest of the stack and validates the transaction-bearing block path against genuine sequencer output — the one thing a stub cannot prove.
Preconditions
- Full toolchain provisioned including the wallet: r0vm + circuits + real
sequencer_service(T1) andsetupcomplete so the LEZ-localwalletis built and the default wallet is seeded (see "Provisioning the Real LEZ Sequencer Toolchain", step 4). - The wallet targets the
sequencer_addrin the wallet config (wallet_config.jsonunder the wallet home named by$NSSA_WALLET_HOME_DIR/$LEE_WALLET_HOME_DIR) when set; otherwise it defaults tohttp://127.0.0.1:<localnet.port>(default 3040). Start the test-node on that port (or updatesequencer_addr) so the wallet talks to it.
Commands / Actions
From the project root:
SJ=$("$SCAFFOLD_BIN" test-node start --project "$P" --port 3040 --json)
URL=$(echo "$SJ" | jq -r .rpc_url); NODE=$(echo "$SJ" | jq -r .state_dir | xargs basename)
HEAD0=$("$SCAFFOLD_BIN" test-node blocks head --url "$URL" --json | jq -r .block_id)
# Submit a REAL faucet transaction against the test-node:
"$SCAFFOLD_BIN" wallet topup # account-get → (auth-transfer init if uninitialized) → pinata claim
# Observe the committed USER transaction(s) through the test-node client:
"$SCAFFOLD_BIN" test-node blocks wait --url "$URL" --after "$HEAD0" --count 6 --timeout-sec 90 --json \
| jq -c '.blocks[] | select(.has_user_transactions) | {block_id, transaction_count, user:[.transactions[]|select(.is_clock|not)|{hash:.hash[0:12],kind}]}'
"$SCAFFOLD_BIN" test-node stop --node "$NODE" --project "$P"
Expected Success Signals
wallet topupexits 0 against the test-node: the real sequencer executes theauth-transfer initandpinata claimtransactions (via r0vm in dev mode).blocks waitsurfaces one or more blocks withhas_user_transactions: true, each withtransaction_count: 2(the mandatory clock tx plus the user tx) and a non-clockkind: "public"user tx carrying a real sha256 hash — parsed by the same client/borsh path the unit tests exercise, now against genuine block bytes.- Equivalently, a wallet-reported tx hash (when one is printed) resolves to
committedviatest-node tx wait --url "$URL" --hash <hash> --json.
Failure Signals / Common Pitfalls
wallet topupfailing with a sequencer-unreachable hint means the test-node is not on the wallet's expected port — start it with--port <localnet.port>.- A block parser that mis-classifies the user-tx block as clock-only, or reports
fully_parsed: falseon a plain public/deploy tx, against real bytes is a wire-format regression — the highest-value signal this scenario protects. - A real sequencer that boots but never executes the tx (block count rises with clock-only blocks but no user tx ever lands) usually means r0vm is missing or version-mismatched — recheck the exact version match in provisioning step 2.
Evidence to Capture
wallet topupoutput (the account-get → init → claim sequence) and its exit code.- The
blocks waitJSON showing the real committed user transaction(s). - The node id and confirmation it was stopped and its runtime dir cleaned up.
Execution Notes
- This is the only scenario that requires both the sequencer and the wallet built; it is the capstone real-e2e check. The block parsing it exercises is shared with T2, so a green T4 is strong evidence the entire
blocks/txclient surface is correct against real output. - A previous agent run validated this exact flow:
wallet topupproducedauth-transfer initandpinata claimtxs that landed in real blocks (transaction_count: 2, apublicuser tx distinct from the clock tx), observed throughblocks wait. Reproduce it; do not downgrade to a stub.
Minimum Rerun Guidance for Future Changes
- Changes to onboarding, project creation, setup, localnet, or build flows: rerun
D1,D2, andD6. - Changes to deploy behavior or deploy output formatting: rerun
D3andD6. - Changes to wallet flows or wallet-related defaults: rerun
D4. - Changes to the wallet-home env contract (
WALLET_HOME_ENV_VARS,set_wallet_home_env, or any new upstream rename), to default-wallet seeding (ensure_default_wallet_seededand its storage-initialization fallback), or to the LEZ pin across the v0.2.0 boundary: rerunD1,D4,D6, andD7—D1/D4cover seeding,D6covers the direct-cargo runexport,D7covers the hook env contract. - Changes to diagnostics, report contents, or redaction logic: rerun
D5. - Changes to example runner binaries or template
src/bin/*code: rerunD6. - Changes to
runstep ordering, thedeploy = falsedeploy-skip branch, thetopup = falsetopup-skip branch, post-deploy env vars, post-deploy CLI override flag handling, or[run]config parsing: rerunD7. - Changes to LEZ template scaffolding or generated outputs: rerun
L1,L2,L3, andL4. - Changes to CLI argument parsing, help text, or error messages: rerun
E1. - Changes to
create/newflags or template selection logic: rerunE2. - Changes to AI skill materialization (
apply_skills, the canonicalskills/source, frontmatter rewrite,AGENTS.mdtemplate, orinitre-run semantics): rerunE3. - Changes to
basecamp setup(pin sync, lgpm build, profile seeding, idempotency), per-platform[repos.basecamp.attr], orbasecamp doctor: rerunB1. - Changes to
[modules]derivation, dependency resolution, sibling--override-inputhandling, orbasecamp installinvocation oflgpm: rerunB2. - Changes to
basecamp paths,[basecamp.profiles.*],env_file,runtime_dir,log_file,launch --log-file, or single-profile launch path resolution: rerunB2. - Changes to
basecamp launch(kill-and-scrub semantics, XDG isolation, runtime/log/env export, port-override env vars, p2p surface): rerunB3. - Changes to clean-slate scrub semantics, profile-name validation, path-root bounds, or the empty
[modules]guard onlaunch: rerunB4. - Changes to
basecamp build,basecamp build-portable, variant normalization,--modulefiltering, or build attr selection: rerunB5. - Changes to
basecamp run,standalone_app, or module source validation for run: rerunB6. - Changes to
resolve_basecamp_binary,basecamp_comm_candidates, orlgpm_install_hint: rerunB7(andB4for the relaunch kill path). These are the pieces that silently do the wrong thing rather than failing — a wrong entry point starts an app with no Qt environment, an unmatchedcommskips the kill, and an over-broad hint misdirects. - Changes to the basecamp/
lgpm/companion pin set (DEFAULT_BASECAMP_PIN,DEFAULT_LGPM_PIN,BASECAMP_DEPENDENCIES,BASECAMP_PREINSTALLED_MODULES): rerun the wholeBseries on both a Linux and a macOS host, and on macOS cover bothattr = "app"andattr = "bin-macos-app". The three pins are one set — the app embeds the same package-manager library the CLI installs with — so a change to any of them re-opens every basecamp scenario. Confirm from the built$outwhat the release actually bundles ($out/modules,$out/plugins) rather than trusting the constant. Where the basecamp app build is out of reach (a container without the RAM to evaluate the flake), runB7and report it as the partial coverage it is — it still exercises thelgpmpin, the.lgxcontract, both install-hint paths, and the launcher selection. - Changes to the public
logos_scaffold::apisurface (entry points, typed result models, categorized errors,CommandFailed, or the documented examples/doctests): rerunA1, and rerun the matching CLI scenario for any command whose*_for_projectcore changed. - Changes to
test-nodelifecycle, pin resolution, prepare/doctor, run-slot concurrency, or caller-checkout validation: rerunT1(andA1if theapi::testnodelifecycle types changed). - Changes to the
test-nodeRPC client (transaction outcomes, block/clock parsing, account/proof reads, or their JSON shapes): rerunT2. - Changes to
test-nodestate seeding (snapshot formats, validation classes, genesis-config injection, or database seeding): rerunT3. - Changes to the transaction-bearing block path (
sendTransaction/getBlockhandling, the committed-block scan, user-vs-clock classification, or the r0vm/sequencer spawn env): rerunT4(the only check that proves the path against a real executed transaction). - Changes to
[circuits]config parsing/serialization, circuits install-dir resolution, circuits materialization/export, ordoctorcircuits checks: rerunD1,D2,D6,T1,T4, andA1. - Changes to circuits/r0vm provisioning, the LEZ/circuits pins, or the sequencer/wallet build invocation (
SEQUENCER_BUILD_ARGS,setup): re-verify "Provisioning the Real LEZ Sequencer Toolchain", then rerunT1andT4.
The T-series must be run against a real sequencer (see the Agent Execution Directives and the provisioning section). When in doubt, rerun more scenarios rather than fewer — and never substitute a stub for the real node in a T scenario.