Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.97.1 # pinned; keep in sync with rust-toolchain.toml
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Toolchain pin is consistent
run: ./scripts/toolchain-pin-check.sh
- name: Format
run: cargo fmt --all -- --check
- name: Clippy (deny warnings)
Expand Down Expand Up @@ -57,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.97.1 # pinned; keep in sync with rust-toolchain.toml
with:
components: clippy
- uses: Swatinem/rust-cache@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.97.1 # pinned; keep in sync with rust-toolchain.toml
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
Expand Down
24 changes: 19 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,25 @@ dist-ssr/
*.p12
*.provisionprofile

# Project documentation is local-only and must not be published.
# README.md is the sole exception — it is the repo's public front page.
*.md
!README.md
/docs/
# Documentation.
#
# Contributor-essential docs ARE tracked: AGENTS.md, CLAUDE.md, docs/DOCS_INDEX.md,
# and docs/{architecture,agents,development}. Someone cloning this repository needs
# them to build correctly, and AGENTS.md is referenced as the canonical contributor
# guide, so it has to exist in a fresh clone.
#
# These stay local-only. This repository is public; product strategy, unshipped
# design, decision rationale, and competitor research are not published.
/docs/product/
/docs/design/
/docs/decisions/
/docs/research/
/RESEARCH_SUMMARY.md

# Working logs and scratch notes — long, machine-generated, not part of the project.
/AUDIT_LOG.md
/POLISH_LOG.md
/audit-goal.txt

# Local agent instructions and design previews
/.claude/
Expand Down
76 changes: 76 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# AGENTS.md — working on Lore

> Instructions for humans **and** coding agents contributing to this repository. Read this before making changes. It is intentionally short and points to canonical docs; do not duplicate their content here.

## What Lore is
Lore is a **local-first desktop app that turns the sessions your coding agents already write to disk into a searchable, git-anchored knowledge base** — running entirely on the user's machine, no account, no cloud. Tagline: *"Your coding agents forget. Lore doesn't."* Positioning: **"git memory for coding agents"** — the differentiator is provenance-aware git *depth* (session-recorded commit/patch evidence where present, ingest-time state where not, worktree/rebase-aware) + git-evidenced skills, **not** cross-agent viewing (table stakes; competitors like CCHV already do it). Go **narrower + deeper** (Claude Code + Codex), not broader. Full framing + the competitive reasoning: `docs/product/VISION.md`, `docs/product/PRD.md`, `docs/research/COMPETITIVE_LANDSCAPE.md` — all *(internal, not in this repository; see `docs/DOCS_INDEX.md`)*.

> **Status: active implementation.** The repository contains a Rust archive core, Claude Code and Codex adapters, SQLite storage/search, Git enrichment, a Tauri shell, and a React UI. It is not packaged for general installation. Treat `docs/product/ROADMAP.md` *(internal)* as the canonical implementation-status record, and never claim an acceptance gate passed without running it.

## Core principles (non-negotiable)
1. **Local-first & private by default.** Archive modules have no network capability; the separate update check is off by default and sends only documented release fields when explicitly invoked/enabled. No telemetry, accounts, or LLM calls in V0. Enforced by dependency/call-site guards and OS-level egress tests. See `docs/architecture/SECURITY.md`, `LOCAL_FIRST.md`, ADR-0005.
2. **Read, don't wrap.** We ingest agents' on-disk logs **read-only**; we never modify or wrap the agents. New agent = new **adapter**, not a fork.
3. **Git evidence keeps provenance.** Agent-recorded values, agent-recorded patches, Lore's ingest-time observation, and later re-verification are first-class and never blurred into a fictional exact session-time snapshot. `docs/architecture/GIT_INTEGRATION.md`.
4. **Fidelity before synthesis.** Store what happened faithfully; interpret (skills, summaries) later, always traceable to evidence.
5. **Tolerant parsing.** Never hard-fail on unknown/partial/newer input; degrade to `partial` with a note; isolate adapter panics. `docs/architecture/AGENT_ADAPTERS.md` §5.
6. **Simple, observable, testable, replaceable** over clever/distributed. One process, one primary SQLite database plus bounded app-owned support files, adapters behind a trait. No microservices/daemon/cloud.

