@team-agent/installer 0.5.44 → 0.5.46
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 +44 -33
- package/crates/team-agent/src/cli/mod.rs +6 -0
- package/crates/team-agent/src/cli/named_address.rs +206 -4
- package/crates/team-agent/src/cli/send.rs +105 -2
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port.rs +70 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +7 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +14 -22
- package/crates/team-agent/src/lifecycle/restart/agent.rs +103 -27
- package/crates/team-agent/src/lifecycle/restart/common.rs +136 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +168 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -7
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +10 -0
- package/crates/team-agent/src/mcp_server/tools.rs +67 -8
- package/crates/team-agent/src/model/mod.rs +6 -0
- package/crates/team-agent/src/model/name_similarity.rs +266 -0
- package/crates/team-agent/src/provider/session/capture.rs +113 -22
- package/crates/team-agent/src/provider/session_scan/codex.rs +51 -4
- package/crates/team-agent/src/tmux_backend/tests.rs +116 -10
- package/crates/team-agent/src/tmux_backend.rs +5 -16
- package/crates/team-agent/src/transport/tests/wire.rs +6 -0
- package/crates/team-agent/src/transport.rs +8 -2
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -308,7 +308,24 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
308
308
|
Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).".to_string(),
|
|
309
309
|
Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
|
|
310
310
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
311
|
-
Some("send") =>
|
|
311
|
+
Some("send") => concat!(
|
|
312
|
+
"usage: team-agent send TARGET MESSAGE... ",
|
|
313
|
+
"[--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] ",
|
|
314
|
+
"[--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] ",
|
|
315
|
+
"[--watch-result] [--requires-ack|--no-ack] [--no-wait] ",
|
|
316
|
+
"[--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]\n\n",
|
|
317
|
+
"TARGET is a short id scoped by --team; MCP `to` is a short id scoped ",
|
|
318
|
+
"by the worker's owner team.\n",
|
|
319
|
+
"--to-name accepts: --to-name AGENT, --to-name TEAM/AGENT, or ",
|
|
320
|
+
"--to-name WORKSPACE::TEAM/AGENT.\n",
|
|
321
|
+
"--team scopes only a bare --to-name AGENT; qualified forms keep the ",
|
|
322
|
+
"scope in the address.\n",
|
|
323
|
+
"TEAM/AGENT and WORKSPACE::TEAM/AGENT are not valid positional TARGET ",
|
|
324
|
+
"or MCP `to` values.\n\n",
|
|
325
|
+
"MVP: name-based cross-workspace addressing assumes trusted local ",
|
|
326
|
+
"caller; no auth gate."
|
|
327
|
+
)
|
|
328
|
+
.to_string(),
|
|
312
329
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
313
330
|
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n默认输出: worker,空闲|工作|错误;错误细分走 status --summary".to_string(),
|
|
314
331
|
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
@@ -357,43 +374,33 @@ fn emit_unknown_subcommand_usage(command: &str) -> ExitCode {
|
|
|
357
374
|
if let Some(suggestion) = nearest_subcommand(command) {
|
|
358
375
|
eprintln!("team-agent: did you mean `{suggestion}`?");
|
|
359
376
|
}
|
|
360
|
-
|
|
377
|
+
// 0.5.45 naming-addressing (RED-6): unknown subcommand exits 1
|
|
378
|
+
// (Error), aligned with the family of typo refusals throughout
|
|
379
|
+
// send/named. Pre-0.5.45 mapped to Usage (2) argparse-style; the
|
|
380
|
+
// shared refusal shape ("typo diagnostic + advisory suggestion +
|
|
381
|
+
// exit 1") is the invariant callers of `team-agent send` /
|
|
382
|
+
// `--to-name` also see, so keeping unknown-subcommand at 2 was
|
|
383
|
+
// internal drift.
|
|
384
|
+
ExitCode::Error
|
|
361
385
|
}
|
|
362
386
|
|
|
363
|
-
/// 在已知子命令里找与 `input`
|
|
387
|
+
/// 在已知子命令里找与 `input` 最接近的一个。0.5.45 naming-addressing
|
|
388
|
+
/// (design §3.2/§4.1) 抽公 shared `model::name_similarity` — 距离
|
|
389
|
+
/// 阈值与排序规则跟 CLI `--to-name` typo suggestion 走同一份纯函数,
|
|
390
|
+
/// 避免两套 fuzzy 逻辑漂移。既有 `statu -> status` 行为保留(RED-6
|
|
391
|
+
/// grep guard + prefix-hit priority)。
|
|
364
392
|
fn nearest_subcommand(input: &str) -> Option<&'static str> {
|
|
365
|
-
|
|
393
|
+
use crate::model::name_similarity::{rank, Candidate};
|
|
394
|
+
let candidates: Vec<Candidate<&'static str>> = COMMAND_SPECS
|
|
366
395
|
.iter()
|
|
367
396
|
.filter(|spec| spec.suggestion_index)
|
|
368
|
-
.map(|spec|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
candidates
|
|
376
|
-
.map(|c| (c, levenshtein(input, c)))
|
|
377
|
-
.filter(|(_, d)| *d <= max_distance)
|
|
378
|
-
.min_by_key(|(_, d)| *d)
|
|
379
|
-
.map(|(c, _)| c)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/// 标准 Levenshtein 编辑距离(纯函数,无依赖;子命令建议用)。
|
|
383
|
-
fn levenshtein(a: &str, b: &str) -> usize {
|
|
384
|
-
let a: Vec<char> = a.chars().collect();
|
|
385
|
-
let b: Vec<char> = b.chars().collect();
|
|
386
|
-
let mut prev: Vec<usize> = (0..=b.len()).collect();
|
|
387
|
-
let mut curr = vec![0usize; b.len() + 1];
|
|
388
|
-
for (i, &ca) in a.iter().enumerate() {
|
|
389
|
-
curr[0] = i + 1;
|
|
390
|
-
for (j, &cb) in b.iter().enumerate() {
|
|
391
|
-
let cost = if ca == cb { 0 } else { 1 };
|
|
392
|
-
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
|
|
393
|
-
}
|
|
394
|
-
std::mem::swap(&mut prev, &mut curr);
|
|
395
|
-
}
|
|
396
|
-
prev[b.len()]
|
|
397
|
+
.map(|spec| Candidate {
|
|
398
|
+
match_key: spec.name.to_string(),
|
|
399
|
+
stable_key: spec.name.to_string(),
|
|
400
|
+
payload: spec.name,
|
|
401
|
+
})
|
|
402
|
+
.collect();
|
|
403
|
+
rank(input, &candidates).into_iter().next()
|
|
397
404
|
}
|
|
398
405
|
|
|
399
406
|
fn emit_usage_error(message: &str) {
|
|
@@ -2011,6 +2018,10 @@ mod tests {
|
|
|
2011
2018
|
|
|
2012
2019
|
#[test]
|
|
2013
2020
|
fn e8_levenshtein_basic() {
|
|
2021
|
+
// 0.5.45 naming-addressing: distance function moved to
|
|
2022
|
+
// `crate::model::name_similarity` (shared with --to-name typo
|
|
2023
|
+
// suggestions). Same math, single source.
|
|
2024
|
+
use crate::model::name_similarity::levenshtein;
|
|
2014
2025
|
assert_eq!(levenshtein("kitten", "sitting"), 3);
|
|
2015
2026
|
assert_eq!(levenshtein("status", "status"), 0);
|
|
2016
2027
|
assert_eq!(levenshtein("statu", "status"), 1);
|
|
@@ -2275,6 +2275,9 @@ pub mod lifecycle_port {
|
|
|
2275
2275
|
discarded_session_id,
|
|
2276
2276
|
session_id,
|
|
2277
2277
|
new_session_id,
|
|
2278
|
+
capture_state,
|
|
2279
|
+
reset_proof,
|
|
2280
|
+
weak_reset_warning,
|
|
2278
2281
|
}) => Ok(json!({
|
|
2279
2282
|
"ok": true,
|
|
2280
2283
|
"agent_id": env.agent_id.as_str(),
|
|
@@ -2285,6 +2288,9 @@ pub mod lifecycle_port {
|
|
|
2285
2288
|
"discarded_session_id": discarded_session_id.as_ref().map(|id| id.as_str()),
|
|
2286
2289
|
"session_id": session_id.as_ref().map(|id| id.as_str()),
|
|
2287
2290
|
"new_session_id": new_session_id.as_ref().map(|id| id.as_str()),
|
|
2291
|
+
"capture_state": capture_state,
|
|
2292
|
+
"reset_proof": reset_proof,
|
|
2293
|
+
"weak_reset_warning": weak_reset_warning,
|
|
2288
2294
|
})),
|
|
2289
2295
|
Ok(crate::lifecycle::ResetAgentOutcome::Refused { reason }) => Ok(json!({
|
|
2290
2296
|
"ok": false,
|
|
@@ -61,6 +61,11 @@ pub(crate) struct NamedAddressError {
|
|
|
61
61
|
pub requested_team: Option<String>,
|
|
62
62
|
pub available_team_keys: Vec<String>,
|
|
63
63
|
pub suggested_name: Option<String>,
|
|
64
|
+
/// 0.5.45 naming-addressing (design §3.4, RED-2/RED-3): echo the
|
|
65
|
+
/// user's original typo so JSON consumers + N38 human can pair
|
|
66
|
+
/// `requested_name`/`suggested_name` without re-parsing. Absent
|
|
67
|
+
/// for exact refusals like empty-workspace.
|
|
68
|
+
pub requested_name: Option<String>,
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
impl NamedAddressError {
|
|
@@ -74,18 +79,149 @@ impl NamedAddressError {
|
|
|
74
79
|
requested_team: None,
|
|
75
80
|
available_team_keys: Vec::new(),
|
|
76
81
|
suggested_name: None,
|
|
82
|
+
requested_name: None,
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
pub(crate) fn n38_message(&self) -> String {
|
|
87
|
+
// 0.5.45 naming-addressing (design §3.4, RED-3): the N38 first
|
|
88
|
+
// line embeds the human-readable `message` (which contains
|
|
89
|
+
// the original typo like `qa-namng`/`btea`) after the reason.
|
|
90
|
+
// Pre-0.5.45 the line was `Error: <reason>` only; the typo
|
|
91
|
+
// hid in the JSON-only `error` field, so the human line
|
|
92
|
+
// couldn't be scanned to recover the original request.
|
|
81
93
|
format!(
|
|
82
|
-
"Error: {}\nAction: {}\nLog: {}",
|
|
94
|
+
"Error: {}: {}\nAction: {}\nLog: {}",
|
|
83
95
|
self.kind.as_str(),
|
|
96
|
+
self.message,
|
|
84
97
|
self.action,
|
|
85
98
|
self.log
|
|
86
99
|
)
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
/// 0.5.45 (design §3.4, RED-2 named-team): rank team-scope typo
|
|
103
|
+
/// against target workspace `teams.*` keys; candidate `name`
|
|
104
|
+
/// preserves the qualified `team/entity` shape so the copyable
|
|
105
|
+
/// token in Action can be pasted back into `--to-name` unchanged.
|
|
106
|
+
fn with_scoped_suggestions_for_team(
|
|
107
|
+
self,
|
|
108
|
+
state: &Value,
|
|
109
|
+
team_typo: &str,
|
|
110
|
+
entity: &str,
|
|
111
|
+
parsed: &ParsedNamedAddress,
|
|
112
|
+
) -> Self {
|
|
113
|
+
use crate::model::name_similarity::{rank, Candidate};
|
|
114
|
+
let candidates: Vec<Candidate<(String, String)>> = state
|
|
115
|
+
.get("teams")
|
|
116
|
+
.and_then(Value::as_object)
|
|
117
|
+
.map(|teams| {
|
|
118
|
+
teams
|
|
119
|
+
.keys()
|
|
120
|
+
.map(|team_key| Candidate {
|
|
121
|
+
match_key: team_key.clone(),
|
|
122
|
+
stable_key: team_key.clone(),
|
|
123
|
+
payload: (team_key.clone(), entity.to_string()),
|
|
124
|
+
})
|
|
125
|
+
.collect()
|
|
126
|
+
})
|
|
127
|
+
.unwrap_or_default();
|
|
128
|
+
let ranked = rank(team_typo, &candidates);
|
|
129
|
+
let requested_display = parsed.display_name();
|
|
130
|
+
let candidate_values: Vec<Value> = ranked
|
|
131
|
+
.iter()
|
|
132
|
+
.map(|(team_key, entity)| {
|
|
133
|
+
let name = format!("{team_key}/{entity}");
|
|
134
|
+
json!({
|
|
135
|
+
"name": name,
|
|
136
|
+
"team_key": team_key,
|
|
137
|
+
"agent_id": entity,
|
|
138
|
+
"advisory": true,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
.collect();
|
|
142
|
+
let best = ranked
|
|
143
|
+
.first()
|
|
144
|
+
.map(|(team_key, entity)| format!("{team_key}/{entity}"));
|
|
145
|
+
self.with_scoped_suggestions(requested_display, candidate_values, best)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// 0.5.45 (design §3.4, RED-2 named-agent): rank agent-scope typo
|
|
149
|
+
/// against the legal `teams[team].agents` keys.
|
|
150
|
+
fn with_scoped_suggestions_for_agent(
|
|
151
|
+
self,
|
|
152
|
+
team_entry: &Value,
|
|
153
|
+
team: &str,
|
|
154
|
+
agent_typo: &str,
|
|
155
|
+
parsed: &ParsedNamedAddress,
|
|
156
|
+
) -> Self {
|
|
157
|
+
use crate::model::name_similarity::{rank, Candidate};
|
|
158
|
+
let candidates: Vec<Candidate<String>> = team_entry
|
|
159
|
+
.get("agents")
|
|
160
|
+
.and_then(Value::as_object)
|
|
161
|
+
.map(|agents| {
|
|
162
|
+
agents
|
|
163
|
+
.keys()
|
|
164
|
+
.map(|agent_id| Candidate {
|
|
165
|
+
match_key: agent_id.clone(),
|
|
166
|
+
stable_key: agent_id.clone(),
|
|
167
|
+
payload: agent_id.clone(),
|
|
168
|
+
})
|
|
169
|
+
.collect()
|
|
170
|
+
})
|
|
171
|
+
.unwrap_or_default();
|
|
172
|
+
let ranked = rank(agent_typo, &candidates);
|
|
173
|
+
let requested_display = parsed.display_name();
|
|
174
|
+
let candidate_values: Vec<Value> = ranked
|
|
175
|
+
.iter()
|
|
176
|
+
.map(|agent_id| {
|
|
177
|
+
json!({
|
|
178
|
+
"name": format!("{team}/{agent_id}"),
|
|
179
|
+
"team_key": team,
|
|
180
|
+
"agent_id": agent_id,
|
|
181
|
+
"advisory": true,
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
.collect();
|
|
185
|
+
let best = ranked
|
|
186
|
+
.first()
|
|
187
|
+
.map(|agent_id| format!("{team}/{agent_id}"));
|
|
188
|
+
self.with_scoped_suggestions(requested_display, candidate_values, best)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// 0.5.45 naming-addressing (design §3.3/§3.4): attach an
|
|
192
|
+
/// advisory suggestion payload. `requested` is the original typo
|
|
193
|
+
/// (surfaced verbatim in JSON + N38 Action); `ranked_candidates`
|
|
194
|
+
/// carry the scope-safe candidate payloads produced by
|
|
195
|
+
/// `model::name_similarity::rank`. The best candidate populates
|
|
196
|
+
/// `suggested_name` for convenience; the full list appears as
|
|
197
|
+
/// `candidates` in the JSON envelope.
|
|
198
|
+
pub(crate) fn with_scoped_suggestions(
|
|
199
|
+
mut self,
|
|
200
|
+
requested: impl Into<String>,
|
|
201
|
+
ranked_candidates: Vec<Value>,
|
|
202
|
+
best_copyable: Option<String>,
|
|
203
|
+
) -> Self {
|
|
204
|
+
let requested = requested.into();
|
|
205
|
+
self.requested_name = Some(requested.clone());
|
|
206
|
+
self.candidates = ranked_candidates;
|
|
207
|
+
self.suggested_name = best_copyable.clone();
|
|
208
|
+
// Rewrite Action so the human line points at the actual
|
|
209
|
+
// returned candidate (RED-4). Falls back to a status-JSON
|
|
210
|
+
// hint when no candidate cleared the threshold.
|
|
211
|
+
if let Some(best) = best_copyable {
|
|
212
|
+
self.action = format!(
|
|
213
|
+
"Did you mean `{best}`? Rerun with `--to-name {best}`."
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
self.action = format!(
|
|
217
|
+
"No close matches for `{requested}`. \
|
|
218
|
+
Run `team-agent status --json` for the current team_key + agent list \
|
|
219
|
+
and combine them as `<team>/<agent>` for `--to-name`."
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
self
|
|
223
|
+
}
|
|
224
|
+
|
|
89
225
|
pub(crate) fn to_json(&self) -> Value {
|
|
90
226
|
let mut obj = serde_json::Map::new();
|
|
91
227
|
obj.insert("ok".to_string(), Value::Bool(false));
|
|
@@ -124,6 +260,12 @@ impl NamedAddressError {
|
|
|
124
260
|
Value::String(suggested.to_string()),
|
|
125
261
|
);
|
|
126
262
|
}
|
|
263
|
+
if let Some(requested) = self.requested_name.as_deref() {
|
|
264
|
+
obj.insert(
|
|
265
|
+
"requested_name".to_string(),
|
|
266
|
+
Value::String(requested.to_string()),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
127
269
|
Value::Object(obj)
|
|
128
270
|
}
|
|
129
271
|
}
|
|
@@ -261,8 +403,20 @@ pub(crate) fn resolve_name_with_workspace_transports(
|
|
|
261
403
|
pub(crate) fn resolve_name_for_cli(
|
|
262
404
|
sender_workspace: &Path,
|
|
263
405
|
raw_name: &str,
|
|
406
|
+
bare_team_scope: Option<&str>,
|
|
264
407
|
) -> Result<(ResolvedNamedAddress, Box<dyn Transport>), NamedAddressError> {
|
|
265
408
|
let parsed = parse_named_address(raw_name)?;
|
|
409
|
+
// 0.5.45 naming-addressing (design §3.1, RED-1): bare `--to-name`
|
|
410
|
+
// + explicit `--team` normalizes to `ParsedTarget::TeamEntity`
|
|
411
|
+
// BEFORE transport selection. Only fires when:
|
|
412
|
+
// 1. address parsed as bare (no `team/`, no `workspace::`),
|
|
413
|
+
// 2. caller passed a non-empty `bare_team_scope`.
|
|
414
|
+
// Qualified addresses keep the scope in the address (design §1
|
|
415
|
+
// priority: workspace::team/entity > team/entity > bare + --team
|
|
416
|
+
// > bare workspace-unique). resolve_name_with_transport and the
|
|
417
|
+
// registry-to-E6 internal helper deliberately pass `None` so
|
|
418
|
+
// caller-supplied `--team` never overrides a full address.
|
|
419
|
+
let parsed = normalize_bare_with_team_scope(parsed, bare_team_scope);
|
|
266
420
|
let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())?;
|
|
267
421
|
let state = load_state_checked(&target_workspace)?;
|
|
268
422
|
let transport = transport_for_cli_target(&target_workspace, &state, &parsed);
|
|
@@ -276,6 +430,34 @@ pub(crate) fn resolve_name_for_cli(
|
|
|
276
430
|
Ok((resolved, transport))
|
|
277
431
|
}
|
|
278
432
|
|
|
433
|
+
/// 0.5.45 naming-addressing (design §3.1): apply bare-team scope归一.
|
|
434
|
+
/// Only bare targets with no workspace prefix consume `--team`;
|
|
435
|
+
/// qualified addresses keep the scope embedded in the address.
|
|
436
|
+
fn normalize_bare_with_team_scope(
|
|
437
|
+
parsed: ParsedNamedAddress,
|
|
438
|
+
bare_team_scope: Option<&str>,
|
|
439
|
+
) -> ParsedNamedAddress {
|
|
440
|
+
let Some(scope) = bare_team_scope.map(str::trim).filter(|s| !s.is_empty()) else {
|
|
441
|
+
return parsed;
|
|
442
|
+
};
|
|
443
|
+
if parsed.workspace.is_some() {
|
|
444
|
+
return parsed;
|
|
445
|
+
}
|
|
446
|
+
match parsed.target {
|
|
447
|
+
ParsedTarget::BareAgent(agent) => ParsedNamedAddress {
|
|
448
|
+
workspace: parsed.workspace,
|
|
449
|
+
target: ParsedTarget::TeamEntity {
|
|
450
|
+
team: scope.to_string(),
|
|
451
|
+
entity: agent,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
target => ParsedNamedAddress {
|
|
455
|
+
workspace: parsed.workspace,
|
|
456
|
+
target,
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
279
461
|
/// E6 wiring helper (`cli::send::maybe_enqueue_offline_leader_mailbox`):
|
|
280
462
|
/// re-parse a `<workspace>::<team>/leader` name and return the resolved
|
|
281
463
|
/// target workspace + canonical team_key so send.rs can enqueue the
|
|
@@ -462,12 +644,32 @@ fn resolve_worker(
|
|
|
462
644
|
parsed: &ParsedNamedAddress,
|
|
463
645
|
transport: &dyn Transport,
|
|
464
646
|
) -> Result<ResolvedNamedAddress, NamedAddressError> {
|
|
465
|
-
let team_entry = team_entry(state, team)
|
|
466
|
-
|
|
647
|
+
let team_entry = match team_entry(state, team) {
|
|
648
|
+
Some(entry) => entry,
|
|
649
|
+
None => {
|
|
650
|
+
// 0.5.45 naming-addressing (design §3.4, RED-2 named):
|
|
651
|
+
// typo in team scope. Candidates come from the target
|
|
652
|
+
// workspace's `teams.*.*` — scope-safe because we're
|
|
653
|
+
// already inside the requested workspace projection.
|
|
654
|
+
return Err(name_not_resolvable_team(team)
|
|
655
|
+
.with_scoped_suggestions_for_team(state, team, agent, parsed));
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
let agent_entry = match team_entry
|
|
467
659
|
.get("agents")
|
|
468
660
|
.and_then(Value::as_object)
|
|
469
661
|
.and_then(|agents| agents.get(agent))
|
|
470
|
-
|
|
662
|
+
{
|
|
663
|
+
Some(entry) => entry,
|
|
664
|
+
None => {
|
|
665
|
+
// 0.5.45 naming-addressing (design §3.4, RED-2 named):
|
|
666
|
+
// typo in agent within a legal team. Candidates come from
|
|
667
|
+
// `teams[team].agents` — scope stays in the requested
|
|
668
|
+
// team, so sibling teams cannot leak.
|
|
669
|
+
return Err(name_not_resolvable_agent(team, agent)
|
|
670
|
+
.with_scoped_suggestions_for_agent(team_entry, team, agent, parsed));
|
|
671
|
+
}
|
|
672
|
+
};
|
|
471
673
|
let session = string_field(team_entry, "session_name")
|
|
472
674
|
.ok_or_else(|| name_not_resolvable("team is missing session_name"))?;
|
|
473
675
|
let window = string_field(agent_entry, "window")
|
|
@@ -55,8 +55,17 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
55
55
|
"--to-name requires a non-empty message".to_string(),
|
|
56
56
|
));
|
|
57
57
|
}
|
|
58
|
+
// 0.5.45 naming-addressing (design §3.1, RED-1): thread
|
|
59
|
+
// `--team` down to resolver so bare `--to-name agent --team T`
|
|
60
|
+
// scopes to `T` BEFORE workspace scanning. Qualified addresses
|
|
61
|
+
// (`team/agent`, `workspace::team/agent`) ignore the scope
|
|
62
|
+
// per §1 priority ladder.
|
|
58
63
|
let (resolved, transport) =
|
|
59
|
-
match crate::cli::named_address::resolve_name_for_cli(
|
|
64
|
+
match crate::cli::named_address::resolve_name_for_cli(
|
|
65
|
+
&args.workspace,
|
|
66
|
+
to_name,
|
|
67
|
+
args.team.as_deref(),
|
|
68
|
+
) {
|
|
60
69
|
Ok(resolved) => resolved,
|
|
61
70
|
Err(error) => {
|
|
62
71
|
// E6 (0.5.9 offline-mailbox-toname-design §3.1/§6.2): when
|
|
@@ -166,6 +175,15 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
166
175
|
outcome = observe_initial_delivery_for_watch(&selected, &target, &outcome, &opts)?;
|
|
167
176
|
}
|
|
168
177
|
let mut value = delivery_outcome_json(&outcome, &target, &content, &opts);
|
|
178
|
+
// 0.5.45 naming-addressing (design §3.5, RED-2/RED-3 positional):
|
|
179
|
+
// when the refusal reason is `target_not_in_team` AND the target
|
|
180
|
+
// was a Single non-special short id, attach scope-safe
|
|
181
|
+
// suggestions ranked from `selected.state.agents` — never from
|
|
182
|
+
// raw workspace `teams` (design §3.5 & risk table). Zero DB
|
|
183
|
+
// write / zero inject: the request stays refused, only the JSON
|
|
184
|
+
// envelope gains `requested_name`/`suggested_name`/`candidates`
|
|
185
|
+
// and the human `Action` gains a "Did you mean" line.
|
|
186
|
+
attach_positional_typo_suggestions(&mut value, &target, &selected.state);
|
|
169
187
|
append_loud_ensure_fields(&mut value, coordinator_ensure.as_ref());
|
|
170
188
|
if opts.watch_result && initial_delivery_allows_watch(outcome.status) {
|
|
171
189
|
if let Some(obj) = value.as_object_mut() {
|
|
@@ -773,6 +791,67 @@ fn watch_notice_json(target: &MessageTarget, opts: &SendOptions) -> Value {
|
|
|
773
791
|
})
|
|
774
792
|
}
|
|
775
793
|
|
|
794
|
+
/// 0.5.45 naming-addressing (design §3.5, RED-2/RED-3 positional):
|
|
795
|
+
/// after `messaging::send_message` refuses with `target_not_in_team`
|
|
796
|
+
/// for a Single non-special short id, attach scope-safe advisory
|
|
797
|
+
/// suggestions to the outbound JSON envelope. Candidate source =
|
|
798
|
+
/// selected team's projected `agents` map (never the raw workspace
|
|
799
|
+
/// `teams`) so sibling teams cannot leak. Zero DB write, zero inject
|
|
800
|
+
/// — the refusal exit code is unchanged.
|
|
801
|
+
fn attach_positional_typo_suggestions(
|
|
802
|
+
value: &mut Value,
|
|
803
|
+
target: &MessageTarget,
|
|
804
|
+
selected_state: &Value,
|
|
805
|
+
) {
|
|
806
|
+
use crate::model::name_similarity::{rank, Candidate};
|
|
807
|
+
let requested = match target {
|
|
808
|
+
MessageTarget::Single(id) if id != "*" && id != "leader" => id.clone(),
|
|
809
|
+
_ => return,
|
|
810
|
+
};
|
|
811
|
+
let Some(obj) = value.as_object_mut() else {
|
|
812
|
+
return;
|
|
813
|
+
};
|
|
814
|
+
if obj.get("reason").and_then(Value::as_str) != Some("target_not_in_team") {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
let team_key = selected_state
|
|
818
|
+
.get("active_team_key")
|
|
819
|
+
.or_else(|| selected_state.get("team_key"))
|
|
820
|
+
.and_then(Value::as_str)
|
|
821
|
+
.unwrap_or("");
|
|
822
|
+
let candidates: Vec<Candidate<String>> = selected_state
|
|
823
|
+
.get("agents")
|
|
824
|
+
.and_then(Value::as_object)
|
|
825
|
+
.map(|agents| {
|
|
826
|
+
agents
|
|
827
|
+
.keys()
|
|
828
|
+
.map(|agent_id| Candidate {
|
|
829
|
+
match_key: agent_id.clone(),
|
|
830
|
+
stable_key: agent_id.clone(),
|
|
831
|
+
payload: agent_id.clone(),
|
|
832
|
+
})
|
|
833
|
+
.collect()
|
|
834
|
+
})
|
|
835
|
+
.unwrap_or_default();
|
|
836
|
+
let ranked = rank(&requested, &candidates);
|
|
837
|
+
let candidate_values: Vec<Value> = ranked
|
|
838
|
+
.iter()
|
|
839
|
+
.map(|agent_id| {
|
|
840
|
+
json!({
|
|
841
|
+
"name": agent_id,
|
|
842
|
+
"team_key": team_key,
|
|
843
|
+
"agent_id": agent_id,
|
|
844
|
+
"advisory": true,
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
.collect();
|
|
848
|
+
obj.insert("requested_name".to_string(), json!(requested));
|
|
849
|
+
if let Some(best) = ranked.first() {
|
|
850
|
+
obj.insert("suggested_name".to_string(), json!(best));
|
|
851
|
+
}
|
|
852
|
+
obj.insert("candidates".to_string(), Value::Array(candidate_values));
|
|
853
|
+
}
|
|
854
|
+
|
|
776
855
|
fn delivery_outcome_json(
|
|
777
856
|
outcome: &DeliveryOutcome,
|
|
778
857
|
target: &MessageTarget,
|
|
@@ -973,6 +1052,25 @@ fn send_human_output(value: &Value) -> String {
|
|
|
973
1052
|
parts.push(send_human_field(value, key));
|
|
974
1053
|
}
|
|
975
1054
|
}
|
|
1055
|
+
// 0.5.45 naming-addressing (design §3.4/§3.5, RED-3 positional):
|
|
1056
|
+
// when the refusal envelope carries a scope-safe suggestion,
|
|
1057
|
+
// surface it verbatim in human output so users can copy the
|
|
1058
|
+
// right short id. `requested_name` echoes the typo, `suggested_
|
|
1059
|
+
// name` is the copyable canonical.
|
|
1060
|
+
if let Some(requested) = value
|
|
1061
|
+
.get("requested_name")
|
|
1062
|
+
.and_then(Value::as_str)
|
|
1063
|
+
.filter(|s| !s.is_empty())
|
|
1064
|
+
{
|
|
1065
|
+
parts.push(format!("requested_name: {requested}"));
|
|
1066
|
+
}
|
|
1067
|
+
if let Some(suggested) = value
|
|
1068
|
+
.get("suggested_name")
|
|
1069
|
+
.and_then(Value::as_str)
|
|
1070
|
+
.filter(|s| !s.is_empty())
|
|
1071
|
+
{
|
|
1072
|
+
parts.push(format!("Did you mean `{suggested}`? suggested_name: {suggested}"));
|
|
1073
|
+
}
|
|
976
1074
|
parts.join(" ")
|
|
977
1075
|
}
|
|
978
1076
|
|
|
@@ -1250,8 +1348,13 @@ pub fn send_to_canonical_leader_target(
|
|
|
1250
1348
|
// synthesized `<workspace>::<team_key>/leader` name so live inject +
|
|
1251
1349
|
// mailbox both go through one code path.
|
|
1252
1350
|
let to_name = format!("{}::{}/leader", entry.workspace.display(), entry.team_key);
|
|
1351
|
+
// 0.5.45 naming-addressing (design §3.1 / §4.1): internal
|
|
1352
|
+
// registry-to-E6 delegation MUST pass None for bare_team_scope.
|
|
1353
|
+
// The synthesized name above is a full `workspace::team/leader`
|
|
1354
|
+
// form, and the caller's `--team` flag (if any) must not
|
|
1355
|
+
// override the registry's authoritative team_key.
|
|
1253
1356
|
let (resolved, transport) =
|
|
1254
|
-
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name) {
|
|
1357
|
+
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name, None) {
|
|
1255
1358
|
Ok(r) => r,
|
|
1256
1359
|
Err(err) => {
|
|
1257
1360
|
// Named-address refusal — surface it verbatim but tag as
|
|
@@ -153,7 +153,7 @@ pub(crate) struct CommandSpec {
|
|
|
153
153
|
#[rustfmt::skip]
|
|
154
154
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
155
155
|
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 },
|
|
156
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "send a message/task", usage: "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
156
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "send a message/task", usage: "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME | --to-name agent | --to-name team/agent | --to-name workspace::team/agent] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
157
157
|
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 },
|
|
158
158
|
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 },
|
|
159
159
|
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 },
|