One HTTP service, one SQLite table, one retention loop.
The full roadmap in ROADMAP.md imagines an 11-phase system: Screenpipe, an interaction daemon, a Chrome extension, and a file watcher feeding the hub, with a classifier, waste detector, chain detector, and interest scorer consuming it, ending in a digest, nudge, and MCP output layer. None of that exists in this repo. Phase 1 built the plumbing underneath all of it: a CLI (observ serve) that resolves host and port from flags or a config file, hands off to uvicorn, and exposes exactly two routes for anything to post events into and check on.
Configuration lives at ~/.observ/config.toml, auto-created with defaults on first run and loaded through a Pydantic v2 singleton. The database lives at ~/.observ/events.db, outside the repo entirely; there is no database file checked in and no deploy target beyond a developer's own machine.
Every figure below traces to a command or a file path.
| Metric | Value | Evidence |
|---|---|---|
| Commits | 22 | git log --oneline | wc -l |
| First / last commit | 2026-03-13 / 2026-04-09 | git log --format='%ad' --date=short |
| Tags / releases | 0 | git tag (empty output) |
| Branches | 1 (master) | git branch -a |
| Source LOC (Python) | 579 | find src -name "*.py" | xargs wc -l |
| Test LOC (Python) | 646 | find tests -name "*.py" | xargs wc -l |
| Test files | 4 (test_api.py, test_storage.py, test_config.py, test_retention.py) | find tests -name "test_*.py" |
| Test cases | 48 | pytest --collect-only -q → "48 tests collected" |
| Requirements tracked | 85, 7 satisfied | .planning/REQUIREMENTS.md; PROGRESS.md "7/85 requirements satisfied" |
| Package name / version | observ 0.1.0, unpublished | pyproject.toml:2-3 |
| API surface | 2 endpoints: POST /api/events, GET /api/health | src/observ/api/events.py:12, src/observ/api/health.py |
| CLI surface | 1 command, 4 flags (serve --host --port --reload --config) | src/observ/cli/commands.py |
| Runtime deps | fastapi, uvicorn, pydantic, aiosqlite, tomli-w, click | pyproject.toml:6-13 |
| Deploy target | none, local-only, bind 127.0.0.1:8400 | CLAUDE.md "Default bind is 127.0.0.1 (safe)" |
| Database in repo | none (lives at ~/.observ/events.db) | find for *.db / *.sqlite under repo returns nothing |
| Planning docs | 15 files under .planning/ | find .planning -type f |
A linear path from HTTP POST to SQLite row.
Data flow for Phase 1 is a straight line: HTTP POST → Pydantic validation → Database.insert_event → SQLite (WAL) → HTTP response. No capture sources, analysis, or output layers sit on either end.
Entry point
main.py, 45 linesA FastAPI app with an async lifespan context manager that opens the SQLite database, starts the RetentionScheduler, and tears both down on shutdown. Two routers mount under /api.
Storage
storage/db.py, 155 linesAn async Database class over aiosqlite. Each operation, insert_event, health_check, purge_old_events, opens its own short-lived connection rather than reusing one. Schema lives in storage/schema.sql: one events table (id, timestamp, source, event_type, content, metadata, content_hash, created_at) with a UNIQUE(source, timestamp, event_type) constraint enforcing dedup, plus three DESC indexes on timestamp, (source, timestamp), and (event_type, timestamp).
Configuration
config/settings.py, 137 linesA Pydantic v2 ObservSettings singleton loaded from ~/.observ/config.toml via stdlib tomllib, write path via tomli_w, auto-created with defaults on first run, with a reset_settings() test hook.
Retention
retention.py, 128 linesA RetentionScheduler running as an asyncio.create_task, purging events older than retention_days (default 90) once immediately and then every interval_seconds, re-raising CancelledError on shutdown so task.done() reports correctly.
CLI
cli/commands.pyOne Click command, observ serve, which resets the settings singleton, resolves host and port from flags or config, and hands off to uvicorn.run("observ.main:app", ...).
Six decisions visible in the code, the commits, and the docs.
The WAL PRAGMA resets on every write
bug, disclosedWAL mode, synchronous = NORMAL, and wal_autocheckpoint = 1000 are set once in Database.init_schema, but insert_event and purge_old_events each open a fresh aiosqlite.connect(), which reverts synchronous to SQLite's default FULL on every write. journal_mode = WAL persists in the file header, but the code comment claiming "3x faster writes" is not realized on inserts.
content_hash is stored but never used
dead fieldComputed and stored on every insert, but never queried or indexed. Deduplication runs solely on the UNIQUE(source, timestamp, event_type) constraint.
Dedup keys on the second, not the sub-second
known edge caseTwo distinct events from the same source and event_type in the same second are treated as duplicates and the second is silently dropped: insert_event returns None, the API returns status="duplicate_dropped" with HTTP 201, not 409.
Pydantic BaseModel, not BaseSettings
deliberateChosen to avoid environment-variable coupling; the codebase owns config loading via tomllib / tomli_w instead (decision [01-03]).
asyncio.create_task over ensure_future
deliberateCancelledError is caught and re-raised rather than swallowed, because a silent swallow breaks task.done() detection for the retention scheduler (decision [01-04]).
No agent scaffolding added
scoped outNo skills, agents, or hooks under .claude/, only a settings.local.json. The project's own CLAUDE.md states the bar for agent scaffolding was judged not met for a project this small.
One build session, one sync commit, then silence.
git log --format='%ad %s' --date=short shows one continuous build on 2026-03-13: 21 commits running research synthesis, requirements, the 11-phase roadmap, a Phase 1 plan across three waves, then four feat/test/docs cycles for the scaffold, the SQLite store, the config system, and the retention scheduler, each closed out with a phase-completion doc commit.
A single commit follows, "chore: sync uncommitted changes", dated 2026-04-09, 27 days later. No feature commit has landed since.
Three AI systems rebuilt the same Phase 1 spec, months apart.
Three sibling directories, observ-grok, observ-kimi, observ-manus, each attempt an independent rebuild of the same Observ Phase 1 spec by a different AI system, named for the tool that built it. None is a fork or clone of the canonical git history: observ-grok's .git exists with zero commits; observ-kimi and observ-manus have no .git directory at all. All four share one architectural constant: the events table schema, columns, types, defaults, and all three indexes, is identical byte-for-byte in intent across every variant, differing only in comment style.
| Metric | observ (canonical) | observ-grok | observ-kimi | observ-manus |
|---|---|---|---|---|
| Built | 2026-03-13 to 04-09 | 2026-07-21 | 2026-07-28 | 2026-07-21 |
| Prompt input | git history, planning docs | 1,012-line few-shot prompt embedding literal source excerpts | planning docs / prose (HANDOVER.md cites canonical ROADMAP.md) | none in repo, no prompt or handover doc |
| Source LOC | 579 (Python) | 415 | 1,144 Python + 1,053 HTML/CSS/JS | 317 |
| Test LOC | 646 | 735 | 696 | 305 (single file) |
| Tests collected | 48 | 67 | 69 | 53 |
| API endpoints | 2 | 2 | 10 | 2 |
| DB connection pattern | per-operation connect, WAL PRAGMA reset on reconnect | same per-operation pattern, same WAL-reset characteristic | single persistent connection, no PRAGMAs issued | single persistent connection, no PRAGMAs issued |
| Self-report vs. actual test count | n/a | PDF claims "52+ tests"; repo has 67 | HANDOVER.md claims 32; repo has 69 | no self-report document exists |
Closest architectural match
Given literal excerpts of the canonical Database class, PRAGMA sequencing, and dedup logic, with instructions to match "whitespace, parameter handling, async patterns exactly." Reproduced the identical 2-endpoint API and the same per-operation-connection, WAL-reset characteristic as the canonical build.
docs/observ-handover.pdf is internally contradictory: it claims "pytest structure ready (52+ tests spec-compliant)" in its verification section, then separately admits "No full 48 tests written" under deviations. Neither figure matches the 67 tests actually collected.
Built past Phase 1 scope
Pointed at planning docs rather than source, and used that latitude to add a capture/ package, a second BackgroundScheduler, and a full static/ web dashboard (1,053 combined lines) with pause and resume controls, none of which Phase 1 or the other two variants have. 10 routes, not 2.
HANDOVER.md claims "32 tests, all passing"; the repo as it stands collects 69. It also flags its own Screenpipe client as untested against a live instance, and a stray directory literally named ~ holds a leftover 218-row database from an unexpanded shell path, not a code defect.
Closest to spec size
No prompt, handover, or planning document exists in the repo at all, the earliest of the three builds. Smallest footprint: 317 source lines, 305 test lines, 53 collected test cases, and the only variant matching the canonical 2-endpoint API exactly.
Retention is a plain while True loop with inline asyncio.sleep(86400); on shutdown it cancels the task but never awaits it. The only variant with author metadata in pyproject.toml and a checked-in egg-info build artifact.
Fidelity to the canonical implementation tracks directly with how much of the actual source a model was given. Grok, handed literal code excerpts as a few-shot pattern, reproduced the architecture, WAL-reset bug included, almost exactly. Kimi and Manus, working from prose plans, independently converged on a different, simpler choice (one persistent connection, no WAL PRAGMAs at all) and diverged from each other in scope, with Kimi alone building substantially beyond Phase 1 into an unrequested dashboard and capture-polling layer.
What Phase 1 is not.
Observ Phase 1 is an event ingestion and storage backbone only. It is not a behavioral analysis system: no Screenpipe integration, interaction daemon, browser extension, file watcher, activity classifier, chain detector, digest generator, or MCP server exists in this repo. It has never been packaged for distribution, no tags, no PyPI publish, and has no deployment target beyond a developer's own machine.