@team-agent/installer 0.5.59 → 0.5.61

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.
Files changed (68) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +44 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +183 -0
  5. package/crates/team-agent/src/cli/emit.rs +63 -14
  6. package/crates/team-agent/src/cli/leader.rs +8 -1
  7. package/crates/team-agent/src/cli/mod.rs +263 -5
  8. package/crates/team-agent/src/cli/named_address.rs +18 -1
  9. package/crates/team-agent/src/cli/send/presentation.rs +2 -0
  10. package/crates/team-agent/src/cli/spec.rs +4 -1
  11. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  12. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  13. package/crates/team-agent/src/cli/types.rs +33 -0
  14. package/crates/team-agent/src/communication_mode/mod.rs +53 -0
  15. package/crates/team-agent/src/compiler/tests.rs +5 -5
  16. package/crates/team-agent/src/compiler.rs +53 -1
  17. package/crates/team-agent/src/coordinator/tick.rs +21 -8
  18. package/crates/team-agent/src/db/message_store.rs +45 -1
  19. package/crates/team-agent/src/fake_worker.rs +21 -1
  20. package/crates/team-agent/src/leader/start.rs +732 -94
  21. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  22. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  23. package/crates/team-agent/src/lib.rs +4 -0
  24. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
  25. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
  26. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
  27. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
  28. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/launch.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  32. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
  33. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  34. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  35. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  36. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  37. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  38. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  39. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  40. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  41. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  42. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  44. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  45. package/crates/team-agent/src/messaging/results.rs +165 -17
  46. package/crates/team-agent/src/messaging/send.rs +94 -81
  47. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  48. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  50. package/crates/team-agent/src/messaging/types.rs +5 -0
  51. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  52. package/crates/team-agent/src/model/mod.rs +1 -0
  53. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  54. package/crates/team-agent/src/model/spec.rs +16 -0
  55. package/crates/team-agent/src/provider/adapter.rs +57 -0
  56. package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
  57. package/crates/team-agent/src/provider/session/capture.rs +69 -34
  58. package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
  59. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
  60. package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
  61. package/crates/team-agent/src/provider/session/mod.rs +3 -2
  62. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  63. package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
  64. package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
  65. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  66. package/crates/team-agent/src/topology.rs +3 -0
  67. package/package.json +4 -4
  68. package/skills/team-agent/command-coverage.json +379 -0
