@team-agent/installer 0.5.2 → 0.5.3

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.
Files changed (35) hide show
  1. package/Cargo.lock +3 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +20 -0
  4. package/crates/team-agent/src/cli/emit.rs +5 -0
  5. package/crates/team-agent/src/cli/mod.rs +121 -56
  6. package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
  7. package/crates/team-agent/src/codex_app_server.rs +62 -1
  8. package/crates/team-agent/src/conpty/backend.rs +120 -8
  9. package/crates/team-agent/src/coordinator/backoff.rs +88 -2
  10. package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
  11. package/crates/team-agent/src/coordinator/health.rs +97 -11
  12. package/crates/team-agent/src/coordinator/mod.rs +8 -0
  13. package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
  14. package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
  15. package/crates/team-agent/src/diagnose/orphans.rs +18 -7
  16. package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
  17. package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
  18. package/crates/team-agent/src/lib.rs +14 -1
  19. package/crates/team-agent/src/lifecycle/launch.rs +80 -91
  20. package/crates/team-agent/src/lifecycle/lock.rs +40 -33
  21. package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
  22. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
  23. package/crates/team-agent/src/mcp_server/wire.rs +6 -7
  24. package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
  25. package/crates/team-agent/src/packaging/tests.rs +41 -6
  26. package/crates/team-agent/src/packaging/types.rs +31 -3
  27. package/crates/team-agent/src/platform/argv.rs +324 -0
  28. package/crates/team-agent/src/platform/errors.rs +95 -0
  29. package/crates/team-agent/src/platform/file_lock.rs +418 -0
  30. package/crates/team-agent/src/platform/mod.rs +66 -0
  31. package/crates/team-agent/src/platform/process.rs +555 -0
  32. package/crates/team-agent/src/state/persist.rs +205 -25
  33. package/crates/team-agent/src/tmux_backend.rs +63 -13
  34. package/crates/team-agent/src/transport_factory.rs +124 -5
  35. package/package.json +4 -4
