# Dispatch Assignee Notification 设计

创建日期：2026-08-12

更新日期：2026-08-13

状态：implemented（默认关闭）

基线：`main@0a86299a`

关联设计：

- [Ticket Dispatcher 业务设计](./2026-07-10-ticket-dispatcher-design.md)
- [Ticket Dispatcher 模块设计](./2026-07-10-ticket-dispatcher-module-design.md)
- [Dispatch Triage Action 设计](./2026-07-14-dispatch-triage-action-design.md)

## 1. 结论与范围

本需求复用现有 Dispatch Trigger 和 Policy/Rule/Action 执行模型，不新增 assignment event API、
event outbox 或 notification 领域表。

最终方案如下：

1. Jira Automation 监听 Assignee/L2 Assignee 的非空字段变更，以及工单从 non-urgent 进入 urgent；
   不监听 `issue_created`，创建时已经带 Assignee 的工单明确忽略。
2. Automation 直接调用现有
   `POST /api/v1/ticketmanager/dispatch/triggers`。变化的业务输入只有 `input.issue_key`。
3. 使用两个 Dispatch 保留 source 区分角色通道：
   `jira_assignment_assignee` 和 `jira_assignment_l2_assignee`。
4. `notify-assignee`、`notify-l2-assignee` Label 继续保留，但只作为 durable pending marker；Policy
   路由同时依赖 `source_in + project_key_in + issue_label_all`，不再采用 Label-only 路由。
5. 一共发布三类 Policy：TCOC/GTOC/TCT Assignee、TCOC/GTOC/TCT L2 Assignee、七个 Severity 项目的
   Assignee。七个 Severity 项目不启用 L2 通知。
6. 每个 Policy 固定两条 Rule：
   `always -> send_assignment_notification -> continue`；随后 urgent Rule 匹配时执行
   `mark_assignment_message_urgent -> page_assignment x 3 -> escalate_assignment_to_leader`。
7. non-urgent 工单只发送一张普通飞书卡片。Priority/Severity 缺失或未知时按 non-urgent 处理。
8. urgent 工单先发送普通卡片，再对 Ticket Manager 自己发送的卡片做飞书 App 内加急；之后最多
   三次调用现有 Oncall `/api/v1/page`，每次调用后等待 ACK。
9. ACK 只包括：目标用户已读 Ticket Manager 本次发送的卡片，或 Jira status 已不在该项目的
   `ack_pending_statuses` 中。Oncall 自己发送的消息被读取不算 ACK。
10. 飞书卡片或 App urgent 失败不阻断电话通道；Oncall 无手机号、明确呼叫失败或 HTTP timeout
    均消耗当前 attempt，并进入本 attempt 的 ACK wait。
11. 最后一次等待仍未 ACK 时，可把当前角色更新为其直接 Leader。自动更新到 Leader 后，Leader
    仍会收到卡片并最多被 page 三次，但不能再自动升级到 Grand Leader。
12. v1 保持同步 wait，接受 urgent Execution 占用现有 10 个 execution worker 之一；部署保持单副本，
    不在本期建设通用 scheduler、跨副本 lease/fencing 或通用失败重试。
13. 语义是 latest-state/coalescing：保证 Jira 最新负责人得到处理，不保证逐一回放快速发生的
    `A -> B -> A` 等所有中间事件。

## 2. 端到端流程

```mermaid
sequenceDiagram
    participant Jira as "Jira Automation"
    participant Trigger as "Dispatch Trigger"
    participant Exec as "Dispatch Execution"
    participant Lark as "Feishu/Lark"
    participant Oncall as "Oncall"
    participant Core as "Core User"

    Jira->>Jira: "add role pending Label"
    Jira->>Trigger: "POST reserved source + issue_key"
    Trigger->>Jira: "load latest Issue"
    Trigger->>Exec: "match source/project/Label Policy"
    Exec->>Lark: "send ordinary assignment card"
    Exec->>Jira: "guarded consume role Label"

    alt "Rule 2 does not match urgent"
        Exec-->>Exec: "complete"
    else "Rule 2 matches urgent"
        Exec->>Jira: "fresh target/urgency/status guard"
        Exec->>Lark: "mark this card App urgent"
        loop "attempt 1..3"
            Exec->>Jira: "fresh guard and ACK check"
            Exec->>Lark: "check this card's read users"
            alt "ACK or workflow invalidated"
                Exec-->>Exec: "terminate remaining actions"
            else "not ACK"
                Exec->>Oncall: "POST /api/v1/page"
                loop "until this attempt's absolute deadline"
                    Exec->>Jira: "poll target/urgency/status"
                    Exec->>Lark: "poll this card's read users"
                end
            end
        end
        Exec->>Core: "resolve direct Leader"
        Exec->>Jira: "guarded update current role"
        Exec->>Trigger: "ensure same-role Label + Trigger"
        Note over Exec,Trigger: "Leader gets a new workflow, but no Grand Leader escalation"
    end
```

Policy 仍然是必需的。Trigger 只负责持久化一次 wake-up、读取 Jira 最新状态并寻找 active Policy；
Policy 才定义项目范围、角色、urgent 判断、ACK 状态、等待时间、page 次数和 Leader escalation。
Automation 不传易变化的 `policy_id`，因此 Policy replacement 不需要同步修改 Jira Automation。

## 3. Jira Automation 与 Trigger 路由

### 3.1 Automation 监听范围

| 场景 | 项目 | 动作 |
| --- | --- | --- |
| Assignee 变为非空值 | 全部十个项目 | 添加 `notify-assignee`，调用 source `jira_assignment_assignee` |
| L2 Assignee 变为非空值 | TCOC、GTOC、TCT | 添加 `notify-l2-assignee`，调用 source `jira_assignment_l2_assignee` |
| non-P0/P1 -> P0/P1 | TCOC、GTOC、TCT | 对非空 Assignee 执行 Assignee 动作；L2 非空时再执行 L2 动作 |
| non-S1/S2 -> S1/S2 | TCID、ST、APID、JPID、EMID、NAID、TCB | 只执行 Assignee 动作 |

Automation 明确不监听或不重复触发：

- `issue_created`；
- 创建时已经设置的 Assignee；
- P1 -> P0；
- S2 -> S1；
- urgent -> non-urgent。

urgent 降级不创建新 Execution；正在运行的 urgent Action 会通过 fresh guard 终止。non-urgent -> urgent
必须触发新的 Execution，因为已完成的普通通知不会重新评估 Rule。

每个角色通道的 Automation 动作顺序为：

1. 添加该角色 Label；
2. 调用对应 reserved source 的标准 Dispatch Trigger；
3. API 非 2xx 时由 Jira Automation 告警。

