Skip to content
Merged
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
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,4 @@ build/

# Internal docs (local only)
docs/internal/

# 論文資料(ローカルのみ)
mem/
.strata
31 changes: 31 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# codeatrium — Agent Usage Guide

`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.

Primary user is **the agent itself**, not a human. The tool is invoked via `loci search "..." --json` from within agent prompts.

## When to use

- When asked "where did we implement X?" or "where is X?"
- When checking if a similar bug was fixed before
- When verifying if a feature already exists
- When looking up the reasoning behind a past design decision
- Before editing code you lack context about — use `loci context --symbol` to review past discussions
- Before refactoring or changing the behavior of a function — use `loci context --symbol` to check past design decisions
- When recalling work done on a specific branch — use `loci context --branch` to find past conversations

## CLI Commands

```bash
loci init # Initialize .codeatrium/ in project root
loci index # Index new .jsonl files
loci distill [--limit N] # Distill queued exchanges via claude --print
loci search "query" --json --limit 5 # Semantic search (agent-facing)
loci search "query" --branch NAME --json # Branch-filtered semantic search
loci context --symbol "Foo.bar" --json # Reverse lookup: code -> past conversations (lightweight; use loci show <verbatim_ref> for full text)
loci context --branch NAME --json # Branch reverse lookup (undistilled exchanges included)
loci show "~/.claude/.../abc.jsonl:ply=42" # Fetch verbatim exchange
loci status # Show index state
loci server start / stop / status # Embedding server management
loci hook install # Register hooks to ~/.claude/settings.json
```
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ loci init # Initialize .codeatrium/ in projec
loci index # Index new .jsonl files
loci distill [--limit N] # Distill queued exchanges via claude --print
loci search "query" --json --limit 5 # Semantic search (agent-facing)
loci search "query" --branch NAME --json # Branch-filtered semantic search
loci context --symbol "Foo.bar" --json # Reverse lookup: code -> past conversations
loci context --branch NAME --json # Branch reverse lookup (undistilled exchanges included)
loci show "~/.claude/.../abc.jsonl:ply=42" # Fetch verbatim exchange
loci status # Show index state
loci server start / stop / status # Embedding server management
Expand Down
6 changes: 5 additions & 1 deletion src/codeatrium/cli/prime_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- **Before editing or refactoring a function** — recall past design decisions and known constraints for that symbol.
- **Before starting a new implementation** — check if similar work was done before; reuse decisions and avoid re-debating settled choices.
- **When you encounter a known or recurring error** — search for past fixes; the solution may already be documented.
- **When asked about work on a specific branch** — recall what was done and discussed on that branch.

### Search — semantic query over past conversations

Expand All @@ -30,13 +31,16 @@
loci show "<verbatim_ref>" --json
```

### Context — reverse lookup from code symbol to past conversations
### Context — reverse lookup from code symbol or git branch to past conversations

Touching a symbol = recalling memory about that symbol. Before changing any function or class, look up what was decided about it.

