@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +18 -2
- package/crates/team-agent/src/cli/emit.rs +111 -3
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +12 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
- package/crates/team-agent/src/db/message_store.rs +31 -2
- package/crates/team-agent/src/db/migration.rs +7 -6
- package/crates/team-agent/src/db/schema.rs +18 -5
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +324 -57
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tools.rs +89 -4
- package/crates/team-agent/src/mcp_server/types.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +65 -4
- package/crates/team-agent/src/messaging/delivery.rs +2 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +100 -21
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/persist.rs +68 -2
- package/crates/team-agent/src/messaging/presentation.rs +307 -0
- package/crates/team-agent/src/messaging/results.rs +130 -3
- package/crates/team-agent/src/messaging/selftest.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +54 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +85 -24
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +395 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +53 -92
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/schemas/result-envelope.schema.json +10 -0
- 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(¢ral, "sessions", "id", new_id), 1);
|
|
56
|
+
assert_eq!(
|
|
57
|
+
session_count(¢ral, "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(¢ral, table, "session_id", new_id), 1);
|
|
69
|
+
assert_eq!(
|
|
70
|
+
session_count(¢ral, 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
|
+
}
|
|
@@ -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::{
|
|
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/
|
|
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/
|
|
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
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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 (
|
|
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`) |
|
|
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
|
|
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.
|