@team-agent/installer 0.5.58 → 0.5.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +30 -2
  4. package/crates/team-agent/src/cli/send/mailbox.rs +68 -25
  5. package/crates/team-agent/src/cli/spec.rs +1 -1
  6. package/crates/team-agent/src/coordinator/tick.rs +21 -8
  7. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
  8. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +56 -50
  9. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
  10. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
  11. package/crates/team-agent/src/lifecycle/launch.rs +1 -1
  12. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
  13. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  14. package/crates/team-agent/src/mcp_server/normalize.rs +53 -1
  15. package/crates/team-agent/src/mcp_server/tools.rs +8 -1
  16. package/crates/team-agent/src/provider/adapter.rs +57 -0
  17. package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
  18. package/crates/team-agent/src/provider/session/capture.rs +29 -16
  19. package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
  20. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
  21. package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
  22. package/crates/team-agent/src/provider/session/mod.rs +3 -2
  23. package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
  24. package/crates/team-agent/src/provider/session_scan/common.rs +11 -1
  25. package/crates/team-agent/src/provider/session_scan.rs +3 -0
  26. package/package.json +4 -4
  27. package/skills/team-agent/SKILL.md +1 -0
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.58"
578
+ version = "0.5.60"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.58"
12
+ version = "0.5.60"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  use super::spec::{command_spec, CommandKind, CommandTier, ALL_DISPATCH_KINDS, COMMAND_SPECS};
5
5
  use super::*;
6
- use std::io::Write as _;
6
+ use std::io::{ErrorKind, Write as _};
7
7
 
8
8
  /// `emit`(`helpers.py:12-23`):`--json`→`json.dumps(indent=2, ensure_ascii=False, sort_keys=True)`;
9
9
  /// 否则 dict 逐键 `key: value`(嵌套 dict/list 内联 compact json,`ensure_ascii=False`)、非 dict 直接 print。
@@ -77,12 +77,40 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
77
77
  /// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
78
78
  /// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
79
79
  fn emit_result(r: CmdResult) -> ExitCode {
80
+ let persisted_message_id = match &r.output {
81
+ CmdOutput::Json(value) => value
82
+ .get("message_id")
83
+ .and_then(Value::as_str)
84
+ .map(ToString::to_string),
85
+ _ => None,
86
+ };
80
87
  if let Some(text) = emit(&r.output, r.as_json) {
81
- println!("{text}");
88
+ if let Err(error) = write_stdout_line(&text) {
89
+ if let Some(message_id) = persisted_message_id {
90
+ let stderr = std::io::stderr();
91
+ let mut stderr = stderr.lock();
92
+ let _ = writeln!(
93
+ stderr,
94
+ "stdout unavailable after durable send; persisted_message_id={message_id}"
95
+ );
96
+ }
97
+ return if error.kind() == ErrorKind::BrokenPipe {
98
+ r.exit
99
+ } else {
100
+ ExitCode::Error
101
+ };
102
+ }
82
103
  }
83
104
  r.exit
84
105
  }
85
106
 
107
+ fn write_stdout_line(text: &str) -> std::io::Result<()> {
108
+ let stdout = std::io::stdout();
109
+ let mut stdout = stdout.lock();
110
+ stdout.write_all(text.as_bytes())?;
111
+ stdout.write_all(b"\n")
112
+ }
113
+
86
114
  fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
