Skip to content

Commit 727a7ca

Browse files
authored
feat(scoring): transport-agnostic CI grounding — WebFetch, skills, CLI (#34)
Context Intelligence only counted MCP knowledge calls as grounding, missing WebFetch/WebSearch (98% knowledge validated), knowledge skills (deep-research, sdd-explore, claude-api, graphify), and CLI tools (codegraph explore, gh issue/pr view). This made CI invisible for users who consult context via non-MCP channels. Arm grounding from 6 channels: MCP knowledge (existing), MCP project/data/design explore-class (existing), WebFetch/WebSearch with localhost exclusion, knowledge skills via Skill tool and attributionSkill, knowledge Agent subagent_types, and CLI knowledge commands via Bash. All idempotent per-session — no double counting.
1 parent edb86b1 commit 727a7ca

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

gnomon/cli/accumulator.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
from gnomon.config import parse_ts, line_count, strip_injections
2828
from gnomon.taxonomy import (
2929
SCHEDULE_TOOLS, ASK_TOOLS, PLAN_SIGNAL_TOOLS, PLAN_SKILL_NEEDLES,
30+
KNOWLEDGE_SKILL_NEEDLES,
3031
classify_tool, classify_mcp_subcategory, CI_CONTEXT_SUBCATS,
31-
bash_writes_file, bash_runs_tests, _extract_clis,
32+
bash_writes_file, bash_runs_tests, bash_runs_knowledge, _extract_clis,
3233
_is_compounding_path, _SKILL_MD_RX,
3334
)
3435
from gnomon.sources.discovery import _AGENT_UNSUPPORTED_SOURCES
@@ -47,6 +48,15 @@
4748
BURST_GAP_S = 1800 # a gap > 30 min ends a contiguous work "run"
4849
DOW = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
4950

51+
_KNOWLEDGE_NATIVE_TOOLS = frozenset({"WebFetch", "WebSearch"})
52+
53+
54+
def _is_local_url(url):
55+
if not url:
56+
return False
57+
low = url.lower()
58+
return any(h in low for h in ("localhost", "127.0.0.1", "0.0.0.0", "[::1]"))
59+
5060

5161
def _zero_tok():
5262
return {"input": 0, "output": 0, "cache_read": 0, "cache_creation": 0}
@@ -218,6 +228,10 @@ def _is_plan_skill(name):
218228
sl = str(name).lower()
219229
return any(nd in sl for nd in PLAN_SKILL_NEEDLES)
220230

231+
def _is_knowledge_skill(self, name):
232+
low = (name or "").lower()
233+
return any(n in low for n in KNOWLEDGE_SKILL_NEEDLES)
234+
221235
def _mark_plan_session(self, sid, mkey):
222236
"""Record that session `sid` (in month `mkey`) contained a planning signal.
223237
Idempotent per session — repeated signals in one session count once."""
@@ -433,6 +447,8 @@ def observe(self, ev, since_dt, until_dt):
433447
self.month_skill_counter[mkey][ev["attributionSkill"]] += 1
434448
if self._is_plan_skill(ev["attributionSkill"]):
435449
self._mark_plan_session(sid, mkey)
450+
if self._is_knowledge_skill(ev["attributionSkill"]):
451+
self._pending_knowledge_grounding[sid] = True
436452
content = msg.get("content")
437453
if isinstance(content, list):
438454
for b in content:
@@ -484,6 +500,11 @@ def observe(self, ev, since_dt, until_dt):
484500
else:
485501
self.native_calls += 1
486502

503+
if sid and name in _KNOWLEDGE_NATIVE_TOOLS:
504+
url = inp.get("url", "")
505+
if not _is_local_url(url):
506+
self._pending_knowledge_grounding[sid] = True
507+
487508
# a tool use after a pending error = recovery
488509
if sid and self._pending_error.get(sid):
489510
self.recovered_errors += 1
@@ -500,6 +521,8 @@ def observe(self, ev, since_dt, until_dt):
500521
# a planning Skill also marks this a planning session
501522
if self._is_plan_skill(s):
502523
self._mark_plan_session(sid, mkey)
524+
if self._is_knowledge_skill(s):
525+
self._pending_knowledge_grounding[sid] = True
503526
# Plan-ceremony signal: mark this SESSION as a planning session.
504527
# PLAN_SIGNAL_TOOLS normalizes across sources (EnterPlanMode = Cursor
505528
# create_plan; ExitPlanMode = Claude Code native plan mode, shift+tab ->
@@ -512,10 +535,10 @@ def observe(self, ev, since_dt, until_dt):
512535
if name == "Agent":
513536
st = inp.get("subagent_type", "general-purpose")
514537
self.subagent_counter[st] += 1
515-
# A planning subagent (SDD planning phases, the built-in Plan
516-
# agent, *-planner types) also marks this a planning session.
517538
if self._is_plan_skill(st):
518539
self._mark_plan_session(sid, mkey)
540+
if self._is_knowledge_skill(st):
541+
self._pending_knowledge_grounding[sid] = True
519542
if mkey:
520543
self.month_subagent_counter[mkey][st] += 1
521544
if sid:
@@ -623,6 +646,8 @@ def observe(self, ev, since_dt, until_dt):
623646
self.month_skill_counter[mkey][_sm.group(1)] += 1
624647
if self._is_plan_skill(_sm.group(1)):
625648
self._mark_plan_session(sid, mkey)
649+
if self._is_knowledge_skill(_sm.group(1)):
650+
self._pending_knowledge_grounding[sid] = True
626651
if bash_writes_file(cmd):
627652
self.bash_write_calls += 1
628653
_bash_nl = cmd.count("\n")
@@ -634,6 +659,8 @@ def observe(self, ev, since_dt, until_dt):
634659
self.shell_test_runs += 1
635660
if mkey:
636661
self.month_shell_test_runs[mkey] += 1
662+
if bash_runs_knowledge(cmd):
663+
self._pending_knowledge_grounding[sid] = True
637664
if "git commit" in cmd:
638665
self.git_commits += 1
639666
# flush iteration-depth run for this session

gnomon/taxonomy.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@
2727
if _extra_plan_needles:
2828
PLAN_SKILL_NEEDLES = PLAN_SKILL_NEEDLES + tuple(
2929
n.strip().lower() for n in _extra_plan_needles.split(",") if n.strip())
30+
KNOWLEDGE_SKILL_NEEDLES = (
31+
"deep-research",
32+
"explore",
33+
"claude-api",
34+
"graphify",
35+
"codegraph",
36+
"find-docs",
37+
"find-skills",
38+
"explain-code",
39+
)
40+
_extra_knowledge_needles = os.environ.get("GNOMON_KNOWLEDGE_SKILL_NEEDLES", "")
41+
if _extra_knowledge_needles:
42+
KNOWLEDGE_SKILL_NEEDLES = KNOWLEDGE_SKILL_NEEDLES + tuple(
43+
n.strip().lower() for n in _extra_knowledge_needles.split(",") if n.strip())
3044
SCHEDULE_TOOLS = {"ScheduleWakeup", "CronCreate", "CronDelete", "CronList",
3145
"RemoteTrigger", "PushNotification", "Monitor"}
3246
SKILL_TOOLS = {"Skill"}
@@ -182,6 +196,20 @@ def bash_runs_tests(cmd):
182196
return bool(_SHELL_TEST_RE.search(cmd or ""))
183197

184198

199+
_SHELL_KNOWLEDGE_RE = re.compile(
200+
r'(?:^|[\s;&|(/])('
201+
r'codegraph\s+(?:explore|search|query)'
202+
r'|graphify'
203+
r'|gh\s+(?:issue|pr)\s+view'
204+
r')(?=$|[\s;&|):])',
205+
re.IGNORECASE,
206+
)
207+
208+
209+
def bash_runs_knowledge(cmd):
210+
return bool(_SHELL_KNOWLEDGE_RE.search(cmd or ""))
211+
212+
185213
def _extract_clis(command):
186214
"""Return the known-CLI heads invoked in a shell command (one per &&/|/;-separated part)."""
187215
found = []

0 commit comments

Comments
 (0)