@team-agent/installer 0.3.4 → 0.3.6
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 +8 -0
- package/crates/team-agent/src/cli/diagnose.rs +51 -10
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +217 -80
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/status_port.rs +135 -7
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
- package/crates/team-agent/src/cli/types.rs +5 -1
- package/crates/team-agent/src/coordinator/backoff.rs +57 -9
- package/crates/team-agent/src/coordinator/health.rs +65 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tick.rs +195 -43
- package/crates/team-agent/src/leader/helpers.rs +2 -0
- package/crates/team-agent/src/leader/rediscover.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +9 -1
- package/crates/team-agent/src/leader/takeover.rs +18 -1
- package/crates/team-agent/src/lifecycle/launch.rs +434 -29
- package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +19 -2
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +44 -9
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
- package/crates/team-agent/src/mcp_server/tools.rs +65 -9
- package/crates/team-agent/src/mcp_server/wire.rs +2 -1
- package/crates/team-agent/src/message_store.rs +80 -0
- package/crates/team-agent/src/messaging/results.rs +76 -5
- package/crates/team-agent/src/messaging/send.rs +3 -1
- package/crates/team-agent/src/messaging/types.rs +15 -1
- package/crates/team-agent/src/messaging/watchers.rs +68 -30
- package/crates/team-agent/src/model/enums.rs +7 -1
- package/crates/team-agent/src/model/permissions.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +3 -1
- package/crates/team-agent/src/provider/adapter.rs +472 -7
- package/crates/team-agent/src/provider/classify.rs +6 -2
- package/crates/team-agent/src/provider/faults.rs +3 -2
- package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
- package/crates/team-agent/src/provider/types.rs +11 -0
- package/crates/team-agent/src/session_capture.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +95 -19
- package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
- package/crates/team-agent/src/tmux_backend.rs +80 -6
- package/crates/team-agent/src/transport.rs +32 -0
- package/npm/install.mjs +21 -0
- package/package.json +4 -4
|
@@ -320,6 +320,54 @@ impl MessageStore {
|
|
|
320
320
|
Ok(rows.into_iter().rev().collect())
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/// `latest_results` (`core.py:458-471`): newest non-invalid result rows, oldest
|
|
324
|
+
/// first (Python fetches `created_at desc limit ?` then reverses).
|
|
325
|
+
pub fn latest_results(
|
|
326
|
+
&self,
|
|
327
|
+
limit: usize,
|
|
328
|
+
owner_team_id: Option<&str>,
|
|
329
|
+
) -> Result<Vec<serde_json::Value>, MessageStoreError> {
|
|
330
|
+
let conn = crate::db::schema::open_db(&self.path)?;
|
|
331
|
+
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
|
|
332
|
+
let sql = match owner_team_id {
|
|
333
|
+
Some(_) => {
|
|
334
|
+
"select owner_team_id, result_id, task_id, agent_id, envelope, status, created_at
|
|
335
|
+
from results
|
|
336
|
+
where status != 'invalid' and owner_team_id = ?2
|
|
337
|
+
order by created_at desc
|
|
338
|
+
limit ?1"
|
|
339
|
+
}
|
|
340
|
+
None => {
|
|
341
|
+
"select owner_team_id, result_id, task_id, agent_id, envelope, status, created_at
|
|
342
|
+
from results
|
|
343
|
+
where status != 'invalid'
|
|
344
|
+
order by created_at desc
|
|
345
|
+
limit ?1"
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
let mut stmt = conn.prepare(sql)?;
|
|
349
|
+
let map_row = |row: &rusqlite::Row<'_>| {
|
|
350
|
+
Ok(serde_json::json!({
|
|
351
|
+
"owner_team_id": row.get::<_, Option<String>>(0)?,
|
|
352
|
+
"result_id": row.get::<_, String>(1)?,
|
|
353
|
+
"task_id": row.get::<_, Option<String>>(2)?,
|
|
354
|
+
"agent_id": row.get::<_, Option<String>>(3)?,
|
|
355
|
+
"envelope": row.get::<_, Option<String>>(4)?,
|
|
356
|
+
"status": row.get::<_, Option<String>>(5)?,
|
|
357
|
+
"created_at": row.get::<_, Option<String>>(6)?,
|
|
358
|
+
}))
|
|
359
|
+
};
|
|
360
|
+
let rows = match owner_team_id {
|
|
361
|
+
Some(team) => stmt
|
|
362
|
+
.query_map(params![limit, team], map_row)?
|
|
363
|
+
.collect::<Result<Vec<_>, _>>()?,
|
|
364
|
+
None => stmt
|
|
365
|
+
.query_map(params![limit], map_row)?
|
|
366
|
+
.collect::<Result<Vec<_>, _>>()?,
|
|
367
|
+
};
|
|
368
|
+
Ok(rows.into_iter().rev().collect())
|
|
369
|
+
}
|
|
370
|
+
|
|
323
371
|
/// Allow direct peer messages in both directions. Golden stores `(a,b)` and
|
|
324
372
|
/// `(b,a)` so either sender/recipient lookup can use a single ordered key.
|
|
325
373
|
pub fn allow_peer(&self, a: &str, b: &str) -> Result<(), MessageStoreError> {
|
|
@@ -410,6 +458,38 @@ fn row_to_message_value(row: &rusqlite::Row<'_>) -> rusqlite::Result<serde_json:
|
|
|
410
458
|
}))
|
|
411
459
|
}
|
|
412
460
|
|
|
461
|
+
/// `result_summary_from_row`(`status/queries.py:92-106`):解析 result 行的 envelope,
|
|
462
|
+
/// 产出 status/watch 共用的 result 摘要;envelope 坏/非对象 → `None`。
|
|
463
|
+
pub fn result_summary_from_row(row: &serde_json::Value) -> Option<serde_json::Value> {
|
|
464
|
+
let envelope = match row.get("envelope") {
|
|
465
|
+
Some(serde_json::Value::String(text)) => {
|
|
466
|
+
serde_json::from_str::<serde_json::Value>(text).ok()?
|
|
467
|
+
}
|
|
468
|
+
Some(value @ serde_json::Value::Object(_)) => value.clone(),
|
|
469
|
+
_ => return None,
|
|
470
|
+
};
|
|
471
|
+
if !envelope.is_object() {
|
|
472
|
+
return None;
|
|
473
|
+
}
|
|
474
|
+
// Python `envelope.get(k) or row.get(k)` — falsy (null/empty) falls through to the row.
|
|
475
|
+
let pick = |key: &str| {
|
|
476
|
+
envelope
|
|
477
|
+
.get(key)
|
|
478
|
+
.filter(|v| !v.is_null() && v.as_str() != Some(""))
|
|
479
|
+
.or_else(|| row.get(key))
|
|
480
|
+
.cloned()
|
|
481
|
+
.unwrap_or(serde_json::Value::Null)
|
|
482
|
+
};
|
|
483
|
+
Some(serde_json::json!({
|
|
484
|
+
"result_id": row.get("result_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
485
|
+
"task_id": pick("task_id"),
|
|
486
|
+
"agent_id": pick("agent_id"),
|
|
487
|
+
"status": pick("status"),
|
|
488
|
+
"summary": envelope.get("summary").cloned().unwrap_or(serde_json::Value::Null),
|
|
489
|
+
"created_at": row.get("created_at").cloned().unwrap_or(serde_json::Value::Null),
|
|
490
|
+
}))
|
|
491
|
+
}
|
|
492
|
+
|
|
413
493
|
fn now_ts() -> String {
|
|
414
494
|
chrono::Utc::now().to_rfc3339()
|
|
415
495
|
}
|
|
@@ -40,7 +40,6 @@ fn collect_scoped(
|
|
|
40
40
|
ensure_coordinator: bool,
|
|
41
41
|
owner_team_id: Option<&str>,
|
|
42
42
|
) -> Result<serde_json::Value, MessagingError> {
|
|
43
|
-
let _ = ensure_coordinator;
|
|
44
43
|
let paths = collect_paths(workspace)?;
|
|
45
44
|
let log = EventLog::new(&paths.run_workspace);
|
|
46
45
|
let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
|
|
@@ -195,6 +194,13 @@ fn collect_scoped(
|
|
|
195
194
|
}
|
|
196
195
|
}
|
|
197
196
|
let counts = result_counts(&conn, owner_team_id)?;
|
|
197
|
+
// results.py:157 — ensure_coordinator=true runs the REAL ensure step; the
|
|
198
|
+
// `{ok:false,status:"not_required"}` literal is ONLY the ensure=false branch.
|
|
199
|
+
let coordinator = if ensure_coordinator {
|
|
200
|
+
ensure_coordinator_after_collect(&paths.run_workspace, &state, &log)
|
|
201
|
+
} else {
|
|
202
|
+
serde_json::json!({"ok": false, "status": "not_required"})
|
|
203
|
+
};
|
|
198
204
|
Ok(serde_json::json!({
|
|
199
205
|
"ok": fatal_invalid_results == 0,
|
|
200
206
|
"collected": collected,
|
|
@@ -203,13 +209,78 @@ fn collect_scoped(
|
|
|
203
209
|
"invalid_results": invalid_results,
|
|
204
210
|
"results": counts,
|
|
205
211
|
"state_file": spec_workspace.join("team_state.md").to_string_lossy().to_string(),
|
|
206
|
-
"coordinator":
|
|
207
|
-
"ok": false,
|
|
208
|
-
"status": "not_required",
|
|
209
|
-
},
|
|
212
|
+
"coordinator": coordinator,
|
|
210
213
|
}))
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
/// `_ensure_coordinator_after_collect`(`results.py:176-184`)。
|
|
217
|
+
fn ensure_coordinator_after_collect(
|
|
218
|
+
workspace: &Path,
|
|
219
|
+
state: &serde_json::Value,
|
|
220
|
+
log: &EventLog,
|
|
221
|
+
) -> serde_json::Value {
|
|
222
|
+
if !coordinator_should_run(state) {
|
|
223
|
+
return serde_json::json!({"ok": false, "status": "not_required"});
|
|
224
|
+
}
|
|
225
|
+
let workspace_path = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
226
|
+
let coordinator = match crate::coordinator::start_coordinator(&workspace_path) {
|
|
227
|
+
Ok(report) => start_report_value(&report),
|
|
228
|
+
Err(e) => serde_json::json!({"ok": false, "status": "start_failed", "error": e.to_string()}),
|
|
229
|
+
};
|
|
230
|
+
let _ = log.write(
|
|
231
|
+
"collect.coordinator_checked",
|
|
232
|
+
serde_json::json!({"coordinator": coordinator.clone()}),
|
|
233
|
+
);
|
|
234
|
+
coordinator
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// `_coordinator_should_run`(`results.py:187-188`)。
|
|
238
|
+
fn coordinator_should_run(state: &serde_json::Value) -> bool {
|
|
239
|
+
let has_session = state
|
|
240
|
+
.get("session_name")
|
|
241
|
+
.and_then(serde_json::Value::as_str)
|
|
242
|
+
.is_some_and(|s| !s.is_empty());
|
|
243
|
+
has_session || leader_receiver_is_direct(state.get("leader_receiver"))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// `_leader_receiver_is_direct`(`messaging/leader.py:449-450`)。
|
|
247
|
+
fn leader_receiver_is_direct(receiver: Option<&serde_json::Value>) -> bool {
|
|
248
|
+
receiver.is_some_and(|receiver| {
|
|
249
|
+
receiver.get("mode").and_then(serde_json::Value::as_str) == Some("direct_tmux")
|
|
250
|
+
&& receiver
|
|
251
|
+
.get("pane_id")
|
|
252
|
+
.and_then(serde_json::Value::as_str)
|
|
253
|
+
.is_some_and(|pane| !pane.is_empty())
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// `start_coordinator` dict 形(`lifecycle.py:54/86/121` 的 JSON 面)。
|
|
258
|
+
fn start_report_value(report: &crate::coordinator::StartReport) -> serde_json::Value {
|
|
259
|
+
let status = match report.status {
|
|
260
|
+
crate::coordinator::StartOutcome::AlreadyRunning => "already_running",
|
|
261
|
+
crate::coordinator::StartOutcome::RestartIncompatibleStopFailed => {
|
|
262
|
+
"restart_incompatible_stop_failed"
|
|
263
|
+
}
|
|
264
|
+
crate::coordinator::StartOutcome::SchemaIncompatible => "schema_incompatible",
|
|
265
|
+
crate::coordinator::StartOutcome::Started => "started",
|
|
266
|
+
};
|
|
267
|
+
let mut value = serde_json::json!({
|
|
268
|
+
"ok": report.ok,
|
|
269
|
+
"pid": report.pid.map(|p| p.get()),
|
|
270
|
+
"status": status,
|
|
271
|
+
});
|
|
272
|
+
if let Some(log) = &report.log {
|
|
273
|
+
value["log"] = serde_json::json!(log.to_string_lossy().to_string());
|
|
274
|
+
}
|
|
275
|
+
if let Some(error) = &report.schema_error {
|
|
276
|
+
value["schema_error"] = serde_json::json!(format!("{error:?}"));
|
|
277
|
+
}
|
|
278
|
+
if let Some(action) = &report.action {
|
|
279
|
+
value["action"] = serde_json::json!(action);
|
|
280
|
+
}
|
|
281
|
+
value
|
|
282
|
+
}
|
|
283
|
+
|
|
213
284
|
fn resolve_owner_team_for_read(
|
|
214
285
|
workspace: &Path,
|
|
215
286
|
requested: &str,
|
|
@@ -121,6 +121,8 @@ pub fn send_message(
|
|
|
121
121
|
return fanout_send(workspace, &state, &recipients, content, opts, &event_log, "*");
|
|
122
122
|
}
|
|
123
123
|
MessageTarget::Fanout(recipients) if recipients.is_empty() => {
|
|
124
|
+
// swallow batch 3 ②: a failed send carries its reason (Python send error
|
|
125
|
+
// reason style) — "failed with no reason" is an unexplained exit.
|
|
124
126
|
return Ok(DeliveryOutcome {
|
|
125
127
|
ok: false,
|
|
126
128
|
status: DeliveryStatus::Failed,
|
|
@@ -128,7 +130,7 @@ pub fn send_message(
|
|
|
128
130
|
message_id: None,
|
|
129
131
|
verification: None,
|
|
130
132
|
stage: None,
|
|
131
|
-
reason:
|
|
133
|
+
reason: Some(crate::messaging::DeliveryRefusal::EmptyTargetList),
|
|
132
134
|
channel: None,
|
|
133
135
|
});
|
|
134
136
|
}
|
|
@@ -39,7 +39,7 @@ pub enum DeliveryStatus {
|
|
|
39
39
|
|
|
40
40
|
/// 投递/发件拒绝原因 (card §42)。Python 散裸字符串靠 `==` 比对易拼错;Rust 穷尽 enum。
|
|
41
41
|
/// 值散落 `send.py`/`delivery.py`/`leader.py`/`session_drift.py`/`owner_gate`。
|
|
42
|
-
#[derive(
|
|
42
|
+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
43
43
|
#[serde(rename_all = "snake_case")]
|
|
44
44
|
pub enum DeliveryRefusal {
|
|
45
45
|
TargetNotInTeam,
|
|
@@ -62,6 +62,20 @@ pub enum DeliveryRefusal {
|
|
|
62
62
|
/// is content, not a target. Distinct from `TargetNotInTeam` (where caller
|
|
63
63
|
/// did pick a target but it's unknown).
|
|
64
64
|
RoutingAmbiguous,
|
|
65
|
+
/// swallow batch 3: an explicit empty `--to` target list (a failed send always
|
|
66
|
+
/// carries its reason; an unexplained `failed` is a swallowed error).
|
|
67
|
+
EmptyTargetList,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Debug 输出 = wire snake_case 字面(单一真相源 = serde rename),与事件/JSON 面一致,
|
|
71
|
+
/// 测试与日志里的 `{:?}` 不再出现与 wire 不同的 CamelCase 第二形态。
|
|
72
|
+
impl std::fmt::Debug for DeliveryRefusal {
|
|
73
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
74
|
+
match serde_json::to_value(self) {
|
|
75
|
+
Ok(serde_json::Value::String(wire)) => write!(f, "{wire}"),
|
|
76
|
+
_ => write!(f, "delivery_refusal"),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/// 注入失败阶段 (审计用;`delivery.py:309` injection.stage)。card §43。
|
|
@@ -298,48 +298,86 @@ pub fn retry_result_deliveries(
|
|
|
298
298
|
) -> Result<Vec<WatcherNotice>, MessagingError> {
|
|
299
299
|
let store = MessageStore::open(workspace)?;
|
|
300
300
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
301
|
+
// result_delivery.py:19-35 — retries route through notify_result_watchers (the REAL
|
|
302
|
+
// delivery path with dedupe/attempt bounds); a watcher is never flipped to
|
|
303
|
+
// `notified` without a delivery. Missing result rows are skipped (still retryable).
|
|
301
304
|
let mut stmt = conn.prepare(
|
|
302
|
-
"select watcher_id,
|
|
303
|
-
|
|
305
|
+
"select watcher_id, owner_team_id, task_id, agent_id, leader_id, status, created_at,
|
|
306
|
+
result_id, notified_message_id
|
|
307
|
+
from result_watchers
|
|
308
|
+
where status in ('pending', 'notify_failed')
|
|
304
309
|
order by created_at, watcher_id",
|
|
305
310
|
)?;
|
|
306
|
-
let
|
|
311
|
+
let watchers = stmt
|
|
312
|
+
.query_map([], |row| {
|
|
313
|
+
Ok(serde_json::json!({
|
|
314
|
+
"watcher_id": row.get::<_, String>(0)?,
|
|
315
|
+
"owner_team_id": row.get::<_, Option<String>>(1)?,
|
|
316
|
+
"task_id": row.get::<_, Option<String>>(2)?,
|
|
317
|
+
"agent_id": row.get::<_, Option<String>>(3)?,
|
|
318
|
+
"leader_id": row.get::<_, Option<String>>(4)?,
|
|
319
|
+
"status": row.get::<_, Option<String>>(5)?,
|
|
320
|
+
"created_at": row.get::<_, Option<String>>(6)?,
|
|
321
|
+
"result_id": row.get::<_, Option<String>>(7)?,
|
|
322
|
+
"notified_message_id": row.get::<_, Option<String>>(8)?,
|
|
323
|
+
}))
|
|
324
|
+
})?
|
|
325
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
326
|
+
drop(stmt);
|
|
307
327
|
let mut notices = Vec::new();
|
|
308
|
-
for
|
|
309
|
-
|
|
310
|
-
|
|
328
|
+
for watcher in watchers {
|
|
329
|
+
if watcher.get("status").and_then(|v| v.as_str()) != Some("notify_failed") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
let Some(result_id) = watcher
|
|
333
|
+
.get("result_id")
|
|
334
|
+
.and_then(|v| v.as_str())
|
|
335
|
+
.filter(|id| !id.is_empty())
|
|
336
|
+
.map(ToString::to_string)
|
|
337
|
+
else {
|
|
338
|
+
continue;
|
|
339
|
+
};
|
|
340
|
+
let row: Option<(String, Option<String>)> = conn
|
|
311
341
|
.query_row(
|
|
312
|
-
"select envelope from results where result_id = ?1",
|
|
342
|
+
"select envelope, created_at from results where result_id = ?1",
|
|
313
343
|
params![result_id],
|
|
314
|
-
|r| r.get(0),
|
|
344
|
+
|r| Ok((r.get(0)?, r.get(1)?)),
|
|
315
345
|
)
|
|
316
346
|
.optional()?;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
watcher_id,
|
|
329
|
-
result_id: Some(result_id),
|
|
330
|
-
ok: true,
|
|
331
|
-
status: Some("notified".to_string()),
|
|
332
|
-
notified_message_id: delivered_result_message(&store, parsed.get("result_id").and_then(|v| v.as_str()).unwrap_or(""), None, None)?
|
|
333
|
-
.and_then(|v| v.get("message_id").and_then(|id| id.as_str()).map(ToString::to_string)),
|
|
334
|
-
primary_watcher_id: None,
|
|
335
|
-
prior_state: None,
|
|
336
|
-
error: None,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
347
|
+
let Some((envelope, created_at)) = row else {
|
|
348
|
+
continue;
|
|
349
|
+
};
|
|
350
|
+
let result = result_entry_from_row(&result_id, &envelope, created_at.as_deref())?;
|
|
351
|
+
notices.extend(notify_result_watchers(
|
|
352
|
+
workspace,
|
|
353
|
+
&result,
|
|
354
|
+
event_log,
|
|
355
|
+
Some(std::slice::from_ref(&watcher)),
|
|
356
|
+
Some("rebind_retry"),
|
|
357
|
+
)?);
|
|
339
358
|
}
|
|
340
359
|
Ok(notices)
|
|
341
360
|
}
|
|
342
361
|
|
|
362
|
+
/// `_result_entry_from_row`(`result_delivery.py:365-377`)。
|
|
363
|
+
fn result_entry_from_row(
|
|
364
|
+
result_id: &str,
|
|
365
|
+
envelope: &str,
|
|
366
|
+
created_at: Option<&str>,
|
|
367
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
368
|
+
let envelope: serde_json::Value = serde_json::from_str(envelope)?;
|
|
369
|
+
Ok(serde_json::json!({
|
|
370
|
+
"result_id": result_id,
|
|
371
|
+
"task_id": envelope.get("task_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
372
|
+
"agent_id": envelope.get("agent_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
373
|
+
"status": envelope.get("status").cloned().unwrap_or(serde_json::Value::Null),
|
|
374
|
+
"summary": envelope.get("summary").cloned().unwrap_or(serde_json::Value::Null),
|
|
375
|
+
"tests": envelope.get("tests").cloned().unwrap_or_else(|| serde_json::json!([])),
|
|
376
|
+
"created_at": created_at,
|
|
377
|
+
"scope": "task",
|
|
378
|
+
}))
|
|
379
|
+
}
|
|
380
|
+
|
|
343
381
|
/// `requeue_after_claim_leader` (`result_delivery.py:428`):Gap 26 —— 认领新 leader pane 后把
|
|
344
382
|
/// 未投递 watcher 重路由到新 pane。**`notified_message_id` 必须存活** (Gap 32,清空会二次注入)。
|
|
345
383
|
/// step 10 claim-leader 调。
|
|
@@ -15,6 +15,10 @@ pub enum Provider {
|
|
|
15
15
|
Claude,
|
|
16
16
|
ClaudeCode,
|
|
17
17
|
Codex,
|
|
18
|
+
/// GitHub Copilot CLI(0.3.x 新增)。一期 subscription-only(已登录态),无 fork
|
|
19
|
+
/// 能力(caps.fork=false → CapabilityUnsupported),system prompt 走 per-worker
|
|
20
|
+
/// AGENTS.md + `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` env(B2 灵魂件降级,§C2)。
|
|
21
|
+
Copilot,
|
|
18
22
|
GeminiCli,
|
|
19
23
|
Fake,
|
|
20
24
|
}
|
|
@@ -257,7 +261,9 @@ mod tests {
|
|
|
257
261
|
#[test]
|
|
258
262
|
fn unknown_provider_string_is_rejected() {
|
|
259
263
|
// 与 receive_worker_outputs 不同:Provider 是封闭集,未知值必须 Err(不 passthrough)。
|
|
260
|
-
|
|
264
|
+
// NOTE: "copilot" 已 0.3.5 加入(design + cr verdict 全 APPROVE),改用一个
|
|
265
|
+
// 仍未注册的串验"封闭集"语义。
|
|
266
|
+
assert!(serde_json::from_str::<Provider>("\"gibberish\"").is_err());
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
#[test]
|
|
@@ -184,6 +184,13 @@ pub fn provider_enforcement(provider: Provider, tool: Tool) -> Enforcement {
|
|
|
184
184
|
FsRead | FsWrite | FsList | ExecuteBash | GitDiff | ProviderBuiltin => Hard,
|
|
185
185
|
},
|
|
186
186
|
Provider::Fake => Hard,
|
|
187
|
+
// Copilot(C-2-1 cr verdict):execute_bash/fs_write/network/mcp_team = hard,
|
|
188
|
+
// fs_read/fs_list/git_diff/provider_builtin = prompt_only(诚实:copilot 无
|
|
189
|
+
// 对应 deny kind,framework 不替决,留给 provider prompt 控制;MUST-NOT-13)。
|
|
190
|
+
Provider::Copilot => match tool {
|
|
191
|
+
FsRead | FsList | GitDiff | ProviderBuiltin => PromptOnly,
|
|
192
|
+
Network | FsWrite | ExecuteBash | McpTeam => Hard,
|
|
193
|
+
},
|
|
187
194
|
// codex: 全 prompt_only。claude: 不在表中 → 全 prompt_only(同 fallback)。
|
|
188
195
|
Provider::Codex | Provider::Claude => PromptOnly,
|
|
189
196
|
}
|
|
@@ -175,7 +175,9 @@ fn result_schema_errors(envelope: &Value) -> Vec<String> {
|
|
|
175
175
|
const ROOT_KEYS: &[&str] = &[
|
|
176
176
|
"version", "team", "leader", "agents", "routing", "communication", "runtime", "context", "tasks",
|
|
177
177
|
];
|
|
178
|
-
|
|
178
|
+
// Copilot 一期加入白名单(design §B compiler.py:249-251 同位 + cr verdict 总裁,
|
|
179
|
+
// MUST-NOT-7 跨厂商等价 — 设计 / cr 已落地 26 约束)。
|
|
180
|
+
const SUPPORTED_PROVIDERS: &[&str] = &["claude", "claude_code", "codex", "copilot", "gemini_cli", "fake"];
|
|
179
181
|
const AUTH_MODES: &[&str] = &["subscription", "official_api", "compatible_api"];
|
|
180
182
|
const VALID_DISPLAY_BACKENDS: &[&str] = &[
|
|
181
183
|
"none", "tmux_attach", "iterm", "ghostty", "ghostty_window", "ghostty_workspace", "adaptive",
|