## Architecture (one screen)
Tauri 2 app: **Rust core** (discovery · watcher · adapters · ingest · git · storage(SQLite+FTS5) · search · secrets · jobs) + **React/TS UI** over a generated, versioned IPC contract. Details: `docs/architecture/ARCHITECTURE.md`. Data model: `docs/architecture/DATA_MODEL.md`.

## Repository layout
- `docs/` — canonical documentation (start at `docs/DOCS_INDEX.md`).
- `crates/lore-core/` — testable Rust archive core (adapters, ingest, Git, storage, search, safety, jobs).
- `crates/lore-ipc/` — versioned Rust DTOs and generated TypeScript bindings.
- `src-tauri/` — thin Tauri command and application layer.
- `src/` — React/TypeScript UI.
- `AGENTS.md` (this) · `CLAUDE.md` · `README.md` · `RESEARCH_SUMMARY.md` *(internal)*.

## Development
Commands, layout, and conventions: `docs/development/DEVELOPMENT.md`. Testing (fixtures, guards, perf): `docs/development/TESTING.md`. Releases: `docs/development/RELEASES.md`.
> Commands now exist, but **run before you claim**. Report exactly what ran and what did not (global rule).

## Coding conventions (summary)
- Rust: `fmt` + `clippy -D warnings`; no `unwrap()/panic!` on untrusted input; errors at boundaries.
- TS: strict; heavy work in Rust via IPC, never in the webview.
- IPC DTOs are **generated** from Rust (`ts-rs`) — never hand-edit the contract.
- Adapters: implement the trait, add fixtures + a `docs/agents/<AGENT>.md`, degrade gracefully.
- Migrations: additive-first; every schema change = migration + `DATA_MODEL.md` update + test.
- Every bug fix gets a regression fixture.

## Architectural constraints (hard limits)
- **No network capability in archive modules.** The updater is the sole V0 network-capable component and is off by default. Any other off-machine flow requires a **new ADR** + `SECURITY.md` data-flow review + explicit opt-in.
- **No server / daemon / hosted DB / auth / Kubernetes / microservices.**
- **No LLM calls in V0.** Skill synthesis (V0.5+) needs a settled privacy model first.
- **Read-only** on all agent files. Never write/rename/delete a user's session logs or repos.
- Opaque/encrypted agent fields (e.g. Codex `encrypted_content`) are **never** indexed or exported.

## What NOT to do
- Don't turn Lore into an IDE, agent runtime/orchestrator, forward-memory/RAG injector, note-taking app, or SaaS. See `docs/product/NON_GOALS.md` *(internal)*.
- Don't add Claude-specific code outside an adapter.
- Don't introduce embeddings/semantic search before FTS is proven insufficient (ADR-0004).
- Don't fake screenshots or claim unbuilt features (README must stay honest).
- Don't test parsers against real user history; use anonymized fixtures.
- Don't spawn subagents/orchestration for routine work unless the user asks.

## Documentation you MUST keep in sync (self-maintaining docs)
If your change affects any of the following, update the **canonical doc in the same change** (map in `docs/DOCS_INDEX.md`). This is a review gate, not a suggestion:

| If you change… | Update… |
|---|---|
| the DB schema / entities | `docs/architecture/DATA_MODEL.md` (+ a migration + test) |
| an agent's parsing / a new adapter | `docs/agents/<AGENT>.md` and `docs/architecture/AGENT_ADAPTERS.md`; if cross-cutting, also `docs/research/AGENT_STORAGE_FORMATS.md` *(internal)* |
| the IPC/command/event contract or a CLI/API surface | `docs/architecture/ARCHITECTURE.md` §5 (+ regenerate TS types) |
| git identity/snapshot/worktree logic | `docs/architecture/GIT_INTEGRATION.md` |
| search behavior/ranking/tokenizer | `docs/architecture/SEARCH.md` |
| any security/privacy assumption or a new data flow | `docs/architecture/SECURITY.md` (+ possibly a new ADR) |
| a UX flow / screen | `docs/design/WIREFRAMES.md` / `INFORMATION_ARCHITECTURE.md` *(both internal)* |
| a significant technical decision | add/append an **ADR** in `docs/decisions/` (Context/Options/Decision/Why/Tradeoffs/Consequences/Revisit) |

