@team-agent/installer 0.5.112 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +119 -50
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +173 -109
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +158 -14
- package/crates/team-agent/src/cli/models.rs +68 -18
- 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 +35 -29
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +94 -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/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +8 -8
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +65 -26
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +265 -16
- package/crates/team-agent/src/leader/tests/identity.rs +21 -0
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- 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/stale_team_saveconflict_contract.rs +3 -2
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- 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/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -420,7 +420,7 @@ fn format_status_csv_preserves_agent_order_and_collapses_errors() {
|
|
|
420
420
|
});
|
|
421
421
|
assert_eq!(
|
|
422
422
|
format_status_csv(&data),
|
|
423
|
-
"zeta
|
|
423
|
+
"zeta,idle\nalpha,working\nerr_failed,error\nerr_missing_pane,error\nerr_unknown,error\nerr_stopped,error"
|
|
424
424
|
);
|
|
425
425
|
}
|
|
426
426
|
|
|
@@ -489,7 +489,7 @@ fn cli_error_payload_plain_runtime() {
|
|
|
489
489
|
assert_eq!(payload.error, "some other error");
|
|
490
490
|
assert_eq!(
|
|
491
491
|
payload.action,
|
|
492
|
-
"
|
|
492
|
+
"Run team-agent doctor --workspace . for the selected team, or inspect the error log shown here."
|
|
493
493
|
);
|
|
494
494
|
assert_eq!(payload.log, "/tmp/y.log");
|
|
495
495
|
assert_eq!(payload.reason, None);
|
|
@@ -511,7 +511,7 @@ fn cli_error_payload_tmux_conflict_quick_start_enrichment() {
|
|
|
511
511
|
// context reset is only through restart --allow-fresh with explicit consent.
|
|
512
512
|
assert_eq!(
|
|
513
513
|
payload.action,
|
|
514
|
-
"
|
|
514
|
+
"Terminal session `my-team` 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."
|
|
515
515
|
);
|
|
516
516
|
let next = payload.next_actions.as_ref().unwrap();
|
|
517
517
|
assert_eq!(next.len(), 2);
|
|
@@ -529,11 +529,11 @@ fn cli_error_payload_tmux_conflict_non_quick_start_enrichment() {
|
|
|
529
529
|
assert_eq!(payload.session_name.as_deref(), Some("my-team"));
|
|
530
530
|
assert_eq!(
|
|
531
531
|
payload.action,
|
|
532
|
-
"
|
|
532
|
+
"Terminal session `my-team` already exists and may belong to a running team. Do not shut it down; choose another team name before starting."
|
|
533
533
|
);
|
|
534
534
|
let next = payload.next_actions.as_ref().unwrap();
|
|
535
535
|
assert_eq!(next.len(), 1);
|
|
536
|
-
assert!(next[0].contains("
|
|
536
|
+
assert!(next[0].contains("team") && next[0].contains("name"));
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
#[test]
|
|
@@ -749,7 +749,7 @@ fn cmd_leader_passthrough_help_is_pure_human_guidance() {
|
|
|
749
749
|
let result = cmd_leader_passthrough(provider, &[alias.into()], &cwd).unwrap();
|
|
750
750
|
assert_eq!(result.exit, ExitCode::Ok);
|
|
751
751
|
assert!(
|
|
752
|
-
matches!(&result.output, CmdOutput::Human(text) if text.contains(provider) && text.contains("
|
|
752
|
+
matches!(&result.output, CmdOutput::Human(text) if text.contains(provider) && text.contains("Usage") && text.contains("Examples") && text.contains("Next Action"))
|
|
753
753
|
);
|
|
754
754
|
}
|
|
755
755
|
}
|
|
@@ -797,3 +797,91 @@ fn cmd_leader_passthrough_maps_grok_and_cursor_not_claude() {
|
|
|
797
797
|
assert_eq!(leader_passthrough_provider("cursor_agent"), None);
|
|
798
798
|
assert_eq!(leader_passthrough_provider("not-a-passthrough"), None);
|
|
799
799
|
}
|
|
800
|
+
|
|
801
|
+
#[cfg(unix)]
|
|
802
|
+
#[test]
|
|
803
|
+
#[serial_test::serial(env)]
|
|
804
|
+
fn kiro_doctor_inventory_is_read_only_and_does_not_claim_native_readiness() {
|
|
805
|
+
use std::os::unix::fs::PermissionsExt;
|
|
806
|
+
let env = crate::cli::hermetic_test_support::HermeticTestEnv::enter("kiro-inventory");
|
|
807
|
+
let bin = env.workspace("bin");
|
|
808
|
+
let _path = env.with_env("PATH", &bin.to_string_lossy());
|
|
809
|
+
let inventory = provider_doctor_checks();
|
|
810
|
+
assert_eq!(inventory["kiro"]["installed"], false);
|
|
811
|
+
assert_eq!(inventory["codex"]["command"], "codex");
|
|
812
|
+
let launcher = bin.join("kiro-cli");
|
|
813
|
+
std::fs::write(
|
|
814
|
+
&launcher,
|
|
815
|
+
"#!/bin/sh\nprintf called > \"$0.called\"\nexit 0\n",
|
|
816
|
+
)
|
|
817
|
+
.unwrap();
|
|
818
|
+
for (mode, installed) in [(0o600, false), (0o700, true)] {
|
|
819
|
+
std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(mode)).unwrap();
|
|
820
|
+
assert_eq!(
|
|
821
|
+
provider_doctor_checks()["kiro"],
|
|
822
|
+
json!({
|
|
823
|
+
"auth": "unknown",
|
|
824
|
+
"command": "kiro-cli",
|
|
825
|
+
"installed": installed,
|
|
826
|
+
"version": "unknown",
|
|
827
|
+
"probe_status": "not_run",
|
|
828
|
+
"provider_probe_status": "not_run",
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
assert!(!bin.join("kiro-cli.called").exists());
|
|
833
|
+
assert!(env.registry_entries().is_empty());
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
#[cfg(unix)]
|
|
837
|
+
#[test]
|
|
838
|
+
#[serial_test::serial(env)]
|
|
839
|
+
fn kiro_leader_entry_has_help_and_typed_refusal_without_native_or_fallback_effects() {
|
|
840
|
+
use std::os::unix::fs::PermissionsExt;
|
|
841
|
+
let env = crate::cli::hermetic_test_support::HermeticTestEnv::enter("kiro-leader-gate");
|
|
842
|
+
let cwd = env.workspace("empty");
|
|
843
|
+
let bin = env.workspace("bin");
|
|
844
|
+
for tool in ["kiro-cli", "kiro-cli-chat", "codex", "pi", "tmux"] {
|
|
845
|
+
let path = bin.join(tool);
|
|
846
|
+
std::fs::write(&path, "#!/bin/sh\nprintf called > \"$0.called\"\nexit 0\n").unwrap();
|
|
847
|
+
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
848
|
+
}
|
|
849
|
+
let _path = env.with_env("PATH", &bin.to_string_lossy());
|
|
850
|
+
assert!(!crate::cli::emit::is_leader_passthrough_command("kiro"));
|
|
851
|
+
assert_eq!(leader_passthrough_provider("kiro"), None);
|
|
852
|
+
let help = crate::cli::emit::command_help(Some("kiro"));
|
|
853
|
+
assert!(help.contains("team-agent kiro [--json]"));
|
|
854
|
+
assert!(help.contains("kiro_leader_not_admitted"));
|
|
855
|
+
assert!(crate::cli::emit::default_help().contains("Check Kiro leader launch admission"));
|
|
856
|
+
for alias in ["-h", "--help"] {
|
|
857
|
+
assert_eq!(
|
|
858
|
+
crate::cli::run(&["kiro".into(), alias.into()], &cwd),
|
|
859
|
+
ExitCode::Ok
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
let cases: &[&[&str]] = &[&[], &["--json"], &["--attach-existing", "--confirm"]];
|
|
863
|
+
for values in cases {
|
|
864
|
+
let args: Vec<String> = values.iter().map(|arg| (*arg).into()).collect();
|
|
865
|
+
let result = cmd_kiro_leader(&args);
|
|
866
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
867
|
+
assert_eq!(result.as_json, values.contains(&"--json"));
|
|
868
|
+
let CmdOutput::Json(value) = result.output else {
|
|
869
|
+
panic!("expected structured capability refusal");
|
|
870
|
+
};
|
|
871
|
+
assert_eq!(value["schema_version"], "leader_launch.v1");
|
|
872
|
+
assert_eq!(value["ok"], false);
|
|
873
|
+
assert_eq!(value["provider"], "kiro");
|
|
874
|
+
assert_eq!(value["capability"], "leader_launch");
|
|
875
|
+
assert_eq!(value["reason"], "kiro_leader_not_admitted");
|
|
876
|
+
let mut argv = vec!["kiro".into()];
|
|
877
|
+
argv.extend(args);
|
|
878
|
+
assert_eq!(crate::cli::run(&argv, &cwd), ExitCode::Error);
|
|
879
|
+
}
|
|
880
|
+
assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
|
|
881
|
+
assert_eq!(
|
|
882
|
+
std::fs::read_dir(&bin).unwrap().count(),
|
|
883
|
+
5,
|
|
884
|
+
"no tool invoked"
|
|
885
|
+
);
|
|
886
|
+
assert!(env.registry_entries().is_empty());
|
|
887
|
+
}
|
|
@@ -457,7 +457,7 @@ fn red_send_target_none_is_not_broadcast() {
|
|
|
457
457
|
assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
//
|
|
460
|
+
// Doctor retains bounded triage navigation plus complete diagnostic details.
|
|
461
461
|
#[test]
|
|
462
462
|
fn cmd_doctor_comms_human_is_compact_triage_without_boundary_prefix() {
|
|
463
463
|
let mut args = DoctorArgs {
|
|
@@ -483,8 +483,9 @@ fn cmd_doctor_comms_human_is_compact_triage_without_boundary_prefix() {
|
|
|
483
483
|
};
|
|
484
484
|
assert_eq!(text, crate::cli::triage::render("doctor", &report));
|
|
485
485
|
assert!(text.starts_with("doctor:"));
|
|
486
|
-
assert_eq!(text.lines().count(),
|
|
486
|
+
assert_eq!(text.lines().count(), 2 + report["issues"].as_array().unwrap().len()
|
|
487
487
|
+ report["suggested_repairs"].as_array().unwrap().len());
|
|
488
|
+
assert!(text.lines().last().unwrap().starts_with("details: "));
|
|
488
489
|
}
|
|
489
490
|
|
|
490
491
|
// ---- #13 / #27 (P2): run() must NOT treat 'claude_code' as a passthrough trigger ----
|
|
@@ -257,7 +257,7 @@ fn inbox_excludes_leader_stage_result_notifications() {
|
|
|
257
257
|
// the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
|
|
258
258
|
#[test]
|
|
259
259
|
fn comms_selftest_golden_boundary_scope_and_check_shapes() {
|
|
260
|
-
let boundary = "
|
|
260
|
+
let boundary = "This checks terminal connection consistency only; it does not send a message or prove that a worker replied.";
|
|
261
261
|
let ws = tmp_workspace();
|
|
262
262
|
let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
|
|
263
263
|
let obj = v.as_object().expect("comms dict");
|
|
@@ -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);
|
|
@@ -436,6 +436,8 @@ fn leader_attach_command_for_plan_uses_plan_leader_window() {
|
|
|
436
436
|
)),
|
|
437
437
|
argv: Vec::new(),
|
|
438
438
|
provider_argv: Vec::new(),
|
|
439
|
+
leader_prompt: None,
|
|
440
|
+
session_capture: None,
|
|
439
441
|
leader_window: Some(crate::transport::WindowName::new("claude_code")),
|
|
440
442
|
is_external_leader: false,
|
|
441
443
|
leader_env: std::collections::BTreeMap::new(),
|
|
@@ -641,11 +643,11 @@ fn cmd_send_joins_message_with_single_space() {
|
|
|
641
643
|
// 应立即失败以强制重新审视,而不是悄悄跑一条从未验过的分支。
|
|
642
644
|
assert!(
|
|
643
645
|
!delivered,
|
|
644
|
-
"hermetic seed
|
|
646
|
+
"hermetic seed must not produce delivered=true; the fixture assumption changed"
|
|
645
647
|
);
|
|
646
648
|
assert_eq!(
|
|
647
649
|
v.get("reminder").and_then(|reminder| reminder.as_str()),
|
|
648
|
-
Some("
|
|
650
|
+
Some("Task accepted, but delivery is unconfirmed. Check team-agent status/doctor; wait for an actual reply without repeated sends.")
|
|
649
651
|
);
|
|
650
652
|
}
|
|
651
653
|
}
|
|
@@ -665,15 +667,15 @@ fn cmd_send_default_human_output_is_brief_without_false_delivered() {
|
|
|
665
667
|
"send must separate outcome and next action: {text}"
|
|
666
668
|
);
|
|
667
669
|
assert!(
|
|
668
|
-
lines[0].contains("
|
|
670
|
+
lines[0].contains("Task accepted") && lines[0].contains("delivery to the agent's conversation is still pending"),
|
|
669
671
|
"queued send must report its true pending outcome: {text}"
|
|
670
672
|
);
|
|
671
673
|
assert!(
|
|
672
|
-
lines[1].contains("
|
|
674
|
+
lines[1].contains("Next:")
|
|
673
675
|
&& lines[1].contains("team-agent status")
|
|
674
676
|
&& lines[1].contains("doctor")
|
|
675
|
-
&& lines[1].contains("
|
|
676
|
-
&& lines[1].contains("
|
|
677
|
+
&& lines[1].contains("wait for an actual reply")
|
|
678
|
+
&& lines[1].contains("without repeated sends"),
|
|
677
679
|
"queued send must give honest next steps: {text}"
|
|
678
680
|
);
|
|
679
681
|
assert!(
|
|
@@ -951,7 +953,7 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
951
953
|
);
|
|
952
954
|
assert_eq!(
|
|
953
955
|
payload.action,
|
|
954
|
-
"
|
|
956
|
+
"Run team-agent doctor --workspace . for the selected team, or inspect the error log shown here."
|
|
955
957
|
);
|
|
956
958
|
let _ = std::fs::remove_dir_all(&ws);
|
|
957
959
|
}
|
|
@@ -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!({
|
|
@@ -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
|
|
@@ -7,6 +7,18 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
7
7
|
|
|
8
8
|
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
|
9
9
|
|
|
10
|
+
#[test]
|
|
11
|
+
fn cursor_effort_is_rejected_by_shared_admission_for_every_level() {
|
|
12
|
+
for effort in ["low", "medium", "high", "xhigh", "max", "ultra"] {
|
|
13
|
+
let role = write_tmp("cursor.md", &format!("---\nprovider: cursor_agent\neffort: {effort}\n---\nRole\n"));
|
|
14
|
+
let error = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace")
|
|
15
|
+
.err()
|
|
16
|
+
.expect("Cursor must reject effort before command construction");
|
|
17
|
+
assert!(error.to_string().contains("cursor_agent does not support effort; the Cursor CLI has no --effort flag"));
|
|
18
|
+
fs::remove_dir_all(role.parent().unwrap()).unwrap();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
fn unique_base() -> PathBuf {
|
|
11
23
|
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
12
24
|
std::env::temp_dir().join(format!("ta_rs_compile_{}_{}", std::process::id(), n))
|
|
@@ -42,10 +42,16 @@ pub struct IgnoredTeamField {
|
|
|
42
42
|
pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
43
43
|
let text = fs::read_to_string(path)
|
|
44
44
|
.map_err(|e| ModelError::Runtime(format!("{}: {e}", path.display())))?;
|
|
45
|
+
Ok(split_front_matter(&text))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Parse the same captured bytes that a prompt receipt hashes; the existing
|
|
49
|
+
/// path-based compiler keeps its original normalization/plain-text fallback.
|
|
50
|
+
pub(crate) fn split_front_matter(text: &str) -> (Value, String) {
|
|
45
51
|
let text = text.replace("\r\n", "\n").replace('\r', "\n");
|
|
46
52
|
let empty_meta = || Value::Map(Vec::new());
|
|
47
53
|
let Some(rest) = text.strip_prefix("---\n") else {
|
|
48
|
-
return
|
|
54
|
+
return (empty_meta(), text);
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
let mut offset = 0;
|
|
@@ -58,7 +64,7 @@ pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
|
58
64
|
offset += line.len();
|
|
59
65
|
}
|
|
60
66
|
let Some(close) = close else {
|
|
61
|
-
return
|
|
67
|
+
return (empty_meta(), text);
|
|
62
68
|
};
|
|
63
69
|
let raw_meta = &rest[..close];
|
|
64
70
|
let meta = if raw_meta.trim().is_empty() {
|
|
@@ -66,11 +72,11 @@ pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
|
66
72
|
} else {
|
|
67
73
|
match yaml::loads(raw_meta) {
|
|
68
74
|
Ok(meta) if meta.is_map() => meta,
|
|
69
|
-
_ => return
|
|
75
|
+
_ => return (empty_meta(), text),
|
|
70
76
|
}
|
|
71
77
|
};
|
|
72
78
|
let after_marker = &rest[close + 3..];
|
|
73
|
-
|
|
79
|
+
(meta, after_marker.trim_start_matches('\n').to_string())
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
pub fn ignored_owner_team_id_from_team_md(
|
|
@@ -489,6 +495,17 @@ fn compile_role_agent_with_mode(
|
|
|
489
495
|
.filter(|provider| !provider.trim().is_empty())
|
|
490
496
|
.unwrap_or_else(|| "pi".to_string());
|
|
491
497
|
let is_pi = parse_canonical_provider(&provider) == Some(Provider::Pi);
|
|
498
|
+
#[cfg(unix)]
|
|
499
|
+
let contract = crate::contract_runtime::registry::descriptor(&provider);
|
|
500
|
+
#[cfg(unix)]
|
|
501
|
+
let contract_role = contract.is_some();
|
|
502
|
+
#[cfg(not(unix))]
|
|
503
|
+
let contract_role = false;
|
|
504
|
+
#[cfg(unix)]
|
|
505
|
+
let skip_team_effort =
|
|
506
|
+
contract.is_some_and(|descriptor| !descriptor.effort.inherit_team_default);
|
|
507
|
+
#[cfg(not(unix))]
|
|
508
|
+
let skip_team_effort = false;
|
|
492
509
|
validate_pi_role_fields(&meta, role_path, &provider)?;
|
|
493
510
|
let model = string_field(&meta, "model")
|
|
494
511
|
.filter(|value| !value.trim().is_empty())
|
|
@@ -567,7 +584,7 @@ fn compile_role_agent_with_mode(
|
|
|
567
584
|
Some(raw) if !raw.trim().is_empty() => ProviderEffort::parse(raw.trim()),
|
|
568
585
|
_ => None,
|
|
569
586
|
};
|
|
570
|
-
let resolved_effort = if is_pi {
|
|
587
|
+
let resolved_effort = if is_pi || skip_team_effort {
|
|
571
588
|
role_effort
|
|
572
589
|
} else {
|
|
573
590
|
role_effort.or(team_effort)
|
|
@@ -582,22 +599,30 @@ fn compile_role_agent_with_mode(
|
|
|
582
599
|
_ => None,
|
|
583
600
|
})
|
|
584
601
|
.unwrap_or("");
|
|
585
|
-
|
|
586
|
-
if let
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
602
|
+
#[cfg(unix)]
|
|
603
|
+
if let Some(descriptor) = contract {
|
|
604
|
+
use team_agent_contract::contract::descriptor::{Effort, EffortAdmission};
|
|
605
|
+
let native_effort = Effort::parse(effort.as_str())
|
|
606
|
+
.map_err(|error| ModelError::Validation(error.to_string()))?;
|
|
607
|
+
if let EffortAdmission::Reject(reason) =
|
|
608
|
+
descriptor.effort.admission[native_effort.index()]
|
|
609
|
+
{
|
|
610
|
+
return Err(ModelError::Validation(format!(
|
|
611
|
+
"{}: {}",
|
|
612
|
+
role_path.display(),
|
|
613
|
+
reason.message
|
|
614
|
+
)));
|
|
615
|
+
}
|
|
592
616
|
}
|
|
593
|
-
if
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
617
|
+
if !contract_role {
|
|
618
|
+
let provider_enum = parse_canonical_provider(provider_str).unwrap_or(Provider::Codex);
|
|
619
|
+
if let Err(reason) = effort.resolve_for_provider(provider_enum) {
|
|
620
|
+
return Err(ModelError::Validation(format!(
|
|
621
|
+
"{}: {reason} (effort: {}; provider: {provider_str})",
|
|
622
|
+
role_path.display(),
|
|
623
|
+
effort.as_str()
|
|
624
|
+
)));
|
|
625
|
+
}
|
|
601
626
|
}
|
|
602
627
|
agent_items.push(("effort", Value::Str(effort.as_str().to_string())));
|
|
603
628
|
}
|
|
@@ -721,12 +746,17 @@ mod pi_preflight_tests {
|
|
|
721
746
|
|
|
722
747
|
#[test]
|
|
723
748
|
fn omitted_qualified_and_non_pi_do_not_discover() {
|
|
724
|
-
for meta in [
|
|
749
|
+
for meta in [
|
|
750
|
+
role("pi", None),
|
|
751
|
+
role("pi", Some("openai/gpt-5")),
|
|
752
|
+
role("codex", Some("gpt-5")),
|
|
753
|
+
] {
|
|
725
754
|
let calls = Cell::new(0);
|
|
726
755
|
assert!(preflight_pi_role_model_with(&meta, |_| {
|
|
727
756
|
calls.set(calls.get() + 1);
|
|
728
757
|
Ok(vec![])
|
|
729
|
-
})
|
|
758
|
+
})
|
|
759
|
+
.is_ok());
|
|
730
760
|
assert_eq!(calls.get(), 0);
|
|
731
761
|
}
|
|
732
762
|
}
|
|
@@ -737,12 +767,21 @@ mod pi_preflight_tests {
|
|
|
737
767
|
let error = preflight_pi_role_model_with(&role("pi", Some("gpt-5.6-sol")), |requested| {
|
|
738
768
|
calls.set(calls.get() + 1);
|
|
739
769
|
assert_eq!(requested, "gpt-5.6-sol");
|
|
740
|
-
Ok(vec![
|
|
741
|
-
|
|
770
|
+
Ok(vec![
|
|
771
|
+
"openai-codex/gpt-5.6-sol".into(),
|
|
772
|
+
"azure/gpt-5.6-sol".into(),
|
|
773
|
+
])
|
|
774
|
+
})
|
|
775
|
+
.expect_err("unqualified model must fail closed");
|
|
742
776
|
assert_eq!(calls.get(), 1);
|
|
743
|
-
assert_eq!(
|
|
777
|
+
assert_eq!(
|
|
778
|
+
error.candidates,
|
|
779
|
+
vec!["openai-codex/gpt-5.6-sol", "azure/gpt-5.6-sol"]
|
|
780
|
+
);
|
|
744
781
|
assert!(!error.not_ready);
|
|
745
|
-
assert!(error
|
|
782
|
+
assert!(error
|
|
783
|
+
.action
|
|
784
|
+
.contains("team-agent models --provider pi --search gpt-5.6-sol"));
|
|
746
785
|
}
|
|
747
786
|
|
|
748
787
|
#[test]
|