@team-agent/installer 0.4.6 → 0.4.8
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 +33 -1
- package/crates/team-agent/src/cli/status_port.rs +119 -66
- package/crates/team-agent/src/cli/tests/status_send.rs +74 -48
- package/crates/team-agent/src/coordinator/tick.rs +2 -21
- package/crates/team-agent/src/leader/helpers.rs +1 -22
- package/crates/team-agent/src/lifecycle/launch.rs +1 -11
- package/crates/team-agent/src/lifecycle/profile_launch.rs +25 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +27 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +20 -26
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +30 -8
- package/crates/team-agent/src/lifecycle/restart/selection.rs +67 -16
- package/crates/team-agent/src/lifecycle/restart.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +99 -0
- package/crates/team-agent/src/messaging/delivery.rs +113 -0
- package/crates/team-agent/src/provider/adapter.rs +18 -372
- package/crates/team-agent/src/provider/adapters/claude.rs +106 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +117 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +124 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +17 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +27 -0
- package/crates/team-agent/src/provider/mod.rs +6 -0
- package/crates/team-agent/src/provider/session/capture.rs +131 -1
- package/crates/team-agent/src/provider/wire.rs +139 -0
- package/crates/team-agent/src/state/paths.rs +8 -4
- package/crates/team-agent/src/state/persist.rs +17 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +6 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -226,6 +226,33 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
226
226
|
"status --summary does not accept an agent argument".to_string(),
|
|
227
227
|
));
|
|
228
228
|
}
|
|
229
|
+
// S4QR-001 (0.4.8): selected-team ambiguity gate. status is a selected-team
|
|
230
|
+
// command: when the workspace has 2+ alive teams and no --team was passed,
|
|
231
|
+
// refuse instead of silently defaulting to the active team. Uses the same
|
|
232
|
+
// CommandScope::resolve helper as refuse_if_multi_alive_team_missing_scope
|
|
233
|
+
// (cli/emit.rs:964-979) so the destructive-command ambiguity gate and the
|
|
234
|
+
// selected-team-read ambiguity gate share a single source of truth.
|
|
235
|
+
if team.is_none() {
|
|
236
|
+
let scope = crate::state::paths::CommandScope::resolve(&args.workspace, None);
|
|
237
|
+
if scope.is_ambiguous() {
|
|
238
|
+
let candidates: Vec<String> = scope.candidates().to_vec();
|
|
239
|
+
let message = format!(
|
|
240
|
+
"status: workspace has multiple alive teams ({}); pass `--team <key>` to choose one",
|
|
241
|
+
candidates.join(", ")
|
|
242
|
+
);
|
|
243
|
+
if args.json {
|
|
244
|
+
let payload = serde_json::json!({
|
|
245
|
+
"ok": false,
|
|
246
|
+
"status": "refused",
|
|
247
|
+
"reason": "team_target_ambiguous",
|
|
248
|
+
"candidates": candidates,
|
|
249
|
+
"message": message,
|
|
250
|
+
});
|
|
251
|
+
return Ok(CmdResult::from_json(payload, args.json));
|
|
252
|
+
}
|
|
253
|
+
return Err(CliError::Usage(message));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
229
256
|
let selected = match crate::state::selector::resolve_active_team(
|
|
230
257
|
&args.workspace,
|
|
231
258
|
team,
|
|
@@ -240,11 +267,16 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
240
267
|
}
|
|
241
268
|
};
|
|
242
269
|
if args.summary {
|
|
270
|
+
// 0.4.x compact slimming: `--summary` renders the five-line triage
|
|
271
|
+
// text via `format_status_summary`, which needs the full payload
|
|
272
|
+
// fields (coordinator, leader_receiver, agent_health, queued_messages,
|
|
273
|
+
// latest_results). The slim compact projection drops those, so the
|
|
274
|
+
// summary path must request the full payload (compact=false).
|
|
243
275
|
let value = status_port::status_scoped(
|
|
244
276
|
&selected.run_workspace,
|
|
245
277
|
&selected.state,
|
|
246
278
|
Some(&selected.team_key),
|
|
247
|
-
|
|
279
|
+
false,
|
|
248
280
|
false,
|
|
249
281
|
)?;
|
|
250
282
|
return Ok(CmdResult::human(append_reminder(
|
|
@@ -601,39 +601,127 @@ use rusqlite::params;
|
|
|
601
601
|
Ok(Value::Array(values))
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
/// 0.4.x: slim default compact payload — exactly 7 top-level fields.
|
|
605
|
+
/// Diagnostic detail moves to `--detail`. Plan:
|
|
606
|
+
/// /Users/alauda/Documents/code/team-agent-public/.team/artifacts/status-compact-plan.md
|
|
604
607
|
fn compact_status(full: Value) -> Value {
|
|
608
|
+
let not_ready = compact_not_ready(&full);
|
|
609
|
+
let ready = compact_ready(&full, ¬_ready);
|
|
605
610
|
json!({
|
|
611
|
+
"ok": true,
|
|
606
612
|
"team": full.get("team").cloned().unwrap_or(Value::Null),
|
|
607
613
|
"session_name": full.get("session_name").cloned().unwrap_or(Value::Null),
|
|
608
|
-
"leader_topology": full.get("leader_topology").cloned().unwrap_or_else(|| json!("managed")),
|
|
609
|
-
"is_external_leader": full.get("is_external_leader").cloned().unwrap_or(Value::Bool(false)),
|
|
610
614
|
"leader_attach_command": full.get("leader_attach_command").cloned().unwrap_or(Value::Null),
|
|
611
|
-
"
|
|
612
|
-
"
|
|
613
|
-
"all_spawned": full.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
|
|
614
|
-
"all_attached_receiver": full.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
|
|
615
|
-
"all_resumable_have_session": full.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
|
|
616
|
-
"session_capture_complete": full.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
|
|
617
|
-
"session_capture_incomplete": full.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
|
|
618
|
-
"incomplete_session_capture_agents": full.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
|
|
619
|
-
"pending_session_agent_ids": full.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
|
|
620
|
-
"leader_receiver": compact_object(full.get("leader_receiver"), &[
|
|
621
|
-
"status", "provider", "mode", "session_name", "window_name", "pane_id", "pane_current_command",
|
|
622
|
-
]),
|
|
615
|
+
"ready": ready,
|
|
616
|
+
"not_ready": not_ready,
|
|
623
617
|
"agents": compact_agents(full.get("agents")),
|
|
624
|
-
"agent_health": full.get("agent_health").cloned().unwrap_or_else(|| json!({})),
|
|
625
|
-
"tasks": compact_tasks(full.get("tasks")),
|
|
626
|
-
"messages": full.get("messages").cloned().unwrap_or_else(|| json!({})),
|
|
627
|
-
"queued_messages": take_array(full.get("queued_messages"), 8),
|
|
628
|
-
"results": full.get("results").cloned().unwrap_or_else(|| json!({})),
|
|
629
|
-
"latest_results": take_array(full.get("latest_results"), 5),
|
|
630
|
-
"readiness": full.get("readiness").cloned().unwrap_or_else(|| json!({})),
|
|
631
|
-
"coordinator": compact_object(full.get("coordinator"), &["status", "pid", "metadata_ok", "schema_ok"]),
|
|
632
|
-
"reminder": full.get("reminder").cloned().unwrap_or_else(|| json!(crate::cli::STATUS_REMINDER)),
|
|
633
|
-
"last_events": take_array_tail(full.get("last_events"), 10),
|
|
634
618
|
})
|
|
635
619
|
}
|
|
636
620
|
|
|
621
|
+
/// Synthesized readiness boolean for the slim payload. Stricter than the
|
|
622
|
+
/// raw `readiness.ready` because it also folds in coordinator + schema +
|
|
623
|
+
/// tmux session presence so operators don't need to read separate booleans.
|
|
624
|
+
fn compact_ready(full: &Value, not_ready: &Value) -> bool {
|
|
625
|
+
not_ready.is_null()
|
|
626
|
+
&& full
|
|
627
|
+
.get("readiness")
|
|
628
|
+
.and_then(|r| r.get("ready"))
|
|
629
|
+
.and_then(Value::as_bool)
|
|
630
|
+
.unwrap_or(false)
|
|
631
|
+
&& full
|
|
632
|
+
.get("coordinator")
|
|
633
|
+
.and_then(|c| c.get("status"))
|
|
634
|
+
.and_then(Value::as_str)
|
|
635
|
+
.is_some_and(|s| s == "running" || s == "ok")
|
|
636
|
+
&& full
|
|
637
|
+
.get("coordinator")
|
|
638
|
+
.and_then(|c| c.get("schema_ok"))
|
|
639
|
+
.and_then(Value::as_bool)
|
|
640
|
+
.unwrap_or(true)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/// Returns `Value::Null` when fully ready, otherwise an object:
|
|
644
|
+
/// `{"reasons": [...], "agents": [...]}` listing every gating issue.
|
|
645
|
+
fn compact_not_ready(full: &Value) -> Value {
|
|
646
|
+
let reasons = not_ready_reasons(full);
|
|
647
|
+
if reasons.is_empty() {
|
|
648
|
+
return Value::Null;
|
|
649
|
+
}
|
|
650
|
+
let agents = full
|
|
651
|
+
.get("incomplete_session_capture_agents")
|
|
652
|
+
.and_then(Value::as_array)
|
|
653
|
+
.cloned()
|
|
654
|
+
.or_else(|| {
|
|
655
|
+
full.get("pending_session_agent_ids")
|
|
656
|
+
.and_then(Value::as_array)
|
|
657
|
+
.cloned()
|
|
658
|
+
})
|
|
659
|
+
.unwrap_or_default();
|
|
660
|
+
let mut obj = Map::new();
|
|
661
|
+
obj.insert(
|
|
662
|
+
"reasons".to_string(),
|
|
663
|
+
Value::Array(reasons.into_iter().map(Value::String).collect()),
|
|
664
|
+
);
|
|
665
|
+
obj.insert("agents".to_string(), Value::Array(agents));
|
|
666
|
+
Value::Object(obj)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
fn not_ready_reasons(full: &Value) -> Vec<String> {
|
|
670
|
+
let mut reasons = Vec::new();
|
|
671
|
+
let coord = full.get("coordinator");
|
|
672
|
+
let coord_status = coord
|
|
673
|
+
.and_then(|c| c.get("status"))
|
|
674
|
+
.and_then(Value::as_str)
|
|
675
|
+
.unwrap_or("");
|
|
676
|
+
if coord_status != "running" && coord_status != "ok" {
|
|
677
|
+
reasons.push("coordinator_not_running".to_string());
|
|
678
|
+
}
|
|
679
|
+
if coord
|
|
680
|
+
.and_then(|c| c.get("schema_ok"))
|
|
681
|
+
.and_then(Value::as_bool)
|
|
682
|
+
== Some(false)
|
|
683
|
+
{
|
|
684
|
+
reasons.push("coordinator_schema_not_ok".to_string());
|
|
685
|
+
}
|
|
686
|
+
if full
|
|
687
|
+
.get("tmux_session_present")
|
|
688
|
+
.and_then(Value::as_bool)
|
|
689
|
+
== Some(false)
|
|
690
|
+
{
|
|
691
|
+
reasons.push("tmux_session_missing".to_string());
|
|
692
|
+
}
|
|
693
|
+
let readiness = full.get("readiness");
|
|
694
|
+
if readiness
|
|
695
|
+
.and_then(|r| r.get("all_spawned"))
|
|
696
|
+
.and_then(Value::as_bool)
|
|
697
|
+
== Some(false)
|
|
698
|
+
{
|
|
699
|
+
reasons.push("workers_not_spawned".to_string());
|
|
700
|
+
}
|
|
701
|
+
if readiness
|
|
702
|
+
.and_then(|r| r.get("all_attached_receiver"))
|
|
703
|
+
.and_then(Value::as_bool)
|
|
704
|
+
== Some(false)
|
|
705
|
+
{
|
|
706
|
+
reasons.push("leader_receiver_unbound".to_string());
|
|
707
|
+
}
|
|
708
|
+
if readiness
|
|
709
|
+
.and_then(|r| r.get("session_capture_complete"))
|
|
710
|
+
.and_then(Value::as_bool)
|
|
711
|
+
== Some(false)
|
|
712
|
+
{
|
|
713
|
+
reasons.push("session_capture_incomplete".to_string());
|
|
714
|
+
}
|
|
715
|
+
if readiness
|
|
716
|
+
.and_then(|r| r.get("awaiting_trust_prompt"))
|
|
717
|
+
.and_then(Value::as_bool)
|
|
718
|
+
== Some(true)
|
|
719
|
+
{
|
|
720
|
+
reasons.push("awaiting_trust_prompt".to_string());
|
|
721
|
+
}
|
|
722
|
+
reasons
|
|
723
|
+
}
|
|
724
|
+
|
|
637
725
|
fn compact_agents(value: Option<&Value>) -> Value {
|
|
638
726
|
let Some(Value::Object(input)) = value else {
|
|
639
727
|
return json!({});
|
|
@@ -645,56 +733,21 @@ use rusqlite::params;
|
|
|
645
733
|
Value::Object(out)
|
|
646
734
|
}
|
|
647
735
|
|
|
648
|
-
|
|
736
|
+
/// 0.4.x: agent rows in the slim payload have exactly 4 fields. agent_id
|
|
737
|
+
/// is no longer copied in — the map key already carries it. Diagnostic
|
|
738
|
+
/// fields (model, tmux_window_present, session_id, captured_via,
|
|
739
|
+
/// attribution_confidence, display, interacted) move to `--detail`.
|
|
740
|
+
/// `activity` + `last_output_at` are preserved (RM-039-STAT-001).
|
|
741
|
+
fn compact_agent_state(_agent_id: &str, agent: &Value) -> Value {
|
|
649
742
|
let Some(input) = agent.as_object() else {
|
|
650
743
|
return json!({});
|
|
651
744
|
};
|
|
652
745
|
let mut out = Map::new();
|
|
653
|
-
|
|
654
|
-
"agent_id".to_string(),
|
|
655
|
-
input
|
|
656
|
-
.get("agent_id")
|
|
657
|
-
.cloned()
|
|
658
|
-
.unwrap_or_else(|| Value::String(agent_id.to_string())),
|
|
659
|
-
);
|
|
660
|
-
for key in [
|
|
661
|
-
"status",
|
|
662
|
-
"provider",
|
|
663
|
-
"model",
|
|
664
|
-
"tmux_window_present",
|
|
665
|
-
"session_id",
|
|
666
|
-
"captured_via",
|
|
667
|
-
"attribution_confidence",
|
|
668
|
-
// RM-039-STAT-001 fix (real-machine evidence 2026-06-22): the
|
|
669
|
-
// coordinator-tick activity classifier writes activity (status,
|
|
670
|
-
// confidence, rationale) to the top-level agent state, but the
|
|
671
|
-
// status --json compact projection dropped it. Operators saw
|
|
672
|
-
// "status: running" with no working/idle signal even when the
|
|
673
|
-
// pane was clearly producing output. Add the classifier output
|
|
674
|
-
// here so the compact form surfaces it.
|
|
675
|
-
// last_output_at is the timestamp the classifier advanced when
|
|
676
|
-
// the scrollback digest changed; keeping it adjacent to activity
|
|
677
|
-
// gives the operator a one-glance "is something moving" view.
|
|
678
|
-
// interacted is enriched in enrich_agents (true|false|never)
|
|
679
|
-
// and is the canonical "leader has ever sent this worker a
|
|
680
|
-
// message" signal — also needed for status sanity at a glance.
|
|
681
|
-
"activity",
|
|
682
|
-
"last_output_at",
|
|
683
|
-
"interacted",
|
|
684
|
-
] {
|
|
746
|
+
for key in ["status", "provider", "activity", "last_output_at"] {
|
|
685
747
|
if let Some(value) = input.get(key) {
|
|
686
748
|
out.insert(key.to_string(), value.clone());
|
|
687
749
|
}
|
|
688
750
|
}
|
|
689
|
-
if let Some(display) = input.get("display") {
|
|
690
|
-
let compact_display = compact_object(
|
|
691
|
-
Some(display),
|
|
692
|
-
&["backend", "status", "workspace_window", "pane_id", "pid", "pids", "reason"],
|
|
693
|
-
);
|
|
694
|
-
if compact_display.as_object().is_some_and(|obj| !obj.is_empty()) {
|
|
695
|
-
out.insert("display".to_string(), compact_display);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
751
|
Value::Object(out)
|
|
699
752
|
}
|
|
700
753
|
|
|
@@ -36,7 +36,7 @@ use super::*;
|
|
|
36
36
|
// projection MUST NOT collapse them.
|
|
37
37
|
// =========================================================================
|
|
38
38
|
#[test]
|
|
39
|
-
fn
|
|
39
|
+
fn rm039_stat001_compact_status_preserves_activity_and_last_output() {
|
|
40
40
|
let ws = seed_status_workspace();
|
|
41
41
|
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
42
42
|
if let Some(agents) = state
|
|
@@ -99,13 +99,11 @@ use super::*;
|
|
|
99
99
|
Some("2026-06-22T02:52:30+00:00"),
|
|
100
100
|
"compact projection must preserve `last_output_at` for the \"is something moving\" view"
|
|
101
101
|
);
|
|
102
|
-
//
|
|
103
|
-
// compact
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Some("2026-01-01T00:00:00Z"),
|
|
108
|
-
"compact projection must preserve `interacted` from enrich_agents"
|
|
102
|
+
// 0.4.x compact slim: `interacted` moves to --detail; the 4-field
|
|
103
|
+
// compact agent row keeps only status/provider/activity/last_output_at.
|
|
104
|
+
assert!(
|
|
105
|
+
agent.get("interacted").is_none(),
|
|
106
|
+
"0.4.x: compact projection drops `interacted` (moved to --detail)"
|
|
109
107
|
);
|
|
110
108
|
let _ = std::fs::remove_dir_all(&ws);
|
|
111
109
|
}
|
|
@@ -215,23 +213,37 @@ use super::*;
|
|
|
215
213
|
#[test]
|
|
216
214
|
fn status_port_status_compact_json_shape_against_seeded_fixture() {
|
|
217
215
|
// cmd_status json branch (detail=false) delegates status_port::status(compact=true).
|
|
218
|
-
//
|
|
219
|
-
//
|
|
216
|
+
// 0.4.x compact slim: exactly 7 top-level fields; diagnostics moved
|
|
217
|
+
// to --detail. Plan: .team/artifacts/status-compact-plan.md.
|
|
220
218
|
let ws = seed_status_workspace();
|
|
221
219
|
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
222
220
|
.expect("seeded fixture status should project a value");
|
|
223
221
|
let obj = v.as_object().expect("--json status is a dict");
|
|
224
|
-
//
|
|
225
|
-
|
|
222
|
+
// Exactly these 7 keys, no more.
|
|
223
|
+
let expected: std::collections::BTreeSet<&str> = [
|
|
224
|
+
"ok",
|
|
226
225
|
"team",
|
|
227
226
|
"session_name",
|
|
227
|
+
"leader_attach_command",
|
|
228
|
+
"ready",
|
|
229
|
+
"not_ready",
|
|
230
|
+
"agents",
|
|
231
|
+
]
|
|
232
|
+
.iter()
|
|
233
|
+
.copied()
|
|
234
|
+
.collect();
|
|
235
|
+
let actual: std::collections::BTreeSet<&str> = obj.keys().map(String::as_str).collect();
|
|
236
|
+
assert_eq!(
|
|
237
|
+
actual, expected,
|
|
238
|
+
"0.4.x compact must expose exactly 7 keys; got {actual:?}"
|
|
239
|
+
);
|
|
240
|
+
// Diagnostic keys must NOT leak into the default compact payload.
|
|
241
|
+
for forbidden in [
|
|
228
242
|
"leader_topology",
|
|
229
243
|
"is_external_leader",
|
|
230
|
-
"leader_attach_command",
|
|
231
244
|
"leader_client",
|
|
232
245
|
"tmux_session_present",
|
|
233
246
|
"leader_receiver",
|
|
234
|
-
"agents",
|
|
235
247
|
"agent_health",
|
|
236
248
|
"tasks",
|
|
237
249
|
"messages",
|
|
@@ -239,23 +251,20 @@ use super::*;
|
|
|
239
251
|
"results",
|
|
240
252
|
"latest_results",
|
|
241
253
|
"coordinator",
|
|
254
|
+
"readiness",
|
|
242
255
|
"reminder",
|
|
243
256
|
"last_events",
|
|
244
257
|
] {
|
|
245
|
-
assert!(
|
|
258
|
+
assert!(
|
|
259
|
+
!obj.contains_key(forbidden),
|
|
260
|
+
"0.4.x: compact must NOT contain diagnostic key `{forbidden}` (--detail only)"
|
|
261
|
+
);
|
|
246
262
|
}
|
|
247
|
-
assert_eq!(
|
|
248
|
-
obj.get("reminder").and_then(|v| v.as_str()),
|
|
249
|
-
Some(crate::cli::STATUS_REMINDER)
|
|
250
|
-
);
|
|
251
263
|
// seeded agent surfaces through the projection.
|
|
252
264
|
assert!(
|
|
253
265
|
obj["agents"].as_object().unwrap().contains_key("a1"),
|
|
254
266
|
"seeded agent a1 must appear in compact agents projection"
|
|
255
267
|
);
|
|
256
|
-
// compact bounds: queued_messages[:8] and latest_results[:5] -> arrays.
|
|
257
|
-
assert!(obj["queued_messages"].is_array());
|
|
258
|
-
assert!(obj["latest_results"].as_array().unwrap().len() <= 5);
|
|
259
268
|
let _ = std::fs::remove_dir_all(&ws);
|
|
260
269
|
}
|
|
261
270
|
|
|
@@ -291,14 +300,17 @@ use super::*;
|
|
|
291
300
|
}
|
|
292
301
|
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
293
302
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
let attach = v["leader_attach_command"]
|
|
303
|
+
// 0.4.x: leader_topology / is_external_leader moved to --detail.
|
|
304
|
+
// leader_attach_command stays in the slim compact payload.
|
|
305
|
+
let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
306
|
+
let attach = slim["leader_attach_command"]
|
|
299
307
|
.as_str()
|
|
300
|
-
.expect("
|
|
308
|
+
.expect("compact still includes leader_attach_command");
|
|
301
309
|
assert!(attach.contains("attach -t team-current:leader"), "{attach}");
|
|
310
|
+
|
|
311
|
+
let detail = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
|
|
312
|
+
assert_eq!(detail["leader_topology"], json!("managed"));
|
|
313
|
+
assert_eq!(detail["is_external_leader"], json!(false));
|
|
302
314
|
let _ = std::fs::remove_dir_all(&ws);
|
|
303
315
|
}
|
|
304
316
|
|
|
@@ -365,33 +377,48 @@ use super::*;
|
|
|
365
377
|
}
|
|
366
378
|
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
367
379
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
assert_eq!(v["is_external_leader"], json!(false));
|
|
372
|
-
let attach = v["leader_attach_command"]
|
|
380
|
+
// 0.4.x: topology/external markers moved to --detail; compact keeps attach.
|
|
381
|
+
let slim = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
382
|
+
let attach = slim["leader_attach_command"]
|
|
373
383
|
.as_str()
|
|
374
384
|
.expect("missing marker defaults to managed attach command");
|
|
375
385
|
assert!(attach.contains("attach -t team-current:leader"), "{attach}");
|
|
386
|
+
|
|
387
|
+
let detail = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true).expect("status detail");
|
|
388
|
+
assert_eq!(detail["leader_topology"], json!("managed"));
|
|
389
|
+
assert_eq!(detail["is_external_leader"], json!(false));
|
|
376
390
|
let _ = std::fs::remove_dir_all(&ws);
|
|
377
391
|
}
|
|
378
392
|
|
|
379
393
|
#[test]
|
|
380
394
|
fn status_port_status_detail_full_keeps_uncompacted_events() {
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
// so the seeded task's full row (incl. fields compact would drop) survives.
|
|
395
|
+
// 0.4.x: --detail (compact=false) preserves ALL diagnostic fields the
|
|
396
|
+
// compact slim payload drops. Pin the must-keep set so future
|
|
397
|
+
// refactors can't accidentally strip detail diagnostics.
|
|
385
398
|
let ws = seed_status_workspace();
|
|
386
399
|
let full = status_port::status(&ws, /*compact=*/ false, /*detail=*/ true)
|
|
387
400
|
.expect("seeded fixture full status should project a value");
|
|
388
401
|
let compact = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
|
|
389
402
|
.expect("seeded fixture compact status should project a value");
|
|
390
|
-
// The CLI-owned invariant: detail=true => compact=false; the two projections MUST
|
|
391
|
-
// differ in shape (full carries `messages`/`results` count maps + verbatim tasks).
|
|
392
403
|
assert_ne!(full, compact, "detail (full) and default (compact) projections must differ");
|
|
393
404
|
let full_obj = full.as_object().expect("full status is a dict");
|
|
394
|
-
|
|
405
|
+
for key in [
|
|
406
|
+
"coordinator",
|
|
407
|
+
"readiness",
|
|
408
|
+
"leader_receiver",
|
|
409
|
+
"agent_health",
|
|
410
|
+
"tasks",
|
|
411
|
+
"messages",
|
|
412
|
+
"queued_messages",
|
|
413
|
+
"results",
|
|
414
|
+
"latest_results",
|
|
415
|
+
"last_events",
|
|
416
|
+
] {
|
|
417
|
+
assert!(
|
|
418
|
+
full_obj.contains_key(key),
|
|
419
|
+
"0.4.x: --detail must preserve `{key}` (compact slimming escape hatch)"
|
|
420
|
+
);
|
|
421
|
+
}
|
|
395
422
|
assert_eq!(full_obj["agents"].as_object().unwrap().len(), 1);
|
|
396
423
|
let _ = std::fs::remove_dir_all(&ws);
|
|
397
424
|
}
|
|
@@ -565,16 +592,15 @@ fn status_noncompact_coordinator_includes_ok() {
|
|
|
565
592
|
}
|
|
566
593
|
|
|
567
594
|
#[test]
|
|
568
|
-
fn
|
|
595
|
+
fn status_compact_omits_coordinator_entirely() {
|
|
596
|
+
// 0.4.x compact slim: the entire `coordinator` block moved to --detail.
|
|
597
|
+
// Compact folds coordinator health into the top-level `ready`/`not_ready`
|
|
598
|
+
// synthesis (`coordinator_not_running` / `coordinator_schema_not_ok`).
|
|
569
599
|
let ws = seed_status_workspace();
|
|
570
600
|
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
["metadata_ok", "pid", "schema_ok", "status"].into_iter().collect();
|
|
575
|
-
assert_eq!(
|
|
576
|
-
keys, expected,
|
|
577
|
-
"compact coordinator key set must be EXACTLY {{metadata_ok,pid,schema_ok,status}} (no ok/metadata)"
|
|
601
|
+
assert!(
|
|
602
|
+
v.as_object().unwrap().get("coordinator").is_none(),
|
|
603
|
+
"0.4.x: compact must NOT include `coordinator`; reasons fold into not_ready"
|
|
578
604
|
);
|
|
579
605
|
let _ = std::fs::remove_dir_all(&ws);
|
|
580
606
|
}
|
|
@@ -1656,16 +1656,7 @@ fn turn_state_wire(state: TurnState) -> &'static str {
|
|
|
1656
1656
|
}
|
|
1657
1657
|
}
|
|
1658
1658
|
|
|
1659
|
-
|
|
1660
|
-
match provider {
|
|
1661
|
-
crate::model::enums::Provider::Claude => "claude",
|
|
1662
|
-
crate::model::enums::Provider::ClaudeCode => "claude_code",
|
|
1663
|
-
crate::model::enums::Provider::Codex => "codex",
|
|
1664
|
-
crate::model::enums::Provider::Copilot => "copilot",
|
|
1665
|
-
crate::model::enums::Provider::GeminiCli => "gemini_cli",
|
|
1666
|
-
crate::model::enums::Provider::Fake => "fake",
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1659
|
+
use crate::provider::wire::provider_wire;
|
|
1669
1660
|
|
|
1670
1661
|
#[derive(Debug, Clone)]
|
|
1671
1662
|
struct AbnormalWatchAgent {
|
|
@@ -2409,17 +2400,7 @@ fn monotonic_seconds() -> f64 {
|
|
|
2409
2400
|
}
|
|
2410
2401
|
}
|
|
2411
2402
|
|
|
2412
|
-
|
|
2413
|
-
match raw {
|
|
2414
|
-
"claude" => Some(crate::model::enums::Provider::Claude),
|
|
2415
|
-
"claude_code" => Some(crate::model::enums::Provider::ClaudeCode),
|
|
2416
|
-
"codex" => Some(crate::model::enums::Provider::Codex),
|
|
2417
|
-
"copilot" => Some(crate::model::enums::Provider::Copilot),
|
|
2418
|
-
"gemini_cli" => Some(crate::model::enums::Provider::GeminiCli),
|
|
2419
|
-
"fake" => Some(crate::model::enums::Provider::Fake),
|
|
2420
|
-
_ => None,
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2403
|
+
use crate::provider::wire::parse_provider;
|
|
2423
2404
|
|
|
2424
2405
|
fn capture_window_target(
|
|
2425
2406
|
agent: &Value,
|
|
@@ -9,28 +9,7 @@ use crate::provider::{Provider, RolloutPath, TurnState};
|
|
|
9
9
|
|
|
10
10
|
use super::LeaderError;
|
|
11
11
|
|
|
12
|
-
pub(crate)
|
|
13
|
-
match provider {
|
|
14
|
-
Provider::Claude => "claude",
|
|
15
|
-
Provider::ClaudeCode => "claude_code",
|
|
16
|
-
Provider::Codex => "codex",
|
|
17
|
-
Provider::Copilot => "copilot",
|
|
18
|
-
Provider::GeminiCli => "gemini_cli",
|
|
19
|
-
Provider::Fake => "fake",
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
pub(crate) fn parse_provider(s: &str) -> Option<Provider> {
|
|
24
|
-
match s {
|
|
25
|
-
"claude" => Some(Provider::Claude),
|
|
26
|
-
"claude_code" => Some(Provider::ClaudeCode),
|
|
27
|
-
"codex" => Some(Provider::Codex),
|
|
28
|
-
"copilot" => Some(Provider::Copilot),
|
|
29
|
-
"gemini_cli" => Some(Provider::GeminiCli),
|
|
30
|
-
"fake" => Some(Provider::Fake),
|
|
31
|
-
_ => None,
|
|
32
|
-
}
|
|
33
|
-
}
|
|
12
|
+
pub(crate) use crate::provider::wire::{parse_provider, provider_wire};
|
|
34
13
|
|
|
35
14
|
pub(crate) fn turn_state_wire(state: TurnState) -> &'static str {
|
|
36
15
|
match state {
|
|
@@ -2455,17 +2455,7 @@ fn spec_display_backend(spec: &Value) -> DisplayBackend {
|
|
|
2455
2455
|
crate::lifecycle::display::resolve_display_backend(requested, None).backend
|
|
2456
2456
|
}
|
|
2457
2457
|
|
|
2458
|
-
|
|
2459
|
-
match raw {
|
|
2460
|
-
"claude" => Some(Provider::Claude),
|
|
2461
|
-
"claude_code" => Some(Provider::ClaudeCode),
|
|
2462
|
-
"codex" => Some(Provider::Codex),
|
|
2463
|
-
"copilot" => Some(Provider::Copilot),
|
|
2464
|
-
"gemini_cli" => Some(Provider::GeminiCli),
|
|
2465
|
-
"fake" => Some(Provider::Fake),
|
|
2466
|
-
_ => None,
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2458
|
+
use crate::provider::wire::parse_provider;
|
|
2469
2459
|
|
|
2470
2460
|
fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
2471
2461
|
match raw {
|
|
@@ -419,6 +419,30 @@ fn provider_env_unsets(provider: Provider, auth_mode: AuthMode) -> BTreeSet<Stri
|
|
|
419
419
|
let mut unsets = BTreeSet::new();
|
|
420
420
|
match provider {
|
|
421
421
|
Provider::Claude | Provider::ClaudeCode => {
|
|
422
|
+
// E57 (0.3.27 P0): unset CLAUDE_CODE_SESSION_ID inherited from the
|
|
423
|
+
// leader's environment. When a Claude leader spawns a Claude worker,
|
|
424
|
+
// the worker inherits the leader's CLAUDE_CODE_SESSION_ID; Claude
|
|
425
|
+
// Code then routes the worker's transcript to the leader's session
|
|
426
|
+
// file (~/.claude/projects/<hash>/<leader-session-uuid>.jsonl),
|
|
427
|
+
// corrupting attribution and transcripts. Always unset so Claude
|
|
428
|
+
// Code generates a fresh session id per worker.
|
|
429
|
+
//
|
|
430
|
+
// S1-CAPTURE (0.4.x P0): also unset the rest of the Claude Code
|
|
431
|
+
// child/team env block. These variables make the spawned process
|
|
432
|
+
// believe it is a CHILD of the parent Claude (CLAUDE_CODE_CHILD_SESSION),
|
|
433
|
+
// a TEAM member (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS), or otherwise
|
|
434
|
+
// inherit the parent's invocation identity (ENTRYPOINT / EXECPATH /
|
|
435
|
+
// CLAUDECODE marker). Without unsetting these, a Claude worker
|
|
436
|
+
// spawned from a Claude leader is not an INDEPENDENT process — it
|
|
437
|
+
// joins the leader's session tree and its transcript / state /
|
|
438
|
+
// billing are attributed to the leader. CLAUDE_EFFORT is preserved
|
|
439
|
+
// (effort preference does not affect session attribution).
|
|
440
|
+
unsets.insert("CLAUDE_CODE_SESSION_ID".to_string());
|
|
441
|
+
unsets.insert("CLAUDE_CODE_CHILD_SESSION".to_string());
|
|
442
|
+
unsets.insert("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS".to_string());
|
|
443
|
+
unsets.insert("CLAUDE_CODE_ENTRYPOINT".to_string());
|
|
444
|
+
unsets.insert("CLAUDE_CODE_EXECPATH".to_string());
|
|
445
|
+
unsets.insert("CLAUDECODE".to_string());
|
|
422
446
|
if auth_mode == AuthMode::CompatibleApi {
|
|
423
447
|
unsets.insert("ANTHROPIC_API_KEY".to_string());
|
|
424
448
|
}
|
|
@@ -894,17 +918,7 @@ fn effective_profile_or_agent_model<'a>(
|
|
|
894
918
|
agent.model.as_deref().or_else(|| profile_model(values))
|
|
895
919
|
}
|
|
896
920
|
|
|
897
|
-
pub(crate)
|
|
898
|
-
match raw {
|
|
899
|
-
"claude" => Some(Provider::Claude),
|
|
900
|
-
"claude_code" => Some(Provider::ClaudeCode),
|
|
901
|
-
"codex" => Some(Provider::Codex),
|
|
902
|
-
"copilot" => Some(Provider::Copilot),
|
|
903
|
-
"gemini_cli" => Some(Provider::GeminiCli),
|
|
904
|
-
"fake" => Some(Provider::Fake),
|
|
905
|
-
_ => None,
|
|
906
|
-
}
|
|
907
|
-
}
|
|
921
|
+
pub(crate) use crate::provider::wire::parse_provider;
|
|
908
922
|
|
|
909
923
|
pub(crate) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
910
924
|
match raw {
|