```bash
# Retrieve all past conversations that involved this symbol
loci context --symbol "SymbolResolver.extract" --json

# Retrieve past conversations from work on a specific branch
loci context --branch "feature/foo" --json
```\
"""

Expand Down
177 changes: 129 additions & 48 deletions src/codeatrium/cli/search_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def search(
query: Annotated[str, typer.Argument(help="検索クエリ")],
limit: Annotated[int, typer.Option("--limit", "-n", help="返す件数")] = 5,
json_output: Annotated[bool, typer.Option("--json", help="JSON で出力")] = False,
branch: Annotated[str | None, typer.Option("--branch", "-b", help="ブランチ名で絞り込む(部分一致)")] = None,
) -> None:
"""BM25(V) + HNSW(D) RRF でクエリに近い過去会話を返す"""
from codeatrium.embedder import Embedder
Expand All @@ -32,7 +33,7 @@ def search(

embedder = Embedder()
query_vec = embedder.embed(query)
results = search_combined(db, query, query_vec, limit=limit)
results = search_combined(db, query, query_vec, limit=limit, branch=branch)

if not results:
typer.echo("No results found.")
Expand All @@ -46,6 +47,7 @@ def search(
"rooms": r.rooms,
"symbols": r.symbols,
"verbatim_ref": r.verbatim_ref,
"git_branch": r.git_branch,
}
for r in results
]
Expand All @@ -62,14 +64,17 @@ def search(


def context(
symbol: Annotated[
str, typer.Option("--symbol", "-s", help="シンボル名(部分一致)")
],
symbol: Annotated[str | None, typer.Option("--symbol", "-s", help="シンボル名(部分一致)")] = None,
limit: Annotated[int, typer.Option("--limit", "-n", help="返す件数")] = 5,
json_output: Annotated[bool, typer.Option("--json", help="JSON で出力")] = False,
full: Annotated[bool, typer.Option("--full", help="全文(user_content / agent_content)を含める")] = False,
branch: Annotated[str | None, typer.Option("--branch", "-b", help="ブランチ名で絞り込む(部分一致)")] = None,
) -> None:
"""シンボル名から関連する過去会話を逆引きする"""
if symbol is None and branch is None:
typer.echo("Error: --symbol or --branch is required.", err=True)
raise typer.Exit(1)

from codeatrium.db import get_connection
from codeatrium.paths import db_path, find_project_root

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

con = get_connection(db)
rows = con.execute(
"""
SELECT
s.symbol_name,
s.symbol_kind,
s.file_path,
s.signature,
s.line,
e.id AS exchange_id,
e.user_content,
e.agent_content,
p.exchange_core,
p.specific_context,
c.source_path,
e.ply_start
FROM symbols s
JOIN palace_objects p ON p.id = s.palace_object_id
JOIN exchanges e ON e.id = p.exchange_id
JOIN conversations c ON c.id = e.conversation_id
WHERE s.symbol_name LIKE ?
LIMIT ?
""",
(f"%{symbol}%", limit),
).fetchall()

if symbol is not None and branch is not None:
# Both symbol and branch specified
rows = con.execute(
"""
SELECT
s.symbol_name,
s.symbol_kind,
s.file_path,
s.signature,
s.line,
e.id AS exchange_id,
e.user_content,
e.agent_content,
p.exchange_core,
p.specific_context,
c.source_path,
e.ply_start,
e.git_branch
FROM symbols s
JOIN palace_objects p ON p.id = s.palace_object_id
JOIN exchanges e ON e.id = p.exchange_id
JOIN conversations c ON c.id = e.conversation_id
WHERE s.symbol_name LIKE ? AND e.git_branch LIKE ?
LIMIT ?
""",
(f"%{symbol}%", f"%{branch}%", limit),
).fetchall()
elif symbol is not None:
# Symbol only (existing behavior with git_branch added)
rows = con.execute(
"""
SELECT
s.symbol_name,
s.symbol_kind,
s.file_path,
s.signature,
s.line,
e.id AS exchange_id,
e.user_content,
e.agent_content,
p.exchange_core,
p.specific_context,
c.source_path,
e.ply_start,
e.git_branch
FROM symbols s
JOIN palace_objects p ON p.id = s.palace_object_id
JOIN exchanges e ON e.id = p.exchange_id
JOIN conversations c ON c.id = e.conversation_id
WHERE s.symbol_name LIKE ?
LIMIT ?
""",
(f"%{symbol}%", limit),
).fetchall()
else:
# Branch only (LEFT JOIN to include undistilled exchanges)
rows = con.execute(
"""
SELECT
e.id AS exchange_id,
e.git_branch,
e.user_content,
e.agent_content,
p.exchange_core,
p.specific_context,
c.source_path,
e.ply_start
FROM exchanges e
JOIN conversations c ON c.id = e.conversation_id
LEFT JOIN palace_objects p ON p.exchange_id = e.id
WHERE e.git_branch LIKE ?
ORDER BY c.started_at, e.ply_start
LIMIT ?
""",
(f"%{branch}%", limit),
).fetchall()
con.close()

if not rows:
Expand All @@ -114,27 +172,50 @@ def context(
if json_output:
output = []
for r in rows:
base = {
"symbol_name": r["symbol_name"],
"symbol_kind": r["symbol_kind"],
"file_path": r["file_path"],
"signature": r["signature"],
"line": r["line"],
"exchange_id": r["exchange_id"],
"exchange_core": r["exchange_core"],
"specific_context": r["specific_context"],
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
}
if full:
base["user_content"] = r["user_content"]
base["agent_content"] = r["agent_content"]
if symbol is not None:
# Symbol mode (symbol only or both)
base = {
"symbol_name": r["symbol_name"],
"symbol_kind": r["symbol_kind"],
"file_path": r["file_path"],
"signature": r["signature"],
"line": r["line"],
"exchange_id": r["exchange_id"],
"exchange_core": r["exchange_core"],
"specific_context": r["specific_context"],
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
"git_branch": r["git_branch"] if "git_branch" in r.keys() else None,
}
if full:
base["user_content"] = r["user_content"]
base["agent_content"] = r["agent_content"]
else:
# Branch-only mode
base = {
"exchange_id": r["exchange_id"],
"git_branch": r["git_branch"],
"exchange_core": r["exchange_core"],
"specific_context": r["specific_context"],
"verbatim_ref": f"{r['source_path']}:ply={r['ply_start']}",
}
if full:
base["user_content"] = r["user_content"]
base["agent_content"] = r["agent_content"]
output.append(base)
typer.echo(json.dumps(output, ensure_ascii=False, indent=2))
else:
for i, r in enumerate(rows, 1):
typer.echo(f"\n[{i}] {r['symbol_kind']} {r['symbol_name']}")
typer.echo(f" {r['file_path']}:{r['line']}")
typer.echo(f" {r['signature']}")
if r["exchange_core"]:
typer.echo(f" Core: {r['exchange_core']}")
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")
if symbol is not None:
# Symbol mode display
typer.echo(f"\n[{i}] {r['symbol_kind']} {r['symbol_name']}")
typer.echo(f" {r['file_path']}:{r['line']}")
typer.echo(f" {r['signature']}")
if r["exchange_core"]:
typer.echo(f" Core: {r['exchange_core']}")
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")
else:
# Branch-only mode display
typer.echo(f"\n[{i}] exchange_id={r['exchange_id']} git_branch={r['git_branch']}")
if r["exchange_core"]:
typer.echo(f" Core: {r['exchange_core']}")
typer.echo(f" {r['source_path']}:ply={r['ply_start']}")
Loading
Loading