# sla — Planning

SLA duration settings, escalation rules, and conditional SLA overrides. Applied at either root (whole case) or stage level.

## When to Use

Pick this plugin whenever the sdd.md mentions deadlines, service-level agreements, time-to-complete expectations, or escalation notifications:

- "This case must resolve within 5 days"
- "Notify the manager when the SLA is at 80% risk"
- "If the case is flagged Urgent, use a 30-minute SLA"
- "Escalate to the group when the SLA breaches"

## Three Sub-Operations (one plugin, three workflows)

| Sub-op | Purpose |
|--------|---------|
| **Default SLA** | The time-based catch-all SLA. One per target (root or stage). Written into the SLA rules array with `expression: "=js:true"`. See [impl-json.md § Target resolution](impl-json.md) for the destination paths. |
| **Conditional SLA rules** | Expression-driven SLA overrides. Supported on root and stage targets. Prepended to the target's SLA rules array ahead of the default. |
| **Escalation rules** | Status markers and notifications triggered at-risk or on breach. Attached to a specific rule via `escalationRule[]`; their IDs may also drive an interrupting secondary-stage `sla-status-change` entry. |

## Applying SLA at Root vs Stage

- **Root** — the default SLA for the whole case. Target `"root"`; written to `metadata.slaRules[]`.
- **Stage** — stage-specific SLA. Target `"<stage-name>"`; written to the stage node's `data.slaRules[]`. Overrides the root default while the stage is active.

Set root SLA first, then stage SLAs. This mirrors the schema precedence: stage > root.

> **Conditional SLA rules use the same target scope as defaults.** Root rules live in `metadata.slaRules[]`; stage rules live in the stage node's `data.slaRules[]`. On either target, conditional entries precede the trailing `=js:true` default.

> **Secondary-stage SLA is supported.** Author it the same way as a regular Stage SLA — write `data.slaRules[]` on the `case-management:Stage` node (the secondary stage, i.e. `data.stageType: "secondary"`). See [`impl-json.md`](impl-json.md).

> **Per-conditional-rule escalations are supported.** Attach an escalation rule to any entry in `slaRules[]`, not only the default `"=js:true"` rule.

## Required Fields from sdd.md

### Default SLA

| Field | Source | Notes |
|-------|--------|-------|
| `count` | sdd.md duration number | Positive integer |
| `unit` | sdd.md duration unit | `min` (minutes) \| `h` (hours) \| `d` (days) \| `w` (weeks) \| `m` (months). Carry the sdd.md token verbatim — `m` is **months**, never minutes. |
| `target` | sdd.md target (root vs stage) | `"root"` or `"<stage-name>"` |
| `display-name` | sdd.md `SLA Title` (§1 Case Metadata for root; `**SLA Title:**` under `#### Stage SLA`) or generated fallback | Required non-empty SLA title, unique within the target, and MUST NOT contain `:`. Carry the SDD title verbatim. If the SDD has no title, ask for one or use the deterministic fallback `SLA Rule {N}` and record it. |
| `rationale` | sdd.md case/stage SLA Design Rationale | Required reviewer context for the target, duration, threshold, and escalation behavior. |

### Conditional SLA rule

| Field | Source | Notes |
|-------|--------|-------|
| `target` | sdd.md target (root vs stage) | `"root"` or `"<stage-name>"` |
| `expression` | sdd.md condition | Natural-language in planning; the execution phase translates. **Do not fabricate syntax during planning.** |
| `count`, `unit` | sdd.md duration for this condition | Same units as default |
| `display-name` | sdd.md or generated fallback | Required non-empty unique title, no `:`; use `SLA Rule {N}` only when the author supplied no title. |
| `rationale` | sdd.md case/stage SLA Design Rationale | Required reviewer context; not emitted into JSON. |

Rules are evaluated in insertion order — first truthy expression wins. The default SLA acts as the fallback.

### Escalation rule