Rule of thumb: **if the change would make a doc's statement false, fix the doc in the same PR.** Prefer updating the one canonical source (no duplicated truth).

## Nested AGENTS.md
Subsystem-specific instructions may live in nested `AGENTS.md` files (e.g. `src-tauri/src/adapters/AGENTS.md` once code exists) covering that subsystem's invariants. Keep them thin and local.

## Open decisions (need human input — don't unilaterally resolve)
See `RESEARCH_SUMMARY.md` *(internal)* §9: the **skill-promotion privacy model** and confirmation of the **Tauri** choice remain open — flag, don't silently decide. **Naming is DECIDED (2026-08-10): keep "Lore" everywhere** (product + skill feature), differentiating via git-evidenced skills; a trademark/SEO check is still owed before public launch.
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# CLAUDE.md

Claude Code / Claude-specific notes. **The canonical contributor guide is [`AGENTS.md`](AGENTS.md)** — read it first. This file only adds Claude-specific pointers; it deliberately does **not** duplicate AGENTS.md.

## Orientation
- Start: [`AGENTS.md`](AGENTS.md), then [`docs/DOCS_INDEX.md`](docs/DOCS_INDEX.md).
- Status: **active implementation**. The Rust core, Claude Code and Codex adapters, storage/search/Git paths, Tauri shell, and React UI exist; packaging and V0 acceptance work remain. Use `docs/product/ROADMAP.md` *(internal)* for current status. Don't claim a gate passed without running it.

## Especially relevant when working here
- **Lore parses Claude Code's own sessions.** The authoritative schema (verified by direct inspection) is [`docs/agents/CLAUDE_CODE.md`](docs/agents/CLAUDE_CODE.md) — read it before touching the Claude adapter. Storage: `~/.claude/projects/<encoded-cwd>/<uuid>.jsonl`.
- Never rely on the lossy `<encoded-cwd>` dir name for identity; use each event's `cwd`.
- `thinking` blocks are sensitive: not indexed by default, never exported without redaction awareness.
- Test the parser on **anonymized fixtures**, never on real `~/.claude` history in CI (`docs/development/TESTING.md`).

## Guardrails (from AGENTS.md — the ones easiest to trip)
- No network capability in archive modules; the separate update check is off by default and explicit. No LLM calls in V0.
- Read-only on all agent files.
- Keep agent-specific code inside an adapter.
- If you change schema/adapter/IPC/git/search/security/UX, update the canonical doc in the same change (map: `docs/DOCS_INDEX.md`).

## Naming note
**SpecStory ships a feature called "Lore"** (sessions→skills). **Decided (2026-08-10): keep "Lore" everywhere** and differentiate via git-evidenced skills (`RESEARCH_SUMMARY.md` *(internal)* §9.1). A trademark/SEO check is still owed before public launch.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Everything in the archive stays on the machine. V0 has no accounts, telemetry, c

You will need Rust, Node.js, the Xcode command-line tools, and the Tauri 2 prerequisites for macOS.

The Rust toolchain is pinned in `rust-toolchain.toml`, so rustup selects the right compiler automatically. CI lints with `-D warnings`, and pinning keeps a new clippy lint in a stable release from breaking the build on an unrelated commit.

