Shared agent context

Jurati is shared context for AI coding agents.

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.

Git-backedfull audit trail
Reviewabledecisions go through PR
Auto-expireshandoffs stay clean
1
PersonRecords a decision or handoff in natural language
2
JuratiStores it as a markdown file in a git repo
3
Next sessionAgent starts with team context already loaded

The problem

Agents start every task cold.

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.

Meeting notes

Decisions made verbally, written down if someone remembers, scattered across docs nobody searches.

Slack threads

Context buried in chat history. Hard to find a week later, impossible after a month. No structure.

People's heads

When someone's on PTO or leaves, their context leaves with them. There's no export button.

AI agents make this worse. Each new session starts with zero team context — just the code in front of it.

Measured test

One person records. Every agent benefits.

Two developers, two separate AI sessions, one shared knowledge repo. Tested end-to-end with real Claude Code sessions.

repo: AU0035lmeasured: May 2026

Task-loops GE validation fix

Ed → "I fixed the loop output regex. Save this handoff."
Mike → "I'm starting on the task-review lab. What do I need to know?"
Mike's agent: "The task-loops GE just had its validation false-positive bug fixed — worth looking at that fix as a reference for how to write reliable regex matching against Ansible output."
Mike never asked about task-loops. His agent surfaced Ed's handoff because it was relevant to his work.

Knowledge model

Five entry types, four escalation levels.

Different kinds of knowledge have different lifetimes, review needs, and audiences. Jurati matches the formality to the content.

Principle

Long-lived engineering principles. "We never store secrets in entries." Requires team review.

Decision

Design decisions with rationale. "We chose file adapters over pure MCP." Goes through PR review.

Workflow

Recurring processes. "How to cut a release." Reviewed before merge.

Project

Project status and goals. "AU0035l is 40% through content development." Lightweight PR.

Active

Short-lived handoffs and current work. "Lab scripts done, DNS issue still open." Auto-expires.

L0
Fire and forget

Active refs, no PR, auto-expires in days. Opt-in.

L1
Quick review

Lightweight PR. Low-ceremony, usually merged quickly.

L2
Team review

PR that needs team sign-off before it lands.

L3
Careful review

Long-term principles. Requires deliberate team agreement.

Usage

You talk to your agent. Jurati handles the rest.

No new UI. Developers use natural language through their existing AI coding assistant. Jurati translates the request to the right operation.

"Save this as a team decision"

"We decided to standardize on AAP 2.6 terminology across all courses."

Recorded the terminology decision.

"Hand this off for the next person"

"Lab scripts are done, but the DNS issue on workstation is still open."

Saved the handoff.

"What's the status on task-review?"

Agent searches team context and answers from what's been recorded.

Returns matching entries ranked by relevance.

"What do I need to know before starting?"

Agent loads all active handoffs and recent decisions at session start.

Context already loaded — no lookup needed.

Content safety

Every write goes through content policy before commit.

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.

BLOCK

Reject the write entirely. Returns an error. Credentials, API keys, connection strings, passwords — caught before commit.

REQUIRE_REVIEW

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_WITH_WARNING

Allow but flag in response metadata. The agent sees the advisory. Used for patterns that are usually fine but worth noting.

Same gate for CLI and MCP writes. Same rules. No way to bypass it — WritePolicy.evaluate() runs before materialize_write() in every code path.

Privacy

Privacy is structural, not policy.

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.

NO
No query loggingSearch queries are passed through scoring functions and discarded. There's nowhere they get stored.
NO
No read receiptsNobody can see who viewed what entry. There is no read tracking mechanism in the codebase.
NO
No participation metricsThere's no way to ask "who hasn't contributed this week." Git history exists for accountability, not measurement.
NO
No individual activity tracking"What has Ed been working on?" — the system refuses this by design. It's team context, not individual surveillance.
Restricted entries (sensitivity: restricted) are excluded from search, context, and the BM25 index entirely. Only accessible by direct ID lookup.

Coordination signals

Only labeled PRs from configured repos enter the system.

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.

ConfigWatch reposJURATI_PR_SIGNAL_REPOS
FilterLabeled PRs onlyjurati label required
ClassifyKeyword matchrevise, handoff, etc.
OutputCoordinationSignaleffect + urgency
DisplayGuidance in contextshown at session start

Revise keywords

workflow, decision, migration, deprecation, architecture, release, security, ownership — signal that existing knowledge may need updating.

Handoff keywords

handoff, hand-off, owner, review, blocked, waiting, assigned — signal work in transition between people.

Keywords are configurable via environment variables. No unlabeled PRs, comments, chat messages, editor activity, or commit messages are ever collected.

Search

Search ranks by relevance, not recency alone.

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.

BM25 text relevance

