@team-agent/installer 0.3.12 → 0.3.13
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 +23 -0
- package/crates/team-agent/src/cli/leader.rs +2 -5
- package/crates/team-agent/src/cli/mod.rs +90 -4
- package/crates/team-agent/src/cli/tests/compile.rs +69 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
- package/crates/team-agent/src/compiler.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +16 -83
- package/crates/team-agent/src/leader/lease.rs +4 -20
- package/crates/team-agent/src/leader/mod.rs +3 -1
- package/crates/team-agent/src/leader/owner_bind.rs +46 -48
- package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
- package/crates/team-agent/src/leader/rediscover.rs +34 -17
- package/crates/team-agent/src/lifecycle/launch.rs +158 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +229 -28
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +135 -8
- package/crates/team-agent/src/lifecycle/types.rs +28 -0
- package/crates/team-agent/src/messaging/delivery.rs +5 -1
- package/crates/team-agent/src/messaging/helpers.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
- package/crates/team-agent/src/provider/adapter.rs +6 -3
- package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
- package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
- package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
- package/crates/team-agent/src/transport/test_support.rs +22 -7
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -166,6 +166,7 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
166
166
|
pub fn cmd_compile(args: &CompileArgs) -> Result<CmdResult, CliError> {
|
|
167
167
|
let spec = crate::compiler::compile_team(&args.team)
|
|
168
168
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
169
|
+
warn_ignored_owner_team_id(&args.team);
|
|
169
170
|
std::fs::write(&args.out, crate::model::yaml::dumps(&spec))?;
|
|
170
171
|
Ok(CmdResult::from_json(
|
|
171
172
|
json!({
|
|
@@ -178,6 +179,28 @@ pub fn cmd_compile(args: &CompileArgs) -> Result<CmdResult, CliError> {
|
|
|
178
179
|
))
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
|
|
183
|
+
let Ok(Some(ignored)) = crate::compiler::ignored_owner_team_id_from_team_md(team_dir) else {
|
|
184
|
+
return;
|
|
185
|
+
};
|
|
186
|
+
let workspace = crate::model::paths::team_workspace(team_dir)
|
|
187
|
+
.unwrap_or_else(|_| team_dir.parent().unwrap_or(team_dir).to_path_buf());
|
|
188
|
+
eprintln!("Warning: ignored TEAM.md {}={}", ignored.field, ignored.value);
|
|
189
|
+
eprintln!("Reason: owner identity is the canonical runtime team key, not TEAM.md front matter");
|
|
190
|
+
eprintln!("Action: remove {} from TEAM.md", ignored.field);
|
|
191
|
+
let fields = json!({
|
|
192
|
+
"field": ignored.field,
|
|
193
|
+
"source": team_dir.join("TEAM.md").to_string_lossy().to_string(),
|
|
194
|
+
"value": ignored.value,
|
|
195
|
+
"warning": "ignored user-set owner_team_id",
|
|
196
|
+
"reason": "owner identity is derived from the canonical runtime team key",
|
|
197
|
+
"action": "remove owner_team_id from TEAM.md",
|
|
198
|
+
});
|
|
199
|
+
if let Err(err) = crate::event_log::EventLog::new(&workspace).write("spec.field_ignored", fields) {
|
|
200
|
+
eprintln!("Warning: spec.field_ignored event write failed: {err}");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
181
204
|
/// `cmd_status`(`commands.py:90`)。三态:`--summary`(xor json,xor agent)→五行文本;
|
|
182
205
|
/// `--json`→`status_port::status(compact=!detail)`;else→`status_port::format_status(agent)`。
|
|
183
206
|
pub fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
|
|
@@ -88,11 +88,8 @@ pub fn cmd_leader_passthrough(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
pub(crate) fn leader_passthrough_provider(command: &str) -> crate::model::enums::Provider {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"copilot" => crate::model::enums::Provider::Copilot,
|
|
94
|
-
_ => crate::model::enums::Provider::ClaudeCode,
|
|
95
|
-
}
|
|
91
|
+
crate::leader::attribute_command_provider(command)
|
|
92
|
+
.unwrap_or(crate::model::enums::Provider::ClaudeCode)
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
// =============================================================================
|
|
@@ -452,7 +452,9 @@ pub mod lifecycle_port {
|
|
|
452
452
|
let coordinator_pid = stopped
|
|
453
453
|
.as_ref()
|
|
454
454
|
.and_then(|stopped| stopped.pid.map(|p| p.get()));
|
|
455
|
-
let
|
|
455
|
+
let coordinator_clean =
|
|
456
|
+
!coordinator_timeout && stopped.as_ref().map(|stopped| stopped.ok).unwrap_or(true);
|
|
457
|
+
let ok = coordinator_clean
|
|
456
458
|
&& kill_error.is_none()
|
|
457
459
|
&& session_residuals.is_empty()
|
|
458
460
|
&& process_residuals.is_empty()
|
|
@@ -1484,9 +1486,28 @@ pub mod lifecycle_port {
|
|
|
1484
1486
|
}
|
|
1485
1487
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
1486
1488
|
match crate::lifecycle::remove_agent(workspace, &agent_id, from_spec, force, team) {
|
|
1487
|
-
Ok(report) => {
|
|
1488
|
-
|
|
1489
|
-
|
|
1489
|
+
Ok(report @ crate::lifecycle::RemoveAgentOutcome::Removed { .. }) => Ok(json!({
|
|
1490
|
+
"ok": true,
|
|
1491
|
+
"agent_id": agent,
|
|
1492
|
+
"status": "removed",
|
|
1493
|
+
"report": format!("{report:?}"),
|
|
1494
|
+
})),
|
|
1495
|
+
Ok(crate::lifecycle::RemoveAgentOutcome::RefusedFromSpecConfirm { .. }) => Ok(json!({
|
|
1496
|
+
"ok": false,
|
|
1497
|
+
"agent_id": agent,
|
|
1498
|
+
"status": "refused",
|
|
1499
|
+
"reason": "from_spec_confirm_required",
|
|
1500
|
+
"error": "remove-agent requires --from-spec --confirm for spec-defined agents",
|
|
1501
|
+
"action": "rerun with --from-spec --confirm, or omit --from-spec only for dynamic agents",
|
|
1502
|
+
})),
|
|
1503
|
+
Ok(crate::lifecycle::RemoveAgentOutcome::RefusedForceRequired { .. }) => Ok(json!({
|
|
1504
|
+
"ok": false,
|
|
1505
|
+
"agent_id": agent,
|
|
1506
|
+
"status": "refused",
|
|
1507
|
+
"reason": "force_required",
|
|
1508
|
+
"error": "agent is running; remove-agent requires --force",
|
|
1509
|
+
"action": "rerun with --force to stop and remove the running agent",
|
|
1510
|
+
})),
|
|
1490
1511
|
Err(e) => Ok(error_value(e)),
|
|
1491
1512
|
}
|
|
1492
1513
|
}
|
|
@@ -1913,6 +1934,71 @@ pub mod lifecycle_port {
|
|
|
1913
1934
|
"next_actions": next_actions,
|
|
1914
1935
|
"attach_commands": attach_commands,
|
|
1915
1936
|
}),
|
|
1937
|
+
crate::lifecycle::RestartReport::Partial {
|
|
1938
|
+
session_name,
|
|
1939
|
+
agents,
|
|
1940
|
+
failed_agents,
|
|
1941
|
+
coordinator_started,
|
|
1942
|
+
next_actions,
|
|
1943
|
+
attach_commands,
|
|
1944
|
+
} => json!({
|
|
1945
|
+
"ok": false,
|
|
1946
|
+
"status": "partial",
|
|
1947
|
+
"reason": "restart_agent_failed",
|
|
1948
|
+
"session_name": session_name.as_str(),
|
|
1949
|
+
"agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
|
|
1950
|
+
"failed_agents": failed_agents.iter().map(|failure| json!({
|
|
1951
|
+
"agent_id": failure.agent_id.as_str(),
|
|
1952
|
+
"restart_mode": failure.restart_mode,
|
|
1953
|
+
"decision": failure.decision,
|
|
1954
|
+
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
1955
|
+
"phase": failure.phase,
|
|
1956
|
+
"error": failure.error,
|
|
1957
|
+
"action": format!(
|
|
1958
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
1959
|
+
failure.agent_id,
|
|
1960
|
+
failure.agent_id
|
|
1961
|
+
),
|
|
1962
|
+
"log": format!(
|
|
1963
|
+
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
1964
|
+
failure.agent_id
|
|
1965
|
+
),
|
|
1966
|
+
})).collect::<Vec<_>>(),
|
|
1967
|
+
"coordinator_started": coordinator_started,
|
|
1968
|
+
"next_actions": next_actions,
|
|
1969
|
+
"attach_commands": attach_commands,
|
|
1970
|
+
}),
|
|
1971
|
+
crate::lifecycle::RestartReport::Failed {
|
|
1972
|
+
session_name,
|
|
1973
|
+
failed_agents,
|
|
1974
|
+
next_actions,
|
|
1975
|
+
attach_commands,
|
|
1976
|
+
} => json!({
|
|
1977
|
+
"ok": false,
|
|
1978
|
+
"status": "failed",
|
|
1979
|
+
"reason": "restart_all_agents_failed",
|
|
1980
|
+
"session_name": session_name.as_str(),
|
|
1981
|
+
"agents": [],
|
|
1982
|
+
"failed_agents": failed_agents.iter().map(|failure| json!({
|
|
1983
|
+
"agent_id": failure.agent_id.as_str(),
|
|
1984
|
+
"restart_mode": failure.restart_mode,
|
|
1985
|
+
"decision": failure.decision,
|
|
1986
|
+
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
1987
|
+
"phase": failure.phase,
|
|
1988
|
+
"error": failure.error,
|
|
1989
|
+
"action": format!(
|
|
1990
|
+
"inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
|
|
1991
|
+
failure.agent_id,
|
|
1992
|
+
failure.agent_id
|
|
1993
|
+
),
|
|
1994
|
+
"log": format!(
|
|
1995
|
+
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
1996
|
+
failure.agent_id
|
|
1997
|
+
),
|
|
1998
|
+
})).collect::<Vec<_>>(),
|
|
1999
|
+
"next_actions": next_actions,
|
|
2000
|
+
"attach_commands": attach_commands,
|
|
2001
|
+
}),
|
|
1916
2002
|
crate::lifecycle::RestartReport::RefusedResumeAtomicity {
|
|
1917
2003
|
unresumable,
|
|
1918
2004
|
allow_fresh,
|
|
@@ -16,6 +16,16 @@ fn compile_team_dir(tag: &str) -> std::path::PathBuf {
|
|
|
16
16
|
team
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
fn compile_team_dir_with_owner_team_id(tag: &str) -> std::path::PathBuf {
|
|
20
|
+
let team = compile_team_dir(tag);
|
|
21
|
+
std::fs::write(
|
|
22
|
+
team.join("TEAM.md"),
|
|
23
|
+
"---\nname: compileteam\nobjective: Compile probe.\nprovider: fake\nowner_team_id: user-set-owner\n---\n\nCompile team.\n",
|
|
24
|
+
)
|
|
25
|
+
.unwrap();
|
|
26
|
+
team
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
#[test]
|
|
20
30
|
fn cmd_compile_json_and_human_match_golden_shape_and_writes_out() {
|
|
21
31
|
let team = compile_team_dir("compile-ok");
|
|
@@ -47,6 +57,65 @@ fn cmd_compile_json_and_human_match_golden_shape_and_writes_out() {
|
|
|
47
57
|
assert_eq!(human_text, expected_human, "golden human output preserves cmd_compile insertion order");
|
|
48
58
|
}
|
|
49
59
|
|
|
60
|
+
#[test]
|
|
61
|
+
fn cmd_compile_ignores_user_owner_team_id_and_emits_warning_event() {
|
|
62
|
+
let team = compile_team_dir_with_owner_team_id("compile-ignored-owner");
|
|
63
|
+
let workspace = team.parent().unwrap().to_path_buf();
|
|
64
|
+
let out = workspace.join("ignored-owner-out.yaml");
|
|
65
|
+
let args = CompileArgs { team: team.clone(), out: out.clone(), json: true };
|
|
66
|
+
|
|
67
|
+
let result = cmd_compile(&args).expect("compile");
|
|
68
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
69
|
+
let compiled = std::fs::read_to_string(&out).unwrap();
|
|
70
|
+
assert!(
|
|
71
|
+
!compiled.contains("owner_team_id"),
|
|
72
|
+
"user-set owner_team_id must not be compiled into team.spec.yaml"
|
|
73
|
+
);
|
|
74
|
+
let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
|
|
75
|
+
let event = events
|
|
76
|
+
.iter()
|
|
77
|
+
.find(|event| {
|
|
78
|
+
event
|
|
79
|
+
.get("event")
|
|
80
|
+
.and_then(serde_json::Value::as_str)
|
|
81
|
+
== Some("spec.field_ignored")
|
|
82
|
+
})
|
|
83
|
+
.expect("owner_team_id warning event");
|
|
84
|
+
assert_eq!(
|
|
85
|
+
event.get("field").and_then(serde_json::Value::as_str),
|
|
86
|
+
Some("owner_team_id")
|
|
87
|
+
);
|
|
88
|
+
assert_eq!(
|
|
89
|
+
event.get("value").and_then(serde_json::Value::as_str),
|
|
90
|
+
Some("user-set-owner")
|
|
91
|
+
);
|
|
92
|
+
assert_eq!(
|
|
93
|
+
event.get("action").and_then(serde_json::Value::as_str),
|
|
94
|
+
Some("remove owner_team_id from TEAM.md")
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn cmd_compile_without_owner_team_id_emits_no_ignored_field_event() {
|
|
100
|
+
let team = compile_team_dir("compile-no-ignored-owner");
|
|
101
|
+
let workspace = team.parent().unwrap().to_path_buf();
|
|
102
|
+
let out = workspace.join("no-ignored-owner-out.yaml");
|
|
103
|
+
let args = CompileArgs { team: team.clone(), out, json: true };
|
|
104
|
+
|
|
105
|
+
let result = cmd_compile(&args).expect("compile");
|
|
106
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
107
|
+
let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
|
|
108
|
+
assert!(
|
|
109
|
+
events.iter().all(|event| {
|
|
110
|
+
event
|
|
111
|
+
.get("event")
|
|
112
|
+
.and_then(serde_json::Value::as_str)
|
|
113
|
+
!= Some("spec.field_ignored")
|
|
114
|
+
}),
|
|
115
|
+
"TEAM.md without owner_team_id must not emit ignored-field warning"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
50
119
|
#[test]
|
|
51
120
|
fn run_dispatches_compile_and_error_path_exits_error() {
|
|
52
121
|
let team = compile_team_dir("compile-dispatch");
|
|
@@ -472,6 +472,74 @@ fn seed_team_spec(ws: &std::path::Path) {
|
|
|
472
472
|
other => panic!("expected JSON output, got {other:?}"),
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
|
+
fn seed_remove_agent_workspace(ws: &std::path::Path, status: &str) {
|
|
476
|
+
seed_team_spec(ws);
|
|
477
|
+
crate::state::persist::save_runtime_state(
|
|
478
|
+
ws,
|
|
479
|
+
&json!({
|
|
480
|
+
"session_name": "team-agent-fake-e2e",
|
|
481
|
+
"agents": {
|
|
482
|
+
"fake_impl": {
|
|
483
|
+
"status": status,
|
|
484
|
+
"provider": "fake",
|
|
485
|
+
"window": "fake_impl"
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
"spec_path": ws.join("team.spec.yaml").to_string_lossy()
|
|
489
|
+
}),
|
|
490
|
+
)
|
|
491
|
+
.unwrap();
|
|
492
|
+
}
|
|
493
|
+
#[test]
|
|
494
|
+
fn remove_agent_running_refusal_is_not_success_envelope() {
|
|
495
|
+
let ws = tmp_workspace();
|
|
496
|
+
seed_remove_agent_workspace(&ws, "running");
|
|
497
|
+
let out = json_output(
|
|
498
|
+
cmd_remove_agent(&RemoveAgentArgs {
|
|
499
|
+
agent: "fake_impl".to_string(),
|
|
500
|
+
workspace: ws.clone(),
|
|
501
|
+
team: None,
|
|
502
|
+
from_spec: true,
|
|
503
|
+
confirm: true,
|
|
504
|
+
force: false,
|
|
505
|
+
json: true,
|
|
506
|
+
})
|
|
507
|
+
.unwrap(),
|
|
508
|
+
);
|
|
509
|
+
assert_eq!(out["ok"], json!(false));
|
|
510
|
+
assert_eq!(out["status"], json!("refused"));
|
|
511
|
+
assert_eq!(out["reason"], json!("force_required"));
|
|
512
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
513
|
+
assert!(
|
|
514
|
+
state["agents"].get("fake_impl").is_some(),
|
|
515
|
+
"refused remove-agent must not delete the running agent"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
#[test]
|
|
519
|
+
fn remove_agent_from_spec_refusal_is_not_success_envelope() {
|
|
520
|
+
let ws = tmp_workspace();
|
|
521
|
+
seed_remove_agent_workspace(&ws, "stopped");
|
|
522
|
+
let out = json_output(
|
|
523
|
+
cmd_remove_agent(&RemoveAgentArgs {
|
|
524
|
+
agent: "fake_impl".to_string(),
|
|
525
|
+
workspace: ws.clone(),
|
|
526
|
+
team: None,
|
|
527
|
+
from_spec: false,
|
|
528
|
+
confirm: true,
|
|
529
|
+
force: false,
|
|
530
|
+
json: true,
|
|
531
|
+
})
|
|
532
|
+
.unwrap(),
|
|
533
|
+
);
|
|
534
|
+
assert_eq!(out["ok"], json!(false));
|
|
535
|
+
assert_eq!(out["status"], json!("refused"));
|
|
536
|
+
assert_eq!(out["reason"], json!("from_spec_confirm_required"));
|
|
537
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
538
|
+
assert!(
|
|
539
|
+
state["agents"].get("fake_impl").is_some(),
|
|
540
|
+
"refused remove-agent must not delete the spec-defined agent"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
475
543
|
#[test]
|
|
476
544
|
fn validate_result_file_good_and_inline_garbage_are_distinct() {
|
|
477
545
|
let ws = tmp_workspace();
|
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
//! 集成面由 tests/b5_leader_terminal_kill_red.rs 的真 tmux 契约覆盖,此处锁纯决策 + 4 反向 case。
|
|
6
6
|
|
|
7
7
|
use crate::cli::lifecycle_port::{sessions_to_kill, KillDecision};
|
|
8
|
-
use crate::transport::
|
|
9
|
-
|
|
8
|
+
use crate::transport::{
|
|
9
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
10
|
+
Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome, SpawnResult, Target, Transport,
|
|
11
|
+
TransportError, WindowName,
|
|
12
|
+
};
|
|
13
|
+
use serde_json::json;
|
|
14
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
15
|
+
use std::path::{Path, PathBuf};
|
|
16
|
+
use std::sync::Mutex;
|
|
10
17
|
|
|
11
18
|
fn names(raw: &[&str]) -> Vec<SessionName> {
|
|
12
19
|
raw.iter().map(|name| SessionName::new(*name)).collect()
|
|
@@ -102,3 +109,154 @@ fn union_prefix_and_anchor_no_double_count() {
|
|
|
102
109
|
KillDecision::KillIndividually { to_kill: vec![], spared: names(&["team-agent-leader-claude-ws-beef"]) }
|
|
103
110
|
);
|
|
104
111
|
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn missing_coordinator_is_ok_when_shutdown_cleaned_session() {
|
|
115
|
+
let ws = tmp_shutdown_workspace("missing-coordinator-clean");
|
|
116
|
+
crate::state::persist::save_runtime_state(
|
|
117
|
+
&ws,
|
|
118
|
+
&json!({
|
|
119
|
+
"session_name": "team-lane-l1-clean-shutdown",
|
|
120
|
+
"agents": {
|
|
121
|
+
"fake_impl": {
|
|
122
|
+
"status": "running",
|
|
123
|
+
"provider": "fake",
|
|
124
|
+
"window": "fake_impl"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
.unwrap();
|
|
130
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
131
|
+
&ws,
|
|
132
|
+
true,
|
|
133
|
+
None,
|
|
134
|
+
&CleanShutdownTransport::new(),
|
|
135
|
+
)
|
|
136
|
+
.expect("shutdown should complete");
|
|
137
|
+
assert_eq!(out["coordinator"]["status"], json!("missing"));
|
|
138
|
+
assert_eq!(
|
|
139
|
+
out["ok"], json!(true),
|
|
140
|
+
"coordinator.status=missing alone must not make a fully cleaned shutdown partial: {out}"
|
|
141
|
+
);
|
|
142
|
+
assert_eq!(out["status"], json!("ok"));
|
|
143
|
+
assert_eq!(out["residuals"]["sessions"], json!([]));
|
|
144
|
+
assert_eq!(out["residuals"]["processes"], json!([]));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
|
|
148
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
149
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
150
|
+
let dir = std::env::temp_dir().join(format!(
|
|
151
|
+
"ta-shutdown-{tag}-{}-{}",
|
|
152
|
+
std::process::id(),
|
|
153
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
154
|
+
));
|
|
155
|
+
std::fs::create_dir_all(dir.join(".team").join("runtime")).unwrap();
|
|
156
|
+
dir
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
struct CleanShutdownTransport {
|
|
160
|
+
session_present: Mutex<bool>,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
impl CleanShutdownTransport {
|
|
164
|
+
fn new() -> Self {
|
|
165
|
+
Self { session_present: Mutex::new(true) }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
impl Transport for CleanShutdownTransport {
|
|
170
|
+
fn kind(&self) -> BackendKind {
|
|
171
|
+
BackendKind::Tmux
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn spawn_first(
|
|
175
|
+
&self,
|
|
176
|
+
_session: &SessionName,
|
|
177
|
+
_window: &WindowName,
|
|
178
|
+
_argv: &[String],
|
|
179
|
+
_cwd: &Path,
|
|
180
|
+
_env: &BTreeMap<String, String>,
|
|
181
|
+
) -> Result<SpawnResult, TransportError> {
|
|
182
|
+
unimplemented!("shutdown test does not spawn")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn spawn_into(
|
|
186
|
+
&self,
|
|
187
|
+
_session: &SessionName,
|
|
188
|
+
_window: &WindowName,
|
|
189
|
+
_argv: &[String],
|
|
190
|
+
_cwd: &Path,
|
|
191
|
+
_env: &BTreeMap<String, String>,
|
|
192
|
+
) -> Result<SpawnResult, TransportError> {
|
|
193
|
+
unimplemented!("shutdown test does not spawn")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn inject(
|
|
197
|
+
&self,
|
|
198
|
+
_target: &Target,
|
|
199
|
+
_payload: &InjectPayload,
|
|
200
|
+
_submit: Key,
|
|
201
|
+
_bracketed: bool,
|
|
202
|
+
) -> Result<InjectReport, TransportError> {
|
|
203
|
+
unimplemented!("shutdown test does not inject")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
207
|
+
Ok(())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn capture(
|
|
211
|
+
&self,
|
|
212
|
+
_target: &Target,
|
|
213
|
+
range: CaptureRange,
|
|
214
|
+
) -> Result<CapturedText, TransportError> {
|
|
215
|
+
Ok(CapturedText { text: String::new(), range })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
|
|
219
|
+
Ok(None)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn liveness(
|
|
223
|
+
&self,
|
|
224
|
+
_pane: &PaneId,
|
|
225
|
+
) -> Result<crate::model::enums::PaneLiveness, TransportError> {
|
|
226
|
+
Ok(crate::model::enums::PaneLiveness::Live)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
230
|
+
Ok(Vec::new())
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
234
|
+
Ok(*self.session_present.lock().unwrap())
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
238
|
+
Ok(Vec::new())
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn set_session_env(
|
|
242
|
+
&self,
|
|
243
|
+
_session: &SessionName,
|
|
244
|
+
_key: &str,
|
|
245
|
+
_value: &str,
|
|
246
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
247
|
+
Ok(SetEnvOutcome::Applied)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
251
|
+
*self.session_present.lock().unwrap() = false;
|
|
252
|
+
Ok(())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
256
|
+
Ok(())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
260
|
+
Ok(AttachOutcome::Attached)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -29,6 +29,14 @@ use std::path::Path;
|
|
|
29
29
|
use crate::model::yaml::Value;
|
|
30
30
|
use crate::model::{paths, spec, yaml, ModelError};
|
|
31
31
|
|
|
32
|
+
pub const IGNORED_OWNER_TEAM_ID_FIELD: &str = "owner_team_id";
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
35
|
+
pub struct IgnoredTeamField {
|
|
36
|
+
pub field: &'static str,
|
|
37
|
+
pub value: String,
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
/// `compiler._read_front_matter` (compiler.py:173-185).
|
|
33
41
|
///
|
|
34
42
|
/// Reads `path` (UTF-8). If the text does not start with `"---\n"`, returns
|
|
@@ -76,6 +84,21 @@ pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
|
|
|
76
84
|
Ok((meta, after_marker.trim_start_matches('\n').to_string()))
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
pub fn ignored_owner_team_id_from_team_md(team_dir: &Path) -> Result<Option<IgnoredTeamField>, ModelError> {
|
|
88
|
+
let team_md = team_dir.join("TEAM.md");
|
|
89
|
+
if !team_md.exists() {
|
|
90
|
+
return Ok(None);
|
|
91
|
+
}
|
|
92
|
+
let (team_meta, _) = read_front_matter(&team_md)?;
|
|
93
|
+
let Some(value) = team_meta.get(IGNORED_OWNER_TEAM_ID_FIELD) else {
|
|
94
|
+
return Ok(None);
|
|
95
|
+
};
|
|
96
|
+
Ok(Some(IgnoredTeamField {
|
|
97
|
+
field: IGNORED_OWNER_TEAM_ID_FIELD,
|
|
98
|
+
value: front_matter_value_label(value),
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
/// `compiler.compile_team` (compiler.py:23-135) — returns the compiled spec dict.
|
|
80
103
|
///
|
|
81
104
|
/// `TEAM.md` + sorted `agents/*.md` → the canonical spec `Value::Map` with the
|
|
@@ -518,5 +541,12 @@ fn max_active_agents(count: usize) -> i64 {
|
|
|
518
541
|
}
|
|
519
542
|
}
|
|
520
543
|
|
|
544
|
+
fn front_matter_value_label(value: &Value) -> String {
|
|
545
|
+
value
|
|
546
|
+
.as_str()
|
|
547
|
+
.map(ToString::to_string)
|
|
548
|
+
.unwrap_or_else(|| yaml::dumps(value).trim().to_string())
|
|
549
|
+
}
|
|
550
|
+
|
|
521
551
|
#[cfg(test)]
|
|
522
552
|
mod tests;
|