# `CMDC.Plan`
[🔗](https://github.com/tupleyun/cmdc/blob/v0.5.3/lib/cmdc/plan.ex#L1)

结构化执行计划 — Agent Planning Pattern 的运行时实体。

`Plan` 将一个高层目标（goal）拆成一列可执行的 **Step**，并通过
状态机语义 `:pending → :in_progress → :completed | :failed | :skipped`
记录 Agent 的进度。Plugin / Tool / 用户都可以读写计划，从而实现
"规划→执行→反馈"的闭环。

配套插件：`CMDC.Plugin.Builtin.Planning`（v0.3）

## 构造

    iex> plan = CMDC.Plan.new("发布新版", ["写 CHANGELOG", "跑 preflight", "mix hex.publish"])
    iex> length(plan.steps)
    3

    iex> {:ok, plan} = CMDC.Plan.from_markdown("发布", """
    ...> - [ ] 写 CHANGELOG
    ...> - [ ] 跑 preflight
    ...> - [x] 跑 mix format
    ...> """)
    iex> Enum.count(plan.steps, &(&1.status == :completed))
    1

## Step 状态流转

- `step_started/2`：`:pending → :in_progress`
- `step_completed/3`：`:pending | :in_progress → :completed`
- `step_failed/3`：`:pending | :in_progress → :failed`
- `step_skipped/3`：`:pending → :skipped`

非法流转（如 `:completed → :in_progress`）返回 `{:error, {:illegal_transition, from, to}}`。

## 查询

    iex> plan = CMDC.Plan.new("task", ["a", "b", "c"])
    iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1")
    iex> CMDC.Plan.progress(plan)
    %{completed: 1, total: 3, failed: 0, skipped: 0, pending: 2, percentage: 33.3}

    iex> plan = CMDC.Plan.new("task", ["a", "b"])
    iex> %{id: "step-1"} = CMDC.Plan.current_step(plan)

## 渲染

- `to_markdown/1`：渲染为 GitHub Checklist（`- [x]`）
- `to_prompt_section/1`：渲染为注入 system prompt 的结构化段落

## 与 Plugin 的配合

`CMDC.Plugin.Builtin.Planning` 在 Plugin state 里保存 `%CMDC.Plan{}`，
并在关键生命周期 emit `:plan_generated` / `:plan_step_completed` / `:plan_completed` 事件。
第三方 Tool 可通过 Plugin state 或 EventBus 读写。

# `t`

```elixir
@type t() :: %CMDC.Plan{
  approved_at: DateTime.t() | nil,
  created_at: DateTime.t() | nil,
  goal: String.t(),
  metadata: map(),
  steps: [CMDC.Plan.Step.t()]
}
```

# `add_step`

```elixir
@spec add_step(t(), String.t(), keyword()) :: t()
```

在计划末尾追加一个步骤。自动分配下一个 `step-N` id（基于现有最大编号 + 1）。

    iex> plan = CMDC.Plan.new("t", ["a"])
    iex> plan = CMDC.Plan.add_step(plan, "b")
    iex> Enum.map(plan.steps, & &1.id)
    ["step-1", "step-2"]

# `all_completed?`

```elixir
@spec all_completed?(t()) :: boolean()
```

判断是否所有 step 都成功完成。

# `all_finished?`

```elixir
@spec all_finished?(t()) :: boolean()
```

判断所有 step 是否都进入终态（completed / failed / skipped）。

仅在全部 step 都已推进时返回 `true`。

# `annotate_step`

```elixir
@spec annotate_step(t() | {:ok, t()} | {:error, term()}, String.t(), String.t()) ::
  {:ok, t()} | {:error, :not_found | term()}
```

附加 notes 到某个 step，不改变其状态。可在 Step 执行前后写入上下文信息。

    iex> plan = CMDC.Plan.new("t", ["a"])
    iex> {:ok, plan} = CMDC.Plan.annotate_step(plan, "step-1", "需要审批")
    iex> CMDC.Plan.get_step(plan, "step-1").notes
    "需要审批"

# `approve`

```elixir
@spec approve(t()) :: t()
```

用户批准 Plan，记录 approved_at 时间戳（供后续审计）。

# `current_step`

```elixir
@spec current_step(t()) :: CMDC.Plan.Step.t() | nil
```

返回当前应执行的 step（第一个 `:in_progress`，否则第一个 `:pending`）。

全部完成/失败/跳过时返回 `nil`。

# `from_markdown`

```elixir
@spec from_markdown(String.t(), String.t()) :: {:ok, t()} | {:error, :no_steps_found}
```

从 markdown checklist 字符串解析 Plan。

支持以下两种格式：

- `- [ ]` / `- [x]` / `- [X]` — GitHub Checklist
- `1. xxx` / `1) xxx` — 有序列表（全部按 pending 处理）

未能识别为列表项的行被忽略，可用于 LLM 在 checklist 前后附加解释性文字。

    iex> md = """
    ...> 这是计划：
    ...> 
    ...> - [ ] 步骤一
    ...> - [x] 步骤二 已完成
    ...> - [ ] 步骤三
    ...> 
    ...> 希望有用。
    ...> """
    iex> {:ok, plan} = CMDC.Plan.from_markdown("demo", md)
    iex> length(plan.steps)
    3
    iex> Enum.at(plan.steps, 1).status
    :completed

返回 `{:error, :no_steps_found}` 当解析结果为空。

# `get_step`

```elixir
@spec get_step(t(), String.t()) :: CMDC.Plan.Step.t() | nil
```

根据 step_id 获取 Step，找不到返回 `nil`。

# `insert_step`

```elixir
@spec insert_step(t(), String.t(), String.t(), keyword()) ::
  {:ok, t()} | {:error, :not_found}
```

在指定 step_id 之前（`before: true`）或之后（默认）插入新步骤。

新步骤的 id 自动分配。不存在的 step_id 返回 `{:error, :not_found}`。

    iex> plan = CMDC.Plan.new("t", ["a", "c"])
    iex> {:ok, plan} = CMDC.Plan.insert_step(plan, "step-1", "b")
    iex> Enum.map(plan.steps, & &1.description)
    ["a", "b", "c"]

# `new`

```elixir
@spec new(String.t(), [String.t()]) :: t()
```

从目标 + 步骤描述列表构建新 Plan。

每个步骤自动分配 `step-N` 格式的 id。

    iex> plan = CMDC.Plan.new("任务", ["A", "B"])
    iex> Enum.map(plan.steps, & &1.id)
    ["step-1", "step-2"]
    iex> Enum.all?(plan.steps, &(&1.status == :pending))
    true

# `progress`

```elixir
@spec progress(t()) :: %{
  completed: non_neg_integer(),
  failed: non_neg_integer(),
  skipped: non_neg_integer(),
  pending: non_neg_integer(),
  total: non_neg_integer(),
  percentage: float()
}
```

返回 Plan 进度摘要：各状态计数 + 百分比。

    iex> plan = CMDC.Plan.new("t", ["a", "b", "c", "d"])
    iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1")
    iex> {:ok, plan} = CMDC.Plan.step_failed(plan, "step-2", "err")
    iex> {:ok, plan} = CMDC.Plan.step_skipped(plan, "step-3")
    iex> CMDC.Plan.progress(plan)
    %{completed: 1, failed: 1, skipped: 1, pending: 1, total: 4, percentage: 75.0}

# `remove_step`

```elixir
@spec remove_step(t(), String.t()) :: {:ok, t()} | {:error, :not_found}
```

删除指定 id 的步骤。不存在返回 `{:error, :not_found}`。

    iex> plan = CMDC.Plan.new("t", ["a", "b"])
    iex> {:ok, plan} = CMDC.Plan.remove_step(plan, "step-1")
    iex> Enum.map(plan.steps, & &1.description)
    ["b"]

# `replace_steps`

```elixir
@spec replace_steps(t(), [String.t()]) :: t()
```

**完全替换** Plan 的步骤列表，重新从 `step-1` 开始编号。

当 LLM 根据新情况（障碍 / 新信息 / 用户反馈）重新生成整份计划时使用。
执行后 `approved_at` 会被清空（需要重新批准），`metadata[:last_replanned_at]`
记录重规划时间戳。

    iex> plan = CMDC.Plan.new("旅行", ["订机票", "订酒店"])
    iex> plan = CMDC.Plan.approve(plan)
    iex> plan = CMDC.Plan.replace_steps(plan, ["重新选择目的地", "订机票", "订酒店"])
    iex> length(plan.steps)
    3
    iex> plan.approved_at
    nil

# `step_completed`

```elixir
@spec step_completed(t() | {:ok, t()}, String.t(), keyword()) ::
  {:ok, t()}
  | {:error,
     :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
```

标记某个 step 已完成，可附带结果数据（`:pending | :in_progress → :completed`）。

`Plan.step_completed/2` 为 **任务 11.16** 定义的 API，允许 Tool/外部调用方
在完成某个步骤后批量推进计划。

    iex> plan = CMDC.Plan.new("t", ["a", "b"])
    iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1", result: "ok")
    iex> step = CMDC.Plan.get_step(plan, "step-1")
    iex> step.status
    :completed
    iex> step.result
    "ok"

# `step_failed`

```elixir
@spec step_failed(t() | {:ok, t()}, String.t(), String.t()) ::
  {:ok, t()}
  | {:error,
     :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
```

标记某个 step 已失败。

    iex> plan = CMDC.Plan.new("t", ["a"])
    iex> {:ok, plan} = CMDC.Plan.step_failed(plan, "step-1", "网络超时")
    iex> CMDC.Plan.get_step(plan, "step-1").error
    "网络超时"

# `step_skipped`

```elixir
@spec step_skipped(t() | {:ok, t()}, String.t(), String.t() | nil) ::
  {:ok, t()}
  | {:error,
     :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
```

标记某个 step 已跳过（`:pending → :skipped`）。

# `step_started`

```elixir
@spec step_started(t() | {:ok, t()}, String.t()) ::
  {:ok, t()}
  | {:error,
     :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
```

标记某个 step 已开始执行（`:pending → :in_progress`）。

    iex> plan = CMDC.Plan.new("t", ["a"])
    iex> {:ok, plan} = CMDC.Plan.step_started(plan, "step-1")
    iex> CMDC.Plan.get_step(plan, "step-1").status
    :in_progress

# `to_markdown`

```elixir
@spec to_markdown(t()) :: String.t()
```

将 Plan 渲染为 Markdown Checklist。`:completed` 为 `[x]`，其他状态附带 tag。

    iex> plan = CMDC.Plan.new("发布", ["写 CHANGELOG", "发布"])
    iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1")
    iex> CMDC.Plan.to_markdown(plan)
    "- [x] 写 CHANGELOG\n- [ ] 发布"

# `to_prompt_section`

```elixir
@spec to_prompt_section(t()) :: String.t()
```

将 Plan 渲染为注入 system prompt 的结构化段落。含目标、进度摘要、全部步骤、当前步骤。

---

*Consult [api-reference.md](api-reference.md) for complete listing*
