diff --git a/crates/ironclaw_engine/src/runtime/mission.rs b/crates/ironclaw_engine/src/runtime/mission.rs index 63863a2a..4615a64c 100644 --- a/crates/ironclaw_engine/src/runtime/mission.rs +++ b/crates/ironclaw_engine/src/runtime/mission.rs @@ -154,7 +154,9 @@ impl MissionManager { mission.next_fire_at.is_some_and(|next| next <= now) } MissionCadence::Manual => false, - MissionCadence::OnEvent { .. } | MissionCadence::OnPush => false, + MissionCadence::OnEvent { .. } + | MissionCadence::OnSystemEvent { .. } + | MissionCadence::Webhook { .. } => false, }; if should_fire && let Some(tid) = self.fire_mission(mid, user_id).await? { @@ -539,6 +541,7 @@ mod tests { "periodic goal", MissionCadence::Cron { expression: "* * * * *".into(), + timezone: None, }, ) .await diff --git a/crates/ironclaw_engine/src/types/mission.rs b/crates/ironclaw_engine/src/types/mission.rs index 94870e41..3e79fea3 100644 --- a/crates/ironclaw_engine/src/types/mission.rs +++ b/crates/ironclaw_engine/src/types/mission.rs @@ -47,15 +47,31 @@ pub enum MissionStatus { } /// How a mission triggers new threads. +/// +/// The engine defines the trigger *types*. The bridge/host implements the +/// actual trigger infrastructure (cron tickers, webhook endpoints, event +/// matchers). The engine just needs to be told "fire this mission now." #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MissionCadence { /// Spawn on a cron schedule (e.g., "0 */6 * * *" for every 6 hours). - Cron { expression: String }, - /// Spawn in response to a named event. + Cron { + expression: String, + timezone: Option, + }, + /// Spawn in response to a channel message matching a pattern. OnEvent { event_pattern: String }, - /// Spawn when code is pushed (webhook-driven). - OnPush, - /// Only spawn when manually triggered. + /// Spawn in response to a structured system event (from tools or external). + OnSystemEvent { + source: String, + event_type: String, + }, + /// Spawn when an external webhook is received at a registered path. + /// The bridge registers the webhook endpoint and routes payloads here. + Webhook { + path: String, + secret: Option, + }, + /// Only spawn when manually triggered (via mission_fire tool or API). Manual, } @@ -68,10 +84,30 @@ pub struct Mission { pub goal: String, pub status: MissionStatus, pub cadence: MissionCadence, + + // ── Evolving strategy ── + /// What the next thread should focus on (updated after each thread). + pub current_focus: Option, + /// What approaches have been tried and what happened. + pub approach_history: Vec, + + // ── Progress tracking ── /// History of threads spawned by this mission. pub thread_history: Vec, /// Optional criteria for declaring the mission complete. pub success_criteria: Option, + + // ── Budget ── + /// Maximum threads per day (0 = unlimited). + pub max_threads_per_day: u32, + /// Threads spawned today (reset daily by the cron ticker). + pub threads_today: u32, + + // ── Trigger payload ── + /// Payload from the most recent trigger (webhook body, event data, etc.). + /// Injected into the thread's context so the code can access it. + pub last_trigger_payload: Option, + pub metadata: serde_json::Value, pub created_at: DateTime, pub updated_at: DateTime, @@ -94,8 +130,13 @@ impl Mission { goal: goal.into(), status: MissionStatus::Active, cadence, + current_focus: None, + approach_history: Vec::new(), thread_history: Vec::new(), success_criteria: None, + max_threads_per_day: 10, + threads_today: 0, + last_trigger_payload: None, metadata: serde_json::Value::Object(serde_json::Map::new()), created_at: now, updated_at: now, diff --git a/docs/plans/2026-03-24-missions.md b/docs/plans/2026-03-24-missions.md index 24ab12c9..44e762e5 100644 --- a/docs/plans/2026-03-24-missions.md +++ b/docs/plans/2026-03-24-missions.md @@ -44,6 +44,29 @@ pub struct Mission { Already defined in `crates/ironclaw_engine/src/types/mission.rs`. +## Trigger Types + +The engine defines trigger *types*. The bridge implements the actual infrastructure: + +| Trigger | Engine type | Bridge implementation | +|---|---|---| +| Cron schedule | `MissionCadence::Cron { expression, timezone }` | Tokio interval task, cron parser | +| Channel message | `MissionCadence::OnEvent { event_pattern }` | Regex match in `handle_message` before routing | +| System event | `MissionCadence::OnSystemEvent { source, event_type }` | Match events from `event_emit` tool | +| Webhook | `MissionCadence::Webhook { path, secret }` | Register HTTP endpoint on webhook server | +| Manual | `MissionCadence::Manual` | `mission_fire` tool or API call | + +**Webhook-based integrations** (GitHub, email, etc.) use the generic `Webhook` cadence. The webhook payload is stored as `mission.last_trigger_payload` and injected into the thread's context: + +```python +# Inside the mission's thread, the trigger payload is accessible: +payload = state["trigger_payload"] +# For a GitHub webhook: payload["action"], payload["issue"]["title"], etc. +# For email: payload["from"], payload["subject"], payload["body"] +``` + +This means GitHub issues, PRs, email, Slack events, etc. all work through the same webhook mechanism — no special-casing in the engine. + ## Architecture ```