```bash
npm install
npm run build
Expand Down
10 changes: 8 additions & 2 deletions crates/lore-core/src/adapters/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,10 @@ mod tests {
assert_eq!(session.status, crate::model::ParseStatus::Ok);
assert_eq!(session.tool_calls.len(), 3);
assert_eq!(session.file_events.len(), 1);
assert_eq!(session.file_events[0].tool_native_call_id.as_deref(), Some("c_empty"));
assert_eq!(
session.file_events[0].tool_native_call_id.as_deref(),
Some("c_empty")
);
assert_eq!(session.file_events[0].path, "");
}

Expand Down Expand Up @@ -953,7 +956,10 @@ mod tests {
);
let session = ClaudeCodeAdapter::new().parse_str(jsonl, "null-id-tools");
assert_eq!(session.status, crate::model::ParseStatus::Partial);
assert!(session.notes.iter().any(|n| n.message == "tool_use without id"));
assert!(session
.notes
.iter()
.any(|n| n.message == "tool_use without id"));
}

#[test]
Expand Down
46 changes: 33 additions & 13 deletions crates/lore-core/src/adapters/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,11 @@ mod tests {
);
let s = CodexAdapter::new().parse_str(content, "unknown-item");
assert_eq!(s.status, crate::model::ParseStatus::Partial);
assert_eq!(s.messages.len(), 1, "known message before unknown item is preserved");
assert_eq!(
s.messages.len(),
1,
"known message before unknown item is preserved"
);
}

#[test]
Expand All @@ -1196,11 +1200,19 @@ mod tests {
assert_eq!(s.status, crate::model::ParseStatus::Ok);
assert_eq!(s.file_events.len(), 2);

let empty_event = s.file_events.iter().find(|e| e.path == "src/empty.txt").unwrap();
let empty_event = s
.file_events
.iter()
.find(|e| e.path == "src/empty.txt")
.unwrap();
assert_eq!(empty_event.change_kind, FileChangeKind::Create);
assert_eq!(empty_event.patch_text.as_deref(), Some(""));

let null_event = s.file_events.iter().find(|e| e.path == "src/null.txt").unwrap();
let null_event = s
.file_events
.iter()
.find(|e| e.path == "src/null.txt")
.unwrap();
assert_eq!(null_event.change_kind, FileChangeKind::Create);
assert_eq!(null_event.patch_text, None);
}
Expand All @@ -1218,7 +1230,10 @@ mod tests {

assert_eq!(s.messages[0].role, Role::System);
assert_eq!(s.messages[0].parts.len(), 1);
assert_eq!(s.messages[0].parts[0].text.as_deref(), Some("system prompt"));
assert_eq!(
s.messages[0].parts[0].text.as_deref(),
Some("system prompt")
);

assert_eq!(s.messages[1].role, Role::User);
assert_eq!(s.messages[1].parts.len(), 2);
Expand All @@ -1227,7 +1242,10 @@ mod tests {

assert_eq!(s.messages[2].role, Role::User);
assert_eq!(s.messages[2].parts.len(), 1);
assert_eq!(s.messages[2].parts[0].text.as_deref(), Some("fallback to user"));
assert_eq!(
s.messages[2].parts[0].text.as_deref(),
Some("fallback to user")
);
}

#[test]
Expand All @@ -1245,17 +1263,13 @@ mod tests {

#[test]
fn title_derivation_from_multipart_user_prompt_and_synthetic_flag() {
let content = concat!(
"{\"type\":\"response_item\",\"timestamp\":\"2026-08-11T10:00:00.000Z\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"text\":\"Refactor session store\"}]}}\n"
);
let content = "{\"type\":\"response_item\",\"timestamp\":\"2026-08-11T10:00:00.000Z\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"text\":\"Refactor session store\"}]}}\n";
let s = CodexAdapter::new().parse_str(content, "multipart-title");
assert_eq!(s.status, crate::model::ParseStatus::Ok);
assert_eq!(s.title.as_deref(), Some("Refactor session store"));
assert!(s.title_is_synthetic);

let system_only = concat!(
"{\"type\":\"response_item\",\"timestamp\":\"2026-08-11T10:00:00.000Z\",\"payload\":{\"type\":\"message\",\"role\":\"system\",\"content\":\"system prompt\"}}\n"
);
let system_only = "{\"type\":\"response_item\",\"timestamp\":\"2026-08-11T10:00:00.000Z\",\"payload\":{\"type\":\"message\",\"role\":\"system\",\"content\":\"system prompt\"}}\n";
let s2 = CodexAdapter::new().parse_str(system_only, "system-only");
assert_eq!(s2.title, None);
assert!(!s2.title_is_synthetic);
Expand Down Expand Up @@ -1302,7 +1316,10 @@ mod tests {
assert_eq!(s.messages.len(), 1);
assert_eq!(s.messages[0].parts.len(), 2);
assert_eq!(s.messages[0].parts[0].kind, PartKind::Thinking);
assert_eq!(s.messages[0].parts[0].text.as_deref(), Some("Planning steps"));
assert_eq!(
s.messages[0].parts[0].text.as_deref(),
Some("Planning steps")
);
assert_eq!(s.messages[0].parts[1].kind, PartKind::Opaque);
}

Expand Down Expand Up @@ -1335,6 +1352,9 @@ mod tests {
let content = "{\"type\":\"response_item\",\"timestamp\":\"2026-08-11T10:00:00.000Z\",\"payload\":{\"type\":\"\"}}\n";
let s = CodexAdapter::new().parse_str(content, "empty-type");
assert_eq!(s.status, crate::model::ParseStatus::Partial);
assert!(s.notes.iter().any(|n| n.message.contains("unknown response_item")));
assert!(s
.notes
.iter()
.any(|n| n.message.contains("unknown response_item")));
}
}
18 changes: 14 additions & 4 deletions crates/lore-core/src/adapters/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ mod tests {
assert_eq!(unified_diff_line_counts(context_only), Some((0, 0)));

// Git headers only diff (0 additions, 0 deletions)
let headers_only = "diff --git a/f.rs b/f.rs\nindex 0000000..1234567\n--- a/f.rs\n+++ b/f.rs\n";
let headers_only =
"diff --git a/f.rs b/f.rs\nindex 0000000..1234567\n--- a/f.rs\n+++ b/f.rs\n";
assert_eq!(unified_diff_line_counts(headers_only), Some((0, 0)));

// Hunk headers only (0 additions, 0 deletions)
Expand All @@ -270,7 +271,10 @@ mod tests {
Some("Fix repository discovery".to_string())
);
assert_eq!(title_from_text("<empty_tag>\n</empty_tag>"), None);
assert_eq!(title_from_text("<environment_context>\nOS: Mac\nCwd: /repo\n"), None);
assert_eq!(
title_from_text("<environment_context>\nOS: Mac\nCwd: /repo\n"),
None
);
assert_eq!(title_from_text("<skill>\nName: rust-dev\n"), None);
assert_eq!(
title_from_text("<context>repository info</context>\nRefactor SQLite queries"),
Expand Down Expand Up @@ -441,11 +445,17 @@ mod tests {
"Users/test/file.txt"
);
assert_eq!(sanitize_path("./src/./main.rs"), "src/main.rs");
assert_eq!(sanitize_path("/absolute/path/file.rs"), "absolute/path/file.rs");
assert_eq!(
sanitize_path("/absolute/path/file.rs"),
"absolute/path/file.rs"
);
assert_eq!(sanitize_path("a/b/c/../../d.rs"), "a/d.rs");
assert_eq!(sanitize_path("src/app/"), "src/app");
assert_eq!(sanitize_path(r"src\app\"), "src/app");
assert_eq!(sanitize_path(r"\\server\share\file.rs"), "server/share/file.rs");
assert_eq!(
sanitize_path(r"\\server\share\file.rs"),
"server/share/file.rs"
);
assert_eq!(sanitize_path(".../src/lib.rs"), ".../src/lib.rs");
assert_eq!(sanitize_path("..../src/lib.rs"), "..../src/lib.rs");
assert_eq!(sanitize_path("a///b///c.rs"), "a/b/c.rs");
Expand Down
4 changes: 3 additions & 1 deletion crates/lore-core/tests/enrich.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,9 @@ fn multi_segment_session_in_same_repo_resolves_to_single_repository() {

// Both segments must link to the EXACT same repository row.
let repo_ids: Vec<String> = conn
.prepare("SELECT repository_id FROM session_segment WHERE session_id = ?1 ORDER BY seq_start")
.prepare(
"SELECT repository_id FROM session_segment WHERE session_id = ?1 ORDER BY seq_start",
)
.unwrap()
.query_map([&sid], |r| r.get(0))
.unwrap()
Expand Down
Loading
Loading