@team-agent/installer 0.4.5 → 0.4.7
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 +6 -1
- package/crates/team-agent/src/cli/status_port.rs +119 -66
- package/crates/team-agent/src/cli/tests/status_send.rs +74 -48
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +6 -1
- package/crates/team-agent/src/coordinator/tick.rs +46 -21
- package/crates/team-agent/src/leader/helpers.rs +1 -22
- package/crates/team-agent/src/lifecycle/launch.rs +34 -17
- package/crates/team-agent/src/lifecycle/profile_launch.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +160 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -22
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -50
- package/crates/team-agent/src/lifecycle/restart/selection.rs +23 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +19 -11
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +23 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +163 -15
- package/crates/team-agent/src/provider/adapter.rs +217 -377
- package/crates/team-agent/src/provider/adapters/claude.rs +106 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +117 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +124 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +17 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +27 -0
- package/crates/team-agent/src/provider/mod.rs +6 -0
- package/crates/team-agent/src/provider/session/capture.rs +593 -55
- package/crates/team-agent/src/provider/wire.rs +139 -0
- package/crates/team-agent/src/state/persist.rs +225 -10
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +6 -0
|
@@ -6,7 +6,7 @@ use std::process::Command;
|
|
|
6
6
|
use super::helpers::{find_session_id, parse_jsonl_records, patterns};
|
|
7
7
|
use super::types::{
|
|
8
8
|
AuthHintStatus, CaptureVia, CapturedSession, CommandPlan, Confidence, McpConfig,
|
|
9
|
-
ProviderCaps, ProviderCommandContext,
|
|
9
|
+
ProviderCaps, ProviderCommandContext, ProviderError, RolloutPath,
|
|
10
10
|
SessionId, StatusPatterns,
|
|
11
11
|
};
|
|
12
12
|
use super::{AuthMode, Provider};
|
|
@@ -296,7 +296,7 @@ pub fn get_adapter(p: Provider) -> Box<dyn ProviderAdapter> {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
#[derive(Debug, Clone, Copy)]
|
|
299
|
-
struct BasicProviderAdapter {
|
|
299
|
+
pub(crate) struct BasicProviderAdapter {
|
|
300
300
|
provider: Provider,
|
|
301
301
|
}
|
|
302
302
|
|
|
@@ -438,6 +438,23 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
438
438
|
) -> Result<CommandPlan, ProviderError> {
|
|
439
439
|
match self.provider {
|
|
440
440
|
Provider::Claude | Provider::ClaudeCode => {
|
|
441
|
+
// 0.4.7 (B1 verified, restart partial resume): RESTORE
|
|
442
|
+
// `--session-id <uuid>` on Claude fresh spawn. B1 confirmed
|
|
443
|
+
// Claude ≥ 2.1.185 honours the framework-supplied --session-id
|
|
444
|
+
// and creates a transcript at that id. The earlier 0.4.6
|
|
445
|
+
// P0 removal (commit 9feafc31) is reverted; the actual issue
|
|
446
|
+
// it tried to fix (leader-marker pollution) is addressed
|
|
447
|
+
// separately in commit d39b5104 (leader session exclusion
|
|
448
|
+
// in capture/repair).
|
|
449
|
+
//
|
|
450
|
+
// Fresh path: --session-id <expected> → Claude writes
|
|
451
|
+
// transcript at that id → capture/restart restore use the
|
|
452
|
+
// SAME id we predicted, so apply-time backing-store check
|
|
453
|
+
// passes immediately (no cwd+spawned_at attribution race).
|
|
454
|
+
//
|
|
455
|
+
// Resume path unchanged — `build_resume_command_plan` uses
|
|
456
|
+
// `--resume <existing_id>` on a session id that already has
|
|
457
|
+
// a real transcript.
|
|
441
458
|
let expected = next_session_token();
|
|
442
459
|
let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
|
|
443
460
|
let projects_root = ctx
|
|
@@ -458,9 +475,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
458
475
|
// Layer 1 self-healing (architect probe 2026-06-22, claude help
|
|
459
476
|
// `-n, --name <name>`): pass `--name <agent_id>` so the
|
|
460
477
|
// resume picker and on-disk `~/.claude/sessions/*.json name`
|
|
461
|
-
// field carry our role label.
|
|
462
|
-
// hint — primary restart key remains `session_id + backing
|
|
463
|
-
// store revalidation` (see provider/session/resume.rs).
|
|
478
|
+
// field carry our role label.
|
|
464
479
|
if let Some(agent_id) = ctx.agent_id_hint {
|
|
465
480
|
if !agent_id.is_empty() {
|
|
466
481
|
argv.push("--name".to_string());
|
|
@@ -913,26 +928,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
913
928
|
}
|
|
914
929
|
}
|
|
915
930
|
|
|
916
|
-
|
|
917
|
-
match provider {
|
|
918
|
-
Provider::Claude | Provider::ClaudeCode => "claude",
|
|
919
|
-
Provider::Codex => "codex",
|
|
920
|
-
Provider::Copilot => "copilot",
|
|
921
|
-
Provider::GeminiCli => "gemini",
|
|
922
|
-
Provider::Fake => "team-agent",
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
fn provider_wire(provider: Provider) -> &'static str {
|
|
927
|
-
match provider {
|
|
928
|
-
Provider::Claude => "claude",
|
|
929
|
-
Provider::ClaudeCode => "claude_code",
|
|
930
|
-
Provider::Codex => "codex",
|
|
931
|
-
Provider::Copilot => "copilot",
|
|
932
|
-
Provider::GeminiCli => "gemini_cli",
|
|
933
|
-
Provider::Fake => "fake",
|
|
934
|
-
}
|
|
935
|
-
}
|
|
931
|
+
use crate::provider::wire::{command_name, provider_wire};
|
|
936
932
|
|
|
937
933
|
fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
|
|
938
934
|
match auth_mode {
|
|
@@ -1045,6 +1041,18 @@ fn scan_session_candidates_once(
|
|
|
1045
1041
|
};
|
|
1046
1042
|
let positive_agent_id_match = candidate_text_has_team_agent_id(&text, context);
|
|
1047
1043
|
let agent_path_match = candidate_path_matches_agent_id(&path, context);
|
|
1044
|
+
// P0 (lane-046-capture-gap): Claude leader transcripts must NEVER be
|
|
1045
|
+
// returned as a worker capture candidate. The macmini repro showed a
|
|
1046
|
+
// 590MB leader transcript (sessionId=ea059b82, customTitle="claude
|
|
1047
|
+
// leader") being attributed to a fresh release-engineer worker via the
|
|
1048
|
+
// cwd+spawned_at time window. Filter by leader marker in the head
|
|
1049
|
+
// records — limited to Claude/ClaudeCode (codex/copilot transcripts
|
|
1050
|
+
// don't use customTitle/agentName the same way).
|
|
1051
|
+
if matches!(provider, Provider::Claude | Provider::ClaudeCode)
|
|
1052
|
+
&& claude_records_have_leader_marker(&records)
|
|
1053
|
+
{
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1048
1056
|
out.push(CapturedSessionCandidate {
|
|
1049
1057
|
captured: CapturedSession {
|
|
1050
1058
|
session_id: session_id.map(SessionId::new),
|
|
@@ -1181,13 +1189,36 @@ fn parse_spawned_at(raw: &str) -> Option<std::time::SystemTime> {
|
|
|
1181
1189
|
/// `collect_optional_candidate_files` 对不存在目录是 no-op)。
|
|
1182
1190
|
fn claude_projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<PathBuf> {
|
|
1183
1191
|
let canonical = std::fs::canonicalize(spawn_cwd).unwrap_or_else(|_| spawn_cwd.to_path_buf());
|
|
1184
|
-
let encoded = canonical.to_string_lossy()
|
|
1192
|
+
let encoded = encode_claude_projects_dir(&canonical.to_string_lossy());
|
|
1185
1193
|
if encoded.is_empty() {
|
|
1186
1194
|
return None;
|
|
1187
1195
|
}
|
|
1188
1196
|
Some(home.join(".claude").join("projects").join(encoded))
|
|
1189
1197
|
}
|
|
1190
1198
|
|
|
1199
|
+
/// 0.4.6 Stage 4: Claude CLI's project-dir encoding rule. Collapse every
|
|
1200
|
+
/// non-ASCII-alphanumeric character (slashes, dots, spaces, punctuation,
|
|
1201
|
+
/// non-ASCII codepoints like Chinese) into a single `-`. The pre-fix
|
|
1202
|
+
/// implementation only replaced `/` → `-` which silently produced wrong
|
|
1203
|
+
/// directory names for any cwd containing Chinese / spaces / dots
|
|
1204
|
+
/// (`/Users/alauda/.../agent前沿探索/多agent协作` produced raw UTF-8 while
|
|
1205
|
+
/// Claude actually creates `-Users-alauda-...-agent------agent--`).
|
|
1206
|
+
///
|
|
1207
|
+
/// Note: each non-alnum CHARACTER produces one `-`. A 2-char Chinese word
|
|
1208
|
+
/// like "协作" becomes 2 dashes. Adjacent non-alnums each contribute one
|
|
1209
|
+
/// dash (Claude does NOT collapse runs).
|
|
1210
|
+
fn encode_claude_projects_dir(path: &str) -> String {
|
|
1211
|
+
let mut out = String::with_capacity(path.len());
|
|
1212
|
+
for c in path.chars() {
|
|
1213
|
+
if c.is_ascii_alphanumeric() {
|
|
1214
|
+
out.push(c);
|
|
1215
|
+
} else {
|
|
1216
|
+
out.push('-');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
out
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1191
1222
|
/// §C4 cr verdict — copilot session 真相源 sqlite 点查。
|
|
1192
1223
|
///
|
|
1193
1224
|
/// 路径:`<HOME>/.copilot/session-store.db`,sessions 表(id/cwd/created_at/updated_at)
|
|
@@ -1509,364 +1540,29 @@ fn path_is_under_team_runtime(path: &Path) -> bool {
|
|
|
1509
1540
|
.any(|c| c.as_os_str() == std::ffi::OsStr::new(".team"))
|
|
1510
1541
|
}
|
|
1511
1542
|
|
|
1512
|
-
fn
|
|
1513
|
-
let exe = std::env::current_exe()
|
|
1514
|
-
.ok()
|
|
1515
|
-
.and_then(|p| p.into_os_string().into_string().ok())
|
|
1516
|
-
.unwrap_or_else(|| "team-agent".to_string());
|
|
1517
|
-
vec![
|
|
1518
|
-
exe,
|
|
1519
|
-
"fake-worker".to_string(),
|
|
1520
|
-
"--workspace".to_string(),
|
|
1521
|
-
"{workspace}".to_string(),
|
|
1522
|
-
"--agent-id".to_string(),
|
|
1523
|
-
"{agent_id}".to_string(),
|
|
1524
|
-
]
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
fn claude_launch_command(
|
|
1528
|
-
adapter: &BasicProviderAdapter,
|
|
1529
|
-
auth_mode: AuthMode,
|
|
1530
|
-
mcp_config: Option<&McpConfig>,
|
|
1531
|
-
system_prompt: Option<&str>,
|
|
1532
|
-
model: Option<&str>,
|
|
1533
|
-
tools: &[&str],
|
|
1534
|
-
) -> Result<Vec<String>, ProviderError> {
|
|
1535
|
-
let mut argv = claude_base_command(adapter, auth_mode, mcp_config, system_prompt, model, tools, false)?;
|
|
1536
|
-
argv.push("--session-id".to_string());
|
|
1537
|
-
argv.push(next_session_token());
|
|
1538
|
-
Ok(argv)
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
fn claude_base_command(
|
|
1542
|
-
adapter: &BasicProviderAdapter,
|
|
1543
|
-
auth_mode: AuthMode,
|
|
1544
|
-
mcp_config: Option<&McpConfig>,
|
|
1545
|
-
system_prompt: Option<&str>,
|
|
1546
|
-
model: Option<&str>,
|
|
1547
|
-
tools: &[&str],
|
|
1548
|
-
managed_mcp_config: bool,
|
|
1549
|
-
) -> Result<Vec<String>, ProviderError> {
|
|
1550
|
-
let mut argv = vec!["claude".to_string()];
|
|
1551
|
-
if claude_dangerous_auto_approve(tools) {
|
|
1552
|
-
argv.push("--dangerously-skip-permissions".to_string());
|
|
1553
|
-
} else {
|
|
1554
|
-
argv.push("--permission-mode".to_string());
|
|
1555
|
-
argv.push("default".to_string());
|
|
1556
|
-
}
|
|
1557
|
-
if let Some(model) = model {
|
|
1558
|
-
argv.push("--model".to_string());
|
|
1559
|
-
argv.push(model.to_string());
|
|
1560
|
-
}
|
|
1561
|
-
if let Some(prompt) = system_prompt {
|
|
1562
|
-
argv.push("--append-system-prompt".to_string());
|
|
1563
|
-
argv.push(prompt.to_string());
|
|
1564
|
-
}
|
|
1565
|
-
if !managed_mcp_config
|
|
1566
|
-
&& (mcp_config.is_some()
|
|
1567
|
-
|| auth_mode == AuthMode::CompatibleApi
|
|
1568
|
-
|| system_prompt.is_some_and(prompt_needs_native_mcp))
|
|
1569
|
-
{
|
|
1570
|
-
let raw = if let Some(config) = mcp_config {
|
|
1571
|
-
serde_json::json!({"mcpServers": config.raw.clone()})
|
|
1572
|
-
} else {
|
|
1573
|
-
serde_json::json!({"mcpServers": adapter.mcp_config(auth_mode)?.raw})
|
|
1574
|
-
};
|
|
1575
|
-
argv.push("--mcp-config".to_string());
|
|
1576
|
-
argv.push(raw.to_string());
|
|
1577
|
-
}
|
|
1578
|
-
for tool in claude_disallowed_tools(tools) {
|
|
1579
|
-
argv.push("--disallowedTools".to_string());
|
|
1580
|
-
argv.push(tool.to_string());
|
|
1581
|
-
}
|
|
1582
|
-
Ok(argv)
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
fn prompt_needs_native_mcp(prompt: &str) -> bool {
|
|
1543
|
+
pub(crate) fn prompt_needs_native_mcp(prompt: &str) -> bool {
|
|
1586
1544
|
prompt.contains('\n') || prompt.contains('"')
|
|
1587
1545
|
}
|
|
1588
1546
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
system_prompt: Option<&str>,
|
|
1594
|
-
model: Option<&str>,
|
|
1595
|
-
tools: &[&str],
|
|
1596
|
-
overrides: Option<&ProviderCommandOverrides>,
|
|
1597
|
-
) -> Vec<String> {
|
|
1598
|
-
let mut argv = vec![
|
|
1599
|
-
"codex".to_string(),
|
|
1600
|
-
];
|
|
1601
|
-
if let Some(subcommand) = subcommand {
|
|
1602
|
-
argv.push(subcommand.to_string());
|
|
1603
|
-
}
|
|
1604
|
-
argv.extend([
|
|
1605
|
-
"--no-alt-screen".to_string(),
|
|
1606
|
-
"--disable".to_string(),
|
|
1607
|
-
"shell_snapshot".to_string(),
|
|
1608
|
-
"--disable".to_string(),
|
|
1609
|
-
"apps".to_string(),
|
|
1610
|
-
]);
|
|
1611
|
-
// codex.py:105-107 — profile CODEX_PROFILE before the sandbox/approval flags.
|
|
1612
|
-
if let Some(profile) = overrides.and_then(|o| o.codex_profile.as_deref()) {
|
|
1613
|
-
argv.push("--profile".to_string());
|
|
1614
|
-
argv.push(profile.to_string());
|
|
1615
|
-
}
|
|
1616
|
-
if codex_dangerous_auto_approve(tools) {
|
|
1617
|
-
argv.push("--dangerously-bypass-approvals-and-sandbox".to_string());
|
|
1618
|
-
} else {
|
|
1619
|
-
argv.push("--sandbox".to_string());
|
|
1620
|
-
argv.push(codex_sandbox_mode(tools).to_string());
|
|
1621
|
-
argv.push("--ask-for-approval".to_string());
|
|
1622
|
-
argv.push("on-request".to_string());
|
|
1623
|
-
}
|
|
1624
|
-
if let Some(model) = model {
|
|
1625
|
-
argv.push("--model".to_string());
|
|
1626
|
-
argv.push(model.to_string());
|
|
1627
|
-
}
|
|
1628
|
-
// codex.py:117-118 — profile codex_config `-c` items before developer_instructions.
|
|
1629
|
-
if let Some(overrides) = overrides {
|
|
1630
|
-
for config in &overrides.codex_config {
|
|
1631
|
-
argv.push("-c".to_string());
|
|
1632
|
-
argv.push(config.clone());
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
if let Some(prompt) = system_prompt {
|
|
1636
|
-
// codex.py:120 — escape order matters: backslash first, then quote, then newline.
|
|
1637
|
-
let escaped = prompt
|
|
1638
|
-
.replace('\\', "\\\\")
|
|
1639
|
-
.replace('"', "\\\"")
|
|
1640
|
-
.replace('\n', "\\n");
|
|
1641
|
-
argv.push("-c".to_string());
|
|
1642
|
-
argv.push(format!("developer_instructions=\"{escaped}\""));
|
|
1643
|
-
}
|
|
1644
|
-
// Contract C / MUST-8: Codex CLI (2026-06) does NOT take Claude's `--mcp-config <json>` flag;
|
|
1645
|
-
// instead it uses `-c mcp_servers.<name>.<field>=...` overrides, the same pattern used by
|
|
1646
|
-
// the live Team Agent workers in this very session (attach-leader codex panes spawn with
|
|
1647
|
-
// `-c mcp_servers.team_orchestrator.command="..."`, ` ...args=[...]`, `...env.TEAM_AGENT_ID=...`).
|
|
1648
|
-
// Inject the resolved MCP config that way so the Codex worker has a real callable
|
|
1649
|
-
// `team_orchestrator` server (not prompt-only metadata).
|
|
1650
|
-
if let Some(config) = mcp_config {
|
|
1651
|
-
append_codex_mcp_overrides(&mut argv, &config.raw);
|
|
1652
|
-
}
|
|
1653
|
-
argv
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
/// Render an `McpConfig::raw` ({ name: { type, command, args, env: {...} } }) into Codex
|
|
1657
|
-
/// `-c mcp_servers.<name>.<field>=...` overrides. JSON values are stringified with serde
|
|
1658
|
-
/// so arrays/objects survive (Codex parses the right-hand side as JSON; this is what the
|
|
1659
|
-
/// Python golden + the live attached Codex panes do).
|
|
1660
|
-
fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_json::Value) {
|
|
1661
|
-
let Some(servers) = raw.as_object() else {
|
|
1662
|
-
return;
|
|
1663
|
-
};
|
|
1664
|
-
for (name, server) in servers {
|
|
1665
|
-
let Some(obj) = server.as_object() else {
|
|
1666
|
-
continue;
|
|
1667
|
-
};
|
|
1668
|
-
for (key, value) in obj {
|
|
1669
|
-
if key == "env" {
|
|
1670
|
-
if let Some(env) = value.as_object() {
|
|
1671
|
-
for (env_key, env_value) in env {
|
|
1672
|
-
argv.push("-c".to_string());
|
|
1673
|
-
argv.push(format!(
|
|
1674
|
-
"mcp_servers.{name}.env.{env_key}={}",
|
|
1675
|
-
json_inline(env_value)
|
|
1676
|
-
));
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
continue;
|
|
1680
|
-
}
|
|
1681
|
-
argv.push("-c".to_string());
|
|
1682
|
-
argv.push(format!("mcp_servers.{name}.{key}={}", json_inline(value)));
|
|
1683
|
-
}
|
|
1684
|
-
// codex.py:129 — every MCP server gets a 600s tool timeout so long-running
|
|
1685
|
-
// team_orchestrator calls (report_result etc.) survive the codex default.
|
|
1686
|
-
argv.push("-c".to_string());
|
|
1687
|
-
argv.push(format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
fn json_inline(value: &serde_json::Value) -> String {
|
|
1547
|
+
/// Shared JSON renderer used by the codex `-c mcp_servers.*=...` overrides
|
|
1548
|
+
/// and any other adapter that wants inline JSON. String values are quoted
|
|
1549
|
+
/// (with quote escape); other values use serde's default Display.
|
|
1550
|
+
pub(crate) fn json_inline(value: &serde_json::Value) -> String {
|
|
1692
1551
|
match value {
|
|
1693
1552
|
serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
|
|
1694
1553
|
other => other.to_string(),
|
|
1695
1554
|
}
|
|
1696
1555
|
}
|
|
1697
1556
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
// copilot --no-color --no-auto-update --no-remote # C-1-2 噪音 + 禁远控
|
|
1708
|
-
// --disable-builtin-mcps # C-3-1 P0 禁内建 github-mcp-server
|
|
1709
|
-
// --additional-mcp-config @<file> # C-3-4 用 @file 形,避 wrapper 语义
|
|
1710
|
-
// --allow-tool 'team_orchestrator' # C-3-5 mcp_team 免审批 (server 级)
|
|
1711
|
-
// --session-id <uuid> -n <agent_id> # C-7-1 plan/launch 加
|
|
1712
|
-
// -C <workspace> # 双保险,launch 加
|
|
1713
|
-
// [--allow-all | <granular deny>] # C-5-1/C-5-2
|
|
1714
|
-
// [--model <m>]
|
|
1715
|
-
// [--log-dir <dir> --log-level info] # C-6-2 launch 加
|
|
1716
|
-
// env: COPILOT_CUSTOM_INSTRUCTIONS_DIRS=<ws>/.../<agent_id>/ # C-2-1 launch 注入
|
|
1717
|
-
// COPILOT_DISABLE_TERMINAL_TITLE=1 # C-4 P0 N39 红线,launch 注入
|
|
1718
|
-
// banner 不入 argv(v1 错的 --banner=never 删除;banner 走 config 文件,非 CLI flag)。
|
|
1719
|
-
// `-i`/`-p`/`--share*`/`--no-ask-user` **绝不**入 argv(RC-1/RC-14/RC-16)。
|
|
1720
|
-
//
|
|
1721
|
-
// system_prompt(B2 灵魂件)**不进 argv**:走 spawn env COPILOT_CUSTOM_INSTRUCTIONS_DIRS
|
|
1722
|
-
// + per-worker AGENTS.md(B2 单源,不另拼);本函数 system_prompt 参数静默忽略。
|
|
1723
|
-
fn copilot_base_command(
|
|
1724
|
-
auth_mode: AuthMode,
|
|
1725
|
-
mcp_config: Option<&McpConfig>,
|
|
1726
|
-
system_prompt: Option<&str>,
|
|
1727
|
-
model: Option<&str>,
|
|
1728
|
-
tools: &[&str],
|
|
1729
|
-
) -> Vec<String> {
|
|
1730
|
-
let _ = (auth_mode, system_prompt);
|
|
1731
|
-
let mut argv = vec![
|
|
1732
|
-
"copilot".to_string(),
|
|
1733
|
-
// C-1-2 v2:噪音控制三件 + 禁远控(防 GitHub web 远控 worker)
|
|
1734
|
-
"--no-color".to_string(),
|
|
1735
|
-
"--no-auto-update".to_string(),
|
|
1736
|
-
"--no-remote".to_string(),
|
|
1737
|
-
// C-3-1 v2 (P0):禁内建 github-mcp-server(main-help:70-71);残留风险
|
|
1738
|
-
// 通过 spawn 前 `copilot mcp list` 扫描 + 按名 `--disable-mcp-server <n>` 补
|
|
1739
|
-
// (那一段在 launch 路径加,因为需要 spawn-time 探测)。
|
|
1740
|
-
"--disable-builtin-mcps".to_string(),
|
|
1741
|
-
];
|
|
1742
|
-
if copilot_dangerous_auto_approve(tools) {
|
|
1743
|
-
// C-5-1 v2 实证:--allow-all == --yolo == 三件套(tools+paths+urls 等价),
|
|
1744
|
-
// help-permissions Enabling All Permissions 节原文。**禁** --allow-all-tools
|
|
1745
|
-
// (仅 tools 一档,语义不全,RC-13)。
|
|
1746
|
-
argv.push("--allow-all".to_string());
|
|
1747
|
-
} else {
|
|
1748
|
-
// C-5-2 v2:角色缺某 canonical 能力 → 精细 deny(deny 恒优先,即便
|
|
1749
|
-
// --allow-all-tools,help-permissions 原文);**禁** --allow-all/--yolo(RC-14)。
|
|
1750
|
-
for flag in copilot_permission_flags(tools) {
|
|
1751
|
-
argv.push(flag);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
// C-3-5 v2:mcp_team ∈ canonical(team_orchestrator 是我们的 server)→ 免审批
|
|
1755
|
-
// (模式 `<mcp-server-name>(tool-name?)` 省略 tool = 该 server 全工具集)。
|
|
1756
|
-
argv.push("--allow-tool".to_string());
|
|
1757
|
-
argv.push("team_orchestrator".to_string());
|
|
1758
|
-
if let Some(model) = model {
|
|
1759
|
-
argv.push("--model".to_string());
|
|
1760
|
-
argv.push(model.to_string());
|
|
1761
|
-
}
|
|
1762
|
-
if let Some(config) = mcp_config {
|
|
1763
|
-
// §C1 v2 + C-3-4 cr verdict v2(te 真机实证 cmd-mcp-add schema):
|
|
1764
|
-
// copilot 的 mcp 配置 schema 字段名是 `transport`(取值 stdio|http|sse),
|
|
1765
|
-
// 不是 codex/claude 的 `type`。McpConfig.raw 是 canonical(type),写
|
|
1766
|
-
// --additional-mcp-config 时必须翻译 type→transport(仅 copilot 走此分支)。
|
|
1767
|
-
argv.push("--additional-mcp-config".to_string());
|
|
1768
|
-
argv.push(copilot_translate_mcp_config(&config.raw).to_string());
|
|
1769
|
-
}
|
|
1770
|
-
argv
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
/// C-3-4 cr verdict v2 — 把 McpConfig.raw 的 canonical schema(`type`)翻译成
|
|
1774
|
-
/// copilot mcp add/--additional-mcp-config 期望的 `transport` 字段(stdio|http|sse)。
|
|
1775
|
-
/// 仅 Copilot 适配走此翻译,claude/codex 路径不动。
|
|
1776
|
-
fn copilot_translate_mcp_config(raw: &serde_json::Value) -> serde_json::Value {
|
|
1777
|
-
let Some(servers) = raw.as_object() else {
|
|
1778
|
-
return raw.clone();
|
|
1779
|
-
};
|
|
1780
|
-
let mut translated = serde_json::Map::new();
|
|
1781
|
-
for (name, server) in servers {
|
|
1782
|
-
let Some(obj) = server.as_object() else {
|
|
1783
|
-
translated.insert(name.clone(), server.clone());
|
|
1784
|
-
continue;
|
|
1785
|
-
};
|
|
1786
|
-
let mut out = serde_json::Map::new();
|
|
1787
|
-
for (key, value) in obj {
|
|
1788
|
-
if key == "type" {
|
|
1789
|
-
out.insert("transport".to_string(), value.clone());
|
|
1790
|
-
} else {
|
|
1791
|
-
out.insert(key.clone(), value.clone());
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
translated.insert(name.clone(), serde_json::Value::Object(out));
|
|
1795
|
-
}
|
|
1796
|
-
serde_json::Value::Object(translated)
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
/// resume 路径同 base + `--resume <sid>`(去 --session-id);单列出
|
|
1800
|
-
/// 避免 plan 端误 push --session-id 与 --resume 同帧。
|
|
1801
|
-
fn copilot_base_command_resume(
|
|
1802
|
-
auth_mode: AuthMode,
|
|
1803
|
-
mcp_config: Option<&McpConfig>,
|
|
1804
|
-
system_prompt: Option<&str>,
|
|
1805
|
-
model: Option<&str>,
|
|
1806
|
-
tools: &[&str],
|
|
1807
|
-
) -> Vec<String> {
|
|
1808
|
-
copilot_base_command(auth_mode, mcp_config, system_prompt, model, tools)
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
fn copilot_dangerous_auto_approve(tools: &[&str]) -> bool {
|
|
1812
|
-
tools.contains(&"dangerous_auto_approve")
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
/// C-5-2 v2 verdict — copilot 细粒度 deny 映射(canonical tool → copilot flag,
|
|
1816
|
-
/// 全部走 `--deny-tool <kind>`,help-permissions Tool Permissions 节四 kind:
|
|
1817
|
-
/// shell/write/mcp/url):
|
|
1818
|
-
/// execute_bash ∉ allowed → `--deny-tool 'shell'`
|
|
1819
|
-
/// fs_write ∉ allowed → `--deny-tool 'write'`
|
|
1820
|
-
/// network ∉ allowed → `--deny-tool 'url'`(help-permissions: "url(domain-or-url?)
|
|
1821
|
-
/// … If omitted, matches all URLs")
|
|
1822
|
-
/// fs_read/fs_list 在 copilot 上无对应 deny kind(C-5-3 prompt_only 诚实)。
|
|
1823
|
-
fn copilot_permission_flags(tools: &[&str]) -> Vec<String> {
|
|
1824
|
-
let mut flags = Vec::new();
|
|
1825
|
-
if !tools.contains(&"execute_bash") {
|
|
1826
|
-
flags.push("--deny-tool".to_string());
|
|
1827
|
-
flags.push("shell".to_string());
|
|
1828
|
-
}
|
|
1829
|
-
if !tools.contains(&"fs_write") {
|
|
1830
|
-
flags.push("--deny-tool".to_string());
|
|
1831
|
-
flags.push("write".to_string());
|
|
1832
|
-
}
|
|
1833
|
-
if !tools.contains(&"network") {
|
|
1834
|
-
// v2 修正:`--deny-tool 'url'`(省略 domain 匹配全 URL),不是 `--deny-url '*'`
|
|
1835
|
-
// (RC-19 反向 case 守 — 全 URL 拒绝走 deny-tool kind,不走 deny-url path)。
|
|
1836
|
-
flags.push("--deny-tool".to_string());
|
|
1837
|
-
flags.push("url".to_string());
|
|
1838
|
-
}
|
|
1839
|
-
flags
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
|
|
1843
|
-
tools.contains(&"dangerous_auto_approve")
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
fn claude_disallowed_tools(tools: &[&str]) -> Vec<&'static str> {
|
|
1847
|
-
let mut disallowed = Vec::new();
|
|
1848
|
-
if !tools.contains(&"execute_bash") {
|
|
1849
|
-
disallowed.push("Bash");
|
|
1850
|
-
}
|
|
1851
|
-
if !tools.contains(&"fs_read") {
|
|
1852
|
-
disallowed.push("Read");
|
|
1853
|
-
}
|
|
1854
|
-
if !tools.contains(&"fs_write") {
|
|
1855
|
-
disallowed.extend(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1856
|
-
}
|
|
1857
|
-
if !tools.contains(&"fs_list") {
|
|
1858
|
-
disallowed.extend(["Glob", "Grep"]);
|
|
1859
|
-
}
|
|
1860
|
-
disallowed
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
fn codex_sandbox_mode(tools: &[&str]) -> &'static str {
|
|
1864
|
-
if tools.iter().any(|tool| matches!(*tool, "fs_write" | "execute_bash")) {
|
|
1865
|
-
"workspace-write"
|
|
1866
|
-
} else {
|
|
1867
|
-
"read-only"
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1557
|
+
// 0.4.x decoupling step 2: provider-local command builders moved to provider/adapters/.
|
|
1558
|
+
// Only the entry points the trait impl actually calls are re-imported here;
|
|
1559
|
+
// the per-provider helper fns (dangerous_auto_approve, permission flags,
|
|
1560
|
+
// disallowed_tools, sandbox_mode, mcp_overrides) are called from within the
|
|
1561
|
+
// extracted base_command fns, not directly by this file.
|
|
1562
|
+
use super::adapters::claude::{claude_base_command, claude_launch_command};
|
|
1563
|
+
use super::adapters::codex::codex_base_command;
|
|
1564
|
+
use super::adapters::copilot::{copilot_base_command, copilot_base_command_resume};
|
|
1565
|
+
use super::adapters::fake::fake_worker_command;
|
|
1870
1566
|
|
|
1871
1567
|
/// Contract C / MUST-8: the per-worker Team Agent MCP server config used by Claude
|
|
1872
1568
|
/// (`--mcp-config`) and Codex (`-c mcp_servers.*` injection). Placeholders
|
|
@@ -1909,11 +1605,58 @@ fn current_team_agent_command() -> String {
|
|
|
1909
1605
|
"/usr/local/bin/team-agent".to_string()
|
|
1910
1606
|
}
|
|
1911
1607
|
|
|
1608
|
+
/// P0 (lane-046-capture-gap): detect Claude leader transcripts by their head
|
|
1609
|
+
/// marker records. A leader transcript head contains records of the form
|
|
1610
|
+
/// `{"type":"custom-title","customTitle":"claude leader",...}` or
|
|
1611
|
+
/// `{"type":"agent-name","agentName":"claude leader",...}` written when the
|
|
1612
|
+
/// leader pane starts. Workers must never be attributed to such transcripts.
|
|
1613
|
+
/// E57 (lane-046-capture-gap postflight): expose the Claude leader-marker
|
|
1614
|
+
/// scanner for the event-log repair path. `recover_resume_session_from_events`
|
|
1615
|
+
/// must apply the SAME marker filter the capture allocator applies, otherwise
|
|
1616
|
+
/// a stale `session.captured` event from a pre-fix run still pulls the leader
|
|
1617
|
+
/// transcript onto a worker on the next restart.
|
|
1618
|
+
///
|
|
1619
|
+
/// Returns `true` ONLY for Claude/ClaudeCode providers when the rollout file's
|
|
1620
|
+
/// head records carry `customTitle == "claude leader"` or `agentName ==
|
|
1621
|
+
/// "claude leader"` (case-insensitive). Other providers always return `false`
|
|
1622
|
+
/// — codex/copilot transcripts don't use those fields the same way.
|
|
1623
|
+
pub(crate) fn rollout_path_has_claude_leader_marker(
|
|
1624
|
+
provider: crate::provider::Provider,
|
|
1625
|
+
rollout_path: &Path,
|
|
1626
|
+
) -> bool {
|
|
1627
|
+
if !matches!(
|
|
1628
|
+
provider,
|
|
1629
|
+
crate::provider::Provider::Claude | crate::provider::Provider::ClaudeCode
|
|
1630
|
+
) {
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
let Ok(text) = read_head_text(rollout_path, CAPTURE_HEAD_BYTES) else {
|
|
1634
|
+
return false;
|
|
1635
|
+
};
|
|
1636
|
+
let records = parse_session_records(&text);
|
|
1637
|
+
claude_records_have_leader_marker(&records)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
fn claude_records_have_leader_marker(records: &[serde_json::Value]) -> bool {
|
|
1641
|
+
records.iter().any(|record| {
|
|
1642
|
+
let custom_title = record
|
|
1643
|
+
.get("customTitle")
|
|
1644
|
+
.and_then(serde_json::Value::as_str)
|
|
1645
|
+
.map(str::to_ascii_lowercase);
|
|
1646
|
+
let agent_name = record
|
|
1647
|
+
.get("agentName")
|
|
1648
|
+
.and_then(serde_json::Value::as_str)
|
|
1649
|
+
.map(str::to_ascii_lowercase);
|
|
1650
|
+
matches!(custom_title.as_deref(), Some("claude leader"))
|
|
1651
|
+
|| matches!(agent_name.as_deref(), Some("claude leader"))
|
|
1652
|
+
})
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1912
1655
|
fn has_cwd_field(record: &serde_json::Value) -> bool {
|
|
1913
1656
|
record_cwd(record).is_some()
|
|
1914
1657
|
}
|
|
1915
1658
|
|
|
1916
|
-
fn next_session_token() -> String {
|
|
1659
|
+
pub(crate) fn next_session_token() -> String {
|
|
1917
1660
|
use sha2::Digest;
|
|
1918
1661
|
|
|
1919
1662
|
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
@@ -1952,6 +1695,70 @@ fn next_session_token() -> String {
|
|
|
1952
1695
|
)
|
|
1953
1696
|
}
|
|
1954
1697
|
|
|
1698
|
+
#[cfg(test)]
|
|
1699
|
+
mod lane_046_leader_marker_tests {
|
|
1700
|
+
use super::*;
|
|
1701
|
+
|
|
1702
|
+
#[test]
|
|
1703
|
+
fn claude_leader_marker_in_custom_title_is_detected() {
|
|
1704
|
+
let records = vec![serde_json::json!({
|
|
1705
|
+
"type": "custom-title",
|
|
1706
|
+
"customTitle": "claude leader",
|
|
1707
|
+
"sessionId": "ea059b82",
|
|
1708
|
+
})];
|
|
1709
|
+
assert!(
|
|
1710
|
+
claude_records_have_leader_marker(&records),
|
|
1711
|
+
"customTitle=='claude leader' must be detected as leader marker"
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
#[test]
|
|
1716
|
+
fn claude_leader_marker_in_agent_name_is_detected() {
|
|
1717
|
+
let records = vec![serde_json::json!({
|
|
1718
|
+
"type": "agent-name",
|
|
1719
|
+
"agentName": "claude leader",
|
|
1720
|
+
"sessionId": "ea059b82",
|
|
1721
|
+
})];
|
|
1722
|
+
assert!(
|
|
1723
|
+
claude_records_have_leader_marker(&records),
|
|
1724
|
+
"agentName=='claude leader' must be detected as leader marker"
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
#[test]
|
|
1729
|
+
fn claude_worker_records_have_no_leader_marker() {
|
|
1730
|
+
let records = vec![
|
|
1731
|
+
serde_json::json!({
|
|
1732
|
+
"type": "custom-title",
|
|
1733
|
+
"customTitle": "claude release-engineer",
|
|
1734
|
+
"sessionId": "abc12345",
|
|
1735
|
+
}),
|
|
1736
|
+
serde_json::json!({
|
|
1737
|
+
"type": "user",
|
|
1738
|
+
"content": "Team Agent message from leader: do X",
|
|
1739
|
+
"sessionId": "abc12345",
|
|
1740
|
+
}),
|
|
1741
|
+
];
|
|
1742
|
+
assert!(
|
|
1743
|
+
!claude_records_have_leader_marker(&records),
|
|
1744
|
+
"worker transcripts must NOT be flagged as leader marker (the \
|
|
1745
|
+
literal 'claude leader' in user content does not count — only \
|
|
1746
|
+
customTitle/agentName fields)"
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
#[test]
|
|
1751
|
+
fn claude_leader_marker_is_case_insensitive() {
|
|
1752
|
+
let records = vec![serde_json::json!({
|
|
1753
|
+
"customTitle": "Claude Leader",
|
|
1754
|
+
})];
|
|
1755
|
+
assert!(
|
|
1756
|
+
claude_records_have_leader_marker(&records),
|
|
1757
|
+
"leader marker detection must be case-insensitive"
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1955
1762
|
#[cfg(test)]
|
|
1956
1763
|
mod e6_session_attribution_tests {
|
|
1957
1764
|
#![allow(clippy::unwrap_used)]
|
|
@@ -1988,11 +1795,44 @@ mod e6_session_attribution_tests {
|
|
|
1988
1795
|
let cwd = tmp_root("encode");
|
|
1989
1796
|
let got = claude_projects_dir_for_cwd(home, &cwd).unwrap();
|
|
1990
1797
|
let canon = std::fs::canonicalize(&cwd).unwrap();
|
|
1991
|
-
let expected_leaf = canon.to_string_lossy()
|
|
1798
|
+
let expected_leaf = encode_claude_projects_dir(&canon.to_string_lossy());
|
|
1992
1799
|
assert_eq!(got, home.join(".claude").join("projects").join(expected_leaf));
|
|
1993
1800
|
let _ = std::fs::remove_dir_all(&cwd);
|
|
1994
1801
|
}
|
|
1995
1802
|
|
|
1803
|
+
/// **0.4.6 Stage 4 RED**: encode every non-[A-Za-z0-9] character to `-`,
|
|
1804
|
+
/// matching Claude CLI's actual project-dir naming rule. Pre-fix code
|
|
1805
|
+
/// only replaced `/` → `-` and silently produced wrong directories for
|
|
1806
|
+
/// non-ASCII / dotted / spaced cwds — invisible to the scanner.
|
|
1807
|
+
#[test]
|
|
1808
|
+
fn encode_claude_projects_dir_parity_with_real_claude_naming() {
|
|
1809
|
+
// Pure-ASCII parity (the pre-fix happy path).
|
|
1810
|
+
assert_eq!(
|
|
1811
|
+
encode_claude_projects_dir("/Users/alauda/code"),
|
|
1812
|
+
"-Users-alauda-code"
|
|
1813
|
+
);
|
|
1814
|
+
// The user's actual project root (Chinese):
|
|
1815
|
+
// /Users/alauda/Documents/code/agent前沿探索/多agent协作
|
|
1816
|
+
// Each Chinese character → one `-`. "前沿探索"=4 chars → 4 dashes,
|
|
1817
|
+
// "多agent协作" = "多"+"agent"+"协作" = 1 + (alphanumeric kept) + 2.
|
|
1818
|
+
assert_eq!(
|
|
1819
|
+
encode_claude_projects_dir(
|
|
1820
|
+
"/Users/alauda/Documents/code/agent前沿探索/多agent协作"
|
|
1821
|
+
),
|
|
1822
|
+
"-Users-alauda-Documents-code-agent------agent--"
|
|
1823
|
+
);
|
|
1824
|
+
// Dots and spaces also collapse.
|
|
1825
|
+
assert_eq!(
|
|
1826
|
+
encode_claude_projects_dir("/Users/foo bar.baz/v1.2"),
|
|
1827
|
+
"-Users-foo-bar-baz-v1-2"
|
|
1828
|
+
);
|
|
1829
|
+
// Hidden directory ".team" → "-team".
|
|
1830
|
+
assert_eq!(
|
|
1831
|
+
encode_claude_projects_dir("/proj/.team/runtime"),
|
|
1832
|
+
"-proj--team-runtime"
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1996
1836
|
#[test]
|
|
1997
1837
|
fn parse_spawned_at_rfc3339_roundtrips_and_rejects_junk() {
|
|
1998
1838
|
assert!(parse_spawned_at("2026-06-10T21:40:00+00:00").is_some());
|