@team-agent/installer 0.5.31 → 0.5.33
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 +0 -313
- package/crates/team-agent/src/cli/diagnose.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +3 -157
- package/crates/team-agent/src/cli/send.rs +0 -241
- package/crates/team-agent/src/cli/spec.rs +0 -16
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -60
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +4 -54
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/types.rs +1 -64
- package/crates/team-agent/src/coordinator/health.rs +1 -1
- package/crates/team-agent/src/coordinator/tick.rs +111 -2
- package/crates/team-agent/src/db/agent_health_capture.rs +24 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +14 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +27 -2
- package/crates/team-agent/src/messaging/helpers.rs +13 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/packaging/mod.rs +1 -1
- package/crates/team-agent/src/packaging/types.rs +1 -1
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +5 -20
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +0 -374
- package/crates/team-agent/src/cli/tests/verb_settle.rs +0 -238
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
use super::*;
|
|
2
|
-
|
|
3
|
-
const REPAIR_SPEC_TEMPLATE: &str = r#"version: 1
|
|
4
|
-
team:
|
|
5
|
-
name: "repair-team"
|
|
6
|
-
mode: "supervisor_worker"
|
|
7
|
-
objective: "Exercise repair-state."
|
|
8
|
-
workspace: "__WS__"
|
|
9
|
-
leader:
|
|
10
|
-
id: "leader"
|
|
11
|
-
role: "leader"
|
|
12
|
-
provider: "fake"
|
|
13
|
-
model: null
|
|
14
|
-
tools:
|
|
15
|
-
- "fs_read"
|
|
16
|
-
- "fs_list"
|
|
17
|
-
- "mcp_team"
|
|
18
|
-
context_policy:
|
|
19
|
-
keep_user_thread: true
|
|
20
|
-
receive_worker_outputs: "structured_only"
|
|
21
|
-
max_worker_result_tokens: 2000
|
|
22
|
-
agents:
|
|
23
|
-
- id: "fake_impl"
|
|
24
|
-
role: "implementation_engineer"
|
|
25
|
-
provider: "fake"
|
|
26
|
-
model: null
|
|
27
|
-
working_directory: "__WS__"
|
|
28
|
-
system_prompt:
|
|
29
|
-
inline: "Handle fake implementation tasks."
|
|
30
|
-
file: null
|
|
31
|
-
tools:
|
|
32
|
-
- "fs_read"
|
|
33
|
-
- "mcp_team"
|
|
34
|
-
permission_mode: "restricted"
|
|
35
|
-
preferred_for:
|
|
36
|
-
- "implementation"
|
|
37
|
-
avoid_for: []
|
|
38
|
-
output_contract:
|
|
39
|
-
format: "result_envelope_v1"
|
|
40
|
-
required_fields:
|
|
41
|
-
- "task_id"
|
|
42
|
-
- "status"
|
|
43
|
-
- "summary"
|
|
44
|
-
- "artifacts"
|
|
45
|
-
routing:
|
|
46
|
-
default_assignee: "leader"
|
|
47
|
-
rules:
|
|
48
|
-
- id: "implementation-to-fake"
|
|
49
|
-
match:
|
|
50
|
-
type:
|
|
51
|
-
- "implementation"
|
|
52
|
-
assign_to: "fake_impl"
|
|
53
|
-
priority: 10
|
|
54
|
-
communication:
|
|
55
|
-
protocol: "mcp_inbox"
|
|
56
|
-
topology: "leader_centered"
|
|
57
|
-
worker_to_worker: true
|
|
58
|
-
ack_timeout_sec: 2
|
|
59
|
-
result_format: "result_envelope_v1"
|
|
60
|
-
message_store:
|
|
61
|
-
sqlite: ".team/runtime/team.db"
|
|
62
|
-
mirror_files: ".team/messages"
|
|
63
|
-
runtime:
|
|
64
|
-
backend: "tmux"
|
|
65
|
-
display_backend: "none"
|
|
66
|
-
session_name: "team-agent-repair"
|
|
67
|
-
auto_launch: true
|
|
68
|
-
require_user_approval_before_launch: false
|
|
69
|
-
max_active_agents: 1
|
|
70
|
-
startup_order:
|
|
71
|
-
- "fake_impl"
|
|
72
|
-
context:
|
|
73
|
-
state_file: "team_state.md"
|
|
74
|
-
artifact_dir: ".team/artifacts"
|
|
75
|
-
log_dir: ".team/logs"
|
|
76
|
-
summarization:
|
|
77
|
-
worker_full_logs: "retain_outside_leader_context"
|
|
78
|
-
state_update: "after_each_result"
|
|
79
|
-
tasks:
|
|
80
|
-
- id: "task_impl"
|
|
81
|
-
title: "Fake implementation"
|
|
82
|
-
type: "implementation"
|
|
83
|
-
assignee: null
|
|
84
|
-
deps: []
|
|
85
|
-
acceptance:
|
|
86
|
-
- "fake result collected"
|
|
87
|
-
status: "pending"
|
|
88
|
-
"#;
|
|
89
|
-
|
|
90
|
-
fn seed_repair_workspace(ws: &std::path::Path) {
|
|
91
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
92
|
-
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
93
|
-
let spec_path = ws.join("team.spec.yaml");
|
|
94
|
-
std::fs::write(
|
|
95
|
-
&spec_path,
|
|
96
|
-
REPAIR_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
97
|
-
)
|
|
98
|
-
.unwrap();
|
|
99
|
-
crate::state::persist::save_runtime_state(
|
|
100
|
-
ws,
|
|
101
|
-
&json!({
|
|
102
|
-
"spec_path": spec_path.to_string_lossy(),
|
|
103
|
-
"session_name": "team-agent-repair",
|
|
104
|
-
"leader": {"id": "leader"},
|
|
105
|
-
"agents": {"fake_impl": {"status": "stopped", "provider": "fake"}},
|
|
106
|
-
"tasks": [{
|
|
107
|
-
"id": "task_impl",
|
|
108
|
-
"title": "Fake implementation",
|
|
109
|
-
"type": "implementation",
|
|
110
|
-
"assignee": null,
|
|
111
|
-
"deps": [],
|
|
112
|
-
"acceptance": ["fake result collected"],
|
|
113
|
-
"status": "pending"
|
|
114
|
-
}]
|
|
115
|
-
}),
|
|
116
|
-
)
|
|
117
|
-
.unwrap();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
fn repair_args(
|
|
121
|
-
ws: &std::path::Path,
|
|
122
|
-
status: &str,
|
|
123
|
-
summary: Option<&str>,
|
|
124
|
-
json: bool,
|
|
125
|
-
) -> RepairStateArgs {
|
|
126
|
-
RepairStateArgs {
|
|
127
|
-
workspace: ws.to_path_buf(),
|
|
128
|
-
task_id: "task_impl".to_string(),
|
|
129
|
-
assignee: Some("fake_impl".to_string()),
|
|
130
|
-
status: status.to_string(),
|
|
131
|
-
summary: summary.map(str::to_string),
|
|
132
|
-
json,
|
|
133
|
-
team: None,
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fn team_repair_state(ws: &std::path::Path, team: &str) -> serde_json::Value {
|
|
138
|
-
json!({
|
|
139
|
-
"active_team_key": team,
|
|
140
|
-
"status": "alive",
|
|
141
|
-
"spec_path": ws.join(".team").join("runtime").join(team).join("team.spec.yaml").to_string_lossy(),
|
|
142
|
-
"session_name": format!("team-agent-{team}"),
|
|
143
|
-
"leader": {"id": "leader"},
|
|
144
|
-
"agents": {"fake_impl": {"status": "stopped", "provider": "fake"}},
|
|
145
|
-
"tasks": [{
|
|
146
|
-
"id": "task_impl",
|
|
147
|
-
"title": format!("{team} implementation"),
|
|
148
|
-
"type": "implementation",
|
|
149
|
-
"assignee": null,
|
|
150
|
-
"deps": [],
|
|
151
|
-
"acceptance": ["fake result collected"],
|
|
152
|
-
"status": "pending"
|
|
153
|
-
}]
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fn seed_two_team_repair_workspace(ws: &std::path::Path) {
|
|
158
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
159
|
-
for team in ["alpha", "beta"] {
|
|
160
|
-
let spec_dir = ws.join(".team").join("runtime").join(team);
|
|
161
|
-
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
162
|
-
std::fs::write(
|
|
163
|
-
spec_dir.join("team.spec.yaml"),
|
|
164
|
-
REPAIR_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
165
|
-
)
|
|
166
|
-
.unwrap();
|
|
167
|
-
}
|
|
168
|
-
crate::state::persist::save_runtime_state(
|
|
169
|
-
ws,
|
|
170
|
-
&json!({
|
|
171
|
-
"active_team_key": "alpha",
|
|
172
|
-
"teams": {
|
|
173
|
-
"alpha": team_repair_state(ws, "alpha"),
|
|
174
|
-
"beta": team_repair_state(ws, "beta"),
|
|
175
|
-
}
|
|
176
|
-
}),
|
|
177
|
-
)
|
|
178
|
-
.unwrap();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Golden source:
|
|
182
|
-
// - cli/parser.py:303-310 registers `repair-state --workspace --task --assignee --status --summary --json`.
|
|
183
|
-
// - cli/commands.py:196-203 delegates all five args to `runtime.repair_state`.
|
|
184
|
-
// - diagnose/quick_start.py:285-324:
|
|
185
|
-
// * loads state + spec, validates assignee against spec agents plus leader id,
|
|
186
|
-
// * validates status against task_graph.py TASK_STATUSES,
|
|
187
|
-
// * before/after are three-field projections `{assignee,status,last_result_summary}`,
|
|
188
|
-
// * summary writes `last_result_summary` (not `summary`),
|
|
189
|
-
// * writes EventLog event `repair_state.task` with task_id/before/after,
|
|
190
|
-
// * returns `{ok,task_id,before,after,state_file}` in that insertion order.
|
|
191
|
-
// - task_graph.py:5-14 legal statuses are exactly:
|
|
192
|
-
// blocked,cancelled,done,failed,needs_retry,pending,ready,running.
|
|
193
|
-
//
|
|
194
|
-
// Golden probe:
|
|
195
|
-
// PYTHONPATH=/Users/alauda/Documents/code/team-agent-public/src python3 /tmp/probe_repair_state_cli.py
|
|
196
|
-
#[test]
|
|
197
|
-
fn repair_state_success_json_human_and_event_byte_shape() {
|
|
198
|
-
let ws = tmp_workspace();
|
|
199
|
-
seed_repair_workspace(&ws);
|
|
200
|
-
|
|
201
|
-
let json_result = cmd_repair_state(&repair_args(&ws, "done", Some("patched"), true))
|
|
202
|
-
.expect("repair-state success should return ok");
|
|
203
|
-
assert_eq!(json_result.exit, ExitCode::Ok);
|
|
204
|
-
assert_eq!(
|
|
205
|
-
emit(&json_result.output, true).unwrap(),
|
|
206
|
-
format!(
|
|
207
|
-
"{{\n \"after\": {{\n \"assignee\": \"fake_impl\",\n \"last_result_summary\": \"patched\",\n \"status\": \"done\"\n }},\n \"before\": {{\n \"assignee\": null,\n \"last_result_summary\": null,\n \"status\": \"pending\"\n }},\n \"ok\": true,\n \"state_file\": \"{}\",\n \"task_id\": \"task_impl\"\n}}",
|
|
208
|
-
ws.join("team_state.md").to_string_lossy()
|
|
209
|
-
),
|
|
210
|
-
"repair-state --json must match Python pretty sorted JSON byte shape",
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
let event_line = std::fs::read_to_string(ws.join(".team/logs/events.jsonl"))
|
|
214
|
-
.expect("repair-state must write events.jsonl")
|
|
215
|
-
.lines()
|
|
216
|
-
.last()
|
|
217
|
-
.expect("repair-state must append an event")
|
|
218
|
-
.to_string();
|
|
219
|
-
assert!(
|
|
220
|
-
event_line.starts_with("{\"after\": {\"assignee\": \"fake_impl\", \"last_result_summary\": \"patched\", \"status\": \"done\"}, \"before\": {\"assignee\": null, \"last_result_summary\": null, \"status\": \"pending\"}, \"event\": \"repair_state.task\", \"task_id\": \"task_impl\", \"ts\": \""),
|
|
221
|
-
"repair_state.task event must be Python sort_keys JSON with after/before/event/task_id/ts order; got {event_line}",
|
|
222
|
-
);
|
|
223
|
-
assert!(event_line.ends_with("\"}"), "event line must end with timestamp string; got {event_line}");
|
|
224
|
-
|
|
225
|
-
let ws_human = tmp_workspace();
|
|
226
|
-
seed_repair_workspace(&ws_human);
|
|
227
|
-
let human_result = cmd_repair_state(&repair_args(&ws_human, "done", Some("patched"), false))
|
|
228
|
-
.expect("repair-state human success should return ok");
|
|
229
|
-
assert_eq!(
|
|
230
|
-
emit(&human_result.output, false).unwrap(),
|
|
231
|
-
format!(
|
|
232
|
-
"ok: True\ntask_id: task_impl\nbefore: {{\"assignee\": null, \"status\": \"pending\", \"last_result_summary\": null}}\nafter: {{\"assignee\": \"fake_impl\", \"status\": \"done\", \"last_result_summary\": \"patched\"}}\nstate_file: {}",
|
|
233
|
-
ws_human.join("team_state.md").to_string_lossy()
|
|
234
|
-
),
|
|
235
|
-
"repair-state human output must preserve Python returned-dict and nested-dict insertion order",
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
239
|
-
assert_eq!(
|
|
240
|
-
state["tasks"][0]["last_result_summary"],
|
|
241
|
-
json!("patched"),
|
|
242
|
-
"golden writes summary into last_result_summary, not summary",
|
|
243
|
-
);
|
|
244
|
-
assert!(
|
|
245
|
-
state["tasks"][0].get("summary").is_none(),
|
|
246
|
-
"golden does not create a task.summary field during repair-state",
|
|
247
|
-
);
|
|
248
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
249
|
-
let _ = std::fs::remove_dir_all(&ws_human);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
#[test]
|
|
253
|
-
fn repair_state_rejects_status_outside_task_statuses() {
|
|
254
|
-
let ws = tmp_workspace();
|
|
255
|
-
seed_repair_workspace(&ws);
|
|
256
|
-
let legal_statuses = vec![
|
|
257
|
-
"blocked",
|
|
258
|
-
"cancelled",
|
|
259
|
-
"done",
|
|
260
|
-
"failed",
|
|
261
|
-
"needs_retry",
|
|
262
|
-
"pending",
|
|
263
|
-
"ready",
|
|
264
|
-
"running",
|
|
265
|
-
];
|
|
266
|
-
assert_eq!(
|
|
267
|
-
legal_statuses,
|
|
268
|
-
vec![
|
|
269
|
-
"blocked",
|
|
270
|
-
"cancelled",
|
|
271
|
-
"done",
|
|
272
|
-
"failed",
|
|
273
|
-
"needs_retry",
|
|
274
|
-
"pending",
|
|
275
|
-
"ready",
|
|
276
|
-
"running"
|
|
277
|
-
],
|
|
278
|
-
"golden TASK_STATUSES from task_graph.py:5 must stay locked",
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
let err = cmd_repair_state(&repair_args(&ws, "assigned", None, true))
|
|
282
|
-
.expect_err("status outside TASK_STATUSES must error");
|
|
283
|
-
assert_eq!(err.to_string(), "unknown task status for repair: assigned");
|
|
284
|
-
let payload = err.to_payload(&ws.join(".team/logs/cli-error-123.log"), "repair-state");
|
|
285
|
-
assert_eq!(
|
|
286
|
-
serde_json::to_string(&payload).unwrap(),
|
|
287
|
-
format!(
|
|
288
|
-
"{{\"ok\":false,\"error\":\"unknown task status for repair: assigned\",\"action\":\"run `team-agent doctor` or inspect the log path shown here\",\"log\":\"{}\"}}",
|
|
289
|
-
ws.join(".team/logs/cli-error-123.log").to_string_lossy()
|
|
290
|
-
),
|
|
291
|
-
"repair-state --json error envelope must match Python compact key order and text",
|
|
292
|
-
);
|
|
293
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
#[test]
|
|
297
|
-
fn repair_state_explicit_team_updates_selected_team_not_active_team() {
|
|
298
|
-
let ws = tmp_workspace();
|
|
299
|
-
seed_two_team_repair_workspace(&ws);
|
|
300
|
-
let mut args = repair_args(&ws, "done", Some("patched beta"), true);
|
|
301
|
-
args.team = Some("beta".to_string());
|
|
302
|
-
|
|
303
|
-
let cmd = cmd_repair_state(&args).expect("repair-state --team beta should update beta");
|
|
304
|
-
assert_eq!(cmd.exit, ExitCode::Ok);
|
|
305
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
306
|
-
assert_eq!(
|
|
307
|
-
state["teams"]["alpha"]["tasks"][0]["status"],
|
|
308
|
-
json!("pending"),
|
|
309
|
-
"repair-state --team beta must not mutate active/default alpha"
|
|
310
|
-
);
|
|
311
|
-
assert_eq!(
|
|
312
|
-
state["teams"]["beta"]["tasks"][0]["status"],
|
|
313
|
-
json!("done"),
|
|
314
|
-
"repair-state must consume args.team and update beta"
|
|
315
|
-
);
|
|
316
|
-
assert_eq!(
|
|
317
|
-
state["teams"]["beta"]["tasks"][0]["last_result_summary"],
|
|
318
|
-
json!("patched beta")
|
|
319
|
-
);
|
|
320
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// CONTRACT (real-machine repair_state product FAIL): rt-host-d ran `repair-state --task rp1 --status done`
|
|
324
|
-
// in a quick-start workspace whose spec lives in a teamdir (state.spec_path -> <ws>/teamdir/team.spec.yaml),
|
|
325
|
-
// NOT at <ws>/team.spec.yaml. Rust cmd_repair_state calls load_team_spec(workspace) which HARDCODES
|
|
326
|
-
// workspace.join("team.spec.yaml") (adapters.rs:982) -> read_to_string fails 'No such file or directory'.
|
|
327
|
-
// golden (quick_start.py:295) resolves spec_path = state.get("spec_path", workspace/"team.spec.yaml") and
|
|
328
|
-
// SUCCEEDS. NB: golden ALSO writes team_state.md (write_team_state -> state.py mkdir+write) and returns
|
|
329
|
-
// state_file=team_state.md, so the fix is NOT to drop write_team_state (that diverges from golden and breaks
|
|
330
|
-
// repair_state_success_json_human_and_event_byte_shape) — it is to resolve the spec from state.spec_path
|
|
331
|
-
// (Rust already has load_team_spec_optional(workspace, state) that does exactly this).
|
|
332
|
-
#[test]
|
|
333
|
-
fn contract_repair_state_resolves_spec_from_state_spec_path_teamdir_layout() {
|
|
334
|
-
let ws = tmp_workspace();
|
|
335
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
336
|
-
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
337
|
-
let teamdir = ws.join("teamdir");
|
|
338
|
-
std::fs::create_dir_all(&teamdir).unwrap();
|
|
339
|
-
let spec_path = teamdir.join("team.spec.yaml");
|
|
340
|
-
std::fs::write(
|
|
341
|
-
&spec_path,
|
|
342
|
-
REPAIR_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
343
|
-
)
|
|
344
|
-
.unwrap();
|
|
345
|
-
// NOTE: NO <ws>/team.spec.yaml (the hardcoded path) and NO pre-existing team_state.md.
|
|
346
|
-
crate::state::persist::save_runtime_state(
|
|
347
|
-
&ws,
|
|
348
|
-
&json!({
|
|
349
|
-
"spec_path": spec_path.to_string_lossy(),
|
|
350
|
-
"session_name": "team-agent-repair",
|
|
351
|
-
"leader": {"id": "leader"},
|
|
352
|
-
"agents": {"fake_impl": {"status": "running", "provider": "fake"}},
|
|
353
|
-
"tasks": [{
|
|
354
|
-
"id": "task_impl", "title": "x", "type": "implementation",
|
|
355
|
-
"assignee": null, "deps": [], "acceptance": ["a"], "status": "pending"
|
|
356
|
-
}]
|
|
357
|
-
}),
|
|
358
|
-
)
|
|
359
|
-
.unwrap();
|
|
360
|
-
|
|
361
|
-
let cmd = cmd_repair_state(&repair_args(&ws, "done", Some("patched"), true)).expect(
|
|
362
|
-
"CONTRACT: repair-state --status done must resolve the spec from state.spec_path (golden \
|
|
363
|
-
quick_start.py:295), not the hardcoded <ws>/team.spec.yaml (adapters.rs:982) which is absent in \
|
|
364
|
-
the teamdir layout -> currently io 'No such file or directory'",
|
|
365
|
-
);
|
|
366
|
-
assert_eq!(cmd.exit, ExitCode::Ok);
|
|
367
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
368
|
-
assert_eq!(
|
|
369
|
-
state["tasks"][0]["status"],
|
|
370
|
-
json!("done"),
|
|
371
|
-
"the task must persist status=done after repair"
|
|
372
|
-
);
|
|
373
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
374
|
-
}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
use super::*;
|
|
2
|
-
|
|
3
|
-
const SETTLE_SPEC_TEMPLATE: &str = r#"version: 1
|
|
4
|
-
team:
|
|
5
|
-
name: "fake-e2e"
|
|
6
|
-
mode: "supervisor_worker"
|
|
7
|
-
objective: "Exercise settle."
|
|
8
|
-
workspace: "__WS__"
|
|
9
|
-
leader:
|
|
10
|
-
id: "leader"
|
|
11
|
-
role: "leader"
|
|
12
|
-
provider: "fake"
|
|
13
|
-
model: null
|
|
14
|
-
tools:
|
|
15
|
-
- "fs_read"
|
|
16
|
-
- "fs_list"
|
|
17
|
-
- "mcp_team"
|
|
18
|
-
context_policy:
|
|
19
|
-
keep_user_thread: true
|
|
20
|
-
receive_worker_outputs: "structured_only"
|
|
21
|
-
max_worker_result_tokens: 2000
|
|
22
|
-
agents:
|
|
23
|
-
- id: "fake_impl"
|
|
24
|
-
role: "implementation_engineer"
|
|
25
|
-
provider: "fake"
|
|
26
|
-
model: null
|
|
27
|
-
working_directory: "__WS__"
|
|
28
|
-
system_prompt:
|
|
29
|
-
inline: "Handle fake implementation tasks."
|
|
30
|
-
file: null
|
|
31
|
-
tools:
|
|
32
|
-
- "fs_read"
|
|
33
|
-
- "fs_write"
|
|
34
|
-
- "fs_list"
|
|
35
|
-
- "execute_bash"
|
|
36
|
-
- "git_diff"
|
|
37
|
-
- "mcp_team"
|
|
38
|
-
- "provider_builtin"
|
|
39
|
-
permission_mode: "restricted"
|
|
40
|
-
preferred_for:
|
|
41
|
-
- "implementation"
|
|
42
|
-
avoid_for: []
|
|
43
|
-
output_contract:
|
|
44
|
-
format: "result_envelope_v1"
|
|
45
|
-
required_fields:
|
|
46
|
-
- "task_id"
|
|
47
|
-
- "status"
|
|
48
|
-
- "summary"
|
|
49
|
-
- "artifacts"
|
|
50
|
-
routing:
|
|
51
|
-
default_assignee: "leader"
|
|
52
|
-
rules:
|
|
53
|
-
- id: "implementation-to-fake"
|
|
54
|
-
match:
|
|
55
|
-
type:
|
|
56
|
-
- "implementation"
|
|
57
|
-
assign_to: "fake_impl"
|
|
58
|
-
priority: 10
|
|
59
|
-
communication:
|
|
60
|
-
protocol: "mcp_inbox"
|
|
61
|
-
topology: "leader_centered"
|
|
62
|
-
worker_to_worker: true
|
|
63
|
-
ack_timeout_sec: 2
|
|
64
|
-
result_format: "result_envelope_v1"
|
|
65
|
-
message_store:
|
|
66
|
-
sqlite: ".team/runtime/team.db"
|
|
67
|
-
mirror_files: ".team/messages"
|
|
68
|
-
runtime:
|
|
69
|
-
backend: "tmux"
|
|
70
|
-
display_backend: "none"
|
|
71
|
-
session_name: "team-agent-fake-e2e"
|
|
72
|
-
auto_launch: true
|
|
73
|
-
require_user_approval_before_launch: false
|
|
74
|
-
max_active_agents: 1
|
|
75
|
-
startup_order:
|
|
76
|
-
- "fake_impl"
|
|
77
|
-
context:
|
|
78
|
-
state_file: "team_state.md"
|
|
79
|
-
artifact_dir: ".team/artifacts"
|
|
80
|
-
log_dir: ".team/logs"
|
|
81
|
-
summarization:
|
|
82
|
-
worker_full_logs: "retain_outside_leader_context"
|
|
83
|
-
state_update: "after_each_result"
|
|
84
|
-
tasks:
|
|
85
|
-
- id: "task_impl"
|
|
86
|
-
title: "Fake implementation"
|
|
87
|
-
type: "implementation"
|
|
88
|
-
assignee: null
|
|
89
|
-
deps: []
|
|
90
|
-
acceptance:
|
|
91
|
-
- "fake result collected"
|
|
92
|
-
status: "pending"
|
|
93
|
-
"#;
|
|
94
|
-
|
|
95
|
-
fn seed_settle_workspace(ws: &std::path::Path) {
|
|
96
|
-
std::fs::create_dir_all(ws.join(".team").join("logs")).unwrap();
|
|
97
|
-
std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
|
|
98
|
-
std::fs::write(
|
|
99
|
-
ws.join("team.spec.yaml"),
|
|
100
|
-
SETTLE_SPEC_TEMPLATE.replace("__WS__", &ws.to_string_lossy()),
|
|
101
|
-
)
|
|
102
|
-
.unwrap();
|
|
103
|
-
crate::state::persist::save_runtime_state(
|
|
104
|
-
ws,
|
|
105
|
-
&json!({
|
|
106
|
-
"spec_path": ws.join("team.spec.yaml").to_string_lossy(),
|
|
107
|
-
"session_name": "team-agent-fake-e2e",
|
|
108
|
-
"leader": {"id": "leader"},
|
|
109
|
-
"agents": {"fake_impl": {"status": "stopped", "provider": "fake"}},
|
|
110
|
-
"tasks": [{
|
|
111
|
-
"id": "task_impl",
|
|
112
|
-
"title": "Fake implementation",
|
|
113
|
-
"type": "implementation",
|
|
114
|
-
"assignee": null,
|
|
115
|
-
"deps": [],
|
|
116
|
-
"acceptance": ["fake result collected"],
|
|
117
|
-
"status": "pending"
|
|
118
|
-
}]
|
|
119
|
-
}),
|
|
120
|
-
)
|
|
121
|
-
.unwrap();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Golden source:
|
|
125
|
-
// - cli/parser.py:177-180 registers `settle --workspace . --json`; no timeout or polling args.
|
|
126
|
-
// - cli/commands.py:86-87 delegates to `runtime.settle(Path(args.workspace).resolve())`.
|
|
127
|
-
// - diagnose/quick_start.py:269-284: `settle` synchronously calls `collect(workspace)`,
|
|
128
|
-
// then `status(workspace, as_json=True)`, writes `settle-<int(time.time())>.json`,
|
|
129
|
-
// and returns `{ok, summary, next_actions, details_log, collect}`.
|
|
130
|
-
// - cli/helpers.py:12-16 success `--json` uses pretty sorted JSON; human output uses dict
|
|
131
|
-
// insertion order from the returned dict.
|
|
132
|
-
//
|
|
133
|
-
// Golden probe:
|
|
134
|
-
// PYTHONPATH=/Users/alauda/Documents/code/team-agent-public/src python3 /tmp/probe_settle_cli.py
|
|
135
|
-
// With a fake `tmux` executable returning exit 1, golden exits 0 and reports
|
|
136
|
-
// summary `collected 0 result(s)`.
|
|
137
|
-
#[test]
|
|
138
|
-
fn settle_success_json_byte_shape_wraps_collect() {
|
|
139
|
-
let ws = tmp_workspace();
|
|
140
|
-
seed_settle_workspace(&ws);
|
|
141
|
-
|
|
142
|
-
let result = cmd_settle(&SettleArgs {
|
|
143
|
-
team: None,
|
|
144
|
-
workspace: ws.clone(),
|
|
145
|
-
json: true,
|
|
146
|
-
})
|
|
147
|
-
.expect("settle should return an ok result for an empty valid workspace");
|
|
148
|
-
|
|
149
|
-
assert_eq!(result.exit, ExitCode::Ok);
|
|
150
|
-
let CmdOutput::Json(value) = &result.output else {
|
|
151
|
-
panic!("settle must return a JSON dict");
|
|
152
|
-
};
|
|
153
|
-
assert_eq!(value["ok"], json!(true));
|
|
154
|
-
assert_eq!(value["summary"], json!("collected 0 result(s)"));
|
|
155
|
-
assert_eq!(
|
|
156
|
-
value["next_actions"],
|
|
157
|
-
json!(["Review team_state.md and decide whether to continue or shutdown."])
|
|
158
|
-
);
|
|
159
|
-
assert!(
|
|
160
|
-
value["details_log"]
|
|
161
|
-
.as_str()
|
|
162
|
-
.is_some_and(|p| p.contains("/.team/logs/settle-") && p.ends_with(".json")),
|
|
163
|
-
"settle must write a settle-<timestamp>.json details log; got {value}",
|
|
164
|
-
);
|
|
165
|
-
assert_eq!(value["collect"]["ok"], json!(true));
|
|
166
|
-
assert_eq!(value["collect"]["collected"], json!([]));
|
|
167
|
-
assert_eq!(value["collect"]["collected_results"], json!([]));
|
|
168
|
-
assert_eq!(value["collect"]["delivered_messages"], json!([]));
|
|
169
|
-
assert_eq!(value["collect"]["invalid_results"], json!([]));
|
|
170
|
-
assert_eq!(
|
|
171
|
-
value["collect"]["results"],
|
|
172
|
-
json!({
|
|
173
|
-
"total": 0,
|
|
174
|
-
"uncollected": 0,
|
|
175
|
-
"collected": 0,
|
|
176
|
-
"invalid": 0,
|
|
177
|
-
"by_status": {},
|
|
178
|
-
})
|
|
179
|
-
);
|
|
180
|
-
assert_eq!(
|
|
181
|
-
value["collect"]["state_file"],
|
|
182
|
-
json!(ws.join("team_state.md").to_string_lossy().to_string())
|
|
183
|
-
);
|
|
184
|
-
assert_eq!(value["collect"]["coordinator"]["ok"], json!(true));
|
|
185
|
-
assert_eq!(value["collect"]["coordinator"]["status"], json!("started"));
|
|
186
|
-
assert!(
|
|
187
|
-
value["collect"]["coordinator"]["log"]
|
|
188
|
-
.as_str()
|
|
189
|
-
.is_some_and(|p| p.ends_with("/.team/runtime/coordinator.log")),
|
|
190
|
-
"collect.coordinator.log must point at .team/runtime/coordinator.log; got {value}",
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
let expected = serde_json::to_string_pretty(&crate::cli::sort_json(value)).unwrap();
|
|
194
|
-
assert_eq!(
|
|
195
|
-
emit(&result.output, true).unwrap(),
|
|
196
|
-
expected,
|
|
197
|
-
"settle --json must emit Python's json.dumps(indent=2, sort_keys=True) byte shape",
|
|
198
|
-
);
|
|
199
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
#[test]
|
|
203
|
-
fn settle_success_human_output_insertion_order() {
|
|
204
|
-
let ws = tmp_workspace();
|
|
205
|
-
seed_settle_workspace(&ws);
|
|
206
|
-
|
|
207
|
-
let result = cmd_settle(&SettleArgs {
|
|
208
|
-
team: None,
|
|
209
|
-
workspace: ws.clone(),
|
|
210
|
-
json: false,
|
|
211
|
-
})
|
|
212
|
-
.expect("settle should return an ok result for an empty valid workspace");
|
|
213
|
-
|
|
214
|
-
let human = emit(&result.output, false).unwrap();
|
|
215
|
-
assert!(
|
|
216
|
-
human.starts_with(
|
|
217
|
-
"ok: True\nsummary: collected 0 result(s)\nnext_actions: [\"Review team_state.md and decide whether to continue or shutdown.\"]\ndetails_log: "
|
|
218
|
-
),
|
|
219
|
-
"non-json settle output must preserve Python returned-dict insertion order; got {human}",
|
|
220
|
-
);
|
|
221
|
-
assert!(
|
|
222
|
-
human.contains("\ncollect: {\"ok\": true, \"collected\": [], \"collected_results\": []"),
|
|
223
|
-
"non-json settle output must render nested collect as compact JSON after details_log; got {human}",
|
|
224
|
-
);
|
|
225
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Golden source:
|
|
229
|
-
// - cli/parser.py:177-180 default workspace is `.`.
|
|
230
|
-
// Current Rust dispatch has no `settle` arm; this must be RED until the porter routes it.
|
|
231
|
-
#[test]
|
|
232
|
-
fn dispatch_routes_settle_default_workspace() {
|
|
233
|
-
let ws = tmp_workspace();
|
|
234
|
-
seed_settle_workspace(&ws);
|
|
235
|
-
let code = run(&["settle".to_string(), "--json".to_string()], &ws);
|
|
236
|
-
assert_eq!(code, ExitCode::Ok, "`settle --json` must route and exit 0 for default workspace");
|
|
237
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
238
|
-
}
|