OmniNode-ai

Record Friction

Record a friction event for a skill process blocker

OmniNode-ai 4 3 Updated 4mo ago
GitHub

Install

npx skillscat add omninode-ai/omniclaude/plugins-onex-skills-record-friction

Install via the SkillsCat registry.

SKILL.md

Record Friction

Records a friction event to $ONEX_STATE_DIR/state/friction/friction.ndjson.

Friction events are the deterministic backbone of the friction tracking system.
Use this skill when a skill run is blocked by an unresolved external
issue (missing Kafka topic, CI misconfiguration, permission denied, etc.).

Usage

/record-friction --skill integration_sweep --surface kafka/missing-topic --severity medium
/record-friction --skill gap --surface ci/missing-workflow --severity low --description "Workflow file not found in repo"
/record-friction --skill pr_polish --surface linear/api-timeout --severity high --ticket_id TASK-5132

Surface Taxonomy

Format: <category>/<specific> where category is one of:
kafka | ci | config | permissions | linear | network | auth | tooling | unknown

Unknown categories are normalized to unknown/<mangled>.

Severity Thresholds

Severity Weight When to use
low 1 Minor, easily worked around
medium 3 Significant degradation, multi-step workaround
high 9 Data loss, security issue, complete unavailability

A single high event already crosses the score threshold (9) and will create a
Linear ticket on next /friction-triage run.

Implementation

Step 1: Resolve ticket context (nullable)

import json
import os
from pathlib import Path

ticket_id = "{{ticket_id}}" or None
# If not provided, attempt to read from session state
if not ticket_id:
    session_file = Path.home() / ".claude" / "state" / "session.json"
    if session_file.exists():
        try:
            data = json.loads(session_file.read_text())
            ticket_id = data.get("active_ticket_id") or None
        except Exception:
            pass
    # ticket_id may remain None — this is valid; report to user

Step 2: Record the friction event

import sys
import os
from datetime import UTC, datetime

plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
shared_path = f"{plugin_root}/skills/_shared"
if shared_path not in sys.path:
    sys.path.insert(0, shared_path)

from friction_recorder import FrictionEvent, FrictionSeverity, record_friction

sev_str = "{{severity}}" or "medium"
try:
    severity = FrictionSeverity(sev_str)
except ValueError:
    severity = FrictionSeverity.MEDIUM

event = FrictionEvent(
    skill="{{skill}}",
    surface="{{surface}}",
    severity=severity,
    description="{{description}}" or "",
    context_ticket_id=ticket_id,
    session_id=os.environ.get("CLAUDE_SESSION_ID", ""),
    timestamp=datetime.now(UTC),
)
record_friction(event, emit_kafka=True)

# Capture normalized values for report
normalized_surface = event.surface

Step 3: Report result to user

Friction recorded:
  skill:    {{skill}}
  surface:  <normalized_surface>
  severity: <severity> (weight: <weight>)
  ticket:   <ticket_id or "(none — no ticket context resolved)">

Registry: $ONEX_STATE_DIR/state/friction/friction.ndjson

<if severity == high>
Note: A single 'high' event (score=9) crosses the threshold. Run /friction-triage
to escalate this surface to a Linear ticket immediately.
</if>

Relationship to friction_triage

/record-friction appends individual events. /friction-triage reads the
registry, aggregates by skill:surface over 30 days, and creates Linear tickets
when count >= 3 OR severity_score >= 9.

Registry Format

Each line in $ONEX_STATE_DIR/state/friction/friction.ndjson is a JSON object:

{
  "skill": "integration_sweep",
  "surface": "kafka/missing-topic",
  "severity": "medium",
  "description": "Topic onex.evt.foo.v1 not found",
  "context_ticket_id": "TASK-5132",
  "session_id": "abc123",
  "timestamp": "2026-03-19T12:00:00+00:00"
}