Skip to content

feat(cli): add 'gradata status' subcommand (GRA-1206 / GH #206)#208

Merged
Gradata merged 1 commit into
mainfrom
feat/gradata-status-subcommand
May 20, 2026
Merged

feat(cli): add 'gradata status' subcommand (GRA-1206 / GH #206)#208
Gradata merged 1 commit into
mainfrom
feat/gradata-status-subcommand

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 20, 2026

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:status slash command does, but in the SDK CLI so all hosts get it (claude-code, codex, gemini, hermes, opencode, cursor).

Output (live, on oliver-admin)

Brain: /home/olive/.gradata/brain
  Database: 1.33 MB  (3 markdown files)
  Rules graduated: 13
  Lessons: 35
  Corrections: 40
  Last correction: 2026-05-20T06:40:55.967039+00:00
  Sync queue: drained (26 synced)

Daemon:
  Status: up  (uptime 11h29m)
  Brain dir: /home/olive/.gradata/brain
  SDK version: 0.7.5

Cloud:
  Last sync: 2026-05-20T06:40:58.674815+00:00
  Corrections: 8895  (local: 40)
  Lessons: 226  (local: 35)

Convergence (last 7d):
  Sessions: 1  (40 corrections, avg 40.0/session)

Design notes

  • Plain text, no color codes, no Unicode boxes — terminal-renderable everywhere
  • Every external probe is best-effort with tight timeouts (2s daemon, 4s cloud); command never blocks
  • Brain dir mismatch warning fires when daemon serves a different brain than the one queried
  • Cloud-vs-local count comparison surfaces sync drift visually

Test plan

pytest tests/test_status_command.py
=> 5 passed in 0.56s

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 add forget, promote, prove.

Layering

cmd_status uses Brain.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 inside cmd_install) are NOT touched by this PR.

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.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Review Change Stack

📝 Walkthrough
  • New CLI subcommand: Added gradata status command providing a single human-readable health summary of the "brain" (replaces the plugin /gradata:status slash command)
  • Aggregated information: Displays Brain (path, DB size, files, rules, lessons, corrections, last correction timestamp, sync queue), Daemon (status, uptime, brain dir, SDK version), Cloud (last sync time, counts comparison), and Convergence (7-day correction trends)
  • Defensive implementation: Catches and gracefully handles SQLite errors and network/timeout errors; best-effort probes with tight timeouts (2s daemon, 4s cloud) prevent blocking
  • Plain text output: No color or Unicode characters; suitable for all host environments
  • New public API method: cmd_status(args) function added to CLI command dispatch; status registered as a new subcommand in the argument parser
  • Comprehensive test coverage: 5 new pytest tests covering fresh brain, corrections data, missing system.db tolerance, drained sync queue, and pending sync states (all passing)
  • Minor updates: Adjusted local imports in gradata install verify path; no changes to existing commands
  • Low risk: New subcommand only; pre-existing lint warnings untouched

Walkthrough

This PR adds a new gradata status command that outputs a plain-text health summary by aggregating brain stats, SQLite event counts, sync queue state, daemon health probes, cloud sync status, and convergence metrics over the last 7 days. The implementation is defensive against database and network failures. The command is registered in the CLI parser and dispatch table, and a comprehensive test suite validates behavior across edge cases.

Changes

Status Command Feature

Layer / File(s) Summary
Status command core implementation
Gradata/src/gradata/cli.py
cmd_status imports UTC for timestamp calculations and implements a best-effort aggregator that queries system.db for event/sync-queue data, probes the daemon health endpoint with timeouts and brain-dir validation, checks cloud sync via ~/.gradata/key and cloud API, computes 7-day convergence grouped by session, and catches SQLite and network errors to render partial results.
CLI parser and dispatch registration
Gradata/src/gradata/cli.py
status subcommand registered in argument parser and wired to cmd_status in the command dispatch table.
Install verification simplification
Gradata/src/gradata/cli.py
Minor refactor: adjusts local imports and simplifies the verification-marker presence check using a generator expression.
Status command test suite
Gradata/tests/test_status_command.py
Test module with helpers (_seed_minimal_brain, _run_status) to prepare test brains and run the command with mocked network endpoints; validates output across fresh brain, event count aggregation, missing database tolerance, and sync queue states (drained and pending).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a 'gradata status' subcommand to the CLI with a ticket reference.
Description check ✅ Passed The description is directly related to the changeset, providing clear context, design rationale, output examples, test results, and layering information about the new status command.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gradata-status-subcommand

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):
┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m
[00.16][ERROR]: Error: exception Glob.Lexer.Syntax_error("malformed glob pattern: missing ']'")
Raised at Glob__Lexer.syntax_error in file "libs/glob/Lexer.mll", line 8, characters 2-26
Called from Glob__Lexer.__ocaml_lex_token_rec in file "libs/glob/Lexer.mll", line 29, characters 26-53
Cal


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the feature label May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 92a4c81 and 57e51ca.

📒 Files selected for processing (2)
  • Gradata/src/gradata/cli.py
  • Gradata/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: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests
Add unit tests in tests/test_*.py for every CI push without LLM calls (deterministic); mark integration tests with @pytest.mark.integration and 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: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at 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 bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* 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 inside gradata/*
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!

Comment on lines +172 to +207
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.
+        pass

Apply 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

@Gradata Gradata merged commit aae9bba into main May 20, 2026
9 checks passed
@Gradata Gradata deleted the feat/gradata-status-subcommand branch May 20, 2026 06:53
Gradata added a commit that referenced this pull request May 20, 2026
…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>
Gradata added a commit that referenced this pull request May 20, 2026
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant