@team-agent/installer 0.5.101 → 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/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +49 -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/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- 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/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 +14 -18
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +1 -33
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +5 -5
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +24 -116
- 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
|
@@ -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",
|
|
@@ -575,6 +577,7 @@ fn compile_role_agent_with_mode(
|
|
|
575
577
|
})?;
|
|
576
578
|
Some(parsed)
|
|
577
579
|
}
|
|
580
|
+
_ if is_pi => Some(ProviderEffort::Max),
|
|
578
581
|
_ => None,
|
|
579
582
|
};
|
|
580
583
|
let team_effort = match string_field(team_meta, "provider_effort") {
|
|
@@ -671,15 +674,6 @@ fn string_field(meta: &Value, key: &str) -> Option<String> {
|
|
|
671
674
|
.map(ToString::to_string)
|
|
672
675
|
}
|
|
673
676
|
|
|
674
|
-
fn required_string(meta: &Value, path: &Path, key: &str) -> Result<String, ModelError> {
|
|
675
|
-
string_field(meta, key).ok_or_else(|| {
|
|
676
|
-
ModelError::Validation(format!(
|
|
677
|
-
"{}: missing front matter field {key}",
|
|
678
|
-
path.display()
|
|
679
|
-
))
|
|
680
|
-
})
|
|
681
|
-
}
|
|
682
|
-
|
|
683
677
|
/// 缺 model 时框架会填内建默认(grok 上是 grok-4),席位因此静默拿到
|
|
684
678
|
/// 与角色文件无关的模型和上下文窗口,而 argv 看起来完全正常。
|
|
685
679
|
/// 角色文件必须自己写死;内建默认、team 级默认、CLI 全局默认都是隐式来源。
|
|
@@ -749,19 +743,10 @@ fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<
|
|
|
749
743
|
Ok(())
|
|
750
744
|
}
|
|
751
745
|
|
|
752
|
-
|
|
753
|
-
/// 缺 = 编译失败(消息含硬串);非 bool = 同 fail-loud。
|
|
754
|
-
fn required_dangerously_skip_permissions(meta: &Value, path: &Path) -> Result<bool, ModelError> {
|
|
746
|
+
fn optional_dangerously_skip_permissions(meta: &Value) -> bool {
|
|
755
747
|
match meta.get("dangerously_skip_permissions") {
|
|
756
|
-
Some(Value::Bool(value)) =>
|
|
757
|
-
|
|
758
|
-
"{}: 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.",
|
|
759
|
-
path.display()
|
|
760
|
-
))),
|
|
761
|
-
None => Err(ModelError::Validation(format!(
|
|
762
|
-
"{}: missing front matter field dangerously_skip_permissions. This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
|
|
763
|
-
path.display()
|
|
764
|
-
))),
|
|
748
|
+
Some(Value::Bool(value)) => *value,
|
|
749
|
+
_ => true,
|
|
765
750
|
}
|
|
766
751
|
}
|
|
767
752
|
|
|
@@ -883,10 +868,9 @@ fn non_empty_trimmed(text: &str) -> Option<String> {
|
|
|
883
868
|
}
|
|
884
869
|
}
|
|
885
870
|
|
|
886
|
-
fn
|
|
887
|
-
|
|
888
|
-
.
|
|
889
|
-
.and_then(Path::file_name)
|
|
871
|
+
fn workspace_dir_name(workspace: &Path) -> String {
|
|
872
|
+
workspace
|
|
873
|
+
.file_name()
|
|
890
874
|
.and_then(|s| s.to_str())
|
|
891
875
|
.filter(|s| !s.is_empty())
|
|
892
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,
|
|
@@ -167,7 +167,7 @@ pub(super) fn spawn_agents(
|
|
|
167
167
|
model: command_model,
|
|
168
168
|
effort: agent_effort,
|
|
169
169
|
system_prompt: &system_prompt,
|
|
170
|
-
team_mcp_tools: &["send_message", "report_result"],
|
|
170
|
+
team_mcp_tools: &["send_message", "report_result", "get_team_status"],
|
|
171
171
|
mcp_config: &mcp_config,
|
|
172
172
|
session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
|
|
173
173
|
},
|
|
@@ -397,7 +397,7 @@ pub(super) fn spawn_agent_window(
|
|
|
397
397
|
model: command_model,
|
|
398
398
|
effort: restart_effort,
|
|
399
399
|
system_prompt: &system_prompt,
|
|
400
|
-
team_mcp_tools: &["send_message", "report_result"],
|
|
400
|
+
team_mcp_tools: &["send_message", "report_result", "get_team_status"],
|
|
401
401
|
mcp_config: &mcp_config,
|
|
402
402
|
session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
|
|
403
403
|
};
|
|
@@ -532,22 +532,6 @@ fn reset_stopped_true_still_rechecks_same_role_residue_before_start() {
|
|
|
532
532
|
);
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
-
#[test]
|
|
536
|
-
fn mcp_reset_output_exposes_capture_outcome_and_weak_reset_proof() {
|
|
537
|
-
let src = include_str!(concat!(
|
|
538
|
-
env!("CARGO_MANIFEST_DIR"),
|
|
539
|
-
"/src/mcp_server/lifecycle_tools/agent_ops.rs"
|
|
540
|
-
));
|
|
541
|
-
let reset = source_section(src, "pub(crate) fn reset_agent", "pub(crate) fn fork_agent");
|
|
542
|
-
let json_object = reset_success_json_object(reset);
|
|
543
|
-
let missing = ["\"capture_state\":", "\"reset_proof\":", "\"transcript_missing\"", "\"attribution_ambiguous\"", "\"weak\""]
|
|
544
|
-
.into_iter().filter(|needle| !json_object.contains(needle)).collect::<Vec<_>>();
|
|
545
|
-
assert!(
|
|
546
|
-
missing.is_empty(),
|
|
547
|
-
"P0 MCP reset: ok=true/status=reset must expose additive capture_state and reset_proof=weak inside the ResetAgentOutcome::Reset serde_json::json! object; missing={missing:?}; reset_json={json_object}"
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
535
|
#[test]
|
|
552
536
|
#[serial(env)]
|
|
553
537
|
fn abnormal_shell_pane_with_live_delivery_is_not_dead_and_dedupe_ignores_size() {
|
|
@@ -972,15 +956,3 @@ impl ProviderRegistry for RealAdapterRegistry {
|
|
|
972
956
|
get_adapter(provider)
|
|
973
957
|
}
|
|
974
958
|
}
|
|
975
|
-
|
|
976
|
-
fn source_section<'a>(src: &'a str, start: &str, end: &str) -> &'a str { let start = src.find(start).unwrap_or(0); let rest = &src[start..]; &rest[..rest.find(end).unwrap_or(rest.len())] }
|
|
977
|
-
|
|
978
|
-
fn reset_success_json_object(reset_src: &str) -> &str {
|
|
979
|
-
let success = source_section(reset_src, "ResetAgentOutcome::Reset", "ResetAgentOutcome::Refused");
|
|
980
|
-
let macro_start = success.find("serde_json::json!({").expect("reset success json object"); let object_start = macro_start + success[macro_start..].find('{').unwrap(); let mut depth = 0_i32;
|
|
981
|
-
for (offset, ch) in success[object_start..].char_indices() {
|
|
982
|
-
if ch == '{' { depth += 1; } if ch == '}' { depth -= 1; }
|
|
983
|
-
if depth == 0 { return &success[object_start..object_start + offset + 1]; }
|
|
984
|
-
}
|
|
985
|
-
panic!("unterminated reset success json object")
|
|
986
|
-
}
|
|
@@ -108,12 +108,13 @@ fn pi_role_ignores_tools_and_preserves_provider_defaults() {
|
|
|
108
108
|
.expect("Pi model and effort may use provider defaults");
|
|
109
109
|
assert_eq!(
|
|
110
110
|
compiled.agent.get("model"),
|
|
111
|
-
Some(&Value::
|
|
112
|
-
"omitted Pi model
|
|
111
|
+
Some(&Value::Str("openai-codex/gpt-6-luna".to_string())),
|
|
112
|
+
"omitted Pi model uses the Luna default rather than team metadata"
|
|
113
113
|
);
|
|
114
|
-
|
|
115
|
-
compiled.agent.get("effort")
|
|
116
|
-
"
|
|
114
|
+
assert_eq!(
|
|
115
|
+
compiled.agent.get("effort"),
|
|
116
|
+
Some(&Value::Str("max".to_string())),
|
|
117
|
+
"omitted Pi effort uses the global max default rather than team metadata"
|
|
117
118
|
);
|
|
118
119
|
|
|
119
120
|
let models = parse_pi_list_models_table(CATALOG).expect("catalog fixture");
|
|
@@ -160,10 +161,11 @@ fn pi_role_uses_standard_bypass_field_without_fabricating_argv() {
|
|
|
160
161
|
);
|
|
161
162
|
|
|
162
163
|
let missing_ack = valid_role_with("").replace("dangerously_skip_permissions: true\n", "");
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
164
|
+
let compiled = compile_pi_role(&missing_ack)
|
|
165
|
+
.expect("omitted bypass metadata defaults to true");
|
|
166
|
+
assert_eq!(
|
|
167
|
+
compiled.agent.get("dangerously_skip_permissions"),
|
|
168
|
+
Some(&Value::Bool(true))
|
|
167
169
|
);
|
|
168
170
|
}
|
|
169
171
|
|