@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +52 -7
- package/crates/team-agent/src/cli/diagnose.rs +9 -0
- package/crates/team-agent/src/cli/emit.rs +175 -0
- package/crates/team-agent/src/cli/mod.rs +455 -63
- package/crates/team-agent/src/cli/status_port.rs +62 -0
- package/crates/team-agent/src/cli/tests/base.rs +9 -4
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +83 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +10 -2
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +86 -21
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +76 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/compiler.rs +73 -50
- package/crates/team-agent/src/coordinator/tick.rs +108 -20
- package/crates/team-agent/src/db/migration.rs +17 -1
- package/crates/team-agent/src/leader/owner_bind.rs +59 -20
- package/crates/team-agent/src/lifecycle/launch.rs +378 -56
- package/crates/team-agent/src/lifecycle/restart/common.rs +4 -9
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +91 -12
- package/crates/team-agent/src/lifecycle/restart/selection.rs +6 -4
- package/crates/team-agent/src/lifecycle/tests/core.rs +238 -3
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +257 -7
- package/crates/team-agent/src/lifecycle/types.rs +2 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +29 -7
- package/crates/team-agent/src/mcp_server/tests/golden.rs +7 -5
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +5 -2
- package/crates/team-agent/src/mcp_server/tools.rs +25 -1
- package/crates/team-agent/src/mcp_server/wire.rs +11 -1
- package/crates/team-agent/src/model/paths.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +23 -1
- package/crates/team-agent/src/packaging/install.rs +42 -4
- package/crates/team-agent/src/packaging/tests.rs +91 -14
- package/crates/team-agent/src/packaging/types.rs +13 -1
- package/crates/team-agent/src/provider/adapter.rs +381 -15
- package/crates/team-agent/src/state/identity.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +48 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +44 -0
- package/crates/team-agent/src/tmux_backend.rs +104 -9
- package/crates/team-agent/src/transport/test_support.rs +57 -4
- package/crates/team-agent/src/transport.rs +13 -0
- package/npm/install.mjs +31 -35
- package/package.json +4 -4
- 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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"provider"
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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") =>
|
|
198
|
-
Some("codex") =>
|
|
199
|
-
Some("
|
|
200
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"
|
|
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
|
+
}
|