Git-native project memory for agents. Everything is derived from sources of truth — Slack, GitHub, Jira, Granola meetings, Notion, Figma, and the client's email — except what you explicitly ask it to remember. Ask an agent literally anything about a project and it can find it.
No server, no database service. Text in git is the source of truth; a timer keeps it fresh; the CLI is the interface — the repo is just the database.
lore setup # wizard: create a context repo, wire every source, sync it, link this repo
lore sync # connectors → context/streams/ + context/work/ (deterministic, no LLM)
lore extract # streams → derived artifacts (LLM fold — Claude API or subscription)
lore grep # search project memory — from any linked repo, or anywhere with -p
lore recall # pinned facts + derived artifacts + live work tables
lore remember # pin a fact (an explicit write)
lore sow add # attach a statement of work: human-weeks sold over a period (the other explicit write)
lore doc add # add a document a client sent — spec, brief, deck — to the raw material, folded like email
lore refresh # pull the latest memory; --trigger makes the host sync right now
lore mcp # the same verbs as MCP tools over stdio, for agents
Storage and interface are separate layers:
- Storage is a context repo — a plain private git repo, usually one per client, separate from the code. Slack history has a different audience than code, and a standalone repo has no CI or branch protection for the sync to fight.
- Interface is this CLI. Project repos carry a one-line pointer; the CLI
resolves it, keeps a clone in
~/.lore/cache/, pulls before reads, pushes writes. Neither agents nor humans need to know where the files live.
Two ways to host the storage, chosen once in ~/.lore/config.json and
otherwise identical:
- Self-hosted (recommended): one VM holds every client's context repo as
a bare git origin and a systemd timer runs the sync and fold every 15
minutes. Vendor tokens live in the host platform's secret injection, never
on disk. docs/DEPLOY_EXE.md is the exe.dev runbook;
deploy/exe/setup.shprovisions the host. - GitHub-hosted: each context repo is a private GitHub repo and a daily GitHub Action runs the sync, with tokens as Actions secrets.
Sync pulls raw material into context/streams/ as permalinked markdown,
scrubbing anything that looks like a token, key, or password on the way in
(git history is forever). Lore is also the work tracker of record:
context/work/lore/<PREFIX>.yaml holds the project's tickets (CAR-1,
JOI-14…) in rank order. Jira and GitHub Issues are inputs — sync mirrors
their open issues into that table and keeps their own snapshots under
context/work/jira/ and context/work/github/ — and every ticket carries a
history of who moved it, through which surface, why, and on what evidence:
a person via lore work, an agent over MCP, sync when the external tracker
moves, or the fold when the conversation is unambiguous evidence that
something shipped, stalled, or changed priority. Extract folds the streams
into structured artifacts — derived/requests.yaml, decisions.yaml,
roadmap.yaml, weekly reports — every item citing its source, and reviews
the tracker as part of the same pass. facts.yaml is the pinned layer:
written only via lore remember, and it wins over derived data on conflict.
Every explicit write lands in context/audit.jsonl with who asked, through
which surface, and the supporting source.
Requires Node >= 20.12 and git; the GitHub-hosted mode also needs the
gh CLI, authenticated. Not on npm yet:
npm install -g github:nerdburn/loreSelf-hosted? Provision the host first (docs/DEPLOY_EXE.md)
and set remote in ~/.lore/config.json to it; lore setup then creates
bare repos there instead of on GitHub. GitHub-hosted? The first lore setup
asks which GitHub org context repos belong in (yours, never the client's)
and saves it.
Only for the sources you use. Every client after the first reuses these.
- Slack —
lore manifest slack | pbcopy, then at api.slack.com/apps: Create New App → From a manifest → pick the workspace → paste → Create, then Install App and copy the Bot User OAuth Token (xoxb-…). The app is read-only by design — nochat:write, no events; the bot never posts.export SLACK_TOKEN=xoxb-…(or.env— never git), or register it as the host'sslackintegration when self-hosted. - GitHub — a fine-grained token (or a GitHub App installation) with read
access to Contents, Issues, Pull requests, and Metadata on just the
client's repos, as
LORE_GITHUB_TOKEN. Never a broad personal token, and not an Actions workflow's ownGITHUB_TOKEN, which only sees the context repo. - Granola — OAuth:
lore auth granolaonce on the machine that syncs. It prints a URL and a code, you approve in a browser, and the grant is saved to~/.lore/granola-auth.jsonand refreshed automatically. (A statictokenor a proxyendpointalso work.) - Notion — an internal integration; its token as
NOTION_TOKEN. Sharing pages with the integration (Notion → page ··· → Connections) is the consent model. - Jira Cloud — an Atlassian account email + API token as
JIRA_EMAIL/JIRA_TOKEN(HTTP Basic), or anapi_baseproxy that injects the header. - Gmail — a Google Workspace service account with domain-wide
delegation, scopes
gmail.readonly(andadmin.directory.user.readonlyforusers: "all"), granted once by a Workspace admin. Key JSON at~/.lore/gmail-sa.jsonon the syncing host. It can't sit behind a header-injecting proxy, because the bearer differs per mailbox.
From inside the client's project repo:
cd ~/code/acme
lore setup --channels "#acme,#acme-dev" \
--github "acme/web,acme/mobile" \
--granola "Acme" \
--notion "https://www.notion.so/acme/…" \
--jira "ACM" --jira-site https://acme.atlassian.net \
--gmail all \
--client "Acme" --domains "acme.com"Every flag is optional and every value is a prompt with a derived default —
the context repo name comes from the git remote (lore-acme), backfill
defaults to 3 months. Give it any subset of sources: --channels is not
required when another source is given, so a client without Slack works.
Agents and scripts pass flags plus --yes to skip prompts.
Setup writes lore.json with the sources and the client block, scaffolds
and pushes the context repo, and links the repo you're standing in. Then,
per mode: self-hosted, the host's timer picks the new repo up on its next
run; GitHub-hosted, it creates the private repo, sets the token secrets,
verifies the daily workflow registered, and dispatches the first sync.
The first sync backfills and can be slow (Slack rate-limits new apps hard; lore waits and resumes automatically). After that, syncs are incremental.
- Slack:
/invite @lorein each channel. The bot can only read channels it's been invited to — that's the consent model, not a limitation:lore.jsonsays what lore wants, invitations control what it can, and the bot in the member list means a synced channel is never a secret. - Notion: share the client's root page(s) with the integration.
- GitHub, self-hosted: one integration per client repo on the host.
- Gmail, first time only: a Workspace admin grants the delegation.
cd ~/code/acme-mobile
lore link your-org/lore-acme # or just `lore link lore-acme` when self-hostedWrites the one-line lore.json pointer and an AGENTS.md section — commit
both. That's the entire footprint in a code repo: no context files, no
workflow, no secrets. Any number of repos can point at one context repo.
Each is an entry under sources in the context repo's lore.json; the
wizard writes them, lore source add widens them later (an agent can too,
via lore_source_add), and this is what they mean. Every scope belongs to
exactly one client repo. Secrets are always env: references (lore loads
.env from the working directory; real env vars win) or an api_base /
endpoint proxy that injects them, so a token never sits in git.
"sources": {
"slack": { "channels": ["#acme", "#acme-dev"], "token": "env:SLACK_TOKEN" },
"github": { "repos": ["acme/web", "acme/mobile"], "token": "env:LORE_GITHUB_TOKEN" },
"granola": { "folders": ["Acme"], "attendee_domains": ["acme.com"] },
"notion": { "roots": ["<page or database url>"], "token": "env:NOTION_TOKEN" },
"jira": { "projects": ["ACM"], "site": "https://acme.atlassian.net", "email": "env:JIRA_EMAIL", "token": "env:JIRA_TOKEN" },
"linear": { "teams": ["PRP"], "token": "env:LINEAR_API_KEY" },
"gmail": { "users": "all", "admin": "shawn@inputlogic.ca", "exclude": [], "query": "-subject:\"Invitation:\" -label:newsletters" }
}- Slack syncs messages and threads per channel. Channel names must match
Slack exactly (hyphens!). Every run re-reads a rolling
overlap_dayswindow (default 1) for late deliveries and tracks replies per thread forthread_window_days(default 30), so late replies to old threads are picked up regardless of the overlap. Channels added tolore.jsonlater backfill automatically. - Linear syncs issues and comments for each team through the GraphQL
API and maintains
context/work/linear/<TEAM>.yaml— the live issue table (categoryis Linear's state type), seeded with every open issue on the first sync and mirrored into the lore tracker like Jira:startedis in progress,completed/canceledare done. A personal API key goes in asAuthorization— on the host through two http-proxy integrations,api_basefor api.linear.app anduploads_basefor uploads.linear.app, where files pasted into issues live. - GitHub syncs issues, pull requests, comments, reviews, commits, and
releases for each repo, and maintains
context/work/github/<owner>__<repo>.yaml— the live issue/PR table, seeded with every open item on the first sync.includenarrows the kinds (issues,comments,reviews,commits,releases). - Granola talks to Granola's MCP server as a client, so it sees exactly
what a Granola-connected agent sees. Meetings are scoped by
folders(titles or ids),attendee_domains, and the repo'sclientblock — any attendee at a client domain or any listed client-side contact marks a meeting; the union is synced. Each meeting becomes a notes doc (title, attendees with emails, private notes, AI summary) plus a threaded transcript ("transcripts": falseto skip). Meetings sync once they aresettle_hoursold (default 1) so the summary exists. Granola allows roughly one transcript fetch every two minutes per account, so a run syncs notes for every meeting first (cheap; the cursor advances) and then spends at mosttranscript_seconds(default 300) on the transcripts it still owes, oldest first, carrying the rest in the cursor for later runs. Transcript calls are paced across every client in a run, the gap widens each time Granola says slow down, and a transcript already in the stream is never fetched twice. A backfill's summaries land in the first run and its transcripts trickle in over the following hours. Meeting content is evidence for extraction, never authoritative work or facts. - Notion reads pages and databases the integration has been connected to.
Scope a client's docs with
roots— page or database ids or URLs; anything beneath one is in — or leave it empty for everything shared. Each edit becomes a stream doc with the page rendered to markdown (title, database properties, blocks), so documentation history is grep-able and folded like everything else. Pages edited in the lastsettle_minutes(default 30) wait for the next run. - Jira Cloud syncs issues and comments, rendering Atlassian Document
Format to markdown, and maintains
context/work/jira/<KEY>.yaml— the live issue table with Jira's status and status category (state: openunless the category is Done), seeded with every unresolved issue on the first sync. Scope by project keys (projects) or, for workspaces that run clients as boards inside one project, by board ids (boards; a board resolves to its saved filter's JQL —lore setup --jira board:293).siteis the Atlassian URL for permalinks and, without a proxy, the API. - Figma syncs the design as evidence: for each configured file (design
file or FigJam board; the REST API does not serve Slides decks — export
those to PDF and
lore doc addthem) one doc per top-level frame per page — its text layers in reading order under nested-frame headings, component instances by name, a deep link to the node — re-emitted only when that frame's content changes; an index of pages and frames per file version; and design comments as threaded docs anchored to the frame they were left on. So an agent can check a request or ticket against what the design actually says and link the screen. It is design intent, not the shipped UI. Scope byfiles(URLs or keys) and/orprojects; auth is a personal access token astoken(env:FIGMA_TOKEN, sent as X-Figma-Token) or anapi_baseproxy that injects it. - Gmail reads the client's email out of your team's inboxes — for the
client who doesn't use Slack. Documents the client links (Google Docs,
Sheets, Slides, uploaded Word/PowerPoint/Excel, Drive-hosted PDFs) or
attaches (PDF, Word, PowerPoint, Excel, text) are read too — Drive as the
mailbox owner, attachments from Gmail — and filed into the
docsstream dated and attributed like the email, so the content is in memory, not just the link. A link the owner cannot open is logged, never fatal; folders, images and Figma links stay links.documents: falseon the source opts out. Mailboxes default to theteam-side contacts in theclientblock;usersnames them, orusers: "all"reads every active mailbox in the Workspace (listed through the Directory API asadmin, defaultclient.owner), so a teammate the client emails for the first time is covered with no config change.excludeis an opt-out either way. In each mailbox it searches for mail from/to/cc the client's domains or client-side contacts, so internal mail never matches. A thread that landed in four inboxes is one doc, keyed on its Message-ID (meta.mailboxessays who had it); replies chain onReferences. Quoted history and signatures are trimmed. Setup:--gmail(team contacts),--gmail all, or--gmail "a@x.com,b@x.com". - Documents (
docs) is the stream with no connector: the spec, brief, deck, or handoff package a client sends by whatever means.lore doc add <link-or-file>puts it undercontext/streams/docs/<title-slug>/as one stream doc — the sender as author, the link as permalink — so it is grep-able at once and the next fold reads it exactly like a day of email: requirements a spec states become requests from whoever sent it, a plan in a brief becomes roadmap, every item citing the document. Nothing in a document is authoritative (that is what an SOW is for). Google Docs, Slides, Sheets, and Drive-hosted PDFs or text are read through the Workspace service account asclient.owner(--asto read as someone else), so nothing needs to be link-shareable; local.md,.txt, and.pdffiles work too. Re-adding identical content is a no-op; a changed document lands as a new version in the same channel. A 400-page spec is fine: the fold splits it across batches and the weekly report only sees its opening. Every add is audited, committed, and pushed.
Sources are independent: one that fails doesn't hold up the rest. The
others sync, commit and fold as usual; the failed source keeps its old
cursor, resumes from there on a later run, and the fold — a content delta —
picks up the catch-up material whenever it lands. A source with no
connector or an unresolved env: ref fails that source, not the run. To
keep a source configured but skipped entirely, set "disabled": true on it.
What replaces the old all-or-nothing rule is saying so. Per-source health
lives in state.json and surfaces everywhere freshness does: lore check
and lore source list for a human, the host page's client table, and
recall — whose synced.sources classifies each source ok, stale
(synced before, currently failing, with how many hours of gap), never, or
disabled, and whose synced.degraded names the ones an answer must be
hedged on. A run-level "synced 2 minutes ago" can hide a source that has
been down for days; the per-source layer is what keeps an agent from
presenting partial memory as complete. backfill seeds the first sync N months back (per-source
overrides supported); everything after is forward-incremental.
Who the client is. Every context repo carries a client block — name,
email domains, known contacts with side: client|team|vendor (and
aliases for a person's other addresses, such as a personal gmail, matched
like the primary email by Gmail and Granola), and owner:
the team-side account lead, the Workspace user the service account acts as
for Gmail directory listing and Google Doc export (asked once by lore setup, saved as owner in ~/.lore/config.json, --owner overrides).
Email is the identity key; names are display. Connectors use it to scope material
(Granola matches attendees, Gmail matches correspondents and picks
mailboxes), extract uses it to tell client asks from team decisions, and
recall returns it so an agent knows who it is talking about.
"client": {
"name": "Acme",
"domains": ["acme.com"],
"contacts": [{ "name": "Priya Patel", "email": "priya@acme.com", "role": "Product owner" }]
}Manual setup (what the wizard does, by hand)
lore init in a fresh repo scaffolds lore.json, context/, AGENTS.md,
and .github/workflows/lore-sync.yml. Edit the config, then sync and push:
{
"project": "acme",
"sources": {
"slack": { "channels": ["#acme", "#acme-dev"], "token": "env:SLACK_TOKEN" }
},
"backfill": { "months": 3 },
"extract": ["requests", "decisions", "roadmap", "weekly-report"]
}lore check && lore sync
git add -A && git commit -m "lore: first sync" && git push
gh secret set SLACK_TOKEN --repo your-org/lore-acme # GitHub-hosted onlyOptional "write": { "allow": ["shawn", "priya"] } restricts who may
remember. The actor is the OS user (or --by on the CLI; MCP callers can
never name one). It is an honesty check, not authentication — anyone who can
push to the context repo can bypass it.
extract picks its LLM backend automatically: with ANTHROPIC_API_KEY (or
ANTHROPIC_AUTH_TOKEN) set it calls the Claude API, billed per token;
otherwise it shells out to a logged-in claude CLI, which runs on your
Claude subscription — in CI, set a CLAUDE_CODE_OAUTH_TOKEN secret instead
of an API key (Max plans: claude setup-token). Override with
LORE_LLM=sdk|cli. The fold returns a delta (new or changed items only;
everything else is kept verbatim), so an incremental fold is small and
never drops an existing item. A first fold or multi-batch re-fold uses
LORE_MODEL (default claude-opus-4-8); the routine one-batch fold onto
existing artifacts uses LORE_MODEL_INCREMENTAL (default claude-sonnet-5).
From a linked repo:
lore grep "black friday" # search everything
lore grep -i --channel acme-dev "deploy" # case-insensitive, one channel
lore recall decisions # pinned facts + derived, one category
lore recall reports # the latest weekly reports
lore recall work # the lore tracker (open tickets, who moved each and why) + Jira/GitHub snapshots
lore work list # open tickets in rank order
lore work move CAR-3 done --reason "Cory shipped it Friday" --source https://slack.com/…
lore remember "client wants launch before Black Friday" -c decisions
lore refresh --trigger --fold # self-hosted: sync + fold right now, wait for itFrom anywhere — ~/.lore/registry.json learns project names as you use them:
lore grep -p acme "launch date"
lore --context your-org/lore-acme recallReads pull the cache first (--no-pull to skip). remember commits and
pushes the pin immediately — a fact that only exists in a local cache isn't
remembered, it's misplaced. Resolution order: --context flag → nearest
lore.json walking up from cwd → -p/--project via the registry. The cache
is disposable; delete ~/.lore/cache/ any time.
refresh pulls what the host already has. On a self-hosted setup,
--trigger runs the host's sync unit over SSH and waits (about a minute);
--fold runs the timer's sync + LLM fold unit instead, so derived artifacts
update too. Both are rate-limited to once per 5 minutes (--force), and a
run already in flight is waited out rather than restarted.
The roadmap describes overarching short- and long-term outcomes, why they
matter, what success means, and how they influence work priorities. It can
span several epics or tickets. lore recall roadmap and the MCP roadmap
category return plain English, with descriptions of supporting work followed
by ticket keys in brackets. --json retains structured access.
Goals live in context/derived/roadmap.yaml; concrete delivery work lives in
the tracker. The fold can propose evidence-backed priority changes in support
of a goal, with a reason explaining the tradeoff. It never automatically copies
a goal's priority to every ticket. Older task-shaped roadmap entries are kept
for history and classified separately as the fold reviews them. See
Roadmap goals for the fields and migration behavior.
Every project has a lore tracker, context/work/lore/<PREFIX>.yaml
(work.prefix in lore.json, derived from the client name: CareMobi →
CAR-1). It is the answer to "what is open, in progress, blocked, done" —
for clients with Jira or GitHub Issues and for clients with nothing.
- Mirrored on sync. Each open actionable Jira/GitHub issue becomes a ticket with
externalpointing at it; when the tracker moves an issue, sync records the move in the ticket's history and lore's status follows. Closed issues lore never tracked are not imported. Jira epics stay planning evidence; their child tasks import normally. PRs stay delivery evidence. - Reviewed by the fold.
lore extractsees the tracker next to the new material and may move, reprioritise, or re-rank a ticket when the evidence is unambiguous — a merged PR, "shipped", "blocked on X" — and may open a ticket for work that is committed but not yet tracked: the team agreed to it, someone is doing it, it is scheduled, or it is a signed-off concrete action supporting a goal. New tickets keep the request they came from (request: req-0007). Only with high confidence and a cited source; never toarchivedand never borndone; never over a person's more recent call on the same field; never a second ticket for a request already tracked or a title that reads like an existing one (Jira and GitHub issues are mirrored in, so the fold sees them). Skipped proposals are logged.lore extract --reviewruns one fold with no new material, to apply the current rules to what memory already holds. Where lore and the external tracker disagree, recall marks the ticketdrift: true— whatlore work pushwrites back. - Moved by people and agents with
lore workor thelore_work_*MCP tools. A reason is required on every move; it is the paper trail. - On the web board — list and kanban views at
/boardon the lore host, for your team and the client's people. Email one-time-code sign-in, per project, members edit and viewers look (below).
Related PRs and delivery reconciliation. Sync attaches PRs that explicitly
reference a Lore key, a linked tracker issue, or a PR URL on the ticket, and
refreshes their draft/open/merged/closed state. Related PRs live separately
from the editable description in related_prs; they appear on the board,
in lore work show, MCP recall, and the status page. An issue number such as
#42 is matched only within its repository; cross-repository references
must include the repository or URL.
With TYPESAFE_API_KEY set, each extract also uses Jev to match open tickets
against open PRs and PRs updated within the past 90 days in the configured
repositories. Relevance and whole-task coverage are evaluated separately;
relationships scoring at least 0.85 relevance are retained. Explicit and
already-linked PRs remain eligible regardless of age. Results, including
negative matches, are cached by the ticket requirements, PR evidence and
model in context/delivery/matches.json. Each extract evaluates at most 240
uncached pairs; later runs automatically continue the catch-up. API failures
keep known links and retry on a later extract. LORE_PR_MATCHING=off disables
new Jev evaluations.
Changed relationships create delivery stream events for extraction. The fold reads ticket descriptions, related PR states, coverage judgments and PR descriptions, then applies its existing cited, high-confidence status-change rules. A related or merged PR alone never automatically completes a ticket; partial implementation and outstanding acceptance requirements remain for the fold to assess. This runs even when there are no other new streams, so previously missed PRs can be recovered without a manual review queue. On upgrade, GitHub sync refreshes the past 90 days and all open issues/PRs once to populate descriptions in old tables.
lore work add "Caregiver onboarding email sequence" --priority P1 --source https://slack.com/archives/…
lore work promote req-0007 # a derived request becomes a ticket, keeping its evidence
lore work move CAR-2 in_progress --reason "Cory started the draft" --source https://slack.com/…
lore work set CAR-2 --assignee cory --external jira:INPT-9 --reason "same work as the Jira story"
lore work rank CAR-3 --top --reason "report is due Friday"
lore work list --all
lore status # what's outstanding: the fold's summary + the live open work (what lore_status returns)
lore work label CAR-2 CAR-5 --add "stripe integration" --reason "group the Stripe work"
lore work list --label "stripe integration" # one theme's open tickets (recall --label: open and closed)
lore work show CAR-2 # the ticket with its full historyStatuses: todo, in_progress, blocked, done, archived. Priorities
P1–P3; file order is rank. A ticket may carry a markdown description
(--description), shown on the board. Tickets deliberately carry no estimates or
SOW weeks — they are delivery tracking, not capacity burn.
The board. An optional web view over the tracker, served by lore www
on the host when LORE_BOARD=1: a kanban (drag between columns and within
one — a move and a rank, as if from lore work) and a sortable, filterable
list, with a ticket drawer for editing fields and the description and
reading the full history. It is off per project until turned on:
lore board enable --context lore-acme
lore board add jane@acme.com --context lore-acme # member: create, edit, move
lore board add @acme.com --viewer --context lore-acme # everyone at the domain, read-only
lore board remove jane@acme.com --context lore-acme # their session stops working at once
lore board show --context lore-acmePeople sign in with an emailed 6-digit code and stay signed in for 90 days.
The server caches parsed board configs and tickets in memory, checking each
file's Git blob ID on every read. Ticket edits and access changes take effect
immediately; unchanged files stay cached across sync commits. The cache holds
at most 128 files and 16 MiB of source data (parsed heap usage may be larger),
evicting the least recently used entries. No Redis or extra service is needed.
Host admins (LORE_BOARD_ADMINS) are members of every enabled board. Every
board change is a history entry via: web signed with the person's email,
committed and pushed like any other write — the fold treats it as a human
decision. Setup on the host: docs/DEPLOY_EXE.md §8.
Attachments and comments. A ticket's drawer on the board shows its
files — screenshots, recordings, PDFs, dropped, pasted or picked on the
board, or imported by lore sync from the linked Jira, GitHub or Linear
issue or a Slack message cited in the ticket's sources — and one comment thread: comments made on the board (or with
lore work comment CAR-3 "…", or an agent's lore_work_comment) together
with the linked issue's own comments. Board comments are stream docs
(context/streams/board/<KEY>/), so they are searchable and the fold reads
them; nothing is posted back to the tracker. Files are content-addressed:
git holds only context/attachments.yaml (name, type, size, sha256, where
it came from); the bytes live in the asset store — Cloudflare R2 behind a
small Worker, with a cache on the host (docs/DEPLOY_EXE.md §9). Sync imports
files on open linked tickets only, up to 100 MB each; a larger one is
recorded as a link to where it lives.
Slack sync retains file names and types in the message stream, including
file-only messages and replies, and keeps download metadata in
context/source-files/slack.yaml. Files from each exact cited message
are imported onto open tickets; unrelated messages in its thread are not
automatically attached. Newly extracted tickets import those files in the
same extraction run. This attaches the images for people to view on the
board; it does not add image understanding to the extraction model.
The Slack app needs files:read (reinstall existing apps after adding the
scope). Proxy-only setups also need sources.slack.files_base targeting
https://files.slack.com with the workspace token injected; see
docs/DEPLOY_EXE.md §9. Older messages need to be re-read by sync to collect
their file metadata: increase overlap_days within the configured backfill
window, or reset the channel cursor for a backfill, including old replies.
Existing cited tickets then pick up their attachments on sync without
recreating the tickets.
Pushing to Jira. Lore's state flows back out only when someone asks:
lore work push --all --dry-run # what would change in Jira, changing nothing
lore work push JNT-3 JNT-7 # these tickets
lore work push --all # every ticket that differs from JiraA ticket linked to a Jira issue whose status disagrees with lore's gets the
matching workflow transition (chosen by status category, so "In Review"
still counts as in progress and a "Blocked" transition is used when the
workflow has one). An open ticket with no Jira issue gets one created in the
client's project and scoped onto their board — the board filter's clauses
("Client Project" = Jointly) are resolved to the create screen's field and
option ids — then linked back as external. sources.jira.push in
lore.json overrides the project, issue type, and scoping fields when
inference cannot. Archived tickets and done tickets without an issue are
never pushed. Every push is a history entry on the ticket and an audit line.
Sprints stay in Jira. Lore's tickets carry no sprint — Jira owns sprint
planning — but a push respects it. A ticket that is in flight in lore
(in_progress or blocked) and sits in no open sprint goes into the
board's active sprint; one the client already planned into a sprint is left
there, and with no active sprint (between sprints) it stays in the backlog
and the push says so. When the client asks for specific work in a sprint:
lore work push JNT-3 JNT-7 --sprint active # the board's current sprint
lore work push JNT-9 --sprint "Sprint 15" # a named sprint, active or futureThe board is sources.jira.push.board, else the first of boards, else
the project's only scrum board; kanban boards have no sprints, and
push.sprints: false turns this off for a client. Sync mirrors each Jira
issue's current sprint onto Jira's own table (sprint, sprint_state) and
into its stream docs, so recall and the fold can say "JNT-12 is in Sprint 14".
Jira is reachable only from the lore host, so from a laptop or agent the
command runs itself there over SSH (like lore refresh --trigger) and pulls
the result back. The timer never pushes.
Load counts retained PR openings, emails, meetings, board changes, and assignment transitions, with evidence and project-specific actor roles. It separates human, agent, service, and unknown accounts. These are activity counts, not hours or capacity. Coverage gaps and ambiguous historical board changes remain visible. An optional semantic stage also tracks decisions, customer requests, and human commitments with quoted evidence and separate accepted/candidate counts.
lore load rebuild --preview --json
lore load rebuild
lore load --from 2026-09-01 --to 2026-10-01 --type pr.created --group-by role
lore load events --type meeting.held --role technical_lead --participation attendee
lore load extract --max-batches 1
lore load events --type decision.made --method inferred --json
lore load --type ticket.completedConfigure load.actors and load.timezone in lore.json; load.enabled: true
enables rebuilding after self-hosted sync, independently of LLM extraction.
load.semantic.enabled: true enables bounded semantic batches during scheduled
extraction; set load.semantic.from to choose a historical starting date.
With TYPESAFE_API_KEY, load.semantic.gate: {"mode":"shadow","threshold":0.3}
measures Jev's event screening without skipping evidence. After validating a
threshold, mode: "filter" lets clearly low-signal passages bypass Claude;
uncertain passages, long documents and provider failures still reach extraction.
Scores are cached by content/model/classifier version, and disabling or changing
the gate reopens its skipped checkpoints. Ordinary artifact extraction does
not use this filter. Load also records separate Jev and
Claude usage metrics; see the evaluation and configuration details below.
Commit inputs/config before rebuilding in a Git context. CLI and the read-only
lore_load MCP tool query the same cached projection. See
Load configuration, counting rules, and roadmap.
An SOW in lore is a capacity commitment: how many human-weeks were sold,
over what period, sometimes with a few named scope items. It is the one
document that is authoritative about what was agreed, so it gets its own
layer, context/sow/<slug>.md, written only by a human (or an agent on
explicit instruction), never by sync or the fold.
lore sow add "https://docs.google.com/document/d/…" --name "Jointly SOW 4" --weeks 12 \
--start 2026-09-01 --signed 2026-08-28 \
--source "https://docs.google.com/document/d/…" --scope "Agreement builder v2; Onboarding"
lore sow listThe document can be a Google Doc link — lore exports it as Markdown
through the Workspace service account (the Gmail one, with
drive.readonly added to its domain-wide delegation and the Drive API
enabled in its Cloud project), reading as client.owner or --as <teammate>, so contracts never need to be link-shareable — or a Markdown,
text, or PDF file. A machine without the key hands the link to the lore host
over SSH. The body is scrubbed like every stream doc and, by
default, stripped of lines carrying currency amounts — the commitment lore
needs is in weeks, and the repo is readable by every agent pointed at it
(--keep-commercials to keep them). Re-adding the same name updates it;
--status exhausted|superseded|closed retires it. Every add commits, pushes,
and lands in the audit log.
lore recall (and lore_recall) then return a sow layer: each SOW's
weeks sold, effective date, status, and scope. Nothing is derived from the
calendar — the team measures a commitment by the weeks allocated against
it, and that will come from the scheduling source as a work table next to
this layer. The fold sees the active SOWs and notes when a request falls
outside named scope; the weekly report gets a Budget line restating the
figures. Agents
attach one with lore_sow_add, passing the Google Doc link (or the text
they read) plus the numbers the user or the document states.
lore doc add "https://docs.google.com/document/d/…" # title, author, link from Drive
lore doc add ~/Downloads/alpha-gtm-brief.pdf --from "Matthew" --date 2026-09-09
lore doc add "https://docs.google.com/presentation/d/…" --title "Vision deck"
lore doc listA document is raw material, not a layer: it lands in the docs stream (see
Sources) and from there behaves like anything else lore synced —
lore grep finds it immediately, the next lore extract folds it into
requests, decisions, and roadmap citing the document's link, and the weekly
report mentions that it arrived. Agents add one with lore_doc_add, passing
the Google link (or the text they read plus a title). When a document is
the commitment — weeks sold over a period — use lore sow add instead.
docs/PLAYBOOK.md is the step-by-step: the facts to
gather, the human steps per source, one lore setup command, first sync,
verify, point an agent at it. A self-hosted lore host serves the same
playbook plus a live status table of every client at its HTTPS URL
(lore www, installed by deploy/exe/setup.sh). The lore-onboard skill
in the plugin walks an agent through it: say "set up lore for Acme".
lore archive --context inputlogic/lore-acme # or -p acme, or from inside the repo
lore archive --restore --context inputlogic/lore-acmearchive marks the context repo lifecycle: archived in lore.json,
commits and pushes, archives the GitHub repo when there is one (read-only,
history kept, daily cron stops), and drops the local cache clone and
registry entry. From then on sync and extract are no-ops, remember is
refused, and every read — CLI and MCP — is labelled ARCHIVED with the date,
so an agent never presents old history as current state. The repo is never
deleted: it is the only copy of the synced history and the pins.
--restore reopens it. What stays manual: unlinking any project repos, and
removing @lore from the Slack channels.
GitHub Projects v2 can project Lore tickets onto a board, with two-way status,
title, and description sync. Add an explicit mapping to the context's lore.json:
{
"sources": {
"github_projects": [{
"owner": "inputlogic",
"number": 8,
"status_field": "Status",
"status_map": {
"Todo": "todo",
"In Progress": "in_progress",
"In Review": "review",
"Done": "done"
}
}]
}
}The host uses LORE_GITHUB_TOKEN directly. It needs Projects read/write and
permission to edit linked repository issues (or PRs). Each project may override
token with an env:VARIABLE reference. Organization and personal projects are
supported; the repository-scoped aggregate GitHub proxy cannot serve this source.
Run lore check, then lore sync, or lore project sync inputlogic/8.
Items linked to an already-tracked GitHub issue auto-link to that Lore ticket.
Existing issue references and ticket IDs stay intact; an additive project_items
list holds each board link and its last successful sync values. On first link,
Lore supplies the title and status, and a missing description is filled from
GitHub. Later project-only edits flow into Lore. Lore-only edits flow back
immediately from MCP, CLI, board, or fold changes. If both sides change a field,
Lore wins and state.json.conflicts records the drift.
lore project item add inputlogic/8 "Investigate retry" --body "Steps to reproduce" --status todo
lore project item add inputlogic/8 "Existing ticket" --link-work COI-14
lore project item move COI-14 review --reason "Ready for review"Agents use lore_project_item_add and lore_project_item_move. lore_work_add
also accepts project: {owner, number, column?}. An add creates a draft unless
the linked Lore row already references a GitHub issue. Assignees stay read-only
metadata. Removing an item from a board unlinks it and keeps the Lore ticket.
Unknown columns remain status_raw metadata; a Lore status without a configured
column fails visibly on write-back.
GitHub and git cannot commit atomically. On a partial failure, Lore keeps the
pending change and successful ledger fields for the next sync to retry. An
interrupted add reports its work key: retry with link_work to reuse the item.
Changes are stored per day in context/streams/github-projects/; current
snapshots live in context/projects/github/. See
the implementation notes for schema details and
release validation. Configure mappings in lore.json; lore source add does not
infer a mapping.
From your own machine (Claude Code, no SSH). With the board on, the
host's MCP endpoint also takes OAuth: run the command the board's Connect
Claude Code page shows (claude mcp add --transport http lore-acme https://<host>/mcp/lore-acme), then /mcp in Claude Code to sign in — a
browser opens, you sign in with the board's email code and approve. Access
follows your board role on that project: members get every tool (writes are
attributed to your email), viewers the read tools only; being removed from
a board ends it. Connected apps are listed, and revocable, on the same page.
Agent VMs keep using the peer integration (§6 of docs/DEPLOY_EXE.md).
lore mcp serves the query surface over stdio: lore_grep, lore_read,
lore_recall, lore_sync_now, lore_source_list, lore_remember,
lore_sow_add, lore_doc_add, lore_source_add (widen what is synced — a
repo, channel, folder, page, design file, board, or mailbox — on explicit
instruction; identifiers only, credentials stay on the host), and the tracker
verbs lore_work_add, lore_work_promote, lore_work_move, lore_work_set
(each requires a reason), and lore_work_push (write ticket state to Jira,
on request only). lore_recall
returns exactly what the CLI does — pins, every derived artifact, the lore
tracker and the external work tables, recent reports, and sync/extract
timestamps so an agent can say how fresh its answer is. lore_sync_now is lore refresh: it pulls, optionally
triggers the host's sync (and fold) and waits, and reports before/after
freshness — agents never run lore sync themselves.
lore_source_add is the scope half of onboarding a source: it points one of
the built-in connectors at more of the client — another Slack channel, a repo
on a client that had only Figma, a Notion page — and the new scope backfills
on the next sync. Connector kinds stay in code; an agent chooses what of a
vendor belongs to the client, never invents a new kind of source. The
credential and consent half stays human, and the result says what is still
owed (/invite @lore, a host integration, sharing the Notion page). Because
sync fails a whole client on an unusable source, a newly configured source
whose credentials don't resolve is written "disabled": true rather than
live — it can never take a client's sync down. In a linked repo with
Claude Code:
claude mcp add lore -- lore mcpAny MCP client, pinned to a project:
{ "mcpServers": { "lore": { "command": "lore", "args": ["mcp", "-p", "acme"] } } }LORE_CONTEXT=<owner/repo or path> in the server's environment does the same
as --context.
This repo is a Claude Code plugin marketplace. One install gives an agent the
lore MCP server and the two skills that teach it to use lore well:
/plugin marketplace add nerdburn/lore
/plugin install lore@lore
(claude plugin install lore@lore --scope project pins it for a whole team
via .claude/settings.json.) The plugin lives in
plugins/lore/ and needs the lore CLI on the PATH.
plugins/lore/skills/lore-mcp/SKILL.md
teaches an agent to use the memory: the trust order (pins → work tables →
derived → reports → raw streams), when to recall versus grep, that the
GitHub and Jira work tables are authoritative for delivery state and
meetings are evidence rather than decisions, when to refresh (and to fold
before answering status questions), to cite sources and report freshness,
and to pin facts only on explicit user instruction — plus how to connect
the server when it isn't already.
plugins/lore/skills/lore-onboard/SKILL.md
teaches the whole onboarding: check the hosting mode, run lore setup --yes
with flags, relay the human steps, verify with a real query before declaring
success — plus the rules an agent must not relax.
plugins/lore/evals/ holds behavioural evals for
the skill against a synthetic fixture client (answers from the work table,
cites sources, never pins uninvited); run them with
claude plugin eval ./plugins/lore — see its README.
| Command | What it does |
|---|---|
lore setup [owner/repo] [--channels s] [--github repos] [--granola folders] [--notion roots] [--jira keys|board:id --jira-site url] [--gmail [mailboxes]] [--figma files] [--client n] [--domains d] [--backfill n] [--org o] [-y] |
wizard: create + scaffold + push a context repo, secrets (GitHub mode), first sync, link cwd |
lore link <owner/repo> |
point a project repo at its context repo (or a bare name when remote is configured) |
lore refresh [--trigger] [--fold] [--force] |
pull the latest memory; --trigger asks the self-hosted host to sync now and waits (~1 min); --fold also runs the LLM fold so derived artifacts update (1–2 min more); --force overrides the 5-minute rate limit |
lore auth granola [--file p] |
OAuth device-code flow; saves a self-refreshing grant for the Granola connector |
lore archive [--restore] [--keep-local] |
end (or reopen) an engagement: lifecycle flag, GitHub archive, local cleanup |
lore grep <pattern> [-i] [--channel s] [--limit n] [--json] |
search streams + facts + derived |
lore recall [category] [--json] |
pinned facts + derived artifacts + the lore tracker + external work tables |
lore work add <title> [--status s] [--priority p] [--assignee who] [--labels l] [--source urls] [--external ref] [--reason why] [--by who] |
add a ticket (--external jira:KEY / github:owner/repo#n links an existing issue) |
lore work promote <req-id> [--title t] [--priority p] [--reason why] |
a derived request becomes a ticket, keeping its evidence |
lore work move <key> <status> --reason why [--source urls] |
change status (todo, in_progress, blocked, done, archived) |
lore work set <key> [--title] [--priority] [--assignee] [--labels] [--source] [--external] --reason why |
change fields or link a tracker issue |
lore work rank <key> (--above key | --top | --bottom) --reason why |
move in the priority order |
lore work list [--all] [--json] / lore work show <key> [--json] |
open tickets in rank order / one ticket with its history |
lore work push [keys...] [--all] [--dry-run] [--json] [--by who] |
write lore's ticket state to Jira: transition linked issues, create issues for open unlinked tickets; runs on the host |
lore remember <fact> [-c cat] [--by who] [--source url] |
pin a fact; pushes immediately in pointer mode |
lore sow add <file-or-gdoc-link> --name n --weeks n --start d [--end d] [--signed d] [--source url] [--scope items] [--status s] [--as email] [--by who] [--keep-commercials] |
attach a statement of work (Google Doc link, .md/.txt/.pdf): weeks sold over a period; commits + pushes |
lore sow list [--json] |
attached SOWs |
lore doc add <file-or-google-link> [--title t] [--from who] [--date d] [--source url] [--as email] [--by who] |
add a document (Google Doc/Slides/Sheet, Drive PDF/text, or local .md/.txt/.pdf) to the docs stream — raw material, folded like email; commits + pushes |
lore doc list [--json] |
documents in the docs stream |
lore source add <kind> [scope...] [--site url] [--by who] |
add a source or widen its scope on an existing client — github owner/repo, slack "#chan", granola "Folder", notion <url>, figma <url>, jira KEY|board:id, gmail [emails|all]; identifiers only (proxy api_base from ~/.lore/config.json or env: refs), validated, audited, commits + pushes; the host backfills the new scope; prints the human steps left |
lore source list [--json] |
configured sources with scope, auth mode, and freshness (ok / stale <age> / never / disabled) with last success and last error |
lore gdoc export <url> --as <email> [--json] |
export a Google Doc (Slides, Sheet, Drive-hosted PDF/text) as text via the service account (used by sow add / doc add on the host) |
lore mcp |
MCP server over stdio |
lore sync |
pull new docs into context/streams/ (run in the context repo; new channels backfill automatically; a failed source doesn't stop the others, but exits non-zero if any enabled source failed) |
lore extract [--report] [--review] |
LLM fold: streams → derived artifacts + weekly report (API key, or a Claude subscription via the claude CLI) |
lore run-all --repos d --work d [--extract] [--report] [--concurrency n] |
self-hosted scheduler: sync every bare repo under a dir (n at a time, default 3), commit, push, then fold if --extract (from a timer on the host; a sync-only run may overlap a fold — see docs/DEPLOY_EXE.md) |
lore www --repos d [--port 8000] [--board] |
serve the onboarding playbook + live client status (self-hosted host page), the hosted MCP endpoint, and with --board/LORE_BOARD=1 the web board |
lore board enable|disable|add|remove|show |
per project: turn the web board on, and say who may sign in (--viewer for read-only) |
lore init |
scaffold a context repo by hand |
lore check |
validate config, connectors, env refs; print per-source sync health |
lore manifest slack |
print the bundled Slack app manifest |
grep, recall, remember, sow, doc, source, refresh, archive, and mcp all take
--context <owner/repo>, -p/--project <name>, and --no-pull.
✗ slack: channel #x not found or bot not a member— the sync fails (non-zero exit, nothing committed) until fixed:/invite @lore, or check the exact channel name in Slack (hyphens!). Other channels still synced and their cursors were kept.✗ <source>: no such connector— a source is configured that lore doesn't know; fix the key or set"disabled": true.✗ github: repo acme/x not found or token lacks access— the token's repository access doesn't include it, or the name is wrong. Other repos still synced.✗ granola: folder "Acme" not found— folder titles are matched case-insensitively againstlist_meeting_folders; pass the folder id instead if the title is ambiguous.- Notion syncs nothing for a root — the page isn't shared with the integration yet (page ··· → Connections), the id/URL is wrong, or it was edited in the last 30 minutes and is still settling. A
notion 404on the root means the first. ✗ jira: no credentials …/jira 401—email+token(or the proxy's Basic header) don't resolve or don't matchsite; a board id must belong to a project the account can see.✗ gmail: mailbox x@…: token for x@…: unauthorized_client— domain-wide delegation isn't granted for the service account's client id (Workspace Admin → Security → API controls → Domain-wide delegation, scopehttps://www.googleapis.com/auth/gmail.readonly), or that address isn't a user in the domain. Other mailboxes still synced.gmail: no service account key— put the key JSON at~/.lore/gmail-sa.jsonon the syncing host, or setkey_file/key: env:GMAIL_SA_KEY.gmail: listing Workspace users as x@… failed—users: "all"needs thehttps://www.googleapis.com/auth/admin.directory.user.readonlyscope in the same domain-wide delegation entry, andadmin(defaultclient.owner) must be a Workspace admin."<user>" is not in lore.json write.allow— pins are restricted; runlore remember --by <allowed-name>or editwrite.allow.refresh: host … skipped-recent— the host synced (or folded) within the last 5 minutes;--forceto run anyway.- Re-backfill one channel — delete that channel's cursor from
state.jsonandlore sync; it refetches its backfill window and dedupes. (Channels newly added tolore.jsonbackfill automatically.) - No "lore sync" workflow in the Actions tab (GitHub-hosted) — GitHub sometimes misses workflows pushed in the repo-creating commit;
setupnudges automatically, otherwise push any commit touching the file. - Scheduled syncs stopped after ~2 months of quiet (GitHub-hosted) — GitHub disables cron on inactive repos; re-enable from the Actions tab.
could not pull … using cached copy— offline or no read access; queries serve the cache. Delete~/.lore/cache/<owner>__<repo>to force a fresh clone.rememberfailed to push — the pin is committed in the cache clone; fix access andgit -C ~/.lore/cache/<owner>__<repo> push.
npm install
npm test # node:test via tsx — fixtures only, no credentials needed
npm run typecheck
npm run build
npm run build:web # the board SPA (web/, Vite + React + HeroUI) → web/distThe board UI lives in web/ with its own dependencies, so the CLI install
never pulls in React. For UI work, run lore www --repos <dir> --board on
:8000 and npm --prefix web run dev — Vite proxies /api to it. Without
LORE_BOARD_EMAIL_FROM/LORE_BOARD_EMAIL_API, sign-in codes are printed
to the server log instead of emailed.
CI (.github/workflows/ci.yml) runs the same three on every push and PR.
Tests cover config validation (typed per-source schemas), the secrets
scrubber, stream writing and dedup, every connector against a fake API
(Slack, GitHub, Granola via captured MCP responses, Notion, Jira, Gmail),
Granola OAuth, sync/check failure handling, context resolution, archive,
recall, every MCP tool over an in-memory transport, remember + audit,
extract output parsing and merging, the self-hosted runner and refresh,
statements of work (add, recall layer, MCP tool, PDF extraction), the
lore www host page, and the board (sign-in, roles, writes over HTTP).
Connectors: Slack, GitHub, Jira, Granola, Notion, Gmail, Figma — GitHub and Jira
each with a source-owned work table, mirrored into the lore work tracker
(the tracker of record, with per-ticket history; moved by people, agents,
sync, and the fold). extract (delta fold into
requests/decisions/roadmap + tracker review + weekly report + pin-contradiction audit), the
query surface — grep, recall, remember, refresh, mcp — with
pointer resolution + ~/.lore/cache, two hosting modes (self-hosted timer
or GitHub Actions), a lore setup wizard covering every source, a Claude
Code plugin with skills and evals, client lifecycle (archive), statements of work as a human-owned
commitments layer, fail-safe sync with per-source health, secret scrubbing,
and an audit log. Next (see
docs/IMPLEMENTATION_BACKLOG.md): contact
identity resolution, GitHub issue creation outside Projects,
client-scoped MCP tools, remote MCP.
- Text in git is the source of truth — no binary databases in the repo; any search index is a derived, gitignored cache
- Everything is derived unless explicitly pinned — derived data is a regenerable cache; only pins are irreplaceable
- Sync and extraction never mix — connectors don't call LLMs; extraction doesn't call external APIs
- Provenance is mandatory — every item links back to where it came from
- Pointers, never credentials — the context stores "see 1Password vault X", not secrets