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-proposals would tell you, stitched into one panel. Use this as your first command after make 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/readiness against the substrate URL and performs a TCP-level reachability check against the Prism URL. Whenever systemctl show is 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_migrations and compares to the number of .sql files in scidex_substrate.migrations. Reports drift (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), edges rows, 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-*.md for 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.json and counts paths whose URL contains scidex.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

  1. Drop a handler function _handle_<area>_<verb>(args) into src/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.

  2. Register the subparser in build_parser() next to the related group (schema, economy, etc.). Use set_defaults(func=...) to wire the handler in.

  3. Add a row to the wiring table at the top of this doc.

  4. Add a test in tests/unit/cli/test_unified_cli.py that patches the wrapped function and asserts the CLI calls it. Follow the patterns of test_schema_status_invokes_reconcile_dry_run (sync wrapper) or test_economy_balance_invokes_token_ledger_balance (async wrapper with patched pool).

  5. If the verb opens a connection pool, make sure the handler’s _async() helper closes the pool in a finally: 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 legacy scidex-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(...). --help and unknown-command paths therefore never open a pool.

  • Imports are deferred. Subcommand handlers import their downstream modules lazily so that running scidex --help on a partial install still works.