@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/coordinator/tick.rs +21 -8
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/adapter.rs +57 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
- package/crates/team-agent/src/provider/session/capture.rs +69 -34
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
- package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
- package/crates/team-agent/src/provider/session/mod.rs +3 -2
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- 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
|
|
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
|
-
|
|
164
|
-
|
|
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 =
|
|
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
|
-
|
|
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] [--
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
+
}
|