diff --git a/docs/specs/FR.md b/docs/specs/FR.md index b758bbf4..ff9390c6 100644 --- a/docs/specs/FR.md +++ b/docs/specs/FR.md @@ -258,14 +258,13 @@ replacing vendor agent executables as the primary detection path. - **AC-006.23:** `sharecli proc --pid N` (N ≥ 1) prints a one-shot detail view for a live host process: `ppid`, parent `comm`, `cmdline`, live RSS/FD samples, direct agent `family` when the PID is a known agent, otherwise nearest agent - ancestor when under an agent subtree. `--pid --json` emits a structured object; - missing or dead PIDs MUST fail loudly; `--pid --watch` is allowed per AC-007.87. + ancestor when under an agent subtree. `--pid --json` emits a structured object; + missing or dead PIDs MUST fail loudly; `--pid` MUST NOT combine with `--watch`. - **AC-006.24:** `sharecli proc --csv` emits RFC 4180-style CSV of flat agent inventory (`pid,family,comm,mem_rss_bytes,mem_rss,fd_count`) after `--family`, `--min-rss`, `--sort`, and `--limit`; empty inventory emits the header row only; fields with commas or quotes MUST be escaped; `--csv` MUST - NOT combine with `--json`; `--pid --csv` is detail CSV per AC-007.86; - `--csv --watch` is unlocked per AC-007.88. + NOT combine with `--json`, `--watch`, or `--pid` (without `--tree`). - **AC-006.25:** `sharecli proc --ppid N` keeps flat inventory rows and `--tree` root forests whose agent parent PID equals `N`, composed with `--family`, `--min-rss`, `--sort`, `--limit`, `--json`, and `--csv`; `--ppid` MUST NOT @@ -275,8 +274,8 @@ replacing vendor agent executables as the primary detection path. (`root_index,depth,pid,ppid,family,comm,mem_rss_bytes,mem_rss,fd_count`) after `--family`, `--min-rss`, `--ppid`, `--sort`, and `--limit`; `root_index` separates forests, `depth` is 0 at each agent root; empty forests emit the - header row only; `--tree --csv` MUST NOT combine with `--json` or `--pid`; `--tree --csv --watch` - is proc-only per AC-007.88. + header row only; `--tree --csv` MUST NOT combine with `--json`, `--watch`, or + `--pid`. - **AC-006.27:** `sharecli proc --max-rss ` keeps flat inventory rows and `--tree` root forests at or below the RSS bound (same `K`/`M`/`G`/bytes parsing as `--min-rss`), composed with `--family`, `--min-rss`, `--ppid`, @@ -828,13 +827,13 @@ coalesce. Thermal watch signals MAY surface via FR-011. Legacy frames missing required `pool` / `status` fields MAY yield Unknown (no silent partial decode). Dashboard untyped WS envelope parity remains AC-007.70 / AC-007.41. -- **AC-007.81:** `sharecli report --format csv` (one-shot) MUST emit a fleet analytics CSV body - (summary + per-project + top-consumer sections via +- **AC-007.81:** `sharecli report --format csv` (one-shot, no `--watch`) MUST emit a fleet + analytics CSV body (summary + per-project + top-consumer sections via [`render_report_csv_body`](src/commands/report.rs)) followed by companion `gate`, `host_watch`, `pool`, and `status` CSV records (parity with proc CSV AC-007.79 and report text AC-007.74). Companion blocks MUST use the same shapes/order as AC-007.79: `gate` → `host_watch` → `pool` → `status`. **stderr** MUST remain silent on success. - `--format csv --watch` is unlocked per AC-007.90. + `--format csv` MUST NOT combine with `--watch` (one-shot export only, like proc `--csv`). - **AC-007.82:** `sharecli health --csv`, `sharecli pool --csv`, and `sharecli status --csv` (one-shot, no `--watch`) MUST emit command-specific CSV bodies via @@ -843,135 +842,9 @@ coalesce. Thermal watch signals MAY surface via FR-011. `pool`, and `status` CSV records via [`append_operator_csv_companions`](src/commands/mod.rs) (parity with proc CSV AC-007.79 and report CSV AC-007.81). Companion blocks MUST use the same shapes/order as AC-007.79: `gate` → `host_watch` → `pool` → `status`. **stderr** MUST remain - silent on success. `--csv` MUST NOT combine with `--json` (one-shot export only; `--csv --watch` - per AC-007.89). - -- **AC-007.83:** `sharecli ps --all --csv` (one-shot, no `--watch`) MUST emit a managed-process + - host agent-inventory CSV body via [`render_ps_all_csv_body`](src/commands/mod.rs) followed by - companion `gate`, `host_watch`, `pool`, and `status` CSV records via - [`append_operator_csv_companions`](src/commands/mod.rs) (parity with health/pool/status CSV - AC-007.82 and proc/report CSV AC-007.79/81). Companion blocks MUST use the same shapes/order as - AC-007.79: `gate` → `host_watch` → `pool` → `status`. **stderr** MUST remain silent on success. - `--csv` MUST require `--all` (parity with `--json` AC-007.43) and MUST NOT combine with `--json` - (one-shot export only; `--csv --watch` per AC-007.89). - -- **AC-007.84:** A focused integration/meta regression suite MUST lock the FR-007 operator - envelope matrix across `proc`, `report`, `health`, `pool`, `status`, and `ps --all` in text, - JSON, and CSV one-shot modes, plus IPC (`health.status`, `monitoring.report`), WS - `health_update` decode, dashboard HTML operator panels, tray formatter markers - (Linux/Windows/Swift/C#), and thermal TUI pool/status panel markers. The suite MUST assert - companion markers or key order where cheap and MUST NOT re-run long `--watch` dwell cycles - (those remain in per-AC integration files). Matrix drift MUST fail loudly. - -- **AC-007.85:** The AC-007.84 parity suite MUST include `sharecli proc --tree` text, - `proc --tree --json`, and `proc --tree --csv` one-shot rows asserting the same - gate → host_watch → pool → status operator envelope as flat `proc` (parity with text - AC-007.75, JSON AC-007.77, and CSV AC-007.79). Implementation MUST already satisfy - these surfaces; this AC locks matrix coverage only. - -- **AC-007.86:** The AC-007.84 parity suite MUST include `sharecli proc --pid N` text, - `proc --pid N --json`, and `proc --pid N --csv` one-shot rows asserting the same - gate → host_watch → pool → status operator envelope as flat `proc` (parity with text - AC-007.75, JSON AC-007.77, and CSV AC-007.79). Tests MUST target a live process via - self-PID (`std::process::id()`) or an equivalent spawn/fixture pattern from existing - proc tests. `proc --pid` detail surfaces MUST embed pool/status siblings on JSON, - append pool/proc-scan operator lines on text, and append CSV companions on `--csv` - (extends AC-007.16/17 detail gate/host_watch paths). - -- **AC-007.87:** `sharecli proc --pid N --watch [secs]` MUST refresh like flat - `proc --watch` / `proc --tree --watch`: text watch clears and re-renders stdout - each interval with full PID detail plus gate → host_watch → pool → status - operator envelope; stderr silent on success; `[watch]` footer on stdout (parity - AC-007.35). JSON watch emits NDJSON lines on stdout each tick with the same JSON - shape as one-shot `proc --pid N --json` (including gate/host_watch/pool/status); - gate → host_watch text companions on stderr each refresh (parity AC-007.28); - stdout pipe-clean. `--pid --csv --watch` is unlocked per AC-007.91. Other - meaningless `--pid` combos that still apply MUST remain rejected. - -- **AC-007.88:** `sharecli proc --csv --watch [secs]` and `sharecli proc --tree --csv --watch [secs]` - MUST refresh like one-shot CSV (AC-007.79): each tick emits the full inventory/tree CSV body - followed by companion rows `gate` → `host_watch` → `pool` → `status` on **stdout** only. - Streaming consumers MUST be able to split refresh frames: implementation uses a leading - `# sharecli-proc-watch-frame` comment line before each tick's CSV body (headers repeat every - frame). **stderr** MUST remain silent on success (parity with text watch AC-007.35 and - one-shot CSV AC-007.33); MUST NOT emit ANSI clear sequences. Each frame MUST end with a - `# [watch] Refreshing every Ns — press Ctrl-C to stop.` CSV comment line. `--watch` interval - MUST be >= 1. `--csv --json --watch` MUST remain rejected. `--pid --csv --watch` is unlocked - per AC-007.91. - -- **AC-007.89:** `sharecli health --csv --watch [secs]`, `sharecli pool --csv --watch [secs]`, - `sharecli status --csv --watch [secs]`, and `sharecli ps --all --csv --watch [secs]` MUST refresh - like one-shot CSV (AC-007.82/83) and proc CSV watch (AC-007.88): each tick emits the full - command-specific CSV body followed by companion rows `gate` → `host_watch` → `pool` → `status` - on **stdout** only. Streaming consumers MUST split refresh frames via a leading - `# sharecli--watch-frame` comment line before each tick's CSV body (headers repeat every - frame). **stderr** MUST remain silent on success; MUST NOT emit ANSI clear sequences. Each frame - MUST end with a `# [watch] Refreshing every Ns — press Ctrl-C to stop.` CSV comment line. - `--watch` interval MUST be >= 1. `--csv --json --watch` MUST remain rejected. - `ps --csv --watch` without `--all` MUST remain rejected (parity AC-007.83). - -- **AC-007.90:** `sharecli report --format csv --watch [secs]` MUST refresh like one-shot report CSV - (AC-007.81) and operator/proc CSV watch (AC-007.88/89): each tick emits the full fleet analytics - CSV body followed by companion rows `gate` → `host_watch` → `pool` → `status` on **stdout** only. - Streaming consumers MUST split refresh frames via a leading `# sharecli-report-watch-frame` - comment line before each tick's CSV body (headers repeat every frame). **stderr** MUST remain - silent on success; MUST NOT emit ANSI clear sequences. Each frame MUST end with a - `# [watch] Refreshing every Ns — press Ctrl-C to stop.` CSV comment line. `--watch` interval - MUST be >= 1. - -- **AC-007.91:** `sharecli proc --pid N --csv --watch [secs]` MUST refresh like one-shot - `proc --pid N --csv` (AC-007.86) and CSV watch siblings (AC-007.88–90): each tick emits the - PID detail CSV body followed by companion rows `gate` → `host_watch` → `pool` → `status` on - **stdout** only. Streaming consumers MUST split refresh frames via a leading - `# sharecli-proc-pid-watch-frame` comment line before each tick's CSV body (headers repeat - every frame). **stderr** MUST remain silent on success; MUST NOT emit ANSI clear sequences. - Each frame MUST end with a `# [watch] Refreshing every Ns — press Ctrl-C to stop.` CSV - comment line. `--watch` interval MUST be >= 1. `--pid --csv --json --watch` MUST remain - rejected (csv+json ban). - -- **AC-007.92:** `sharecli proc --pid N` is detail mode. Combining `--pid` with inventory-mode - flags MUST fail loudly (no silent ignore): `--tree`, `--family`, `--exclude-family`, - `--comm`, `--cmdline`, `--state`, `--min-rss`, `--max-rss`, `--min-fd`, `--max-fd`, - `--sort`, and `--limit`. Permanent bans that remain: `--pid --ppid` (AC-006.25) and - `--csv --json` (any surface). Allowed with `--pid`: `--json`, `--csv`, `--watch` - (and combinations unlocked by AC-007.86/87/91). - -- **AC-007.93:** The AC-007.84 parity suite MUST include a short CSV `--watch` frame-marker - smoke matrix for `proc --csv --watch`, `proc --tree --csv --watch`, `proc --pid --csv --watch`, - `report --format csv --watch`, `health|pool|status --csv --watch`, and `ps --all --csv --watch` - (AC-007.88–91). Each row MUST assert: leading `# sharecli-*-watch-frame` marker, >=1 complete - frame with body → gate → host_watch → pool → status companion order, stderr silent, no ANSI - clear, and a `# [watch]` footer comment. Dwell MUST stay short (single-frame smoke); multi-frame - dwell contracts remain in per-AC integration files. - -- **AC-007.94:** Every CSV `--watch` surface (AC-007.88–91) MUST flush stdout after emitting the - `# [watch]` footer comment so pipe consumers observe a complete frame (marker + body + companions - + footer) within the same tick — without waiting for the next tick's flush. Deferred footer - delivery via a subsequent frame's buffer flush is forbidden. - -- **AC-007.95:** The AC-007.84 parity suite MUST lock AC-007.94 same-tick `# [watch]` flush across - the full CSV `--watch` matrix (`proc --csv --watch`, `proc --tree --csv --watch`, - `proc --pid --csv --watch`, `report --format csv --watch`, `health|pool|status --csv --watch`, - and `ps --all --csv --watch`). For each surface, when the first `# [watch]` footer becomes - visible on a pipe, exactly one leading `# sharecli-*-watch-frame` marker MUST precede it. - -- **AC-007.96:** Text `--watch` surfaces that emit a stdout `[watch]` footer (`proc --watch`, - `proc --tree --watch`, `proc --pid --watch`, `report --watch` without `--format csv|json`) - MUST flush stdout after the footer so pipe consumers observe it in the same tick (parity with - CSV AC-007.94). Operator text `--watch` already flushes via `emit_operator_watch_footer`. - -- **AC-007.97:** The AC-007.84 parity suite MUST lock AC-007.96 same-tick text `[watch]` flush - across `proc --watch`, `proc --tree --watch`, `proc --pid --watch`, and `report --watch`. - For each surface, when the first `[watch]` footer becomes visible on a pipe, exactly one - `=== Thermal Gate (FR-011) ===` section MUST precede it. - -- **AC-007.98:** The AC-007.84 parity suite MUST also lock AC-007.96 same-tick text `[watch]` - flush for operator surfaces `health --watch`, `pool --watch`, `status --watch`, and - `ps --all --watch` (already flushed via `emit_operator_watch_footer`). Same one-gate-before- - footer contract as AC-007.97. - -**Test refs:** `tests/fr007_resource_thermal_watch.rs`, `tests/fr007_thermal_tui_watch.rs`, `tests/fr007_thermal_tui_gate_parity.rs`, `tests/fr007_thermal_tui_pool_status.rs`, `tests/fr004_status_health.rs`, `tests/fr007_proc_json_host_watch.rs`, `tests/fr007_proc_text_csv_host_watch.rs`, `tests/fr007_proc_tree_json_host_watch.rs`, `tests/fr007_proc_pid_json_host_watch.rs`, `tests/fr007_proc_pid_gate.rs`, `tests/fr007_proc_pid_watch.rs`, `tests/fr007_proc_csv_watch.rs`, `tests/fr007_proc_pid_csv_watch.rs`, `tests/fr007_proc_pid_combo_rejects.rs`, `tests/fr007_operator_csv_watch.rs`, `tests/fr007_report_csv_watch.rs`, `tests/fr007_proc_tree_json_gate.rs`, `tests/fr007_proc_text_csv_gate.rs`, `tests/fr007_proc_tree_text_gate.rs`, `tests/fr007_proc_text_gate.rs`, `tests/fr007_proc_watch_gate_order.rs`, `tests/fr007_proc_tree_watch_gate_order.rs`, `tests/fr007_proc_json_gate_order.rs`, `tests/fr007_status_json_host_watch.rs`, `tests/fr007_status_text_gate_order.rs`, `tests/fr007_proc_watch_stderr_footer.rs`, `tests/fr007_proc_tree_watch_stderr_footer.rs`, `tests/fr007_proc_json_stderr_silent.rs`, `tests/fr007_status_json_stderr_silent.rs`, `tests/fr007_proc_csv_stderr_silent.rs`, `tests/fr007_proc_csv_pool_status.rs`, `tests/fr007_report_csv_pool_status.rs`, `tests/fr007_report_csv_stderr_silent.rs`, `tests/fr007_health_pool_status_csv.rs`, `tests/fr007_ps_all_csv.rs`, `tests/fr007_operator_envelope_parity_suite.rs`, -`tests/fr007_proc_text_stderr_silent.rs`, `tests/fr007_proc_text_pool_status.rs`, `tests/fr007_proc_watch_text_stderr_silent.rs`, `tests/fr007_status_text_stderr_silent.rs`, `tests/fr007_health_pool_text_stderr_silent.rs`, `tests/fr007_health_watch_text_stderr_silent.rs`, `tests/fr007_health_watch_json_gate_host_watch.rs`, `tests/fr007_pool_watch_text_stderr_silent.rs`, `tests/fr007_pool_watch_json_gate_host_watch.rs`, `tests/fr007_status_watch_text_stderr_silent.rs`, `tests/fr007_status_watch_json_gate_host_watch.rs`, `tests/fr007_ps_all_text_stderr_silent.rs`, `tests/fr007_ps_all_json_gate_host_watch.rs`, `tests/fr007_ps_all_watch_json_gate_host_watch.rs`, `tests/fr007_ps_all_watch_text_stderr_silent.rs`, `tests/fr007_health_pool_status_ps_text_pool_status.rs`, `tests/fr007_operator_json_pool_status.rs`, `tests/fr007_ipc_health_pool_status.rs`, `tests/fr007_ws_client_health_update_pool_status.rs`, `tests/fr007_report_text_stderr_silent.rs`, `tests/fr007_report_text_pool_status.rs`, `tests/fr007_report_json_gate_host_watch.rs`, `tests/fr007_report_json_pool_status.rs`, `tests/fr007_report_watch_json_gate_host_watch.rs`, `tests/fr007_dashboard_ws_operator_envelope.rs`, `tests/fr007_health_pool_json_gate_host_watch.rs`, `tests/fr007_ipc_health_status_gate_host_watch.rs`, `tests/fr007_ipc_monitoring_report_gate_host_watch.rs`, `tests/fr007_ipc_monitoring_report_pool_status.rs`, `tests/fr007_ipc_pool_status_snapshot.rs`, `tests/fr007_ipc_pool_status_tray_wire.rs`, `tests/fr007_tray_pool_status_consume.rs`, `tests/fr007_tray_monitoring_report_consume.rs`, `tests/fr007_tray_windows_monitoring_report_consume.rs`, `tests/fr007_tray_windows_poll_interval.rs`, `tests/fr007_tray_linux_poll_interval.rs`, `tests/fr007_tray_swift_poll_interval.rs`, `tests/fr007_tray_windows_kill.rs`, `tests/fr007_tray_linux_kill.rs`, `tests/fr007_tray_swift_kill.rs`, `tests/fr007_tray_windows_harness.rs`, `tests/fr007_tray_gate_host_watch_ui.rs`, `tests/fr007_tray_thermal_visual.rs`, `src/commands/serve.rs` (`DashboardWsSnapshot`, `build_dashboard_ws_snapshot`), `src/commands/mod.rs` (`PsAllJson`, `PsAllNdjsonLine`, `HealthJson`, `HealthNdjsonLine`, `PoolJson`, `PoolNdjsonLine`, `StatusJson`, `StatusNdjsonLine`, `build_pool_json`, `build_status_json`, `fetch_operator_pool_status_siblings`, `print_live_pool_status_operator_sections`, `render_health_csv_body`, `render_pool_csv_body`, `render_status_csv_body`, `render_ps_all_csv_body`, `append_operator_csv_companions`, `ps --all --json`, `ps --all --csv`, `ps --all --watch --json`, `health --json`, `health --csv`, `health --watch`, `health --watch --json`, `pool --json`, `pool --csv`, `pool --watch`, `pool --watch --json`, `status --json`, `status --csv`, `status --watch`, `status --watch --json`), `src/commands/report.rs` (`FleetReportJson`, `FleetReportNdjsonLine`, `ReportFormat::Csv`, `REPORT_CSV_WATCH_FRAME_MARKER`, `render_report_csv_body`, `render_once`, `report --format csv --watch`), `src/commands/proc.rs` (`AgentProcSnapshot`, `AgentTreeSnapshot`, `PROC_PID_CSV_WATCH_FRAME_MARKER`, `append_proc_csv_companions`, `render_once` JSON path, `proc --pid --csv --watch`), `crates/sharecli-fleet/src/operator_pool_status.rs`, `crates/sharecli-ipc/src/handler.rs` (`HealthSnapshot`, `PoolSnapshot`, `StatusSnapshot`, `MonitoringReportSnapshot`, `health.status`, `pool.status`, `status.snapshot`, `monitoring.report`, `process.kill`, `process.kill_all`, `capture_pool_snapshot`, `capture_status_snapshot`), `crates/sharecli-ipc/src/ws_client.rs` (`ClientMessage`, `ClientMessage::from_json`, `SharecliClient`, `SharecliStream`), `crates/sharecli-tray-linux/src/ipc.rs`, `crates/sharecli-tray-linux/src/operator_display.rs`, `crates/sharecli-tray-linux/src/poll.rs`, `crates/sharecli-tray-linux/src/main.rs`, `desktop/ShareCLITray/Sources/ShareCLICore/OperatorDisplay.swift`, `desktop/ShareCLITray/Sources/ShareCLICore/AppState.swift`, `desktop/ShareCLITray/Sources/ShareCLICore/TrayPoll.swift`, `desktop/ShareCLITray/Sources/ShareCLICore/IPCClient.swift`, `desktop/ShareCLITray/Sources/ShareCLITray/TrayPopoverView.swift`, `desktop/ShareCLITray/Sources/ShareCLITray/DashboardView.swift`, `desktop/ShareCLITray/Sources/ShareCLITray/AppEntry.swift`, `crates/sharecli-tray-windows/src/ipc.rs`, `crates/sharecli-tray-windows/src/operator_display.rs`, `crates/sharecli-tray-windows/src/poll.rs`, `windows/ShareCLITray/MonitoringReportSnapshot.cs`, `windows/ShareCLITray/PoolStatusSnapshot.cs`, `windows/ShareCLITray/OperatorDisplay.cs`, `windows/ShareCLITray/IpcKill.cs`, `windows/ShareCLITray/TrayPoll.cs`, `windows/ShareCLITray/TrayWindow.xaml`, `windows/ShareCLITray/TrayWindow.xaml.cs`, `src/dashboard.html` + silent on success. `--csv` MUST NOT combine with `--json` or `--watch` (one-shot export only). + +**Test refs:** `tests/fr007_resource_thermal_watch.rs`, `tests/fr007_thermal_tui_watch.rs`, `tests/fr007_thermal_tui_gate_parity.rs`, `tests/fr007_thermal_tui_pool_status.rs`, `tests/fr004_status_health.rs`, `tests/fr007_proc_json_host_watch.rs`, `tests/fr007_proc_text_csv_host_watch.rs`, `tests/fr007_proc_tree_json_host_watch.rs`, `tests/fr007_proc_pid_json_host_watch.rs`, `tests/fr007_proc_pid_gate.rs`, `tests/fr007_proc_tree_json_gate.rs`, `tests/fr007_proc_text_csv_gate.rs`, `tests/fr007_proc_tree_text_gate.rs`, `tests/fr007_proc_text_gate.rs`, `tests/fr007_proc_watch_gate_order.rs`, `tests/fr007_proc_tree_watch_gate_order.rs`, `tests/fr007_proc_json_gate_order.rs`, `tests/fr007_status_json_host_watch.rs`, `tests/fr007_status_text_gate_order.rs`, `tests/fr007_proc_watch_stderr_footer.rs`, `tests/fr007_proc_tree_watch_stderr_footer.rs`, `tests/fr007_proc_json_stderr_silent.rs`, `tests/fr007_status_json_stderr_silent.rs`, `tests/fr007_proc_csv_stderr_silent.rs`, `tests/fr007_proc_csv_pool_status.rs`, `tests/fr007_report_csv_pool_status.rs`, `tests/fr007_report_csv_stderr_silent.rs`, `tests/fr007_health_pool_status_csv.rs`, `tests/fr007_proc_text_stderr_silent.rs`, `tests/fr007_proc_text_pool_status.rs`, `tests/fr007_proc_watch_text_stderr_silent.rs`, `tests/fr007_status_text_stderr_silent.rs`, `tests/fr007_health_pool_text_stderr_silent.rs`, `tests/fr007_health_watch_text_stderr_silent.rs`, `tests/fr007_health_watch_json_gate_host_watch.rs`, `tests/fr007_pool_watch_text_stderr_silent.rs`, `tests/fr007_pool_watch_json_gate_host_watch.rs`, `tests/fr007_status_watch_text_stderr_silent.rs`, `tests/fr007_status_watch_json_gate_host_watch.rs`, `tests/fr007_ps_all_text_stderr_silent.rs`, `tests/fr007_ps_all_json_gate_host_watch.rs`, `tests/fr007_ps_all_watch_json_gate_host_watch.rs`, `tests/fr007_ps_all_watch_text_stderr_silent.rs`, `tests/fr007_health_pool_status_ps_text_pool_status.rs`, `tests/fr007_operator_json_pool_status.rs`, `tests/fr007_ipc_health_pool_status.rs`, `tests/fr007_ws_client_health_update_pool_status.rs`, `tests/fr007_report_text_stderr_silent.rs`, `tests/fr007_report_text_pool_status.rs`, `tests/fr007_report_json_gate_host_watch.rs`, `tests/fr007_report_json_pool_status.rs`, `tests/fr007_report_watch_json_gate_host_watch.rs`, `tests/fr007_dashboard_ws_operator_envelope.rs`, `tests/fr007_health_pool_json_gate_host_watch.rs`, `tests/fr007_ipc_health_status_gate_host_watch.rs`, `tests/fr007_ipc_monitoring_report_gate_host_watch.rs`, `tests/fr007_ipc_monitoring_report_pool_status.rs`, `tests/fr007_ipc_pool_status_snapshot.rs`, `tests/fr007_ipc_pool_status_tray_wire.rs`, `tests/fr007_tray_pool_status_consume.rs`, `tests/fr007_tray_monitoring_report_consume.rs`, `tests/fr007_tray_windows_monitoring_report_consume.rs`, `tests/fr007_tray_windows_poll_interval.rs`, `tests/fr007_tray_linux_poll_interval.rs`, `tests/fr007_tray_swift_poll_interval.rs`, `tests/fr007_tray_windows_kill.rs`, `tests/fr007_tray_linux_kill.rs`, `tests/fr007_tray_swift_kill.rs`, `tests/fr007_tray_windows_harness.rs`, `tests/fr007_tray_gate_host_watch_ui.rs`, `tests/fr007_tray_thermal_visual.rs`, `src/commands/serve.rs` (`DashboardWsSnapshot`, `build_dashboard_ws_snapshot`), `src/commands/mod.rs` (`PsAllJson`, `PsAllNdjsonLine`, `HealthJson`, `HealthNdjsonLine`, `PoolJson`, `PoolNdjsonLine`, `StatusJson`, `StatusNdjsonLine`, `build_pool_json`, `build_status_json`, `fetch_operator_pool_status_siblings`, `print_live_pool_status_operator_sections`, `render_health_csv_body`, `render_pool_csv_body`, `render_status_csv_body`, `append_operator_csv_companions`, `ps --all --json`, `ps --all --watch --json`, `health --json`, `health --csv`, `health --watch`, `health --watch --json`, `pool --json`, `pool --csv`, `pool --watch`, `pool --watch --json`, `status --json`, `status --csv`, `status --watch`, `status --watch --json`), `src/commands/report.rs` (`FleetReportJson`, `FleetReportNdjsonLine`, `ReportFormat::Csv`, `render_report_csv_body`, `render_once`), `src/commands/proc.rs` (`AgentProcSnapshot`, `AgentTreeSnapshot`, `append_proc_csv_companions`, `render_once` JSON path), `crates/sharecli-fleet/src/operator_pool_status.rs`, `crates/sharecli-ipc/src/handler.rs` (`HealthSnapshot`, `PoolSnapshot`, `StatusSnapshot`, `MonitoringReportSnapshot`, `health.status`, `pool.status`, `status.snapshot`, `monitoring.report`, `process.kill`, `process.kill_all`, `capture_pool_snapshot`, `capture_status_snapshot`), `crates/sharecli-ipc/src/ws_client.rs` (`ClientMessage`, `ClientMessage::from_json`, `SharecliClient`, `SharecliStream`), `crates/sharecli-tray-linux/src/ipc.rs`, `crates/sharecli-tray-linux/src/operator_display.rs`, `crates/sharecli-tray-linux/src/poll.rs`, `crates/sharecli-tray-linux/src/main.rs`, `desktop/ShareCLITray/Sources/ShareCLICore/OperatorDisplay.swift`, `desktop/ShareCLITray/Sources/ShareCLICore/AppState.swift`, `desktop/ShareCLITray/Sources/ShareCLICore/TrayPoll.swift`, `desktop/ShareCLITray/Sources/ShareCLICore/IPCClient.swift`, `desktop/ShareCLITray/Sources/ShareCLITray/TrayPopoverView.swift`, `desktop/ShareCLITray/Sources/ShareCLITray/DashboardView.swift`, `desktop/ShareCLITray/Sources/ShareCLITray/AppEntry.swift`, `crates/sharecli-tray-windows/src/ipc.rs`, `crates/sharecli-tray-windows/src/operator_display.rs`, `crates/sharecli-tray-windows/src/poll.rs`, `windows/ShareCLITray/MonitoringReportSnapshot.cs`, `windows/ShareCLITray/PoolStatusSnapshot.cs`, `windows/ShareCLITray/OperatorDisplay.cs`, `windows/ShareCLITray/IpcKill.cs`, `windows/ShareCLITray/TrayPoll.cs`, `windows/ShareCLITray/TrayWindow.xaml`, `windows/ShareCLITray/TrayWindow.xaml.cs`, `src/dashboard.html` --- @@ -1009,23 +882,16 @@ be served from the coalesce cache. - **AC-008.16:** harness-native [`queue`](crates/harness-native/src/strategies/queue.rs) and [`priority_queue`](crates/harness-native/src/strategies/mod.rs) strategies MUST execute via [`Hypervisor::run_queued`](crates/sharecli-core/src/lib.rs) with [`SpawnRequest::from_operator`](crates/sharecli-core/src/lib.rs) (`rules.conf` `priority=` + env); MUST NOT use raw `Command::spawn`; repeated identical invocations MUST NOT set `from_cache`. - **AC-008.17:** harness-native [`coalesce`](crates/harness-native/src/strategies/coalesce.rs) and [`cache`](crates/harness-native/src/strategies/mod.rs) strategies MUST execute via [`Hypervisor::run`](crates/sharecli-core/src/lib.rs) with [`SpawnRequest::from_operator`](crates/sharecli-core/src/lib.rs); cache root MUST be `{harness_home}/var/sharecli-hypervisor`; [`RuleOpts`](crates/harness-native/src/strategies/mod.rs) `ttl=` / `debounce_ms=` / `max_concurrent=` MUST map into [`HypervisorConfig`](crates/sharecli-core/src/lib.rs); repeated identical invocations MUST set `from_cache` on replay; MUST NOT use raw `Command::spawn`. - **AC-008.18:** harness-native [`debounce`](crates/harness-native/src/strategies/debounce.rs) strategy MUST execute via [`Hypervisor::run`](crates/sharecli-core/src/lib.rs) with [`SpawnRequest::from_operator`](crates/sharecli-core/src/lib.rs) and [`RuleOpts`](crates/harness-native/src/strategies/mod.rs) `debounce_ms=` mapped into [`HypervisorConfig::coalesce_debounce`](crates/sharecli-core/src/lib.rs) via [`hypervisor_lane`](crates/harness-native/src/strategies/hypervisor_lane.rs); MUST share in-window sibling stores per AC-008.6; repeated identical invocations MUST set `from_cache` on replay; MUST NOT use raw `Command::spawn`. -- **AC-008.19:** [`CacheKeyMode`](crates/sharecli-ipc/src/cache_key.rs) (`time` / `args` / `git`) MUST be selectable via rules.conf `cache_key=` and plumbed through [`HypervisorConfig::cache_key_mode`](crates/sharecli-core/src/lib.rs) into [`command_key_with_mode`](crates/sharecli-ipc/src/cache_key.rs) on every [`Hypervisor::run`](crates/sharecli-core/src/lib.rs) coalesce path; `args` MUST hash argv only (CWD-independent); `git` MUST incorporate cwd + git porcelain + HEAD fingerprint (Feb `harness::cache_key` git branch); per-rule `nocache_args=` MUST override [`DEFAULT_NOCACHE_ARGS`](crates/sharecli-ipc/src/nocache.rs) when present, explicit empty `nocache_args=` MUST disable bypass for that rule, omitted `nocache_args` MUST keep Hypervisor defaults for harness [`build_hypervisor`](crates/harness-native/src/strategies/hypervisor_lane.rs). -- **AC-008.20:** When rules.conf `semantic=1`, [`HypervisorConfig::semantic`](crates/sharecli-core/src/lib.rs) MUST apply [`semantic_normalize_argv`](crates/sharecli-ipc/src/semantic.rs) (ruff/mypy/pylint/flake8 path normalization ported from Feb `harness::semantic::normalize`) before cache-key hashing; repeated semantically normalized lint invocations MUST coalesce on replay. -- **AC-008.21:** harness-native [`retry`](crates/harness-native/src/strategies/retry.rs), [`circuit_breaker`](crates/harness-native/src/strategies/circuit_breaker.rs), [`passthrough`](crates/harness-native/src/strategies/process.rs) / process-delegating strategies (`incremental`, `resource_throttle`, `jobserver`, `load_balance`, `speculative`, `proactive_warm`, `batch`, `causal_order`) MUST execute via [`Hypervisor::run`](crates/sharecli-core/src/lib.rs) with [`SpawnRequest::from_operator`](crates/sharecli-core/src/lib.rs) through [`hypervisor_lane`](crates/harness-native/src/strategies/hypervisor_lane.rs); MUST NOT use raw `Command::spawn`; open circuit breaker MUST fail loudly; retry exhaustion MUST surface non-zero exit. -**Test refs:** `tests/fr008_coalesce_mesh.rs`; `tests/fr008_queue_priority_operator.rs`; `tests/fr008_coalesce_status.rs`; `tests/fr004_status_health.rs`; `tests/fr007_thermal_tui_watch.rs`; `tests/e2e_hypervisor_nocache.rs`; `crates/harness-native/tests/native_harness_contract.rs`; `sharecli-core` `hypervisor_run_queued_skips_coalesce_cache`; `crates/harness-native/src/strategies/coalesce.rs` (`coalesce_strategy_executes_via_hypervisor`, `coalesce_strategy_serves_cache_on_replay`); `crates/harness-native/src/strategies/debounce.rs`; `crates/harness-native/src/strategies/retry.rs`; `crates/harness-native/src/strategies/circuit_breaker.rs`; `crates/harness-native/src/strategies/process.rs` (`debounce_strategy_executes_via_hypervisor`, `debounce_strategy_serves_cache_on_replay`, `debounce_strategy_shares_in_window_store`); `crates/harness-native/src/strategies/hypervisor_lane.rs` (`rule_opts_plumb_hypervisor_config`); `sharecli-ipc` unit tests for TTL/debounce/queue/nocache/meters. +**Test refs:** `tests/fr008_coalesce_mesh.rs`; `tests/fr008_queue_priority_operator.rs`; `tests/fr008_coalesce_status.rs`; `tests/fr004_status_health.rs`; `tests/fr007_thermal_tui_watch.rs`; `tests/e2e_hypervisor_nocache.rs`; `crates/harness-native/tests/native_harness_contract.rs`; `sharecli-core` `hypervisor_run_queued_skips_coalesce_cache`; `crates/harness-native/src/strategies/coalesce.rs` (`coalesce_strategy_executes_via_hypervisor`, `coalesce_strategy_serves_cache_on_replay`); `crates/harness-native/src/strategies/debounce.rs` (`debounce_strategy_executes_via_hypervisor`, `debounce_strategy_serves_cache_on_replay`, `debounce_strategy_shares_in_window_store`); `crates/harness-native/src/strategies/hypervisor_lane.rs` (`rule_opts_plumb_hypervisor_config`); `sharecli-ipc` unit tests for TTL/debounce/queue/nocache/meters. --- ## FR-009 — FUSE IO Intercept -**Statement:** On Linux, macOS, and Windows (WinFsp), sharecli MUST provide a -FUSE/WinFsp attach point (`InterceptFs` / `mount`) over a backing path as the -hypervisor IO intercept extension point. WSL2 uses the Linux path when -`/dev/fuse` is available. Other platforms MUST return a clear unsupported -error. Privileged mount smoke MUST be runnable via the OS×arch matrix -(`fuse-smoke` / `Containerfile.fuse-smoke`) without requiring a macOS host -reboot when the Linux-on-Mac (Colima) cell is used. +**Statement:** On Linux and macOS, sharecli MUST provide a FUSE attach point +(`InterceptFs` / `mount`) over a backing path as the hypervisor IO intercept +extension point. Other platforms MUST return a clear unsupported error. Core VFS ops (lookup, getattr, open, read, write, readdir, mkdir, unlink, rmdir, rename) MUST forward to the backing filesystem via an inode map. In-process read content cache MUST key by path+mtime with hit/miss meters. @@ -1047,8 +913,7 @@ until create / mkdir / rename-into invalidates the entry. - `crates/sharecli-fuse/src/write_serialize.rs` — `WriteSerialize` - `crates/sharecli-fuse/src/provenance.rs` — write provenance xattrs - `crates/sharecli-fuse/src/mount_smoke.rs` — opt-in privileged mount smoke -- `crates/sharecli-fuse/src/session_registry.rs` — process-local mount registry -- `src/commands/fuse.rs` — `sharecli fuse` operator surface +- `src/commands/fuse.rs` — `sharecli fuse provenance` **Acceptance Criteria:** @@ -1106,52 +971,8 @@ until create / mkdir / rename-into invalidates the entry. translate paths under the mount to backing equivalents (prefix-safe, `None` outside subtree). [`FuseGuard`](crates/sharecli-core/src/lib.rs) MUST remain mounted for the full coalesce spawn window and force-unmount on drop after the child exits. -- **AC-009.15:** FUSE `create` / `mknod` (regular files) and in-process [`create_rel`](crates/sharecli-fuse/src/lib.rs) - MUST create backing files without ENOSYS; successful create MUST stamp write provenance - (parity with AC-009.6), invalidate negative dentry + read cache (parity with AC-009.7). -- **AC-009.16:** Live FUSE [`Filesystem::write`](crates/sharecli-fuse/src/lib.rs) MUST call - [`record_passthrough_write`](crates/sharecli-fuse/src/write_serialize_meters.rs) on success - (parity with `write_rel` / AC-009.10). With `SHARECLI_FUSE_MOUNT_SMOKE=1`, [`run_mount_smoke`](crates/sharecli-fuse/src/mount_smoke.rs) - MUST also exercise create, mkdir, unlink, and rename round-trip through the mount. -- **AC-009.17:** `sharecli fuse` MUST expose operator subcommands: `mount`, `unmount`, `status`, - `commit`, `discard`, and `list` (in addition to `provenance`). [`FuseSessionRegistry`](crates/sharecli-fuse/src/session_registry.rs) - tracks background mounts (`mountpoint` → [`InterceptFs`]) so `commit`/`discard` operate on - staged CoW via `commit_rel`/`discard_rel`; `list` enumerates mounts and pending relative paths; - `status` prints global read-cache + write-serialize meter sections. -- **AC-009.18:** [`AgentsConf`](crates/sharecli-fuse/src/agents_conf.rs) MUST parse Feb-format - `agents.conf` (comments/blanks ignored; substring match) and validate/sanitize agent ids. -- **AC-009.19:** [`AgentCowStore`](crates/sharecli-fuse/src/agent_cow.rs) MUST isolate CoW staging - per agent under `{cow_root}/{agent}/` so two agents can hold pending edits for the same - backing path; `commit_all_for_agent` / `discard_all_for_agent` MUST promote or drop all - pending paths for one agent. CLI: `fuse mount --cow [--cow-dir] [--agent] [--agents-conf]` - and `fuse commit|discard [relpath] [--agent]`. -- **AC-009.20:** `fuse mount --no-serialize` MUST disable per-path write locks (Feb - `--no-serialize` parity) while still allowing CoW stage/commit. -- **AC-009.21:** `sharecli fuse mount` MUST loud-reject invalid `--agent` ids and missing - `--agents-conf` paths before attempting a FUSE mount. `fuse mount --help` MUST document - Feb-parity flags `--cow`, `--cow-dir`, `--agent`, `--agents-conf`, and `--no-serialize`. -- **AC-009.22:** [`fuse-smoke`](crates/fuse-smoke-runner) MUST run an OS×arch privileged - mount-smoke matrix with structured loud-fail reasons (never silent skip when a cell is - selected). [`Containerfile.fuse-smoke`](Containerfile.fuse-smoke) MUST provide a Linux - libfuse3 smoke image distinct from the hardened serve Containerfile. On macOS, - `mac_host_linux_colima` MUST be able to green without a host macFUSE reboot. -- **AC-009.23:** Matrix cells `macos_native`, `mac_host_linux_colima`, and `macos_vm_tart` - MUST be implemented; missing Driver Extension / Tart tooling MUST fail with - `driver_missing` / `tooling_missing`. -- **AC-009.24:** Matrix cell `wsl2` MUST run Linux FUSE smoke inside WSL2 when selected on - Windows; missing WSL or `/dev/fuse` MUST fail loudly. -- **AC-009.25:** On Windows, `mount` / `fuse-mount-smoke` MUST use WinFsp when installed - (`crates/sharecli-fuse/src/winfsp_mount.rs`); missing WinFsp MUST fail with - `winfsp_missing`. Write provenance MUST use NTFS ADS/EA via `xattr` and MUST fail - loudly when EA is unavailable (no silent skip). Matrix cell `windows_winfsp` MUST run - privileged smoke or fail with `winfsp_missing`. -- **AC-009.26:** CI workflow `fuse-mount-smoke.yml` MUST mirror matrix cells; local - `just fuse-smoke` remains the merge gate when Actions billing blocks runners. -- **AC-009.27:** On Windows, `fuse mount --cow` MUST register a [`CowMountHandle`](crates/sharecli-fuse/src/cow_session.rs) - so `fuse commit|discard` works via the session registry (same AgentCowStore semantics - as Linux/macOS). Missing `--cow` when staging MUST fail loudly. - -**Test refs:** `tests/fr009_fuse_intercept.rs`; `tests/fr009_fuse_cli.rs`; `tests/fr009_fuse_hypervisor_session.rs`; `tests/fr004_status_health.rs`; `tests/fr007_thermal_tui_watch.rs`; `sharecli-fuse` unit tests (`agents_conf`, `agent_cow`, `cow_session`, `winfsp_mount` probe); `fuse-smoke-runner` unit tests; `docs/ops/fuse-mount-smoke-matrix.md`; `docs/ops/winfsp-fuse-mount.md`. + +**Test refs:** `tests/fr009_fuse_intercept.rs`; `tests/fr009_fuse_cli.rs`; `tests/fr009_fuse_hypervisor_session.rs`; `tests/fr004_status_health.rs`; `tests/fr007_thermal_tui_watch.rs`; `sharecli-fuse` unit tests. --- diff --git a/docs/specs/TRACEABILITY.md b/docs/specs/TRACEABILITY.md index b18ef376..c4cfa877 100644 --- a/docs/specs/TRACEABILITY.md +++ b/docs/specs/TRACEABILITY.md @@ -212,24 +212,8 @@ | AC-007.78 | `tests/fr007_ipc_health_pool_status.rs`; `tests/fr007_ipc_health_status_gate_host_watch.rs`; `tests/fr007_ipc_pool_status_snapshot.rs`; `crates/sharecli-ipc/src/handler.rs` (`HealthSnapshot`, `PoolSnapshot`, `StatusSnapshot`, `health.status`, `pool.status`, `status.snapshot`, `capture_pool_snapshot`, `capture_status_snapshot`) | IPC health.status embeds pool + status; pool.status nested status only; status.snapshot nested pool only; gate → host_watch → siblings key order; no cross-sibling recursion | | AC-007.79 | `tests/fr007_proc_csv_pool_status.rs`; `tests/fr007_proc_csv_stderr_silent.rs`; `tests/fr007_proc_text_csv_gate.rs`; `tests/fr007_proc_text_csv_host_watch.rs`; `src/commands/proc.rs` (`append_proc_csv_companions`); `crates/sharecli-fleet/src/operator_pool_status.rs` (`PoolOperatorPanel::format_csv_companion`, `StatusOperatorPanel::format_csv_companion`) | proc --csv / --tree --csv gate → host_watch → pool → status companion rows on stdout; stderr silent on success (extends AC-007.33) | | AC-007.80 | `tests/fr007_ws_client_health_update_pool_status.rs`; `crates/sharecli-ipc/src/ws_client.rs` (`ClientMessage`, `ClientMessage::from_json`, `HealthUpdate`); `crates/sharecli-ipc/src/handler.rs` (`HealthSnapshot`) | WS `health_update` decodes expanded HealthSnapshot pool + status; IPC health.status wrap roundtrip; legacy missing siblings → Unknown | -| AC-007.81 | `tests/fr007_report_csv_pool_status.rs`; `tests/fr007_report_csv_stderr_silent.rs`; `src/commands/report.rs` (`ReportFormat::Csv`, `render_report_csv_body`, `append_report_csv_companions`, `render_once` CSV path); `crates/sharecli-fleet/src/operator_pool_status.rs` | report --format csv fleet body → gate → host_watch → pool → status companion rows on stdout; stderr silent on success (parity with proc CSV AC-007.79); `--format csv --watch` per AC-007.90 | -| AC-007.82 | `tests/fr007_health_pool_status_csv.rs`; `src/commands/mod.rs` (`render_health_csv_body`, `render_pool_csv_body`, `render_status_csv_body`, `append_operator_csv_companions`, `health --csv`, `pool --csv`, `status --csv`); `src/main.rs` (`Health.csv`, `Pool.csv`, `Status.csv`); `crates/sharecli-fleet/src/operator_pool_status.rs` | health/pool/status --csv command body → gate → host_watch → pool → status companion rows on stdout; rejects --json; stderr silent on success (parity with proc/report CSV AC-007.79/81); `--csv --watch` per AC-007.89 | -| AC-007.83 | `tests/fr007_ps_all_csv.rs`; `src/commands/mod.rs` (`render_ps_all_csv_body`, `append_operator_csv_companions`, `ps --all --csv`); `src/main.rs` (`Ps.csv`); `crates/sharecli-fleet/src/operator_pool_status.rs` | ps --all --csv managed + agent-inventory body → gate → host_watch → pool → status companion rows on stdout; requires --all; rejects --json; stderr silent on success (parity with health/pool/status CSV AC-007.82); `--csv --watch` per AC-007.89 | -| AC-007.84 | `tests/fr007_operator_envelope_parity_suite.rs`; `src/commands/mod.rs`; `src/commands/report.rs`; `src/commands/proc.rs`; `src/dashboard.html`; `crates/sharecli-ipc/src/handler.rs`; `crates/sharecli-ipc/src/ws_client.rs`; `crates/sharecli-tray-linux/src/operator_display.rs`; `crates/sharecli-tray-windows/src/operator_display.rs`; `crates/sharecli-thermal-tui/src/lib.rs`; `desktop/ShareCLITray/Sources/ShareCLICore/OperatorDisplay.swift`; `windows/ShareCLITray/OperatorDisplay.cs` | Meta regression locks operator envelope matrix: CLI text/JSON/CSV one-shot (proc/report/health/pool/status/ps --all), IPC health.status + monitoring.report, WS health_update decode, dashboard/tray/TUI companion markers; no watch dwell | -| AC-007.85 | `tests/fr007_operator_envelope_parity_suite.rs`; `src/commands/proc.rs` (`render_once` text/JSON/CSV tree paths); `src/commands/mod.rs` (`print_live_pool_status_operator_sections`, `append_proc_csv_companions`); `crates/sharecli-fleet/src/operator_pool_status.rs` | Parity suite proc --tree text/JSON/CSV one-shot rows lock gate → host_watch → pool → status envelope (parity with flat proc AC-007.75/77/79) | -| AC-007.86 | `tests/fr007_operator_envelope_parity_suite.rs`; `src/commands/proc.rs` (`ProcDetailSnapshot`, `render_proc_detail_text`, `render_proc_detail_csv`, `render_pid_detail`); `src/commands/mod.rs` (`print_live_pool_status_operator_sections`, `append_proc_csv_companions`); `crates/sharecli-fleet/src/operator_pool_status.rs` | Parity suite proc --pid text/JSON/CSV one-shot rows lock gate → host_watch → pool → status envelope via self-PID (parity with flat proc AC-007.75/77/79) | -| AC-007.87 | `tests/fr007_proc_pid_watch.rs`; `src/commands/proc.rs` (`ProcDetailNdjsonLine`, `render_pid_detail_once`, `run` pid watch loop); `src/commands/mod.rs` (`print_live_pool_status_operator_sections`, `fetch_operator_pool_status_siblings`) | proc --pid --watch text/NDJSON refresh parity with flat proc --watch (AC-007.35/28); `--pid --csv --watch` per AC-007.91 | -| AC-007.88 | `tests/fr007_proc_csv_watch.rs`; `src/commands/proc.rs` (`PROC_CSV_WATCH_FRAME_MARKER`, `run` CSV watch loop, `render_once` CSV paths); `src/commands/mod.rs` (`append_proc_csv_companions`); `crates/sharecli-fleet/src/operator_pool_status.rs` | proc --csv / --tree --csv --watch frame marker + full CSV envelope each tick on stdout; stderr silent; rejects --csv --json --watch | -| AC-007.89 | `tests/fr007_operator_csv_watch.rs`; `src/commands/mod.rs` (`HEALTH_CSV_WATCH_FRAME_MARKER`, `POOL_CSV_WATCH_FRAME_MARKER`, `STATUS_CSV_WATCH_FRAME_MARKER`, `PS_CSV_WATCH_FRAME_MARKER`, `emit_operator_csv_watch_frame`, `emit_operator_watch_footer`, `health --csv --watch`, `pool --csv --watch`, `status --csv --watch`, `ps --all --csv --watch`); `crates/sharecli-fleet/src/operator_pool_status.rs` | health/pool/status/ps --all --csv --watch frame marker + full CSV envelope each tick on stdout; stderr silent; rejects --csv --json --watch and ps --csv --watch without --all | -| AC-007.90 | `tests/fr007_report_csv_watch.rs`; `src/commands/report.rs` (`REPORT_CSV_WATCH_FRAME_MARKER`, `run` CSV watch loop, `render_once` CSV path); `crates/sharecli-fleet/src/operator_pool_status.rs` | report --format csv --watch frame marker + full fleet CSV envelope each tick on stdout; stderr silent; pipe-safe (no ANSI clear) | -| AC-007.91 | `tests/fr007_proc_pid_csv_watch.rs`; `src/commands/proc.rs` (`PROC_PID_CSV_WATCH_FRAME_MARKER`, `run` pid CSV watch loop, `render_pid_detail` CSV path); `src/commands/mod.rs` (`append_proc_csv_companions`); `crates/sharecli-fleet/src/operator_pool_status.rs` | proc --pid --csv --watch frame marker + PID CSV envelope each tick on stdout; stderr silent; rejects --pid --csv --json --watch | -| AC-007.92 | `tests/fr007_proc_pid_combo_rejects.rs`; `src/commands/proc.rs` (`reject_pid_inventory_combos`, `run`) | proc --pid loud-rejects inventory flags (--tree/--family/--sort/--limit/…); --pid --csv/--json/--watch remain allowed | -| AC-007.93 | `tests/fr007_operator_envelope_parity_suite.rs` (`fr007_operator_matrix_cli_csv_watch_frame_smoke`) | Parity suite short CSV --watch frame-marker smoke for proc/tree/pid/report/health/pool/status/ps (AC-007.88–91); no long multi-frame dwell | -| AC-007.94 | `tests/fr007_proc_csv_watch.rs` (`fr007_proc_csv_watch_footer_flushed_same_tick`); `src/commands/proc.rs`; `src/commands/report.rs`; `src/commands/mod.rs` (`emit_operator_csv_watch_footer`) | CSV --watch flushes `# [watch]` footer in the same tick (no deferred delivery via next frame) | -| AC-007.95 | `tests/fr007_operator_envelope_parity_suite.rs` (`fr007_operator_matrix_cli_csv_watch_footer_same_tick`) | Parity suite same-tick `# [watch]` flush matrix for all AC-007.93 CSV watch surfaces | -| AC-007.96 | `tests/fr007_proc_watch_text_stderr_silent.rs` (`fr007_proc_watch_text_footer_flushed_same_tick`); `src/commands/proc.rs`; `src/commands/report.rs` | Text `--watch` flushes `[watch]` footer same tick (proc/report; operator already flushed) | -| AC-007.97 | `tests/fr007_operator_envelope_parity_suite.rs` (`fr007_operator_matrix_cli_text_watch_footer_same_tick`) | Parity suite same-tick text `[watch]` flush matrix for proc/tree/pid/report | -| AC-007.98 | `tests/fr007_operator_envelope_parity_suite.rs` (`fr007_operator_matrix_cli_text_watch_footer_same_tick` + health/pool/status/ps rows) | Extends text flush matrix to operator health/pool/status/ps --all --watch | +| AC-007.81 | `tests/fr007_report_csv_pool_status.rs`; `tests/fr007_report_csv_stderr_silent.rs`; `src/commands/report.rs` (`ReportFormat::Csv`, `render_report_csv_body`, `append_report_csv_companions`, `render_once` CSV path); `crates/sharecli-fleet/src/operator_pool_status.rs` | report --format csv fleet body → gate → host_watch → pool → status companion rows on stdout; rejects --watch; stderr silent on success (parity with proc CSV AC-007.79) | +| AC-007.82 | `tests/fr007_health_pool_status_csv.rs`; `src/commands/mod.rs` (`render_health_csv_body`, `render_pool_csv_body`, `render_status_csv_body`, `append_operator_csv_companions`, `health --csv`, `pool --csv`, `status --csv`); `src/main.rs` (`Health.csv`, `Pool.csv`, `Status.csv`); `crates/sharecli-fleet/src/operator_pool_status.rs` | health/pool/status --csv command body → gate → host_watch → pool → status companion rows on stdout; rejects --json/--watch; stderr silent on success (parity with proc/report CSV AC-007.79/81) | ### FR-008 — Coalesce @@ -248,9 +232,6 @@ | AC-008.16 | `crates/sharecli-core/src/lib.rs` (`Hypervisor::run_queued`, `hypervisor_run_queued_skips_coalesce_cache`); `crates/harness-native/src/strategies/queue.rs` (`queue_strategy_executes_via_hypervisor`, `priority_queue_strategy_executes_via_hypervisor`) | harness queue/priority_queue → Hypervisor nocache lane via `from_operator` | | AC-008.17 | `crates/sharecli-core/src/lib.rs` (`Hypervisor::from_config`, `HypervisorConfig::coalesce_ttl`, `coalesce_ttl`); `crates/harness-native/src/strategies/coalesce.rs` (`coalesce_strategy_executes_via_hypervisor`, `coalesce_strategy_serves_cache_on_replay`, `cache_strategy_executes_via_hypervisor`); `crates/harness-native/src/strategies/hypervisor_lane.rs` (`rule_opts_plumb_hypervisor_config`, `config_from_rule_opts`) | harness coalesce/cache → Hypervisor::run via from_operator + RuleOpts ttl/debounce/max_concurrent | | AC-008.18 | `crates/harness-native/src/strategies/debounce.rs` (`debounce_strategy_executes_via_hypervisor`, `debounce_strategy_serves_cache_on_replay`, `debounce_strategy_shares_in_window_store`); `crates/harness-native/src/strategies/hypervisor_lane.rs` (`config_from_rule_opts`, `build_hypervisor`) | harness debounce → Hypervisor::run via hypervisor_lane + debounce_ms → coalesce_debounce | -| AC-008.19 | `crates/sharecli-ipc/src/cache_key.rs` (`cache_key_args_mode_ignores_cwd_and_env`, `cache_key_git_mode_parse_and_differs_from_args`); `tests/fr008_coalesce_mesh.rs` (`fr008_hypervisor_args_cache_key_mode_ignores_cwd`); `crates/harness-native/src/strategies/hypervisor_lane.rs` (`rule_opts_plumb_cache_key_mode_and_semantic`, `build_hypervisor_rule_nocache_args_*`, `build_hypervisor_omitted_nocache_args_keeps_defaults`); `crates/sharecli-core/src/lib.rs` (`HypervisorConfig::cache_key_mode`, `Hypervisor::run`) | CacheKeyMode time/args/git + per-rule nocache_args override semantics | -| AC-008.20 | `crates/sharecli-ipc/src/semantic.rs` (`semantic_normalize_dot_to_project_root`, `semantic_normalize_canonicalizes_directory`); `tests/fr008_coalesce_mesh.rs` (`fr008_hypervisor_semantic_coalesces_repeated_lint_dot`); `crates/sharecli-core/src/lib.rs` (`HypervisorConfig::semantic`, `Hypervisor::run`) | semantic lint argv normalization before cache hash | -| AC-008.21 | `crates/harness-native/src/strategies/retry.rs`; `crates/harness-native/src/strategies/circuit_breaker.rs`; `crates/harness-native/src/strategies/process.rs`; `crates/harness-native/src/strategies/mod.rs` | harness retry/circuit_breaker/passthrough (+ process delegates) → Hypervisor::run; no raw Command::spawn | ### FR-009 — FUSE @@ -269,19 +250,6 @@ | AC-009.12 | `tests/fr009_fuse_hypervisor_session.rs`; `sharecli-core` `fuse_session_id_for_command_key`; `sharecli-fuse` `mount_with_session` | Hypervisor FUSE session from coalesce CommandKey | | AC-009.13 | `tests/fr009_fuse_hypervisor_session.rs` (`fr009_hypervisor_spawn_outcome_fuse_session_id`); `sharecli-core` `SpawnOutcome::fuse_session_id` | SpawnOutcome exposes FUSE session when intercept active | | AC-009.14 | `tests/fr009_fuse_hypervisor_session.rs` (`fr009_remap_mount_to_backing_subtree`, `fr009_hypervisor_spawn_outcome_fuse_path_remap`); `sharecli-fuse` `path_remap.rs`; `sharecli-core` `SpawnOutcome::remap_fuse_path`, `FuseGuard` teardown | FUSE mount/backing remap + spawn/teardown lifecycle | -| AC-009.15 | `tests/fr009_fuse_intercept.rs` (`fr009_create_rel_provenance_and_neg_invalidate`); `crates/sharecli-fuse/src/lib.rs` FUSE `create`/`mknod` | create files through intercept; provenance + cache/dentry invalidate | -| AC-009.16 | `crates/sharecli-fuse/src/lib.rs` (`Filesystem::write` + `record_passthrough_write`); `tests/fr009_fuse_intercept.rs` (`fr009_privileged_mount_smoke`); `mount_smoke` extended smoke | live write metering + create/mkdir/unlink/rename mount smoke | -| AC-009.17 | `tests/fr009_fuse_cli.rs`; `src/commands/fuse.rs`; `crates/sharecli-fuse/src/session_registry.rs` | fuse mount/unmount/status/list/commit/discard CLI + registry | -| AC-009.18 | `crates/sharecli-fuse/src/agents_conf.rs` (`ac_009_18_*`); `src/commands/fuse.rs` | Feb `agents.conf` parse + agent id sanitize/validate | -| AC-009.19 | `crates/sharecli-fuse/src/agent_cow.rs` (`ac_009_19_*`); `src/commands/fuse.rs` (`--cow` / `--agent`) | Per-agent CoW isolation + commit_all/discard_all | -| AC-009.20 | `crates/sharecli-fuse/src/agent_cow.rs` (`ac_009_20_no_serialize_runs_callback`); `src/commands/fuse.rs` (`--no-serialize`) | `--no-serialize` skips per-path write locks | -| AC-009.21 | `tests/fr009_fuse_cli.rs` (`fr009_cli_fuse_mount_rejects_*`, `fr009_cli_fuse_mount_help_documents_feb_flags`); `src/commands/fuse.rs` | CLI loud-reject invalid agent / missing agents.conf; help documents Feb flags | -| AC-009.22 | `crates/fuse-smoke-runner`; `Containerfile.fuse-smoke`; `docs/ops/fuse-mount-smoke-matrix.md`; `just fuse-smoke` | OS×arch privileged mount-smoke matrix + Linux container / Colima path (no host macFUSE reboot) | -| AC-009.23 | `crates/fuse-smoke-runner` (`macos_native`, `mac_host_linux_colima`, `macos_vm_tart`) | macOS native + Mac-hosted Colima/Tart cells | -| AC-009.24 | `crates/fuse-smoke-runner` (`wsl2`); `docs/ops/fuse-mount-smoke-matrix.md` | WSL2 Linux FUSE smoke cell | -| AC-009.25 | `crates/sharecli-fuse/src/winfsp_mount.rs`; `session_registry` Windows mounts; `docs/ops/winfsp-fuse-mount.md` | Windows WinFsp mount + smoke; loud `winfsp_missing` when absent | -| AC-009.26 | `.github/workflows/fuse-mount-smoke.yml` | CI matrix mirroring local fuse-smoke cells | -| AC-009.27 | `crates/sharecli-fuse/src/cow_session.rs`; Windows `session_registry` + `winfsp_mount` | WinFsp `--cow` + `fuse commit|discard` via shared CowMountHandle | ### FR-010 — Mesh @@ -315,66 +283,6 @@ ## Change log -- **2026-07-23 — FR-009 FUSE mount-smoke matrix (AC-009.22..26):** - `fuse-smoke-runner` + `Containerfile.fuse-smoke` + Colima/mac/WSL/WinFsp cells; - workflow `fuse-mount-smoke.yml`; docs `fuse-mount-smoke-matrix.md`. -- **2026-07-22 — FR-009 fuse CLI Feb-flag validation + TRACE backfill (AC-009.18..21):** - TRACE rows for agents.conf / AgentCowStore / `--no-serialize`; AC-009.21 CLI loud-rejects - invalid `--agent` and missing `--agents-conf`, and locks `fuse mount --help` Feb flags. -- **2026-07-21 — FR-008 harness retry/circuit_breaker/process via Hypervisor (AC-008.21):** `retry`, `circuit_breaker`, and `passthrough`/process-delegating strategies execute via `Hypervisor::run` + `SpawnRequest::from_operator`; open circuit fails loudly; retry exhaustion surfaces non-zero exit. -- **2026-07-21 — FR-008 cache_key modes + semantic normalization (AC-008.19..20):** `CacheKeyMode` time/args/git plumbed via rules.conf; per-rule `nocache_args=` override; `semantic=1` applies lint argv normalization before cache hash. -- **2026-07-21 — FR-007 proc --pid --watch refresh surfaces (AC-007.87):** - `tests/fr007_proc_pid_watch.rs` locks text/NDJSON watch parity for `proc --pid N --watch`; - `render_pid_detail_once` + pid watch loop in `src/commands/proc.rs`; `--pid --csv --watch` - unlocked per AC-007.91. -- **2026-07-22 — FR-007 operator text --watch same-tick flush matrix (AC-007.98):** - Extends `TEXT_WATCH_MATRIX` with health/pool/status/ps --all --watch rows under the same - one-gate-before-footer contract as AC-007.97. -- **2026-07-22 — FR-007 text --watch same-tick flush matrix (AC-007.97):** - `fr007_operator_matrix_cli_text_watch_footer_same_tick` locks one gate before first - `[watch]` across proc/tree/pid/report text watch surfaces. -- **2026-07-22 — FR-007 text --watch footer same-tick flush (AC-007.96):** - Flush stdout after text `[watch]` on proc/report watch loops; locked by - `fr007_proc_watch_text_footer_flushed_same_tick` (exactly one gate before first footer). -- **2026-07-22 — FR-007 CSV watch same-tick flush matrix (AC-007.95):** - `fr007_operator_matrix_cli_csv_watch_footer_same_tick` locks one frame marker before first - `# [watch]` across proc/tree/pid/report/health/pool/status/ps CSV watch surfaces. -- **2026-07-22 — FR-007 CSV watch footer same-tick flush (AC-007.94):** - Flush stdout after `# [watch]` on proc/report/operator CSV `--watch` loops so pipe - consumers see a complete frame without waiting for the next tick; locked by - `fr007_proc_csv_watch_footer_flushed_same_tick`. -- **2026-07-22 — FR-007 CSV watch frame smoke in parity suite (AC-007.93):** - `fr007_operator_matrix_cli_csv_watch_frame_smoke` locks short single-frame marker + envelope - checks for AC-007.88–91 surfaces without multi-frame dwell. -- **2026-07-22 — FR-007 proc --pid inventory combo rejects (AC-007.92):** - `reject_pid_inventory_combos` + `tests/fr007_proc_pid_combo_rejects.rs`; AC-006.24 - updated so `--pid --csv` points at AC-007.86. -- **2026-07-22 — FR-007 proc --pid --csv --watch (AC-007.91):** - `tests/fr007_proc_pid_csv_watch.rs` locks frame marker + CSV envelope for - `proc --pid N --csv --watch`; `PROC_PID_CSV_WATCH_FRAME_MARKER` in `src/commands/proc.rs`. -- **2026-07-22 — FR-007 report --format csv --watch (AC-007.90):** - `tests/fr007_report_csv_watch.rs` locks `# sharecli-report-watch-frame` + fleet CSV envelope - each tick; `REPORT_CSV_WATCH_FRAME_MARKER` in `src/commands/report.rs`. -- **2026-07-22 — FR-007 operator health/pool/status/ps --csv --watch (AC-007.89):** - `tests/fr007_operator_csv_watch.rs` locks per-command frame markers + CSV envelope; - `emit_operator_csv_watch_frame` / `emit_operator_watch_footer` in `src/commands/mod.rs`. -- **2026-07-22 — FR-007 proc --csv / --tree --csv --watch (AC-007.88):** - `tests/fr007_proc_csv_watch.rs` locks `# sharecli-proc-watch-frame` + inventory CSV envelope; - `PROC_CSV_WATCH_FRAME_MARKER` in `src/commands/proc.rs`. -- **2026-07-21 — FR-007 proc --pid operator envelope parity suite rows (AC-007.86):** - `tests/fr007_operator_envelope_parity_suite.rs` adds `proc --pid` text/JSON/CSV one-shot matrix - rows (self-PID) locking gate → host_watch → pool → status companions; `render_pid_detail` - embeds pool/status on JSON, operator lines on text, and CSV companions on `--csv`. -- **2026-07-21 — FR-007 proc --tree operator envelope parity suite rows (AC-007.85):** - `tests/fr007_operator_envelope_parity_suite.rs` adds `proc --tree` text/JSON/CSV one-shot matrix - rows locking gate → host_watch → pool → status companions (implementation from AC-007.75/77/79). -- **2026-07-21 — FR-007 operator envelope matrix parity suite (AC-007.84):** `tests/fr007_operator_envelope_parity_suite.rs` - locks CLI text/JSON/CSV one-shot matrix (proc/report/health/pool/status/ps --all), IPC - `health.status` + `monitoring.report`, WS `health_update` decode, dashboard/tray/TUI companion - markers; no long `--watch` dwell cycles. -- **2026-07-21 — FR-007 ps --all --csv operator companions (AC-007.83):** `sharecli ps --all --csv` - emits managed-process + agent-inventory CSV body then companion `gate` → `host_watch` → `pool` → - `status` records; requires `--all`; rejects `--json` / `--watch`; stderr silent on success. - **2026-07-21 — FR-007 health/pool/status CSV operator companions (AC-007.82):** `sharecli health --csv`, `pool --csv`, and `status --csv` emit command-specific CSV bodies then companion `gate` → `host_watch` → `pool` → `status` records (parity with proc CSV AC-007.79 and report @@ -724,8 +632,6 @@ - **2026-07-20 — FR-007 thermal TUI dashboard slice:** `sharecli thermal` polls ResourceWatchSample + FUSE read-coalesce meters each redraw (AC-007.9 TUI + AC-007.11); formalized AC-007.7..9 in FR.md. -- **2026-07-21 — FR-008 cache key modes + semantic + per-rule nocache:** `CacheKeyMode` - (`time`/`args`/`git`), `semantic_normalize_argv`, `RuleOpts.nocache_args` (AC-008.19..20). - **2026-07-20 — FR-008 Hypervisor nocache e2e:** AC-008.10 side-effect re-exec + concurrent SlotQueue serialize + coalesce isolation (`tests/e2e_hypervisor_nocache.rs`). @@ -740,9 +646,6 @@ `SHARECLI_FUSE_MOUNT_SMOKE=1` (`run_mount_smoke`, AC-009.8). - **2026-07-20 — FR-009 write provenance:** `user.sharecli.session` / `user.sharecli.written_at` stamped on `write_rel` / `commit_rel` (AC-009.6). -- **2026-07-21 — FR-009 FUSE gaps:** FUSE `create`/`mknod`, live `write` passthrough metering, - extended mount smoke (create/mkdir/unlink/rename), `FuseSessionRegistry`, and `sharecli fuse` - mount/unmount/status/list/commit/discard CLI (AC-009.15..17). - **2026-07-19 — FR-009/010 A+ closeout:** CoW `stage_bytes`/`commit_pending`/ `discard_pending` (AC-009.5); `SmartMerger` + `WorktreePool` (AC-010.7..8). - **2026-07-19 — FR-009 A+ recovery:** InterceptFs passthrough + inode map + diff --git a/src/agent_call_policy.rs b/src/agent_call_policy.rs new file mode 100644 index 00000000..f434e968 --- /dev/null +++ b/src/agent_call_policy.rs @@ -0,0 +1,211 @@ +//! Pure admission policy for agent-issued commands. + +use std::cell::Cell; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +const DEFAULT_DEADLINE: Duration = Duration::from_secs(30); + +/// The reason an agent call must wait before it can run. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PauseCode { + /// The command would search a host-level or otherwise unsafe root. + HazardousRoot, + /// The configured per-project call limit has been reached. + ProjectLimit, + /// The host has no thermal headroom for a new call. + Thermal, + /// The configured build-command slot limit has been reached. + BuildSlot, +} + +/// An admitted command or a structured pause instruction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AgentCallDecision { + command: String, + pause_code: Option, + resume_condition: Option, + deadline: Duration, +} + +impl AgentCallDecision { + /// The command after policy normalization. + pub fn command(&self) -> &str { + &self.command + } + + /// The reason the command is paused, when admission was refused. + pub fn pause_code(&self) -> Option { + self.pause_code + } + + /// A human-readable condition that permits retrying a paused command. + pub fn resume_condition(&self) -> Option<&str> { + self.resume_condition.as_deref() + } + + /// The bounded execution deadline for this decision. + pub fn deadline(&self) -> Duration { + self.deadline + } +} + +/// Deterministic, local-only admission policy for agent calls. +#[derive(Debug)] +pub struct AgentCallPolicy { + project_root: PathBuf, + project_limit: usize, + admitted_calls: Cell, + thermal_headroom: bool, + build_slots: usize, + admitted_builds: Cell, +} + +impl AgentCallPolicy { + /// Create a policy scoped to `project_root` with unrestricted local limits. + pub fn new(project_root: PathBuf) -> Self { + Self { + project_root, + project_limit: usize::MAX, + admitted_calls: Cell::new(0), + thermal_headroom: true, + build_slots: usize::MAX, + admitted_builds: Cell::new(0), + } + } + + /// Set the maximum number of admitted calls for this project. + pub fn with_project_limit(mut self, limit: usize) -> Self { + self.project_limit = limit; + self + } + + /// Set whether the host has headroom for another call. + pub fn with_thermal_headroom(mut self, available: bool) -> Self { + self.thermal_headroom = available; + self + } + + /// Set the number of available build-command slots. + pub fn with_build_slots(mut self, slots: usize) -> Self { + self.build_slots = slots; + self + } + + /// Normalize and admit a command, or return a pause decision. + pub fn admit(&self, command: &str) -> AgentCallDecision { + let normalized = self.normalize(command); + + if targets_hazardous_root(&normalized) { + return self.paused( + normalized, + PauseCode::HazardousRoot, + "use a path inside the project root", + ); + } + if !self.thermal_headroom { + return self.paused(normalized, PauseCode::Thermal, "wait for thermal headroom"); + } + if self.admitted_calls.get() >= self.project_limit { + return self.paused( + normalized, + PauseCode::ProjectLimit, + "wait for an active project call to finish", + ); + } + + let build = is_build_command(&normalized); + if build && self.admitted_builds.get() >= self.build_slots { + return self.paused( + normalized, + PauseCode::BuildSlot, + "wait for an available build slot", + ); + } + + self.admitted_calls.set(self.admitted_calls.get().saturating_add(1)); + if build { + self.admitted_builds.set(self.admitted_builds.get().saturating_add(1)); + } + + AgentCallDecision { + command: normalized, + pause_code: None, + resume_condition: None, + deadline: DEFAULT_DEADLINE, + } + } + + fn normalize(&self, command: &str) -> String { + let words: Vec<_> = command.split_whitespace().collect(); + let Some(program) = words.first() else { + return command.to_owned(); + }; + + if !matches!(*program, "grep" | "egrep") || !has_recursive_flag(&words[1..]) { + return command.to_owned(); + } + + let mut positional = words[1..].iter().copied().filter(|word| !word.starts_with('-')); + let pattern = positional.next().unwrap_or(""); + let target = match positional.next() { + Some(".") | None => self.project_root.as_path(), + Some(target) => Path::new(target), + }; + format!( + "rg --hidden --glob '!target' --glob '!node_modules' {pattern} {}", + target.display() + ) + } + + fn paused( + &self, + command: String, + pause_code: PauseCode, + resume_condition: &str, + ) -> AgentCallDecision { + AgentCallDecision { + command, + pause_code: Some(pause_code), + resume_condition: Some(resume_condition.to_owned()), + deadline: DEFAULT_DEADLINE, + } + } +} + +fn has_recursive_flag(words: &[&str]) -> bool { + words.iter().any(|word| { + *word == "--recursive" + || word.starts_with('-') && word[1..].chars().any(|flag| matches!(flag, 'r' | 'R')) + }) +} + +fn is_build_command(command: &str) -> bool { + matches!(command.split_whitespace().next(), Some("cargo" | "make" | "just")) +} + +fn targets_hazardous_root(command: &str) -> bool { + command.split_whitespace().any(|word| is_hazardous_root(Path::new(word))) +} + +fn is_hazardous_root(path: &Path) -> bool { + const ROOTS: &[&str] = &[ + "/", + "/Applications", + "/Library", + "/System", + "/Users", + "/bin", + "/dev", + "/etc", + "/opt", + "/private", + "/proc", + "/sys", + "/tmp", + "/usr", + "/var", + "/Volumes", + ]; + ROOTS.iter().any(|root| path == Path::new(root)) +} diff --git a/src/lib.rs b/src/lib.rs index fb051f1f..2407634b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ //! - Multi-project orchestration // --- Tier A/B: product + ops (root pub surface) --- +pub mod agent_call_policy; pub mod audit_log; pub mod cast; pub mod commands; diff --git a/tests/agent_call_policy.rs b/tests/agent_call_policy.rs new file mode 100644 index 00000000..f92840ae --- /dev/null +++ b/tests/agent_call_policy.rs @@ -0,0 +1,73 @@ +use std::path::PathBuf; +use std::time::Duration; + +use sharecli::agent_call_policy::{AgentCallPolicy, PauseCode}; + +fn policy() -> AgentCallPolicy { + AgentCallPolicy::new(PathBuf::from("/workspace/project")) +} + +#[test] +fn rewrites_recursive_grep_to_a_bounded_ripgrep_command() { + let decision = policy().admit("grep -R TODO ."); + + assert_eq!( + decision.command(), + "rg --hidden --glob '!target' --glob '!node_modules' TODO /workspace/project" + ); +} + +#[test] +fn leaves_nonrecursive_grep_unchanged() { + let decision = policy().admit("grep TODO README.md"); + + assert_eq!(decision.command(), "grep TODO README.md"); +} + +#[test] +fn pauses_searches_targeting_a_hazardous_root() { + let decision = policy().admit("rg TODO /System"); + + assert_eq!(decision.pause_code(), Some(PauseCode::HazardousRoot)); + assert!(decision.resume_condition().is_some()); +} + +#[test] +fn pauses_recursive_grep_targeting_filesystem_root() { + let decision = policy().admit("grep -R TODO /"); + + assert_eq!(decision.pause_code(), Some(PauseCode::HazardousRoot)); + assert!(decision.resume_condition().is_some()); +} + +#[test] +fn pauses_when_the_project_concurrency_limit_is_reached() { + let policy = policy().with_project_limit(1); + let _first = policy.admit("rg TODO src"); + let decision = policy.admit("rg FIXME src"); + + assert_eq!(decision.pause_code(), Some(PauseCode::ProjectLimit)); +} + +#[test] +fn pauses_when_thermal_headroom_is_unavailable() { + let decision = policy().with_thermal_headroom(false).admit("rg TODO src"); + + assert_eq!(decision.pause_code(), Some(PauseCode::Thermal)); +} + +#[test] +fn pauses_builds_when_no_build_slot_is_available() { + let policy = policy().with_build_slots(1); + let _first = policy.admit("cargo test"); + let decision = policy.admit("cargo build"); + + assert_eq!(decision.pause_code(), Some(PauseCode::BuildSlot)); +} + +#[test] +fn attaches_a_nonzero_deadline_to_admitted_calls() { + let decision = policy().admit("rg TODO src"); + + assert!(decision.deadline() > Duration::ZERO); +} diff --git a/tests/fr007_health_pool_status_csv.rs b/tests/fr007_health_pool_status_csv.rs index f77acf14..3b454a54 100644 --- a/tests/fr007_health_pool_status_csv.rs +++ b/tests/fr007_health_pool_status_csv.rs @@ -139,11 +139,13 @@ fn fr007_render_health_csv_body() { gate: gate_status_snapshot(ThermalLevel::Green, 0), host_watch: sharecli::monitoring::HostResourceWatchJson::default(), pool: None, - log_location: None, }, }; let csv = render_health_csv_body(&health); - assert!(csv.contains(HEALTH_CSV_HEADER), "health CSV body MUST include header; got: {csv}"); + assert!( + csv.contains(HEALTH_CSV_HEADER), + "health CSV body MUST include header; got: {csv}" + ); assert!( csv.lines().any(|line| line.starts_with("health,")), "health CSV body MUST include data row; got: {csv}" @@ -167,7 +169,10 @@ fn fr007_render_pool_csv_body() { status: None, }; let csv = render_pool_csv_body(&pool); - assert!(csv.contains(POOL_BODY_CSV_HEADER), "pool CSV body MUST include header; got: {csv}"); + assert!( + csv.contains(POOL_BODY_CSV_HEADER), + "pool CSV body MUST include header; got: {csv}" + ); assert!( csv.lines().any(|line| line.starts_with("pool,")), "pool CSV body MUST include data row; got: {csv}" @@ -179,7 +184,11 @@ fn fr007_render_pool_csv_body() { #[serial_test::serial] fn fr007_health_csv_pool_status_companion() { let out = bin().args(["health", "--csv"]).output().expect("spawn sharecli health --csv"); - assert!(out.status.success(), "health --csv MUST exit 0; stderr: {:?}", out.stderr); + assert!( + out.status.success(), + "health --csv MUST exit 0; stderr: {:?}", + out.stderr + ); let s = String::from_utf8_lossy(&out.stdout); assert_body_precedes_companions(&s, HEALTH_CSV_HEADER, "health --csv"); assert_csv_companion_order(&s, "health --csv"); @@ -191,7 +200,11 @@ fn fr007_health_csv_pool_status_companion() { #[serial_test::serial] fn fr007_pool_csv_pool_status_companion() { let out = bin().args(["pool", "--csv"]).output().expect("spawn sharecli pool --csv"); - assert!(out.status.success(), "pool --csv MUST exit 0; stderr: {:?}", out.stderr); + assert!( + out.status.success(), + "pool --csv MUST exit 0; stderr: {:?}", + out.stderr + ); let s = String::from_utf8_lossy(&out.stdout); assert_body_precedes_companions(&s, POOL_BODY_CSV_HEADER, "pool --csv"); assert_csv_companion_order(&s, "pool --csv"); @@ -203,20 +216,48 @@ fn fr007_pool_csv_pool_status_companion() { #[serial_test::serial] fn fr007_status_csv_pool_status_companion() { let out = bin().args(["status", "--csv"]).output().expect("spawn sharecli status --csv"); - assert!(out.status.success(), "status --csv MUST exit 0; stderr: {:?}", out.stderr); + assert!( + out.status.success(), + "status --csv MUST exit 0; stderr: {:?}", + out.stderr + ); let s = String::from_utf8_lossy(&out.stdout); assert_body_precedes_companions(&s, STATUS_SUMMARY_CSV_HEADER, "status --csv"); assert_csv_companion_order(&s, "status --csv"); assert_stderr_silent(&out.stderr, "status --csv"); } +/// FR-007 / AC-007.82 — health --csv rejects --watch (one-shot export only). +#[test] +#[serial_test::serial] +fn fr007_health_csv_rejects_watch() { + let out = bin() + .args(["health", "--csv", "--watch", "1"]) + .output() + .expect("spawn sharecli health --csv --watch 1"); + assert!( + !out.status.success(), + "health --csv --watch MUST fail loudly (AC-007.82)" + ); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("csv") && stderr.contains("watch"), + "error MUST mention csv/watch incompatibility (AC-007.82); stderr: {stderr}" + ); +} + /// FR-007 / AC-007.82 — pool --csv rejects --json. #[test] #[serial_test::serial] fn fr007_pool_csv_rejects_json() { - let out = - bin().args(["pool", "--csv", "--json"]).output().expect("spawn sharecli pool --csv --json"); - assert!(!out.status.success(), "pool --csv --json MUST fail loudly (AC-007.82)"); + let out = bin() + .args(["pool", "--csv", "--json"]) + .output() + .expect("spawn sharecli pool --csv --json"); + assert!( + !out.status.success(), + "pool --csv --json MUST fail loudly (AC-007.82)" + ); let stderr = String::from_utf8_lossy(&out.stderr); assert!( stderr.contains("csv") && stderr.contains("json"), @@ -238,8 +279,12 @@ fn fr007_health_pool_status_csv_companion_helpers_match_proc_shapes() { .format_csv_companion(); assert!(pool_csv.contains(POOL_COMPANION_CSV_HEADER)); - let status_csv = - StatusOperatorPanel { scanned: 1, watched: 0, total_processes: 3, agent_rows: 0 } - .format_csv_companion(); + let status_csv = StatusOperatorPanel { + scanned: 1, + watched: 0, + total_processes: 3, + agent_rows: 0, + } + .format_csv_companion(); assert!(status_csv.contains(STATUS_COMPANION_CSV_HEADER)); }