@@ -151,7 +151,7 @@ pub mod lifecycle_port {
151
151
  .attach_session
152
152
  .as_ref()
153
153
  .map(|name| crate::transport::SessionName::new(name.clone()));
154
- let plan = crate::leader::start::leader_start_plan(
154
+ let prepared = match crate::leader::start::prepare_leader_start_with_nested_attach(
155
155
  provider,
156
156
  provider_args,
157
157
  cwd,
@@ -159,16 +159,29 @@ pub mod lifecycle_port {
159
159
  attach.confirm_attach,
160
160
  attach_session.as_ref(),
161
161
  attach.external_leader,
162
- )
163
- .map_err(|e| CliError::Runtime(e.to_string()))?;
164
- let outcome = crate::leader::start::execute_leader_plan(&plan, cwd)
162
+ attach.allow_nested_attach,
163
+ ) {
164
+ Ok(prepared) => prepared,
165
+ Err(crate::leader::start::PrepareLeaderStartError::PaneAuthorityRefused(refusal)) => {
166
+ return Ok(pane_authority_refusal_value(provider, attach, &refusal));
167
+ }
168
+ Err(crate::leader::start::PrepareLeaderStartError::Leader(error)) => {
169
+ return Err(CliError::Runtime(error.to_string()));
170
+ }
171
+ };
172
+ let outcome = crate::leader::start::execute_prepared_leader_start(&prepared, cwd)
165
173
  .map_err(|e| CliError::Runtime(e.to_string()))?;
174
+ let plan = prepared.plan();
166
175
  let ok = match outcome.status {
167
176
  crate::leader::LeaderLaunchStatus::Exited => outcome.exit_code == Some(0),
168
177
  crate::leader::LeaderLaunchStatus::Detached => true,
169
178
  crate::leader::LeaderLaunchStatus::NotStarted => false,
170
179
  };
171
- let leader_attach_command = leader_attach_command_for_plan(cwd, &plan);
180
+ let leader_attach_command = if outcome.reason.as_deref() == Some("PaneWorkspaceMismatch") {
181
+ Some("team-agent attach-leader".to_string())
182
+ } else {
183
+ leader_attach_command_for_plan(cwd, &plan)
184
+ };
172
185
  Ok(json!({
173
186
  "ok": ok,
174
187
  "provider": provider,
@@ -187,6 +200,251 @@ pub mod lifecycle_port {
187
200
  }))
188
201
  }
189
202
 
203
+ fn pane_authority_refusal_value(
204
+ provider: Provider,
205
+ attach: &LeaderLauncherArgs,
206
+ refusal: &crate::model::pane_authority_refusal::PaneAuthorityRefusal,
207
+ ) -> Value {
208
+ use crate::model::pane_authority_refusal::{
209
+ PaneAuthorityRecoveryAction, PaneAuthorityRecoveryHint, PaneAuthorityRefusalFacts,
210
+ PaneAuthorityRefusalField, ACTION_FIELD, ACTION_REQUIRED_FIELD, HINT_ACTION_FIELD,
211
+ REASON_FIELD,
212
+ };
213
+
214
+ let mut facts = Map::new();
215
+ facts.insert(
216
+ REASON_FIELD.to_string(),
217
+ Value::String(refusal.reason().as_str().to_string()),
218
+ );
219
+ match &refusal.facts {
220
+ PaneAuthorityRefusalFacts::AmbientTmuxEndpointUnavailable(payload) => {
221
+ insert_path_fact(
222
+ &mut facts,
223
+ PaneAuthorityRefusalField::RequestedWorkspace,
224
+ &payload.requested_workspace,
225
+ );
226
+ insert_unavailable_fact(
227
+ &mut facts,
228
+ PaneAuthorityRefusalField::Endpoint,
229
+ payload.endpoint.availability().as_str(),
230
+ payload.endpoint.cause.as_str(),
231
+ );
232
+ }
233
+ PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(payload) => {
234
+ insert_path_fact(
235
+ &mut facts,
236
+ PaneAuthorityRefusalField::RequestedWorkspace,
237
+ &payload.requested_workspace,
238
+ );
239
+ insert_unavailable_fact(
240
+ &mut facts,
241
+ PaneAuthorityRefusalField::ObservedPaneId,
242
+ payload.observed_pane_id.availability().as_str(),
243
+ payload.observed_pane_id.cause.as_str(),
244
+ );
245
+ insert_string_fact(
246
+ &mut facts,
247
+ PaneAuthorityRefusalField::Endpoint,
248
+ &payload.endpoint,
249
+ );
250
+ }
251
+ PaneAuthorityRefusalFacts::AmbientPaneWorkspaceUnavailable(payload) => {
252
+ insert_path_fact(
253
+ &mut facts,
254
+ PaneAuthorityRefusalField::RequestedWorkspace,
255
+ &payload.requested_workspace,
256
+ );
257
+ insert_string_fact(
258
+ &mut facts,
259
+ PaneAuthorityRefusalField::ObservedPaneId,
260
+ &payload.observed_pane_id,
261
+ );
262
+ insert_unavailable_fact(
263
+ &mut facts,
264
+ PaneAuthorityRefusalField::ObservedPaneWorkspace,
265
+ payload.observed_pane_workspace.availability().as_str(),
266
+ payload.observed_pane_workspace.cause.as_str(),
267
+ );
268
+ insert_string_fact(
269
+ &mut facts,
270
+ PaneAuthorityRefusalField::Endpoint,
271
+ &payload.endpoint,
272
+ );
273
+ }
274
+ PaneAuthorityRefusalFacts::CallerControllingTtyUnavailable(payload) => {
275
+ insert_path_fact(
276
+ &mut facts,
277
+ PaneAuthorityRefusalField::RequestedWorkspace,
278
+ &payload.requested_workspace,
279
+ );
280
+ insert_string_fact(
281
+ &mut facts,
282
+ PaneAuthorityRefusalField::ObservedPaneId,
283
+ &payload.observed_pane_id,
284
+ );
285
+ insert_string_fact(
286
+ &mut facts,
287
+ PaneAuthorityRefusalField::Endpoint,
288
+ &payload.endpoint,
289
+ );
290
+ insert_unavailable_fact(
291
+ &mut facts,
292
+ PaneAuthorityRefusalField::CallerControllingTty,
293
+ payload.caller_controlling_tty.availability().as_str(),
294
+ payload.caller_controlling_tty.cause.as_str(),
295
+ );
296
+ }
297
+ PaneAuthorityRefusalFacts::ObservedPaneTtyUnavailable(payload) => {
298
+ insert_path_fact(
299
+ &mut facts,
300
+ PaneAuthorityRefusalField::RequestedWorkspace,
301
+ &payload.requested_workspace,
302
+ );
303
+ insert_string_fact(
304
+ &mut facts,
305
+ PaneAuthorityRefusalField::ObservedPaneId,
306
+ &payload.observed_pane_id,
307
+ );
308
+ insert_string_fact(
309
+ &mut facts,
310
+ PaneAuthorityRefusalField::Endpoint,
311
+ &payload.endpoint,
312
+ );
313
+ insert_unavailable_fact(
314
+ &mut facts,
315
+ PaneAuthorityRefusalField::ObservedPaneTty,
316
+ payload.observed_pane_tty.availability().as_str(),
317
+ payload.observed_pane_tty.cause.as_str(),
318
+ );
319
+ }
320
+ PaneAuthorityRefusalFacts::PaneTtyMismatch(payload) => {
321
+ insert_path_fact(
322
+ &mut facts,
323
+ PaneAuthorityRefusalField::RequestedWorkspace,
324
+ &payload.requested_workspace,
325
+ );
326
+ insert_string_fact(
327
+ &mut facts,
328
+ PaneAuthorityRefusalField::ObservedPaneId,
329
+ &payload.observed_pane_id,
330
+ );
331
+ insert_string_fact(
332
+ &mut facts,
333
+ PaneAuthorityRefusalField::Endpoint,
334
+ &payload.endpoint,
335
+ );
336
+ facts.insert(
337
+ PaneAuthorityRefusalField::CallerControllingTty
338
+ .as_str()
339
+ .to_string(),
340
+ Value::from(payload.caller_controlling_tty),
341
+ );
342
+ facts.insert(
343
+ PaneAuthorityRefusalField::ObservedPaneTty
344
+ .as_str()
345
+ .to_string(),
346
+ Value::from(payload.observed_pane_tty),
347
+ );
348
+ }
349
+ PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(payload) => {
350
+ insert_path_fact(
351
+ &mut facts,
352
+ PaneAuthorityRefusalField::RequestedWorkspace,
353
+ &payload.requested_workspace,
354
+ );
355
+ insert_string_fact(
356
+ &mut facts,
357
+ PaneAuthorityRefusalField::ObservedPaneId,
358
+ &payload.observed_pane_id,
359
+ );
360
+ insert_path_fact(
361
+ &mut facts,
362
+ PaneAuthorityRefusalField::ObservedPaneWorkspace,
363
+ &payload.observed_pane_workspace,
364
+ );
365
+ insert_string_fact(
366
+ &mut facts,
367
+ PaneAuthorityRefusalField::Endpoint,
368
+ &payload.endpoint,
369
+ );
370
+ }
371
+ }
372
+
373
+ let hint_action = match refusal.recovery.hint_action {
374
+ PaneAuthorityRecoveryHint::AttachLeader => "team-agent attach-leader",
375
+ };
376
+ let action = match refusal.recovery.action {
377
+ PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane => {
378
+ "open a new terminal window outside the current tmux/pane, confirm TMUX and \
379
+ TMUX_PANE are absent, change directory to the requested workspace, and restart \
380
+ the leader; alternatively enter the matching workspace pane and run \
381
+ `team-agent attach-leader`"
382
+ }
383
+ };
384
+ facts.insert(
385
+ ACTION_REQUIRED_FIELD.to_string(),
386
+ Value::Bool(refusal.recovery.action_required),
387
+ );
388
+ facts.insert(
389
+ HINT_ACTION_FIELD.to_string(),
390
+ Value::String(hint_action.to_string()),
391
+ );
392
+ facts.insert(ACTION_FIELD.to_string(), Value::String(action.to_string()));
393
+
394
+ let mut value = json!({
395
+ "ok": false,
396
+ "provider": provider,
397
+ "mode": "exec_provider",
398
+ "leader_topology": if attach.external_leader { "external" } else { "managed" },
399
+ "is_external_leader": attach.external_leader,
400
+ "leader_window": null,
401
+ "leader_attach_command": hint_action,
402
+ "status": "not_started",
403
+ "exit_code": null,
404
+ "attach_existing": attach.attach_existing,
405
+ "confirm_attach": attach.confirm_attach,
406
+ "attach_session": attach.attach_session,
407
+ "session_name": null,
408
+ });
409
+ if let Some(object) = value.as_object_mut() {
410
+ object.extend(facts);
411
+ }
412
+ value
413
+ }
414
+
415
+ fn insert_path_fact(
416
+ facts: &mut Map<String, Value>,
417
+ field: crate::model::pane_authority_refusal::PaneAuthorityRefusalField,
418
+ value: &Path,
419
+ ) {
420
+ insert_string_fact(facts, field, &value.to_string_lossy());
421
+ }
422
+
423
+ fn insert_string_fact(
424
+ facts: &mut Map<String, Value>,
425
+ field: crate::model::pane_authority_refusal::PaneAuthorityRefusalField,
426
+ value: &str,
427
+ ) {
428
+ facts.insert(field.as_str().to_string(), Value::String(value.to_string()));
429
+ }
430
+
431
+ fn insert_unavailable_fact(
432
+ facts: &mut Map<String, Value>,
433
+ field: crate::model::pane_authority_refusal::PaneAuthorityRefusalField,
434
+ availability: &str,
435
+ cause: &str,
436
+ ) {
437
+ use crate::model::pane_authority_refusal::{AVAILABILITY_FIELD, CAUSE_FIELD};
438
+
439
+ let mut value = Map::new();
440
+ value.insert(
441
+ AVAILABILITY_FIELD.to_string(),
442
+ Value::String(availability.to_string()),
443
+ );
444
+ value.insert(CAUSE_FIELD.to_string(), Value::String(cause.to_string()));
445
+ facts.insert(field.as_str().to_string(), Value::Object(value));
446
+ }
447
+
190
448
  pub(crate) fn leader_attach_command_for_plan(
191
449
  cwd: &Path,
192
450
  plan: &crate::leader::LeaderStartPlan,
@@ -54,6 +54,10 @@ pub(crate) struct NamedAddressError {
54
54
  pub action: String,
55
55
  pub log: String,
56
56
  pub candidates: Vec<Value>,
57
+ /// A typed live-channel refusal retained separately from the broader
58
+ /// address error (for example `PaneWorkspaceMismatch` while the address
59
+ /// remains a leader-not-attached refusal).
60
+ pub channel_reason: Option<String>,
57
61
  // E6: when the caller supplied a `<team>/leader` name that isn't a runtime
58
62
  // team_key, echo the requested value + the canonical alternatives so
59
63
  // spec/display-name confusion is visibly diagnosed. Empty for other
@@ -76,6 +80,7 @@ impl NamedAddressError {
76
80
  action: "inspect team-agent status and use an explicit workspace/team name".to_string(),
77
81
  log: "named-address resolver refused before injection".to_string(),
78
82
  candidates: Vec::new(),
83
+ channel_reason: None,
79
84
  requested_team: None,
80
85
  available_team_keys: Vec::new(),
81
86
  suggested_name: None,
@@ -237,6 +242,12 @@ impl NamedAddressError {
237
242
  "candidates".to_string(),
238
243
  Value::Array(self.candidates.clone()),
239
244
  );
245
+ if let Some(channel_reason) = self.channel_reason.as_deref() {
246
+ obj.insert(
247
+ "channel_reason".to_string(),
248
+ Value::String(channel_reason.to_string()),
249
+ );
250
+ }
240
251
  if let Some(requested) = self.requested_team.as_deref() {
241
252
  obj.insert(
242
253
  "requested_team".to_string(),
@@ -987,7 +998,13 @@ fn resolve_leader(
987
998
  window,
988
999
  socket,
989
1000
  );
990
- err.log = format!("{} channel_unbound={reason:?}", err.log);
1001
+ if matches!(
1002
+ &reason,
1003
+ crate::messaging::LeaderChannelUnbound::PaneWorkspaceMismatch(_)
1004
+ ) {
1005
+ err.channel_reason = Some(reason.reason_code().to_string());
1006
+ }
1007
+ err.log = format!("{} channel_unbound={}", err.log, reason.reason_code());
991
1008
  err.candidates =
992
1009
  leader_advisory_candidates(state, team, transport, "state_recorded_socket");
993
1010
  Err(err)
@@ -169,6 +169,7 @@ pub(super) fn cmd_send_result(value: Value, as_json: bool) -> CmdResult {
169
169
  output: CmdOutput::Human(send_human_output(&value)),
170
170
  exit,
171
171
  as_json: false,
172
+ preserve_json_order: false,
172
173
  }
173
174
  }
174
175
  }
@@ -312,6 +313,7 @@ pub(super) fn delivery_refusal_wire(reason: DeliveryRefusal) -> &'static str {
312
313
  DeliveryRefusal::TmuxTargetMissing => "tmux_target_missing",
313
314
  DeliveryRefusal::MessageAlreadyClaimed => "message_already_claimed",
314
315
  DeliveryRefusal::LeaderNotAttached => "leader_not_attached",
316
+ DeliveryRefusal::PaneWorkspaceMismatch => "PaneWorkspaceMismatch",
315
317
  DeliveryRefusal::CoordinatorUnavailable => "coordinator_unavailable",
316
318
  DeliveryRefusal::NoCallerPane => "no_caller_pane",
317
319
  DeliveryRefusal::TeamOwnerMismatch => "team_owner_mismatch",
@@ -76,6 +76,7 @@ pub(crate) enum DispatchKind {
76
76
  InstallSkill,
77
77
  Profile,
78
78
  Collect,
79
+ Results,
79
80
  Diagnose,
80
81
  Preflight,
81
82
  WaitReady,
@@ -120,6 +121,7 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
120
121
  DispatchKind::InstallSkill,
121
122
  DispatchKind::Profile,
122
123
  DispatchKind::Collect,
124
+ DispatchKind::Results,
123
125
  DispatchKind::Diagnose,
124
126
  DispatchKind::Preflight,
125
127
  DispatchKind::WaitReady,
@@ -155,9 +157,10 @@ pub(crate) struct CommandSpec {
155
157
  #[rustfmt::skip]
156
158
  pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
157
159
  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 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 },
160
+ 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] [--mailbox] [--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
161
  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
162
  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 },
163
+ CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
161
164
  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 },
162
165
  CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "stop the selected team", usage: "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
163
166
  CommandSpec { name: "add-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::AddAgent), summary: "add or force-recreate a worker", usage: "usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
@@ -393,12 +393,16 @@ pub(super) fn count_undelivered_backlog(
393
393
  conn: &rusqlite::Connection,
394
394
  owner_team_id: Option<&str>,
395
395
  ) -> Result<i64, CliError> {
396
- // Backlog statuses chosen to mirror what `deliver_pending` would pick up.
396
+ // Delivery debt is broader than requeue eligibility: a physically injected
397
+ // row awaiting a future receipt remains undelivered even though it must not
398
+ // be claimed or injected again.
397
399
  let sql = match owner_team_id {
398
400
  Some(_) => "select count(*) from messages
399
- where owner_team_id = ?1 and status in ('accepted','pending','queued','queued_until_trust')",
401
+ where owner_team_id = ?1 and status in
402
+ ('accepted','pending','queued','queued_until_trust','injected_awaiting_receipt')",
400
403
  None => "select count(*) from messages
401
- where status in ('accepted','pending','queued','queued_until_trust')",
404
+ where status in
405
+ ('accepted','pending','queued','queued_until_trust','injected_awaiting_receipt')",
402
406
  };
403
407
  let count: i64 = match owner_team_id {
404
408
  Some(team) => conn
@@ -135,6 +135,17 @@ fn assert_kind(
135
135
  assert!(n38.contains("Log: "), "{n38}");
136
136
  }
137
137
 
138
+ fn assert_broad_leader_not_attached(err: NamedAddressError) {
139
+ assert_eq!(err.kind, NamedAddressErrorKind::LeaderNotAttached);
140
+ assert_eq!(err.channel_reason, None);
141
+ let value = err.to_json();
142
+ assert_eq!(value["reason"], json!("leader_not_attached"));
143
+ assert!(
144
+ value.get("channel_reason").is_none(),
145
+ "broad refusal must not expose a typed channel reason: {value}"
146
+ );
147
+ }
148
+
138
149
  fn named_send_args(
139
150
  ws: &Path,
140
151
  to_name: Option<&str>,
@@ -372,6 +383,60 @@ fn resolve_leader_name_not_live() {
372
383
  let _ = std::fs::remove_dir_all(&ws);
373
384
  }
374
385
 
386
+ #[test]
387
+ fn resolve_leader_receiver_not_attached_stays_broad() {
388
+ let ws = named_ws("leader-receiver-not-attached");
389
+ let mut team = leader_team("alpha-session", "%leader", "/tmp/leader.sock");
390
+ team["leader_receiver"]["status"] = json!("rebind_required");
391
+ seed_state(
392
+ &ws,
393
+ state_with_teams(json!({
394
+ "alpha": team
395
+ })),
396
+ );
397
+ let transport = OfflineTransport::new()
398
+ .with_tmux_endpoint("/tmp/leader.sock")
399
+ .with_targets(vec![pane("alpha-session-leader", "codex", "%leader")]);
400
+
401
+ let err = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap_err();
402
+ assert_broad_leader_not_attached(err);
403
+ let _ = std::fs::remove_dir_all(&ws);
404
+ }
405
+
406
+ #[test]
407
+ fn resolve_leader_pane_not_live_stays_broad() {
408
+ let ws = named_ws("leader-pane-not-live");
409
+ seed_state(
410
+ &ws,
411
+ state_with_teams(json!({
412
+ "alpha": leader_team("alpha-session", "%missing", "/tmp/leader.sock")
413
+ })),
414
+ );
415
+ let transport = OfflineTransport::new().with_tmux_endpoint("/tmp/leader.sock");
416
+
417
+ let err = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap_err();
418
+ assert_broad_leader_not_attached(err);
419
+ let _ = std::fs::remove_dir_all(&ws);
420
+ }
421
+
422
+ #[test]
423
+ fn resolve_leader_probe_failure_stays_broad() {
424
+ let ws = named_ws("leader-probe-failed");
425
+ seed_state(
426
+ &ws,
427
+ state_with_teams(json!({
428
+ "alpha": leader_team("alpha-session", "%leader", "/tmp/leader.sock")
429
+ })),
430
+ );
431
+ let transport = OfflineTransport::new()
432
+ .with_tmux_endpoint("/tmp/leader.sock")
433
+ .with_list_targets_error("injected leader pane probe failure");
434
+
435
+ let err = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap_err();
436
+ assert_broad_leader_not_attached(err);
437
+ let _ = std::fs::remove_dir_all(&ws);
438
+ }
439
+
375
440
  #[test]
376
441
  fn resolve_leader_name_after_rebind() {
377
442
  let ws = named_ws("leader-rebind");
@@ -107,6 +107,10 @@ impl CliError {
107
107
  payload.action = String::from(
108
108
  "move the Team Agent launcher flag before `--`; only provider flags belong after `--`",
109
109
  );
110
+ } else if error.contains("managed launcher refuses a different ambient tmux server") {
111
+ payload.action = String::from(
112
+ "leave the current tmux client and retry, or pass `--allow-nested-attach` before `--` to opt in to a nested managed attach",
113
+ );
110
114
  }
111
115
  payload
112
116
  }
@@ -160,6 +164,8 @@ pub struct CmdResult {
160
164
  pub exit: ExitCode,
161
165
  /// `--json` 旗标(决定 `emit` 与 inbox 摘要落 stderr vs stdout,`parser.py:505-506`)。
162
166
  pub as_json: bool,
167
+ /// Preserve nested object insertion order for byte-faithful stored envelopes.
168
+ pub preserve_json_order: bool,
163
169
  }
164
170
 
165
171
  impl CmdResult {
@@ -174,6 +180,20 @@ impl CmdResult {
174
180
  output: CmdOutput::Json(value),
175
181
  exit,
176
182
  as_json,
183
+ preserve_json_order: false,
184
+ }
185
+ }
186
+ pub fn from_ordered_json(value: Value) -> Self {
187
+ let exit = if value.get("ok").and_then(Value::as_bool) == Some(false) {
188
+ ExitCode::Error
189
+ } else {
190
+ ExitCode::Ok
191
+ };
192
+ Self {
193
+ output: CmdOutput::Json(value),
194
+ exit,
195
+ as_json: true,
196
+ preserve_json_order: true,
177
197
  }
178
198
  }
179
199
  pub fn human(text: impl Into<String>) -> Self {
@@ -181,6 +201,7 @@ impl CmdResult {
181
201
  output: CmdOutput::Human(text.into()),
182
202
  exit: ExitCode::Ok,
183
203
  as_json: false,
204
+ preserve_json_order: false,
184
205
  }
185
206
  }
186
207
  pub fn none() -> Self {
@@ -188,6 +209,7 @@ impl CmdResult {
188
209
  output: CmdOutput::None,
189
210
  exit: ExitCode::Ok,
190
211
  as_json: false,
212
+ preserve_json_order: false,
191
213
  }
192
214
  }
193
215
  }
@@ -210,6 +232,8 @@ pub struct LeaderLauncherArgs {
210
232
  pub attach_session: Option<String>,
211
233
  /// 0.3.16 topology opt-out: keep the old external/current-pane leader launcher path.
212
234
  pub external_leader: bool,
235
+ /// N41 explicit opt-in for attaching a managed leader from another tmux server.
236
+ pub allow_nested_attach: bool,
213
237
  }
214
238
 
215
239
  // =============================================================================
@@ -643,6 +667,15 @@ pub struct CollectArgs {
643
667
  pub team: Option<String>,
644
668
  }
645
669
 
670
+ /// Read-only lookup of all stored result envelopes for one case.
671
+ #[derive(Debug, Clone, PartialEq, Eq)]
672
+ pub struct ResultsArgs {
673
+ pub case_id: String,
674
+ pub workspace: PathBuf,
675
+ pub team: Option<String>,
676
+ pub json: bool,
677
+ }
678
+
646
679
  /// `diagnose`(`parser.py:298`) runtime health report, distinct from `doctor`.
647
680
  #[derive(Debug, Clone, PartialEq, Eq)]
648
681
  pub struct DiagnoseArgs {
@@ -0,0 +1,53 @@
1
+ //! Official communication modes shared by configuration and projection consumers.
2
+
3
+ /// The complete product-supported communication-mode catalog.
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ pub enum CommunicationMode {
6
+ LeaderCentric,
7
+ Orchestrated,
8
+ }
9
+
10
+ const LEADER_CENTRIC_RUNTIME_CONTRACT: &str = r#"# Team Agent communication contract: leader_centric
11
+
12
+ - Progress, blockers, questions: team_orchestrator.send_message(to='leader', content='...')
13
+
14
+ When you receive a message from the leader or a teammate, you MUST respond
15
+ through MCP tools. Writing a reply in your terminal does nothing — the sender
16
+ will never see it."#;
17
+
18
+ const ORCHESTRATED_RUNTIME_CONTRACT: &str = r#"# Team Agent communication contract: orchestrated
19
+
20
+ - Send progress only through the declared channel for the assigned task.
21
+ - Respond to task-related messages through Team Agent MCP tools.
22
+ - A pure ACK, unrelated status, or non-task message does not require a response."#;
23
+
24
+ impl CommunicationMode {
25
+ pub const ALL: &[Self] = &[Self::LeaderCentric, Self::Orchestrated];
26
+
27
+ pub const fn as_str(self) -> &'static str {
28
+ match self {
29
+ Self::LeaderCentric => "leader_centric",
30
+ Self::Orchestrated => "orchestrated",
31
+ }
32
+ }
33
+
34
+ pub fn parse(value: &str) -> Option<Self> {
35
+ Self::ALL
36
+ .iter()
37
+ .copied()
38
+ .find(|mode| mode.as_str() == value)
39
+ }
40
+
41
+ pub const fn runtime_contract(self) -> &'static str {
42
+ match self {
43
+ Self::LeaderCentric => LEADER_CENTRIC_RUNTIME_CONTRACT,
44
+ Self::Orchestrated => ORCHESTRATED_RUNTIME_CONTRACT,
45
+ }
46
+ }
47
+ }
48
+
49
+ impl Default for CommunicationMode {
50
+ fn default() -> Self {
51
+ Self::LeaderCentric
52
+ }
53
+ }