@team-agent/installer 0.3.6 → 0.3.8

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.
Files changed (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +52 -7
  4. package/crates/team-agent/src/cli/diagnose.rs +9 -0
  5. package/crates/team-agent/src/cli/emit.rs +175 -0
  6. package/crates/team-agent/src/cli/mod.rs +455 -63
  7. package/crates/team-agent/src/cli/status_port.rs +62 -0
  8. package/crates/team-agent/src/cli/tests/base.rs +9 -4
  9. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +83 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +10 -2
  12. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +86 -21
  13. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +76 -0
  14. package/crates/team-agent/src/cli/types.rs +3 -2
  15. package/crates/team-agent/src/compiler.rs +73 -50
  16. package/crates/team-agent/src/coordinator/tick.rs +108 -20
  17. package/crates/team-agent/src/db/migration.rs +17 -1
  18. package/crates/team-agent/src/leader/owner_bind.rs +59 -20
  19. package/crates/team-agent/src/lifecycle/launch.rs +378 -56
  20. package/crates/team-agent/src/lifecycle/restart/common.rs +4 -9
  21. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +91 -12
  22. package/crates/team-agent/src/lifecycle/restart/selection.rs +6 -4
  23. package/crates/team-agent/src/lifecycle/tests/core.rs +238 -3
  24. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +257 -7
  25. package/crates/team-agent/src/lifecycle/types.rs +2 -0
  26. package/crates/team-agent/src/mcp_server/normalize.rs +29 -7
  27. package/crates/team-agent/src/mcp_server/tests/golden.rs +7 -5
  28. package/crates/team-agent/src/mcp_server/tests/normalize.rs +5 -2
  29. package/crates/team-agent/src/mcp_server/tools.rs +25 -1
  30. package/crates/team-agent/src/mcp_server/wire.rs +11 -1
  31. package/crates/team-agent/src/model/paths.rs +7 -0
  32. package/crates/team-agent/src/model/spec.rs +23 -1
  33. package/crates/team-agent/src/packaging/install.rs +42 -4
  34. package/crates/team-agent/src/packaging/tests.rs +91 -14
  35. package/crates/team-agent/src/packaging/types.rs +13 -1
  36. package/crates/team-agent/src/provider/adapter.rs +381 -15
  37. package/crates/team-agent/src/state/identity.rs +29 -0
  38. package/crates/team-agent/src/state/selector.rs +48 -14
  39. package/crates/team-agent/src/tmux_backend/tests.rs +44 -0
  40. package/crates/team-agent/src/tmux_backend.rs +104 -9
  41. package/crates/team-agent/src/transport/test_support.rs +57 -4
  42. package/crates/team-agent/src/transport.rs +13 -0
  43. package/npm/install.mjs +31 -35
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +82 -5
@@ -228,8 +228,18 @@ impl Coordinator {
228
228
  );
229
229
  }
230
230
 
231
+ // B-4 / 036b N36 三路可用 — 监测步(runtime_prompts / sync_health /
232
+ // detect_abnormal_exits)失败必须降级+continue,**不能**用 `?` 中断 tick,
233
+ // 否则 deliver_pending(下行投递主干)够不到,消息卡 accepted。
234
+ // bug-084 哲学 + A-6 同族:每步独立 try,失败写 `coordinator.tick.<step>_failed`
235
+ // 事件后继续走下一步;tick 本身仍返 Ok。
231
236
  self.record_step("runtime_prompts");
232
- self.handle_runtime_approval_prompts(&mut state, &event_log)?;
237
+ if let Err(error) = self.handle_runtime_approval_prompts(&mut state, &event_log) {
238
+ let _ = event_log.write(
239
+ "coordinator.tick.runtime_prompts_failed",
240
+ serde_json::json!({"error": error.to_string()}),
241
+ );
242
+ }
233
243
 
234
244
  self.record_step("sync_health");
235
245
  // P5 (C-P5-1, N3): ONE pane snapshot per tick, shared by sync_health and the
