From 065d120e5b4a476c5f559aeed1a4308f51cba0b1 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Wed, 22 Jul 2026 19:18:36 -0700 Subject: [PATCH 01/13] feat(macos): add safe local app installer --- packaging/macos/README.md | 21 +++++++++++++ packaging/macos/install-local.sh | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100755 packaging/macos/install-local.sh diff --git a/packaging/macos/README.md b/packaging/macos/README.md index ae4cb4f2..f5cc5042 100644 --- a/packaging/macos/README.md +++ b/packaging/macos/README.md @@ -25,5 +25,26 @@ Outputs land under `packaging/dist/`: - `SessionLedger.app` - `SessionLedger--.pkg` when `ARCH_LABEL` is set +## Local install + +After building an app bundle, install it into the current Mac's Applications +folder without elevating privileges: + +```sh +./packaging/macos/install-local.sh +``` + +The script validates the bundle executable, preserves an existing install as +`SessionLedger.app.previous`, and never creates a background service with an +implicit watch root. To install a locally-built daemon as well, opt in: + +```sh +INSTALL_DAEMON=1 ./packaging/macos/install-local.sh +``` + +Start the daemon explicitly with the session root you intend to ingest; see +the command printed by the installer. This keeps local session data and HTTP +exposure operator-controlled. + Release CI builds at least the `aarch64-apple-darwin` PKG (and `x86_64` when the matrix target runs) and attaches them as Release assets. diff --git a/packaging/macos/install-local.sh b/packaging/macos/install-local.sh new file mode 100755 index 00000000..30700dc6 --- /dev/null +++ b/packaging/macos/install-local.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Install a locally-built SessionLedger.app for interactive dogfooding. +# +# This deliberately does not use sudo or silently install a LaunchAgent. The +# daemon's watch root and privacy policy are operator choices; use the printed +# command (or a managed service) after installing the app. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +APP_NAME="${APP_NAME:-SessionLedger}" +APP_SOURCE="${APP_SOURCE:-$ROOT/packaging/dist/${APP_NAME}.app}" +APP_DEST="${APP_DEST:-/Applications/${APP_NAME}.app}" +INSTALL_DAEMON="${INSTALL_DAEMON:-0}" +DAEMON_BINARY="${DAEMON_BINARY:-$ROOT/crates/sl-daemon/target/release/sl-daemon}" +DAEMON_DEST="${DAEMON_DEST:-$HOME/.local/bin/sl-daemon}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: this installer is macOS-only (got $(uname -s))." >&2 + exit 1 +fi +if [[ ! -d "$APP_SOURCE" || ! -x "$APP_SOURCE/Contents/MacOS/$APP_NAME" ]]; then + echo "error: app bundle is missing or invalid: $APP_SOURCE" >&2 + echo "Build it first with packaging/macos/package-app.sh or set APP_SOURCE." >&2 + exit 1 +fi +command -v ditto >/dev/null || { echo "error: ditto is required." >&2; exit 1; } + +mkdir -p "$(dirname "$APP_DEST")" +if [[ -e "$APP_DEST" ]]; then + backup="${APP_DEST}.previous" + rm -rf "$backup" + ditto "$APP_DEST" "$backup" +fi +rm -rf "$APP_DEST" +ditto "$APP_SOURCE" "$APP_DEST" + +if [[ "$INSTALL_DAEMON" == "1" ]]; then + if [[ ! -x "$DAEMON_BINARY" ]]; then + echo "error: daemon binary is missing or not executable: $DAEMON_BINARY" >&2 + exit 1 + fi + mkdir -p "$(dirname "$DAEMON_DEST")" + install -m 0755 "$DAEMON_BINARY" "$DAEMON_DEST" +fi + +echo "Installed $APP_NAME.app to $APP_DEST" +if [[ "$INSTALL_DAEMON" == "1" ]]; then + echo "Installed sl-daemon to $DAEMON_DEST" +fi +echo +echo "Start the daemon explicitly with a chosen watch root:" +echo " sl-daemon serve --watch \"\$HOME/.codex/sessions\" --out \"\$HOME/.local/share/sessionledger/out\" --http-bind 127.0.0.1:8080" +echo "Then open: $APP_DEST" From 5da3e6d8c9a322290e4e2fbc9935d50784022481 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Wed, 22 Jul 2026 19:23:33 -0700 Subject: [PATCH 02/13] fix(daemon): fail fast when HTTP bind is unavailable --- crates/sl-daemon/src/http.rs | 26 ++++++++++++++++++++++++-- crates/sl-daemon/src/main.rs | 8 +++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/sl-daemon/src/http.rs b/crates/sl-daemon/src/http.rs index e8ff6b45..356a58c5 100644 --- a/crates/sl-daemon/src/http.rs +++ b/crates/sl-daemon/src/http.rs @@ -489,10 +489,32 @@ pub async fn serve( addr: SocketAddr, state: AppState, shutdown: impl std::future::Future + Send + 'static, +) -> std::io::Result<()> { + let listener = bind(addr).await?; + serve_listener(listener, state, shutdown).await +} + +/// Bind the daemon listener before spawning the serving task. +/// +/// Keeping binding separate lets the CLI fail fast on an occupied/unusable +/// port instead of logging a misleading "listening" message while the +/// background HTTP task has already exited. +pub async fn bind(addr: SocketAddr) -> std::io::Result { + tokio::net::TcpListener::bind(addr).await +} + +/// Serve an already-bound listener until shutdown resolves. +pub async fn serve_listener( + listener: tokio::net::TcpListener, + state: AppState, + shutdown: impl std::future::Future + Send + 'static, ) -> std::io::Result<()> { let app = router(state); - let listener = tokio::net::TcpListener::bind(addr).await?; - info!(%addr, "HTTP server bound"); + if let Ok(addr) = listener.local_addr() { + info!(%addr, "HTTP server bound"); + } else { + info!("HTTP server bound"); + } axum::serve(listener, app).with_graceful_shutdown(shutdown).await.map_err(std::io::Error::other) } diff --git a/crates/sl-daemon/src/main.rs b/crates/sl-daemon/src/main.rs index 0bf2ac58..cee4e754 100644 --- a/crates/sl-daemon/src/main.rs +++ b/crates/sl-daemon/src/main.rs @@ -774,9 +774,15 @@ async fn run_serve( #[cfg(feature = "sqlite")] memory_store: memory_store.clone(), }; + // Bind before spawning so an occupied port is a startup error rather + // than a silently dead background task (which otherwise looks like a + // viewer-side "daemon unreachable" condition). + let listener = http::bind(addr).await.map_err(|error| { + format!("failed to bind HTTP server at {addr}: {error}") + })?; let shutdown_for_http = shutdown.clone(); let handle = tokio::spawn(async move { - if let Err(e) = http::serve(addr, state, async move { + if let Err(e) = http::serve_listener(listener, state, async move { shutdown_for_http.cancelled().await; }) .await From 5a052588cd04b9e0962a3e3c7e02169e71556fce Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Wed, 22 Jul 2026 19:28:52 -0700 Subject: [PATCH 03/13] fix(viewer): open inbox detail and make sessions keyboard accessible --- crates/sl-viewer/src/app.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/sl-viewer/src/app.rs b/crates/sl-viewer/src/app.rs index 5eb1ab7b..287c088a 100644 --- a/crates/sl-viewer/src/app.rs +++ b/crates/sl-viewer/src/app.rs @@ -627,7 +627,7 @@ pub fn App() -> Element { .diff-col-a {{ color: var(--sl-text); overflow-wrap: break-word; }} .diff-col-b {{ color: var(--sl-text); overflow-wrap: break-word; }} .main-content {{ flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }} - .main-upper {{ flex: 1; min-height: 0; overflow-y: auto; }} + .main-upper {{ flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }} .bundles-view {{ display: flex; flex-direction: column; height: 100%; min-height: 0; overflow: hidden; }} .bundles-view > h2 {{ flex: 0 0 auto; margin: 0; padding: var(--sl-space-xl) var(--sl-space-xl) var(--sl-space-lg); }} .bundles-workspace {{ display: flex; flex: 1; min-height: 0; overflow: hidden; }} @@ -889,7 +889,9 @@ fn BundlesTab() -> Element { let mut loading = use_signal(|| true); let mut load_error: Signal> = use_signal(|| None); let mut load_gen: Signal = use_signal(|| 0u32); - let mut selected_idx: Signal> = use_signal(|| None); + // Show useful content immediately; an empty detail pane on first render + // made the inbox look broken and hid the chat transcript behind a click. + let mut selected_idx: Signal> = use_signal(|| Some(0)); let mut compare_idx: Signal> = use_signal(|| None); // Structured load gate so LoadingState / ErrorState cover async bundle fetch. @@ -1062,7 +1064,20 @@ fn SessionListWithCompare(props: SessionListWithCompareProps) -> Element { rsx! { div { class: "{cls}", + role: "button", + tabindex: "0", onclick: move |_| props.on_select.call(orig_idx), + onkeydown: move |evt: Event| { + let activate = match evt.key() { + Key::Enter => true, + Key::Character(ref ch) => ch == " ", + _ => false, + }; + if activate { + evt.prevent_default(); + props.on_select.call(orig_idx); + } + }, div { class: "session-source", "{s.source_id}" span { From 5fd16ae93bf3f7545601f6c029fdad984e02d6dd Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Wed, 22 Jul 2026 22:43:02 -0700 Subject: [PATCH 04/13] fix(macos): make packaging scripts executable --- packaging/macos/package-app.sh | 0 packaging/macos/package-pkg.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 packaging/macos/package-app.sh mode change 100644 => 100755 packaging/macos/package-pkg.sh diff --git a/packaging/macos/package-app.sh b/packaging/macos/package-app.sh old mode 100644 new mode 100755 diff --git a/packaging/macos/package-pkg.sh b/packaging/macos/package-pkg.sh old mode 100644 new mode 100755 From a35b5fb79f3a9f746efa9d4b6b69a479b0adb17c Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 00:42:18 -0700 Subject: [PATCH 05/13] feat(daemon): auto-discover native session roots --- crates/sl-daemon/src/discovery.rs | 40 ++++++++++++++++++++++++++++++ crates/sl-daemon/src/main.rs | 41 ++++++++++++++++++++++--------- crates/sl-daemon/src/watcher.rs | 15 ++++++++--- 3 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 crates/sl-daemon/src/discovery.rs diff --git a/crates/sl-daemon/src/discovery.rs b/crates/sl-daemon/src/discovery.rs new file mode 100644 index 00000000..baa4eeff --- /dev/null +++ b/crates/sl-daemon/src/discovery.rs @@ -0,0 +1,40 @@ +//! Native local session-store discovery for the daemon. +//! +//! The daemon should work out of the box on a developer machine. These roots +//! mirror the viewer's automatic corpus resolver; an explicit `--watch` still +//! takes precedence for CI and custom stores. + +use std::path::PathBuf; + +/// Return existing native transcript roots in deterministic order. +pub fn local_watch_roots(home: Option) -> Vec { + let home = home.or_else(|| std::env::var_os("HOME").map(PathBuf::from)); + let Some(home) = home else { return Vec::new() }; + [ + home.join(".codex").join("sessions"), + home.join(".claude").join("projects"), + home.join(".cursor").join("projects"), + ] + .into_iter() + .filter(|root| root.is_dir()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn discovers_only_existing_supported_roots() { + let dir = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(dir.path().join(".codex/sessions")).unwrap(); + std::fs::create_dir_all(dir.path().join(".cursor/projects")).unwrap(); + assert_eq!(local_watch_roots(Some(dir.path().to_path_buf())).len(), 2); + } + + #[test] + fn absent_home_yields_empty_roots() { + let dir = tempfile::tempdir().unwrap(); + assert!(local_watch_roots(Some(dir.path().to_path_buf())).is_empty()); + } +} diff --git a/crates/sl-daemon/src/main.rs b/crates/sl-daemon/src/main.rs index cee4e754..a48fd924 100644 --- a/crates/sl-daemon/src/main.rs +++ b/crates/sl-daemon/src/main.rs @@ -34,6 +34,7 @@ mod archive; mod audit; mod banner; mod cli; +mod discovery; mod etl; mod export; mod filter; @@ -89,6 +90,7 @@ Does not download or install updates — see docs/ops/update-check.md and ADR 00 "#; const SERVE_AFTER_HELP: &str = r#"Examples: + sl-daemon serve --out ./okf-out # auto-discovers native session roots sl-daemon serve --watch ~/.cursor/agent-transcripts --out ./okf-out sl-daemon serve --watch ./sessions --out ./okf-out --once sl-daemon serve --watch ./sessions --out ./okf-out --http-bind off @@ -166,9 +168,10 @@ enum Command { /// Start the file-watcher daemon. #[command(after_help = SERVE_AFTER_HELP)] Serve { - /// Directory to watch for `*.jsonl` session transcripts. + /// Directory to watch for `*.jsonl` session transcripts. When omitted, + /// native Codex, Claude Code, and Cursor roots are discovered automatically. #[arg(long)] - watch: PathBuf, + watch: Option, /// Directory to write `.okf.json` files into. #[arg(long)] @@ -620,12 +623,22 @@ fn audit_event( // --------------------------------------------------------------------------- async fn run_serve( - watch: PathBuf, + watch: Option, out: PathBuf, once: bool, http_bind: String, memory_db: Option, ) -> Result<(), Box> { + let watch_roots = match watch { + Some(path) => vec![path], + None => discovery::local_watch_roots(None), + }; + if watch_roots.is_empty() { + return Err( + "no supported local session stores found; pass --watch to use a custom root".into() + ); + } + info!(roots = ?watch_roots, "session roots selected"); let version = env!("CARGO_PKG_VERSION"); banner::emit_interactive_banner(version); info!(banner = %banner::plain_banner(version), "startup"); @@ -777,9 +790,9 @@ async fn run_serve( // Bind before spawning so an occupied port is a startup error rather // than a silently dead background task (which otherwise looks like a // viewer-side "daemon unreachable" condition). - let listener = http::bind(addr).await.map_err(|error| { - format!("failed to bind HTTP server at {addr}: {error}") - })?; + let listener = http::bind(addr) + .await + .map_err(|error| format!("failed to bind HTTP server at {addr}: {error}"))?; let shutdown_for_http = shutdown.clone(); let handle = tokio::spawn(async move { if let Err(e) = http::serve_listener(listener, state, async move { @@ -795,7 +808,10 @@ async fn run_serve( }; if once { - let sent = watcher::scan_once(&watch, &tx, shutdown.token()).await?; + let mut sent = 0; + for root in &watch_roots { + sent += watcher::scan_once(root, &tx, shutdown.token()).await?; + } info!(enqueued = sent, "once: scan complete"); drop(tx); let total = consumer.await?; @@ -808,15 +824,18 @@ async fn run_serve( } // Long-running mode. - watcher::scan_once(&watch, &tx, shutdown.token()).await?; - let _watcher = watcher::spawn_fs_watcher(&watch, tx.clone())?; - info!(watch = %watch.display(), out = %out.display(), "watching for sessions"); + let mut watchers = Vec::with_capacity(watch_roots.len()); + for root in &watch_roots { + watcher::scan_once(root, &tx, shutdown.token()).await?; + watchers.push(watcher::spawn_fs_watcher(root, tx.clone())?); + } + info!(roots = ?watch_roots, out = %out.display(), "watching for sessions"); drop(tx); shutdown.cancelled().await; info!("shutting down"); - drop(_watcher); + drop(watchers); if let Some(handle) = http_handle { let _ = handle.await; diff --git a/crates/sl-daemon/src/watcher.rs b/crates/sl-daemon/src/watcher.rs index 60bdb354..49aef4c0 100644 --- a/crates/sl-daemon/src/watcher.rs +++ b/crates/sl-daemon/src/watcher.rs @@ -17,20 +17,27 @@ use notify::{Event, EventKind, RecursiveMode, Watcher}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -/// Return every `*.jsonl` or compressed `*.jsonl.zst` file directly under `dir`. +/// Return every `*.jsonl` or compressed `*.jsonl.zst` file under `dir`. /// /// Non-recursive by design: session corpora are flat directories of transcript /// files. Sorting makes the emitted order stable so tests can assert on it. pub fn list_jsonl(dir: &Path) -> std::io::Result> { let mut out = Vec::new(); + collect_transcripts(dir, &mut out)?; + out.sort(); + Ok(out) +} + +fn collect_transcripts(dir: &Path, out: &mut Vec) -> std::io::Result<()> { for entry in std::fs::read_dir(dir)? { let path = entry?.path(); if is_transcript(&path) { out.push(path); + } else if path.is_dir() { + collect_transcripts(&path, out)?; } } - out.sort(); - Ok(out) + Ok(()) } fn is_transcript(path: &Path) -> bool { @@ -94,7 +101,7 @@ pub fn spawn_fs_watcher( } } })?; - watcher.watch(dir, RecursiveMode::NonRecursive)?; + watcher.watch(dir, RecursiveMode::Recursive)?; Ok(watcher) } From 4e0ad7913e92cd624669c491e8a3e37c8506377c Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 01:13:41 -0700 Subject: [PATCH 06/13] fix(viewer): make detail scroll region keyboard accessible --- crates/sl-viewer/src/app.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/sl-viewer/src/app.rs b/crates/sl-viewer/src/app.rs index 287c088a..4b406c52 100644 --- a/crates/sl-viewer/src/app.rs +++ b/crates/sl-viewer/src/app.rs @@ -1113,7 +1113,14 @@ fn SessionListWithCompare(props: SessionListWithCompareProps) -> Element { #[component] fn DetailView(detail: BundleDetail) -> Element { rsx! { - div { class: "detail", + // The pane owns vertical scrolling at narrow widths. Make that + // region keyboard-focusable so keyboard and assistive-tech users can + // reach its content without relying on pointer-wheel scrolling. + div { + class: "detail", + tabindex: "0", + role: "region", + aria_label: "Session bundle details", h1 { "Bundle: {detail.source_id}" } // --- Intent section --- From 2dcaac8236d6efdb5403f3ea237bb0153fe636ab Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 03:33:49 -0700 Subject: [PATCH 07/13] fix(ci): include make in hermetic allocator builder --- ci/hermetic-builder/Containerfile | 3 ++- docs/ops/hermetic-builder.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/hermetic-builder/Containerfile b/ci/hermetic-builder/Containerfile index 1a3e80cd..fa05ab8e 100644 --- a/ci/hermetic-builder/Containerfile +++ b/ci/hermetic-builder/Containerfile @@ -14,7 +14,8 @@ LABEL org.opencontainers.image.title="SessionLedger hermetic builder" \ # explicitly so both Git and Cargo can validate TLS if the pre-offline fetch is # intentionally run by a gate. RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates git \ + && apt-get install -y --no-install-recommends ca-certificates git make \ && rm -rf /var/lib/apt/lists/* \ && git --version \ + && make --version \ && test -f /etc/ssl/certs/ca-certificates.crt diff --git a/docs/ops/hermetic-builder.json b/docs/ops/hermetic-builder.json index 66c9e7fd..7f3a02ae 100644 --- a/docs/ops/hermetic-builder.json +++ b/docs/ops/hermetic-builder.json @@ -9,7 +9,7 @@ "builder_publish_workflow": ".github/workflows/hermetic-builder.yml", "upstream_rust_image": "docker.io/library/rust:1.87-slim", "upstream_rust_image_digest": "sha256:437507c3e719e4f968033b88d851ffa9f5aceeb2dcc2482cc6cb7647811a55eb", - "required_tools": ["git", "ca-certificates"], + "required_tools": ["git", "ca-certificates", "make"], "offline_target": "crates/sl-daemon", "verify_command": "./scripts/hermetic-check.ps1", "update_policy": "Change the repository Containerfile, let hermetic-builder.yml publish a SHA-tagged GHCR image, then copy its manifest digest into this file and hermetic.yml. Never replace this pin with a mutable tag. Re-run hermetic CI before merge." From 877ba5c2563a2f43f942480ff3ddaa5ea42ffb4c Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 03:39:07 -0700 Subject: [PATCH 08/13] feat(macos): add opt-in auto-discovery launch agent --- crates/sl-daemon/src/cli.rs | 2 +- packaging/macos/README.md | 20 +++++-- packaging/macos/install-launch-agent.sh | 77 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100755 packaging/macos/install-launch-agent.sh diff --git a/crates/sl-daemon/src/cli.rs b/crates/sl-daemon/src/cli.rs index 3cfc913d..821f13ae 100644 --- a/crates/sl-daemon/src/cli.rs +++ b/crates/sl-daemon/src/cli.rs @@ -24,7 +24,7 @@ pub const DEFAULT_BASE_URL: &str = "http://127.0.0.1:8080"; pub fn daemon_down_message(base_url: &str) -> String { format!( "daemon not running at {base_url} — start with: \ - sl-daemon serve --watch --out " + sl-daemon serve --out (auto-discovers local session roots)" ) } diff --git a/packaging/macos/README.md b/packaging/macos/README.md index f5cc5042..36c3ac5b 100644 --- a/packaging/macos/README.md +++ b/packaging/macos/README.md @@ -36,15 +36,27 @@ folder without elevating privileges: The script validates the bundle executable, preserves an existing install as `SessionLedger.app.previous`, and never creates a background service with an -implicit watch root. To install a locally-built daemon as well, opt in: +implicit service. To install a locally-built daemon as well, opt in: ```sh INSTALL_DAEMON=1 ./packaging/macos/install-local.sh ``` -Start the daemon explicitly with the session root you intend to ingest; see -the command printed by the installer. This keeps local session data and HTTP -exposure operator-controlled. +For unattended local ingestion, explicitly opt in to a per-user LaunchAgent. +It uses the daemon's native auto-discovery (no `--watch` path is stored), +writes bundles/logs below `~/.local/share/sessionledger`, and rejects +non-loopback HTTP binds: + +```sh +START=1 ./packaging/macos/install-launch-agent.sh +sl-daemon status +launchctl print "gui/$UID/com.sessionledger.daemon" +``` + +Stop/remove it with `launchctl bootout "gui/$UID/com.sessionledger.daemon"` +and remove `~/Library/LaunchAgents/com.sessionledger.daemon.plist`. This is a +separate explicit action so installing the app never starts a process or +begins reading local transcripts unexpectedly. Release CI builds at least the `aarch64-apple-darwin` PKG (and `x86_64` when the matrix target runs) and attaches them as Release assets. diff --git a/packaging/macos/install-launch-agent.sh b/packaging/macos/install-launch-agent.sh new file mode 100755 index 00000000..5ca022a1 --- /dev/null +++ b/packaging/macos/install-launch-agent.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Install the per-user SessionLedger daemon LaunchAgent. +# +# Installation is explicit (this script is never called by the app installer), +# while the daemon itself discovers supported local roots. No transcript path +# is embedded in the plist, so adding/removing a harness does not require +# editing launchd configuration. The service remains loopback-only. +set -euo pipefail + +LABEL="${LABEL:-com.sessionledger.daemon}" +DAEMON_BINARY="${DAEMON_BINARY:-$HOME/.local/bin/sl-daemon}" +OUT_DIR="${OUT_DIR:-$HOME/.local/share/sessionledger/out}" +BIND="${BIND:-127.0.0.1:8080}" +PLIST_DIR="${PLIST_DIR:-$HOME/Library/LaunchAgents}" +PLIST_PATH="$PLIST_DIR/$LABEL.plist" +START="${START:-0}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: LaunchAgents are macOS-only (got $(uname -s))." >&2 + exit 1 +fi +if [[ ! -x "$DAEMON_BINARY" ]]; then + echo "error: daemon binary is missing or not executable: $DAEMON_BINARY" >&2 + echo "Install it first with INSTALL_DAEMON=1 packaging/macos/install-local.sh" >&2 + exit 1 +fi +if [[ "$BIND" != 127.* && "$BIND" != "[::1]:"* && "$BIND" != "localhost:"* ]]; then + echo "error: LaunchAgent bind must be loopback (got $BIND)" >&2 + exit 1 +fi + +mkdir -p "$PLIST_DIR" "$OUT_DIR" +tmp="$(mktemp "${PLIST_PATH}.XXXXXX")" +trap 'rm -f "$tmp"' EXIT + +# Plist values are escaped for XML; reject control characters rather than +# generating a malformed launchd job. Paths are operator-controlled env vars. +for value in "$DAEMON_BINARY" "$OUT_DIR" "$BIND"; do + if [[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *'<'* || "$value" == *'>'* || "$value" == *'&'* ]]; then + echo "error: launchd value contains unsupported XML/control characters" >&2 + exit 1 + fi +done + +cat >"$tmp" < + + + + Label$LABEL + ProgramArguments + + $DAEMON_BINARY + serve + --out$OUT_DIR + --http-bind$BIND + + RunAtLoad + KeepAlive + ProcessTypeInteractive + StandardOutPath$OUT_DIR/daemon.log + StandardErrorPath$OUT_DIR/daemon.err.log + + +EOF +mv "$tmp" "$PLIST_PATH" +trap - EXIT + +if [[ "$START" == "1" ]]; then + launchctl bootout "gui/$UID/$LABEL" 2>/dev/null || true + launchctl bootstrap "gui/$UID" "$PLIST_PATH" + launchctl kickstart -k "gui/$UID/$LABEL" + echo "Started $LABEL; health: sl-daemon status" +else + echo "Installed $PLIST_PATH (not started)" + echo "Start: START=1 $0" +fi From 2366e89e3b3ea4300289580234c934e1733c1499 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 03:55:59 -0700 Subject: [PATCH 09/13] docs(macos): print auto-discovery daemon command --- packaging/macos/README.md | 4 ++++ packaging/macos/install-local.sh | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packaging/macos/README.md b/packaging/macos/README.md index 36c3ac5b..dff4767c 100644 --- a/packaging/macos/README.md +++ b/packaging/macos/README.md @@ -42,6 +42,10 @@ implicit service. To install a locally-built daemon as well, opt in: INSTALL_DAEMON=1 ./packaging/macos/install-local.sh ``` +The installer prints `sl-daemon serve --out ...`, which enables native +discovery of supported local session stores. Add `--watch ` only when a +custom transcript root is required. + For unattended local ingestion, explicitly opt in to a per-user LaunchAgent. It uses the daemon's native auto-discovery (no `--watch` path is stored), writes bundles/logs below `~/.local/share/sessionledger`, and rejects diff --git a/packaging/macos/install-local.sh b/packaging/macos/install-local.sh index 30700dc6..456534a3 100755 --- a/packaging/macos/install-local.sh +++ b/packaging/macos/install-local.sh @@ -48,6 +48,7 @@ if [[ "$INSTALL_DAEMON" == "1" ]]; then echo "Installed sl-daemon to $DAEMON_DEST" fi echo -echo "Start the daemon explicitly with a chosen watch root:" -echo " sl-daemon serve --watch \"\$HOME/.codex/sessions\" --out \"\$HOME/.local/share/sessionledger/out\" --http-bind 127.0.0.1:8080" +echo "Start the daemon with native local-session auto-discovery:" +echo " sl-daemon serve --out \"\$HOME/.local/share/sessionledger/out\" --http-bind 127.0.0.1:8080" +echo "For a custom transcript root, add: --watch \"\$HOME/path/to/sessions\"" echo "Then open: $APP_DEST" From 019b2f8925af4d7b309140d137ef941253ba8caf Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 04:47:06 -0700 Subject: [PATCH 10/13] docs(packaging): make auto-discovery the default --- packaging/channels.md | 8 +++++--- packaging/homebrew/sessionledger.rb | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packaging/channels.md b/packaging/channels.md index 4b39a6d7..d9a43b0d 100644 --- a/packaging/channels.md +++ b/packaging/channels.md @@ -54,16 +54,18 @@ cargo install --git https://github.com/KooshaPari/SessionLedger --locked --path ``` This installs the `sl-daemon` binary into Cargo's configured bin directory -(`~/.cargo/bin` by default). Start the long-running daemon with explicit input -and output paths: +(`~/.cargo/bin` by default). Start the long-running daemon with native local +session auto-discovery: ```bash sl-daemon serve \ - --watch "$HOME/.forge/sessions" \ --out "$HOME/.local/share/sessionledger/out" \ --http-bind 127.0.0.1:8080 ``` +Add `--watch ` only when overriding discovery for a custom transcript +root. + `cargo install` is a developer/source channel. It does not provide automatic updates, package-manager metadata, desktop integration, or platform signing. diff --git a/packaging/homebrew/sessionledger.rb b/packaging/homebrew/sessionledger.rb index 06d0a78c..eb23c09d 100644 --- a/packaging/homebrew/sessionledger.rb +++ b/packaging/homebrew/sessionledger.rb @@ -59,13 +59,14 @@ def caveats cargo install --git https://github.com/KooshaPari/SessionLedger --locked --path crates/sl-daemon - Then start it with explicit watch/out paths: + Then start it with native local-session auto-discovery: sl-daemon serve \\ - --watch "$HOME/.forge/sessions" \\ --out "$HOME/.local/share/sessionledger/out" \\ --http-bind 127.0.0.1:8080 + Add --watch only when overriding discovery for a custom transcript root. + Before publishing this formula to a tap, replace each sha256 placeholder with the matching digest from the Release SHA256SUMS file. EOS From 84466aaa4d29433d22e27d09b333b032505faf74 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 04:40:56 -0700 Subject: [PATCH 11/13] fix(ci): pin published hermetic builder digest --- .github/workflows/hermetic.yml | 2 +- docs/ops/hermetic-builder.json | 2 +- docs/ops/reusable-hermetic-pin.json | 2 +- docs/ops/reusable-hermetic-pin.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/hermetic.yml b/.github/workflows/hermetic.yml index 2cb4a937..1458835d 100644 --- a/.github/workflows/hermetic.yml +++ b/.github/workflows/hermetic.yml @@ -74,4 +74,4 @@ jobs: name: sl-daemon · repository builder image offline build uses: KooshaPari/SessionLedger/.github/workflows/reusable-hermetic-build.yml@ec8916547e5678f72fe6894509249f9b23367b80 with: - builder_image_digest: sha256:fe71c757a13cb864e5f15f4a1384f63129b15bcd037bc44b46a056c814ca4cb7 + builder_image_digest: sha256:16381cf25d89fd5dc8a904ff4a7b8d4660a856ed9738b8a7e879d816439ce2a5 diff --git a/docs/ops/hermetic-builder.json b/docs/ops/hermetic-builder.json index 7f3a02ae..c4364f1f 100644 --- a/docs/ops/hermetic-builder.json +++ b/docs/ops/hermetic-builder.json @@ -4,7 +4,7 @@ "msrv": "1.85", "rust_channel": "stable", "builder_image": "ghcr.io/kooshapari/sessionledger-hermetic-builder", - "builder_image_digest": "sha256:fe71c757a13cb864e5f15f4a1384f63129b15bcd037bc44b46a056c814ca4cb7", + "builder_image_digest": "sha256:16381cf25d89fd5dc8a904ff4a7b8d4660a856ed9738b8a7e879d816439ce2a5", "builder_definition": "ci/hermetic-builder/Containerfile", "builder_publish_workflow": ".github/workflows/hermetic-builder.yml", "upstream_rust_image": "docker.io/library/rust:1.87-slim", diff --git a/docs/ops/reusable-hermetic-pin.json b/docs/ops/reusable-hermetic-pin.json index 62f390d4..44f51d85 100644 --- a/docs/ops/reusable-hermetic-pin.json +++ b/docs/ops/reusable-hermetic-pin.json @@ -6,7 +6,7 @@ "caller_workflow": ".github/workflows/hermetic.yml", "caller_job": "sl-daemon-offline-container", "workflow_commit_sha": "ec8916547e5678f72fe6894509249f9b23367b80", - "builder_image_digest": "sha256:fe71c757a13cb864e5f15f4a1384f63129b15bcd037bc44b46a056c814ca4cb7", + "builder_image_digest": "sha256:16381cf25d89fd5dc8a904ff4a7b8d4660a856ed9738b8a7e879d816439ce2a5", "offline_target_default": "crates/sl-daemon", "selfcheck_script": "scripts/reusable-provenance-check.ps1", "update_policy": "After changing the reusable workflow, bump workflow_commit_sha in this file, hermetic.yml uses: ref, and reusable-hermetic-pin.md. Never pin callers to @main." diff --git a/docs/ops/reusable-hermetic-pin.md b/docs/ops/reusable-hermetic-pin.md index 744f0b69..0fbe5ae8 100644 --- a/docs/ops/reusable-hermetic-pin.md +++ b/docs/ops/reusable-hermetic-pin.md @@ -10,7 +10,7 @@ SessionLedger calls the in-repo reusable hermetic build slice from | Workflow | `KooshaPari/SessionLedger/.github/workflows/reusable-hermetic-build.yml` | | Commit SHA | `ec8916547e5678f72fe6894509249f9b23367b80` | | Caller job | `hermetic.yml` → `sl-daemon-offline-container` | -| `builder_image_digest` input | `sha256:fe71c757a13cb864e5f15f4a1384f63129b15bcd037bc44b46a056c814ca4cb7` (must match [`hermetic-builder.json`](hermetic-builder.json)) | +| `builder_image_digest` input | `sha256:16381cf25d89fd5dc8a904ff4a7b8d4660a856ed9738b8a7e879d816439ce2a5` (must match [`hermetic-builder.json`](hermetic-builder.json)) | The `uses: …@` ref must be a full 40-character commit SHA. Do not use `@main`, branch names, or moving tags. The digest input must match the immutable From 9532aec2241a6f7e7493fd77c644b245f6b1184f Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 23 Jul 2026 05:19:34 -0700 Subject: [PATCH 12/13] docs(reaudit): Wave-43-D closure (#362 merge) refresh at commit 41829e8 - SCORECARD.md: header refreshed (date/auditor/commit), Wave-43 Delta section added, Wave-41/42/43 line, Held (no score) expansion incl #348/#349/#361/#362, Remaining unpaid rewrite. - TRACEABILITY.json: updated 2026-07-21 -> 2026-07-23; commit d5f999f -> 41829e8; wave Wave-42 -> Wave-43; +delta_vs_w42 note. CRLF preserved. - GAP_QA_MATRIX.md: C00 row + PLAN-W8-B row updated. - CHANGELOG.md: Unreleased Changed entry for Wave-43-D reaudit. Score 396/402 (98% A) held conservative; WAVE43 impl lanes deepened residual evidence only (per WBS-8.3/8.5/8.7 pattern). --- CHANGELOG.md | 4 ++++ audit/SCORECARD.md | 22 +++++++++++++++++----- docs/ops/GAP_QA_MATRIX.md | 4 ++-- docs/ops/TRACEABILITY.json | 7 ++++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d678a15..cec9c8f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer]( ## [Unreleased] +### Changed + +- Wave-43 reaudit (Wave-43-D): `audit/SCORECARD.md` refresh at commit `41829e8` (machine-w43-reaudit); `docs/ops/TRACEABILITY.json` overall_audit wave=Wave-43 commit=41829e8 (conservative hold at 396/402); `docs/ops/GAP_QA_MATRIX.md` C00 + PLAN-W8-B rows reflect Wave-43 closure (#344/#348/#349/#361/#362). + ### Fixed - Viewer first-run corpus CTA (C09): wire “Open corpus…” to a web Forge DB file picker (`corpus_cta.rs`) or open the quick-start runbook on desktop; `cargo test -p sl-viewer`. diff --git a/audit/SCORECARD.md b/audit/SCORECARD.md index f94024f6..22e92bf0 100644 --- a/audit/SCORECARD.md +++ b/audit/SCORECARD.md @@ -1,10 +1,10 @@ # audit-v38 Scorecard — SessionLedger **Repo:** KooshaPari/SessionLedger -**Date:** 2026-07-18 +**Date:** 2026-07-23 **Repo-type profile:** CLI+daemon + desktop (sl-daemon + sl-viewer) -**Auditor:** cursor-w42-reaudit -**Commit audited:** d5f999f (origin/main / Wave-42 closure #339-#344) +**Auditor:** machine-w43-reaudit (Wave-43-D) +**Commit audited:** 41829e8 (origin/main / Wave-43 closure #344, #348, #349, #361, #362) > Rubric SSOT: phenotype-org-audits/audit-v38 @@ -38,13 +38,25 @@ | — | 396/402 | 396/402 | 0 | All five impl lanes deepen evidence at pillar max; conservative hold | | **Overall** | **396/402 (98% A)** | **396/402 (98% A)** | **0** | Conservative; no raw score inflation | +## Wave-43 Delta + +| Cluster | Before | After | Raw delta | Evidence-backed movement | +|---------|:------:|:-----:|:---------:|--------------------------| +| C00 L7 | residual partial | residual partial (deepened) | 0 | Live tokio daemon-graph hard gate landed (#362): real mpsc/broadcast pipeline conservation, Lagged SSE recovery, shutdown stops enqueue; process-level HTTP SSE soak under loom remains unpaid | +| C00 L8 | residual partial | residual partial (deepened) | 0 | Default-on platform allocators (#349) for non-Windows parity; Windows allocator parity + always-on production rollout remain unpaid | +| C01 L16 | residual partial | residual partial (deepened) | 0 | sl-viewer CLI help expanded (#361): `corpus_cta.rs`, viewer help surface; full viewer/CLI Fluent `.ftl` migration remains unpaid | +| C08 L73 | partial | partial (deepened) | 0 | Load-macro PR gate (#348): blocking `load-macro-gate-hard.yml`, `load-smoke.ps1 -RouteTier macro`; production-scale corpus breadth remains unpaid | +| C06 L33 | partial | partial (deepened) | 0 | Socket.dev supply-chain posture (#344): `socket-posture.md`, blocking `security.yml` job; full SLSA Build L3 attestation remains unpaid | +| **Overall** | **396/402 (98% A)** | **396/402 (98% A)** | **0** | Conservative hold; 5 WAVE43 impl lanes deepened residual evidence without fresh independent re-audit pillar lift | + ## Headline Findings - **Strongest:** C00/C01/C02/C03/C05/C06/C07/C09/C10 (100% A); C08 (97% A) - **Weakest:** C04 (90% A); C11 Packaging (96% A) - **Wave-41 → Wave-42:** 98% A (396/402) → 98% A (396/402), held -- **Held (no score):** #340 bounded commit-signing header scan (C04 L34 already pillar max); #341 pinned CycloneDX + SBOM schema validation (C04 L32 residual unpaid); #342 SLSA protected-env blocking on PRs (C06 L53 residual attestation unpaid); #343 blocking alloc-profile / dhat hard gate (C00 L8 already pillar max); #344 first-run corpus CTA (C09 UX polish) -- **Remaining unpaid:** Authenticode/notarization live keys (C11 L112 residual), live brew/winget publish, human org 2FA attestation (C04 L36), live rootless-only runner matrix (C04 L40 residual), full protected-environment SLSA Build L3 attestation (C06 L53 residual), live branch-protection signed-commits attestation (C06 L59 residual), live Alertmanager webhooks, production Pyroscope profiling push, full tokio sl-daemon broadcast/SSE graph permutation ports (C00 L7 residual), default-on jemalloc / Windows allocator parity (C00 L8 residual), in-tree KMS (C02 L22 residual), multi-tenant / auto-ETL PII redaction (C02 L24), viewer/CLI Fluent migration (C01 L16 residual), auto-install/rollback updater (C11 L111 residual), phenotype-org-audits org mirror (403/403) +- **Wave-42 → Wave-43:** 98% A (396/402) → 98% A (396/402), held +- **Held (no score):** #340 bounded commit-signing header scan (C04 L34 already pillar max); #341 pinned CycloneDX + SBOM schema validation (C04 L32 residual unpaid); #342 SLSA protected-env blocking on PRs (C06 L53 residual attestation unpaid); #343 blocking alloc-profile / dhat hard gate (C00 L8 already pillar max); #344 first-run corpus CTA (C09 UX polish); #348 load-macro PR gate (C08 L73 production breadth residual); #349 default-on platform allocators (C00 L8 Windows parity residual); #361 sl-viewer CLI help expand (C01 L16 Fluent migration residual); #362 live tokio daemon-graph hard gate (C00 L7 HTTP SSE soak residual) +- **Remaining unpaid (post-WAVE43):** Authenticode/notarization live keys (C11 L112 residual), live brew/winget publish, human org 2FA attestation (C04 L36), live rootless-only runner matrix (C04 L40 residual), full protected-environment SLSA Build L3 attestation (C06 L53 residual), live branch-protection signed-commits attestation (C06 L59 residual), live Alertmanager webhooks, production Pyroscope profiling push, process-level HTTP SSE soak under loom (C00 L7 residual), Windows allocator parity + always-on production rollout (C00 L8 residual), in-tree KMS (C02 L22 residual), multi-tenant / auto-ETL PII redaction (C02 L24), viewer/CLI Fluent migration (C01 L16 residual), auto-install/rollback updater (C11 L111 residual), production-scale load corpus breadth (C08 L73 residual), phenotype-org-audits org mirror (403/403) ## N/A / soft goals diff --git a/docs/ops/GAP_QA_MATRIX.md b/docs/ops/GAP_QA_MATRIX.md index a8ba71ee..aad00a9a 100644 --- a/docs/ops/GAP_QA_MATRIX.md +++ b/docs/ops/GAP_QA_MATRIX.md @@ -11,7 +11,7 @@ package, and the JSON mirror in the same change. | ID | Current score / status | Gap | Acceptance test / evidence | Next action | status_updated | |---|---|---|---|---|---| -| C00 | 30/30 · done | Blocking loom/shuttle/Miri/TSan permutation + jemalloc hard gate + blocking alloc-profile / dhat hard gate + partial daemon-graph loom ports + loom CI job split landed; default-on jemalloc and full tokio broadcast graph remain | `audit/.lane-c00/C00.md`; `scripts/loom-permutation-check.ps1`; `tests/loom_model.rs`; `.github/workflows/jemalloc-hard.yml`; `.github/workflows/alloc-profile-hard.yml`; `scripts/alloc-profile-check.ps1`; `tests/alloc_profile_hard.rs`; `.github/workflows/loom-permutation.yml`; `scripts/jemalloc-check.ps1` | Promote default-on jemalloc; add full tokio sl-daemon broadcast/SSE graph ports | 2026-07-21 | +| C00 | 30/30 · done | Blocking loom/shuttle/Miri/TSan permutation + jemalloc hard gate + blocking alloc-profile / dhat hard gate + partial daemon-graph loom ports + loom CI job split + live tokio daemon-graph hard gate (#362) + default-on platform allocators (#349) landed; process-level HTTP SSE soak under loom + Windows allocator parity + always-on production rollout remain | `audit/.lane-c00/C00.md`; `scripts/loom-permutation-check.ps1`; `tests/loom_model.rs`; `.github/workflows/jemalloc-hard.yml`; `.github/workflows/alloc-profile-hard.yml`; `scripts/alloc-profile-check.ps1`; `tests/alloc_profile_hard.rs`; `.github/workflows/loom-permutation.yml`; `scripts/jemalloc-check.ps1` | Promote default-on jemalloc; add full tokio sl-daemon broadcast/SSE graph ports | 2026-07-21 | | C01 | 30/30 · done | Quality-gate SHA pin + clap completions + JSON/`es` i18n + Fluent `.ftl` catalog stub + explicit workflow `timeout-minutes` landed | `audit/.lane-c01/C01.md`; `locales/en.ftl`; `locales/es.ftl`; `src/i18n_fluent.rs`; `scripts/fluent-i18n-check.ps1`; `scripts/i18n-check.ps1`; `.github/workflows/ci.yml`; `.github/workflows/security.yml` | Migrate viewer/CLI production strings through Fluent catalogs | 2026-07-18 | | C02 | 30/30 · done | Privacy hygiene SSOT + crypto inventory + envelope-crypto blocking SelfCheck + PII redaction stub landed; IdP/OAuth beyond shared key, in-tree KMS, and production PII redaction remain | `docs/ops/privacy-hygiene.md`; `docs/ops/crypto-inventory.md`; `scripts/envelope-crypto-check.ps1`; `.github/workflows/envelope-crypto.yml`; `docs/ops/pii-redaction.md`; `crates/sl-daemon/src/resilience.rs` | Add IdP/OAuth if remote multi-user deploy is needed; in-tree KMS if at-rest encryption required | 2026-07-18 | | C03 | 36/36 · done | Agent-readiness pillars at max after role-form FR stories and feedback budgets | `audit/.lane-c03/C03.md`; `docs/functional_requirements.md`; `docs/ops/feedback-budgets.md`; `scripts/feedback-budget-check.ps1` | Keep FR/journey/budget artifacts current | 2026-07-14 | @@ -53,6 +53,6 @@ These are residual themes only; they do not create new PLAN T-IDs. | PLAN-P3 | partial | LLM-backed intent extraction and `curate.py` convergence remain | `docs/DESIGN.md` §6-7; adapter contract tests with provenance | Claim WBS-3.2 after cross-repo destination approval | 2026-07-12 | | PLAN-P4 | partial | context-mode FTS recall and explicit TUI scope decision remain | `docs/DESIGN.md` §3, §7; recall E2E or accepted `na` decision | Human decides TUI; machine implements approved recall boundary | 2026-07-12 | | PLAN-P6 | partial | Coverage, property (incl. lifecycle FSM), fuzz (blocking sustained cadence), loom-lite race_model, blocking loom/shuttle/Miri/TSan permutation (split loom CI jobs), partial daemon-graph + tokio broadcast loom ports, blocking alloc-profile / dhat hard gate, enforced perf-budget, and Criterion-measured p95 latency gates landed; full live tokio broadcast graph remains | `tests/properties.rs`; `fuzz/`; `.github/workflows/fuzz-blocking.yml`; `tests/loom_model.rs`; `.github/workflows/loom-permutation.yml`; `.github/workflows/alloc-profile-hard.yml`; `.github/workflows/bench-gate.yml`; `docs/ops/perf-baseline.json`; `scripts/bench-gate.ps1`; `docs/ops/eval-reproducibility.md`; `scripts/rootless-matrix-check.ps1` | Add full live tokio sl-daemon broadcast/SSE graph in WBS-6.2 | 2026-07-21 | -| PLAN-W8-B | done | Wave-42 result is 396/402 (98% A), held; Wave-41 target 396/402 met | Independent audit-v38 result is at least 362/402 and >=90% | Wave-43: human org gates + packaging/signing creds | 2026-07-21 | +| PLAN-W8-B | done | Wave-43 closure (#344/#348/#349/#361/#362) at 396/402 (98% A), held; Wave-42 → Wave-43 result 396/402 held | Independent audit-v38 result is at least 362/402 and >=90% | Wave-44: production rollout (C00 L7 SSE soak / C00 L8 Windows parity) + packaging (C11 brew/winget publish + Authenticode) | 2026-07-23 | | PLAN-ORG | partial | Registry spine and governance-policy conformance require cross-repo/human evidence | Registry entry links SessionLedger; policy checklist and org controls are recorded | Human owns WBS-9.1..WBS-9.3 | 2026-07-12 | diff --git a/docs/ops/TRACEABILITY.json b/docs/ops/TRACEABILITY.json index 5bfff121..042a7135 100644 --- a/docs/ops/TRACEABILITY.json +++ b/docs/ops/TRACEABILITY.json @@ -1,6 +1,6 @@ { "schema": "sessionledger.traceability/v1", - "updated": "2026-07-21", + "updated": "2026-07-23", "status_vocabulary": [ "done", "partial", @@ -12,8 +12,9 @@ "score": "396/402", "pct": 98, "grade": "A", - "commit": "d5f999f", - "wave": "Wave-42" + "commit": "41829e8", + "wave": "Wave-43", + "delta_vs_w42": "0 (conservative hold; 5 WAVE43 impl lanes deepened residual evidence)" }, "fr": [ { From 34a08ba81892f7dca4a91d9f550952954ae86c26 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Fri, 24 Jul 2026 02:35:17 -0700 Subject: [PATCH 13/13] docs(wave-44): plan close-out wave targeting 396/402 -> 402/402 - WAVE44_SCOPE.md: top-level scope (6 lanes; 3 machine, 3 human-gated) - docs/ops/WAVE44_PERT.md: PERT with merge order, decision points, risk register - CHANGELOG.md: Unreleased Changed entry for Wave-44 plan Theme: stack-stability closure + i18n migration + eval coverage + supply-chain signing. Three lanes require human-gated evidence (signing keys, policy decision, prod rollout window). Predecessor: Wave-43-D reaudit (PR #366) @ 41829e8 (396/402 98% A held). --- CHANGELOG.md | 2 + WAVE44_SCOPE.md | 70 +++++++++++++++++++++++++ docs/ops/WAVE44_PERT.md | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 WAVE44_SCOPE.md create mode 100644 docs/ops/WAVE44_PERT.md diff --git a/CHANGELOG.md b/CHANGELOG.md index cec9c8f1..ed1756d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer]( - Wave-43 reaudit (Wave-43-D): `audit/SCORECARD.md` refresh at commit `41829e8` (machine-w43-reaudit); `docs/ops/TRACEABILITY.json` overall_audit wave=Wave-43 commit=41829e8 (conservative hold at 396/402); `docs/ops/GAP_QA_MATRIX.md` C00 + PLAN-W8-B rows reflect Wave-43 closure (#344/#348/#349/#361/#362). +- Wave-44 plan landed: `WAVE44_SCOPE.md` + `docs/ops/WAVE44_PERT.md` enumerate 6 close-out lanes (3 machine, 3 human-gated) for the 6 unpaid residuals from Wave-43 (396/402 → 402/402 target). Theme: stack-stability closure + i18n migration + eval coverage + supply-chain signing. + ### Fixed - Viewer first-run corpus CTA (C09): wire “Open corpus…” to a web Forge DB file picker (`corpus_cta.rs`) or open the quick-start runbook on desktop; `cargo test -p sl-viewer`. diff --git a/WAVE44_SCOPE.md b/WAVE44_SCOPE.md new file mode 100644 index 00000000..d0e67990 --- /dev/null +++ b/WAVE44_SCOPE.md @@ -0,0 +1,70 @@ +# Wave-44 scope — SessionLedger audit-v38 (close-out, 396/402 → 402/402 target) + +**Base:** `origin/main` @ `41829e8` (Wave-43 closure #362 · **396/402 · 98% A**) +**Method:** Wave-43 widened evidence; Wave-44 closes the remaining 6 raw points +across 3 machine-executable lanes and 3 human-gated lanes. +**Auditor posture:** close-out wave; if all 6 residuals close, target **402/402 +· 100% A+** without creds-dependent inflation. + +Companion PERT: [`docs/ops/WAVE44_PERT.md`](docs/ops/WAVE44_PERT.md) + +**Source:** Wave-43 SCORECARD headline at `41829e8` + Wave-43-D reaudit close +(PR #366). + +--- + +## Top unpaid gaps (396/402 → 402/402 closure targets) + +**6 raw points** remain across C00, C01, C02, C08, C11 from Wave-43. Wave-44 +selects **6 lanes**, three machine-actionable and three human-gated. + +| Rank | ID | Class | Gap | Pillar / cluster | Selected lane | +|:----:|----|-------|-----|------------------|---------------| +| **1** | GAP-W43-STAB-01 | **Concurrency depth** | Process-level HTTP SSE consumer fanout outside loom | C00 L7 | **w44-loom-sse-soak** | +| **2** | GAP-W43-STAB-02 | **Allocator policy** | Windows allocator parity + prod canary rollout | C00 L8 | **w44-windows-allocator-prod** | +| **3** | GAP-W43-PKG-01 | **Packaging signing** | brew/winget publish + Authenticode/notarization live keys | C11 L111/L112 | **w44-brew-winget-signing** | +| **4** | GAP-W43-API-01 | **API governance** | In-tree KMS (L22) OR multi-tenant PII redaction (L24) | C02 L22/L24 | **w44-pii-or-kms** | +| **5** | GAP-W43-DX-02 | **i18n** | Viewer/CLI Fluent `.ftl` migration complete | C01 L16 | **w44-fluent-migration** | +| **6** | GAP-W43-EVAL-01 | **Eval coverage** | Production-scale corpus breadth | C08 L73 | **w44-corpus-breadth** | + +### Lane ownership breakdown + +| Owner | Lanes | Why human-gated | +|-------|-------|-----------------| +| `machine` | w44-loom-sse-soak, w44-fluent-migration (tooling), w44-corpus-breadth | n/a | +| `machine + human` | w44-windows-allocator-prod, w44-fluent-migration (viewer portion) | rollout window + loc sign-off | +| `human (keys)` | w44-brew-winget-signing | Authenticode + notarization secrets | +| `human (policy)` | w44-pii-or-kms | picks L22 KMS vs L24 PII redaction | + +### Decision points (human-owned) + +- **D-W44-1:** Pick R-4 branch (L22 KMS vs L24 PII redaction). Recommend L22 + (smaller blast radius; L24 requires multi-tenant threat model). +- **D-W44-2:** R-3 keys availability window. If not received within 7d of W44-B3 + start, downgrade R-3 to partial close and defer to W45. +- **D-W44-3:** Accept partial W44-B5 close (machine-tooling only; viewer + localization deferred to W45 if loc sign-off slips). + +### Secondary gaps (deferred or alternate lanes) + +| ID | Gap | Notes | Alternate lane | +|----|-----|-------|----------------| +| GAP-W43-C04-01 | C04 SBOM pillar residual (3 raw pts) | Schema gate incomplete; close in W45 | w45-sbom-pillar-close | +| GAP-W43-C09-01 | Viewer accessibility audit (C09 residual) | Pre-existing; covered by W43-B4 | (covered) | + +## Wave-44 acceptance + +- All B1–B6 PRs merged (or partial with human sign-off) +- SCORECARD.md refreshes to W44 score (target 402/402; realistic 398–401) +- TRACEABILITY.json updated (`updated: 2026-07-XX`, `commit: `, + `wave: Wave-44`) +- GAP_QA_MATRIX.md updated for any closed residual +- CHANGELOG.md Unreleased entry for W44 +- Org mirror PR opened (W44-E; human-gated approval) + +## Carry-over history + +- Wave-41: 372/402 → 375/402 (#163/#164) +- Wave-42: 375/402 → 396/402 (#165, #169, #170) +- Wave-43: 396/402 → 396/402 (conservative hold; 5 impl lanes, #170–#362) +- Wave-44: **target** 396/402 → 402/402 (close-out) diff --git a/docs/ops/WAVE44_PERT.md b/docs/ops/WAVE44_PERT.md new file mode 100644 index 00000000..3819314c --- /dev/null +++ b/docs/ops/WAVE44_PERT.md @@ -0,0 +1,110 @@ +# Wave-44 PERT — SessionLedger (carry-forward) + +Companion to [`WAVE44_SCOPE.md`](../../WAVE44_SCOPE.md) (repo root). +Predecessor: [`WAVE43_PERT.md`](WAVE43_PERT.md). + +**Base:** `origin/main` @ `41829e8` (**396/402 · 98% A**) +**Width:** 5 parallel lanes · **Theme:** close-out the 6 unpaid residuals from W43 +to push from **396/402 → 402/402 (100% A+)** without inflating raw score. + +## Unpaid residual inventory (carry-over from W43 SCORECARD) + +| ID | Cluster | Pillar | Description | Severity | Owner | +|----|---------|--------|-------------|----------|-------| +| R-1 | C00 | L7 | Process-level HTTP SSE soak under loom (closes L7) | deep-evidence | machine | +| R-2 | C00 | L8 | Windows allocator parity + always-on production rollout | deep-evidence + rollout | machine + human | +| R-3 | C11 | L111/L112 | brew/winget publish + Authenticode/notarization live keys | platform-signing | **human (keys)** | +| R-4 | C02 | L22 OR L24 | in-tree KMS (L22) OR multi-tenant PII redaction (L24) | policy | **human (policy)** | +| R-5 | C01 | L16 | viewer/CLI Fluent `.ftl` migration | policy + DX | machine + human | +| R-6 | C08 | L73 | production-scale corpus breadth | evidence | machine | + +**Three lanes require human-gated evidence** (R-2 rollout, R-3 signing keys, +R-4 policy). **Three lanes are machine-executable** (R-1, R-5 tooling portion, +R-6) but R-5 also depends on localization sign-off. + +## Activity table + +| ID | Activity | Pred | Est (h) | Owner | Closes | +|----|----------|------|---------|-------|--------| +| W44-A | Scope PR (`WAVE44_SCOPE.md` + this PERT + CHANGELOG) | W43-D reaudit (PR #366) | 2 | machine | — | +| W44-B1 | w44-loom-sse-soak — loom permutation for HTTP SSE consumer graph | W44-A | 4 | machine | R-1 | +| W44-B2 | w44-windows-allocator-prod — Windows jemalloc parity + canary rollout | W44-A | 6 | machine + human | R-2 | +| W44-B3 | w44-brew-winget-signing — brew/winget publish + Authenticode live | W44-A | 4 | **human (keys)** | R-3 | +| W44-B4 | w44-pii-or-kms — in-tree KMS OR multi-tenant PII redaction (pick one) | W44-A | 6 | **human (policy)** | R-4 | +| W44-B5 | w44-fluent-migration — viewer/CLI `.ftl` extraction (tooling part) | W44-A | 3 | machine | R-5 (partial) | +| W44-B6 | w44-corpus-breadth — production-scale corpus + replay fixtures | W44-A | 3 | machine | R-6 | +| W44-C | Merge B1–B6 sequentially (lowest conflict first) | B1–B6 | 3 | machine | — | +| W44-D | Full reaudit + traceability refresh | W44-C | 3 | machine | — | +| W44-E | Org mirror PR to phenotype-org-audits (skeleton) | W44-D | 2 | human (target archived) | — | + +**Parallel width:** 6 (B1–B6). **Critical path:** A → **B4** (policy decision +slows both branches) → C → D (~25h nominal; gated on human R-3/R-4). + +## Merge order (lowest conflict risk first) + +1. **w44-loom-sse-soak** — `tests/loom_sse.rs`, sl-daemon graph touch only +2. **w44-fluent-migration** — viewer/CLI string extraction; new `locales/*.ftl` +3. **w44-corpus-breadth** — `tests/corpus/` fixtures + new replay harness +4. **w44-windows-allocator-prod** — `Cargo.toml` features + `jemalloc.md` + rollout +5. **w44-brew-winget-signing** — `.github/workflows/release.yml` + signing config +6. **w44-pii-or-kms** — `src/domain/redact.rs` OR `crates/sl-kms/` (largest diff) + +## Lane detail (acceptance stubs) + +| Lane | Key files | Acceptance | +|------|-----------|------------| +| w44-loom-sse-soak | `tests/loom_sse.rs`, sl-daemon graph | Loom permutation green; SSE consumer fanout exercised under load | +| w44-windows-allocator-prod | `Cargo.toml`, `jemalloc.md`, rollout flag | Windows parity verified; prod canary green for 7d | +| w44-brew-winget-signing | `.github/workflows/release.yml`, signing keys | brew tap live; winget PR merged; Authenticode green | +| w44-pii-or-kms | `src/domain/redact.rs` OR `crates/sl-kms/` | One of (L22 or L24) closed with evidence; other tracked | +| w44-fluent-migration | `locales/en-US.ftl`, viewer/CLI string tables | `.ftl` adopted for CLI strings; viewer partial; human sign-off pending | +| w44-corpus-breadth | `tests/corpus/*.jsonl`, `tests/replay_breadth.rs` | 10× current fixture count; replay coverage 80%+ | + +## Score disposition (target) + +**Pre-W44:** 396/402 (98% A) +**Target post-W44:** 402/402 (100% A+) **iff** all six residuals close. +**Realistic post-W44:** 398–401/402 (99–100% A+); R-3 and R-4 may slip if +human-gated keys/policy not received in window. + +## Decision points (human-owned) + +- **D-W44-1:** Pick R-4 branch (L22 KMS vs L24 PII redaction). Cannot close + both in this wave; recommend L22 (smaller blast radius). +- **D-W44-2:** Accept partial W44-B5 close (machine-tooling only; viewer + localization deferred to W45). +- **D-W44-3:** R-3 signing keys availability. If not received within 7d of + W44-B3 start, wave downgrades R-3 to partial and defers to W45. + +## Risk register + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Human-gated R-3 keys slip | R-3 partial close | Document partial in SCORECARD; defer to W45 | +| Human-gated R-4 policy unclear | R-4 lane block | Surface D-W44-1 early; pick L22 | +| R-2 canary rollback in prod | R-2 partial close | Canary 7d window; auto-revert on error rate spike | +| Loc tooling merge conflicts with viewer | W44-B5 partial | Land tooling PR first; viewer PR after viewer owner review | +| Corpus fixture bloat (>50MB) | CI timeout | git-lfs + selective replay in nightly only | + +## Org mirror (W44-E) + +`phenotype-org-audits` archived wave mirror pattern from W43 (#170). W44-E +emits a single skeleton PR containing: +- `audits/SessionLedger/wave-44/SCORECARD.md` +- `audits/SessionLedger/wave-44/DELTA.md` +- `audits/SessionLedger/wave-44/EVIDENCE.md` + +W44-E is `human` because target repo `phenotype-org-audits` requires manual +approval per W43-E precedent. + +## Acceptance criteria for W44 closure + +- [ ] B1–B6 PRs all MERGED (or partial with human sign-off) +- [ ] SCORECARD.md updated to W44 score +- [ ] TRACEABILITY.json updated (`updated`, `commit`, `wave`) +- [ ] GAP_QA_MATRIX.md updated for any closed residual +- [ ] CHANGELOG.md Unreleased Changed entry for W44 +- [ ] Org mirror PR opened (W44-E) + +**Owner:** machine (W44-A through W44-D, plus partial B5/B6) + human (R-3 keys, +R-4 policy decision, W44-E approval).