Skip to content

feat: measure agent install success#274

Open
Gradata wants to merge 1 commit into
mainfrom
GRA-2507-install-success
Open

feat: measure agent install success#274
Gradata wants to merge 1 commit into
mainfrom
GRA-2507-install-success

Conversation

@Gradata

@Gradata Gradata commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add local JSONL install-success measurements for gradata install --agent ....
  • Measures Claude Code, Codex, Hermes, and Cursor with per-agent status, action, and failure_kind.
  • Classifies adapter write/parse failures as code_failure and missing --agent all host configs as docs_friction.
  • Documents the measurement file path and fields.

Paperclip: GRA-2507 / 4f4443e3-f6a4-4d47-99c7-f9e15e343cfe

Verification

  • PYTHONPATH=src env -u BRAIN_DIR -u GRADATA_BRAIN pytest -q tests/test_cli_install_agent.py tests/test_install_smoke_matrix.py tests/test_telemetry.py → 52 passed, 2 skipped
  • /home/olive/.local/bin/uvx ruff check src/gradata/_install_measurement.py src/gradata/cli.py tests/test_cli_install_agent.py → All checks passed
  • Smoke: fake-HOME gradata install --agent all wrote JSONL rows for Codex success plus Claude Code/Hermes/Cursor docs_friction.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

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

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough
  • New install measurement system: Records local JSONL measurements for gradata install --agent ... in <gradata-config-dir>/install_measurements.jsonl with timestamp, agent, status, action, and failure classification.
  • Failure classification: Categorizes failures as code_failure (adapter write/parse errors) or docs_friction (missing config for --agent all host detection); success marked as none.
  • Supported agents: Measurement tracking for Claude Code, Codex, Hermes, and Cursor.
  • New public API (in _install_measurement.py):
    • measurement_path() -> Path: Returns per-user measurement file path
    • classify_result(result: InstallResult) -> FailureKind: Determines failure classification
    • append_measurement(result: InstallResult, *, brain_dir: Path, source: str, failure_kind: FailureKind | None): Records install attempt
    • append_docs_friction(agent: str, *, config_path: Path, brain_dir: Path, message: str, source: str): Logs documentation friction
    • Constants: MEASURED_INSTALL_AGENTS, INSTALL_MEASUREMENTS_FILE, FailureKind type alias
  • CLI integration: cmd_install_agent now automatically records measurements for both successful and failed installs, including docs friction for missing agent configs in --agent all mode.
  • Documentation updates: README and installation guide now document the measurement file location and field meanings.
  • Test coverage: Three new tests validate success recording, code failure classification, and docs friction detection for missing CLI configs.
  • No breaking changes: All changes are additive; existing behavior preserved.

Walkthrough

This PR adds agent install attempt measurement recording to Gradata. A new module records per-agent install status, actions, and failure classifications to a JSONL file in the config directory. The CLI's install command now emits measurements for both successful and failed installs, and detects missing agent configurations to record docs-friction failures. Comprehensive tests cover success, code failure, and multi-agent scenarios, with documentation updates explaining the measurement fields and failure kinds.

Changes

Install measurement recording for agent integrations

Layer / File(s) Summary
Measurement recording module
Gradata/src/gradata/_install_measurement.py
New module defines constants MEASURED_INSTALL_AGENTS and INSTALL_MEASUREMENTS_FILE, type alias FailureKind ("none" | "code_failure" | "docs_friction"), and core functions: measurement_path() computes the JSONL destination, classify_result() maps InstallResult to FailureKind, append_measurement() persists structured measurements with timestamp/source/agent/status/action/failure_kind/paths/message, and append_docs_friction() logs docs-friction failures.
CLI install command measurement integration
Gradata/src/gradata/cli.py
cmd_install_agent expands agent selection logic to detect missing measured agent configs and record docs-friction for each missing agent when using --agent all. During per-agent install, exceptions trigger measurement recording with status "failed", and successful installs call append_measurement() with resolved brain_dir.
Measurement test coverage
Gradata/tests/test_cli_install_agent.py
Adds _read_install_measurements() helper to load JSONL records and three tests: success case records status: success, failure_kind: none for codex; corrupt config case records status: failure, failure_kind: code_failure for claude-code; and --agent all case records success for codex and docs_friction for missing claude-code, hermes, and cursor configs.
User documentation
Gradata/README.md, Gradata/docs/getting-started/install.md
README and install guide document that gradata install --agent records per-attempt JSONL measurements with fields agent, status, action, failure_kind, and explain the three failure_kind values: none (success or already present), code_failure (adapter config write/parse failures), docs_friction (missing tool config on --agent all).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Gradata/gradata#228: Modifies the same cmd_install_agent install/verification flow in Gradata/src/gradata/cli.py for agent installation handling.
  • Gradata/gradata#215: Records per-agent install metadata on disk via a separate manifest file, intersecting the same install instrumentation code paths.
  • Gradata/gradata#242: Modifies agent install brain directory resolution in Gradata/src/gradata/cli.py that this PR depends on for measurement recording.

