Skip to main content

gwk_domain/
command.rs

1//! The v1 kernel command set — every mutation the kernel accepts.
2//!
3//! Commands are REQUESTS, not writes: the kernel decides, appends events, and
4//! answers. Each command travels as the `payload` of a [`CommandEnvelope`],
5//! which carries the governance metadata (actor, origin, CAS expectation, the
6//! required `idempotency_key`). The envelope's `command_type` and the body's
7//! own `type` tag must agree — [`KernelCommand::from_envelope`] is the one
8//! place that check lives, so no handler can drift from the routing metadata.
9//!
10//! The set is CLOSED. A new command is a contract change with a new binding,
11//! never an open string a caller can invent.
12
13use serde_json::Value;
14
15use crate::entity::Budget;
16use crate::envelope::{Actor, CommandEnvelope, JsonValue, PayloadRef};
17use crate::fsm::{
18    AttemptState, CommandState, GateVerdict, LeaseMode, MessageState, Outcome, TaskState,
19};
20use crate::ids::{
21    AttemptId, AttentionItemId, AuthorityGrantId, ByteCount, CommandId, CorrelationId,
22    DispatchNodeId, EngineId, EngineSessionId, EvidenceId, GateId, LeaseId, MessageId, ReceiptId,
23    TaskId, Timestamp, WorktreeId,
24};
25use crate::ingestion::IngestionKind;
26use crate::inherited::{FindingAction, OrchestratorCheckpoint, RoundFindingSummary};
27
28/// One requested mutation, tagged by `type`.
29///
30/// Variants are grouped by aggregate. Absent optionals are OMITTED on the wire
31/// (the tri-state discipline in `docs/contract/NAMING.md`): absent means "not
32/// specified", never zero and never `null`.
33// A wire union: the variants ARE the contract's shape. Sizing them uniformly
34// (boxing the wide ones) would change the generated bindings to buy a
35// micro-optimization on a type the kernel handles one of at a time.
36#[allow(clippy::large_enum_variant)]
37#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
38#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
39pub enum KernelCommand {
40    // ---- epoch ----
41    /// The irreversible cutover boundary. Appended once, at aggregate version
42    /// 2, under idempotency key `kernel_activated:<cutover_id>`: the same
43    /// cutover ID retries stably, a different one conflicts.
44    ActivateKernel {
45        cutover_id: String,
46        /// Lowercase 64-hex SHA-256 of the archive manifest
47        /// ([`crate::blob::is_sha256_hex`]).
48        archive_manifest_sha256: String,
49    },
50
51    // ---- task ----
52    CreateTask {
53        task_id: TaskId,
54        #[serde(default, skip_serializing_if = "Option::is_none")]
55        #[specta(optional)]
56        kind: Option<String>,
57        #[serde(default, skip_serializing_if = "Option::is_none")]
58        #[specta(optional)]
59        title: Option<String>,
60        #[serde(default, skip_serializing_if = "Option::is_none")]
61        #[specta(optional)]
62        spec_ref: Option<String>,
63        #[serde(default, skip_serializing_if = "Option::is_none")]
64        #[specta(optional)]
65        project: Option<String>,
66        #[serde(default, skip_serializing_if = "Option::is_none")]
67        #[specta(optional)]
68        priority: Option<i32>,
69        #[serde(default, skip_serializing_if = "Option::is_none")]
70        #[specta(optional)]
71        tracker_ref: Option<String>,
72    },
73    TransitionTask {
74        task_id: TaskId,
75        to: TaskState,
76        expected_version: u32,
77    },
78    UpdateBudget {
79        attempt_id: AttemptId,
80        expected_version: u32,
81        budget: Budget,
82    },
83
84    // ---- attempt ----
85    CreateAttempt {
86        attempt_id: AttemptId,
87        task_id: TaskId,
88        engine: EngineId,
89        #[serde(default, skip_serializing_if = "Option::is_none")]
90        #[specta(optional)]
91        capability: Option<String>,
92        #[serde(default, skip_serializing_if = "Option::is_none")]
93        #[specta(optional)]
94        role: Option<String>,
95        #[serde(default, skip_serializing_if = "Option::is_none")]
96        #[specta(optional)]
97        model_lane: Option<String>,
98        #[serde(default, skip_serializing_if = "Option::is_none")]
99        #[specta(optional)]
100        permission_profile: Option<String>,
101        #[serde(default, skip_serializing_if = "Option::is_none")]
102        #[specta(optional)]
103        worktree_lease_id: Option<LeaseId>,
104        #[serde(default, skip_serializing_if = "Option::is_none")]
105        #[specta(optional)]
106        base_sha: Option<String>,
107        #[serde(default, skip_serializing_if = "Option::is_none")]
108        #[specta(optional)]
109        budget: Option<Budget>,
110    },
111    TransitionAttempt {
112        attempt_id: AttemptId,
113        to: AttemptState,
114        expected_version: u32,
115        /// REQUIRED on the `running` ⇄ `blocked` flip (the liveness-producer
116        /// rule in [`crate::transition`]); absent elsewhere.
117        #[serde(default, skip_serializing_if = "Option::is_none")]
118        #[specta(optional)]
119        receipt_id: Option<ReceiptId>,
120    },
121    RecordAttemptOutcome {
122        attempt_id: AttemptId,
123        expected_version: u32,
124        #[serde(default, skip_serializing_if = "Option::is_none")]
125        #[specta(optional)]
126        exit_code: Option<i32>,
127        #[serde(default, skip_serializing_if = "Option::is_none")]
128        #[specta(optional)]
129        provider_terminal_event: Option<String>,
130        #[serde(default, skip_serializing_if = "Option::is_none")]
131        #[specta(optional)]
132        result_valid: Option<bool>,
133        #[serde(default, skip_serializing_if = "Option::is_none")]
134        #[specta(optional)]
135        evidence_manifest_ref: Option<String>,
136    },
137
138    // ---- engine session ----
139    OpenEngineSession {
140        engine_session_id: EngineSessionId,
141        attempt_id: AttemptId,
142        engine: EngineId,
143        #[serde(default, skip_serializing_if = "Option::is_none")]
144        #[specta(optional)]
145        provider_session_ref: Option<String>,
146    },
147    CloseEngineSession {
148        engine_session_id: EngineSessionId,
149    },
150
151    // ---- message ----
152    SendMessage {
153        message_id: MessageId,
154        #[serde(default, skip_serializing_if = "Option::is_none")]
155        #[specta(optional)]
156        correlation_id: Option<CorrelationId>,
157        #[serde(default, skip_serializing_if = "Option::is_none")]
158        #[specta(optional)]
159        reply_to: Option<MessageId>,
160        #[serde(default, skip_serializing_if = "Option::is_none")]
161        #[specta(optional)]
162        sender: Option<String>,
163        #[serde(default, skip_serializing_if = "Option::is_none")]
164        #[specta(optional)]
165        recipient: Option<String>,
166        #[serde(default, skip_serializing_if = "Option::is_none")]
167        #[specta(optional)]
168        channel: Option<String>,
169        #[serde(default, skip_serializing_if = "Option::is_none")]
170        #[specta(optional)]
171        kind: Option<String>,
172        #[serde(default, skip_serializing_if = "Option::is_none")]
173        #[specta(optional, type = Option<JsonValue>)]
174        payload: Option<Value>,
175        #[serde(default, skip_serializing_if = "Option::is_none")]
176        #[specta(optional)]
177        deadline: Option<Timestamp>,
178    },
179    TransitionMessage {
180        message_id: MessageId,
181        to: MessageState,
182        expected_version: u32,
183        #[serde(default, skip_serializing_if = "Option::is_none")]
184        #[specta(optional)]
185        dead_letter_reason: Option<String>,
186    },
187
188    // ---- execution command (the stop/kill spine) ----
189    IssueCommand {
190        command_id: CommandId,
191        /// OPEN bounded string (e.g. `stop_attempt`).
192        kind: String,
193        /// A stop may name several subjects; the projection keeps the first as
194        /// its primary target.
195        targets: Vec<String>,
196        #[serde(default, skip_serializing_if = "Option::is_none")]
197        #[specta(optional)]
198        actor: Option<Actor>,
199    },
200    TransitionCommand {
201        command_id: CommandId,
202        to: CommandState,
203        expected_version: u32,
204    },
205    RecordCommandOutcome {
206        command_id: CommandId,
207        expected_version: u32,
208        outcome: Outcome,
209    },
210
211    // ---- lease ----
212    AcquireLease {
213        lease_id: LeaseId,
214        mode: LeaseMode,
215        #[serde(default, skip_serializing_if = "Option::is_none")]
216        #[specta(optional)]
217        holder: Option<String>,
218        #[serde(default, skip_serializing_if = "Option::is_none")]
219        #[specta(optional)]
220        scope: Option<String>,
221        #[serde(default, skip_serializing_if = "Option::is_none")]
222        #[specta(optional)]
223        repo: Option<String>,
224        #[serde(default, skip_serializing_if = "Option::is_none")]
225        #[specta(optional)]
226        path: Option<String>,
227        #[serde(default, skip_serializing_if = "Option::is_none")]
228        #[specta(optional)]
229        branch: Option<String>,
230        #[serde(default, skip_serializing_if = "Option::is_none")]
231        #[specta(optional)]
232        base_sha: Option<String>,
233        #[serde(default, skip_serializing_if = "Option::is_none")]
234        #[specta(optional)]
235        expires_at: Option<Timestamp>,
236    },
237    RenewLease {
238        lease_id: LeaseId,
239        expected_version: u32,
240        #[serde(default, skip_serializing_if = "Option::is_none")]
241        #[specta(optional)]
242        expires_at: Option<Timestamp>,
243    },
244    ReleaseLease {
245        lease_id: LeaseId,
246        expected_version: u32,
247        #[serde(default, skip_serializing_if = "Option::is_none")]
248        #[specta(optional)]
249        disposition: Option<String>,
250    },
251    ExpireLease {
252        lease_id: LeaseId,
253        expected_version: u32,
254    },
255
256    // ---- gate ----
257    OpenGate {
258        gate_id: GateId,
259        #[serde(default, skip_serializing_if = "Option::is_none")]
260        #[specta(optional)]
261        attempt_id: Option<AttemptId>,
262        #[serde(default, skip_serializing_if = "Option::is_none")]
263        #[specta(optional)]
264        phase_ref: Option<String>,
265        #[serde(default, skip_serializing_if = "Option::is_none")]
266        #[specta(optional)]
267        kind: Option<String>,
268    },
269    DecideGate {
270        gate_id: GateId,
271        expected_version: u32,
272        verdict: GateVerdict,
273        #[serde(default, skip_serializing_if = "Option::is_none")]
274        #[specta(optional)]
275        evidence_ref: Option<String>,
276    },
277
278    // ---- authority ----
279    GrantAuthority {
280        authority_grant_id: AuthorityGrantId,
281        grantee: Actor,
282        /// OPEN action class this grant covers (e.g. `deploy`, `auto_answer`).
283        action_class: String,
284        #[serde(default, skip_serializing_if = "Option::is_none")]
285        #[specta(optional)]
286        scope: Option<String>,
287        #[serde(default, skip_serializing_if = "Option::is_none")]
288        #[specta(optional)]
289        expires_at: Option<Timestamp>,
290    },
291    RevokeAuthority {
292        authority_grant_id: AuthorityGrantId,
293        #[serde(default, skip_serializing_if = "Option::is_none")]
294        #[specta(optional)]
295        reason: Option<String>,
296    },
297
298    // ---- attention ----
299    RaiseAttention {
300        attention_item_id: AttentionItemId,
301        /// OPEN kind; deduplicated with `subject_ref` while unresolved.
302        kind: String,
303        summary: String,
304        #[serde(default, skip_serializing_if = "Option::is_none")]
305        #[specta(optional)]
306        subject_ref: Option<String>,
307        #[serde(default, skip_serializing_if = "Option::is_none")]
308        #[specta(optional)]
309        raised_by: Option<Actor>,
310    },
311    ResolveAttention {
312        attention_item_id: AttentionItemId,
313        #[serde(default, skip_serializing_if = "Option::is_none")]
314        #[specta(optional)]
315        resolution: Option<String>,
316    },
317
318    // ---- evidence ----
319    RecordEvidence {
320        evidence_id: EvidenceId,
321        /// OPEN bounded string (e.g. `transcript`, `diff`, `log`).
322        kind: String,
323        r#ref: String,
324        #[serde(default, skip_serializing_if = "Option::is_none")]
325        #[specta(optional)]
326        digest: Option<String>,
327        #[serde(default, skip_serializing_if = "Option::is_none")]
328        #[specta(optional)]
329        byte_size: Option<ByteCount>,
330    },
331
332    // ---- worktree ----
333    RegisterWorktree {
334        worktree_id: WorktreeId,
335        repo: String,
336        path: String,
337        branch: String,
338        #[serde(default, skip_serializing_if = "Option::is_none")]
339        #[specta(optional)]
340        base_sha: Option<String>,
341        #[serde(default, skip_serializing_if = "Option::is_none")]
342        #[specta(optional)]
343        lease_id: Option<LeaseId>,
344    },
345    UpdateWorktree {
346        worktree_id: WorktreeId,
347        dirty: bool,
348        unpushed: bool,
349        #[serde(default, skip_serializing_if = "Option::is_none")]
350        #[specta(optional)]
351        base_sha: Option<String>,
352    },
353    ReleaseWorktree {
354        worktree_id: WorktreeId,
355        #[serde(default, skip_serializing_if = "Option::is_none")]
356        #[specta(optional)]
357        disposition: Option<String>,
358    },
359
360    // ---- dispatch tree ----
361    RegisterDispatchNode {
362        dispatch_node_id: DispatchNodeId,
363        #[serde(default, skip_serializing_if = "Option::is_none")]
364        #[specta(optional)]
365        parent_id: Option<DispatchNodeId>,
366        #[serde(default, skip_serializing_if = "Option::is_none")]
367        #[specta(optional)]
368        attempt_id: Option<AttemptId>,
369        /// OPEN kind (e.g. `orchestrator`, `subagent`, `engine`).
370        kind: String,
371        #[serde(default, skip_serializing_if = "Option::is_none")]
372        #[specta(optional)]
373        label: Option<String>,
374    },
375    TransitionDispatchNode {
376        dispatch_node_id: DispatchNodeId,
377        /// OPEN bounded lifecycle label. A dispatch node is bookkeeping over
378        /// the spawn tree, not one of the four public state machines, so this
379        /// is deliberately not an FSM state.
380        to: String,
381        expected_version: u32,
382    },
383
384    // ---- orchestrator ----
385    WriteOrchestratorCheckpoint {
386        checkpoint: OrchestratorCheckpoint,
387    },
388    RecordRound {
389        attempt_id: AttemptId,
390        round: u32,
391        findings: RoundFindingSummary,
392    },
393    RecordFinding {
394        attempt_id: AttemptId,
395        round: u32,
396        action: FindingAction,
397        summary: String,
398        #[serde(default, skip_serializing_if = "Option::is_none")]
399        #[specta(optional)]
400        subject_ref: Option<String>,
401    },
402
403    // ---- ingestion ----
404    IngestRecord {
405        kind: IngestionKind,
406        /// Bounded inline JSON
407        /// (≤ [`INLINE_PAYLOAD_MAX_BYTES`](crate::envelope::INLINE_PAYLOAD_MAX_BYTES)
408        /// serialized); larger content travels as `payload_ref`.
409        #[specta(type = JsonValue)]
410        payload: Value,
411        #[serde(default, skip_serializing_if = "Option::is_none")]
412        #[specta(optional)]
413        payload_ref: Option<PayloadRef>,
414    },
415}
416
417/// Why a [`CommandEnvelope`] did not yield a typed command body.
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum CommandDecodeError {
420    /// The envelope's `command_type` does not name the body's variant.
421    TypeMismatch { declared: String, body: String },
422    /// The payload is not a legal command body.
423    MalformedPayload(String),
424}
425
426impl std::fmt::Display for CommandDecodeError {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        match self {
429            Self::TypeMismatch { declared, body } => write!(
430                f,
431                "envelope command_type {declared} does not match command body type {body}"
432            ),
433            Self::MalformedPayload(reason) => write!(f, "malformed command payload: {reason}"),
434        }
435    }
436}
437
438impl std::error::Error for CommandDecodeError {}
439
440impl KernelCommand {
441    /// The `command_type` an envelope carrying this body must declare —
442    /// identical to the body's own `type` tag.
443    pub fn command_type(&self) -> &'static str {
444        match self {
445            Self::ActivateKernel { .. } => "activate_kernel",
446            Self::CreateTask { .. } => "create_task",
447            Self::TransitionTask { .. } => "transition_task",
448            Self::UpdateBudget { .. } => "update_budget",
449            Self::CreateAttempt { .. } => "create_attempt",
450            Self::TransitionAttempt { .. } => "transition_attempt",
451            Self::RecordAttemptOutcome { .. } => "record_attempt_outcome",
452            Self::OpenEngineSession { .. } => "open_engine_session",
453            Self::CloseEngineSession { .. } => "close_engine_session",
454            Self::SendMessage { .. } => "send_message",
455            Self::TransitionMessage { .. } => "transition_message",
456            Self::IssueCommand { .. } => "issue_command",
457            Self::TransitionCommand { .. } => "transition_command",
458            Self::RecordCommandOutcome { .. } => "record_command_outcome",
459            Self::AcquireLease { .. } => "acquire_lease",
460            Self::RenewLease { .. } => "renew_lease",
461            Self::ReleaseLease { .. } => "release_lease",
462            Self::ExpireLease { .. } => "expire_lease",
463            Self::OpenGate { .. } => "open_gate",
464            Self::DecideGate { .. } => "decide_gate",
465            Self::GrantAuthority { .. } => "grant_authority",
466            Self::RevokeAuthority { .. } => "revoke_authority",
467            Self::RaiseAttention { .. } => "raise_attention",
468            Self::ResolveAttention { .. } => "resolve_attention",
469            Self::RecordEvidence { .. } => "record_evidence",
470            Self::RegisterWorktree { .. } => "register_worktree",
471            Self::UpdateWorktree { .. } => "update_worktree",
472            Self::ReleaseWorktree { .. } => "release_worktree",
473            Self::RegisterDispatchNode { .. } => "register_dispatch_node",
474            Self::TransitionDispatchNode { .. } => "transition_dispatch_node",
475            Self::WriteOrchestratorCheckpoint { .. } => "write_orchestrator_checkpoint",
476            Self::RecordRound { .. } => "record_round",
477            Self::RecordFinding { .. } => "record_finding",
478            Self::IngestRecord { .. } => "ingest_record",
479        }
480    }
481
482    /// Decode the typed body out of an envelope and require the envelope's
483    /// `command_type` to name the same variant.
484    ///
485    /// Routing metadata and the discriminant disagreeing is a refusal, not a
486    /// preference for one of them: a handler picked by `command_type` would
487    /// otherwise execute a body of some other shape.
488    pub fn from_envelope(envelope: &CommandEnvelope) -> Result<Self, CommandDecodeError> {
489        // Borrow the payload rather than cloning it: a command body can carry a
490        // 64 KiB inline payload, and this runs on every submission.
491        let command = <Self as serde::Deserialize>::deserialize(&envelope.payload)
492            .map_err(|e| CommandDecodeError::MalformedPayload(e.to_string()))?;
493        if envelope.command_type != command.command_type() {
494            return Err(CommandDecodeError::TypeMismatch {
495                declared: envelope.command_type.clone(),
496                body: command.command_type().to_owned(),
497            });
498        }
499        Ok(command)
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use crate::envelope::Origin;
507    use crate::ids::{IdempotencyKey, ProjectId};
508
509    fn activate() -> KernelCommand {
510        KernelCommand::ActivateKernel {
511            cutover_id: "00000000-0000-4000-8000-000000000001".to_owned(),
512            archive_manifest_sha256: "b".repeat(64),
513        }
514    }
515
516    fn envelope(command_type: &str, payload: Value) -> CommandEnvelope {
517        CommandEnvelope {
518            command_id: CommandId::new("cmd-1"),
519            project_id: ProjectId::new("system"),
520            command_type: command_type.into(),
521            schema_version: 1,
522            issued_at: Timestamp::new("2026-07-28T00:00:00Z"),
523            actor: Actor {
524                kind: "operator".into(),
525                id: None,
526            },
527            origin: Origin {
528                system: "gw".into(),
529                r#ref: None,
530            },
531            target_aggregate_type: Some("kernel".into()),
532            target_aggregate_id: None,
533            expected_version: Some(1),
534            idempotency_key: IdempotencyKey::new("kernel_activated:cut-1"),
535            causation_id: None,
536            correlation_id: None,
537            payload,
538        }
539    }
540
541    #[test]
542    fn every_variant_tag_matches_its_command_type() {
543        // One representative per aggregate family is not enough: a copy-paste
544        // slip in `command_type()` would silently route the wrong handler. Walk
545        // the serialized tag of a value from EVERY variant instead.
546        let all = all_variants();
547        assert_eq!(all.len(), 34, "the v1 command set is 34 variants");
548        for command in &all {
549            let json = serde_json::to_value(command).expect("serialize");
550            let tag = json["type"].as_str().expect("tagged with a string type");
551            assert_eq!(
552                tag,
553                command.command_type(),
554                "command_type() disagrees with the serde tag for {command:?}"
555            );
556        }
557        let mut tags: Vec<&str> = all.iter().map(|c| c.command_type()).collect();
558        tags.sort_unstable();
559        tags.dedup();
560        assert_eq!(tags.len(), all.len(), "two variants share a command_type");
561    }
562
563    #[test]
564    fn every_variant_round_trips_through_json() {
565        for command in all_variants() {
566            let json = serde_json::to_value(&command).expect("serialize");
567            let back: KernelCommand = serde_json::from_value(json).expect("deserialize");
568            assert_eq!(back, command);
569        }
570    }
571
572    #[test]
573    fn absent_optionals_are_omitted_not_null() {
574        let command = KernelCommand::CreateTask {
575            task_id: TaskId::new("task-1"),
576            kind: None,
577            title: None,
578            spec_ref: None,
579            project: None,
580            priority: None,
581            tracker_ref: None,
582        };
583        let json = serde_json::to_string(&command).expect("serialize");
584        assert_eq!(json, "{\"type\":\"create_task\",\"task_id\":\"task-1\"}");
585    }
586
587    #[test]
588    fn envelope_decode_requires_the_declared_type_to_match_the_body() {
589        let payload = serde_json::to_value(activate()).expect("serialize");
590        let good = envelope("activate_kernel", payload.clone());
591        assert_eq!(
592            KernelCommand::from_envelope(&good).expect("decodes"),
593            activate()
594        );
595
596        // The envelope claims one command, the body is another: refuse rather
597        // than let a handler chosen by command_type run a foreign body.
598        let mismatched = envelope("create_task", payload);
599        assert!(matches!(
600            KernelCommand::from_envelope(&mismatched),
601            Err(CommandDecodeError::TypeMismatch { .. })
602        ));
603
604        let garbage = envelope("activate_kernel", serde_json::json!({ "type": "nope" }));
605        assert!(matches!(
606            KernelCommand::from_envelope(&garbage),
607            Err(CommandDecodeError::MalformedPayload(_))
608        ));
609    }
610
611    #[test]
612    fn activate_kernel_is_the_pinned_wire_shape() {
613        let json = serde_json::to_value(activate()).expect("serialize");
614        assert_eq!(json["type"], "activate_kernel");
615        assert_eq!(json["cutover_id"], "00000000-0000-4000-8000-000000000001");
616        assert!(crate::blob::is_sha256_hex(
617            json["archive_manifest_sha256"]
618                .as_str()
619                .expect("hex string")
620        ));
621    }
622
623    /// One value of EVERY variant. Adding a command without extending this
624    /// list fails `every_variant_tag_matches_its_command_type`'s count pin.
625    fn all_variants() -> Vec<KernelCommand> {
626        let ts = || Timestamp::new("2026-07-28T00:00:00Z");
627        vec![
628            activate(),
629            KernelCommand::CreateTask {
630                task_id: TaskId::new("task-1"),
631                kind: Some("execution".into()),
632                title: Some("ship the kernel".into()),
633                spec_ref: None,
634                project: Some("proj-alpha".into()),
635                priority: Some(2),
636                tracker_ref: None,
637            },
638            KernelCommand::TransitionTask {
639                task_id: TaskId::new("task-1"),
640                to: TaskState::Working,
641                expected_version: 1,
642            },
643            KernelCommand::UpdateBudget {
644                attempt_id: AttemptId::new("att-1"),
645                expected_version: 2,
646                budget: Budget {
647                    max_tokens: Some(2_000_000),
648                    max_tool_calls: None,
649                    max_wall_ms: None,
650                    max_cost_micros: None,
651                },
652            },
653            KernelCommand::CreateAttempt {
654                attempt_id: AttemptId::new("att-1"),
655                task_id: TaskId::new("task-1"),
656                engine: EngineId::new("engine-a"),
657                capability: Some("code_write".into()),
658                role: None,
659                model_lane: Some("standard".into()),
660                permission_profile: None,
661                worktree_lease_id: Some(LeaseId::new("lease-1")),
662                base_sha: None,
663                budget: None,
664            },
665            KernelCommand::TransitionAttempt {
666                attempt_id: AttemptId::new("att-1"),
667                to: AttemptState::Blocked,
668                expected_version: 3,
669                receipt_id: Some(ReceiptId::new("r-1")),
670            },
671            KernelCommand::RecordAttemptOutcome {
672                attempt_id: AttemptId::new("att-1"),
673                expected_version: 4,
674                exit_code: Some(0),
675                provider_terminal_event: None,
676                result_valid: Some(true),
677                evidence_manifest_ref: None,
678            },
679            KernelCommand::OpenEngineSession {
680                engine_session_id: EngineSessionId::new("sess-1"),
681                attempt_id: AttemptId::new("att-1"),
682                engine: EngineId::new("engine-a"),
683                provider_session_ref: None,
684            },
685            KernelCommand::CloseEngineSession {
686                engine_session_id: EngineSessionId::new("sess-1"),
687            },
688            KernelCommand::SendMessage {
689                message_id: MessageId::new("msg-1"),
690                correlation_id: Some(CorrelationId::new("corr-7")),
691                reply_to: None,
692                sender: Some("orchestrator".into()),
693                recipient: Some("operator".into()),
694                channel: Some("chat".into()),
695                kind: Some("status_update".into()),
696                payload: Some(serde_json::json!({ "text": "verify passed" })),
697                deadline: None,
698            },
699            KernelCommand::TransitionMessage {
700                message_id: MessageId::new("msg-1"),
701                to: MessageState::Delivered,
702                expected_version: 1,
703                dead_letter_reason: None,
704            },
705            KernelCommand::IssueCommand {
706                command_id: CommandId::new("cmd-1"),
707                kind: "stop_attempt".into(),
708                targets: vec!["att-1".into()],
709                actor: None,
710            },
711            KernelCommand::TransitionCommand {
712                command_id: CommandId::new("cmd-1"),
713                to: CommandState::Targeted,
714                expected_version: 1,
715            },
716            KernelCommand::RecordCommandOutcome {
717                command_id: CommandId::new("cmd-1"),
718                expected_version: 3,
719                outcome: Outcome::Clean,
720            },
721            KernelCommand::AcquireLease {
722                lease_id: LeaseId::new("lease-1"),
723                mode: LeaseMode::Exclusive,
724                holder: Some("att-1".into()),
725                scope: Some("worktree".into()),
726                repo: None,
727                path: None,
728                branch: None,
729                base_sha: None,
730                expires_at: Some(ts()),
731            },
732            KernelCommand::RenewLease {
733                lease_id: LeaseId::new("lease-1"),
734                expected_version: 1,
735                expires_at: Some(ts()),
736            },
737            KernelCommand::ReleaseLease {
738                lease_id: LeaseId::new("lease-1"),
739                expected_version: 2,
740                disposition: Some("clean".into()),
741            },
742            KernelCommand::ExpireLease {
743                lease_id: LeaseId::new("lease-1"),
744                expected_version: 2,
745            },
746            KernelCommand::OpenGate {
747                gate_id: GateId::new("gate-1"),
748                attempt_id: Some(AttemptId::new("att-1")),
749                phase_ref: None,
750                kind: Some("review".into()),
751            },
752            KernelCommand::DecideGate {
753                gate_id: GateId::new("gate-1"),
754                expected_version: 1,
755                verdict: GateVerdict::Pass,
756                evidence_ref: None,
757            },
758            KernelCommand::GrantAuthority {
759                authority_grant_id: AuthorityGrantId::new("grant-1"),
760                grantee: Actor {
761                    kind: "orchestrator".into(),
762                    id: None,
763                },
764                action_class: "deploy".into(),
765                scope: None,
766                expires_at: None,
767            },
768            KernelCommand::RevokeAuthority {
769                authority_grant_id: AuthorityGrantId::new("grant-1"),
770                reason: None,
771            },
772            KernelCommand::RaiseAttention {
773                attention_item_id: AttentionItemId::new("att-item-1"),
774                kind: "risk_tag".into(),
775                summary: "data-migration pages".into(),
776                subject_ref: Some("task-1".into()),
777                raised_by: None,
778            },
779            KernelCommand::ResolveAttention {
780                attention_item_id: AttentionItemId::new("att-item-1"),
781                resolution: None,
782            },
783            KernelCommand::RecordEvidence {
784                evidence_id: EvidenceId::new("ev-1"),
785                kind: "transcript".into(),
786                r#ref: "blob://transcript".into(),
787                digest: None,
788                byte_size: Some(ByteCount::new(4096)),
789            },
790            KernelCommand::RegisterWorktree {
791                worktree_id: WorktreeId::new("wt-1"),
792                repo: "gridwork".into(),
793                path: "/w/kernel".into(),
794                branch: "feature/kernel".into(),
795                base_sha: None,
796                lease_id: Some(LeaseId::new("lease-1")),
797            },
798            KernelCommand::UpdateWorktree {
799                worktree_id: WorktreeId::new("wt-1"),
800                dirty: true,
801                unpushed: false,
802                base_sha: None,
803            },
804            KernelCommand::ReleaseWorktree {
805                worktree_id: WorktreeId::new("wt-1"),
806                disposition: None,
807            },
808            KernelCommand::RegisterDispatchNode {
809                dispatch_node_id: DispatchNodeId::new("node-1"),
810                parent_id: None,
811                attempt_id: Some(AttemptId::new("att-1")),
812                kind: "subagent".into(),
813                label: None,
814            },
815            KernelCommand::TransitionDispatchNode {
816                dispatch_node_id: DispatchNodeId::new("node-1"),
817                to: "finished".into(),
818                expected_version: 1,
819            },
820            KernelCommand::WriteOrchestratorCheckpoint {
821                checkpoint: OrchestratorCheckpoint {
822                    orchestrator_id: None,
823                    seq: crate::ids::Seq::new(42),
824                    native_session_ref: None,
825                    active_goal: None,
826                    active_step_ref: None,
827                    latest_command_ref: None,
828                    open_attempts: Some(vec![]),
829                    leases: None,
830                    pending_approvals: None,
831                    budget_cursor: None,
832                },
833            },
834            KernelCommand::RecordRound {
835                attempt_id: AttemptId::new("att-1"),
836                round: 2,
837                findings: RoundFindingSummary {
838                    total: 3,
839                    auto_fix: 1,
840                    ask_user: 1,
841                    no_op: 1,
842                },
843            },
844            KernelCommand::RecordFinding {
845                attempt_id: AttemptId::new("att-1"),
846                round: 2,
847                action: FindingAction::AutoFix,
848                summary: "unbounded read limit".into(),
849                subject_ref: None,
850            },
851            KernelCommand::IngestRecord {
852                kind: IngestionKind::Cost,
853                payload: serde_json::json!({ "spent_cost_micros": "5000000" }),
854                payload_ref: None,
855            },
856        ]
857    }
858}