87
115
  let Some(spec) = command_spec(command) else {
88
116
  return Ok(emit_unknown_subcommand_usage(command));
@@ -37,8 +37,8 @@ pub(super) fn maybe_enqueue_offline_leader_mailbox(
37
37
  Ok(s) => s,
38
38
  Err(_) => return Ok(None),
39
39
  };
40
- let team_alive = target_team_is_alive_for_mailbox(&state, &team_key);
41
- if !team_alive {
40
+ let attachment_history = target_team_mailbox_history(&state, &team_key);
41
+ if attachment_history == MailboxAttachmentHistory::NotEligible {
42
42
  return Ok(None);
43
43
  }
44
44
  let event_log = crate::event_log::EventLog::new(&target_workspace);
@@ -53,47 +53,90 @@ pub(super) fn maybe_enqueue_offline_leader_mailbox(
53
53
  )
54
54
  .map_err(|e| CliError::Runtime(e.to_string()))?;
55
55
  let message_id = outcome.message_id.clone().unwrap_or_else(|| "".to_string());
56
- Ok(Some(json!({
57
- "ok": true,
58
- "status": "queued_until_leader_attach",
59
- "message_status": "queued_until_leader_attach",
60
- "channel": "leader_mailbox",
61
- "delivered": false,
62
- "to_name": to_name,
63
- "target_workspace": target_workspace.display().to_string(),
64
- "team_key": team_key,
65
- "recipient": "leader",
66
- "leader_attached": false,
67
- "message_id": message_id,
68
- })))
56
+ let receipt = match attachment_history {
57
+ MailboxAttachmentHistory::NeverAttached => json!({
58
+ "ok": true,
59
+ "status": "deferred",
60
+ "deferred_reason": "never_attached",
61
+ "message_status": "queued_until_leader_attach",
62
+ "channel": "leader_mailbox",
63
+ "delivered": false,
64
+ "to_name": to_name,
65
+ "target_workspace": target_workspace.display().to_string(),
66
+ "team_key": team_key,
67
+ "recipient": "leader",
68
+ "leader_attached": false,
69
+ "message_id": message_id,
70
+ }),
71
+ MailboxAttachmentHistory::PreviouslyAttached => json!({
72
+ "ok": true,
73
+ "status": "queued_until_leader_attach",
74
+ "deferred_reason": "leader_currently_unattached",
75
+ "message_status": "queued_until_leader_attach",
76
+ "channel": "leader_mailbox",
77
+ "delivered": false,
78
+ "to_name": to_name,
79
+ "target_workspace": target_workspace.display().to_string(),
80
+ "team_key": team_key,
81
+ "recipient": "leader",
82
+ "leader_attached": false,
83
+ "message_id": message_id,
84
+ }),
85
+ MailboxAttachmentHistory::NotEligible => unreachable!("refused before persistence"),
86
+ };
87
+ Ok(Some(receipt))
88
+ }
89
+
90
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
91
+ enum MailboxAttachmentHistory {
92
+ PreviouslyAttached,
93
+ NeverAttached,
94
+ NotEligible,
69
95
  }
70
96
 
71
- /// Positive-source liveness heuristic per offline-mailbox-toname-design.md §4:
97
+ /// Positive-source mailbox eligibility per offline-mailbox-toname-design.md §4:
72
98
  /// - target workspace has state and the team key is present + not archived/down;
73
- /// - AND at least one live tmux fact — a persisted `session_name` OR any
74
- /// agent with a recorded pane on the recorded socket.
99
+ /// - AND a persisted session name establishes a durable replay target;
100
+ /// - a persisted receiver attachment timestamp distinguishes previously attached
101
+ /// teams from teams whose first successful leader attachment is still pending.
75
102
  ///
76
103
  /// We deliberately do NOT poll coordinator health here — enqueuing is
77
104
  /// safe even when the coordinator is transiently down; attach-leader
78
105
  /// itself replays via `requeue_blocked_leader_messages` regardless.
79
- pub(super) fn target_team_is_alive_for_mailbox(state: &Value, team_key: &str) -> bool {
106
+ fn target_team_mailbox_history(state: &Value, team_key: &str) -> MailboxAttachmentHistory {
80
107
  let team = state
81
108
  .get("teams")
82
109
  .and_then(|v| v.as_object())
83
110
  .and_then(|teams| teams.get(team_key));
84
111
  let Some(team) = team else {
85
- return false;
112
+ return MailboxAttachmentHistory::NotEligible;
86
113
  };
87
114
  let status = team
88
115
  .get("status")
89
116
  .and_then(|v| v.as_str())
90
117
  .unwrap_or("alive");
91
118
  if matches!(status, "archived" | "down" | "stopped") {
92
- return false;
119
+ return MailboxAttachmentHistory::NotEligible;
93
120
  }
94
- // A recorded session_name is enough — target's coordinator/attach
95
- // path will re-verify tmux presence when the replay fires.
96
- team.get("session_name")
121
+ let has_session = team
122
+ .get("session_name")
97
123
  .and_then(|v| v.as_str())
98
- .is_some_and(|s| !s.is_empty())
124
+ .is_some_and(|s| !s.is_empty());
125
+ if !has_session {
126
+ return MailboxAttachmentHistory::NotEligible;
127
+ }
128
+ let previously_attached = team
129
+ .get("leader_receiver")
130
+ .and_then(Value::as_object)
131
+ .is_some_and(|receiver| {
132
+ receiver
133
+ .get("attached_at")
134
+ .and_then(Value::as_str)
135
+ .is_some_and(|value| !value.is_empty())
136
+ });
137
+ if previously_attached {
138
+ MailboxAttachmentHistory::PreviouslyAttached
139
+ } else {
140
+ MailboxAttachmentHistory::NeverAttached
141
+ }
99
142
  }
@@ -155,7 +155,7 @@ pub(crate) struct CommandSpec {
155
155
  #[rustfmt::skip]
156
156
  pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
157
157
  CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
158
- CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for a logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
158
+ CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
159
159
  CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
160
160
  CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
161
161
  CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
@@ -269,12 +269,17 @@ impl Coordinator {
269
269
  }
270
270
 
271
271
  self.record_step("capture_missing");
272
- if let Err(error) = self.capture_missing_sessions(&mut state, &event_log) {
273
- let _ = event_log.write(
274
- "coordinator.tick.capture_missing_failed",
275
- serde_json::json!({"error": error.to_string()}),
276
- );
277
- }
272
+ let pending_context_fork_audits =
273
+ match self.capture_missing_sessions(&mut state, &event_log) {
274
+ Ok(audits) => audits,
275
+ Err(error) => {
276
+ let _ = event_log.write(
277
+ "coordinator.tick.capture_missing_failed",
278
+ serde_json::json!({"error": error.to_string()}),
279
+ );
280
+ Vec::new()
281
+ }
282
+ };
278
283
 
279
284
  // Slice 1 energy gate: one pane snapshot per tick feeds probe eligibility,
280
285
  // health sync, and abnormal-exit detection. Missing panes are filtered
@@ -454,6 +459,14 @@ impl Coordinator {
454
459
  collections,
455
460
  ));
456
461
  }
462
+ for context_fork in &pending_context_fork_audits {
463
+ context_fork.write_audit(&event_log).map_err(|error| {
464
+ eprintln!(
465
+ "[coordinator] context_fork audit publish failed after state commit: {error}"
466
+ );
467
+ TickError::EventLog(error)
468
+ })?;
469
+ }
457
470
 
458
471
  // 0.5.36 (`.team/artifacts/supermarket-api-error-recovery-locate.md` §7.3):
459
472
  // post-save recovery step. Reloads fresh state, consumes due
@@ -487,7 +500,7 @@ impl Coordinator {
487
500
  &self,
488
501
  state: &mut Value,
489
502
  event_log: &EventLog,
490
- ) -> Result<(), TickError> {
503
+ ) -> Result<Vec<crate::lifecycle::launch::ContextForkFinalized>, TickError> {
491
504
  let report = crate::session_capture::capture_missing_provider_sessions_once(
492
505
  state,
493
506
  &mut |provider| self.provider_registry.adapter_for(provider),
@@ -651,7 +664,7 @@ impl Coordinator {
651
664
  }),
652
665
  )?;
653
666
  }
