feat(cli): add 'gradata status' subcommand (GRA-1206 / GH #206)#208
Conversation
Single human-readable summary of brain health: stats + daemon probe + cloud sync state + 7d convergence trend. Designed for the daily 'what's going on with my brain' check. Output is plain text, terminal-renderable, <30 lines for a typical brain. Every external probe (daemon at 127.0.0.1:8765, cloud at api.gradata.ai) is best-effort with a tight timeout — the command never blocks even if the network is dead or the daemon is offline. Sections: - Brain: rules graduated / lessons / corrections / last correction ts - Sync queue: pending vs drained - Daemon: status + uptime + brain dir (with warning on brain_dir mismatch) - Cloud: last_sync + cloud-vs-local count comparison + stale warning - Convergence: sessions and avg corrections/session in the last 7 days Wires the existing slash-command UX from gradata-plugin into the SDK CLI so plugin users get the same functionality via 'gradata status' instead of '/gradata:status'. Part of the kill-the-plugin epic (GRA-1198 / GH #206). Test plan: pytest tests/test_status_command.py => 5 passed in 0.56s Tests cover: - Fresh brain (no events, no daemon, no cloud key) doesn't crash - Brain with seeded CORRECTION events shows correct counts - Missing system.db tolerated gracefully - sync_queue drained vs pending reporting Live verification on oliver-admin brain: 'gradata status' renders the full block with real numbers (40 corrections, 35 lessons, 13 rules graduated, cloud at 8895 corrections, daemon up 11h). Layering: cmd_status uses Brain.stats() (Layer 2 public API) and direct SQLite reads (Layer 0 primitives). No Layer 0 -> 2 imports.
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
📝 Walkthrough
WalkthroughThis PR adds a new ChangesStatus Command Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.21.0)OpenGrep fatal error (exit code 2): �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Gradata/src/gradata/cli.py`:
- Line 239: Remove the redundant local import "from pathlib import Path as
_Path" in gradata/cli.py and use the already-imported module-level symbol Path
instead (or drop the alias import entirely); update any local references to
_Path to use Path so no shadowing occurs and the duplicate import is eliminated.
- Around line 172-207: The three places that call _sqlite3.connect(db_path) and
then con.close() (the blocks that compute
rules_total/lessons_total/corr_total/last_correction_ts, the sync_queue block
that computes pending/total_q, and the convergence block) can leak connections
if an unexpected exception occurs; replace these with context managers using
"with _sqlite3.connect(db_path) as con:" (or consolidate into a single
with-block when safe) so cursors and connections are closed automatically,
remove explicit con.close() calls, and ensure you still handle
_sqlite3.OperationalError/OSError around the with if you need to swallow
schema-missing errors while preserving the variables rules_total, lessons_total,
corr_total, last_correction_ts, pending, total_q, etc.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 449101a9-ad6f-4578-961f-9da561083160
📒 Files selected for processing (2)
Gradata/src/gradata/cli.pyGradata/tests/test_status_command.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: pytest windows-latest / py3.12
- GitHub Check: pytest windows-latest / py3.11
- GitHub Check: pytest macos-latest / py3.12
- GitHub Check: pytest macos-latest / py3.11
- GitHub Check: pytest ubuntu-latest / py3.12
- GitHub Check: pytest ubuntu-latest / py3.11
- GitHub Check: pytest (py3.11)
- GitHub Check: pytest (py3.12)
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/tests/**/*.py
📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Gradata/tests/**/*.py: SetBRAIN_DIRenvironment variable viatmp_pathin conftest.py for test isolation — ensure_paths.pymodule cache refreshes when callingBrain.init()directly inside tests
Add unit tests intests/test_*.pyfor every CI push without LLM calls (deterministic); mark integration tests with@pytest.mark.integrationand skip them by default (they hit real LLM APIs)
Files:
Gradata/tests/test_status_command.py
Gradata/src/**/*.py
📄 CodeRabbit inference engine (Gradata/AGENTS.md)
Gradata/src/**/*.py: Prefersentence-transformersfor local embeddings,google-genaifor Gemini embeddings,cryptographyfor AES-GCM encrypted system.db,bm25sfor BM25 rule ranking, andmem0aifor external memory adapters — guard all optional dependency imports withtry / except ImportErrorat the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bareexcept: pass— use typed exceptions or at minimumlogger.warning(...)withexc_info=Trueto avoid silent failure in a memory product
Never import from out-of-scope sibling directories../Sprites/or../Hausgem/withingradata/*code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to../Sprites/,../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from insidegradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes
Files:
Gradata/src/gradata/cli.py
🔇 Additional comments (9)
Gradata/src/gradata/cli.py (4)
27-27: LGTM!
501-502: LGTM!Also applies to: 514-518
1605-1607: LGTM!
1917-1917: LGTM!Gradata/tests/test_status_command.py (5)
1-24: LGTM!
26-59: LGTM!
62-93: LGTM!
96-167: LGTM!
170-209: LGTM!
| try: | ||
| con = _sqlite3.connect(db_path) | ||
| cur = con.cursor() | ||
| rules_total = cur.execute( | ||
| "SELECT COUNT(*) FROM events WHERE type='RULE_GRADUATED'" | ||
| ).fetchone()[0] | ||
| lessons_total = cur.execute( | ||
| "SELECT COUNT(*) FROM events WHERE type IN ('LESSON_ADDED','LESSON_CHANGE')" | ||
| ).fetchone()[0] | ||
| corr_total = cur.execute("SELECT COUNT(*) FROM events WHERE type='CORRECTION'").fetchone()[ | ||
| 0 | ||
| ] | ||
| row = cur.execute("SELECT MAX(ts) FROM events WHERE type='CORRECTION'").fetchone() | ||
| last_correction_ts = row[0] if row else None | ||
| con.close() | ||
| except (_sqlite3.OperationalError, OSError): | ||
| # Fresh brain or schema drift — show zeros, don't crash. | ||
| pass | ||
|
|
||
| print(f" Rules graduated: {rules_total}") | ||
| print(f" Lessons: {lessons_total}") | ||
| print(f" Corrections: {corr_total}") | ||
| if last_correction_ts: | ||
| print(f" Last correction: {last_correction_ts}") | ||
|
|
||
| # Sync queue state | ||
| pending = total_q = 0 | ||
| try: | ||
| con = _sqlite3.connect(db_path) | ||
| pending = con.execute("SELECT COUNT(*) FROM sync_queue WHERE synced_at IS NULL").fetchone()[ | ||
| 0 | ||
| ] | ||
| total_q = con.execute("SELECT COUNT(*) FROM sync_queue").fetchone()[0] | ||
| con.close() | ||
| except _sqlite3.OperationalError: | ||
| pass |
There was a problem hiding this comment.
Use context managers for SQLite connections to prevent resource leaks.
The three SQLite connection blocks (lines 172-186, 199-205, 278-292) call con.close() inside the try block. If an unexpected exception (not OperationalError or OSError) occurs after connect() but before close(), the connection leaks.
Consider consolidating into a single connection with proper context management:
🛡️ Proposed refactor using context manager
- try:
- con = _sqlite3.connect(db_path)
- cur = con.cursor()
- rules_total = cur.execute(
- "SELECT COUNT(*) FROM events WHERE type='RULE_GRADUATED'"
- ).fetchone()[0]
- lessons_total = cur.execute(
- "SELECT COUNT(*) FROM events WHERE type IN ('LESSON_ADDED','LESSON_CHANGE')"
- ).fetchone()[0]
- corr_total = cur.execute("SELECT COUNT(*) FROM events WHERE type='CORRECTION'").fetchone()[
- 0
- ]
- row = cur.execute("SELECT MAX(ts) FROM events WHERE type='CORRECTION'").fetchone()
- last_correction_ts = row[0] if row else None
- con.close()
- except (_sqlite3.OperationalError, OSError):
- # Fresh brain or schema drift — show zeros, don't crash.
- pass
+ try:
+ with _sqlite3.connect(db_path) as con:
+ cur = con.cursor()
+ rules_total = cur.execute(
+ "SELECT COUNT(*) FROM events WHERE type='RULE_GRADUATED'"
+ ).fetchone()[0]
+ lessons_total = cur.execute(
+ "SELECT COUNT(*) FROM events WHERE type IN ('LESSON_ADDED','LESSON_CHANGE')"
+ ).fetchone()[0]
+ corr_total = cur.execute(
+ "SELECT COUNT(*) FROM events WHERE type='CORRECTION'"
+ ).fetchone()[0]
+ row = cur.execute("SELECT MAX(ts) FROM events WHERE type='CORRECTION'").fetchone()
+ last_correction_ts = row[0] if row else None
+ except (_sqlite3.OperationalError, OSError):
+ # Fresh brain or schema drift — show zeros, don't crash.
+ passApply the same pattern to the sync_queue block (lines 199-207) and the convergence block (lines 277-292).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Gradata/src/gradata/cli.py` around lines 172 - 207, The three places that
call _sqlite3.connect(db_path) and then con.close() (the blocks that compute
rules_total/lessons_total/corr_total/last_correction_ts, the sync_queue block
that computes pending/total_q, and the convergence block) can leak connections
if an unexpected exception occurs; replace these with context managers using
"with _sqlite3.connect(db_path) as con:" (or consolidate into a single
with-block when safe) so cursors and connections are closed automatically,
remove explicit con.close() calls, and ensure you still handle
_sqlite3.OperationalError/OSError around the with if you need to swallow
schema-missing errors while preserving the variables rules_total, lessons_total,
corr_total, last_correction_ts, pending, total_q, etc.
| print() | ||
| print("Cloud:") | ||
| try: | ||
| from pathlib import Path as _Path |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Remove redundant Path import.
Path is already imported at module level (line 28). This local import shadows it unnecessarily.
♻️ Proposed fix
- from pathlib import Path as _Path
-
- key_path = _Path.home() / ".gradata" / "key"
+ key_path = Path.home() / ".gradata" / "key"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Gradata/src/gradata/cli.py` at line 239, Remove the redundant local import
"from pathlib import Path as _Path" in gradata/cli.py and use the
already-imported module-level symbol Path instead (or drop the alias import
entirely); update any local references to _Path to use Path so no shadowing
occurs and the duplicate import is eliminated.
…RA-1198) (#211) Replaces the two-path 'Claude Code via /plugin marketplace OR Python SDK via pipx' branching with a single canonical install: pipx + gradata install --agent <host>. Why: the plugin marketplace path was a duplicate surface that did the same thing the SDK install command already does (apply hooks + slash commands to the host config). Two paths created onboarding friction ('which one am I supposed to use?') for zero functional gain. Council voted Option A 'kill the plugin' on 2026-05-01. Also surfaces the 6 first-class subcommands the SDK now ships: status, correct, forget, prove, recall, doctor. Three of those were shipped earlier today (PR #208 status, #209 forget, #210 prove) and replaced the equivalent plugin slash commands. Removed: - '/plugin marketplace add Gradata/gradata' + '/plugin install gradata' - The 'pick one' framing - .claude-plugin/ from the repo layout (manifest stays in tree until PR retiring the directory ships separately — keeps the layout description accurate as of THIS commit) Parent: GRA-1198 (kill the plugin epic) GH: Gradata/gradata #206 Co-authored-by: data-engineer <data-engineer@gradata.ai>
…-1198) (#214) The .claude-plugin/ directory itself was already removed in a prior cleanup (see CHANGELOG: 'Remove orphaned gradata-plugin/ subdir (#54)'). What remained were stale string references in docs and examples now that the SDK ships all subcommands directly (PRs #208/#209/#210/#211 + #213). Changes: - .dockerignore: removed dead .claude-plugin exclude line - examples/with_claude_code.py: replaced '/plugin install gradata' language with the canonical 'gradata install --agent claude-code' - examples/README.md: fix broken link to .claude-plugin/README.md - CHANGELOG.md: BREAKING entry under Unreleased documenting the retirement This closes out the kill-the-plugin epic (GRA-1198 / GH #206) from the references side. Anyone who installed via /plugin marketplace before 2026-05-20 must migrate to the SDK install path. Verified: - pip install /home/olive/work/gradata-sdk/Gradata in a fresh venv succeeds - gradata install --agent claude-code --brain /tmp/test-brain --help works - pytest tests/ -x -q passes (816 tests, 7 skipped, 1 known-skip on test_byo_key_provider for missing httpx in dev env unrelated to this) - ruff check clean on touched files - grep for 'claude-plugin|gradata-plugin' on src/ + docs/ shows only the intentional CHANGELOG entries (current BREAKING note + historical refs) Branch authored by delegate_task subagent (hit max_iterations on PR-open); parent agent verified + extracted clean diff + opened PR. Co-authored-by: data-engineer <data-engineer@gradata.ai>
Summary
Adds
gradata status— single human-readable summary of brain health: stats + daemon probe + cloud sync state + 7d convergence trend.Replaces what the plugin's
/gradata:statusslash command does, but in the SDK CLI so all hosts get it (claude-code, codex, gemini, hermes, opencode, cursor).Output (live, on oliver-admin)
Design notes
Test plan
5 cases: fresh brain, brain with corrections, missing system.db tolerance, sync_queue drained, sync_queue pending.
Epic context
GRA-1198 (kill the plugin epic) / GH #206 — fold plugin slash commands into SDK CLI subcommands. This PR ships the first of 4 (
status); follow-up PRs will addforget,promote,prove.Layering
cmd_statususesBrain.stats()(Layer 2 public API) + direct SQLite reads (Layer 0 primitives). No Layer 0 → 2 imports.Risk
Low. New subcommand, no changes to existing commands. Pre-existing lint warnings in
cli.py(F841/I001 insidecmd_install) are NOT touched by this PR.