@team-agent/installer 0.5.99 → 0.5.101
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 +1 -156
- package/crates/team-agent/src/cli/diagnose.rs +32 -10
- package/crates/team-agent/src/cli/emit.rs +6 -185
- package/crates/team-agent/src/cli/mod.rs +32 -182
- package/crates/team-agent/src/cli/send/coordinator.rs +2 -2
- package/crates/team-agent/src/cli/spec.rs +41 -67
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -185
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +9 -9
- package/crates/team-agent/src/cli/triage.rs +1 -1
- package/crates/team-agent/src/cli/types.rs +0 -49
- package/crates/team-agent/src/compiler.rs +3 -0
- package/crates/team-agent/src/leader/lease.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +114 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +14 -8
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +9 -2
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +0 -60
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/messaging/send.rs +2 -2
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +3 -1
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository.rs +125 -9
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +17 -46
- package/skills/team-agent/references/recovery-runbook.md +2 -2
- package/skills/team-agent/references/team-agent-operator.md +4 -2
- package/skills/team-agent/references/team-in-team.md +0 -6
|
@@ -106,92 +106,6 @@ fn seed_team_spec(ws: &std::path::Path) {
|
|
|
106
106
|
std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// ── ACK-CRACK [P1 byte-shape] — acknowledge_idle must write golden's TTL suppression shape ───────
|
|
110
|
-
// golden runtime.py:680-688: manual-acknowledge persists
|
|
111
|
-
// coordinator.idle_acknowledged[team] = {acknowledged_at, expires_at, ttl_seconds}
|
|
112
|
-
// coordinator.suppressed_idle_alerts[team][worker].idle_fallback =
|
|
113
|
-
// {suppressed_at, suppressed_by:"manual_acknowledge", manual_acknowledge:true, expires_at, ttl_seconds}
|
|
114
|
-
// The clear logic does datetime.fromisoformat(entry["expires_at"]); a MISSING expires_at -> ValueError
|
|
115
|
-
// -> "invalid_suppression_timestamp" -> immediate self-clear (latent crack once detect_idle_fallbacks
|
|
116
|
-
// is ported). So BOTH idle_acknowledged and the entry MUST carry a non-empty expires_at.
|
|
117
|
-
#[test]
|
|
118
|
-
fn acknowledge_idle_writes_golden_ttl_suppression_shape() {
|
|
119
|
-
let ws = tmp_workspace();
|
|
120
|
-
crate::state::persist::save_runtime_state(
|
|
121
|
-
&ws,
|
|
122
|
-
&serde_json::json!({
|
|
123
|
-
"active_team_key": "teamX",
|
|
124
|
-
"agents": {"w1": {"status": "running", "provider": "codex"}}
|
|
125
|
-
}),
|
|
126
|
-
)
|
|
127
|
-
.unwrap();
|
|
128
|
-
let _ = lifecycle_port::acknowledge_idle(&ws, None).expect("acknowledge_idle ok");
|
|
129
|
-
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
130
|
-
let ack = &state["coordinator"]["idle_acknowledged"]["teamX"];
|
|
131
|
-
assert!(
|
|
132
|
-
ack.get("expires_at")
|
|
133
|
-
.and_then(serde_json::Value::as_str)
|
|
134
|
-
.is_some_and(|s| !s.is_empty()),
|
|
135
|
-
"ACK-CRACK: idle_acknowledged[team] must carry a non-empty expires_at (golden); got {ack}"
|
|
136
|
-
);
|
|
137
|
-
assert!(
|
|
138
|
-
ack.get("ttl_seconds").is_some(),
|
|
139
|
-
"idle_acknowledged[team] must carry ttl_seconds; got {ack}"
|
|
140
|
-
);
|
|
141
|
-
let entry = &state["coordinator"]["suppressed_idle_alerts"]["teamX"]["w1"]["idle_fallback"];
|
|
142
|
-
assert!(
|
|
143
|
-
entry.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
|
|
144
|
-
"ACK-CRACK: the manual-ack suppression entry must carry expires_at (else clear logic ValueErrors \
|
|
145
|
-
-> instant self-clear); got {entry}"
|
|
146
|
-
);
|
|
147
|
-
assert_eq!(
|
|
148
|
-
entry["suppressed_by"],
|
|
149
|
-
serde_json::json!("manual_acknowledge"),
|
|
150
|
-
"golden suppressed_by; got {entry}"
|
|
151
|
-
);
|
|
152
|
-
assert_eq!(
|
|
153
|
-
entry["manual_acknowledge"],
|
|
154
|
-
serde_json::json!(true),
|
|
155
|
-
"golden manual_acknowledge:true; got {entry}"
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
// ── ACK return-shape [P1 byte-parity] — acknowledge_idle must RETURN golden's keys ───────────────
|
|
159
|
-
// golden runtime.py:691: return {ok, team, agent_id, acknowledged_at, expires_at, ttl_seconds}.
|
|
160
|
-
// Rust (cli/mod.rs) returns only {ok, team, ttl_seconds} -> missing agent_id, acknowledged_at,
|
|
161
|
-
// expires_at. RED. (acknowledged_at/expires_at are the same values written into idle_acknowledged.)
|
|
162
|
-
#[test]
|
|
163
|
-
fn acknowledge_idle_return_carries_golden_keys() {
|
|
164
|
-
let ws = tmp_workspace();
|
|
165
|
-
crate::state::persist::save_runtime_state(
|
|
166
|
-
&ws,
|
|
167
|
-
&serde_json::json!({ "active_team_key": "teamX", "agents": {"w1": {"status": "running", "provider": "codex"}} }),
|
|
168
|
-
)
|
|
169
|
-
.unwrap();
|
|
170
|
-
let r = lifecycle_port::acknowledge_idle(&ws, None).expect("acknowledge_idle ok");
|
|
171
|
-
let obj = r.as_object().expect("ack returns a dict");
|
|
172
|
-
for key in [
|
|
173
|
-
"ok",
|
|
174
|
-
"team",
|
|
175
|
-
"agent_id",
|
|
176
|
-
"acknowledged_at",
|
|
177
|
-
"expires_at",
|
|
178
|
-
"ttl_seconds",
|
|
179
|
-
] {
|
|
180
|
-
assert!(
|
|
181
|
-
obj.contains_key(key),
|
|
182
|
-
"ACK return-shape: golden return carries `{key}` (runtime.py:691: ok/team/agent_id/\
|
|
183
|
-
acknowledged_at/expires_at/ttl_seconds); Rust omits it. got keys {:?}",
|
|
184
|
-
obj.keys().collect::<Vec<_>>()
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
assert!(
|
|
188
|
-
obj.get("expires_at")
|
|
189
|
-
.and_then(serde_json::Value::as_str)
|
|
190
|
-
.is_some_and(|s| !s.is_empty()),
|
|
191
|
-
"ACK return-shape: expires_at must be a non-empty timestamp; got {r}"
|
|
192
|
-
);
|
|
193
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
194
|
-
}
|
|
195
109
|
// ── BUG-2 [real bug] — inbox must RETURN the stored messages, not a hardcoded []. ────────────────
|
|
196
110
|
// Golden status/inbox.py:35-38 -> MessageStore.inbox(agent_id) (core.py:242, owner_team_id=None):
|
|
197
111
|
// select <MESSAGE_SELECT> from messages where sender = ? or recipient = ? order by created_at desc
|
|
@@ -680,16 +594,7 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
|
|
|
680
594
|
fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
|
|
681
595
|
let ws = tmp_workspace();
|
|
682
596
|
seed_collect_state(&ws);
|
|
683
|
-
let out =
|
|
684
|
-
cmd_stuck_cancel(&StuckCancelArgs {
|
|
685
|
-
agent: "fake_impl".to_string(),
|
|
686
|
-
workspace: ws.clone(),
|
|
687
|
-
alert_type: None,
|
|
688
|
-
json: true,
|
|
689
|
-
team: None,
|
|
690
|
-
})
|
|
691
|
-
.unwrap(),
|
|
692
|
-
);
|
|
597
|
+
let out = messaging::stuck_cancel(&ws, "fake_impl", None, "leader").unwrap();
|
|
693
598
|
let team_key = seeded_team_key(&ws);
|
|
694
599
|
assert_eq!(out["ok"], json!(true));
|
|
695
600
|
assert_eq!(
|
|
@@ -715,98 +620,10 @@ fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
|
|
|
715
620
|
&& e["agent_id"] == json!("fake_impl")),
|
|
716
621
|
"stuck_cancel must write coordinator.idle_alert_suppressed"
|
|
717
622
|
);
|
|
718
|
-
let listed =
|
|
719
|
-
cmd_stuck_list(&StuckListArgs {
|
|
720
|
-
workspace: ws.clone(),
|
|
721
|
-
json: true,
|
|
722
|
-
team: None,
|
|
723
|
-
})
|
|
724
|
-
.unwrap(),
|
|
725
|
-
);
|
|
623
|
+
let listed = messaging::stuck_list(&ws).unwrap();
|
|
726
624
|
assert_eq!(
|
|
727
625
|
listed["suppressed_idle_alerts"]["fake_impl"]["stuck"]["suppressed_by"],
|
|
728
626
|
json!("leader"),
|
|
729
627
|
"stuck-list must read the persisted state mirror, not return a hard-coded empty list"
|
|
730
628
|
);
|
|
731
629
|
}
|
|
732
|
-
|
|
733
|
-
#[test]
|
|
734
|
-
fn stuck_cancel_explicit_team_is_rejected_until_backend_is_scoped() {
|
|
735
|
-
let ws = tmp_workspace();
|
|
736
|
-
seed_collect_state(&ws);
|
|
737
|
-
let err = cmd_stuck_cancel(&StuckCancelArgs {
|
|
738
|
-
agent: "fake_impl".to_string(),
|
|
739
|
-
workspace: ws.clone(),
|
|
740
|
-
alert_type: None,
|
|
741
|
-
json: true,
|
|
742
|
-
team: Some("current".to_string()),
|
|
743
|
-
})
|
|
744
|
-
.expect_err("explicit --team must not silently write global stuck suppression");
|
|
745
|
-
assert!(
|
|
746
|
-
err.to_string().contains("not supported yet"),
|
|
747
|
-
"stuck-cancel --team must be an explicit refusal until backend supports scoped writes; got {err}"
|
|
748
|
-
);
|
|
749
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
#[test]
|
|
753
|
-
fn stuck_cancel_invalid_alert_type_is_rejected() {
|
|
754
|
-
let ws = tmp_workspace();
|
|
755
|
-
seed_collect_state(&ws);
|
|
756
|
-
let code = run(
|
|
757
|
-
&cli_argv(&[
|
|
758
|
-
"stuck-cancel",
|
|
759
|
-
"fake_impl",
|
|
760
|
-
"--workspace",
|
|
761
|
-
&ws.to_string_lossy(),
|
|
762
|
-
"--alert-type",
|
|
763
|
-
"bogus",
|
|
764
|
-
"--json",
|
|
765
|
-
]),
|
|
766
|
-
&ws,
|
|
767
|
-
);
|
|
768
|
-
assert_eq!(
|
|
769
|
-
code,
|
|
770
|
-
ExitCode::Error,
|
|
771
|
-
"Python rejects alert_type outside stuck/idle_fallback/cross_worker_deadlock/all; Rust must not silently coerce bogus to stuck"
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
#[test]
|
|
775
|
-
fn acknowledge_idle_records_manual_idle_fallback_suppression_and_event() {
|
|
776
|
-
let ws = tmp_workspace();
|
|
777
|
-
seed_collect_state(&ws);
|
|
778
|
-
let out = json_output(
|
|
779
|
-
cmd_acknowledge_idle(&AcknowledgeIdleArgs {
|
|
780
|
-
team: None,
|
|
781
|
-
workspace: ws.clone(),
|
|
782
|
-
json: true,
|
|
783
|
-
})
|
|
784
|
-
.unwrap(),
|
|
785
|
-
);
|
|
786
|
-
let team_key = seeded_team_key(&ws);
|
|
787
|
-
assert_eq!(out["ok"], json!(true));
|
|
788
|
-
assert_eq!(out["team"], json!(team_key));
|
|
789
|
-
assert_eq!(out["ttl_seconds"], json!(1800));
|
|
790
|
-
let state = read_state(&ws);
|
|
791
|
-
let ack = &state["coordinator"]["idle_acknowledged"][&team_key];
|
|
792
|
-
assert_eq!(ack["ttl_seconds"], json!(1800));
|
|
793
|
-
assert!(ack["acknowledged_at"].as_str().is_some());
|
|
794
|
-
assert_eq!(
|
|
795
|
-
state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]
|
|
796
|
-
["suppressed_by"],
|
|
797
|
-
json!("manual_acknowledge")
|
|
798
|
-
);
|
|
799
|
-
assert_eq!(
|
|
800
|
-
state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]
|
|
801
|
-
["manual_acknowledge"],
|
|
802
|
-
json!(true)
|
|
803
|
-
);
|
|
804
|
-
assert!(
|
|
805
|
-
read_events(&ws)
|
|
806
|
-
.iter()
|
|
807
|
-
.any(|e| e["event"] == json!("coordinator.idle_acknowledged")
|
|
808
|
-
&& e["team"] == json!(team_key)
|
|
809
|
-
&& e["ttl_seconds"] == json!(1800)),
|
|
810
|
-
"acknowledge-idle must emit coordinator.idle_acknowledged"
|
|
811
|
-
);
|
|
812
|
-
}
|
|
@@ -332,15 +332,15 @@ fn dispatch_routes_diagnose_checks_runtime_beyond_healthy_leader() {
|
|
|
332
332
|
leader_env: Default::default(),
|
|
333
333
|
}]);
|
|
334
334
|
crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
let result = cmd_diagnose(&DiagnoseArgs {
|
|
335
|
+
let code = run(
|
|
336
|
+
&cli_argv(&["doctor", "--workspace", &ws.to_string_lossy(), "--json"]),
|
|
337
|
+
&ws,
|
|
338
|
+
);
|
|
339
|
+
assert_eq!(code, ExitCode::Error, "healthy binding alone cannot hide missing coordinator");
|
|
340
|
+
let result = cmd_doctor(&DoctorArgs {
|
|
343
341
|
workspace: ws.clone(), json: true, team: None,
|
|
342
|
+
spec: None, gate: None, comms: false, fix: false,
|
|
343
|
+
fix_schema: false, cleanup_orphans: false, confirm: false,
|
|
344
344
|
}).expect("diagnostic report");
|
|
345
345
|
let CmdOutput::Json(report) = result.output else {
|
|
346
346
|
panic!("expected JSON diagnostic report");
|
|
@@ -377,7 +377,7 @@ fn dispatch_routes_diagnose_self_signed_attached_without_registry_is_not_ok() {
|
|
|
377
377
|
let ws = tmp_workspace();
|
|
378
378
|
seed_self_signed_attached_leader(&ws);
|
|
379
379
|
let code = run(
|
|
380
|
-
&cli_argv(&["
|
|
380
|
+
&cli_argv(&["doctor", "--workspace", &ws.to_string_lossy(), "--json"]),
|
|
381
381
|
&ws,
|
|
382
382
|
);
|
|
383
383
|
assert_ne!(
|
|
@@ -138,7 +138,7 @@ mod tests {
|
|
|
138
138
|
"providers": {"claude": {"version": "secret"}},
|
|
139
139
|
"runtime": {"workspace": "/tmp/secret"}
|
|
140
140
|
});
|
|
141
|
-
let output = render("
|
|
141
|
+
let output = render("doctor", &report);
|
|
142
142
|
assert!(output.contains("issue: short_issue"));
|
|
143
143
|
assert!(output.contains("issue: code=object_issue"));
|
|
144
144
|
assert!(output.contains("repair: action=run repair"));
|
|
@@ -312,14 +312,6 @@ pub struct QuickStartArgs {
|
|
|
312
312
|
// user-confirmed. quick-start is now an initial-creation-only verb.
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
/// `init`(`parser.py` bootstrap verb)。
|
|
316
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
317
|
-
pub struct InitArgs {
|
|
318
|
-
pub workspace: PathBuf,
|
|
319
|
-
pub force: bool,
|
|
320
|
-
pub json: bool,
|
|
321
|
-
}
|
|
322
|
-
|
|
323
315
|
/// `send`(`parser.py:262`)。`target` xor `--to`(fanout);`message` 多 token join 空格。
|
|
324
316
|
#[derive(Debug, Clone, PartialEq)]
|
|
325
317
|
pub struct SendArgs {
|
|
@@ -554,36 +546,6 @@ pub struct RemoveAgentArgs {
|
|
|
554
546
|
pub json: bool,
|
|
555
547
|
}
|
|
556
548
|
|
|
557
|
-
/// `stuck-list`(`parser.py:424`)。
|
|
558
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
559
|
-
pub struct StuckListArgs {
|
|
560
|
-
pub workspace: PathBuf,
|
|
561
|
-
pub json: bool,
|
|
562
|
-
/// Stage 4: explicit `--team` scope (read-only enumeration).
|
|
563
|
-
pub team: Option<String>,
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/// `stuck-cancel`(`parser.py:429`)。`--alert-type` ∈ {stuck, idle_fallback, all},默认 stuck。
|
|
567
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
568
|
-
pub struct StuckCancelArgs {
|
|
569
|
-
pub agent: String,
|
|
570
|
-
pub workspace: PathBuf,
|
|
571
|
-
/// `None` 表 `all`(展开全集);`Some(AlertType)` 表单类型。
|
|
572
|
-
pub alert_type: Option<AlertType>,
|
|
573
|
-
pub json: bool,
|
|
574
|
-
/// Parsed but rejected until stuck suppression storage grows a team-scoped
|
|
575
|
-
/// backend selector.
|
|
576
|
-
pub team: Option<String>,
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/// `acknowledge-idle`(`parser.py:436`)。
|
|
580
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
581
|
-
pub struct AcknowledgeIdleArgs {
|
|
582
|
-
pub team: Option<String>,
|
|
583
|
-
pub workspace: PathBuf,
|
|
584
|
-
pub json: bool,
|
|
585
|
-
}
|
|
586
|
-
|
|
587
549
|
/// `doctor`(`parser.py:318`)。`--gate` ∈ {orphans, comms};`--fix` 须配 `--gate`(`cmd_doctor` 校验)。
|
|
588
550
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
589
551
|
pub struct DoctorArgs {
|
|
@@ -683,17 +645,6 @@ pub struct WaitArgs {
|
|
|
683
645
|
pub json: bool,
|
|
684
646
|
}
|
|
685
647
|
|
|
686
|
-
/// `diagnose` compatibility spelling for the canonical `doctor` health report.
|
|
687
|
-
/// The CLI normalizes it to `DoctorArgs`; this narrow type remains for direct
|
|
688
|
-
/// Rust callers that used the pre-unification API.
|
|
689
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
690
|
-
pub struct DiagnoseArgs {
|
|
691
|
-
pub workspace: PathBuf,
|
|
692
|
-
pub json: bool,
|
|
693
|
-
/// Stage 4: explicit `--team` scope (read-only health report).
|
|
694
|
-
pub team: Option<String>,
|
|
695
|
-
}
|
|
696
|
-
|
|
697
648
|
/// `preflight`(`parser.py:160`)。
|
|
698
649
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
699
650
|
pub struct PreflightArgs {
|
|
@@ -555,6 +555,9 @@ fn compile_role_agent_with_mode(
|
|
|
555
555
|
]),
|
|
556
556
|
),
|
|
557
557
|
];
|
|
558
|
+
if let Some(label) = string_field(&meta, "label") {
|
|
559
|
+
agent_items.insert(1, ("label", Value::Str(label)));
|
|
560
|
+
}
|
|
558
561
|
if let Some(profile) = string_field(&meta, "profile") {
|
|
559
562
|
agent_items.push(("profile", Value::Str(profile)));
|
|
560
563
|
}
|
|
@@ -1313,7 +1313,7 @@ fn apply_endpoint_convergence(
|
|
|
1313
1313
|
"persisted": false,
|
|
1314
1314
|
"checked_paths": convergence_checked_paths(team_id.as_str()),
|
|
1315
1315
|
"action": format!(
|
|
1316
|
-
"old tmux endpoint {old_endpoint} still has this team's session or pane tuple; run team-agent
|
|
1316
|
+
"old tmux endpoint {old_endpoint} still has this team's session or pane tuple; run team-agent doctor --json before retrying restart"
|
|
1317
1317
|
),
|
|
1318
1318
|
})),
|
|
1319
1319
|
false,
|
|
@@ -177,6 +177,216 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
177
177
|
"runtime state agents is not an object".to_string(),
|
|
178
178
|
));
|
|
179
179
|
};
|
|
180
|
+
let entry = starting_agent_entry(workspace, agent_id, meta, dynamic_role_file);
|
|
181
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
182
|
+
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
183
|
+
save_launched_team_state_for_key(
|
|
184
|
+
workspace,
|
|
185
|
+
&state,
|
|
186
|
+
Some(canonical_team_key),
|
|
187
|
+
Some(agent_id.as_str()),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// ---
|
|
192
|
+
/// purpose: 首次登记 Pi fork 席位,原子带入目标角色与完整 fork capture seed
|
|
193
|
+
/// returns: 成功后 TARGET 已按 ForkAgent typed intent 登记为 starting
|
|
194
|
+
/// errors: 重复席位、无效 capture seed 或 state 写入失败时拒绝
|
|
195
|
+
/// ---
|
|
196
|
+
pub(crate) fn fork_upsert_agent_state_from_role(
|
|
197
|
+
workspace: &Path,
|
|
198
|
+
canonical_team_key: &str,
|
|
199
|
+
agent_id: &AgentId,
|
|
200
|
+
meta: &Value,
|
|
201
|
+
compiled_agent: &Value,
|
|
202
|
+
dynamic_role_file: &Path,
|
|
203
|
+
seed: &ForkCaptureSeed,
|
|
204
|
+
) -> Result<(), LifecycleError> {
|
|
205
|
+
if agent_id == &seed.source_agent_id {
|
|
206
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
207
|
+
"fork target must differ from source".to_string(),
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
if seed.captured.captured_via != crate::provider::CaptureVia::ForkSnapshot
|
|
211
|
+
|| seed.captured.attribution_confidence != crate::provider::Confidence::High
|
|
212
|
+
|| seed.captured.session_id.is_none()
|
|
213
|
+
|| seed.captured.rollout_path.is_none()
|
|
214
|
+
|| seed.captured_at.is_empty()
|
|
215
|
+
{
|
|
216
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
217
|
+
"fork capture seed is incomplete or not a verified Pi snapshot".to_string(),
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
if compiled_agent.get("id").and_then(Value::as_str) != Some(agent_id.as_str()) {
|
|
221
|
+
return Err(LifecycleError::Compile(
|
|
222
|
+
"compiled fork role id does not match target".to_string(),
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
let sessions_root = std::fs::canonicalize(&seed.pi_sessions_root)
|
|
226
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
227
|
+
let backing = seed.captured.rollout_path.as_ref().ok_or_else(|| {
|
|
228
|
+
LifecycleError::RequirementUnmet("fork backing path is missing".to_string())
|
|
229
|
+
})?;
|
|
230
|
+
let backing_path = std::fs::canonicalize(&backing.0)
|
|
231
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
232
|
+
if !backing_path.starts_with(&sessions_root) {
|
|
233
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
234
|
+
"fork backing is outside the target Pi session root".to_string(),
|
|
235
|
+
));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let mut state =
|
|
239
|
+
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
240
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
241
|
+
if !state.is_object() {
|
|
242
|
+
state = serde_json::json!({});
|
|
243
|
+
}
|
|
244
|
+
let root = state.as_object_mut().ok_or_else(|| {
|
|
245
|
+
LifecycleError::StatePersist("runtime state root is not an object".to_string())
|
|
246
|
+
})?;
|
|
247
|
+
let agents = root
|
|
248
|
+
.entry("agents".to_string())
|
|
249
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
250
|
+
if !agents.is_object() {
|
|
251
|
+
*agents = serde_json::json!({});
|
|
252
|
+
}
|
|
253
|
+
let agent_map = agents.as_object_mut().ok_or_else(|| {
|
|
254
|
+
LifecycleError::StatePersist("runtime state agents is not an object".to_string())
|
|
255
|
+
})?;
|
|
256
|
+
if agent_map.contains_key(agent_id.as_str()) {
|
|
257
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
258
|
+
"agent id already exists in selected team: {}",
|
|
259
|
+
agent_id
|
|
260
|
+
)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let source_profile_dir = agent_map
|
|
264
|
+
.get(seed.source_agent_id.as_str())
|
|
265
|
+
.and_then(serde_json::Value::as_object)
|
|
266
|
+
.and_then(|source| source.get("_profile_dir"))
|
|
267
|
+
.and_then(serde_json::Value::as_str)
|
|
268
|
+
.filter(|path| !path.is_empty())
|
|
269
|
+
.map(|path| serde_json::json!(path));
|
|
270
|
+
let mut entry = starting_agent_entry(workspace, agent_id, meta, dynamic_role_file);
|
|
271
|
+
let entry_object = entry.as_object_mut().ok_or_else(|| {
|
|
272
|
+
LifecycleError::StatePersist("fork target entry is not an object".to_string())
|
|
273
|
+
})?;
|
|
274
|
+
for field in [
|
|
275
|
+
"provider",
|
|
276
|
+
"auth_mode",
|
|
277
|
+
"role",
|
|
278
|
+
"model",
|
|
279
|
+
"effort",
|
|
280
|
+
"profile",
|
|
281
|
+
"tools",
|
|
282
|
+
"system_prompt",
|
|
283
|
+
"output_contract",
|
|
284
|
+
"communication_mode",
|
|
285
|
+
"working_directory",
|
|
286
|
+
"cwd",
|
|
287
|
+
"config",
|
|
288
|
+
"name",
|
|
289
|
+
"dangerously_skip_permissions",
|
|
290
|
+
] {
|
|
291
|
+
if let Some(value) = compiled_agent.get(field) {
|
|
292
|
+
entry_object.insert(field.to_string(), yaml_value_to_json(value));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if compiled_agent.get("model").is_some() && !entry_object.contains_key("model_source") {
|
|
296
|
+
entry_object.insert("model_source".to_string(), serde_json::json!("team"));
|
|
297
|
+
}
|
|
298
|
+
entry_object.insert("agent_id".to_string(), serde_json::json!(agent_id.as_str()));
|
|
299
|
+
entry_object.insert("label".to_string(), serde_json::json!(agent_id.as_str()));
|
|
300
|
+
entry_object.insert(
|
|
301
|
+
"owner_team_id".to_string(),
|
|
302
|
+
serde_json::json!(canonical_team_key),
|
|
303
|
+
);
|
|
304
|
+
entry_object.insert(
|
|
305
|
+
"claude_projects_root".to_string(),
|
|
306
|
+
serde_json::json!(sessions_root.to_string_lossy().to_string()),
|
|
307
|
+
);
|
|
308
|
+
entry_object.insert(
|
|
309
|
+
"forked_from".to_string(),
|
|
310
|
+
serde_json::json!(seed.source_agent_id.as_str()),
|
|
311
|
+
);
|
|
312
|
+
entry_object.insert(
|
|
313
|
+
"spawn_cwd".to_string(),
|
|
314
|
+
serde_json::json!(seed.captured.spawn_cwd.to_string_lossy().to_string()),
|
|
315
|
+
);
|
|
316
|
+
entry_object.insert(
|
|
317
|
+
"session_id".to_string(),
|
|
318
|
+
serde_json::json!(seed.captured.session_id.as_ref().map(|id| id.as_str())),
|
|
319
|
+
);
|
|
320
|
+
let backing = seed.captured.rollout_path.as_ref().ok_or_else(|| {
|
|
321
|
+
LifecycleError::RequirementUnmet("fork backing path is missing".to_string())
|
|
322
|
+
})?;
|
|
323
|
+
entry_object.insert(
|
|
324
|
+
"rollout_path".to_string(),
|
|
325
|
+
serde_json::json!(backing.0.to_string_lossy().to_string()),
|
|
326
|
+
);
|
|
327
|
+
entry_object.insert(
|
|
328
|
+
"captured_at".to_string(),
|
|
329
|
+
serde_json::json!(seed.captured_at.as_str()),
|
|
330
|
+
);
|
|
331
|
+
entry_object.insert(
|
|
332
|
+
"captured_via".to_string(),
|
|
333
|
+
serde_json::to_value(seed.captured.captured_via)
|
|
334
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
335
|
+
);
|
|
336
|
+
entry_object.insert(
|
|
337
|
+
"attribution_confidence".to_string(),
|
|
338
|
+
serde_json::to_value(seed.captured.attribution_confidence)
|
|
339
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
340
|
+
);
|
|
341
|
+
entry_object.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
342
|
+
if let Some(profile_dir) = source_profile_dir {
|
|
343
|
+
entry_object.insert("_profile_dir".to_string(), profile_dir);
|
|
344
|
+
} else {
|
|
345
|
+
entry_object.insert(
|
|
346
|
+
"_profile_dir".to_string(),
|
|
347
|
+
serde_json::json!(seed.profile_dir.to_string_lossy().to_string()),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
351
|
+
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
352
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
353
|
+
.save(
|
|
354
|
+
crate::state::repository::StateWriteIntent::ForkAgent {
|
|
355
|
+
team_key: canonical_team_key,
|
|
356
|
+
agent_id: agent_id.as_str(),
|
|
357
|
+
},
|
|
358
|
+
&state,
|
|
359
|
+
)
|
|
360
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn yaml_value_to_json(value: &Value) -> serde_json::Value {
|
|
364
|
+
match value {
|
|
365
|
+
Value::Null => serde_json::Value::Null,
|
|
366
|
+
Value::Bool(value) => serde_json::json!(value),
|
|
367
|
+
Value::Int(value) => serde_json::json!(value),
|
|
368
|
+
Value::Float(value) => serde_json::Number::from_f64(*value)
|
|
369
|
+
.map(serde_json::Value::Number)
|
|
370
|
+
.unwrap_or(serde_json::Value::Null),
|
|
371
|
+
Value::Str(value) => serde_json::json!(value),
|
|
372
|
+
Value::List(values) => {
|
|
373
|
+
serde_json::Value::Array(values.iter().map(yaml_value_to_json).collect())
|
|
374
|
+
}
|
|
375
|
+
Value::Map(values) => serde_json::Value::Object(
|
|
376
|
+
values
|
|
377
|
+
.iter()
|
|
378
|
+
.map(|(key, value)| (key.clone(), yaml_value_to_json(value)))
|
|
379
|
+
.collect(),
|
|
380
|
+
),
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn starting_agent_entry(
|
|
385
|
+
workspace: &Path,
|
|
386
|
+
agent_id: &AgentId,
|
|
387
|
+
meta: &Value,
|
|
388
|
+
dynamic_role_file: &Path,
|
|
389
|
+
) -> serde_json::Value {
|
|
180
390
|
let provider = meta
|
|
181
391
|
.get("provider")
|
|
182
392
|
.and_then(Value::as_str)
|
|
@@ -189,10 +399,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
189
399
|
.get("role")
|
|
190
400
|
.and_then(Value::as_str)
|
|
191
401
|
.unwrap_or_else(|| agent_id.as_str());
|
|
192
|
-
// E42 (0.3.24 P0, double-spec deadlock): persist the initial state row as
|
|
193
|
-
// "starting" (not "running"). The caller (add_agent_with_transport_at_paths)
|
|
194
|
-
// promotes to "running" only after start_agent_at_paths returns Running.
|
|
195
|
-
// If the spawn fails, the rollback below removes the entry entirely.
|
|
196
402
|
let mut entry = serde_json::json!({
|
|
197
403
|
"provider": provider,
|
|
198
404
|
"auth_mode": auth_mode,
|
|
@@ -221,9 +427,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
221
427
|
}
|
|
222
428
|
}
|
|
223
429
|
}
|
|
224
|
-
// 0.4.x provider effort MVP step 8 (dynamic add-agent): persist effort
|
|
225
|
-
// from the role doc front matter (compiler.rs validates syntax/semantics
|
|
226
|
-
// at compile; add-agent path validates here too in case of direct YAML).
|
|
227
430
|
if let Some(effort_str) = meta.get("effort").and_then(Value::as_str) {
|
|
228
431
|
if !effort_str.is_empty() {
|
|
229
432
|
if let Some(obj) = entry.as_object_mut() {
|
|
@@ -232,7 +435,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
232
435
|
}
|
|
233
436
|
}
|
|
234
437
|
if let Some(obj) = entry.as_object_mut() {
|
|
235
|
-
// 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
|
|
236
438
|
let meta_provider = meta
|
|
237
439
|
.get("provider")
|
|
238
440
|
.and_then(Value::as_str)
|
|
@@ -240,14 +442,7 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
240
442
|
.unwrap_or(Provider::Codex);
|
|
241
443
|
persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
|
|
242
444
|
}
|
|
243
|
-
|
|
244
|
-
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
245
|
-
save_launched_team_state_for_key(
|
|
246
|
-
workspace,
|
|
247
|
-
&state,
|
|
248
|
-
Some(canonical_team_key),
|
|
249
|
-
Some(agent_id.as_str()),
|
|
250
|
-
)
|
|
445
|
+
entry
|
|
251
446
|
}
|
|
252
447
|
|
|
253
448
|
/// ---
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
//! ---
|
|
2
|
-
//! purpose:
|
|
2
|
+
//! purpose: Pi 以完整会话快照创建独立席位;已验证的其他 provider 保持窗口内分身
|
|
3
3
|
//! contract:
|
|
4
4
|
//! provides:
|
|
5
5
|
//! - name: fork_agent_with_transport
|
|
6
|
-
//! what:
|
|
6
|
+
//! what: Pi 走锁内快照新席位;已验证的其他 provider 保持窗口内斜杠 fork
|
|
7
7
|
//! - name: in_window_fork
|
|
8
8
|
//! what: 已验证的 provider+subscription 给出 command+screen_mark;未验证返回 None
|
|
9
9
|
//! depends:
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
//! - crate::lifecycle::profile_launch
|
|
13
13
|
//! - crate::transport::Transport
|
|
14
14
|
//! boundary:
|
|
15
|
-
//! -
|
|
16
|
-
//! -
|
|
15
|
+
//! - 仅 Pi 的锁内专用管道读取 exact backing、注册并启动新席位
|
|
16
|
+
//! - 非 Pi 不 spawn 新 pane,不改 TEAM_AGENT_ID / MCP / 席位名
|
|
17
17
|
//! - 重试只重按回车,不重粘 /fork
|
|
18
18
|
//! - 不把 pane 锁超时从 200ms 调大
|
|
19
19
|
//! - 未验证的 provider 不猜斜杠命令
|
|
@@ -99,7 +99,7 @@ pub fn fork_agent_with_transport(
|
|
|
99
99
|
workspace: &Path,
|
|
100
100
|
source_agent_id: &AgentId,
|
|
101
101
|
as_agent_id: &AgentId,
|
|
102
|
-
|
|
102
|
+
label: Option<&str>,
|
|
103
103
|
_open_display: bool,
|
|
104
104
|
team: Option<&str>,
|
|
105
105
|
transport: &dyn Transport,
|
|
@@ -142,6 +142,15 @@ pub fn fork_agent_with_transport(
|
|
|
142
142
|
.unwrap_or("subscription");
|
|
143
143
|
let provider = parse_provider(provider_raw).unwrap_or(Provider::Grok);
|
|
144
144
|
let auth = parse_auth_mode(auth_raw).unwrap_or(AuthMode::Subscription);
|
|
145
|
+
if provider == Provider::Pi {
|
|
146
|
+
return crate::lifecycle::restart::fork_pi_new_seat_locked(
|
|
147
|
+
&selected,
|
|
148
|
+
source_agent_id,
|
|
149
|
+
as_agent_id,
|
|
150
|
+
label,
|
|
151
|
+
transport,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
145
154
|
let Some(spec) = in_window_fork(provider, auth) else {
|
|
146
155
|
return Err(refuse_missing_in_window_fork(provider, provider_raw));
|
|
147
156
|
};
|
|
@@ -201,6 +210,7 @@ pub fn fork_agent_with_transport(
|
|
|
201
210
|
},
|
|
202
211
|
session_id: None,
|
|
203
212
|
backing_state: ForkBackingState::Verified,
|
|
213
|
+
pi_fork: None,
|
|
204
214
|
})
|
|
205
215
|
}
|
|
206
216
|
|