Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8225bd5
feat(protocol): implement SQLite store with DAG, conflict detection, …
Emerald-dev0 Jul 27, 2026
ba59a84
feat(compiler): implement Context Compiler with scope resolution, con…
Emerald-dev0 Jul 27, 2026
9287f78
feat(mcp): implement MCP server v2 with auth, rate limiting, and two-…
Emerald-dev0 Jul 27, 2026
11de1de
feat(protocol): implement sync engine, conflict resolver, and consist…
Emerald-dev0 Jul 27, 2026
6920c99
feat(sdk): implement TypeScript and Python agent SDKs with cross-sess…
Emerald-dev0 Jul 27, 2026
9b1a9cf
feat(observability): add audit_log table migration to Store
Emerald-dev0 Jul 27, 2026
3070f37
feat(observability): pass author to supersedeEntry for audit trail
Emerald-dev0 Jul 27, 2026
959fac6
feat(observability): export observability module from protocol index
Emerald-dev0 Jul 27, 2026
448d707
feat(observability): add observability types
Emerald-dev0 Jul 27, 2026
100b02e
feat(observability): implement AuditLog with immutable append-only st…
Emerald-dev0 Jul 27, 2026
4b37590
feat(observability): implement DecisionTracer for commitment-to-outpu…
Emerald-dev0 Jul 27, 2026
3eb4baa
feat(observability): implement AuditExporter for tenant-scoped GDPR/S…
Emerald-dev0 Jul 27, 2026
aaa63d4
feat(observability): implement MetricsCollector with OpenTelemetry-co…
Emerald-dev0 Jul 27, 2026
1f4e719
feat(observability): implement AlertingEngine with configurable rules…
Emerald-dev0 Jul 27, 2026
d72c816
feat(observability): implement DebugTooling with explain(), whyDroppe…
Emerald-dev0 Jul 27, 2026
9970f06
feat(observability): add observability module exports to protocol index
Emerald-dev0 Jul 27, 2026
9cf46b1
test(observability): add comprehensive tests for audit, tracer, metri…
Emerald-dev0 Jul 27, 2026
e1025ec
feat(api): implement REST API server with all protocol primitives
Emerald-dev0 Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Repository Rules (Session Memory)

These rules override any conflicting instructions elsewhere in the conversation.

## 1. Never work on main/master directly

- Before writing any code, check the current branch. If it's main/master, create a new branch first.
- Branch naming: `feature/<desc>`, `fix/<desc>`, `refactor/<desc>`, `docs/<desc>`, `chore/<desc>`, `spike/<desc>`
- Branch names are lowercase, hyphenated, no ticket numbers unless provided.

## 2. Commit discipline

- One logical change per commit. Never one giant commit at end of session.
- Never commit broken code to a shared branch.
- Format: Conventional Commits — `<type>(<scope>): <short summary>` with body explaining why.
- Types: feat, fix, refactor, docs, test, chore, perf, ci
- Scopes: storage, compiler, mcp, sync, sdk, cli, dashboard, infra, docs
- No messages like "fix stuff", "wip", "updates", "asdf".
- Never rewrite history on a pushed/shared branch unless asked.

## 3. Pull requests, not direct merges

- Open a PR from feature branch into main when work is complete.
- Every PR includes: what changed and why, how tested, breaking changes/migration steps, screenshots for dashboard.
- Do not merge own PR unless explicitly told to auto-merge.

## 4. Repository hygiene

- README.md must always be current (what, quick start, how to run, how to test, link to docs).
- .gitignore must be correct for the stack.
- No secrets, API keys, tokens, or .env files ever committed. Example config goes in `.env.example`.
- Every new package/service gets its own README.
- Consistent formatting/linting enforced via CI — set it up before writing more code.
- No dead code, commented-out blocks, or debug console.logs in commits headed for main.
- Folder structure should be self-explanatory.

## 5. CI enforcement

- Every PR must pass: build, lint, and relevant test suite.
- Never disable a failing test to make CI green.

## 6. Traceability

- Every commit/PR implementing a numbered prompt (Prompts 1–19) should reference which prompt/phase in the commit body or PR description.

## 7. When in doubt

- Default to smaller, more atomic commits.
- Never push directly to main — flag it first even for "trivial" changes.
64 changes: 64 additions & 0 deletions api/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//!/usr/bin/env node
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { createClientRoute } from './routes/index.js';
import { setupWebhooks } from './webhooks/index.js';
import { initDatabase } from './db/init.js';
import { logger, errorHandler } from './middleware/error.js';
import { authMiddleware } from './middleware/auth.js';
import { rateLimitMiddleware } from './middleware/rate-limit.js';

const app = express();

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(logger);

// Rate limiting
app.use(rateLimitMiddleware);

// Authentication middleware
app.use(authMiddleware);