Suggested labels

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% 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 'feat: measure agent install success' accurately summarizes the main change: adding measurement functionality for agent installation outcomes.
Description check ✅ Passed The description directly relates to the changeset, detailing the measurement system for agent installs, failure classification, documentation updates, and verification results.
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 GRA-2507-install-success

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.22.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.20][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 Jun 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/_install_measurement.py`:
- Around line 59-60: The append currently using path.open("a") can leave a
partial JSONL line; change it to use the project atomic-write helper (or
implement the atomic-write pattern) so each measurement is written atomically:
create a temp file in the same directory, write the original file contents plus
the new json.dumps(payload, sort_keys=True) + "\n" into the temp, fsync the
temp, close it and then os.replace the temp into place (or call the atomic
helper) instead of using path.open("a"); update the code locations referencing
path.open and payload to use this atomic-write flow.
🪄 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: 4a526318-32da-4905-917d-0cc8a7b3e591

📥 Commits

Reviewing files that changed from the base of the PR and between 2b12800 and f8b0f3f.

📒 Files selected for processing (5)
  • Gradata/README.md
  • Gradata/docs/getting-started/install.md
  • Gradata/src/gradata/_install_measurement.py
  • Gradata/src/gradata/cli.py
  • Gradata/tests/test_cli_install_agent.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 ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest macos-latest / py3.11
  • 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/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
  • Gradata/src/gradata/_install_measurement.py
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_cli_install_agent.py
🧠 Learnings (1)
📚 Learning: 2026-04-17T17:18:07.439Z
Learnt from: Gradata
Repo: Gradata/gradata PR: 0
File: :0-0
Timestamp: 2026-04-17T17:18:07.439Z
Learning: In PR `#102` (gradata/gradata), Round 2 addressed: cli.py env-first brain resolution (GRADATA_BRAIN > --brain-dir > cwd), _tenant.py corrupt .tenant_id overwrite, _env_int default clamping to minimum, and _events.py tenant-scoped fallback SELECT for dedup. All ruff and 99 tests green after these fixes.

Applied to files:

  • Gradata/README.md
🔇 Additional comments (9)
Gradata/src/gradata/cli.py (3)

458-463: LGTM!


467-479: LGTM!


490-507: LGTM!

Gradata/tests/test_cli_install_agent.py (4)

222-227: LGTM!


230-241: LGTM!


244-257: LGTM!


260-276: LGTM!

Gradata/README.md (1)

121-122: LGTM!

Gradata/docs/getting-started/install.md (1)

107-117: LGTM!

Comment on lines +59 to +60
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, sort_keys=True) + "\n")

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 | 🟠 Major | ⚡ Quick win

Use atomic-write helper for JSONL append to prevent partial-write corruption.

The coding guidelines require: "Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes." While this is append-only JSONL, a crash during the write could leave a partial line that breaks downstream parsing. Consider using an atomic-write pattern (write to temp file, then rename) to ensure each measurement is fully written or not at all.

🛡️ Suggested atomic-write pattern
+import tempfile
+import shutil
+
 def append_measurement(
     result: InstallResult,
     *,
     brain_dir: Path,
     source: str = "gradata install --agent",
     failure_kind: FailureKind | None = None,
 ) -> dict[str, object]:
     status = "success" if result.action != "failed" else "failure"
     payload: dict[str, object] = {
         "ts": datetime.now(UTC).isoformat(),
         "source": source,
         "agent": result.agent,
         "status": status,
         "action": result.action,
         "failure_kind": failure_kind or classify_result(result),
         "config_path": str(result.config_path),
         "brain_dir": str(brain_dir),
         "message": result.message,
     }
     path = measurement_path()
     path.parent.mkdir(parents=True, exist_ok=True)
-    with path.open("a", encoding="utf-8") as f:
-        f.write(json.dumps(payload, sort_keys=True) + "\n")
+    # Atomic append: write to temp, then move
+    tmp_fd, tmp_path = tempfile.mkstemp(dir=path.parent, text=True)
+    try:
+        with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
+            # Read existing content if file exists
+            if path.exists():
+                f.write(path.read_text(encoding="utf-8"))
+            f.write(json.dumps(payload, sort_keys=True) + "\n")
+        shutil.move(tmp_path, path)
+    except Exception:
+        Path(tmp_path).unlink(missing_ok=True)
+        raise
     return payload

Alternatively, use a project-standard atomic-write helper if one exists in the codebase.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, sort_keys=True) + "\n")
import tempfile
import shutil
import os
def append_measurement(
result: InstallResult,
*,
brain_dir: Path,
source: str = "gradata install --agent",
failure_kind: FailureKind | None = None,
) -> dict[str, object]:
status = "success" if result.action != "failed" else "failure"
payload: dict[str, object] = {
"ts": datetime.now(UTC).isoformat(),
"source": source,
"agent": result.agent,
"status": status,
"action": result.action,
"failure_kind": failure_kind or classify_result(result),
"config_path": str(result.config_path),
"brain_dir": str(brain_dir),
"message": result.message,
}
path = measurement_path()
path.parent.mkdir(parents=True, exist_ok=True)
# Atomic append: write to temp, then move
tmp_fd, tmp_path = tempfile.mkstemp(dir=path.parent, text=True)
try:
with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
# Read existing content if file exists
if path.exists():
f.write(path.read_text(encoding="utf-8"))
f.write(json.dumps(payload, sort_keys=True) + "\n")
shutil.move(tmp_path, path)
except Exception:
Path(tmp_path).unlink(missing_ok=True)
raise
return payload
🤖 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/_install_measurement.py` around lines 59 - 60, The append
currently using path.open("a") can leave a partial JSONL line; change it to use
the project atomic-write helper (or implement the atomic-write pattern) so each
measurement is written atomically: create a temp file in the same directory,
write the original file contents plus the new json.dumps(payload,
sort_keys=True) + "\n" into the temp, fsync the temp, close it and then
os.replace the temp into place (or call the atomic helper) instead of using
path.open("a"); update the code locations referencing path.open and payload to
use this atomic-write flow.

Source: Coding guidelines

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