@team-agent/installer 0.5.111 → 0.5.113
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 +93 -779
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +158 -373
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +8 -15
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +29 -32
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/tests/base.rs +6 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +12 -116
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +0 -9
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/start.rs +5 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +21 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
- package/crates/team-agent/src/lifecycle/launch.rs +2 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -144,155 +144,12 @@ fn seed_leader_registry_entry(ws: &std::path::Path) {
|
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
//
|
|
148
|
-
// WAVE-2 NON-SUB CHECKPOINT — missing CLI subcommands (ABSENT from cli/emit.rs dispatch).
|
|
149
|
-
// emit.rs `dispatch` has NO arm for sessions, peek, collect, e2e, diagnose,
|
|
150
|
-
// preflight, wait-ready -> they fall to `_ => Ok(ExitCode::Error)`. These REDs
|
|
151
|
-
// assert the dispatch ROUTES each subcommand: `run([sub,...]) == ExitCode::Ok` for a golden
|
|
152
|
-
// EXIT-0 scenario (today unrouted -> ExitCode::Error -> RED; green once the porter adds the
|
|
153
|
-
// dispatch arm + handler). Golden exit codes + JSON shapes probed via `python3 -m team_agent <sub>`.
|
|
154
|
-
//
|
|
155
|
-
// OBSERVABILITY NOTE: `run()` exposes only ExitCode (Ok=0/Error=1); it prints via println!, which
|
|
156
|
-
// libtest intercepts (thread-local capture), so an fd-level stdout byte-capture is unreliable
|
|
157
|
-
// under `cargo test`. The exact golden --json byte-shape is therefore LOCKED in each doc-comment
|
|
158
|
-
// as the porter's parity obligation; the in-process assertion is the routing (exit code). A
|
|
159
|
-
// follow-up can byte-lock output once each handler exists as a callable `cmd_*`/`*_port` symbol.
|
|
160
|
-
//
|
|
161
|
-
// Only golden-EXIT-0, tmux-SAFE scenarios make a clean RED (an exit-1 scenario's Error is
|
|
162
|
-
// indistinguishable from the unknown-subcommand Error -> false-green, forbidden). preflight/
|
|
163
|
-
// wait-ready/e2e/peek cannot reach golden-exit-0 on CI without real tmux/providers/a live team,
|
|
164
|
-
// so they are #[ignore] real-effect seams (documented shape), NOT false-green exit-1 asserts.
|
|
165
|
-
// =========================================================================
|
|
147
|
+
// Retained doctor dispatch and shared status-readiness contracts.
|
|
166
148
|
|
|
167
149
|
fn cli_argv(items: &[&str]) -> Vec<String> {
|
|
168
150
|
items.iter().map(|s| (*s).to_string()).collect()
|
|
169
151
|
}
|
|
170
152
|
|
|
171
|
-
/// The exact golden fake spec (cli/e2e.py `_fake_spec`, dumped via simple_yaml) with the literal
|
|
172
|
-
/// `/WS` placeholder substituted to the real workspace — the minimal VALID team.spec.yaml that
|
|
173
|
-
/// `collect` accepts. (load_spec rejects partial specs: requires
|
|
174
|
-
/// communication/context/leader/routing/runtime/tasks.)
|
|
175
|
-
const FAKE_SPEC_YAML: &str = r#"version: 1
|
|
176
|
-
team:
|
|
177
|
-
name: "fake-e2e"
|
|
178
|
-
mode: "supervisor_worker"
|
|
179
|
-
objective: "Exercise fake provider orchestration."
|
|
180
|
-
workspace: "/WS"
|
|
181
|
-
leader:
|
|
182
|
-
id: "leader"
|
|
183
|
-
role: "leader"
|
|
184
|
-
provider: "fake"
|
|
185
|
-
model: null
|
|
186
|
-
tools:
|
|
187
|
-
- "fs_read"
|
|
188
|
-
- "fs_list"
|
|
189
|
-
- "mcp_team"
|
|
190
|
-
context_policy:
|
|
191
|
-
keep_user_thread: true
|
|
192
|
-
receive_worker_outputs: "structured_only"
|
|
193
|
-
max_worker_result_tokens: 2000
|
|
194
|
-
agents:
|
|
195
|
-
- id: "fake_impl"
|
|
196
|
-
role: "implementation_engineer"
|
|
197
|
-
provider: "fake"
|
|
198
|
-
model: null
|
|
199
|
-
working_directory: "/WS"
|
|
200
|
-
system_prompt:
|
|
201
|
-
inline: "Handle fake implementation tasks."
|
|
202
|
-
file: null
|
|
203
|
-
tools:
|
|
204
|
-
- "fs_read"
|
|
205
|
-
- "fs_write"
|
|
206
|
-
- "fs_list"
|
|
207
|
-
- "execute_bash"
|
|
208
|
-
- "git_diff"
|
|
209
|
-
- "mcp_team"
|
|
210
|
-
- "provider_builtin"
|
|
211
|
-
permission_mode: "restricted"
|
|
212
|
-
preferred_for:
|
|
213
|
-
- "implementation"
|
|
214
|
-
avoid_for: []
|
|
215
|
-
output_contract:
|
|
216
|
-
format: "result_envelope_v1"
|
|
217
|
-
required_fields:
|
|
218
|
-
- "task_id"
|
|
219
|
-
- "status"
|
|
220
|
-
- "summary"
|
|
221
|
-
- "artifacts"
|
|
222
|
-
routing:
|
|
223
|
-
default_assignee: "leader"
|
|
224
|
-
rules:
|
|
225
|
-
- id: "implementation-to-fake"
|
|
226
|
-
match:
|
|
227
|
-
type:
|
|
228
|
-
- "implementation"
|
|
229
|
-
assign_to: "fake_impl"
|
|
230
|
-
priority: 10
|
|
231
|
-
communication:
|
|
232
|
-
protocol: "mcp_inbox"
|
|
233
|
-
topology: "leader_centered"
|
|
234
|
-
worker_to_worker: true
|
|
235
|
-
ack_timeout_sec: 2
|
|
236
|
-
result_format: "result_envelope_v1"
|
|
237
|
-
message_store:
|
|
238
|
-
sqlite: ".team/runtime/team.db"
|
|
239
|
-
mirror_files: ".team/messages"
|
|
240
|
-
runtime:
|
|
241
|
-
backend: "tmux"
|
|
242
|
-
session_name: "team-agent-fake-e2e"
|
|
243
|
-
auto_launch: true
|
|
244
|
-
require_user_approval_before_launch: false
|
|
245
|
-
max_active_agents: 1
|
|
246
|
-
startup_order:
|
|
247
|
-
- "fake_impl"
|
|
248
|
-
context:
|
|
249
|
-
state_file: "team_state.md"
|
|
250
|
-
artifact_dir: ".team/artifacts"
|
|
251
|
-
log_dir: ".team/logs"
|
|
252
|
-
summarization:
|
|
253
|
-
worker_full_logs: "retain_outside_leader_context"
|
|
254
|
-
state_update: "after_each_result"
|
|
255
|
-
tasks:
|
|
256
|
-
- id: "task_impl"
|
|
257
|
-
title: "Fake implementation"
|
|
258
|
-
type: "implementation"
|
|
259
|
-
assignee: null
|
|
260
|
-
deps: []
|
|
261
|
-
acceptance:
|
|
262
|
-
- "fake result collected"
|
|
263
|
-
status: "pending"
|
|
264
|
-
requires_tools:
|
|
265
|
-
- "fs_write"
|
|
266
|
-
- "execute_bash"
|
|
267
|
-
files:
|
|
268
|
-
- "src/example.py"
|
|
269
|
-
risk: "low"
|
|
270
|
-
"#;
|
|
271
|
-
|
|
272
|
-
fn seed_team_spec(ws: &std::path::Path) {
|
|
273
|
-
let spec = FAKE_SPEC_YAML.replace("/WS", &ws.to_string_lossy());
|
|
274
|
-
std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ── sessions ── golden cli/parser.py:230 `cmd_sessions` -> runtime.sessions(ws). EXIT 0.
|
|
278
|
-
// `team-agent sessions --workspace <ws> --json` on an empty ws ->
|
|
279
|
-
// {"ok":true,"sessions":[],"workspace":"<ws>"} (--json sort_keys). RED: unrouted -> Error.
|
|
280
|
-
#[test]
|
|
281
|
-
fn dispatch_routes_sessions_subcommand() {
|
|
282
|
-
let ws = tmp_workspace();
|
|
283
|
-
let code = run(
|
|
284
|
-
&cli_argv(&["sessions", "--workspace", &ws.to_string_lossy(), "--json"]),
|
|
285
|
-
&ws,
|
|
286
|
-
);
|
|
287
|
-
assert_eq!(
|
|
288
|
-
code,
|
|
289
|
-
ExitCode::Ok,
|
|
290
|
-
"`sessions` must ROUTE to cmd_sessions (golden parser.py:230, exit 0 {{ok,sessions,workspace}}); \
|
|
291
|
-
today it falls to the unknown-subcommand arm (emit.rs:77) -> Error"
|
|
292
|
-
);
|
|
293
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
153
|
// ── diagnose ── attached is host-registry authority, not state.json self-sign.
|
|
297
154
|
// Seed leader_receiver=attached + a ~/.team-agent/leaders/ row for this workspace
|
|
298
155
|
// + NO session_name + NO agents -> issues=[] -> EXIT 0.
|
|
@@ -389,120 +246,6 @@ fn dispatch_routes_diagnose_self_signed_attached_without_registry_is_not_ok() {
|
|
|
389
246
|
let _ = std::fs::remove_dir_all(&ws);
|
|
390
247
|
}
|
|
391
248
|
|
|
392
|
-
// ── preflight (#[ignore] real-machine) ── golden parser.py:160 `cmd_preflight` -> runtime.preflight
|
|
393
|
-
// (Path(args.team)). Validates a team role-doc dir: checks compile(TEAM.md)/tmux/ghostty/rust_core/
|
|
394
|
-
// profile_dir; golden -> {"blockers":[...],"checks":[...],"details_log":...,"next_actions":[...],
|
|
395
|
-
// "ok":bool,"summary":...}. Reaches ok:true (exit 0) ONLY with a valid TEAM.md + ghostty installed,
|
|
396
|
-
// which CI cannot supply (ghostty absent -> blocker) -> not a clean in-process exit-0 RED.
|
|
397
|
-
#[test]
|
|
398
|
-
#[ignore = "real-machine: `preflight --team <dir>` needs a valid TEAM.md role-doc dir + ghostty to \
|
|
399
|
-
reach ok:true; on CI it golden-exits 1 (compile/ghostty blockers) which is \
|
|
400
|
-
indistinguishable from the unknown-subcommand Error (false-green). Routes to cmd_preflight \
|
|
401
|
-
(parser.py:160); shape {blockers,checks,details_log,next_actions,ok,summary}"]
|
|
402
|
-
fn dispatch_routes_preflight_real_machine() {
|
|
403
|
-
let ws = tmp_workspace();
|
|
404
|
-
let code = run(
|
|
405
|
-
&cli_argv(&["preflight", "--team", &ws.to_string_lossy(), "--json"]),
|
|
406
|
-
&ws,
|
|
407
|
-
);
|
|
408
|
-
assert_eq!(
|
|
409
|
-
code,
|
|
410
|
-
ExitCode::Ok,
|
|
411
|
-
"preflight must ROUTE + exit 0 on a valid team dir"
|
|
412
|
-
);
|
|
413
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// ── wait-ready (#[ignore] real-machine) ── golden parser.py:171 `cmd_wait_ready` -> runtime.wait_ready
|
|
417
|
-
// (ws, timeout). Polls worker readiness; golden -> {"details_log":...,"next_actions":[...],"ok":bool,
|
|
418
|
-
// "readiness":{cli_prompt_ready,mcp_ready,process_started,task_prompt_delivered},"summary":...}.
|
|
419
|
-
// ok:true (exit 0) needs a LIVE ready team; CI has none -> golden-exits 1 (false-green vs unknown).
|
|
420
|
-
#[test]
|
|
421
|
-
#[ignore = "real-machine: `wait-ready` polls a LIVE team's readiness; with no workers golden-exits 1 \
|
|
422
|
-
(false-green vs unknown-subcommand Error). Routes to cmd_wait_ready (parser.py:171); shape \
|
|
423
|
-
{details_log,next_actions,ok,readiness{cli_prompt_ready,mcp_ready,process_started,task_prompt_delivered},summary}"]
|
|
424
|
-
fn dispatch_routes_wait_ready_real_machine() {
|
|
425
|
-
let ws = tmp_workspace();
|
|
426
|
-
let code = run(
|
|
427
|
-
&cli_argv(&[
|
|
428
|
-
"wait-ready",
|
|
429
|
-
"--workspace",
|
|
430
|
-
&ws.to_string_lossy(),
|
|
431
|
-
"--timeout",
|
|
432
|
-
"1",
|
|
433
|
-
"--json",
|
|
434
|
-
]),
|
|
435
|
-
&ws,
|
|
436
|
-
);
|
|
437
|
-
assert_eq!(
|
|
438
|
-
code,
|
|
439
|
-
ExitCode::Ok,
|
|
440
|
-
"wait-ready must ROUTE + exit 0 once the team is ready"
|
|
441
|
-
);
|
|
442
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// ── e2e --providers fake (#[ignore] real-machine) ── golden parser.py:449 `cmd_e2e` (cli/e2e.py:12).
|
|
446
|
-
// `--providers fake` runs a REAL end-to-end: runtime.launch(spec, auto_approve=True) (spawns a tmux
|
|
447
|
-
// team) -> send_message -> sleep -> collect -> shutdown. Result envelope (e2e.py:16,48-56):
|
|
448
|
-
// {"workspace":str,"providers":{"fake":{"ok":bool,"launch":{...},"send":{...},"collect":{...},
|
|
449
|
-
// "shutdown":{...}}},"ok":bool}. Real tmux spawn -> #[ignore] (NOT runnable in-process).
|
|
450
|
-
#[test]
|
|
451
|
-
#[ignore = "real-machine: `e2e --providers fake` calls runtime.launch -> spawns a REAL tmux team \
|
|
452
|
-
(send/collect/shutdown). Routes to cmd_e2e (parser.py:449 / e2e.py:12); envelope \
|
|
453
|
-
{workspace,providers:{fake:{ok,launch,send,collect,shutdown}},ok}"]
|
|
454
|
-
fn dispatch_routes_e2e_fake_real_machine() {
|
|
455
|
-
let ws = tmp_workspace();
|
|
456
|
-
let code = run(
|
|
457
|
-
&cli_argv(&[
|
|
458
|
-
"e2e",
|
|
459
|
-
"--providers",
|
|
460
|
-
"fake",
|
|
461
|
-
"--workspace",
|
|
462
|
-
&ws.to_string_lossy(),
|
|
463
|
-
"--json",
|
|
464
|
-
]),
|
|
465
|
-
&ws,
|
|
466
|
-
);
|
|
467
|
-
assert_eq!(
|
|
468
|
-
code,
|
|
469
|
-
ExitCode::Ok,
|
|
470
|
-
"e2e --providers fake must ROUTE + run the fake end-to-end (ok:true)"
|
|
471
|
-
);
|
|
472
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ── peek (#[ignore] real-machine) ── golden parser.py:201 `cmd_peek` (commands.py:118). Requires
|
|
476
|
-
// `--allow-raw-screen` (else TeamAgentError) + a mutually-exclusive --head/--tail/--search; then
|
|
477
|
-
// runtime.peek captures a LIVE tmux pane (tmux capture-pane). Real tmux -> #[ignore]. Routes to
|
|
478
|
-
// cmd_peek; --json returns the peek dict (text + capture metadata).
|
|
479
|
-
#[test]
|
|
480
|
-
#[ignore = "real-machine: `peek <agent> --tail N --allow-raw-screen` captures a LIVE tmux pane \
|
|
481
|
-
(tmux capture-pane); needs a running worker pane. Routes to cmd_peek (parser.py:201); \
|
|
482
|
-
--json returns the peek dict (raw screen text + metadata)"]
|
|
483
|
-
fn dispatch_routes_peek_real_machine() {
|
|
484
|
-
let ws = tmp_workspace();
|
|
485
|
-
let code = run(
|
|
486
|
-
&cli_argv(&[
|
|
487
|
-
"peek",
|
|
488
|
-
"fake_impl",
|
|
489
|
-
"--workspace",
|
|
490
|
-
&ws.to_string_lossy(),
|
|
491
|
-
"--tail",
|
|
492
|
-
"20",
|
|
493
|
-
"--allow-raw-screen",
|
|
494
|
-
"--json",
|
|
495
|
-
]),
|
|
496
|
-
&ws,
|
|
497
|
-
);
|
|
498
|
-
assert_eq!(
|
|
499
|
-
code,
|
|
500
|
-
ExitCode::Ok,
|
|
501
|
-
"peek must ROUTE + capture the live pane (real machine)"
|
|
502
|
-
);
|
|
503
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
249
|
// CONTRACT (shared-root, real-machine-driven; golden = correct-behavior baseline): wait_readiness
|
|
507
250
|
// derives cli_prompt_ready from the LIFECYCLE status — an alive worker (status="running") IS
|
|
508
251
|
// cli_prompt_ready (golden quick_start.py:173 `status ∈ {running, busy}`). Rust (diagnose.rs:280)
|
|
@@ -129,11 +129,9 @@ mod leader_watch;
|
|
|
129
129
|
mod main_preserved;
|
|
130
130
|
mod missing_subcommands;
|
|
131
131
|
mod named_address;
|
|
132
|
-
mod peer_allow;
|
|
133
132
|
mod run_delegation;
|
|
134
133
|
mod shutdown_kill_plan;
|
|
135
134
|
mod status_brief;
|
|
136
135
|
mod status_send;
|
|
137
136
|
mod verb_install_skill;
|
|
138
137
|
mod verb_profile;
|
|
139
|
-
mod verb_validate;
|
|
@@ -377,21 +377,21 @@ fn send_to_name_mutual_exclusion() {
|
|
|
377
377
|
let args = named_send_args(&cwd, Some("team-a/qa"), Some("%1"), None, &["hello"]);
|
|
378
378
|
let err = cmd_send(&args).unwrap_err();
|
|
379
379
|
assert!(
|
|
380
|
-
matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("
|
|
380
|
+
matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("cannot be combined")),
|
|
381
381
|
"expected --to-name mutual exclusion, got {err:?}"
|
|
382
382
|
);
|
|
383
383
|
|
|
384
384
|
let args = named_send_args(&cwd, Some("team-a/qa"), None, Some("worker"), &["hello"]);
|
|
385
385
|
let err = cmd_send(&args).unwrap_err();
|
|
386
386
|
assert!(
|
|
387
|
-
matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("
|
|
387
|
+
matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("cannot be combined")),
|
|
388
388
|
"expected --to-name/--to mutual exclusion, got {err:?}"
|
|
389
389
|
);
|
|
390
390
|
|
|
391
391
|
let args = named_send_args(&cwd, Some("team-a/qa"), None, None, &[]);
|
|
392
392
|
let err = cmd_send(&args).unwrap_err();
|
|
393
393
|
assert!(
|
|
394
|
-
matches!(err, CliError::Usage(ref message) if message == "
|
|
394
|
+
matches!(err, CliError::Usage(ref message) if message == "Provide a task message; --to-name cannot send an empty message"),
|
|
395
395
|
"expected empty-message usage error, got {err:?}"
|
|
396
396
|
);
|
|
397
397
|
let _ = std::fs::remove_dir_all(&cwd);
|
|
@@ -641,11 +641,11 @@ fn cmd_send_joins_message_with_single_space() {
|
|
|
641
641
|
// 应立即失败以强制重新审视,而不是悄悄跑一条从未验过的分支。
|
|
642
642
|
assert!(
|
|
643
643
|
!delivered,
|
|
644
|
-
"hermetic seed
|
|
644
|
+
"hermetic seed must not produce delivered=true; the fixture assumption changed"
|
|
645
645
|
);
|
|
646
646
|
assert_eq!(
|
|
647
647
|
v.get("reminder").and_then(|reminder| reminder.as_str()),
|
|
648
|
-
Some("
|
|
648
|
+
Some("Task accepted, but delivery is unconfirmed. Check team-agent status/doctor; wait for an actual reply without repeated sends.")
|
|
649
649
|
);
|
|
650
650
|
}
|
|
651
651
|
}
|
|
@@ -665,15 +665,15 @@ fn cmd_send_default_human_output_is_brief_without_false_delivered() {
|
|
|
665
665
|
"send must separate outcome and next action: {text}"
|
|
666
666
|
);
|
|
667
667
|
assert!(
|
|
668
|
-
lines[0].contains("
|
|
668
|
+
lines[0].contains("Task accepted") && lines[0].contains("delivery to the agent's conversation is still pending"),
|
|
669
669
|
"queued send must report its true pending outcome: {text}"
|
|
670
670
|
);
|
|
671
671
|
assert!(
|
|
672
|
-
lines[1].contains("
|
|
672
|
+
lines[1].contains("Next:")
|
|
673
673
|
&& lines[1].contains("team-agent status")
|
|
674
674
|
&& lines[1].contains("doctor")
|
|
675
|
-
&& lines[1].contains("
|
|
676
|
-
&& lines[1].contains("
|
|
675
|
+
&& lines[1].contains("wait for an actual reply")
|
|
676
|
+
&& lines[1].contains("without repeated sends"),
|
|
677
677
|
"queued send must give honest next steps: {text}"
|
|
678
678
|
);
|
|
679
679
|
assert!(
|
|
@@ -951,7 +951,7 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
951
951
|
);
|
|
952
952
|
assert_eq!(
|
|
953
953
|
payload.action,
|
|
954
|
-
"
|
|
954
|
+
"Run team-agent doctor --workspace . for the selected team, or inspect the error log shown here."
|
|
955
955
|
);
|
|
956
956
|
let _ = std::fs::remove_dir_all(&ws);
|
|
957
957
|
}
|
|
@@ -56,9 +56,34 @@ pub(crate) fn render(command: &str, report: &Value) -> String {
|
|
|
56
56
|
for repair in repairs {
|
|
57
57
|
lines.push(bounded(format!("repair: {}", summarize(repair, true))));
|
|
58
58
|
}
|
|
59
|
+
// Short triage lines are navigation, not the authoritative refusal. Retain
|
|
60
|
+
// complete causes/actions as JSON, without dumping unrelated runtime or
|
|
61
|
+
// provider diagnostics. Values have already crossed the redaction boundary.
|
|
62
|
+
let details = diagnostic_details(&report);
|
|
63
|
+
if details.as_object().is_some_and(|details| !details.is_empty()) {
|
|
64
|
+
lines.push(format!("details: {details}"));
|
|
65
|
+
}
|
|
59
66
|
lines.join("\n")
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
// Shared by doctor/diagnose and send. Never include message bodies, profiles or
|
|
70
|
+
// unrelated runtime/provider dumps in a command's diagnostic output.
|
|
71
|
+
pub(crate) fn diagnostic_details(report: &Value) -> Value {
|
|
72
|
+
let report = crate::redaction::redact_external_value(report);
|
|
73
|
+
let mut details = serde_json::Map::new();
|
|
74
|
+
for key in [
|
|
75
|
+
"error", "status", "summary", "reason", "blockers", "action",
|
|
76
|
+
"next_action", "next_actions", "issues", "suggested_repairs",
|
|
77
|
+
"state_path", "log", "schema_error", "stage", "delivery_status",
|
|
78
|
+
"message_status", "verification", "channel", "turn_verification",
|
|
79
|
+
] {
|
|
80
|
+
if let Some(value) = report.get(key) {
|
|
81
|
+
details.insert(key.to_string(), value.clone());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
Value::Object(details)
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
fn array_items(value: Option<&Value>) -> &[Value] {
|
|
63
88
|
value.and_then(Value::as_array).map(Vec::as_slice).unwrap_or(&[])
|
|
64
89
|
}
|
|
@@ -126,6 +151,7 @@ fn bounded(line: String) -> String {
|
|
|
126
151
|
|
|
127
152
|
#[cfg(test)]
|
|
128
153
|
mod tests {
|
|
154
|
+
#![allow(clippy::unwrap_used)]
|
|
129
155
|
use super::*;
|
|
130
156
|
use serde_json::json;
|
|
131
157
|
|
|
@@ -149,12 +175,37 @@ mod tests {
|
|
|
149
175
|
#[test]
|
|
150
176
|
fn renderer_sanitizes_and_bounds_utf8_lines() {
|
|
151
177
|
let report = json!({"ok": false, "issues": ["问题\n".to_string() + &"x".repeat(200)]});
|
|
152
|
-
|
|
153
|
-
|
|
178
|
+
let output = render("doctor", &report);
|
|
179
|
+
for line in output.lines() {
|
|
180
|
+
if let Some(details) = line.strip_prefix("details: ") {
|
|
181
|
+
let parsed: Value = serde_json::from_str(details).unwrap();
|
|
182
|
+
assert_eq!(parsed.get("issues"), report.get("issues"));
|
|
183
|
+
} else {
|
|
184
|
+
assert!(line.len() <= LINE_LIMIT_BYTES);
|
|
185
|
+
}
|
|
154
186
|
assert!(!line.chars().any(char::is_control));
|
|
155
187
|
}
|
|
156
188
|
}
|
|
157
189
|
|
|
190
|
+
#[test]
|
|
191
|
+
fn renderer_preserves_typed_failure_details_and_long_actions() {
|
|
192
|
+
let action = "inspect the precise failure ".repeat(20);
|
|
193
|
+
let report = json!({
|
|
194
|
+
"ok": false,
|
|
195
|
+
"status": "preflight_blocked",
|
|
196
|
+
"reason": "exact refusal",
|
|
197
|
+
"blockers": [{"stage": "database", "reason": "permission denied"}],
|
|
198
|
+
"next_actions": [action],
|
|
199
|
+
"state_path": "/tmp/workspace/.team/runtime/state.json"
|
|
200
|
+
});
|
|
201
|
+
let output = render("doctor", &report);
|
|
202
|
+
let details = output.lines().find_map(|line| line.strip_prefix("details: ")).unwrap();
|
|
203
|
+
let parsed: Value = serde_json::from_str(details).unwrap();
|
|
204
|
+
for key in ["status", "reason", "blockers", "next_actions", "state_path"] {
|
|
205
|
+
assert_eq!(parsed.get(key), report.get(key), "lost {key}");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
158
209
|
#[test]
|
|
159
210
|
fn renderer_projects_secret_findings_without_values() {
|
|
160
211
|
let report = json!({
|
|
@@ -37,8 +37,8 @@ pub enum CliError {
|
|
|
37
37
|
/// 委派的 runtime/lifecycle/state 错误(对应 Python `TeamAgentError`)。message == `str(exc)`。
|
|
38
38
|
#[error("{0}")]
|
|
39
39
|
Runtime(String),
|
|
40
|
-
/// argparse 风格用法错误 / 互斥违反(如 `--summary` + `--json`、`--fix` 缺 `--gate
|
|
41
|
-
///
|
|
40
|
+
/// argparse 风格用法错误 / 互斥违反(如 `--summary` + `--json`、`--fix` 缺 `--gate`)。
|
|
41
|
+
/// 对应 Python 抛 `TeamAgentError` 或 parser.error。
|
|
42
42
|
#[error("usage error: {0}")]
|
|
43
43
|
Usage(String),
|
|
44
44
|
/// Preserve the original refusal for audit while carrying safe human guidance.
|
|
@@ -84,7 +84,7 @@ impl CliError {
|
|
|
84
84
|
ok: false,
|
|
85
85
|
error: error.clone(),
|
|
86
86
|
action: if super::spec::command_spec(command).is_some() {
|
|
87
|
-
"
|
|
87
|
+
"Run team-agent doctor --workspace . for the selected team, or inspect the error log shown here."
|
|
88
88
|
} else {
|
|
89
89
|
"run `team-agent doctor` or inspect the log path shown here"
|
|
90
90
|
}
|
|
@@ -99,27 +99,27 @@ impl CliError {
|
|
|
99
99
|
payload.session_name = Some(session.clone());
|
|
100
100
|
if command == "quick-start" {
|
|
101
101
|
payload.action = format!(
|
|
102
|
-
"
|
|
102
|
+
"Terminal session `{session}` already exists and may belong to your existing team. Resume with team-agent restart; use --allow-fresh only after explicitly accepting loss of saved context. To create another team, change the name in TEAM.md before quick-start. Do not shut down an existing team to bypass this refusal."
|
|
103
103
|
);
|
|
104
104
|
payload.next_actions = Some(vec![
|
|
105
|
-
"
|
|
106
|
-
"
|
|
105
|
+
"If this is your existing team, resume it with team-agent restart.".to_string(),
|
|
106
|
+
"To create another team, change the name in TEAM.md before running team-agent quick-start."
|
|
107
107
|
.to_string(),
|
|
108
108
|
]);
|
|
109
109
|
} else {
|
|
110
110
|
payload.action = format!(
|
|
111
|
-
"
|
|
111
|
+
"Terminal session `{session}` already exists and may belong to a running team. Do not shut it down; choose another team name before starting."
|
|
112
112
|
);
|
|
113
|
-
payload.next_actions = Some(vec!["
|
|
113
|
+
payload.next_actions = Some(vec!["Choose another team name before starting.".to_string()]);
|
|
114
114
|
}
|
|
115
115
|
} else if error.contains("Team Agent launcher flag")
|
|
116
116
|
&& error.contains("must appear before --")
|
|
117
117
|
{
|
|
118
118
|
payload.action =
|
|
119
|
-
String::from("
|
|
119
|
+
String::from("Place Team Agent launcher options before --; only native provider arguments belong after --.");
|
|
120
120
|
} else if error.contains("managed launcher refuses a different ambient tmux server") {
|
|
121
121
|
payload.action = String::from(
|
|
122
|
-
"
|
|
122
|
+
"Leave the current tmux client before retrying. Add --allow-nested-attach before -- only when nested attachment is intentional.",
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
payload
|
|
@@ -127,7 +127,6 @@ impl CliError {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/// CLI 进程退出码(`main`:`result.ok is False` 或异常 → `SystemExit(1)`,否则 0)。
|
|
130
|
-
/// `watch` 路径直接 `SystemExit(0)`(`cmd_watch`)。
|
|
131
130
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
132
131
|
pub enum ExitCode {
|
|
133
132
|
Ok,
|
|
@@ -153,14 +152,14 @@ impl ExitCode {
|
|
|
153
152
|
/// Python `cmd_*` 返回 `dict | str | None`:
|
|
154
153
|
/// - `Json(Value)` ← 委派 runtime 的稳定 `{ok, ...}` dict(`--json` 序列化 sort_keys+indent=2)。
|
|
155
154
|
/// - `Human(String)` ← 人读渲染(`format_status`/`cmd_advanced`/五行 summary/comms boundary text)。
|
|
156
|
-
/// - `None` ← passthrough
|
|
155
|
+
/// - `None` ← passthrough 命令(`cmd_codex`/`cmd_claude`,直接 SystemExit,无 emit)。
|
|
157
156
|
#[derive(Debug, Clone, PartialEq)]
|
|
158
157
|
pub enum CmdOutput {
|
|
159
158
|
/// 机器可读稳定 dict(`--json` 路径或 `result.get("ok") is False` 路径)。
|
|
160
159
|
Json(Value),
|
|
161
160
|
/// 人读字符串(`emit` 非 dict 分支直接 print)。
|
|
162
161
|
Human(String),
|
|
163
|
-
/// 无输出(passthrough
|
|
162
|
+
/// 无输出(passthrough;不经 `emit`)。
|
|
164
163
|
None,
|
|
165
164
|
}
|
|
166
165
|
|
|
@@ -193,19 +192,6 @@ impl CmdResult {
|
|
|
193
192
|
preserve_json_order: false,
|
|
194
193
|
}
|
|
195
194
|
}
|
|
196
|
-
pub fn from_ordered_json(value: Value) -> Self {
|
|
197
|
-
let exit = if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
198
|
-
ExitCode::Error
|
|
199
|
-
} else {
|
|
200
|
-
ExitCode::Ok
|
|
201
|
-
};
|
|
202
|
-
Self {
|
|
203
|
-
output: CmdOutput::Json(value),
|
|
204
|
-
exit,
|
|
205
|
-
as_json: true,
|
|
206
|
-
preserve_json_order: true,
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
195
|
pub fn human(text: impl Into<String>) -> Self {
|
|
210
196
|
Self {
|
|
211
197
|
output: CmdOutput::Human(text.into()),
|
|
@@ -357,18 +343,6 @@ pub struct SendArgs {
|
|
|
357
343
|
pub to_leader: Option<String>,
|
|
358
344
|
}
|
|
359
345
|
|
|
360
|
-
/// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
|
|
361
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
362
|
-
pub struct AllowPeerTalkArgs {
|
|
363
|
-
pub a: String,
|
|
364
|
-
pub b: String,
|
|
365
|
-
pub workspace: PathBuf,
|
|
366
|
-
pub json: bool,
|
|
367
|
-
/// Parsed but rejected until peer allowlist storage grows a team-scoped
|
|
368
|
-
/// backend column.
|
|
369
|
-
pub team: Option<String>,
|
|
370
|
-
}
|
|
371
|
-
|
|
372
346
|
/// `status`(`parser.py:182`)。`--summary`/`--json`/`--detail` 三态(summary xor json,见 `cmd_status`)。
|
|
373
347
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
374
348
|
pub struct StatusArgs {
|
|
@@ -382,13 +356,6 @@ pub struct StatusArgs {
|
|
|
382
356
|
pub team: Option<String>,
|
|
383
357
|
}
|
|
384
358
|
|
|
385
|
-
/// `watch`(`parser.py:190`)。无 `--json`(纯 stream 到 SystemExit)。
|
|
386
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
387
|
-
pub struct WatchArgs {
|
|
388
|
-
pub workspace: PathBuf,
|
|
389
|
-
pub team: Option<String>,
|
|
390
|
-
}
|
|
391
|
-
|
|
392
359
|
/// `approvals`(`parser.py:195`)。
|
|
393
360
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
394
361
|
pub struct ApprovalsArgs {
|
|
@@ -452,14 +419,6 @@ pub struct AttachAppServerLeaderArgs {
|
|
|
452
419
|
pub json: bool,
|
|
453
420
|
}
|
|
454
421
|
|
|
455
|
-
/// `identity`(`parser.py:256`)。
|
|
456
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
457
|
-
pub struct IdentityArgs {
|
|
458
|
-
pub workspace: PathBuf,
|
|
459
|
-
pub team: Option<String>,
|
|
460
|
-
pub json: bool,
|
|
461
|
-
}
|
|
462
|
-
|
|
463
422
|
/// `shutdown`(`parser.py:355`)。`--keep-logs` 默认 true。
|
|
464
423
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
465
424
|
pub struct ShutdownArgs {
|
|
@@ -584,15 +543,6 @@ pub enum DoctorGate {
|
|
|
584
543
|
Unknown(String),
|
|
585
544
|
}
|
|
586
545
|
|
|
587
|
-
/// `sessions`(`parser.py:230`)。
|
|
588
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
589
|
-
pub struct SessionsArgs {
|
|
590
|
-
pub workspace: PathBuf,
|
|
591
|
-
pub json: bool,
|
|
592
|
-
/// Stage 4: explicit `--team` scope (read-only enumeration).
|
|
593
|
-
pub team: Option<String>,
|
|
594
|
-
}
|
|
595
|
-
|
|
596
546
|
/// E7 (0.5.9 host-leader-registry-design §4.1): `team-agent leaders`
|
|
597
547
|
/// enumerates the host discovery index and classifies each entry as
|
|
598
548
|
/// LIVE, STALE, or AMBIGUOUS after re-validating against canonical
|
|
@@ -621,13 +571,6 @@ pub struct ModelsArgs {
|
|
|
621
571
|
pub json: bool,
|
|
622
572
|
}
|
|
623
573
|
|
|
624
|
-
/// `validate [spec=team.spec.yaml] --json`(`parser.py:120`)。
|
|
625
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
626
|
-
pub struct ValidateArgs {
|
|
627
|
-
pub spec: PathBuf,
|
|
628
|
-
pub json: bool,
|
|
629
|
-
}
|
|
630
|
-
|
|
631
574
|
/// `profile {init,doctor,show}`(`parser.py:131`)。
|
|
632
575
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
633
576
|
pub struct ProfileArgs {
|
|
@@ -640,56 +583,9 @@ pub struct ProfileArgs {
|
|
|
640
583
|
pub json: bool,
|
|
641
584
|
}
|
|
642
585
|
|
|
643
|
-
/// Read-only lookup of all stored result envelopes for one case.
|
|
644
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
645
|
-
pub struct ResultsArgs {
|
|
646
|
-
pub case_id: String,
|
|
647
|
-
pub workspace: PathBuf,
|
|
648
|
-
pub team: Option<String>,
|
|
649
|
-
pub json: bool,
|
|
650
|
-
}
|
|
651
|
-
|
|
652
586
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
653
587
|
pub struct WaitArgs {
|
|
654
588
|
pub task_id: String,
|
|
655
589
|
pub workspace: PathBuf,
|
|
656
590
|
pub json: bool,
|
|
657
591
|
}
|
|
658
|
-
|
|
659
|
-
/// `preflight`(`parser.py:160`)。
|
|
660
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
661
|
-
pub struct PreflightArgs {
|
|
662
|
-
pub team: PathBuf,
|
|
663
|
-
pub json: bool,
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/// `wait-ready`(`parser.py:171`)。
|
|
667
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
668
|
-
pub struct WaitReadyArgs {
|
|
669
|
-
pub workspace: PathBuf,
|
|
670
|
-
pub timeout: f64,
|
|
671
|
-
pub json: bool,
|
|
672
|
-
/// Stage 4: explicit `--team` scope (read-only readiness probe).
|
|
673
|
-
pub team: Option<String>,
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/// `e2e`(`parser.py:449`)。
|
|
677
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
678
|
-
pub struct E2eArgs {
|
|
679
|
-
pub workspace: PathBuf,
|
|
680
|
-
pub providers: Vec<String>,
|
|
681
|
-
pub real: bool,
|
|
682
|
-
pub json: bool,
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/// `peek`(`parser.py:201`)。
|
|
686
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
687
|
-
pub struct PeekArgs {
|
|
688
|
-
pub agent: String,
|
|
689
|
-
pub workspace: PathBuf,
|
|
690
|
-
pub tail: usize,
|
|
691
|
-
pub head: Option<usize>,
|
|
692
|
-
pub search: Option<String>,
|
|
693
|
-
pub allow_raw_screen: bool,
|
|
694
|
-
pub json: bool,
|
|
695
|
-
}
|