Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/hermetic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ 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).

- 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`.
Expand Down
70 changes: 70 additions & 0 deletions WAVE44_SCOPE.md
Original file line number Diff line number Diff line change
@@ -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: <W44-tip>`,
`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)
22 changes: 17 additions & 5 deletions audit/SCORECARD.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion ci/hermetic-builder/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion crates/sl-daemon/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sessions-dir> --out <okf-out-dir>"
sl-daemon serve --out <okf-out-dir> (auto-discovers local session roots)"
)
}

Expand Down
40 changes: 40 additions & 0 deletions crates/sl-daemon/src/discovery.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>) -> Vec<PathBuf> {
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());
}
}
26 changes: 24 additions & 2 deletions crates/sl-daemon/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,32 @@ pub async fn serve(
addr: SocketAddr,
state: AppState,
shutdown: impl std::future::Future<Output = ()> + 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> {
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<Output = ()> + 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)
}

Expand Down
43 changes: 34 additions & 9 deletions crates/sl-daemon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod archive;
mod audit;
mod banner;
mod cli;
mod discovery;
mod etl;
mod export;
mod filter;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PathBuf>,

/// Directory to write `<session-id>.okf.json` files into.
#[arg(long)]
Expand Down Expand Up @@ -620,12 +623,22 @@ fn audit_event(
// ---------------------------------------------------------------------------

async fn run_serve(
watch: PathBuf,
watch: Option<PathBuf>,
out: PathBuf,
once: bool,
http_bind: String,
memory_db: Option<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
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");
Expand Down Expand Up @@ -774,9 +787,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
Expand All @@ -789,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?;
Expand All @@ -802,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;
Expand Down
15 changes: 11 additions & 4 deletions crates/sl-daemon/src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<PathBuf>> {
let mut out = Vec::new();
collect_transcripts(dir, &mut out)?;
out.sort();
Ok(out)
}

fn collect_transcripts(dir: &Path, out: &mut Vec<PathBuf>) -> 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 {
Expand Down Expand Up @@ -94,7 +101,7 @@ pub fn spawn_fs_watcher(
}
}
})?;
watcher.watch(dir, RecursiveMode::NonRecursive)?;
watcher.watch(dir, RecursiveMode::Recursive)?;
Ok(watcher)
}

Expand Down
Loading
Loading