@team-agent/installer 0.5.58 → 0.5.59
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/emit.rs +30 -2
- package/crates/team-agent/src/cli/send/mailbox.rs +68 -25
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/mcp_server/normalize.rs +53 -1
- package/crates/team-agent/src/mcp_server/tools.rs +8 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +1 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
use super::spec::{command_spec, CommandKind, CommandTier, ALL_DISPATCH_KINDS, COMMAND_SPECS};
|
|
5
5
|
use super::*;
|
|
6
|
-
use std::io::Write as _;
|
|
6
|
+
use std::io::{ErrorKind, Write as _};
|
|
7
7
|
|
|
8
8
|
/// `emit`(`helpers.py:12-23`):`--json`→`json.dumps(indent=2, ensure_ascii=False, sort_keys=True)`;
|
|
9
9
|
/// 否则 dict 逐键 `key: value`(嵌套 dict/list 内联 compact json,`ensure_ascii=False`)、非 dict 直接 print。
|
|
@@ -77,12 +77,40 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
77
77
|
/// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
|
|
78
78
|
/// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
|
|
79
79
|
fn emit_result(r: CmdResult) -> ExitCode {
|
|
80
|
+
let persisted_message_id = match &r.output {
|
|
81
|
+
CmdOutput::Json(value) => value
|
|
82
|
+
.get("message_id")
|
|
83
|
+
.and_then(Value::as_str)
|
|
84
|
+
.map(ToString::to_string),
|
|
85
|
+
_ => None,
|
|
86
|
+
};
|
|
80
87
|
if let Some(text) = emit(&r.output, r.as_json) {
|
|
81
|
-
|
|
88
|
+
if let Err(error) = write_stdout_line(&text) {
|
|
89
|
+
if let Some(message_id) = persisted_message_id {
|
|
90
|
+
let stderr = std::io::stderr();
|
|
91
|
+
let mut stderr = stderr.lock();
|
|
92
|
+
let _ = writeln!(
|
|
93
|
+
stderr,
|
|
94
|
+
"stdout unavailable after durable send; persisted_message_id={message_id}"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return if error.kind() == ErrorKind::BrokenPipe {
|
|
98
|
+
r.exit
|
|
99
|
+
} else {
|
|
100
|
+
ExitCode::Error
|
|
101
|
+
};
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
r.exit
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
fn write_stdout_line(text: &str) -> std::io::Result<()> {
|
|
108
|
+
let stdout = std::io::stdout();
|
|
109
|
+
let mut stdout = stdout.lock();
|
|
110
|
+
stdout.write_all(text.as_bytes())?;
|
|
111
|
+
stdout.write_all(b"\n")
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
87
115
|
let Some(spec) = command_spec(command) else {
|
|
88
116
|
return Ok(emit_unknown_subcommand_usage(command));
|
|
@@ -37,8 +37,8 @@ pub(super) fn maybe_enqueue_offline_leader_mailbox(
|
|
|
37
37
|
Ok(s) => s,
|
|
38
38
|
Err(_) => return Ok(None),
|
|
39
39
|
};
|
|
40
|
-
let
|
|
41
|
-
if
|
|
40
|
+
let attachment_history = target_team_mailbox_history(&state, &team_key);
|
|
41
|
+
if attachment_history == MailboxAttachmentHistory::NotEligible {
|
|
42
42
|
return Ok(None);
|
|
43
43
|
}
|
|
44
44
|
let event_log = crate::event_log::EventLog::new(&target_workspace);
|
|
@@ -53,47 +53,90 @@ pub(super) fn maybe_enqueue_offline_leader_mailbox(
|
|
|
53
53
|
)
|
|
54
54
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
55
55
|
let message_id = outcome.message_id.clone().unwrap_or_else(|| "".to_string());
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
let receipt = match attachment_history {
|
|
57
|
+
MailboxAttachmentHistory::NeverAttached => json!({
|
|
58
|
+
"ok": true,
|
|
59
|
+
"status": "deferred",
|
|
60
|
+
"deferred_reason": "never_attached",
|
|
61
|
+
"message_status": "queued_until_leader_attach",
|
|
62
|
+
"channel": "leader_mailbox",
|
|
63
|
+
"delivered": false,
|
|
64
|
+
"to_name": to_name,
|
|
65
|
+
"target_workspace": target_workspace.display().to_string(),
|
|
66
|
+
"team_key": team_key,
|
|
67
|
+
"recipient": "leader",
|
|
68
|
+
"leader_attached": false,
|
|
69
|
+
"message_id": message_id,
|
|
70
|
+
}),
|
|
71
|
+
MailboxAttachmentHistory::PreviouslyAttached => json!({
|
|
72
|
+
"ok": true,
|
|
73
|
+
"status": "queued_until_leader_attach",
|
|
74
|
+
"deferred_reason": "leader_currently_unattached",
|
|
75
|
+
"message_status": "queued_until_leader_attach",
|
|
76
|
+
"channel": "leader_mailbox",
|
|
77
|
+
"delivered": false,
|
|
78
|
+
"to_name": to_name,
|
|
79
|
+
"target_workspace": target_workspace.display().to_string(),
|
|
80
|
+
"team_key": team_key,
|
|
81
|
+
"recipient": "leader",
|
|
82
|
+
"leader_attached": false,
|
|
83
|
+
"message_id": message_id,
|
|
84
|
+
}),
|
|
85
|
+
MailboxAttachmentHistory::NotEligible => unreachable!("refused before persistence"),
|
|
86
|
+
};
|
|
87
|
+
Ok(Some(receipt))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
91
|
+
enum MailboxAttachmentHistory {
|
|
92
|
+
PreviouslyAttached,
|
|
93
|
+
NeverAttached,
|
|
94
|
+
NotEligible,
|
|
69
95
|
}
|
|
70
96
|
|
|
71
|
-
/// Positive-source
|
|
97
|
+
/// Positive-source mailbox eligibility per offline-mailbox-toname-design.md §4:
|
|
72
98
|
/// - target workspace has state and the team key is present + not archived/down;
|
|
73
|
-
/// - AND
|
|
74
|
-
///
|
|
99
|
+
/// - AND a persisted session name establishes a durable replay target;
|
|
100
|
+
/// - a persisted receiver attachment timestamp distinguishes previously attached
|
|
101
|
+
/// teams from teams whose first successful leader attachment is still pending.
|
|
75
102
|
///
|
|
76
103
|
/// We deliberately do NOT poll coordinator health here — enqueuing is
|
|
77
104
|
/// safe even when the coordinator is transiently down; attach-leader
|
|
78
105
|
/// itself replays via `requeue_blocked_leader_messages` regardless.
|
|
79
|
-
|
|
106
|
+
fn target_team_mailbox_history(state: &Value, team_key: &str) -> MailboxAttachmentHistory {
|
|
80
107
|
let team = state
|
|
81
108
|
.get("teams")
|
|
82
109
|
.and_then(|v| v.as_object())
|
|
83
110
|
.and_then(|teams| teams.get(team_key));
|
|
84
111
|
let Some(team) = team else {
|
|
85
|
-
return
|
|
112
|
+
return MailboxAttachmentHistory::NotEligible;
|
|
86
113
|
};
|
|
87
114
|
let status = team
|
|
88
115
|
.get("status")
|
|
89
116
|
.and_then(|v| v.as_str())
|
|
90
117
|
.unwrap_or("alive");
|
|
91
118
|
if matches!(status, "archived" | "down" | "stopped") {
|
|
92
|
-
return
|
|
119
|
+
return MailboxAttachmentHistory::NotEligible;
|
|
93
120
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
team.get("session_name")
|
|
121
|
+
let has_session = team
|
|
122
|
+
.get("session_name")
|
|
97
123
|
.and_then(|v| v.as_str())
|
|
98
|
-
.is_some_and(|s| !s.is_empty())
|
|
124
|
+
.is_some_and(|s| !s.is_empty());
|
|
125
|
+
if !has_session {
|
|
126
|
+
return MailboxAttachmentHistory::NotEligible;
|
|
127
|
+
}
|
|
128
|
+
let previously_attached = team
|
|
129
|
+
.get("leader_receiver")
|
|
130
|
+
.and_then(Value::as_object)
|
|
131
|
+
.is_some_and(|receiver| {
|
|
132
|
+
receiver
|
|
133
|
+
.get("attached_at")
|
|
134
|
+
.and_then(Value::as_str)
|
|
135
|
+
.is_some_and(|value| !value.is_empty())
|
|
136
|
+
});
|
|
137
|
+
if previously_attached {
|
|
138
|
+
MailboxAttachmentHistory::PreviouslyAttached
|
|
139
|
+
} else {
|
|
140
|
+
MailboxAttachmentHistory::NeverAttached
|
|
141
|
+
}
|
|
99
142
|
}
|
|
@@ -155,7 +155,7 @@ pub(crate) struct CommandSpec {
|
|
|
155
155
|
#[rustfmt::skip]
|
|
156
156
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
157
157
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
158
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for
|
|
158
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
159
159
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
160
160
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
161
161
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -336,9 +336,20 @@ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
|
|
|
336
336
|
.or_else(|| obj.get("name"))
|
|
337
337
|
.or_else(|| obj.get("test"))
|
|
338
338
|
.and_then(text_of_value)?;
|
|
339
|
+
let status =
|
|
340
|
+
match normalize_token(obj.get("status").and_then(Value::as_str)).as_str() {
|
|
341
|
+
"executed" => {
|
|
342
|
+
if obj.get("exit_code").and_then(Value::as_i64) == Some(0) {
|
|
343
|
+
TestStatus::Passed
|
|
344
|
+
} else {
|
|
345
|
+
TestStatus::Failed
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
_ => normalize_test_status(obj.get("status").and_then(Value::as_str)),
|
|
349
|
+
};
|
|
339
350
|
Some(NormalizedTest {
|
|
340
351
|
command,
|
|
341
|
-
status
|
|
352
|
+
status,
|
|
342
353
|
detail: obj
|
|
343
354
|
.get("detail")
|
|
344
355
|
.or_else(|| obj.get("output"))
|
|
@@ -346,6 +357,7 @@ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
|
|
|
346
357
|
.or_else(|| obj.get("stderr"))
|
|
347
358
|
.or_else(|| obj.get("summary"))
|
|
348
359
|
.or_else(|| obj.get("message"))
|
|
360
|
+
.or_else(|| obj.get("log_path"))
|
|
349
361
|
.and_then(text_of_value),
|
|
350
362
|
})
|
|
351
363
|
}
|
|
@@ -358,6 +370,46 @@ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
|
|
|
358
370
|
.collect()
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
pub(crate) fn validate_test_evidence_schema(value: Option<&Value>) -> Result<(), String> {
|
|
374
|
+
const ALLOWED: &str = r#"allowed schema: {"status":"executed","command":string,"exit_code":integer,"log_path":string}"#;
|
|
375
|
+
for (index, item) in items_from_value(value).iter().enumerate() {
|
|
376
|
+
let Value::Object(obj) = item else {
|
|
377
|
+
continue;
|
|
378
|
+
};
|
|
379
|
+
let status = normalize_token(obj.get("status").and_then(Value::as_str));
|
|
380
|
+
if status == "executed" {
|
|
381
|
+
let valid = obj.get("command").and_then(Value::as_str).is_some()
|
|
382
|
+
&& obj.get("exit_code").and_then(Value::as_i64).is_some()
|
|
383
|
+
&& obj.get("log_path").and_then(Value::as_str).is_some();
|
|
384
|
+
if !valid {
|
|
385
|
+
return Err(format!(
|
|
386
|
+
"unsupported_test_evidence_schema at tests[{index}]; {ALLOWED}"
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
} else if !status.is_empty()
|
|
390
|
+
&& !matches!(
|
|
391
|
+
status.as_str(),
|
|
392
|
+
"passed"
|
|
393
|
+
| "pass"
|
|
394
|
+
| "ok"
|
|
395
|
+
| "success"
|
|
396
|
+
| "failed"
|
|
397
|
+
| "fail"
|
|
398
|
+
| "error"
|
|
399
|
+
| "skipped"
|
|
400
|
+
| "skip"
|
|
401
|
+
| "not_run"
|
|
402
|
+
| "notrun"
|
|
403
|
+
)
|
|
404
|
+
{
|
|
405
|
+
return Err(format!(
|
|
406
|
+
"unsupported_test_evidence_schema at tests[{index}]; {ALLOWED}"
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
Ok(())
|
|
411
|
+
}
|
|
412
|
+
|
|
361
413
|
pub(crate) fn normalize_risks(value: Option<&Value>) -> Vec<NormalizedRisk> {
|
|
362
414
|
items_from_value(value)
|
|
363
415
|
.iter()
|
|
@@ -27,7 +27,7 @@ use super::helpers::{
|
|
|
27
27
|
};
|
|
28
28
|
use super::normalize::{
|
|
29
29
|
compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
|
|
30
|
-
report_result_integrity_warnings,
|
|
30
|
+
report_result_integrity_warnings, validate_test_evidence_schema,
|
|
31
31
|
};
|
|
32
32
|
use super::types::{
|
|
33
33
|
Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
|
|
@@ -522,6 +522,13 @@ impl TeamOrchestratorTools {
|
|
|
522
522
|
{
|
|
523
523
|
self.note_unknown_result_status(&raw);
|
|
524
524
|
}
|
|
525
|
+
if let Err(error) = validate_test_evidence_schema(base.get("tests")) {
|
|
526
|
+
return Err(ToolError::new(
|
|
527
|
+
ToolErrorReason::InvalidToolArguments,
|
|
528
|
+
error,
|
|
529
|
+
"UnsupportedTestEvidenceSchema",
|
|
530
|
+
));
|
|
531
|
+
}
|
|
525
532
|
let normalized = normalize_report_envelope(&base);
|
|
526
533
|
if let Some(error) = normalized.presentation_error.as_deref() {
|
|
527
534
|
return Err(ToolError::new(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.59",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.59",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.59",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.59"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -166,6 +166,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
|
|
|
166
166
|
- `quick-start` is only for first-time team creation from role docs. If that team already has runtime state, use `team-agent restart . --team <session_name_or_team_name>` to resume it. If restart cannot recover context, explain the loss and wait for explicit user consent before using `team-agent restart . --allow-fresh`; never reset context through quick-start.
|
|
167
167
|
- If the user explicitly asks a worker to create or operate a nested child team, first read `references/team-in-team.md`. Child teams must use an independent child workspace, never the parent `.team/current`.
|
|
168
168
|
- `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
|
|
169
|
+
- Positional `TO` has two co-equal forms: an in-team short name, for example `team-agent send reviewer "Review this change"`, and a fully-qualified logical name, `<workspace>::<team>/<agent>`. Use the fully-qualified form across workspaces or when the local team scope is ambiguous.
|
|
169
170
|
- Advanced orchestration callers may add `--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]`. All sinks remain durable and pullable; `casefile`/`silent` suppress only live leader injection. Missing presentation metadata preserves the normal leader-visible behavior.
|
|
170
171
|
- After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
|
|
171
172
|
- `team-agent send --task task_initial "Start"` routes by task.
|