@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 CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.58"
578
+ version = "0.5.59"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.58"
12
+ version = "0.5.59"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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
- println!("{text}");
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 team_alive = target_team_is_alive_for_mailbox(&state, &team_key);
41
- if !team_alive {
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
- Ok(Some(json!({
57
- "ok": true,
58
- "status": "queued_until_leader_attach",
59
- "message_status": "queued_until_leader_attach",
60
- "channel": "leader_mailbox",
61
- "delivered": false,
62
- "to_name": to_name,
63
- "target_workspace": target_workspace.display().to_string(),
64
- "team_key": team_key,
65
- "recipient": "leader",
66
- "leader_attached": false,
67
- "message_id": message_id,
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 liveness heuristic per offline-mailbox-toname-design.md §4:
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 at least one live tmux fact — a persisted `session_name` OR any
74
- /// agent with a recorded pane on the recorded socket.
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
- pub(super) fn target_team_is_alive_for_mailbox(state: &Value, team_key: &str) -> bool {
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 false;
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 false;
119
+ return MailboxAttachmentHistory::NotEligible;
93
120
  }
94
- // A recorded session_name is enough — target's coordinator/attach
95
- // path will re-verify tmux presence when the replay fires.
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 a 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]", 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 },
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: normalize_test_status(obj.get("status").and_then(Value::as_str)),
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.58",
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.58",
24
- "@team-agent/cli-darwin-x64": "0.5.58",
25
- "@team-agent/cli-linux-x64": "0.5.58"
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.