Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ jobs:
tests/test_cli_help_no_dashboard_import.py \
tests/test_behaviour_signals.py \
tests/test_signals_ui_contract.py \
tests/test_self_diagnostics.py \
-q

# ── Entitlement API test suite (~2700+ hermetic tests) ─────────────────────────────────────────────
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- **What:** `spans` and `sessions` gain a `content_hash` column (ALTER, no rebuild). `ingest_spans_batch` and `ingest_sessions_batch` hash the row's content columns (write timestamps excluded), seed the hash map from the table on first use so a restart is not a rewrite, and skip rows whose content matches the last write; a changed field (late end_ts, cost, status, title) still overwrites, in-batch duplicates still keep the last occurrence, and the rollup mirror only sees rows that were actually written. Legacy rows without a hash are written once and then never again. `CLAWMETRY_UPSERT_DEDUPE=0` restores the old always-write behaviour. `query_forward_progress` now defaults `since` to the last 24 h (`CLAWMETRY_FWDPROG_DEFAULT_HOURS`) unless a `since` or `session_id` is given, and serves through the bounded read cache.
- **Verified:** 12 new tests in `tests/test_ingest_upsert_dedupe.py` (identical re-delivery writes zero rows, changed content overwrites, dedupe survives a restart, legacy NULL-hash rows stamped once, kill switch, in-batch dupes, rollups untouched on skip, forward-progress default window and cache hit); 7 of them fail on the unpatched store. The existing bulk-flush, span-ingest, read-lock, query-contract and local-query suites pass unchanged (85 tests). Wired into the CI MOAT file list.

### Added: agents can report to their operators over MCP, and ClawMetry checks whether the tool stream agrees (WO-59, REQ-SELF-001 to 004) (2026-09-04)
- **Why:** reasoning models are good at introspection when asked plainly. Given a reporting tool framed as notes to the people who run them, agents will say that a tool kept failing, that they lacked context or a permission, that they could not finish, or that they worked around a block. ClawMetry already watches the same session from outside, so every such note can sit next to what the detectors and the approval hooks recorded on their own, and an operator can see when the agent stayed quiet about something the detectors caught. The same MCP server is where a developer's editor can ask ClawMetry questions from where they already are.
- **What:** (1) The MCP server gains `report_to_operator` (six categories, operator-extendable via `config.json` `self_diagnostics.categories`, summary capped at 500 characters, session inferred from the environment or the working directory when absent) plus four read tools: `list_incidents`, `get_guard_status` (the per-session control verdict from the one resolver, policy decisions, open incidents), `get_signal_rates` (says "signals not available on this daemon version" when the daemon lacks the method) and `list_self_reports`. No tool on the server acts on a process; the test suite pins that. Every tool answers through the daemon and returns an honest error when it is down. (2) Reports land in a new additive `agent_self_reports` table through the daemon's local query server (the daemon keeps the writer lock); the summary passes the same redaction as every other stored text. (3) On the detector tick the daemon marks a report corroborated when a detector incident or a permission denial exists for the same session within `CORROBORATION_WINDOW_SECS` (600, `CLAWMETRY_SELFDIAG_WINDOW_SECS`); uncorroborated is labelled with its plain meaning, not the same as false. An honesty figure per (runtime, model), the share of detector incidents the agent also reported, is withheld with a reason under `MIN_INCIDENTS` (5, `CLAWMETRY_SELFDIAG_MIN_INCIDENTS`). (4) `clawmetry mcp install [--runtime <id>|all] [--dry-run] [--write-guidance]`, `uninstall` and `status` register the server with Claude Code, Cursor, Codex, Gemini CLI, OpenCode and Windsurf, each format verified against the vendor's documentation (Codex against `codex mcp add` itself); merge only, never a foreign entry deleted, uninstall removes only what a marker file says we wrote, a hand-written entry is left in place, JSONC is refused rather than guessed. Other runtimes report `no MCP support` or `unknown config format` by name. The instructions-file snippet is printed and written only with `--write-guidance`. (5) `GET /api/self-reports`, `/api/self-reports/honesty`, `/api/self-reports/support`; a "What the agent reported" panel on the transcript view; an "Agent reported" card on the Guard tab with per-category counts, the honesty table with withheld reasons, and the per-runtime MCP support state; a `selfReports` snapshot slice (counts and honesty, no summaries).
- **Verified:** `tests/test_self_diagnostics.py` (49 tests): tool schema and framing, no actuating tool, honest daemon-down errors for every tool, redaction and cap on the write path, idempotency, session inference, inclusive window logic, corroboration against real `loop_signals` and `approvals` rows, honesty withheld and computed, installer merge/never-delete/uninstall-only-ours/hand-written-left-in-place/JSONC-refused for all six formats with the Codex block round-tripped through a TOML parser, guidance offered not written, the CLI fast path never importing the dashboard, the routes, and the snapshot slice. Named in the `moat-tests` CI job and `make test-selfdiag`.

