Per-Repo Tenancy¶
What coding learns in a repo belongs to that repo: it is kept next to it in .coding/, optionally versioned in a private <repo>-history repo, and shared with teammates by git. Teams are sets of repos, and they decide what the viewer shows and what an agent is told.
The one idea¶
The repo is the unit of persistence and sharing. Each repo X you work in gets a folder X/.coding/ that holds everything learned there — session logs, observations, digests, insights, the knowledge-graph slice, token-usage measurements. That folder is its own small git repo (X-history), private, and ignored by X itself.

First launch in a repo¶
The first coding in a repo asks once where its learned data should go:

| You answer | What happens |
|---|---|
| Enter | a private bmw.ghe.com/<you>/X-history is created (or reused) and checked out at X/.coding/ |
| a teammate's URL | their X-history is cloned — from now on you share what is learned in X |
| skip | X/.coding/ stays local and untracked; never asked again |
The answer is kept in ~/.coding/repos.yaml.
Sharing¶
coding sync # what each learning repo has to push / pull
coding sync --push # asks, then pushes — nothing else ever pushes
coding sync pull # what every launch does in the background
Commits are automatic (session end, every 30 minutes); pushing is always your call.
Teams¶
A team is a named set of repos. Pick teams in Dashboard → Teams; the knowledge viewer follows that selection. Agents are scoped by the teams of the repo they run in — knowledge from another team is never injected.
Two kinds of data¶
Kept per repo (X/.coding/, shareable) | Kept per machine (~/.coding/data/<scope>/var/, never shared) |
|---|---|
history/ — live session logs (LSL, .jsonl) | the live knowledge store (LevelDB, owned by obs-api) |
kb/knowledge-graph/X.json — entities + relations of project X | the Qdrant vectors used for injection |
kb/observation-export/ — observations, digests, insights of X | the proxy's token DB, raw measurements, run snapshots |
kb/usage/<user_hash>.json — token usage per day and per task | discovery cache, sync state, shared clones |
The knowledge on the per-machine side is a cache: a fresh machine hydrates its store from every .coding/kb/ it can see and re-embeds it. The per-repo side is what travels.
There is no X/.specstory/ any more, and nothing addresses the old path. X/.coding/ is excluded through X/.git/info/exclude — no change to X's tracked .gitignore.
What happened to .specstory/¶
| Was | Is now |
|---|---|
X/.specstory/history/ (session logs, logs/) | X/.coding/history/ — an old real directory is migrated on the next launch; an old .specstory/history symlink is removed |
.specstory/config/redaction-patterns.json, sensitivity topics | config/redaction/ in the coding tools repo — shared by the ETM, the obs-api writers and the LLM proxy's raw-body redaction |
.specstory/config/knowledge-system.json | config/knowledge-system.json in the tools repo |
.specstory/trajectory/ | deleted — nothing read it |
Machine-local leftovers (LSL archive, validator report) live in X/.coding/var/, which is git-ignored.
How a repo gets its learning repo¶

- A public remote is refused: learned data contains prompts and code.
- An old layout (a real
.specstory/historydirectory, with or without its own git checkout) is migrated on the next launch without a question: it moves toX/.coding/history/, an existing remote is recorded, and.specstory/is not recreated. - Unattended launches use
LSL_HISTORY_AUTO=yes|no; with nobody to ask, only the local layout is created and the question waits for the next interactive launch.
How sharing works¶

Git is the only transport, and it carries files, not database state:
- obs-api exports each project's slice of the live store to that repo's
kb/knowledge-graph/X.json— sorted, and only rewritten when it changed, so two machines with the same knowledge produce byte-identical files. - Local commits happen at session end and every 30 minutes.
coding sync --pushis the only push. - A teammate's launch pulls. A git merge driver merges concurrent edits of the same graph file by entity — newest
updatedAtwins, relations by(from, type, to), deletions travel as tombstones — so there are no text conflicts in JSON. - obs-api merges what arrived into the live store, embeds the new entities and drops the vectors of deleted ones. The next prompt can be answered from it.
A repo you do not have checked out can still be read: list its X-history remote under a team's repos: and it is cloned (pull-only) under var/shared/X/.
Teams and filtering¶

