Note
Archived. senkani is no longer maintained. The repository is read-only; the code and the project page stay available. Current work: ckluis.github.io/experiments.
One macOS binary, two jobs: a native multi-pane workspace (SwiftUI, sub-3ms renders, 20 pane types) and an MCP intelligence layer that cuts 50–90% of the tokens your AI spends on perception. Compression, symbol indexing, secret redaction, local validators, and Layer-3 hook interception run before the request ever leaves your machine. No workflow changes — point Claude Code at it and your session just costs less.
git clone https://github.com/ckluis/senkani.git && cd senkani
swift build -c release
.build/release/SenkaniApp # launch the workspace (auto-registers global MCP + hook wrapper)
cd /path/to/your/project
.build/release/senkani init # register PreToolUse + PostToolUse hooks for THIS project
.build/release/senkani doctor # verify setup + MCP registrationSenkaniApp's first launch installs the hook wrapper at ~/.senkani/bin/senkani-hook
and writes the global MCP entry to ~/.claude/settings.json. The MCP server only
activates inside Senkani-managed terminal panes (gated by SENKANI_PANE_ID) — non-
Senkani terminals never see the tools even if the app is running. senkani init
is per-project: it only writes hook entries to <project>/.claude/settings.json,
never global. See CHANGELOG.md for the full shipped feature list
and roadmap.
The empty-workspace surface is project-first and task-first: pick a project
folder, then choose one of four verb-led starters — Ask Claude in <project>,
Use Ollama in <project>, Open a tracked shell, or Inspect this project.
The full 20-pane gallery is one level deeper behind a Show all panes link
for advanced users. The first agent launch auto-assembles a witnessed layout —
Ask Claude and Open a tracked shell open the terminal next to a live
Agent Timeline pane so optimization events appear as the user works, with
no manual "open Agent Timeline" step. Subsequent launches add only the primary
pane so re-clicking a starter never stacks duplicate timelines.
The compact F C S I T letters in every pane header — Filter, Cache,
Secrets, Indexer, Terse — open the pane's settings panel when clicked,
where each toggle sits next to its one-line explanation and a
Learn more → link to
docs/concepts/per-pane-optimizers.html.
Letter color tint conveys on/off state at a glance; toggling lives inside
the panel so the explanation is always one click away. Every early-use
empty state — Analytics, Knowledge, Models, Sprint Review — ends in a
concrete next action instead of a passive "data will appear" wall.
A modest Next: <step> banner on the Welcome screen surfaces the next
early-use milestone the user hasn't reached yet (project picked → agent
launched → first tracked event → first non-zero savings → first budget
set → first workstream created → first staged proposal reviewed); the
banner disappears once all seven fire. When the next milestone is
first workstream created, the banner becomes tappable and opens
New Workstream inline, so a fresh-install user has an active CTA
alongside the passive WORKSTREAMS sidebar — which is now visible
whenever a project is active, instead of unlocking only after the
project already has two or more workstreams. Removing a project or
workstream is a hover-revealed X on the sidebar row that opens a
tiered-checkbox confirmation dialog (operator picks per-artifact what
to delete — sidebar entry mandatory, per-project app-support directory,
per-workstream .worktrees/<slug>/, per-workstream feature branch);
branches with unpushed commits surface an inline warning, force-remove
is an explicit opt-in only after a failure, and + New Workstream
discloses the worktree path + branch name (editable) before it fires.
The milestone log is
local-only at ~/.senkani/onboarding/milestones.json (mode 0600, no
upload path) — opt out with SENKANI_ONBOARDING_MILESTONES=off.
Inspect the live env-var state + on-disk JSON with
senkani onboarding milestones (read-only; ignores the env gate so
=off still surfaces on-disk truth — the gate is opt-out-of-recording,
not opt-out-of-observability). cat ~/.senkani/onboarding/milestones.json
is the script-friendly raw fallback.
See docs/guides/first-session.html
for the walkthrough.
23 tools that sit between Claude and your filesystem, compressing everything before it hits your token budget.
| Tool | What it does | Savings |
|---|---|---|
senkani_read |
File reads: returns outline by default (symbols + line numbers), full content via full: true. Cache, secrets, filter on full reads. |
80–99% |
senkani_exec |
Shell commands: 24+ filter rules. Background mode for long builds (poll, kill). Adaptive truncation. | 60–90% |
senkani_search |
Symbol lookup: BM25 FTS5-ranked results + optional RRF fusion with MiniLM file embeddings. ~50 tokens vs ~5000 for grepping. | 99% |
senkani_fetch |
Read only a symbol's lines, not the entire file | 50–99% |
senkani_explore |
Navigate codebase via import/dependency graph | 90%+ |
senkani_deps |
Query bidirectional dependency graph (what imports X, what X imports) | — |
senkani_outline |
File-level structure: top-level functions, classes, types | — |
senkani_validate |
Local syntax validation across 20 languages. full: true for complete output (summary is default). |
100% |
senkani_parse |
AST dump via tree-sitter | — |
senkani_embed |
Text embeddings on Apple Silicon (no API cost) | $0/call |
senkani_vision |
Vision model on Apple Silicon (no API cost) | $0/call |
senkani_watch |
FSEvents ring buffer — query changed files by cursor + glob | near-zero |
senkani_web |
Render http:///https:// page with full JS, return AXTree markdown. DNS-resolved SSRF guard + redirect re-validation; file:// not accepted (use senkani_read). The W.2 MarkdownFirstFetcher gives a three-tier ladder (Accept: text/markdown → HTML→markdown transform → headless render) with caller-forceable method; tier wired into the tool path is W.2-bis. |
~99% vs raw HTML |
senkani_pane |
Control workspace panes — open, close, focus, resize (via Unix socket) | — |
senkani_session |
View stats, toggle features, pin/unpin symbol context (pin/unpin/pins) |
— |
senkani_knowledge |
Query/update the project knowledge graph — entities, links, decisions, FTS5 search. full: true for complete entity detail (summary is default). |
near-zero |
senkani_version |
Version negotiation: server_version, tool_schemas_version, schema_db_version, list of exposed tools. Cache client schemas keyed on tool_schemas_version. |
— |
senkani_bundle |
Budget-bounded repo snapshot. Local mode composes symbol outlines + dep graph + KB entities + README in a canonical truncation-robust order. Remote mode (remote: "owner/name") snapshots any public GitHub repo via senkani_repo — same host allowlist + SecretDetector. Emits format: "markdown" (default) or stable-schema format: "json" (BundleDocument). preview: true (U.10a) returns a ContextManifest review surface (lanes + modes + per-item token estimates) instead of the body; modes: and lanes: narrow the preview. U.10b modes ship live: slice: "<path>:<start>:<end>" adds a file-lane slice item with the resolved content; diff: "<selector>" (unstaged/staged/branch:<ref>/range:<a>..<b>) shells git diff and emits one diff-lane item per affected file; summary mode emits per-entity Gemma summaries (falls back to KnowledgeEntity.compiledUnderstanding, or summary_unavailable if neither is present). U.10a-2 secret gate: preview is refused when any item carries a SecretDetector hit unless allow_secrets: true is passed — every override writes a chained bundle.secret.allow audit row, every preview call writes a chained bundle.dispatch audit row. Path-validated. |
repo-level |
senkani_repo |
Query any public GitHub repo without cloning. Actions: tree / file / readme / search. Host-allowlisted (api.github.com + raw.githubusercontent.com), anonymous by default (60 req/h); GITHUB_TOKEN env raises the limit. All responses SecretDetector-scanned. TTL+LRU cache. |
query-level |
senkani_search_web |
Web search via DuckDuckGo Lite (no key, no quota). Returns {title, url, snippet} triples. Host-pinned to lite.duckduckgo.com, SSRF-guarded, redirect-pinned, every snippet + title scanned by SecretDetector. guard-research denies queries containing absolute paths, globs, or secret-shaped tokens. CAPTCHA / soft-block pages auto-rotate to the next region in [primary, wt-wt, us-en, uk-en, de-de] within the same call (bounded 2 HTTP attempts) and persist a 30-min cool-down to ~/.senkani/state/search_web_cooldown.json (mode 0600); when every region is cooling the call returns a structured allRegionsCooling error naming each expiry. Snippets are adversarial third-party text — pass URLs through senkani_web before acting. |
query-level |
Compound learning (Phase K) — the system learns your workflow across sessions. Proposals go .recurring → .staged → .applied with a lazy session-start sweep (recurrence ≥ 3 + confidence ≥ 0.7). Four artifact types:
- Filter rules (H, H+1) —
head(50)+ substringstripMatching(...)from the post-session waste analyzer. Regression-gated on realcommands.output_previewsamples. Laplace-smoothed confidence. - Context docs (H+2b) — files read across ≥3 distinct sessions become priming documents at
.senkani/context/<title>.md, injected into the next session's brief as a one-line "Learned:" section. Body isSecretDetector-scanned on every read/write. - Instruction patches (H+2c) — tool hints derived from per-session retry patterns. Never auto-apply from the daily sweep — Schneier constraint forces explicit
senkani learn apply <id>. - Workflow playbooks (H+2c) — named multi-step recipes mined from ordered tool-call pairs within 60 s. Applied at
.senkani/playbooks/learned/<title>.md— namespace-isolated from shipped skills.
Thresholds are operator-tunable via ~/.senkani/compound-learning.json or SENKANI_COMPOUND_* env vars. CLI: senkani learn status --type <filter|context> · apply · reject · sweep · enrich · config {show,set} · review [--days N] (sprint cadence) · audit [--idle D] (quarterly currency review).
Gemma 4 optionally enriches rationale strings (H+2a) — contained to a dedicated enrichedRationale field, never enters FilterPipeline.
Knowledge base (Phase F + F+1..F+5) integrates with compound learning: .senkani/knowledge/*.md is the source of truth, SQLite is a rebuilt index (KBLayer1Coordinator detects staleness + corrupt-DB recovery), entities mentioned ≥5× per session get queued for Gemma enrichment, EnrichmentValidator flags information loss / contradiction / excessive rewrite before commit, senkani kb rollback / history / timeline wrap the append-only evidence + history archive. KBCompoundBridge knits the two systems: high-mention entities boost compound-learning confidence; applied context docs seed KB entity stubs; rolling back a KB entity cascades to invalidate derived context docs.
Plain-md vault (Phase V.7) makes the markdown vault portable. Set kb_vault_path in ~/.senkani/config.json (or pass senkani kb migrate --to <path>) to relocate the vault out of the project tree — useful for keeping it next to existing Obsidian vaults. The resolver appends <project-slug> per-project so multi-project vaults don't collide. senkani kb migrate and senkani kb unmigrate move files in either direction; both are content-hash idempotent and surface conflicts rather than silently overwriting. WikiLinkResolver provides click-through resolution for [[Name]] with folder-hint disambiguation.
Authorship tags (Phase V.5) carry explicit provenance — ai-authored / human-authored / mixed / unset — on every KB entity. The save path prompts before persisting an ambiguous row (V.5b), senkani authorship backfill heals legacy NULL rows under a chain-participating audit row (V.5c), and the KB / Timeline / Skills panes render typographic badges that surface "Untagged" rather than silently inferring a tag (V.5d). Cavoukian's contract: provenance metadata, never policy.
HandManifest skills (Phase U.5 round 1) are a portable canonical capability-package shape: one JSON manifest carries tools, settings, metrics, multi-phase system_prompt, skill_md, guardrails (requires_confirm / egress_allow / secret_scope), cadence (HookRouter triggers + cron), sandbox, and declared capabilities. senkani skill lint <path> enforces 12 schema invariants (kebab-case names, requires_confirm ⊆ tools[], known cadence triggers); senkani skill export --target claude-code|cursor|codex|opencode|senkani <path> round-trips one source into per-harness output (claude-code SKILL.md and senkani WARP.md are first-class; cursor/codex/opencode emit canonical envelopes pending V.10/V.11 hardening). Schema v1 is frozen at spec/skills.md.
SkillPacks (Phase V.11a–V.11b) bundle multiple HandManifest skills with a HookRouter policy fragment and a context doc into one unit-of-distribution. senkani pack install <dir> copies the pack tree under ~/.senkani/packs/<name>/, runs collision detection on skill names + policy scope-keys + context filenames (refuses without --force), and writes a chained pack_audits row provenance-tracked through T.5. senkani pack uninstall <name> reverses the operation with zero residue and a chained uninstall row. senkani pack list surfaces installed packs + version. Three in-tree packs ship under spec/packs/: code-quality (denies lint-suppression markers — eslint-disable-next-line, swiftlint:disable, # noqa), security (denies hard-coded API_KEY=, password = " literals, eval( invocations), and devops (denies kubectl delete, terraform destroy, --context=prod targeting). HookRouter merges installed pack policy fragments live: PackInstaller re-reads on apply() / uninstall(), and the daemon re-reads on every hook event whose install-root mtime has advanced — no app restart required between install and policy-firing. Format frozen at spec/skill_packs.md.
Session continuity (Phase W.4) — ContextSaturationGate is a pure decision (.ok / .warn / .block) that reads tokens_in + tokens_out from agent_trace_event and compares against a configurable budget (defaults: warn 65 %, block 80 %, 200 000-token active window). When the gate blocks, PreCompactHandoffWriter lands a structured handoff card under ~/.senkani/handoffs/<sessionId>.json — openFiles, currentIntent, lastValidation (pulled from validation_results), nextActionHint, recentTraceKeys — atomically (temp+rename, <1 s SLO). PreCompactHandoffLoader.load(...) / loadLatest(...) reads the card on the next session start and returns nil for missing / corrupt / future-schema files. HookRouter PreCompact wiring + a status-bar saturation chip + senkani doctor --handoff are W.4-bis (operator-decision pending).
Annotation evidence (Phase V.6 round 1) lets the operator mark working/failing portions of a skill or KB entity. Each row is append-only — target_kind (skill / kb-entity), inclusive byte range, verdict (works / fails / note), free-text notes, an authored_by handle, and a V.5 AuthorshipTag (explicit by construction). renameAnnotationTarget rewrites target_id so the lineage survives an artifact rename or fork. AnnotationSignalGenerator rolls up evidence per (kind, target) and feeds it into CompoundLearning.runPostSession — read-only in round 1: each evidence row bumps compound_learning.annotation.observed plus a failing / working / mixed counter. Future rounds (V.6 round 3) can wire annotations into the Propose pathway; round 1 stops at evidence so an operator's fails call is never silently mutated into a learned rule.
A horizontal canvas of panes. Each pane is a primitive type; you arrange them however makes sense for what you're doing right now, and Senkani persists the layout per project.
20 pane types:
- Terminal — SwiftTerm, configurable font size, kill/restart buttons, broadcast mode. The active terminal pane shows a five-chip "Senkani Active" proof strip (PROJECT, MCP, HOOKS, TRACK, EVENTS) that surfaces a runnable next action whenever a setup component is missing — no need to wait for the first intercepted command to know whether Senkani is wired in.
- Dashboard — multi-project portfolio: total savings, project table, feature charts, insights, Models & Inference tile (MLX prefix-cache hit rate + cached tokens + active tier + memory pressure, JOIN of
cache_lifecyclespans ×token_eventscache rows with a stale badge on JOIN mismatch) - Code Editor — tree-sitter syntax highlighting (25 languages, incl. Dart/TOML/GraphQL), symbol navigation, file tree
- Browser — WKWebView embedded, localhost or any URL. Optional click-to-capture Design Mode (env-gate
SENKANI_BROWSER_DESIGN=on, ⌥⇧D toggles) — click an element, get a fixed-schema Markdown block on the clipboard. - Markdown Preview — live render from file, updates on save
- Analytics — token/cost savings with charts, persistent across restarts
- Model Manager — install → verify → ready state machine for local ML (MiniLM-L6 embeddings + Gemma 4 vision tiers). One-click Install drives
Available → Installing N% → Installed → Verifying… → Ready; verification loads the model into an MLX container (real fixture) and flips toReadyorVerification failedwith a one-click Re-verify. Per-tier output quality is measured bysenkani ml-eval— runs the 20-task harness against each installed Gemma tier, writes~/.senkani/ml-tier-eval.json, andsenkani doctorthen surfaces the rating (excellent / acceptable / degraded) so 8 GB Mac users learn the smaller tier's quality cost up-front. - Savings Test — fixture bench + live session replay + scenario simulator
- Agent Timeline — timeline of optimization events, interactive tool calls, and scheduled-task runs (start/end/blocked). Rows whose
session_idcorrelates to OTLP spans withstatus_code = ERRORorduration > p99show a runtime-error badge dot (V.18a-7); first click expands a compact inline summary, second click opens a side pane that loads the full span tree on demand - Knowledge Base — project knowledge entities, freshness indicators
- Diff Viewer — LCS-hunk blocks with a severity-tagged annotations sidebar (
[must-fix]/[suggestion]/[question]/[nit]); click any annotation to jump to its hunk. HookRouter denials that block real work (ConfirmationGate, budget gate) feed[must-fix]annotations into the same sidebar viaHookAnnotationFeed(V.12b); a per-minute rate cap suppresses must-fix floods without changing the deny response, with each closed window writing toannotation_rate_cap_log(Migration v13). - Log Viewer — searchable log output
- Scratchpad — auto-saving markdown notepad
- Schedules — manage recurring tasks via launchd. The New Schedule form composes a cadence three ways — prose ("every weekday at 9am", compiled live), counter ("every 10 tool_calls"), or cron (preset or custom) — with a live
AmplificationGuardverdict banner, aCronPreviewnext-fires preview, and a Create button gated on an OK verdict (amplifying schedules need an explicit override). Ships with five day-1 presets (log-rotation,morning-brief,autoresearch,competitive-scan,senkani-improve) installable from the pane or viasenkani schedule preset install <name>; each preset's resolved command is secret-scanned before install, and missing companion prerequisites (Ollama daemon,guard-researchhook preset,senkani briefCLI, …) surface as warnings, not blockers. Existing schedules support edit-in-place (selecting one reopens the compose surface prefilled with its current cadence), drag-to-reorder (persisted viaScheduleStore), and inline validation tooltips surfacing theAmplificationGuardrationale and any compile error. - Autorun loop (Phase U.3, legs 1–4) —
senkani autorun --tasks <path> [--dry-run] [--supervise-first N] [--allow-classes <list>]runs a free-form markdown task file (one top-level bullet per task) as a one-task-one-commit loop with a per-task validation gate. ATaskDecomposerconverts each bullet into a U.11WorkstreamTaskContract(inline[cmd: …]/[files: …]annotations, default test/lint gate otherwise) and persists the plan to~/.senkani/autorun/<run-id>/contracts.json(atomic, canonical JSON) as a resume seam. Each task's gate commands run with CI-style short-circuit through the existingValidationStore(a "clean" row per command, scoped to a uniqueautorun-<run-id>session); all-clean commits and emits anotifyDone, any failure HALTS the loop with anotifyFailure.--dry-runprints the decomposed plan + per-task gate and exits without executing anything.--supervise-first N(leg 2) pauses for an operator y/n AFTER the gate clears and BEFORE the commit-event on the firstNtasks —naborts the run (distinct from a gate halt), tasks at index ≥ N run unattended; it defaults to0(fully unattended) so CI / non-interactive / dry-run stay safe, and refuses up front when no operator is on the TTY. The supervision question arrives through an injectedSupervisionPromptseam so the Core spine never reads stdin (headless-tested). An unattended run (stdin not a tty) refuses unless Pushover is seeded, namingsenkani doctor --seed-pushover-key.--allow-classes <comma-list>(leg 3) restricts the loop to tasks whose inferredTaskClass(a whole-word keyword heuristic —test-fix/bug-fix/feature/docs/refactor/chore/unknown, stamped per bullet) is on the list; a task whose class is off a non-empty list pauses for an operator ack regardless of--supervise-first(an empty list = allow-all, an unrecognized/unknownclass is never auto-allowed — fail-safe). The class rides in thecontracts.jsonenvelope only (no DB column). Leg 4 wires the real seed-gated Pushover transport: anAutorunSinkSelectorruns a VALUE-FREE (CredentialVault.list, neverread) FAIL-CLOSED probe for the seeded credential and, when present, installs the realKeychainPushoverTransport(otherwise an inert sink) — the same probe drives the unattended-refusal gate, replacing leg 1's fake-transport stopgap. It is plumbing: the branch stays inert until the operator seeds the credential and the real macOS Keychain (T.4c) lands. The TUI / decomposer pane, the real-device Pushover push proof (operator leg C), and first-run approval are later legs (ctrl+.WIP-stash halt is disqualified — the loop never mutates the working tree). - Skill Library — browse, install, and manage AI agent skills
- Sprint Review — GUI counterpart to
senkani learn review: accept/reject staged compound-learning proposals (filter rules, context docs, instruction patches, workflow playbooks) plus a stale-applied section mirroring the quarterly audit - Artifact Gallery — split-pane browser over PaneDiary entries, SprintReview snapshots, and on-disk files under
~/.senkani/artifacts/. Reads through V.9a'sArtifactStore(filter by tag / source-pane / version / since). Single-click selects; double-click navigates to the source pane (paneDiary focus, SprintReview focus, Finder reveal for filesystem). Inline lineage drill-in steps back throughversions(of:). Redacted artifacts gate behind a confirmation sheet that writes a chainedartifact.secret.allowaudit row on override. - Ollama — first-class local-LLM launcher: availability-gated, per-pane default-model selector, same MCP env injection terminal panes get. Header download button opens a curated-catalog drawer: pick from 5 LLMs (llama3.1:8b, qwen2.5-coder:7b, deepseek-r1:7b, mistral:7b, gemma2:2b), each row discloses size before the pull click and streams
ollama pullprogress into the UI. Install CTA when the daemon isn't running. - Served Requests — read-only live feed of
senkani serve --openai's OpenAI-compatible requests (metadata only — never request/completion bodies). One row per served/v1call: relative-age timestamp, surface,model_logged(raw, producer-sanitized), resolved tier, input/output tokens, key label, and HTTP status with semantic color. Refreshes on a 500ms poll while visible; per-column VoiceOver labels.
⌘K command palette opens everything: new panes, themes, actions. Search-as-you-type with category grouping.
Senkani is a trust boundary for LLM-driven tool calls. Security-sensitive features default to on; opt-outs are explicit env vars, not hidden flags.
- Prompt injection guard — on by default.
InjectionGuardscans every MCP tool response for instruction-override, tool-call injection, context-manipulation, and exfiltration patterns, with anti-evasion normalization (lowercase, zero-width strip, Cyrillic→Latin homoglyphs). Runs in a single linear pass. Override:SENKANI_INJECTION_GUARD=off. - Web fetch SSRF hardening.
senkani_webresolves the target host viagetaddrinfobefore fetch and blocks any address in private/link-local/CGNAT/multicast ranges (including IPv4-mapped IPv6, octal/hex IPv4, and IPv4-compatible IPv6). Redirects are re-validated viaWKNavigationDelegate.decidePolicyFor— a 3xx Location header to10.x/169.254.169.254/::ffff:…is cancelled.file://scheme is not accepted — read local files withsenkani_read. Subresource filter (F2): aWKContentRuleListblocks<img>/<script>/<xhr>/etc. requests to the same private ranges — a hostile HTML page embedding<img src="http://169.254.169.254/…">cannot reach cloud metadata through WebKit's auto-rendering. Override for internal docs servers:SENKANI_WEB_ALLOW_PRIVATE=on. - Secret redaction — on by default.
SecretDetectornow short-circuits withfirstMatchso no-match inputs don't pay the full regex cost (1 MB benign input scans in ~25 ms). - Schema migrations — versioned + crash-safe. Session DB uses
PRAGMA user_version+ aschema_migrationsaudit log. Cross-process coordination viaflocksidecar. On failed migration, a kill-switch lockfile is written and subsequent boots refuse to run migrations until the operator inspects the DB. - Retention — scheduled.
RetentionSchedulerprunestoken_events(90 d),sandboxed_results(24 h), andvalidation_results(24 h) on an hourly tick. Tune via~/.senkani/config.json→"retention": { "token_events_days": 30, ... }. - Instruction-payload byte cap. The
instructionsstring injected at MCP server start (repo map + session brief + skills) is capped at 2 KB by default. Tune viaSENKANI_INSTRUCTIONS_BUDGET_BYTES. Prevents the per-session-start token tax from growing with project size. - Socket authentication — opt-in. Setting
SENKANI_SOCKET_AUTH=ongenerates a 32-byte random token at~/.senkani/.token(mode 0600), rotated on every server start. Every connection tomcp.sock/hook.sock/pane.sockmust send a length-prefixed handshake frame matching the token before normal protocol begins. Raises the bar from ambient same-UID socket access to must-read-token-file — blocks prompt-injected subagents and postinstall scripts that don't parse dot-files. Default off this release for backward compat; flipping to on next release. - Structured logging — opt-in + sink-redacted.
SENKANI_LOG_JSON=1emits one JSON object per critical event to stderr. Every.string(_)log field passes throughSecretDetector.scanat emit time (Cavoukian C5), so a stray API key / bearer token / AWS / Slack / Stripe / GCP / npm / HuggingFace / GitHub token in a log field is automatically[REDACTED:…]'d. UseLogValue.path(_)for filesystem paths —/Users/<name>collapses to~(current user) or/Users/***(foreign). Default is backward-compatible[event] key=valueformat. - Observability counters — surfaced via CLI + MCP tool. Every security-defense site increments an
event_countersrow (migration v2): injection detections, SSRF blocks, socket handshake rejections, schema migrations, retention prunes, command redactions. Read them viasenkani stats --security(Gelman rate annotation:count/total (pct%)) orsenkani_session action:"stats". Per-project paths are redacted (Cavoukian C2). - Published SLOs — surfaced via
senkani doctor. Four latency contracts on the hot path (cache hit p99 < 1 ms, pipeline cache-miss p99 < 20 ms, hook passthrough p99 < 1 ms, hook active p99 < 3 ms). 24-hour rolling window with a 1% error budget.senkani doctorshows green / warn / burn / unknown per SLO with the rolling p99 and sample count. The CI perf gate (tools/perf-gate.shandTests/SenkaniTests/SLOTests.swift) synthesises a representative workload for each SLO and fails the build if any p99 crosses its ceiling. Seespec/slos.md. - Release commitments — five shape numbers per release. Beyond the runtime SLOs above, every release reports cold-start (< 250 ms p95), idle memory (< 75 MB), install size (< 50 MB), classifier latency (< 2 ms p95, slot pending U.1 TierScorer), and OpenAI-endpoint cold-start (< 1500 ms p95 —
senkani serve --openaispawn → listener-ready, Phase V.13e-3). Capture istools/measure-slos.sh, which appends a row to~/.senkani/slo-history.jsonlwithgit_sha+versionfor trend correlation.senkani doctorreads the latest row and runs a median-of-5 baseline regression check — anything ≥10% over baseline fails the gate. Improvements never fail. - Data portability (GDPR-adjacent).
senkani export --output <file> [--since DATE] [--redact]streams sessions + commands + token_events as JSONL via a read-only SQLite connection — doesn't block the live MCP server. - Tamper-evident audit chain (Phase T.5). Every row in fourteen chained tables —
token_events,validation_results,sandboxed_results,commands,pane_refresh_state,confirmations,trust_audits,policy_snapshots,egress_decisions,pack_audits,eval_results,surrogate_writes,workstream_handoffs, andopenai_request_log(introduced across migrations v4 / v5 / v6 / v11 / v12 / v17 / v19 / v20 / v24 / v27 / v40 / v41, listed migration-chronologically so future additions append rather than re-sort) — carriesprev_hash+entry_hash+chain_anchor_id.entry_hash = SHA-256(prev_hash || canonical_row_bytes)— a single-byte tamper at row N invalidates row N's hash and every subsequent row's hash.senkani doctor --verify-chainwalks every chain and reports OK or names the first broken(table, rowid); the integrity line surfaces insenkani doctoraschain integrity: OK since <ISO-date> / N repairs. Recovery viasenkani doctor --repair-chain --table <T> --from-rowid <N>opens a fresh chain segment with typed-string double-confirm, tty enforcement, and prior-tip linkage in the new anchor'soperator_note. The repair surface covers every integer-keyed chain participant;sandboxed_results(TEXT primary keys) is repaired by hand pending an--from-created-atflag. - Confirmation gate (Phase T.6a). Every write/exec-tagged tool call walks
ConfirmationGateand writes a chainedconfirmationsrow recording the decision (approve/deny/auto) and who decided it (operator/policy/auto).MCPToolCatalogtags Claude Code hook tools (Edit/Write/Bash) and the senkani MCP surface withread/write/exec/network; read-only tools short-circuit with no row. Default policy is auto-approve so today's flow is unblocked, but every approval is auditable through the T.5 chain. Real notification adapters (StdoutSink,MacOSLocalSink,PushoverSink) land in T.6b/T.6c; round 1 ships theNotificationSinkprotocol + null/mock implementations + a fan-out helper that swallows throws so a bad adapter doesn't block other sinks. - Notification sinks (Phase T.6b + T.6 production hookup, 2026-05-21).
StdoutSinkwrites one canonical JSON line perNotifyEvent(sorted-key, scalar-only payload + ISO-8601ts) — pipesenkanistdout into a structured-log collector and the gate's outcomes round-trip cleanly.MacOSLocalSinkposts banners via an injectedLocalNotifierBridge; SenkaniApp now wires the realUNUserNotificationCenter-backedUNNotifierBridgeat App boot (requests TCC authorization on first launch) while CLI / MCP / CI run silent viaNullLocalNotifierBridge. CI tests useSpyLocalNotifierBridge.NotificationRouterreads~/.senkani/notifications.json({"sinks": {"stdout": {"events": ["notify_failure"]}}}) to pick which sink fires for which event variant — defaults to subscribe-all when the file is missing. Producers fan out through the process-globalNotificationDeliveryholder, which isnil(silent) in CLI / MCP / test processes and only non-nil once SenkaniApp'sNotificationBootstrapruns. The first production producer fires on the first-everfirstNonzeroSavingsmilestone ("Save your first tokens") viaOnboardingMilestoneStore.record. The Settings → Notifications matrix (2026-06-11) is the sidebar's Notifications tool view: a per-sink × per-event grid of checkboxes that edits~/.senkani/notifications.jsonthrough the deterministicConfigwriter and live-reloads the router on every flip (no app restart) — plus test-fire buttons that push a synthetic event through the live router. The Pushover adapter's Keychain transport is built; its live wiring (real token seed + egress allow rule) is the remaining T.6c operator leg. - EgressProxy live listener (Phase T.1a + T.1a.2 + T.1b + T.1c + T.1b-followup).
EgressRuleEnginematches hosts against an exact / prefix / suffix / single-*glob rule set with deny-wins precedence and default-deny-on-miss;EgressHostNormalizercanonicalizes case, default ports (:80/:443), trailing dots (T.1c added the second post-port-strip dot pass soEXAMPLE.com.:80canonicalizes cleanly), and trailing slashes so a rule forexample.commatchesExample.COM:80.exactly the same way;EgressDecisionStorechains every decision into theegress_decisionsaudit table (migration v19) sosenkani doctor --verify-chainproves no allow/deny was redacted.EgressListenerbinds a POSIX TCP socket to127.0.0.1:<port>(port 0 = kernel-assigned, persisted to~/.senkani/egress.portvia atomic rename), andEgressConnectionHandlerreads the request line, evaluates the host throughEgressRuleEngine, and either pipes to the upstream (plain HTTP rewritten to origin form, CONNECT tunnels with peeked TLS ClientHello SNI validated against the CONNECT host) or replies403 Forbidden. SNI mismatch writes a deny row with sentinelrule_id="sni_mismatch"and tears down without piping a single byte upstream. T.1b layers in aPaneModetaxonomy (research,write,redteam,general— typed enum, parsed from an internalX-Senkani-Pane-Modeproxy header stripped before forward), per-mode allowlists loaded from~/.senkani/egress-policy.json(with fallback to the legacy flategress-rules.jsonfor T.1a operators), and a Gemma judge fallback on static-rule miss (JudgeAdapterprotocol withGemmaJudgeAdapterenforcing a 300 ms hard timeout that defaults to.denyon expiry — Schneier P0) whose rationale lands inegress_decisions.judge_rationale(migration v23 with per-anchor chain shape switch viafresh-install-pre-v23rename +migration-v23anchor opened atMAX(id))..redteampanes deny on every static-miss without invoking the judge — verified by inference-call counter, not by timing (Vitalik posture: no model layer can be asked to allow exfiltration from a redteam pane). T.1c hardens the defense with a 20-scenario adversarial corpus (DNS rebinding ×4, redirect chain to private IP ×4, SSRF ×4, CONNECT-vs-SNI mismatch ×2, allowlist boundary ×4, judge-prompt-injection ×2) that runs as part of everyswift test, plus asenkani doctor --check-egress5-scenario smoke subset (engine-level, 11 ms wall-clock, exits non-zero on any miss) operators (and CI) can run on demand. T.1b-followup ships the write side:PaneModeStore(JSON at~/.senkani/pane-modes.json, NSLock-guarded cache, silent.generaldefault on missing-or-malformed file) backsHookRouter.paneModeResolver, andPaneLaunchEnvinjectsSENKANI_PANE_MODE=<mode>into every pane subprocess env so senkani-aware HTTP clients can emit the matchingX-Senkani-Pane-Modeheader on outbound traffic — proven end-to-end by a redteam integration test that spawns a/bin/bash -csubprocess with the env set, splices$SENKANI_PANE_MODEinto the request header viaprintf, and asserts the daemon recordspane_mode == .redteamwhileMockJudgeAdapter.callCountstays at0.senkani egress start|stopdaemonize / signal via~/.senkani/egress.pid;senkani egress status [--recent N]reports listener state + decision count;senkani doctorflips to "Egress proxy: running on :PORT (decisions: N)" once the listener is up. HTTPS body MITM (T.1d) is tracked separately. - Credential gateway injection (Phase T.4b).
MCPToolConfig.credentialGatewaydeclares whichvaultKeysa tool needs at whichscope, with a per-tooldryRunopt-in forFAKE_KEYplaceholders.HookRouterconsultsCredentialGatewayafter the confirmation gate (so a.denyshort-circuits before any vault read), fetches each declared key fromCredentialVault.shared, and on a missing key +dryRun: falsereturns a structured deny carrying both keyname AND scope so the operator can runsenkani vault addwithout guessing. The audit row written totoken_eventsrecords keyname + scope + the dry-run flag — the credential value itself is never serialized. Round T.4b ships the API surface againstInMemoryKeychainStore; the macOS Keychain conformance for secrets and adversarial corpus eval land in T.4c. The firstsenkani vaultverb shipped early in V.13a-2 —senkani vault add openai-key(below) — backed by a 0600 JSON store of verifier hashes (not secrets), so it does not wait on the T.4c Keychain work. - OpenAI-compatible endpoint bearer auth (Phase V.13a-2).
senkani serve --openai(the V.13a-1NWListenerscaffold) gains bearer-token auth on the/v1/*prefix, enforced before the not-yet-wired501surface.senkani vault add openai-key --preset <auto|build|research|quick|local> [--scope chat,embeddings] [--rate <rpm>] [--expires <iso8601>] [--label <name>]provisions ansk-senkani-…key, prints the plaintext once to stdout, and persists only its SHA-256 hash.--presetis the routing tier the key uses (aModelPresetcase, validated at provision time — a provider name likeopenaiis rejected with the valid-cases list).OpenAIAuthGateenforces the contract: missing/malformed/unknown/expired key →401, out-of-scope surface →403, over the per-key rate limit (default 60 rpm) →429+Retry-After. Key matching is a constant-time, no-short-circuit hash compare that loops every record — "unknown key" and "wrong key" are timing-indistinguishable and return the same401. A key provisioned (or revoked) whileserve --openaiis already running takes effect on the next request — no restart (the listener reads a live(mtime, size)-gated snapshot of~/.senkani/openai-keys.json); and that disk store is sealed to its non-secret scope, refusing to persist secrets under any other scope. Streaming chat / embeddings / tool-use surfaces land in v13b … v13d. - OpenAI-compatible chat (non-streaming) + preset routing + audit (Phase V.13a-3).
POST /v1/chat/completionsreturns a strict OpenAIchat.completionobject (id/object/created/model/choices[].message/usage). The provisioned key'sModelPresetwins routing throughModelRouter— the request'smodel(e.g.gpt-4o) is logged for telemetry only and never changes routing, and the response'smodelreports the actual tier model (Quick →claude-haiku-3.5). Every request appends one tamper-evident audit-chain entry ({ ts, key_label, surface, model_logged, preset_used, resolved_tier, prompt/completion_token_count, status }, hashed with the Phase T.5ChainHasher); request/response bodies are stored only under--audit-bodies(default off). Array-valuedcontentreturns400;stream: trueopens an SSE stream (Phase V.13b, below). This child ships the routing + audit surface; the real local-Gemma chat backend now backs.localend-to-end (Phase V.13 chat-backend-1…4, below). - OpenAI-compatible chat streaming (SSE) (Phase V.13b).
POST /v1/chat/completionswithstream: trueemits Server-Sent Events (Content-Type: text/event-stream) carrying the OpenAIchat.completion.chunkdelta shape — a leading{role: "assistant"}chunk, incremental{content: …}chunks (which concatenate byte-for-byte back to the full message), a terminalfinish_reason: "stop"chunk, then the finaldata: [DONE]sentinel. Rate limiting is enforced before the stream opens (a429is a complete framed response, never a half-open SSE); a client disconnect cancels the in-flight stream on the next chunk write; and a streamed request still appends exactly one tamper-evident audit-chain entry whosestatusdistinguishes completion (ok) from client-cancel (client_cancel).OpenAIChatStream(Sources/Core/OpenAIEndpoint/OpenAIChatStream.swift) holds theNetwork-free framing + drive loop; the liveNWConnectionwriter streams the chunks and tears the connection down exactly once. - OpenAI-compatible embeddings (Phase V.13c).
POST /v1/embeddingsreturns a strict OpenAIlistobject —data[]of{ object: "embedding", index, embedding }plususage(prompt_tokens/total_tokens). Theinputfield accepts a single string or an array of strings; token-id forms and an empty input are rejected with400. The embedding model is sourced fromModelManager(the singleminilm-l6registry entry the MCPsenkani_embedtool already uses —EmbedEngine.modelIdnow referencesModelManager.embeddingModelID, so there is no parallel embedding stack), and the response reports that model. A key withoutembeddingsin scope gets403; each request appends exactly one tamper-evident audit-chain entry withsurface: "embeddings"and no input text.OpenAIEmbeddingsHandler(Sources/Core/OpenAIEndpoint/) is the pure, fully-testable surface; real on-device MiniLM-L6 inference now backs the surface when the MCP target is active (via anEmbeddingEngineprotocol seam +ModelManager.registerEmbeddingHandler), with amodel_not_availableframed 503 when the model isn't downloaded — no auto-pull on the hot path. - OpenAI-compatible tool-use / function-calling (Phase V.13d).
POST /v1/chat/completionsnow acceptstools: [{ type: "function", function: { name, description, parameters: <JSON-Schema> } }]and emitstool_callswhen the model calls a tool —choices[].message.tool_calls[].function.{name, arguments}withcontent: nullandfinish_reason: "tool_calls", whereargumentsis a JSON-encoded string (matching OpenAI exactly). A tool'sparametersJSON-Schema round-trips verbatim (newJSONValue), and a multi-turn[user → assistant(tool_calls) → tool(result)]context is accepted and resolves to a text answer. The declared tools bridge through senkani's existingMCPToolConfigcatalog (newOpenAIToolBridge, function tools →.exec) — no parallel tool registry. Thetoolsscope is body-derived, so it is enforced in the handler: a key withouttoolsin scope gets403 insufficient_scopeeven with a validtoolspayload (a streaming tool-use request cannot bypass it), and a malformed declaration →400. Each tool-use request still appends exactly one tamper-evident audit-chain entry. Tool calls are produced by the placeholder engine until the real model backend lands; SSEdelta.tool_callsstreaming shipped in V.13d-1 (below). - Real on-device Gemma 4 backs
POST /v1/chat/completions(Phase V.13 chat-backend-1, 2026-05-30). When the MCP target is active,senkani serve --openairesolves a registeredChatEngine(new pure-Swift protocol inSources/Core/OpenAIEndpoint/ChatEngine.swift+ aModelManager.registerChatHandler(_:)slot mirroring the V.13c embedding-handler pattern) and routes chat completions throughOpenAIChatServeBridge.syncEngine(...)— aDispatchSemaphore-based sync wrapper over the async chat call so the existingNWListenerclosure signature stays unchanged. The MCP-sideMLXChatEngineAdapter(Sources/MCP/ChatHandlerRegistration.swift) loads the largest Gemma 4 tier that fits in available RAM (gemma4-26b-apex→gemma4-e4b→gemma4-e2b, same fallback chain assenkani_visionandGemmaInferenceAdapter), runs text-only inference underMLXInferenceLock.sharedwith the memory-pressure unload hook, and assembles a role-prefixed prompt ([<role>] <content>per message) so Gemma sees turn boundaries. When MCP is NOT started in-process — pure CLIsenkani serve --openai, unit tests — the listener falls back to the V.13a-3 placeholder and logsopenai-serve chat_backend=placeholderonce at startup so test environments stay self-contained. Non-streaming surface only; the SSE token-by-token streaming path,model_not_availablereadiness 503, tokenizer-accurateusagecounts, and audit-chain regression sweep continue under V.13 sub-items 2-4. Core stays MLX-free (grep -rE '^import MLX' Sources/Core/returns empty); CLI install size unchanged. - Real token-by-token Gemma 4 SSE streaming (Phase V.13 chat-backend-2, 2026-05-30). A
stream: truerequest now emits SSE content deltas as MLX produces each token, not as a post-hoc split over a one-shot completion. Newpublic protocol StreamingChatEngine: Sendable(Sources/Core/OpenAIEndpoint/StreamingChatEngine.swift) returnsAsyncThrowingStream<OpenAIChatHandler.TokenDelta, Error>— the first async-stream shape in the OpenAIEndpoint surface; the MCP-sideMLXStreamingChatEngineAdapter(Sources/MCP/ChatHandlerRegistration.swift) drives a newMLXChatEngine.stream(prompt:)that yields eachGeneration.chunkto the continuation as MLX'scontainer.generateproduces it (clients accumulatedelta.contentverbatim — the streaming path skips end-of-stream trimming because it can't unwrite earlier deltas).OpenAIChatHandler.renderStreamingEvents(...)translatesTokenDeltadeltas into pre-encoded SSE chunks (role chunk + content chunks + terminalfinish_reason: "stop");OpenAIChatStream.Plangains astreamingEvents:async-source init carried through a newdriveStreaming(...)channel pump that bridges the async source to the listener's synchronousSink(peer disconnect → producer Task cancel; producer error →.clientCancel).ServeCommandroutes non-tool-use content streams through the registered handler when present, with an NSLock-protected accumulator teeing each delta so thechatStreamaudit entry'scompletion_token_countreflects bytes actually streamed; tool-use streams (V.13d-1'stool_callsdelta path) and unregistered-handler runs keep the V.13b collected-then-chunk fallback. Core stays MLX-free (grep -rE '^import MLX' Sources/Core/returns empty); the wire shape is unchanged from V.13b (text/event-streamhead,data: {…}\n\nevents,data: [DONE]\n\nsentinel) — only the arrival timing differs. Non-local-tier 503s, readiness 503, tokenizer-accurateusagecounts, audit-chain regression sweep + phase row flip continue under V.13 sub-items 3-4. - OpenAI-compatible streaming tool-calls (Phase V.13d-1). A
stream: truerequest that resolves to a tool call now emits OpenAI-shapedchoices[].delta.tool_calls[]SSE fragments instead of dropping the call into an empty content stream (the V.13d deferral). The first fragment for a tool call carries{ index, id, type: "function", function: { name, arguments: "" } }; subsequent fragments appendfunction.argumentsstring pieces that concatenate byte-for-byte back to the full JSON-encoded arguments string; a terminal chunk carriesfinish_reason: "tool_calls"before thedata: [DONE]sentinel. The tool-call path reuses the sameOpenAIChatStreamchunk model,run(…)drive loop, andsplitForStreamingsplitter as the content path — no parallel streaming stack — with a newtoolCallEvents(…)builder and a sparseChunk.Delta.ToolCallFragment(indexalways present so a client keys fragments by array index). Scope-gate parity holds: an out-of-scope or malformedstream: truetool-use request is refused with a framed403/400before any SSE byte (no half-open stream), and each streamed tool-use request still appends exactly one tamper-evident audit-chain entry (statusdistinguishingokvsclient_cancel). - Soft-flag fragmentation detector (Phase U.4a).
FragmentationDetectorwatches a per-session sliding window of HookRouter events and emits flags when three patterns hit:toolBurst(≥3 same-tool calls inside 10 s),fragmentStitch(overlapping prompt fragments inside 30 s),crossPane(same tool in two panes inside one session).TrustScoreraggregates flags into a 0–100 score. Flags persist into the chainedtrust_auditstable (Migration v12) — operator FP/TP labels are append-only rows so re-labelling is detectable. The detector is non-blocking by design: nothing in HookRouter's denial path reads its output. The Trust Flags sidebar tool surfaces the flags with plain-language "False alarm" / "Real" buttons, andsenkani doctorreportstrust flags — soft flags last 30d: N | confirmed FP: M | confirmed TP: K. Promotion to blocking is U.4b — gated on a 30-day operator-labelled FP rate.
Call senkani_version (tool) or senkani doctor to confirm the active security posture.
Layer 1 — MCP Tools: The agent calls senkani_read instead of Read. Cached, compressed, secret-redacted.
Layer 2 — Smart Hooks: Budget enforcement, metrics, compliance tracking, auto-validate reactions. PostToolUse triggers background syntax/type checking.
Layer 3 — Preemptive Interception: 5 patterns that work with agents that can't cooperate:
- Re-read suppression (file already served + unchanged → deny)
- Command replay (deterministic test/build + no file changes → deny with cached result)
- Trivial routing (ls/pwd/echo → answer in deny reason, saves round-trip)
- Search upgrade (3+ sequential reads → hint to use senkani_search)
- Redundant validation (covered by command replay for build/lint/check commands)
Per-pane model presets control which Claude model handles tasks:
| Preset | Model | Est. Cost |
|---|---|---|
| Auto | Difficulty-scored routing | ~$0.30/hr |
| Build | Sonnet 4 | ~$0.45/hr |
| Research | Opus 4 | ~$2.25/hr |
| Quick | Haiku 3.5 | ~$0.12/hr |
| Local | Gemma 4 on-device | $0/hr |
TaskTier + FallbackLadder + BudgetGate (Phase U.1a, internal API). A second routing surface alongside the prompt-scored heuristic: callers who already have a TaskTier (simple / standard / complex / reasoning) hand it to ModelRouter.resolve(taskTier:budget:...), which clamps the desired tier against the configured budget (daily-equivalent ceiling) and walks a 3-rung-capped FallbackLadder to a concrete ModelTier. TaskTier names the work; ModelTier (local / quick / balanced / frontier) names the engine — the separation lets the budget gate floor what kind of task can run without reshaping the model bins. The 3-rung cap is a deliberate undercut against ladder-bloat anti-patterns.
Routing corpus + ≥0.85 accuracy gate (Phase U.1b). ModelRouter.classify(prompt:) maps a prompt to its TaskTier; a 60-row hand-labeled corpus (≥10 per tier) at Tests/SenkaniTests/Fixtures/routing-corpus.json pins the classifier at ≥0.85 accuracy in CI. The gate is loose by design — flakiness is more expensive than a tight bar that catches nothing. ModelRouter.Decision now carries taskTier + ladderPosition and migration v10 adds ladder_position to agent_trace_event so the U.1c Analytics chart can split "primary rung used" from "fell back" without a second schema change.
Tier-distribution chart in AnalyticsView (Phase U.1c). A new "Routing — TaskTier Distribution" card in Analytics renders router output across the past 24h or 7d, per TaskTier. Stacked view (default) shows totals per tier; Grouped view splits each tier into Primary / Fallback 1 / Fallback 2 bars so operators can spot fallback churn without a second query. Tapping a bar opens a drill-down sheet listing the underlying agent_trace_event rows (feature, model, tokens, latency, result). Empty-state copy explicitly links the blank chart back to U.1a so operators don't think the analytics broke.
ContextPlan schema + plan/actual pairing (Phase U.6a, internal API). First slice of the context-orchestration split. New ContextPlan Swift struct in Sources/Core/ (id / sessionId / plannedFanout / leafSize / reducerChoice / estimatedCost / createdAt) backed by a context_plans table (Migration v14) with insert / fetchById / fetchBySession. agent_trace_event gains a nullable plan_id foreign-key column so combinator-emitted plans can be paired with their realized actuals. The combinator API + BudgetGate rejection path (U.6b) and the variance histogram + 90% pairing eval (U.6c) ride on top of this schema. Pure-Swift plumbing — no UI or operator-visible surface yet.
split / filter / reduce combinators + BudgetGate plan rejection (Phase U.6b, internal API). New Sources/Core/CombinatorPipeline.swift adds three named operators that map to the closed ReducerChoice vocabulary (split → merge, filter → select, reduce → summarize). Each call writes a ContextPlan row up-front, asks BudgetGate.rejectPlan(estimatedCost:budget:planId:) whether the plan fits the active budget's daily-equivalent ceiling, and either runs the caller's closure (stamping plan_id onto the returned AgentTraceEvent with withPlanId(_:) so callers can't accidentally drop the pairing) or returns a structured PlanRejection(reason:, ceilingCents:, estimatedCost:, planId:). Rejected plans persist the plan row but skip the trace; closure throws propagate with the plan still persisted (matches the reality of mid-execution crashes). The variance histogram + 90% pairing eval ride on this in U.6c.
Plan-variance histogram + ≥ 90% pairing eval (Phase U.6c). AnalyticsView gains a "Plan Variance — Actual vs. Planned Cost" card that plots Karpathy's residual (actual − planned cost in cents) for the U.6b combinator pairings. New db.contextPlanPairs(since:) joins context_plans to agent_trace_event on plan_id; pure helpers in VarianceHistogram (Core) bin residuals into signed […) ranges with under-budget / exact / over-budget kinds so the chart can colour-code without re-deriving. Header surfaces N paired, unpaired count, median Δ, and percent paired; the chart waits for ≥ 3 paired plans before drawing bars (Gelman gate against thin-data misreads). A new Tests/SenkaniTests/ContextPlanCorpusTests.swift exercises split / filter / reduce over a 20-item synthetic corpus and pins the parent acceptance #1 metric: ≥ 90% of executable corpus operations land paired plan + trace rows.
Senkani publishes its savings claim as a pair of numbers, never one alone. The fixture multiplier measures the optimization math under controlled conditions; the live-session multiplier measures the same stack on real Claude Code sessions. Both are legitimate, and the pairing is the differentiator — citing only the ceiling sets the product up for a credibility loss when real-world performance lands lower.
| Number | Value | What it measures |
|---|---|---|
| Fixture bench multiplier | 80.37× (synthetic, controlled) | 10-task × 7-config bench in Sources/Bench/BenchmarkFixtures.swift. Maximizes cache reuse, picks commands with aggressive filter rules. The optimization math's ceiling. |
| Live-session multiplier | pending (target capture: 2026-05-31) | Median of 5–10 representative Claude Code sessions replayed through SessionDatabase.liveSessionMultiplier(projectRoot:since:). Lower than the fixture by design — the floor. |
Why a pair, not a point estimate. Real sessions read mostly-novel files (cache hit rate drops), pay symbol-index cold-start costs, vary in command output shape, and don't follow the bench's linear permutation. The fixture number is the honest ceiling; the live number is the honest floor. We report both so operators can pre-calibrate expectations before the first session, then reconcile against their own workflow once they've run it. Hard rule for the README and any marketing surface: 80× never appears without its paired live-session number (or the explicit pending placeholder while Phase G capture is in flight).
The full caveat — including the action item, owner, and the tools/check-multiplier-claims.sh automated gate that fails the build on bare claims — lives at spec/testing.md → Live Session Caveat.
Other published numbers (see senkani doctor for current values):
| Metric | Value |
|---|---|
| Hot-path SLOs | cache hit p99 < 1 ms · pipeline cache-miss p99 < 20 ms · hook passthrough p99 < 1 ms · hook active p99 < 3 ms |
| Release commitments | cold-start < 250 ms p95 · idle memory < 75 MB · install size < 50 MB · classifier latency < 2 ms p95 (slot pending U.1) · OpenAI-endpoint cold-start < 1500 ms p95 |
| Terminal render | p50 ~2 ms, p99 ~3.4 ms |
| Filter throughput | > 10 k lines/sec |
| Symbol search | < 5 ms cold, < 1 ms cached |
| Secret scan | < 2 ms / KB |
| Unit tests | 2577 passing |
| Binary size | ~28 MB universal |
| Module | Deps | Role |
|---|---|---|
| Core | Filter | Session DB (incl. canonical trace rows), feature config, metrics, budget, hook routing, model routing, auto-validate, adaptive truncation |
| Filter | — | Token compression: 44 cmd rules, ANSI strip, dedup, secrets, terse |
| Indexer | SwiftTreeSitter | 25 tree-sitter backends, FTS5 search, dependency graph, incremental parsing, FSEvents |
| Bench | Core, Filter, Indexer | Token savings test suite: 10 tasks × 7 configs, quality gates, JSON export |
| MCP | Core, Filter, Indexer, Bundle, MLX | 23 MCP tools, socket server (mcp + hook + pane), vision + embedding inference, Gemma 4 rationale adapter |
| Bundle | Core, Filter, Indexer | BundleComposer — budget-bounded repo-snapshot composition for senkani_bundle |
| HookRelay | — | Zero-dep hook relay library shared by senkani-hook binary and app's --hook mode |
| CLI | Core, Filter, Indexer, Bench | 24 commands: exec, search, bench, doctor, grammars, kb, eval, learn, init, authorship, models, … |
| SenkaniApp | All + SwiftTerm | SwiftUI workspace: 20 pane types, multi-project, ⌘K palette, dashboard, menu bar |
Prerequisites: macOS 14+, Swift 6.0+, Xcode 15+
swift build # debug
swift build -c release
swift test # parallel run (fast; see caveat below)
./tools/test-safe.sh # deterministic full-suite run (slower but always terminates)
senkani doctor # verify grammar and database setupTest harness caveat: default
swift testcan hang during parallel startup on some machines due to Swift concurrency pool starvation in a handful of NSLock-wrapped test helpers. See spec/testing.md — "Full-suite hang" for root cause + workaround../tools/test-safe.shis the documented deterministic path until the underlying helpers are migrated off cooperative-pool blocking primitives.
The GUI target (SenkaniApp) includes SwiftUI, SwiftTerm, and MLX. The CLI (senkani) and the hook (senkani-hook) both stay lean — no MLX, no SwiftUI. The schedule create --prose MLX-fallback path shells out to senkani-mcp prose over a stdin/stdout JSON envelope so the CLI binary doesn't have to link MLXLMCommon + MLXVLM (U.8b subprocess-delegation, 2026-05-28; see spec/ml_models.md "Prose→cron compiler — composite seam").
Senkani runs locally on a Mac. When the operator wants to drive a headless Mac mini from a phone or laptop on the road, the documented pattern is three off-the-shelf tools — not Senkani features, just the stack we recommend:
- Tailscale (Personal tier, free). WireGuard mesh that gives every device a stable hostname inside your tailnet without exposing any port to the public internet. The Personal tier covers up to 6 users and unlimited devices — comfortably above any solo operator's needs. Install Tailscale on the Senkani Mac and on the operator's phone/laptop; both sign into the same tailnet.
- Screens 5 (one-time $179.99 lifetime, or $29.99/year subscription). Native VNC client on iOS and macOS. Tap a saved Mac on the iPhone, get the actual Senkani desktop over the WireGuard tunnel. Use the
screens://<saved-name>URL scheme — on macOS,vnc://andssh://may be intercepted by Screen Sharing or Terminal. The lifetime tier is the right fit for the Senkani audience; if that flinches, take the annual subscription, not a fork-and-self-host alternative. - Pushover (one-time ~$4.99 per platform, 10 k messages/mo free tier). Simple HTTPS push API —
POST https://api.pushover.net/1/messages.jsonwithtoken+user+message. Senkani'sNotificationSinkadapter populates theurlfield withscreens://<tailnet-host>so the notification itself becomes the deep link.
The closed loop: a Senkani job finishes → Pushover fires a push to the operator's phone → the notification's url field is screens://<tailnet-host> → tap → Screens 5 opens that exact Mac's desktop over the tunnel. No Senkani-side daemon ever listened on the public internet.
Over SSH, skip the GUI: senkani monitor --tui is a read-only operator dashboard that lives entirely in the terminal — a live view over the session database with q/j/k/r// keybindings, so you can watch a headless Mac's optimization activity through a plain ssh session without VNC. The refresh cadence is tunable with --poll-interval (e.g. --poll-interval=30s; default 5s, minimum 1s), and after the first paint each tick repaints only the changed cells, keeping steady-state updates byte-cheap over the tunnel. (senkani monitor --single-frame prints one frame and exits, for scripts.)
Macs go to sleep — set System Settings → Battery → Power Adapter → Wake for network access so the headless Mac mini answers when Tailscale pokes it. Tailscale Personal-tier policy and Screens pricing have shifted before; treat the named products as the current recommendation and the pattern — any WireGuard mesh + any VNC client + any HTTP-API push service — as the durable contract. See spec/inspirations/native-app-ux/tailscale-plus-screens-5.md for the full design analysis.
MIT (core). See LICENSE.