Automation 不能在调用 API 后删除 Label。Trigger 是异步处理，Label 只能由
`send_assignment_notification` 在卡片 attempt 被 checkpoint 后消费。

Dispatch 自动升级 Leader 后会主动 ensure 同角色 Label 和 Trigger，不能完全依赖 Jira 是否对 bot
更新产生 Automation 事件；Automation 产生的重复 Trigger 按 latest-state 语义合并。

### 3.2 Trigger 请求

Assignee 示例：

```http
POST /api/v1/ticketmanager/dispatch/triggers
Content-Type: application/json
X-API-KEY: <jira-automation-api-key>

{
  "source": "jira_assignment_assignee",
  "input": {
    "issue_key": "TCID-12345"
  }
}
```

L2 Assignee 只替换 source：

```json
{
  "source": "jira_assignment_l2_assignee",
  "input": {
    "issue_key": "TCOC-12345"
  }
}
```

请求不需要 `delivery_id`、changelog、role、expected target、priority、attempt 或 retry count。reserved
source 请求也不允许指定 `policy_id`，避免绕过正常 Policy 路由。现有 Trigger ID 继续承担接收确认和
运维查询用途。

当前 handler 位于内部 `/api/v1/ticketmanager`。如果 Jira Automation 不能访问内部 ingress，只在
gateway 为同一个 handler 增加受保护的 public alias；这不是新的 assignment event API。入口使用专用
token、来源限制和速率限制，日志不得记录 token。

### 3.3 `source_in` 语义

`PolicyTrigger` 增加：

```go
SourceIn []string `json:"source_in,omitempty"`
```

matcher 需要同时接收 Trigger source 和 Jira Issue。匹配规则为：

1. `source_in` 非空时，Trigger source 必须精确包含在列表中；
2. 对普通 source，空 `source_in` 继续保持现有 wildcard 行为，避免破坏已有 Policy；
3. 对 `jira_assignment_assignee`、`jira_assignment_l2_assignee`，空 `source_in` **不匹配**；
4. reserved source 只能进入显式声明相同 source 的 assignment-notification Policy。

这条特殊约束同时放在 matcher 测试和 Policy validator 中。它消除了 Label-only 方案中 wildcard Policy
误匹配、Assignee/L2 跨角色 fan-out 和全局 Label overlap audit。

### 3.4 Label pending marker 与 live conflict

Label 表达“这个角色的 Jira 最新状态仍待处理”，不是事件 ID、attempt 计数或 urgency。不会新增
`notify-assignee-once/twice` 等 Label。

assignment-notification 的 live conflict 按 `(issue_key, policy_family_id)` 判断，而不是
`(issue_key, policy_id)`。Policy replacement 后，旧版本 Execution 尚在等待时，新版本不能并行创建
第二条同角色 workflow。

reserved source 遇到 live Execution 时采用固定 defer 语义，不增加可配置的
`live_conflict_strategy`：

1. 角色 Label 仍存在时，把 Trigger 恢复为 `pending`，并用内部固定短间隔更新 `available_at`；
2. 旧 Execution 结束后，deferred Trigger 重新读取 Jira 最新状态；
3. Label 已被消费且 target/urgency 没有新变化时，重复 Trigger completed/no-op；
4. 多个同 source、同 Issue 的 pending Trigger 可以合并，但必须至少保留一个 wake-up 直到 Label 被
   正确消费。

同一个 Trigger 只处理一个角色 source，因此不需要跨角色 partial-resolution 或在 output 中反复 union
Assignee/L2 Execution ID。

## 4. JSM 项目事实与 Jira Issue 模型

以下显示值已于 2026-08-13 通过生产 Jira 只读 API 核对。Policy 直接使用 Jira IssueLoader 已有的
Priority、Severity 和 Status 文字字段，避免 sandbox、生产环境的 Jira ID 不一致。

| 项目 | urgent 字段 | urgent values | ACK pending statuses | L2 通知 |
| --- | --- | --- | --- | --- |
| TCOC、GTOC、TCT | `issue.priority` | `P0`、`P1` | Assignee：`Todo`、`ESCALATE TO L3`；L2 Assignee：`Todo` | 是 |
| TCID、ST、APID、JPID、EMID、NAID、TCB | `issue.custom_fields.severity` | `S1`、`S2`、`S1 - Service Unavailable`、`S2 - Significant Impact` | `WAITING FOR FIRST RESPONSE` | 否 |

Severity 原始字段是 `customfield_10076`。四个文字值同时存在是因为 JSM 中有两代 S1/S2 option；
Policy 必须同时包含四个值。

生产 L2 Assignee 字段是 `customfield_10053`，类型为 single user picker。Jira 返回值至少需要解析：

```json
{
  "accountId": "<atlassian-account-id>",
  "accountType": "atlassian",
  "active": true,
  "displayName": "<display-name>",
  "emailAddress": "<email>"
}
```

该字段在 Jira 层面是全局字段，并不代表所有项目都应启用 L2 通知。当前 L2 Policy 的
`project_key_in` 只配置 TCOC/GTOC/TCT；七个 Severity 项目不配置 L2 Policy。项目范围由 versioned
Policy 承载，service validator 不维护第二份项目白名单。sandbox 的 L2 field ID 仍需在上线该环境前核对。

需要扩展共享 Jira entity 和 IssueLoader：

- `JiraUser.AccountID`；
- `JiraUser.Active`；
- `JiraIssue.L2Assignee *JiraUser`；

`JiraIssue.Priority`、`JiraIssue.Status`、`JiraIssueCustomFields.Severity` 以及 Rule evaluator 的
`issue.priority`、`issue.custom_fields.severity` 均为已有能力，不新增 ID 版本字段。

target 稳定比较使用 Jira `accountId`，Core User、Lark 和 Oncall 身份解析使用规范化 email。目标飞书
`open_id` 优先从 Core User 读取；Core User 不可用或未返回 `open_id` 时，通过飞书通讯录按 email
解析。Assignee、
L2、Priority、Status 和项目对应的 Severity field 必须包含在 Trigger load、RunExecution load 以及 urgent
Action fresh load 中。

## 5. Policy 与 Rule

### 5.1 三个 Policy family

| Policy family | `source_in` | `project_key_in` | `issue_label_all` | role |
| --- | --- | --- | --- | --- |
| Priority Assignee | `jira_assignment_assignee` | TCOC、GTOC、TCT | `notify-assignee` | `assignee` |
| Priority L2 | `jira_assignment_l2_assignee` | TCOC、GTOC、TCT | `notify-l2-assignee` | `l2_assignee` |
| Severity Assignee | `jira_assignment_assignee` | TCID、ST、APID、JPID、EMID、NAID、TCB | `notify-assignee` | `assignee` |

