@team-agent/installer 0.4.5 → 0.4.7
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 +6 -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/tests/health_sync.rs +6 -1
- package/crates/team-agent/src/coordinator/tick.rs +46 -21
- package/crates/team-agent/src/leader/helpers.rs +1 -22
- package/crates/team-agent/src/lifecycle/launch.rs +34 -17
- package/crates/team-agent/src/lifecycle/profile_launch.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +160 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -22
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -50
- package/crates/team-agent/src/lifecycle/restart/selection.rs +23 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +19 -11
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +23 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +163 -15
- package/crates/team-agent/src/provider/adapter.rs +217 -377
- 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 +593 -55
- package/crates/team-agent/src/provider/wire.rs +139 -0
- package/crates/team-agent/src/state/persist.rs +225 -10
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +6 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//! Provider wire-format helpers — the single source of truth for converting
|
|
2
|
+
//! `Provider` ↔ string forms used across state JSON, CLI args, log fields,
|
|
3
|
+
//! and the workers we shell out to.
|
|
4
|
+
//!
|
|
5
|
+
//! This module replaces 7 hand-rolled copies of these match arms that lived
|
|
6
|
+
//! in `leader/helpers.rs`, `coordinator/tick.rs` (×2), `lifecycle/restart/common.rs`,
|
|
7
|
+
//! `lifecycle/profile_launch.rs`, `lifecycle/launch.rs`, `provider/adapter.rs`,
|
|
8
|
+
//! and `lifecycle/restart/rebuild.rs`. Centralising them removes the kind of
|
|
9
|
+
//! drift that left `restart/rebuild.rs` with a `claude-code` alias the other
|
|
10
|
+
//! sites silently dropped, and shrinks the surface area when a new provider
|
|
11
|
+
//! is added.
|
|
12
|
+
//!
|
|
13
|
+
//! Wire-format invariants (do not change without a coordinated migration):
|
|
14
|
+
//! * `provider_wire(Provider)` returns the canonical snake_case string used
|
|
15
|
+
//! in state JSON. These values are persisted on disk and read by older
|
|
16
|
+
//! runtimes — changing them is a breaking change.
|
|
17
|
+
//! * `parse_provider(&str)` accepts the canonical form AND every historical
|
|
18
|
+
//! alias we have ever written. New aliases go in `aliases()` and become
|
|
19
|
+
//! a parse target automatically.
|
|
20
|
+
//! * `command_name(Provider)` returns the binary name we exec for that
|
|
21
|
+
//! provider. `Claude` and `ClaudeCode` both shell out to `claude` —
|
|
22
|
+
//! they are distinct state values but share an executable.
|
|
23
|
+
|
|
24
|
+
use crate::model::enums::Provider;
|
|
25
|
+
|
|
26
|
+
/// Canonical snake_case wire string for `Provider`. Used for state JSON,
|
|
27
|
+
/// log fields, and any cross-process serialization. Stable: do not change
|
|
28
|
+
/// existing values.
|
|
29
|
+
pub(crate) fn provider_wire(provider: Provider) -> &'static str {
|
|
30
|
+
match provider {
|
|
31
|
+
Provider::Claude => "claude",
|
|
32
|
+
Provider::ClaudeCode => "claude_code",
|
|
33
|
+
Provider::Codex => "codex",
|
|
34
|
+
Provider::Copilot => "copilot",
|
|
35
|
+
Provider::GeminiCli => "gemini_cli",
|
|
36
|
+
Provider::Fake => "fake",
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Parse a wire string back to `Provider`. Accepts the canonical
|
|
41
|
+
/// `provider_wire` output AND every historical alias listed by `aliases()`.
|
|
42
|
+
/// Returns `None` for unknown strings.
|
|
43
|
+
pub(crate) fn parse_provider(raw: &str) -> Option<Provider> {
|
|
44
|
+
for &provider in ALL_PROVIDERS {
|
|
45
|
+
for alias in aliases(provider) {
|
|
46
|
+
if *alias == raw {
|
|
47
|
+
return Some(provider);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
None
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// All aliases (including the canonical wire form) that parse to `provider`.
|
|
55
|
+
/// The canonical wire form is always the first entry, so callers that need
|
|
56
|
+
/// the "primary" string can take `aliases(p)[0]` — though most code should
|
|
57
|
+
/// use `provider_wire(p)` directly for clarity.
|
|
58
|
+
pub(crate) fn aliases(provider: Provider) -> &'static [&'static str] {
|
|
59
|
+
match provider {
|
|
60
|
+
Provider::Claude => &["claude"],
|
|
61
|
+
// `claude-code` (kebab) is a historical alias that appeared in
|
|
62
|
+
// restart/rebuild.rs:1186-1194 only. Keeping it parseable means
|
|
63
|
+
// legacy state files still load.
|
|
64
|
+
Provider::ClaudeCode => &["claude_code", "claude-code"],
|
|
65
|
+
Provider::Codex => &["codex"],
|
|
66
|
+
Provider::Copilot => &["copilot"],
|
|
67
|
+
Provider::GeminiCli => &["gemini_cli"],
|
|
68
|
+
Provider::Fake => &["fake"],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Executable name we exec for the worker. `Claude` and `ClaudeCode` both
|
|
73
|
+
/// resolve to `claude` — the distinction is in state semantics
|
|
74
|
+
/// (`ClaudeCode` is the canonical CLI; `Claude` is a legacy alias kept for
|
|
75
|
+
/// older specs).
|
|
76
|
+
pub(crate) fn command_name(provider: Provider) -> &'static str {
|
|
77
|
+
match provider {
|
|
78
|
+
Provider::Claude | Provider::ClaudeCode => "claude",
|
|
79
|
+
Provider::Codex => "codex",
|
|
80
|
+
Provider::Copilot => "copilot",
|
|
81
|
+
Provider::GeminiCli => "gemini",
|
|
82
|
+
Provider::Fake => "team-agent",
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ALL_PROVIDERS: &[Provider] = &[
|
|
87
|
+
Provider::Claude,
|
|
88
|
+
Provider::ClaudeCode,
|
|
89
|
+
Provider::Codex,
|
|
90
|
+
Provider::Copilot,
|
|
91
|
+
Provider::GeminiCli,
|
|
92
|
+
Provider::Fake,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
#[cfg(test)]
|
|
96
|
+
mod tests {
|
|
97
|
+
use super::*;
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn wire_round_trip_for_every_provider() {
|
|
101
|
+
for &p in ALL_PROVIDERS {
|
|
102
|
+
let wire = provider_wire(p);
|
|
103
|
+
assert_eq!(parse_provider(wire), Some(p), "round-trip {p:?}");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn claude_code_kebab_alias_parses() {
|
|
109
|
+
// Historical alias preserved from restart/rebuild.rs:1186-1194.
|
|
110
|
+
assert_eq!(parse_provider("claude-code"), Some(Provider::ClaudeCode));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn unknown_string_returns_none() {
|
|
115
|
+
assert_eq!(parse_provider(""), None);
|
|
116
|
+
assert_eq!(parse_provider("openai"), None);
|
|
117
|
+
assert_eq!(parse_provider("CLAUDE"), None); // case-sensitive
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn aliases_first_entry_is_canonical_wire() {
|
|
122
|
+
for &p in ALL_PROVIDERS {
|
|
123
|
+
assert_eq!(aliases(p)[0], provider_wire(p), "canonical first for {p:?}");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn command_name_claude_and_claude_code_both_resolve_to_claude_binary() {
|
|
129
|
+
assert_eq!(command_name(Provider::Claude), "claude");
|
|
130
|
+
assert_eq!(command_name(Provider::ClaudeCode), "claude");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn command_name_covers_every_provider() {
|
|
135
|
+
for &p in ALL_PROVIDERS {
|
|
136
|
+
assert!(!command_name(p).is_empty(), "{p:?} has no command name");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -519,28 +519,62 @@ fn preserve_missing_agents(
|
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
///
|
|
523
|
-
///
|
|
524
|
-
///
|
|
525
|
-
///
|
|
522
|
+
/// 0.4.6 tuple-atomic backfill (restart-persist-capture-contract-audit.md):
|
|
523
|
+
/// the authoritative session tuple is `session_id + rollout_path +
|
|
524
|
+
/// captured_at + captured_via`. `attribution_confidence` is metadata on
|
|
525
|
+
/// that tuple. Persist may preserve an already-complete tuple across stale
|
|
526
|
+
/// saves, but it must NEVER:
|
|
527
|
+
/// * synthesise a partial tuple (e.g. copy `session_id` while the other
|
|
528
|
+
/// three are absent — this caused bug-045 by resurrecting an
|
|
529
|
+
/// intentionally cleared session id);
|
|
530
|
+
/// * mix tuple fields across two different session_ids (one writer's
|
|
531
|
+
/// `session_id` glued to another writer's `rollout_path`);
|
|
532
|
+
/// * create non-null session truth where none existed.
|
|
533
|
+
///
|
|
534
|
+
/// Rules:
|
|
535
|
+
/// 1. If LATEST has a complete tuple AND INCOMING has the same `session_id`
|
|
536
|
+
/// (or null), backfill the full tuple together — concurrent capture
|
|
537
|
+
/// protection (A0/R2).
|
|
538
|
+
/// 2. If LATEST has a complete tuple AND INCOMING has a DIFFERENT non-null
|
|
539
|
+
/// `session_id`, copy nothing (incoming is a different worker session
|
|
540
|
+
/// identity; the latest tuple belongs to the previous identity).
|
|
541
|
+
/// 3. If LATEST tuple is INCOMPLETE (any of the 4 fields null), copy
|
|
542
|
+
/// nothing for the tuple. An incomplete latest tuple is not
|
|
543
|
+
/// authoritative truth, so persist cannot use it as backfill source.
|
|
544
|
+
/// 4. `attribution_confidence` rides with the tuple (copied iff tuple
|
|
545
|
+
/// backfill fires).
|
|
526
546
|
fn backfill_capture_fields(incoming_agent: &mut Value, latest_agent: &Value) {
|
|
527
|
-
const
|
|
547
|
+
const TUPLE_FIELDS: [&str; 4] = [
|
|
528
548
|
"session_id",
|
|
529
549
|
"rollout_path",
|
|
530
550
|
"captured_at",
|
|
531
551
|
"captured_via",
|
|
532
|
-
"attribution_confidence",
|
|
533
552
|
];
|
|
534
553
|
let Some(incoming_row) = incoming_agent.as_object_mut() else {
|
|
535
554
|
return;
|
|
536
555
|
};
|
|
556
|
+
// Rule 3: latest tuple must be COMPLETE to be a backfill source.
|
|
557
|
+
let latest_complete = TUPLE_FIELDS
|
|
558
|
+
.iter()
|
|
559
|
+
.all(|field| latest_agent.get(field).is_some_and(|v| !v.is_null()));
|
|
560
|
+
if !latest_complete {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Rule 2: incoming carries a DIFFERENT non-null session_id → do not mix.
|
|
537
564
|
let incoming_session = incoming_row.get("session_id").and_then(Value::as_str);
|
|
538
565
|
let latest_session = latest_agent.get("session_id").and_then(Value::as_str);
|
|
539
|
-
let
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
continue;
|
|
566
|
+
if let Some(inc) = incoming_session {
|
|
567
|
+
if Some(inc) != latest_session {
|
|
568
|
+
return;
|
|
543
569
|
}
|
|
570
|
+
}
|
|
571
|
+
// Rule 1 + 4: copy the full tuple together (only fields incoming has as
|
|
572
|
+
// null/missing) plus attribution_confidence.
|
|
573
|
+
for field in TUPLE_FIELDS
|
|
574
|
+
.iter()
|
|
575
|
+
.copied()
|
|
576
|
+
.chain(std::iter::once("attribution_confidence"))
|
|
577
|
+
{
|
|
544
578
|
if incoming_row.get(field).is_none_or(Value::is_null) {
|
|
545
579
|
if let Some(value) = latest_agent.get(field).filter(|value| !value.is_null()) {
|
|
546
580
|
incoming_row.insert(field.to_string(), value.clone());
|
|
@@ -1313,4 +1347,185 @@ mod tests {
|
|
|
1313
1347
|
"migration must be persisted to disk so subsequent processes see it"
|
|
1314
1348
|
);
|
|
1315
1349
|
}
|
|
1350
|
+
|
|
1351
|
+
/// 0.4.6 tuple-atomic contract test #1 (audit §Required Tests, line 264):
|
|
1352
|
+
/// latest has `session_id` only (PARTIAL TUPLE), incoming has null
|
|
1353
|
+
/// tuple → no backfill. This is bug-045's core failure mode: persist
|
|
1354
|
+
/// must not resurrect a scalar session_id when the latest row's tuple
|
|
1355
|
+
/// is incomplete.
|
|
1356
|
+
#[test]
|
|
1357
|
+
fn backfill_capture_skips_when_latest_tuple_is_partial() {
|
|
1358
|
+
let mut incoming = json!({
|
|
1359
|
+
"session_id": Value::Null,
|
|
1360
|
+
"rollout_path": Value::Null,
|
|
1361
|
+
"captured_at": Value::Null,
|
|
1362
|
+
"captured_via": Value::Null,
|
|
1363
|
+
});
|
|
1364
|
+
let latest = json!({
|
|
1365
|
+
"session_id": "partial-only-uuid",
|
|
1366
|
+
"rollout_path": Value::Null,
|
|
1367
|
+
"captured_at": Value::Null,
|
|
1368
|
+
"captured_via": Value::Null,
|
|
1369
|
+
});
|
|
1370
|
+
backfill_capture_fields(&mut incoming, &latest);
|
|
1371
|
+
assert_eq!(
|
|
1372
|
+
incoming.get("session_id"),
|
|
1373
|
+
Some(&Value::Null),
|
|
1374
|
+
"0.4.6: partial latest tuple must NOT backfill session_id; got {incoming}"
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/// 0.4.6 tuple-atomic contract test #2 (audit, line 265):
|
|
1379
|
+
/// latest has complete tuple, incoming has null tuple → all 4 fields
|
|
1380
|
+
/// backfilled together. This is the concurrent-capture protection
|
|
1381
|
+
/// (A0/R2) that must still work after the tuple-atomic rewrite.
|
|
1382
|
+
#[test]
|
|
1383
|
+
fn backfill_capture_atomically_copies_complete_tuple() {
|
|
1384
|
+
let mut incoming = json!({
|
|
1385
|
+
"session_id": Value::Null,
|
|
1386
|
+
"rollout_path": Value::Null,
|
|
1387
|
+
"captured_at": Value::Null,
|
|
1388
|
+
"captured_via": Value::Null,
|
|
1389
|
+
});
|
|
1390
|
+
let latest = json!({
|
|
1391
|
+
"session_id": "real-uuid",
|
|
1392
|
+
"rollout_path": "/tmp/real.jsonl",
|
|
1393
|
+
"captured_at": "2026-06-25T10:00:00+00:00",
|
|
1394
|
+
"captured_via": "session.captured",
|
|
1395
|
+
"attribution_confidence": "high",
|
|
1396
|
+
});
|
|
1397
|
+
backfill_capture_fields(&mut incoming, &latest);
|
|
1398
|
+
assert_eq!(
|
|
1399
|
+
incoming.get("session_id").and_then(Value::as_str),
|
|
1400
|
+
Some("real-uuid")
|
|
1401
|
+
);
|
|
1402
|
+
assert_eq!(
|
|
1403
|
+
incoming.get("rollout_path").and_then(Value::as_str),
|
|
1404
|
+
Some("/tmp/real.jsonl")
|
|
1405
|
+
);
|
|
1406
|
+
assert_eq!(
|
|
1407
|
+
incoming.get("captured_at").and_then(Value::as_str),
|
|
1408
|
+
Some("2026-06-25T10:00:00+00:00")
|
|
1409
|
+
);
|
|
1410
|
+
assert_eq!(
|
|
1411
|
+
incoming.get("captured_via").and_then(Value::as_str),
|
|
1412
|
+
Some("session.captured")
|
|
1413
|
+
);
|
|
1414
|
+
assert_eq!(
|
|
1415
|
+
incoming.get("attribution_confidence").and_then(Value::as_str),
|
|
1416
|
+
Some("high"),
|
|
1417
|
+
"attribution_confidence rides with the tuple"
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/// 0.4.6 tuple-atomic contract test #3 (audit, line 266):
|
|
1422
|
+
/// latest has complete tuple for one session_id, incoming has non-null
|
|
1423
|
+
/// DIFFERENT session_id → no mixed tuple. Prevents one writer's
|
|
1424
|
+
/// session_id getting glued to another writer's rollout_path/captured_at.
|
|
1425
|
+
#[test]
|
|
1426
|
+
fn backfill_capture_refuses_mixing_different_session_ids() {
|
|
1427
|
+
let mut incoming = json!({
|
|
1428
|
+
"session_id": "incoming-different-uuid",
|
|
1429
|
+
"rollout_path": Value::Null,
|
|
1430
|
+
"captured_at": Value::Null,
|
|
1431
|
+
"captured_via": Value::Null,
|
|
1432
|
+
});
|
|
1433
|
+
let latest = json!({
|
|
1434
|
+
"session_id": "latest-uuid",
|
|
1435
|
+
"rollout_path": "/tmp/latest.jsonl",
|
|
1436
|
+
"captured_at": "2026-06-25T10:00:00+00:00",
|
|
1437
|
+
"captured_via": "session.captured",
|
|
1438
|
+
});
|
|
1439
|
+
backfill_capture_fields(&mut incoming, &latest);
|
|
1440
|
+
// session_id stays as incoming (not overwritten).
|
|
1441
|
+
assert_eq!(
|
|
1442
|
+
incoming.get("session_id").and_then(Value::as_str),
|
|
1443
|
+
Some("incoming-different-uuid"),
|
|
1444
|
+
"0.4.6: incoming session_id must not be overwritten by a different latest"
|
|
1445
|
+
);
|
|
1446
|
+
// Sibling fields stay null (no cross-session mixing).
|
|
1447
|
+
assert_eq!(
|
|
1448
|
+
incoming.get("rollout_path"),
|
|
1449
|
+
Some(&Value::Null),
|
|
1450
|
+
"0.4.6: must not copy latest rollout_path onto a different session_id"
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/// 0.4.6 tuple-atomic contract test #4 (audit, line 267):
|
|
1455
|
+
/// `normalize_agent_session_state` may only insert null defaults — it
|
|
1456
|
+
/// must never create non-null session truth.
|
|
1457
|
+
#[test]
|
|
1458
|
+
fn normalize_agent_session_state_only_inserts_null_defaults() {
|
|
1459
|
+
let mut state = json!({
|
|
1460
|
+
"agents": {
|
|
1461
|
+
"alpha": {}
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
normalize_agent_session_state(&mut state);
|
|
1465
|
+
let alpha = state.pointer("/agents/alpha").expect("alpha");
|
|
1466
|
+
for field in [
|
|
1467
|
+
"session_id",
|
|
1468
|
+
"rollout_path",
|
|
1469
|
+
"captured_at",
|
|
1470
|
+
"captured_via",
|
|
1471
|
+
"attribution_confidence",
|
|
1472
|
+
"spawn_cwd",
|
|
1473
|
+
] {
|
|
1474
|
+
assert_eq!(
|
|
1475
|
+
alpha.get(field),
|
|
1476
|
+
Some(&Value::Null),
|
|
1477
|
+
"normalize must insert null for {field}; got {alpha}"
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/// Bug 045 end-to-end persist regression: poisoned latest (session_id
|
|
1483
|
+
/// but no captured_at/captured_via) cannot resurrect a cleared incoming
|
|
1484
|
+
/// row through a real save → reload cycle.
|
|
1485
|
+
#[test]
|
|
1486
|
+
fn poisoned_partial_latest_does_not_resurrect_on_save_reload() {
|
|
1487
|
+
let ws = temp_ws();
|
|
1488
|
+
// Seed disk with the exact bug-045 poisoned shape: session_id set,
|
|
1489
|
+
// no captured_at / captured_via / rollout_path.
|
|
1490
|
+
let poisoned = json!({
|
|
1491
|
+
"session_name": "team-bug045-poisoned",
|
|
1492
|
+
"team_key": "teamP",
|
|
1493
|
+
"agents": {
|
|
1494
|
+
"alpha": {
|
|
1495
|
+
"status": "stopped",
|
|
1496
|
+
"provider": "claude",
|
|
1497
|
+
"session_id": "stale-poisoned-uuid",
|
|
1498
|
+
"rollout_path": Value::Null,
|
|
1499
|
+
"captured_at": Value::Null,
|
|
1500
|
+
"captured_via": Value::Null
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
save_runtime_state(&ws, &poisoned).unwrap();
|
|
1505
|
+
|
|
1506
|
+
// Restart-fresh writes a cleared row.
|
|
1507
|
+
let cleared = json!({
|
|
1508
|
+
"session_name": "team-bug045-poisoned",
|
|
1509
|
+
"team_key": "teamP",
|
|
1510
|
+
"agents": {
|
|
1511
|
+
"alpha": {
|
|
1512
|
+
"status": "running",
|
|
1513
|
+
"provider": "claude",
|
|
1514
|
+
"session_id": Value::Null,
|
|
1515
|
+
"rollout_path": Value::Null,
|
|
1516
|
+
"captured_at": Value::Null,
|
|
1517
|
+
"captured_via": Value::Null
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
save_runtime_state(&ws, &cleared).unwrap();
|
|
1522
|
+
|
|
1523
|
+
let on_disk = read_state(&ws);
|
|
1524
|
+
let alpha = on_disk.pointer("/agents/alpha").expect("alpha");
|
|
1525
|
+
assert_eq!(
|
|
1526
|
+
alpha.get("session_id"),
|
|
1527
|
+
Some(&Value::Null),
|
|
1528
|
+
"0.4.6: partial poisoned latest must not resurrect cleared session_id; got {alpha}"
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1316
1531
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.4.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.4.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.4.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.4.7",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.4.7",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.4.7"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -18,6 +18,12 @@ team-agent claude
|
|
|
18
18
|
|
|
19
19
|
Pass provider flags after the provider name, for example `team-agent codex --dangerously-bypass-approvals-and-sandbox`. Existing tmux layouts are valid too, including Finder/Ghostty launchers, as long as `team-agent quick-start` is invoked from the leader's current tmux pane. Do not start a real team from a naked terminal that Team Agent cannot address through tmux.
|
|
20
20
|
|
|
21
|
+
## Leader Role
|
|
22
|
+
|
|
23
|
+
Invoking this skill turns the current agent into the team leader. The leader **orchestrates**: read reports, set direction, decompose work, dispatch tasks to teammates, review results, and decide. The leader does **not** execute hands-on work — no `cargo test`, no product-code edits, no `git push`, no build/verify cycles. Those belong to teammates. If the leader catches themselves running tests, editing source files, or pushing commits, they have stepped out of role; stop and re-dispatch.
|
|
24
|
+
|
|
25
|
+
When the user has been communicating in Chinese throughout the conversation, all leader↔teammate messaging (`send`, `report_result`, MCP messages, task descriptions) must also be in Chinese. The leader dispatches in Chinese, the worker reports back in Chinese. Switch back to the user's language only at the user-facing boundary.
|
|
26
|
+
|
|
21
27
|
## Minimal Copy-Paste Team
|
|
22
28
|
|
|
23
29
|
```bash
|