@team-agent/installer 0.3.1 → 0.3.3
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
//! status_port extracted from cli::mod inline placeholder.
|
|
2
2
|
use super::*;
|
|
3
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
3
4
|
use crate::transport::Transport;
|
|
5
|
+
use rusqlite::params;
|
|
4
6
|
|
|
5
7
|
/// `status.status(workspace, as_json, compact)`(`queries.py:33`,**有副作用**:capture→refresh→save)。
|
|
6
8
|
pub fn status(workspace: &Path, compact: bool, detail: bool) -> Result<Value, CliError> {
|
|
7
|
-
let _ = detail;
|
|
8
9
|
let state = read_runtime_state(workspace);
|
|
10
|
+
status_scoped(workspace, &state, None, compact, detail)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn status_scoped(
|
|
14
|
+
workspace: &Path,
|
|
15
|
+
state: &Value,
|
|
16
|
+
owner_team_id: Option<&str>,
|
|
17
|
+
compact: bool,
|
|
18
|
+
detail: bool,
|
|
19
|
+
) -> Result<Value, CliError> {
|
|
20
|
+
let _ = detail;
|
|
21
|
+
let resolved_owner_team_id = resolve_status_owner_team(workspace, owner_team_id)?;
|
|
22
|
+
let owner_team_id = resolved_owner_team_id.as_deref().or(owner_team_id);
|
|
9
23
|
let health = crate::coordinator::coordinator_health(
|
|
10
24
|
&crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
|
|
11
25
|
);
|
|
@@ -23,19 +37,34 @@ use crate::transport::Transport;
|
|
|
23
37
|
.cloned()
|
|
24
38
|
.unwrap_or_else(|| json!({}));
|
|
25
39
|
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
40
|
+
let tmux_present = tmux_session_present(workspace, session_name.as_str());
|
|
41
|
+
let mut readiness_state = state.clone();
|
|
42
|
+
if let Some(obj) = readiness_state.as_object_mut() {
|
|
43
|
+
obj.insert("tmux_session_present".to_string(), serde_json::json!(tmux_present));
|
|
44
|
+
}
|
|
45
|
+
let readiness = crate::cli::diagnose::wait_readiness(&readiness_state);
|
|
26
46
|
let full = json!({
|
|
47
|
+
"ok": true,
|
|
27
48
|
"team": state.pointer("/leader/id").cloned().unwrap_or_else(|| json!("leader")),
|
|
28
49
|
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
29
|
-
"tmux_session_present":
|
|
50
|
+
"tmux_session_present": tmux_present,
|
|
51
|
+
"all_spawned": readiness.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
|
|
52
|
+
"all_attached_receiver": readiness.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
|
|
53
|
+
"all_resumable_have_session": readiness.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
|
|
54
|
+
"session_capture_complete": readiness.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
|
|
55
|
+
"session_capture_incomplete": readiness.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
|
|
56
|
+
"incomplete_session_capture_agents": readiness.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
|
|
57
|
+
"pending_session_agent_ids": readiness.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
|
|
30
58
|
"leader_receiver": leader_receiver,
|
|
31
59
|
"teams": state.get("teams").cloned().unwrap_or_else(|| json!({})),
|
|
32
60
|
"agents": agents,
|
|
33
|
-
"agent_health": agent_health(&conn)?,
|
|
61
|
+
"agent_health": agent_health(&conn, owner_team_id)?,
|
|
34
62
|
"tasks": tasks,
|
|
35
|
-
"messages": message_counts(&conn)?,
|
|
36
|
-
"queued_messages": queued_messages(&conn, 8)?,
|
|
37
|
-
"results": result_counts(&conn)?,
|
|
63
|
+
"messages": message_counts(&conn, owner_team_id)?,
|
|
64
|
+
"queued_messages": queued_messages(&conn, owner_team_id, 8)?,
|
|
65
|
+
"results": result_counts(&conn, owner_team_id)?,
|
|
38
66
|
"latest_results": json!([]),
|
|
67
|
+
"readiness": readiness,
|
|
39
68
|
"coordinator": coordinator_health_value(health),
|
|
40
69
|
"last_events": Value::Array(
|
|
41
70
|
crate::event_log::EventLog::new(workspace)
|
|
@@ -51,7 +80,17 @@ use crate::transport::Transport;
|
|
|
51
80
|
}
|
|
52
81
|
/// `status.format_status(workspace, agent)`(人读)。
|
|
53
82
|
pub fn format_status(workspace: &Path, agent: Option<&str>) -> Result<String, CliError> {
|
|
54
|
-
let
|
|
83
|
+
let state = read_runtime_state(workspace);
|
|
84
|
+
format_status_scoped(workspace, &state, None, agent)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn format_status_scoped(
|
|
88
|
+
workspace: &Path,
|
|
89
|
+
state: &Value,
|
|
90
|
+
owner_team_id: Option<&str>,
|
|
91
|
+
agent: Option<&str>,
|
|
92
|
+
) -> Result<String, CliError> {
|
|
93
|
+
let status = status_scoped(workspace, state, owner_team_id, true, false)?;
|
|
55
94
|
Ok(match agent {
|
|
56
95
|
Some(agent) => format!("agent {agent}: {}", status.pointer("/agents").is_some()),
|
|
57
96
|
None => crate::cli::format_status_summary(&status),
|
|
@@ -165,6 +204,32 @@ use crate::transport::Transport;
|
|
|
165
204
|
.unwrap_or_else(|| json!({}))
|
|
166
205
|
}
|
|
167
206
|
|
|
207
|
+
fn resolve_status_owner_team(
|
|
208
|
+
workspace: &Path,
|
|
209
|
+
owner_team_id: Option<&str>,
|
|
210
|
+
) -> Result<Option<String>, CliError> {
|
|
211
|
+
let Some(requested) = owner_team_id.filter(|team| !team.is_empty()) else {
|
|
212
|
+
return Ok(None);
|
|
213
|
+
};
|
|
214
|
+
let state = read_runtime_state(workspace);
|
|
215
|
+
match crate::state::projection::resolve_owner_team_id(&state, requested) {
|
|
216
|
+
OwnerTeamResolution::Canonical(canonical) => Ok(Some(canonical)),
|
|
217
|
+
OwnerTeamResolution::LegacyAlias { requested, canonical } => {
|
|
218
|
+
let log = crate::event_log::EventLog::new(workspace);
|
|
219
|
+
crate::messaging::delivery::normalize_owner_team_id_rows(
|
|
220
|
+
workspace,
|
|
221
|
+
&requested,
|
|
222
|
+
&canonical,
|
|
223
|
+
None,
|
|
224
|
+
Some(&log),
|
|
225
|
+
)
|
|
226
|
+
.map_err(CliError::from)?;
|
|
227
|
+
Ok(Some(canonical))
|
|
228
|
+
}
|
|
229
|
+
OwnerTeamResolution::Unresolved { .. } | OwnerTeamResolution::Ambiguous { .. } => Ok(None),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
168
233
|
fn agent_window(agent_id: &str, agent_state: &Value) -> String {
|
|
169
234
|
["window", "window_name"]
|
|
170
235
|
.iter()
|
|
@@ -224,15 +289,15 @@ use crate::transport::Transport;
|
|
|
224
289
|
.unwrap_or(false)
|
|
225
290
|
}
|
|
226
291
|
|
|
227
|
-
fn message_counts(conn: &rusqlite::Connection) -> Result<Value, CliError> {
|
|
228
|
-
status_counts(conn, "messages")
|
|
292
|
+
fn message_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
|
|
293
|
+
status_counts(conn, "messages", owner_team_id)
|
|
229
294
|
}
|
|
230
295
|
|
|
231
|
-
fn result_counts(conn: &rusqlite::Connection) -> Result<Value, CliError> {
|
|
232
|
-
let by_status = result_status_counts(conn)?;
|
|
233
|
-
let total = count_rows(conn, "results")?;
|
|
234
|
-
let invalid = count_where_status(conn, "results", "invalid")?;
|
|
235
|
-
let collected = count_where_status(conn, "results", "collected")?;
|
|
296
|
+
fn result_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
|
|
297
|
+
let by_status = result_status_counts(conn, owner_team_id)?;
|
|
298
|
+
let total = count_rows(conn, "results", owner_team_id)?;
|
|
299
|
+
let invalid = count_where_status(conn, "results", owner_team_id, "invalid")?;
|
|
300
|
+
let collected = count_where_status(conn, "results", owner_team_id, "collected")?;
|
|
236
301
|
let uncollected = total.saturating_sub(collected).saturating_sub(invalid);
|
|
237
302
|
Ok(json!({
|
|
238
303
|
"total": total,
|
|
@@ -243,10 +308,24 @@ use crate::transport::Transport;
|
|
|
243
308
|
}))
|
|
244
309
|
}
|
|
245
310
|
|
|
246
|
-
fn status_counts(
|
|
247
|
-
|
|
311
|
+
fn status_counts(
|
|
312
|
+
conn: &rusqlite::Connection,
|
|
313
|
+
table: &str,
|
|
314
|
+
owner_team_id: Option<&str>,
|
|
315
|
+
) -> Result<Value, CliError> {
|
|
316
|
+
let sql = match owner_team_id {
|
|
317
|
+
Some(_) => format!(
|
|
318
|
+
"select status, count(*) from {table}
|
|
319
|
+
where owner_team_id = ?1
|
|
320
|
+
group by status order by status"
|
|
321
|
+
),
|
|
322
|
+
None => format!("select status, count(*) from {table} group by status order by status"),
|
|
323
|
+
};
|
|
248
324
|
let mut stmt = conn.prepare(&sql).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
249
|
-
let mut rows =
|
|
325
|
+
let mut rows = match owner_team_id {
|
|
326
|
+
Some(team) => stmt.query(params![team]).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
327
|
+
None => stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
328
|
+
};
|
|
250
329
|
let mut out = Map::new();
|
|
251
330
|
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
252
331
|
let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
@@ -256,16 +335,28 @@ use crate::transport::Transport;
|
|
|
256
335
|
Ok(Value::Object(out))
|
|
257
336
|
}
|
|
258
337
|
|
|
259
|
-
fn result_status_counts(conn: &rusqlite::Connection) -> Result<Value, CliError> {
|
|
260
|
-
let
|
|
261
|
-
|
|
338
|
+
fn result_status_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
|
|
339
|
+
let sql = match owner_team_id {
|
|
340
|
+
Some(_) => {
|
|
341
|
+
"select status, count(*) from results
|
|
342
|
+
where status not in ('collected', 'invalid') and owner_team_id = ?1
|
|
343
|
+
group by status
|
|
344
|
+
order by status"
|
|
345
|
+
}
|
|
346
|
+
None => {
|
|
262
347
|
"select status, count(*) from results
|
|
263
348
|
where status not in ('collected', 'invalid')
|
|
264
349
|
group by status
|
|
265
|
-
order by status"
|
|
266
|
-
|
|
350
|
+
order by status"
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
let mut stmt = conn
|
|
354
|
+
.prepare(sql)
|
|
267
355
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
268
|
-
let mut rows =
|
|
356
|
+
let mut rows = match owner_team_id {
|
|
357
|
+
Some(team) => stmt.query(params![team]).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
358
|
+
None => stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
359
|
+
};
|
|
269
360
|
let mut out = Map::new();
|
|
270
361
|
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
271
362
|
let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
@@ -275,19 +366,32 @@ use crate::transport::Transport;
|
|
|
275
366
|
Ok(Value::Object(out))
|
|
276
367
|
}
|
|
277
368
|
|
|
278
|
-
fn queued_messages(
|
|
369
|
+
fn queued_messages(
|
|
370
|
+
conn: &rusqlite::Connection,
|
|
371
|
+
owner_team_id: Option<&str>,
|
|
372
|
+
limit: usize,
|
|
373
|
+
) -> Result<Value, CliError> {
|
|
279
374
|
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
|
|
280
|
-
let
|
|
281
|
-
|
|
375
|
+
let sql = match owner_team_id {
|
|
376
|
+
Some(_) => {
|
|
377
|
+
"select message_id, recipient, status, created_at, delivery_attempts
|
|
378
|
+
from messages
|
|
379
|
+
where status like 'queued%' and owner_team_id = ?1
|
|
380
|
+
order by created_at desc
|
|
381
|
+
limit ?2"
|
|
382
|
+
}
|
|
383
|
+
None => {
|
|
282
384
|
"select message_id, recipient, status, created_at, delivery_attempts
|
|
283
385
|
from messages
|
|
284
386
|
where status like 'queued%'
|
|
285
387
|
order by created_at desc
|
|
286
|
-
limit ?1"
|
|
287
|
-
|
|
388
|
+
limit ?1"
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
let mut stmt = conn
|
|
392
|
+
.prepare(sql)
|
|
288
393
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
289
|
-
let
|
|
290
|
-
.query_map([limit], |row| {
|
|
394
|
+
let map_row = |row: &rusqlite::Row<'_>| {
|
|
291
395
|
Ok(json!({
|
|
292
396
|
"message_id": row.get::<_, String>(0)?,
|
|
293
397
|
"recipient": row.get::<_, Option<String>>(1)?,
|
|
@@ -295,8 +399,12 @@ use crate::transport::Transport;
|
|
|
295
399
|
"created_at": row.get::<_, Option<String>>(3)?,
|
|
296
400
|
"delivery_attempts": row.get::<_, i64>(4)?,
|
|
297
401
|
}))
|
|
298
|
-
}
|
|
299
|
-
|
|
402
|
+
};
|
|
403
|
+
let rows = match owner_team_id {
|
|
404
|
+
Some(team) => stmt.query_map(params![team, limit], map_row),
|
|
405
|
+
None => stmt.query_map(params![limit], map_row),
|
|
406
|
+
}
|
|
407
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
300
408
|
let values = rows
|
|
301
409
|
.collect::<Result<Vec<_>, _>>()
|
|
302
410
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
@@ -308,6 +416,13 @@ use crate::transport::Transport;
|
|
|
308
416
|
"team": full.get("team").cloned().unwrap_or(Value::Null),
|
|
309
417
|
"session_name": full.get("session_name").cloned().unwrap_or(Value::Null),
|
|
310
418
|
"tmux_session_present": full.get("tmux_session_present").cloned().unwrap_or(Value::Bool(false)),
|
|
419
|
+
"all_spawned": full.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
|
|
420
|
+
"all_attached_receiver": full.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
|
|
421
|
+
"all_resumable_have_session": full.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
|
|
422
|
+
"session_capture_complete": full.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
|
|
423
|
+
"session_capture_incomplete": full.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
|
|
424
|
+
"incomplete_session_capture_agents": full.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
|
|
425
|
+
"pending_session_agent_ids": full.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
|
|
311
426
|
"leader_receiver": compact_object(full.get("leader_receiver"), &[
|
|
312
427
|
"status", "provider", "mode", "session_name", "window_name", "pane_id", "pane_current_command",
|
|
313
428
|
]),
|
|
@@ -318,6 +433,7 @@ use crate::transport::Transport;
|
|
|
318
433
|
"queued_messages": take_array(full.get("queued_messages"), 8),
|
|
319
434
|
"results": full.get("results").cloned().unwrap_or_else(|| json!({})),
|
|
320
435
|
"latest_results": take_array(full.get("latest_results"), 5),
|
|
436
|
+
"readiness": full.get("readiness").cloned().unwrap_or_else(|| json!({})),
|
|
321
437
|
"coordinator": compact_object(full.get("coordinator"), &["status", "pid", "metadata_ok", "schema_ok"]),
|
|
322
438
|
"last_events": take_array_tail(full.get("last_events"), 10),
|
|
323
439
|
})
|
|
@@ -377,7 +493,7 @@ use crate::transport::Transport;
|
|
|
377
493
|
};
|
|
378
494
|
Value::Array(
|
|
379
495
|
tasks.iter()
|
|
380
|
-
.map(|task| compact_object(Some(task), &["id", "title", "status", "assignee", "type"]))
|
|
496
|
+
.map(|task| compact_object(Some(task), &["id", "title", "status", "assignee", "type", "accepted_result_id"]))
|
|
381
497
|
.collect(),
|
|
382
498
|
)
|
|
383
499
|
}
|
|
@@ -410,30 +526,63 @@ use crate::transport::Transport;
|
|
|
410
526
|
Value::Array(items.iter().skip(start).cloned().collect())
|
|
411
527
|
}
|
|
412
528
|
|
|
413
|
-
fn count_rows(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
529
|
+
fn count_rows(
|
|
530
|
+
conn: &rusqlite::Connection,
|
|
531
|
+
table: &str,
|
|
532
|
+
owner_team_id: Option<&str>,
|
|
533
|
+
) -> Result<i64, CliError> {
|
|
534
|
+
match owner_team_id {
|
|
535
|
+
Some(team) => {
|
|
536
|
+
let sql = format!("select count(*) from {table} where owner_team_id = ?1");
|
|
537
|
+
conn.query_row(&sql, [team], |row| row.get::<_, i64>(0))
|
|
538
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
539
|
+
}
|
|
540
|
+
None => {
|
|
541
|
+
let sql = format!("select count(*) from {table}");
|
|
542
|
+
conn.query_row(&sql, [], |row| row.get::<_, i64>(0))
|
|
543
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
544
|
+
}
|
|
545
|
+
}
|
|
417
546
|
}
|
|
418
547
|
|
|
419
548
|
fn count_where_status(
|
|
420
549
|
conn: &rusqlite::Connection,
|
|
421
550
|
table: &str,
|
|
551
|
+
owner_team_id: Option<&str>,
|
|
422
552
|
status: &str,
|
|
423
553
|
) -> Result<i64, CliError> {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
554
|
+
match owner_team_id {
|
|
555
|
+
Some(team) => {
|
|
556
|
+
let sql = format!("select count(*) from {table} where status = ?1 and owner_team_id = ?2");
|
|
557
|
+
conn.query_row(&sql, params![status, team], |row| row.get::<_, i64>(0))
|
|
558
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
559
|
+
}
|
|
560
|
+
None => {
|
|
561
|
+
let sql = format!("select count(*) from {table} where status = ?1");
|
|
562
|
+
conn.query_row(&sql, [status], |row| row.get::<_, i64>(0))
|
|
563
|
+
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
564
|
+
}
|
|
565
|
+
}
|
|
427
566
|
}
|
|
428
567
|
|
|
429
|
-
fn agent_health(conn: &rusqlite::Connection) -> Result<Value, CliError> {
|
|
430
|
-
let
|
|
431
|
-
|
|
568
|
+
fn agent_health(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
|
|
569
|
+
let sql = match owner_team_id {
|
|
570
|
+
Some(_) => {
|
|
571
|
+
"select agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at, owner_team_id
|
|
572
|
+
from agent_health where owner_team_id = ?1 order by agent_id"
|
|
573
|
+
}
|
|
574
|
+
None => {
|
|
432
575
|
"select agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at, owner_team_id
|
|
433
|
-
from agent_health order by agent_id"
|
|
434
|
-
|
|
576
|
+
from agent_health order by agent_id"
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
let mut stmt = conn
|
|
580
|
+
.prepare(sql)
|
|
435
581
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
436
|
-
let mut rows =
|
|
582
|
+
let mut rows = match owner_team_id {
|
|
583
|
+
Some(team) => stmt.query(params![team]).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
584
|
+
None => stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?,
|
|
585
|
+
};
|
|
437
586
|
let mut out = Map::new();
|
|
438
587
|
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
439
588
|
let agent_id: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
@@ -456,7 +456,7 @@ Hint: team-agent inbox leader";
|
|
|
456
456
|
// Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
|
|
457
457
|
#[test]
|
|
458
458
|
fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
|
|
459
|
-
const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency. Does NOT perform live runtime message round-trip.
|
|
459
|
+
const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
460
460
|
let args = DoctorArgs {
|
|
461
461
|
spec: None,
|
|
462
462
|
workspace: PathBuf::from("."),
|
|
@@ -506,4 +506,3 @@ Hint: team-agent inbox leader";
|
|
|
506
506
|
assert_eq!(run(&["codex".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
|
|
507
507
|
assert_eq!(run(&["claude".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
|
|
508
508
|
}
|
|
509
|
-
|
|
@@ -216,27 +216,35 @@ use super::*;
|
|
|
216
216
|
// the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
|
|
217
217
|
#[test]
|
|
218
218
|
fn comms_selftest_golden_boundary_scope_and_check_shapes() {
|
|
219
|
-
let boundary = "validates live pane binding consistency
|
|
220
|
-
|
|
221
|
-
(zero token, zero pollution)";
|
|
219
|
+
let boundary = "validates live pane binding consistency and zero-token comms contracts. \
|
|
220
|
+
Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
222
221
|
let ws = tmp_workspace();
|
|
223
222
|
let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
|
|
224
223
|
let obj = v.as_object().expect("comms dict");
|
|
225
224
|
assert_eq!(v["boundary"], json!(boundary), "golden COMMS_BOUNDARY_TEXT prefix (comms.py:11-14)");
|
|
226
225
|
assert_eq!(v["scope"], json!("binding_consistency"), "golden scope");
|
|
227
|
-
assert_eq!(
|
|
226
|
+
assert_eq!(
|
|
227
|
+
v["status"],
|
|
228
|
+
json!("fail"),
|
|
229
|
+
"empty workspace has no runtime receiver binding, so comms gate must not pass"
|
|
230
|
+
);
|
|
228
231
|
assert!(obj.contains_key("run_id"), "golden carries a run_id (uuid hex[:12])");
|
|
229
232
|
assert!(!obj.contains_key("team"), "golden has NO `team` key");
|
|
230
233
|
assert!(!obj.contains_key("gate"), "golden has NO `gate` key");
|
|
231
|
-
assert_eq!(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
assert_eq!(v["checks"]["contract_suite"]["status"], json!("pass"));
|
|
235
|
+
assert_eq!(v["checks"]["contract_suite"]["failed"], json!([]));
|
|
236
|
+
let suite_names: Vec<&str> = v["checks"]["contract_suite"]["checks"]
|
|
237
|
+
.as_array()
|
|
238
|
+
.expect("contract suite checks")
|
|
239
|
+
.iter()
|
|
240
|
+
.filter_map(|check| check.get("name").and_then(Value::as_str))
|
|
241
|
+
.collect();
|
|
242
|
+
assert!(
|
|
243
|
+
suite_names.contains(&"message_store_schema")
|
|
244
|
+
&& suite_names.contains(&"message_token_shape")
|
|
245
|
+
&& suite_names.contains(&"result_notification_render")
|
|
246
|
+
&& suite_names.contains(&"leader_projection_owner_team"),
|
|
247
|
+
"contract suite must be executable, not deferred: {suite_names:?}"
|
|
240
248
|
);
|
|
241
249
|
assert_eq!(
|
|
242
250
|
v["checks"]["provider_sdk_calls"]["calls"],
|
|
@@ -271,6 +279,8 @@ use super::*;
|
|
|
271
279
|
// {ok,scanned,orphans,dry_run:true,scanned_at,action_required}. RUST mod.rs:534-535 stub
|
|
272
280
|
// {ok,confirm,cleaned}. scanned/scanned_at are machine/clock-derived; lock the deterministic ones. RED.
|
|
273
281
|
#[test]
|
|
282
|
+
#[ignore = "real-machine: cleanup_orphans scans machine-wide ta-* tmux/process residue"]
|
|
283
|
+
#[serial_test::file_serial(tmux)]
|
|
274
284
|
fn cleanup_orphans_dryrun_golden_envelope() {
|
|
275
285
|
let v = diagnose_port::cleanup_orphans(/*confirm=*/ false).expect("cleanup_orphans");
|
|
276
286
|
let obj = v.as_object().expect("cleanup dict");
|
|
@@ -4,6 +4,39 @@ struct TeamSocketGuard {
|
|
|
4
4
|
ws: std::path::PathBuf,
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
struct EnvGuard {
|
|
8
|
+
previous: Vec<(&'static str, Option<String>)>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl EnvGuard {
|
|
12
|
+
fn unset(keys: &[&'static str]) -> Self {
|
|
13
|
+
let previous = keys
|
|
14
|
+
.iter()
|
|
15
|
+
.map(|key| (*key, std::env::var(key).ok()))
|
|
16
|
+
.collect::<Vec<_>>();
|
|
17
|
+
for key in keys {
|
|
18
|
+
unsafe {
|
|
19
|
+
std::env::remove_var(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
Self { previous }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl Drop for EnvGuard {
|
|
27
|
+
fn drop(&mut self) {
|
|
28
|
+
for (key, value) in self.previous.drain(..).rev() {
|
|
29
|
+
unsafe {
|
|
30
|
+
if let Some(value) = value {
|
|
31
|
+
std::env::set_var(key, value);
|
|
32
|
+
} else {
|
|
33
|
+
std::env::remove_var(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
impl Drop for TeamSocketGuard {
|
|
8
41
|
fn drop(&mut self) {
|
|
9
42
|
crate::tmux_backend::TmuxBackend::for_workspace(&self.ws).kill_server();
|
|
@@ -122,7 +155,18 @@ fn current_uid() -> Option<String> {
|
|
|
122
155
|
#[ignore = "real-machine: quick-start --yes spawns a real team (tmux) + coordinator daemon"]
|
|
123
156
|
fn run_dispatches_quick_start_compiles_spec() {
|
|
124
157
|
// The full quick-start path spawns workers + the coordinator; on a real machine we assert it
|
|
125
|
-
// dispatched to cmd_quick_start (team.spec.yaml compiled under the team dir)
|
|
158
|
+
// dispatched to cmd_quick_start (team.spec.yaml compiled under the team dir). With no positive
|
|
159
|
+
// caller pane, honest-readiness reports leader_receiver_unbound and exits nonzero.
|
|
160
|
+
let _env = EnvGuard::unset(&[
|
|
161
|
+
"TMUX",
|
|
162
|
+
"TMUX_PANE",
|
|
163
|
+
"TEAM_AGENT_ID",
|
|
164
|
+
"TEAM_AGENT_TEAM_ID",
|
|
165
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
166
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
167
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
168
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
169
|
+
]);
|
|
126
170
|
let dir = std::env::temp_dir().join(format!("ta-cli-qs-{}", std::process::id()));
|
|
127
171
|
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
|
128
172
|
std::fs::write(dir.join("TEAM.md"), "---\nname: t\nobjective: o\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
@@ -134,7 +178,11 @@ fn current_uid() -> Option<String> {
|
|
|
134
178
|
let argv = vec!["quick-start".to_string(), dir.to_string_lossy().to_string(), "--yes".to_string()];
|
|
135
179
|
let exit = run(&argv, Path::new("."));
|
|
136
180
|
assert!(dir.join("team.spec.yaml").exists(), "quick-start must compile the spec under the team dir");
|
|
137
|
-
assert_eq!(
|
|
181
|
+
assert_eq!(
|
|
182
|
+
exit,
|
|
183
|
+
ExitCode::Error,
|
|
184
|
+
"quick-start must still dispatch and compile, but an unbound leader receiver is an honest-readiness failure, not ExitCode::Ok"
|
|
185
|
+
);
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
// =========================================================================
|
|
@@ -152,6 +200,7 @@ fn current_uid() -> Option<String> {
|
|
|
152
200
|
let ws = crate::model::paths::team_workspace(&team).unwrap();
|
|
153
201
|
let _guard = TeamSocketGuard { ws };
|
|
154
202
|
let args = QuickStartArgs {
|
|
203
|
+
workspace: crate::model::paths::team_workspace(&team).unwrap(),
|
|
155
204
|
agents_dir: team.clone(),
|
|
156
205
|
name: None,
|
|
157
206
|
team_id: None,
|
|
@@ -177,6 +226,7 @@ fn current_uid() -> Option<String> {
|
|
|
177
226
|
std::fs::write(team.join("TEAM.md"), DELEG_TEAM_MD).unwrap();
|
|
178
227
|
std::fs::write(team.join("agents").join("broken.md"), DELEG_INVALID_ROLE).unwrap();
|
|
179
228
|
let args = QuickStartArgs {
|
|
229
|
+
workspace: crate::model::paths::team_workspace(&team).unwrap(),
|
|
180
230
|
agents_dir: team,
|
|
181
231
|
name: None,
|
|
182
232
|
team_id: None,
|
|
@@ -198,7 +248,13 @@ fn current_uid() -> Option<String> {
|
|
|
198
248
|
#[test]
|
|
199
249
|
fn cli_restart_missing_spec_surfaces_real_teamselect() {
|
|
200
250
|
let ws = deleg_uniq_dir("restart"); // no team.spec.yaml
|
|
201
|
-
let args = RestartArgs {
|
|
251
|
+
let args = RestartArgs {
|
|
252
|
+
workspace: ws,
|
|
253
|
+
team: None,
|
|
254
|
+
allow_fresh: false,
|
|
255
|
+
session_converge_deadline_ms: None,
|
|
256
|
+
json: true,
|
|
257
|
+
};
|
|
202
258
|
let text = outcome_text(cmd_restart(&args));
|
|
203
259
|
assert!(
|
|
204
260
|
text.contains("missing spec for restart"),
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//! 每子命令的 clap-style arg 结构 / 五行 summary 计数桶。
|
|
3
3
|
|
|
4
4
|
use super::*;
|
|
5
|
+
use crate::provider::Provider;
|
|
6
|
+
use crate::transport::PaneId;
|
|
5
7
|
|
|
6
8
|
// =============================================================================
|
|
7
9
|
// ERRORS / EXIT(helpers.py `_emit_cli_error` / `_cli_error_payload`)
|
|
@@ -253,6 +255,7 @@ pub struct InteractionCounts {
|
|
|
253
255
|
/// `quick-start`(`parser.py:105`)。
|
|
254
256
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
255
257
|
pub struct QuickStartArgs {
|
|
258
|
+
pub workspace: PathBuf,
|
|
256
259
|
pub agents_dir: PathBuf,
|
|
257
260
|
pub name: Option<String>,
|
|
258
261
|
pub team_id: Option<String>,
|
|
@@ -362,6 +365,18 @@ pub struct ClaimLeaderArgs {
|
|
|
362
365
|
pub json: bool,
|
|
363
366
|
}
|
|
364
367
|
|
|
368
|
+
/// `attach-leader` public CLI args. `cmd_attach_leader` consumes the typed pane/provider
|
|
369
|
+
/// fields and returns/writes a `leader_receiver` binding through the leader lease port.
|
|
370
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
371
|
+
pub struct AttachLeaderArgs {
|
|
372
|
+
pub workspace: PathBuf,
|
|
373
|
+
pub team: Option<String>,
|
|
374
|
+
pub pane: Option<PaneId>,
|
|
375
|
+
pub provider: Provider,
|
|
376
|
+
pub confirm: bool,
|
|
377
|
+
pub json: bool,
|
|
378
|
+
}
|
|
379
|
+
|
|
365
380
|
/// `identity`(`parser.py:256`)。
|
|
366
381
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
367
382
|
pub struct IdentityArgs {
|
|
@@ -385,6 +400,7 @@ pub struct RestartArgs {
|
|
|
385
400
|
pub workspace: PathBuf,
|
|
386
401
|
pub team: Option<String>,
|
|
387
402
|
pub allow_fresh: bool,
|
|
403
|
+
pub session_converge_deadline_ms: Option<u64>,
|
|
388
404
|
pub json: bool,
|
|
389
405
|
}
|
|
390
406
|
|
|
@@ -600,6 +616,8 @@ pub struct PeekArgs {
|
|
|
600
616
|
pub agent: String,
|
|
601
617
|
pub workspace: PathBuf,
|
|
602
618
|
pub tail: usize,
|
|
619
|
+
pub head: Option<usize>,
|
|
620
|
+
pub search: Option<String>,
|
|
603
621
|
pub allow_raw_screen: bool,
|
|
604
622
|
pub json: bool,
|
|
605
623
|
}
|
|
@@ -158,7 +158,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
158
158
|
let tools = required_tools(&meta, &path)?;
|
|
159
159
|
let prompt_inline = non_empty_trimmed(&body).unwrap_or_else(|| role.clone());
|
|
160
160
|
agent_ids.push(id.clone());
|
|
161
|
-
|
|
161
|
+
let mut agent_items = vec![
|
|
162
162
|
("id", Value::Str(id.clone())),
|
|
163
163
|
("role", Value::Str(role.clone())),
|
|
164
164
|
("provider", Value::Str(provider)),
|
|
@@ -186,7 +186,11 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
186
186
|
),
|
|
187
187
|
]),
|
|
188
188
|
),
|
|
189
|
-
]
|
|
189
|
+
];
|
|
190
|
+
if let Some(profile) = string_field(&meta, "profile") {
|
|
191
|
+
agent_items.push(("profile", Value::Str(profile)));
|
|
192
|
+
}
|
|
193
|
+
agents.push(map(agent_items));
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
let default_assignee = agent_ids.first().cloned().unwrap_or_default();
|
|
@@ -409,10 +413,16 @@ fn resolve_model(role_meta: &Value, team_meta: &Value, provider: &str) -> Value
|
|
|
409
413
|
if let Some(model) = string_field(role_meta, "model") {
|
|
410
414
|
return Value::Str(model);
|
|
411
415
|
}
|
|
412
|
-
provider_model(team_meta, provider)
|
|
416
|
+
if let Some(model) = provider_model(team_meta, provider)
|
|
413
417
|
.or_else(|| string_field(team_meta, "default_model"))
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
{
|
|
419
|
+
return Value::Str(model);
|
|
420
|
+
}
|
|
421
|
+
if role_meta.get("profile").is_some() {
|
|
422
|
+
return Value::Null;
|
|
423
|
+
}
|
|
424
|
+
builtin_provider_model(provider)
|
|
425
|
+
.map(|m| Value::Str(m.to_string()))
|
|
416
426
|
.unwrap_or(Value::Null)
|
|
417
427
|
}
|
|
418
428
|
|