三个 family 的 source/project 组合互斥。replacement Policy 复用同一个 `family_id`，从而让 live conflict
跨 Policy version 生效。示例创建请求不手工指定 `family_id`；由现有 Policy create 流程生成符合
`varchar(20)` 约束的 ID，后续 replacement 继承它。

### 5.2 固定的两条 Rule

以 Severity Assignee 为完整示例：

```json
{
  "name": "Assignment Notification - Severity Assignee",
  "trigger": {
    "enable": true,
    "source_in": ["jira_assignment_assignee"],
    "project_key_in": ["TCID", "ST", "APID", "JPID", "EMID", "NAID", "TCB"],
    "issue_label_all": ["notify-assignee"]
  },
  "rules": [
    {
      "name": "send-assignment-notification",
      "priority": 10,
      "when": {"always": true},
      "then": [
        {
          "type": "send_assignment_notification",
          "send_assignment_notification": {
            "role": "assignee",
            "label": "notify-assignee"
          }
        }
      ],
      "transition": {"on_success": "continue"}
    },
    {
      "name": "urgent-assignment-notification",
      "priority": 20,
      "when": {
        "field": "issue.custom_fields.severity",
        "operator": "in",
        "values": ["S1", "S2", "S1 - Service Unavailable", "S2 - Significant Impact"]
      },
      "then": [
        {"type": "mark_assignment_message_urgent"},
        {
          "type": "page_assignment",
          "page_assignment": {
            "attempt": 1,
            "ack_pending_statuses": ["WAITING FOR FIRST RESPONSE"],
            "ack_timeout": "5m",
            "poll_interval": "5s"
          }
        },
        {
          "type": "page_assignment",
          "page_assignment": {
            "attempt": 2,
            "ack_pending_statuses": ["WAITING FOR FIRST RESPONSE"],
            "ack_timeout": "5m",
            "poll_interval": "5s"
          }
        },
        {
          "type": "page_assignment",
          "page_assignment": {
            "attempt": 3,
            "ack_pending_statuses": ["WAITING FOR FIRST RESPONSE"],
            "ack_timeout": "5m",
            "poll_interval": "5s"
          }
        },
        {"type": "escalate_assignment_to_leader"}
      ],
      "transition": {"on_success": "terminate"}
    }
  ]
}
```

`5m/5s` 是初始 Policy 值，之后可以通过 replacement Policy 调整。Priority Assignee 和 Priority L2
只需要替换以下配置：

| 项 | Priority Assignee | Priority L2 |
| --- | --- | --- |
| source | `jira_assignment_assignee` | `jira_assignment_l2_assignee` |
| role | `assignee` | `l2_assignee` |
| label | `notify-assignee` | `notify-l2-assignee` |
| Rule 2 field | `issue.priority` | `issue.priority` |
| Rule 2 values | `P0`, `P1` | `P0`, `P1` |
| page statuses | `Todo`, `ESCALATE TO L3` | `Todo` |

Rule 1 必须 `continue`。Rule 2 不匹配时，它的 Action 被标记为 skipped；引擎找不到下一条 Rule 后，
Execution 自然成功结束。Rule 2 匹配后显式 `terminate`。

### 5.3 Rule snapshot 语义

正常连续运行时，Rule 1 和 Rule 2 使用同一次 `RunExecution` 开始时加载的 Jira snapshot。
`send_assignment_notification` 不在 prepare 与 send 之间再次 refresh；prepare 的价值是把 target、role、
项目、Priority/Severity、Status 和卡片计划先持久化，形成外部副作用前的恢复锚点。
卡片发送后的 Label guard 会做独立的 live read，但不能用该结果覆盖 evaluator 使用的 Rule snapshot。

如果 Rule 1 已完成、Rule 2 尚未评估时服务重启，`RunExecution` 会重新加载 Jira，Rule 2 使用新的
snapshot。Rule 条件一旦评估并持久化，就不再反复评估：

- urgent 降级、target 变化、status ACK 由后续 Action 的 fresh guard 处理；
- non-urgent -> urgent 由 Jira Automation 新建 wake-up；
- Priority/Severity 缺失或未知时 `in` 不匹配，只发送普通卡片。

### 5.4 Policy validator

assignment-notification Policy 发布时必须校验：

- reserved source、role、Label 必须是匹配的角色组合，`project_key_in` 必须非空；
- 项目范围以及项目使用 Priority 还是 Severity 完全由 versioned Policy 配置，validator 不维护项目白名单；
- 恰好两条 Rule，priority 顺序确定；
- Rule 1 是 `always`，只包含一个 `send_assignment_notification`，并且 `on_success=continue`；
- Rule 2 只使用 `issue.priority` 或 `issue.custom_fields.severity` 和 `in`，values 必须非空；JSM 显示值由
  versioned Policy 承载，不在 validator 代码中再硬编码一份；
- Rule 2 的 Action 数量、顺序和 `on_success` 由 versioned Policy 定义，validator 不要求固定为 urgent、
  三次 page、escalate 或 terminate；
- 每个 page Action 独立校验：attempt 为正数，`ack_pending_statuses` 非空且不重复，timeout/poll 为正数且
  `poll_interval <= ack_timeout`；不同 page Action 不要求连续 attempt 或相同 ACK 参数；
- 不允许配置 `terminal_statuses`、Action 级 urgency 列表、`max_hops` 或 retry Label。

## 6. Action 与 ACK 语义

### 6.1 为什么仍需 Action 控制流改造

拆成两条 Rule 和多个 Action 可以获得自然的持久化边界，但不能替代 Action 控制流。任意
`page_assignment` 都可能发现 ACK、改派或 urgency 降级，此时必须立刻跳过后续 page 和 escalation。

Executor 的 `Execute` 因此要从“只返回 error”扩展为：

```go
type ActionResult string

const (
    ActionResultContinue  ActionResult = "continue"
    ActionResultTerminate ActionResult = "terminate"
)
```

`terminate` 会把尚未执行的 Action 标记为 skipped，并成功完成当前 Execution；真正的技术错误仍使
Execution 进入 failed。

### 6.2 `send_assignment_notification`

这个 Action 合并原先的 prepare、普通卡片发送和 Label 消费：

1. `Plan` 从当前 `RunExecution` snapshot 固化 role、target account ID/email、项目、Priority/Severity、
   Status 和 Label；Plan 在任何外部调用前持久化。
2. target 为空、inactive、没有稳定 Jira account ID 或没有可供后续通道使用的 email 时，记录 warning，
   guarded consume Label，并 terminate。只有 Lark 身份解析或发卡失败时仍按 card failure 继续 urgent
   电话通道。
