High-level module boundaries for cli-schwab.
Keep the CLI thin and push reusable logic into focused modules.
The repo is easiest to reason about as a decision pipeline over household investment state:
- Portfolio rail — Schwab portfolio truth: balances, positions, transactions, account metadata, and portfolio-side auth.
- Market rail — Schwab market truth: quotes, VIX, indices, sectors, price history, options/IV, market hours, and market-side auth.
- Canonical state — normalized snapshot/context/history artifacts built from the two rails plus local manual-account/config inputs.
- Constraints and signals — policy/pacing/account rules plus interpretive market and macro signals such as regime, Polymarket, and heuristics.
- Recommendation and evaluation loop — structured recommendation episodes, operator feedback, later-state evaluation, and review/journal workflows.
The current schwab-advisor entrypoint is therefore best understood as the
recommendation engine, even though it remains operationally isolated behind a
separate CLI and SQLite store while experimental.
Business logic and analysis assembly.
Put here:
- portfolio aggregation
- market signal calculations
- snapshot merge helpers
- policy / scoring heuristics
- policy profile loading from public templates or ignored local files
- context assembly for agent-facing analysis
- morning-brief scorecard / analysis / rendering / orchestration helpers
- recommendation-engine prompt/model/scoring helpers
- model-subprocess environment filtering (
model_subprocess.py) so local model commands do not inherit Schwab, Resend, or recipient secrets - small shared payload/helper types such as
brief_types.py,context_models.py, andjson_types.py
Prefer to keep most modules here free of side effects.
context.py is the main exception today. The notable orchestration boundaries are
brief_service.py for the morning brief pipeline and advisor_sidecar.py for the
recommendation engine: both bridge snapshot/context capture, model execution, and
SQLite-backed persistence. The recommendation engine now reuses in-process
snapshot/context/history services rather than shelling back through the schwab CLI,
and recommendation generation itself now depends on PortfolioContext as its internal
canonical decision-context contract.
Avoid putting here:
- CLI printing
- ad hoc filesystem writes
- environment/config loading spread across many modules
- duplicated API orchestration when one focused assembler will do
Command-line interface.
Responsibilities:
- parse args in
parser.py - route commands in
router.py - lazy-load command handler modules through
commands/__init__.py - keep package entrypoint/compatibility exports in
__init__.py - call the appropriate service or wrapper
- format text output
- emit JSON envelopes
- map errors to CLI exit behavior
Authentication flows and local token persistence.
Responsibilities:
- keep browser/manual OAuth flows in the auth entrypoint modules
- keep Schwab/Authlib imports lazy so help/version output stays warning-free
- keep token paths, locking, metadata, and refresh writes in
auth_tokens.py - enforce owner-only modes for token files and SQLite sidecars via
secure_files.py
Separate opt-in CLI entrypoint for the recommendation engine.
Responsibilities:
- parse
schwab-advisorsubcommands - keep recommendation-journal workflows out of the main
schwabparser - emit the same agent-friendly JSON envelope style as the main CLI
Internal recommendation store (legacy _advisor/ path retained for now).
Responsibilities:
- separate SQLite schema for recommendation episodes
- schema migrations for experimental recommendation data
- persistence/query implementation for runs, feedback, evaluations, and notes
Internal client mixins and shared wrapper helpers.
Responsibilities:
- account and quote accessors
- order entry and order-management helpers
- shared transport protocols for the raw Schwab client (
_client/protocols.py) - public wrapper implementation details for
SchwabClientWrapper
Public entry point remains:
src/schwab_client/client.py
Internal history subsystem.
Responsibilities:
- SQLite schema
- document normalization
- import/backfill logic
- persistence/query implementation (
store.pyplus focused brief/snapshot-writer mixins) - DB-backed morning brief state (
brief_runs,brief_deliveries)
Public entry point remains:
src/schwab_client/history.py
Explicit runtime-only environment loading.
Responsibilities:
- load shell-exported secrets for CLI entrypoints when cron shells do not provide them
- keep environment mutation out of library-module import side effects
Centralized path and env-var resolution.
Responsibilities:
- history DB path
- report/export paths
- manual accounts path
- default import roots
Snapshot orchestration.
Responsibilities:
- collect current portfolio state
- merge manual accounts
- collect market context
- sanitize account identifiers
- produce the canonical snapshot document
The CLI should treat snapshot as the primary capture path; report is the
export-oriented wrapper that writes the same canonical snapshot JSON to disk.
AGENTS.md: agent workflow and operating rulesREADME.md: overview and quickstartCLAUDE.md: runtime/local operator notesdocs/history.md: canonical history/snapshot reference, including the DB-backed brief flowdocs/account-config.md: canonical account-config referencedocs/advisor-sidecar.md: recommendation-engine reference (the file path is retained while the terminology evolves)docs/_solutions.md: append-only solved-problems log
- Implement the handler in
src/schwab_client/cli/commands/ - Move reusable logic into
src/core/or a focused integration module if needed - Register the handler in the lazy command map, parser wiring in
cli/parser.py, and routing incli/router.py
- update canonical snapshot collection in
snapshot.pywhen the snapshot shape changes - update normalization in
_history/normalizer.pyif imports need to support it - update schema or views in
_history/schema.py - update persistence in
_history/store.py,_history/brief_store.py, or_history/snapshot_writers.py - update
docs/history.md
- Brief flow
- update deterministic helpers in
src/core/brief_*.py - keep end-to-end orchestration in
src/core/brief_service.py - update CLI wiring in
src/schwab_client/cli/commands/brief_cmd.py - update history-backed run state in
src/schwab_client/_history/ - document behavior changes in
docs/history.md
- update deterministic helpers in
- Recommendation engine
- treat
PortfolioContextas the internal decision-context contract unless a narrower model clearly earns its keep - update
src/core/advisor_models.py/advisor_prompts.py/advisor_scoring.pyas needed - keep orchestration in
src/core/advisor_sidecar.pyuntil a code rename is worth the churn - update recommendation persistence in
src/schwab_client/_advisor/ - document behavior changes in
docs/advisor-sidecar.md
- treat
- update
config/accounts.template.jsonif needed - update
config/secure_account_config.py - update
docs/account-config.md
- prefer
src/core/json_types.pyfor repo-wide JSON aliases/helpers - prefer
src/core/context_models.pyfor context-only helper models - prefer
src/core/brief_types.pyfor brief-pipeline shared types - prefer
src/schwab_client/_client/protocols.pyfor raw Schwab transport protocols
- update
config/policy.template.jsonfor the public generic template - keep real household/account aliases in
private/policy.jsonorSCHWAB_POLICY_PATH - update
src/core/policy.pyonly when the policy schema/engine changes