@team-agent/installer 0.4.4 → 0.4.6

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.
@@ -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. This is a secondary diagnostic
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());
@@ -1045,6 +1060,18 @@ fn scan_session_candidates_once(
1045
1060
  };
1046
1061
  let positive_agent_id_match = candidate_text_has_team_agent_id(&text, context);
1047
1062
  let agent_path_match = candidate_path_matches_agent_id(&path, context);
1063
+ // P0 (lane-046-capture-gap): Claude leader transcripts must NEVER be
1064
+ // returned as a worker capture candidate. The macmini repro showed a
1065
+ // 590MB leader transcript (sessionId=ea059b82, customTitle="claude
1066
+ // leader") being attributed to a fresh release-engineer worker via the
1067
+ // cwd+spawned_at time window. Filter by leader marker in the head
1068
+ // records — limited to Claude/ClaudeCode (codex/copilot transcripts
1069
+ // don't use customTitle/agentName the same way).
1070
+ if matches!(provider, Provider::Claude | Provider::ClaudeCode)
1071
+ && claude_records_have_leader_marker(&records)
1072
+ {
1073
+ continue;
1074
+ }
1048
1075
  out.push(CapturedSessionCandidate {
1049
1076
  captured: CapturedSession {
1050
1077
  session_id: session_id.map(SessionId::new),
@@ -1181,13 +1208,36 @@ fn parse_spawned_at(raw: &str) -> Option<std::time::SystemTime> {
1181
1208
  /// `collect_optional_candidate_files` 对不存在目录是 no-op)。
1182
1209
  fn claude_projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<PathBuf> {
1183
1210
  let canonical = std::fs::canonicalize(spawn_cwd).unwrap_or_else(|_| spawn_cwd.to_path_buf());
1184
- let encoded = canonical.to_string_lossy().replace('/', "-");
1211
+ let encoded = encode_claude_projects_dir(&canonical.to_string_lossy());
1185
1212
  if encoded.is_empty() {
1186
1213
  return None;
1187
1214
  }
1188
1215
  Some(home.join(".claude").join("projects").join(encoded))
1189
1216
  }
1190
1217
 
1218
+ /// 0.4.6 Stage 4: Claude CLI's project-dir encoding rule. Collapse every
1219
+ /// non-ASCII-alphanumeric character (slashes, dots, spaces, punctuation,
1220
+ /// non-ASCII codepoints like Chinese) into a single `-`. The pre-fix
1221
+ /// implementation only replaced `/` → `-` which silently produced wrong
1222
+ /// directory names for any cwd containing Chinese / spaces / dots
1223
+ /// (`/Users/alauda/.../agent前沿探索/多agent协作` produced raw UTF-8 while
1224
+ /// Claude actually creates `-Users-alauda-...-agent------agent--`).
1225
+ ///
1226
+ /// Note: each non-alnum CHARACTER produces one `-`. A 2-char Chinese word
1227
+ /// like "协作" becomes 2 dashes. Adjacent non-alnums each contribute one
1228
+ /// dash (Claude does NOT collapse runs).
1229
+ fn encode_claude_projects_dir(path: &str) -> String {
1230
+ let mut out = String::with_capacity(path.len());
1231
+ for c in path.chars() {
1232
+ if c.is_ascii_alphanumeric() {
1233
+ out.push(c);
1234
+ } else {
1235
+ out.push('-');
1236
+ }
1237
+ }
1238
+ out
1239
+ }
1240
+
1191
1241
  /// §C4 cr verdict — copilot session 真相源 sqlite 点查。
1192
1242
  ///
1193
1243
  /// 路径:`<HOME>/.copilot/session-store.db`,sessions 表(id/cwd/created_at/updated_at)
@@ -1909,6 +1959,53 @@ fn current_team_agent_command() -> String {
1909
1959
  "/usr/local/bin/team-agent".to_string()
1910
1960
  }
1911
1961
 
1962
+ /// P0 (lane-046-capture-gap): detect Claude leader transcripts by their head
1963
+ /// marker records. A leader transcript head contains records of the form
1964
+ /// `{"type":"custom-title","customTitle":"claude leader",...}` or
1965
+ /// `{"type":"agent-name","agentName":"claude leader",...}` written when the
1966
+ /// leader pane starts. Workers must never be attributed to such transcripts.
1967
+ /// E57 (lane-046-capture-gap postflight): expose the Claude leader-marker
1968
+ /// scanner for the event-log repair path. `recover_resume_session_from_events`
1969
+ /// must apply the SAME marker filter the capture allocator applies, otherwise
1970
+ /// a stale `session.captured` event from a pre-fix run still pulls the leader
1971
+ /// transcript onto a worker on the next restart.
1972
+ ///
1973
+ /// Returns `true` ONLY for Claude/ClaudeCode providers when the rollout file's
1974
+ /// head records carry `customTitle == "claude leader"` or `agentName ==
1975
+ /// "claude leader"` (case-insensitive). Other providers always return `false`
1976
+ /// — codex/copilot transcripts don't use those fields the same way.
1977
+ pub(crate) fn rollout_path_has_claude_leader_marker(
1978
+ provider: crate::provider::Provider,
1979
+ rollout_path: &Path,
1980
+ ) -> bool {
1981
+ if !matches!(
1982
+ provider,
1983
+ crate::provider::Provider::Claude | crate::provider::Provider::ClaudeCode
1984
+ ) {
1985
+ return false;
1986
+ }
1987
+ let Ok(text) = read_head_text(rollout_path, CAPTURE_HEAD_BYTES) else {
1988
+ return false;
1989
+ };
1990
+ let records = parse_session_records(&text);
1991
+ claude_records_have_leader_marker(&records)
1992
+ }
1993
+
1994
+ fn claude_records_have_leader_marker(records: &[serde_json::Value]) -> bool {
1995
+ records.iter().any(|record| {
1996
+ let custom_title = record
1997
+ .get("customTitle")
1998
+ .and_then(serde_json::Value::as_str)
1999
+ .map(str::to_ascii_lowercase);
2000
+ let agent_name = record
2001
+ .get("agentName")
2002
+ .and_then(serde_json::Value::as_str)
2003
+ .map(str::to_ascii_lowercase);
2004
+ matches!(custom_title.as_deref(), Some("claude leader"))
2005
+ || matches!(agent_name.as_deref(), Some("claude leader"))
2006
+ })
2007
+ }
2008
+
1912
2009
  fn has_cwd_field(record: &serde_json::Value) -> bool {
1913
2010
  record_cwd(record).is_some()
1914
2011
  }
@@ -1952,6 +2049,70 @@ fn next_session_token() -> String {
1952
2049
  )
1953
2050
  }
1954
2051
 
2052
+ #[cfg(test)]
2053
+ mod lane_046_leader_marker_tests {
2054
+ use super::*;
2055
+
2056
+ #[test]
2057
+ fn claude_leader_marker_in_custom_title_is_detected() {
2058
+ let records = vec![serde_json::json!({
2059
+ "type": "custom-title",
2060
+ "customTitle": "claude leader",
2061
+ "sessionId": "ea059b82",
2062
+ })];
2063
+ assert!(
2064
+ claude_records_have_leader_marker(&records),
2065
+ "customTitle=='claude leader' must be detected as leader marker"
2066
+ );
2067
+ }
2068
+
2069
+ #[test]
2070
+ fn claude_leader_marker_in_agent_name_is_detected() {
2071
+ let records = vec![serde_json::json!({
2072
+ "type": "agent-name",
2073
+ "agentName": "claude leader",
2074
+ "sessionId": "ea059b82",
2075
+ })];
2076
+ assert!(
2077
+ claude_records_have_leader_marker(&records),
2078
+ "agentName=='claude leader' must be detected as leader marker"
2079
+ );
2080
+ }
2081
+
2082
+ #[test]
2083
+ fn claude_worker_records_have_no_leader_marker() {
2084
+ let records = vec![
2085
+ serde_json::json!({
2086
+ "type": "custom-title",
2087
+ "customTitle": "claude release-engineer",
2088
+ "sessionId": "abc12345",
2089
+ }),
2090
+ serde_json::json!({
2091
+ "type": "user",
2092
+ "content": "Team Agent message from leader: do X",
2093
+ "sessionId": "abc12345",
2094
+ }),
2095
+ ];
2096
+ assert!(
2097
+ !claude_records_have_leader_marker(&records),
2098
+ "worker transcripts must NOT be flagged as leader marker (the \
2099
+ literal 'claude leader' in user content does not count — only \
2100
+ customTitle/agentName fields)"
2101
+ );
2102
+ }
2103
+
2104
+ #[test]
2105
+ fn claude_leader_marker_is_case_insensitive() {
2106
+ let records = vec![serde_json::json!({
2107
+ "customTitle": "Claude Leader",
2108
+ })];
2109
+ assert!(
2110
+ claude_records_have_leader_marker(&records),
2111
+ "leader marker detection must be case-insensitive"
2112
+ );
2113
+ }
2114
+ }
2115
+
1955
2116
  #[cfg(test)]
1956
2117
  mod e6_session_attribution_tests {
1957
2118
  #![allow(clippy::unwrap_used)]
@@ -1988,11 +2149,44 @@ mod e6_session_attribution_tests {
1988
2149
  let cwd = tmp_root("encode");
1989
2150
  let got = claude_projects_dir_for_cwd(home, &cwd).unwrap();
1990
2151
  let canon = std::fs::canonicalize(&cwd).unwrap();
1991
- let expected_leaf = canon.to_string_lossy().replace('/', "-");
2152
+ let expected_leaf = encode_claude_projects_dir(&canon.to_string_lossy());
1992
2153
  assert_eq!(got, home.join(".claude").join("projects").join(expected_leaf));
1993
2154
  let _ = std::fs::remove_dir_all(&cwd);
1994
2155
  }
1995
2156
 
2157
+ /// **0.4.6 Stage 4 RED**: encode every non-[A-Za-z0-9] character to `-`,
2158
+ /// matching Claude CLI's actual project-dir naming rule. Pre-fix code
2159
+ /// only replaced `/` → `-` and silently produced wrong directories for
2160
+ /// non-ASCII / dotted / spaced cwds — invisible to the scanner.
2161
+ #[test]
2162
+ fn encode_claude_projects_dir_parity_with_real_claude_naming() {
2163
+ // Pure-ASCII parity (the pre-fix happy path).
2164
+ assert_eq!(
2165
+ encode_claude_projects_dir("/Users/alauda/code"),
2166
+ "-Users-alauda-code"
2167
+ );
2168
+ // The user's actual project root (Chinese):
2169
+ // /Users/alauda/Documents/code/agent前沿探索/多agent协作
2170
+ // Each Chinese character → one `-`. "前沿探索"=4 chars → 4 dashes,
2171
+ // "多agent协作" = "多"+"agent"+"协作" = 1 + (alphanumeric kept) + 2.
2172
+ assert_eq!(
2173
+ encode_claude_projects_dir(
2174
+ "/Users/alauda/Documents/code/agent前沿探索/多agent协作"
2175
+ ),
2176
+ "-Users-alauda-Documents-code-agent------agent--"
2177
+ );
2178
+ // Dots and spaces also collapse.
2179
+ assert_eq!(
2180
+ encode_claude_projects_dir("/Users/foo bar.baz/v1.2"),
2181
+ "-Users-foo-bar-baz-v1-2"
2182
+ );
2183
+ // Hidden directory ".team" → "-team".
2184
+ assert_eq!(
2185
+ encode_claude_projects_dir("/proj/.team/runtime"),
2186
+ "-proj--team-runtime"
2187
+ );
2188
+ }
2189
+
1996
2190
  #[test]
1997
2191
  fn parse_spawned_at_rfc3339_roundtrips_and_rejects_junk() {
1998
2192
  assert!(parse_spawned_at("2026-06-10T21:40:00+00:00").is_some());