| Field | Source | Notes |
|-------|--------|-------|
| `trigger-type` | sdd.md | `at-risk` \| `sla-breached` |
| `at-risk-percentage` | sdd.md | Required when `trigger-type: at-risk`; preserve the supplied value because FE validates presence (it does not enforce a numeric range here). |
| `recipient-scope` | sdd.md | `User` \| `UserGroup` |
| `recipient-target` | sdd.md → resolver | Recipient UUID. When sdd gives an email or group name, run [§ Identity Resolution](#identity-resolution) — resolved UUID written inline. On resolver failure or user decline, mark `<UNRESOLVED: user-uuid for <email>>` / `<UNRESOLVED: group-uuid for <name>>`. |
| `recipient-value` | sdd.md | Display value (typically the email for User, group name for UserGroup). |
| `display-name` | sdd.md escalation-table `Display Name` cell | Required non-empty escalation title, unique across the target, and MUST NOT contain `:`. Carry the SDD title verbatim. If omitted, use `Escalation Rule {N}` and record the fallback. |
| `target` | sdd.md target (root vs stage) | `"root"` or `"<stage-name>"` |
| `attach-to` | sdd.md | `default` (attach to the `=js:true` rule) or `T<m>` pointing to the conditional-rule element the escalation fires under. |
| `rationale` | sdd.md case/stage SLA Design Rationale | Required reviewer context. If this escalation enters a secondary stage, name that lane and why it is global/interrupting. |

**In the planner's SDD, a stage notification names its recipient only in prose.** The stage SLA table carries titles (`At-Risk Escalation Display Name` / `Breach Escalation Display Name`), and the `### SLA Response Map` row gives the response, with Target left blank for `notify-only`. Every At-Risk row and every `notify-only` Breached row needs an escalation ([impl-json.md § The clock is not the response](impl-json.md#the-clock-is-not-the-response)). The template has the stage's Design Rationale name the recipient, and that row's Rationale cell can repeat it (e.g. "bumped up to the Category Management group so it does not stall"). Read both before writing `recipients[]`.

## Identity Resolution

When sdd gives an escalation recipient as an email (`User: manager@corp.com`) or group name (`UserGroup: "Order Management Team"`), resolve to a directory UUID via `uip admin` while authoring the escalation rule. Resolved UUIDs land in `recipients-resolved.json`; [`impl-json.md`](impl-json.md) writes them straight into `escalationRule[].action.recipients[].target` — no sentinel needed. Resolution runs **Phase 1 only** — the design lane records email / group name as a string in sdd.md.

### Skip — UUID pass-through

Recipient value already matches `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$` → skip CLI. Write `<uuid> / <uuid>` in `recipients-resolved.json`. Audit rationale: `uuid-pass-through`.

### Resolve `User`

1. `uip admin users list --search "<email>" --output json`
2. **Auto-accept** when response has exactly 1 entry AND `entry.email` equals sdd's email case-insensitively. Write `User: <id> / <email>`. Rationale: `auto-exact-email`.
3. **Fallback** on any other shape (0, >1, partial). If sdd carries a display name, retry `--search "<display-name>"`. Merge results, dedupe by `id`.
4. **Ask** via AskUserQuestion with up to 3 candidates. Each option: label `<displayName>`, description `<email> · id=<uuid-first-8>...`. Final option: `Keep as <UNRESOLVED>`. Rationale: `user-picked-from-N` or `user-declined-keep-unresolved`.

### Resolve `UserGroup`

`uip admin groups list` has **no `--search` flag**. Filter client-side.

1. **Pull once.** First group lookup in the planning session: `uip admin groups list --output json`. Cache the array in memory for the rest of Phase 1.
2. **Exact match** — entries where `name` OR `displayName` equals sdd's group name case-insensitively. Exactly 1 match → write `UserGroup: <id> / "<group-name>"`. Rationale: `auto-exact-name`.
3. **Substring fallback** — case-insensitive substring on `name` / `displayName`. Any hits → AskUserQuestion with top 3 (alphabetical by `name`) + `Keep as <UNRESOLVED>`.
4. **Empty** — both filters return 0 → AskUserQuestion: `No matching group found for "<group-name>". Keep as <UNRESOLVED>?` with a single `Keep as <UNRESOLVED>` option. Do NOT fabricate "fuzzy candidates"; the user patches the UUID externally per the standard decline path. Rationale: `user-declined-keep-unresolved`.

### Session cache

In-memory, scoped to the Phase 1 run. Key: lowercased sdd input. **Positive resolutions only** — auto-accept results and user-picked UUIDs. Do NOT cache `Keep as <UNRESOLVED>` decisions; same recipient appearing in a later element re-asks.

### CLI failure (auth / network / 403)

Non-zero exit from `uip admin ...` → AskUserQuestion:

```
Question: Identity resolution failed (<stderr first line>). How should we proceed?
Header:   Resolver failed
Options:
  - Retry (max 2 attempts)
      → re-run the same `uip admin ...`. Continue on success. After 2 failed retries the resolver auto-routes to "Skip resolution for this build" — do not loop further.
  - Skip resolution for this build
      → leave every recipient as <UNRESOLVED: ...>, log to tasks/build-issues.md, surface in completion report. Subsequent recipient lookups in this Phase 1 skip the CLI.
  - Abort planning
      → halt Phase 1.
```

### Audit — `tasks/recipients-resolved.json`

Append one object per resolution attempt (incremental Edit, mirroring `registry-resolved.json` discipline):

```json
{
  "sddInput": "manager@corp.com",
  "kind": "user",
  "searchTerm": "manager@corp.com",
  "allCandidates": [
    {"id": "a1b2c3d4-0000-0000-0000-000000000000", "email": "manager@corp.com", "displayName": "Anne Manager"}
  ],
  "selected": "a1b2c3d4-0000-0000-0000-000000000000",
  "rationale": "auto-exact-email"
}
```

Rationale values: `auto-exact-email`, `auto-exact-name`, `user-picked-from-N`, `user-declined-keep-unresolved`, `uuid-pass-through`, `cli-failed-skipped`.

## Ordering

SLA is the **last** element class written, after conditions. For each target, order within the target:

1. Default SLA element
2. Conditional SLA rule elements for that target
3. Escalation rule elements (one per rule)

## Fields to Resolve

SLA rules and escalations produce **no `tasks/registry-resolved.json` entry** — that ledger holds registry lookups. Resolved recipient identities go to `tasks/recipients-resolved.json` instead; everything below is a reasoning field Phase 2 reads from `sdd.md` ([planning.md § Step 4](../../planning.md)).

### Default SLA

```text
default SLA on "<target>" — <duration>
- target: "<root>" | "<stage-name>"
- display-name: "SLA Rule 1"              # required; use authored title or SLA Rule {N}
- rationale: "<why this SLA target and duration fit>"
- count: 5
- unit: d
- verify: Confirm Result: Success
```

### Conditional SLA rule

```text
conditional SLA rule on "<target>" — <condition summary>
- target: "root" | "<stage-name>"
- display-name: "Urgent SLA"              # required; target-unique, no ':'
- rationale: "<why this condition changes the SLA>"
- condition: "<natural-language condition from sdd.md>"
- count: 30
- unit: min
- verify: Confirm Result: Success
```

### Escalation rule

```text
escalation rule on "<target>" — <trigger summary>
- target: "<root>" | "<stage-name>"
- attach-to: default | T<m>
- rationale: "<why this threshold/recipient/action fits; name any interrupting secondary stage>"
- trigger-type: at-risk
- at-risk-percentage: 80
- recipients:
  - User: a1b2c3d4-0000-0000-0000-000000000000 / manager@corp.com
  - UserGroup: <UNRESOLVED: group-uuid for "Order Management Team"> / "Order Management Team"
- display-name: "Notify Manager"
- verify: Confirm Result: Success, capture EscalationRuleId
```

**Recipient format:** `<target> / <value>` where `<target>` is the resolved UUID (per [§ Identity Resolution](#identity-resolution)) — or `<UNRESOLVED: …>` sentinel when the resolver failed or the user declined — and `<value>` is the display string. Unresolved recipients survive into execution; the user patches the UUID externally after the build and the completion report lists every unresolved recipient.

**`attach-to: default`** is the default. Use `T<m>` when sdd.md attaches an escalation to a specific conditional SLA rule.

## Frontend validation parity

Before emitting SLA elements, reject or repair the same cases the Case App rejects:

- every SLA rule has a non-empty, target-unique `display-name`;
- every escalation has a non-empty, target-unique `display-name`;
- every SLA `count` is positive, and `unit: "min"` counts are between 15 and 1000 inclusive. This floor applies ONLY to `min`; `h` / `d` / `w` / `m` counts carry no floor, so never rewrite a unit or clamp a count to satisfy it;
- every non-default rule has a condition/expression;
- every escalation has at least one recipient, and every `at-risk` escalation carries `at-risk-percentage`.

## Anti-Patterns

- **Do not fabricate expression syntax.** Describe conditional SLA rules in natural language during planning; the execution phase handles the exact syntax.
- **Do not lose the conditional rule's target.** Root and stage rules have the same entry shape but different destinations (`metadata.slaRules[]` vs `node.data.slaRules[]`). Preserve the SDD row's target through to the caseplan write.
- **Do not normalize the sdd.md duration unit.** `m` in an sdd.md SLA row is **months**, not minutes — emit `unit: "m"` and keep the count as written. A count below 15 is legal for `m` / `h` / `d` / `w`; the 15–1000 floor is `min`-only. Rewriting the unit or clamping the count to clear a floor that does not apply silently moves every escalation deadline in the case.
- **Do not invert rule order.** Conditional rules are evaluated in insertion order — insert them in the priority order the sdd.md specifies.
- **Do not skip the resolver to save a CLI call.** Email / group-name recipients MUST go through [§ Identity Resolution](#identity-resolution). Writing `<UNRESOLVED: ...>` directly without attempting `uip admin users/groups list` is a planning bug.
- **Do not fabricate UUIDs.** When the resolver returns 0 / multi / partial matches, AskUserQuestion or keep `<UNRESOLVED>` — never guess a UUID, never auto-pick the first candidate without the exact-email / exact-name gate.
- **Do not cache user declines.** Session cache holds positive resolutions only. Re-ask on each occurrence of the same unresolved recipient.

<!-- END: planning.md -->
