@team-agent/installer 0.5.100 → 0.5.102
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/diagnose.rs +29 -7
- package/crates/team-agent/src/cli/mod.rs +24 -8
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +52 -65
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
- package/crates/team-agent/src/leader/start.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +115 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
- package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
- package/crates/team-agent/src/mcp_server/mod.rs +7 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
- package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
- package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
- package/crates/team-agent/src/mcp_server/tests.rs +4 -200
- package/crates/team-agent/src/mcp_server/tools.rs +5 -396
- package/crates/team-agent/src/mcp_server/types.rs +3 -36
- package/crates/team-agent/src/mcp_server/wire.rs +2 -223
- package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/scheduler.rs +2 -205
- package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
- package/crates/team-agent/src/messaging/types.rs +0 -20
- package/crates/team-agent/src/model/spec.rs +15 -18
- package/crates/team-agent/src/provider/types.rs +2 -0
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +126 -37
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +18 -47
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +28 -118
- package/skills/team-agent/references/team-in-team.md +4 -19
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -94,6 +94,14 @@ pub(crate) fn classify_tmux_server_error(error_text: &str) -> &'static str {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
|
|
97
|
+
diagnose_runtime_with_workspace(None, state, backend)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn diagnose_runtime_with_workspace(
|
|
101
|
+
workspace: Option<&std::path::Path>,
|
|
102
|
+
state: &Value,
|
|
103
|
+
backend: &dyn Transport,
|
|
104
|
+
) -> (Value, Value) {
|
|
97
105
|
let mut issues = Vec::new();
|
|
98
106
|
let mut repairs = Vec::new();
|
|
99
107
|
|
|
@@ -248,12 +256,25 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
248
256
|
.and_then(Value::as_str)
|
|
249
257
|
.filter(|path| !path.is_empty())
|
|
250
258
|
.map(crate::provider::RolloutPath::new);
|
|
251
|
-
let
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
let agent_identity = crate::model::ids::AgentId::new(agent_id.clone());
|
|
260
|
+
let identity_probe = workspace
|
|
261
|
+
.map(|workspace| {
|
|
262
|
+
crate::lifecycle::restart::session_identity_probe_for_agent_with_fork_ancestry(
|
|
263
|
+
workspace,
|
|
264
|
+
state,
|
|
265
|
+
&agent_identity,
|
|
266
|
+
agent_state,
|
|
267
|
+
provider,
|
|
268
|
+
rollout_path.as_ref(),
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
.unwrap_or_else(|| {
|
|
272
|
+
crate::lifecycle::restart::session_identity_probe_for_agent(
|
|
273
|
+
&agent_identity,
|
|
274
|
+
provider,
|
|
275
|
+
rollout_path.as_ref(),
|
|
276
|
+
)
|
|
277
|
+
});
|
|
257
278
|
if identity_probe.identity_ok == Some(false) {
|
|
258
279
|
let issue = format!("session_identity_mismatch:{agent_id}");
|
|
259
280
|
issues.push(json!(issue));
|
|
@@ -304,7 +325,8 @@ pub(crate) fn diagnose_runtime_for_workspace(
|
|
|
304
325
|
selected_team_key: Option<&str>,
|
|
305
326
|
coordinator_health: &crate::coordinator::HealthReport,
|
|
306
327
|
) -> (Value, Value) {
|
|
307
|
-
let (mut issues, mut repairs) =
|
|
328
|
+
let (mut issues, mut repairs) =
|
|
329
|
+
diagnose_runtime_with_workspace(Some(workspace), state, backend);
|
|
308
330
|
append_live_leader_workspace_mismatch_issue(
|
|
309
331
|
workspace,
|
|
310
332
|
state,
|
|
@@ -2926,14 +2926,30 @@ pub mod lifecycle_port {
|
|
|
2926
2926
|
let source = crate::model::ids::AgentId::new(source_agent);
|
|
2927
2927
|
let dest = crate::model::ids::AgentId::new(as_agent_id);
|
|
2928
2928
|
match crate::lifecycle::fork_agent(workspace, &source, &dest, label, true, team) {
|
|
2929
|
-
Ok(report) =>
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2929
|
+
Ok(report) => {
|
|
2930
|
+
let mut value = json!({
|
|
2931
|
+
"ok": true,
|
|
2932
|
+
"source_agent_id": report.source_agent_id.as_str(),
|
|
2933
|
+
"new_agent_id": report.new_agent_id.as_str(),
|
|
2934
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2935
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2936
|
+
"backing_state": report.backing_state,
|
|
2937
|
+
});
|
|
2938
|
+
if let (Some(details), Some(object)) = (report.pi_fork, value.as_object_mut()) {
|
|
2939
|
+
object.insert("status".to_string(), json!("forked"));
|
|
2940
|
+
object.insert("target".to_string(), json!(report.new_agent_id.as_str()));
|
|
2941
|
+
object.insert(
|
|
2942
|
+
"source_session_id".to_string(),
|
|
2943
|
+
json!(details.source_session_id.as_str()),
|
|
2944
|
+
);
|
|
2945
|
+
object.insert("session_id".to_string(), json!(details.session_id.as_str()));
|
|
2946
|
+
object.insert(
|
|
2947
|
+
"backing_path".to_string(),
|
|
2948
|
+
json!(details.backing_path.0.to_string_lossy().to_string()),
|
|
2949
|
+
);
|
|
2950
|
+
}
|
|
2951
|
+
Ok(value)
|
|
2952
|
+
}
|
|
2937
2953
|
Err(e) => Ok(error_value(e)),
|
|
2938
2954
|
}
|
|
2939
2955
|
}
|
|
@@ -414,31 +414,6 @@ fn valid_result_envelope() -> serde_json::Value {
|
|
|
414
414
|
"next_actions": []
|
|
415
415
|
})
|
|
416
416
|
}
|
|
417
|
-
fn seed_collect_state(ws: &std::path::Path) {
|
|
418
|
-
seed_team_spec(ws);
|
|
419
|
-
crate::state::persist::save_runtime_state(
|
|
420
|
-
ws,
|
|
421
|
-
&json!({
|
|
422
|
-
"agents": {"fake_impl": {"status": "idle"}},
|
|
423
|
-
"tasks": [{
|
|
424
|
-
"id": "task_impl",
|
|
425
|
-
"title": "Fake implementation",
|
|
426
|
-
"type": "implementation",
|
|
427
|
-
"assignee": "fake_impl",
|
|
428
|
-
"deps": [],
|
|
429
|
-
"acceptance": ["fake result collected"],
|
|
430
|
-
"status": "pending",
|
|
431
|
-
"requires_tools": [],
|
|
432
|
-
"files": [],
|
|
433
|
-
"risk": "low"
|
|
434
|
-
}],
|
|
435
|
-
"session_name": Value::Null,
|
|
436
|
-
"active_team_key": Value::Null,
|
|
437
|
-
"spec_path": ws.join("team.spec.yaml").to_string_lossy()
|
|
438
|
-
}),
|
|
439
|
-
)
|
|
440
|
-
.unwrap();
|
|
441
|
-
}
|
|
442
417
|
fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
|
|
443
418
|
let store = crate::message_store::MessageStore::open(ws).unwrap();
|
|
444
419
|
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
@@ -450,18 +425,6 @@ fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
|
|
|
450
425
|
)
|
|
451
426
|
.unwrap();
|
|
452
427
|
}
|
|
453
|
-
fn read_state(ws: &std::path::Path) -> serde_json::Value {
|
|
454
|
-
serde_json::from_str(
|
|
455
|
-
&std::fs::read_to_string(crate::state::persist::runtime_state_path(ws)).unwrap(),
|
|
456
|
-
)
|
|
457
|
-
.unwrap()
|
|
458
|
-
}
|
|
459
|
-
fn read_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
|
|
460
|
-
crate::event_log::EventLog::new(ws).tail(50).unwrap()
|
|
461
|
-
}
|
|
462
|
-
fn seeded_team_key(ws: &std::path::Path) -> String {
|
|
463
|
-
ws.file_name().unwrap().to_string_lossy().to_string()
|
|
464
|
-
}
|
|
465
428
|
fn json_output(result: CmdResult) -> serde_json::Value {
|
|
466
429
|
match result.output {
|
|
467
430
|
CmdOutput::Json(v) => v,
|
|
@@ -590,40 +553,3 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
|
|
|
590
553
|
"refused remove-agent must not delete the spec-defined agent"
|
|
591
554
|
);
|
|
592
555
|
}
|
|
593
|
-
#[test]
|
|
594
|
-
fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
|
|
595
|
-
let ws = tmp_workspace();
|
|
596
|
-
seed_collect_state(&ws);
|
|
597
|
-
let out = messaging::stuck_cancel(&ws, "fake_impl", None, "leader").unwrap();
|
|
598
|
-
let team_key = seeded_team_key(&ws);
|
|
599
|
-
assert_eq!(out["ok"], json!(true));
|
|
600
|
-
assert_eq!(
|
|
601
|
-
out["alert_types"],
|
|
602
|
-
json!(["cross_worker_deadlock", "idle_fallback", "stuck"])
|
|
603
|
-
);
|
|
604
|
-
assert!(
|
|
605
|
-
out["suppressed"]["idle_fallback"]["snapshot"]["assigned_task_ids"]
|
|
606
|
-
.as_array()
|
|
607
|
-
.unwrap()
|
|
608
|
-
.contains(&json!("task_impl"))
|
|
609
|
-
);
|
|
610
|
-
let state = read_state(&ws);
|
|
611
|
-
assert_eq!(
|
|
612
|
-
state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]
|
|
613
|
-
["suppressed_by"],
|
|
614
|
-
json!("leader")
|
|
615
|
-
);
|
|
616
|
-
assert!(
|
|
617
|
-
read_events(&ws)
|
|
618
|
-
.iter()
|
|
619
|
-
.any(|e| e["event"] == json!("coordinator.idle_alert_suppressed")
|
|
620
|
-
&& e["agent_id"] == json!("fake_impl")),
|
|
621
|
-
"stuck_cancel must write coordinator.idle_alert_suppressed"
|
|
622
|
-
);
|
|
623
|
-
let listed = messaging::stuck_list(&ws).unwrap();
|
|
624
|
-
assert_eq!(
|
|
625
|
-
listed["suppressed_idle_alerts"]["fake_impl"]["stuck"]["suppressed_by"],
|
|
626
|
-
json!("leader"),
|
|
627
|
-
"stuck-list must read the persisted state mirror, not return a hard-coded empty list"
|
|
628
|
-
);
|
|
629
|
-
}
|
|
@@ -163,25 +163,21 @@ fn front_matter_body_lstrip_strips_only_newlines() {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
#[test]
|
|
166
|
-
fn
|
|
167
|
-
let
|
|
168
|
-
let
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
166
|
+
fn front_matter_unterminated_falls_back_to_plain_text() {
|
|
167
|
+
let text = "---\nname: x\n";
|
|
168
|
+
let p = write_tmp("unterminated.md", text);
|
|
169
|
+
let (meta, body) = read_front_matter(&p).expect("unterminated front matter is plain text");
|
|
170
|
+
assert_eq!(meta, Value::Map(Vec::new()));
|
|
171
|
+
assert_eq!(body, text);
|
|
173
172
|
}
|
|
174
173
|
|
|
175
174
|
#[test]
|
|
176
|
-
fn
|
|
177
|
-
|
|
178
|
-
let p = write_tmp("list.md",
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.contains("front matter must be a YAML object"),
|
|
183
|
-
"got: {err}"
|
|
184
|
-
);
|
|
175
|
+
fn front_matter_non_object_falls_back_to_plain_text() {
|
|
176
|
+
let text = "---\n- a\n- b\n---\nbody\n";
|
|
177
|
+
let p = write_tmp("list.md", text);
|
|
178
|
+
let (meta, body) = read_front_matter(&p).expect("non-object front matter is plain text");
|
|
179
|
+
assert_eq!(meta, Value::Map(Vec::new()));
|
|
180
|
+
assert_eq!(body, text);
|
|
185
181
|
}
|
|
186
182
|
|
|
187
183
|
// ───────────────────────────── compile_team: full dict parity ─────────────────────────────
|
|
@@ -395,7 +391,7 @@ const ROLE_MISSING_PROVIDER: &str = "\
|
|
|
395
391
|
---
|
|
396
392
|
name: implementer
|
|
397
393
|
role: Implementation Engineer
|
|
398
|
-
model:
|
|
394
|
+
model: team-agent/qwen3.8-27b
|
|
399
395
|
auth_mode: subscription
|
|
400
396
|
dangerously_skip_permissions: false
|
|
401
397
|
tools:
|
|
@@ -406,13 +402,14 @@ Implement bounded tasks.
|
|
|
406
402
|
";
|
|
407
403
|
|
|
408
404
|
#[test]
|
|
409
|
-
fn
|
|
405
|
+
fn compile_missing_provider_defaults_to_pi() {
|
|
410
406
|
let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_MISSING_PROVIDER)], &[]);
|
|
411
|
-
let
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
"
|
|
407
|
+
let spec = compile_team(&team).expect("an omitted provider defaults to Pi");
|
|
408
|
+
let agent = agent0(&spec);
|
|
409
|
+
assert_eq!(agent.get("provider"), Some(&Value::Str("pi".to_string())));
|
|
410
|
+
assert_eq!(
|
|
411
|
+
agent.get("model"),
|
|
412
|
+
Some(&Value::Str("team-agent/qwen3.8-27b".to_string()))
|
|
416
413
|
);
|
|
417
414
|
}
|
|
418
415
|
|
|
@@ -44,48 +44,38 @@ pub struct IgnoredTeamField {
|
|
|
44
44
|
pub value: String,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
///
|
|
48
|
-
///
|
|
49
|
-
/// Reads `path` (UTF-8). If the text does not start with `"---\n"`, returns
|
|
50
|
-
/// `({}, full_text)`. Otherwise splits on the first `"\n---"` after byte 4:
|
|
51
|
-
/// unterminated (no closing marker) → `ValidationError "{path}: unterminated
|
|
52
|
-
/// front matter"`; the front-matter block is parsed via `simple_yaml.loads`
|
|
53
|
-
/// (empty block → `{}`); a non-dict block → `ValidationError "{path}: front
|
|
54
|
-
/// matter must be a YAML object"`. The body is everything after the closing
|
|
55
|
-
/// marker, `lstrip("\n")` (leading NEWLINES only — not other whitespace).
|
|
47
|
+
/// Read optional YAML front matter; malformed or non-object headers are plain text.
|
|
56
48
|
pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
57
49
|
let text = fs::read_to_string(path)
|
|
58
50
|
.map_err(|e| ModelError::Runtime(format!("{}: {e}", path.display())))?;
|
|
59
51
|
let text = text.replace("\r\n", "\n").replace('\r', "\n");
|
|
52
|
+
let empty_meta = || Value::Map(Vec::new());
|
|
60
53
|
let Some(rest) = text.strip_prefix("---\n") else {
|
|
61
|
-
return Ok((
|
|
54
|
+
return Ok((empty_meta(), text));
|
|
62
55
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
))
|
|
56
|
+
|
|
57
|
+
let mut offset = 0;
|
|
58
|
+
let mut close = None;
|
|
59
|
+
for line in rest.split_inclusive('\n') {
|
|
60
|
+
if line.strip_suffix('\n').unwrap_or(line) == "---" {
|
|
61
|
+
close = Some(offset);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
offset += line.len();
|
|
65
|
+
}
|
|
66
|
+
let Some(close) = close else {
|
|
67
|
+
return Ok((empty_meta(), text));
|
|
68
68
|
};
|
|
69
|
-
let raw_meta = rest
|
|
70
|
-
ModelError::Validation(format!("{}: unterminated front matter", path.display()))
|
|
71
|
-
})?;
|
|
72
|
-
let after_meta = rest.get(close..).ok_or_else(|| {
|
|
73
|
-
ModelError::Validation(format!("{}: unterminated front matter", path.display()))
|
|
74
|
-
})?;
|
|
75
|
-
let after_marker = after_meta.strip_prefix("\n---").ok_or_else(|| {
|
|
76
|
-
ModelError::Validation(format!("{}: unterminated front matter", path.display()))
|
|
77
|
-
})?;
|
|
69
|
+
let raw_meta = &rest[..close];
|
|
78
70
|
let meta = if raw_meta.trim().is_empty() {
|
|
79
|
-
|
|
71
|
+
empty_meta()
|
|
80
72
|
} else {
|
|
81
|
-
yaml::loads(raw_meta)
|
|
73
|
+
match yaml::loads(raw_meta) {
|
|
74
|
+
Ok(meta) if meta.is_map() => meta,
|
|
75
|
+
_ => return Ok((empty_meta(), text)),
|
|
76
|
+
}
|
|
82
77
|
};
|
|
83
|
-
|
|
84
|
-
return Err(ModelError::Validation(format!(
|
|
85
|
-
"{}: front matter must be a YAML object",
|
|
86
|
-
path.display()
|
|
87
|
-
)));
|
|
88
|
-
}
|
|
78
|
+
let after_marker = &rest[close + 3..];
|
|
89
79
|
Ok((meta, after_marker.trim_start_matches('\n').to_string()))
|
|
90
80
|
}
|
|
91
81
|
|
|
@@ -161,7 +151,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
161
151
|
let workspace = paths::team_workspace(team_dir)?;
|
|
162
152
|
let workspace_s = workspace.display().to_string();
|
|
163
153
|
let team_name =
|
|
164
|
-
string_field(&team_meta, "name").unwrap_or_else(||
|
|
154
|
+
string_field(&team_meta, "name").unwrap_or_else(|| workspace_dir_name(&workspace));
|
|
165
155
|
let objective = string_field(&team_meta, "objective")
|
|
166
156
|
.or_else(|| non_empty_trimmed(&team_body))
|
|
167
157
|
.unwrap_or_else(|| "Team Agent document-driven team.".to_string());
|
|
@@ -487,9 +477,23 @@ fn compile_role_agent_with_mode(
|
|
|
487
477
|
let (meta, body) = read_front_matter(role_path)?;
|
|
488
478
|
let communication_mode =
|
|
489
479
|
communication_mode_field(&meta, role_path)?.unwrap_or(team_communication_mode);
|
|
490
|
-
let id =
|
|
491
|
-
|
|
492
|
-
|
|
480
|
+
let id = string_field(&meta, "agent_id")
|
|
481
|
+
.or_else(|| string_field(&meta, "name"))
|
|
482
|
+
.filter(|id| !id.trim().is_empty())
|
|
483
|
+
.or_else(|| {
|
|
484
|
+
role_path
|
|
485
|
+
.file_stem()
|
|
486
|
+
.and_then(|stem| stem.to_str())
|
|
487
|
+
.filter(|stem| !stem.is_empty())
|
|
488
|
+
.map(ToString::to_string)
|
|
489
|
+
})
|
|
490
|
+
.unwrap_or_else(|| "worker".to_string());
|
|
491
|
+
let role = string_field(&meta, "role")
|
|
492
|
+
.filter(|role| !role.trim().is_empty())
|
|
493
|
+
.unwrap_or_else(|| id.clone());
|
|
494
|
+
let provider = string_field(&meta, "provider")
|
|
495
|
+
.filter(|provider| !provider.trim().is_empty())
|
|
496
|
+
.unwrap_or_else(|| "pi".to_string());
|
|
493
497
|
require_explicit_grok_role_model(&meta, role_path, &provider)?;
|
|
494
498
|
require_explicit_cursor_role_model(&meta, role_path, &provider)?;
|
|
495
499
|
validate_pi_role_fields(&meta, role_path, &provider)?;
|
|
@@ -498,7 +502,7 @@ fn compile_role_agent_with_mode(
|
|
|
498
502
|
string_field(&meta, "model")
|
|
499
503
|
.filter(|value| !value.trim().is_empty())
|
|
500
504
|
.map(Value::Str)
|
|
501
|
-
.
|
|
505
|
+
.unwrap_or_else(|| Value::Str("openai-codex/gpt-6-luna".to_string()))
|
|
502
506
|
} else {
|
|
503
507
|
resolve_model(&meta, team_meta, &provider)
|
|
504
508
|
};
|
|
@@ -532,11 +536,9 @@ fn compile_role_agent_with_mode(
|
|
|
532
536
|
("file", Value::Null),
|
|
533
537
|
]),
|
|
534
538
|
),
|
|
535
|
-
// 0.5.66 bypass 单源:compiler 透传角色 md 的 `dangerously_skip_permissions`
|
|
536
|
-
// (必填 bool,spec 校验保证存在)。取代旧"恒发 permission_mode: restricted"。
|
|
537
539
|
(
|
|
538
540
|
"dangerously_skip_permissions",
|
|
539
|
-
Value::Bool(
|
|
541
|
+
Value::Bool(optional_dangerously_skip_permissions(&meta)),
|
|
540
542
|
),
|
|
541
543
|
(
|
|
542
544
|
"communication_mode",
|
|
@@ -555,6 +557,9 @@ fn compile_role_agent_with_mode(
|
|
|
555
557
|
]),
|
|
556
558
|
),
|
|
557
559
|
];
|
|
560
|
+
if let Some(label) = string_field(&meta, "label") {
|
|
561
|
+
agent_items.insert(1, ("label", Value::Str(label)));
|
|
562
|
+
}
|
|
558
563
|
if let Some(profile) = string_field(&meta, "profile") {
|
|
559
564
|
agent_items.push(("profile", Value::Str(profile)));
|
|
560
565
|
}
|
|
@@ -572,6 +577,7 @@ fn compile_role_agent_with_mode(
|
|
|
572
577
|
})?;
|
|
573
578
|
Some(parsed)
|
|
574
579
|
}
|
|
580
|
+
_ if is_pi => Some(ProviderEffort::Max),
|
|
575
581
|
_ => None,
|
|
576
582
|
};
|
|
577
583
|
let team_effort = match string_field(team_meta, "provider_effort") {
|
|
@@ -668,15 +674,6 @@ fn string_field(meta: &Value, key: &str) -> Option<String> {
|
|
|
668
674
|
.map(ToString::to_string)
|
|
669
675
|
}
|
|
670
676
|
|
|
671
|
-
fn required_string(meta: &Value, path: &Path, key: &str) -> Result<String, ModelError> {
|
|
672
|
-
string_field(meta, key).ok_or_else(|| {
|
|
673
|
-
ModelError::Validation(format!(
|
|
674
|
-
"{}: missing front matter field {key}",
|
|
675
|
-
path.display()
|
|
676
|
-
))
|
|
677
|
-
})
|
|
678
|
-
}
|
|
679
|
-
|
|
680
677
|
/// 缺 model 时框架会填内建默认(grok 上是 grok-4),席位因此静默拿到
|
|
681
678
|
/// 与角色文件无关的模型和上下文窗口,而 argv 看起来完全正常。
|
|
682
679
|
/// 角色文件必须自己写死;内建默认、team 级默认、CLI 全局默认都是隐式来源。
|
|
@@ -746,19 +743,10 @@ fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<
|
|
|
746
743
|
Ok(())
|
|
747
744
|
}
|
|
748
745
|
|
|
749
|
-
|
|
750
|
-
/// 缺 = 编译失败(消息含硬串);非 bool = 同 fail-loud。
|
|
751
|
-
fn required_dangerously_skip_permissions(meta: &Value, path: &Path) -> Result<bool, ModelError> {
|
|
746
|
+
fn optional_dangerously_skip_permissions(meta: &Value) -> bool {
|
|
752
747
|
match meta.get("dangerously_skip_permissions") {
|
|
753
|
-
Some(Value::Bool(value)) =>
|
|
754
|
-
|
|
755
|
-
"{}: front matter field dangerously_skip_permissions must be a boolean. This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
|
|
756
|
-
path.display()
|
|
757
|
-
))),
|
|
758
|
-
None => Err(ModelError::Validation(format!(
|
|
759
|
-
"{}: missing front matter field dangerously_skip_permissions. This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
|
|
760
|
-
path.display()
|
|
761
|
-
))),
|
|
748
|
+
Some(Value::Bool(value)) => *value,
|
|
749
|
+
_ => true,
|
|
762
750
|
}
|
|
763
751
|
}
|
|
764
752
|
|
|
@@ -880,10 +868,9 @@ fn non_empty_trimmed(text: &str) -> Option<String> {
|
|
|
880
868
|
}
|
|
881
869
|
}
|
|
882
870
|
|
|
883
|
-
fn
|
|
884
|
-
|
|
885
|
-
.
|
|
886
|
-
.and_then(Path::file_name)
|
|
871
|
+
fn workspace_dir_name(workspace: &Path) -> String {
|
|
872
|
+
workspace
|
|
873
|
+
.file_name()
|
|
887
874
|
.and_then(|s| s.to_str())
|
|
888
875
|
.filter(|s| !s.is_empty())
|
|
889
876
|
.unwrap_or("team")
|
|
@@ -105,12 +105,7 @@ fn s3_process_worker() {
|
|
|
105
105
|
assert_eq!(line.trim(), "commit");
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
-
let tools = crate::mcp_server::TeamOrchestratorTools::with_identity(
|
|
109
|
-
&dir, Some(crate::model::ids::AgentId::new("leader")), team.map(crate::model::ids::TeamKey::new),
|
|
110
|
-
);
|
|
111
108
|
match action.as_str() {
|
|
112
|
-
"assign" => { tools.assign_task(&payload, Some("bounded contract")).unwrap(); }
|
|
113
|
-
"note" => { println!("S3 note={:?}", tools.update_state(payload.as_str().unwrap()).unwrap()); }
|
|
114
109
|
"collect" => {
|
|
115
110
|
let output = crate::messaging::results::collect_for_team(&dir, Some(&dir.join("result.json")), false, team).unwrap();
|
|
116
111
|
assert_eq!(output["ok"], true);
|
|
@@ -152,10 +147,7 @@ fn s3_tick_preserves_tasks_notes_and_siblings() {
|
|
|
152
147
|
seed(dir, false);
|
|
153
148
|
let hook: SaveHook = Box::new(|ws, observed| {
|
|
154
149
|
assert_eq!(observed["agents"]["w1"]["worker_state"], "DEAD", "tick must produce a real observation delta");
|
|
155
|
-
write(ws.as_path(), "assign", Some("T1"), task("new-task"));
|
|
156
|
-
write(ws.as_path(), "note", Some("T1"), json!("concurrent note"));
|
|
157
150
|
write(ws.as_path(), "collect", Some("T1"), Value::Null);
|
|
158
|
-
write(ws.as_path(), "note", Some("T2"), json!("sibling note"));
|
|
159
151
|
write(ws.as_path(), "sibling", None, Value::Null);
|
|
160
152
|
let latest = disk(ws.as_path());
|
|
161
153
|
assert_eq!(latest["teams"]["T1"]["agents"]["w1"]["pane_id"], observed["agents"]["w1"]["pane_id"]);
|
|
@@ -166,11 +158,11 @@ fn s3_tick_preserves_tasks_notes_and_siblings() {
|
|
|
166
158
|
assert!(report.ok, "tick must save its observation: {report:?}");
|
|
167
159
|
let state = disk(dir);
|
|
168
160
|
let team = &state["teams"]["T1"];
|
|
161
|
+
assert_eq!(team["tasks"][0]["id"], "original");
|
|
169
162
|
assert_eq!(team["tasks"][0]["accepted_result_id"], "result-original");
|
|
170
|
-
|
|
171
|
-
assert_eq!(team["notes"], json!(["original note", "concurrent note"]));
|
|
163
|
+
assert_eq!(team["notes"], json!(["original note"]));
|
|
172
164
|
assert_eq!(team["agents"]["w1"]["worker_state"], "DEAD");
|
|
173
|
-
assert_eq!(state["teams"]["T2"]["notes"], json!(["original note"
|
|
165
|
+
assert_eq!(state["teams"]["T2"]["notes"], json!(["original note"]));
|
|
174
166
|
assert_eq!(state["teams"]["T3"]["notes"], json!(["third"]));
|
|
175
167
|
assert_eq!(state["tasks"], team["tasks"]);
|
|
176
168
|
assert_eq!(StateRepository::new(dir).load_team(Some("T1")).unwrap()["tasks"], team["tasks"]);
|
|
@@ -179,50 +171,26 @@ fn s3_tick_preserves_tasks_notes_and_siblings() {
|
|
|
179
171
|
}
|
|
180
172
|
|
|
181
173
|
#[test]
|
|
182
|
-
fn
|
|
183
|
-
isolated("
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
seed(&dir, false);
|
|
193
|
-
let waiting = start_writer(&dir, left.0, Some("T1"), left.1, true);
|
|
194
|
-
write(&dir, right.0, Some("T1"), right.1);
|
|
195
|
-
finish(waiting, true);
|
|
196
|
-
let state = disk(&dir);
|
|
197
|
-
let team = &state["teams"]["T1"];
|
|
198
|
-
if left.0 == "assign" { assert!(team["tasks"].as_array().unwrap().iter().any(|task| task["id"] == "left")); }
|
|
199
|
-
if right.0 == "assign" { assert!(team["tasks"].as_array().unwrap().iter().any(|task| task["id"] == "right")); }
|
|
200
|
-
if left.0 == "collect" { assert_eq!(team["tasks"][0]["accepted_result_id"], "result-original"); }
|
|
201
|
-
let notes = usize::from(left.0 == "note") + usize::from(right.0 == "note");
|
|
202
|
-
assert_eq!(team["notes"].as_array().unwrap().len(), 1 + notes);
|
|
203
|
-
if left.0 == "note" && right.0 == "assign" {
|
|
204
|
-
let markdown = std::fs::read_to_string(dir.join(".team/runtime/T1/team_state.md")).unwrap();
|
|
205
|
-
assert!(markdown.contains("right"), "note must render the committed task view: {markdown}");
|
|
206
|
-
}
|
|
207
|
-
if left.0 == "collect" {
|
|
208
|
-
write(&dir, "assign", Some("T1"), json!({"id": "original", "assignee": "w1", "title": "edited"}));
|
|
209
|
-
assert_eq!(disk(&dir)["teams"]["T1"]["tasks"][0]["accepted_result_id"], "result-original");
|
|
210
|
-
}
|
|
211
|
-
println!("S3 pair {i} canonical={team}");
|
|
212
|
-
}
|
|
174
|
+
fn s3_result_collection_commit_preserves_concurrent_sibling() {
|
|
175
|
+
isolated("s3_result_collection_commit_preserves_concurrent_sibling", |dir| {
|
|
176
|
+
seed(dir, false);
|
|
177
|
+
let waiting = start_writer(&dir, "collect", Some("T1"), Value::Null, true);
|
|
178
|
+
write(&dir, "sibling", None, Value::Null);
|
|
179
|
+
finish(waiting, true);
|
|
180
|
+
let state = disk(&dir);
|
|
181
|
+
assert_eq!(state["teams"]["T1"]["tasks"][0]["accepted_result_id"], "result-original");
|
|
182
|
+
assert_eq!(state["teams"]["T3"]["notes"], json!(["third"]));
|
|
183
|
+
assert_eq!(state["teams"]["T2"]["notes"], json!(["original note"]));
|
|
213
184
|
});
|
|
214
185
|
}
|
|
215
186
|
|
|
216
187
|
#[test]
|
|
217
|
-
fn
|
|
218
|
-
isolated("
|
|
188
|
+
fn s3_legacy_result_collection_preserves_root_shape() {
|
|
189
|
+
isolated("s3_legacy_result_collection_preserves_root_shape", |dir| {
|
|
219
190
|
seed(dir, true);
|
|
220
|
-
|
|
221
|
-
write(dir, "note", None, json!("legacy note"));
|
|
222
|
-
finish(waiting, true);
|
|
191
|
+
write(dir, "collect", None, Value::Null);
|
|
223
192
|
let state = disk(dir);
|
|
224
193
|
assert_eq!(state["tasks"][0]["accepted_result_id"], "result-original");
|
|
225
|
-
assert_eq!(state["notes"], json!(["original note", "legacy note"]));
|
|
226
194
|
assert!(state.get("teams").is_none(), "legacy single Team shape remains valid");
|
|
227
195
|
});
|
|
228
196
|
}
|
|
@@ -304,20 +304,7 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
304
304
|
identity.team_id.as_str(),
|
|
305
305
|
);
|
|
306
306
|
let prompt = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
|
|
307
|
-
let team_mcp_tools = [
|
|
308
|
-
"assign_task",
|
|
309
|
-
"send_message",
|
|
310
|
-
"update_state",
|
|
311
|
-
"get_team_status",
|
|
312
|
-
"stop_agent",
|
|
313
|
-
"reset_agent",
|
|
314
|
-
"add_agent",
|
|
315
|
-
"clone_agent",
|
|
316
|
-
"fork_agent",
|
|
317
|
-
"request_human",
|
|
318
|
-
"stuck_list",
|
|
319
|
-
"stuck_cancel",
|
|
320
|
-
];
|
|
307
|
+
let team_mcp_tools = ["send_message", "report_result", "get_team_status"];
|
|
321
308
|
crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
|
|
322
309
|
crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
|
|
323
310
|
workspace,
|