@team-agent/installer 0.5.68 → 0.5.70
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 +54 -7
- 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 +838 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +60 -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 +136 -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 +145 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +243 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +856 -0
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +387 -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 +146 -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 +9 -5
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: Pi CLI 的纯工具映射与 fresh/resume argv 构造
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: build_pi_command_argv
|
|
6
|
+
//! what: 仅从已验证输入构造保留 direct Pi 配置的 Team Agent argv
|
|
7
|
+
//! boundary:
|
|
8
|
+
//! - 不发现 executable、catalog、adapter 或 MCP 配置
|
|
9
|
+
//! - 除 resume exact backing 存在性外不读文件;不写文件、不启动进程
|
|
10
|
+
//! maturity: wired
|
|
11
|
+
//! ---
|
|
12
|
+
|
|
13
|
+
use std::path::Path;
|
|
14
|
+
|
|
15
|
+
use crate::model::enums::ProviderEffort;
|
|
16
|
+
use crate::provider::ProviderError;
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
19
|
+
pub(crate) enum PiToolMapping {
|
|
20
|
+
Mcp,
|
|
21
|
+
Builtin(&'static [&'static str]),
|
|
22
|
+
Unsupported,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// ---
|
|
26
|
+
/// purpose: 把 Team tool category 映射为 exact Pi builtin/proxy names
|
|
27
|
+
/// returns: 单个或多个 builtin、MCP proxy 或 Unsupported
|
|
28
|
+
/// ---
|
|
29
|
+
pub(crate) fn pi_tool_mapping(category: &str) -> PiToolMapping {
|
|
30
|
+
match category {
|
|
31
|
+
"mcp_team" => PiToolMapping::Mcp,
|
|
32
|
+
"fs_read" => PiToolMapping::Builtin(&["read"]),
|
|
33
|
+
"fs_list" => PiToolMapping::Builtin(&["grep", "find", "ls"]),
|
|
34
|
+
"fs_write" => PiToolMapping::Builtin(&["edit", "write"]),
|
|
35
|
+
"execute_bash" => PiToolMapping::Builtin(&["bash"]),
|
|
36
|
+
_ => PiToolMapping::Unsupported,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Copy)]
|
|
41
|
+
pub(crate) enum PiSessionSelector<'a> {
|
|
42
|
+
Fresh { session_id: &'a str },
|
|
43
|
+
Resume { path: &'a Path },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub(crate) struct PiCommandRequest<'a> {
|
|
47
|
+
pub executable: &'a Path,
|
|
48
|
+
pub extension: &'a Path,
|
|
49
|
+
pub model: Option<&'a str>,
|
|
50
|
+
pub effort: Option<ProviderEffort>,
|
|
51
|
+
pub system_prompt: &'a str,
|
|
52
|
+
pub tool_categories: &'a [&'a str],
|
|
53
|
+
pub session_dir: &'a Path,
|
|
54
|
+
pub session: PiSessionSelector<'a>,
|
|
55
|
+
pub agent_id: &'a str,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// ---
|
|
59
|
+
/// purpose: 从已验证 semantic request 构造仅追加 Team Agent MCP/提示/session 的 Pi argv
|
|
60
|
+
/// returns: exact ordered fresh/resume argv
|
|
61
|
+
/// errors: 必需字段、mcp_team 或工具 category 非法时返回 ProviderError
|
|
62
|
+
/// ---
|
|
63
|
+
pub(crate) fn build_pi_command_argv(
|
|
64
|
+
request: PiCommandRequest<'_>,
|
|
65
|
+
) -> Result<Vec<String>, ProviderError> {
|
|
66
|
+
if request.executable.as_os_str().is_empty()
|
|
67
|
+
|| request.extension.as_os_str().is_empty()
|
|
68
|
+
|| request.session_dir.as_os_str().is_empty()
|
|
69
|
+
|| request.model.is_some_and(|model| model.trim().is_empty())
|
|
70
|
+
|| request.system_prompt.trim().is_empty()
|
|
71
|
+
|| request.agent_id.trim().is_empty()
|
|
72
|
+
{
|
|
73
|
+
return Err(ProviderError::Command(
|
|
74
|
+
"Pi command requires executable, extension, prompt, session directory, and agent id"
|
|
75
|
+
.to_string(),
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let mut has_mcp = false;
|
|
80
|
+
for category in request.tool_categories {
|
|
81
|
+
match pi_tool_mapping(category) {
|
|
82
|
+
PiToolMapping::Mcp => has_mcp = true,
|
|
83
|
+
PiToolMapping::Builtin(_) => {}
|
|
84
|
+
PiToolMapping::Unsupported => {
|
|
85
|
+
return Err(ProviderError::Command(format!(
|
|
86
|
+
"Pi does not support Team Agent tool category {category:?}"
|
|
87
|
+
)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if !has_mcp {
|
|
92
|
+
return Err(ProviderError::Command(
|
|
93
|
+
"Pi command requires mcp_team".to_string(),
|
|
94
|
+
));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let mut argv = vec![
|
|
98
|
+
request.executable.to_string_lossy().into_owned(),
|
|
99
|
+
"-e".to_string(),
|
|
100
|
+
request.extension.to_string_lossy().into_owned(),
|
|
101
|
+
];
|
|
102
|
+
if let Some(model) = request.model {
|
|
103
|
+
argv.push("--model".to_string());
|
|
104
|
+
argv.push(model.to_string());
|
|
105
|
+
}
|
|
106
|
+
if let Some(effort) = request.effort {
|
|
107
|
+
argv.push("--thinking".to_string());
|
|
108
|
+
argv.push(effort.as_str().to_string());
|
|
109
|
+
}
|
|
110
|
+
argv.extend([
|
|
111
|
+
"--append-system-prompt".to_string(),
|
|
112
|
+
request.system_prompt.to_string(),
|
|
113
|
+
"--session-dir".to_string(),
|
|
114
|
+
request.session_dir.to_string_lossy().into_owned(),
|
|
115
|
+
]);
|
|
116
|
+
// The shared lifecycle materializer always revalidates resume root/path/header.
|
|
117
|
+
// Keep the pure argv builder usable before a fixture root is materialized, while
|
|
118
|
+
// refusing a missing exact file once the recorded path's parent exists.
|
|
119
|
+
match request.session {
|
|
120
|
+
PiSessionSelector::Fresh { session_id } if !session_id.trim().is_empty() => {
|
|
121
|
+
argv.push("--session-id".to_string());
|
|
122
|
+
argv.push(session_id.to_string());
|
|
123
|
+
}
|
|
124
|
+
PiSessionSelector::Resume { path }
|
|
125
|
+
if !path.as_os_str().is_empty()
|
|
126
|
+
&& (!path.parent().is_some_and(Path::is_dir) || path.is_file()) =>
|
|
127
|
+
{
|
|
128
|
+
argv.push("--session".to_string());
|
|
129
|
+
argv.push(path.to_string_lossy().into_owned());
|
|
130
|
+
}
|
|
131
|
+
PiSessionSelector::Resume { path } if !path.as_os_str().is_empty() => {
|
|
132
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
133
|
+
"Pi exact session backing is missing: {}",
|
|
134
|
+
path.display()
|
|
135
|
+
)));
|
|
136
|
+
}
|
|
137
|
+
_ => {
|
|
138
|
+
return Err(ProviderError::Command(
|
|
139
|
+
"Pi command requires a non-empty session selector".to_string(),
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
argv.push("--name".to_string());
|
|
144
|
+
argv.push(request.agent_id.to_string());
|
|
145
|
+
Ok(argv)
|
|
146
|
+
}
|
|
@@ -24,6 +24,7 @@ pub(crate) fn provider_bypass_flag(provider: Provider) -> Option<&'static str> {
|
|
|
24
24
|
Provider::Copilot => Some("--allow-all"),
|
|
25
25
|
Provider::Grok => Some("--always-approve"),
|
|
26
26
|
Provider::CursorAgent => Some("--force"),
|
|
27
|
+
Provider::Pi => None,
|
|
27
28
|
// TODO: 查 gemini_cli 的等价 bypass 参数;未定义前 fail-loud。
|
|
28
29
|
Provider::GeminiCli => None,
|
|
29
30
|
Provider::Fake => None,
|
|
@@ -215,7 +215,7 @@ fn latest_explicit_error_fact_for_record(
|
|
|
215
215
|
Provider::Claude | Provider::ClaudeCode => claude_latest_explicit_error_fact(record),
|
|
216
216
|
// C-3-5 cr verdict: copilot N35 通知不依赖 turn-state 分类;一期 Unknown,
|
|
217
217
|
// 与 GeminiCli/Fake 同精神。二期接 sqlite turns 表后再回填。
|
|
218
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
218
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
219
219
|
| Provider::Fake => None,
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -379,7 +379,7 @@ fn extract_lifecycle_facts(
|
|
|
379
379
|
Provider::Codex => codex_lifecycle_fact(record),
|
|
380
380
|
// C-3-1 cr verdict: copilot lifecycle facts 一期不导出(classify→None,
|
|
381
381
|
// Unknown);二期读 sqlite turns 表(turn_index/assistant_response)。
|
|
382
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
382
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
383
383
|
| Provider::Fake => None,
|
|
384
384
|
})
|
|
385
385
|
.collect()
|
|
@@ -46,7 +46,7 @@ fn fault_fact(provider: Provider, record: &serde_json::Value) -> Option<FaultFac
|
|
|
46
46
|
match provider {
|
|
47
47
|
Provider::Claude | Provider::ClaudeCode => claude_fault_fact(record),
|
|
48
48
|
Provider::Codex => codex_fault_fact(record),
|
|
49
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
49
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
50
50
|
| Provider::Fake => None,
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -69,7 +69,7 @@ pub(crate) fn context_fork_convergence_deadline(provider: Provider) -> Duration
|
|
|
69
69
|
match provider {
|
|
70
70
|
Provider::Claude | Provider::ClaudeCode => Duration::from_secs(45),
|
|
71
71
|
Provider::Codex => Duration::from_secs(10),
|
|
72
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
72
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
73
73
|
| Provider::Fake => Duration::from_secs(5),
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -252,6 +252,7 @@ fn provider_backing_root(provider: Provider, plan: &CommandPlan) -> PathBuf {
|
|
|
252
252
|
Provider::Claude | Provider::ClaudeCode => home.join(".claude").join("projects"),
|
|
253
253
|
Provider::Codex => home.join(".codex").join("sessions"),
|
|
254
254
|
Provider::Copilot => home.join(".copilot").join("session-state"),
|
|
255
|
+
Provider::Pi => PathBuf::new(),
|
|
255
256
|
Provider::Grok | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => home,
|
|
256
257
|
}
|
|
257
258
|
}
|
|
@@ -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.70",
|
|
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.70",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.70",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.70"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|