// Initialize database
initDatabase().catch(console.error);

// API Routes - V1 Version (Protocol primitives)
app.use('/v1', createClientRoute());

// Webhook endpoints
app.use('/v1/webhooks', setupWebhooks());

// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: '1.0.0'
});
});

// Error handling
app.use(errorHandler);

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
console.log(`Contextly API v1.0.0 running on port ${PORT}`);
});

// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});

process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
process.exit(0);
});
79 changes: 79 additions & 0 deletions docs/AGENT_SDK_QUICKSTART.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Contextly Agent SDK Quickstart

The SDK is how agents read and write context programmatically. Three verbs,
one config value, no boilerplate.

## Installation

```bash
# TypeScript
npm install @contextly/sdk

# Python
pip install contextly
```

## Quickstart (TypeScript)

```typescript
import { Contextly } from "@contextly/sdk";

const ctx = new Contextly({ token: "ctx_project.abc_K4xq7T2mN9pV1cF8jL3wR5bY6aH0gDe" });

const brief = await ctx.read({ task: "What tech stack?", budget: 2000 });
console.log(brief.entries.map(e => `${e.cid}: ${e.message}`).join("\n"));

await ctx.commit({ cid: "stack.choice", message: "Next.js + Supabase" });
```

That is the entire API surface for 90 % of use cases: **read** what the team
already decided, then **commit** your own decisions.

## Quickstart (Python)

```python
from contextly import Contextly

ctx = Contextly(token="ctx_project.abc_K4xq7T2mN9pV1cF8jL3wR5bY6aH0gDe")

brief = ctx.read(task="What tech stack?", budget=2000)
for e in brief["entries"]:
print(f"{e['cid']}: {e['message']}")

ctx.commit(cid="stack.choice", message="Next.js + Supabase")
```

## API

| Method | Purpose | Key fields |
|--------|---------|------------|
| `read(options?)` | Compiled context for this scope | `budget`, `kind`, `cid`, `task` |
| `commit(input)` | Persist a decision or rule | `cid`, `message`, `kind`, `supersedes` |
| `query(filter?)` | Raw entry lookup | `id`, `cid`, `kind`, `status` |
| `resolve(input)` | Override a conflicting entry | `cid`, `message`, `kind`, `supersedingId` |
| `fork(scope, parent)` | Branch a scope | — |
| `merge(input)` | Reconcile two scopes | `source`, `target` |
| `onConflict(handler, pollMs?)` | Subscribe to conflicts | Returns unsubscribe fn |

## Token format

```
ctx_{scope}_{base62random}
```

The scope is embedded in the token — you never pass it separately. Generate
tokens via the Contextly CLI or dashboard.

## Error messages

Every error returns a human explanation, not an HTTP code:

- `"Scope not authorized: your token cannot access the requested scope."`
- `"Conflict detected: another agent already made a different decision for this cid."`
- `"Token must start with 'ctx_'."`

## Next steps

- See `examples/persistent-agent/` for a cross-session persistence demo
- Read `docs/PROTOCOL.md` for the wire format
- Read `docs/API_CONTRACTS.md` for exact MCP tool signatures
180 changes: 180 additions & 0 deletions docs/CONFLICT_RESOLUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Conflict Resolution in the Context Compiler

**How the Compiler resolves conflicting commitments — with worked examples.**

## The Two Sources of Conflict

Conflicts arise from two distinct mechanisms:

### 1. Divergent Supersession (within a scope)

Two agents independently write to the same `(scope, cid)` with different messages, and neither supersedes the other. Both entries remain active. The compiler detects this and returns both.

### 2. Scope Distribution (across scopes)

A parent scope and child scope both have entries for the same cid. The child overrides the parent. No conflict — this is intentional delegation.

---

## Resolution Rules (the Truth Model)

The compiler applies these rules, in order:

| Rule | What it does | Priority |
|------|-------------|----------|
| **Supersession** | If entry B explicitly supersedes entry A, A is marked `superseded` and dropped from output | 1 (highest) |
| **Scope override** | If scope `project.auth` has entry for cid X, it overrides any entry for X in `project` | 2 |
| **Conflict flagging** | If multiple entries for same cid within the same scope have no supersession relationship, both are returned and flagged | 3 |
| **Graceful degradation** | When over token budget, compress observations first, then decisions; never drop rules silently | 4 (lowest) |

---

## Worked Examples

### Example 1: Clean Supersession ✅

```
Entry A: cid="auth.provider" message="Use Supabase." status="active"
Entry B: cid="auth.provider" message="Use Auth0." supersedes=A
```

**Compiler output:** Entry B only. Entry A is dropped (superseded).

```
entries: [{ message: "Use Auth0.", provenance: { supersedesChain: ["sha256:B", "sha256:A"] } }]
conflicts: []
```

