OmniNode-ai

Friction Triage

Read friction registry, escalate surfaces crossing threshold to Linear tickets

OmniNode-ai 4 3 Updated 3w ago
GitHub

Install

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

Install via the SkillsCat registry.

SKILL.md

Friction Triage

Reads $ONEX_STATE_DIR/state/friction/friction.ndjson, aggregates events by
skill:surface over a rolling window, and creates Linear tickets for surfaces
where count >= 3 OR severity_score >= 9.

Usage

/friction-triage                    # creates tickets automatically (default)
/friction-triage --dry_run true     # preview only — no tickets created
/friction-triage --window_days 7

Thresholds

Rule Threshold Rationale
Count-based count >= 3 Recurring nuisance (3x low = score 3, still noisy)
Score-based severity_score >= 9 One high event OR three medium events

Implementation

Step 1: Load and aggregate the registry

import sys
import os
from pathlib import Path

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_aggregator import aggregate_friction, THRESHOLD_COUNT, THRESHOLD_SCORE, WINDOW_DAYS

window_days = int("{{window_days}}" or WINDOW_DAYS)
dry_run = "{{dry_run}}".lower() == "true"

aggregates = aggregate_friction(window_days=window_days)
crossed = [a for a in aggregates if a.threshold_crossed]

print(f"{len(aggregates)} surfaces tracked over {window_days} days")
print(f"{len(crossed)} crossed threshold (count>={THRESHOLD_COUNT} OR score>={THRESHOLD_SCORE})")

Step 2: Deduplicate against existing Linear tickets

For each crossed aggregate, check if an open ticket already exists using
tracker.list_issues with the stable dedup marker
friction_surface_key: {surface_key} in the description.

# Search for existing open ticket with this surface key
existing = tracker.list_issues(
    query=f"[Friction] {agg.surface_key}",
    state="Todo,In Progress,Backlog",
    team="Omninode",
)

# Also check description for the stable dedup marker
for issue in existing.issues:
    if f"friction_surface_key: {agg.surface_key}" in (issue.description or ""):
        # Skip — open ticket already exists
        skip = True
        break

Step 3: Create Linear ticket for new surfaces

Use tracker.save_issue for each untracked crossing:

# Determine crossing reason
if agg.count >= THRESHOLD_COUNT and agg.severity_score >= THRESHOLD_SCORE:
    reason = f"count={agg.count} AND score={agg.severity_score}"
elif agg.count >= THRESHOLD_COUNT:
    reason = f"count={agg.count} (>= {THRESHOLD_COUNT})"
else:
    reason = f"score={agg.severity_score} (>= {THRESHOLD_SCORE})"

# Sample descriptions (up to 3 most recent)
sample_descriptions = "\n".join(
    f"- {d}" for d in agg.descriptions[-3:]
) if agg.descriptions else "(no descriptions recorded)"

ticket_context = f"Related ticket: {agg.most_recent_ticket}" if agg.most_recent_ticket else ""

tracker.save_issue(
    title=f"[Friction] {agg.surface_key} — {agg.count} occurrences / score {agg.severity_score} ({window_days}d)",
    team="Omninode",
    state="Backlog",  # no project: new tickets are created in the Backlog
    priority=2,  # High — friction crosses threshold
    description=f"""## Friction Surface Escalation

friction_surface_key: {agg.surface_key}

**Skill**: `{agg.skill}`
**Surface**: `{agg.surface}`
**Threshold crossed**: {reason}
**Rolling window**: {window_days} days
{ticket_context}

## Sample Descriptions

{sample_descriptions}

## Next Steps

1. Investigate root cause of `{agg.surface}` friction in skill `{agg.skill}`
2. Fix the underlying blocker if possible
3. If not fixable, add explicit handling / fallback in the skill
4. Close this ticket when the surface stops crossing threshold for one full window

## Friction Tracking

This ticket was auto-created by `/friction-triage`.
To query current state: `/friction-triage --dry_run true`
""",
)

Step 4: Report summary

Friction Triage Report ({window_days}-day window)
=================================================

Surfaces tracked: {len(aggregates)}
Threshold crossings: {len(crossed)}
New tickets created: {tickets_created}
Skipped (already tracked): {tickets_skipped}

| Surface Key | Count | Score | Threshold | Action |
|-------------|-------|-------|-----------|--------|
| gap:ci/missing-workflow | 4 | 4 | count>=3 | Created OMN-XXXX |
| integration_sweep:kafka/missing-topic | 1 | 9 | score>=9 | Created OMN-XXXX |
| pr_polish:linear/api-timeout | 2 | 6 | (below) | — |

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

Dedup Marker

Every ticket created by this skill includes the stable marker:

friction_surface_key: <surface_key>

This marker is checked before creating new tickets to prevent duplicates.
The marker is intentionally in the description (not the title) so it survives
title edits.

Relationship to record_friction

/record-friction appends individual events. /friction-triage performs the
rollup and escalation step. Run /friction-triage periodically (daily or
after a batch of skill runs) to convert accumulated friction into actionable
Linear tickets.