3. 发送一张普通 assignment card；成功后保存 Ticket Manager card message ID 和目标 Lark user ID。
4. 卡片确认发送成功后，在 Jira issue 尝试写一条 `[ticket-manager]` internal comment，记录目标并标记
   `Urgent: no`；只有实际成功受理的电话 page attempt 才记录 `Urgent: yes`。comment attempt 结果
   持久化后继续消费 Label。卡片失败或结果未知时不写“已发送” comment。
5. 卡片失败或结果未知时保存 `card_status=failed|unknown` 和 warning，不返回技术 error；urgent Rule
   仍可继续电话通道。
6. 卡片 attempt 和 internal comment attempt 结果完成 checkpoint 后，才消费本角色 Label。

Action context 至少保存：

- snapshot target/role/project/urgency/status；
- `card_phase=prepared|started|finished`；
- `card_status=sent|failed|unknown`；
- `message_id`、目标 Lark ID；
- `notification_comment_phase=started|finished`、`notification_comment_status=written|failed|unknown`；
- `label_phase=pending|remove_started|removed|restored`；
- warning/error 摘要，不保存 token 或手机号。

Label 消费必须防止旧 Execution 删除新改派或新 urgency 刚写入的同名 Label：

1. 删除前 fresh-read 同角色 target；target 已变化时不删除 Label，并 terminate 旧 Execution；
2. 删除后再读取 target 和 urgency classification；如果 target 或 non-urgent/urgent 相对 Plan snapshot 已
   变化，补回同角色 Label并 ensure 对应 reserved-source Trigger；
3. pending/processing Trigger 本身不是“发生了新事件”的证据，不能仅因为存在 deferred duplicate 就
   补回 Label；否则一次重复 Automation 会被放大为第二次有效通知；
4. 补偿操作允许重复 wake-up，但只能由 post-delete 观察到的 target/urgency 变化触发，不能漏掉最新
   pending marker，也不能把旧的重复 Trigger 变成新业务事件。

这仍是 latest-state 保护，不承诺恢复 target 最终又回到原值的每个中间事件。

### 6.3 `mark_assignment_message_urgent`

该 Action 从 Rule 1 已持久化的 context 读取 expected target 和 message ID：

- 先 fresh-read Jira，并在存在 message ID 时检查 read users；target 变化、urgent 已降级或任一 ACK
  已成立时返回 terminate；
- 没有 message ID 时在 Action context 中记录 `urgent_status=skipped` 和 warning，返回 continue；
- 只对 Ticket Manager 自己发送的该 message ID 做 App urgent；
- App urgent 失败或结果未知时记录 warning，不阻断 page；
- 调用前保存 `started`，恢复时不重复一个结果未知的 App urgent。

### 6.4 `page_assignment`

一个 `page_assignment` 固定表示“一次 Oncall `/api/v1/page` + 一段 ACK wait”。三个 attempt 是三个
独立 Action，因此每次超时后会先持久化成功边界，再进入下一次。

每个 Action 的顺序：

1. 从 Rule 1 context 取得 expected target；
2. fresh-read Jira 并检查 target、urgent、Status；有 message ID 时再检查该卡片 read users；
3. 已 ACK 或 workflow invalidated 时返回 terminate，不调用 Oncall；
4. 保存 `page_phase=started` 和本 attempt 的绝对 wait deadline；
5. 调用现有 Oncall `POST /api/v1/page`；Oncall 自己额外发送一条不同内容的飞书消息是已接受行为；
6. 保存 Oncall response 摘要；Page 明确受理成功时，为本 attempt 写一条独立的 `[ticket-manager]`
   internal comment，标记 `Urgent: yes` 和 attempt number；comment 写入失败仅记录
   warning，不阻止 ACK wait；
7. 按 `poll_interval` 等待到绝对 deadline；
8. 每轮 poll 重新执行 Jira guard 和本卡片 read ACK；ACK/invalidated 返回 terminate，超时返回
   continue，进入下一个 attempt 或 escalation。

每个 `page_assignment` 有独立的 `notification_comment_phase/status`。因此 attempt 1 超时后，attempt 2
电话请求成功会再写一条 notification comment；attempt 3 同理。

以下结果都算当前 attempt 已消耗，并继续本次 ACK wait：

- 没有手机号；
- `call_error` 非空；
- HTTP timeout；
- Oncall 已接收请求但返回明确失败。

page 调用前先保存 `started`。如果外部调用后、结果 checkpoint 前崩溃，恢复时不再调用同一 attempt，
而是直接进入它原有 deadline 的 wait。这选择 at-most-once，避免服务重启导致重复电话；代价是结果
未知时可能少打一通。

fresh guard 与外部电话之间仍有极小竞态，可能给刚被换掉的旧 target 多打一通，v1 接受该风险。

### 6.5 ACK 和 invalidation

ACK 条件：

1. expected target 的 Lark user ID 出现在 Ticket Manager 本次 card message ID 的 read users 中；或
2. Jira `status` 非空，且不在本 page Action 的 `ack_pending_statuses` 中。

`terminal_statuses` 不需要存在。这里关心的是“是否离开等待首次处理的状态”，例如进入处理中就已经
算 ACK，不要求工单关闭。Status 缺失不算 ACK。

以下情况不是 ACK，但会 invalidated 并结束旧 workflow：

- 当前角色 target 为空或 account ID 不再等于 expected target；
- TCOC/GTOC/TCT 当前 Priority 不再是 P0/P1；
- 七个 Severity 项目当前 Severity 不再是四个 urgent value 之一；
- 项目或角色不再属于当前 Policy 的业务白名单。

Oncall 自己的消息、其他历史 assignment card、其他用户读取本卡片，都不能满足 read ACK。

任一 ACK guard 首次确认上述 ACK 后，必须在 Jira issue 写一条 `[ticket-manager]` internal comment，只记录
assignment notification 已 ACK 和目标，不展示 role、channel 或 read/status 的具体检测原因。comment 写入前保存
`ack_comment_phase=started`，成功后
保存 `ack_comment_phase=finished`、`ack_comment_status=written`；这些字段保存在实际发现 ACK 的 urgent、
page 或 escalation Action context 中。invalidation 不写 ACK comment。Jira 明确返回 comment 写入失败时按
best-effort 失败处理：保存 `ack_comment_status=failed` 和 warning，但不阻止 ACK 控制流正常 terminate。

## 7. Leader escalation：最多一跳

### 7.1 Leader 来源和 Jira 更新

`escalate_assignment_to_leader` 在第三次 wait 超时后：