### Example 2: Real Conflict ⚠️

```
Agent Alice writes: cid="db.orm" message="Use Prisma."
Agent Bob writes: cid="db.orm" message="Use Drizzle." (no supersedes)
```

Both entries are active. Neither supersedes the other. The compiler:

```
entries:
- { message: "Use Prisma.", provenance: { sourceScope: "project" } }
- { message: "Use Drizzle.", provenance: { sourceScope: "project" } }
conflicts:
- { cid: "db.orm", existingEntry: "Use Prisma.", incomingEntry: "Use Drizzle." }
```

**Downstream:** An agent reading this context sees both options and can decide which to follow, then resolve by writing a new entry with `supersedes` set to the id of the entry it disagrees with.

### Example 3: Scope Inheritance (intentional) ✅

```
Root scope "project": cid="tech.stack" message="Uses TypeScript."
Child scope "project.auth": cid="auth.provider" message="Uses Auth0."
```

Compiling `project.auth`:

```
entries:
- { message: "Uses TypeScript.", provenance: { inherited: true, fromParent: "project" } }
- { message: "Uses Auth0.", provenance: { inherited: false, fromParent: null } }
```

The child inherits the parent's tech.stack decision and adds its own auth.provider. No conflict.

### Example 4: Scope Override (intentional) ✅

```
Root scope "project": cid="auth.provider" message="Use Supabase."
Child scope "project.auth": cid="auth.provider" message="Use Auth0."
```

Compiling `project.auth`:

```
entries:
- { message: "Use Auth0.", provenance: { inherited: false, fromParent: null } }
stats: { overridden: 1 }
```

The child's entry for `auth.provider` overrides the parent's. The parent's entry is not included.

### Example 5: Token Budget — Graceful Degradation 🪣

Active set (6 entries, ~120 tokens total):

| Priority | Entry | Tokens |
|----------|-------|--------|
| Rule | `Must use parameterized queries.` | 6 |
| Decision | `Use Drizzle ORM.` | 5 |
| Decision | `Deploy on Vercel.` | 5 |
| Observation | `API averages 240ms response time.` | 8 |
| Observation | `Database has 15 tables.` | 6 |
| Observation | `Frontend uses React 19.` | 6 |

**Budget = 30 tokens:**

1. First pass: compress observations (keep first sentence, or truncate at 80 chars).
2. Second pass: if still over, drop observations starting with the least relevant.
3. Third pass: if still over, drop decisions (never drop rules).
4. All dropped entries are logged with reason `"budget"`.

```
entries: [rule, decision(Use Drizzle), decision(Deploy), ...compressed observations]
dropped: [
{ cid: "frontend", kind: "observation", reason: "budget" }
]
stats: { compressed: 2, dropped: 1 }
```

Rules are **never** dropped. Observations are compressed first, then dropped. Decisions are compressed second, then dropped. If somehow the budget is exceeded after dropping all observations and decisions, the compiler still returns what it can with an error budget stat.

### Example 6: Task Relevance Ranking 🎯

```
Task: "Add authentication to the API"
```

Entries ranked by keyword overlap:

1. `auth.provider` — "Uses Supabase RLS" → matches "authentication" (rank: 1)
2. `api.routing` — "Uses Next.js App Router" → matches "API" (rank: 1)
3. `tech.stack` — "Uses TypeScript" → no match (rank: 0)
4. `db.orm` — "Uses Drizzle" → no match (rank: 0)

Within same relevance score, kind ordering applies: rules first, then decisions, then observations.

---

## What Never Happens

| Scenario | Why impossible |
|----------|---------------|
| Circular supersession (A→B→A) | Append-only — A must exist before B can reference it |
| Superseding a superseded entry | Store rejects it: "target is already superseded" |
| Self-supersession | Store rejects it: "cannot supersede itself" |
| Rules dropped before observations | Compiler enforces kind priority — rules are always kept last |
| Silent drop without logging | Every dropped entry is recorded in `dropped[]` with reason |
| Cross-scope id collision | `computeId` includes scope in hash: `SHA256(scope + "." + cid + "." + message)` |

---

## How to Resolve a Conflict

When an agent receives a `Conflict` in the compiled output:

```
Option 1: Write a new entry with supersedes
write({ cid: "db.orm", message: "Use Drizzle.",
supersedes: "sha256:abc..." })
→ Entry B is now superseded. Conflict resolved.

Option 2: Acknowledge and move on
Both entries remain active. Conflict persists.
Next compile() returns both + conflict.

Option 3: Fork the scope and resolve separately
fork("db.decision", "project")
→ Work in isolation, merge when decided.
```

The protocol does not auto-resolve conflicts. It surfaces them and lets agents or humans decide.
Loading
Loading