@team-agent/installer 0.5.52 → 0.5.54

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 (87) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +18 -2
  4. package/crates/team-agent/src/cli/emit.rs +111 -3
  5. package/crates/team-agent/src/cli/mod.rs +155 -4
  6. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  7. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  8. package/crates/team-agent/src/cli/send.rs +1 -0
  9. package/crates/team-agent/src/cli/spec.rs +4 -1
  10. package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
  11. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  12. package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
  13. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  14. package/crates/team-agent/src/cli/types.rs +12 -0
  15. package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
  16. package/crates/team-agent/src/db/message_store.rs +31 -2
  17. package/crates/team-agent/src/db/migration.rs +7 -6
  18. package/crates/team-agent/src/db/schema.rs +18 -5
  19. package/crates/team-agent/src/diagnose/orphans.rs +24 -3
  20. package/crates/team-agent/src/kill_audit.rs +67 -0
  21. package/crates/team-agent/src/leader/lease.rs +324 -57
  22. package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
  23. package/crates/team-agent/src/leader/rediscover.rs +6 -0
  24. package/crates/team-agent/src/leader/start.rs +144 -23
  25. package/crates/team-agent/src/leader/tests/idle.rs +3 -0
  26. package/crates/team-agent/src/leader/types.rs +6 -0
  27. package/crates/team-agent/src/lib.rs +1 -0
  28. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
  30. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
  31. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
  32. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
  33. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
  34. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
  35. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
  36. package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
  37. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  38. package/crates/team-agent/src/lifecycle/launch.rs +14 -2
  39. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  40. package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
  42. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
  43. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
  44. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
  45. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
  46. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
  47. package/crates/team-agent/src/lifecycle/types.rs +17 -0
  48. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
  51. package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
  52. package/crates/team-agent/src/mcp_server/tools.rs +89 -4
  53. package/crates/team-agent/src/mcp_server/types.rs +7 -0
  54. package/crates/team-agent/src/mcp_server/wire.rs +65 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +2 -0
  56. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  57. package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
  58. package/crates/team-agent/src/messaging/leader_receiver.rs +100 -21
  59. package/crates/team-agent/src/messaging/mod.rs +2 -1
  60. package/crates/team-agent/src/messaging/persist.rs +68 -2
  61. package/crates/team-agent/src/messaging/presentation.rs +307 -0
  62. package/crates/team-agent/src/messaging/results.rs +130 -3
  63. package/crates/team-agent/src/messaging/selftest.rs +1 -0
  64. package/crates/team-agent/src/messaging/send.rs +54 -2
  65. package/crates/team-agent/src/messaging/tests/runtime.rs +85 -24
  66. package/crates/team-agent/src/messaging/types.rs +2 -0
  67. package/crates/team-agent/src/messaging/watchers.rs +1 -0
  68. package/crates/team-agent/src/provider/adapter.rs +54 -13
  69. package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
  70. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
  71. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  72. package/crates/team-agent/src/provider/session/capture.rs +395 -52
  73. package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
  74. package/crates/team-agent/src/provider/session/mod.rs +6 -0
  75. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
  76. package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
  77. package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
  78. package/crates/team-agent/src/provider/session_scan/common.rs +53 -92
  79. package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
  80. package/crates/team-agent/src/provider/session_scan.rs +3 -3
  81. package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
  82. package/crates/team-agent/src/provider/tests.rs +1 -0
  83. package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
  84. package/crates/team-agent/src/tmux_backend.rs +29 -2
  85. package/package.json +4 -4
  86. package/schemas/result-envelope.schema.json +10 -0
  87. package/skills/team-agent/SKILL.md +11 -3
@@ -0,0 +1,191 @@
1
+ use std::path::Path;
2
+
3
+ use rusqlite::Connection;
4
+
5
+ #[test]
6
+ #[serial_test::serial(env)]
7
+ fn copilot_fork_copies_isolated_home_and_rekeys_every_session_table() {
8
+ use crate::provider::adapters::copilot_fork::materialize_copilot_fork;
9
+ use std::sync::atomic::{AtomicU64, Ordering};
10
+
11
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
12
+ let root = std::env::temp_dir().join(format!(
13
+ "ta-copilot-fork-{}-{}",
14
+ std::process::id(),
15
+ COUNTER.fetch_add(1, Ordering::Relaxed)
16
+ ));
17
+ let source_home = root.join("source-home");
18
+ let workspace = root.join("workspace");
19
+ std::fs::create_dir_all(&workspace).unwrap();
20
+ let source = SessionId::new("11111111-2222-4333-8444-555555555555");
21
+ seed_copilot_fork_fixture(&source_home, source.as_str(), None);
22
+ let source_events = std::fs::read(
23
+ source_home
24
+ .join("session-state")
25
+ .join(source.as_str())
26
+ .join("events.jsonl"),
27
+ )
28
+ .unwrap();
29
+ let previous = std::env::var_os("COPILOT_HOME");
30
+ std::env::set_var("COPILOT_HOME", &source_home);
31
+
32
+ let materialized = materialize_copilot_fork(&workspace, "fork-a", &source).unwrap();
33
+ assert_ne!(materialized.session_id(), &source);
34
+ assert!(materialized
35
+ .home()
36
+ .starts_with(workspace.join(".team/runtime/provider-session-forks/copilot/fork-a")));
37
+ let new_id = materialized.session_id().as_str();
38
+ let target_state = materialized.home().join("session-state").join(new_id);
39
+ assert!(target_state.is_dir());
40
+ assert_eq!(
41
+ std::fs::read_to_string(target_state.join("events.jsonl")).unwrap(),
42
+ format!("{{\"session_id\":\"{new_id}\"}}\n")
43
+ );
44
+ let per_session = Connection::open(target_state.join("session.db")).unwrap();
45
+ let recipient: String = per_session
46
+ .query_row(
47
+ "select recipient_session_id from inbox_entries",
48
+ [],
49
+ |row| row.get(0),
50
+ )
51
+ .unwrap();
52
+ assert_eq!(recipient, new_id);
53
+
54
+ let central = Connection::open(materialized.home().join("session-store.db")).unwrap();
55
+ assert_eq!(session_count(&central, "sessions", "id", new_id), 1);
56
+ assert_eq!(
57
+ session_count(&central, "sessions", "id", source.as_str()),
58
+ 0
59
+ );
60
+ for table in [
61
+ "turns",
62
+ "checkpoints",
63
+ "session_files",
64
+ "session_refs",
65
+ "forge_trajectory_events",
66
+ "search_index",
67
+ ] {
68
+ assert_eq!(session_count(&central, table, "session_id", new_id), 1);
69
+ assert_eq!(
70
+ session_count(&central, table, "session_id", source.as_str()),
71
+ 0
72
+ );
73
+ }
74
+ assert_eq!(
75
+ std::fs::read(
76
+ source_home
77
+ .join("session-state")
78
+ .join(source.as_str())
79
+ .join("events.jsonl")
80
+ )
81
+ .unwrap(),
82
+ source_events,
83
+ "fork must not mutate source backing"
84
+ );
85
+
86
+ restore_env("COPILOT_HOME", previous);
87
+ let _ = std::fs::remove_dir_all(root);
88
+ }
89
+
90
+ #[test]
91
+ #[serial_test::serial(env)]
92
+ fn copilot_fork_fails_closed_when_a_required_session_table_is_missing() {
93
+ let root = std::env::temp_dir().join(format!("ta-copilot-fork-missing-{}", std::process::id()));
94
+ let source_home = root.join("source-home");
95
+ let workspace = root.join("workspace");
96
+ std::fs::create_dir_all(&workspace).unwrap();
97
+ let source = SessionId::new("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
98
+ seed_copilot_fork_fixture(&source_home, source.as_str(), Some("session_refs"));
99
+ let previous = std::env::var_os("COPILOT_HOME");
100
+ std::env::set_var("COPILOT_HOME", &source_home);
101
+
102
+ let error = crate::provider::adapters::copilot_fork::materialize_copilot_fork(
103
+ &workspace, "fork-b", &source,
104
+ )
105
+ .unwrap_err();
106
+ assert!(error.to_string().contains("session_refs"), "{error}");
107
+ let managed_parent = workspace.join(".team/runtime/provider-session-forks/copilot/fork-b");
108
+ assert!(
109
+ !managed_parent.exists() || std::fs::read_dir(managed_parent).unwrap().next().is_none(),
110
+ "failed materialization must leave no clone home"
111
+ );
112
+
113
+ restore_env("COPILOT_HOME", previous);
114
+ let _ = std::fs::remove_dir_all(root);
115
+ }
116
+
117
+ fn seed_copilot_fork_fixture(home: &Path, source: &str, omitted: Option<&str>) {
118
+ let state = home.join("session-state").join(source);
119
+ std::fs::create_dir_all(state.join("checkpoints")).unwrap();
120
+ std::fs::write(
121
+ state.join("workspace.yaml"),
122
+ format!("session_id: {source}\n"),
123
+ )
124
+ .unwrap();
125
+ std::fs::write(
126
+ state.join("events.jsonl"),
127
+ format!("{{\"session_id\":\"{source}\"}}\n"),
128
+ )
129
+ .unwrap();
130
+ let session_db = Connection::open(state.join("session.db")).unwrap();
131
+ session_db
132
+ .execute_batch(
133
+ "create table inbox_entries (id text primary key, recipient_session_id text not null);",
134
+ )
135
+ .unwrap();
136
+ session_db
137
+ .execute("insert into inbox_entries values ('one', ?1)", [source])
138
+ .unwrap();
139
+
140
+ let db = Connection::open(home.join("session-store.db")).unwrap();
141
+ db.execute_batch(
142
+ "create table sessions (id text primary key);
143
+ create table turns (id integer primary key, session_id text not null);
144
+ create table checkpoints (id integer primary key, session_id text not null);
145
+ create table session_files (id integer primary key, session_id text not null);
146
+ create table session_refs (id integer primary key, session_id text not null);
147
+ create table forge_trajectory_events (id integer primary key, session_id text not null);
148
+ create virtual table search_index using fts5(content, session_id unindexed);",
149
+ )
150
+ .unwrap();
151
+ db.execute("insert into sessions values (?1)", [source])
152
+ .unwrap();
153
+ for table in [
154
+ "turns",
155
+ "checkpoints",
156
+ "session_files",
157
+ "session_refs",
158
+ "forge_trajectory_events",
159
+ ] {
160
+ if omitted != Some(table) {
161
+ db.execute(
162
+ &format!("insert into {table} (session_id) values (?1)"),
163
+ [source],
164
+ )
165
+ .unwrap();
166
+ } else {
167
+ db.execute(&format!("drop table {table}"), []).unwrap();
168
+ }
169
+ }
170
+ db.execute(
171
+ "insert into search_index (content, session_id) values ('x', ?1)",
172
+ [source],
173
+ )
174
+ .unwrap();
175
+ }
176
+
177
+ fn session_count(conn: &Connection, table: &str, column: &str, id: &str) -> i64 {
178
+ conn.query_row(
179
+ &format!("select count(*) from {table} where {column} = ?1"),
180
+ [id],
181
+ |row| row.get(0),
182
+ )
183
+ .unwrap()
184
+ }
185
+
186
+ fn restore_env(key: &str, value: Option<std::ffi::OsString>) {
187
+ match value {
188
+ Some(value) => std::env::set_var(key, value),
189
+ None => std::env::remove_var(key),
190
+ }
191
+ }
@@ -29,3 +29,4 @@ include!("tests/classify.rs");
29
29
  include!("tests/idle.rs");
30
30
  include!("tests/adapter.rs");
31
31
  include!("tests/faults.rs");
32
+ include!("tests/copilot_fork.rs");
@@ -12,7 +12,9 @@ use std::os::unix::net::UnixListener;
12
12
  use std::path::Path;
13
13
  use std::sync::{Arc, Mutex};
14
14
 
15
- use super::{CommandOutput, CommandRunner, RealCommandRunner, TmuxBackend};
15
+ use super::{
16
+ CommandOutput, CommandRunner, RealCommandRunner, TmuxBackend, PANE_BINDING_NONCE_METADATA_KEY,
17
+ };
16
18
  use crate::model::enums::PaneLiveness;
17
19
  use crate::transport::{
18
20
  normalize_capture, tmux_capture_argv, tmux_query_argv, tmux_send_keys_argv, tmux_spawn_argv,
@@ -395,10 +397,7 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
395
397
  let pane_inventory = "%3\tteamsess\t0\tw1\t0\t/dev/ttys003\tnode\t1\t/work/dir\t1\t0\t123\n";
396
398
  let (be, rec) = backend_with(
397
399
  MockResp::Out(ok("")),
398
- vec![
399
- MockResp::Out(ok("%3\n")),
400
- MockResp::Out(ok(pane_inventory)),
401
- ],
400
+ vec![MockResp::Out(ok("%3\n")), MockResp::Out(ok(pane_inventory))],
402
401
  );
403
402
  let s = SessionName::new("teamsess");
404
403
  let w = WindowName::new("w1");
@@ -443,10 +442,7 @@ fn spawn_into_frames_via_new_window_builder() {
443
442
  let pane_inventory = "%4\tteamsess\t1\tw2\t0\t/dev/ttys004\tnode\t1\t/work/dir\t1\t0\t124\n";
444
443
  let (be, rec) = backend_with(
445
444
  MockResp::Out(ok("")),
446
- vec![
447
- MockResp::Out(ok("%4\n")),
448
- MockResp::Out(ok(pane_inventory)),
449
- ],
445
+ vec![MockResp::Out(ok("%4\n")), MockResp::Out(ok(pane_inventory))],
450
446
  );
451
447
  let s = SessionName::new("teamsess");
452
448
  let w = WindowName::new("w2");
@@ -518,10 +514,7 @@ fn spawn_with_command_refuses_and_rolls_back_spawn_pane_owned_by_other_window()
518
514
  let pane_inventory = "%5\tteamsess\t1\tw2\t0\t/dev/ttys005\tnode\t1\t/work/dir\t1\t0\t125\n";
519
515
  let (be, rec) = backend_with(
520
516
  MockResp::Out(ok("")),
521
- vec![
522
- MockResp::Out(ok("%5\n")),
523
- MockResp::Out(ok(pane_inventory)),
524
- ],
517
+ vec![MockResp::Out(ok("%5\n")), MockResp::Out(ok(pane_inventory))],
525
518
  );
526
519
  let err = be
527
520
  .spawn_into(
@@ -1791,9 +1784,9 @@ fn query_single_field_argv_and_nonzero_maps_to_none() {
1791
1784
  // display-message N+1 fallback is gone). leader_env stays the reverse-env real-machine bit.
1792
1785
  #[test]
1793
1786
  fn list_targets_argv_and_parses_tmux_pane_format() {
1794
- const FMT: &str = "#{pane_id}__TA_FIELD__#{session_name}__TA_FIELD__#{window_index}__TA_FIELD__#{window_name}__TA_FIELD__#{pane_index}__TA_FIELD__#{pane_tty}__TA_FIELD__#{pane_current_command}__TA_FIELD__#{pane_active}__TA_FIELD__#{pane_current_path}__TA_FIELD__#{session_attached}__TA_FIELD__#{pane_in_mode}__TA_FIELD__#{pane_pid}";
1795
- let stdout = "%7__TA_FIELD__team-x__TA_FIELD__0__TA_FIELD__win0__TA_FIELD__0__TA_FIELD__/dev/ttys003__TA_FIELD__codex__TA_FIELD__1__TA_FIELD__/Users/me/work__TA_FIELD__1__TA_FIELD__0__TA_FIELD__41001\n\
1796
- %8__TA_FIELD__team-x__TA_FIELD__1__TA_FIELD__win1__TA_FIELD__0__TA_FIELD__/dev/ttys004__TA_FIELD__node__TA_FIELD__0__TA_FIELD__/Users/me/other__TA_FIELD__0__TA_FIELD__0__TA_FIELD__41002\n";
1787
+ const FMT: &str = "#{pane_id}__TA_FIELD__#{session_name}__TA_FIELD__#{window_index}__TA_FIELD__#{window_name}__TA_FIELD__#{pane_index}__TA_FIELD__#{pane_tty}__TA_FIELD__#{pane_current_command}__TA_FIELD__#{pane_active}__TA_FIELD__#{pane_current_path}__TA_FIELD__#{session_attached}__TA_FIELD__#{pane_in_mode}__TA_FIELD__#{pane_pid}__TA_FIELD__#{@team_agent_pane_binding_nonce}";
1788
+ let stdout = "%7__TA_FIELD__team-x__TA_FIELD__0__TA_FIELD__win0__TA_FIELD__0__TA_FIELD__/dev/ttys003__TA_FIELD__codex__TA_FIELD__1__TA_FIELD__/Users/me/work__TA_FIELD__1__TA_FIELD__0__TA_FIELD__41001__TA_FIELD__nonce-7\n\
1789
+ %8__TA_FIELD__team-x__TA_FIELD__1__TA_FIELD__win1__TA_FIELD__0__TA_FIELD__/dev/ttys004__TA_FIELD__node__TA_FIELD__0__TA_FIELD__/Users/me/other__TA_FIELD__0__TA_FIELD__0__TA_FIELD__41002__TA_FIELD__\n";
1797
1790
  let (be, rec) = backend_with(MockResp::Out(ok(stdout)), vec![]);
1798
1791
  let panes = be.list_targets().expect("list_targets ok");
1799
1792
  assert_eq!(
@@ -1845,6 +1838,19 @@ fn list_targets_argv_and_parses_tmux_pane_format() {
1845
1838
  Some(41002),
1846
1839
  "field[11] -> pane_pid (second pane)"
1847
1840
  );
1841
+ assert_eq!(
1842
+ p.leader_env
1843
+ .get(PANE_BINDING_NONCE_METADATA_KEY)
1844
+ .map(String::as_str),
1845
+ Some("nonce-7"),
1846
+ "field[12] pane option -> pane-instance binding metadata"
1847
+ );
1848
+ assert!(
1849
+ !panes[1]
1850
+ .leader_env
1851
+ .contains_key(PANE_BINDING_NONCE_METADATA_KEY),
1852
+ "an empty pane option must remain absent"
1853
+ );
1848
1854
 
1849
1855
  // nonzero exit -> empty vec (golden returncode != 0 -> []).
1850
1856
  let (be, _r) = backend_with(
@@ -1859,6 +1865,35 @@ fn list_targets_argv_and_parses_tmux_pane_format() {
1859
1865
  );
1860
1866
  }
1861
1867
 
1868
+ #[test]
1869
+ fn set_pane_binding_nonce_is_socket_scoped_and_pane_local() {
1870
+ let rec = Arc::new(Mutex::new(Vec::new()));
1871
+ let runner = MockCommandRunner {
1872
+ recorded: Arc::clone(&rec),
1873
+ stdin_recorded: Arc::new(Mutex::new(Vec::new())),
1874
+ queue: Mutex::new(VecDeque::new()),
1875
+ default: MockResp::Out(ok("")),
1876
+ };
1877
+ let be =
1878
+ TmuxBackend::with_runner_for_tmux_endpoint(Box::new(runner), "/tmp/ta-pane-binding.sock");
1879
+ be.set_pane_binding_nonce(&PaneId::new("%7"), "nonce-7")
1880
+ .expect("set pane binding nonce");
1881
+ assert_eq!(
1882
+ rec.lock().unwrap()[0],
1883
+ svec(&[
1884
+ "tmux",
1885
+ "-S",
1886
+ "/tmp/ta-pane-binding.sock",
1887
+ "set-option",
1888
+ "-p",
1889
+ "-t",
1890
+ "%7",
1891
+ "@team_agent_pane_binding_nonce",
1892
+ "nonce-7",
1893
+ ])
1894
+ );
1895
+ }
1896
+
1862
1897
  // ── 12. attach_session (TRANSPORT TRIO) — `tmux attach-session -t <s>` -> Attached ──────────────
1863
1898
  // Golden tmux attach is `tmux attach-session -t <session>`; a successful attach -> AttachOutcome::
1864
1899
  // Attached. RED today: attach_session is unimplemented!() -> PANIC. The in-process lock asserts the
@@ -40,6 +40,9 @@ use crate::transport::{
40
40
  TransportError, TurnVerification, WindowName,
41
41
  };
42
42
 
43
+ pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
44
+ const TMUX_PANE_BINDING_NONCE_OPTION: &str = "@team_agent_pane_binding_nonce";
45
+
43
46
  /// Result of running an external command — the typed output of the OS edge.
44
47
  #[derive(Debug, Clone)]
45
48
  pub struct CommandOutput {
@@ -279,6 +282,23 @@ impl TmuxBackend {
279
282
  }
280
283
  }
281
284
 
285
+ pub(crate) fn set_pane_binding_nonce(
286
+ &self,
287
+ pane: &PaneId,
288
+ nonce: &str,
289
+ ) -> Result<(), TransportError> {
290
+ let argv = [
291
+ "tmux".to_string(),
292
+ "set-option".to_string(),
293
+ "-p".to_string(),
294
+ "-t".to_string(),
295
+ pane.as_str().to_string(),
296
+ TMUX_PANE_BINDING_NONCE_OPTION.to_string(),
297
+ nonce.to_string(),
298
+ ];
299
+ self.run_ok(&argv)
300
+ }
301
+
282
302
  /// Backend with an injected runner (tests: canned/recording tmux output). Shared default socket.
283
303
  pub fn with_runner(runner: Box<dyn CommandRunner>) -> Self {
284
304
  Self {
@@ -2340,7 +2360,7 @@ impl Transport for TmuxBackend {
2340
2360
  // killing the per-pane display-message N+1 fallback.
2341
2361
  // Use a printable sentinel so the 12-field frame stays explicit in argv/log evidence;
2342
2362
  // `parse_pane_info_line` retains compatibility with legacy tab-delimited output.
2343
- const TMUX_PANE_FORMAT: &str = "#{pane_id}__TA_FIELD__#{session_name}__TA_FIELD__#{window_index}__TA_FIELD__#{window_name}__TA_FIELD__#{pane_index}__TA_FIELD__#{pane_tty}__TA_FIELD__#{pane_current_command}__TA_FIELD__#{pane_active}__TA_FIELD__#{pane_current_path}__TA_FIELD__#{session_attached}__TA_FIELD__#{pane_in_mode}__TA_FIELD__#{pane_pid}";
2363
+ const TMUX_PANE_FORMAT: &str = "#{pane_id}__TA_FIELD__#{session_name}__TA_FIELD__#{window_index}__TA_FIELD__#{window_name}__TA_FIELD__#{pane_index}__TA_FIELD__#{pane_tty}__TA_FIELD__#{pane_current_command}__TA_FIELD__#{pane_active}__TA_FIELD__#{pane_current_path}__TA_FIELD__#{session_attached}__TA_FIELD__#{pane_in_mode}__TA_FIELD__#{pane_pid}__TA_FIELD__#{@team_agent_pane_binding_nonce}";
2344
2364
  let argv = self.tmux_argv(&[
2345
2365
  "tmux".to_string(),
2346
2366
  "list-panes".to_string(),
@@ -2544,6 +2564,13 @@ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
2544
2564
  if fields.len() < 11 {
2545
2565
  return None;
2546
2566
  }
2567
+ let mut leader_env = BTreeMap::new();
2568
+ if let Some(nonce) = fields.get(12).and_then(|raw| non_empty(raw)) {
2569
+ leader_env.insert(
2570
+ PANE_BINDING_NONCE_METADATA_KEY.to_string(),
2571
+ nonce.to_string(),
2572
+ );
2573
+ }
2547
2574
  Some(PaneInfo {
2548
2575
  pane_id: PaneId::new(fields[0]),
2549
2576
  session: SessionName::new(fields[1]),
@@ -2555,7 +2582,7 @@ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
2555
2582
  active: fields[7] == "1",
2556
2583
  current_path: non_empty(fields[8]).map(PathBuf::from),
2557
2584
  pane_pid: fields.get(11).and_then(|raw| parse_optional_u32(raw)),
2558
- leader_env: BTreeMap::new(),
2585
+ leader_env,
2559
2586
  })
2560
2587
  }
2561
2588
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.52",
3
+ "version": "0.5.54",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.52",
24
- "@team-agent/cli-darwin-x64": "0.5.52",
25
- "@team-agent/cli-linux-x64": "0.5.52"
23
+ "@team-agent/cli-darwin-arm64": "0.5.54",
24
+ "@team-agent/cli-darwin-x64": "0.5.54",
25
+ "@team-agent/cli-linux-x64": "0.5.54"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -11,6 +11,16 @@
11
11
  "agent_id": { "type": "string", "minLength": 1 },
12
12
  "status": { "enum": ["success", "blocked", "failed", "partial"] },
13
13
  "summary": { "type": "string" },
14
+ "presentation": {
15
+ "type": "object",
16
+ "required": ["sink", "class"],
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "sink": { "enum": ["leader", "casefile", "silent"] },
20
+ "class": { "enum": ["message", "progress", "stage_result", "stage_pass", "bounce", "blocking", "final_review", "timeout"] },
21
+ "case_id": { "type": "string", "minLength": 1 }
22
+ }
23
+ },
14
24
  "changes": {
15
25
  "type": "array",
16
26
  "items": {
@@ -87,15 +87,15 @@ Use `team-agent attach-leader` / `team-agent claim-leader` to bind the leader pa
87
87
 
88
88
  | Provider | Resume | Turn-state detection | Per-worker model override | Native session fork |
89
89
  |---|---|---|---|---|
90
- | `claude` / `claude_code` | yes (`--resume <id>`, transcript-verified) | yes (JSONL stream) | yes (role `model` overrides `provider_models`) | yes (`--fork-session` + new `--session-id`) |
90
+ | `claude` / `claude_code` | yes (`--resume <id>`, transcript-verified) | yes (JSONL stream) | yes (role `model` overrides `provider_models`) | yes (snapshot copy + only `--resume <snapshot-id>`) |
91
91
  | `codex` | yes (`codex resume <id>`, session-store-verified) | yes (turn JSONL) | yes (role `model`) | yes (`codex fork`) |
92
- | `copilot` | yes (`copilot --resume <id|name>`, sqlite `sessions` row) | not yet (phase 1: `provider.classify.unsupported` event) | yes (role `model`) | **no `CapabilityUnsupported`** |
92
+ | `copilot` | yes (`copilot --resume <id|name>`, sqlite `sessions` row) | not yet (phase 1: `provider.classify.unsupported` event) | yes (role `model`) | yes (isolated `COPILOT_HOME` store fork) |
93
93
  | `gemini_cli` | no | no | yes | no |
94
94
  | `fake` (testing only) | no | no | n/a | no |
95
95
 
96
96
  Notes:
97
97
  - Per-worker model override means a role-doc `model:` value wins over `TEAM.md` `provider_models.<provider>`; subscription defaults still fill blanks.
98
- - Copilot's `caps.fork=false` is a hard refusal in 0.3.7 `team-agent fork-agent` against a Copilot worker returns a structured `CapabilityUnsupported` error instead of falling back to a fresh spawn (honest by design).
98
+ - Copilot fork copies the source session into an isolated `COPILOT_HOME` and rekeys its SQLite session references atomically. Missing or incomplete backing fails closed; it never falls back to a fresh spawn.
99
99
  - Copilot phase-1 idle/turn detection is intentionally Unknown; tick emits a single explicit `provider.classify.unsupported` event per state change (P4 dedup), never a silent default.
100
100
 
101
101
  ## Provider Prep
@@ -166,6 +166,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
166
166
  - `quick-start` is only for first-time team creation from role docs. If that team already has runtime state, use `team-agent restart . --team <session_name_or_team_name>` to resume it. If restart cannot recover context, explain the loss and wait for explicit user consent before using `team-agent restart . --allow-fresh`; never reset context through quick-start.
167
167
  - If the user explicitly asks a worker to create or operate a nested child team, first read `references/team-in-team.md`. Child teams must use an independent child workspace, never the parent `.team/current`.
168
168
  - `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
169
+ - Advanced orchestration callers may add `--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]`. All sinks remain durable and pullable; `casefile`/`silent` suppress only live leader injection. Missing presentation metadata preserves the normal leader-visible behavior.
169
170
  - After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
170
171
  - `team-agent send --task task_initial "Start"` routes by task.
171
172
  - `team-agent status` shows team, worker health, result-store counts, `session_id`, `captured_via`, and attribution confidence. `team-agent status --json` is compact and context-safe by default; use `team-agent status --detail --json` only for raw runtime-state diagnostics.
@@ -213,10 +214,15 @@ team-agent add-agent reviewer --role-file .team/current/agents/reviewer.md --wor
213
214
  Semantic distinction:
214
215
 
215
216
  - `team-agent add-agent <agent> --role-file <file>` — add a **new** worker not yet in team state.
217
+ - `team-agent clone-agent <source> --as <new>` — reread the source worker's latest role file and start a fresh provider seat. It never copies conversation context. Success is initially honest `capture_state: pending_first_turn` with `session_id`, `new_session_id`, and `backing_path` all null; after the first turn, canonical capture changes the state to `captured` and fills the backing tuple.
218
+ - `team-agent fork-agent <source> --as <new>` — reread the same latest role file and create a distinct, verified provider session that forks the source context. If the provider backing cannot be verified, the command fails and rolls back instead of silently cloning fresh.
216
219
  - `team-agent start-agent <agent>` — (re)launch a worker that **already exists** in team state but whose window is missing.
220
+ - `team-agent reset-agent <agent> --discard-session` — keep the same seat and deliberately start it with fresh context.
217
221
  - `team-agent restart .` — resume a fully **stopped** team from stored worker sessions.
218
222
  - `team-agent quick-start <dir>` — first-time team creation from role docs; for existing teams use `restart`, and use `restart --allow-fresh` only after explicit user consent to discard context.
219
223
 
224
+ Clone/fork names are always explicit: run concurrent calls with a different `--as` value for each new seat. Fork success includes a verified new `session_id` and independent backing; a tmux window alone is not fork success. Clone success uses the honest `pending_first_turn` state above until first-turn capture, never a fabricated verified tuple. Updating the source role file affects the next clone/fork without requiring a full-team rebuild. Automatic knowledge write-back from a clone/fork into the source role file is not provided.
225
+
220
226
  Removing a worker at runtime is the symmetric `team-agent remove-agent <agent> --workspace . --confirm`.
221
227
 
222
228
  ## Worker Protocol
@@ -232,6 +238,8 @@ team_orchestrator.send_message(to="*", content="short broadcast")
232
238
  team_orchestrator.report_result(summary="short completion", status="success", tests=[{"command":"command","status":"passed"}])
233
239
  ```
234
240
 
241
+ For typed orchestration traffic, both `send_message` and `report_result` accept `presentation={"sink":"leader|casefile|silent","class":"message|progress|stage_result|stage_pass|bounce|blocking|final_review|timeout","case_id":"optional-case"}`. If the object is present, `sink` and `class` are required and unknown values fail closed. `casefile` and `silent` are durable-only, not deletion. The fixed critical classes `stage_pass`, `bounce`, `blocking`, `final_review`, and `timeout` always appear on the leader screen even when another sink is requested. Routing uses the typed class, never words in the content or summary.
242
+
235
243
  Do not pass `sender`, `task_id`, `requires_ack`, `schema_version`, or `agent_id` unless doing a low-level compatibility diagnostic. The MCP runtime fills those fields and keeps delivery metadata in runtime state and event logs. If provider env loses the worker id, MCP infers it from active task/message state and falls back to an explicit `unknown` sender instead of treating the worker as leader.
236
244
 
237
245
  Message targets are team-scoped. Use `leader`, another teammate agent id, or `*` for all other team members. The runtime excludes the sender from `*` broadcasts and never scans unrelated terminal windows for recipients.