Skip to content
This repository was archived by the owner on Oct 5, 2026. It is now read-only.
ckluisPublic archive

About

an opinionated development environment interface

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

825 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Senkani (閃蟹)

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.

tests license release

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.

Quick Start

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 registration

SenkaniApp'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.


The MCP Intelligence Layer

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) + substring stripMatching(...) from the post-session waste analyzer. Regression-gated on real commands.output_preview samples. 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 is SecretDetector-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.


The Workspace

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_lifecycle spans × token_events cache 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 to Ready or Verification failed with a one-click Re-verify. Per-tier output quality is measured by senkani ml-eval — runs the 20-task harness against each installed Gemma tier, writes ~/.senkani/ml-tier-eval.json, and senkani doctor then 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_id correlates to OTLP spans with status_code = ERROR or duration > p99 show 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 via HookAnnotationFeed (V.12b); a per-minute rate cap suppresses must-fix floods without changing the deny response, with each closed window writing to annotation_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 AmplificationGuard verdict banner, a CronPreview next-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 via senkani schedule preset install <name>; each preset's resolved command is secret-scanned before install, and missing companion prerequisites (Ollama daemon, guard-research hook preset, senkani brief CLI, …) 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 via ScheduleStore), and inline validation tooltips surfacing the AmplificationGuard rationale 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. A TaskDecomposer converts each bullet into a U.11 WorkstreamTaskContract (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 existing ValidationStore (a "clean" row per command, scoped to a unique autorun-<run-id> session); all-clean commits and emits a notifyDone, any failure HALTS the loop with a notifyFailure. --dry-run prints 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 first N tasks — n aborts the run (distinct from a gate halt), tasks at index ≥ N run unattended; it defaults to 0 (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 injected SupervisionPrompt seam so the Core spine never reads stdin (headless-tested). An unattended run (stdin not a tty) refuses unless Pushover is seeded, naming senkani doctor --seed-pushover-key. --allow-classes <comma-list> (leg 3) restricts the loop to tasks whose inferred TaskClass (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/unknown class is never auto-allowed — fail-safe). The class rides in the contracts.json envelope only (no DB column). Leg 4 wires the real seed-gated Pushover transport: an AutorunSinkSelector runs a VALUE-FREE (CredentialVault.list, never read) FAIL-CLOSED probe for the seeded credential and, when present, installs the real KeychainPushoverTransport (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's ArtifactStore (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 through versions(of:). Redacted artifacts gate behind a confirmation sheet that writes a chained artifact.secret.allow audit 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 pull progress 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 /v1 call: 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.


Security Defaults (v0.3.0)

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. InjectionGuard scans 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_web resolves the target host via getaddrinfo before 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 via WKNavigationDelegate.decidePolicyFor — a 3xx Location header to 10.x/169.254.169.254/::ffff:… is cancelled. file:// scheme is not accepted — read local files with senkani_read. Subresource filter (F2): a WKContentRuleList blocks <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. SecretDetector now short-circuits with firstMatch so 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 + a schema_migrations audit log. Cross-process coordination via flock sidecar. On failed migration, a kill-switch lockfile is written and subsequent boots refuse to run migrations until the operator inspects the DB.
  • Retention — scheduled. RetentionScheduler prunes token_events (90 d), sandboxed_results (24 h), and validation_results (24 h) on an hourly tick. Tune via ~/.senkani/config.json → "retention": { "token_events_days": 30, ... }.
  • Instruction-payload byte cap. The instructions string injected at MCP server start (repo map + session brief + skills) is capped at 2 KB by default. Tune via SENKANI_INSTRUCTIONS_BUDGET_BYTES. Prevents the per-session-start token tax from growing with project size.
  • Socket authentication — opt-in. Setting SENKANI_SOCKET_AUTH=on generates a 32-byte random token at ~/.senkani/.token (mode 0600), rotated on every server start. Every connection to mcp.sock/hook.sock/pane.sock must 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=1 emits one JSON object per critical event to stderr. Every .string(_) log field passes through SecretDetector.scan at 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. Use LogValue.path(_) for filesystem paths — /Users/<name> collapses to ~ (current user) or /Users/*** (foreign). Default is backward-compatible [event] key=value format.
  • Observability counters — surfaced via CLI + MCP tool. Every security-defense site increments an event_counters row (migration v2): injection detections, SSRF blocks, socket handshake rejections, schema migrations, retention prunes, command redactions. Read them via senkani stats --security (Gelman rate annotation: count/total (pct%)) or senkani_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 doctor shows green / warn / burn / unknown per SLO with the rolling p99 and sample count. The CI perf gate (tools/perf-gate.sh and Tests/SenkaniTests/SLOTests.swift) synthesises a representative workload for each SLO and fails the build if any p99 crosses its ceiling. See spec/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 --openai spawn → listener-ready, Phase V.13e-3). Capture is tools/measure-slos.sh, which appends a row to ~/.senkani/slo-history.jsonl with git_sha + version for trend correlation. senkani doctor reads 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, and openai_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) — carries prev_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-chain walks every chain and reports OK or names the first broken (table, rowid); the integrity line surfaces in senkani doctor as chain integrity: OK since <ISO-date> / N repairs. Recovery via senkani 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's operator_note. The repair surface covers every integer-keyed chain participant; sandboxed_results (TEXT primary keys) is repaired by hand pending an --from-created-at flag.
  • Confirmation gate (Phase T.6a). Every write/exec-tagged tool call walks ConfirmationGate and writes a chained confirmations row recording the decision (approve/deny/auto) and who decided it (operator/policy/auto). MCPToolCatalog tags Claude Code hook tools (Edit/Write/Bash) and the senkani MCP surface with read/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 the NotificationSink protocol + 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). StdoutSink writes one canonical JSON line per NotifyEvent (sorted-key, scalar-only payload + ISO-8601 ts) — pipe senkani stdout into a structured-log collector and the gate's outcomes round-trip cleanly. MacOSLocalSink posts banners via an injected LocalNotifierBridge; SenkaniApp now wires the real UNUserNotificationCenter-backed UNNotifierBridge at App boot (requests TCC authorization on first launch) while CLI / MCP / CI run silent via NullLocalNotifierBridge. CI tests use SpyLocalNotifierBridge. NotificationRouter reads ~/.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-global NotificationDelivery holder, which is nil (silent) in CLI / MCP / test processes and only non-nil once SenkaniApp's NotificationBootstrap runs. The first production producer fires on the first-ever firstNonzeroSavings milestone ("Save your first tokens") via OnboardingMilestoneStore.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.json through the deterministic Config writer 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). EgressRuleEngine matches hosts against an exact / prefix / suffix / single-* glob rule set with deny-wins precedence and default-deny-on-miss; EgressHostNormalizer canonicalizes case, default ports (:80 / :443), trailing dots (T.1c added the second post-port-strip dot pass so EXAMPLE.com.:80 canonicalizes cleanly), and trailing slashes so a rule for example.com matches Example.COM:80. exactly the same way; EgressDecisionStore chains every decision into the egress_decisions audit table (migration v19) so senkani doctor --verify-chain proves no allow/deny was redacted. EgressListener binds a POSIX TCP socket to 127.0.0.1:<port> (port 0 = kernel-assigned, persisted to ~/.senkani/egress.port via atomic rename), and EgressConnectionHandler reads the request line, evaluates the host through EgressRuleEngine, 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 replies 403 Forbidden. SNI mismatch writes a deny row with sentinel rule_id="sni_mismatch" and tears down without piping a single byte upstream. T.1b layers in a PaneMode taxonomy (research, write, redteam, general — typed enum, parsed from an internal X-Senkani-Pane-Mode proxy header stripped before forward), per-mode allowlists loaded from ~/.senkani/egress-policy.json (with fallback to the legacy flat egress-rules.json for T.1a operators), and a Gemma judge fallback on static-rule miss (JudgeAdapter protocol with GemmaJudgeAdapter enforcing a 300 ms hard timeout that defaults to .deny on expiry — Schneier P0) whose rationale lands in egress_decisions.judge_rationale (migration v23 with per-anchor chain shape switch via fresh-install-pre-v23 rename + migration-v23 anchor opened at MAX(id)). .redteam panes 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 every swift test, plus a senkani doctor --check-egress 5-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 .general default on missing-or-malformed file) backs HookRouter.paneModeResolver, and PaneLaunchEnv injects SENKANI_PANE_MODE=<mode> into every pane subprocess env so senkani-aware HTTP clients can emit the matching X-Senkani-Pane-Mode header on outbound traffic — proven end-to-end by a redteam integration test that spawns a /bin/bash -c subprocess with the env set, splices $SENKANI_PANE_MODE into the request header via printf, and asserts the daemon records pane_mode == .redteam while MockJudgeAdapter.callCount stays at 0. senkani egress start|stop daemonize / signal via ~/.senkani/egress.pid; senkani egress status [--recent N] reports listener state + decision count; senkani doctor flips 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.credentialGateway declares which vaultKeys a tool needs at which scope, with a per-tool dryRun opt-in for FAKE_KEY placeholders. HookRouter consults CredentialGateway after the confirmation gate (so a .deny short-circuits before any vault read), fetches each declared key from CredentialVault.shared, and on a missing key + dryRun: false returns a structured deny carrying both keyname AND scope so the operator can run senkani vault add without guessing. The audit row written to token_events records keyname + scope + the dry-run flag — the credential value itself is never serialized. Round T.4b ships the API surface against InMemoryKeychainStore; the macOS Keychain conformance for secrets and adversarial corpus eval land in T.4c. The first senkani vault verb 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-1 NWListener scaffold) gains bearer-token auth on the /v1/* prefix, enforced before the not-yet-wired 501 surface. senkani vault add openai-key --preset <auto|build|research|quick|local> [--scope chat,embeddings] [--rate <rpm>] [--expires <iso8601>] [--label <name>] provisions an sk-senkani-… key, prints the plaintext once to stdout, and persists only its SHA-256 hash. --preset is the routing tier the key uses (a ModelPreset case, validated at provision time — a provider name like openai is rejected with the valid-cases list). OpenAIAuthGate enforces 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 same 401. A key provisioned (or revoked) while serve --openai is 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/completions returns a strict OpenAI chat.completion object (id / object / created / model / choices[].message / usage). The provisioned key's ModelPreset wins routing through ModelRouter — the request's model (e.g. gpt-4o) is logged for telemetry only and never changes routing, and the response's model reports 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.5 ChainHasher); request/response bodies are stored only under --audit-bodies (default off). Array-valued content returns 400; stream: true opens an SSE stream (Phase V.13b, below). This child ships the routing + audit surface; the real local-Gemma chat backend now backs .local end-to-end (Phase V.13 chat-backend-1…4, below).
  • OpenAI-compatible chat streaming (SSE) (Phase V.13b). POST /v1/chat/completions with stream: true emits Server-Sent Events (Content-Type: text/event-stream) carrying the OpenAI chat.completion.chunk delta shape — a leading {role: "assistant"} chunk, incremental {content: …} chunks (which concatenate byte-for-byte back to the full message), a terminal finish_reason: "stop" chunk, then the final data: [DONE] sentinel. Rate limiting is enforced before the stream opens (a 429 is 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 whose status distinguishes completion (ok) from client-cancel (client_cancel). OpenAIChatStream (Sources/Core/OpenAIEndpoint/OpenAIChatStream.swift) holds the Network-free framing + drive loop; the live NWConnection writer streams the chunks and tears the connection down exactly once.
  • OpenAI-compatible embeddings (Phase V.13c). POST /v1/embeddings returns a strict OpenAI list object — data[] of { object: "embedding", index, embedding } plus usage (prompt_tokens / total_tokens). The input field accepts a single string or an array of strings; token-id forms and an empty input are rejected with 400. The embedding model is sourced from ModelManager (the single minilm-l6 registry entry the MCP senkani_embed tool already uses — EmbedEngine.modelId now references ModelManager.embeddingModelID, so there is no parallel embedding stack), and the response reports that model. A key without embeddings in scope gets 403; each request appends exactly one tamper-evident audit-chain entry with surface: "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 an EmbeddingEngine protocol seam + ModelManager.registerEmbeddingHandler), with a model_not_available framed 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/completions now accepts tools: [{ type: "function", function: { name, description, parameters: <JSON-Schema> } }] and emits tool_calls when the model calls a tool — choices[].message.tool_calls[].function.{name, arguments} with content: null and finish_reason: "tool_calls", where arguments is a JSON-encoded string (matching OpenAI exactly). A tool's parameters JSON-Schema round-trips verbatim (new JSONValue), 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 existing MCPToolConfig catalog (new OpenAIToolBridge, function tools → .exec) — no parallel tool registry. The tools scope is body-derived, so it is enforced in the handler: a key without tools in scope gets 403 insufficient_scope even with a valid tools payload (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; SSE delta.tool_calls streaming 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 --openai resolves a registered ChatEngine (new pure-Swift protocol in Sources/Core/OpenAIEndpoint/ChatEngine.swift + a ModelManager.registerChatHandler(_:) slot mirroring the V.13c embedding-handler pattern) and routes chat completions through OpenAIChatServeBridge.syncEngine(...) — a DispatchSemaphore-based sync wrapper over the async chat call so the existing NWListener closure signature stays unchanged. The MCP-side MLXChatEngineAdapter (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 as senkani_vision and GemmaInferenceAdapter), runs text-only inference under MLXInferenceLock.shared with 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 CLI senkani serve --openai, unit tests — the listener falls back to the V.13a-3 placeholder and logs openai-serve chat_backend=placeholder once at startup so test environments stay self-contained. Non-streaming surface only; the SSE token-by-token streaming path, model_not_available readiness 503, tokenizer-accurate usage counts, 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: true request now emits SSE content deltas as MLX produces each token, not as a post-hoc split over a one-shot completion. New public protocol StreamingChatEngine: Sendable (Sources/Core/OpenAIEndpoint/StreamingChatEngine.swift) returns AsyncThrowingStream<OpenAIChatHandler.TokenDelta, Error> — the first async-stream shape in the OpenAIEndpoint surface; the MCP-side MLXStreamingChatEngineAdapter (Sources/MCP/ChatHandlerRegistration.swift) drives a new MLXChatEngine.stream(prompt:) that yields each Generation.chunk to the continuation as MLX's container.generate produces it (clients accumulate delta.content verbatim — the streaming path skips end-of-stream trimming because it can't unwrite earlier deltas). OpenAIChatHandler.renderStreamingEvents(...) translates TokenDelta deltas into pre-encoded SSE chunks (role chunk + content chunks + terminal finish_reason: "stop"); OpenAIChatStream.Plan gains a streamingEvents: async-source init carried through a new driveStreaming(...) channel pump that bridges the async source to the listener's synchronous Sink (peer disconnect → producer Task cancel; producer error → .clientCancel). ServeCommand routes non-tool-use content streams through the registered handler when present, with an NSLock-protected accumulator teeing each delta so the chatStream audit entry's completion_token_count reflects bytes actually streamed; tool-use streams (V.13d-1's tool_calls delta 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-stream head, data: {…}\n\n events, data: [DONE]\n\n sentinel) — only the arrival timing differs. Non-local-tier 503s, readiness 503, tokenizer-accurate usage counts, 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: true request that resolves to a tool call now emits OpenAI-shaped choices[].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 append function.arguments string pieces that concatenate byte-for-byte back to the full JSON-encoded arguments string; a terminal chunk carries finish_reason: "tool_calls" before the data: [DONE] sentinel. The tool-call path reuses the same OpenAIChatStream chunk model, run(…) drive loop, and splitForStreaming splitter as the content path — no parallel streaming stack — with a new toolCallEvents(…) builder and a sparse Chunk.Delta.ToolCallFragment (index always present so a client keys fragments by array index). Scope-gate parity holds: an out-of-scope or malformed stream: true tool-use request is refused with a framed 403/400 before any SSE byte (no half-open stream), and each streamed tool-use request still appends exactly one tamper-evident audit-chain entry (status distinguishing ok vs client_cancel).
  • Soft-flag fragmentation detector (Phase U.4a). FragmentationDetector watches 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). TrustScorer aggregates flags into a 0–100 score. Flags persist into the chained trust_audits table (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, and senkani doctor reports trust 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.


Three-Layer Optimization

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)

Model Routing

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.


Paired Performance Numbers

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

Architecture

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

Building from Source

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 setup

Test harness caveat: default swift test can 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.sh is 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").


Companion Stack (remote-operator pattern)

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:

  1. 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.
  2. 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:// and ssh:// 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.
  3. Pushover (one-time ~$4.99 per platform, 10 k messages/mo free tier). Simple HTTPS push API — POST https://api.pushover.net/1/messages.json with token + user + message. Senkani's NotificationSink adapter populates the url field with screens://<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.


License

MIT (core). See LICENSE.

About

an opinionated development environment interface

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages