Update DESIGN.md, TECHNICAL.md, and TODO.md for Enhanced Delivery Preferences

- Revised DESIGN.md to incorporate support for multiple delivery targets across email, webhook, and process channels, enhancing the flexibility of the delivery system.
- Expanded TECHNICAL.md with detailed descriptions of the new DeliveryPreferences structure, including EmailPreference, WebhookPreference, and ProcessPreference, to clarify their configurations and usage.
- Updated TODO.md to reflect the integration of multiple targets in delivery preferences, ensuring comprehensive tracking of the ongoing enhancements in the delivery architecture.
- Enhanced documentation to outline the new delivery channels and their configurations, improving clarity and usability for developers and users.
This commit is contained in:
Max 2026-01-18 12:09:26 +08:00
parent 9e14d8b7af
commit b64f4c8930
3 changed files with 264 additions and 152 deletions

View file

@ -262,7 +262,7 @@ Human/Event: P1 → P2 → P3 → P4 → P5
| P1 | Goal Gen | Report + history | Goals | Always |
| P2 | Task Plan | Goals + tools | Tasks | Always |
| P3 | Run + Valid | Tasks + Experts | TaskResults | Always |
| P4 | Delivery | All results | Email/Webhook | Always |
| P4 | Delivery | All results | Email/Webhook/Process | Always |
| P5 | Learning | Summary | KB entries | Always |
### 4.2 P0: Inspiration (Clock only)
@ -514,7 +514,7 @@ P4 generates delivery content and pushes to Delivery Center. **Agent only genera
│ Role: │
│ 1. Read Robot/User delivery preferences │
│ 2. Decide which channels to use │
│ 3. Execute delivery (email, webhook)
│ 3. Execute delivery (email, webhook, process)
│ 4. Future: auto-notify based on user subscriptions │
│ │
│ (Current: internal, future: yao/delivery) │
@ -571,11 +571,12 @@ Attachments use the standard `yao/attachment` wrapper format:
**Delivery Channels (Delivery Center decides):**
| Channel | Description | When |
|---------|-------------|------|
| `email` | Send via yao/messenger | If configured in preferences |
| `webhook` | POST to URL (Slack, 飞书, etc.) | If configured, every execution |
| `notify` | In-app push notification | Based on user subscriptions (future) |
| Channel | Description | Multiple Targets |
|---------|-------------|------------------|
| `email` | Send via yao/messenger | ✅ Multiple recipients/emails |
| `webhook` | POST to external URL | ✅ Multiple URLs |
| `process` | Yao Process call | ✅ Multiple processes |
| `notify` | In-app notification | Future (auto by subscriptions) |
**Delivery Agent:**
@ -625,12 +626,13 @@ type DeliveryResult struct {
Error string `json:"error,omitempty"`
}
// ChannelResult - result for a single channel
// ChannelResult - result for a single delivery target
type ChannelResult struct {
Type DeliveryType `json:"type"`
Type DeliveryType `json:"type"` // email | webhook | process | notify
Target string `json:"target,omitempty"` // Target identifier
Success bool `json:"success"`
Recipients []string `json:"recipients,omitempty"` // For email
Details interface{} `json:"details,omitempty"`
Details interface{} `json:"details,omitempty"` // Channel-specific response
Error string `json:"error,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
}
@ -638,22 +640,45 @@ type ChannelResult struct {
**Config (Delivery Preferences):**
Robot config defines delivery **preferences** (Delivery Center reads and executes):
Robot config defines delivery **preferences** (Delivery Center reads and executes).
Each channel supports **multiple targets**:
```yaml
delivery:
preferences:
email:
enabled: true
to: ["manager@company.com"]
cc: ["team@company.com"]
targets: # Multiple email targets
- to: ["manager@company.com"]
cc: ["team@company.com"]
- to: ["ceo@company.com"]
subject_template: "Executive Summary"
webhook:
enabled: true
url: "https://slack.com/webhook/reports"
# Every execution pushes to webhook automatically
targets: # Multiple webhook URLs
- url: "https://slack.com/webhook/sales"
- url: "https://feishu.cn/webhook/reports"
headers: {"X-Custom": "value"}
process:
enabled: true
targets: # Multiple Yao Process calls
- name: "orders.UpdateStatus"
args: ["completed"]
- name: "audit.LogDelivery"
# Note: notify handled by Delivery Center based on user subscriptions (future)
```
**Use Cases:**
| Scenario | Channels | Description |
|----------|----------|-------------|
| Event callback | `process` | DB change → Robot → Update data via Process |
| Multi-channel notify | `email` + `webhook` | Send to multiple emails and Slack/飞书 |
| Data pipeline | `process` | Robot result → Save to DB → Update dashboard |
### 4.7 P5: Learn
Save to KB:
@ -680,7 +705,7 @@ type Config struct {
DB *DB `json:"db,omitempty"` // shared DB (same as assistant)
Learn *Learn `json:"learn,omitempty"` // learning for private KB
Resources *Resources `json:"resources"`
Delivery *Delivery `json:"delivery"`
Delivery *DeliveryPreferences `json:"delivery,omitempty"`
Events []Event `json:"events,omitempty"`
Executor *Executor `json:"executor,omitempty"` // executor mode settings
}
@ -721,7 +746,8 @@ type DeliveryType string
const (
DeliveryEmail DeliveryType = "email" // Email via yao/messenger
DeliveryWebhook DeliveryType = "webhook" // POST to URL
DeliveryWebhook DeliveryType = "webhook" // POST to external URL
DeliveryProcess DeliveryType = "process" // Yao Process call
DeliveryNotify DeliveryType = "notify" // In-app notification (future)
)
@ -815,10 +841,44 @@ type MCP struct {
Tools []string `json:"tools,omitempty"` // empty = all
}
// Delivery
type Delivery struct {
Type DeliveryType `json:"type"`
Opts map[string]interface{} `json:"opts"`
// DeliveryPreferences - Robot delivery preferences (read by Delivery Center)
// Each channel supports multiple targets
type DeliveryPreferences struct {
Email *EmailPreference `json:"email,omitempty"`
Webhook *WebhookPreference `json:"webhook,omitempty"`
Process *ProcessPreference `json:"process,omitempty"`
// notify is handled automatically based on user subscriptions
}
type EmailPreference struct {
Enabled bool `json:"enabled"`
Targets []EmailTarget `json:"targets"`
}
type EmailTarget struct {
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
SubjectTemplate string `json:"subject_template,omitempty"`
}
type WebhookPreference struct {
Enabled bool `json:"enabled"`
Targets []WebhookTarget `json:"targets"`
}
type WebhookTarget struct {
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}
type ProcessPreference struct {
Enabled bool `json:"enabled"`
Targets []ProcessTarget `json:"targets"`
}
type ProcessTarget struct {
Name string `json:"name"` // Process name, e.g., "orders.UpdateStatus"
Args []any `json:"args,omitempty"`
}
// ExecutorMode - executor mode enum

View file

@ -855,7 +855,8 @@ type DeliveryType string
const (
DeliveryEmail DeliveryType = "email" // Email via yao/messenger
DeliveryWebhook DeliveryType = "webhook" // POST to URL
DeliveryWebhook DeliveryType = "webhook" // POST to external URL
DeliveryProcess DeliveryType = "process" // Yao Process call
DeliveryNotify DeliveryType = "notify" // In-app notification (future)
)
@ -953,7 +954,7 @@ type Config struct {
DB *DB `json:"db,omitempty"` // shared database (same as assistant)
Learn *Learn `json:"learn,omitempty"` // learning config for private KB
Resources *Resources `json:"resources,omitempty"`
Delivery *Delivery `json:"delivery,omitempty"`
Delivery *DeliveryPreferences `json:"delivery,omitempty"` // see section 6.2
Events []Event `json:"events,omitempty"`
}
@ -1133,11 +1134,7 @@ type MCPConfig struct {
Tools []string `json:"tools,omitempty"` // empty = all
}
// Delivery - output delivery
type Delivery struct {
Type DeliveryType `json:"type"`
Opts map[string]interface{} `json:"opts,omitempty"`
}
// Note: Delivery preferences moved to DeliveryPreferences (see section 6.2)
// Event - event trigger config
type Event struct {
@ -1435,22 +1432,46 @@ type DeliveryContext struct {
}
// DeliveryPreferences - Robot/User delivery preferences (read by Delivery Center)
// Each channel supports multiple targets
type DeliveryPreferences struct {
Email *EmailPreference `json:"email,omitempty"`
Webhook *WebhookPreference `json:"webhook,omitempty"`
Process *ProcessPreference `json:"process,omitempty"`
// notify is handled automatically based on user subscriptions
}
// EmailPreference - multiple email targets
type EmailPreference struct {
Enabled bool `json:"enabled"`
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
Enabled bool `json:"enabled"`
Targets []EmailTarget `json:"targets"`
}
type EmailTarget struct {
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
SubjectTemplate string `json:"subject_template,omitempty"` // Optional, default: content.Summary
}
// WebhookPreference - multiple webhook targets
type WebhookPreference struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
// If enabled, every execution pushes automatically
Enabled bool `json:"enabled"`
Targets []WebhookTarget `json:"targets"`
}
type WebhookTarget struct {
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}
// ProcessPreference - multiple Yao Process targets
type ProcessPreference struct {
Enabled bool `json:"enabled"`
Targets []ProcessTarget `json:"targets"`
}
type ProcessTarget struct {
Name string `json:"name"` // Process name, e.g., "orders.UpdateStatus"
Args []any `json:"args,omitempty"` // Additional args (DeliveryContent passed as first arg)
}
// DeliveryResult - P4 delivery output (returned by Delivery Center)
@ -1462,9 +1483,10 @@ type DeliveryResult struct {
Error string `json:"error,omitempty"` // Overall error if any
}
// ChannelResult - result for a single delivery channel
// ChannelResult - result for a single delivery target
type ChannelResult struct {
Type DeliveryType `json:"type"` // email | webhook | notify
Type DeliveryType `json:"type"` // email | webhook | process | notify
Target string `json:"target,omitempty"` // Target identifier (email, URL, process name)
Success bool `json:"success"`
Recipients []string `json:"recipients,omitempty"` // Who received (for email)
Details interface{} `json:"details,omitempty"` // Channel-specific response
@ -2040,25 +2062,29 @@ type DeliveryContext struct {
**Channel Decision by Delivery Center:**
Delivery Center reads Robot/User preferences and decides channels:
Delivery Center reads Robot/User preferences and executes delivery to all enabled targets:
```go
// DeliveryPreferences - from Robot config
// DeliveryPreferences - from Robot config (each channel supports multiple targets)
type DeliveryPreferences struct {
Email *EmailPreference `json:"email,omitempty"`
Webhook *WebhookPreference `json:"webhook,omitempty"`
Process *ProcessPreference `json:"process,omitempty"`
}
type EmailPreference struct {
Enabled bool `json:"enabled"`
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
Enabled bool `json:"enabled"`
Targets []EmailTarget `json:"targets"` // Multiple email targets
}
type WebhookPreference struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
// If enabled, every execution pushes automatically
Enabled bool `json:"enabled"`
Targets []WebhookTarget `json:"targets"` // Multiple webhook URLs
}
type ProcessPreference struct {
Enabled bool `json:"enabled"`
Targets []ProcessTarget `json:"targets"` // Multiple Yao Process calls
}
```
@ -2139,57 +2165,59 @@ The agent focuses on content generation:
### 6.5 Delivery Center
The Delivery Center receives `DeliveryRequest`, **decides channels based on preferences**, and executes delivery.
The Delivery Center receives `DeliveryRequest`, reads preferences, and executes delivery to **all enabled targets**.
**Current implementation:** Internal to P4 (in `executor/delivery.go`)
**Future:** Can be extracted to standalone `yao/delivery` package
```go
// DeliveryCenter - handles channel decision and delivery execution
// DeliveryCenter - handles delivery execution to multiple targets
type DeliveryCenter struct {
handlers map[DeliveryType]ChannelHandler
}
// ChannelHandler - interface for channel implementations
type ChannelHandler interface {
Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error)
messenger *messenger.Manager
}
// Deliver - main entry point
func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *DeliveryResult {
requestID := generateID()
// 1. Get Robot/User delivery preferences
prefs := dc.getDeliveryPreferences(ctx, req.Context.RobotID)
// 2. Decide channels based on preferences
channels := dc.decideChannels(prefs)
// 3. Execute delivery to each channel
var results []ChannelResult
allSuccess := true
for _, ch := range channels {
handler, ok := dc.handlers[ch.Type]
if !ok {
results = append(results, ChannelResult{
Type: ch.Type,
Success: false,
Error: fmt.Sprintf("unsupported channel: %s", ch.Type),
})
allSuccess = false
continue
// Email - send to all targets
if prefs.Email != nil && prefs.Email.Enabled {
for _, target := range prefs.Email.Targets {
result := dc.sendEmail(ctx, req.Content, target)
results = append(results, result)
if !result.Success {
allSuccess = false
}
}
result, err := handler.Deliver(ctx, req.Content, ch.Options)
if err != nil {
result = &ChannelResult{Type: ch.Type, Success: false, Error: err.Error()}
allSuccess = false
}
results = append(results, *result)
}
// 4. Future: check user subscriptions and send notifications
// Webhook - POST to all targets
if prefs.Webhook != nil && prefs.Webhook.Enabled {
for _, target := range prefs.Webhook.Targets {
result := dc.postWebhook(ctx, req.Content, target)
results = append(results, result)
if !result.Success {
allSuccess = false
}
}
}
// Process - call all targets
if prefs.Process != nil && prefs.Process.Enabled {
for _, target := range prefs.Process.Targets {
result := dc.callProcess(ctx, req.Content, target)
results = append(results, result)
if !result.Success {
allSuccess = false
}
}
}
// Future: auto-notify based on user subscriptions
// dc.sendNotifications(ctx, req)
return &DeliveryResult{
@ -2199,40 +2227,15 @@ func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *De
Results: results,
}
}
// decideChannels - decide which channels to use based on preferences
func (dc *DeliveryCenter) decideChannels(prefs *DeliveryPreferences) []channelWithOpts {
var channels []channelWithOpts
if prefs.Email != nil && prefs.Email.Enabled {
channels = append(channels, channelWithOpts{
Type: DeliveryEmail,
Options: map[string]interface{}{"to": prefs.Email.To, "cc": prefs.Email.CC},
})
}
if prefs.Webhook != nil && prefs.Webhook.Enabled {
channels = append(channels, channelWithOpts{
Type: DeliveryWebhook,
Options: map[string]interface{}{"url": prefs.Webhook.URL},
})
}
return channels
}
```
### 6.6 Channel Handlers
Each delivery channel has a dedicated handler implementing `ChannelHandler`:
Each delivery channel is handled by dedicated methods in DeliveryCenter:
```go
// EmailHandler - uses yao/messenger
type EmailHandler struct {
messenger *messenger.Manager
}
func (h *EmailHandler) Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) {
// sendEmail - send to a single email target
func (dc *DeliveryCenter) sendEmail(ctx context.Context, content *DeliveryContent, target EmailTarget) ChannelResult {
// Convert attachments to messenger format
var attachments []messenger.Attachment
for _, att := range content.Attachments {
@ -2248,50 +2251,87 @@ func (h *EmailHandler) Deliver(ctx context.Context, content *DeliveryContent, op
})
}
to := opts["to"].([]string)
err := h.messenger.Send(ctx, &messenger.Message{
To: to,
Subject: content.Summary, // Use summary as subject
subject := content.Summary
if target.SubjectTemplate != "" {
subject = target.SubjectTemplate
}
err := dc.messenger.Send(ctx, &messenger.Message{
To: target.To,
CC: target.CC,
Subject: subject,
Body: content.Body,
Attachments: attachments,
})
now := time.Now()
return &ChannelResult{
return ChannelResult{
Type: DeliveryEmail,
Target: strings.Join(target.To, ","),
Success: err == nil,
Recipients: to,
Recipients: target.To,
SentAt: &now,
}, err
Error: errStr(err),
}
}
// WebhookHandler - POST JSON to URL
type WebhookHandler struct{}
func (h *WebhookHandler) Deliver(ctx context.Context, content *DeliveryContent, opts map[string]interface{}) (*ChannelResult, error) {
// postWebhook - POST to a single webhook target
func (dc *DeliveryCenter) postWebhook(ctx context.Context, content *DeliveryContent, target WebhookTarget) ChannelResult {
payload, _ := json.Marshal(content)
req, _ := http.NewRequestWithContext(ctx, "POST", opts["url"].(string), bytes.NewReader(payload))
req, _ := http.NewRequestWithContext(ctx, "POST", target.URL, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
// Add custom headers
for k, v := range target.Headers {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
now := time.Now()
if err != nil {
return &ChannelResult{Type: DeliveryWebhook, Success: false}, err
return ChannelResult{
Type: DeliveryWebhook,
Target: target.URL,
Success: false,
Error: err.Error(),
SentAt: &now,
}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return &ChannelResult{Type: DeliveryWebhook, Success: false}, fmt.Errorf("webhook failed: %d", resp.StatusCode)
}
now := time.Now()
return &ChannelResult{
success := resp.StatusCode < 400
return ChannelResult{
Type: DeliveryWebhook,
Success: true,
Target: target.URL,
Success: success,
Details: map[string]interface{}{"status_code": resp.StatusCode},
Error: ternary(!success, fmt.Sprintf("HTTP %d", resp.StatusCode), ""),
SentAt: &now,
}, nil
}
}
// callProcess - call a single Yao Process target
func (dc *DeliveryCenter) callProcess(ctx context.Context, content *DeliveryContent, target ProcessTarget) ChannelResult {
// DeliveryContent as first arg, then additional args
args := append([]interface{}{content}, target.Args...)
proc := process.Of(target.Name, args...)
result, err := proc.Execute()
now := time.Now()
return ChannelResult{
Type: DeliveryProcess,
Target: target.Name,
Success: err == nil,
Details: map[string]interface{}{
"process": target.Name,
"result": result,
},
Error: errStr(err),
SentAt: &now,
}
}
```
**Note on Notifications:**

View file

@ -62,7 +62,7 @@
- [x] `RobotStatus` - robot status (idle, working, paused, error, maintenance)
- [x] `InterventionAction` - human actions (task.add, goal.adjust, etc.)
- [x] `Priority` - priority levels (high, normal, low)
- [x] `DeliveryType` - delivery types (email, webhook, notify)
- [x] `DeliveryType` - delivery types (email, webhook, process, notify)
- [x] `DedupResult` - dedup results (skip, merge, proceed)
- [x] `EventSource` - event sources (webhook, database)
- [x] `LearningType` - learning types (execution, feedback, insight)
@ -916,16 +916,21 @@ Supported channels:
### 10.3 Type Updates (Prerequisite)
- [ ] Update `types/enums.go` - Remove `DeliveryFile` from `DeliveryType`
- [ ] Update `types/enums.go` - Update `DeliveryType` enum
- [ ] Remove `DeliveryFile`
- [ ] Add `DeliveryProcess`
- [ ] Update `types/robot.go` - Delivery types for new architecture
- [ ] `DeliveryResult` - update to new structure (RequestID, Content, Results[])
- [ ] Add `DeliveryContent` struct
- [ ] Add `DeliveryAttachment` struct
- [ ] Add `DeliveryRequest` struct
- [ ] Add `DeliveryContext` struct
- [ ] Add `DeliveryPreferences` struct
- [ ] Add `ChannelResult` struct
- [ ] Update `types/enums_test.go` - Remove `DeliveryFile` test
- [ ] Add `DeliveryPreferences` struct (with Email, Webhook, Process)
- [ ] Add `EmailPreference`, `EmailTarget` structs
- [ ] Add `WebhookPreference`, `WebhookTarget` structs
- [ ] Add `ProcessPreference`, `ProcessTarget` structs
- [ ] Add `ChannelResult` struct (with Target field)
- [ ] Update `types/enums_test.go` - Update DeliveryType tests
- [ ] Update `types/robot_test.go` - Update delivery result tests
### 10.4 Delivery Agent Setup
@ -980,12 +985,13 @@ type DeliveryContext struct {
- Parse: `attachment.Parse(value)``(uploader, fileID, isWrapper)`
- Read: `attachment.Base64(ctx, value)` → base64 content
**Delivery Channels (Delivery Center decides):**
| Channel | Description |
|---------|-------------|
| `email` | Send via yao/messenger (if configured in preferences) |
| `webhook` | POST JSON to URL (if configured, every execution) |
| `notify` | In-app notification based on user subscriptions (future) |
**Delivery Channels (each supports multiple targets):**
| Channel | Description | Multiple Targets |
|---------|-------------|------------------|
| `email` | Send via yao/messenger | ✅ Multiple recipients |
| `webhook` | POST to external URL | ✅ Multiple URLs |
| `process` | Yao Process call | ✅ Multiple processes |
| `notify` | In-app notification | Future (auto by subscriptions) |
### 10.6 Implementation
@ -999,30 +1005,36 @@ type DeliveryContext struct {
**Delivery Center (executor/delivery.go, future: yao/delivery):**
- [ ] `DeliveryCenter.Deliver(ctx, request)` - main entry
- [ ] Read Robot/User delivery preferences
- [ ] Decide which channels to use based on preferences
- [ ] Call appropriate handler for each channel
- [ ] Iterate through all enabled targets for each channel
- [ ] Aggregate ChannelResults into DeliveryResult
- [ ] `ChannelHandler` interface
- [ ] `Deliver(ctx, content, opts) (*ChannelResult, error)`
**Channel Handlers:**
- [ ] `EmailHandler` - uses yao/messenger
**Channel Handlers (each supports multiple targets):**
- [ ] `sendEmail()` - uses yao/messenger
- [ ] Convert DeliveryAttachment to messenger.Attachment
- [ ] Use Summary as email subject
- [ ] Support to, cc from preferences
- [ ] `WebhookHandler` - POST JSON
- [ ] Support multiple EmailTarget
- [ ] Support custom subject_template per target
- [ ] `postWebhook()` - POST JSON
- [ ] POST DeliveryContent as JSON payload
- [ ] If enabled, every execution pushes automatically
- [ ] Support multiple WebhookTarget
- [ ] Support custom headers per target
- [ ] `callProcess()` - Yao Process call
- [ ] DeliveryContent as first arg
- [ ] Support multiple ProcessTarget
- [ ] Support additional args per target
### 10.7 Tests
- [ ] `executor/delivery_test.go` - P4 delivery
- [ ] Test: Delivery Agent generates content (only content)
- [ ] Test: DeliveryCenter reads preferences and decides channels
- [ ] Test: DeliveryCenter dispatches to multiple channels
- [ ] Test: EmailHandler with attachments
- [ ] Test: WebhookHandler POST JSON
- [ ] Test: Partial success (some channels fail)
- [ ] Test: DeliveryCenter reads preferences
- [ ] Test: Multiple email targets
- [ ] Test: Multiple webhook targets
- [ ] Test: Multiple process targets
- [ ] Test: Mixed channels (email + webhook + process)
- [ ] Test: sendEmail with attachments
- [ ] Test: postWebhook with custom headers
- [ ] Test: callProcess with args
- [ ] Test: Partial success (some targets fail)
- [ ] Test: DeliveryResult aggregation
---
@ -1239,7 +1251,7 @@ func TestWithLLM(t *testing.T) {
| 7. P1 Goals | ✅ | Goal Generation Agent integration |
| 8. P2 Tasks | ✅ | Task Planning Agent integration |
| 9. P3 Run | ✅ | Task execution + validation + yao/assert + multi-turn conversation |
| 10. P4 Delivery | ⬜ | Output delivery (email/webhook, notify future) |
| 10. P4 Delivery | ⬜ | Output delivery (email/webhook/process, notify future) |
| 11. API & Integration | ⬜ | Complete API, end-to-end tests (main flow: P0→P1→P2→P3→P4) |
| 12. Advanced | ⬜ | P5 Learning, dedup, plan queue, Sandbox mode |