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
7 changes: 7 additions & 0 deletions clawmetry/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ class Session:
cost_usd: float | None = None
cost_status: str = ""
end_reason: str = ""
# Working directory the session ran in ("" when the runtime hides it).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Drift Bot (ClawMetry) — Blueprint: Runtime and Session Observability

The Session dataclass adds cwd as a first-class field. The blueprint's ADR-010 describes cwd as "extracted and normalized at ingest via alias walk," but does not specify that it should also be a first-class Session field that adapters must populate alongside the extra["cwd"] mirror for backward compatibility. The dual persistence contradicts the single-source-of-truth principle implied by the blueprint.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Drift Bot (ClawMetry) — Requirement: Extended Runtime Support

The Session dataclass adds cwd as a first-class field (line 119) documented as required for kill/pause pid resolution. However, the requirement does not specify cwd as a first-class Session field; the existing contract expected adapters to provide it through extra["cwd"] and the dual persistence (both first-class and extra) contradicts a single-source-of-truth design.

# First-class because kill/pause pid resolution keys on it
# (process_control.resolve_by_cwd); adapters should ALSO mirror it into
# extra["cwd"] while older OSS wheels without this field are in the
# fleet (a pro adapter passing cwd= against an old wheel would crash).
cwd: str = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Drift Bot (ClawMetry) — Blueprint: Runtime and Session Observability

The Session dataclass adds cwd as a first-class field, documented as required for kill/pause pid resolution. However, the blueprint's section on session location (ADR-010) describes cwd as extracted and normalized at ingest, not as a first-class Session field that adapters must populate.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Drift Bot (ClawMetry) — Blueprint: Runtime and Session Observability

The Session dataclass adds cwd as a first-class field with a comment stating adapters "should ALSO mirror it into extra[cwd]" for backward compatibility. The blueprint's ADR-010 describes cwd as extracted/normalized at ingest via alias walk, not as a field requiring dual persistence in both first-class and extra dictionary forms.

extra: dict[str, Any] = field(default_factory=dict)

def to_dict(self) -> dict[str, Any]:
Expand All @@ -134,6 +140,7 @@ def to_dict(self) -> dict[str, Any]:
"costUsd": self.cost_usd,
"costStatus": self.cost_status,
"endReason": self.end_reason,
"cwd": self.cwd,
}
if self.extra:
d["extra"] = self.extra
Expand Down
11 changes: 11 additions & 0 deletions clawmetry/approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -1896,6 +1896,17 @@ def _register_default_gate_handlers() -> None:
GATE_HANDLERS.setdefault("claude_code", _cc_gate)
except Exception as e: # never let a broken module kill the watcher
log.debug("claude_code gate handler unavailable: %s", e)
# Cursor + Copilot CLI native pre-tool gates (2026-08-19 matrix-gap
# sprint) — same install-only-when-policies-want-it lifecycle.
try:
from clawmetry.runtime_gates import (
copilot_gate_handler as _cp_gate,
cursor_gate_handler as _cu_gate,
)
GATE_HANDLERS.setdefault("cursor", _cu_gate)
GATE_HANDLERS.setdefault("copilot", _cp_gate)
except Exception as e:
log.debug("cursor/copilot gate handlers unavailable: %s", e)
_default_gates_registered = True


Expand Down
7 changes: 7 additions & 0 deletions clawmetry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6860,6 +6860,13 @@ def main() -> None:
if len(sys.argv) > 2 and sys.argv[2] == "attention":
from clawmetry.attention_hook import attention_main
raise SystemExit(attention_main(sys.argv[3:]))
# `clawmetry hook cursor|copilot --base <url>` — same gate-client
# contract as claude-code (stdlib-only, fail-open, always exit 0)
# but speaking the runtime's own hook payload/response shapes. See
# clawmetry/runtime_gates.py.
if len(sys.argv) > 2 and sys.argv[2] in ("cursor", "copilot"):
from clawmetry.runtime_gates import hook_main as _rt_hook_cli
raise SystemExit(_rt_hook_cli(sys.argv[2:]))
from clawmetry.claude_code_gate import hook_main as _hook_cli
raise SystemExit(_hook_cli(sys.argv[2:]))
# FAST PATH — agent-facing read CLI (`clawmetry sessions|activity|waste|
Expand Down
53 changes: 53 additions & 0 deletions clawmetry/local_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2463,6 +2463,59 @@ def update_session_location(
session_id, exc_info=True)
return False

def get_session_location(
self,
session_id: str,
agent_type: str | None = None,
) -> dict[str, Any] | None:
"""Read one session's ``cwd`` / ``git_branch`` / decoded ``metadata``.

Companion read for :meth:`update_session_location`, added for the
process-control cwd backfill (the cloud Stop/Pause relay carries no
cwd, so the daemon looks the directory up here before resolving the
session to a pid).

``agent_type`` scopes the lookup: the table is keyed
``(agent_type, session_id)``, and an unscoped match could hand back a
DIFFERENT runtime's directory for a colliding id — which would then
be used to pick a process to signal. Goes through ``_fetch`` so it
takes the same lock as every other read rather than touching the
connection directly. Returns None when unknown; never raises.
"""
sid = _clean_str(session_id)
if not sid:
return None
try:
if agent_type:
rows = self._fetch(
"SELECT cwd, git_branch, metadata FROM sessions "
"WHERE agent_type = ? AND session_id = ? LIMIT 1",
[str(agent_type), sid],
)
else:
rows = self._fetch(
"SELECT cwd, git_branch, metadata FROM sessions "
"WHERE session_id = ? LIMIT 1",
[sid],
)
except Exception:
log.debug("local store: get_session_location failed for %s",
sid, exc_info=True)
return None
if not rows:
return None
row = rows[0]
meta: dict[str, Any] = {}
if row[2]:
try:
decoded = json.loads(row[2])
if isinstance(decoded, dict):
meta = decoded
except Exception:
pass
return {"session_id": sid, "cwd": row[0], "git_branch": row[1],
"metadata": meta}

def apply_session_attention(self, items: list[dict[str, Any]]) -> int:
"""Publish the daemon's INFERRED "needs you" pass onto session rows.

Expand Down
Loading
Loading