@team-agent/installer 0.5.89 → 0.5.92
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 +145 -172
- package/crates/team-agent/src/cli/diagnose.rs +78 -39
- package/crates/team-agent/src/cli/emit.rs +12 -75
- package/crates/team-agent/src/cli/mod.rs +38 -10
- package/crates/team-agent/src/cli/spec.rs +1 -4
- package/crates/team-agent/src/cli/status_port/inbox.rs +42 -17
- package/crates/team-agent/src/cli/tests/lane_c.rs +34 -26
- package/crates/team-agent/src/cli/tests/main_preserved.rs +4 -70
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -68
- package/crates/team-agent/src/cli/triage.rs +183 -0
- package/crates/team-agent/src/cli/types.rs +3 -16
- package/crates/team-agent/src/coordinator/health.rs +41 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +5 -6
- package/crates/team-agent/src/coordinator/tick.rs +1 -16
- package/crates/team-agent/src/coordinator/types.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +10 -5
- package/crates/team-agent/src/db/migration.rs +36 -15
- package/crates/team-agent/src/fake_worker.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -10
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -9
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -26
- package/crates/team-agent/src/main.rs +7 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +234 -386
- package/crates/team-agent/src/messaging/tests/runtime.rs +91 -135
- package/crates/team-agent/src/messaging/watchers.rs +1 -1
- package/crates/team-agent/src/redaction.rs +5 -1
- package/crates/team-agent/src/topology.rs +17 -11
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
- package/skills/team-agent/command-coverage.json +2 -9
- package/skills/team-agent/references/team-agent-operator.md +661 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//! Command-specific human triage projections for `doctor` and `diagnose`.
|
|
2
|
+
//!
|
|
3
|
+
//! The JSON report remains the source of truth. This renderer only projects its
|
|
4
|
+
//! stable issue and repair entries; it does not perform I/O or recompute health.
|
|
5
|
+
|
|
6
|
+
use serde_json::Value;
|
|
7
|
+
|
|
8
|
+
use super::{CmdOutput, CmdResult};
|
|
9
|
+
|
|
10
|
+
const LINE_LIMIT_BYTES: usize = 160;
|
|
11
|
+
|
|
12
|
+
pub(crate) fn report(value: Value, as_json: bool, command: &str) -> CmdResult {
|
|
13
|
+
let result = CmdResult::from_json(value, as_json);
|
|
14
|
+
if as_json {
|
|
15
|
+
result
|
|
16
|
+
} else {
|
|
17
|
+
human_result(result, command)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub(crate) fn human_result(result: CmdResult, command: &str) -> CmdResult {
|
|
22
|
+
let text = match &result.output {
|
|
23
|
+
CmdOutput::Json(report) => render(command, report),
|
|
24
|
+
CmdOutput::Human(text) => text.clone(),
|
|
25
|
+
CmdOutput::None => String::new(),
|
|
26
|
+
};
|
|
27
|
+
CmdResult {
|
|
28
|
+
output: CmdOutput::Human(text),
|
|
29
|
+
..result
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(crate) fn render(command: &str, report: &Value) -> String {
|
|
34
|
+
// Keep the renderer downstream of the external redaction boundary. The
|
|
35
|
+
// generic emitter redacts again for defense in depth.
|
|
36
|
+
let report = crate::redaction::redact_external_value(report);
|
|
37
|
+
let ok = report.get("ok").and_then(Value::as_bool).unwrap_or(false);
|
|
38
|
+
let mut lines = vec![bounded(format!(
|
|
39
|
+
"{command}: {}",
|
|
40
|
+
if ok { "ok" } else { "needs attention" }
|
|
41
|
+
))];
|
|
42
|
+
|
|
43
|
+
if command == "doctor" {
|
|
44
|
+
for finding in array_items(
|
|
45
|
+
report
|
|
46
|
+
.get("secret_scan")
|
|
47
|
+
.and_then(|scan| scan.get("findings")),
|
|
48
|
+
) {
|
|
49
|
+
let Some(object) = finding.as_object() else {
|
|
50
|
+
continue;
|
|
51
|
+
};
|
|
52
|
+
let (Some(rule), Some(path), Some(line)) = (
|
|
53
|
+
object.get("rule").and_then(Value::as_str),
|
|
54
|
+
object.get("path").and_then(Value::as_str),
|
|
55
|
+
object.get("line").and_then(Value::as_u64),
|
|
56
|
+
) else {
|
|
57
|
+
continue;
|
|
58
|
+
};
|
|
59
|
+
lines.push(bounded(format!("warn: {rule} in {path}:{line}")));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for issue in array_items(report.get("issues")) {
|
|
64
|
+
lines.push(bounded(format!("issue: {}", summarize(issue, false))));
|
|
65
|
+
}
|
|
66
|
+
for repair in array_items(report.get("suggested_repairs")) {
|
|
67
|
+
lines.push(bounded(format!("repair: {}", summarize(repair, true))));
|
|
68
|
+
}
|
|
69
|
+
if let Some(error) = report.get("error").filter(|value| !value.is_null()) {
|
|
70
|
+
lines.push(bounded(format!("detail: {}", summarize(error, false))));
|
|
71
|
+
}
|
|
72
|
+
if lines.len() == 1 {
|
|
73
|
+
lines.push(bounded("detail: no issues detected".to_string()));
|
|
74
|
+
}
|
|
75
|
+
lines.join("\n")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn array_items(value: Option<&Value>) -> &[Value] {
|
|
79
|
+
value.and_then(Value::as_array).map(Vec::as_slice).unwrap_or(&[])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn summarize(value: &Value, repair: bool) -> String {
|
|
83
|
+
match value {
|
|
84
|
+
Value::String(value) => value.clone(),
|
|
85
|
+
Value::Null => "null".to_string(),
|
|
86
|
+
Value::Bool(value) => value.to_string(),
|
|
87
|
+
Value::Number(value) => value.to_string(),
|
|
88
|
+
Value::Array(values) => format!("{} entries", values.len()),
|
|
89
|
+
Value::Object(object) => {
|
|
90
|
+
let keys = if repair {
|
|
91
|
+
["action", "hint_action", "next_action", "issue", "code", "reason"]
|
|
92
|
+
.as_slice()
|
|
93
|
+
} else {
|
|
94
|
+
["code", "issue", "id", "type", "status", "reason", "message"]
|
|
95
|
+
.as_slice()
|
|
96
|
+
};
|
|
97
|
+
let mut parts = Vec::new();
|
|
98
|
+
for key in keys {
|
|
99
|
+
if let Some(value) = object.get(*key).and_then(Value::as_str) {
|
|
100
|
+
parts.push(format!("{key}={value}"));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if parts.is_empty() {
|
|
104
|
+
let mut names = object.keys().cloned().collect::<Vec<_>>();
|
|
105
|
+
names.sort_unstable();
|
|
106
|
+
format!("fields={}", names.join(","))
|
|
107
|
+
} else {
|
|
108
|
+
parts.join("; ")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn bounded(line: String) -> String {
|
|
115
|
+
let clean = line
|
|
116
|
+
.chars()
|
|
117
|
+
.map(|ch| if ch.is_control() { ' ' } else { ch })
|
|
118
|
+
.collect::<String>();
|
|
119
|
+
if clean.len() <= LINE_LIMIT_BYTES {
|
|
120
|
+
return clean;
|
|
121
|
+
}
|
|
122
|
+
let budget = LINE_LIMIT_BYTES.saturating_sub('…'.len_utf8());
|
|
123
|
+
let mut end = 0;
|
|
124
|
+
for (index, ch) in clean.char_indices() {
|
|
125
|
+
let next = index + ch.len_utf8();
|
|
126
|
+
if next > budget {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
end = next;
|
|
130
|
+
}
|
|
131
|
+
format!("{}…", &clean[..end.min(budget)])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[cfg(test)]
|
|
135
|
+
mod tests {
|
|
136
|
+
use super::*;
|
|
137
|
+
use serde_json::json;
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn renderer_projects_each_issue_and_repair_without_dumping_runtime() {
|
|
141
|
+
let report = json!({
|
|
142
|
+
"ok": false,
|
|
143
|
+
"issues": ["short_issue", {"code": "object_issue", "message": "detail"}],
|
|
144
|
+
"suggested_repairs": [{"issue": "object_issue", "action": "run repair"}],
|
|
145
|
+
"providers": {"claude": {"version": "secret"}},
|
|
146
|
+
"runtime": {"workspace": "/tmp/secret"}
|
|
147
|
+
});
|
|
148
|
+
let output = render("diagnose", &report);
|
|
149
|
+
assert!(output.contains("issue: short_issue"));
|
|
150
|
+
assert!(output.contains("issue: code=object_issue"));
|
|
151
|
+
assert!(output.contains("repair: action=run repair"));
|
|
152
|
+
assert!(!output.contains("providers:"));
|
|
153
|
+
assert!(!output.contains("runtime:"));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn renderer_sanitizes_and_bounds_utf8_lines() {
|
|
158
|
+
let report = json!({"ok": false, "issues": ["问题\n".to_string() + &"x".repeat(200)]});
|
|
159
|
+
for line in render("doctor", &report).lines() {
|
|
160
|
+
assert!(line.len() <= LINE_LIMIT_BYTES);
|
|
161
|
+
assert!(!line.chars().any(char::is_control));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn renderer_projects_secret_findings_without_values() {
|
|
167
|
+
let report = json!({
|
|
168
|
+
"ok": false,
|
|
169
|
+
"secret_scan": {
|
|
170
|
+
"findings": [{
|
|
171
|
+
"rule": "api_key_assignment",
|
|
172
|
+
"path": "leaky-role.md",
|
|
173
|
+
"line": 7,
|
|
174
|
+
"match_excerpt": "OPENAI_API_KEY=secret"
|
|
175
|
+
}]
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
let output = render("doctor", &report);
|
|
179
|
+
assert!(output.contains("warn: api_key_assignment in leaky-role.md:7"));
|
|
180
|
+
assert!(!output.contains("match_excerpt"));
|
|
181
|
+
assert!(!output.contains("OPENAI_API_KEY=secret"));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -44,10 +44,10 @@ pub enum CliError {
|
|
|
44
44
|
/// state 解析失败(歧义/未找到 team 等)。透传 step 5。
|
|
45
45
|
#[error("{0}")]
|
|
46
46
|
State(#[from] crate::state::StateError),
|
|
47
|
-
/// messaging 委派失败(send/
|
|
47
|
+
/// messaging 委派失败(send/stuck)。透传 step 11。
|
|
48
48
|
#[error("{0}")]
|
|
49
49
|
Messaging(crate::messaging::MessagingError),
|
|
50
|
-
/// I/O(cli-error
|
|
50
|
+
/// I/O(cli-error 落盘)。bug-084:写路径降级,不裸 panic。
|
|
51
51
|
#[error("io: {0}")]
|
|
52
52
|
Io(#[from] std::io::Error),
|
|
53
53
|
/// JSON 解析。
|
|
@@ -407,13 +407,12 @@ pub struct ApprovalsArgs {
|
|
|
407
407
|
pub team: Option<String>,
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
/// `inbox`(`parser.py:217`)
|
|
410
|
+
/// `inbox`(`parser.py:217`)。极简消息兜底查看器。
|
|
411
411
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
412
412
|
pub struct InboxArgs {
|
|
413
413
|
pub agent: String,
|
|
414
414
|
pub workspace: PathBuf,
|
|
415
415
|
pub limit: usize,
|
|
416
|
-
pub since: Option<String>,
|
|
417
416
|
pub json: bool,
|
|
418
417
|
/// Stage 4: explicit `--team` scope.
|
|
419
418
|
pub team: Option<String>,
|
|
@@ -682,18 +681,6 @@ pub struct ProfileArgs {
|
|
|
682
681
|
pub json: bool,
|
|
683
682
|
}
|
|
684
683
|
|
|
685
|
-
/// `collect`(`parser.py:292`)。
|
|
686
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
687
|
-
pub struct CollectArgs {
|
|
688
|
-
pub workspace: PathBuf,
|
|
689
|
-
pub result_file: Option<PathBuf>,
|
|
690
|
-
pub json: bool,
|
|
691
|
-
/// Stage 4: explicit `--team` scope. Destructive (consumes result
|
|
692
|
-
/// envelopes); CLI dispatch refuses bare invocation in a
|
|
693
|
-
/// multi-alive-team workspace.
|
|
694
|
-
pub team: Option<String>,
|
|
695
|
-
}
|
|
696
|
-
|
|
697
684
|
/// Read-only lookup of all stored result envelopes for one case.
|
|
698
685
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
699
686
|
pub struct ResultsArgs {
|
|
@@ -74,7 +74,19 @@ use super::types::{
|
|
|
74
74
|
/// returns: HealthReport。ok = 进程在跑 ∧ metadata 三元全等 ∧ 二进制身份一致 ∧ schema 兼容;service_available 刻意排除二进制身份,表示「这个 daemon 还能处理本队队列」;status 区分 Missing / InvalidPid / Running / Stale
|
|
75
75
|
/// ---
|
|
76
76
|
pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
|
|
77
|
-
|
|
77
|
+
coordinator_health_with_schema(workspace, message_store_schema_health(workspace))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Read-only coordinator health for diagnose. Unlike the lifecycle path, this
|
|
81
|
+
/// never opens MessageStore and therefore cannot initialize or rewrite team.db.
|
|
82
|
+
pub fn coordinator_health_read_only(workspace: &WorkspacePath) -> HealthReport {
|
|
83
|
+
coordinator_health_with_schema(workspace, message_store_schema_health_read_only(workspace))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn coordinator_health_with_schema(
|
|
87
|
+
workspace: &WorkspacePath,
|
|
88
|
+
schema: SchemaHealth,
|
|
89
|
+
) -> HealthReport {
|
|
78
90
|
let current_binary_identity = current_coordinator_binary_identity();
|
|
79
91
|
let pid_path = coordinator_pid_path(workspace);
|
|
80
92
|
let pid = read_pid_file(&pid_path);
|
|
@@ -1004,6 +1016,34 @@ pub(crate) fn message_store_schema_health(workspace: &WorkspacePath) -> SchemaHe
|
|
|
1004
1016
|
}
|
|
1005
1017
|
}
|
|
1006
1018
|
|
|
1019
|
+
fn message_store_schema_health_read_only(workspace: &WorkspacePath) -> SchemaHealth {
|
|
1020
|
+
let action = Some("run team-agent doctor --fix-schema --json".to_string());
|
|
1021
|
+
match crate::db::migration::schema_diagnosis_workspace_read_only(workspace.as_path()) {
|
|
1022
|
+
Ok(diagnosis) if diagnosis.ok && diagnosis.status == "ok" => SchemaHealth {
|
|
1023
|
+
ok: true,
|
|
1024
|
+
schema_version: crate::db::schema::SCHEMA_VERSION,
|
|
1025
|
+
error: None,
|
|
1026
|
+
action: None,
|
|
1027
|
+
},
|
|
1028
|
+
Ok(diagnosis) => SchemaHealth {
|
|
1029
|
+
ok: false,
|
|
1030
|
+
schema_version: crate::db::schema::SCHEMA_VERSION,
|
|
1031
|
+
error: Some(SchemaError::InitFailed {
|
|
1032
|
+
message: format!("team.db status: {}", diagnosis.status),
|
|
1033
|
+
}),
|
|
1034
|
+
action,
|
|
1035
|
+
},
|
|
1036
|
+
Err(error) => SchemaHealth {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
schema_version: crate::db::schema::SCHEMA_VERSION,
|
|
1039
|
+
error: Some(SchemaError::InitFailed {
|
|
1040
|
+
message: error.to_string(),
|
|
1041
|
+
}),
|
|
1042
|
+
action,
|
|
1043
|
+
},
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1007
1047
|
fn read_pid_file(path: &Path) -> Option<Pid> {
|
|
1008
1048
|
let text = std::fs::read_to_string(path).ok()?;
|
|
1009
1049
|
let raw = text.trim().parse::<u32>().ok()?;
|
|
@@ -82,7 +82,7 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
|
82
82
|
// startup_prompts -> runtime_prompts -> sync_health -> deliver_pending ->
|
|
83
83
|
// fire_scheduled -> detect_stuck -> record_unknown_idle -> evaluate_takeover ->
|
|
84
84
|
// detect_deadlocks -> detect_compaction -> detect_drift -> detect_api_errors ->
|
|
85
|
-
// ATOMIC_save (bug-084 wrap) ->
|
|
85
|
+
// ATOMIC_save (bug-084 wrap) -> prune_dedupe_log.
|
|
86
86
|
// The porter must push each step name into the injected recorder at its call site.
|
|
87
87
|
let recorder: OrderRecorder = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
88
88
|
let (coord, _calls) = coord_for_test(true, None, Some(std::sync::Arc::clone(&recorder)));
|
|
@@ -106,19 +106,18 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
|
|
|
106
106
|
"detect_drift",
|
|
107
107
|
"detect_api_errors",
|
|
108
108
|
"atomic_save",
|
|
109
|
-
"collect_results",
|
|
110
109
|
"prune_dedupe_log",
|
|
111
110
|
];
|
|
112
111
|
assert_eq!(
|
|
113
112
|
order, expected,
|
|
114
113
|
"tick side-effect ORDER must match the fixed sequence"
|
|
115
114
|
);
|
|
116
|
-
// ATOMIC save is the LAST mutation before read-only
|
|
115
|
+
// ATOMIC save is the LAST mutation before the read-only prune (bug-084 wrap point).
|
|
117
116
|
let save_idx = order.iter().position(|s| *s == "atomic_save").unwrap();
|
|
118
|
-
let
|
|
117
|
+
let prune_idx = order.iter().position(|s| *s == "prune_dedupe_log").unwrap();
|
|
119
118
|
assert!(
|
|
120
|
-
save_idx <
|
|
121
|
-
"save precedes
|
|
119
|
+
save_idx < prune_idx,
|
|
120
|
+
"save precedes prune (bug-084 wrap is the last mutation)"
|
|
122
121
|
);
|
|
123
122
|
}
|
|
124
123
|
|
|
@@ -104,7 +104,7 @@ pub struct TickReport {
|
|
|
104
104
|
pub session_drift: Vec<SessionDriftResult>,
|
|
105
105
|
/// `detect_leader_api_errors`(`lifecycle.py:344`)——cross-dep step 11。
|
|
106
106
|
pub api_errors: Vec<LeaderApiError>,
|
|
107
|
-
///
|
|
107
|
+
/// Reserved result projection slot; report_result now finalizes directly.
|
|
108
108
|
pub results: Vec<CollectedResult>,
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -528,12 +528,6 @@ impl Coordinator {
|
|
|
528
528
|
collections,
|
|
529
529
|
));
|
|
530
530
|
}
|
|
531
|
-
self.record_step(TickStepGroup::Delivery, "collect_results");
|
|
532
|
-
collections.results =
|
|
533
|
-
collect_results(crate::messaging::collect_results_and_notify_watchers(
|
|
534
|
-
self.workspace.as_path(),
|
|
535
|
-
&event_log,
|
|
536
|
-
)?);
|
|
537
531
|
self.record_step(TickStepGroup::Persist, "prune_dedupe_log");
|
|
538
532
|
Ok(base_tick_report(true, false, None, Some(true), collections))
|
|
539
533
|
}
|
|
@@ -1554,15 +1548,6 @@ fn empty_tick_report(
|
|
|
1554
1548
|
base_tick_report(ok, stop, reason, persisted, TickCollections::default())
|
|
1555
1549
|
}
|
|
1556
1550
|
|
|
1557
|
-
fn collect_results(value: Value) -> Vec<CollectedResult> {
|
|
1558
|
-
let Some(result_id) = value.get("result_id").and_then(Value::as_str) else {
|
|
1559
|
-
return Vec::new();
|
|
1560
|
-
};
|
|
1561
|
-
vec![CollectedResult {
|
|
1562
|
-
result_id: result_id.to_string(),
|
|
1563
|
-
}]
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
1551
|
struct ProviderTurnClassifier;
|
|
1567
1552
|
|
|
1568
1553
|
impl TurnStateClassifier for ProviderTurnClassifier {
|
|
@@ -543,7 +543,7 @@ pub struct LeaderApiError {
|
|
|
543
543
|
pub message: String,
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
-
///
|
|
546
|
+
/// Result projection reported by a coordinator tick. Report results now finalize inline.
|
|
547
547
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
548
548
|
pub struct CollectedResult {
|
|
549
549
|
pub result_id: String,
|
|
@@ -727,9 +727,11 @@ impl MessageStore {
|
|
|
727
727
|
Ok(changed == 1)
|
|
728
728
|
}
|
|
729
729
|
|
|
730
|
-
/// Read inbox rows for an agent.
|
|
731
|
-
///
|
|
732
|
-
///
|
|
730
|
+
/// Read ordinary inbox rows for an agent. Leader stage-result notifications are
|
|
731
|
+
/// delivered through the leader receiver, not exposed as mailbox fallback rows.
|
|
732
|
+
/// This projection intentionally has no owner-team filter when the caller does
|
|
733
|
+
/// not provide one: legacy/CLI inbox must surface NULL-owner messages stored for
|
|
734
|
+
/// the agent.
|
|
733
735
|
pub fn inbox(
|
|
734
736
|
&self,
|
|
735
737
|
agent_id: &str,
|
|
@@ -744,7 +746,9 @@ impl MessageStore {
|
|
|
744
746
|
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
745
747
|
acknowledged_at, error, delivery_attempts
|
|
746
748
|
from messages
|
|
747
|
-
where (sender = ?1 or recipient = ?1)
|
|
749
|
+
where (sender = ?1 or recipient = ?1)
|
|
750
|
+
and owner_team_id = ?3
|
|
751
|
+
and coalesce(json_extract(presentation, '$.class'), 'message') != 'stage_result'
|
|
748
752
|
order by created_at desc
|
|
749
753
|
limit ?2"
|
|
750
754
|
}
|
|
@@ -753,7 +757,8 @@ impl MessageStore {
|
|
|
753
757
|
status, content, artifact_refs, created_at, updated_at, delivered_at,
|
|
754
758
|
acknowledged_at, error, delivery_attempts
|
|
755
759
|
from messages
|
|
756
|
-
where sender = ?1 or recipient = ?1
|
|
760
|
+
where (sender = ?1 or recipient = ?1)
|
|
761
|
+
and coalesce(json_extract(presentation, '$.class'), 'message') != 'stage_result'
|
|
757
762
|
order by created_at desc
|
|
758
763
|
limit ?2"
|
|
759
764
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
use std::path::{Path, PathBuf};
|
|
10
10
|
|
|
11
|
-
use rusqlite::Connection;
|
|
11
|
+
use rusqlite::{Connection, OpenFlags};
|
|
12
12
|
|
|
13
13
|
use crate::db::schema::{ensure_schema_indexes, table_layout, SCHEMA_VERSION};
|
|
14
14
|
use crate::db::DbError;
|
|
@@ -423,22 +423,33 @@ pub fn ensure_table_layout(
|
|
|
423
423
|
/// `schema_migration.py:schema_diagnosis`:只读判定(不变更 DB)。
|
|
424
424
|
fn schema_diagnosis(db_path: &Path, schema_version: i64) -> Result<Diagnosis, DbError> {
|
|
425
425
|
if !db_path.exists() {
|
|
426
|
-
|
|
427
|
-
// first-use state — ok:true is layered with the explicit status axis and the
|
|
428
|
-
// recommended_action guidance (Python schema_migration.py:180-190 verbatim),
|
|
429
|
-
// never a silent fake-green.
|
|
430
|
-
return Ok(Diagnosis {
|
|
431
|
-
ok: true,
|
|
432
|
-
status: "missing".to_string(),
|
|
433
|
-
user_version: 0,
|
|
434
|
-
layout_diffs: vec![],
|
|
435
|
-
recommended_action:
|
|
436
|
-
"No team.db exists yet; initialize_schema will create it on first use.".to_string(),
|
|
437
|
-
});
|
|
426
|
+
return Ok(missing_schema_diagnosis());
|
|
438
427
|
}
|
|
439
428
|
let conn = Connection::open(db_path)?;
|
|
440
|
-
|
|
441
|
-
|
|
429
|
+
schema_diagnosis_from_connection(&conn, schema_version)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fn missing_schema_diagnosis() -> Diagnosis {
|
|
433
|
+
// T3-3 cr verdict (A parity lock, 2026-06-10): a missing db is the LEGAL
|
|
434
|
+
// first-use state — ok:true is layered with the explicit status axis and the
|
|
435
|
+
// recommended_action guidance (Python schema_migration.py:180-190 verbatim),
|
|
436
|
+
// never a silent fake-green.
|
|
437
|
+
Diagnosis {
|
|
438
|
+
ok: true,
|
|
439
|
+
status: "missing".to_string(),
|
|
440
|
+
user_version: 0,
|
|
441
|
+
layout_diffs: vec![],
|
|
442
|
+
recommended_action:
|
|
443
|
+
"No team.db exists yet; initialize_schema will create it on first use.".to_string(),
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
fn schema_diagnosis_from_connection(
|
|
448
|
+
conn: &Connection,
|
|
449
|
+
schema_version: i64,
|
|
450
|
+
) -> Result<Diagnosis, DbError> {
|
|
451
|
+
let uv = pragma_user_version(conn)?;
|
|
452
|
+
let diffs = layout_diffs(conn)?;
|
|
442
453
|
let diff_tables: Vec<String> = diffs.iter().map(|d| d.table.to_string()).collect();
|
|
443
454
|
let ok = diffs.is_empty() && uv == schema_version;
|
|
444
455
|
Ok(Diagnosis {
|
|
@@ -466,6 +477,16 @@ pub fn schema_diagnosis_workspace(workspace: &Path) -> Result<Diagnosis, DbError
|
|
|
466
477
|
)
|
|
467
478
|
}
|
|
468
479
|
|
|
480
|
+
/// 对 workspace 的 team.db 执行严格只读 diagnosis,不创建或初始化缺失/空文件。
|
|
481
|
+
pub fn schema_diagnosis_workspace_read_only(workspace: &Path) -> Result<Diagnosis, DbError> {
|
|
482
|
+
let db_path = workspace.join(".team").join("runtime").join("team.db");
|
|
483
|
+
if !db_path.exists() {
|
|
484
|
+
return Ok(missing_schema_diagnosis());
|
|
485
|
+
}
|
|
486
|
+
let conn = Connection::open_with_flags(&db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
|
|
487
|
+
schema_diagnosis_from_connection(&conn, SCHEMA_VERSION)
|
|
488
|
+
}
|
|
489
|
+
|
|
469
490
|
/// `schema_migration.py:_workspace_from_db_path`:`<ws>/.team/runtime/team.db` → `<ws>`。
|
|
470
491
|
fn workspace_from_db_path(db_path: &Path) -> Option<PathBuf> {
|
|
471
492
|
let parts: Vec<_> = db_path.iter().collect();
|
|
@@ -220,8 +220,8 @@ mod tests {
|
|
|
220
220
|
let (task_id, agent_id, status, env) = stored_result(ws);
|
|
221
221
|
assert_eq!(
|
|
222
222
|
(task_id.as_str(), agent_id.as_str(), status.as_str()),
|
|
223
|
-
("t1", "w1", "
|
|
224
|
-
"stored result row must be task_id=t1 agent_id=w1 status=
|
|
223
|
+
("t1", "w1", "collected"),
|
|
224
|
+
"stored result row must be task_id=t1 agent_id=w1 status=collected"
|
|
225
225
|
);
|
|
226
226
|
assert_eq!(
|
|
227
227
|
env["schema_version"],
|
|
@@ -291,8 +291,8 @@ mod tests {
|
|
|
291
291
|
let (task_id, agent_id, status, envelope) = stored_result(&ws);
|
|
292
292
|
assert_eq!(
|
|
293
293
|
(task_id.as_str(), agent_id.as_str(), status.as_str()),
|
|
294
|
-
("m1", "w1", "
|
|
295
|
-
"a direct-message result must use its message id for message-scope
|
|
294
|
+
("m1", "w1", "collected"),
|
|
295
|
+
"a direct-message result must use its message id for message-scope auto-finalization"
|
|
296
296
|
);
|
|
297
297
|
assert_ne!(
|
|
298
298
|
envelope["task_id"],
|
|
@@ -255,8 +255,8 @@ fn t3_unknown_business_status_is_preserved_without_normalized_event() {
|
|
|
255
255
|
let envelope: serde_json::Value =
|
|
256
256
|
serde_json::from_str(&row.envelope).expect("T3-1 durable envelope JSON");
|
|
257
257
|
assert_eq!(
|
|
258
|
-
row.status,
|
|
259
|
-
"T3-1 ingress:
|
|
258
|
+
row.status, "collected",
|
|
259
|
+
"T3-1 ingress: report_result auto-finalization must mark the durable row collected"
|
|
260
260
|
);
|
|
261
261
|
assert_eq!(
|
|
262
262
|
envelope["status"],
|
|
@@ -410,16 +410,6 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
410
410
|
"status should normalize teamdir to project-root runtime state: {status:?}"
|
|
411
411
|
);
|
|
412
412
|
|
|
413
|
-
let collect = crate::cli::cmd_collect(&crate::cli::CollectArgs {
|
|
414
|
-
result_file: None,
|
|
415
|
-
workspace: team.clone(),
|
|
416
|
-
json: true,
|
|
417
|
-
team: None,
|
|
418
|
-
});
|
|
419
|
-
assert!(
|
|
420
|
-
collect.is_ok(),
|
|
421
|
-
"collect should normalize teamdir to the same project-root state/spec: {collect:?}"
|
|
422
|
-
);
|
|
423
413
|
}
|
|
424
414
|
|
|
425
415
|
#[test]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
use super::launch_spawn::seed_healthy_coordinator;
|
|
2
2
|
use super::*;
|
|
3
3
|
use crate::cli::{
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
cmd_diagnose, cmd_send, cmd_status, lifecycle_port, CmdOutput, CmdResult, DiagnoseArgs,
|
|
5
|
+
SendArgs, StatusArgs,
|
|
6
6
|
};
|
|
7
7
|
use crate::transport::test_support::OfflineTransport;
|
|
8
8
|
use crate::transport::WindowName;
|
|
@@ -275,12 +275,6 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
275
275
|
to_name: None,
|
|
276
276
|
to_leader: None,
|
|
277
277
|
});
|
|
278
|
-
let collect = cmd_collect(&CollectArgs {
|
|
279
|
-
workspace: workspace.clone(),
|
|
280
|
-
result_file: None,
|
|
281
|
-
json: true,
|
|
282
|
-
team: Some(spec.team_key.to_string()),
|
|
283
|
-
});
|
|
284
278
|
let lifecycle_transport = codex_ready_transport()
|
|
285
279
|
.with_session_present(true)
|
|
286
280
|
.with_windows(vec![WindowName::new("w1"), WindowName::new("w2")]);
|
|
@@ -303,7 +297,6 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
303
297
|
{ "step": "status-json", "result": cmd_value(status_compact) },
|
|
304
298
|
{ "step": "status-detail-json", "result": cmd_value(status_detail) },
|
|
305
299
|
{ "step": "send", "result": cmd_value(send) },
|
|
306
|
-
{ "step": "collect", "result": cmd_value(collect) },
|
|
307
300
|
{ "step": "phase-lifecycle-op", "output": lifecycle },
|
|
308
301
|
{
|
|
309
302
|
"step": "shutdown-keep-logs",
|
|
@@ -19,9 +19,7 @@ use std::sync::Mutex;
|
|
|
19
19
|
use rusqlite::params;
|
|
20
20
|
use serde_json::{json, Value};
|
|
21
21
|
use serial_test::serial;
|
|
22
|
-
use team_agent::cli::{
|
|
23
|
-
cmd_collect_for_team, cmd_status_for_team, CmdOutput, CollectArgs, StatusArgs,
|
|
24
|
-
};
|
|
22
|
+
use team_agent::cli::{cmd_status_for_team, CmdOutput, StatusArgs};
|
|
25
23
|
use team_agent::db::schema::{initialize_schema, open_db, table_layout, SCHEMA_VERSION};
|
|
26
24
|
use team_agent::event_log::EventLog;
|
|
27
25
|
use team_agent::lifecycle::restart::classify_restart_plan;
|
|
@@ -239,29 +237,6 @@ fn upgrade_status_collect_scope_by_selected_team_key() {
|
|
|
239
237
|
"status --team upgrade-key must not expose sibling rows; status={status}"
|
|
240
238
|
);
|
|
241
239
|
|
|
242
|
-
let collect = json_output(cmd_collect_for_team(
|
|
243
|
-
&CollectArgs {
|
|
244
|
-
workspace: fixture.workspace.clone(),
|
|
245
|
-
result_file: None,
|
|
246
|
-
json: true,
|
|
247
|
-
team: None,
|
|
248
|
-
},
|
|
249
|
-
Some(TEAM_KEY),
|
|
250
|
-
));
|
|
251
|
-
let empty = Vec::new();
|
|
252
|
-
let collected_ids = collect
|
|
253
|
-
.get("collected_results")
|
|
254
|
-
.and_then(Value::as_array)
|
|
255
|
-
.unwrap_or(&empty)
|
|
256
|
-
.iter()
|
|
257
|
-
.filter_map(|item| item.get("result_id").and_then(Value::as_str))
|
|
258
|
-
.collect::<Vec<_>>();
|
|
259
|
-
assert!(
|
|
260
|
-
collected_ids.contains(&"res-existing-upgrade")
|
|
261
|
-
&& collected_ids.contains(&"res-upgrade")
|
|
262
|
-
&& !collected_ids.contains(&"res-sibling"),
|
|
263
|
-
"collect --team upgrade-key must collect only selected runtime-key rows; collect={collect}"
|
|
264
|
-
);
|
|
265
240
|
}
|
|
266
241
|
|
|
267
242
|
#[test]
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
//! `team-agent` 二进制入口。
|
|
2
2
|
|
|
3
3
|
fn main() -> anyhow::Result<()> {
|
|
4
|
-
let mut args = std::env::
|
|
4
|
+
let mut args = std::env::args_os()
|
|
5
|
+
.skip(1)
|
|
6
|
+
.map(|arg| arg.to_string_lossy().into_owned());
|
|
5
7
|
let command = args.next();
|
|
6
8
|
if matches!(command.as_deref(), Some("fake-worker")) {
|
|
7
9
|
let mut workspace = None;
|
|
@@ -35,7 +37,10 @@ fn main() -> anyhow::Result<()> {
|
|
|
35
37
|
team_agent::mcp_server::main(&workspace, &[])?;
|
|
36
38
|
return Ok(());
|
|
37
39
|
}
|
|
38
|
-
let argv = std::env::
|
|
40
|
+
let argv = std::env::args_os()
|
|
41
|
+
.skip(1)
|
|
42
|
+
.map(|arg| arg.to_string_lossy().into_owned())
|
|
43
|
+
.collect::<Vec<_>>();
|
|
39
44
|
let cwd = std::env::current_dir()?;
|
|
40
45
|
std::process::exit(team_agent::cli::run(&argv, &cwd).code());
|
|
41
46
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
//! `mcp.identity_inference_failed` / `mcp.task_inference_failed` audit events.
|
|
23
23
|
//! - [`load_runtime_state`] / [`save_runtime_state`] (step 5 persist) — `assign_task`
|
|
24
24
|
//! / `update_state` read-modify-write; `get_visible_peers` reads team scope.
|
|
25
|
-
//! - [`messaging`] (step 11) — `send_message` / `report_result` /
|
|
25
|
+
//! - [`messaging`] (step 11) — `send_message` / auto-finalizing `report_result` /
|
|
26
26
|
//! `stuck_list` / `stuck_cancel` delegated by the tool handlers.
|
|
27
27
|
//! - [`crate::model::enums`] (step 2) — [`ResultStatus`] / [`ChangeKind`] /
|
|
28
28
|
//! [`TestStatus`] / [`RiskSeverity`] are the normalized result-envelope value
|
|
@@ -102,8 +102,8 @@ pub use persist::{
|
|
|
102
102
|
LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
|
|
103
103
|
};
|
|
104
104
|
pub use results::{
|
|
105
|
-
collect, collect_for_team,
|
|
106
|
-
|
|
105
|
+
collect, collect_for_team, report_result, report_result_for_owner_team,
|
|
106
|
+
report_result_for_owner_team_with_primary_error,
|
|
107
107
|
};
|
|
108
108
|
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel, stuck_list};
|
|
109
109
|
pub use selftest::{run_comms_selftest, CommsSelftestDriver};
|