1use serde::{Deserialize, Serialize};
4
5pub const A2A_PROTOCOL_NAME: &str = "oris.a2a";
6pub const A2A_PROTOCOL_VERSION: &str = "0.1.0-experimental";
7pub const A2A_PROTOCOL_VERSION_V1: &str = "1.0.0";
8pub const A2A_SUPPORTED_PROTOCOL_VERSIONS: [&str; 2] =
9 [A2A_PROTOCOL_VERSION_V1, A2A_PROTOCOL_VERSION];
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
12pub struct A2aProtocol {
13 pub name: String,
14 pub version: String,
15}
16
17impl A2aProtocol {
18 pub fn current() -> Self {
19 Self {
20 name: A2A_PROTOCOL_NAME.to_string(),
21 version: A2A_PROTOCOL_VERSION.to_string(),
22 }
23 }
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
27pub enum A2aCapability {
28 Coordination,
29 MutationProposal,
30 ReplayFeedback,
31 SupervisedDevloop,
32 EvolutionPublish,
33 EvolutionFetch,
34 EvolutionRevoke,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
38pub struct A2aHandshakeRequest {
39 pub agent_id: String,
40 pub role: AgentRole,
41 pub capability_level: AgentCapabilityLevel,
42 pub supported_protocols: Vec<A2aProtocol>,
43 pub advertised_capabilities: Vec<A2aCapability>,
44}
45
46impl A2aHandshakeRequest {
47 pub fn supports_protocol_version(&self, version: &str) -> bool {
48 self.supported_protocols
49 .iter()
50 .any(|protocol| protocol.name == A2A_PROTOCOL_NAME && protocol.version == version)
51 }
52
53 pub fn supports_current_protocol(&self) -> bool {
54 self.supports_protocol_version(A2A_PROTOCOL_VERSION)
55 }
56
57 pub fn negotiate_supported_protocol(&self) -> Option<A2aProtocol> {
58 for version in A2A_SUPPORTED_PROTOCOL_VERSIONS {
59 if self.supports_protocol_version(version) {
60 return Some(A2aProtocol {
61 name: A2A_PROTOCOL_NAME.to_string(),
62 version: version.to_string(),
63 });
64 }
65 }
66 None
67 }
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
71pub struct A2aHandshakeResponse {
72 pub accepted: bool,
73 pub negotiated_protocol: Option<A2aProtocol>,
74 pub enabled_capabilities: Vec<A2aCapability>,
75 pub message: Option<String>,
76 pub error: Option<A2aErrorEnvelope>,
77}
78
79impl A2aHandshakeResponse {
80 pub fn accept(enabled_capabilities: Vec<A2aCapability>) -> Self {
81 Self {
82 accepted: true,
83 negotiated_protocol: Some(A2aProtocol::current()),
84 enabled_capabilities,
85 message: Some("handshake accepted".to_string()),
86 error: None,
87 }
88 }
89
90 pub fn reject(code: A2aErrorCode, message: impl Into<String>, details: Option<String>) -> Self {
91 Self {
92 accepted: false,
93 negotiated_protocol: None,
94 enabled_capabilities: Vec::new(),
95 message: Some("handshake rejected".to_string()),
96 error: Some(A2aErrorEnvelope {
97 code,
98 message: message.into(),
99 retriable: true,
100 details,
101 }),
102 }
103 }
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
107pub enum A2aTaskLifecycleState {
108 Queued,
109 Running,
110 Succeeded,
111 Failed,
112 Cancelled,
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
116pub struct A2aTaskLifecycleEvent {
117 pub task_id: String,
118 pub state: A2aTaskLifecycleState,
119 pub summary: String,
120 pub updated_at_ms: u64,
121 pub error: Option<A2aErrorEnvelope>,
122}
123
124pub const A2A_TASK_SESSION_PROTOCOL_VERSION: &str = A2A_PROTOCOL_VERSION;
125
126#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
127pub enum A2aTaskSessionState {
128 Started,
129 Dispatched,
130 InProgress,
131 Completed,
132 Failed,
133 Cancelled,
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
137pub struct A2aTaskSessionStartRequest {
138 pub sender_id: String,
139 pub protocol_version: String,
140 pub task_id: String,
141 pub task_summary: String,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
145pub struct A2aTaskSessionDispatchRequest {
146 pub sender_id: String,
147 pub protocol_version: String,
148 pub dispatch_id: String,
149 pub summary: String,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
153pub struct A2aTaskSessionProgressRequest {
154 pub sender_id: String,
155 pub protocol_version: String,
156 pub progress_pct: u8,
157 pub summary: String,
158 pub retryable: bool,
159 pub retry_after_ms: Option<u64>,
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
163pub struct A2aTaskSessionCompletionRequest {
164 pub sender_id: String,
165 pub protocol_version: String,
166 pub terminal_state: A2aTaskLifecycleState,
167 pub summary: String,
168 pub retryable: bool,
169 pub retry_after_ms: Option<u64>,
170 pub failure_code: Option<A2aErrorCode>,
171 pub failure_details: Option<String>,
172 pub used_capsule: bool,
173 pub capsule_id: Option<String>,
174 pub reasoning_steps_avoided: u64,
175 pub fallback_reason: Option<String>,
176 pub reason_code: Option<ReplayFallbackReasonCode>,
177 pub repair_hint: Option<String>,
178 pub next_action: Option<ReplayFallbackNextAction>,
179 pub confidence: Option<u8>,
180 pub task_class_id: String,
181 pub task_label: String,
182}
183
184#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
185pub struct A2aTaskSessionProgressItem {
186 pub progress_pct: u8,
187 pub summary: String,
188 pub retryable: bool,
189 pub retry_after_ms: Option<u64>,
190 pub updated_at_ms: u64,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
194pub struct A2aTaskSessionAck {
195 pub session_id: String,
196 pub task_id: String,
197 pub state: A2aTaskSessionState,
198 pub summary: String,
199 pub retryable: bool,
200 pub retry_after_ms: Option<u64>,
201 pub updated_at_ms: u64,
202}
203
204#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
205pub struct A2aTaskSessionResult {
206 pub terminal_state: A2aTaskLifecycleState,
207 pub summary: String,
208 pub retryable: bool,
209 pub retry_after_ms: Option<u64>,
210 pub failure_code: Option<A2aErrorCode>,
211 pub failure_details: Option<String>,
212 pub replay_feedback: ReplayFeedback,
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
216pub struct A2aTaskSessionCompletionResponse {
217 pub ack: A2aTaskSessionAck,
218 pub result: A2aTaskSessionResult,
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
222pub struct A2aTaskSessionSnapshot {
223 pub session_id: String,
224 pub sender_id: String,
225 pub task_id: String,
226 pub protocol_version: String,
227 pub state: A2aTaskSessionState,
228 pub created_at_ms: u64,
229 pub updated_at_ms: u64,
230 pub dispatch_ids: Vec<String>,
231 pub progress: Vec<A2aTaskSessionProgressItem>,
232 pub result: Option<A2aTaskSessionResult>,
233}
234
235#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
236pub enum A2aErrorCode {
237 UnsupportedProtocol,
238 UnsupportedCapability,
239 ValidationFailed,
240 AuthorizationDenied,
241 Timeout,
242 Internal,
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
246pub struct A2aErrorEnvelope {
247 pub code: A2aErrorCode,
248 pub message: String,
249 pub retriable: bool,
250 pub details: Option<String>,
251}
252
253#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
254pub enum AgentCapabilityLevel {
255 A0,
256 A1,
257 A2,
258 A3,
259 A4,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
263pub enum ProposalTarget {
264 WorkspaceRoot,
265 Paths(Vec<String>),
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize)]
269pub struct AgentTask {
270 pub id: String,
271 pub description: String,
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
275pub enum AgentRole {
276 Planner,
277 Coder,
278 Repair,
279 Optimizer,
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
283pub enum CoordinationPrimitive {
284 Sequential,
285 Parallel,
286 Conditional,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
290pub struct CoordinationTask {
291 pub id: String,
292 pub role: AgentRole,
293 pub description: String,
294 pub depends_on: Vec<String>,
295}
296
297#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
298pub struct CoordinationMessage {
299 pub from_role: AgentRole,
300 pub to_role: AgentRole,
301 pub task_id: String,
302 pub content: String,
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
306pub struct CoordinationPlan {
307 pub root_goal: String,
308 pub primitive: CoordinationPrimitive,
309 pub tasks: Vec<CoordinationTask>,
310 pub timeout_ms: u64,
311 pub max_retries: u32,
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
315pub struct CoordinationResult {
316 pub completed_tasks: Vec<String>,
317 pub failed_tasks: Vec<String>,
318 pub messages: Vec<CoordinationMessage>,
319 pub summary: String,
320}
321
322#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
323pub struct MutationProposal {
324 pub intent: String,
325 pub files: Vec<String>,
326 pub expected_effect: String,
327}
328
329#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
330#[serde(rename_all = "snake_case")]
331pub enum MutationProposalContractReasonCode {
332 Accepted,
333 CandidateRejected,
334 MissingTargetFiles,
335 OutOfBoundsPath,
336 UnsupportedTaskClass,
337 ValidationBudgetExceeded,
338 ExpectedEvidenceMissing,
339 UnknownFailClosed,
340}
341
342#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "snake_case")]
344pub enum MutationProposalEvidence {
345 HumanApproval,
346 BoundedScope,
347 ValidationPass,
348 ExecutionAudit,
349}
350
351#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
352pub struct MutationProposalValidationBudget {
353 pub max_diff_bytes: usize,
354 pub max_changed_lines: usize,
355 pub validation_timeout_ms: u64,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
359pub struct ExecutionFeedback {
360 pub accepted: bool,
361 pub asset_state: Option<String>,
362 pub summary: String,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
366pub enum ReplayPlannerDirective {
367 SkipPlanner,
368 PlanFallback,
369}
370
371#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
372#[serde(rename_all = "snake_case")]
373pub enum ReplayFallbackReasonCode {
374 NoCandidateAfterSelect,
375 ScoreBelowThreshold,
376 CandidateHasNoCapsule,
377 MutationPayloadMissing,
378 PatchApplyFailed,
379 ValidationFailed,
380 UnmappedFallbackReason,
381}
382
383#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
384#[serde(rename_all = "snake_case")]
385pub enum ReplayFallbackNextAction {
386 PlanFromScratch,
387 ValidateSignalsThenPlan,
388 RebuildCapsule,
389 RegenerateMutationPayload,
390 RebasePatchAndRetry,
391 RepairAndRevalidate,
392 EscalateFailClosed,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
396pub struct ReplayFallbackContract {
397 pub reason_code: ReplayFallbackReasonCode,
398 pub fallback_reason: String,
399 pub repair_hint: String,
400 pub next_action: ReplayFallbackNextAction,
401 pub confidence: u8,
403}
404
405pub fn infer_replay_fallback_reason_code(reason: &str) -> Option<ReplayFallbackReasonCode> {
406 let normalized = reason.trim().to_ascii_lowercase();
407 if normalized.is_empty() {
408 return None;
409 }
410 if normalized == "no_candidate_after_select" || normalized.contains("no matching gene") {
411 return Some(ReplayFallbackReasonCode::NoCandidateAfterSelect);
412 }
413 if normalized == "score_below_threshold" || normalized.contains("below replay threshold") {
414 return Some(ReplayFallbackReasonCode::ScoreBelowThreshold);
415 }
416 if normalized == "candidate_has_no_capsule" || normalized.contains("has no capsule") {
417 return Some(ReplayFallbackReasonCode::CandidateHasNoCapsule);
418 }
419 if normalized == "mutation_payload_missing" || normalized.contains("payload missing") {
420 return Some(ReplayFallbackReasonCode::MutationPayloadMissing);
421 }
422 if normalized == "patch_apply_failed" || normalized.contains("patch apply failed") {
423 return Some(ReplayFallbackReasonCode::PatchApplyFailed);
424 }
425 if normalized == "validation_failed" || normalized.contains("validation failed") {
426 return Some(ReplayFallbackReasonCode::ValidationFailed);
427 }
428 None
429}
430
431pub fn normalize_replay_fallback_contract(
432 planner_directive: &ReplayPlannerDirective,
433 fallback_reason: Option<&str>,
434 reason_code: Option<ReplayFallbackReasonCode>,
435 repair_hint: Option<&str>,
436 next_action: Option<ReplayFallbackNextAction>,
437 confidence: Option<u8>,
438) -> Option<ReplayFallbackContract> {
439 if !matches!(planner_directive, ReplayPlannerDirective::PlanFallback) {
440 return None;
441 }
442
443 let normalized_reason = normalize_optional_text(fallback_reason);
444 let normalized_repair_hint = normalize_optional_text(repair_hint);
445 let mut resolved_reason_code = reason_code
446 .or_else(|| {
447 normalized_reason
448 .as_deref()
449 .and_then(infer_replay_fallback_reason_code)
450 })
451 .unwrap_or(ReplayFallbackReasonCode::UnmappedFallbackReason);
452 let mut defaults = replay_fallback_defaults(&resolved_reason_code);
453
454 let mut force_fail_closed = false;
455 if let Some(provided_action) = next_action {
456 if provided_action != defaults.next_action {
457 resolved_reason_code = ReplayFallbackReasonCode::UnmappedFallbackReason;
458 defaults = replay_fallback_defaults(&resolved_reason_code);
459 force_fail_closed = true;
460 }
461 }
462
463 Some(ReplayFallbackContract {
464 reason_code: resolved_reason_code,
465 fallback_reason: normalized_reason.unwrap_or_else(|| defaults.fallback_reason.to_string()),
466 repair_hint: normalized_repair_hint.unwrap_or_else(|| defaults.repair_hint.to_string()),
467 next_action: if force_fail_closed {
468 defaults.next_action
469 } else {
470 next_action.unwrap_or(defaults.next_action)
471 },
472 confidence: if force_fail_closed {
473 defaults.confidence
474 } else {
475 confidence.unwrap_or(defaults.confidence).min(100)
476 },
477 })
478}
479
480#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
481pub struct ReplayFeedback {
482 pub used_capsule: bool,
483 pub capsule_id: Option<String>,
484 pub planner_directive: ReplayPlannerDirective,
485 pub reasoning_steps_avoided: u64,
486 pub fallback_reason: Option<String>,
487 pub reason_code: Option<ReplayFallbackReasonCode>,
488 pub repair_hint: Option<String>,
489 pub next_action: Option<ReplayFallbackNextAction>,
490 pub confidence: Option<u8>,
491 pub task_class_id: String,
492 pub task_label: String,
493 pub summary: String,
494}
495
496#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
497#[serde(rename_all = "snake_case")]
498pub enum MutationNeededFailureReasonCode {
499 PolicyDenied,
500 ValidationFailed,
501 UnsafePatch,
502 Timeout,
503 MutationPayloadMissing,
504 UnknownFailClosed,
505}
506
507#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509pub enum MutationNeededRecoveryAction {
510 NarrowScopeAndRetry,
511 RepairAndRevalidate,
512 ProduceSafePatch,
513 ReduceExecutionBudget,
514 RegenerateMutationPayload,
515 EscalateFailClosed,
516}
517
518#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
519pub struct MutationNeededFailureContract {
520 pub reason_code: MutationNeededFailureReasonCode,
521 pub failure_reason: String,
522 pub recovery_hint: String,
523 pub recovery_action: MutationNeededRecoveryAction,
524 pub fail_closed: bool,
525}
526
527pub fn infer_mutation_needed_failure_reason_code(
528 reason: &str,
529) -> Option<MutationNeededFailureReasonCode> {
530 let normalized = reason.trim().to_ascii_lowercase();
531 if normalized.is_empty() {
532 return None;
533 }
534 if normalized.contains("mutation payload missing") || normalized == "mutation_payload_missing" {
535 return Some(MutationNeededFailureReasonCode::MutationPayloadMissing);
536 }
537 if normalized.contains("command timed out") || normalized.contains(" timeout") {
538 return Some(MutationNeededFailureReasonCode::Timeout);
539 }
540 if normalized.contains("patch rejected")
541 || normalized.contains("patch apply failed")
542 || normalized.contains("target violation")
543 || normalized.contains("unsafe patch")
544 {
545 return Some(MutationNeededFailureReasonCode::UnsafePatch);
546 }
547 if normalized.contains("validation failed") {
548 return Some(MutationNeededFailureReasonCode::ValidationFailed);
549 }
550 if normalized.contains("command denied by policy")
551 || normalized.contains("rejected task")
552 || normalized.contains("unsupported task outside the bounded scope")
553 || normalized.contains("budget exceeds bounded policy")
554 {
555 return Some(MutationNeededFailureReasonCode::PolicyDenied);
556 }
557 None
558}
559
560pub fn normalize_mutation_needed_failure_contract(
561 failure_reason: Option<&str>,
562 reason_code: Option<MutationNeededFailureReasonCode>,
563) -> MutationNeededFailureContract {
564 let normalized_reason = normalize_optional_text(failure_reason);
565 let resolved_reason_code = reason_code
566 .or_else(|| {
567 normalized_reason
568 .as_deref()
569 .and_then(infer_mutation_needed_failure_reason_code)
570 })
571 .unwrap_or(MutationNeededFailureReasonCode::UnknownFailClosed);
572 let defaults = mutation_needed_failure_defaults(&resolved_reason_code);
573
574 MutationNeededFailureContract {
575 reason_code: resolved_reason_code,
576 failure_reason: normalized_reason.unwrap_or_else(|| defaults.failure_reason.to_string()),
577 recovery_hint: defaults.recovery_hint.to_string(),
578 recovery_action: defaults.recovery_action,
579 fail_closed: true,
580 }
581}
582
583fn normalize_optional_text(value: Option<&str>) -> Option<String> {
584 let trimmed = value?.trim();
585 if trimmed.is_empty() {
586 None
587 } else {
588 Some(trimmed.to_string())
589 }
590}
591
592#[derive(Clone, Copy)]
593struct ReplayFallbackDefaults {
594 fallback_reason: &'static str,
595 repair_hint: &'static str,
596 next_action: ReplayFallbackNextAction,
597 confidence: u8,
598}
599
600fn replay_fallback_defaults(reason_code: &ReplayFallbackReasonCode) -> ReplayFallbackDefaults {
601 match reason_code {
602 ReplayFallbackReasonCode::NoCandidateAfterSelect => ReplayFallbackDefaults {
603 fallback_reason: "no matching gene",
604 repair_hint:
605 "No reusable capsule matched deterministic signals; run planner for a minimal patch.",
606 next_action: ReplayFallbackNextAction::PlanFromScratch,
607 confidence: 92,
608 },
609 ReplayFallbackReasonCode::ScoreBelowThreshold => ReplayFallbackDefaults {
610 fallback_reason: "candidate score below replay threshold",
611 repair_hint:
612 "Best replay candidate is below threshold; validate task signals and re-plan.",
613 next_action: ReplayFallbackNextAction::ValidateSignalsThenPlan,
614 confidence: 86,
615 },
616 ReplayFallbackReasonCode::CandidateHasNoCapsule => ReplayFallbackDefaults {
617 fallback_reason: "candidate gene has no capsule",
618 repair_hint: "Matched gene has no executable capsule; rebuild capsule from planner output.",
619 next_action: ReplayFallbackNextAction::RebuildCapsule,
620 confidence: 80,
621 },
622 ReplayFallbackReasonCode::MutationPayloadMissing => ReplayFallbackDefaults {
623 fallback_reason: "mutation payload missing from store",
624 repair_hint:
625 "Mutation payload is missing; regenerate and persist a minimal mutation payload.",
626 next_action: ReplayFallbackNextAction::RegenerateMutationPayload,
627 confidence: 76,
628 },
629 ReplayFallbackReasonCode::PatchApplyFailed => ReplayFallbackDefaults {
630 fallback_reason: "replay patch apply failed",
631 repair_hint: "Replay patch cannot be applied cleanly; rebase patch and retry planning.",
632 next_action: ReplayFallbackNextAction::RebasePatchAndRetry,
633 confidence: 68,
634 },
635 ReplayFallbackReasonCode::ValidationFailed => ReplayFallbackDefaults {
636 fallback_reason: "replay validation failed",
637 repair_hint: "Replay validation failed; produce a repair mutation and re-run validation.",
638 next_action: ReplayFallbackNextAction::RepairAndRevalidate,
639 confidence: 64,
640 },
641 ReplayFallbackReasonCode::UnmappedFallbackReason => ReplayFallbackDefaults {
642 fallback_reason: "unmapped replay fallback reason",
643 repair_hint:
644 "Fallback reason is unmapped; fail closed and require explicit planner intervention.",
645 next_action: ReplayFallbackNextAction::EscalateFailClosed,
646 confidence: 0,
647 },
648 }
649}
650
651#[derive(Clone, Copy)]
652struct MutationNeededFailureDefaults {
653 failure_reason: &'static str,
654 recovery_hint: &'static str,
655 recovery_action: MutationNeededRecoveryAction,
656}
657
658fn mutation_needed_failure_defaults(
659 reason_code: &MutationNeededFailureReasonCode,
660) -> MutationNeededFailureDefaults {
661 match reason_code {
662 MutationNeededFailureReasonCode::PolicyDenied => MutationNeededFailureDefaults {
663 failure_reason: "mutation needed denied by bounded execution policy",
664 recovery_hint:
665 "Narrow changed scope to the approved docs boundary and re-run with explicit approval.",
666 recovery_action: MutationNeededRecoveryAction::NarrowScopeAndRetry,
667 },
668 MutationNeededFailureReasonCode::ValidationFailed => MutationNeededFailureDefaults {
669 failure_reason: "mutation needed validation failed",
670 recovery_hint:
671 "Repair mutation and re-run validation to produce a deterministic pass before capture.",
672 recovery_action: MutationNeededRecoveryAction::RepairAndRevalidate,
673 },
674 MutationNeededFailureReasonCode::UnsafePatch => MutationNeededFailureDefaults {
675 failure_reason: "mutation needed rejected unsafe patch",
676 recovery_hint:
677 "Generate a safer minimal diff confined to approved paths and verify patch applicability.",
678 recovery_action: MutationNeededRecoveryAction::ProduceSafePatch,
679 },
680 MutationNeededFailureReasonCode::Timeout => MutationNeededFailureDefaults {
681 failure_reason: "mutation needed execution timed out",
682 recovery_hint:
683 "Reduce execution budget or split the mutation into smaller steps before retrying.",
684 recovery_action: MutationNeededRecoveryAction::ReduceExecutionBudget,
685 },
686 MutationNeededFailureReasonCode::MutationPayloadMissing => MutationNeededFailureDefaults {
687 failure_reason: "mutation payload missing from store",
688 recovery_hint: "Regenerate and persist mutation payload before retrying mutation-needed.",
689 recovery_action: MutationNeededRecoveryAction::RegenerateMutationPayload,
690 },
691 MutationNeededFailureReasonCode::UnknownFailClosed => MutationNeededFailureDefaults {
692 failure_reason: "mutation needed failed with unmapped reason",
693 recovery_hint:
694 "Unknown failure class; fail closed and require explicit maintainer triage before retry.",
695 recovery_action: MutationNeededRecoveryAction::EscalateFailClosed,
696 },
697 }
698}
699
700#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
701pub enum BoundedTaskClass {
702 DocsSingleFile,
703 DocsMultiFile,
704 CargoDepUpgrade,
707 LintFix,
710}
711
712#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
713pub struct MutationProposalScope {
714 pub task_class: BoundedTaskClass,
715 pub target_files: Vec<String>,
716}
717
718#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
719pub struct SelfEvolutionMutationProposalContract {
720 pub mutation_proposal: MutationProposal,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub proposal_scope: Option<MutationProposalScope>,
723 pub validation_budget: MutationProposalValidationBudget,
724 pub approval_required: bool,
725 pub expected_evidence: Vec<MutationProposalEvidence>,
726 pub summary: String,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub failure_reason: Option<String>,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub recovery_hint: Option<String>,
731 pub reason_code: MutationProposalContractReasonCode,
732 pub fail_closed: bool,
733}
734
735#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
736pub struct SelfEvolutionCandidateIntakeRequest {
737 pub issue_number: u64,
738 pub title: String,
739 pub body: String,
740 #[serde(default)]
741 pub labels: Vec<String>,
742 pub state: String,
743 #[serde(default)]
744 pub candidate_hint_paths: Vec<String>,
745}
746
747#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
749#[serde(rename_all = "snake_case")]
750pub enum AutonomousCandidateSource {
751 CiFailure,
752 TestRegression,
753 CompileRegression,
754 LintRegression,
755 RuntimeIncident,
756}
757
758#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
760#[serde(rename_all = "snake_case")]
761pub enum AutonomousIntakeReasonCode {
762 Accepted,
763 UnsupportedSignalClass,
764 AmbiguousSignal,
765 DuplicateCandidate,
766 UnknownFailClosed,
767}
768
769#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
772pub struct DiscoveredCandidate {
773 pub dedupe_key: String,
775 pub candidate_source: AutonomousCandidateSource,
777 pub candidate_class: Option<BoundedTaskClass>,
779 pub signals: Vec<String>,
781 pub accepted: bool,
783 pub reason_code: AutonomousIntakeReasonCode,
785 pub summary: String,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub failure_reason: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub recovery_hint: Option<String>,
791 pub fail_closed: bool,
793}
794
795#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
797pub struct AutonomousIntakeInput {
798 pub source_id: String,
800 pub candidate_source: AutonomousCandidateSource,
802 pub raw_signals: Vec<String>,
804}
805
806#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
809pub struct AutonomousIntakeOutput {
810 pub candidates: Vec<DiscoveredCandidate>,
811 pub accepted_count: usize,
812 pub denied_count: usize,
813}
814
815#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
819#[serde(rename_all = "snake_case")]
820pub enum AutonomousRiskTier {
821 Low,
823 Medium,
825 High,
827}
828
829#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
831#[serde(rename_all = "snake_case")]
832pub enum AutonomousPlanReasonCode {
833 Approved,
834 DeniedHighRisk,
835 DeniedLowFeasibility,
836 DeniedUnsupportedClass,
837 DeniedNoEvidence,
838 UnknownFailClosed,
839}
840
841#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
843pub struct AutonomousDenialCondition {
844 pub reason_code: AutonomousPlanReasonCode,
845 pub description: String,
846 pub recovery_hint: String,
847}
848
849#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
851pub struct AutonomousTaskPlan {
852 pub plan_id: String,
854 pub dedupe_key: String,
856 pub task_class: Option<BoundedTaskClass>,
858 pub risk_tier: AutonomousRiskTier,
860 pub feasibility_score: u8,
862 pub validation_budget: u8,
864 pub expected_evidence: Vec<String>,
866 pub approved: bool,
868 pub reason_code: AutonomousPlanReasonCode,
870 pub summary: String,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub denial_condition: Option<AutonomousDenialCondition>,
875 pub fail_closed: bool,
877}
878
879pub fn approve_autonomous_task_plan(
880 plan_id: impl Into<String>,
881 dedupe_key: impl Into<String>,
882 task_class: BoundedTaskClass,
883 risk_tier: AutonomousRiskTier,
884 feasibility_score: u8,
885 validation_budget: u8,
886 expected_evidence: Vec<String>,
887 summary: Option<&str>,
888) -> AutonomousTaskPlan {
889 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
890 format!("autonomous task plan approved for {task_class:?} at {risk_tier:?} risk")
891 });
892 AutonomousTaskPlan {
893 plan_id: plan_id.into(),
894 dedupe_key: dedupe_key.into(),
895 task_class: Some(task_class),
896 risk_tier,
897 feasibility_score,
898 validation_budget,
899 expected_evidence,
900 approved: true,
901 reason_code: AutonomousPlanReasonCode::Approved,
902 summary,
903 denial_condition: None,
904 fail_closed: false,
905 }
906}
907
908pub fn deny_autonomous_task_plan(
909 plan_id: impl Into<String>,
910 dedupe_key: impl Into<String>,
911 risk_tier: AutonomousRiskTier,
912 reason_code: AutonomousPlanReasonCode,
913) -> AutonomousTaskPlan {
914 let (description, recovery_hint) = match reason_code {
915 AutonomousPlanReasonCode::DeniedHighRisk => (
916 "task plan denied because risk tier is too high for autonomous execution",
917 "reduce blast radius by scoping the change to a single bounded file before retrying",
918 ),
919 AutonomousPlanReasonCode::DeniedLowFeasibility => (
920 "task plan denied because feasibility score is below the policy threshold",
921 "provide stronger evidence or narrow the task scope before retrying",
922 ),
923 AutonomousPlanReasonCode::DeniedUnsupportedClass => (
924 "task plan denied because task class is not supported for autonomous planning",
925 "route this task class through the supervised planning path instead",
926 ),
927 AutonomousPlanReasonCode::DeniedNoEvidence => (
928 "task plan denied because no evidence was available to assess feasibility",
929 "ensure signals and candidate class are populated before planning",
930 ),
931 AutonomousPlanReasonCode::UnknownFailClosed => (
932 "task plan failed with an unmapped reason; fail closed",
933 "require explicit maintainer triage before retry",
934 ),
935 AutonomousPlanReasonCode::Approved => (
936 "unexpected approved reason on deny path",
937 "use approve_autonomous_task_plan for approved outcomes",
938 ),
939 };
940 let summary = format!("autonomous task plan denied [{reason_code:?}]: {description}");
941 AutonomousTaskPlan {
942 plan_id: plan_id.into(),
943 dedupe_key: dedupe_key.into(),
944 task_class: None,
945 risk_tier,
946 feasibility_score: 0,
947 validation_budget: 0,
948 expected_evidence: Vec::new(),
949 approved: false,
950 reason_code,
951 summary,
952 denial_condition: Some(AutonomousDenialCondition {
953 reason_code,
954 description: description.to_string(),
955 recovery_hint: recovery_hint.to_string(),
956 }),
957 fail_closed: true,
958 }
959}
960
961#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
965#[serde(rename_all = "snake_case")]
966pub enum AutonomousApprovalMode {
967 AutoApproved,
969 RequiresHumanReview,
971}
972
973#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
975#[serde(rename_all = "snake_case")]
976pub enum AutonomousProposalReasonCode {
977 Proposed,
978 DeniedPlanNotApproved,
979 DeniedNoTargetScope,
980 DeniedWeakEvidence,
981 DeniedOutOfBounds,
982 UnknownFailClosed,
983}
984
985#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
987pub struct AutonomousProposalScope {
988 pub target_paths: Vec<String>,
990 pub scope_rationale: String,
992 pub max_files: u8,
994}
995
996#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
998pub struct AutonomousMutationProposal {
999 pub proposal_id: String,
1001 pub plan_id: String,
1003 pub dedupe_key: String,
1005 pub scope: Option<AutonomousProposalScope>,
1007 pub expected_evidence: Vec<String>,
1009 pub rollback_conditions: Vec<String>,
1011 pub approval_mode: AutonomousApprovalMode,
1013 pub proposed: bool,
1015 pub reason_code: AutonomousProposalReasonCode,
1017 pub summary: String,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub denial_condition: Option<AutonomousDenialCondition>,
1022 pub fail_closed: bool,
1024}
1025
1026pub fn approve_autonomous_mutation_proposal(
1028 proposal_id: impl Into<String>,
1029 plan_id: impl Into<String>,
1030 dedupe_key: impl Into<String>,
1031 scope: AutonomousProposalScope,
1032 expected_evidence: Vec<String>,
1033 rollback_conditions: Vec<String>,
1034 approval_mode: AutonomousApprovalMode,
1035 summary: Option<&str>,
1036) -> AutonomousMutationProposal {
1037 let dedupe_key = dedupe_key.into();
1038 let summary = summary
1039 .and_then(|s| {
1040 if s.trim().is_empty() {
1041 None
1042 } else {
1043 Some(s.to_string())
1044 }
1045 })
1046 .unwrap_or_else(|| format!("autonomous mutation proposal approved for {dedupe_key}"));
1047 AutonomousMutationProposal {
1048 proposal_id: proposal_id.into(),
1049 plan_id: plan_id.into(),
1050 dedupe_key,
1051 scope: Some(scope),
1052 expected_evidence,
1053 rollback_conditions,
1054 approval_mode,
1055 proposed: true,
1056 reason_code: AutonomousProposalReasonCode::Proposed,
1057 summary,
1058 denial_condition: None,
1059 fail_closed: false,
1060 }
1061}
1062
1063pub fn deny_autonomous_mutation_proposal(
1065 proposal_id: impl Into<String>,
1066 plan_id: impl Into<String>,
1067 dedupe_key: impl Into<String>,
1068 reason_code: AutonomousProposalReasonCode,
1069) -> AutonomousMutationProposal {
1070 let (description, recovery_hint) = match reason_code {
1071 AutonomousProposalReasonCode::DeniedPlanNotApproved => (
1072 "source plan was not approved; cannot generate proposal",
1073 "ensure the autonomous task plan is approved before proposing a mutation",
1074 ),
1075 AutonomousProposalReasonCode::DeniedNoTargetScope => (
1076 "no bounded target scope could be determined for this task class",
1077 "verify that the task class maps to a known set of target paths",
1078 ),
1079 AutonomousProposalReasonCode::DeniedWeakEvidence => (
1080 "the expected evidence set is empty or insufficient",
1081 "strengthen evidence requirements before reattempting proposal",
1082 ),
1083 AutonomousProposalReasonCode::DeniedOutOfBounds => (
1084 "proposal would mutate files outside of bounded policy scope",
1085 "restrict targets to explicitly allowed paths and retry",
1086 ),
1087 AutonomousProposalReasonCode::UnknownFailClosed => (
1088 "proposal generation failed with an unmapped reason; fail closed",
1089 "require explicit maintainer triage before retry",
1090 ),
1091 AutonomousProposalReasonCode::Proposed => (
1092 "unexpected proposed reason on deny path",
1093 "use approve_autonomous_mutation_proposal for proposed outcomes",
1094 ),
1095 };
1096 let dedupe_key = dedupe_key.into();
1097 let summary = format!("autonomous mutation proposal denied [{reason_code:?}]: {description}");
1098 AutonomousMutationProposal {
1099 proposal_id: proposal_id.into(),
1100 plan_id: plan_id.into(),
1101 dedupe_key,
1102 scope: None,
1103 expected_evidence: Vec::new(),
1104 rollback_conditions: Vec::new(),
1105 approval_mode: AutonomousApprovalMode::RequiresHumanReview,
1106 proposed: false,
1107 reason_code,
1108 summary,
1109 denial_condition: Some(AutonomousDenialCondition {
1110 reason_code: AutonomousPlanReasonCode::UnknownFailClosed,
1111 description: description.to_string(),
1112 recovery_hint: recovery_hint.to_string(),
1113 }),
1114 fail_closed: true,
1115 }
1116}
1117
1118#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1128#[serde(rename_all = "snake_case")]
1129pub enum TaskEquivalenceClass {
1130 DocumentationEdit,
1133 StaticAnalysisFix,
1135 DependencyManifestUpdate,
1137 Unclassified,
1139}
1140
1141#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1144pub struct EquivalenceExplanation {
1145 pub task_equivalence_class: TaskEquivalenceClass,
1147 pub rationale: String,
1149 pub matching_features: Vec<String>,
1151 pub replay_match_confidence: u8,
1153}
1154
1155#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1157#[serde(rename_all = "snake_case")]
1158pub enum SemanticReplayReasonCode {
1159 EquivalenceMatchApproved,
1161 LowConfidenceDenied,
1163 NoEquivalenceClassMatch,
1165 EquivalenceClassNotAllowed,
1168 UnknownFailClosed,
1170}
1171
1172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1178pub struct SemanticReplayDecision {
1179 pub evaluation_id: String,
1181 pub task_id: String,
1183 pub replay_decision: bool,
1185 pub equivalence_explanation: Option<EquivalenceExplanation>,
1187 pub reason_code: SemanticReplayReasonCode,
1189 pub summary: String,
1191 pub fail_closed: bool,
1194}
1195
1196pub fn approve_semantic_replay(
1198 evaluation_id: impl Into<String>,
1199 task_id: impl Into<String>,
1200 explanation: EquivalenceExplanation,
1201) -> SemanticReplayDecision {
1202 let summary = format!(
1203 "semantic replay approved [equivalence_class={:?}, confidence={}]",
1204 explanation.task_equivalence_class, explanation.replay_match_confidence
1205 );
1206 SemanticReplayDecision {
1207 evaluation_id: evaluation_id.into(),
1208 task_id: task_id.into(),
1209 replay_decision: true,
1210 reason_code: SemanticReplayReasonCode::EquivalenceMatchApproved,
1211 equivalence_explanation: Some(explanation),
1212 summary,
1213 fail_closed: false,
1214 }
1215}
1216
1217pub fn deny_semantic_replay(
1219 evaluation_id: impl Into<String>,
1220 task_id: impl Into<String>,
1221 reason_code: SemanticReplayReasonCode,
1222 context: impl Into<String>,
1223) -> SemanticReplayDecision {
1224 let context: String = context.into();
1225 let summary = format!("semantic replay denied [{reason_code:?}]: {context}");
1226 SemanticReplayDecision {
1227 evaluation_id: evaluation_id.into(),
1228 task_id: task_id.into(),
1229 replay_decision: false,
1230 equivalence_explanation: None,
1231 reason_code,
1232 summary,
1233 fail_closed: true,
1234 }
1235}
1236
1237#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1243#[serde(rename_all = "snake_case")]
1244pub enum AutonomousMergeGateStatus {
1245 MergeApproved,
1247 MergeBlocked,
1250}
1251
1252#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1254#[serde(rename_all = "snake_case")]
1255pub enum AutonomousReleaseGateStatus {
1256 ReleaseApproved,
1258 ReleaseBlocked,
1260}
1261
1262#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1264#[serde(rename_all = "snake_case")]
1265pub enum AutonomousPublishGateStatus {
1266 PublishApproved,
1268 PublishBlocked,
1270}
1271
1272#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1274#[serde(rename_all = "snake_case")]
1275pub enum KillSwitchState {
1276 Inactive,
1278 Active,
1280}
1281
1282#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1284#[serde(rename_all = "snake_case")]
1285pub enum AutonomousReleaseReasonCode {
1286 ApprovedForAutonomousRelease,
1288 TaskClassNotApproved,
1290 IncompleteStageEvidence,
1292 KillSwitchActive,
1294 RiskTierTooHigh,
1296 PostGateDriftDetected,
1298 UnknownFailClosed,
1300}
1301
1302#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1304pub struct RollbackPlan {
1305 pub rollback_id: String,
1307 pub description: String,
1309 pub actionable: bool,
1311}
1312
1313#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1315pub struct AutonomousReleaseGateDecision {
1316 pub gate_id: String,
1318 pub gate_summary: String,
1320 pub merge_gate_result: AutonomousMergeGateStatus,
1322 pub release_gate_result: AutonomousReleaseGateStatus,
1324 pub publish_gate_result: AutonomousPublishGateStatus,
1326 pub kill_switch_state: KillSwitchState,
1328 #[serde(default, skip_serializing_if = "Option::is_none")]
1330 pub rollback_plan: Option<RollbackPlan>,
1331 pub reason_code: AutonomousReleaseReasonCode,
1333 pub fail_closed: bool,
1336}
1337
1338pub fn approve_autonomous_release_gate(
1340 gate_id: impl Into<String>,
1341 task_id: impl Into<String>,
1342) -> AutonomousReleaseGateDecision {
1343 let task_id: String = task_id.into();
1344 let gate_summary =
1345 format!("autonomous release gate approved for task {task_id}: all gates passed");
1346 AutonomousReleaseGateDecision {
1347 gate_id: gate_id.into(),
1348 gate_summary,
1349 merge_gate_result: AutonomousMergeGateStatus::MergeApproved,
1350 release_gate_result: AutonomousReleaseGateStatus::ReleaseApproved,
1351 publish_gate_result: AutonomousPublishGateStatus::PublishApproved,
1352 kill_switch_state: KillSwitchState::Inactive,
1353 rollback_plan: None,
1354 reason_code: AutonomousReleaseReasonCode::ApprovedForAutonomousRelease,
1355 fail_closed: false,
1356 }
1357}
1358
1359pub fn deny_autonomous_release_gate(
1361 gate_id: impl Into<String>,
1362 task_id: impl Into<String>,
1363 reason_code: AutonomousReleaseReasonCode,
1364 kill_switch_state: KillSwitchState,
1365 detail: impl Into<String>,
1366 rollback_plan: Option<RollbackPlan>,
1367) -> AutonomousReleaseGateDecision {
1368 let task_id: String = task_id.into();
1369 let detail: String = detail.into();
1370 let gate_summary = format!("autonomous release gate denied for task {task_id}: {detail}");
1371 AutonomousReleaseGateDecision {
1372 gate_id: gate_id.into(),
1373 gate_summary,
1374 merge_gate_result: AutonomousMergeGateStatus::MergeBlocked,
1375 release_gate_result: AutonomousReleaseGateStatus::ReleaseBlocked,
1376 publish_gate_result: AutonomousPublishGateStatus::PublishBlocked,
1377 kill_switch_state,
1378 rollback_plan,
1379 reason_code,
1380 fail_closed: true,
1381 }
1382}
1383
1384#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1390#[serde(rename_all = "snake_case")]
1391pub enum AutonomousPrLaneStatus {
1392 PrReady,
1394 Denied,
1396}
1397
1398#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1400#[serde(rename_all = "snake_case")]
1401pub enum PrLaneApprovalState {
1402 ClassApproved,
1404 ClassNotApproved,
1406}
1407
1408#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1410#[serde(rename_all = "snake_case")]
1411pub enum AutonomousPrLaneReasonCode {
1412 ApprovedForAutonomousPr,
1414 TaskClassNotApproved,
1416 PatchEvidenceMissing,
1418 ValidationEvidenceMissing,
1420 RiskTierTooHigh,
1422 UnknownFailClosed,
1424}
1425
1426#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1428pub struct PrEvidenceBundle {
1429 pub patch_summary: String,
1431 pub validation_passed: bool,
1433 pub audit_trail: Vec<String>,
1435}
1436
1437#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1439pub struct AutonomousPrLaneDecision {
1440 pub pr_lane_id: String,
1442 pub delivery_summary: String,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 pub branch_name: Option<String>,
1448 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 pub pr_payload: Option<String>,
1451 #[serde(default, skip_serializing_if = "Option::is_none")]
1453 pub evidence_bundle: Option<PrEvidenceBundle>,
1454 pub pr_ready: bool,
1456 pub delivery_status: AutonomousPrLaneStatus,
1458 pub approval_state: PrLaneApprovalState,
1460 pub reason_code: AutonomousPrLaneReasonCode,
1462 pub fail_closed: bool,
1465}
1466
1467pub fn approve_autonomous_pr_lane(
1469 pr_lane_id: impl Into<String>,
1470 task_id: impl Into<String>,
1471 branch_name: impl Into<String>,
1472 evidence_bundle: PrEvidenceBundle,
1473) -> AutonomousPrLaneDecision {
1474 let task_id: String = task_id.into();
1475 let branch = branch_name.into();
1476 let pr_payload = format!("Autonomous PR for task {task_id} on branch {branch}");
1477 let delivery_summary =
1478 format!("autonomous PR lane approved for task {task_id}: branch {branch} ready");
1479 AutonomousPrLaneDecision {
1480 pr_lane_id: pr_lane_id.into(),
1481 delivery_summary,
1482 branch_name: Some(branch),
1483 pr_payload: Some(pr_payload),
1484 evidence_bundle: Some(evidence_bundle),
1485 pr_ready: true,
1486 delivery_status: AutonomousPrLaneStatus::PrReady,
1487 approval_state: PrLaneApprovalState::ClassApproved,
1488 reason_code: AutonomousPrLaneReasonCode::ApprovedForAutonomousPr,
1489 fail_closed: false,
1490 }
1491}
1492
1493pub fn deny_autonomous_pr_lane(
1495 pr_lane_id: impl Into<String>,
1496 task_id: impl Into<String>,
1497 reason_code: AutonomousPrLaneReasonCode,
1498 detail: impl Into<String>,
1499) -> AutonomousPrLaneDecision {
1500 let task_id: String = task_id.into();
1501 let detail: String = detail.into();
1502 let delivery_summary = format!("autonomous PR lane denied for task {task_id}: {detail}");
1503 AutonomousPrLaneDecision {
1504 pr_lane_id: pr_lane_id.into(),
1505 delivery_summary,
1506 branch_name: None,
1507 pr_payload: None,
1508 evidence_bundle: None,
1509 pr_ready: false,
1510 delivery_status: AutonomousPrLaneStatus::Denied,
1511 approval_state: PrLaneApprovalState::ClassNotApproved,
1512 reason_code,
1513 fail_closed: true,
1514 }
1515}
1516
1517#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1523#[serde(rename_all = "snake_case")]
1524pub enum ConfidenceState {
1525 Active,
1527 Decaying,
1530 Revalidating,
1533 Demoted,
1536 Quarantined,
1539}
1540
1541#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1543#[serde(rename_all = "snake_case")]
1544pub enum RevalidationOutcome {
1545 Passed,
1547 Failed,
1549 Pending,
1551 ErrorFailClosed,
1553}
1554
1555#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1557#[serde(rename_all = "snake_case")]
1558pub enum ConfidenceDemotionReasonCode {
1559 ConfidenceDecayThreshold,
1561 RepeatedReplayFailure,
1563 MaxFailureCountExceeded,
1565 ExplicitRevocation,
1567 UnknownFailClosed,
1569}
1570
1571#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1573#[serde(rename_all = "snake_case")]
1574pub enum ReplayEligibility {
1575 Eligible,
1577 Ineligible,
1579}
1580
1581#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1583pub struct ConfidenceRevalidationResult {
1584 pub revalidation_id: String,
1586 pub asset_id: String,
1588 pub confidence_state: ConfidenceState,
1590 pub revalidation_result: RevalidationOutcome,
1592 pub replay_eligibility: ReplayEligibility,
1594 pub summary: String,
1596 pub fail_closed: bool,
1599}
1600
1601#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1603pub struct DemotionDecision {
1604 pub demotion_id: String,
1606 pub asset_id: String,
1608 pub prior_state: ConfidenceState,
1610 pub new_state: ConfidenceState,
1612 pub reason_code: ConfidenceDemotionReasonCode,
1614 pub replay_eligibility: ReplayEligibility,
1616 pub summary: String,
1618 pub quarantine_transition: bool,
1620 pub fail_closed: bool,
1622}
1623
1624pub fn pass_confidence_revalidation(
1626 revalidation_id: impl Into<String>,
1627 asset_id: impl Into<String>,
1628 _prior_state: ConfidenceState,
1629) -> ConfidenceRevalidationResult {
1630 let asset_id: String = asset_id.into();
1631 let summary =
1632 format!("confidence revalidation passed for asset {asset_id}: restoring to Active");
1633 ConfidenceRevalidationResult {
1634 revalidation_id: revalidation_id.into(),
1635 asset_id,
1636 confidence_state: ConfidenceState::Active,
1637 revalidation_result: RevalidationOutcome::Passed,
1638 replay_eligibility: ReplayEligibility::Eligible,
1639 summary,
1640 fail_closed: false,
1641 }
1642}
1643
1644pub fn fail_confidence_revalidation(
1646 revalidation_id: impl Into<String>,
1647 asset_id: impl Into<String>,
1648 prior_state: ConfidenceState,
1649 outcome: RevalidationOutcome,
1650) -> ConfidenceRevalidationResult {
1651 let asset_id: String = asset_id.into();
1652 let summary = format!(
1653 "confidence revalidation failed for asset {asset_id} [{outcome:?}]: replay suspended"
1654 );
1655 ConfidenceRevalidationResult {
1656 revalidation_id: revalidation_id.into(),
1657 asset_id,
1658 confidence_state: prior_state,
1659 revalidation_result: outcome,
1660 replay_eligibility: ReplayEligibility::Ineligible,
1661 summary,
1662 fail_closed: true,
1663 }
1664}
1665
1666pub fn demote_asset(
1668 demotion_id: impl Into<String>,
1669 asset_id: impl Into<String>,
1670 prior_state: ConfidenceState,
1671 new_state: ConfidenceState,
1672 reason_code: ConfidenceDemotionReasonCode,
1673) -> DemotionDecision {
1674 let asset_id: String = asset_id.into();
1675 let quarantine_transition = new_state == ConfidenceState::Quarantined;
1676 let summary =
1677 format!("asset {asset_id} demoted from {prior_state:?} to {new_state:?} [{reason_code:?}]");
1678 DemotionDecision {
1679 demotion_id: demotion_id.into(),
1680 asset_id,
1681 prior_state,
1682 new_state,
1683 reason_code,
1684 replay_eligibility: ReplayEligibility::Ineligible,
1685 summary,
1686 quarantine_transition,
1687 fail_closed: true,
1688 }
1689}
1690
1691#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1692#[serde(rename_all = "snake_case")]
1693pub enum SelfEvolutionSelectionReasonCode {
1694 Accepted,
1695 IssueClosed,
1696 MissingEvolutionLabel,
1697 MissingFeatureLabel,
1698 ExcludedByLabel,
1699 UnsupportedCandidateScope,
1700 UnknownFailClosed,
1701}
1702
1703#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1704pub struct SelfEvolutionSelectionDecision {
1705 pub issue_number: u64,
1706 pub selected: bool,
1707 #[serde(default, skip_serializing_if = "Option::is_none")]
1708 pub candidate_class: Option<BoundedTaskClass>,
1709 pub summary: String,
1710 #[serde(default, skip_serializing_if = "Option::is_none")]
1711 pub reason_code: Option<SelfEvolutionSelectionReasonCode>,
1712 #[serde(default, skip_serializing_if = "Option::is_none")]
1713 pub failure_reason: Option<String>,
1714 #[serde(default, skip_serializing_if = "Option::is_none")]
1715 pub recovery_hint: Option<String>,
1716 pub fail_closed: bool,
1717}
1718
1719#[derive(Clone, Copy)]
1720struct SelfEvolutionSelectionDefaults {
1721 failure_reason: &'static str,
1722 recovery_hint: &'static str,
1723}
1724
1725fn self_evolution_selection_defaults(
1726 reason_code: &SelfEvolutionSelectionReasonCode,
1727) -> Option<SelfEvolutionSelectionDefaults> {
1728 match reason_code {
1729 SelfEvolutionSelectionReasonCode::Accepted => None,
1730 SelfEvolutionSelectionReasonCode::IssueClosed => Some(SelfEvolutionSelectionDefaults {
1731 failure_reason: "self-evolution candidate rejected because the issue is closed",
1732 recovery_hint: "Reopen the issue or choose an active open issue before retrying selection.",
1733 }),
1734 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => {
1735 Some(SelfEvolutionSelectionDefaults {
1736 failure_reason: "self-evolution candidate rejected because the issue is missing area/evolution",
1737 recovery_hint:
1738 "Add the area/evolution label or choose an issue already scoped to self-evolution.",
1739 })
1740 }
1741 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => {
1742 Some(SelfEvolutionSelectionDefaults {
1743 failure_reason: "self-evolution candidate rejected because the issue is missing type/feature",
1744 recovery_hint:
1745 "Add the type/feature label or narrow the issue to a bounded feature slice before retrying.",
1746 })
1747 }
1748 SelfEvolutionSelectionReasonCode::ExcludedByLabel => Some(SelfEvolutionSelectionDefaults {
1749 failure_reason: "self-evolution candidate rejected by an excluded issue label",
1750 recovery_hint:
1751 "Remove the excluded label or choose a non-duplicate, non-invalid, actionable issue.",
1752 }),
1753 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1754 Some(SelfEvolutionSelectionDefaults {
1755 failure_reason:
1756 "self-evolution candidate rejected because the hinted file scope is outside the bounded docs policy",
1757 recovery_hint:
1758 "Narrow candidate paths to the approved docs/*.md boundary before retrying selection.",
1759 })
1760 }
1761 SelfEvolutionSelectionReasonCode::UnknownFailClosed => Some(SelfEvolutionSelectionDefaults {
1762 failure_reason: "self-evolution candidate failed with an unmapped selection reason",
1763 recovery_hint: "Unknown selection failure; fail closed and require explicit maintainer triage before retry.",
1764 }),
1765 }
1766}
1767
1768pub fn accept_self_evolution_selection_decision(
1769 issue_number: u64,
1770 candidate_class: BoundedTaskClass,
1771 summary: Option<&str>,
1772) -> SelfEvolutionSelectionDecision {
1773 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1774 format!("selected GitHub issue #{issue_number} as a bounded self-evolution candidate")
1775 });
1776 SelfEvolutionSelectionDecision {
1777 issue_number,
1778 selected: true,
1779 candidate_class: Some(candidate_class),
1780 summary,
1781 reason_code: Some(SelfEvolutionSelectionReasonCode::Accepted),
1782 failure_reason: None,
1783 recovery_hint: None,
1784 fail_closed: false,
1785 }
1786}
1787
1788pub fn reject_self_evolution_selection_decision(
1789 issue_number: u64,
1790 reason_code: SelfEvolutionSelectionReasonCode,
1791 failure_reason: Option<&str>,
1792 summary: Option<&str>,
1793) -> SelfEvolutionSelectionDecision {
1794 let defaults = self_evolution_selection_defaults(&reason_code)
1795 .unwrap_or(SelfEvolutionSelectionDefaults {
1796 failure_reason: "self-evolution candidate rejected",
1797 recovery_hint:
1798 "Review candidate selection inputs and retry within the bounded self-evolution policy.",
1799 });
1800 let failure_reason = normalize_optional_text(failure_reason)
1801 .unwrap_or_else(|| defaults.failure_reason.to_string());
1802 let reason_code_key = match reason_code {
1803 SelfEvolutionSelectionReasonCode::Accepted => "accepted",
1804 SelfEvolutionSelectionReasonCode::IssueClosed => "issue_closed",
1805 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => "missing_evolution_label",
1806 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => "missing_feature_label",
1807 SelfEvolutionSelectionReasonCode::ExcludedByLabel => "excluded_by_label",
1808 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1809 "unsupported_candidate_scope"
1810 }
1811 SelfEvolutionSelectionReasonCode::UnknownFailClosed => "unknown_fail_closed",
1812 };
1813 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1814 format!(
1815 "rejected GitHub issue #{issue_number} as a self-evolution candidate [{reason_code_key}]"
1816 )
1817 });
1818
1819 SelfEvolutionSelectionDecision {
1820 issue_number,
1821 selected: false,
1822 candidate_class: None,
1823 summary,
1824 reason_code: Some(reason_code),
1825 failure_reason: Some(failure_reason),
1826 recovery_hint: Some(defaults.recovery_hint.to_string()),
1827 fail_closed: true,
1828 }
1829}
1830
1831pub fn accept_discovered_candidate(
1832 dedupe_key: impl Into<String>,
1833 candidate_source: AutonomousCandidateSource,
1834 candidate_class: BoundedTaskClass,
1835 signals: Vec<String>,
1836 summary: Option<&str>,
1837) -> DiscoveredCandidate {
1838 let summary = normalize_optional_text(summary)
1839 .unwrap_or_else(|| format!("accepted autonomous candidate from {candidate_source:?}"));
1840 DiscoveredCandidate {
1841 dedupe_key: dedupe_key.into(),
1842 candidate_source,
1843 candidate_class: Some(candidate_class),
1844 signals,
1845 accepted: true,
1846 reason_code: AutonomousIntakeReasonCode::Accepted,
1847 summary,
1848 failure_reason: None,
1849 recovery_hint: None,
1850 fail_closed: false,
1851 }
1852}
1853
1854pub fn deny_discovered_candidate(
1855 dedupe_key: impl Into<String>,
1856 candidate_source: AutonomousCandidateSource,
1857 signals: Vec<String>,
1858 reason_code: AutonomousIntakeReasonCode,
1859) -> DiscoveredCandidate {
1860 let (failure_reason, recovery_hint) = match reason_code {
1861 AutonomousIntakeReasonCode::UnsupportedSignalClass => (
1862 "signal class is not supported by the bounded evolution policy",
1863 "review supported candidate signal classes and filter input before retry",
1864 ),
1865 AutonomousIntakeReasonCode::AmbiguousSignal => (
1866 "signals do not map to a unique bounded candidate class",
1867 "provide more specific signal tokens or triage manually before resubmitting",
1868 ),
1869 AutonomousIntakeReasonCode::DuplicateCandidate => (
1870 "an equivalent candidate has already been discovered in this intake window",
1871 "deduplicate signals before resubmitting or check the existing candidate queue",
1872 ),
1873 AutonomousIntakeReasonCode::UnknownFailClosed => (
1874 "candidate intake failed with an unmapped reason; fail closed",
1875 "require explicit maintainer triage before retry",
1876 ),
1877 AutonomousIntakeReasonCode::Accepted => (
1878 "unexpected accepted reason on deny path",
1879 "use accept_discovered_candidate for accepted outcomes",
1880 ),
1881 };
1882 let summary =
1883 format!("denied autonomous candidate from {candidate_source:?}: {failure_reason}");
1884 DiscoveredCandidate {
1885 dedupe_key: dedupe_key.into(),
1886 candidate_source,
1887 candidate_class: None,
1888 signals,
1889 accepted: false,
1890 reason_code,
1891 summary,
1892 failure_reason: Some(failure_reason.to_string()),
1893 recovery_hint: Some(recovery_hint.to_string()),
1894 fail_closed: true,
1895 }
1896}
1897
1898#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1899pub struct HumanApproval {
1900 pub approved: bool,
1901 pub approver: Option<String>,
1902 pub note: Option<String>,
1903}
1904
1905#[derive(Clone, Debug, Serialize, Deserialize)]
1906pub struct SupervisedDevloopRequest {
1907 pub task: AgentTask,
1908 pub proposal: MutationProposal,
1909 pub approval: HumanApproval,
1910}
1911
1912#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1913pub enum SupervisedDevloopStatus {
1914 AwaitingApproval,
1915 RejectedByPolicy,
1916 FailedClosed,
1917 Executed,
1918}
1919
1920#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1921#[serde(rename_all = "snake_case")]
1922pub enum SupervisedDeliveryStatus {
1923 Prepared,
1924 Denied,
1925}
1926
1927#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1928#[serde(rename_all = "snake_case")]
1929pub enum SupervisedDeliveryApprovalState {
1930 Approved,
1931 MissingExplicitApproval,
1932}
1933
1934#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1935#[serde(rename_all = "snake_case")]
1936pub enum SupervisedDeliveryReasonCode {
1937 DeliveryPrepared,
1938 AwaitingApproval,
1939 DeliveryEvidenceMissing,
1940 ValidationEvidenceMissing,
1941 UnsupportedTaskScope,
1942 InconsistentDeliveryEvidence,
1943 UnknownFailClosed,
1944}
1945
1946#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1947pub struct SupervisedDeliveryContract {
1948 pub delivery_summary: String,
1949 #[serde(default, skip_serializing_if = "Option::is_none")]
1950 pub branch_name: Option<String>,
1951 #[serde(default, skip_serializing_if = "Option::is_none")]
1952 pub pr_title: Option<String>,
1953 #[serde(default, skip_serializing_if = "Option::is_none")]
1954 pub pr_summary: Option<String>,
1955 pub delivery_status: SupervisedDeliveryStatus,
1956 pub approval_state: SupervisedDeliveryApprovalState,
1957 pub reason_code: SupervisedDeliveryReasonCode,
1958 #[serde(default)]
1959 pub fail_closed: bool,
1960 #[serde(default, skip_serializing_if = "Option::is_none")]
1961 pub recovery_hint: Option<String>,
1962}
1963
1964#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1965#[serde(rename_all = "snake_case")]
1966pub enum SupervisedExecutionDecision {
1967 AwaitingApproval,
1968 ReplayHit,
1969 PlannerFallback,
1970 RejectedByPolicy,
1971 FailedClosed,
1972}
1973
1974#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1975#[serde(rename_all = "snake_case")]
1976pub enum SupervisedValidationOutcome {
1977 NotRun,
1978 Passed,
1979 FailedClosed,
1980}
1981
1982#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1983#[serde(rename_all = "snake_case")]
1984pub enum SupervisedExecutionReasonCode {
1985 AwaitingHumanApproval,
1986 ReplayHit,
1987 ReplayFallback,
1988 PolicyDenied,
1989 ValidationFailed,
1990 UnsafePatch,
1991 Timeout,
1992 MutationPayloadMissing,
1993 UnknownFailClosed,
1994}
1995#[derive(Clone, Debug, Serialize, Deserialize)]
1996pub struct SupervisedDevloopOutcome {
1997 pub task_id: String,
1998 pub task_class: Option<BoundedTaskClass>,
1999 pub status: SupervisedDevloopStatus,
2000 pub execution_decision: SupervisedExecutionDecision,
2001 #[serde(default, skip_serializing_if = "Option::is_none")]
2002 pub replay_outcome: Option<ReplayFeedback>,
2003 #[serde(default, skip_serializing_if = "Option::is_none")]
2004 pub fallback_reason: Option<String>,
2005 pub validation_outcome: SupervisedValidationOutcome,
2006 pub evidence_summary: String,
2007 #[serde(default, skip_serializing_if = "Option::is_none")]
2008 pub reason_code: Option<SupervisedExecutionReasonCode>,
2009 #[serde(default, skip_serializing_if = "Option::is_none")]
2010 pub recovery_hint: Option<String>,
2011 pub execution_feedback: Option<ExecutionFeedback>,
2012 #[serde(default, skip_serializing_if = "Option::is_none")]
2013 pub failure_contract: Option<MutationNeededFailureContract>,
2014 pub summary: String,
2015}
2016
2017#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
2018#[serde(rename_all = "snake_case")]
2019pub enum SelfEvolutionAuditConsistencyResult {
2020 Consistent,
2021 Inconsistent,
2022}
2023
2024#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
2025#[serde(rename_all = "snake_case")]
2026pub enum SelfEvolutionAcceptanceGateReasonCode {
2027 Accepted,
2028 MissingSelectionEvidence,
2029 MissingProposalEvidence,
2030 MissingApprovalEvidence,
2031 MissingExecutionEvidence,
2032 MissingDeliveryEvidence,
2033 InconsistentReasonCodeMatrix,
2034 UnknownFailClosed,
2035}
2036
2037#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2038pub struct SelfEvolutionApprovalEvidence {
2039 pub approval_required: bool,
2040 pub approved: bool,
2041 #[serde(default, skip_serializing_if = "Option::is_none")]
2042 pub approver: Option<String>,
2043}
2044
2045#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2046pub struct SelfEvolutionDeliveryOutcome {
2047 pub delivery_status: SupervisedDeliveryStatus,
2048 pub approval_state: SupervisedDeliveryApprovalState,
2049 pub reason_code: SupervisedDeliveryReasonCode,
2050}
2051
2052#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2053pub struct SelfEvolutionReasonCodeMatrix {
2054 #[serde(default, skip_serializing_if = "Option::is_none")]
2055 pub selection_reason_code: Option<SelfEvolutionSelectionReasonCode>,
2056 pub proposal_reason_code: MutationProposalContractReasonCode,
2057 #[serde(default, skip_serializing_if = "Option::is_none")]
2058 pub execution_reason_code: Option<SupervisedExecutionReasonCode>,
2059 pub delivery_reason_code: SupervisedDeliveryReasonCode,
2060}
2061
2062#[derive(Clone, Debug, Serialize, Deserialize)]
2063pub struct SelfEvolutionAcceptanceGateInput {
2064 pub selection_decision: SelfEvolutionSelectionDecision,
2065 pub proposal_contract: SelfEvolutionMutationProposalContract,
2066 pub supervised_request: SupervisedDevloopRequest,
2067 pub execution_outcome: SupervisedDevloopOutcome,
2068 pub delivery_contract: SupervisedDeliveryContract,
2069}
2070
2071#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2072pub struct SelfEvolutionAcceptanceGateContract {
2073 pub acceptance_gate_summary: String,
2074 pub audit_consistency_result: SelfEvolutionAuditConsistencyResult,
2075 pub approval_evidence: SelfEvolutionApprovalEvidence,
2076 pub delivery_outcome: SelfEvolutionDeliveryOutcome,
2077 pub reason_code_matrix: SelfEvolutionReasonCodeMatrix,
2078 pub fail_closed: bool,
2079 pub reason_code: SelfEvolutionAcceptanceGateReasonCode,
2080 #[serde(default, skip_serializing_if = "Option::is_none")]
2081 pub recovery_hint: Option<String>,
2082}
2083
2084#[cfg(test)]
2085mod tests {
2086 use super::*;
2087
2088 fn handshake_request_with_versions(versions: &[&str]) -> A2aHandshakeRequest {
2089 A2aHandshakeRequest {
2090 agent_id: "agent-test".into(),
2091 role: AgentRole::Planner,
2092 capability_level: AgentCapabilityLevel::A2,
2093 supported_protocols: versions
2094 .iter()
2095 .map(|version| A2aProtocol {
2096 name: A2A_PROTOCOL_NAME.into(),
2097 version: (*version).into(),
2098 })
2099 .collect(),
2100 advertised_capabilities: vec![A2aCapability::Coordination],
2101 }
2102 }
2103
2104 #[test]
2105 fn negotiate_supported_protocol_prefers_v1_when_available() {
2106 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION, A2A_PROTOCOL_VERSION_V1]);
2107 let negotiated = req
2108 .negotiate_supported_protocol()
2109 .expect("expected protocol negotiation success");
2110 assert_eq!(negotiated.name, A2A_PROTOCOL_NAME);
2111 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION_V1);
2112 }
2113
2114 #[test]
2115 fn negotiate_supported_protocol_falls_back_to_experimental() {
2116 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION]);
2117 let negotiated = req
2118 .negotiate_supported_protocol()
2119 .expect("expected protocol negotiation success");
2120 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION);
2121 }
2122
2123 #[test]
2124 fn negotiate_supported_protocol_returns_none_without_overlap() {
2125 let req = handshake_request_with_versions(&["0.0.1"]);
2126 assert!(req.negotiate_supported_protocol().is_none());
2127 }
2128
2129 #[test]
2130 fn normalize_replay_fallback_contract_maps_known_reason() {
2131 let contract = normalize_replay_fallback_contract(
2132 &ReplayPlannerDirective::PlanFallback,
2133 Some("no matching gene"),
2134 None,
2135 None,
2136 None,
2137 None,
2138 )
2139 .expect("contract should exist");
2140
2141 assert_eq!(
2142 contract.reason_code,
2143 ReplayFallbackReasonCode::NoCandidateAfterSelect
2144 );
2145 assert_eq!(
2146 contract.next_action,
2147 ReplayFallbackNextAction::PlanFromScratch
2148 );
2149 assert_eq!(contract.confidence, 92);
2150 }
2151
2152 #[test]
2153 fn normalize_replay_fallback_contract_fails_closed_for_unknown_reason() {
2154 let contract = normalize_replay_fallback_contract(
2155 &ReplayPlannerDirective::PlanFallback,
2156 Some("something unexpected"),
2157 None,
2158 None,
2159 None,
2160 None,
2161 )
2162 .expect("contract should exist");
2163
2164 assert_eq!(
2165 contract.reason_code,
2166 ReplayFallbackReasonCode::UnmappedFallbackReason
2167 );
2168 assert_eq!(
2169 contract.next_action,
2170 ReplayFallbackNextAction::EscalateFailClosed
2171 );
2172 assert_eq!(contract.confidence, 0);
2173 }
2174
2175 #[test]
2176 fn normalize_replay_fallback_contract_rejects_conflicting_next_action() {
2177 let contract = normalize_replay_fallback_contract(
2178 &ReplayPlannerDirective::PlanFallback,
2179 Some("replay validation failed"),
2180 Some(ReplayFallbackReasonCode::ValidationFailed),
2181 None,
2182 Some(ReplayFallbackNextAction::PlanFromScratch),
2183 Some(88),
2184 )
2185 .expect("contract should exist");
2186
2187 assert_eq!(
2188 contract.reason_code,
2189 ReplayFallbackReasonCode::UnmappedFallbackReason
2190 );
2191 assert_eq!(
2192 contract.next_action,
2193 ReplayFallbackNextAction::EscalateFailClosed
2194 );
2195 assert_eq!(contract.confidence, 0);
2196 }
2197
2198 #[test]
2199 fn normalize_mutation_needed_failure_contract_maps_policy_denied() {
2200 let contract = normalize_mutation_needed_failure_contract(
2201 Some("supervised devloop rejected task because it is outside bounded scope"),
2202 None,
2203 );
2204
2205 assert_eq!(
2206 contract.reason_code,
2207 MutationNeededFailureReasonCode::PolicyDenied
2208 );
2209 assert_eq!(
2210 contract.recovery_action,
2211 MutationNeededRecoveryAction::NarrowScopeAndRetry
2212 );
2213 assert!(contract.fail_closed);
2214 }
2215
2216 #[test]
2217 fn normalize_mutation_needed_failure_contract_maps_timeout() {
2218 let contract = normalize_mutation_needed_failure_contract(
2219 Some("command timed out: git apply --check patch.diff"),
2220 None,
2221 );
2222
2223 assert_eq!(
2224 contract.reason_code,
2225 MutationNeededFailureReasonCode::Timeout
2226 );
2227 assert_eq!(
2228 contract.recovery_action,
2229 MutationNeededRecoveryAction::ReduceExecutionBudget
2230 );
2231 assert!(contract.fail_closed);
2232 }
2233
2234 #[test]
2235 fn normalize_mutation_needed_failure_contract_fails_closed_for_unknown_reason() {
2236 let contract =
2237 normalize_mutation_needed_failure_contract(Some("unexpected runner panic"), None);
2238
2239 assert_eq!(
2240 contract.reason_code,
2241 MutationNeededFailureReasonCode::UnknownFailClosed
2242 );
2243 assert_eq!(
2244 contract.recovery_action,
2245 MutationNeededRecoveryAction::EscalateFailClosed
2246 );
2247 assert!(contract.fail_closed);
2248 }
2249
2250 #[test]
2251 fn reject_self_evolution_selection_decision_maps_closed_issue_defaults() {
2252 let decision = reject_self_evolution_selection_decision(
2253 234,
2254 SelfEvolutionSelectionReasonCode::IssueClosed,
2255 None,
2256 None,
2257 );
2258
2259 assert!(!decision.selected);
2260 assert_eq!(decision.issue_number, 234);
2261 assert_eq!(
2262 decision.reason_code,
2263 Some(SelfEvolutionSelectionReasonCode::IssueClosed)
2264 );
2265 assert!(decision.fail_closed);
2266 assert!(decision
2267 .failure_reason
2268 .as_deref()
2269 .is_some_and(|reason| reason.contains("closed")));
2270 assert!(decision.recovery_hint.is_some());
2271 }
2272
2273 #[test]
2274 fn accept_self_evolution_selection_decision_marks_candidate_selected() {
2275 let decision =
2276 accept_self_evolution_selection_decision(235, BoundedTaskClass::DocsSingleFile, None);
2277
2278 assert!(decision.selected);
2279 assert_eq!(decision.issue_number, 235);
2280 assert_eq!(
2281 decision.candidate_class,
2282 Some(BoundedTaskClass::DocsSingleFile)
2283 );
2284 assert_eq!(
2285 decision.reason_code,
2286 Some(SelfEvolutionSelectionReasonCode::Accepted)
2287 );
2288 assert!(!decision.fail_closed);
2289 assert_eq!(decision.failure_reason, None);
2290 assert_eq!(decision.recovery_hint, None);
2291 }
2292}
2293
2294#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2296pub enum HubTrustTier {
2297 Full,
2299 ReadOnly,
2301}
2302
2303#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
2305pub enum HubOperationClass {
2306 Hello,
2307 Fetch,
2308 Publish,
2309 Revoke,
2310 TaskClaim,
2311 TaskComplete,
2312 WorkerRegister,
2313 Recipe,
2314 Session,
2315 Dispute,
2316 Swarm,
2317}
2318
2319impl HubOperationClass {
2320 pub fn is_read_only(&self) -> bool {
2322 matches!(self, HubOperationClass::Hello | HubOperationClass::Fetch)
2323 }
2324}
2325
2326#[derive(Clone, Debug, Serialize, Deserialize)]
2328pub struct HubProfile {
2329 pub hub_id: String,
2330 pub base_url: String,
2331 pub trust_tier: HubTrustTier,
2332 pub priority: u32,
2334 pub health_url: Option<String>,
2336}
2337
2338impl HubProfile {
2339 pub fn allows_operation(&self, operation: &HubOperationClass) -> bool {
2341 match &self.trust_tier {
2342 HubTrustTier::Full => true,
2343 HubTrustTier::ReadOnly => operation.is_read_only(),
2344 }
2345 }
2346}
2347
2348#[derive(Clone, Debug)]
2350pub struct HubSelectionPolicy {
2351 pub allowed_tiers_for_operation: Vec<(HubOperationClass, Vec<HubTrustTier>)>,
2353 pub default_allowed_tiers: Vec<HubTrustTier>,
2355}
2356
2357impl Default for HubSelectionPolicy {
2358 fn default() -> Self {
2359 Self {
2360 allowed_tiers_for_operation: vec![
2361 (
2362 HubOperationClass::Hello,
2363 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
2364 ),
2365 (
2366 HubOperationClass::Fetch,
2367 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
2368 ),
2369 (HubOperationClass::Publish, vec![HubTrustTier::Full]),
2371 (HubOperationClass::Revoke, vec![HubTrustTier::Full]),
2372 (HubOperationClass::TaskClaim, vec![HubTrustTier::Full]),
2373 (HubOperationClass::TaskComplete, vec![HubTrustTier::Full]),
2374 (HubOperationClass::WorkerRegister, vec![HubTrustTier::Full]),
2375 (HubOperationClass::Recipe, vec![HubTrustTier::Full]),
2376 (HubOperationClass::Session, vec![HubTrustTier::Full]),
2377 (HubOperationClass::Dispute, vec![HubTrustTier::Full]),
2378 (HubOperationClass::Swarm, vec![HubTrustTier::Full]),
2379 ],
2380 default_allowed_tiers: vec![HubTrustTier::Full],
2381 }
2382 }
2383}
2384
2385impl HubSelectionPolicy {
2386 pub fn allowed_tiers(&self, operation: &HubOperationClass) -> &[HubTrustTier] {
2388 self.allowed_tiers_for_operation
2389 .iter()
2390 .find(|(op, _)| op == operation)
2391 .map(|(_, tiers)| tiers.as_slice())
2392 .unwrap_or(&self.default_allowed_tiers)
2393 }
2394}