Skip to main content

oris_agent_contract/
lib.rs

1//! Proposal-only runtime contract for external agents.
2
3use 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    /// Confidence score in [0, 100].
402    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    /// Dependency version bump: restricted to `Cargo.toml` / `Cargo.lock`
705    /// paths, version fields only, max 5 manifests.
706    CargoDepUpgrade,
707    /// Lint / formatting fix: auto-fixable `cargo fmt` or `cargo clippy --fix`
708    /// changes, no logic modifications, max 5 source files.
709    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/// Signal source for an autonomously discovered candidate.
748#[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/// Reason code for the outcome of autonomous candidate classification.
759#[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/// A candidate discovered autonomously from CI or runtime signals without
770/// a caller-supplied issue number.
771#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
772pub struct DiscoveredCandidate {
773    /// Stable identity hash, deterministic for the same raw signals.
774    pub dedupe_key: String,
775    /// Classified signal source.
776    pub candidate_source: AutonomousCandidateSource,
777    /// Normalised candidate class (reuses `BoundedTaskClass`).
778    pub candidate_class: Option<BoundedTaskClass>,
779    /// Normalised signal tokens used as the discovered work description.
780    pub signals: Vec<String>,
781    /// Whether this candidate was accepted for further work.
782    pub accepted: bool,
783    /// Outcome reason code.
784    pub reason_code: AutonomousIntakeReasonCode,
785    /// Human-readable summary.
786    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    /// Fail-closed flag: true on any non-accepted outcome.
792    pub fail_closed: bool,
793}
794
795/// Input for autonomous candidate discovery from raw diagnostic output.
796#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
797pub struct AutonomousIntakeInput {
798    /// Raw source identifier (e.g. CI run ID, log stream name).
799    pub source_id: String,
800    /// Classified origin of the raw signals.
801    pub candidate_source: AutonomousCandidateSource,
802    /// Raw text lines from diagnostics, test output, or incident logs.
803    pub raw_signals: Vec<String>,
804}
805
806/// Output of autonomous candidate intake: one or more discovered candidates
807/// (deduplicated) plus any that were denied with fail-closed reason codes.
808#[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// ── AUTO-02: Bounded Task Planning and Risk Scoring ──────────────────────────
816
817/// Risk tier assigned to an autonomous task plan.
818#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
819#[serde(rename_all = "snake_case")]
820pub enum AutonomousRiskTier {
821    /// Minimal blast radius, fully reversible, single-file scope.
822    Low,
823    /// Multi-file scope or non-trivial dependency changes.
824    Medium,
825    /// High blast radius, wide impact, or unknown effect on public API.
826    High,
827}
828
829/// Reason code for the outcome of autonomous task planning.
830#[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/// A denial condition attached to a rejected autonomous task plan.
842#[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/// An approved or denied autonomous task plan produced from a discovered candidate.
850#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
851pub struct AutonomousTaskPlan {
852    /// Stable identity derived from the originating `DiscoveredCandidate.dedupe_key`.
853    pub plan_id: String,
854    /// The input candidate this plan was derived from.
855    pub dedupe_key: String,
856    /// Normalised task class (same as candidate class when approved).
857    pub task_class: Option<BoundedTaskClass>,
858    /// Assigned risk tier.
859    pub risk_tier: AutonomousRiskTier,
860    /// Feasibility score in [0, 100]; 0 means not feasible.
861    pub feasibility_score: u8,
862    /// Estimated validation budget (number of validation stages required).
863    pub validation_budget: u8,
864    /// Evidence templates required for this plan class.
865    pub expected_evidence: Vec<String>,
866    /// Whether the plan was approved for proposal generation.
867    pub approved: bool,
868    /// Planning outcome reason code.
869    pub reason_code: AutonomousPlanReasonCode,
870    /// Short human-readable summary of the planning outcome.
871    pub summary: String,
872    /// Present when the plan was denied.
873    #[serde(default, skip_serializing_if = "Option::is_none")]
874    pub denial_condition: Option<AutonomousDenialCondition>,
875    /// Fail-closed flag: true on any non-approved outcome.
876    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// ── AUTO-03: Autonomous mutation proposal contracts ───────────────────────────
962
963/// Approval mode for an autonomous mutation proposal.
964#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
965#[serde(rename_all = "snake_case")]
966pub enum AutonomousApprovalMode {
967    /// Proposal is self-approved within bounded policy; no human gate required.
968    AutoApproved,
969    /// Proposal requires explicit human review before execution.
970    RequiresHumanReview,
971}
972
973/// Stable reason code for an autonomous mutation proposal outcome.
974#[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/// Bounded file scope for an autonomous mutation proposal.
986#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
987pub struct AutonomousProposalScope {
988    /// Target files or paths that may be mutated.
989    pub target_paths: Vec<String>,
990    /// Human-readable rationale for why these targets are in scope.
991    pub scope_rationale: String,
992    /// Maximum number of files that may be changed in this proposal.
993    pub max_files: u8,
994}
995
996/// A machine-readable mutation proposal generated from an approved autonomous plan.
997#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
998pub struct AutonomousMutationProposal {
999    /// Stable proposal identity hash.
1000    pub proposal_id: String,
1001    /// Matches `AutonomousTaskPlan.plan_id`.
1002    pub plan_id: String,
1003    /// Matches `AutonomousTaskPlan.dedupe_key`.
1004    pub dedupe_key: String,
1005    /// Bounded file scope for this mutation.
1006    pub scope: Option<AutonomousProposalScope>,
1007    /// Expected evidence templates (cargo commands, lint, tests, etc.).
1008    pub expected_evidence: Vec<String>,
1009    /// Conditions under which the mutation must be rolled back.
1010    pub rollback_conditions: Vec<String>,
1011    /// Approval mode for this proposal.
1012    pub approval_mode: AutonomousApprovalMode,
1013    /// Whether this proposal was successfully generated.
1014    pub proposed: bool,
1015    /// Stable outcome reason code.
1016    pub reason_code: AutonomousProposalReasonCode,
1017    /// Human-readable summary.
1018    pub summary: String,
1019    /// Structured denial information, populated when `proposed` is false.
1020    #[serde(default, skip_serializing_if = "Option::is_none")]
1021    pub denial_condition: Option<AutonomousDenialCondition>,
1022    /// True on any non-proposed outcome — enforces fail-closed semantics.
1023    pub fail_closed: bool,
1024}
1025
1026/// Construct an approved `AutonomousMutationProposal` from a valid plan.
1027pub 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
1063/// Construct a denied fail-closed `AutonomousMutationProposal`.
1064pub 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// ──────────────────────────────────────────────────────────────────────────────
1119// AUTO-04: Semantic Task-Class Generalization Beyond Normalized Signals
1120// ──────────────────────────────────────────────────────────────────────────────
1121
1122/// Broader semantic equivalence families for replay generalization.
1123///
1124/// A `TaskEquivalenceClass` groups bounded task classes that share enough
1125/// semantic properties to allow cross-task replay within the family while
1126/// keeping false-positive replay rates at zero for unrelated work.
1127#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1128#[serde(rename_all = "snake_case")]
1129pub enum TaskEquivalenceClass {
1130    /// Any documentation edit — single-file or multi-file docs changes are
1131    /// in the same semantic family.
1132    DocumentationEdit,
1133    /// Pure static-analysis or compiler-driven code fixes (lints, clippy, fmt).
1134    StaticAnalysisFix,
1135    /// Dependency manifest updates (Cargo.toml, Cargo.lock upgrades).
1136    DependencyManifestUpdate,
1137    /// Catch-all: task does not belong to any recognized equivalence family.
1138    Unclassified,
1139}
1140
1141/// Human-readable and machine-auditable explanation for why two tasks are
1142/// considered semantically equivalent for replay selection purposes.
1143#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1144pub struct EquivalenceExplanation {
1145    /// The equivalence class that was matched.
1146    pub task_equivalence_class: TaskEquivalenceClass,
1147    /// Short human-readable rationale for the equivalence match.
1148    pub rationale: String,
1149    /// Specific features that were used to determine equivalence.
1150    pub matching_features: Vec<String>,
1151    /// Confidence score in [0, 100] that this is a true equivalence match.
1152    pub replay_match_confidence: u8,
1153}
1154
1155/// Reason code explaining the outcome of a semantic replay evaluation.
1156#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1157#[serde(rename_all = "snake_case")]
1158pub enum SemanticReplayReasonCode {
1159    /// Task matched a known equivalence class and replay is permitted.
1160    EquivalenceMatchApproved,
1161    /// Task is in an approved class but confidence is below the minimum threshold.
1162    LowConfidenceDenied,
1163    /// Task did not match any approved semantic equivalence class.
1164    NoEquivalenceClassMatch,
1165    /// Replay was not permitted because the equivalence class is not on the
1166    /// allowed list for the current risk tier.
1167    EquivalenceClassNotAllowed,
1168    /// Semantic evaluation failed with an unexpected state; fail closed.
1169    UnknownFailClosed,
1170}
1171
1172/// Decision produced by semantic replay evaluation.
1173///
1174/// Consumers must check `replay_decision` and `fail_closed` before attempting
1175/// any replay operation. When `fail_closed` is `true`, replay must not proceed
1176/// regardless of `replay_decision`.
1177#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1178pub struct SemanticReplayDecision {
1179    /// Unique identifier for this evaluation.
1180    pub evaluation_id: String,
1181    /// The task identifier being evaluated.
1182    pub task_id: String,
1183    /// Whether semantic replay is permitted for this task.
1184    pub replay_decision: bool,
1185    /// The equivalence explanation that drove the decision, if any.
1186    pub equivalence_explanation: Option<EquivalenceExplanation>,
1187    /// The reason code for this decision.
1188    pub reason_code: SemanticReplayReasonCode,
1189    /// Human-readable decision summary.
1190    pub summary: String,
1191    /// Safety gate. When `true`, replay must be blocked regardless of
1192    /// `replay_decision`.
1193    pub fail_closed: bool,
1194}
1195
1196/// Construct an approved `SemanticReplayDecision`.
1197pub 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
1217/// Construct a denied `SemanticReplayDecision`.
1218pub 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// ──────────────────────────────────────────────────────────────────────────────
1238// AUTO-07: Fail-Closed Autonomous Merge and Release Gate For Narrow Safe Lanes
1239// ──────────────────────────────────────────────────────────────────────────────
1240
1241/// Status of the autonomous merge gate.
1242#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1243#[serde(rename_all = "snake_case")]
1244pub enum AutonomousMergeGateStatus {
1245    /// All merge gate checks passed; merge may proceed.
1246    MergeApproved,
1247    /// A merge gate check failed or evidence was missing; merge must not
1248    /// proceed.
1249    MergeBlocked,
1250}
1251
1252/// Status of the autonomous release gate.
1253#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1254#[serde(rename_all = "snake_case")]
1255pub enum AutonomousReleaseGateStatus {
1256    /// All release gate checks passed; release may proceed.
1257    ReleaseApproved,
1258    /// A release gate check failed; release must not proceed.
1259    ReleaseBlocked,
1260}
1261
1262/// Status of the autonomous publish gate.
1263#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1264#[serde(rename_all = "snake_case")]
1265pub enum AutonomousPublishGateStatus {
1266    /// All publish gate checks passed; publish may proceed.
1267    PublishApproved,
1268    /// A publish gate check failed; publish must not proceed.
1269    PublishBlocked,
1270}
1271
1272/// Kill-switch state for the autonomous release lane.
1273#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1274#[serde(rename_all = "snake_case")]
1275pub enum KillSwitchState {
1276    /// Kill switch is not active; lane may continue.
1277    Inactive,
1278    /// Kill switch is active; lane must halt immediately.
1279    Active,
1280}
1281
1282/// Reason codes for the autonomous release gate.
1283#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1284#[serde(rename_all = "snake_case")]
1285pub enum AutonomousReleaseReasonCode {
1286    /// All gates passed; autonomous release is approved.
1287    ApprovedForAutonomousRelease,
1288    /// Task class is not in the approved narrow safe set.
1289    TaskClassNotApproved,
1290    /// Evidence is incomplete or missing from a prior stage.
1291    IncompleteStageEvidence,
1292    /// Kill switch is active; lane must halt.
1293    KillSwitchActive,
1294    /// Risk tier exceeds policy boundary.
1295    RiskTierTooHigh,
1296    /// Post-gate drift detected; rollback required.
1297    PostGateDriftDetected,
1298    /// Fail-closed fallback when reason cannot be determined.
1299    UnknownFailClosed,
1300}
1301
1302/// Rollback plan attached to a blocked or drifted autonomous release.
1303#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1304pub struct RollbackPlan {
1305    /// Identifier for the rollback action.
1306    pub rollback_id: String,
1307    /// Human-readable description of the rollback steps.
1308    pub description: String,
1309    /// Whether the rollback is immediately actionable.
1310    pub actionable: bool,
1311}
1312
1313/// Combined gate decision record for the autonomous merge and release lane.
1314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1315pub struct AutonomousReleaseGateDecision {
1316    /// Unique identifier for this gate evaluation.
1317    pub gate_id: String,
1318    /// Human-readable summary of the gate decision.
1319    pub gate_summary: String,
1320    /// Result of the merge gate check.
1321    pub merge_gate_result: AutonomousMergeGateStatus,
1322    /// Result of the release gate check.
1323    pub release_gate_result: AutonomousReleaseGateStatus,
1324    /// Result of the publish gate check.
1325    pub publish_gate_result: AutonomousPublishGateStatus,
1326    /// Current kill-switch state at evaluation time.
1327    pub kill_switch_state: KillSwitchState,
1328    /// Rollback plan; populated when any gate is blocked or drift detected.
1329    #[serde(default, skip_serializing_if = "Option::is_none")]
1330    pub rollback_plan: Option<RollbackPlan>,
1331    /// Machine-readable reason code.
1332    pub reason_code: AutonomousReleaseReasonCode,
1333    /// Safety gate: when `true` the lane must not proceed under any
1334    /// circumstance.
1335    pub fail_closed: bool,
1336}
1337
1338/// Construct an approved `AutonomousReleaseGateDecision`.
1339pub 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
1359/// Construct a denied `AutonomousReleaseGateDecision`.
1360pub 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// ──────────────────────────────────────────────────────────────────────────────
1385// AUTO-06: Bounded Autonomous PR Lane For Low-Risk Task Classes
1386// ──────────────────────────────────────────────────────────────────────────────
1387
1388/// Status of an autonomous PR lane decision.
1389#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1390#[serde(rename_all = "snake_case")]
1391pub enum AutonomousPrLaneStatus {
1392    /// All gates passed; PR can be opened autonomously.
1393    PrReady,
1394    /// A gate failed or evidence was missing; PR must not be opened.
1395    Denied,
1396}
1397
1398/// Approval state for the autonomous PR lane.
1399#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1400#[serde(rename_all = "snake_case")]
1401pub enum PrLaneApprovalState {
1402    /// The task class is explicitly approved for autonomous PR creation.
1403    ClassApproved,
1404    /// The task class is not approved for autonomous PR creation.
1405    ClassNotApproved,
1406}
1407
1408/// Reason codes for the autonomous PR lane gate.
1409#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1410#[serde(rename_all = "snake_case")]
1411pub enum AutonomousPrLaneReasonCode {
1412    /// All gates passed; task class is explicitly approved.
1413    ApprovedForAutonomousPr,
1414    /// Task class is not in the approved low-risk set.
1415    TaskClassNotApproved,
1416    /// Required patch evidence is absent.
1417    PatchEvidenceMissing,
1418    /// Validation evidence is missing or incomplete.
1419    ValidationEvidenceMissing,
1420    /// Risk tier is too high for autonomous PR.
1421    RiskTierTooHigh,
1422    /// Fail-closed fallback when the reason cannot be determined.
1423    UnknownFailClosed,
1424}
1425
1426/// Evidence bundle required before autonomous PR creation.
1427#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1428pub struct PrEvidenceBundle {
1429    /// Human-readable summary of the patch.
1430    pub patch_summary: String,
1431    /// Whether the validation gate has passed for this patch.
1432    pub validation_passed: bool,
1433    /// Abbreviated audit trail or evidence key list for reviewer inspection.
1434    pub audit_trail: Vec<String>,
1435}
1436
1437/// Decision record for the autonomous PR lane.
1438#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1439pub struct AutonomousPrLaneDecision {
1440    /// Unique identifier for this PR lane evaluation.
1441    pub pr_lane_id: String,
1442    /// Human-readable summary of the delivery decision.
1443    pub delivery_summary: String,
1444    /// Branch name that would be created; populated only when `pr_ready` is
1445    /// `true`.
1446    #[serde(default, skip_serializing_if = "Option::is_none")]
1447    pub branch_name: Option<String>,
1448    /// Structured PR payload (title + body sketch) for the autonomous PR.
1449    #[serde(default, skip_serializing_if = "Option::is_none")]
1450    pub pr_payload: Option<String>,
1451    /// Combined evidence bundle governing PR creation eligibility.
1452    #[serde(default, skip_serializing_if = "Option::is_none")]
1453    pub evidence_bundle: Option<PrEvidenceBundle>,
1454    /// Whether the PR is ready to be opened.
1455    pub pr_ready: bool,
1456    /// Gate status.
1457    pub delivery_status: AutonomousPrLaneStatus,
1458    /// Approval state for the task class.
1459    pub approval_state: PrLaneApprovalState,
1460    /// Machine-readable reason code.
1461    pub reason_code: AutonomousPrLaneReasonCode,
1462    /// Safety gate: when `true` the lane must not proceed under any
1463    /// circumstance.
1464    pub fail_closed: bool,
1465}
1466
1467/// Construct an approved `AutonomousPrLaneDecision`.
1468pub 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
1493/// Construct a denied `AutonomousPrLaneDecision`.
1494pub 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// ──────────────────────────────────────────────────────────────────────────────
1518// AUTO-05: Continuous Confidence Revalidation and Asset Demotion
1519// ──────────────────────────────────────────────────────────────────────────────
1520
1521/// Current confidence lifecycle state of a reusable asset.
1522#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1523#[serde(rename_all = "snake_case")]
1524pub enum ConfidenceState {
1525    /// Asset is healthy and eligible for replay.
1526    Active,
1527    /// Asset confidence has decayed below the warning threshold; still eligible
1528    /// but flagged for revalidation.
1529    Decaying,
1530    /// Asset is undergoing shadow-mode replay revalidation; normal reuse
1531    /// continues but evidence is accumulated.
1532    Revalidating,
1533    /// Asset has been demoted after failed reuse; replay is suspended pending
1534    /// explicit re-promotion.
1535    Demoted,
1536    /// Asset is quarantined after repeated failures; replay is blocked until
1537    /// explicit triage clears it.
1538    Quarantined,
1539}
1540
1541/// Outcome of a single confidence revalidation round.
1542#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1543#[serde(rename_all = "snake_case")]
1544pub enum RevalidationOutcome {
1545    /// Revalidation completed and confidence was restored above threshold.
1546    Passed,
1547    /// Revalidation completed but confidence remains below threshold.
1548    Failed,
1549    /// Revalidation is still running (shadow phase not yet complete).
1550    Pending,
1551    /// Revalidation encountered an error; treat as failed for safety.
1552    ErrorFailClosed,
1553}
1554
1555/// Reason code for a demotion or quarantine transition.
1556#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1557#[serde(rename_all = "snake_case")]
1558pub enum ConfidenceDemotionReasonCode {
1559    /// Asset was demoted because confidence decayed below the demotion threshold.
1560    ConfidenceDecayThreshold,
1561    /// Asset was demoted due to repeated failed reuse (replay failures).
1562    RepeatedReplayFailure,
1563    /// Asset was quarantined after surpassing the maximum failure count.
1564    MaxFailureCountExceeded,
1565    /// Asset was revoked by explicit maintainer action.
1566    ExplicitRevocation,
1567    /// Demotion failed with an unmapped state; fail closed.
1568    UnknownFailClosed,
1569}
1570
1571/// Whether an asset is currently eligible for replay selection.
1572#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1573#[serde(rename_all = "snake_case")]
1574pub enum ReplayEligibility {
1575    /// Asset is eligible; replay may proceed.
1576    Eligible,
1577    /// Asset is not eligible; replay must not proceed.
1578    Ineligible,
1579}
1580
1581/// Full revalidation decision produced after a confidence evaluation round.
1582#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1583pub struct ConfidenceRevalidationResult {
1584    /// Unique identifier for this revalidation run.
1585    pub revalidation_id: String,
1586    /// The asset being revalidated (gene id or capsule id).
1587    pub asset_id: String,
1588    /// Current confidence state before this revalidation.
1589    pub confidence_state: ConfidenceState,
1590    /// Outcome of this revalidation round.
1591    pub revalidation_result: RevalidationOutcome,
1592    /// Whether replay is eligible after this evaluation.
1593    pub replay_eligibility: ReplayEligibility,
1594    /// Human-readable summary of the revalidation outcome.
1595    pub summary: String,
1596    /// Safety gate. When `true`, replay must not proceed regardless of
1597    /// `replay_eligibility`.
1598    pub fail_closed: bool,
1599}
1600
1601/// Demotion or quarantine transition event.
1602#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1603pub struct DemotionDecision {
1604    /// Unique identifier for this demotion event.
1605    pub demotion_id: String,
1606    /// The asset being demoted or quarantined.
1607    pub asset_id: String,
1608    /// Prior state before this transition.
1609    pub prior_state: ConfidenceState,
1610    /// New state after this transition.
1611    pub new_state: ConfidenceState,
1612    /// Reason for the demotion.
1613    pub reason_code: ConfidenceDemotionReasonCode,
1614    /// Replay eligibility after this transition.
1615    pub replay_eligibility: ReplayEligibility,
1616    /// Human-readable summary.
1617    pub summary: String,
1618    /// Whether this demotion resulted in a quarantine transition.
1619    pub quarantine_transition: bool,
1620    /// Safety gate. Always `true` for any demotion event.
1621    pub fail_closed: bool,
1622}
1623
1624/// Construct a passing `ConfidenceRevalidationResult`.
1625pub 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
1644/// Construct a failing `ConfidenceRevalidationResult`.
1645pub 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
1666/// Construct a `DemotionDecision`.
1667pub 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/// Hub trust tier - defines operational permissions for a Hub
2295#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2296pub enum HubTrustTier {
2297    /// Full trust - allows all operations (internal/private Hub)
2298    Full,
2299    /// Read-only - allows only read operations (public Hub)
2300    ReadOnly,
2301}
2302
2303/// Hub operation class - classifies the type of A2A operation
2304#[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    /// Returns true if the operation is read-only (allowed for ReadOnly hubs)
2321    pub fn is_read_only(&self) -> bool {
2322        matches!(self, HubOperationClass::Hello | HubOperationClass::Fetch)
2323    }
2324}
2325
2326/// Hub profile - describes a Hub's capabilities and configuration
2327#[derive(Clone, Debug, Serialize, Deserialize)]
2328pub struct HubProfile {
2329    pub hub_id: String,
2330    pub base_url: String,
2331    pub trust_tier: HubTrustTier,
2332    /// Priority for hub selection (higher = preferred)
2333    pub priority: u32,
2334    /// Optional health check endpoint
2335    pub health_url: Option<String>,
2336}
2337
2338impl HubProfile {
2339    /// Check if this hub allows the given operation class
2340    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/// Hub selection policy - defines how to choose between multiple hubs
2349#[derive(Clone, Debug)]
2350pub struct HubSelectionPolicy {
2351    /// Map operation class to allowed trust tiers
2352    pub allowed_tiers_for_operation: Vec<(HubOperationClass, Vec<HubTrustTier>)>,
2353    /// Default trust tiers if no specific mapping
2354    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                // All write operations require Full trust
2370                (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    /// Get allowed trust tiers for a given operation
2387    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}