@team-agent/installer 0.5.59 → 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/coordinator/tick.rs +21 -8
- 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/completion.rs +58 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.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/tests/lane_ops.rs +35 -1
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- 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/adapter.rs +57 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
- package/crates/team-agent/src/provider/session/capture.rs +69 -34
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
- package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
- package/crates/team-agent/src/provider/session/mod.rs +3 -2
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
|
@@ -99,6 +99,141 @@ fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
|
|
|
99
99
|
assert!(case.events().contains("leader_receiver.mailbox_requeued"));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
#[test]
|
|
103
|
+
fn leader_submit_without_receipt_source_cannot_park_forever_without_a_completion_writer() {
|
|
104
|
+
let case = Case::new_without_rollout("missing-receipt-source");
|
|
105
|
+
let transport = RecordingTransport::new("");
|
|
106
|
+
let message_id = case.seed_message("missing receipt source canary");
|
|
107
|
+
|
|
108
|
+
let first = deliver_pending_message(
|
|
109
|
+
&case.workspace,
|
|
110
|
+
&case.store,
|
|
111
|
+
&transport,
|
|
112
|
+
&message_id,
|
|
113
|
+
&case.event_log,
|
|
114
|
+
&case.state,
|
|
115
|
+
)
|
|
116
|
+
.expect("verified leader transport submission");
|
|
117
|
+
|
|
118
|
+
assert_ne!(
|
|
119
|
+
case.message_status(&message_id),
|
|
120
|
+
"submitted_pending_acceptance",
|
|
121
|
+
"F4 acceptance-stall: a verified leader transport submit with no reachable \
|
|
122
|
+
receipt source must not enter submitted_pending_acceptance, because that \
|
|
123
|
+
protected state has no completion writer. The runtime may close the \
|
|
124
|
+
transport-acceptance fact or expose a provider-session receipt writer, \
|
|
125
|
+
but it must not report an indefinitely retryable fact-terminal. outcome={first:?}"
|
|
126
|
+
);
|
|
127
|
+
assert_eq!(
|
|
128
|
+
transport.inject_count(),
|
|
129
|
+
1,
|
|
130
|
+
"the acceptance repair must not obtain progress by physically injecting twice"
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
let _ = deliver_pending_message(
|
|
134
|
+
&case.workspace,
|
|
135
|
+
&case.store,
|
|
136
|
+
&transport,
|
|
137
|
+
&message_id,
|
|
138
|
+
&case.event_log,
|
|
139
|
+
&case.state,
|
|
140
|
+
);
|
|
141
|
+
assert_eq!(
|
|
142
|
+
transport.inject_count(),
|
|
143
|
+
1,
|
|
144
|
+
"a second coordinator observation must not replay the already-submitted leader message"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn nonempty_missing_receipt_source_is_not_token_absent() {
|
|
150
|
+
assert_unavailable_rollout_converges(
|
|
151
|
+
"missing-rollout",
|
|
152
|
+
PathBuf::from("definitely-missing-leader-rollout.jsonl"),
|
|
153
|
+
false,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn nonempty_unreadable_receipt_source_is_not_token_absent() {
|
|
159
|
+
assert_unavailable_rollout_converges(
|
|
160
|
+
"unreadable-rollout",
|
|
161
|
+
PathBuf::from("leader-rollout-is-a-directory"),
|
|
162
|
+
true,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn assert_unavailable_rollout_converges(tag: &str, rollout: PathBuf, directory: bool) {
|
|
167
|
+
let case = Case::new_with_unavailable_rollout(tag, rollout, directory);
|
|
168
|
+
let transport = RecordingTransport::new("");
|
|
169
|
+
let message_id = case.seed_message("unavailable receipt source canary");
|
|
170
|
+
|
|
171
|
+
let first = deliver_pending_message(
|
|
172
|
+
&case.workspace,
|
|
173
|
+
&case.store,
|
|
174
|
+
&transport,
|
|
175
|
+
&message_id,
|
|
176
|
+
&case.event_log,
|
|
177
|
+
&case.state,
|
|
178
|
+
)
|
|
179
|
+
.expect("verified leader transport submission");
|
|
180
|
+
|
|
181
|
+
assert_eq!(
|
|
182
|
+
case.message_status(&message_id),
|
|
183
|
+
"injected_awaiting_receipt",
|
|
184
|
+
"F4 receipt observation must distinguish an unreadable nonempty rollout_path \
|
|
185
|
+
from a readable rollout that lacks the token. The former is \
|
|
186
|
+
receipt-source-unavailable and must converge to the typed parked state, \
|
|
187
|
+
not remain submitted_pending_acceptance. shape={tag} outcome={first:?}"
|
|
188
|
+
);
|
|
189
|
+
assert_eq!(
|
|
190
|
+
first.verification.as_deref(),
|
|
191
|
+
Some("leader_receipt_source_unavailable"),
|
|
192
|
+
"the outcome must expose read-unavailable rather than TokenAbsent; shape={tag}"
|
|
193
|
+
);
|
|
194
|
+
assert_eq!(transport.inject_count(), 1);
|
|
195
|
+
|
|
196
|
+
let _ = deliver_pending_message(
|
|
197
|
+
&case.workspace,
|
|
198
|
+
&case.store,
|
|
199
|
+
&transport,
|
|
200
|
+
&message_id,
|
|
201
|
+
&case.event_log,
|
|
202
|
+
&case.state,
|
|
203
|
+
);
|
|
204
|
+
assert_eq!(
|
|
205
|
+
transport.inject_count(),
|
|
206
|
+
1,
|
|
207
|
+
"read-unavailable convergence must not obtain progress by a second physical \
|
|
208
|
+
injection; shape={tag}"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn worker_consumption_writer_remains_a_positive_control() {
|
|
214
|
+
let case = Case::new("worker-positive-control");
|
|
215
|
+
let message_id = case
|
|
216
|
+
.store
|
|
217
|
+
.create_message(
|
|
218
|
+
None,
|
|
219
|
+
"leader",
|
|
220
|
+
"worker",
|
|
221
|
+
"worker consume canary",
|
|
222
|
+
None,
|
|
223
|
+
false,
|
|
224
|
+
None,
|
|
225
|
+
)
|
|
226
|
+
.unwrap();
|
|
227
|
+
|
|
228
|
+
case.store.mark(&message_id, "consumed", None).unwrap();
|
|
229
|
+
|
|
230
|
+
assert_eq!(
|
|
231
|
+
case.message_status(&message_id),
|
|
232
|
+
"consumed",
|
|
233
|
+
"positive control: the independent worker consumption writer must remain reachable"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
102
237
|
struct Case {
|
|
103
238
|
workspace: PathBuf,
|
|
104
239
|
rollout: PathBuf,
|
|
@@ -134,6 +269,58 @@ impl Case {
|
|
|
134
269
|
}
|
|
135
270
|
}
|
|
136
271
|
|
|
272
|
+
fn new_without_rollout(tag: &str) -> Self {
|
|
273
|
+
let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
|
|
274
|
+
let rollout = workspace.join("absent-leader.jsonl");
|
|
275
|
+
let state = serde_json::json!({
|
|
276
|
+
"active_team_key": "acceptance-team",
|
|
277
|
+
"session_name": "team-acceptance",
|
|
278
|
+
"leader_receiver": {
|
|
279
|
+
"pane_id": "%leader",
|
|
280
|
+
"status": "attached",
|
|
281
|
+
"provider": "claude"
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
285
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
286
|
+
let event_log = EventLog::new(&workspace);
|
|
287
|
+
Self {
|
|
288
|
+
workspace,
|
|
289
|
+
rollout,
|
|
290
|
+
store,
|
|
291
|
+
event_log,
|
|
292
|
+
state,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn new_with_unavailable_rollout(tag: &str, relative: PathBuf, directory: bool) -> Self {
|
|
297
|
+
let workspace = tmp_ws(&format!("leader-inject-acceptance-{tag}"));
|
|
298
|
+
let rollout = workspace.join(relative);
|
|
299
|
+
if directory {
|
|
300
|
+
std::fs::create_dir_all(&rollout).unwrap();
|
|
301
|
+
}
|
|
302
|
+
let state = serde_json::json!({
|
|
303
|
+
"active_team_key": "acceptance-team",
|
|
304
|
+
"session_name": "team-acceptance",
|
|
305
|
+
"leader_receiver": {
|
|
306
|
+
"pane_id": "%leader",
|
|
307
|
+
"status": "attached",
|
|
308
|
+
"provider": "claude",
|
|
309
|
+
"rollout_path": rollout,
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
313
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
314
|
+
let event_log = EventLog::new(&workspace);
|
|
315
|
+
Self {
|
|
316
|
+
workspace,
|
|
317
|
+
rollout,
|
|
318
|
+
store,
|
|
319
|
+
event_log,
|
|
320
|
+
state,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
137
324
|
fn seed_message(&self, content: &str) -> String {
|
|
138
325
|
self.store
|
|
139
326
|
.create_message(None, "worker", "leader", content, None, false, None)
|
|
@@ -2976,9 +2976,8 @@ fn gate054_attached_status_still_accepts_when_pane_is_live() {
|
|
|
2976
2976
|
// Injection-case revision: transport submit success is NOT a provider
|
|
2977
2977
|
// receipt, so the old `delivered` pin was a false-positive lock
|
|
2978
2978
|
// (MUST-10: physical submit is the framework's fact; acceptance is the
|
|
2979
|
-
// provider's). The un-refused path now
|
|
2980
|
-
//
|
|
2981
|
-
// schedule advances it.
|
|
2979
|
+
// provider's). The un-refused path now records the SAME row in the typed
|
|
2980
|
+
// post-injection fact state until a provider receipt advances it.
|
|
2982
2981
|
let ws = tmp_ws("gate054ok");
|
|
2983
2982
|
let store = store_for(&ws);
|
|
2984
2983
|
let log = EventLog::new(&ws);
|
|
@@ -3012,10 +3011,18 @@ fn gate054_attached_status_still_accepts_when_pane_is_live() {
|
|
|
3012
3011
|
1,
|
|
3013
3012
|
"gate054: exactly one physical inject must have happened (MUST-10 anchor)"
|
|
3014
3013
|
);
|
|
3014
|
+
let injected_awaiting_receipt = crate::db::message_store::MessageRowStatus::ALL
|
|
3015
|
+
.iter()
|
|
3016
|
+
.map(|status| status.as_str())
|
|
3017
|
+
.find(|status| *status == "injected_awaiting_receipt")
|
|
3018
|
+
.expect(
|
|
3019
|
+
"gate054 signed reanchor: typed status directory must expose \
|
|
3020
|
+
injected_awaiting_receipt",
|
|
3021
|
+
);
|
|
3015
3022
|
assert_eq!(
|
|
3016
|
-
out.message_status.0,
|
|
3017
|
-
"gate054
|
|
3018
|
-
|
|
3023
|
+
out.message_status.0, injected_awaiting_receipt,
|
|
3024
|
+
"gate054 signed reanchor: a successful physical inject without a provider receipt \
|
|
3025
|
+
must remain non-delivered as injected_awaiting_receipt, never claim delivered: {out:?}"
|
|
3019
3026
|
);
|
|
3020
3027
|
}
|
|
3021
3028
|
|
|
@@ -53,6 +53,11 @@ pub enum DeliveryRefusal {
|
|
|
53
53
|
TmuxTargetMissing,
|
|
54
54
|
MessageAlreadyClaimed,
|
|
55
55
|
LeaderNotAttached,
|
|
56
|
+
/// The recorded leader pane is live but belongs to another workspace.
|
|
57
|
+
/// Keep this typed cause distinct from an actually unattached leader so
|
|
58
|
+
/// the send surface can return the existing recovery action.
|
|
59
|
+
#[serde(rename = "PaneWorkspaceMismatch")]
|
|
60
|
+
PaneWorkspaceMismatch,
|
|
56
61
|
NoCallerPane,
|
|
57
62
|
TeamOwnerMismatch,
|
|
58
63
|
Ambiguous,
|
|
@@ -529,6 +529,11 @@ pub(crate) fn requeue_blocked_leader_messages(
|
|
|
529
529
|
"blocked_leader_unbound": counts.blocked_leader_unbound,
|
|
530
530
|
"queued_until_leader_attach": counts.queued_until_leader_attach,
|
|
531
531
|
},
|
|
532
|
+
"attach_window_failures": {
|
|
533
|
+
"reason": "leader_not_attached",
|
|
534
|
+
"count": counts.blocked_leader_unbound,
|
|
535
|
+
"status": "requeued_pending_physical_retry",
|
|
536
|
+
},
|
|
532
537
|
}),
|
|
533
538
|
)?;
|
|
534
539
|
}
|
|
@@ -18,6 +18,7 @@ pub mod ids;
|
|
|
18
18
|
// cli/send.rs (positional adapter), mcp_server/tools.rs (owner-scoped
|
|
19
19
|
// peer suggestions). Never a routing authority — advisory only.
|
|
20
20
|
pub(crate) mod name_similarity;
|
|
21
|
+
pub mod pane_authority_refusal;
|
|
21
22
|
pub mod paths;
|
|
22
23
|
pub mod permissions;
|
|
23
24
|
pub mod routing;
|
|
@@ -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) {
|