1. fresh-check expected target、urgent、status 和本卡片 read ACK；
2. 查询 Core User `GetUserLeader(expectedTarget.Email)`，得到直接 Leader；
3. 校验 Leader 存在、active、不是本人，并把 Leader email 精确解析为 Jira Atlassian account ID；
4. 在 Dispatch Context 中先持久化 escalation intent；
5. 再次 fresh-read Jira，确认当前角色仍是 expected target；
6. 更新 Assignee，或使用 `CustomFieldSet.L2Assignee` 更新 L2 single-user picker；
7. reconcile Jira 当前值，标记 intent applied，并设置 one-hop marker；
8. 添加同角色 pending Label，并主动创建相同 reserved-source Trigger。

Leader 为空、inactive、本人或无法映射 Jira account 时，不更新 Jira，记录告警并正常结束自动流程。

当前 `JiraClient.UpdateAssignee` 和 L2 raw update 都是无条件写，因此仍需明确接受“最后一次
fresh-read 后人工改派仍可能被覆盖”的残余风险。单纯把 fresh-read 放在 update 前不能完全消除该竞态；
是否启用 Leader escalation 由 Policy 是否包含 `escalate_assignment_to_leader` Action 决定。

### 7.2 最小持久化状态

不再维护 `lineage_id`、`active|closed`、`hops`、`closed_same_target` 或 closed tombstone。
`dispatch_contexts` 只新增一个 JSON 列：

```sql
ALTER TABLE dispatch_contexts
    ADD COLUMN assignment_notification_state_json JSON NULL;
```

每个角色有独立 slot：

```json
{
  "assignee": {
    "escalation_intent": {
      "owner_execution_id": 123,
      "from_account_id": "jira-account-A",
      "to_account_id": "jira-account-B",
      "to_email": "leader@example.com",
      "state": "prepared"
    },
    "auto_escalated_to_account_id": null
  },
  "l2_assignee": null
}
```

Jira 更新成功并 reconcile 后，intent 变为 `applied`，同时
`auto_escalated_to_account_id="jira-account-B"`。

更新 JSON 使用短事务和 `SELECT ... FOR UPDATE`：只修改一个 role slot，外部 Core/Jira 调用不能放在
数据库事务内。这样单副本内 Assignee 和 L2 并发更新不会互相覆盖，也不需要额外 version 列。历史
`NULL` 等价于空对象，无需 backfill；Context 行不存在时先用 insert-on-conflict 方式创建空行，再加锁
修改对应 slot。

普通 Dispatch Context update 必须继续按明确列更新，不能整行 replace 该 JSON。Context 中任一 role
仍有 intent/marker 时，`DeleteDispatchContext` 必须拒绝物理删除；两个 slot 都清空后才可删除。

### 7.3 crash reconcile 与 no-Grand-Leader

intent 必须在 Jira write 前持久化。恢复时只有 `owner_execution_id` 对应的 Action 可以 reconcile：

- Jira current target 等于 `to_account_id`：视为 write 已应用，补齐 `applied` 和 marker；
- Jira current target 仍等于 `from_account_id`：重新执行 guarded update；
- Jira current target 是其他人：将本 intent 清理为 superseded，不覆盖人工改派。

这里的 owner 限制只约束“再次向 Jira 写负责人”。任何后续 workflow 的 guard 只要观察到同 role 的
`prepared|applied` intent 且 current target 等于 `intent.to_account_id`，都必须先幂等补齐 applied
marker；`escalate_assignment_to_leader` 遇到同 role 未决 intent 时一律 fail-closed/block，不得继续向上
查询 Leader。这样即使 A -> B 的 Jira write 成功后，A Execution 因 marker checkpoint 失败而成为
terminal failed，B 也不会被错误升级到 C。

Leader B 的新 Execution 看到 current target 等于
`auto_escalated_to_account_id=B` 时，仍完整执行普通卡片、App urgent 和最多三次 page；只有最后的
`escalate_assignment_to_leader` 返回 `escalation_blocked`，不会查询或更新 B 的 Leader C。

只要 target 仍是 B，后续新的 non-urgent -> urgent 事件也可以再次通知/page B，但仍不能 B -> C。
当 `send_assignment_notification` 或 escalation guard 明确看到 target 已变为不同的 D 时，短事务清除
B 的 applied intent 和 marker；D 的新 workflow 重新获得一次 D -> direct leader 的自动升级机会。

## 8. 持久化、恢复与失败语义

### 8.1 数据库存储清单

本功能只有一个物理 schema 变更，即
`dispatch_contexts.assignment_notification_state_json`。其他数据复用既有 JSON 或列：

| 数据 | 保存位置 | schema 变更 |
| --- | --- | --- |
| `source_in`、项目、Label | `dispatch_policies.trigger_json` | 否 |
| urgent 文字条件 | `dispatch_rules.condition_json` | 否 |
| attempt、ACK statuses、timeout、poll | `dispatch_rules.actions_json` | 否 |
| 冻结后的 Action 参数、target、message ID、phase、deadline、结果 | `dispatch_rule_executions.action_executions_json` | 否 |
| Trigger defer 状态和时间 | `dispatch_triggers.status/available_at` | 否 |
| Policy replacement 冲突范围 | 现有 `dispatch_policies.family_id` | 否 |
| one-hop intent/marker | `dispatch_contexts.assignment_notification_state_json` | **是** |

不新增 Policy 配置列、Execution 配置列、notification 表或 retry 表。

Execution 创建时，`BuildContext` 必须把每个 Action 的静态配置复制进
`action_executions_json`。因此 replacement Policy 发布后，在途 Execution 继续使用创建时冻结的
`ack_pending_statuses/timeout/poll/attempt`，不在执行中读取新的业务配置。

### 8.2 Action 恢复矩阵

| 中断点 | 恢复行为 |
| --- | --- |
| card 尚未 started | 执行一次 card attempt |
| card started，结果未保存 | 不自动重发；记 unknown/warning，继续 Label consume 和 urgent phone 通道 |
| card 结果已保存 | 跳过 card；`sent` 时先补 notification comment，再补齐 Label consume |
| Label remove 后未完成 post-check | 重做 post-check；必要时补 Label + reserved Trigger |
| App urgent started，结果未知 | 不重复 urgent；记 warning，继续 page |
| page attempt started，结果未知 | 不重复本 attempt；使用原绝对 deadline 进入 wait |
| page wait 中 | 按原绝对 deadline 继续，不重新开始完整 timeout |
| notification/ACK comment started，结果未保存 | 不重复写 comment；记 `unknown`/warning，并继续既定控制流 |
| ACK/invalidated 已保存 terminate | 后续 page/escalation 保持 skipped |
| escalation intent prepared，Jira 结果未知 | 按 Jira current target reconcile，不重新选择另一个 Leader |
| Jira 已是 Leader，marker 尚未保存 | 补齐 applied + marker + same-role wake-up |