@@ -237,32 +247,71 @@ impl Coordinator {
237
247
  // this tick; every tick re-reads).
238
248
  let pane_snapshot = self.transport.list_targets().unwrap_or_default();
239
249
  let captures_by_agent =
240
- self.sync_agent_health(&mut state, &store, &event_log, &pane_snapshot)?;
250
+ match self.sync_agent_health(&mut state, &store, &event_log, &pane_snapshot) {
251
+ Ok(captures) => captures,
252
+ Err(error) => {
253
+ let _ = event_log.write(
254
+ "coordinator.tick.sync_health_failed",
255
+ serde_json::json!({"error": error.to_string()}),
256
+ );
257
+ BTreeMap::new()
258
+ }
259
+ };
241
260
  // C-3-4 cr verdict — copilot 一期 classify→None(Unknown);为防 silent,
242
261
  // tick 每次发现 copilot agent(从 state.agents 直接扫,不依赖 captures —
243
262
  // 离线/未起 tmux 场景仍能写)就发 `provider.classify.unsupported` 事件
244
263
  // (字面 reason=`phase1_unknown_pending_sample`,含 provider="copilot" + "classify"
245
264
  // 串)。二期接 sqlite turns 表后这条删/降级,届时改 reason 区分。
246
- if let Some(agents) = state.get("agents").and_then(Value::as_object) {
247
- for (agent_id, agent) in agents {
248
- let is_copilot = agent
249
- .get("provider")
250
- .and_then(Value::as_str)
251
- .and_then(parse_provider)
252
- .is_some_and(|p| matches!(p, crate::model::enums::Provider::Copilot));
253
- if is_copilot {
254
- let _ = event_log.write(
255
- "provider.classify.unsupported",
256
- serde_json::json!({
257
- "provider": "copilot",
258
- "agent_id": agent_id,
259
- "reason": "phase1_unknown_pending_sample",
260
- }),
261
- );
262
- }
265
+ //
266
+ // B-4 P4 / 036b 防 dedup-flood:同 (provider, agent_id, reason) 状态跨 tick
267
+ // 只发一次(check_key 范式,同 abnormal_check_key tick.rs:603 同精神);状态
268
+ // 变了才再发。check_key 落 state.agents.<id>.classify_unsupported.last_key,
269
+ // tick-only metadata,不进 #235 owner / receiver 等持久态。
270
+ let agents_snapshot: Vec<(String, Option<String>)> =
271
+ if let Some(agents) = state.get("agents").and_then(Value::as_object) {
272
+ agents
273
+ .iter()
274
+ .filter_map(|(agent_id, agent)| {
275
+ let is_copilot = agent
276
+ .get("provider")
277
+ .and_then(Value::as_str)
278
+ .and_then(parse_provider)
279
+ .is_some_and(|p| matches!(p, crate::model::enums::Provider::Copilot));
280
+ if !is_copilot {
281
+ return None;
282
+ }
283
+ let last_key = agent
284
+ .get("classify_unsupported")
285
+ .and_then(|v| v.get("last_key"))
286
+ .and_then(Value::as_str)
287
+ .map(str::to_string);
288
+ Some((agent_id.clone(), last_key))
289
+ })
290
+ .collect()
291
+ } else {
292
+ Vec::new()
293
+ };
294
+ for (agent_id, last_key) in agents_snapshot {
295
+ let check_key = format!("copilot|{agent_id}|phase1_unknown_pending_sample");
296
+ if last_key.as_deref() == Some(check_key.as_str()) {
297
+ continue;
263
298
  }
299
+ let _ = event_log.write(
300
+ "provider.classify.unsupported",
301
+ serde_json::json!({
302
+ "provider": "copilot",
303
+ "agent_id": agent_id,
304
+ "reason": "phase1_unknown_pending_sample",
305
+ }),
306
+ );
307
+ mark_classify_unsupported(&mut state, &agent_id, &check_key);
308
+ }
309
+ if let Err(error) = self.detect_abnormal_exits(&mut state, &event_log, &pane_snapshot) {
310
+ let _ = event_log.write(
311
+ "coordinator.tick.detect_abnormal_failed",
312
+ serde_json::json!({"error": error.to_string()}),
313
+ );
264
314
  }
265
- self.detect_abnormal_exits(&mut state, &event_log, &pane_snapshot)?;
266
315
 
267
316
  self.record_step("deliver_pending");
268
317
  let delivered = crate::messaging::deliver_pending_messages(
@@ -389,6 +438,12 @@ impl Coordinator {
389
438
  let team = crate::state::projection::team_state_key(&snapshot);
390
439
  let team_key = Some(crate::model::ids::TeamKey::new(team.clone()));
391
440
  let session_name = state.get("session_name").and_then(Value::as_str).map(str::to_string);
441
+ // B-4 / 036b N36 三路可用 — sync_health 内 per-agent capture 失败本就降级
442
+ // (写 coordinator.agent_capture_failed 后 continue),不打断 deliver_pending
443
+ // 主干。但 contract 要求一条【tick 级】可观测的 step-failed 信号 —
444
+ // sync_health 失败一旦发生就在末尾 emit `coordinator.tick.sync_health_failed`
445
+ // (含 "tick" + "_failed" 双串),避免 silent。
446
+ let mut had_capture_failure = false;
392
447
  // P5 (C-P5-2): one list-windows per SESSION per tick — memoized across the
393
448
  // agent loop instead of one fork per agent.
394
449
  let mut windows_by_session: BTreeMap<String, Result<Vec<crate::transport::WindowName>, String>> =
@@ -409,6 +464,7 @@ impl Coordinator {
409
464
  }) {
410
465
  Ok(windows) => windows.clone(),
411
466
  Err(error) => {
467
+ had_capture_failure = true;
412
468
  event_log.write(
413
469
  "coordinator.agent_capture_failed",
414
470
  serde_json::json!({
@@ -429,6 +485,7 @@ impl Coordinator {
429
485
  {
430
486
  Ok(captured) => captured,
431
487
  Err(error) => {
488
+ had_capture_failure = true;
432
489
  event_log.write(
433
490
  "coordinator.agent_capture_failed",
434
491
  serde_json::json!({
@@ -506,6 +563,15 @@ impl Coordinator {
506
563
  },
507
564
  );
508
565
  }
566
+ // B-4 step-level signal:若本 tick 有任一 capture 失败,emit
567
+ // `coordinator.tick.sync_health_failed`(含 "tick" + "_failed")让 contract
568
+ // 可观测,deliver_pending 主干不受影响。
569
+ if had_capture_failure {
570
+ let _ = event_log.write(
571
+ "coordinator.tick.sync_health_failed",
572
+ serde_json::json!({"step": "sync_health", "degraded": true}),
573
+ );
574
+ }
509
575
  Ok(captures)
510
576
  }
511
577
 
@@ -1738,6 +1804,28 @@ fn mark_abnormal_suppressed(state: &mut Value, agent_id: &str, key: &str) {
1738
1804
  }
1739
1805
  }
1740
1806
 
1807
+ /// B-4 P4 dedup marker — 把 classify.unsupported check_key 写到
1808
+ /// state.agents.<id>.classify_unsupported.last_key,只在状态变了才再发事件。
1809
+ /// 同 abnormal_check_key (line 603) 同精神,但落 agents 子 obj(per-agent locality,
1810
+ /// 不污染 abnormal_exit_watch 命名空间)。
1811
+ fn mark_classify_unsupported(state: &mut Value, agent_id: &str, key: &str) {
1812
+ let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
1813
+ return;
1814
+ };
1815
+ let Some(agent) = agents.get_mut(agent_id).and_then(Value::as_object_mut) else {
1816
+ return;
1817
+ };
1818
+ let entry = agent
1819
+ .entry("classify_unsupported".to_string())
1820
+ .or_insert_with(|| serde_json::json!({}));
1821
+ if !entry.is_object() {
1822
+ *entry = serde_json::json!({});
1823
+ }
1824
+ if let Some(obj) = entry.as_object_mut() {
1825
+ obj.insert("last_key".to_string(), serde_json::json!(key));
1826
+ }
1827
+ }
1828
+
1741
1829
  fn mark_abnormal_checked(state: &mut Value, agent_id: &str, key: &str) {
1742
1830
  if let Some(watch) = coordinator_child_object(state, "abnormal_exit_watch") {
1743
1831
  let entry = watch
@@ -84,12 +84,16 @@ pub struct RebuildEvent {
84
84
  }
85
85
 
86
86
  /// `schema_diagnosis` 的只读结论。
87
- #[derive(Debug, Clone, PartialEq, Eq)]
87
+ #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
88
88
  pub struct Diagnosis {
89
89
  pub ok: bool,
90
90
  pub status: String,
91
91
  pub user_version: i64,
92
92
  pub layout_diffs: Vec<String>,
93
+ /// Python parity (`schema_migration.py:188/206`): the layered guidance line —
94
+ /// "missing" carries the initialize_schema first-use hint; drift carries the
95
+ /// fix-schema command; healthy carries "none".
96
+ pub recommended_action: String,
93
97
  }
94
98
 
95
99
  fn table_exists(conn: &Connection, table: &str) -> Result<bool, DbError> {
@@ -316,11 +320,18 @@ pub fn ensure_table_layout(
316
320
  /// `schema_migration.py:schema_diagnosis`:只读判定(不变更 DB)。
317
321
  pub fn schema_diagnosis(db_path: &Path, schema_version: i64) -> Result<Diagnosis, DbError> {
318
322
  if !db_path.exists() {
323
+ // T3-3 cr verdict (A parity lock, 2026-06-10): a missing db is the LEGAL
324
+ // first-use state — ok:true is layered with the explicit status axis and the
325
+ // recommended_action guidance (Python schema_migration.py:180-190 verbatim),
326
+ // never a silent fake-green.
319
327
  return Ok(Diagnosis {
320
328
  ok: true,
321
329
  status: "missing".to_string(),
322
330
  user_version: 0,
323
331
  layout_diffs: vec![],
332
+ recommended_action:
333
+ "No team.db exists yet; initialize_schema will create it on first use."
334
+ .to_string(),
324
335
  });
325
336
  }
326
337
  let conn = Connection::open(db_path)?;
@@ -332,6 +343,11 @@ pub fn schema_diagnosis(db_path: &Path, schema_version: i64) -> Result<Diagnosis
332
343
  ok,
333
344
  status: if ok { "ok".to_string() } else { "schema_repair_available".to_string() },
334
345
  user_version: uv,
346
+ recommended_action: if diff_tables.is_empty() {
347
+ "none".to_string()
348
+ } else {
349
+ "run team-agent doctor --fix-schema --json".to_string()
350
+ },
335
351
  layout_diffs: diff_tables,
336
352
  })
337
353
  }
@@ -189,15 +189,29 @@ fn bind_provider_from_env_or_command(command: &str) -> Provider {
189
189
  std::env::var("TEAM_AGENT_LEADER_PROVIDER")
190
190
  .ok()
191
191
  .and_then(|raw| super::helpers::parse_provider(&raw))
192
- .unwrap_or_else(|| provider_from_command(command))
192
+ .or_else(|| provider_from_command(command))
193
+ // E11 层2:未知命令不再静默默认 codex(会误绑任意 provider + 喂错分类器)。
194
+ // 无法识别时回落 Codex 仅作最末兜底,且该路径已被 provider_from_command 的显式 None 收窄
195
+ // (调用方理应只在已知 leader 命令上 bind);保留以不改 fn 签名/上游 panic 面。
196
+ .unwrap_or(Provider::Codex)
193
197
  }
194
198
 
195
- fn provider_from_command(command: &str) -> Provider {
199
+ /// E11 层2 + N39:command wire 串 → `parse_provider`(**单一映射源**,与
200
+ /// `owner_bind_provider_wire` 共用 [`command_provider_wire`])。未知命令 → `None`
201
+ /// (危险的 `_ => Codex` 默认已删:不静默把任意 provider 误绑成 codex)。
202
+ fn provider_from_command(command: &str) -> Option<Provider> {
203
+ command_provider_wire(command).and_then(super::helpers::parse_provider)
204
+ }
205
+
206
+ /// command 名 → provider wire 串(单一真相;copilot/claude/codex/fake)。未知 → `None`。
207
+ /// `claude.exe` 归一为 `claude`。
208
+ fn command_provider_wire(command: &str) -> Option<&'static str> {
196
209
  match exact_command_name(command).as_deref() {
197
- Some("claude") | Some("claude.exe") => Provider::Claude,
198
- Some("codex") => Provider::Codex,
199
- Some("fake") => Provider::Fake,
200
- _ => Provider::Codex,
210
+ Some("claude") | Some("claude.exe") => Some("claude"),
211
+ Some("codex") => Some("codex"),
212
+ Some("copilot") => Some("copilot"),
213
+ Some("fake") => Some("fake"),
214
+ _ => None,
201
215
  }
202
216
  }
203
217
 
@@ -214,21 +228,15 @@ fn exact_command_name(command: &str) -> Option<String> {
214
228
 
215
229
  pub fn owner_bind_provider_wire(command: &str) -> &'static str {
216
230
  if let Ok(raw) = std::env::var("TEAM_AGENT_LEADER_PROVIDER") {
217
- return match raw.as_str() {
218
- "claude" => "claude",
219
- "claude_code" => "claude_code",
220
- "codex" => "codex",
221
- "gemini_cli" => "gemini_cli",
222
- "fake" => "fake",
223
- _ => "",
224
- };
225
- }
226
- match exact_command_name(command).as_deref() {
227
- Some("claude") | Some("claude.exe") => "claude",
228
- Some("codex") => "codex",
229
- Some("fake") => "fake",
230
- _ => "",
231
+ // env 显式 provider:经 parse_provider(单一表,知 copilot)校验后透传其 wire 串;
232
+ // 不识别 ""(空,与原行为一致:不绑)。
233
+ return super::helpers::parse_provider(&raw)
234
+ .map(super::helpers::provider_wire)
235
+ .unwrap_or("");
231
236
  }
237
+ // E11 层2 + N39:与 provider_from_command 共用 command_provider_wire 单一映射(含 copilot);
238
+ // 未知命令 → ""(不绑),不再静默当 codex。
239
+ command_provider_wire(command).unwrap_or("")
232
240
  }
233
241
 
234
242
  fn family_a_identity(
@@ -290,3 +298,34 @@ fn tmux_pane_current_command(workspace: &Path, pane: &str) -> Result<String, Lea
290
298
  // NOTE: `derive_leader_session_uuid`(`leader_binding.py:146`)已由
291
299
  // `model::ids::LeaderSessionUuid::derive` 字节对齐实现(含 NUL 拒绝 + golden 测试)——
292
300
  // 此 lane REUSE 之,不重声明。
301
+
302
+ #[cfg(test)]
303
+ mod e11_provider_bind_tests {
304
+ #![allow(clippy::unwrap_used)]
305
+ use super::*;
306
+
307
+ // E11 层2:copilot leader 命令必须绑成 Provider::Copilot(此前缺臂 → _ => Codex 误绑)。
308
+ #[test]
309
+ fn copilot_command_binds_copilot_not_codex() {
310
+ assert_eq!(provider_from_command("copilot --banner -C /ws"), Some(Provider::Copilot));
311
+ assert_eq!(provider_from_command("/opt/homebrew/bin/copilot"), Some(Provider::Copilot));
312
+ assert_eq!(owner_bind_provider_wire("copilot --banner"), "copilot");
313
+ }
314
+
315
+ #[test]
316
+ fn known_commands_map_via_single_source() {
317
+ assert_eq!(provider_from_command("claude"), Some(Provider::Claude));
318
+ assert_eq!(provider_from_command("codex"), Some(Provider::Codex));
319
+ assert_eq!(provider_from_command("fake"), Some(Provider::Fake));
320
+ assert_eq!(owner_bind_provider_wire("claude"), "claude");
321
+ assert_eq!(owner_bind_provider_wire("codex"), "codex");
322
+ }
323
+
324
+ // E11 层2:未知命令不再静默默认 codex —— provider_from_command → None,wire → ""。
325
+ #[test]
326
+ fn unknown_command_is_none_not_silent_codex() {
327
+ assert_eq!(provider_from_command("node /some/thing.js"), None);
328
+ assert_eq!(provider_from_command("totally-unknown"), None);
329
+ assert_eq!(owner_bind_provider_wire("totally-unknown"), "");
330
+ }
331
+ }