@team-agent/installer 0.5.41 → 0.5.43
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 +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +27 -7
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/coordinator/tick.rs +19 -1
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +52 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +195 -82
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +6 -1
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +21 -22
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/repository.rs +27 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +27 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -36,10 +36,26 @@ const RESULT_REQUIRED: &[&str] = &[
|
|
|
36
36
|
|
|
37
37
|
/// `RESULT_COLLECTION_SCHEMAS`(`spec.py:93-99`),有序:(field, required, allowed)。
|
|
38
38
|
const RESULT_COLLECTIONS: &[(&str, &[&str], &[&str])] = &[
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
(
|
|
40
|
+
"changes",
|
|
41
|
+
&["path", "kind", "description"],
|
|
42
|
+
&["path", "kind", "description"],
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
"tests",
|
|
46
|
+
&["command", "status"],
|
|
47
|
+
&["command", "status", "detail"],
|
|
48
|
+
),
|
|
49
|
+
(
|
|
50
|
+
"risks",
|
|
51
|
+
&["severity", "description"],
|
|
52
|
+
&["severity", "description"],
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"artifacts",
|
|
56
|
+
&["path", "description"],
|
|
57
|
+
&["path", "description"],
|
|
58
|
+
),
|
|
43
59
|
("next_actions", &["description"], &["description"]),
|
|
44
60
|
];
|
|
45
61
|
|
|
@@ -61,7 +77,13 @@ pub fn validate_result_envelope(envelope: &Value) -> Result<(), ModelError> {
|
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
/// `spec._check_keys`:非 object → "must be an object";否则 missing(排序)+ unknown(排序)。
|
|
64
|
-
fn check_keys(
|
|
80
|
+
fn check_keys(
|
|
81
|
+
obj: &Value,
|
|
82
|
+
path: &str,
|
|
83
|
+
required: &[&str],
|
|
84
|
+
allowed: &[&str],
|
|
85
|
+
errors: &mut Vec<String>,
|
|
86
|
+
) {
|
|
65
87
|
let Some(map) = obj.as_object() else {
|
|
66
88
|
errors.push(format!("{path}: must be an object"));
|
|
67
89
|
return;
|
|
@@ -146,7 +168,11 @@ fn result_schema_errors(envelope: &Value) -> Vec<String> {
|
|
|
146
168
|
&["passed", "failed", "not_run", "skipped"],
|
|
147
169
|
"invalid test status",
|
|
148
170
|
)),
|
|
149
|
-
"risks" => Some((
|
|
171
|
+
"risks" => Some((
|
|
172
|
+
"severity",
|
|
173
|
+
&["low", "medium", "high"],
|
|
174
|
+
"invalid risk severity",
|
|
175
|
+
)),
|
|
150
176
|
_ => None,
|
|
151
177
|
};
|
|
152
178
|
if let Some((key, valid, msg)) = enum_field {
|
|
@@ -174,14 +200,35 @@ fn result_schema_errors(envelope: &Value) -> Vec<String> {
|
|
|
174
200
|
// 对齐 Python `spec.validate_spec`(golden 由真相源双跑锁死)。
|
|
175
201
|
|
|
176
202
|
const ROOT_KEYS: &[&str] = &[
|
|
177
|
-
"version",
|
|
203
|
+
"version",
|
|
204
|
+
"team",
|
|
205
|
+
"leader",
|
|
206
|
+
"agents",
|
|
207
|
+
"routing",
|
|
208
|
+
"communication",
|
|
209
|
+
"runtime",
|
|
210
|
+
"context",
|
|
211
|
+
"tasks",
|
|
178
212
|
];
|
|
179
213
|
const AUTH_MODES: &[&str] = &["subscription", "official_api", "compatible_api"];
|
|
180
214
|
const VALID_DISPLAY_BACKENDS: &[&str] = &[
|
|
181
|
-
"none",
|
|
215
|
+
"none",
|
|
216
|
+
"tmux_attach",
|
|
217
|
+
"iterm",
|
|
218
|
+
"ghostty",
|
|
219
|
+
"ghostty_window",
|
|
220
|
+
"ghostty_workspace",
|
|
221
|
+
"adaptive",
|
|
182
222
|
];
|
|
183
223
|
const TASK_STATUS_STRS: &[&str] = &[
|
|
184
|
-
"pending",
|
|
224
|
+
"pending",
|
|
225
|
+
"ready",
|
|
226
|
+
"running",
|
|
227
|
+
"blocked",
|
|
228
|
+
"needs_retry",
|
|
229
|
+
"done",
|
|
230
|
+
"failed",
|
|
231
|
+
"cancelled",
|
|
185
232
|
];
|
|
186
233
|
|
|
187
234
|
/// `spec.validate_spec`:basic schema + semantic 校验。失败 → `ModelError::Validation`,
|
|
@@ -192,7 +239,11 @@ pub fn validate_spec(spec: &Yaml, base_dir: &Path) -> Result<(), ModelError> {
|
|
|
192
239
|
if errors.is_empty() {
|
|
193
240
|
return Ok(());
|
|
194
241
|
}
|
|
195
|
-
let joined = errors
|
|
242
|
+
let joined = errors
|
|
243
|
+
.iter()
|
|
244
|
+
.map(|m| format!("- {m}"))
|
|
245
|
+
.collect::<Vec<_>>()
|
|
246
|
+
.join("\n");
|
|
196
247
|
Err(ModelError::Validation(format!(
|
|
197
248
|
"team.spec.yaml validation failed:\n{joined}"
|
|
198
249
|
)))
|
|
@@ -210,7 +261,13 @@ fn is_map_some(v: Option<&Yaml>) -> bool {
|
|
|
210
261
|
}
|
|
211
262
|
|
|
212
263
|
/// `spec._check_keys` 的 yaml::Value 版。
|
|
213
|
-
fn check_keys_y(
|
|
264
|
+
fn check_keys_y(
|
|
265
|
+
obj: Option<&Yaml>,
|
|
266
|
+
path: &str,
|
|
267
|
+
required: &[&str],
|
|
268
|
+
allowed: &[&str],
|
|
269
|
+
errors: &mut Vec<String>,
|
|
270
|
+
) {
|
|
214
271
|
let Some(map) = obj.and_then(Yaml::as_map) else {
|
|
215
272
|
errors.push(format!("{path}: must be an object"));
|
|
216
273
|
return;
|
|
@@ -251,7 +308,13 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
|
|
|
251
308
|
// 0.4.x provider effort MVP step 3: provider_effort is allowed but not required.
|
|
252
309
|
let team_required = &["name", "mode", "objective", "workspace"];
|
|
253
310
|
let team_allowed = &["name", "mode", "objective", "workspace", "provider_effort"];
|
|
254
|
-
check_keys_y(
|
|
311
|
+
check_keys_y(
|
|
312
|
+
spec.get("team"),
|
|
313
|
+
"/team",
|
|
314
|
+
team_required,
|
|
315
|
+
team_allowed,
|
|
316
|
+
&mut e,
|
|
317
|
+
);
|
|
255
318
|
if let Some(team) = spec.get("team") {
|
|
256
319
|
if let Some(raw) = team.get("provider_effort").and_then(Yaml::as_str) {
|
|
257
320
|
if crate::model::enums::ProviderEffort::parse(raw).is_none() {
|
|
@@ -261,13 +324,26 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
|
|
|
261
324
|
}
|
|
262
325
|
}
|
|
263
326
|
}
|
|
264
|
-
let mode = spec
|
|
327
|
+
let mode = spec
|
|
328
|
+
.get("team")
|
|
329
|
+
.and_then(|t| t.get("mode"))
|
|
330
|
+
.and_then(Yaml::as_str);
|
|
265
331
|
if !matches!(mode, Some("supervisor_worker" | "swarm_limited")) {
|
|
266
332
|
e.push("/team/mode: invalid mode".to_string());
|
|
267
333
|
}
|
|
268
334
|
let leader_keys = &["id", "role", "provider", "model", "tools", "context_policy"];
|
|
269
|
-
check_keys_y(
|
|
270
|
-
|
|
335
|
+
check_keys_y(
|
|
336
|
+
spec.get("leader"),
|
|
337
|
+
"/leader",
|
|
338
|
+
leader_keys,
|
|
339
|
+
leader_keys,
|
|
340
|
+
&mut e,
|
|
341
|
+
);
|
|
342
|
+
let cp_keys = &[
|
|
343
|
+
"keep_user_thread",
|
|
344
|
+
"receive_worker_outputs",
|
|
345
|
+
"max_worker_result_tokens",
|
|
346
|
+
];
|
|
271
347
|
check_keys_y(
|
|
272
348
|
spec.get("leader").and_then(|l| l.get("context_policy")),
|
|
273
349
|
"/leader/context_policy",
|
|
@@ -300,13 +376,35 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
|
|
|
300
376
|
|
|
301
377
|
fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
302
378
|
let req = &[
|
|
303
|
-
"id",
|
|
304
|
-
"
|
|
379
|
+
"id",
|
|
380
|
+
"role",
|
|
381
|
+
"provider",
|
|
382
|
+
"model",
|
|
383
|
+
"working_directory",
|
|
384
|
+
"system_prompt",
|
|
385
|
+
"tools",
|
|
386
|
+
"permission_mode",
|
|
387
|
+
"preferred_for",
|
|
388
|
+
"avoid_for",
|
|
389
|
+
"output_contract",
|
|
305
390
|
];
|
|
306
391
|
let allowed = &[
|
|
307
|
-
"id",
|
|
308
|
-
"
|
|
309
|
-
"
|
|
392
|
+
"id",
|
|
393
|
+
"role",
|
|
394
|
+
"provider",
|
|
395
|
+
"model",
|
|
396
|
+
"working_directory",
|
|
397
|
+
"system_prompt",
|
|
398
|
+
"tools",
|
|
399
|
+
"permission_mode",
|
|
400
|
+
"preferred_for",
|
|
401
|
+
"avoid_for",
|
|
402
|
+
"output_contract",
|
|
403
|
+
"paused",
|
|
404
|
+
"auth_mode",
|
|
405
|
+
"profile",
|
|
406
|
+
"credential_ref",
|
|
407
|
+
"forked_from",
|
|
310
408
|
// 0.4.x provider effort MVP step 3: per-agent effort override (resolved at compile).
|
|
311
409
|
"effort",
|
|
312
410
|
];
|
|
@@ -333,18 +431,47 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
333
431
|
Some(_) => {}
|
|
334
432
|
}
|
|
335
433
|
}
|
|
336
|
-
check_keys_y(
|
|
434
|
+
check_keys_y(
|
|
435
|
+
agent.get("system_prompt"),
|
|
436
|
+
&format!("{path}/system_prompt"),
|
|
437
|
+
&["inline", "file"],
|
|
438
|
+
&["inline", "file"],
|
|
439
|
+
errors,
|
|
440
|
+
);
|
|
337
441
|
check_list_y(agent.get("tools"), &format!("{path}/tools"), errors);
|
|
338
|
-
check_list_y(
|
|
442
|
+
check_list_y(
|
|
443
|
+
agent.get("preferred_for"),
|
|
444
|
+
&format!("{path}/preferred_for"),
|
|
445
|
+
errors,
|
|
446
|
+
);
|
|
339
447
|
check_list_y(agent.get("avoid_for"), &format!("{path}/avoid_for"), errors);
|
|
340
|
-
check_keys_y(
|
|
341
|
-
|
|
342
|
-
|
|
448
|
+
check_keys_y(
|
|
449
|
+
agent.get("output_contract"),
|
|
450
|
+
&format!("{path}/output_contract"),
|
|
451
|
+
&["format", "required_fields"],
|
|
452
|
+
&["format", "required_fields"],
|
|
453
|
+
errors,
|
|
454
|
+
);
|
|
455
|
+
if agent
|
|
456
|
+
.get("output_contract")
|
|
457
|
+
.and_then(|o| o.get("format"))
|
|
458
|
+
.and_then(Yaml::as_str)
|
|
459
|
+
!= Some("result_envelope_v1")
|
|
460
|
+
{
|
|
461
|
+
errors.push(format!(
|
|
462
|
+
"{path}/output_contract/format: must be result_envelope_v1"
|
|
463
|
+
));
|
|
343
464
|
}
|
|
344
465
|
}
|
|
345
466
|
|
|
346
467
|
fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
347
|
-
check_keys_y(
|
|
468
|
+
check_keys_y(
|
|
469
|
+
routing,
|
|
470
|
+
"/routing",
|
|
471
|
+
&["default_assignee", "rules"],
|
|
472
|
+
&["default_assignee", "rules"],
|
|
473
|
+
errors,
|
|
474
|
+
);
|
|
348
475
|
if !is_map_some(routing) {
|
|
349
476
|
return;
|
|
350
477
|
}
|
|
@@ -353,8 +480,15 @@ fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
|
353
480
|
return;
|
|
354
481
|
};
|
|
355
482
|
for (idx, rule) in rules.iter().enumerate() {
|
|
356
|
-
check_keys_y(
|
|
357
|
-
|
|
483
|
+
check_keys_y(
|
|
484
|
+
Some(rule),
|
|
485
|
+
&format!("/routing/rules/{idx}"),
|
|
486
|
+
&["id", "assign_to", "priority"],
|
|
487
|
+
&["id", "when", "match", "assign_to", "priority"],
|
|
488
|
+
errors,
|
|
489
|
+
);
|
|
490
|
+
let has_clause = rule.get("when").is_some_and(Yaml::is_truthy)
|
|
491
|
+
|| rule.get("match").is_some_and(Yaml::is_truthy);
|
|
358
492
|
if rule.is_map() && !has_clause {
|
|
359
493
|
errors.push(format!("/routing/rules/{idx}: must include when or match"));
|
|
360
494
|
}
|
|
@@ -362,27 +496,64 @@ fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
|
362
496
|
}
|
|
363
497
|
|
|
364
498
|
fn check_communication(comm: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
365
|
-
let req = &[
|
|
499
|
+
let req = &[
|
|
500
|
+
"protocol",
|
|
501
|
+
"topology",
|
|
502
|
+
"worker_to_worker",
|
|
503
|
+
"ack_timeout_sec",
|
|
504
|
+
"result_format",
|
|
505
|
+
"message_store",
|
|
506
|
+
];
|
|
366
507
|
check_keys_y(comm, "/communication", req, req, errors);
|
|
367
508
|
if !is_map_some(comm) {
|
|
368
509
|
return;
|
|
369
510
|
}
|
|
370
|
-
if !matches!(
|
|
511
|
+
if !matches!(
|
|
512
|
+
comm.and_then(|c| c.get("protocol")).and_then(Yaml::as_str),
|
|
513
|
+
Some("mcp_inbox" | "file_bus")
|
|
514
|
+
) {
|
|
371
515
|
errors.push("/communication/protocol: invalid protocol".to_string());
|
|
372
516
|
}
|
|
373
|
-
if comm
|
|
517
|
+
if comm
|
|
518
|
+
.and_then(|c| c.get("result_format"))
|
|
519
|
+
.and_then(Yaml::as_str)
|
|
520
|
+
!= Some("result_envelope_v1")
|
|
521
|
+
{
|
|
374
522
|
errors.push("/communication/result_format: must be result_envelope_v1".to_string());
|
|
375
523
|
}
|
|
376
|
-
check_keys_y(
|
|
524
|
+
check_keys_y(
|
|
525
|
+
comm.and_then(|c| c.get("message_store")),
|
|
526
|
+
"/communication/message_store",
|
|
527
|
+
&["sqlite", "mirror_files"],
|
|
528
|
+
&["sqlite", "mirror_files"],
|
|
529
|
+
errors,
|
|
530
|
+
);
|
|
377
531
|
}
|
|
378
532
|
|
|
379
533
|
fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
380
|
-
let req = &[
|
|
534
|
+
let req = &[
|
|
535
|
+
"backend",
|
|
536
|
+
"session_name",
|
|
537
|
+
"auto_launch",
|
|
538
|
+
"require_user_approval_before_launch",
|
|
539
|
+
"max_active_agents",
|
|
540
|
+
"startup_order",
|
|
541
|
+
];
|
|
381
542
|
let allowed = &[
|
|
382
|
-
"backend",
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
543
|
+
"backend",
|
|
544
|
+
"session_name",
|
|
545
|
+
"auto_launch",
|
|
546
|
+
"require_user_approval_before_launch",
|
|
547
|
+
"max_active_agents",
|
|
548
|
+
"startup_order",
|
|
549
|
+
"display_backend",
|
|
550
|
+
"dangerous_auto_approve",
|
|
551
|
+
"auto_attach_leader",
|
|
552
|
+
"fast",
|
|
553
|
+
"tick_interval_sec",
|
|
554
|
+
"push_min_interval_sec",
|
|
555
|
+
"stuck_timeout_sec",
|
|
556
|
+
"auto_trust_own_workspace",
|
|
386
557
|
];
|
|
387
558
|
check_keys_y(runtime, "/runtime", req, allowed, errors);
|
|
388
559
|
if !is_map_some(runtime) {
|
|
@@ -393,7 +564,10 @@ fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
|
393
564
|
errors.push("/runtime/backend: invalid backend".to_string());
|
|
394
565
|
}
|
|
395
566
|
if let Some(db) = get("display_backend") {
|
|
396
|
-
if !db
|
|
567
|
+
if !db
|
|
568
|
+
.as_str()
|
|
569
|
+
.is_some_and(|s| VALID_DISPLAY_BACKENDS.contains(&s))
|
|
570
|
+
{
|
|
397
571
|
errors.push("/runtime/display_backend: invalid display backend".to_string());
|
|
398
572
|
}
|
|
399
573
|
}
|
|
@@ -410,23 +584,56 @@ fn check_context(context: Option<&Yaml>, errors: &mut Vec<String>) {
|
|
|
410
584
|
let req = &["state_file", "artifact_dir", "log_dir", "summarization"];
|
|
411
585
|
check_keys_y(context, "/context", req, req, errors);
|
|
412
586
|
if is_map_some(context) {
|
|
413
|
-
check_keys_y(
|
|
587
|
+
check_keys_y(
|
|
588
|
+
context.and_then(|c| c.get("summarization")),
|
|
589
|
+
"/context/summarization",
|
|
590
|
+
&["worker_full_logs", "state_update"],
|
|
591
|
+
&["worker_full_logs", "state_update"],
|
|
592
|
+
errors,
|
|
593
|
+
);
|
|
414
594
|
}
|
|
415
595
|
}
|
|
416
596
|
|
|
417
597
|
fn check_task(task: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
418
|
-
let req = &[
|
|
598
|
+
let req = &[
|
|
599
|
+
"id",
|
|
600
|
+
"title",
|
|
601
|
+
"type",
|
|
602
|
+
"assignee",
|
|
603
|
+
"deps",
|
|
604
|
+
"acceptance",
|
|
605
|
+
"status",
|
|
606
|
+
];
|
|
419
607
|
let allowed = &[
|
|
420
|
-
"id",
|
|
421
|
-
"
|
|
608
|
+
"id",
|
|
609
|
+
"title",
|
|
610
|
+
"type",
|
|
611
|
+
"assignee",
|
|
612
|
+
"deps",
|
|
613
|
+
"acceptance",
|
|
614
|
+
"status",
|
|
615
|
+
"description",
|
|
616
|
+
"requires_tools",
|
|
617
|
+
"files",
|
|
618
|
+
"risk",
|
|
619
|
+
"retry_limit",
|
|
620
|
+
"human_confirmation",
|
|
422
621
|
];
|
|
423
622
|
check_keys_y(Some(task), path, req, allowed, errors);
|
|
424
623
|
if !task.is_map() {
|
|
425
624
|
return;
|
|
426
625
|
}
|
|
427
626
|
check_list_y(task.get("deps"), &format!("{path}/deps"), errors);
|
|
428
|
-
check_list_y(
|
|
429
|
-
|
|
627
|
+
check_list_y(
|
|
628
|
+
task.get("acceptance"),
|
|
629
|
+
&format!("{path}/acceptance"),
|
|
630
|
+
errors,
|
|
631
|
+
);
|
|
632
|
+
if !task
|
|
633
|
+
.get("status")
|
|
634
|
+
.and_then(Yaml::as_str)
|
|
635
|
+
.is_some_and(|s| TASK_STATUS_STRS.contains(&s))
|
|
636
|
+
{
|
|
430
637
|
errors.push(format!("{path}/status: invalid task status"));
|
|
431
638
|
}
|
|
432
639
|
}
|
|
@@ -441,7 +648,10 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
441
648
|
// duplicate agent id(集合含 None 复刻 Python `{a.get("id") ...}`)。
|
|
442
649
|
// SMOKE-1 N38 失败可解释性:不仅报"有重复",还点出**哪个 id 重复**(可能多个),
|
|
443
650
|
// 给 operator 直接定位线索;locate.md §"Smallest likely code touch" item 3。
|
|
444
|
-
let id_set: HashSet<Option<&str>> = map_agents
|
|
651
|
+
let id_set: HashSet<Option<&str>> = map_agents
|
|
652
|
+
.iter()
|
|
653
|
+
.map(|a| a.get("id").and_then(Yaml::as_str))
|
|
654
|
+
.collect();
|
|
445
655
|
if id_set.len() != map_agents.len() {
|
|
446
656
|
let mut seen: HashSet<&str> = HashSet::new();
|
|
447
657
|
let mut duplicates: Vec<&str> = Vec::new();
|
|
@@ -466,7 +676,10 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
466
676
|
}
|
|
467
677
|
}
|
|
468
678
|
// all_ids:present 的 agent id + leader id(若 truthy)。
|
|
469
|
-
let mut all_ids: HashSet<&str> = map_agents
|
|
679
|
+
let mut all_ids: HashSet<&str> = map_agents
|
|
680
|
+
.iter()
|
|
681
|
+
.filter_map(|a| a.get("id").and_then(Yaml::as_str))
|
|
682
|
+
.collect();
|
|
470
683
|
if let Some(lid) = leader.and_then(|l| l.get("id")).and_then(Yaml::as_str) {
|
|
471
684
|
if !lid.is_empty() {
|
|
472
685
|
all_ids.insert(lid);
|
|
@@ -478,7 +691,10 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
478
691
|
.and_then(Yaml::as_str)
|
|
479
692
|
.is_some_and(|p| parse_canonical_provider(p).is_some())
|
|
480
693
|
{
|
|
481
|
-
e.push(format!(
|
|
694
|
+
e.push(format!(
|
|
695
|
+
"/leader/provider: unknown provider {}",
|
|
696
|
+
py_repr(leader_provider)
|
|
697
|
+
));
|
|
482
698
|
}
|
|
483
699
|
|
|
484
700
|
for (idx, agent) in agents.iter().enumerate() {
|
|
@@ -487,60 +703,123 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
487
703
|
.and_then(Yaml::as_str)
|
|
488
704
|
.is_some_and(|p| parse_canonical_provider(p).is_some())
|
|
489
705
|
{
|
|
490
|
-
e.push(format!(
|
|
706
|
+
e.push(format!(
|
|
707
|
+
"/agents/{idx}/provider: unknown provider {}",
|
|
708
|
+
py_repr(provider)
|
|
709
|
+
));
|
|
491
710
|
}
|
|
492
711
|
if let Some(auth) = agent.get("auth_mode") {
|
|
493
|
-
if !matches!(auth, Yaml::Null)
|
|
494
|
-
|
|
712
|
+
if !matches!(auth, Yaml::Null)
|
|
713
|
+
&& !auth.as_str().is_some_and(|a| AUTH_MODES.contains(&a))
|
|
714
|
+
{
|
|
715
|
+
e.push(format!(
|
|
716
|
+
"/agents/{idx}/auth_mode: unknown auth_mode {}",
|
|
717
|
+
py_repr(Some(auth))
|
|
718
|
+
));
|
|
495
719
|
}
|
|
496
720
|
}
|
|
497
|
-
if let Some(f) = agent
|
|
721
|
+
if let Some(f) = agent
|
|
722
|
+
.get("system_prompt")
|
|
723
|
+
.and_then(|sp| sp.get("file"))
|
|
724
|
+
.filter(|p| p.is_truthy())
|
|
725
|
+
.and_then(Yaml::as_str)
|
|
726
|
+
{
|
|
498
727
|
let candidate = Path::new(f);
|
|
499
|
-
let full = if candidate.is_absolute() {
|
|
728
|
+
let full = if candidate.is_absolute() {
|
|
729
|
+
candidate.to_path_buf()
|
|
730
|
+
} else {
|
|
731
|
+
base_dir.join(candidate)
|
|
732
|
+
};
|
|
500
733
|
if !full.exists() {
|
|
501
|
-
e.push(format!(
|
|
734
|
+
e.push(format!(
|
|
735
|
+
"/agents/{idx}/system_prompt/file: file not found: {}",
|
|
736
|
+
full.display()
|
|
737
|
+
));
|
|
502
738
|
}
|
|
503
739
|
}
|
|
504
|
-
let tools: Vec<&str> = agent
|
|
740
|
+
let tools: Vec<&str> = agent
|
|
741
|
+
.get("tools")
|
|
742
|
+
.and_then(Yaml::as_list)
|
|
743
|
+
.unwrap_or(&[])
|
|
744
|
+
.iter()
|
|
745
|
+
.filter_map(Yaml::as_str)
|
|
746
|
+
.collect();
|
|
505
747
|
for tool in permissions::expand_tool_strings(tools) {
|
|
506
748
|
if !permissions::is_canonical_tool(&tool) {
|
|
507
|
-
e.push(format!(
|
|
749
|
+
e.push(format!(
|
|
750
|
+
"/agents/{idx}/tools: unknown tool {}",
|
|
751
|
+
py_repr_str(&tool)
|
|
752
|
+
));
|
|
508
753
|
}
|
|
509
754
|
}
|
|
510
755
|
}
|
|
511
756
|
|
|
512
|
-
let leader_tools: Vec<&str> = leader
|
|
757
|
+
let leader_tools: Vec<&str> = leader
|
|
758
|
+
.and_then(|l| l.get("tools"))
|
|
759
|
+
.and_then(Yaml::as_list)
|
|
760
|
+
.unwrap_or(&[])
|
|
761
|
+
.iter()
|
|
762
|
+
.filter_map(Yaml::as_str)
|
|
763
|
+
.collect();
|
|
513
764
|
for tool in permissions::expand_tool_strings(leader_tools) {
|
|
514
765
|
if !permissions::is_canonical_tool(&tool) {
|
|
515
|
-
e.push(format!(
|
|
766
|
+
e.push(format!(
|
|
767
|
+
"/leader/tools: unknown tool {}",
|
|
768
|
+
py_repr_str(&tool)
|
|
769
|
+
));
|
|
516
770
|
}
|
|
517
771
|
}
|
|
518
772
|
|
|
519
773
|
let routing = spec.get("routing");
|
|
520
|
-
if let Some(da) = routing
|
|
774
|
+
if let Some(da) = routing
|
|
775
|
+
.and_then(|r| r.get("default_assignee"))
|
|
776
|
+
.and_then(Yaml::as_str)
|
|
777
|
+
{
|
|
521
778
|
if !da.is_empty() && !all_ids.contains(da) {
|
|
522
|
-
e.push(format!(
|
|
779
|
+
e.push(format!(
|
|
780
|
+
"/routing/default_assignee: unknown agent {}",
|
|
781
|
+
py_repr_str(da)
|
|
782
|
+
));
|
|
523
783
|
}
|
|
524
784
|
}
|
|
525
|
-
let rules = routing
|
|
785
|
+
let rules = routing
|
|
786
|
+
.and_then(|r| r.get("rules"))
|
|
787
|
+
.and_then(Yaml::as_list)
|
|
788
|
+
.unwrap_or(&[]);
|
|
526
789
|
for (idx, rule) in rules.iter().enumerate() {
|
|
527
790
|
let target = rule.get("assign_to");
|
|
528
|
-
if !target
|
|
529
|
-
|
|
791
|
+
if !target
|
|
792
|
+
.and_then(Yaml::as_str)
|
|
793
|
+
.is_some_and(|t| all_ids.contains(t))
|
|
794
|
+
{
|
|
795
|
+
e.push(format!(
|
|
796
|
+
"/routing/rules/{idx}/assign_to: unknown agent {}",
|
|
797
|
+
py_repr(target)
|
|
798
|
+
));
|
|
530
799
|
}
|
|
531
800
|
}
|
|
532
801
|
|
|
533
802
|
let tasks: &[Yaml] = spec.get("tasks").and_then(Yaml::as_list).unwrap_or(&[]);
|
|
534
|
-
let task_ids: HashSet<&str> = tasks
|
|
803
|
+
let task_ids: HashSet<&str> = tasks
|
|
804
|
+
.iter()
|
|
805
|
+
.filter(|t| t.is_map())
|
|
806
|
+
.filter_map(|t| t.get("id").and_then(Yaml::as_str))
|
|
807
|
+
.collect();
|
|
535
808
|
for (idx, task) in tasks.iter().enumerate() {
|
|
536
809
|
if let Some(a) = task.get("assignee").and_then(Yaml::as_str) {
|
|
537
810
|
if !a.is_empty() && !all_ids.contains(a) {
|
|
538
|
-
e.push(format!(
|
|
811
|
+
e.push(format!(
|
|
812
|
+
"/tasks/{idx}/assignee: unknown agent {}",
|
|
813
|
+
py_repr_str(a)
|
|
814
|
+
));
|
|
539
815
|
}
|
|
540
816
|
}
|
|
541
817
|
for dep in task.get("deps").and_then(Yaml::as_list).unwrap_or(&[]) {
|
|
542
818
|
if !dep.as_str().is_some_and(|d| task_ids.contains(d)) {
|
|
543
|
-
e.push(format!(
|
|
819
|
+
e.push(format!(
|
|
820
|
+
"/tasks/{idx}/deps: unknown dependency {}",
|
|
821
|
+
py_repr(Some(dep))
|
|
822
|
+
));
|
|
544
823
|
}
|
|
545
824
|
}
|
|
546
825
|
}
|
|
@@ -549,14 +828,27 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
549
828
|
let nodes: Vec<TaskNode> = tasks
|
|
550
829
|
.iter()
|
|
551
830
|
.filter_map(|t| {
|
|
552
|
-
let id = t
|
|
553
|
-
|
|
831
|
+
let id = t
|
|
832
|
+
.get("id")
|
|
833
|
+
.and_then(Yaml::as_str)
|
|
834
|
+
.filter(|s| !s.is_empty())?;
|
|
835
|
+
let deps: Vec<TaskId> = t
|
|
836
|
+
.get("deps")
|
|
837
|
+
.and_then(Yaml::as_list)
|
|
838
|
+
.unwrap_or(&[])
|
|
839
|
+
.iter()
|
|
840
|
+
.filter_map(|d| d.as_str().map(TaskId::from))
|
|
841
|
+
.collect();
|
|
554
842
|
Some(TaskNode::new(TaskId::from(id), deps, TaskStatus::Pending))
|
|
555
843
|
})
|
|
556
844
|
.collect();
|
|
557
845
|
let cycle = find_dependency_cycle(&nodes);
|
|
558
846
|
if !cycle.is_empty() {
|
|
559
|
-
let chain = cycle
|
|
847
|
+
let chain = cycle
|
|
848
|
+
.iter()
|
|
849
|
+
.map(TaskId::as_str)
|
|
850
|
+
.collect::<Vec<_>>()
|
|
851
|
+
.join(" -> ");
|
|
560
852
|
e.push(format!("/tasks: dependency cycle detected: {chain}"));
|
|
561
853
|
}
|
|
562
854
|
e
|
|
@@ -564,7 +856,11 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
564
856
|
|
|
565
857
|
/// Python `repr()` of a string(选引号 + 转义),用于 `unknown X 'name'`。
|
|
566
858
|
fn py_repr_str(s: &str) -> String {
|
|
567
|
-
let quote = if s.contains('\'') && !s.contains('"') {
|
|
859
|
+
let quote = if s.contains('\'') && !s.contains('"') {
|
|
860
|
+
'"'
|
|
861
|
+
} else {
|
|
862
|
+
'\''
|
|
863
|
+
};
|
|
568
864
|
let mut out = String::new();
|
|
569
865
|
out.push(quote);
|
|
570
866
|
for c in s.chars() {
|