服务正常 shutdown 的 `context.Canceled` 不应把 Action 标记为业务 failed；保留 executing checkpoint，
由下次启动恢复。

### 8.3 failed 是终态

现有 Trigger 轮询只恢复 `pending/processing` 是正确行为；`failed` 不应自动重试。本期不增加通用
retry/backoff、dead-letter 自动重放或 dependency grace period。

需要明确当前限制：

- `processDispatchTrigger` 返回错误后 Trigger 进入 failed，只告警并由人工重新调用相同 reserved source；
- Execution Action 返回真正的技术错误后进入 failed，不自动恢复；
- `RunExecution` 在进入 Behavior 前读取 Jira/Policy/Context 失败时，Execution 仍可能保持
  `created/executing`，当前通常要等服务重启才重新入队；本期接受，不扩建 Execution poller；
- card/App urgent/Oncall 的已定义预期失败不能作为技术 error 返回，而要按本设计记录 warning 并继续。

文中的 `completed-with-warning` 是 metrics/log 和 Action context 的业务 outcome，不新增 Execution 状态；
该 Execution 的持久化终态仍是 `succeeded`。同理，当前 Action 因缺少 message ID 而跳过 App urgent 时，
可以把业务 outcome 记为 `skipped`，但不把整个 Execution 置为 failed。

人工重放规则：Label 尚未消费时，只需重新调用相同 reserved source；Label 已消费时，必须先补回对应
角色 Label，再调用 reserved source。重放采用最新 Jira 状态，可能产生一轮新通知。

## 9. 配置归属

Jira 业务值不进入全局 YAML 或环境变量：

- source、project、Label：`dispatch_policies.trigger_json`；
- Priority 或 Severity 的选择：urgent Rule 的 `dispatch_rules.condition_json.field`；
- P0/P1 或 S1/S2 显示值：urgent Rule 的 `dispatch_rules.condition_json`；
- `ack_pending_statuses`、`ack_timeout`、`poll_interval`、attempt：三个
  `page_assignment` 的 `dispatch_rules.actions_json`；
- in-flight frozen value：`dispatch_rule_executions.action_executions_json`。

`ack_pending_statuses` 在三个 page Action 中重复是有意设计。它让每个 Action 自包含并跟随 Policy
version；validator 保证三份完全一致。不会再增加 policy-level `urgent:`、`terminal_statuses` 或单独的
project mapping YAML。

Service config/Secret 只保存运行依赖：

- Lark、Jira、Core User、Oncall endpoint/credential；
- Lark 应用身份必须开通 `im:message.urgent` 或 `im:message.urgent:app_send`，并具备按 email 解析
  `open_id` 的通讯录权限。

Assignment notification 和 Leader escalation 不设置独立 Service feature flag。是否运行完全由 Policy 的
发布状态、Trigger 和 Action 编排决定；需要关闭时修改或停用对应 Policy。

业务显示值变更通过 replacement Policy 发布；在途 Execution 不受影响。

## 10. 边界条件

| 场景 | 行为 |
| --- | --- |
| Issue create 自带 Assignee | 忽略，不通知 |
| target 被清空 | 旧 workflow fresh guard 后 terminate；不 page |
| Assignee 与 L2 同时变化 | Jira 分别添加两个 Label、发送两个 source；两条独立 workflow |
| 两个角色 Label 同时存在 | source 只匹配自己的角色，不跨角色 fan-out |
| 重复 Automation/Trigger | Label 已消费且无新状态时 no-op；Label 仍在时至少保留一个 deferred wake-up |
| 普通通知过程中 Priority/Severity 进入 urgent | urgency Automation 产生新 Trigger；Label 消费补偿不得吞掉新 marker |
| Rule 2 条件字段缺失/未知 | 只发普通卡片 |
| 初始 status 已不在 pending statuses | 仍发送 assignment card；urgent Action fresh-check 后 terminate，不 page |
| 通知卡片确认发送成功 | 写一条 Jira internal comment，记录目标并标记 `Urgent: no`；写入失败只记录 warning，仍消费 Label |
| 第一次 ACK wait 超时后发送第二次电话通知 | attempt 2 单独写一条 notification internal comment |
| ACK guard 确认 read/status ACK | 写一条 Jira internal comment；写入失败只记录 warning，仍正常 terminate；invalidation 不写 |
| 卡片发送失败 | 消费 Label并记录 warning；normal 完成，urgent 继续 page |
| App urgent 失败 | 记录 warning，继续 page |
| target 只读了 Oncall 消息 | 不算 ACK |
| Oncall 无手机号/call_error/timeout | 消耗 attempt，继续该 attempt 的 wait |
| page 后 Jira/Lark fresh read 技术失败 | Execution failed 并告警；v1 不做无限等待 |
| wait 期间 target 变化 | 旧 workflow terminate；新 source Trigger 通知最新 target |
| wait 期间 urgency 降级 | terminate，不再 page/escalate |
| fresh guard 后 target 立刻变化 | 可能给旧人多打一通，v1 接受 |
| 三次 wait 都超时 | 只尝试 direct Leader；绝不发第四次 page |
| Leader 为空/inactive/self/无法映射 Jira | 不改 Jira，告警并结束 |
| 已自动升级到 Leader，Leader 未 ACK | Leader 仍完成三次 page，随后结束；不升级 Grand Leader |
| Leader 仍是 current target，之后再次进入 urgent | 可以重新通知/page，但 marker 继续禁止向上升级 |
| 后续明确人工改派到其他人 | 清旧 marker；新 target 可以有一次升级 |
| Policy replacement | 新 Trigger 用新版本；旧 Execution 用 frozen context；family 级防并行 |
| 服务重启 | 恢复 created/executing Execution，并按 phase checkpoint 继续 |

## 11. 单副本与容量

v1 的 wait 会同步占用 execution worker。现有并发度为 10；基于 P0/P1、S1/S2 工单量较低且通常会
快速 ACK 的假设，接受这一实现。

部署约束：

```yaml
spec:
  replicas: 1
  strategy:
    type: Recreate
```

不得配置 HPA。默认 RollingUpdate 可能短暂产生两个 Pod，所以必须显式使用 `Recreate`。现有 Trigger
有 DB claim，但 Execution dedupe 和 worker 协调仍主要是进程内语义，不能声称本方案支持多副本。