Standard information retrieval scoring (k1=1.5, b=0.75). Tokenized, case-insensitive, length-normalized.

Recency decay

14-day half-life exponential decay. A 28-day-old entry scores 25% of an identical entry from today.

Type weights

Principles (1.4x) rank higher than active handoffs (0.8x). Long-term knowledge surfaces above short-term notes.

Repository affinity

Entries tagged with your current repo get a 1.5x boost. Work relevant to what you're doing right now ranks higher.

Restricted entries are excluded from the BM25 inverted index entirely. They don't appear in search results and don't affect scoring.

Lifecycle

Entries age, expire, and get superseded.

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.

Auto-expiry

Active entries require an expires_at timestamp. Default: 72 hours. Maximum: 7 days. Expired entries surface in hygiene reports.

Stale detection

Proposed entries older than 30 days without updates surface as stale. Configurable threshold. No activity-based signals — purely time-based.

Supersession

Entries can link to what they replace via supersedes and superseded_by. Full version chains. Old entries stay readable but ranked lower.

Review reminders

Optional review_after timestamp on long-lived entries. Hygiene reports flag entries past their review date.

Hygiene uses time-based signals only. It never reports who wrote, reviewed, or read an entry. No participation data.

Cross-client

Works on Claude Code, Gemini CLI, or any agent.

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.

Claude Code

SessionStart hook writes .jurati/context.md, CLAUDE.md tells the agent to read it. MCP available for interactive queries.

Gemini CLI

Same hook, same file, GEMINI.md has the context instructions. CLI commands for writes. Zero permission prompts or extensions.

Any future client

If it can read a file and run a shell command, it can use Jurati. No plugin, no API key, no MCP required.

The hybrid model: files for startup context (zero overhead), MCP for interactive queries (when available), CLI as universal fallback.

Measured cost

$0.25 per session. $54/dev/month at scale.

The demand for team context is fixed — developers need it regardless. The variable is the supply path. We measured all of them.

Jurati
$0.25
Agent + raw APIs
$2.02
Ask a colleague
$12.96
Manual lookup
$16.16
Standup (amortized)
$19.23
Assumptions: 3 coordination questions per session. Developer cost: $200K/yr fully loaded = $1.60/min. Token costs measured from real Claude Code sessions, May 2026.

Getting started

Getting started takes one command.

Three entry points depending on what you need. Each one sets up the knowledge repo, installs hooks, and wires up your agent client.

jurati quickstart

Creates a new knowledge repo, optionally pushes to a remote, and prints the adoption path. Good for starting from scratch.

jurati onboard

Connects to an existing knowledge repo and sets up local project wiring in one run. Good for joining an existing team.

jurati init

Scaffolds entry directories in an existing repo. Accepts packs: --pack generic, --pack training, --pack engineering.

jurati doctor

Checks that everything is wired up correctly. Validates config, tests connectivity, reports what's missing. Run this when something's not working.

Configuration

Configuration is environment variables.

No config files to manage, no YAML to parse, no settings UI. Set environment variables and go. Most have sensible defaults.

VariablePurposeDefault
JURATI_KNOWLEDGE_REMOTEGit remote URL for the knowledge repo(required)
JURATI_PR_SIGNAL_REPOSRepos to watch for labeled PRs(none)
JURATI_ACTIVE_REFS_ENABLEDEnable L0 fire-and-forget refsfalse
JURATI_SYNC_INTERVALSeconds between background syncs300
JURATI_CONTEXT_DEFAULT_MODEContext output format: manifest, compiler, fullmanifest
JURATI_CLIENT_SLUGIdentifies this client in entriesclaude-code
JURATI_PRIVACY_SALTSalt for opaque identity hashing(auto-generated)
Full configuration reference: 20+ variables covering core, active refs, sync, context, PR signals, and privacy. All documented in the repo.

Open source

Apache 2.0. Git-native. No vendor lock-in.

Jurati is an open source project. It stores everything in git, uses standard tools, and doesn't need a database, cloud service, or account.

What it needs

Git, Python 3.13+, and optionally the gh CLI for creating PRs. That's it.

What it doesn't need

No database, no cloud service, no SaaS account, no API keys, no Docker, no Kubernetes.

Where to find it

pip install jurati
Source: github.com/ostinato-forge/jurati

1,091 testspytest, passing
20x cheapervs manual lookup
$0.25/sessionmeasured token cost

Architecture

Core library with transport adapters.

Jurati is a pure core library with pluggable transport. Files for reads, CLI or MCP for writes. Works on any agent client.

File adapter.jurati/context.md · zero overhead
CLI commandsjurati record · write path
MCP serverinteractive queries · optional
TransportCLIcommands
TransportMCPserver.py
TransportFile adapter.jurati/context.md
CorePure Core Libraryschema, index, scoring, validate, changes, events, privacy, content_policy
I/OGit I/Orepo, commit, branch, pr
I/OProvidersgithub, jira, cache

