Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions skills/escalation-judge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: escalation-judge
version: 0.1.0
description: Judge whether a support thread crosses named escalation thresholds, append one durable case record, and emit a typed packet for a separate governed dispatcher.
links:
source: https://github.com/runxhq/runx/tree/main/skills/escalation-judge
runx:
category: ops
input_resolution:
required:
- triage_packet
- thread_body
- policy_rules
- aggregate_id
- expected_version
- idempotency_key
---

## What this skill does

`escalation-judge` reads a support triage packet, the original thread, named
escalation policy, and prior case projection. It grounds every decision in a
declared severity threshold or churn signal. When escalation is warranted it
appends one case event through `data-store@0.1.2` and emits a typed packet that
names, but never invokes, the downstream rail.

The skill performs no Slack post, email send, paging action, legal notification,
or other egress. A separate governed `slack-notify` or `send-as` run is required.

## Decision procedure

1. Read the prior projection for `aggregate_id`, the stable support thread id.
2. Refuse automatic escalation when policy thresholds or lane mappings are absent.
3. Stop with `already_escalated` when the prior projection records an existing case.
4. Compare the supplied severity only with named `severity_thresholds`.
5. Match churn risk only against literal strings in `churn_risk_signals`.
6. Reject any route not declared by `escalation_lanes`.
7. On a match, append an idempotent `support.escalation.opened` event using the
caller's `expected_version` and `idempotency_key`.
8. Emit a typed escalation packet naming the target rail. Do not dispatch it.

## Inputs

- `triage_packet`: `{classification,severity,confidence}` from a bounded triage run.
- `thread_body`: source text used only to ground declared churn signals.
- `policy_rules`: `{severity_thresholds,churn_risk_signals,escalation_lanes}`.
- `aggregate_id`: support thread id and data-store aggregate key.
- `expected_version`: compare-and-swap version for the case append.
- `idempotency_key`: stable retry key for the event.
- `data_source_ref`: logical data source binding used by `data-store`.

## Outputs and stops

The decision packet contains `decision`, `case_id`, `case_event`,
`escalation_packet`, `stop_state`, and evidence. A normal non-match seals with
`decision.escalate=false`, no case event, no escalation packet, and reason
`no_change`. Missing policy or an undeclared lane returns `needs_human` without
opening a case. Ambiguous or invented severity is never promoted.

## Example

```bash
runx skill ./skills/escalation-judge \
--input-json triage_packet='{"classification":"account_access","severity":"critical","confidence":0.96}' \
--input thread_body='Enterprise renewal is at risk after a production lockout.' \
--input-json policy_rules='{"severity_thresholds":{"executive_review":"critical"},"churn_risk_signals":["renewal is at risk"],"escalation_lanes":{"executive_review":"slack-notify"}}' \
--input data_source_ref='local://escalation-judge/example' \
--input aggregate_id='thread-123' \
--input expected_version=0 \
--input idempotency_key='thread-123:escalate:v1' \
--json
```

Verify the resulting receipt with `runx verify --receipt <receipt.json> --json`.
The packet's `target_rail` is an instruction for a separately authorized driver,
not proof that a message was sent.

143 changes: 143 additions & 0 deletions skills/escalation-judge/X.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
skill: escalation-judge
version: "0.1.0"

catalog:
kind: skill
audience: public
visibility: public
role: canonical

harness:
cases:
- name: escalate-critical-churn-risk
inputs:
data_source_ref: local://escalation-judge/harness
aggregate_id: thread-critical-001
expected_version: 0
idempotency_key: thread-critical-001:escalate:v1
triage_packet:
classification: account_access
severity: critical
confidence: 0.96
thread_body: Enterprise customer reports a production lockout and says renewal is at risk today.
policy_rules:
severity_thresholds:
priority_support: high
executive_review: critical
churn_risk_signals:
- renewal is at risk
escalation_lanes:
executive_review: slack-notify
priority_support: slack-notify
legal_review: send-as
expect:
status: sealed
- name: stop-no-policy-threshold
inputs:
data_source_ref: local://escalation-judge/harness
aggregate_id: thread-routine-002
expected_version: 0
idempotency_key: thread-routine-002:no-change:v1
triage_packet:
classification: how_to
severity: low
confidence: 0.92
thread_body: Customer asks where to find the setup guide.
policy_rules:
severity_thresholds:
priority_support: high
executive_review: critical
churn_risk_signals:
- renewal is at risk
escalation_lanes:
executive_review: slack-notify
priority_support: slack-notify
expect:
status: sealed
- name: refuse-missing-policy
inputs:
data_source_ref: local://escalation-judge/harness
aggregate_id: thread-missing-policy-003
expected_version: 0
idempotency_key: thread-missing-policy-003:refuse:v1
triage_packet:
classification: bug
severity: high
confidence: 0.88
thread_body: Production is unavailable.
policy_rules: {}
expect:
status: sealed

