Shared agent context
People record decisions and hand off work through their AI coding assistant. Jurati stores that context in git so every connected agent starts its next session already knowing what happened.
The problem
People coordinate through Slack, meetings, issues, and memory. Then they re-explain it all to each new agent session. The agent has no idea what the team decided yesterday.
Decisions made verbally, written down if someone remembers, scattered across docs nobody searches.
Context buried in chat history. Hard to find a week later, impossible after a month. No structure.
When someone's on PTO or leaves, their context leaves with them. There's no export button.
Measured test
Two developers, two separate AI sessions, one shared knowledge repo. Tested end-to-end with real Claude Code sessions.
Knowledge model
Different kinds of knowledge have different lifetimes, review needs, and audiences. Jurati matches the formality to the content.
PrincipleLong-lived engineering principles. "We never store secrets in entries." Requires team review.
DecisionDesign decisions with rationale. "We chose file adapters over pure MCP." Goes through PR review.
WorkflowRecurring processes. "How to cut a release." Reviewed before merge.
ProjectProject status and goals. "AU0035l is 40% through content development." Lightweight PR.
ActiveShort-lived handoffs and current work. "Lab scripts done, DNS issue still open." Auto-expires.
Active refs, no PR, auto-expires in days. Opt-in.
Lightweight PR. Low-ceremony, usually merged quickly.
PR that needs team sign-off before it lands.
Long-term principles. Requires deliberate team agreement.
Usage
No new UI. Developers use natural language through their existing AI coding assistant. Jurati translates the request to the right operation.
"We decided to standardize on AAP 2.6 terminology across all courses."
Recorded the terminology decision.
"Lab scripts are done, but the DNS issue on workstation is still open."
Saved the handoff.
Agent searches team context and answers from what's been recorded.
Returns matching entries ranked by relevance.
Agent loads all active handoffs and recent decisions at session start.
Context already loaded — no lookup needed.
Content safety
The content policy gate runs offline — no network, no API calls. It checks text against pattern rules and blocks credentials, PII, and sensitive content before anything reaches git.
Reject the write entirely. Returns an error. Credentials, API keys, connection strings, passwords — caught before commit.
Allow the write but mark it for human review. The PR can't merge without sign-off. Used for content that might be sensitive.
Allow but flag in response metadata. The agent sees the advisory. Used for patterns that are usually fine but worth noting.
WritePolicy.evaluate() runs before materialize_write() in every code path.Privacy
It's not that Jurati promises not to track you. It's that the code doesn't have the ability to. There are no read receipts to turn on, no participation dashboard to build, no query logs to query.
sensitivity: restricted) are excluded from search, context, and the BM25 index entirely. Only accessible by direct ID lookup.Coordination signals
Jurati watches specific repositories for PRs carrying the jurati label. Everything else is ignored by design. No broad mining of PRs, comments, or commit history.
workflow, decision, migration, deprecation, architecture, release, security, ownership — signal that existing knowledge may need updating.
handoff, hand-off, owner, review, blocked, waiting, assigned — signal work in transition between people.
Search
Two search modes. Topic search does exact substring matching on entry IDs and titles. Keyword search uses BM25 with multiple scoring signals to rank results.
Standard information retrieval scoring (k1=1.5, b=0.75). Tokenized, case-insensitive, length-normalized.
14-day half-life exponential decay. A 28-day-old entry scores 25% of an identical entry from today.
Principles (1.4x) rank higher than active handoffs (0.8x). Long-term knowledge surfaces above short-term notes.
Entries tagged with your current repo get a 1.5x boost. Work relevant to what you're doing right now ranks higher.
Lifecycle
Active handoffs auto-expire so the knowledge base stays clean. Decisions and workflows live longer but can be superseded when things change. Hygiene reports surface what's stale — without tracking who read what.
Active entries require an expires_at timestamp. Default: 72 hours. Maximum: 7 days. Expired entries surface in hygiene reports.
Proposed entries older than 30 days without updates surface as stale. Configurable threshold. No activity-based signals — purely time-based.
Entries can link to what they replace via supersedes and superseded_by. Full version chains. Old entries stay readable but ranked lower.
Optional review_after timestamp on long-lived entries. Hygiene reports flag entries past their review date.
Cross-client
The file adapter is the universal primitive. Every agent client can read a file. Same context file, same CLI commands, same knowledge repo. No per-client configuration.
SessionStart hook writes .jurati/context.md, CLAUDE.md tells the agent to read it. MCP available for interactive queries.
Same hook, same file, GEMINI.md has the context instructions. CLI commands for writes. Zero permission prompts or extensions.
If it can read a file and run a shell command, it can use Jurati. No plugin, no API key, no MCP required.
Measured cost
The demand for team context is fixed — developers need it regardless. The variable is the supply path. We measured all of them.
Getting started
Three entry points depending on what you need. Each one sets up the knowledge repo, installs hooks, and wires up your agent client.
jurati quickstartCreates a new knowledge repo, optionally pushes to a remote, and prints the adoption path. Good for starting from scratch.
jurati onboardConnects to an existing knowledge repo and sets up local project wiring in one run. Good for joining an existing team.
jurati initScaffolds entry directories in an existing repo. Accepts packs: --pack generic, --pack training, --pack engineering.
jurati doctorChecks that everything is wired up correctly. Validates config, tests connectivity, reports what's missing. Run this when something's not working.
Configuration
No config files to manage, no YAML to parse, no settings UI. Set environment variables and go. Most have sensible defaults.
| Variable | Purpose | Default |
|---|---|---|
JURATI_KNOWLEDGE_REMOTE | Git remote URL for the knowledge repo | (required) |
JURATI_PR_SIGNAL_REPOS | Repos to watch for labeled PRs | (none) |
JURATI_ACTIVE_REFS_ENABLED | Enable L0 fire-and-forget refs | false |
JURATI_SYNC_INTERVAL | Seconds between background syncs | 300 |
JURATI_CONTEXT_DEFAULT_MODE | Context output format: manifest, compiler, full | manifest |
JURATI_CLIENT_SLUG | Identifies this client in entries | claude-code |
JURATI_PRIVACY_SALT | Salt for opaque identity hashing | (auto-generated) |
Open source
Jurati is an open source project. It stores everything in git, uses standard tools, and doesn't need a database, cloud service, or account.
Git, Python 3.13+, and optionally the gh CLI for creating PRs. That's it.
No database, no cloud service, no SaaS account, no API keys, no Docker, no Kubernetes.
pip install jurati
Source: github.com/ostinato-forge/jurati
Architecture
Jurati is a pure core library with pluggable transport. Files for reads, CLI or MCP for writes. Works on any agent client.
Engineering shape
The core/ directory has zero I/O imports. No filesystem, no network, no subprocess. An AST guard in the test suite enforces this — if you add an import from os, subprocess, or git/, the build breaks.
test_architecture.py AST guard in the test suite.Schema
Standard format. Readable with any text editor. Diffable with git. The body is markdown. The metadata is structured YAML.
--- schema_version: 2 id: task-loops-ge-fix title: Task loops GE validation fix type: active level: L1 status: active created_at: 2026-05-28T14:00:00Z updated_at: 2026-05-28T14:00:00Z expires_at: 2026-05-31T14:00:00Z projects: [AU0035l] tags: [task-loops, ge, validation] visibility: internal sensitivity: normal recorded_by: claude-code source: repo: RedHatTraining/AU0035L author: eparenti review: required: false pr: https://github.com/... --- Fixed the loop output regex in the task-loops GE labexec validator.
Schema evolution
Schema v2 added visibility, sensitivity, and provenance fields. v1 entries receive v2 defaults at parse time. No migration script, no data conversion, no breaking change.
| v2 field | v1 default | Purpose |
|---|---|---|
visibility | INTERNAL | Author intent: internal or external distribution |
sensitivity | NORMAL | Access control: normal, internal, or restricted |
recorded_by | None | Which MCP client submitted the write |
on_source_branch | derived | True if entry is on the source-of-truth branch |
source | None | Git provenance: repo, commit, author |
File adapter
A SessionStart hook runs jurati startup, which compiles team context to a markdown file. The agent reads that file naturally — no MCP connection, no permission prompts, no tool descriptions.
15 MCP tool descriptions = ~735 tokens/turn. Permission prompts on first use. Platform-specific config. Breaks on non-MCP clients.
CLAUDE.md reads context = ~50 tokens/turn. Zero permission prompts. Works on any client that can read a file.
Read path
Git is the source of truth. The server keeps only local state: a repo cache and an in-memory search index. It can rebuild from scratch at any time.
Write path
Writes go through four stages. The content policy gate is the hard stop — if it blocks, nothing reaches git. Entry file and event file are committed atomically.
Entry markdown file + event JSON file committed in a single git commit. If one fails, both fail. No partial writes.
record() handles L0/L1 (handoffs, status). propose() handles L2/L3 (decisions, workflows). You can't create a principle with record().
Content policy
Compiled regex patterns run against title and body separately. No API calls, no ML model, no network. The same input always produces the same output. Lives in core/content_policy.py — pure, zero I/O.
| Action | Behavior | Example |
|---|---|---|
| BLOCK | Reject entirely, return error | API keys, passwords, connection strings |
| REQUIRE_REVIEW | Allow with review label | Content mentioning personnel or security |
| ALLOW_WITH_WARNING | Allow but flag in metadata | Patterns that might be sensitive |
| ALLOW | No match — proceed | Normal team context |
ContentPolicyConfig. Sensitive/restricted entries use stricter rule sets. The policy can be disabled with mode=disabled for testing.
Scoring
The scoring formula in core/scoring.py combines four signals. All scoring is pure — no I/O, no state mutation. The inverted index is built once from the entry set and queried from memory.
# Scoring formula score = bm25(query, entry) × recency_decay(entry.updated_at) × type_weight(entry.type) × repo_affinity(entry, context) # BM25 parameters k1 = 1.5 # term saturation b = 0.75 # length normalization # Recency: 14-day half-life decay = exp(-0.693 × age_days / 14.0) # Type weights PRINCIPLE = 1.4 DECISION = 1.2 WORKFLOW = 1.1 PROJECT = 1.0 ACTIVE = 0.8 # Exact match boost ID match = 10.0× Title match = 10.0× # Repo affinity Project match = 1.5× Same repo = 1.5× Dependency = 1.2×
Validation
Every entry must pass validation rules in core/validate.py. These run on write and can be checked any time with jurati validate.
| Rule | Constraint |
|---|---|
| Type → Level | Principle=L3, Decision=L2/L3, Workflow=L2, Project=L1, Active=L0 |
| ID format | ^[a-z0-9][a-z0-9-]{2,120}$ |
| Active expiry | Active entries MUST have expires_at. Non-active MUST NOT. |
| L0 TTL cap | Maximum 168 hours (7 days) |
| Review gate | L1+ entries with status=active MUST have review.pr set |
| Timestamp ordering | updated_at >= created_at, expires_at > created_at |
| Sensitivity constraint | RESTRICTED + EXTERNAL = error. RESTRICTED + L0 = error. |
| Supersession integrity | IDs must exist, no circular chains |
jurati validate or through the MCP validate() tool.Events
L1+ write operations produce per-event JSON files in events/YYYY-MM-DD/. Events exist for audit — not analytics, not dashboards, not monitoring who does what.
Timestamp (UTC ISO-8601), event type (ENTRY_PROPOSED or ENTRY_EXPIRED), entry_id, entry_type, level, visibility. That's it.
Author identity, query text, entry body or content, who read what, request context, IP addresses, session IDs.
// events/2026-05-28/20260528T140000Z-task-loops-ge-fix.json { "timestamp": "2026-05-28T14:00:00Z", "event_type": "ENTRY_PROPOSED", "entry_id": "task-loops-ge-fix", "entry_type": "active", "level": "L1", "visibility": "internal" }
Active refs
Active refs are short-lived git branches that bypass the PR process entirely. They're for "I'm working on X right now" context that should disappear in hours, not days. Opt-in — disabled by default.
refs/heads/jurati/active/{client}/{entry-id}
Discovered via fetch_all() + index overlay at sync time.
TTL: 1–168 hours (7-day max). Always visibility: internal. Cannot be sensitivity: restricted (no access control on refs). No events produced.
expire() deletes the ref immediately. No PR, no review, no commit on main. The entry simply disappears from the index.
Set JURATI_ACTIVE_REFS_ENABLED=true to enable. Most teams start without L0 and add it when they want faster handoffs.
Signal providers
The SignalProvider is a runtime-checkable Protocol. GitHub and Jira implementations exist. The protocol is open — you can write your own provider for any system that has labeled work items.
# CoordinationSignal shape signal_id: unique identifier provider: github | jira | fixture source: org/repo artifact_ref: PR URL or issue URL title: PR/issue title labels: [jurati, ...] state: open | merged | closed subject_refs: changed file paths subject_areas: classified areas # Classification output effect: continue | reuse | revise | pause | handoff | clarify urgency: quiet | info | notice | warning | critical confidence: 0-100
Privacy internals
When entries cross system boundaries (non-CONTEXT access modes), identities are salted per requesting system via opaque_identity(). One-way hash. No cross-system correlation.
# core/privacy.py def opaque_identity( recorded_by, source, system_id, salt ) -> (opaque_client, opaque_author): # salted SHA256 per requesting system hash = sha256(salt + system_id + identity) # one-way: cannot reverse to original # per-system: same person gets different # hash in different systems # Salt configuration JURATI_PRIVACY_SALT=base64:... # Generate with: $ jurati privacy-salt --bytes 32
Git provenance (commits, PRs, author) stays intact for accountability. Not repurposed as tracking.
Identities are hashed before crossing boundaries. System A can't correlate entries with System B's identities.
CLI reference
Every command works without an MCP server. Same core logic, same content policy, same knowledge repo.
| Command | Purpose |
|---|---|
| SETUP | |
jurati init | Scaffold knowledge repo directories. Accepts packs: generic, training, engineering. |
jurati quickstart | Create starter repo, push to remote, print adoption path. |
jurati onboard | Connect to existing repo + wire up project in one run. |
jurati doctor | Check setup readiness, validate config, test connectivity. |
jurati install-hook | Install post-commit hook for ambient capture. |
jurati install-claude-md | Install CLAUDE.md section and settings for Claude Code. |
| READ | |
jurati startup | Compile team context to file or stdout. --format md/json, --output path. |
jurati query <topic> | Search entries by topic. Returns ranked results. |
jurati read <id> | Read a single entry by ID with full body. |
| WRITE | |
jurati record | Record handoff or status (L0/L1). --type active/project. |
jurati propose | Propose decision or workflow (L2/L3). --type decision/workflow/principle. |
jurati capture | Process commit message through capture pipeline. |
jurati flush | Flush pending captures from offline queue. |
| TOOLS | |
jurati validate | Validate all knowledge entries in a directory. |
jurati html | Generate static HTML browser for local repo. |
jurati import-context | Import context pack tarball. Supports --dry-run. |
jurati import-docs | Import markdown files. Modes: adr, rfc, docs, vault. |
MCP reference
15 tools exposed over stdio MCP. All optional — the CLI and file adapter cover the same functionality. MCP adds interactive queries during a session.
| Tool | Purpose | Key params |
|---|---|---|
| CONTEXT | ||
sync() | Fetch latest, rebuild index | — |
startup() | Sync + compile context snapshot | repo, goal, max_entries |
context() | Session-start context with signals | repo, goal, mode, paths |
explain() | What Jurati does in plain language | — |
status() | Repo, cache, sync health | — |
| READ | ||
query() | Exact topic matching | topic, type, project |
search() | BM25 keyword ranking | query, type, tags, max_results |
read() | Single entry by ID | id |
read_many() | Multiple entries (max 10) | ids |
list_entries() | List with optional filters | type, project, status |
| WRITE | ||
record() | L0/L1 handoffs and status | type, title, content, projects |
propose() | L2/L3 decisions and workflows | type, title, content, projects |
expire() | Expire entry (L0: delete ref, L1+: PR) | id, reason |
| OPS | ||
validate() | Validate one or all entries | id (optional) |
hygiene() | Expired/stale/superseded report | stale_after_days |
Configuration
All configuration lives in environment variables, parsed by git/config.py into a JuratiConfig dataclass. No config files, no YAML.
| Variable | Default | Notes |
|---|---|---|
| CORE | ||
JURATI_KNOWLEDGE_REMOTE | (required) | Git remote URL |
JURATI_CACHE_DIR | ~/.cache/jurati | Local repo cache |
JURATI_MAIN_BRANCH | main | Source of truth branch |
JURATI_CLIENT_SLUG | claude-code | Client identifier in entries |
| ACTIVE REFS | ||
JURATI_ACTIVE_REFS_ENABLED | false | Enable L0 |
JURATI_ACTIVE_REF_TTL_HOURS | 72 | Max 168 (7 days) |
| SYNC & CONTEXT | ||
JURATI_SYNC_INTERVAL | 300 | Seconds between syncs |
JURATI_CONTEXT_DEFAULT_MODE | manifest | manifest/compiler/full |
JURATI_STARTUP_MAX_ENTRIES | 3 | Max 5 |
| SIGNALS | ||
JURATI_SIGNAL_PROVIDER | github | github/jira/fixture |
JURATI_PR_SIGNAL_REPOS | (none) | Comma-separated |
JURATI_REVISE_KEYWORDS | (built-in) | Override default list |
| PRIVACY | ||
JURATI_PRIVACY_SALT | (auto) | base64-encoded salt |
Open source
Jurati is an open source project built to be auditable, testable, and easy to contribute to.
Git, Python 3.13+, optionally gh CLI. No database, no Docker, no cloud service.
Ruff linting, mypy strict mode, conventional commits. Tests mirror src/ structure under tests/.
AST-based test enforces core/ purity — zero I/O imports. If you break the boundary, the build breaks.
Apache 2.0
Shared, reviewable agent context for people and AI coding agents.
github.com/ostinato-forge/jurati
Cost data measured May 2026 · Claude Sonnet 4 · $200K/yr fully loaded developer cost