Skip to content

Commit 9f432fb

Browse files
authored
Merge pull request #11 from senna-lang/feature/branch-linking
feat: link exchanges to git branches with --branch filter
2 parents de40818 + c0cdbe3 commit 9f432fb

14 files changed

Lines changed: 988 additions & 70 deletions

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,4 @@ build/
2121

2222
# Internal docs (local only)
2323
docs/internal/
24-
25-
# 論文資料(ローカルのみ)
26-
mem/
24+
.strata

AGENTS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# codeatrium — Agent Usage Guide
2+
3+
`codeatrium` is a CLI-first memory layer for AI coding agents. The command is `loci`. It lets agents search past conversations, retrieve code locations (file + line + symbol), and link conversation history to code symbols.
4+
5+
Primary user is **the agent itself**, not a human. The tool is invoked via `loci search "..." --json` from within agent prompts.
6+
7+
## When to use
8+
9+
- When asked "where did we implement X?" or "where is X?"
10+
- When checking if a similar bug was fixed before
11+
- When verifying if a feature already exists
12+
- When looking up the reasoning behind a past design decision
13+
- Before editing code you lack context about — use `loci context --symbol` to review past discussions
14+
- Before refactoring or changing the behavior of a function — use `loci context --symbol` to check past design decisions
15+
- When recalling work done on a specific branch — use `loci context --branch` to find past conversations
16+
17+
## CLI Commands
18+
19+
```bash
20+
loci init # Initialize .codeatrium/ in project root
21+
loci index # Index new .jsonl files
22+
loci distill [--limit N] # Distill queued exchanges via claude --print
23+
loci search "query" --json --limit 5 # Semantic search (agent-facing)
24+
loci search "query" --branch NAME --json # Branch-filtered semantic search
25+
loci context --symbol "Foo.bar" --json # Reverse lookup: code -> past conversations (lightweight; use loci show <verbatim_ref> for full text)
26+
loci context --branch NAME --json # Branch reverse lookup (undistilled exchanges included)
27+
loci show "~/.claude/.../abc.jsonl:ply=42" # Fetch verbatim exchange
28+
loci status # Show index state
29+
loci server start / stop / status # Embedding server management
30+
loci hook install # Register hooks to ~/.claude/settings.json
31+
```

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ loci init # Initialize .codeatrium/ in projec
101101
loci index # Index new .jsonl files
102102
loci distill [--limit N] # Distill queued exchanges via claude --print
103103
loci search "query" --json --limit 5 # Semantic search (agent-facing)
104+
loci search "query" --branch NAME --json # Branch-filtered semantic search
104105
loci context --symbol "Foo.bar" --json # Reverse lookup: code -> past conversations
106+
loci context --branch NAME --json # Branch reverse lookup (undistilled exchanges included)
105107
loci show "~/.claude/.../abc.jsonl:ply=42" # Fetch verbatim exchange
106108
loci status # Show index state
107109
loci server start / stop / status # Embedding server management