654
- Ok(())
667
+ Ok(report.context_forks)
655
668
  }
656
669
 
657
670
  fn sync_agent_health(
@@ -0,0 +1,58 @@
1
+ use super::super::*;
2
+
3
+ pub(super) struct CompleteForkInput<'a> {
4
+ pub workspace: &'a Path,
5
+ pub team_key: &'a str,
6
+ pub agent_id: &'a AgentId,
7
+ pub spawn: &'a crate::transport::SpawnResult,
8
+ pub window: &'a WindowName,
9
+ pub transport: &'a dyn Transport,
10
+ pub session_name: &'a SessionName,
11
+ pub mcp_config_path: &'a Path,
12
+ pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
13
+ pub materialized_role: &'a mut MaterializedRole,
14
+ pub claude_fork:
15
+ &'a mut Option<crate::provider::adapters::claude_fork::ClaudeForkMaterialization>,
16
+ pub copilot_fork:
17
+ &'a mut Option<crate::provider::adapters::copilot_fork::CopilotForkMaterialization>,
18
+ }
19
+
20
+ pub(super) fn complete_fork(input: CompleteForkInput<'_>) -> Result<bool, LifecycleError> {
21
+ if let Err(error) = verify_fork_registration(
22
+ input.workspace,
23
+ input.team_key,
24
+ input.agent_id,
25
+ input.spawn,
26
+ input.window,
27
+ ) {
28
+ rollback_fork_after_spawn(
29
+ input.workspace,
30
+ input.transport,
31
+ input.session_name,
32
+ input.window,
33
+ input.mcp_config_path,
34
+ input.agent_id,
35
+ input.profile_launch,
36
+ input.team_key,
37
+ );
38
+ return Err(error);
39
+ }
40
+ let coordinator_started = start_fork_coordinator(ForkCoordinatorInput {
41
+ workspace: input.workspace,
42
+ team_key: input.team_key,
43
+ agent_id: input.agent_id,
44
+ transport: input.transport,
45
+ session_name: input.session_name,
46
+ window: input.window,
47
+ mcp_config_path: input.mcp_config_path,
48
+ profile_launch: input.profile_launch,
49
+ })?;
50
+ input.materialized_role.keep();
51
+ if let Some(materialized) = input.claude_fork.as_mut() {
52
+ materialized.keep();
53
+ }
54
+ if let Some(materialized) = input.copilot_fork.as_mut() {
55
+ materialized.keep();
56
+ }
57
+ Ok(coordinator_started)
58
+ }
@@ -1,16 +1,7 @@
1
1
  use super::*;
