@team-agent/installer 0.5.68 → 0.5.69
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/diagnose.rs +74 -0
- package/crates/team-agent/src/cli/emit.rs +5 -4
- package/crates/team-agent/src/cli/leader.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/compiler.rs +62 -5
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +53 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +67 -15
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +18 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +260 -44
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +10 -59
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +70 -28
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +10 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +3 -2
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +90 -160
- package/crates/team-agent/src/lifecycle/restart/common.rs +146 -28
- package/crates/team-agent/src/lifecycle/restart/remove.rs +37 -1
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +195 -18
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +30 -12
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +35 -48
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
- package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests.rs +7 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +61 -4
- package/crates/team-agent/src/messaging/delivery.rs +36 -12
- package/crates/team-agent/src/model/enums.rs +3 -1
- package/crates/team-agent/src/model/permissions.rs +1 -1
- package/crates/team-agent/src/provider/adapter.rs +29 -3
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +5 -2
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +1 -0
- package/crates/team-agent/src/provider/classify.rs +2 -2
- package/crates/team-agent/src/provider/faults.rs +1 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +2 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
- package/crates/team-agent/src/provider/session_scan.rs +4 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +80 -0
- package/crates/team-agent/src/provider/wire.rs +7 -1
- package/crates/team-agent/src/tmux_backend.rs +52 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +2 -4
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 在记录的 Pi 席位 session root 内按真实 header 捕获 exact JSONL backing
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: scan_session_store
|
|
6
|
+
//! what: 只读每个 JSONL 的首个完整 header,并按 id/cwd/spawned_at 返回候选
|
|
7
|
+
//! - name: validate_exact_backing
|
|
8
|
+
//! what: restart 前重验 exact path 的 id/cwd header
|
|
9
|
+
//! boundary:
|
|
10
|
+
//! - 只递归 recorded provider_projects_root,不扫描 HOME 或消息正文
|
|
11
|
+
//! - 不以文件名、mtime 或唯一文件替代 header 匹配
|
|
12
|
+
//! maturity: wired
|
|
13
|
+
//! ---
|
|
14
|
+
|
|
15
|
+
use std::io::BufRead;
|
|
16
|
+
use std::path::{Path, PathBuf};
|
|
17
|
+
|
|
18
|
+
use crate::provider::types::{CaptureVia, CapturedSession, Confidence, RolloutPath, SessionId};
|
|
19
|
+
use crate::provider::ProviderError;
|
|
20
|
+
|
|
21
|
+
use super::{CaptureSessionContext, CapturedSessionCandidate};
|
|
22
|
+
|
|
23
|
+
#[derive(serde::Deserialize)]
|
|
24
|
+
struct PiSessionHeader {
|
|
25
|
+
#[serde(rename = "type")]
|
|
26
|
+
record_type: String,
|
|
27
|
+
version: u64,
|
|
28
|
+
id: String,
|
|
29
|
+
cwd: PathBuf,
|
|
30
|
+
timestamp: String,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn paths_equal(left: &Path, right: &Path) -> bool {
|
|
34
|
+
let left = std::fs::canonicalize(left).unwrap_or_else(|_| left.to_path_buf());
|
|
35
|
+
let right = std::fs::canonicalize(right).unwrap_or_else(|_| right.to_path_buf());
|
|
36
|
+
left == right
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn read_header(path: &Path) -> Option<PiSessionHeader> {
|
|
40
|
+
let file = std::fs::File::open(path).ok()?;
|
|
41
|
+
let mut first_line = String::new();
|
|
42
|
+
let bytes = std::io::BufReader::new(file)
|
|
43
|
+
.read_line(&mut first_line)
|
|
44
|
+
.ok()?;
|
|
45
|
+
if bytes == 0 || !first_line.ends_with('\n') {
|
|
46
|
+
return None;
|
|
47
|
+
}
|
|
48
|
+
let header: PiSessionHeader = serde_json::from_str(first_line.trim_end()).ok()?;
|
|
49
|
+
(header.record_type == "session" && header.version == 3).then_some(header)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn header_matches(
|
|
53
|
+
header: &PiSessionHeader,
|
|
54
|
+
expected_session_id: &SessionId,
|
|
55
|
+
spawn_cwd: &Path,
|
|
56
|
+
spawned_at: Option<&str>,
|
|
57
|
+
) -> bool {
|
|
58
|
+
if header.id != expected_session_id.as_str() || !paths_equal(&header.cwd, spawn_cwd) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
let Ok(header_timestamp) = chrono::DateTime::parse_from_rfc3339(&header.timestamp) else {
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
let Some(spawned_at) = spawned_at else {
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
let Ok(spawned_at) = chrono::DateTime::parse_from_rfc3339(spawned_at) else {
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
header_timestamp >= spawned_at
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn collect_jsonl(root: &Path, out: &mut Vec<PathBuf>) -> Result<(), ProviderError> {
|
|
74
|
+
let entries = std::fs::read_dir(root)
|
|
75
|
+
.map_err(|error| ProviderError::Io(format!("{}: {error}", root.display())))?;
|
|
76
|
+
for entry in entries {
|
|
77
|
+
let entry =
|
|
78
|
+
entry.map_err(|error| ProviderError::Io(format!("{}: {error}", root.display())))?;
|
|
79
|
+
let file_type = entry
|
|
80
|
+
.file_type()
|
|
81
|
+
.map_err(|error| ProviderError::Io(format!("{}: {error}", entry.path().display())))?;
|
|
82
|
+
if file_type.is_symlink() {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let path = entry.path();
|
|
86
|
+
if file_type.is_dir() {
|
|
87
|
+
collect_jsonl(&path, out)?;
|
|
88
|
+
} else if file_type.is_file() && path.extension().is_some_and(|ext| ext == "jsonl") {
|
|
89
|
+
out.push(path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// ---
|
|
96
|
+
/// purpose: restart 前重验调用方给出的 exact Pi JSONL backing
|
|
97
|
+
/// returns: header type/version/id/cwd 全匹配时成功
|
|
98
|
+
/// errors: 文件缺失、header 不完整或 identity 不匹配时返回 ResumeUnavailable
|
|
99
|
+
/// ---
|
|
100
|
+
pub(crate) fn validate_exact_backing(
|
|
101
|
+
path: &Path,
|
|
102
|
+
expected_session_id: &SessionId,
|
|
103
|
+
spawn_cwd: &Path,
|
|
104
|
+
) -> Result<(), ProviderError> {
|
|
105
|
+
if !path.is_file() {
|
|
106
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
107
|
+
"Pi exact session backing is missing: {}",
|
|
108
|
+
path.display()
|
|
109
|
+
)));
|
|
110
|
+
}
|
|
111
|
+
let header = read_header(path).ok_or_else(|| {
|
|
112
|
+
ProviderError::ResumeUnavailable(format!(
|
|
113
|
+
"Pi exact session backing has no complete session header: {}",
|
|
114
|
+
path.display()
|
|
115
|
+
))
|
|
116
|
+
})?;
|
|
117
|
+
if !header_matches(&header, expected_session_id, spawn_cwd, None) {
|
|
118
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
119
|
+
"Pi exact session backing header does not match id/cwd: {}",
|
|
120
|
+
path.display()
|
|
121
|
+
)));
|
|
122
|
+
}
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// ---
|
|
127
|
+
/// purpose: 仅在 recorded seat root 内递归捕获 Pi session header 候选
|
|
128
|
+
/// returns: 每个 id/cwd/timestamp 精确匹配的 JSONL 各返回一个候选
|
|
129
|
+
/// errors: root 内目录枚举失败时返回 ProviderError
|
|
130
|
+
/// ---
|
|
131
|
+
pub(super) fn scan_session_store(
|
|
132
|
+
context: &CaptureSessionContext,
|
|
133
|
+
) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
|
|
134
|
+
let (Some(root), Some(expected_session_id), Some(spawned_at)) = (
|
|
135
|
+
context.provider_projects_root.as_deref(),
|
|
136
|
+
context.expected_session_id.as_ref(),
|
|
137
|
+
context.spawned_at.as_deref(),
|
|
138
|
+
) else {
|
|
139
|
+
return Ok(Vec::new());
|
|
140
|
+
};
|
|
141
|
+
if !root.is_dir() {
|
|
142
|
+
return Ok(Vec::new());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let mut paths = Vec::new();
|
|
146
|
+
collect_jsonl(root, &mut paths)?;
|
|
147
|
+
paths.sort();
|
|
148
|
+
Ok(paths
|
|
149
|
+
.into_iter()
|
|
150
|
+
.filter_map(|path| {
|
|
151
|
+
let header = read_header(&path)?;
|
|
152
|
+
header_matches(
|
|
153
|
+
&header,
|
|
154
|
+
expected_session_id,
|
|
155
|
+
&context.spawn_cwd,
|
|
156
|
+
Some(spawned_at),
|
|
157
|
+
)
|
|
158
|
+
.then(|| CapturedSessionCandidate {
|
|
159
|
+
captured: CapturedSession {
|
|
160
|
+
session_id: Some(expected_session_id.clone()),
|
|
161
|
+
rollout_path: Some(RolloutPath::new(path)),
|
|
162
|
+
captured_via: CaptureVia::FsWatch,
|
|
163
|
+
attribution_confidence: Confidence::High,
|
|
164
|
+
spawn_cwd: context.spawn_cwd.clone(),
|
|
165
|
+
},
|
|
166
|
+
embedded_agent_id: None,
|
|
167
|
+
positive_agent_id_match: false,
|
|
168
|
+
agent_path_match: false,
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
.collect())
|
|
172
|
+
}
|
|
@@ -16,6 +16,7 @@ pub(crate) mod common;
|
|
|
16
16
|
pub(crate) mod copilot;
|
|
17
17
|
pub(crate) mod cursor;
|
|
18
18
|
pub(crate) mod grok;
|
|
19
|
+
pub(crate) mod pi;
|
|
19
20
|
|
|
20
21
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
21
22
|
pub struct CaptureSessionContext {
|
|
@@ -49,6 +50,9 @@ pub(crate) fn scan_session_candidates_once(
|
|
|
49
50
|
if matches!(provider, Provider::CursorAgent) {
|
|
50
51
|
return Ok(cursor::scan_session_store(context));
|
|
51
52
|
}
|
|
53
|
+
if matches!(provider, Provider::Pi) {
|
|
54
|
+
return pi::scan_session_store(context);
|
|
55
|
+
}
|
|
52
56
|
if matches!(provider, Provider::Claude | Provider::ClaudeCode)
|
|
53
57
|
&& context.expected_session_id.is_some()
|
|
54
58
|
{
|
|
@@ -565,3 +565,83 @@ fn test_grok_unknown_tool_mapping_is_explicitly_unsupported() {
|
|
|
565
565
|
"known tools must stay mapped"
|
|
566
566
|
);
|
|
567
567
|
}
|
|
568
|
+
|
|
569
|
+
#[test]
|
|
570
|
+
fn pi_wire_roundtrip_requires_backing_and_has_no_builtin_model() {
|
|
571
|
+
use crate::provider::wire::{
|
|
572
|
+
aliases, builtin_provider_model, command_name, parse_canonical_provider, parse_provider,
|
|
573
|
+
provider_wire, requires_resume_backing,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
let provider = Provider::Pi;
|
|
577
|
+
assert_eq!(provider_wire(provider), "pi");
|
|
578
|
+
assert_eq!(parse_provider("pi"), Some(provider));
|
|
579
|
+
assert_eq!(parse_canonical_provider("pi"), Some(provider));
|
|
580
|
+
assert_eq!(aliases(provider), &["pi"]);
|
|
581
|
+
assert_eq!(command_name(provider), "pi");
|
|
582
|
+
assert!(requires_resume_backing(provider));
|
|
583
|
+
assert_eq!(builtin_provider_model(provider), None);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#[test]
|
|
587
|
+
fn pi_caps_resume_true_fork_false_native_mcp_false_auth_unknown() {
|
|
588
|
+
let adapter = get_adapter(Provider::Pi);
|
|
589
|
+
assert_eq!(
|
|
590
|
+
adapter.caps(),
|
|
591
|
+
ProviderCaps {
|
|
592
|
+
resume: true,
|
|
593
|
+
fork: false,
|
|
594
|
+
native_mcp_config: false,
|
|
595
|
+
writes_global_settings: false,
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
assert_eq!(
|
|
599
|
+
adapter.auth_hint(AuthMode::Subscription),
|
|
600
|
+
AuthHintStatus::Unknown
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#[test]
|
|
605
|
+
fn pi_fork_is_typed_capability_unsupported() {
|
|
606
|
+
let adapter = get_adapter(Provider::Pi);
|
|
607
|
+
let source = SessionId::new("7dcfed9a-88ce-45bf-b1e3-161696acfe89");
|
|
608
|
+
let result = adapter.fork(Some(&source), AuthMode::Subscription, None);
|
|
609
|
+
assert!(
|
|
610
|
+
matches!(result, Err(ProviderError::CapabilityUnsupported(_))),
|
|
611
|
+
"Pi fork must be a typed capability refusal, got {result:?}"
|
|
612
|
+
);
|
|
613
|
+
if let Err(error) = result {
|
|
614
|
+
let text = error.to_string();
|
|
615
|
+
assert!(text.contains("pi") && text.contains("fork"), "got {text}");
|
|
616
|
+
assert!(!text.to_ascii_lowercase().contains("clone"), "got {text}");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[test]
|
|
621
|
+
fn pi_classify_has_no_lifecycle_facts() {
|
|
622
|
+
let transcript = concat!(
|
|
623
|
+
r#"{"type":"turn_started","turn_id":"turn-1"}"#,
|
|
624
|
+
"\n",
|
|
625
|
+
r#"{"type":"turn_completed","turn_id":"turn-1"}"#,
|
|
626
|
+
);
|
|
627
|
+
let classified = crate::provider::classify::classify(
|
|
628
|
+
Provider::Pi,
|
|
629
|
+
transcript,
|
|
630
|
+
ProcessLiveness::Alive,
|
|
631
|
+
60.0,
|
|
632
|
+
)
|
|
633
|
+
.expect("Pi classification must preserve unknown without a turn reader");
|
|
634
|
+
assert_eq!(classified.state, TurnState::Unknown);
|
|
635
|
+
assert_eq!(classified.reason, "no_turn_lifecycle_fact");
|
|
636
|
+
|
|
637
|
+
let records = [serde_json::json!({
|
|
638
|
+
"type": "system",
|
|
639
|
+
"subtype": "api_error",
|
|
640
|
+
"level": "error",
|
|
641
|
+
"sessionId": "pi-session"
|
|
642
|
+
})];
|
|
643
|
+
assert!(
|
|
644
|
+
crate::provider::faults::read_fault_facts(&records, Provider::Pi).is_empty(),
|
|
645
|
+
"Pi has no JSONL lifecycle/fault reader"
|
|
646
|
+
);
|
|
647
|
+
}
|
|
@@ -36,6 +36,7 @@ pub(crate) fn provider_wire(provider: Provider) -> &'static str {
|
|
|
36
36
|
Provider::GeminiCli => "gemini_cli",
|
|
37
37
|
Provider::Grok => "grok",
|
|
38
38
|
Provider::CursorAgent => "cursor_agent",
|
|
39
|
+
Provider::Pi => "pi",
|
|
39
40
|
Provider::Fake => "fake",
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -83,6 +84,7 @@ pub(crate) fn requires_resume_backing(provider: Provider) -> bool {
|
|
|
83
84
|
| Provider::Copilot
|
|
84
85
|
| Provider::Grok
|
|
85
86
|
| Provider::CursorAgent
|
|
87
|
+
| Provider::Pi
|
|
86
88
|
)
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -95,6 +97,7 @@ pub(crate) fn provider_model_keys(provider: Provider) -> &'static [&'static str]
|
|
|
95
97
|
Provider::GeminiCli => &["gemini_cli"],
|
|
96
98
|
Provider::Grok => &["grok"],
|
|
97
99
|
Provider::CursorAgent => &["cursor_agent"],
|
|
100
|
+
Provider::Pi => &["pi"],
|
|
98
101
|
Provider::Fake => &["fake"],
|
|
99
102
|
}
|
|
100
103
|
}
|
|
@@ -106,7 +109,7 @@ pub(crate) fn builtin_provider_model(provider: Provider) -> Option<&'static str>
|
|
|
106
109
|
// grok: compiler 另有缺 model 硬闸,这里的 builtin 不再被 grok 角色吃到。
|
|
107
110
|
// cursor_agent: 禁止隐式默认(本机 shim 会剥 sonnet-4-thinking);缺 model 编译失败。
|
|
108
111
|
Provider::Grok => Some("grok-4"),
|
|
109
|
-
Provider::CursorAgent => None,
|
|
112
|
+
Provider::CursorAgent | Provider::Pi => None,
|
|
110
113
|
Provider::Copilot | Provider::GeminiCli | Provider::Fake => None,
|
|
111
114
|
}
|
|
112
115
|
}
|
|
@@ -130,6 +133,7 @@ pub(crate) fn aliases(provider: Provider) -> &'static [&'static str] {
|
|
|
130
133
|
// 是 exec 二进制名, 仅作 alias (不参与 command-text 子串匹配, 见
|
|
131
134
|
// leader/provider_attribution.rs TODO)。
|
|
132
135
|
Provider::CursorAgent => &["cursor_agent", "agent"],
|
|
136
|
+
Provider::Pi => &["pi"],
|
|
133
137
|
Provider::Fake => &["fake"],
|
|
134
138
|
}
|
|
135
139
|
}
|
|
@@ -146,6 +150,7 @@ pub(crate) fn command_name(provider: Provider) -> &'static str {
|
|
|
146
150
|
Provider::GeminiCli => "gemini",
|
|
147
151
|
Provider::Grok => "grok",
|
|
148
152
|
Provider::CursorAgent => "agent",
|
|
153
|
+
Provider::Pi => "pi",
|
|
149
154
|
Provider::Fake => "team-agent",
|
|
150
155
|
}
|
|
151
156
|
}
|
|
@@ -158,6 +163,7 @@ const ALL_PROVIDERS: &[Provider] = &[
|
|
|
158
163
|
Provider::GeminiCli,
|
|
159
164
|
Provider::Grok,
|
|
160
165
|
Provider::CursorAgent,
|
|
166
|
+
Provider::Pi,
|
|
161
167
|
Provider::Fake,
|
|
162
168
|
];
|
|
163
169
|
|
|
@@ -240,6 +240,8 @@ impl TmuxSocketEndpoint {
|
|
|
240
240
|
pub(crate) enum RuntimeTmuxEndpointSource {
|
|
241
241
|
StateTmuxEndpoint,
|
|
242
242
|
StateTmuxSocket,
|
|
243
|
+
LeaderReceiverTmuxEndpoint,
|
|
244
|
+
LeaderReceiverTmuxSocket,
|
|
243
245
|
WorkspaceFallback,
|
|
244
246
|
}
|
|
245
247
|
|
|
@@ -253,6 +255,8 @@ impl RuntimeTmuxEndpointSource {
|
|
|
253
255
|
match self {
|
|
254
256
|
Self::StateTmuxEndpoint => "state.tmux_endpoint",
|
|
255
257
|
Self::StateTmuxSocket => "state.tmux_socket",
|
|
258
|
+
Self::LeaderReceiverTmuxEndpoint => "leader_receiver.tmux_endpoint",
|
|
259
|
+
Self::LeaderReceiverTmuxSocket => "leader_receiver.tmux_socket",
|
|
256
260
|
Self::WorkspaceFallback => "workspace_fallback",
|
|
257
261
|
}
|
|
258
262
|
}
|
|
@@ -608,6 +612,50 @@ fn runtime_tmux_endpoint_from_state(
|
|
|
608
612
|
})
|
|
609
613
|
}
|
|
610
614
|
|
|
615
|
+
/// ---
|
|
616
|
+
/// purpose: 读取 selected team's persisted leader receiver endpoint
|
|
617
|
+
/// params:
|
|
618
|
+
/// state: 运行期全量或 selected-team 投影 JSON
|
|
619
|
+
/// returns: nested selected receiver 优先,其次投影顶层 receiver;二者都缺则 None
|
|
620
|
+
/// boundary: 只读 owning receiver,不读取普通 tmux_endpoint,不探活
|
|
621
|
+
/// ---
|
|
622
|
+
pub(crate) fn owning_tmux_endpoint_from_state(
|
|
623
|
+
state: &serde_json::Value,
|
|
624
|
+
) -> Option<(&str, RuntimeTmuxEndpointSource)> {
|
|
625
|
+
fn receiver_endpoint(state: &serde_json::Value) -> Option<(&str, RuntimeTmuxEndpointSource)> {
|
|
626
|
+
state
|
|
627
|
+
.pointer("/leader_receiver/tmux_endpoint")
|
|
628
|
+
.and_then(serde_json::Value::as_str)
|
|
629
|
+
.filter(|endpoint| !endpoint.is_empty())
|
|
630
|
+
.map(|endpoint| {
|
|
631
|
+
(
|
|
632
|
+
endpoint,
|
|
633
|
+
RuntimeTmuxEndpointSource::LeaderReceiverTmuxEndpoint,
|
|
634
|
+
)
|
|
635
|
+
})
|
|
636
|
+
.or_else(|| {
|
|
637
|
+
state
|
|
638
|
+
.pointer("/leader_receiver/tmux_socket")
|
|
639
|
+
.and_then(serde_json::Value::as_str)
|
|
640
|
+
.filter(|endpoint| !endpoint.is_empty())
|
|
641
|
+
.map(|endpoint| {
|
|
642
|
+
(
|
|
643
|
+
endpoint,
|
|
644
|
+
RuntimeTmuxEndpointSource::LeaderReceiverTmuxSocket,
|
|
645
|
+
)
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
state
|
|
651
|
+
.get("active_team_key")
|
|
652
|
+
.and_then(serde_json::Value::as_str)
|
|
653
|
+
.filter(|team_key| !team_key.is_empty())
|
|
654
|
+
.and_then(|team_key| state.get("teams").and_then(|teams| teams.get(team_key)))
|
|
655
|
+
.and_then(receiver_endpoint)
|
|
656
|
+
.or_else(|| receiver_endpoint(state))
|
|
657
|
+
}
|
|
658
|
+
|
|
611
659
|
/// CP-1 socket name: SHORT + DETERMINISTIC per canonical workspace path. `ta-` + 12 hex chars of a
|
|
612
660
|
/// stable FNV-1a hash over the canonicalized path. AF_UNIX `sun_path` is ~104 chars and the socket
|
|
613
661
|
/// lives at `/tmp/tmux-<uid>/<name>`, so we must NOT use the (~88-char) session name. §10: a
|
|
@@ -651,6 +699,10 @@ pub fn runtime_tmux_endpoint_from_state_pub(
|
|
|
651
699
|
let src = match source {
|
|
652
700
|
RuntimeTmuxEndpointSource::StateTmuxEndpoint => "state.tmux_endpoint",
|
|
653
701
|
RuntimeTmuxEndpointSource::StateTmuxSocket => "state.tmux_socket",
|
|
702
|
+
RuntimeTmuxEndpointSource::LeaderReceiverTmuxEndpoint => {
|
|
703
|
+
"leader_receiver.tmux_endpoint"
|
|
704
|
+
}
|
|
705
|
+
RuntimeTmuxEndpointSource::LeaderReceiverTmuxSocket => "leader_receiver.tmux_socket",
|
|
654
706
|
RuntimeTmuxEndpointSource::WorkspaceFallback => "workspace_fallback",
|
|
655
707
|
};
|
|
656
708
|
(endpoint.to_string(), src)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.69",
|
|
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.69",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.69",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.69"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -17,8 +17,6 @@ team-agent --help
|
|
|
17
17
|
team-agent doctor --help
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
**Launch** from a tmux-addressable pane: `team-agent claude` or `team-agent codex`, then `team-agent quick-start .team/current`. Do not start a real team from a naked terminal. Existing tmux/Ghostty layouts are valid if `quick-start` runs from the leader pane.
|
|
21
|
-
|
|
22
20
|
**Operate**
|
|
23
21
|
|
|
24
22
|
- Dispatch: `team-agent send TO MESSAGE` (positional TO; `--watch-result` is deprecated). After success, do not poll with `sleep` / `status` / `inbox` / `collect`.
|
|
@@ -50,7 +48,7 @@ Frontmatter: `provider: cursor_agent` (not `cursor`; launcher verb is `team-agen
|
|
|
50
48
|
|
|
51
49
|
`model:` is required (omit compile-fails; blocks a silent builtin `sonnet-4-thinking`). The flag stays on argv. Same-family catalog ids can change pane chrome. An id not in that provider's catalog silent-falls back (landing not stable; no events/stderr/pane warning). Pick names from the catalog; after spawn, `capture-pane` once for chrome. Do not treat the role field or pane chrome as proof of the live model.
|
|
52
50
|
|
|
53
|
-
One `cursor_agent` per workspace.
|
|
51
|
+
One `cursor_agent` per **provider-config overlay** under `<workspace>/.team/runtime/provider-config/<id>/cursor`. Same workspace may run multiple cursor seats; each writes `TEAM_AGENT_ID` into that project's `.cursor/mcp.json`. Spawn points `--workspace` there and `--add-dir` at the real workspace. `HOME` stays the user home. If isolation is off or cannot be materialized, adding a second seat still fail-closes:
|
|
54
52
|
|
|
55
53
|
```
|
|
56
54
|
error: cursor_agent seat already occupies this workspace
|
|
@@ -58,7 +56,7 @@ reason: <workspace>/.cursor/mcp.json is directory-scoped; a second seat overwrit
|
|
|
58
56
|
action: do not add another CursorAgent in this workspace until per-seat MCP identity is isolated
|
|
59
57
|
```
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
Do not share `<workspace>/.cursor/mcp.json` for team_orchestrator identity. Same seat, fresh context → `reset-agent --discard-session`.
|
|
62
60
|
|
|
63
61
|
`clone-agent` copies the source role (provider unchanged). Runtime add: `clone-agent` → `stop-agent` → `remove-agent --confirm` (deletes `.team/dynamic-role-files/`) → write the role file → `add-agent --role-file` → dispatch.
|
|
64
62
|
|