Engineering shape

core/ is pure. git/ does I/O. Never mixed.

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.

core/schema, index, scoring, validate, changes, events, privacy, content_policy, contract, hygiene, paths, search, pr_signals. Pure functions. Zero I/O.
server.pyMCP tool handlers. Orchestration glue. Calls core/ for logic, git/ for persistence.
cli.pyCLI commands. Same orchestration pattern as server.py. Calls core/ for logic, git/ for persistence.
git/repo.py, commit.py, branch.py, pr.py, config.py, auth.py. I/O boundary. Talks to git and GitHub.
Key boundary: core/ must never import from git/ or providers/ or perform I/O. Enforced by test_architecture.py AST guard in the test suite.

Schema

Every entry is a markdown file with YAML frontmatter.

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

v1 to v2: no data migration needed.

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 fieldv1 defaultPurpose
visibilityINTERNALAuthor intent: internal or external distribution
sensitivityNORMALAccess control: normal, internal, or restricted
recorded_byNoneWhich MCP client submitted the write
on_source_branchderivedTrue if entry is on the source-of-truth branch
sourceNoneGit provenance: repo, commit, author
Backward compatibility rule: v1 entries are valid v2 entries. The index builder fills in defaults at parse time. No data files need to change.

File adapter

Context loads before the agent starts.

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.

Hook firesSessionStartagent client starts
CLIjurati startup--format md --output
Write.jurati/context.mdteam context as markdown
Agentreads the fileno tools, no permissions

Before (MCP-only)

15 MCP tool descriptions = ~735 tokens/turn. Permission prompts on first use. Platform-specific config. Breaks on non-MCP clients.

After (file adapter)

CLAUDE.md reads context = ~50 tokens/turn. Zero permission prompts. Works on any client that can read a file.

Read path

Git to index to ranked results.

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.

I/O · git/clone or fetchensure_repo, fetch_all
I/O · git/Parse entriesload_index_from_repo
CoreBuild Indexbuild_index, BM25 inverted index
CoreSearch / rankrank_entries, query_knowledge
TransportReturn to agentvia CLI / MCP / file
Key boundary: all indexing and ranking logic is pure — no network, no git calls, no filesystem access. I/O lives in separate modules.

Write path

Intent, policy, commit, PR.

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.

CorePlanplan_record() or plan_propose()
CorePolicy gateWritePolicy.evaluate()
I/OCommitmaterialize_write()
I/OBranchcreate_branch()
I/OPushpush to remote
I/OPRgh pr create

Atomic commits

Entry markdown file + event JSON file committed in a single git commit. If one fails, both fail. No partial writes.

Tool separation

record() handles L0/L1 (handoffs, status). propose() handles L2/L3 (decisions, workflows). You can't create a principle with record().

Content policy

WritePolicy.evaluate() is offline and deterministic.

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.

ActionBehaviorExample
BLOCKReject entirely, return errorAPI keys, passwords, connection strings
REQUIRE_REVIEWAllow with review labelContent mentioning personnel or security
ALLOW_WITH_WARNINGAllow but flag in metadataPatterns that might be sensitive
ALLOWNo match — proceedNormal team context
Rules are configurable via ContentPolicyConfig. Sensitive/restricted entries use stricter rule sets. The policy can be disabled with mode=disabled for testing.

Scoring

BM25 with recency decay and type weights.

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

Validation enforces type-level alignment and TTLs.

Every entry must pass validation rules in core/validate.py. These run on write and can be checked any time with jurati validate.

RuleConstraint
Type → LevelPrinciple=L3, Decision=L2/L3, Workflow=L2, Project=L1, Active=L0
ID format^[a-z0-9][a-z0-9-]{2,120}$
Active expiryActive entries MUST have expires_at. Non-active MUST NOT.
L0 TTL capMaximum 168 hours (7 days)
Review gateL1+ entries with status=active MUST have review.pr set
Timestamp orderingupdated_at >= created_at, expires_at > created_at
Sensitivity constraintRESTRICTED + EXTERNAL = error. RESTRICTED + L0 = error.
Supersession integrityIDs must exist, no circular chains
All validation logic lives in core/ — pure functions, no I/O. Can be run standalone via jurati validate or through the MCP validate() tool.

Events

Events record lifecycle, not behavior.

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.

What's in an event

Timestamp (UTC ISO-8601), event type (ENTRY_PROPOSED or ENTRY_EXPIRED), entry_id, entry_type, level, visibility. That's it.

What's excluded

Author identity, query text, entry body or content, who read what, request context, IP addresses, session IDs.