上线后重点观察 active urgent Execution、busy workers、execution queue age 和 deferred Trigger age。
若 worker 长期达到 8/10，或 queue age 接近单次 wait timeout，再把 wait 改成 durable defer；不在 v1
预先实现。

## 12. 可观测性与运维

本期实现以下 Prometheus 指标：

- `ticket_manager_dispatch_assignment_notification_events_total{event}`：从终态 Execution 已持久化的
  Action Context 计数，`event` 包含 `card_failed`、`urgent_failed`、`page_failed`、`ack_timeout` 和
  `leader_escalated`；同一次 page 调用失败后继续等待并超时时，同时累计 `page_failed` 和
  `ack_timeout`；
- `ticket_manager_dispatch_assignment_action_total{action,result,role,attempt}`：记录 card、App urgent、
  page 和 Leader escalation 的完整结果，`result` 为 `succeeded/failed/unknown/skipped`；只有 page
  使用 `attempt=1/2/3`，其他 Action 的 attempt 为空；
- `ticket_manager_dispatch_assignment_ack_total{outcome,role,attempt}` 和
  `ticket_manager_dispatch_assignment_ack_wait_seconds{outcome,role,attempt}`：ACK guard 的结果与耗时；
  attempt `0` 表示 App urgent 前、`1/2/3` 表示对应 page wait；outcome 为
  `message_read/status_changed/target_changed/urgency_downgraded/project_changed/timeout`；
- `ticket_manager_dispatch_assignment_workflow_duration_seconds{outcome,role}`：整个 notification Execution
  从创建到终态的耗时；outcome 为 `completed/acked/invalidated/unacknowledged/escalated/failed`；
- `ticket_manager_dispatch_created_executions{mode}` 和
  `ticket_manager_dispatch_oldest_created_execution_age_seconds{mode}`：已创建但尚未进入 worker 的
  Execution 数量和最长排队时间；
- `ticket_manager_dispatch_executing_executions{mode}`：当前处于 `executing` 的 Execution 数量；
- `ticket_manager_dispatch_oldest_executing_execution_age_seconds{mode}`：当前最老 `executing`
  Execution 的持续时间。持续时间从 Execution 进入 `executing` 时更新的 `updated_at` 起算；没有活动
  Execution 时为 `0`；
- `ticket_manager_dispatch_triggers{status}` 和
  `ticket_manager_dispatch_oldest_trigger_age_seconds{status}`：`pending/processing` Trigger 数量和最老
  Trigger 年龄，包括尚未到 `available_at` 的 deferred Trigger。

Action、ACK 和 workflow 指标仅统计 live Execution，并在 Execution 写入 `succeeded/failed` 终态后从
持久化 Action Context 统计一次，避免 dry-run 污染和服务重启恢复时重复累计。backlog 指标由每分钟运行的
`ObserveDispatchMetrics` 任务刷新，启动后首次采集延迟 1 秒；任一数据库查询失败时保留整份上一采样，
同时 Cron Job 自身的失败会进入现有 service metrics。长时间阈值不写入 Ticket Manager 配置，由监控规则
根据线上 Policy 的最大合理执行时间判断，例如：

```promql
ticket_manager_dispatch_oldest_executing_execution_age_seconds{mode="live"} > 1200
```

后续若需要按 project 或 ACK source 做容量分析，再增加细分指标；本期不把 project、issue key、execution
ID 放入 label，避免时序基数随工单量持续增长。

结构化日志包含 issue key、trigger/execution/policy ID、source、role、target account ID、attempt、deadline、
guard/ACK/termination reason 和 escalation from/to。日志不得包含 token、完整手机号或飞书/Jira credential。

以下情况告警：card/App urgent warning、Oncall 无手机号或 call error、Trigger/Execution failed、deferred
Trigger 超时、worker 饱和、Leader lookup/update 失败、检测到运行 Pod 数大于 1。

## 13. 测试方案

Go 单元测试使用标准 `testing`、`testify` 和 `go.uber.org/mock` 生成的 mock；互不依赖的用例调用
`t.Parallel()`。

### 13.1 Contract、migration 与 repository

- `source_in` JSON round-trip，普通 source wildcard 和 reserved source 特殊语义兼容；
- L2 user/account ID round-trip；
- 新 Action config/context 的 JSON round-trip；
- migration 对新库、旧行和重复运行都安全，历史 `NULL` 读为空状态；
- Assignee/L2 两个 goroutine 更新不同 role slot 不丢数据；
- 普通 Context partial update 不覆盖 notification JSON；
- 有 intent/marker 时 Delete 被拒绝，空 state 时可按现有语义删除。

### 13.2 matcher 与 Policy validator

- reserved source 不匹配 `source_in=[]` 的 wildcard Policy；
- exact source + project + Label 才匹配对应 family；
- 两个 Label 同时存在也不跨角色；
- 普通 source 的原有 wildcard 行为无回归；
- 三个合法 Policy template 可发布和 replacement；
- Severity L2、错误 project/source/role/Label 组合被拒绝；
- Rule 数量、顺序、transition、condition field/values、Action 顺序错误均被拒绝；
- page attempt 不是 1/2/3，或三份 ACK/timeout/poll 不一致时被拒绝；
- `terminal_statuses`、`max_hops`、Action urgency 列表被拒绝。

### 13.3 Jira Automation 与 Trigger

- 十项目 Assignee change、TCOC/GTOC/TCT L2 change 产生正确 source；
- non-P0/P1 -> P0/P1 同时覆盖 Assignee 和非空 L2；
- non-S1/S2 -> S1/S2 只覆盖 Assignee；
- P1 -> P0、S2 -> S1、urgent downgrade、issue created 不触发；
- reserved source 携带 `policy_id` 被拒绝；
- 同 family live Execution 时 Trigger defer，而不是静默 completed；
- deferred Trigger 在旧 Execution 结束后读取 latest target；
- 重复 Trigger/Automation 最终只产生必要 workflow；
- Priority/Severity 进入 urgent 与旧 Label consume 交错时，marker 会被保留或补回。

### 13.4 IssueLoader 与 evaluator

- 十项目 Priority/Status 文字正确解析；
- `customfield_10076` Severity 文字正确解析；
- `customfield_10053` L2 account ID/email 正确解析；
- L2 项目范围由 Policy 配置，不由字段是否存在或 service 代码白名单决定；
- Priority/Severity 缺失时 urgent Rule 不匹配；
- 每个 Policy 只读取其 urgent Rule 配置的 urgency field，卡片展示同一个字段。

### 13.5 Rule 与 normal workflow

- Rule 1 成功后继续评估 Rule 2；Rule 2 不匹配时 Execution succeeded；
- 连续运行时两条 Rule 使用同一 Jira snapshot；重启且 Rule 2 未评估时使用重载 snapshot；
- Rule 2 evaluation 已保存后不重新评估；
- normal 只发送一次普通卡片，不 urgent、不 page、不 escalate；
- card success 在 notification internal comment attempt checkpoint 后 guarded consume Label；card
  failure/unknown 在 card attempt checkpoint 后直接 guarded consume Label；
- notification comment 明确失败时记录 warning 并继续消费 Label；恢复时不重复结果未知的 comment；
- card failure 是 completed-with-warning，不把 normal Execution 置 failed；
- target change 和 urgency change 的 Label remove race 能补偿。

### 13.6 urgent、ACK 与 page

- card 成功后只 urgent 该 message ID；card 失败或 urgent 失败仍进入 page；
- expected target 读取本 card 算 ACK；别人读取、历史 card、Oncall 消息不算；
- Status 离开 pending statuses 算 ACK；Status 缺失不算；
- read/status ACK 在实际发现它的 urgent、page 或 escalation Action 中只写一次 internal comment；
- 每个成功受理的 phone page attempt 都写自己的 notification comment，attempt 1/2/3 不合并；
- target change、urgency downgrade、项目/角色失效返回 terminate；
- 初始已 ACK 时卡片后不 page；
- 每个 attempt 都是一次 page + wait，timeout 后顺序进入 2、3；
- 无手机号、call_error、HTTP timeout 消耗 attempt并 wait；
- 任意 attempt ACK 后剩余 Action 全部 skipped；
- 三次 timeout 后才进入 escalation，永远没有第四次 page。

### 13.7 fault injection 与重启

分别在以下 checkpoint 前后注入崩溃并重启：

- card started/result；
- Label remove/post-check/restore/ensure Trigger；
- App urgent started/result；
- page 1/2/3 started/result/deadline；
- notification/ACK internal comment started/result；
- wait 中途和 terminate 保存；
- escalation intent prepared、Jira update、applied marker、same-role Trigger。

断言已经确认或结果未知的 card/App urgent/page/internal comment 不被重复执行，wait 使用原绝对
deadline，Action 成功边界不会回退。

### 13.8 Leader one-hop

- A 三次 timeout 后只更新到直接 Leader B；
- B 的新 Execution 正常发 card/urgent/page 三次，但绝不 B -> C；
- B 后续再次进入 urgent 仍通知/page，但不 B -> C；
- 明确人工 B -> D 后清 marker，D 可进行一次 D -> Leader；
- empty/inactive/self/无法映射 Leader 均不写 Jira；
- prepared intent 恢复时 current=to/from/other 三种 reconcile 分支正确；
- Jira write 已成功但 owner Execution 在 marker checkpoint 失败时，Leader workflow 会补齐 marker，
  且绝不向 Grand Leader 写 Jira；
- 任意非 owner workflow 遇到同 role 未决 intent 时 fail-closed，不重复 Jira write、不继续升级；
- 并发 Assignee/L2 escalation 不互相覆盖；
- Action 主动 ensure Trigger 与 Jira Automation 重复 Trigger 不产生额外有效通知；
- conditional update 冲突时不覆盖人工改派。

### 13.9 集成与部署验收

- 使用 Jira/Lark/Oncall/Core User sandbox 完成 normal、urgent read ACK、status ACK、三次 timeout、Leader
  no-Grand-Leader 场景；
- 验证 Oncall 额外卡片内容与 Ticket Manager 卡片可同时接受；
- 验证 Automation token、source 限制和日志脱敏；
- 验证 `replicas=1`、`Recreate`、无 HPA；
- 压测多个同步 wait，验证 10 workers、队列和告警阈值；
- 验证 failed Trigger 不被 poller 恢复，pending/processing Trigger 能恢复；
- 验证运维手动补 Label + reserved Trigger 的重放手册。

## 14. 实施顺序

### Phase 1：contract 和 Jira 基础数据

1. 扩展共享 dispatch Action/Policy contract 和 `source_in`；
2. 扩展 Jira entity/IssueLoader 的 target identity 和 L2；
3. 增加 matcher/validator 和项目矩阵测试；
4. 生成或更新受影响的 mock/SDK contract。

### Phase 2：执行控制和通知 Action

1. 增加 `ActionResult continue|terminate`；
2. 实现 `send_assignment_notification` 和 guarded Label consume；
3. 为 Lark client 增加 card urgent/read-users 能力；
4. 实现 `mark_assignment_message_urgent`；
5. 为 Ticket Manager Oncall client 增加 `/api/v1/page`；
6. 实现三个 `page_assignment` 的 phase checkpoint、ACK poll 和 fault-injection 测试。

### Phase 3：Trigger defer 和 one-hop escalation

1. reserved source 使用 family-level live conflict defer；
2. migration 增加一个 notification state JSON；
3. 实现 role-slot row lock、intent reconcile 和 no-Grand-Leader marker；
4. 实现 Core Leader 查询、Jira role guarded update 和主动 same-role wake-up；
5. 完成并发、竞态和 crash recovery 测试。

### Phase 4：Policy、Automation 与灰度

1. 在 dev/sandbox 发布三个 draft Policy，校验后 active；
2. 创建四类 Jira Automation，先只对测试 Issue/filter 生效；
3. 通过测试范围的 Policy 灰度 assignment notification；
4. 验证 Jira update 风险后，再在 Policy 中加入或启用 Leader escalation Action；
5. 观察 warning、deferred Trigger、worker 和 queue 指标，再逐项目扩大范围。

回滚时先关闭 Jira Automation，并修改或停用 assignment-notification Policy。已有 Execution 可按 frozen
context 完成，或由运维明确终止；不要通过删除 Dispatch Context 绕过 one-hop marker。

## 15. 验收标准

- 三类 Policy 按 reserved source、项目和 Label 精确匹配，不影响已有 Dispatch Policy；
- non-urgent 只发普通卡片；urgent 在卡片通道失败时仍最多 page 三次；
- ACK 只认本次 Ticket Manager card 或离开明确的 pending status；
- 改派、urgent 降级和 ACK 会立即终止剩余 page/escalation；
- 每个 workflow 最多三个 Oncall attempt；自动升级到 Leader 后绝不继续升级 Grand Leader；
- 每个外部副作用都有可验证的 pre/post checkpoint，重启不重复结果未知的 page；
- Jira 业务显示值全部由 versioned Policy/Action JSON 承载，在途 Execution 使用 frozen context；
- failed 保持终态，单副本和 10 worker 的 v1 约束有监控与运维手册；
- 上述单元、fault-injection、集成和部署测试全部通过后才启用生产 Automation。
