diff --git a/skills/agent-plugin/SKILL.md b/skills/agent-plugin/SKILL.md index 9424bb6..d57975c 100644 --- a/skills/agent-plugin/SKILL.md +++ b/skills/agent-plugin/SKILL.md @@ -57,6 +57,37 @@ hotspots). - One statement per call; results are capped (a truncation note tells you when) — narrow the query or paginate. **Stable pagination requires `ORDER BY`**: `SKIP` without an `ORDER BY` gives no guaranteed order between calls, so pages can overlap or miss rows. +- **Edge properties are a JSON string in Cypher.** `r.ready`, `r.operations`, `r.via` are + null on a relationship; the props live in `r.props_json`. Read them with + `apoc.convert.fromJsonMap(r.props_json).ready`, or use `get_related` / + `get_resource_details`, which return them parsed. + +## Observed topology is not current traffic + +Every edge is an observation with an age, not a live probe: + +- APM edges (`CALLS_TO`, `USES_DATASTORE`, `PRODUCES_TO`, `CONSUMED_BY`, `USES_ENDPOINT`) + **accumulate**: a dependency seen once stays until pruned, and `get_resource_details` + lists a July edge next to today's. Pass `max_age_hours` (24 for "current") to + `get_related`, or filter on `r.currentAsOf` in Cypher, before saying "A currently calls B". + Read `observedAt` on every edge you cite. +- Structural Kubernetes edges (`EXPOSES`, `SCHEDULED_ON`, `CONTROLS`, …) are maintained + from the cluster and carry no `observedAt`; they say what the control plane declared, + not that packets flow. `EXPOSES {ready}` is the endpoint's readiness at the last + EndpointSlice the agent shipped. +- "Receives traffic right now" is a claim only a live source supports (APM within + `max_age_hours`, or the native tool below). Report graph edges as "declared / last + observed at T", not as present-tense traffic. + +## Verify current-state claims with a native source + +When the session also mounts a native source for the layer in question (`kubectl`, a cloud +CLI, the APM, PagerDuty), confirm any **current-state** claim there before reporting it as +current: which pods back a Service, whether a resource is reachable, who is on call, what +an incident is about. The graph is the evidence for topology, relationships and history; +the native source is the evidence for "right now". Cite both, and say which one each +statement rests on. When no native source is mounted, keep the claim time-stamped +("observed at T") rather than present-tense. ## Safety and trust diff --git a/skills/agent-plugin/references/query-patterns.md b/skills/agent-plugin/references/query-patterns.md index dcc4286..9971c02 100644 --- a/skills/agent-plugin/references/query-patterns.md +++ b/skills/agent-plugin/references/query-patterns.md @@ -15,8 +15,9 @@ mandatory diagnosis step list, and do not encode alert-specific conclusion recip - `get_resource_events` (`ref`/`refs` + `from`/`to` window, `type`/`source` filters) for its change history; - `get_related` for its graph neighborhood — read the NEIGHBOURHOOD SUMMARY block first (exact edge counts per relationship type from the degree store), then drill - into one type with `relationship_types` + a higher `limit`; event edges are - summarized only (`get_resource_events` lists them); + into one type with `relationship_types` + a higher `limit`; pass `max_age_hours` + (24) when the question is about CURRENT APM topology, since those edges accumulate; + event edges are summarized only (`get_resource_events` lists them); - `get_correlated_events` for a full incident chain from a `correlationId`; - `get_recent_events` for the project-wide feed — `since`+`until` give a two-sided window; `type`/`source`/`label`/`only_root`/`exclude_noise_classes` cut volume @@ -44,6 +45,11 @@ mandatory diagnosis step list, and do not encode alert-specific conclusion recip ## Reporting discipline - Cite `hashedID`s and event timestamps for every load-bearing claim. +- An edge is not live traffic. APM edges carry `observedAt`: report them as "last observed + at T". Structural Kubernetes edges carry none: report them as declared state ("the + Service selects these pods"), never as traffic. Confirm any present-tense claim (current + backends, current callers, current on-call, what an incident concerns) with a native + source when one is mounted (`kubectl`, cloud CLI, APM, PagerDuty). - Distinguish observed platform events from provider API records when it matters. - An empty result is not proof of absence — say what you searched and its bounds. - Do not claim causality from temporal proximity alone. diff --git a/skills/agent-plugin/references/recipes.md b/skills/agent-plugin/references/recipes.md index 93515e8..0c442de 100644 --- a/skills/agent-plugin/references/recipes.md +++ b/skills/agent-plugin/references/recipes.md @@ -337,6 +337,49 @@ project has no data for that kind, which is not the same finding. Property names (`replicas`, `storageClass`, `image`) are per-source: confirm with `get_resource_details` on one node first. +## PagerDuty incident → owning service → workload + +"Which incident is about this workload?" and "what does this incident concern?" resolve +through the PagerDuty **service**, never through the incident title: titles are free text +and the loudest one is not the relevant one. The graph stores +`PAGERDUTY_ALERT -[:TRIGGERED]-> PAGERDUTY_INCIDENT -[:AFFECTS]-> PAGERDUTY_SERVICE`, and +`PAGERDUTY_SERVICE -[:RESOLVES_TO]-> ` when the service is mapped (an +`anyshift.resource_id=` / `anyshift.qualified_name=` directive in the service description, +or a service name / URL that normalizes to one hostname). Labels appear in `describe_schema` +only when the project has the PagerDuty integration. + +From a workload (seed by `hashedID` from `find_resources`): + +```cypher +MATCH (w:RESOURCE:ALIVE {hashedID: ''})<-[:RESOLVES_TO]-(s:PAGERDUTY_SERVICE:ALIVE) +OPTIONAL MATCH (i:PAGERDUTY_INCIDENT:ALIVE)-[:AFFECTS]->(s) +WHERE i.status IN ['triggered', 'acknowledged'] +OPTIONAL MATCH (a:PAGERDUTY_ALERT:ALIVE)-[:TRIGGERED]->(i) +RETURN s.name AS service, i.hashedID AS incidentId, i.title AS incident, i.status AS status, + i.urgency AS urgency, count(DISTINCT a) AS alerts +ORDER BY urgency, incident LIMIT 50 +``` + +From an incident (its `hashedID` from `find_resources` on the incident label): + +```cypher +MATCH (i:RESOURCE {hashedID: ''})-[:AFFECTS]->(s:PAGERDUTY_SERVICE:ALIVE) +OPTIONAL MATCH (s)-[:RESOLVES_TO]->(w:ALIVE) +RETURN i.title AS incident, i.status AS status, s.name AS service, + collect(DISTINCT CASE WHEN w IS NULL THEN null + ELSE {cluster: w.clusterID, workload: coalesce(w.namespace + '/', '') + coalesce(w.name, w.hashedID), hashedID: w.hashedID} END) AS workloads +``` + +Keep `incidentId` in the first RETURN (titles repeat, so grouping on title alone merges +concurrent incidents) and `cluster`/`hashedID` in the collected workloads (the same +`namespace/name` exists once per cluster). + +Caveats: an empty `workloads` means the PagerDuty service is **unmapped**, not that the +incident concerns nothing — say so and fall back to the service name. Incident `status` +and the on-call roster are current only in PagerDuty itself: confirm there before +reporting who is paged or whether an incident is still open. Property names (`title`, +`status`, `urgency`) are per source; confirm on one node with `get_resource_details`. + ## Events scoped to one cluster (or namespace) "What happened in cluster X yesterday?" on a multi-cluster project must never be answered