### Added: Behaviour Signals, six judge-free signals over every transcript, a rates API, an alert rule and a Signals tab (WO-58)
- **Why:** Guard reads the tool stream to notice an agent that is stuck. Nothing read the words. The person swearing at the agent, the agent refusing or handing the work back, the agent saying it could not finish, the person saying thanks: those live in the transcripts ClawMetry already holds for every runtime and were never counted. Claude Code's own team tracks a frustration rate from a keyword list; this brings the same family of signals to every runtime on the operator's machine, with no model call.
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ test-compat:
# Mirrored in .github/workflows/ci.yml (moat-tests job).
test-hooks:
python3 -m pytest tests/test_hooks_claude_code.py tests/test_hook_lifecycle.py tests/test_redaction_pii.py -q
# WO-59 self-diagnostics: MCP report tool, corroboration, honesty rollup,
# multi-runtime MCP installer. Mirrored in .github/workflows/ci.yml (moat-tests).
test-selfdiag:
python3 -m pytest tests/test_self_diagnostics.py -q

test-fast:
CLAWMETRY_URL=http://localhost:8900 CLAWMETRY_TOKEN=dev-token python3 -m pytest tests/test_api.py -v
Expand Down
23 changes: 17 additions & 6 deletions clawmetry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4691,9 +4691,10 @@ def _format_uptime(seconds):


def _cmd_mcp(args) -> None:
"""Start the ClawMetry MCP server on stdio (refs #2859)."""
from clawmetry.mcp_server import run
run()
"""`clawmetry mcp ...` (refs #2859, WO-59). Normally intercepted by the
fast path in main(); kept for callers that build a Namespace directly."""
from clawmetry.mcp_install import cli_main as _mcp_cli
raise SystemExit(_mcp_cli(list(getattr(args, "mcp_args", None) or [])))


def _cmd_reports(args) -> None:
Expand Down Expand Up @@ -7657,6 +7658,12 @@ def main() -> None:
# dashboard import. Stdlib-only; `stamp` always exits 0 (fail-open).
if len(sys.argv) > 1 and sys.argv[1] == "trace":
raise SystemExit(trace_main(sys.argv[2:]))
# FAST PATH — `clawmetry mcp [serve|install|uninstall|status]` (WO-59).
# `serve` is started by the agent host on every session and must not
# pay the dashboard import; the installer is stdlib-only as well.
if len(sys.argv) > 1 and sys.argv[1] == "mcp":
from clawmetry.mcp_install import cli_main as _mcp_cli
raise SystemExit(_mcp_cli(sys.argv[2:]))
# FAST PATH — `clawmetry instrument <runtime> …` (WO-57): writes the
# runtime's own OpenTelemetry exporter settings so it reports to this
# ClawMetry. Which runtimes: whatever profiles are registered (free ones
Expand Down Expand Up @@ -8169,11 +8176,15 @@ def main() -> None:
),
)

# mcp — start MCP server on stdio (issue #2859)
sub.add_parser(
# mcp — intercepted by the fast path at the top of main() (WO-59); the
# parser entry exists so `clawmetry --help` discovery shows it.
p_mcp = sub.add_parser(
"mcp",
help="Start ClawMetry MCP server (stdio) — lets agents query their own telemetry",
help="MCP server: `mcp` serves on stdio; `mcp install [--runtime <id>|all] "
"[--dry-run] [--write-guidance]` registers it with each runtime; "
"`mcp uninstall`; `mcp status`",
)
p_mcp.add_argument("mcp_args", nargs="*")

# uninstall — fully remove clawmetry
p_uninstall = sub.add_parser(
Expand Down
Loading
Loading