@team-agent/installer 0.5.75 → 0.5.78
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/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +184 -125
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +59 -15
- package/crates/team-agent/src/cli/mod.rs +600 -134
- package/crates/team-agent/src/cli/models.rs +485 -44
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +538 -41
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/leader/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +12 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -82,6 +82,15 @@ Subagents are one-shot, fire-and-forget, and can't talk to each other. Team Agen
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
+
`team-agent status` prints one concise row per registered node with seven fields:
|
|
86
|
+
`name`, `provider`, `runtime_status`, `activity`, `health`, `session_name`, and `tmux_command`.
|
|
87
|
+
`--json` exposes the same projection as structured JSON. Missing or conflicting evidence is
|
|
88
|
+
reported as `unknown`; a non-null `tmux_command` is safe to copy for the observed socket,
|
|
89
|
+
session, window, and pane. `--summary` and `--detail` remain accepted for compatibility and
|
|
90
|
+
do not add diagnostic or history fields. The accepted nodeprobe pairing and
|
|
91
|
+
operator-trusted capability receipt are documented in
|
|
92
|
+
[`docs/status-nodeprobe.md`](docs/status-nodeprobe.md).
|
|
93
|
+
|
|
85
94
|
**Beta.** Used daily in real projects. Rough edges possible in uncommon configurations. Issues and PRs welcome.
|
|
86
95
|
|
|
87
96
|
## License
|
package/README.zh.md
CHANGED
|
@@ -82,6 +82,14 @@ subagent 是一次性的、干完即焚、彼此不通信。Team Agent 的队友
|
|
|
82
82
|
|
|
83
83
|
## 状态
|
|
84
84
|
|
|
85
|
+
`team-agent status` 为每个已登记节点输出七个简要字段:
|
|
86
|
+
`name`、`provider`、`runtime_status`、`activity`、`health`、`session_name`、`tmux_command`。
|
|
87
|
+
`--json` 与人读输出使用同一信息投影。证据缺失或冲突时显示 `unknown`;非空
|
|
88
|
+
`tmux_command` 可复制到已观测的 socket、session、window 和 pane。`--summary` 与
|
|
89
|
+
`--detail` 保留兼容性,但不会增加诊断或历史字段。已验收 nodeprobe 的配对方式、
|
|
90
|
+
operator-trusted 能力 receipt 与 fail-closed 边界见
|
|
91
|
+
[`docs/status-nodeprobe.md`](docs/status-nodeprobe.md)。
|
|
92
|
+
|
|
85
93
|
**Beta。** 已在真实项目里日常使用,小众配置下可能有毛边。欢迎 issue 和 PR。
|
|
86
94
|
|
|
87
95
|
## License
|
|
@@ -183,10 +183,18 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
183
183
|
append_reminder(out, crate::cli::QUICK_START_REMINDER)
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>) {
|
|
187
|
-
|
|
186
|
+
pub(crate) fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>) {
|
|
187
|
+
let existing = value.get("summary").and_then(Value::as_str) == Some("existing runtime");
|
|
188
|
+
if value.get("ok").and_then(Value::as_bool) != Some(true) && !existing {
|
|
188
189
|
return;
|
|
189
190
|
}
|
|
191
|
+
let team = value
|
|
192
|
+
.get("team")
|
|
193
|
+
.and_then(Value::as_str)
|
|
194
|
+
.filter(|team| !team.is_empty())
|
|
195
|
+
.map(str::to_string)
|
|
196
|
+
.or_else(|| team.map(str::to_string));
|
|
197
|
+
let team = team.as_deref();
|
|
190
198
|
let agents = value
|
|
191
199
|
.get("agent_ids")
|
|
192
200
|
.and_then(Value::as_array)
|
|
@@ -204,11 +212,36 @@ fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>)
|
|
|
204
212
|
}
|
|
205
213
|
}
|
|
206
214
|
|
|
215
|
+
#[cfg(test)]
|
|
216
|
+
pub(crate) fn split_shell_argv(command: &str) -> Vec<String> {
|
|
217
|
+
let mut argv = Vec::new();
|
|
218
|
+
let mut current = String::new();
|
|
219
|
+
let mut chars = command.chars().peekable();
|
|
220
|
+
let mut quote: Option<char> = None;
|
|
221
|
+
while let Some(ch) = chars.next() {
|
|
222
|
+
match (quote, ch) {
|
|
223
|
+
(None, '\'') | (None, '"') => quote = Some(ch),
|
|
224
|
+
(Some(q), c) if c == q => quote = None,
|
|
225
|
+
(None, c) if c.is_whitespace() => {
|
|
226
|
+
if !current.is_empty() {
|
|
227
|
+
argv.push(std::mem::take(&mut current));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
(_, c) => current.push(c),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if !current.is_empty() {
|
|
234
|
+
argv.push(current);
|
|
235
|
+
}
|
|
236
|
+
argv
|
|
237
|
+
}
|
|
238
|
+
|
|
207
239
|
pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) -> Option<String> {
|
|
208
240
|
let workspace = workspace.to_str()?;
|
|
209
241
|
let mut command = format!(
|
|
210
|
-
"team-agent send {}
|
|
242
|
+
"team-agent send {} {} --workspace {}",
|
|
211
243
|
shell_quote(agent),
|
|
244
|
+
shell_quote("MESSAGE"),
|
|
212
245
|
shell_quote(workspace)
|
|
213
246
|
);
|
|
214
247
|
if let Some(team) = team {
|
|
@@ -280,8 +313,8 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
|
|
|
280
313
|
}
|
|
281
314
|
}
|
|
282
315
|
|
|
283
|
-
/// `cmd_status`(`commands.py:90`)
|
|
284
|
-
/// `--json
|
|
316
|
+
/// `cmd_status`(`commands.py:90`)。CLI status 统一为只读七字段 brief;
|
|
317
|
+
/// `--json` 与人读路径共享 nodeprobe-backed projection;`--summary`/`--detail` 保留参数兼容性但不暴露诊断。
|
|
285
318
|
#[cfg(test)]
|
|
286
319
|
pub(crate) fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
|
|
287
320
|
cmd_status_for_team(args, args.team.as_deref())
|
|
@@ -300,123 +333,50 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
300
333
|
}
|
|
301
334
|
// S4QR-001 (0.4.8): selected-team ambiguity gate. status is a selected-team
|
|
302
335
|
// command: when the workspace has 2+ alive teams and no --team was passed,
|
|
303
|
-
// refuse instead of silently defaulting to the active team. Uses the
|
|
304
|
-
// CommandScope
|
|
305
|
-
// (cli/emit.rs:964-979) so the destructive-command ambiguity gate and the
|
|
306
|
-
// selected-team-read ambiguity gate share a single source of truth.
|
|
336
|
+
// refuse instead of silently defaulting to the active team. Uses the
|
|
337
|
+
// read-only CommandScope helper so the gate cannot migrate/write state.
|
|
307
338
|
if team.is_none() {
|
|
308
|
-
let scope = crate::state::paths::CommandScope::
|
|
339
|
+
let scope = crate::state::paths::CommandScope::resolve_readonly(&args.workspace, None);
|
|
309
340
|
if scope.is_ambiguous() {
|
|
310
341
|
let candidates: Vec<String> = scope.candidates().to_vec();
|
|
311
342
|
let message = format!(
|
|
312
343
|
"status: workspace has multiple alive teams ({}); pass `--team <key>` to choose one",
|
|
313
344
|
candidates.join(", ")
|
|
314
345
|
);
|
|
315
|
-
if args.json {
|
|
316
|
-
let payload = serde_json::json!({
|
|
317
|
-
"ok": false,
|
|
318
|
-
"status": "refused",
|
|
319
|
-
"reason": "team_target_ambiguous",
|
|
320
|
-
"candidates": candidates,
|
|
321
|
-
"message": message,
|
|
322
|
-
});
|
|
323
|
-
return Ok(CmdResult::from_json(payload, args.json));
|
|
324
|
-
}
|
|
325
346
|
return Err(CliError::Usage(message));
|
|
326
347
|
}
|
|
327
348
|
}
|
|
328
|
-
let selected =
|
|
349
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
329
350
|
&args.workspace,
|
|
330
351
|
team,
|
|
331
352
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return
|
|
336
|
-
status_selector_error_payload(&error.to_string(), &args.workspace),
|
|
337
|
-
args.json,
|
|
338
|
-
));
|
|
353
|
+
)?;
|
|
354
|
+
if let Some(agent) = args.agent.as_deref() {
|
|
355
|
+
if !status_port::registered_agent_exists(&selected.state, agent) {
|
|
356
|
+
return Err(CliError::Runtime(format!("unknown agent id: {agent}")));
|
|
339
357
|
}
|
|
340
|
-
};
|
|
341
|
-
if args.summary {
|
|
342
|
-
// 0.4.x compact slimming: `--summary` renders the five-line triage
|
|
343
|
-
// text via `format_status_summary`, which needs the full payload
|
|
344
|
-
// fields (coordinator, leader_receiver, agent_health, queued_messages,
|
|
345
|
-
// latest_results). The slim compact projection drops those, so the
|
|
346
|
-
// summary path must request the full payload (compact=false).
|
|
347
|
-
let value = status_port::status_scoped(
|
|
348
|
-
&selected.run_workspace,
|
|
349
|
-
&selected.state,
|
|
350
|
-
Some(&selected.team_key),
|
|
351
|
-
false,
|
|
352
|
-
false,
|
|
353
|
-
)?;
|
|
354
|
-
return Ok(CmdResult::human(append_reminder(
|
|
355
|
-
format_status_summary(&value),
|
|
356
|
-
crate::cli::STATUS_REMINDER,
|
|
357
|
-
)));
|
|
358
358
|
}
|
|
359
|
+
// Status brief is deliberately independent of the legacy RuntimeSnapshot:
|
|
360
|
+
// it performs one bounded nodeprobe sample and exposes exactly seven
|
|
361
|
+
// fields. `--summary` and `--detail` remain parser-compatible but do not
|
|
362
|
+
// re-enable history, runtime diagnostics, or reminder text.
|
|
359
363
|
if args.json {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
return Ok(CmdResult::from_json(
|
|
365
|
+
status_port::status_brief_scoped(
|
|
366
|
+
&selected.run_workspace,
|
|
367
|
+
&selected.state,
|
|
368
|
+
args.agent.as_deref(),
|
|
369
|
+
),
|
|
370
|
+
true,
|
|
371
|
+
));
|
|
368
372
|
}
|
|
369
|
-
|
|
373
|
+
Ok(CmdResult::human(status_port::format_status_brief(
|
|
370
374
|
&selected.run_workspace,
|
|
371
375
|
&selected.state,
|
|
372
|
-
Some(&selected.team_key),
|
|
373
376
|
args.agent.as_deref(),
|
|
374
|
-
)?;
|
|
375
|
-
if args.detail {
|
|
376
|
-
let value = status_port::status_scoped(
|
|
377
|
-
&selected.run_workspace,
|
|
378
|
-
&selected.state,
|
|
379
|
-
Some(&selected.team_key),
|
|
380
|
-
false,
|
|
381
|
-
true,
|
|
382
|
-
)?;
|
|
383
|
-
if let Some(hint) = value
|
|
384
|
-
.pointer("/runtime/hint")
|
|
385
|
-
.and_then(serde_json::Value::as_str)
|
|
386
|
-
.filter(|hint| !hint.is_empty())
|
|
387
|
-
{
|
|
388
|
-
if !text.is_empty() {
|
|
389
|
-
text.push('\n');
|
|
390
|
-
}
|
|
391
|
-
text.push_str(hint);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
Ok(CmdResult::human(append_reminder(
|
|
395
|
-
text,
|
|
396
|
-
crate::cli::STATUS_REMINDER,
|
|
397
377
|
)))
|
|
398
378
|
}
|
|
399
379
|
|
|
400
|
-
fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
|
|
401
|
-
let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
|
|
402
|
-
let log_path = std::env::temp_dir()
|
|
403
|
-
.join("team-agent")
|
|
404
|
-
.join("cli-errors")
|
|
405
|
-
.join(format!("status-{stamp}.log"));
|
|
406
|
-
if let Some(parent) = log_path.parent() {
|
|
407
|
-
let _ = std::fs::create_dir_all(parent);
|
|
408
|
-
}
|
|
409
|
-
let _ = std::fs::write(&log_path, format!("{error}\n"));
|
|
410
|
-
json!({
|
|
411
|
-
"ok": false,
|
|
412
|
-
"error": error,
|
|
413
|
-
"action": "run `team-agent doctor` or inspect the log path shown here",
|
|
414
|
-
"log": log_path.to_string_lossy().to_string(),
|
|
415
|
-
"workspace": workspace.to_string_lossy().to_string(),
|
|
416
|
-
"reminder": crate::cli::STATUS_REMINDER,
|
|
417
|
-
})
|
|
418
|
-
}
|
|
419
|
-
|
|
420
380
|
/// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
|
|
421
381
|
/// 返回 [`CmdResult::none`](不经 emit)。
|
|
422
382
|
pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
|
|
@@ -601,6 +561,42 @@ pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliErr
|
|
|
601
561
|
|
|
602
562
|
/// `cmd_diagnose`(`parser.py:298`)。
|
|
603
563
|
pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
564
|
+
let team_hint = args
|
|
565
|
+
.team
|
|
566
|
+
.as_deref()
|
|
567
|
+
.filter(|team| !team.is_empty())
|
|
568
|
+
.unwrap_or("current");
|
|
569
|
+
// Check the caller-supplied workspace before RuntimeOnly synthesis or
|
|
570
|
+
// transport setup, which can create `.team` paths and make an empty
|
|
571
|
+
// directory look like an unbound team.
|
|
572
|
+
if !crate::cli::diagnose::workspace_has_existing_team_runtime(&args.workspace, team_hint) {
|
|
573
|
+
let (issues, suggested_repairs) =
|
|
574
|
+
crate::cli::diagnose::missing_team_runtime_issues_and_repairs(team_hint);
|
|
575
|
+
let event_log = args
|
|
576
|
+
.workspace
|
|
577
|
+
.join(".team")
|
|
578
|
+
.join("logs")
|
|
579
|
+
.join("events.jsonl");
|
|
580
|
+
return Ok(CmdResult::from_json(
|
|
581
|
+
json!({
|
|
582
|
+
"event_log": event_log.to_string_lossy().to_string(),
|
|
583
|
+
"issues": issues,
|
|
584
|
+
"ok": false,
|
|
585
|
+
"providers": provider_doctor_checks(),
|
|
586
|
+
"runtime": {
|
|
587
|
+
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
588
|
+
"team_key": team_hint,
|
|
589
|
+
"session_name": Value::Null,
|
|
590
|
+
"leader_receiver": Value::Null,
|
|
591
|
+
"agent_count": 0,
|
|
592
|
+
"message_count": 0,
|
|
593
|
+
"result_count": 0,
|
|
594
|
+
},
|
|
595
|
+
"suggested_repairs": suggested_repairs,
|
|
596
|
+
}),
|
|
597
|
+
args.json,
|
|
598
|
+
));
|
|
599
|
+
}
|
|
604
600
|
let selected = crate::state::selector::resolve_active_team(
|
|
605
601
|
&args.workspace,
|
|
606
602
|
args.team.as_deref(),
|
|
@@ -629,7 +625,12 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
|
629
625
|
)),
|
|
630
626
|
};
|
|
631
627
|
let (issues, suggested_repairs) =
|
|
632
|
-
diagnose_runtime_for_workspace(
|
|
628
|
+
diagnose_runtime_for_workspace(
|
|
629
|
+
&selected.run_workspace,
|
|
630
|
+
&state,
|
|
631
|
+
backend.as_ref(),
|
|
632
|
+
Some(selected.team_key.as_str()),
|
|
633
|
+
);
|
|
633
634
|
let ok = issues.as_array().is_some_and(Vec::is_empty);
|
|
634
635
|
Ok(CmdResult::from_json(
|
|
635
636
|
json!({
|
|
@@ -1420,17 +1421,24 @@ pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
|
|
|
1420
1421
|
|
|
1421
1422
|
/// `cmd_start_agent`(`commands.py:348`)。
|
|
1422
1423
|
pub fn cmd_start_agent(args: &StartAgentArgs) -> Result<CmdResult, CliError> {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1424
|
+
let mut value = lifecycle_port::start_agent(
|
|
1425
|
+
&args.workspace,
|
|
1426
|
+
&args.agent,
|
|
1427
|
+
args.force,
|
|
1428
|
+
!args.no_display,
|
|
1429
|
+
args.allow_fresh,
|
|
1430
|
+
args.team.as_deref(),
|
|
1431
|
+
)?;
|
|
1432
|
+
if value.get("agent_ids").is_none() {
|
|
1433
|
+
if let Some(object) = value.as_object_mut() {
|
|
1434
|
+
object.insert(
|
|
1435
|
+
"agent_ids".to_string(),
|
|
1436
|
+
json!([args.agent.as_str()]),
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
|
|
1441
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
1434
1442
|
}
|
|
1435
1443
|
|
|
1436
1444
|
/// `cmd_stop_agent`(`commands.py:359`)。
|
|
@@ -1457,17 +1465,24 @@ pub fn cmd_reset_agent(args: &ResetAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1457
1465
|
|
|
1458
1466
|
/// `cmd_add_agent`(`commands.py:373`)。
|
|
1459
1467
|
pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1468
|
+
let mut value = lifecycle_port::add_agent(
|
|
1469
|
+
&args.workspace,
|
|
1470
|
+
&args.agent,
|
|
1471
|
+
&args.role_file,
|
|
1472
|
+
!args.no_display,
|
|
1473
|
+
args.team.as_deref(),
|
|
1474
|
+
args.force,
|
|
1475
|
+
)?;
|
|
1476
|
+
if value.get("agent_ids").is_none() {
|
|
1477
|
+
if let Some(object) = value.as_object_mut() {
|
|
1478
|
+
object.insert(
|
|
1479
|
+
"agent_ids".to_string(),
|
|
1480
|
+
json!([args.agent.as_str()]),
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
|
|
1485
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
1471
1486
|
}
|
|
1472
1487
|
|
|
1473
1488
|
/// `cmd_fork_agent`(`commands.py:383`)。
|
|
@@ -1613,8 +1628,10 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
|
|
|
1613
1628
|
mod tests {
|
|
1614
1629
|
#![allow(clippy::unwrap_used)]
|
|
1615
1630
|
|
|
1616
|
-
use super::{
|
|
1617
|
-
|
|
1631
|
+
use super::{
|
|
1632
|
+
agent_pane_id, append_send_guidance, quickstart_human, send_command, split_shell_argv,
|
|
1633
|
+
};
|
|
1634
|
+
use serde_json::{json, Value};
|
|
1618
1635
|
use std::path::{Path, PathBuf};
|
|
1619
1636
|
|
|
1620
1637
|
// E13:happy 人类输出必须带 attach 块(此前 else 分支只打 summary 丢 attach_commands)。
|
|
@@ -1658,13 +1675,37 @@ mod tests {
|
|
|
1658
1675
|
);
|
|
1659
1676
|
}
|
|
1660
1677
|
|
|
1678
|
+
#[test]
|
|
1679
|
+
fn send_command_message_is_one_argv_token() {
|
|
1680
|
+
let command = send_command(
|
|
1681
|
+
"worker name; echo unsafe",
|
|
1682
|
+
Path::new("/tmp/my workspace"),
|
|
1683
|
+
Some("team-a"),
|
|
1684
|
+
)
|
|
1685
|
+
.unwrap();
|
|
1686
|
+
let argv = split_shell_argv(&command);
|
|
1687
|
+
assert_eq!(
|
|
1688
|
+
argv,
|
|
1689
|
+
[
|
|
1690
|
+
"team-agent",
|
|
1691
|
+
"send",
|
|
1692
|
+
"worker name; echo unsafe",
|
|
1693
|
+
"MESSAGE",
|
|
1694
|
+
"--workspace",
|
|
1695
|
+
"/tmp/my workspace",
|
|
1696
|
+
"--team",
|
|
1697
|
+
"team-a"
|
|
1698
|
+
]
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1661
1702
|
#[test]
|
|
1662
1703
|
fn send_guidance_is_copyable_and_preserves_explicit_scope() {
|
|
1663
1704
|
let command = send_command("worker name; echo unsafe", Path::new("/tmp/my workspace"), Some("team-a"))
|
|
1664
1705
|
.unwrap();
|
|
1665
1706
|
assert_eq!(
|
|
1666
1707
|
command,
|
|
1667
|
-
"team-agent send 'worker name; echo unsafe'
|
|
1708
|
+
"team-agent send 'worker name; echo unsafe' MESSAGE --workspace '/tmp/my workspace' --team team-a"
|
|
1668
1709
|
);
|
|
1669
1710
|
}
|
|
1670
1711
|
|
|
@@ -1698,6 +1739,24 @@ mod tests {
|
|
|
1698
1739
|
assert!(value.get("send_commands").is_none());
|
|
1699
1740
|
}
|
|
1700
1741
|
|
|
1742
|
+
#[test]
|
|
1743
|
+
fn existing_runtime_gets_canonical_send_guidance() {
|
|
1744
|
+
let mut value = json!({
|
|
1745
|
+
"ok": false,
|
|
1746
|
+
"summary": "existing runtime",
|
|
1747
|
+
"agent_ids": ["worker"]
|
|
1748
|
+
});
|
|
1749
|
+
append_send_guidance(&mut value, Path::new("/tmp/ws"), Some("team-a"));
|
|
1750
|
+
let command = value
|
|
1751
|
+
.pointer("/send_commands/0")
|
|
1752
|
+
.and_then(Value::as_str)
|
|
1753
|
+
.unwrap();
|
|
1754
|
+
assert!(command.contains("send worker"));
|
|
1755
|
+
assert!(command.contains("--team team-a"));
|
|
1756
|
+
let parts: Vec<&str> = command.split_whitespace().collect();
|
|
1757
|
+
assert_eq!(parts[3], "MESSAGE");
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1701
1760
|
#[test]
|
|
1702
1761
|
fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
|
|
1703
1762
|
let state = json!({
|