44//! allocates a terminal, an agent harness) and `exec{}` (a plain process — the ding, daemons, a
55//! stage's script; must NOT allocate a terminal, R09). st2 reads only the runner-normative subset:
66//! `identity`, `host`, `role` (metadata only), `type`, `workspace`, `retired`, `keep`, `supervisor`,
7- //! `restart{}`, Resource bindings (declaration metadata), and the tasks. Everything render-only
7+ //! `restart{}`, task lifecycle, Resource bindings (declaration metadata), and the tasks. Everything render-only
88//! (`harness`, `model`, `persona`, `permissions`, `transport`, `strategy`, `meta{}`) is baked into
99//! the tasks/commands by the render layer and ignored here.
1010//!
@@ -142,6 +142,9 @@ pub struct Task {
142142 pub env : BTreeMap < String , String > ,
143143 /// Per-task GC pin.
144144 pub keep : bool ,
145+ /// Reconciliation policy. `adopt-only` is a migration fence: st2 may adopt a live generation,
146+ /// but must not reap a dead generation or create a missing replacement.
147+ pub lifecycle : TaskLifecycle ,
145148}
146149
147150/// Whether a task allocates a terminal.
@@ -153,6 +156,16 @@ pub enum TaskKind {
153156 Exec ,
154157}
155158
159+ /// How st2 reconciles a declared task.
160+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Default ) ]
161+ pub enum TaskLifecycle {
162+ /// Ordinary service lifecycle: launch when absent and replace when dead.
163+ #[ default]
164+ Service ,
165+ /// Migration fence: adopt an already-live generation, otherwise hold without mutation.
166+ AdoptOnly ,
167+ }
168+
156169/// Restart policy (§4). Applies to long-running `service` tasks.
157170#[ derive( Debug , Clone , PartialEq , Eq ) ]
158171pub struct Restart {
@@ -276,6 +289,8 @@ pub(crate) struct RawSpec {
276289 /// Compact catalog form: include the built-in `st2 ding` sidecar.
277290 #[ serde( default ) ]
278291 pub ding : bool ,
292+ /// Compact catalog form: reconciliation policy for the generated agent PTY.
293+ pub lifecycle : Option < String > ,
279294 /// `pty "<name>" {}` / `[pty.<name>]` — interactive tasks.
280295 #[ serde( default ) ]
281296 pub pty : BTreeMap < String , RawTask > ,
@@ -307,6 +322,7 @@ pub(crate) struct RawTask {
307322 pub env : BTreeMap < String , String > ,
308323 #[ serde( default ) ]
309324 pub keep : bool ,
325+ pub lifecycle : Option < String > ,
310326}
311327
312328#[ derive( Debug , Default , Deserialize ) ]
@@ -606,6 +622,8 @@ impl RawSpec {
606622 tasks. push ( t. lower ( & identity, TaskKind :: Exec , name, & self . env ) ?) ;
607623 }
608624 if self . command . is_some ( ) || self . argv . is_some ( ) {
625+ let lifecycle =
626+ parse_task_lifecycle ( & identity, "compact task" , self . lifecycle . as_deref ( ) ) ?;
609627 let mut tags = BTreeMap :: new ( ) ;
610628 tags. insert ( "role" . to_string ( ) , "agent" . to_string ( ) ) ;
611629 tasks. push ( Task {
@@ -620,6 +638,7 @@ impl RawSpec {
620638 tags,
621639 env : self . env . clone ( ) ,
622640 keep : false ,
641+ lifecycle,
623642 } ) ;
624643 }
625644 if self . ding {
@@ -634,6 +653,7 @@ impl RawSpec {
634653 tags : BTreeMap :: new ( ) ,
635654 env : self . env ,
636655 keep : false ,
656+ lifecycle : TaskLifecycle :: Service ,
637657 } ) ;
638658 }
639659 tasks. sort_by ( |a, b| a. name . cmp ( & b. name ) ) ;
@@ -675,6 +695,11 @@ impl RawTask {
675695 ) ?;
676696 let mut env = inherited_env. clone ( ) ;
677697 env. extend ( self . env ) ;
698+ let lifecycle = parse_task_lifecycle (
699+ identity,
700+ & format ! ( "{kind:?} task '{name}'" ) ,
701+ self . lifecycle . as_deref ( ) ,
702+ ) ?;
678703 Ok ( Task {
679704 kind,
680705 derived : false ,
@@ -686,10 +711,25 @@ impl RawTask {
686711 tags : self . tags ,
687712 env,
688713 keep : self . keep ,
714+ lifecycle,
689715 } )
690716 }
691717}
692718
719+ fn parse_task_lifecycle (
720+ identity : & str ,
721+ location : & str ,
722+ lifecycle : Option < & str > ,
723+ ) -> anyhow:: Result < TaskLifecycle > {
724+ match lifecycle {
725+ None | Some ( "service" ) => Ok ( TaskLifecycle :: Service ) ,
726+ Some ( "adopt-only" ) => Ok ( TaskLifecycle :: AdoptOnly ) ,
727+ Some ( other) => {
728+ anyhow:: bail!( "agent '{identity}' {location} has unknown lifecycle '{other}'" )
729+ }
730+ }
731+ }
732+
693733fn validate_launch (
694734 identity : & str ,
695735 command : Option < & String > ,
0 commit comments