@team-agent/installer 0.5.60 → 0.5.61
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 +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/session/capture.rs +40 -18
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
- package/crates/team-agent/src/provider/session_scan.rs +5 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
//! Shared typed catalog for ambient tmux pane authority refusals.
|
|
2
|
+
//!
|
|
3
|
+
//! This module owns identities and payload shape only. Launchers and CLI
|
|
4
|
+
//! presenters remain responsible for collecting facts and rendering them.
|
|
5
|
+
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
|
|
8
|
+
pub const REASON_FIELD: &str = "reason";
|
|
9
|
+
pub const AVAILABILITY_FIELD: &str = "availability";
|
|
10
|
+
pub const CAUSE_FIELD: &str = "cause";
|
|
11
|
+
pub const ACTION_REQUIRED_FIELD: &str = "action_required";
|
|
12
|
+
pub const HINT_ACTION_FIELD: &str = "hint_action";
|
|
13
|
+
pub const ACTION_FIELD: &str = "action";
|
|
14
|
+
|
|
15
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
16
|
+
pub enum PaneAuthorityRefusalField {
|
|
17
|
+
RequestedWorkspace,
|
|
18
|
+
ObservedPaneId,
|
|
19
|
+
ObservedPaneWorkspace,
|
|
20
|
+
Endpoint,
|
|
21
|
+
CallerControllingTty,
|
|
22
|
+
ObservedPaneTty,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl PaneAuthorityRefusalField {
|
|
26
|
+
pub const fn as_str(self) -> &'static str {
|
|
27
|
+
match self {
|
|
28
|
+
Self::RequestedWorkspace => "requested_workspace",
|
|
29
|
+
Self::ObservedPaneId => "observed_pane_id",
|
|
30
|
+
Self::ObservedPaneWorkspace => "observed_pane_workspace",
|
|
31
|
+
Self::Endpoint => "endpoint",
|
|
32
|
+
Self::CallerControllingTty => "caller_controlling_tty",
|
|
33
|
+
Self::ObservedPaneTty => "observed_pane_tty",
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
39
|
+
pub enum PaneAuthorityRefusalReason {
|
|
40
|
+
AmbientTmuxEndpointUnavailable,
|
|
41
|
+
AmbientPaneIdUnavailable,
|
|
42
|
+
AmbientPaneWorkspaceUnavailable,
|
|
43
|
+
CallerControllingTtyUnavailable,
|
|
44
|
+
ObservedPaneTtyUnavailable,
|
|
45
|
+
PaneTtyMismatch,
|
|
46
|
+
PaneWorkspaceMismatch,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl PaneAuthorityRefusalReason {
|
|
50
|
+
pub const ALL: [Self; 7] = [
|
|
51
|
+
Self::AmbientTmuxEndpointUnavailable,
|
|
52
|
+
Self::AmbientPaneIdUnavailable,
|
|
53
|
+
Self::AmbientPaneWorkspaceUnavailable,
|
|
54
|
+
Self::CallerControllingTtyUnavailable,
|
|
55
|
+
Self::ObservedPaneTtyUnavailable,
|
|
56
|
+
Self::PaneTtyMismatch,
|
|
57
|
+
Self::PaneWorkspaceMismatch,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
pub const fn as_str(self) -> &'static str {
|
|
61
|
+
match self {
|
|
62
|
+
Self::AmbientTmuxEndpointUnavailable => "AmbientTmuxEndpointUnavailable",
|
|
63
|
+
Self::AmbientPaneIdUnavailable => "AmbientPaneIdUnavailable",
|
|
64
|
+
Self::AmbientPaneWorkspaceUnavailable => "AmbientPaneWorkspaceUnavailable",
|
|
65
|
+
Self::CallerControllingTtyUnavailable => "CallerControllingTtyUnavailable",
|
|
66
|
+
Self::ObservedPaneTtyUnavailable => "ObservedPaneTtyUnavailable",
|
|
67
|
+
Self::PaneTtyMismatch => "PaneTtyMismatch",
|
|
68
|
+
Self::PaneWorkspaceMismatch => "PaneWorkspaceMismatch",
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub const fn required_fact_fields(self) -> &'static [PaneAuthorityRefusalField] {
|
|
73
|
+
use PaneAuthorityRefusalField::{
|
|
74
|
+
CallerControllingTty, Endpoint, ObservedPaneId, ObservedPaneTty, ObservedPaneWorkspace,
|
|
75
|
+
RequestedWorkspace,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
match self {
|
|
79
|
+
Self::AmbientTmuxEndpointUnavailable => &[RequestedWorkspace, Endpoint],
|
|
80
|
+
Self::AmbientPaneIdUnavailable => &[RequestedWorkspace, ObservedPaneId, Endpoint],
|
|
81
|
+
Self::AmbientPaneWorkspaceUnavailable => &[
|
|
82
|
+
RequestedWorkspace,
|
|
83
|
+
ObservedPaneId,
|
|
84
|
+
ObservedPaneWorkspace,
|
|
85
|
+
Endpoint,
|
|
86
|
+
],
|
|
87
|
+
Self::CallerControllingTtyUnavailable => &[
|
|
88
|
+
RequestedWorkspace,
|
|
89
|
+
ObservedPaneId,
|
|
90
|
+
Endpoint,
|
|
91
|
+
CallerControllingTty,
|
|
92
|
+
],
|
|
93
|
+
Self::ObservedPaneTtyUnavailable => &[
|
|
94
|
+
RequestedWorkspace,
|
|
95
|
+
ObservedPaneId,
|
|
96
|
+
Endpoint,
|
|
97
|
+
ObservedPaneTty,
|
|
98
|
+
],
|
|
99
|
+
Self::PaneTtyMismatch => &[
|
|
100
|
+
RequestedWorkspace,
|
|
101
|
+
ObservedPaneId,
|
|
102
|
+
Endpoint,
|
|
103
|
+
CallerControllingTty,
|
|
104
|
+
ObservedPaneTty,
|
|
105
|
+
],
|
|
106
|
+
Self::PaneWorkspaceMismatch => &[
|
|
107
|
+
RequestedWorkspace,
|
|
108
|
+
ObservedPaneId,
|
|
109
|
+
ObservedPaneWorkspace,
|
|
110
|
+
Endpoint,
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
117
|
+
pub enum PaneAuthorityFactAvailability {
|
|
118
|
+
Unavailable,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
impl PaneAuthorityFactAvailability {
|
|
122
|
+
pub const fn as_str(self) -> &'static str {
|
|
123
|
+
match self {
|
|
124
|
+
Self::Unavailable => "unavailable",
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
130
|
+
pub struct UnavailablePaneAuthorityFact<C> {
|
|
131
|
+
pub cause: C,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl<C> UnavailablePaneAuthorityFact<C> {
|
|
135
|
+
pub const fn new(cause: C) -> Self {
|
|
136
|
+
Self { cause }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pub const fn availability(&self) -> PaneAuthorityFactAvailability {
|
|
140
|
+
PaneAuthorityFactAvailability::Unavailable
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
macro_rules! unavailable_causes {
|
|
145
|
+
($name:ident { $($variant:ident => $wire:literal),+ $(,)? }) => {
|
|
146
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
147
|
+
pub enum $name {
|
|
148
|
+
$($variant),+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
impl $name {
|
|
152
|
+
pub const fn as_str(self) -> &'static str {
|
|
153
|
+
match self {
|
|
154
|
+
$(Self::$variant => $wire),+
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
unavailable_causes!(AmbientTmuxEndpointUnavailableCause {
|
|
162
|
+
TmuxValueNotUnicode => "tmux_value_not_unicode",
|
|
163
|
+
TmuxTupleFieldCountInvalid => "tmux_tuple_field_count_invalid",
|
|
164
|
+
EndpointMissing => "endpoint_missing",
|
|
165
|
+
EndpointNotAbsolute => "endpoint_not_absolute",
|
|
166
|
+
ServerPidInvalid => "server_pid_invalid",
|
|
167
|
+
SessionIndexInvalid => "session_index_invalid",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
unavailable_causes!(AmbientPaneIdUnavailableCause {
|
|
171
|
+
EnvironmentValueMissing => "environment_value_missing",
|
|
172
|
+
EnvironmentValueEmpty => "environment_value_empty",
|
|
173
|
+
EnvironmentValueNotUnicode => "environment_value_not_unicode",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
unavailable_causes!(AmbientPaneWorkspaceUnavailableCause {
|
|
177
|
+
PaneQueryFailed => "pane_query_failed",
|
|
178
|
+
PaneNotFound => "pane_not_found",
|
|
179
|
+
CurrentPathMissing => "current_path_missing",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
unavailable_causes!(CallerControllingTtyUnavailableCause {
|
|
183
|
+
NoControllingTty => "no_controlling_tty",
|
|
184
|
+
DeviceIdentityUnresolvable => "device_identity_unresolvable",
|
|
185
|
+
PlatformUnsupported => "platform_unsupported",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
unavailable_causes!(ObservedPaneTtyUnavailableCause {
|
|
189
|
+
PaneTtyMissing => "pane_tty_missing",
|
|
190
|
+
PaneTtyUnresolvable => "pane_tty_unresolvable",
|
|
191
|
+
PaneTtyNotCharacterDevice => "pane_tty_not_character_device",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
195
|
+
pub struct AmbientTmuxEndpointUnavailableFacts {
|
|
196
|
+
pub requested_workspace: PathBuf,
|
|
197
|
+
pub endpoint: UnavailablePaneAuthorityFact<AmbientTmuxEndpointUnavailableCause>,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
201
|
+
pub struct AmbientPaneIdUnavailableFacts {
|
|
202
|
+
pub requested_workspace: PathBuf,
|
|
203
|
+
pub observed_pane_id: UnavailablePaneAuthorityFact<AmbientPaneIdUnavailableCause>,
|
|
204
|
+
pub endpoint: String,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
208
|
+
pub struct AmbientPaneWorkspaceUnavailableFacts {
|
|
209
|
+
pub requested_workspace: PathBuf,
|
|
210
|
+
pub observed_pane_id: String,
|
|
211
|
+
pub observed_pane_workspace: UnavailablePaneAuthorityFact<AmbientPaneWorkspaceUnavailableCause>,
|
|
212
|
+
pub endpoint: String,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
216
|
+
pub struct CallerControllingTtyUnavailableFacts {
|
|
217
|
+
pub requested_workspace: PathBuf,
|
|
218
|
+
pub observed_pane_id: String,
|
|
219
|
+
pub endpoint: String,
|
|
220
|
+
pub caller_controlling_tty: UnavailablePaneAuthorityFact<CallerControllingTtyUnavailableCause>,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
224
|
+
pub struct ObservedPaneTtyUnavailableFacts {
|
|
225
|
+
pub requested_workspace: PathBuf,
|
|
226
|
+
pub observed_pane_id: String,
|
|
227
|
+
pub endpoint: String,
|
|
228
|
+
pub observed_pane_tty: UnavailablePaneAuthorityFact<ObservedPaneTtyUnavailableCause>,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
232
|
+
pub struct PaneTtyMismatchFacts {
|
|
233
|
+
pub requested_workspace: PathBuf,
|
|
234
|
+
pub observed_pane_id: String,
|
|
235
|
+
pub endpoint: String,
|
|
236
|
+
pub caller_controlling_tty: u64,
|
|
237
|
+
pub observed_pane_tty: u64,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
241
|
+
pub struct PaneWorkspaceMismatchFacts {
|
|
242
|
+
pub requested_workspace: PathBuf,
|
|
243
|
+
pub observed_pane_id: String,
|
|
244
|
+
pub observed_pane_workspace: PathBuf,
|
|
245
|
+
pub endpoint: String,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
249
|
+
pub enum PaneAuthorityRefusalFacts {
|
|
250
|
+
AmbientTmuxEndpointUnavailable(AmbientTmuxEndpointUnavailableFacts),
|
|
251
|
+
AmbientPaneIdUnavailable(AmbientPaneIdUnavailableFacts),
|
|
252
|
+
AmbientPaneWorkspaceUnavailable(AmbientPaneWorkspaceUnavailableFacts),
|
|
253
|
+
CallerControllingTtyUnavailable(CallerControllingTtyUnavailableFacts),
|
|
254
|
+
ObservedPaneTtyUnavailable(ObservedPaneTtyUnavailableFacts),
|
|
255
|
+
PaneTtyMismatch(PaneTtyMismatchFacts),
|
|
256
|
+
PaneWorkspaceMismatch(PaneWorkspaceMismatchFacts),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
impl PaneAuthorityRefusalFacts {
|
|
260
|
+
pub const fn reason(&self) -> PaneAuthorityRefusalReason {
|
|
261
|
+
match self {
|
|
262
|
+
Self::AmbientTmuxEndpointUnavailable(_) => {
|
|
263
|
+
PaneAuthorityRefusalReason::AmbientTmuxEndpointUnavailable
|
|
264
|
+
}
|
|
265
|
+
Self::AmbientPaneIdUnavailable(_) => {
|
|
266
|
+
PaneAuthorityRefusalReason::AmbientPaneIdUnavailable
|
|
267
|
+
}
|
|
268
|
+
Self::AmbientPaneWorkspaceUnavailable(_) => {
|
|
269
|
+
PaneAuthorityRefusalReason::AmbientPaneWorkspaceUnavailable
|
|
270
|
+
}
|
|
271
|
+
Self::CallerControllingTtyUnavailable(_) => {
|
|
272
|
+
PaneAuthorityRefusalReason::CallerControllingTtyUnavailable
|
|
273
|
+
}
|
|
274
|
+
Self::ObservedPaneTtyUnavailable(_) => {
|
|
275
|
+
PaneAuthorityRefusalReason::ObservedPaneTtyUnavailable
|
|
276
|
+
}
|
|
277
|
+
Self::PaneTtyMismatch(_) => PaneAuthorityRefusalReason::PaneTtyMismatch,
|
|
278
|
+
Self::PaneWorkspaceMismatch(_) => PaneAuthorityRefusalReason::PaneWorkspaceMismatch,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
284
|
+
pub enum PaneAuthorityRecoveryHint {
|
|
285
|
+
AttachLeader,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
289
|
+
pub enum PaneAuthorityRecoveryAction {
|
|
290
|
+
OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
294
|
+
pub struct PaneAuthorityRecovery {
|
|
295
|
+
pub action_required: bool,
|
|
296
|
+
pub hint_action: PaneAuthorityRecoveryHint,
|
|
297
|
+
pub action: PaneAuthorityRecoveryAction,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
impl PaneAuthorityRecovery {
|
|
301
|
+
pub const REQUIRED: Self = Self {
|
|
302
|
+
action_required: true,
|
|
303
|
+
hint_action: PaneAuthorityRecoveryHint::AttachLeader,
|
|
304
|
+
action:
|
|
305
|
+
PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
310
|
+
pub struct PaneAuthorityRefusal {
|
|
311
|
+
pub facts: PaneAuthorityRefusalFacts,
|
|
312
|
+
pub recovery: PaneAuthorityRecovery,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
impl PaneAuthorityRefusal {
|
|
316
|
+
pub const fn new(facts: PaneAuthorityRefusalFacts) -> Self {
|
|
317
|
+
Self {
|
|
318
|
+
facts,
|
|
319
|
+
recovery: PaneAuthorityRecovery::REQUIRED,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
pub const fn reason(&self) -> PaneAuthorityRefusalReason {
|
|
324
|
+
self.facts.reason()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[cfg(test)]
|
|
329
|
+
mod tests {
|
|
330
|
+
use super::{PaneAuthorityRefusalField as Field, PaneAuthorityRefusalReason as Reason};
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn reason_catalog_is_closed_and_mismatch_requires_all_four_workspace_facts() {
|
|
334
|
+
assert_eq!(Reason::ALL.len(), 7);
|
|
335
|
+
assert_eq!(
|
|
336
|
+
Reason::PaneWorkspaceMismatch.required_fact_fields(),
|
|
337
|
+
&[
|
|
338
|
+
Field::RequestedWorkspace,
|
|
339
|
+
Field::ObservedPaneId,
|
|
340
|
+
Field::ObservedPaneWorkspace,
|
|
341
|
+
Field::Endpoint,
|
|
342
|
+
]
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[test]
|
|
347
|
+
fn unavailable_reasons_require_the_fact_they_could_not_observe() {
|
|
348
|
+
assert!(Reason::AmbientTmuxEndpointUnavailable
|
|
349
|
+
.required_fact_fields()
|
|
350
|
+
.contains(&Field::Endpoint));
|
|
351
|
+
assert!(Reason::AmbientPaneIdUnavailable
|
|
352
|
+
.required_fact_fields()
|
|
353
|
+
.contains(&Field::ObservedPaneId));
|
|
354
|
+
assert!(Reason::AmbientPaneWorkspaceUnavailable
|
|
355
|
+
.required_fact_fields()
|
|
356
|
+
.contains(&Field::ObservedPaneWorkspace));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -400,6 +400,7 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
400
400
|
"preferred_for",
|
|
401
401
|
"avoid_for",
|
|
402
402
|
"output_contract",
|
|
403
|
+
"communication_mode",
|
|
403
404
|
"paused",
|
|
404
405
|
"auth_mode",
|
|
405
406
|
"profile",
|
|
@@ -412,6 +413,21 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
412
413
|
if !agent.is_map() {
|
|
413
414
|
return;
|
|
414
415
|
}
|
|
416
|
+
if let Some(value) = agent.get("communication_mode") {
|
|
417
|
+
match value.as_str() {
|
|
418
|
+
Some(raw) if crate::communication_mode::CommunicationMode::parse(raw).is_none() => {
|
|
419
|
+
errors.push(format!(
|
|
420
|
+
"{path}/communication_mode: unknown communication_mode '{raw}'"
|
|
421
|
+
));
|
|
422
|
+
}
|
|
423
|
+
None => {
|
|
424
|
+
errors.push(format!(
|
|
425
|
+
"{path}/communication_mode: unknown communication_mode"
|
|
426
|
+
));
|
|
427
|
+
}
|
|
428
|
+
Some(_) => {}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
415
431
|
// 0.4.x provider effort MVP step 3: effort syntactic + semantic validation.
|
|
416
432
|
if let Some(raw) = agent.get("effort").and_then(Yaml::as_str) {
|
|
417
433
|
match crate::model::enums::ProviderEffort::parse(raw) {
|
|
@@ -284,12 +284,22 @@ where
|
|
|
284
284
|
.zip(candidate.captured.session_id.as_ref())
|
|
285
285
|
.is_some_and(|(expected, captured)| expected.as_str() != captured.as_str());
|
|
286
286
|
if mismatch {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
287
|
+
let ambiguity_changed = agent_obj
|
|
288
|
+
.get("attribution_ambiguous")
|
|
289
|
+
.and_then(Value::as_bool)
|
|
290
|
+
!= Some(true);
|
|
291
|
+
if !finalize_ambiguous || ambiguity_changed {
|
|
292
|
+
report.ambiguous.push(AmbiguousSessionCapture {
|
|
293
|
+
agent_id: item.agent_id.clone(),
|
|
294
|
+
spawn_cwd: item.context.spawn_cwd.to_string_lossy().to_string(),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
291
297
|
if finalize_ambiguous {
|
|
292
|
-
|
|
298
|
+
if ambiguity_changed {
|
|
299
|
+
agent_obj
|
|
300
|
+
.insert("attribution_ambiguous".to_string(), serde_json::json!(true));
|
|
301
|
+
report.changed = true;
|
|
302
|
+
}
|
|
293
303
|
// 0.4.6 tuple-atomic contract (audit:93): do NOT write
|
|
294
304
|
// `captured_at` for ambiguity diagnostics. `captured_at`
|
|
295
305
|
// is part of the authoritative tuple and may only be
|
|
@@ -299,7 +309,6 @@ where
|
|
|
299
309
|
// "looks captured" rows. Ambiguity diagnostics live in
|
|
300
310
|
// the `attribution_ambiguous` flag + events; persist
|
|
301
311
|
// event log for the timestamp.
|
|
302
|
-
report.changed = true;
|
|
303
312
|
}
|
|
304
313
|
continue;
|
|
305
314
|
}
|
|
@@ -317,19 +326,29 @@ where
|
|
|
317
326
|
continue;
|
|
318
327
|
}
|
|
319
328
|
if ambiguous_ids.contains(&item.agent_id) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
329
|
+
let ambiguity_changed = agent_obj
|
|
330
|
+
.get("attribution_ambiguous")
|
|
331
|
+
.and_then(Value::as_bool)
|
|
332
|
+
!= Some(true)
|
|
333
|
+
|| agent_obj.get("capture_state").and_then(Value::as_str)
|
|
334
|
+
!= Some("attribution_ambiguous");
|
|
335
|
+
if !finalize_ambiguous || ambiguity_changed {
|
|
336
|
+
report.ambiguous.push(AmbiguousSessionCapture {
|
|
337
|
+
agent_id: item.agent_id.clone(),
|
|
338
|
+
spawn_cwd: item.context.spawn_cwd.to_string_lossy().to_string(),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
324
341
|
if finalize_ambiguous {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
342
|
+
if ambiguity_changed {
|
|
343
|
+
agent_obj.insert("attribution_ambiguous".to_string(), serde_json::json!(true));
|
|
344
|
+
agent_obj.insert(
|
|
345
|
+
"capture_state".to_string(),
|
|
346
|
+
serde_json::json!("attribution_ambiguous"),
|
|
347
|
+
);
|
|
348
|
+
// 0.4.6 tuple-atomic contract: see comment above. Removed
|
|
349
|
+
// `captured_at` ambiguity write.
|
|
350
|
+
report.changed = true;
|
|
351
|
+
}
|
|
333
352
|
}
|
|
334
353
|
continue;
|
|
335
354
|
}
|
|
@@ -666,6 +685,9 @@ fn pending_session_capture<F>(
|
|
|
666
685
|
where
|
|
667
686
|
F: FnMut(Provider) -> Box<dyn ProviderAdapter>,
|
|
668
687
|
{
|
|
688
|
+
if agent.get("capture_state").and_then(Value::as_str) == Some("attribution_ambiguous") {
|
|
689
|
+
return None;
|
|
690
|
+
}
|
|
669
691
|
let provider = incomplete_resumable_provider(agent, adapter_for)?;
|
|
670
692
|
let spawn_cwd = agent
|
|
671
693
|
.get("spawn_cwd")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use std::path::{Path, PathBuf};
|
|
2
2
|
|
|
3
|
-
use crate::provider::
|
|
3
|
+
use crate::provider::helpers::find_session_id;
|
|
4
|
+
use crate::provider::types::{CaptureVia, CapturedSession, Confidence, RolloutPath, SessionId};
|
|
4
5
|
use crate::provider::Provider;
|
|
5
6
|
|
|
6
7
|
use super::{CaptureSessionContext, CapturedSessionCandidate};
|
|
@@ -14,7 +15,7 @@ pub(crate) fn projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<Path
|
|
|
14
15
|
Some(home.join(".claude").join("projects").join(encoded))
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
fn encode_projects_dir(path: &str) -> String {
|
|
18
|
+
pub(super) fn encode_projects_dir(path: &str) -> String {
|
|
18
19
|
let mut out = String::with_capacity(path.len());
|
|
19
20
|
for c in path.chars() {
|
|
20
21
|
if c.is_ascii_alphanumeric() {
|
|
@@ -26,6 +27,65 @@ fn encode_projects_dir(path: &str) -> String {
|
|
|
26
27
|
out
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
pub(super) fn scan_expected_session(
|
|
31
|
+
context: &CaptureSessionContext,
|
|
32
|
+
) -> Vec<CapturedSessionCandidate> {
|
|
33
|
+
let Some(expected) = context.expected_session_id.as_ref() else {
|
|
34
|
+
return Vec::new();
|
|
35
|
+
};
|
|
36
|
+
let Some(projects_root) = context.provider_projects_root.clone().or_else(|| {
|
|
37
|
+
std::env::var_os("HOME")
|
|
38
|
+
.map(PathBuf::from)
|
|
39
|
+
.map(|home| home.join(".claude").join("projects"))
|
|
40
|
+
}) else {
|
|
41
|
+
return Vec::new();
|
|
42
|
+
};
|
|
43
|
+
let canonical =
|
|
44
|
+
std::fs::canonicalize(&context.spawn_cwd).unwrap_or_else(|_| context.spawn_cwd.clone());
|
|
45
|
+
let encoded = encode_projects_dir(&canonical.to_string_lossy());
|
|
46
|
+
if encoded.is_empty() {
|
|
47
|
+
return Vec::new();
|
|
48
|
+
}
|
|
49
|
+
let path = projects_root
|
|
50
|
+
.join(encoded)
|
|
51
|
+
.join(format!("{}.jsonl", expected.as_str()));
|
|
52
|
+
let Ok(text) = super::common::read_head_text(&path, super::common::CAPTURE_HEAD_BYTES) else {
|
|
53
|
+
return Vec::new();
|
|
54
|
+
};
|
|
55
|
+
let records = super::common::parse_session_records(&text);
|
|
56
|
+
let session_matches = records
|
|
57
|
+
.iter()
|
|
58
|
+
.find_map(find_session_id)
|
|
59
|
+
.is_some_and(|session_id| session_id == expected.as_str());
|
|
60
|
+
let has_lifecycle_record = records.iter().any(|record| {
|
|
61
|
+
matches!(
|
|
62
|
+
record.get("type").and_then(serde_json::Value::as_str),
|
|
63
|
+
Some("user" | "assistant")
|
|
64
|
+
)
|
|
65
|
+
});
|
|
66
|
+
if !session_matches
|
|
67
|
+
|| !has_lifecycle_record
|
|
68
|
+
|| records_have_leader_marker(&records)
|
|
69
|
+
|| !records.iter().any(has_cwd_field)
|
|
70
|
+
{
|
|
71
|
+
return Vec::new();
|
|
72
|
+
}
|
|
73
|
+
let embedded_agent_id = super::common::embedded_team_agent_worker_id_from_text(&text);
|
|
74
|
+
let positive_agent_id_match = embedded_agent_id.as_deref() == Some(context.agent_id.as_str());
|
|
75
|
+
vec![CapturedSessionCandidate {
|
|
76
|
+
captured: CapturedSession {
|
|
77
|
+
session_id: Some(expected.clone()),
|
|
78
|
+
rollout_path: Some(RolloutPath::new(path)),
|
|
79
|
+
captured_via: CaptureVia::FsWatch,
|
|
80
|
+
attribution_confidence: Confidence::High,
|
|
81
|
+
spawn_cwd: context.spawn_cwd.clone(),
|
|
82
|
+
},
|
|
83
|
+
embedded_agent_id,
|
|
84
|
+
positive_agent_id_match,
|
|
85
|
+
agent_path_match: false,
|
|
86
|
+
}]
|
|
87
|
+
}
|
|
88
|
+
|
|
29
89
|
pub(crate) fn rollout_path_has_leader_marker(provider: Provider, rollout_path: &Path) -> bool {
|
|
30
90
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
31
91
|
return false;
|
|
@@ -112,6 +172,7 @@ mod tests {
|
|
|
112
172
|
std::fs::create_dir_all(dir).unwrap();
|
|
113
173
|
let path = dir.join(format!("{uuid}.jsonl"));
|
|
114
174
|
let line = serde_json::json!({
|
|
175
|
+
"type": "user",
|
|
115
176
|
"sessionId": uuid,
|
|
116
177
|
"cwd": cwd.to_string_lossy(),
|
|
117
178
|
});
|
|
@@ -119,6 +180,11 @@ mod tests {
|
|
|
119
180
|
path
|
|
120
181
|
}
|
|
121
182
|
|
|
183
|
+
fn expected_transcript_dir(projects_root: &Path, cwd: &Path) -> PathBuf {
|
|
184
|
+
let canonical = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
|
|
185
|
+
projects_root.join(encode_projects_dir(&canonical.to_string_lossy()))
|
|
186
|
+
}
|
|
187
|
+
|
|
122
188
|
#[test]
|
|
123
189
|
fn claude_leader_marker_in_custom_title_is_detected() {
|
|
124
190
|
let records = vec![serde_json::json!({
|
|
@@ -205,7 +271,11 @@ mod tests {
|
|
|
205
271
|
std::fs::create_dir_all(&cwd).unwrap();
|
|
206
272
|
let proj = base.join("projects");
|
|
207
273
|
write_transcript(&proj, "11111111-1111-4111-8111-111111111111", &cwd);
|
|
208
|
-
write_transcript(
|
|
274
|
+
let expected = write_transcript(
|
|
275
|
+
&expected_transcript_dir(&proj, &cwd),
|
|
276
|
+
"22222222-2222-4222-8222-222222222222",
|
|
277
|
+
&cwd,
|
|
278
|
+
);
|
|
209
279
|
let ctx = CaptureSessionContext {
|
|
210
280
|
agent_id: "w1".to_string(),
|
|
211
281
|
spawn_cwd: cwd.clone(),
|
|
@@ -221,6 +291,10 @@ mod tests {
|
|
|
221
291
|
out[0].captured.session_id.as_ref().unwrap().as_str(),
|
|
222
292
|
"22222222-2222-4222-8222-222222222222"
|
|
223
293
|
);
|
|
294
|
+
assert_eq!(
|
|
295
|
+
out[0].captured.rollout_path.as_ref().unwrap().as_path(),
|
|
296
|
+
expected
|
|
297
|
+
);
|
|
224
298
|
let _ = std::fs::remove_dir_all(&base);
|
|
225
299
|
}
|
|
226
300
|
|
|
@@ -284,6 +358,21 @@ mod tests {
|
|
|
284
358
|
let proj = base.join("projects");
|
|
285
359
|
let leader = write_transcript(&proj, "11111111-1111-4111-8111-111111111111", &cwd);
|
|
286
360
|
let stale = write_transcript(&proj, "22222222-2222-4222-8222-222222222222", &cwd);
|
|
361
|
+
let addressed =
|
|
362
|
+
expected_transcript_dir(&proj, &cwd).join("99999999-9999-4999-8999-999999999999.jsonl");
|
|
363
|
+
std::fs::create_dir_all(addressed.parent().unwrap()).unwrap();
|
|
364
|
+
std::fs::write(
|
|
365
|
+
&addressed,
|
|
366
|
+
format!(
|
|
367
|
+
"{}\n",
|
|
368
|
+
serde_json::json!({
|
|
369
|
+
"type": "user",
|
|
370
|
+
"sessionId": "22222222-2222-4222-8222-222222222222",
|
|
371
|
+
"cwd": cwd.to_string_lossy(),
|
|
372
|
+
})
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
.unwrap();
|
|
287
376
|
let _ = (leader, stale);
|
|
288
377
|
let ctx = CaptureSessionContext {
|
|
289
378
|
agent_id: "claude-worker".to_string(),
|