Entities record the repo they were learned in (metadata.project). Team membership is looked up when reading, never written into the graph — so a repo can sit in several teams and move between them freely.
| Where | Which teams apply |
|---|---|
| Knowledge viewer | the active selection (Dashboard → Teams, or the viewer's Teams rail — kept in sync both ways) |
obs-api ?teams=a,b | the teams named in the request |
| Knowledge injection | the teams the session's repo belongs to; a repo in no team sees only its own project |
The active selection is what you look at; it deliberately does not change what an agent working in some repo is told.
Measurements¶
The LLM proxy stores the project of every call (x-project header, set by the launcher from the repo). obs-api writes this machine's rows for each repo hourly to X/.coding/kb/usage/<user_hash>.json — one file per user, so teammates never conflict.
See Teams & Shared Learning for the dashboard and viewer side.
Decisions behind the design¶
| # | Decision |
|---|---|
| D1 | Learned data lives in a nested checkout <repo>/.coding/ (the <repo>-history repo) with history/ and kb/. There is no <repo>/.specstory/ (T9 dropped the compatibility symlink; launch removes an old one). |
| D3 | Pull automatically at session start, commit locally automatically, push only on confirmation. |
| D6 | One live store per machine — obs-api is the single owner of km-core's LevelDB, one Qdrant. The repo is the unit of persistence and sharing; a team is a set of repos; every filter maps repo → team. No per-team or per-repo live stores. |
| D7 | The scope (~/.coding/scope) names only this machine's runtime home ~/.coding/data/<scope>/. It is not what is persisted or shared. |
Files¶
| Path | Written by | Holds |
|---|---|---|
~/.coding/repos.yaml | launcher (lib/history/repo-link.mjs) | repo path → { remote } \| { skip: true } |
~/.coding/teams.yaml | Dashboard → Teams (lib/teams/config.cjs writeUserTeams) | your teams, memberships, active: |
~/.coding/features.yaml | install.sh, coding-features, Dashboard → Features | the installed tier / feature switches |
<repo>/.coding/history/<yyyy>/<mm>/*.jsonl | the session logger (ETM) | live session logs |
<repo>/.coding/kb/knowledge-graph/<project>.json | obs-api only (lib/kb/layout.mjs, mode owner) | the project's entities, relations, tombstones |
<repo>/.coding/kb/observation-export/*.json | obs-api (ObservationExporter) | per-project slice of the observation cold archive, merged by row id |
<repo>/.coding/kb/usage/<user_hash>.json | obs-api hourly (lib/usage/project-usage.mjs) | this user's token usage for the project, daily + per task |
<data home>/var/projects.json | lib/teams/discover.mjs | discovered repos (TTL 10 min) |
<data home>/var/sync-state.json | coordinator, coding sync | unpushed commits per checkout (the status line's [P:n]) |
<data home>/var/shared/<X>/ | lib/teams/shared.mjs | pull-only clones of teammates' learning repos |
A project with no checkout on this machine is exported to <data home>/kb/knowledge-graph/exports/projects/<project>.json. The container's stores (semantic-analysis) read every file but write only exports/general.json (mode local) — /workspace is mounted read-only.
The launch step¶
scripts/agent-common-setup.sh → ensure_private_history_repo → node lib/history/repo-link.mjs ensure <repo>, then pull_learning_repo (background).
Remote state (remoteState) | Action |
|---|---|
public | refused |
populated | cloneInto — tracked files win, untracked local files are kept |
absent | gh repo create --private (or git init --bare for a local path), push the skeleton |
empty | init + push the skeleton |
unknown (no gh, ls-remote failed) | clone attempted, else init |
Environment: LSL_HISTORY_AUTO=yes|no, LSL_HISTORY_REMOTE_TEMPLATE (with {project}). The default remote is https://bmw.ghe.com/<gh user>/<repo>-history.git (gh's hosts.yml), else derived from the outer repo's bmw.ghe.com origin.
Hydrate is a union merge¶
lib/km-core/src/store/merge.ts, used at startup, on POST /api/kb/reload, by the git merge driver (lib/kb/merge-driver.mjs) and by the embedding backfill:
- entities by id — newest
updatedAtwins (ties: the live graph); - relations by
(from, type, to)— graphology edge keys collide across machines; - deletions as tombstones in
attributes.kmTombstones, kept 90 days — a tombstone beats every copy not edited after it; - a relation to an entity this machine does not have is kept aside and written back to its file, so it is not deleted for the teammate who has both ends.
Files are canonical (sorted) and rewritten only when their content changed. A file that changed under the store (a pull) is merged in before the next write, never overwritten.
Vectors¶
Embeddings are made on write. What arrives by git is embedded by the backfill obs-api runs 60 s after startup and after every reload that changed the graph (lib/kb/embed-index.mjs → dist/embedding/backfill.js, idempotent over content hash and preview version). The same pass deletes the kg_entities points of tombstoned entities by exact id (src/embedding/tombstones.ts). Injection is hybrid — Qdrant plus a keyword search over the live store — so a pulled insight is findable even before it is embedded.
Team configuration¶
| Layer | File | Holds |
|---|---|---|
| 1 | config/teams/<id>.json | ontology + validation (read directly by semantic-analysis) |
| 2 | config/teams.yaml | shipped labels and kinds — no memberships |
| 3 | ~/.coding/teams.yaml | yours: membership, own teams, active: |
| 4 | CODING_TEAMS=a,b | env override of the selection |
teams:
raas:
label: RaaS
kind: team # team | project
repos:
- ~/src/rapid-automations # a local checkout
- https://bmw.ghe.com/me/x-history.git # a learning repo, cloned when not here
- { path: ~/src/x2, id: x } # names the project id stamped there
include: ['rapid-*'] # directory-name globs
active: [raas] # empty / absent = all teams
discovery:
roots: ['~']
depth: 4
A team with neither repos nor include contains the repo whose directory is named like the team — what metadata.team always meant.
Filtering¶
lib/teams/scope.mjs projectsOfTeams(teams) → project ids; matching is case-insensitive on metadata.project, then the legacy metadata.team.
- obs-api —
?teams=a,bon/api/v1/entities,/api/coding/{observations,digests,insights}and/api/coding/lsl/sessions, applied before pagination./api/teamsreturns each team's resolvedprojectsand theactiveselection. - Viewer — one graph fetch, filtered client-side by both canvases, the rail counts and the History sidebar; the LSL strip asks the server. The Teams rail starts from the dashboard's
activeselection and writes changes back; it redraws only when the selection changed. - Injection —
/api/retrievetakesteams/context.teams(hooks forwardCODING_TEAMS), else the teams of the repo atcontext.cwd, else that repo's own project. Qdrant is queried with aprojectmatch.anyfilter on all four collections and every hit is checked again afterwards. Only a session outside every repo is unfiltered.
APIs¶
| Endpoint | Where | Does |
|---|---|---|
GET/PUT /teams, POST /teams/discover, POST /teams/sync | coordinator :3034 | team config, rescan, clone missing shared repos |
/api/teams-config | dashboard :3032 | reverse proxy of the above |
GET /api/teams | obs-api :12436 | teams with resolved projects (for the viewer) |
GET /api/kb/layout | obs-api | which project is written to which file |
POST /api/kb/reload | obs-api | merge changed files into the live store |
Commands¶
coding sync # status of every learning checkout
coding sync pull --repo . # what the launcher runs
coding sync commit # local commit everywhere
coding sync --push [--yes] # asks, then pushes
node lib/history/repo-link.mjs ensure <repo> [--remote <url>] [--no-ask]
curl -s localhost:12436/api/kb/layout | jq
curl -s -X POST localhost:12436/api/kb/reload