@team-agent/installer 0.5.113 → 0.5.115
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +57 -22
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +157 -8
- package/crates/team-agent/src/cli/models.rs +119 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
- package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
- package/crates/team-agent/src/contract_runtime/config.rs +340 -0
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
- package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +353 -21
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
- package/crates/team-agent/src/lifecycle/launch.rs +45 -28
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//! Early, host-global command dispatch: never select or initialize a workspace.
|
|
2
|
+
use super::ExitCode;
|
|
3
|
+
use crate::leader::prompt::{self, Mutation, PromptError};
|
|
4
|
+
use serde_json::json;
|
|
5
|
+
use std::io::{self, Write};
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
|
|
8
|
+
pub(crate) const HELP: &str = "Purpose:\nPersist supplemental system instructions for future Team Agent Leader launches only.\nUsage: team-agent leader-prompt [show|set|append|clear] [--json] [--file PATH | -- TEXT]\n\nOptions:\nshow (default) prints the exact text; set replaces it; append joins old and new text with two newlines; clear removes it.\nset/append require exactly one TEXT token after --, or --file PATH containing UTF-8 text. No implicit stdin.\nEverything after -- is literal text, including --json and --help. Empty text and NUL are rejected.\nConfiguration: ~/.team-agent/leader-prompt.txt, independent of the workspace. Mutations do not echo the text.\nOnly new Pi, Claude, Codex and Grok Leaders apply it; Worker launches never read it. Cursor/Copilot are refused when configured.\nCodex merges explicit argv developer_instructions, but overrides that user/profile configuration slot; migrate its existing text here.\nNative core prompts and unrelated launch arguments remain unchanged. Attach does not update an existing session.\nExamples:\nteam-agent leader-prompt show\nteam-agent leader-prompt set -- 'Keep replies concise.'\nteam-agent leader-prompt clear\n\nNext Action:\nUse show to inspect the text, then start a new Leader with team-agent pi (or claude/codex/grok).\nset/append/clear affect the next new process, not running Leaders or their conversation history.";
|
|
9
|
+
|
|
10
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
11
|
+
enum Operation {
|
|
12
|
+
Show,
|
|
13
|
+
Set,
|
|
14
|
+
Append,
|
|
15
|
+
Clear,
|
|
16
|
+
}
|
|
17
|
+
impl Operation {
|
|
18
|
+
fn name(self) -> &'static str {
|
|
19
|
+
match self {
|
|
20
|
+
Self::Show => "show",
|
|
21
|
+
Self::Set => "set",
|
|
22
|
+
Self::Append => "append",
|
|
23
|
+
Self::Clear => "clear",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
#[derive(Debug, Eq, PartialEq)]
|
|
28
|
+
enum Input {
|
|
29
|
+
Text(String),
|
|
30
|
+
File(PathBuf),
|
|
31
|
+
}
|
|
32
|
+
#[derive(Debug)]
|
|
33
|
+
struct Request {
|
|
34
|
+
operation: Operation,
|
|
35
|
+
input: Option<Input>,
|
|
36
|
+
json: bool,
|
|
37
|
+
help: bool,
|
|
38
|
+
}
|
|
39
|
+
fn usage() -> PromptError {
|
|
40
|
+
PromptError::new(
|
|
41
|
+
"leader_prompt_invalid_input",
|
|
42
|
+
"Invalid or incomplete leader-prompt arguments; use one input for set/append",
|
|
43
|
+
None,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
fn parse(args: &[String]) -> Result<Request, PromptError> {
|
|
47
|
+
let end = args
|
|
48
|
+
.iter()
|
|
49
|
+
.position(|arg| arg == "--")
|
|
50
|
+
.unwrap_or(args.len());
|
|
51
|
+
let mut json = false;
|
|
52
|
+
let mut help = false;
|
|
53
|
+
let mut file = None;
|
|
54
|
+
let mut positional = None;
|
|
55
|
+
let mut i = 0;
|
|
56
|
+
while i < end {
|
|
57
|
+
match args[i].as_str() {
|
|
58
|
+
"--json" if !json => json = true,
|
|
59
|
+
"--help" | "-h" if !help => help = true,
|
|
60
|
+
"--stdin" => {
|
|
61
|
+
return Err(PromptError::new(
|
|
62
|
+
"leader_prompt_invalid_input",
|
|
63
|
+
"--stdin is not supported for leader-prompt; use -- TEXT or --file PATH instead",
|
|
64
|
+
None,
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
"--file" if file.is_none() && i + 1 < end => {
|
|
68
|
+
i += 1;
|
|
69
|
+
if matches!(args[i].as_str(), "--json" | "--help" | "-h" | "--file") {
|
|
70
|
+
return Err(usage());
|
|
71
|
+
}
|
|
72
|
+
file = Some(PathBuf::from(&args[i]));
|
|
73
|
+
}
|
|
74
|
+
token if !token.starts_with('-') && positional.is_none() => positional = Some(token),
|
|
75
|
+
_ => return Err(usage()),
|
|
76
|
+
}
|
|
77
|
+
i += 1;
|
|
78
|
+
}
|
|
79
|
+
let operation = match positional.unwrap_or("show") {
|
|
80
|
+
"show" => Operation::Show,
|
|
81
|
+
"set" => Operation::Set,
|
|
82
|
+
"append" => Operation::Append,
|
|
83
|
+
"clear" => Operation::Clear,
|
|
84
|
+
_ => return Err(usage()),
|
|
85
|
+
};
|
|
86
|
+
let text = if end < args.len() {
|
|
87
|
+
if args.len() != end + 2 {
|
|
88
|
+
return Err(usage());
|
|
89
|
+
}
|
|
90
|
+
Some(args[end + 1].clone())
|
|
91
|
+
} else {
|
|
92
|
+
None
|
|
93
|
+
};
|
|
94
|
+
if (file.is_some() && text.is_some()) || (help && (file.is_some() || text.is_some())) {
|
|
95
|
+
return Err(usage());
|
|
96
|
+
}
|
|
97
|
+
let input = file.map(Input::File).or_else(|| text.map(Input::Text));
|
|
98
|
+
if !help && (matches!(operation, Operation::Set | Operation::Append) != input.is_some()) {
|
|
99
|
+
return Err(usage());
|
|
100
|
+
}
|
|
101
|
+
Ok(Request {
|
|
102
|
+
operation,
|
|
103
|
+
input,
|
|
104
|
+
json,
|
|
105
|
+
help,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
fn execute(request: Request) -> Result<(), PromptError> {
|
|
109
|
+
let path = prompt::config_path()?;
|
|
110
|
+
if request.operation == Operation::Show {
|
|
111
|
+
let text = prompt::read(&path)?.unwrap_or_default();
|
|
112
|
+
if request.json {
|
|
113
|
+
println!(
|
|
114
|
+
"{}",
|
|
115
|
+
json!({"configured": !text.is_empty(), "config_path": path, "prompt": text})
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
// Unlike mutation diagnostics, explicit show must preserve every byte.
|
|
119
|
+
io::stdout()
|
|
120
|
+
.write_all(text.as_bytes())
|
|
121
|
+
.and_then(|_| io::stdout().flush())
|
|
122
|
+
.map_err(|error| PromptError::io("leader_prompt_write_failed", &path, error))?;
|
|
123
|
+
}
|
|
124
|
+
return Ok(());
|
|
125
|
+
}
|
|
126
|
+
let mutation = match request.operation {
|
|
127
|
+
Operation::Clear => Mutation::Clear,
|
|
128
|
+
_ => {
|
|
129
|
+
let text = match request.input {
|
|
130
|
+
Some(Input::Text(text)) => text,
|
|
131
|
+
Some(Input::File(input)) => std::fs::read_to_string(&input).map_err(|error| {
|
|
132
|
+
PromptError::io("leader_prompt_unreadable", &input, error).at_config(&path)
|
|
133
|
+
})?,
|
|
134
|
+
None => return Err(usage()),
|
|
135
|
+
};
|
|
136
|
+
if request.operation == Operation::Set {
|
|
137
|
+
Mutation::Set(text)
|
|
138
|
+
} else {
|
|
139
|
+
Mutation::Append(text)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
let configured = prompt::mutate(&path, mutation)?;
|
|
144
|
+
if request.json {
|
|
145
|
+
println!(
|
|
146
|
+
"{}",
|
|
147
|
+
json!({"operation": request.operation.name(), "configured": configured, "config_path": path})
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
println!(
|
|
151
|
+
"Leader prompt {}: configured={configured}; config={}",
|
|
152
|
+
request.operation.name(),
|
|
153
|
+
path.display()
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
Ok(())
|
|
157
|
+
}
|
|
158
|
+
pub(crate) fn run(args: &[String]) -> ExitCode {
|
|
159
|
+
let end = args
|
|
160
|
+
.iter()
|
|
161
|
+
.position(|arg| arg == "--")
|
|
162
|
+
.unwrap_or(args.len());
|
|
163
|
+
let as_json = args[..end].iter().any(|arg| arg == "--json");
|
|
164
|
+
let result = match parse(args) {
|
|
165
|
+
Ok(request) if request.help => {
|
|
166
|
+
println!("{HELP}");
|
|
167
|
+
return ExitCode::Ok;
|
|
168
|
+
}
|
|
169
|
+
Ok(request) => execute(request),
|
|
170
|
+
Err(error) => Err(error),
|
|
171
|
+
};
|
|
172
|
+
match result {
|
|
173
|
+
Ok(()) => ExitCode::Ok,
|
|
174
|
+
Err(error) => {
|
|
175
|
+
if as_json {
|
|
176
|
+
println!("{}", error.report());
|
|
177
|
+
} else {
|
|
178
|
+
eprintln!("{error}");
|
|
179
|
+
}
|
|
180
|
+
if error.reason == "leader_prompt_invalid_input" {
|
|
181
|
+
ExitCode::Usage
|
|
182
|
+
} else {
|
|
183
|
+
ExitCode::Error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[cfg(test)]
|
|
190
|
+
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
191
|
+
mod tests {
|
|
192
|
+
use super::*;
|
|
193
|
+
fn args(tokens: &[&str]) -> Vec<String> {
|
|
194
|
+
tokens.iter().map(|text| text.to_string()).collect()
|
|
195
|
+
}
|
|
196
|
+
#[test]
|
|
197
|
+
fn leader_prompt_controls_stop_at_delimiter() {
|
|
198
|
+
for text in ["--json", "--help", "--file", "--stdin", "multi\nline🙂", ""] {
|
|
199
|
+
let request = parse(&args(&["set", "--", text])).unwrap();
|
|
200
|
+
assert!(!request.json && !request.help);
|
|
201
|
+
assert_eq!(request.input, Some(Input::Text(text.into())));
|
|
202
|
+
}
|
|
203
|
+
assert!(parse(&args(&[])).unwrap().operation == Operation::Show);
|
|
204
|
+
let request = parse(&args(&["--json", "append", "--file", "path with 中文"])).unwrap();
|
|
205
|
+
assert_eq!(
|
|
206
|
+
request.input,
|
|
207
|
+
Some(Input::File(PathBuf::from("path with 中文")))
|
|
208
|
+
);
|
|
209
|
+
assert!(request.json);
|
|
210
|
+
}
|
|
211
|
+
#[test]
|
|
212
|
+
fn leader_prompt_stdin_control_reports_unsupported_input_with_usage_exit() {
|
|
213
|
+
for tokens in [
|
|
214
|
+
vec!["set", "--stdin"],
|
|
215
|
+
vec!["set", "--json", "--stdin"],
|
|
216
|
+
vec!["append", "--json", "--stdin"],
|
|
217
|
+
] {
|
|
218
|
+
let tokens = args(&tokens);
|
|
219
|
+
let error = parse(&tokens).unwrap_err();
|
|
220
|
+
assert_eq!(error.reason, "leader_prompt_invalid_input");
|
|
221
|
+
assert_eq!(
|
|
222
|
+
error.error,
|
|
223
|
+
"--stdin is not supported for leader-prompt; use -- TEXT or --file PATH instead"
|
|
224
|
+
);
|
|
225
|
+
assert!(error.config_path.is_none());
|
|
226
|
+
assert_eq!(error.report()["reason"], "leader_prompt_invalid_input");
|
|
227
|
+
assert_eq!(error.report()["error"], error.error);
|
|
228
|
+
assert!(error.to_string().contains("--stdin is not supported"));
|
|
229
|
+
assert_eq!(run(&tokens).code(), 2);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
#[test]
|
|
233
|
+
fn leader_prompt_stdin_file_value_is_not_reclassified_as_a_control() {
|
|
234
|
+
for operation in ["set", "append"] {
|
|
235
|
+
let request = parse(&args(&[operation, "--file", "--stdin"])).unwrap();
|
|
236
|
+
assert_eq!(request.input, Some(Input::File(PathBuf::from("--stdin"))));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
#[test]
|
|
240
|
+
fn leader_prompt_rejects_duplicate_missing_unknown_or_extra_controls() {
|
|
241
|
+
for tokens in [
|
|
242
|
+
vec!["set"],
|
|
243
|
+
vec!["append", "--"],
|
|
244
|
+
vec!["set", "--", "one", "two"],
|
|
245
|
+
vec!["set", "--file"],
|
|
246
|
+
vec!["set", "--file", "input", "--", "text"],
|
|
247
|
+
vec!["show", "--", "text"],
|
|
248
|
+
vec!["clear", "--file", "input"],
|
|
249
|
+
vec!["show", "--json", "--json"],
|
|
250
|
+
vec!["set", "--file", "one", "--file", "two"],
|
|
251
|
+
vec!["show", "--unknown"],
|
|
252
|
+
vec!["show", "clear"],
|
|
253
|
+
vec!["unknown", "--help"],
|
|
254
|
+
] {
|
|
255
|
+
assert_eq!(
|
|
256
|
+
parse(&args(&tokens)).unwrap_err().reason,
|
|
257
|
+
"leader_prompt_invalid_input"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -66,6 +66,7 @@ pub mod leaders;
|
|
|
66
66
|
pub mod models;
|
|
67
67
|
pub mod named_address;
|
|
68
68
|
pub mod profile;
|
|
69
|
+
pub(crate) mod leader_prompt;
|
|
69
70
|
pub(crate) mod route;
|
|
70
71
|
pub mod send;
|
|
71
72
|
pub(crate) mod spec;
|
|
@@ -172,12 +173,18 @@ pub mod lifecycle_port {
|
|
|
172
173
|
Err(crate::leader::start::PrepareLeaderStartError::PaneAuthorityRefused(refusal)) => {
|
|
173
174
|
return Ok(pane_authority_refusal_value(provider, attach, &refusal));
|
|
174
175
|
}
|
|
176
|
+
Err(crate::leader::start::PrepareLeaderStartError::Leader(
|
|
177
|
+
crate::leader::LeaderError::Prompt(error),
|
|
178
|
+
)) => return Ok(error.report()),
|
|
175
179
|
Err(crate::leader::start::PrepareLeaderStartError::Leader(error)) => {
|
|
176
180
|
return Err(CliError::Runtime(error.to_string()));
|
|
177
181
|
}
|
|
178
182
|
};
|
|
179
|
-
let outcome = crate::leader::start::execute_prepared_leader_start(&prepared, cwd)
|
|
180
|
-
|
|
183
|
+
let outcome = match crate::leader::start::execute_prepared_leader_start(&prepared, cwd) {
|
|
184
|
+
Ok(outcome) => outcome,
|
|
185
|
+
Err(crate::leader::LeaderError::Prompt(error)) => return Ok(error.report()),
|
|
186
|
+
Err(error) => return Err(CliError::Runtime(error.to_string())),
|
|
187
|
+
};
|
|
181
188
|
let plan = prepared.plan();
|
|
182
189
|
let ok = match outcome.status {
|
|
183
190
|
crate::leader::LeaderLaunchStatus::Exited => outcome.exit_code == Some(0),
|
|
@@ -1297,6 +1304,16 @@ pub mod lifecycle_port {
|
|
|
1297
1304
|
Some(state) => state,
|
|
1298
1305
|
None => shutdown_state_for_team(&run_workspace, team)?,
|
|
1299
1306
|
};
|
|
1307
|
+
#[cfg(unix)]
|
|
1308
|
+
if crate::contract_runtime::framework::has_contract(&state) {
|
|
1309
|
+
crate::contract_runtime::framework::stop_selected(&run_workspace, &state).map_err(
|
|
1310
|
+
|error| {
|
|
1311
|
+
CliError::Runtime(format!(
|
|
1312
|
+
"native shutdown failed; owned resources preserved: {error}"
|
|
1313
|
+
))
|
|
1314
|
+
},
|
|
1315
|
+
)?;
|
|
1316
|
+
}
|
|
1300
1317
|
deadline.check("refresh_provider_sessions")?;
|
|
1301
1318
|
let captured_missing_sessions =
|
|
1302
1319
|
crate::lifecycle::restart::refresh_missing_provider_sessions(&mut state)
|
|
@@ -3108,6 +3125,10 @@ pub mod lifecycle_port {
|
|
|
3108
3125
|
team: Option<&str>,
|
|
3109
3126
|
session_converge_deadline_ms: Option<u64>,
|
|
3110
3127
|
) -> Result<Value, CliError> {
|
|
3128
|
+
#[cfg(unix)]
|
|
3129
|
+
if let Some(receipt) = crate::contract_runtime::commands::restart(workspace, team)? {
|
|
3130
|
+
return Ok(receipt);
|
|
3131
|
+
}
|
|
3111
3132
|
match crate::lifecycle::restart_with_session_convergence_deadline(
|
|
3112
3133
|
workspace,
|
|
3113
3134
|
allow_fresh,
|
|
@@ -3128,6 +3149,23 @@ pub mod lifecycle_port {
|
|
|
3128
3149
|
team: Option<&str>,
|
|
3129
3150
|
role_config: &crate::lifecycle::role_config::RoleConfigPatch,
|
|
3130
3151
|
) -> Result<Value, CliError> {
|
|
3152
|
+
#[cfg(unix)]
|
|
3153
|
+
if let Some(receipt) = crate::contract_runtime::commands::start_agent(workspace, team, agent, role_config)? {
|
|
3154
|
+
return Ok(receipt);
|
|
3155
|
+
}
|
|
3156
|
+
#[cfg(unix)]
|
|
3157
|
+
{
|
|
3158
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3159
|
+
workspace,
|
|
3160
|
+
team,
|
|
3161
|
+
Some(agent),
|
|
3162
|
+
"start-agent",
|
|
3163
|
+
)?;
|
|
3164
|
+
crate::contract_runtime::framework::require_legacy_role(
|
|
3165
|
+
None,
|
|
3166
|
+
role_config.provider.as_deref(),
|
|
3167
|
+
)?;
|
|
3168
|
+
}
|
|
3131
3169
|
let _ = force; // Never grants authority to stop an already-running seat.
|
|
3132
3170
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3133
3171
|
match crate::lifecycle::role_config::start_agent_from_role(
|
|
@@ -3194,7 +3232,15 @@ pub mod lifecycle_port {
|
|
|
3194
3232
|
agent: &str,
|
|
3195
3233
|
team: Option<&str>,
|
|
3196
3234
|
) -> Result<Value, CliError> {
|
|
3235
|
+
#[cfg(unix)]
|
|
3236
|
+
if let Some(receipt) = crate::contract_runtime::commands::stop_agent(workspace, team, agent)? {
|
|
3237
|
+
return Ok(receipt);
|
|
3238
|
+
}
|
|
3197
3239
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3240
|
+
#[cfg(unix)]
|
|
3241
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3242
|
+
workspace, team, Some(agent), "stop-agent",
|
|
3243
|
+
)?;
|
|
3198
3244
|
match crate::lifecycle::stop_agent(workspace, &agent_id, team) {
|
|
3199
3245
|
Ok(report) => Ok(json!({"ok": true, "agent_id": agent, "stopped": report.stopped})),
|
|
3200
3246
|
Err(e) => Ok(error_value(e)),
|
|
@@ -3209,6 +3255,13 @@ pub mod lifecycle_port {
|
|
|
3209
3255
|
team: Option<&str>,
|
|
3210
3256
|
) -> Result<Value, CliError> {
|
|
3211
3257
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3258
|
+
#[cfg(unix)]
|
|
3259
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3260
|
+
workspace,
|
|
3261
|
+
team,
|
|
3262
|
+
Some(agent),
|
|
3263
|
+
"reset-agent",
|
|
3264
|
+
)?;
|
|
3212
3265
|
match crate::lifecycle::reset_agent(workspace, &agent_id, discard_session, true, team) {
|
|
3213
3266
|
Ok(crate::lifecycle::ResetAgentOutcome::Reset {
|
|
3214
3267
|
env,
|
|
@@ -3256,6 +3309,19 @@ pub mod lifecycle_port {
|
|
|
3256
3309
|
) -> Result<Value, CliError> {
|
|
3257
3310
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3258
3311
|
let source = (!role_file.is_empty()).then(|| Path::new(role_file));
|
|
3312
|
+
#[cfg(unix)]
|
|
3313
|
+
{
|
|
3314
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3315
|
+
workspace,
|
|
3316
|
+
team,
|
|
3317
|
+
Some(agent),
|
|
3318
|
+
"add-agent",
|
|
3319
|
+
)?;
|
|
3320
|
+
crate::contract_runtime::framework::require_legacy_role(
|
|
3321
|
+
source,
|
|
3322
|
+
role_config.provider.as_deref(),
|
|
3323
|
+
)?;
|
|
3324
|
+
}
|
|
3259
3325
|
match crate::lifecycle::role_config::add_agent_from_role(
|
|
3260
3326
|
workspace,
|
|
3261
3327
|
&agent_id,
|
|
@@ -3308,6 +3374,13 @@ pub mod lifecycle_port {
|
|
|
3308
3374
|
) -> Result<Value, CliError> {
|
|
3309
3375
|
let source = crate::model::ids::AgentId::new(source_agent);
|
|
3310
3376
|
let dest = crate::model::ids::AgentId::new(as_agent_id);
|
|
3377
|
+
#[cfg(unix)]
|
|
3378
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3379
|
+
workspace,
|
|
3380
|
+
team,
|
|
3381
|
+
Some(source_agent),
|
|
3382
|
+
"fork-agent",
|
|
3383
|
+
)?;
|
|
3311
3384
|
match crate::lifecycle::fork_agent(workspace, &source, &dest, label, true, team) {
|
|
3312
3385
|
Ok(report) => {
|
|
3313
3386
|
let mut value = json!({
|
|
@@ -3345,6 +3418,13 @@ pub mod lifecycle_port {
|
|
|
3345
3418
|
) -> Result<Value, CliError> {
|
|
3346
3419
|
let source = crate::model::ids::AgentId::new(source_agent);
|
|
3347
3420
|
let dest = crate::model::ids::AgentId::new(as_agent_id);
|
|
3421
|
+
#[cfg(unix)]
|
|
3422
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3423
|
+
workspace,
|
|
3424
|
+
team,
|
|
3425
|
+
Some(source_agent),
|
|
3426
|
+
"clone-agent",
|
|
3427
|
+
)?;
|
|
3348
3428
|
match crate::lifecycle::clone_agent(workspace, &source, &dest, label, true, team) {
|
|
3349
3429
|
Ok(report) => Ok(json!({
|
|
3350
3430
|
"ok": true,
|
|
@@ -3370,6 +3450,13 @@ pub mod lifecycle_port {
|
|
|
3370
3450
|
team: Option<&str>,
|
|
3371
3451
|
) -> Result<Value, CliError> {
|
|
3372
3452
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3453
|
+
#[cfg(unix)]
|
|
3454
|
+
crate::contract_runtime::framework::require_legacy_lifecycle(
|
|
3455
|
+
workspace,
|
|
3456
|
+
team,
|
|
3457
|
+
Some(agent),
|
|
3458
|
+
"remove-agent",
|
|
3459
|
+
)?;
|
|
3373
3460
|
match crate::lifecycle::remove_agent_flag_requirements(workspace, &agent_id, team) {
|
|
3374
3461
|
Ok(requirements) => {
|
|
3375
3462
|
if !remove_agent_missing_flags(from_spec, confirm, force, &requirements).is_empty()
|
|
@@ -3712,7 +3799,13 @@ pub mod lifecycle_port {
|
|
|
3712
3799
|
// tool-set load has not been confirmed yet (PendingToolLoad).
|
|
3713
3800
|
let incomplete_session_capture_agents =
|
|
3714
3801
|
launch.session_capture_incomplete_agents.clone();
|
|
3715
|
-
let
|
|
3802
|
+
let agent_ids: Vec<_> = launch
|
|
3803
|
+
.started
|
|
3804
|
+
.iter()
|
|
3805
|
+
.map(|agent| agent.agent_id.as_str())
|
|
3806
|
+
.chain(launch.contract_started.iter().map(|id| id.as_str()))
|
|
3807
|
+
.collect();
|
|
3808
|
+
let all_spawned = !agent_ids.is_empty();
|
|
3716
3809
|
let leader_receiver_attached = launch.leader_receiver_attached;
|
|
3717
3810
|
let all_resumable_have_session = incomplete_session_capture_agents.is_empty();
|
|
3718
3811
|
let all_workers_spawned = all_spawned;
|
|
@@ -3840,7 +3933,7 @@ pub mod lifecycle_port {
|
|
|
3840
3933
|
"ready": readiness_json.get("ready").cloned().unwrap_or(Value::Bool(false)),
|
|
3841
3934
|
"session_name": session_name.as_str(),
|
|
3842
3935
|
"team": team,
|
|
3843
|
-
"agent_ids":
|
|
3936
|
+
"agent_ids": agent_ids,
|
|
3844
3937
|
"dry_run": launch.dry_run,
|
|
3845
3938
|
"next_actions": next_actions,
|
|
3846
3939
|
"attach_commands": attach_commands,
|
|
@@ -3961,9 +4054,8 @@ pub mod lifecycle_port {
|
|
|
3961
4054
|
.is_some());
|
|
3962
4055
|
}
|
|
3963
4056
|
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
let value = quick_start_value(crate::lifecycle::QuickStartReport::Ready {
|
|
4057
|
+
fn pending_tool_load_report() -> crate::lifecycle::QuickStartReport {
|
|
4058
|
+
crate::lifecycle::QuickStartReport::Ready {
|
|
3967
4059
|
session_name: crate::transport::SessionName::new("team-demo"),
|
|
3968
4060
|
launch: Box::new(crate::lifecycle::LaunchReport {
|
|
3969
4061
|
session_name: crate::transport::SessionName::new("team-demo"),
|
|
@@ -3979,6 +4071,7 @@ pub mod lifecycle_port {
|
|
|
3979
4071
|
provider_projects_root: None,
|
|
3980
4072
|
managed_mcp_config: false,
|
|
3981
4073
|
}],
|
|
4074
|
+
contract_started: Vec::new(),
|
|
3982
4075
|
dry_run: false,
|
|
3983
4076
|
tmux_endpoint: None,
|
|
3984
4077
|
routes: Vec::new(),
|
|
@@ -3991,7 +4084,63 @@ pub mod lifecycle_port {
|
|
|
3991
4084
|
attach_commands: Vec::new(),
|
|
3992
4085
|
worker_readiness: crate::lifecycle::QuickStartReadiness::PendingToolLoad,
|
|
3993
4086
|
team: "team-demo".to_string(),
|
|
3994
|
-
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
#[test]
|
|
4091
|
+
fn contract_start_receipts_join_summary_without_bypassing_leader_binding() {
|
|
4092
|
+
for (legacy, contract) in [(false, true), (true, true), (true, false), (false, false)] {
|
|
4093
|
+
for bound in [false, true] {
|
|
4094
|
+
let mut report = pending_tool_load_report();
|
|
4095
|
+
let crate::lifecycle::QuickStartReport::Ready { launch, .. } = &mut report else {
|
|
4096
|
+
panic!("ready fixture required");
|
|
4097
|
+
};
|
|
4098
|
+
if !legacy {
|
|
4099
|
+
launch.started.clear();
|
|
4100
|
+
}
|
|
4101
|
+
if contract {
|
|
4102
|
+
launch
|
|
4103
|
+
.contract_started
|
|
4104
|
+
.push(crate::model::ids::AgentId::new("kiro-worker"));
|
|
4105
|
+
}
|
|
4106
|
+
launch.leader_receiver_attached = bound;
|
|
4107
|
+
launch.leader_bind_stage = (!bound).then(|| "caller_pane".into());
|
|
4108
|
+
launch.leader_bind_reason = (!bound).then(|| "caller_pane_missing".into());
|
|
4109
|
+
assert_eq!(launch.started.len(), usize::from(legacy));
|
|
4110
|
+
let expected: Vec<_> = [(legacy, "worker"), (contract, "kiro-worker")]
|
|
4111
|
+
.into_iter()
|
|
4112
|
+
.filter_map(|(started, id)| started.then_some(id))
|
|
4113
|
+
.collect();
|
|
4114
|
+
let value = quick_start_value(report);
|
|
4115
|
+
assert_eq!(value["agent_ids"], json!(expected));
|
|
4116
|
+
assert_eq!(
|
|
4117
|
+
value["readiness"]["all_workers_spawned"],
|
|
4118
|
+
json!(legacy || contract)
|
|
4119
|
+
);
|
|
4120
|
+
assert_eq!(value["readiness"]["all_spawned"], json!(legacy || contract));
|
|
4121
|
+
assert_eq!(value["readiness"]["leader_receiver_attached"], json!(bound));
|
|
4122
|
+
assert_eq!(value["ok"], json!((legacy || contract) && bound));
|
|
4123
|
+
assert_eq!(value["ready"], json!((legacy || contract) && bound));
|
|
4124
|
+
if !bound {
|
|
4125
|
+
assert_eq!(value["status"], "leader_binding_incomplete");
|
|
4126
|
+
assert_eq!(value["reason"], "caller_pane_missing");
|
|
4127
|
+
} else {
|
|
4128
|
+
assert_eq!(value["status"], "pending_tool_load");
|
|
4129
|
+
}
|
|
4130
|
+
let mut compact = value.clone();
|
|
4131
|
+
compact_quick_start_value(&mut compact);
|
|
4132
|
+
assert_eq!(
|
|
4133
|
+
compact["worker_readiness"]["all_workers_spawned"],
|
|
4134
|
+
json!(legacy || contract)
|
|
4135
|
+
);
|
|
4136
|
+
assert_eq!(compact["ready"], value["ready"]);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
#[test]
|
|
4142
|
+
fn pending_tool_load_success_output_explains_bound_send_without_doctor() {
|
|
4143
|
+
let value = quick_start_value(pending_tool_load_report());
|
|
3995
4144
|
assert_eq!(
|
|
3996
4145
|
value.get("summary").and_then(Value::as_str),
|
|
3997
4146
|
Some("team started; leader bound; send a task next (worker tool load unverified): team-demo")
|