1use 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#[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 ActivateKernel {
45 cutover_id: String,
46 archive_manifest_sha256: String,
49 },
50
51 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 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 #[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 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 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 IssueCommand {
190 command_id: CommandId,
191 kind: String,
193 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 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 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 GrantAuthority {
280 authority_grant_id: AuthorityGrantId,
281 grantee: Actor,
282 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 RaiseAttention {
300 attention_item_id: AttentionItemId,
301 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 RecordEvidence {
320 evidence_id: EvidenceId,
321 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 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 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 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 to: String,
381 expected_version: u32,
382 },
383
384 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 IngestRecord {
405 kind: IngestionKind,
406 #[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#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum CommandDecodeError {
420 TypeMismatch { declared: String, body: String },
422 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 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 pub fn from_envelope(envelope: &CommandEnvelope) -> Result<Self, CommandDecodeError> {
489 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 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 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 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}