emits:
- name: escalation_decision
packet: runx.decision.v1
- name: escalation_packet
packet: runx.decision.v1

runners:
judge:
default: true
type: graph
inputs:
data_source_ref:
type: string
required: true
aggregate_id:
type: string
required: true
description: Stable support thread id.
expected_version:
type: number
required: true
idempotency_key:
type: string
required: true
triage_packet:
type: json
required: true
thread_body:
type: string
required: true
policy_rules:
type: json
required: true
graph:
name: escalation-judge
steps:
- id: read-prior-case
skill: ../data-store
runner: read_projection
inputs:
data_source_ref: "$input.data_source_ref"
store_id: escalation-judge-cases-v1
resource: support_escalation_cases
aggregate_id: "$input.aggregate_id"
- id: decide
run:
type: cli-tool
command: node
args:
- graph/judge/run.mjs
context:
prior_case_projection: read-prior-case.data_operation_result.data.projection
artifacts:
named_emits:
escalation_decision: escalation_decision
packets:
escalation_decision: runx.decision.v1
- id: append-case
when:
field: decide.escalation_decision.data.decision.escalate
equals: true
skill: ../data-store
runner: append_event
inputs:
data_source_ref: "$input.data_source_ref"
store_id: escalation-judge-cases-v1
resource: support_escalation_cases
aggregate_id: "$input.aggregate_id"
expected_version: "$input.expected_version"
idempotency_key: "$input.idempotency_key"
context:
event: decide.escalation_decision.data.case_event
13 changes: 13 additions & 0 deletions skills/escalation-judge/fixtures/escalate-critical.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"triage_packet": {"classification": "account_access", "severity": "critical", "confidence": 0.96},
"thread_body": "Enterprise customer reports a production lockout and says renewal is at risk today.",
"policy_rules": {
"severity_thresholds": {"priority_support": "high", "executive_review": "critical"},
"churn_risk_signals": ["renewal is at risk"],
"escalation_lanes": {"executive_review": "slack-notify", "priority_support": "slack-notify"}
},
"aggregate_id": "thread-critical-001",
"expected_version": 0,
"idempotency_key": "thread-critical-001:escalate:v1"
}

12 changes: 12 additions & 0 deletions skills/escalation-judge/fixtures/stop-no-change.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"triage_packet": {"classification": "how_to", "severity": "low", "confidence": 0.92},
"thread_body": "Customer asks where to find the setup guide.",
"policy_rules": {
"severity_thresholds": {"priority_support": "high", "executive_review": "critical"},
"churn_risk_signals": ["renewal is at risk"],
"escalation_lanes": {"executive_review": "slack-notify", "priority_support": "slack-notify"}
},
"aggregate_id": "thread-routine-002",
"expected_version": 0,
"idempotency_key": "thread-routine-002:no-change:v1"
}
14 changes: 14 additions & 0 deletions skills/escalation-judge/graph/judge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: escalation-judge-decision
version: 0.1.0
description: Deterministically evaluate a support escalation policy without sending or posting anything.
source:
type: cli-tool
command: node
args:
- run.mjs
---

Internal deterministic decision runner for `escalation-judge`. Use the public
parent skill rather than invoking this package directly.

41 changes: 41 additions & 0 deletions skills/escalation-judge/graph/judge/X.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
skill: escalation-judge-decision
version: "0.1.0"

catalog:
kind: skill
audience: public
visibility: public
role: context

runners:
decide:
default: true
type: cli-tool
command: node
args:
- run.mjs
inputs:
triage_packet:
type: json
required: true
thread_body:
type: string
required: true
policy_rules:
type: json
required: true
aggregate_id:
type: string
required: true
expected_version:
type: number
required: true
idempotency_key:
type: string
required: true
prior_case_projection:
type: json
required: false
artifacts:
named_emits:
escalation_decision: runx.business.lane.v1
Loading