@team-agent/installer 0.5.66 → 0.5.68

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 (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,18 +1,105 @@
1
+ //! ---
2
+ //! purpose: 在当前席位窗口注入官方斜杠命令完成就地分身,不读 provider session 落盘
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: fork_agent_with_transport
6
+ //! what: grok/claude subscription 注入纯净斜杠命令;屏幕出现该 provider 的实测标记才算成功
7
+ //! - name: in_window_fork
8
+ //! what: 已验证的 provider+subscription 给出 command+screen_mark;未验证返回 None
9
+ //! depends:
10
+ //! - crate::lifecycle::lock
11
+ //! - crate::lifecycle::pane_input_lock
12
+ //! - crate::lifecycle::profile_launch
13
+ //! - crate::transport::Transport
14
+ //! boundary:
15
+ //! - 不 spawn 新 pane,不改 TEAM_AGENT_ID / MCP / 席位名
16
+ //! - 不读 provider 会话落盘文件取会话身份
17
+ //! - 重试只重按回车,不重粘 /fork
18
+ //! - 不把 pane 锁超时从 200ms 调大
19
+ //! - 未验证的 provider 不猜斜杠命令
20
+ //! maturity: wired
21
+ //! ---
22
+
1
23
  use super::*;
2
- use crate::lifecycle::profile_launch::parse_provider;
24
+ use crate::lifecycle::pane_input_lock::{
25
+ acquire_or_proceed, PaneInputLockRequest, PANE_INPUT_LOCK_TIMEOUT_EVENT,
26
+ };
27
+ use crate::lifecycle::profile_launch::{parse_auth_mode, parse_provider};
28
+ use crate::model::enums::{AuthMode, Provider};
29
+
30
+ const MAX_ENTER_RETRIES: u32 = 8;
31
+
32
+ /// Injected slash command + the screen mark that proves it landed.
33
+ /// Difference between providers lives only in this data.
34
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
35
+ pub struct InWindowFork {
36
+ pub command: &'static str,
37
+ pub screen_mark: &'static str,
38
+ }
39
+
40
+ /// ---
41
+ /// purpose: 给出该 provider 与鉴权方式下窗口内 fork 用的斜杠命令与成功标记
42
+ /// returns: 已实测验证的组合返回命令与屏幕标记,其余一律 None
43
+ /// ---
44
+ /// Official in-window fork. None = unverified or unsupported-auth.
45
+ /// grok `/fork` mark `forked from` (2026-08-17).
46
+ /// claude `/branch` mark `Branched conversation` (Claude Code v2.1.181, 2026-08-17).
47
+ /// claude `/fork` is a background-agent command (`Usage: /fork <directive>`), not the session split.
48
+ pub fn in_window_fork(provider: Provider, auth: AuthMode) -> Option<InWindowFork> {
49
+ match (provider, auth) {
50
+ (Provider::Grok, AuthMode::Subscription) => Some(InWindowFork {
51
+ command: "/fork",
52
+ screen_mark: "forked from",
53
+ }),
54
+ (Provider::Claude | Provider::ClaudeCode, AuthMode::Subscription) => Some(InWindowFork {
55
+ command: "/branch",
56
+ screen_mark: "Branched conversation",
57
+ }),
58
+ _ => None,
59
+ }
60
+ }
3
61
 
4
- mod completion;
62
+ /// ---
63
+ /// purpose: 只取窗口内 fork 的斜杠命令
64
+ /// returns: 有已验证组合时返回命令,否则 None
65
+ /// ---
66
+ pub fn in_window_fork_command(provider: Provider, auth: AuthMode) -> Option<&'static str> {
67
+ in_window_fork(provider, auth).map(|spec| spec.command)
68
+ }
5
69
 
70
+ fn refuse_missing_in_window_fork(provider: Provider, provider_raw: &str) -> LifecycleError {
71
+ if matches!(
72
+ provider,
73
+ Provider::Grok | Provider::Claude | Provider::ClaudeCode | Provider::CursorAgent
74
+ ) {
75
+ LifecycleError::Provider(format!(
76
+ "{provider_raw} does not support native session fork"
77
+ ))
78
+ } else {
79
+ LifecycleError::Provider(format!(
80
+ "{provider_raw} in-window fork is unverified (未验证)"
81
+ ))
82
+ }
83
+ }
84
+
85
+ /// ---
86
+ /// purpose: 在源席窗口内注入官方斜杠命令完成就地分身
87
+ /// params:
88
+ /// source_agent_id: 被 fork 的席位
89
+ /// as_agent_id: 新席位名
90
+ /// transport: 目标 team 实际使用的 transport
91
+ /// returns: fork 报告,屏幕出现该 provider 的实测标记才记为已验证
92
+ /// errors: 选不到 team 返回 TeamSelect,provider 或鉴权方式未验证返回 Provider,注入失败返回 LifecycleError
93
+ /// ---
6
94
  pub fn fork_agent_with_transport(
7
95
  workspace: &Path,
8
96
  source_agent_id: &AgentId,
9
97
  as_agent_id: &AgentId,
10
- label: Option<&str>,
11
- open_display: bool,
98
+ _label: Option<&str>,
99
+ _open_display: bool,
12
100
  team: Option<&str>,
13
101
  transport: &dyn Transport,
14
102
  ) -> Result<ForkAgentReport, LifecycleError> {
15
- let _ = open_display;
16
103
  let selected = crate::state::selector::resolve_active_team(
17
104
  workspace,
18
105
  team,
@@ -21,11 +108,11 @@ pub fn fork_agent_with_transport(
21
108
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
22
109
  let lock_workspace = selected.run_workspace.clone();
23
110
  let lock_team_key = selected.team_key.clone();
24
- let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
111
+ let _lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
25
112
  workspace: &lock_workspace,
26
113
  operation: "fork-agent",
27
114
  team: Some(lock_team_key.as_str()),
28
- agent_id: Some(as_agent_id),
115
+ agent_id: Some(source_agent_id),
29
116
  })?;
30
117
  let selected = crate::state::selector::resolve_active_team(
31
118
  &lock_workspace,
@@ -33,467 +120,128 @@ pub fn fork_agent_with_transport(
33
120
  crate::state::selector::SelectorMode::RequireSpec,
34
121
  )
35
122
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
36
- // E5 §3:team_dir(角色定义+profiles)恒用户目录。spec 读用 selector 解析的 spec_path
37
- // (读序 B:runtime 优先、legacy 回落),写恒走 runtime_spec_path(canonical 落点)。
38
- let fork_team_dir = selected.team_dir.clone();
39
- let fork_team = selected.team_key.clone();
40
- let read_spec_path = selected
41
- .spec_path
42
- .clone()
43
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
44
- let workspace = selected.run_workspace;
45
- let state = selected.state;
46
- ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
47
- let spec_path = crate::model::paths::runtime_spec_path(&workspace, &fork_team);
48
- let text = std::fs::read_to_string(&read_spec_path)
49
- .map_err(|e| LifecycleError::Compile(format!("{}: {e}", read_spec_path.display())))?;
50
- let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
51
- if fork_spec_agent(&spec, as_agent_id).is_some() || leader_id_matches(&spec, as_agent_id) {
52
- return Err(LifecycleError::RequirementUnmet(format!(
53
- "agent id already exists: {as_agent_id}"
54
- )));
55
- }
56
- // Source existence authority: state.get("agents"), matching clone-agent.
57
- let (session_id, source_backing) = fork_source_tuple(&state, source_agent_id)?;
58
- let session_name = state
59
- .get("session_name")
60
- .and_then(|v| v.as_str())
61
- .filter(|s| !s.is_empty())
62
- .map(SessionName::new)
63
- .unwrap_or_else(|| spec_session_name(&spec));
64
- if transport
65
- .list_windows(&session_name)
66
- .map(|windows| windows.iter().any(|w| w.as_str() == as_agent_id.as_str()))
67
- .unwrap_or(false)
68
- {
69
- return Err(LifecycleError::Transport(format!(
70
- "tmux window already exists for fork target: {}:{}",
71
- session_name.as_str(),
72
- as_agent_id.as_str()
73
- )));
74
- }
75
- let mut materialized_role = materialize_latest_role(
76
- &workspace,
77
- &fork_team_dir,
78
- &state,
79
- source_agent_id,
80
- as_agent_id,
81
- label,
82
- )?;
83
- clamp_materialized_role_to_leader(materialized_role.path(), &spec)?;
84
- let team_meta = crate::compiler::read_front_matter(&fork_team_dir.join("TEAM.md"))
85
- .map(|(meta, _)| meta)
86
- .map_err(|error| LifecycleError::Compile(error.to_string()))?;
87
- let workspace_s = workspace.to_string_lossy().to_string();
88
- let compiled =
89
- crate::compiler::compile_role_agent(materialized_role.path(), &team_meta, &workspace_s)
90
- .map_err(|error| LifecycleError::Compile(error.to_string()))?;
91
- let new_spec =
92
- append_forked_agent(&spec, &compiled.agent, source_agent_id, as_agent_id, label)?;
93
- // validate 用角色定义目录的 team_workspace(校验 working_directory),非 spec 落点。
94
- let validate_ws =
95
- crate::model::paths::team_workspace(&fork_team_dir).unwrap_or_else(|_| workspace.clone());
96
- crate::model::spec::validate_spec(&new_spec, &validate_ws)
97
- .map_err(|e| LifecycleError::Compile(e.to_string()))?;
98
- write_spec_atomic(&spec_path, &new_spec)?;
99
- let new_agent = fork_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
100
- LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
101
- })?;
102
- let provider = new_agent
123
+ let run_ws = selected.run_workspace.clone();
124
+ let agent = selected
125
+ .state
126
+ .get("agents")
127
+ .and_then(|agents| agents.get(source_agent_id.as_str()))
128
+ .ok_or_else(|| {
129
+ LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
130
+ })?;
131
+ let provider_raw = agent
103
132
  .get("provider")
104
- .and_then(Value::as_str)
105
- .and_then(parse_provider)
106
- .unwrap_or(Provider::Codex);
107
- let auth_mode = new_agent
133
+ .and_then(serde_json::Value::as_str)
134
+ .unwrap_or("unknown");
135
+ let auth_raw = agent
108
136
  .get("auth_mode")
109
- .and_then(Value::as_str)
110
- .and_then(parse_auth_mode)
111
- .unwrap_or(AuthMode::Subscription);
112
- let adapter = crate::provider::get_adapter(provider);
113
- let provider_str = new_agent
114
- .get("provider")
115
- .and_then(Value::as_str)
116
- .unwrap_or("codex");
117
- if auth_mode == AuthMode::CompatibleApi || !adapter.caps().fork {
118
- let _ = std::fs::write(&spec_path, text.as_bytes());
119
- return Err(LifecycleError::Provider(format!(
120
- "{provider_str} does not support native session fork"
137
+ .and_then(serde_json::Value::as_str)
138
+ .unwrap_or("subscription");
139
+ let provider = parse_provider(provider_raw).unwrap_or(Provider::Grok);
140
+ let auth = parse_auth_mode(auth_raw).unwrap_or(AuthMode::Subscription);
141
+ let Some(spec) = in_window_fork(provider, auth) else {
142
+ return Err(refuse_missing_in_window_fork(provider, provider_raw));
143
+ };
144
+ if as_agent_id.as_str() != source_agent_id.as_str() {
145
+ return Err(LifecycleError::RequirementUnmet(format!(
146
+ "in-place fork refuses --as {as_agent_id}: session stays on {source_agent_id}"
121
147
  )));
122
148
  }
123
- let model = new_agent.get("model").and_then(Value::as_str);
124
- let safety = effective_runtime_config_for_worker_spawn(new_agent, provider)?;
125
- let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
126
- new_agent,
127
- Some(as_agent_id.as_str()),
128
- provider,
129
- )?;
130
- let system_prompt =
131
- crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
132
- let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
133
- &command_agent,
134
- provider,
135
- )?;
136
- let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
137
- let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
138
- let _ = std::fs::write(&spec_path, text.as_bytes());
139
- LifecycleError::Provider(e.to_string())
140
- })?;
141
- let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
142
- let mcp_config_path = write_worker_mcp_config_for_provider(
143
- &workspace,
144
- as_agent_id.as_str(),
145
- &mcp_config,
146
- Some(provider),
147
- )
148
- .map_err(|e| {
149
- let _ = std::fs::write(&spec_path, text.as_bytes());
150
- e
151
- })?;
152
- // E5 §3:profiles 随角色定义目录(team_dir),不随已迁出的 spec。
153
- let profile_dir = fork_team_dir.join("profiles");
154
- let mut profile_launch =
155
- match crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
156
- &workspace,
157
- as_agent_id.as_str(),
158
- new_agent,
159
- Some(&profile_dir),
160
- Some(&mcp_config),
161
- ) {
162
- Ok(profile_launch) => profile_launch,
163
- Err(error) => {
164
- let _ = std::fs::write(&spec_path, text.as_bytes());
165
- let _ = std::fs::remove_file(&mcp_config_path);
166
- return Err(error);
167
- }
168
- };
169
- let mut copilot_fork = None;
170
- if provider == Provider::Copilot {
171
- let materialized = crate::provider::adapters::copilot_fork::materialize_copilot_fork(
172
- &workspace,
173
- as_agent_id.as_str(),
174
- &session_id,
175
- )
176
- .map_err(|error| {
177
- let _ = std::fs::write(&spec_path, text.as_bytes());
178
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
179
- LifecycleError::Provider(error.to_string())
149
+ let pane_raw = agent
150
+ .get("pane_id")
151
+ .and_then(serde_json::Value::as_str)
152
+ .filter(|s| !s.is_empty())
153
+ .ok_or_else(|| {
154
+ LifecycleError::RequirementUnmet(format!(
155
+ "source agent {source_agent_id} has no pane_id"
156
+ ))
180
157
  })?;
181
- profile_launch.env_overlay.insert(
182
- "COPILOT_HOME".to_string(),
183
- materialized.home().to_string_lossy().to_string(),
158
+ let target = Target::Pane(PaneId::new(pane_raw));
159
+ let lock = acquire_or_proceed(PaneInputLockRequest {
160
+ workspace: Some(&run_ws),
161
+ target_key: pane_raw,
162
+ operation: "fork-inject",
163
+ });
164
+ if lock.is_none() {
165
+ let _ = crate::event_log::EventLog::new(&run_ws).write(
166
+ PANE_INPUT_LOCK_TIMEOUT_EVENT,
167
+ serde_json::json!({
168
+ "during": "fork-inject",
169
+ "agent_id": source_agent_id.as_str(),
170
+ "pane_id": pane_raw,
171
+ "observed_at": chrono::Utc::now().to_rfc3339(),
172
+ }),
184
173
  );
185
- profile_launch.env_overlay.insert(
186
- "TEAM_AGENT_INTERNAL_COPILOT_FORK_SESSION_ID".to_string(),
187
- materialized.session_id().as_str().to_string(),
188
- );
189
- copilot_fork = Some(materialized);
190
- }
191
- let command_model = profile_launch.command_overrides.model.as_deref().or(model);
192
- // 0.4.x provider effort MVP: fork inherits effort from the new agent JSON
193
- // (compiler.rs propagated the role/team effort into the agent at fork-spawn).
194
- let fork_effort = provider_effort_for_spawn(new_agent, provider);
195
- if let Some(event_value) =
196
- provider_effort_event_if_dropped(new_agent, provider, as_agent_id.as_str())
197
- {
198
- let _ = crate::event_log::EventLog::new(&workspace)
199
- .write("provider.effort_unsupported", event_value);
200
- }
201
- let mut plan = adapter
202
- .fork_plan(
203
- Some(&session_id),
204
- crate::provider::ProviderCommandContext {
205
- auth_mode,
206
- mcp_config: Some(&mcp_config),
207
- system_prompt: Some(system_prompt.as_str()),
208
- model: command_model,
209
- tools: &resolved_tool_refs,
210
- profile_launch: Some(&profile_launch),
211
- agent_id_hint: Some(as_agent_id.as_str()),
212
- effort: fork_effort,
213
- },
214
- )
215
- .map_err(|e| {
216
- let _ = std::fs::write(&spec_path, text.as_bytes());
217
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
218
- LifecycleError::Provider(e.to_string())
219
- })?;
220
- profile_launch
221
- .env_overlay
222
- .remove("TEAM_AGENT_INTERNAL_COPILOT_FORK_SESSION_ID");
223
- if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
224
- point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
225
- }
226
- fill_spawn_placeholders_full(
227
- &mut plan.argv,
228
- &workspace,
229
- as_agent_id.as_str(),
230
- Some(&fork_team),
231
- );
232
- let spawned_at = spawn_timestamp_for_agent(1);
233
- let mut fork_backing = adapter
234
- .materialize_fork_backing(
235
- &source_backing,
236
- &session_id,
237
- source_agent_id.as_str(),
238
- as_agent_id.as_str(),
239
- &mut plan,
240
- )
241
- .map_err(|error| {
242
- let _ = std::fs::write(&spec_path, text.as_bytes());
243
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
244
- LifecycleError::Provider(error.to_string())
245
- })?;
246
- let mut expected_backing_path = fork_backing
247
- .as_ref()
248
- .map(|materialized| materialized.path().to_path_buf());
249
- if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
250
- plan.provider_projects_root = source_backing.parent().map(Path::to_path_buf);
251
- }
252
- let window = WindowName::new(as_agent_id.as_str());
253
- let mut claude_fork = prepare_claude_fork_backing(
254
- provider,
255
- &plan,
256
- &source_backing,
257
- &session_id,
258
- source_agent_id,
259
- as_agent_id,
260
- )
261
- .map_err(|error| {
262
- let _ = std::fs::write(&spec_path, text.as_bytes());
263
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
264
- error
265
- })?;
266
- if expected_backing_path.is_none() {
267
- expected_backing_path = claude_fork
268
- .as_ref()
269
- .map(|materialized| materialized.path().to_path_buf());
270
174
  }
271
- // The framework-created Claude snapshot is only fork input, not provider
272
- // proof. Observe changes made after materialization so a spawn-only
273
- // provider cannot turn the copied source backing into a Verified result.
274
- let backing_before = crate::provider::session::ContextBackingSnapshot::capture(provider, &plan);
275
- let mut env =
276
- inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
277
- apply_profile_launch_env(&mut env, &profile_launch);
278
- apply_mcp_auto_approval_env(&mut env, &safety);
279
- if provider == Provider::Copilot {
280
- apply_copilot_instructions_overlay(
281
- &workspace,
282
- as_agent_id.as_str(),
283
- &system_prompt,
284
- &mut env,
175
+ inject_clean_command(transport, &target, spec.command)?;
176
+ wait_for_screen_mark(transport, &target, spec.screen_mark)?;
177
+ drop(lock);
178
+ crate::event_log::EventLog::new(&run_ws)
179
+ .write(
180
+ "lifecycle.fork.in_place",
181
+ serde_json::json!({
182
+ "source_agent_id": source_agent_id.as_str(),
183
+ "agent_id": source_agent_id.as_str(),
184
+ "pane_id": pane_raw,
185
+ "command": spec.command,
186
+ "screen_mark": spec.screen_mark,
187
+ }),
285
188
  )
286
- .map_err(|error| {
287
- let _ = std::fs::write(&spec_path, text.as_bytes());
288
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
289
- error
290
- })?;
291
- }
292
- let mut reserved_state = state.clone();
293
- reserve_forked_agent_state(
294
- &mut reserved_state,
295
- source_agent_id,
296
- as_agent_id,
297
- new_agent,
298
- materialized_role.path(),
299
- )?;
300
- if let Err(error) = crate::state::repository::StateRepository::new(&workspace).save(
301
- crate::state::repository::StateWriteIntent::ForkAgent {
302
- team_key: &fork_team,
303
- agent_id: as_agent_id.as_str(),
189
+ .map_err(|e| LifecycleError::StatePersist(format!("fork audit write failed: {e}")))?;
190
+ Ok(ForkAgentReport {
191
+ source_agent_id: source_agent_id.clone(),
192
+ new_agent_id: source_agent_id.clone(),
193
+ env: AgentActionEnvelope {
194
+ agent_id: source_agent_id.clone(),
195
+ state_file: crate::state::persist::runtime_state_path(&run_ws),
196
+ coordinator_started: false,
304
197
  },
305
- &reserved_state,
306
- ) {
307
- let _ = std::fs::write(&spec_path, text.as_bytes());
308
- cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
309
- return Err(LifecycleError::StatePersist(error.to_string()));
310
- }
311
- // Match launch/restart: absent session spawns first; otherwise add a window.
312
- let session_live = transport.has_session(&session_name).unwrap_or(false);
313
- let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
314
- provider,
315
- &mut env,
316
- extend_worker_env_unset_for_effort(
317
- profile_launch.env_unset.iter().cloned().collect(),
318
- provider,
319
- ),
320
- );
321
- // Release the metadata lock before per-seat provider convergence; finalize reacquires it.
322
- drop(_lock);
323
- let spawn_epoch = 1_u64;
324
- let spawn_result = if session_live {
325
- transport.spawn_into_with_env_unset(
326
- &session_name,
327
- &window,
328
- &plan.argv,
329
- &workspace,
330
- &env,
331
- &env_unset,
332
- )
333
- } else {
334
- transport.spawn_first_with_env_unset(
335
- &session_name,
336
- &window,
337
- &plan.argv,
338
- &workspace,
339
- &env,
340
- &env_unset,
198
+ session_id: None,
199
+ backing_state: ForkBackingState::Verified,
200
+ })
201
+ }
202
+
203
+ fn inject_clean_command(
204
+ transport: &dyn Transport,
205
+ target: &Target,
206
+ command: &str,
207
+ ) -> Result<(), LifecycleError> {
208
+ transport
209
+ .inject(
210
+ target,
211
+ &crate::transport::InjectPayload::TextSkipConsumptionPoll(command.to_string()),
212
+ crate::transport::Key::Enter,
213
+ false,
341
214
  )
342
- };
343
- let spawn = match spawn_result {
344
- Ok(spawn) => spawn,
345
- Err(error) => {
346
- rollback_fork_after_spawn(
347
- &workspace,
348
- transport,
349
- &session_name,
350
- &window,
351
- &mcp_config_path,
352
- as_agent_id,
353
- &profile_launch,
354
- &fork_team,
355
- );
356
- return Err(LifecycleError::Transport(error.to_string()));
215
+ .map_err(|e| LifecycleError::Transport(e.to_string()))?;
216
+ Ok(())
217
+ }
218
+
219
+ const NO_CONVERSATION_TO_BRANCH: &str = "Failed to branch conversation: No conversation to branch";
220
+
221
+ fn wait_for_screen_mark(
222
+ transport: &dyn Transport,
223
+ target: &Target,
224
+ screen_mark: &str,
225
+ ) -> Result<(), LifecycleError> {
226
+ for attempt in 0..MAX_ENTER_RETRIES {
227
+ let cap = transport
228
+ .capture(target, crate::transport::CaptureRange::Tail(40))
229
+ .map_err(|e| LifecycleError::Transport(e.to_string()))?;
230
+ if cap.text.contains(screen_mark) {
231
+ return Ok(());
357
232
  }
358
- };
359
- if let Some(materialized) = fork_backing.as_mut() {
360
- materialized.handoff();
361
- }
362
- ensure_fork_spawn_live(ForkPostSpawnInput {
363
- workspace: &workspace,
364
- transport,
365
- session_name: &session_name,
366
- window: &window,
367
- mcp_config_path: &mcp_config_path,
368
- agent_id: as_agent_id,
369
- profile_launch: &profile_launch,
370
- team_key: &fork_team,
371
- spawn: &spawn,
372
- })?;
373
- let convergence_deadline =
374
- crate::provider::session::context_fork_convergence_deadline(provider);
375
- let context_outcome = crate::provider::session::observe_context_fork(
376
- provider,
377
- &session_id,
378
- &plan,
379
- &backing_before,
380
- expected_backing_path.as_deref(),
381
- source_agent_id.as_str(),
382
- as_agent_id.as_str(),
383
- &workspace,
384
- &spawned_at,
385
- convergence_deadline,
386
- );
387
- let context_proof = match context_outcome {
388
- crate::provider::session::ContextForkOutcome::Verified(proof) => Some(proof),
389
- crate::provider::session::ContextForkOutcome::Pending(pending) => {
390
- if let Err(error) = finalize_pending_fork_state(ForkPendingFinalizeInput {
391
- workspace: &workspace,
392
- team_key: &fork_team,
393
- source_agent_id,
394
- agent_id: as_agent_id,
395
- spec_agent: new_agent,
396
- safety: &safety,
397
- plan: &plan,
398
- profile_launch: &profile_launch,
399
- spawn: &spawn,
400
- profile_dir: &profile_dir,
401
- dynamic_role_file: materialized_role.path(),
402
- pending: &pending,
403
- spawn_epoch,
404
- }) {
405
- rollback_fork_after_spawn(
406
- &workspace,
407
- transport,
408
- &session_name,
409
- &window,
410
- &mcp_config_path,
411
- as_agent_id,
412
- &profile_launch,
413
- &fork_team,
414
- );
415
- return Err(error);
416
- }
417
- None
233
+ if cap.text.contains(NO_CONVERSATION_TO_BRANCH) {
234
+ return Err(LifecycleError::RequirementUnmet(
235
+ NO_CONVERSATION_TO_BRANCH.to_string(),
236
+ ));
418
237
  }
419
- crate::provider::session::ContextForkOutcome::Rejected(error) => {
420
- rollback_fork_after_spawn(
421
- &workspace,
422
- transport,
423
- &session_name,
424
- &window,
425
- &mcp_config_path,
426
- as_agent_id,
427
- &profile_launch,
428
- &fork_team,
429
- );
430
- return Err(LifecycleError::Provider(error.to_string()));
238
+ if attempt + 1 < MAX_ENTER_RETRIES {
239
+ // Mixed text in the box is not failure. Retry Enter only — never re-paste the slash command.
240
+ let _ = transport.send_keys(target, &[crate::transport::Key::Enter]);
241
+ std::thread::sleep(std::time::Duration::from_millis(80));
431
242
  }
432
- };
433
- if let Some(context_proof) = context_proof.as_ref() {
434
- let finalized = match finalize_fork_state(ForkFinalizeInput {
435
- workspace: &workspace,
436
- team_key: &fork_team,
437
- source_agent_id,
438
- agent_id: as_agent_id,
439
- spec_agent: new_agent,
440
- safety: &safety,
441
- plan: &plan,
442
- profile_launch: &profile_launch,
443
- spawn: &spawn,
444
- profile_dir: &profile_dir,
445
- dynamic_role_file: materialized_role.path(),
446
- context_proof: &context_proof,
447
- spawned_at: &spawned_at,
448
- spawn_epoch,
449
- }) {
450
- Ok(finalized) => finalized,
451
- Err(error) => {
452
- rollback_fork_after_spawn(
453
- &workspace,
454
- transport,
455
- &session_name,
456
- &window,
457
- &mcp_config_path,
458
- as_agent_id,
459
- &profile_launch,
460
- &fork_team,
461
- );
462
- return Err(error);
463
- }
464
- };
465
- finalized
466
- .write_audit(&crate::event_log::EventLog::new(&workspace))
467
- .map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
468
243
  }
469
- let coordinator_started = completion::complete_fork(completion::CompleteForkInput {
470
- workspace: &workspace,
471
- team_key: &fork_team,
472
- agent_id: as_agent_id,
473
- spawn: &spawn,
474
- window: &window,
475
- transport,
476
- session_name: &session_name,
477
- mcp_config_path: &mcp_config_path,
478
- profile_launch: &profile_launch,
479
- materialized_role: &mut materialized_role,
480
- claude_fork: &mut claude_fork,
481
- copilot_fork: &mut copilot_fork,
482
- })?;
483
- let backing_state = if context_proof.is_some() {
484
- ForkBackingState::Verified
485
- } else {
486
- ForkBackingState::PendingContextFork
487
- };
488
- Ok(ForkAgentReport {
489
- source_agent_id: source_agent_id.clone(),
490
- new_agent_id: as_agent_id.clone(),
491
- env: AgentActionEnvelope {
492
- agent_id: as_agent_id.clone(),
493
- state_file: crate::state::persist::runtime_state_path(&workspace),
494
- coordinator_started,
495
- },
496
- session_id: context_proof.map(|proof| proof.new_session_id),
497
- backing_state,
498
- })
244
+ Err(LifecycleError::RequirementUnmet(format!(
245
+ "fork inject did not produce {screen_mark:?} on screen"
246
+ )))
499
247
  }