src/codeatrium/cli/prime_cmd.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- **Before editing or refactoring a function** — recall past design decisions and known constraints for that symbol.
2020
- **Before starting a new implementation** — check if similar work was done before; reuse decisions and avoid re-debating settled choices.
2121
- **When you encounter a known or recurring error** — search for past fixes; the solution may already be documented.
22+
- **When asked about work on a specific branch** — recall what was done and discussed on that branch.
2223
2324
### Search — semantic query over past conversations
2425
@@ -30,13 +31,16 @@
3031
loci show "<verbatim_ref>" --json
3132
```
3233
33-
### Context — reverse lookup from code symbol to past conversations
34+
### Context — reverse lookup from code symbol or git branch to past conversations
3435
3536
Touching a symbol = recalling memory about that symbol. Before changing any function or class, look up what was decided about it.
3637
3738
```bash
3839
# Retrieve all past conversations that involved this symbol
3940
loci context --symbol "SymbolResolver.extract" --json
41+
42+
# Retrieve past conversations from work on a specific branch
43+
loci context --branch "feature/foo" --json
4044
```\
4145
"""
4246

src/codeatrium/cli/search_cmd.py

Lines changed: 129 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def search(
1212
query: Annotated[str, typer.Argument(help="検索クエリ")],
1313
limit: Annotated[int, typer.Option("--limit", "-n", help="返す件数")] = 5,
1414
json_output: Annotated[bool, typer.Option("--json", help="JSON で出力")] = False,
15+
branch: Annotated[str | None, typer.Option("--branch", "-b", help="ブランチ名で絞り込む(部分一致)")] = None,
1516
) -> None:
1617
"""BM25(V) + HNSW(D) RRF でクエリに近い過去会話を返す"""
1718
from codeatrium.embedder import Embedder
@@ -32,7 +33,7 @@ def search(
3233

3334
embedder = Embedder()
3435
query_vec = embedder.embed(query)
35-
results = search_combined(db, query, query_vec, limit=limit)
36+
results = search_combined(db, query, query_vec, limit=limit, branch=branch)
3637

3738
if not results:
3839
typer.echo("No results found.")
@@ -46,6 +47,7 @@ def search(
4647
"rooms": r.rooms,
4748
"symbols": r.symbols,
4849
"verbatim_ref": r.verbatim_ref,
50+
"git_branch": r.git_branch,
4951
}
5052
for r in results
5153
]
@@ -62,14 +64,17 @@ def search(
6264

6365

6466
def context(
65-
symbol: Annotated[
66-
str, typer.Option("--symbol", "-s", help="シンボル名(部分一致)")
67-
],
67+
symbol: Annotated[str | None, typer.Option("--symbol", "-s", help="シンボル名(部分一致)")] = None,
6868
limit: Annotated[int, typer.Option("--limit", "-n", help="返す件数")] = 5,
6969
json_output: Annotated[bool, typer.Option("--json", help="JSON で出力")] = False,
7070
full: Annotated[bool, typer.Option("--full", help="全文(user_content / agent_content)を含める")] = False,
71+
branch: Annotated[str | None, typer.Option("--branch", "-b", help="ブランチ名で絞り込む(部分一致)")] = None,
7172
) -> None:
7273
"""シンボル名から関連する過去会話を逆引きする"""
74+
if symbol is None and branch is None:
75+
typer.echo("Error: --symbol or --branch is required.", err=True)
76+
raise typer.Exit(1)
77+
7378
from codeatrium.db import get_connection
7479
from codeatrium.paths import db_path, find_project_root
7580

@@ -81,30 +86,83 @@ def context(
8186
raise typer.Exit(1)
8287

8388
con = get_connection(db)
84-
rows = con.execute(
85-
"""
86-
SELECT
87-
s.symbol_name,
88-
s.symbol_kind,
89-
s.file_path,
90-
s.signature,
91-
s.line,
92-
e.id AS exchange_id,
93-
e.user_content,
94-
e.agent_content,
95-
p.exchange_core,
96-
p.specific_context,
97-
c.source_path,
98-
e.ply_start
99-
FROM symbols s
100-
JOIN palace_objects p ON p.id = s.palace_object_id
101-
JOIN exchanges e ON e.id = p.exchange_id
102-
JOIN conversations c ON c.id = e.conversation_id
103-
WHERE s.symbol_name LIKE ?
104-
LIMIT ?
105-
""",
106-
(f"%{symbol}%", limit),
107-
).fetchall()
89+
90+
if symbol is not None and branch is not None:
91+
# Both symbol and branch specified
92+
rows = con.execute(
93+
"""
94+
SELECT
95+
s.symbol_name,
96+
s.symbol_kind,
97+
s.file_path,
98+
s.signature,
99+
s.line,
100+
e.id AS exchange_id,
101+
e.user_content,
102+
e.agent_content,
103+
p.exchange_core,
104+
p.specific_context,
105+
c.source_path,
106+
e.ply_start,
107+
e.git_branch
108+
FROM symbols s
109+
JOIN palace_objects p ON p.id = s.palace_object_id
110+
JOIN exchanges e ON e.id = p.exchange_id
111+
JOIN conversations c ON c.id = e.conversation_id
112+
WHERE s.symbol_name LIKE ? AND e.git_branch LIKE ?
113+
LIMIT ?
114+
""",
115+
(f"%{symbol}%", f"%{branch}%", limit),
116+
).fetchall()
117+
elif symbol is not None:
118+
# Symbol only (existing behavior with git_branch added)
119+
rows = con.execute(
120+
"""
121+
SELECT
122+
s.symbol_name,
123+
s.symbol_kind,
124+
s.file_path,
125+
s.signature,
126+
s.line,
127+
e.id AS exchange_id,
128+
e.user_content,
129+
e.agent_content,
130+
p.exchange_core,
131+
p.specific_context,
132+
c.source_path,
133+
e.ply_start,
134+
e.git_branch
135+
FROM symbols s
136+
JOIN palace_objects p ON p.id = s.palace_object_id
137+
JOIN exchanges e ON e.id = p.exchange_id
138+
JOIN conversations c ON c.id = e.conversation_id
139+
WHERE s.symbol_name LIKE ?
140+
LIMIT ?
141+
""",
142+
(f"%{symbol}%", limit),
143+
).fetchall()
144+
else:
145+
# Branch only (LEFT JOIN to include undistilled exchanges)
146+
rows = con.execute(
147+
"""
148+
SELECT
149+
e.id AS exchange_id,
150+
e.git_branch,
151+
e.user_content,
152+
e.agent_content,
153+
p.exchange_core,
154+
p.specific_context,
155+
c.source_path,
156+
e.ply_start
157+
FROM exchanges e
158+
JOIN conversations c ON c.id = e.conversation_id
159+
LEFT JOIN palace_objects p ON p.exchange_id = e.id
160+
WHERE e.git_branch LIKE ?
161+
ORDER BY c.started_at, e.ply_start
162+
LIMIT ?
163+
""",
164+
(f"%{branch}%", limit),
165+
).fetchall()
108166
con.close()
109167

110168
if not rows:
@@ -114,27 +172,50 @@ def context(
114172
if json_output:
115173
output = []
116174
for r in rows:
117-
base = {
118-
"symbol_name": r["symbol_name"],
119-
"symbol_kind": r["symbol_kind"],
120-
"file_path": r["file_path"],
121-
"signature": r["signature"],
122-
"line": r["line"],
123-
"exchange_id": r["exchange_id"],
124-
"exchange_core": r["exchange_core"],
125-
"specific_context": r["specific_context"],
126-
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
127-
}
128-
if full:
129-
base["user_content"] = r["user_content"]
130-
base["agent_content"] = r["agent_content"]
175+
if symbol is not None:
176+
# Symbol mode (symbol only or both)
177+
base = {
178+
"symbol_name": r["symbol_name"],
179+
"symbol_kind": r["symbol_kind"],
180+
"file_path": r["file_path"],
181+
"signature": r["signature"],
182+
"line": r["line"],
183+
"exchange_id": r["exchange_id"],
184+
"exchange_core": r["exchange_core"],
185+
"specific_context": r["specific_context"],
186+
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
187+
"git_branch": r["git_branch"] if "git_branch" in r.keys() else None,
188+
}
189+
if full:
190+
base["user_content"] = r["user_content"]
191+
base["agent_content"] = r["agent_content"]
192+
else:
193+
# Branch-only mode
194+
base = {
195+
"exchange_id": r["exchange_id"],
196+
"git_branch": r["git_branch"],
197+
"exchange_core": r["exchange_core"],
198+
"specific_context": r["specific_context"],
199+
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
200+
}
201+
if full:
202+
base["user_content"] = r["user_content"]
203+
base["agent_content"] = r["agent_content"]
131204
output.append(base)
132205
typer.echo(json.dumps(output, ensure_ascii=False, indent=2))
133206
else:
134207
for i, r in enumerate(rows, 1):
135-
typer.echo(f"\n[{i}] {r['symbol_kind']} {r['symbol_name']}")
136-
typer.echo(f" {r['file_path']}:{r['line']}")
137-
typer.echo(f" {r['signature']}")
138-
if r["exchange_core"]:
139-
typer.echo(f" Core: {r['exchange_core']}")
140-
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")
208+
if symbol is not None:
209+
# Symbol mode display
210+
typer.echo(f"\n[{i}] {r['symbol_kind']} {r['symbol_name']}")
211+
typer.echo(f" {r['file_path']}:{r['line']}")
212+
typer.echo(f" {r['signature']}")
213+
if r["exchange_core"]:
214+
typer.echo(f" Core: {r['exchange_core']}")
215+
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")
216+
else:
217+
# Branch-only mode display
218+
typer.echo(f"\n[{i}] exchange_id={r['exchange_id']} git_branch={r['git_branch']}")
219+
if r["exchange_core"]:
220+
typer.echo(f" Core: {r['exchange_core']}")
221+
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")

0 commit comments

Comments
 (0)