2
- use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
3
2
  use crate::lifecycle::profile_launch::parse_provider;
4
- use crate::lifecycle::*;
5
- use crate::model::enums::{AuthMode, DisplayBackend, Provider, ProviderEffort};
6
- use crate::model::ids::AgentId;
7
- use crate::model::permissions::{self, AgentPermissionInput};
8
- use crate::model::yaml::{self, Value};
9
- use crate::state::persist::load_runtime_state;
10
- use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
11
- use std::collections::{BTreeMap, BTreeSet};
12
- use std::path::{Path, PathBuf};
13
- use std::process::Command;
3
+
4
+ mod completion;
14
5
 
15
6
  pub fn fork_agent_with_transport(
16
7
  workspace: &Path,
@@ -239,6 +230,23 @@ pub fn fork_agent_with_transport(
239
230
  as_agent_id.as_str(),
240
231
  Some(&fork_team),
241
232
  );
233
+ let spawned_at = spawn_timestamp_for_agent(1);
234
+ let mut fork_backing = adapter
235
+ .materialize_fork_backing(
236
+ &source_backing,
237
+ &session_id,
238
+ source_agent_id.as_str(),
239
+ as_agent_id.as_str(),
240
+ &mut plan,
241
+ )
242
+ .map_err(|error| {
243
+ let _ = std::fs::write(&spec_path, text.as_bytes());
244
+ cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
245
+ LifecycleError::Provider(error.to_string())
246
+ })?;
247
+ let mut expected_backing_path = fork_backing
248
+ .as_ref()
249
+ .map(|materialized| materialized.path().to_path_buf());
242
250
  if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
243
251
  plan.provider_projects_root = source_backing.parent().map(Path::to_path_buf);
244
252
  }
@@ -248,12 +256,19 @@ pub fn fork_agent_with_transport(
248
256
  &plan,
249
257
  &source_backing,
250
258
  &session_id,
259
+ source_agent_id,
260
+ as_agent_id,
251
261
  )
252
262
  .map_err(|error| {
253
263
  let _ = std::fs::write(&spec_path, text.as_bytes());
254
264
  cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
255
265
  error
256
266
  })?;
267
+ if expected_backing_path.is_none() {
268
+ expected_backing_path = claude_fork
269
+ .as_ref()
270
+ .map(|materialized| materialized.path().to_path_buf());
271
+ }
257
272
  // The framework-created Claude snapshot is only fork input, not provider
258
273
  // proof. Observe changes made after materialization so a spawn-only
259
274
  // provider cannot turn the copied source backing into a Verified result.
@@ -307,7 +322,6 @@ pub fn fork_agent_with_transport(
307
322
  // Release the metadata lock before per-seat provider convergence; finalize reacquires it.
308
323
  drop(_lock);
309
324
  let spawn_epoch = 1_u64;
310
- let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
311
325
  let spawn_result = if session_live {
312
326
  transport.spawn_into_with_env_unset(
313
327
  &session_name,
@@ -343,6 +357,9 @@ pub fn fork_agent_with_transport(
343
357
  return Err(LifecycleError::Transport(error.to_string()));
344
358
  }
345
359
  };
360
+ if let Some(materialized) = fork_backing.as_mut() {
361
+ materialized.handoff();
362
+ }
346
363
  ensure_fork_spawn_live(ForkPostSpawnInput {
347
364
  workspace: &workspace,
348
365
  transport,
@@ -361,7 +378,8 @@ pub fn fork_agent_with_transport(
361
378
  &session_id,
362
379
  &plan,
363
380
  &backing_before,
364
- claude_fork.as_ref().map(|materialized| materialized.path()),
381
+ expected_backing_path.as_deref(),
382
+ source_agent_id.as_str(),
365
383
  as_agent_id.as_str(),
366
384
  &workspace,
367
385
  &spawned_at,
@@ -414,7 +432,7 @@ pub fn fork_agent_with_transport(
414
432
  }
415
433
  };
416
434
  if let Some(context_proof) = context_proof.as_ref() {
417
- if let Err(error) = finalize_fork_state(ForkFinalizeInput {
435
+ let finalized = match finalize_fork_state(ForkFinalizeInput {
418
436
  workspace: &workspace,
419
437
  team_key: &fork_team,
420
438
  source_agent_id,
@@ -430,51 +448,39 @@ pub fn fork_agent_with_transport(
430
448
  spawned_at: &spawned_at,
431
449
  spawn_epoch,
432
450
  }) {
433
- rollback_fork_after_spawn(
434
- &workspace,
435
- transport,
436
- &session_name,
437
- &window,
438
- &mcp_config_path,
439
- as_agent_id,
440
- &profile_launch,
441
- &fork_team,
442
- );
443
- return Err(error);
444
- }
445
- }
446
- if let Err(error) =
447
- verify_fork_registration(&workspace, &fork_team, as_agent_id, &spawn, &window)
448
- {
449
- rollback_fork_after_spawn(
450
- &workspace,
451
- transport,
452
- &session_name,
453
- &window,
454
- &mcp_config_path,
455
- as_agent_id,
456
- &profile_launch,
457
- &fork_team,
458
- );
459
- return Err(error);
451
+ Ok(finalized) => finalized,
452
+ Err(error) => {
453
+ rollback_fork_after_spawn(
454
+ &workspace,
455
+ transport,
456
+ &session_name,
457
+ &window,
458
+ &mcp_config_path,
459
+ as_agent_id,
460
+ &profile_launch,
461
+ &fork_team,
462
+ );
463
+ return Err(error);
464
+ }
465
+ };
466
+ finalized
467
+ .write_audit(&crate::event_log::EventLog::new(&workspace))
468
+ .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
460
469
  }
461
- let coordinator_started = start_fork_coordinator(ForkCoordinatorInput {
470
+ let coordinator_started = completion::complete_fork(completion::CompleteForkInput {
462
471
  workspace: &workspace,
463
472
  team_key: &fork_team,
464
473
  agent_id: as_agent_id,
474
+ spawn: &spawn,
475
+ window: &window,
465
476
  transport,
466
477
  session_name: &session_name,
467
- window: &window,
468
478
  mcp_config_path: &mcp_config_path,
469
479
  profile_launch: &profile_launch,
480
+ materialized_role: &mut materialized_role,
481
+ claude_fork: &mut claude_fork,
482
+ copilot_fork: &mut copilot_fork,
470
483
  })?;
471
- materialized_role.keep();
472
- if let Some(materialized) = claude_fork.as_mut() {
473
- materialized.keep();
474
- }
475
- if let Some(materialized) = copilot_fork.as_mut() {
476
- materialized.keep();
477
- }
478
484
  let backing_state = if context_proof.is_some() {
479
485
  ForkBackingState::Verified
480
486
  } else {