L0 entries produce no events at all. They're fire-and-forget — stored on ephemeral git refs, deleted on expiry. No audit trail, by design.
// 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

L0 entries live on ephemeral git branches.

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.

Branch format

refs/heads/jurati/active/{client}/{entry-id}

Discovered via fetch_all() + index overlay at sync time.

Constraints

TTL: 1–168 hours (7-day max). Always visibility: internal. Cannot be sensitivity: restricted (no access control on refs). No events produced.

Expiry

expire() deletes the ref immediately. No PR, no review, no commit on main. The entry simply disappears from the index.

Opt-in

Set JURATI_ACTIVE_REFS_ENABLED=true to enable. Most teams start without L0 and add it when they want faster handoffs.

Signal providers

PR signals use a provider protocol.

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
Signals are cached in memory with TTL. The cache prevents redundant API calls during a session. GitHub auth uses a candidate chain — no manual token configuration needed.

Privacy internals

Opaque identity hashing at integration boundaries.

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

Within Jurati

Git provenance (commits, PRs, author) stays intact for accountability. Not repurposed as tracking.

Across systems

Identities are hashed before crossing boundaries. System A can't correlate entries with System B's identities.

CLI reference

CLI commands: the full reference.

Every command works without an MCP server. Same core logic, same content policy, same knowledge repo.

CommandPurpose
SETUP
jurati initScaffold knowledge repo directories. Accepts packs: generic, training, engineering.
jurati quickstartCreate starter repo, push to remote, print adoption path.
jurati onboardConnect to existing repo + wire up project in one run.
jurati doctorCheck setup readiness, validate config, test connectivity.
jurati install-hookInstall post-commit hook for ambient capture.
jurati install-claude-mdInstall CLAUDE.md section and settings for Claude Code.
READ
jurati startupCompile 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 recordRecord handoff or status (L0/L1). --type active/project.
jurati proposePropose decision or workflow (L2/L3). --type decision/workflow/principle.
jurati captureProcess commit message through capture pipeline.
jurati flushFlush pending captures from offline queue.
TOOLS
jurati validateValidate all knowledge entries in a directory.
jurati htmlGenerate static HTML browser for local repo.
jurati import-contextImport context pack tarball. Supports --dry-run.
jurati import-docsImport markdown files. Modes: adr, rfc, docs, vault.

MCP reference

MCP tools: the full 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.

ToolPurposeKey params
CONTEXT
sync()Fetch latest, rebuild index
startup()Sync + compile context snapshotrepo, goal, max_entries
context()Session-start context with signalsrepo, goal, mode, paths
explain()What Jurati does in plain language
status()Repo, cache, sync health
READ
query()Exact topic matchingtopic, type, project
search()BM25 keyword rankingquery, type, tags, max_results
read()Single entry by IDid
read_many()Multiple entries (max 10)ids
list_entries()List with optional filterstype, project, status
WRITE
record()L0/L1 handoffs and statustype, title, content, projects
propose()L2/L3 decisions and workflowstype, title, content, projects
expire()Expire entry (L0: delete ref, L1+: PR)id, reason
OPS
validate()Validate one or all entriesid (optional)
hygiene()Expired/stale/superseded reportstale_after_days

Configuration

Environment variables configure everything.

All configuration lives in environment variables, parsed by git/config.py into a JuratiConfig dataclass. No config files, no YAML.

VariableDefaultNotes
CORE
JURATI_KNOWLEDGE_REMOTE(required)Git remote URL
JURATI_CACHE_DIR~/.cache/juratiLocal repo cache
JURATI_MAIN_BRANCHmainSource of truth branch
JURATI_CLIENT_SLUGclaude-codeClient identifier in entries
ACTIVE REFS
JURATI_ACTIVE_REFS_ENABLEDfalseEnable L0
JURATI_ACTIVE_REF_TTL_HOURS72Max 168 (7 days)
SYNC & CONTEXT
JURATI_SYNC_INTERVAL300Seconds between syncs
JURATI_CONTEXT_DEFAULT_MODEmanifestmanifest/compiler/full
JURATI_STARTUP_MAX_ENTRIES3Max 5
SIGNALS
JURATI_SIGNAL_PROVIDERgithubgithub/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

Apache 2.0. Python 3.13+. 1,091 tests.

Jurati is an open source project built to be auditable, testable, and easy to contribute to.

Dependencies

Git, Python 3.13+, optionally gh CLI. No database, no Docker, no cloud service.

Code quality

Ruff linting, mypy strict mode, conventional commits. Tests mirror src/ structure under tests/.

Architecture guard

AST-based test enforces core/ purity — zero I/O imports. If you break the boundary, the build breaks.

1,091 testspytest, all passing
core/ purityzero I/O, AST-enforced
v2 schemabackward-compatible, no migration

github.com/ostinato-forge/jurati