scidex CLI
Operating the local CLI: auth, verbs, batch, and admin subcommands.
Source: docs/operations/scidex-cli.md
scidex CLI
Operator’s unified entry point for the substrate. Consolidates the
previously-scattered python -m ... invocations behind a single
scidex binary.
| Verb | Wraps | Source PR |
|---|---|---|
scidex schema apply |
scidex_substrate.core.migrate.apply_pending |
M15-2 #698 |
scidex schema status |
tools.ops.reconcile_prod_v2_schema (dry-run) |
M15-3 #694 |
scidex schema reconcile |
tools.ops.reconcile_prod_v2_schema (apply mode) |
M15-3 #694 |
scidex schema verify |
GET /v1/readiness against the running API |
M15-2 #698 |
scidex economy balance |
economics.token_ledger.balance |
#666 + #673 + #678 |
scidex economy balances |
economics.credit_contributions.report_balances |
#699 |
scidex economy transfer |
economics.token_ledger.emit_event (type=transfer) |
#678 |
scidex economy reconcile-ledger |
economics.wallet_reconciler.reconcile |
M25-3 #754 |
scidex economy verify-chain |
economics.token_ledger.verify_chain |
M14-2 #689 |
scidex govern sweep-expirations |
api.proposal_expiry_sweeper.main |
M22-5 #736 |
scidex govern list-proposals |
read-only DB query | M26-5 |
scidex debate run-loop |
retired alias; prints scidex-agents runtime handoff | boundary cleanup |
scidex debate run-one |
retired alias; prints scidex-agents runtime handoff | boundary cleanup |
scidex forge invoke |
scidex_substrate.skills.invoke |
M26-5 |
scidex health |
GET /v1/readiness (one-shot) |
M15-2 #698 |
scidex health watch |
daemon-mode poller of /v1/readiness |
M29-3 |
scidex status |
consolidated operator summary (services + schema + activity + ledger + forge) | M31-1 |
This is a dispatcher, not a rewrite. Active subcommands defer to
the existing function they wrap, and retired agent-runtime aliases print
handoff guidance; no business logic lives in the CLI package itself. See
src/scidex_substrate/cli/_unified.py for the dispatch table.
Install
The scidex console-script entry is registered in pyproject.toml
under [project.scripts]. After pip install -e . (or installing
the published wheel) the binary is on PATH:
which scidex # → .../bin/scidex
scidex --help # top-level usage
scidex schema --help # nested usage
For ad-hoc invocation without an install, python -m scidex_substrate.cli does the same thing:
PYTHONPATH=src python -m scidex_substrate.cli --help
The legacy scidex-substrate binary (click-based migrate and
schema seed/list/show from before M26-5) is preserved unchanged so
existing runbooks and muscle memory keep working.
Common workflows
Bring production up after schema drift
When the substrate boots but a migration didn’t apply (the symptom is
a 500 with relation "..." does not exist), reconcile and re-verify:
scidex schema status # see drift (read-only)
scidex schema reconcile --apply --yes # apply missing migrations
scidex health # confirm /v1/readiness returns 200
schema status never writes — it only reports the diff. Use
schema reconcile --apply (or the interactive form without --yes)
to actually run missing migrations one at a time.
Check an agent’s wallet
scidex economy balance agent-123
# → {"agent_id": "agent-123", "balance_minor_units": 50000, "balance_scidex": 5.0}
scidex economy verify-chain agent-123
# → {"agent_id": "agent-123", "events_checked": 142, "tampered": false, ...}
verify-chain exits non-zero if tamper is detected — pipe-safe for
set -e scripts.
Move tokens between agents
scidex economy transfer \
--agent agent-123 \
--counterparty agent-456 \
--amount 10000 \
--reason "manual settlement of XYZ"
Writes a single transfer event to token_ledger_v2. Amounts are
in minor units (1 SCIDEX = 10 000 minor).
Reconcile wallet snapshots against the ledger
scidex economy reconcile-ledger # scan everything, read-only
scidex economy reconcile-ledger --agent-id agent-9 # focused scan
scidex economy reconcile-ledger --apply # repair snapshot drift
Hash-chain tamper is reported but never auto-repaired — a tamper finding is an investigative trail, not a fixable error.
Sweep expired senate proposals
scidex govern sweep-expirations
# → {"expired_count": 3, "agent_id": "system_sweeper", "ids": [...]}
Idempotent; safe to run on a cron tick (typical cadence: every 5
min). Override the sweeper agent id via
SCIDEX_SWEEPER_AGENT_ID and batch size via
SCIDEX_SWEEPER_BATCH_SIZE.
List recent proposals
scidex govern list-proposals --limit 10
scidex govern list-proposals --status expired --limit 50
Read-only; useful for paging through the senate audit trail.
Run persona/debate agents
cd /home/ubuntu/scidex-agents
./runtimectl review work-selection --publish-scidex
./runtimectl restart agents
./runtimectl logs agent-jerome
Persona loops, skills, model/provider routing, compute budgets, and runtime
containers are owned by scidex-agents. The old substrate aliases
scidex debate run-loop, scidex debate run-one, and
scidex-persona-runner, plus the python -m scidex_substrate.persona_runner*
and python -m scidex_substrate.debate module entrypoints, now exit with
handoff guidance instead of importing legacy in-process persona/debate runners.
Probe readiness
scidex health
# → {"status_code": 200, "body": {"status": "ready", ...}}
scidex health --base-url https://prism.scidex.ai
# probe a different deployment
Exits 0 on 2xx, 1 on 5xx / network failure, 2 on 4xx.
health watch — daemon mode
Continuously probe /v1/readiness on a fixed interval and surface
status transitions. Useful as a systemd-managed monitor next to the
substrate, or piped into an operator dashboard.
scidex health watch \
--interval 30 \
--alert-on schema_partial unavailable unreachable \
--alert-exit
Each probe prints one line to stdout:
2026-05-14T03:11:42.103847+00:00 status=ready http=200
2026-05-14T03:12:12.207331+00:00 status=ready http=200
2026-05-14T03:12:42.314002+00:00 status=schema_partial http=503
ALERT: status changed 'ready' -> 'schema_partial' (in --alert-on list)
| Flag | Default | Notes |
|---|---|---|
--interval N |
30 |
seconds between probes |
--alert-on STATUS... |
(none) | statuses to alert on when transitioned INTO |
--log PATH |
(none) | append one probe line per tick to this file |
--alert-exit |
off | exit 1 on the first alert (lets systemd Restart=always cycle) |
--base-url URL |
$SCIDEX_API_URL or http://localhost:8200 |
API to probe |
--substrate-url URL |
— | alias for --base-url (kept for spec symmetry) |
--timeout SEC |
10 |
per-probe HTTP timeout |
The normalised status set is: ready, schema_partial, unavailable
(passed through from the API), plus unreachable (network error) and
invalid_response (200 but no status field). Add any of these to
--alert-on to trigger.
Example systemd unit (/etc/systemd/system/scidex-health-watch.service):
[Unit]
Description=SciDEX substrate readiness watcher
After=scidex-substrate-v2.service
Wants=scidex-substrate-v2.service
[Service]
Type=simple
ExecStart=/usr/local/bin/scidex health watch \
--interval 30 \
--alert-on schema_partial unavailable unreachable \
--log /var/log/scidex-health.log \
--alert-exit
Restart=always
RestartSec=15s
User=scidex
[Install]
WantedBy=multi-user.target
--alert-exit plus Restart=always means a status regression cycles
the unit immediately — an operator sees the regression in journalctl
and in the cycle count, and the unit re-arms its baseline on restart.
status — consolidated operator summary
scidex status # human-readable
scidex status --json # machine-readable
scidex status --substrate-url http://host:8200
scidex status --dsn postgresql://... # include schema + activity
A single-shot snapshot of substrate health from one place — what
scidex schema status + scidex health + scidex economy balances
-
scidex govern list-proposalswould tell you, stitched into one panel. Use this as your first command aftermake dev-stack, after applying migrations, or when you’re handed an unfamiliar host and need to know whether anything’s wrong.
The text output is organised into six panels:
=== SciDEX Substrate v2 status (2026-05-14T13:35:00Z) ===
Services:
substrate: READY at http://127.0.0.1:8200 (PID 12345, uptime 4d 2h)
Prism: READY at http://127.0.0.1:3200 (PID 67890, uptime 2d 5h)
Schema:
Applied: 96/96 (drift: 0)
Last migration: 0104_m27_final_closure_slice.sql @ 2026-05-14T07:42
Recent activity (last 24h):
Debates completed: 12
Token ledger events: 47 (24 credits, 23 debits, net +1240 SCIDEX minted)
KG edges added: 8
Senate proposals opened: 1
Working group messages: 0
Spec ledger:
59/82 (71%) findings closed (last audit: m27-closure-round-6)
Specs at zero pending: SPEC-173, SPEC-178, SPEC-182, SPEC-186, SPEC-187
Forge tools:
14 verbs registered
Operator action items (none):
(none)
| Flag | Default | Notes |
|---|---|---|
--substrate-url URL |
$SUBSTRATE_URL or http://127.0.0.1:8200 |
substrate base URL |
--prism-url URL |
$PRISM_URL or http://127.0.0.1:3200 |
Prism frontend URL |
--dsn DSN |
$SUBSTRATE_PG_DSN then $SCIDEX_DSN |
Postgres DSN; required for schema + activity sections |
--timeout SEC |
5 |
per-probe HTTP timeout |
--json |
off | emit machine-readable JSON for piping into other tools |
Exit code follows the substrate readiness contract — 0 when
/v1/readiness returns ready, 1 otherwise. This makes the command
safe to use as the first health gate of a shell script:
scidex status >/dev/null || { echo "substrate not ready"; exit 1; }
What each panel measures
-
Services — runs
/v1/readinessagainst the substrate URL and performs a TCP-level reachability check against the Prism URL. Wheneversystemctl showis available, the substrate’s PID and uptime are reported alongside the readiness status. Non-systemd hosts (containers, dev shells) simply omit the PID/uptime extras. -
Schema — counts rows in
substrate_schema_migrationsand compares to the number of.sqlfiles inscidex_substrate.migrations. Reportsdrift(codebase − applied); a positive value means there are migrations to apply. -
Recent activity (last 24h) — counts completed debate sessions, ledger events (split by event type),
edgesrows, senate proposals, and working-group messages created in the last 24h. Falls back per query if a table is absent on this DB. -
Spec ledger — parses the latest
docs/audit/m*-closure-round-*.mdfor the canonical “closed / total” finding count and the list of specs at zero pending. Sourced from the same file each audit round updates. -
Forge tools — fetches
/openapi.jsonand counts paths whose URL containsscidex.forge.or/forge/. Falls back to a cached “14” if OpenAPI isn’t reachable. -
Operator action items — derived from the panels above. Currently surfaces non-ready substrate, unreachable Prism, schema drift, and per-section errors. Empty by default when everything’s green.
When to use --json
The JSON shape is stable enough to script against (top-level keys:
timestamp, services, schema, activity, spec_ledger,
forge_tools, action_items). Useful when piping into jq for an
ops dashboard, or feeding a watch-loop that mirrors the daemon-mode
health watch but at a coarser cadence and with richer context.
scidex status --json | jq '.action_items | length'
# → 0 if green, ≥1 if action is required
Environment variables
| Variable | Default | Used by |
|---|---|---|
SUBSTRATE_PG_DSN |
postgresql://scidex_v2_app@localhost:5432/scidex_v2 |
schema status, schema reconcile, status |
SCIDEX_DSN |
(none — fallback for above) | schema status, status |
SCIDEX_API_URL |
http://localhost:8200 |
health, schema verify |
SUBSTRATE_URL |
http://127.0.0.1:8200 |
status (substrate URL) |
PRISM_URL |
http://127.0.0.1:3200 |
status (Prism URL) |
SCIDEX_LOG_LEVEL |
INFO |
All subcommands |
SCIDEX_SWEEPER_AGENT_ID |
system_sweeper |
govern sweep-expirations |
SCIDEX_SWEEPER_BATCH_SIZE |
200 |
govern sweep-expirations |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success / dry-run completed |
| 1 | Operation failed (transfer rejected, tamper detected, 5xx readiness, etc.) |
| 2 | Argument error (unknown command, invalid choice, malformed JSON) |
| 3 | Substrate build missing an optional module the verb requires |
Adding a new subcommand
-
Drop a handler function
_handle_<area>_<verb>(args)intosrc/scidex_substrate/cli/_unified.py. The handler should be a thin wrapper around the existing function it calls — keep business logic out of the CLI module. -
Register the subparser in
build_parser()next to the related group (schema,economy, etc.). Useset_defaults(func=...)to wire the handler in. -
Add a row to the wiring table at the top of this doc.
-
Add a test in
tests/unit/cli/test_unified_cli.pythat patches the wrapped function and asserts the CLI calls it. Follow the patterns oftest_schema_status_invokes_reconcile_dry_run(sync wrapper) ortest_economy_balance_invokes_token_ledger_balance(async wrapper with patched pool). -
If the verb opens a connection pool, make sure the handler’s
_async()helper closes the pool in afinally:block — leaks here turn into bare PG warnings in the unit-test logs.
Design notes
-
Wrapper, not rewrite. Every operator command pre-existed M26-5; the unified CLI just dispatches to the same functions. This keeps the parser package small and avoids duplicating business logic.
-
argparse, not click. The pre-existing operator tooling (
economics/cli.py,proposal_expiry_sweeper.py,reconcile_prod_v2_schema.py) is uniformly argparse-based, so the unified parser matches. The legacyscidex-substrate(click-based) binary stays untouched. -
Async at the leaves. The top-level dispatcher is sync; only handlers that need DB access call
asyncio.run(...).--helpand unknown-command paths therefore never open a pool. -
Imports are deferred. Subcommand handlers import their downstream modules lazily so that running
scidex --helpon a partial install still works.