@@ -0,0 +1,418 @@
1
+ //! Cross-platform exclusive file lock primitive.
2
+ //!
3
+ //! ## Batch 2 real implementation (leader msg_c833639b61b8)
4
+ //!
5
+ //! Batch 2 promotes this module from a signature-only scaffold to a
6
+ //! **byte-preserving migration** of the file-lock primitives used by
7
+ //! `state/persist.rs::RuntimeLock` (state-save lock) and
8
+ //! `lifecycle/lock.rs::LifecycleLockGuard` (per-workspace agent
9
+ //! lifecycle lock).
10
+ //!
11
+ //! ## Two-layer API
12
+ //!
13
+ //! The existing callers wrap their own polling loop + timeout +
14
+ //! `held_long` event emission around the raw `flock` syscall. To
15
+ //! preserve the exact caller behavior (persist.rs's 50ms poll + typed
16
+ //! `StateError::Locked`; lifecycle/lock.rs's waiter file + 5s
17
+ //! held-long event + N38 three-line timeout error), platform::file_lock
18
+ //! exposes **two primitives**, not one convenience `try_lock_exclusive`:
19
+ //!
20
+ //! 1. `try_lock_once_nonblocking(&File) -> Result<bool, io::Error>` —
21
+ //! non-blocking exclusive lock attempt. Returns `Ok(true)` on
22
+ //! success, `Ok(false)` on "would block" (both Unix `EWOULDBLOCK`
23
+ //! and Windows `ERROR_LOCK_VIOLATION`), `Err(...)` on real I/O
24
+ //! error. Callers keep their own polling loops so the outer
25
+ //! timeout/event/error shape is byte-preserving.
26
+ //! 2. `unlock(&File)` — release the lock. Called from `Drop` in both
27
+ //! caller sites.
28
+ //!
29
+ //! A high-level `try_lock_exclusive(path, timeout) -> FileLockGuard`
30
+ //! wrapper is also provided (uses the same primitives + a bare polling
31
+ //! loop) for future callers that don't need the metadata/waiter
32
+ //! machinery — but the two existing product callers keep their own
33
+ //! loops per this batch's byte-preserving constraint.
34
+ //!
35
+ //! ## Windows implementation
36
+ //!
37
+ //! Windows implementation uses `LockFileEx` /
38
+ //! `UnlockFileEx` via `windows-sys`. The lock is `LOCKFILE_EXCLUSIVE_LOCK
39
+ //! | LOCKFILE_FAIL_IMMEDIATELY` so it maps 1:1 to
40
+ //! `flock(LOCK_EX|LOCK_NB)`. Range is `[0, u64::MAX)` (whole-file lock).
41
+ //!
42
+ //! ## CR anchors
43
+ //!
44
+ //! - **C-2**: consuming the two callers via these primitives eliminates
45
+ //! the `state/persist.rs::not_yet_implemented` fallback (persist.rs
46
+ //! loop becomes cfg-free) AND the `lifecycle/lock.rs::lock_timeout_error`
47
+ //! non-Unix stub. The `platform_fallback_burndown_batch0.rs` grep
48
+ //! guard is updated to flip its persist.rs assertion from
49
+ //! "fallback present + FIXME marker" to "fallback removed".
50
+ //! - **C-6 N38 (held_long event)**: the 5s `write_lock_held_long_event`
51
+ //! in `lifecycle/lock.rs` stays in the caller (this module is
52
+ //! pure OS primitives). Windows sees the SAME event because the
53
+ //! caller's polling loop is now cfg-free.
54
+
55
+ use std::fs::File;
56
+ use std::io;
57
+ use std::path::Path;
58
+ use std::time::Duration;
59
+
60
+ /// Try to acquire an exclusive advisory lock on `file` without
61
+ /// blocking. Returns `Ok(true)` if the lock was acquired, `Ok(false)`
62
+ /// if the file is already locked by someone else, `Err(...)` on real
63
+ /// I/O error. Callers own the polling loop + timeout + event emission.
64
+ ///
65
+ /// Unix: `flock(fd, LOCK_EX|LOCK_NB)`. `EWOULDBLOCK` → `Ok(false)`.
66
+ /// Windows: `LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY, 0, u32::MAX, u32::MAX, &mut overlapped)`.
67
+ /// `ERROR_LOCK_VIOLATION` / `ERROR_IO_PENDING` → `Ok(false)`.
68
+ pub fn try_lock_once_nonblocking(file: &File) -> io::Result<bool> {
69
+ #[cfg(unix)]
70
+ {
71
+ try_lock_once_unix(file)
72
+ }
73
+ #[cfg(windows)]
74
+ {
75
+ try_lock_once_windows(file)
76
+ }
77
+ #[cfg(not(any(unix, windows)))]
78
+ {
79
+ // Neither Unix nor Windows — return io error so callers
80
+ // surface honest failure instead of silent-ok.
81
+ let _ = file;
82
+ Err(io::Error::new(
83
+ io::ErrorKind::Unsupported,
84
+ "platform::file_lock: unsupported host (neither unix nor windows)",
85
+ ))
86
+ }
87
+ }
88
+
89
+ /// Release the exclusive lock held on `file`. Called from `Drop` in
90
+ /// caller code so the release runs even on panic.
91
+ ///
92
+ /// Unix: `flock(fd, LOCK_UN)`.
93
+ /// Windows: `UnlockFileEx(handle, 0, u32::MAX, u32::MAX, &mut overlapped)`.
94
+ ///
95
+ /// Errors are best-effort: the OS will release the lock when the
96
+ /// file handle closes anyway. Returning `io::Result` gives tests a
97
+ /// hook.
98
+ pub fn unlock(file: &File) -> io::Result<()> {
99
+ #[cfg(unix)]
100
+ {
101
+ unlock_unix(file)
102
+ }
103
+ #[cfg(windows)]
104
+ {
105
+ unlock_windows(file)
106
+ }
107
+ #[cfg(not(any(unix, windows)))]
108
+ {
109
+ let _ = file;
110
+ Ok(())
111
+ }
112
+ }
113
+
114
+ // ─────────────────────────────────────────────────────────────────────
115
+ // Unix impl (byte-preserving migration of the current inline flock
116
+ // code in `state/persist.rs` and `lifecycle/lock.rs`).
117
+ // ─────────────────────────────────────────────────────────────────────
118
+
119
+ #[cfg(unix)]
120
+ fn try_lock_once_unix(file: &File) -> io::Result<bool> {
121
+ use std::os::unix::io::AsRawFd;
122
+ // SAFETY: fd is owned by `file` for the duration of this call.
123
+ // LOCK_EX | LOCK_NB matches the byte-for-byte behavior in
124
+ // `state/persist.rs:172-173` and `lifecycle/lock.rs:108`.
125
+ let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
126
+ if rc == 0 {
127
+ return Ok(true);
128
+ }
129
+ let err = io::Error::last_os_error();
130
+ match err.raw_os_error() {
131
+ Some(code) if code == libc::EWOULDBLOCK => Ok(false),
132
+ Some(code) if code == libc::EAGAIN => Ok(false),
133
+ _ => Err(err),
134
+ }
135
+ }
136
+
137
+ #[cfg(unix)]
138
+ fn unlock_unix(file: &File) -> io::Result<()> {
139
+ use std::os::unix::io::AsRawFd;
140
+ // SAFETY: same fd as above; LOCK_UN releases our own lock.
141
+ let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
142
+ if rc == 0 {
143
+ Ok(())
144
+ } else {
145
+ Err(io::Error::last_os_error())
146
+ }
147
+ }
148
+
149
+ // ─────────────────────────────────────────────────────────────────────
150
+ // Windows impl (LockFileEx/UnlockFileEx, semantic 1:1 with flock).
151
+ // ─────────────────────────────────────────────────────────────────────
152
+
153
+ #[cfg(windows)]
154
+ fn try_lock_once_windows(file: &File) -> io::Result<bool> {
155
+ use std::os::windows::io::AsRawHandle;
156
+ use windows::Win32::Foundation::{ERROR_IO_PENDING, ERROR_LOCK_VIOLATION, HANDLE};
157
+ use windows::Win32::Storage::FileSystem::{
158
+ LockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
159
+ };
160
+ use windows::Win32::System::IO::OVERLAPPED;
161
+ let handle = HANDLE(file.as_raw_handle() as *mut _);
162
+ // LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY:
163
+ // exclusive lock, do not block if already held. Semantic 1:1 with
164
+ // `flock(LOCK_EX | LOCK_NB)`.
165
+ //
166
+ // Range covers `[0, u32::MAX + u32::MAX<<32)` — the entire file
167
+ // extent — the same coverage `flock` gives (whole-file advisory
168
+ // lock).
169
+ let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
170
+ let result = unsafe {
171
+ LockFileEx(
172
+ handle,
173
+ LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
174
+ Some(0),
175
+ u32::MAX,
176
+ u32::MAX,
177
+ &mut overlapped,
178
+ )
179
+ };
180
+ match result {
181
+ Ok(()) => Ok(true),
182
+ Err(err) => {
183
+ let raw = err.code().0 as i32;
184
+ let raw_u32 = raw as u32;
185
+ if raw_u32 == ERROR_LOCK_VIOLATION.0 || raw_u32 == ERROR_IO_PENDING.0 {
186
+ Ok(false)
187
+ } else {
188
+ Err(io::Error::from_raw_os_error(raw))
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ #[cfg(windows)]
195
+ fn unlock_windows(file: &File) -> io::Result<()> {
196
+ use std::os::windows::io::AsRawHandle;
197
+ use windows::Win32::Foundation::HANDLE;
198
+ use windows::Win32::Storage::FileSystem::UnlockFileEx;
199
+ use windows::Win32::System::IO::OVERLAPPED;
200
+ let handle = HANDLE(file.as_raw_handle() as *mut _);
201
+ let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
202
+ let result = unsafe { UnlockFileEx(handle, Some(0), u32::MAX, u32::MAX, &mut overlapped) };
203
+ result.map_err(|e| io::Error::from_raw_os_error(e.code().0 as i32))
204
+ }
205
+
206
+ // ─────────────────────────────────────────────────────────────────────
207
+ // High-level convenience wrapper.
208
+ // ─────────────────────────────────────────────────────────────────────
209
+
210
+ /// Owns the underlying `File` handle + platform lock. `Drop` unlocks
211
+ /// via `unlock(&file)`.
212
+ ///
213
+ /// Callers that don't need the lifecycle-lock metadata/waiter/held_long
214
+ /// event machinery can use `try_lock_exclusive(path, timeout)` directly.
215
+ /// The existing product callers (`state/persist.rs`, `lifecycle/lock.rs`)
216
+ /// use `try_lock_once_nonblocking` + `unlock` directly and keep their
217
+ /// own polling loops for byte-preserving behavior.
218
+ pub struct FileLockGuard {
219
+ file: Option<File>,
220
+ }
221
+
222
+ impl std::fmt::Debug for FileLockGuard {
223
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224
+ f.debug_struct("FileLockGuard")
225
+ .field("held", &self.file.is_some())
226
+ .finish()
227
+ }
228
+ }
229
+
230
+ impl FileLockGuard {
231
+ /// Test-only accessor for the wrapped file handle.
232
+ #[cfg(test)]
233
+ fn file(&self) -> &File {
234
+ self.file.as_ref().expect("file present until Drop")
235
+ }
236
+ }
237
+
238
+ impl Drop for FileLockGuard {
239
+ fn drop(&mut self) {
240
+ if let Some(file) = self.file.take() {
241
+ // Best-effort unlock. If it fails the OS still releases on
242
+ // handle close.
243
+ let _ = unlock(&file);
244
+ }
245
+ }
246
+ }
247
+
248
+ #[derive(Debug, thiserror::Error)]
249
+ pub enum LockError {
250
+ #[error("file lock timeout after {timeout_secs:.2}s on {path}")]
251
+ Timeout { timeout_secs: f64, path: String },
252
+ #[error("file lock io error on {path}: {source}")]
253
+ Io {
254
+ path: String,
255
+ #[source]
256
+ source: io::Error,
257
+ },
258
+ }
259
+
260
+ /// High-level: acquire an exclusive lock on `path` within `timeout`,
261
+ /// polling every 50ms. Returns a guard that unlocks on `Drop`.
262
+ ///
263
+ /// Existing product callers do NOT use this — they need their own
264
+ /// metadata/waiter/held_long event machinery. This wrapper exists for
265
+ /// simple future callers.
266
+ pub fn try_lock_exclusive(
267
+ path: &Path,
268
+ timeout: Duration,
269
+ ) -> Result<FileLockGuard, LockError> {
270
+ let file = File::options()
271
+ .read(true)
272
+ .write(true)
273
+ .create(true)
274
+ .truncate(false)
275
+ .open(path)
276
+ .map_err(|e| LockError::Io {
277
+ path: path.display().to_string(),
278
+ source: e,
279
+ })?;
280
+ let start = std::time::Instant::now();
281
+ loop {
282
+ match try_lock_once_nonblocking(&file) {
283
+ Ok(true) => return Ok(FileLockGuard { file: Some(file) }),
284
+ Ok(false) => {}
285
+ Err(e) => {
286
+ return Err(LockError::Io {
287
+ path: path.display().to_string(),
288
+ source: e,
289
+ });
290
+ }
291
+ }
292
+ if start.elapsed() >= timeout {
293
+ return Err(LockError::Timeout {
294
+ timeout_secs: timeout.as_secs_f64(),
295
+ path: path.display().to_string(),
296
+ });
297
+ }
298
+ std::thread::sleep(Duration::from_millis(50));
299
+ }
300
+ }
301
+
302
+ // ─────────────────────────────────────────────────────────────────────
303
+ // Tests: primitive + convenience wrapper on the host platform.
304
+ // Both branches (unix + windows) exercise the same trait shape via
305
+ // the top-level `try_lock_once_nonblocking` / `unlock` functions —
306
+ // the test source is cfg-free.
307
+ // ─────────────────────────────────────────────────────────────────────
308
+
309
+ #[cfg(test)]
310
+ mod tests {
311
+ use super::*;
312
+
313
+ #[test]
314
+ fn primitives_acquire_and_release_on_host_platform() {
315
+ // Batch 2 anchor: the OS primitive must work on the CURRENT
316
+ // host (macOS/Linux in dev, both macOS/Linux/Windows in CI).
317
+ // This test compiles on both cfg branches and validates the
318
+ // 1:1 shape mapping between `flock` and `LockFileEx`.
319
+ let dir = std::env::temp_dir().join("ta-b2-primitives");
320
+ std::fs::create_dir_all(&dir).unwrap();
321
+ let path = dir.join("primitives-lock.tmp");
322
+ let file = File::options()
323
+ .read(true)
324
+ .write(true)
325
+ .create(true)
326
+ .truncate(false)
327
+ .open(&path)
328
+ .unwrap();
329
+ assert!(
330
+ try_lock_once_nonblocking(&file).unwrap(),
331
+ "fresh lock file should acquire on first attempt"
332
+ );
333
+ assert!(
334
+ unlock(&file).is_ok(),
335
+ "unlock must succeed for our own lock"
336
+ );
337
+ let _ = std::fs::remove_file(&path);
338
+ }
339
+
340
+ #[test]
341
+ fn convenience_try_lock_exclusive_acquires_and_drop_releases() {
342
+ // The high-level convenience wrapper is what future callers
343
+ // will use. Verify the drop-releases-lock invariant.
344
+ let dir = std::env::temp_dir().join("ta-b2-convenience");
345
+ std::fs::create_dir_all(&dir).unwrap();
346
+ let path = dir.join("convenience-lock.tmp");
347
+ {
348
+ let guard = try_lock_exclusive(&path, Duration::from_secs(1))
349
+ .expect("first acquire must succeed");
350
+ let _ = guard.file();
351
+ }
352
+ let guard2 = try_lock_exclusive(&path, Duration::from_secs(1))
353
+ .expect("post-drop reacquire must succeed");
354
+ drop(guard2);
355
+ let _ = std::fs::remove_file(&path);
356
+ }
357
+
358
+ #[test]
359
+ fn second_acquire_blocks_until_first_releases() {
360
+ // C-6 N38 anchor: the primitive returns `Ok(false)` for the
361
+ // "already held" case on BOTH unix and windows (mapped from
362
+ // EWOULDBLOCK or ERROR_LOCK_VIOLATION respectively). Callers
363
+ // use this to drive their polling loop; without the mapping
364
+ // they would treat the transient contention as a hard error.
365
+ let dir = std::env::temp_dir().join("ta-b2-contention");
366
+ std::fs::create_dir_all(&dir).unwrap();
367
+ let path = dir.join("contention-lock.tmp");
368
+ let file1 = File::options()
369
+ .read(true)
370
+ .write(true)
371
+ .create(true)
372
+ .truncate(false)
373
+ .open(&path)
374
+ .unwrap();
375
+ let file2 = File::options()
376
+ .read(true)
377
+ .write(true)
378
+ .create(true)
379
+ .truncate(false)
380
+ .open(&path)
381
+ .unwrap();
382
+ assert!(try_lock_once_nonblocking(&file1).unwrap());
383
+ // A second attempt from a DIFFERENT file handle must see
384
+ // Ok(false), not an error.
385
+ assert!(
386
+ !try_lock_once_nonblocking(&file2).unwrap(),
387
+ "second-holder attempt must return Ok(false) (would-block), not error"
388
+ );
389
+ unlock(&file1).unwrap();
390
+ // Now the second holder can acquire.
391
+ assert!(
392
+ try_lock_once_nonblocking(&file2).unwrap(),
393
+ "post-unlock reacquire from second holder must succeed"
394
+ );
395
+ unlock(&file2).unwrap();
396
+ let _ = std::fs::remove_file(&path);
397
+ }
398
+
399
+ #[test]
400
+ fn timeout_error_carries_path_and_seconds() {
401
+ // Convenience wrapper's timeout error shape.
402
+ let dir = std::env::temp_dir().join("ta-b2-timeout");
403
+ std::fs::create_dir_all(&dir).unwrap();
404
+ let path = dir.join("timeout-lock.tmp");
405
+ let _hold = try_lock_exclusive(&path, Duration::from_secs(1))
406
+ .expect("first acquire must succeed");
407
+ let err = try_lock_exclusive(&path, Duration::from_millis(150))
408
+ .expect_err("second acquire must timeout");
409
+ match err {
410
+ LockError::Timeout { timeout_secs, path: p } => {
411
+ assert!(timeout_secs > 0.0);
412
+ assert!(p.ends_with("timeout-lock.tmp"), "path suffix: {p}");
413
+ }
414
+ other => panic!("expected Timeout, got {other:?}"),
415
+ }
416
+ let _ = std::fs::remove_file(&path);
417
+ }
418
+ }
@@ -0,0 +1,66 @@
1
+ //! 0.5.x Windows portability Batch 0: platform abstraction layer.
2
+ //!
3
+ //! Truth sources (READ-ONLY):
4
+ //! - Design: `.team/artifacts/0.5.x-windows-portability-survey-design.md`
5
+ //! (§Target Design + §Ordered Migration Plan)
6
+ //! - CR verdict: `.team/artifacts/0.5.x-windows-portability-cr-verdict.md`
7
+ //! (6 constraints; C-1 through C-6 anchor into this module)
8
+ //!
9
+ //! ## Scope of Batch 0 (this commit)
10
+ //!
11
+ //! - Add the `platform` module scaffold + Unix-side implementations
12
+ //! populated from the current inline `libc`/`std::os::unix` code
13
+ //! (byte-equivalent behavior on macOS/Linux).
14
+ //! - No caller migration yet — the three legacy fallback sites
15
+ //! (`mcp_server/wire.rs`, `lifecycle/restart/agent.rs`,
16
+ //! `state/persist.rs`) stay in place with `FIXME(portability)`
17
+ //! comments pointing at this design. Batch 2/3 replaces them.
18
+ //! - Windows side is empty stubs today; every fn is `unimplemented!()`
19
+ //! with a `# Safety:` / `# Windows:` doc note explaining what
20
+ //! Batch 2/3/4 will supply.
21
+ //! - CI adds a Windows compile-only gate (`cargo check --target
22
+ //! x86_64-pc-windows-msvc`), reporting RED as the burn-down baseline
23
+ //! for Batches 1-4. The Windows job uses `continue-on-error: true`
24
+ //! so it does not block the Ubuntu/macOS/Windows-binaries jobs.
25
+ //!
26
+ //! ## CR anchors
27
+ //!
28
+ //! - **C-1**: `packaging/types.rs::WindowsX8664 → PlatformSupport::Native`
29
+ //! claim is FALSE while `cargo check --target x86_64-pc-windows-msvc`
30
+ //! is RED. Downgraded to `PreviewCompileOnly` in this batch; will be
31
+ //! promoted back to `Native` only after Batch 6/7 real-machine gates.
32
+ //! - **C-2**: three legacy non-Unix fallbacks
33
+ //! (`wire.rs::parent_pid=0`, `restart/agent.rs::pid_is_alive=true`,
34
+ //! `persist.rs::runtime_lock=not_implemented`) get
35
+ //! `FIXME(portability)` comments referencing this design. Batch 2
36
+ //! removes the persist.rs one; Batch 3 removes the wire.rs +
37
+ //! restart/agent.rs ones. Grep guards verify no reintroduction.
38
+ //! - **C-4**: `platform::process` is NOT importable from
39
+ //! `messaging/` (team-worker liveness must go through the backend
40
+ //! `Transport`, not a shell-out process snapshot). Grep guard test
41
+ //! walks the source tree.
42
+ //! - **C-6**: hard-terminate on Windows lacks Unix grace semantics —
43
+ //! the `terminate_pid`/`terminate_group` signatures include a
44
+ //! `SignalKind` enum + a caller-visible `TerminationOutcome` so
45
+ //! Windows callers can emit a `platform.terminate_force_only` event
46
+ //! when they downgrade from `TerminateGraceful` to `TerminateForce`.
47
+ //!
48
+ //! ## Module layout
49
+ //!
50
+ //! - `process` — process lifecycle + snapshot + termination
51
+ //! - `file_lock` — cross-platform exclusive file lock
52
+ //! - `errors` — retryable error classifier + os_error_name
53
+ //! - `argv` — process argv/environ query (Batch 4)
54
+ //!
55
+ //! Every submodule follows the same pattern: a `unix.rs` file with the
56
+ //! current libc code, a `windows.rs` file with `unimplemented!()`
57
+ //! stubs + Windows API sketch, and a `mod.rs` that re-exports the
58
+ //! platform-appropriate impl via `cfg`.
59
+ //!
60
+ //! Callers see a single `crate::platform::process::pid_liveness(pid)`
61
+ //! (etc.) with no `cfg` at the callsite.
62
+
63
+ pub mod argv;
64
+ pub mod errors;
65
+ pub mod file_lock;
66
+ pub mod process;