| name | On-Call SLA Troubleshooting |
| description | SLA path failure workflow — read sla_paths.json, traceroute-first localization, ECMP handling, protocol triage |
On-Call SLA Troubleshooting Skill
Scope
On-Call mode investigation guide — reading sla_paths.json, traceroute-first localization, ECMP handling, protocol triage order.
Terminology
- Primary On-Call session: Triggered directly by a new SLA failure event. Full workflow: Steps 0 through 3, session closure, Jira, lessons. Runs in non-interactive print mode — exits autonomously after presenting the summary.
- Deferred failures: SLA Down events that arrive while a primary session is active. After the session ends,
watcher.py documents them automatically (Jira comment + Discord embed). No second agent session is spawned.
Before starting: Read cases/lessons.md per CLAUDE.md guidelines — past lessons often shortcut diagnosis.
Step 0: Read the sla_paths.json Entry for the Failed Path
Use the Read tool to read the file sla_paths/paths.json (this is a local file — do NOT use readMcpResource).
Locate the path entry matching the source device from the log event. Extract these key fields:
source_device: the device that generated the SLA failure event
destination_ip: IP being pinged by the SLA
scope_devices: ALL devices you may need to investigate
ecmp: if true, TWO paths exist — both must be checked
ecmp_node: the device where the path splits
ecmp_next_hops: the two next-hop devices after the split
Step 1: Traceroute from Source Device (Always First)
If source_ip is available in the paths.json entry, always pass it:
traceroute(device=<source_device>, destination=<destination_ip>, source=<source_ip>)
Using source_ip forces the traceroute onto the monitored path. If the source interface is down, the traceroute fails immediately at the source device — correctly localizing the issue without alternate-path confusion.
MANDATORY scope check — do this before applying any outcome below:
Compare EVERY hop in the traceroute output against the scope_devices list from Step 0. Resolve IP addresses to device names using the inventory or context you already have. If ANY intermediate hop is a device NOT in scope_devices, this is an off-path transit — even if the traceroute reached the destination. The path is using an alternate route, NOT the monitored path. Apply the third outcome below.
Do NOT treat a completed traceroute as "successful" if it transited an off-scope device.
Read the output:
- Path stops at hop N (or fails at source): issue is on or before that hop → proceed to Step 2.5
- Timeout at first hop: issue is on the source device itself (interface down, OSPF/BGP neighbor lost, no default route) → proceed to Step 2.5
- Path transits a device NOT in scope_devices: routing anomaly on the last in-scope hop. Do NOT investigate the off-path device. Identify the last hop that IS in scope_devices, run
get_routing(<that_device>, prefix=<destination_ip>) to confirm it is routing toward the off-path device. Whether the route is present or absent, treat that in-scope device as the breaking hop and proceed immediately to Step 2.5.
- Full path to destination AND all hops within scope_devices: do NOT conclude "transient" yet — go to Step 1a
Step 1a: Source-Device Sanity Check (when traceroute succeeds)
Even if the traceroute completes, the SLA was triggered for a reason. Verify the source device's local state with exactly two queries:
get_interfaces(device=<source_device>)
get_ospf(device=<source_device>, query="neighbors") ← run on source_device, NOT the next-hop
Critical: Always query routing protocol neighbors on the source_device first. Querying the next-hop device may show healthy adjacencies even when the source-side interface is down.
Branch A — Traceroute completed, source state healthy (source interface Up/Up AND source device's expected routing neighbors present AND all traceroute hops within scope_devices):
A completed traceroute does NOT automatically mean the path is recovered. The SLA may have recovered via a failover/alternate route while the primary defined path still has a failure. Determine which case applies:
Compare the observed traceroute hops against the expected path from Principle 1 (INTENT.json + paths.json):
| Case | Traceroute Result | Action |
|---|
| Truly transient | Hops follow the expected path exactly (same transit devices as the expected path) | Present summary table below, log to Jira as transient/self-resolved (resolution="Won't Fix"), proceed to session closure |
| Failover recovery | Hops reach the destination but via different devices than the expected path (alternate ABR, alternate ISP, different next-hops) | Primary path still broken — treat the first device where the trace diverges from the expected path as the breaking hop. Proceed to Step 2.5 to investigate why traffic is bypassing the expected path |
Present this summary table (fill in actual values):
| Check | Result | Status |
|-------|--------|--------|
| Traceroute to <destination_ip> | Full path, all hops respond | ✓ |
| Source interface (<source_interface>) | Up/Up | ✓ |
| Source device routing neighbors | All expected neighbors present | ✓ |
| Path follows expected route | <Yes — transient / No — via <alternate_device> — investigate> | ✓/✗ |
Branch B — Issue still present (source interface down OR expected neighbor missing):
This is the root cause. Proceed directly to Step 2.5.
Step 2: ECMP Handling
If the path entry has "ecmp": true, the traceroute shows ONE of the two paths. The other may also be broken.
- Both paths broken? If the path still fails after a fix, re-run traceroute with
source=<source_ip>. If it still fails or takes a different failing path, an independent failure exists on the alternate path — return to Step 1 with the new traceroute result.
- One path fixed — verify alternate:
get_routing(ecmp_node, prefix=<destination>) — expect 2 equal-cost entries. If only 1 entry, the alternate path is not restored.
- Path identification: The SLA probe may use either ECMP path. Always use
source=<source_ip> in traceroute to force the probe's perspective and avoid alternate-path confusion.
Proceed to Step 2.5 after identifying the breaking hop via traceroute. If the traceroute from Step 1 completed to the destination, at least one ECMP path is functional — then proceed to Step 2.5 to investigate the alternate path's devices if ECMP verification shows only one RIB entry.
Step 2.5: Basic Operational Checks (Mandatory — Run Before Anything Else)
A missing route on the breaking hop is NOT a reason to investigate other devices.
It is a reason to check the breaking hop's own state first.
get_interfaces(device=<breaking_hop>)
get_ospf(device=<breaking_hop>, query="neighbors") ← or get_bgp per Step 3 triage table
Decision gate:
| Result | Action |
|---|
| Interface down (admin or line-protocol) | Root cause found. Present findings table. Stop. |
| No neighbors / fewer neighbors than expected | Go directly to the Adjacency Checklist in the protocol skill. Do NOT investigate downstream devices. |
| All neighbors FULL, all interfaces Up/Up | Proceed to Step 3. Issue is in LSDB, RIB, or policy layer. |
Do not leave the breaking hop to investigate downstream devices in the path.
Missing adjacencies on the breaking hop explain missing routes everywhere downstream.
Investigating downstream only confirms the problem cascaded — it never finds the root cause.
Timer mismatch, passive interface, and area mismatch are caught in one query.
One fix per layer (Principle 7): If the decision gate above identifies an interface-layer issue (admin-down, line-protocol down), propose only the interface fix — even if investigation also revealed higher-layer issues (missing BGP config, wrong route-maps, etc.). Verify first. If the SLA path is still broken after the interface fix, re-run traceroute and start a new investigation cycle for the next layer.
Step 3: Protocol Triage — Which Skill to Read Next
Map the breaking hop to its protocol:
| Breaking Hop Device | Protocol to Investigate | Skill to Read |
|---|
| A1C, A2C (source) | OSPF Area 1 stub (to C1C or C2C ABR) | skills/ospf/SKILL.md |
| C1C, C2C | OSPF Area 0 core + ABR | skills/ospf/SKILL.md |
| E1C, E2C | OSPF Area 0 + BGP AS1010 + NAT | If OSPF neighbors down → skills/ospf/SKILL.md; if BGP to ISP down → skills/bgp/SKILL.md; NAT issue → skills/routing/SKILL.md |
| X1C | BGP AS2020 (to IAN + IBN) | skills/bgp/SKILL.md |
| IAN, IBN | BGP AS4040/AS5050 (ISP edge — fully managed) | skills/bgp/SKILL.md |
Time Efficiency Rules
- Localize first, don't investigate all: traceroute narrows to 1-2 devices max before running protocol tools
- ECMP: check both paths before concluding the issue is fixed
- Don't re-check devices that are not on the scope_devices list: out-of-scope devices won't affect this SLA path
- Non-scope hop in traceroute: stay in scope: if traceroute exits scope_devices, do NOT query or investigate the off-path device. Find the last in-scope hop, run
get_routing on it, then go to Step 2.5.
- No route on breaking hop → check its neighbors, not downstream devices: a missing route means "run Step 2.5 on this device now". Missing adjacencies on the breaking hop explain missing routes across the entire downstream path.
- Step 2.5 before any protocol skill section: zero neighbors is an adjacency problem (timers, passive, area, auth). Resolve it on the breaking hop before consulting LSDB, redistribution, or area-type sections.
Presenting Findings
Always present your analysis summary in a Markdown table before proposing a fix:
| Finding | Detail | Status |
|---|
| Traceroute result | Stopped at hop N — device X | ✗ |
| Interface / neighbor state | e.g. Ethernet3 admin down | ✗ |
| Root cause | Brief description | ✗ |
Use ✓ for healthy items and ✗ for the identified issues. This lets the user scan the summary instantly before approving any configuration change.
Step 4: Approval, Remediation & Session Closure
After presenting findings, follow this sequence exactly:
-
Call assess_risk — assess_risk(devices=<affected>, commands=<fix_commands>). Include the risk level in the findings table.
-
Call request_approval — request_approval(issue_key, summary, findings, commands, devices, risk_level). This posts an approval embed to Discord (if configured) and writes an audit record that push_config requires before it will execute.
-
Handle the return value:
| Decision | Action |
|---|
"approved" | Proceed to push |
"rejected" | Call post_approval_outcome(message_id=..., decision="rejected", decided_by=<username>), log to Jira, go to session closure |
"expired" | Call post_approval_outcome(message_id=..., decision="expired") to post expiry outcome to Discord. Log to Jira that the fix could not be applied (approval expired), then go to session closure. Never push. |
"skipped" | Discord not configured. The code-level gate blocks push_config (no APPROVED record). Log to Jira that no approval channel is configured and go to session closure. Never push. |
-
NEVER call push_config without approval — push_config enforces this at the code level and will return an error if no approved record exists or if the device list does not match the approval.
-
After approval: call push_config(devices=..., commands=...), then verify the fix resolved the issue (run traceroute and/or get_<protocol> to confirm). Then call post_approval_outcome(message_id=..., decision="approved", decided_by=<username>, verified=<True|False>, verification_detail=<brief_result>).
5a. Push failure retry — when push_config returns errors on one or more devices:
- Diagnose the error. Common causes: wrong AS/area number, syntax error, transport failure.
- For partial failures (some devices succeeded, some failed), scope the retry to only the failed devices.
- Call
post_approval_outcome(message_id=..., decision="approved", decided_by=<username>, verified=False, verification_detail="push failed: <brief error>") for the failed attempt.
- Correct the commands and call
request_approval again with corrected commands and the failed-device list only.
- On approval, call
push_config with the corrected commands and failed devices only. Continue to verification.
- Cap: 2 retries max (3 total push attempts). After the third failure, log to Jira and go to session closure.
- Each retry cycle produces its own Discord outcome embed — expected behavior.
- Session closure: follow CLAUDE.md On-Call Session Closure — log to Jira, evaluate
cases/lessons.md, present summary, then exit autonomously. The watcher resumes monitoring immediately. Deferred failures (if any) are documented by the watcher — no agent action required.
Jira Updates (On-Call)
Follow the Jira comment workflow in CLAUDE.md → Case Management. Use the cases/case_format.md structure for all comments. If no issue key is present, skip all Jira calls silently.