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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ The release workflow uses a tag's section here as its release notes (a
hand-written `docs/releases/<tag>.md` wins if present), so keeping this file
current is part of cutting a release.

## Unreleased

- codedb pairing: `list_dir <path>` is in-process (gitignore, PathConfine,
works without the binary), `status` reports `codedb.snapshot`, and
path-bearing subcommands (`read`/`outline`/`deps`/`file`) share the
file-tool jail. The codedb guard now redirects `ls`/`find`/`tree` even
when the CLI is missing (ADR 0013). One-shot verbs first (ADR 0014):
`context <task>`, `around <name>` (def+callers), `callpath A B`
(codedb's shortest call chain; `path` is an alias). The catalog
advertises only those plus `list_dir`/`status`; hop verbs stay
callable, not on the menu.

## v0.0.267 (2026-08-19)

- Background jobs wait like grok-build: `bash_output(wait_ms>0)` and
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ whole-file reads. Instead of paying for a 2,000-line file to find one function,
the model asks for exactly the shape it needs:

```
codedb outline src/main.zig # just the symbol map, functions/types, no bodies
codedb symbol switchProvider --body # one function, by name
codedb callers recordUsage # who calls it (call sites, not files)
codedb search "parse SSE" # indexed search, ranked hits, not a grep dump
codedb context "add a new provider" # task-shaped orientation across the codebase
codedb context "add a new provider" # defs + neighbors + snippets (one call)
codedb around switchProvider # definition body + callers (one call)
codedb callpath exec codedbGuard # shortest resolved call chain
codedb list_dir src # live tree (gitignore, 10k cap; no index needed)
codedb status # is codedb.snapshot present?
```

Why this keeps token cost low:
Expand All @@ -231,10 +231,13 @@ Why this keeps token cost low:
- **Same index powers the `@` file picker** (`codedb glob`), so attaching a file
by name never shells out to a directory walk.

Pure-Zig client to a pure-Zig server, zero dependencies on either side. Allowed
subcommands: `search · symbol · callers · find · outline · read · tree ·
context · word · deps · glob · ls · file · hot`. Not installed? The tool says so
and points at the one-line install; everything else keeps working without it.
Pure-Zig client to a pure-Zig server, zero dependencies on either side. The
catalog advertises five commands: `context · around · callpath · list_dir ·
status`. Hop verbs (`search`, `outline`, `read`, …) still run if the model
already knows them; they are not on the menu. `list_dir` and `status` are
in-process (PathConfine; they work without the binary). Path-bearing queries
stay inside the cwd, same jail as `read_file`. Not installed? Structural
search says so and points at the one-line install; folder listing still works.

---

Expand Down Expand Up @@ -840,7 +843,7 @@ get web-request economics, and no boot-and-provision tax on time to first token.
| `read_file` | built-in | `Io.Dir.cwd().readFileAlloc` (256 KB cap) |
| `edit_file` | built-in | exact string replace; unique match required unless `replace_all` |
| `write_file` | built-in | `Io.Dir.cwd().writeFile` |
| `codedb` | built-in | shells out to [codedb](https://github.com/justrach/codedb): read-only code-intel (search/symbol/callers/outline/…) |
| `codedb` | built-in | [codedb](https://github.com/justrach/codedb) — advertised: `context` / `around` / `callpath` / `list_dir` / `status` |
| `subagent` | built-in | this same agent loop, recursively (root agent only) |
| `workflow` | built-in | phases of parallel subagents; `{{prev}}` carries results forward (root only) |
| `todo_write`/`_read` | meta | mutate/read the agent's own task list |
Expand Down
30 changes: 30 additions & 0 deletions docs/adr/0013-list-dir-lives-in-codedb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 0013. Directory listing is a codedb subcommand, not a catalog tool

Status: accepted 2026-08-20

## Context

grok-build's `list_dir` is a first-class tool. graff already has `codedb`
for structural nav and tells the model to prefer it over bash `ls`. A new
always-on catalog entry would tax every turn — that is how the first #574
A/B lost. `codedb ls` / `tree` are the **index**; they do not list an
unindexed tree. The native `codedb` spawn also had no PathConfine, so
`codedb read /etc/passwd` bypassed the file-tool jail.

## Decision

`codedb list_dir <path>` is in-process (PathConfine, gitignore, 10k cap)
and works without the codedb binary. `codedb status` reports
`codedb.snapshot` without spawning. Path-bearing subcommands
(`read`/`outline`/`deps`/`file`) and escaping `glob` patterns use the same
cwd jail as `read_file`. `ls` / `tree` stay index queries.

Do not add a sibling graff `list_dir` catalog tool unless an A/B shows the
extra schema bytes beat this subcommand.

## Consequences

- Folder listing no longer needs `bash ls` / `find` for confined trees.
- The codedb guard redirects those shell commands even when the CLI is missing.
- A missing index is a status line, not a dead tool — `list_dir` still works.
- The model has to read the codedb description to find `list_dir`.
35 changes: 35 additions & 0 deletions docs/adr/0014-codedb-one-shot-over-hop-chains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 0014. codedb one-shots beat hop chains

Status: accepted 2026-08-20

## Context

[graphify](https://github.com/safishamsi/graphify) spends fewer agent
calls because the verbs are neighborhoods, not lookups: `query` (task
subgraph), `explain` (ego graph), `path A B` (shortest chain). codedb
already has the same primitives — `context <task>` (task-shaped
composer), `callpath A B` (resolved call graph BFS), PageRank god-nodes
inside the index — and it is deterministic AST, not an LLM extractor.
The harness advertised `search` / `symbol` / `callers` / `outline` as
siblings, so models paid 3–5 round-trips for one question. `callpath`
was implemented in codedb and blocked here.

## Decision

The catalog advertises five commands only: `context <task>`,
`around <name>` (def body + callers in one harness call), `callpath A B`
(`path` is an alias), `list_dir <path>`, `status`. Hop verbs
(`search` / `symbol` / `callers` / `outline` / `read` / …) stay
callable so a follow-up is not a dead end; they are not named in the
tool description or the system prompt. Do not add a graphify-style LLM
extract / wiki / GRAPH_REPORT inside graff — that is a different
product. Do not add a sibling catalog tool for "ask the graph."

## Consequences

- First-touch orientation is one model hop when the model follows the
description.
- `around` is two codedb CLI spawns, one tool result. A codedb release
with a native `explain` can replace the composer later.
- graphify still wins on multimodal docs/images and a written wiki;
codedb still wins on local, labeled, sub-millisecond structural edges.
2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ record only when you need the evidence or the edge cases.
| [0009](0009-gpt-5-6-explicit-prompt-cache-boundary.md) | GPT-5.6 OpenAI Platform marks the stable prefix explicitly; Codex and xAI stay on their supported keyed automatic-cache paths. |
| [0010](0010-background-jobs-wait-for-exit.md) | `bash_output`/`agent_output` `wait_ms>0` blocks until exit (10h cap); do not poll every 30s. |
| [0011](0011-prompt-cache-max-is-visible.md) | Prompt-cache max is `/cache` posture, not a new default; `/btw` rides the parent prefix. |
| [0013](0013-list-dir-lives-in-codedb.md) | Directory listing is `codedb list_dir`, not a catalog tool; path-bearing codedb commands share the file-tool jail. |
| [0014](0014-codedb-one-shot-over-hop-chains.md) | Advertise only `context` / `around` / `callpath` / `list_dir` / `status`; hop verbs stay callable, not on the menu. |

## When to write one

Expand Down
7 changes: 5 additions & 2 deletions sdk/ts/remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,15 @@ describe("RemoteHarness transport", () => {
test("constructor observes create rejection even when the caller never awaits it", async () => {
let unhandled = 0;
const listener = () => { unhandled += 1; };
process.on("unhandledRejection", listener);
// bun-types narrows Process.on/off to memoryPressure; Node's
// unhandledRejection is the event this test actually needs.
const proc = process as unknown as NodeJS.EventEmitter;
proc.on("unhandledRejection", listener);
globalThis.fetch = (async () => { throw new Error("create failed"); }) as unknown as typeof fetch;
const h = new RemoteHarness({ url: "http://bridge.test" });
live.push(h);
await Bun.sleep(30);
process.off("unhandledRejection", listener);
proc.off("unhandledRejection", listener);
expect(unhandled).toBe(0);
await expect(h.sessionId).rejects.toThrow("create failed");
});
Expand Down
110 changes: 110 additions & 0 deletions src/codedb_around.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! One-shot codedb composers. Graphify's hop win is `explain` / `path` /
//! `query` — one call, a neighborhood. codedb already has the graph
//! (`callpath`) and a task composer (`context`); this module collapses the
//! remaining "symbol then callers" dance the model otherwise pays two
//! round-trips for.

const std = @import("std");
const Allocator = std.mem.Allocator;

const tools = @import("tools.zig");
const ToolCtx = tools.ToolCtx;
const ToolOutput = tools.ToolOutput;
const jobs = @import("jobs.zig");

const deadline_ms: u64 = 60 * 1000;
const section_cap = 24 * 1024;

pub fn firstIdent(rest: []const u8) ?[]const u8 {
var it = std.mem.tokenizeAny(u8, rest, " \t");
while (it.next()) |tok| {
if (tok.len == 0 or tok[0] == '-') continue;
return tok;
}
return null;
}

pub fn pathEnds(rest: []const u8) ?struct { from: []const u8, to: []const u8 } {
var it = std.mem.tokenizeAny(u8, rest, " \t");
const from = it.next() orelse return null;
const to = it.next() orelse return null;
if (from[0] == '-' or to[0] == '-') return null;
return .{ .from = from, .to = to };
}

fn spawn(ctx: ToolCtx, argv_tail: []const []const u8) !ToolOutput {
const gpa = ctx.gpa;
var argv: std.ArrayList([]const u8) = .empty;
defer argv.deinit(gpa);
try argv.append(gpa, "codedb");
try argv.appendSlice(gpa, argv_tail);
const run = jobs.runCappedWithOptions(gpa, ctx.io, argv.items, 512 * 1024, 4096, deadline_ms, jobs.toolRunOptions(ctx.agent_cwd)) catch |e| switch (e) {
error.FileNotFound => return .{
.text = try gpa.dupe(u8, "codedb isn't installed — it's open source at github.com/justrach/codedb; install it, then run `codedb` once in the repo to index it. Folder listing still works: codedb list_dir ."),
.is_error = true,
},
else => return tools.failure(gpa, e),
};
gpa.free(run.stderr);
const text = run.stdout;
if (run.timed_out) {
defer gpa.free(text);
return .{
.text = try std.fmt.allocPrint(gpa, "codedb {s} timed out after {d}s and was killed — narrow the query", .{ argv_tail[0], deadline_ms / 1000 }),
.is_error = true,
};
}
return .{ .text = text };
}

fn take(gpa: Allocator, out: ToolOutput, cap: usize) ![]u8 {
defer gpa.free(out.text);
const src = if (out.text.len > cap) out.text[0..cap] else out.text;
return gpa.dupe(u8, src);
}

pub fn execAround(ctx: ToolCtx, rest: []const u8) !ToolOutput {
const gpa = ctx.gpa;
const name = firstIdent(rest) orelse return .{
.text = try gpa.dupe(u8, "usage: codedb around <name> — definition body + callers in one call (graphify explain). Alias: explain <name>."),
.is_error = true,
};

const def = try spawn(ctx, &.{ "symbol", name, "--body" });
if (def.is_error) return def;
const def_text = try take(gpa, def, section_cap);
defer gpa.free(def_text);

const callers = try spawn(ctx, &.{ "callers", name });
if (callers.is_error) {
defer gpa.free(callers.text);
return .{ .text = try std.fmt.allocPrint(gpa, "## definition\n{s}\n\n## callers\n{s}", .{ def_text, callers.text }), .is_error = true };
}
const callers_text = try take(gpa, callers, section_cap);
defer gpa.free(callers_text);

return .{ .text = try std.fmt.allocPrint(gpa, "## definition\n{s}\n\n## callers\n{s}", .{ def_text, callers_text }) };
}

pub fn execPath(ctx: ToolCtx, rest: []const u8) !ToolOutput {
const ends = pathEnds(rest) orelse return .{
.text = try ctx.gpa.dupe(u8, "usage: codedb callpath <from> <to> — shortest resolved call chain (graphify path). Alias: path <from> <to>."),
.is_error = true,
};
return spawn(ctx, &.{ "callpath", ends.from, ends.to });
}

test "firstIdent skips flags" {
try std.testing.expectEqualStrings("handleCallpath", firstIdent("handleCallpath").?);
try std.testing.expectEqualStrings("Store", firstIdent("--body Store").?);
try std.testing.expect(firstIdent("") == null);
try std.testing.expect(firstIdent(" ") == null);
}

test "pathEnds needs two identifiers" {
const got = pathEnds("handleContext handleCallpath").?;
try std.testing.expectEqualStrings("handleContext", got.from);
try std.testing.expectEqualStrings("handleCallpath", got.to);
try std.testing.expect(pathEnds("onlyOne") == null);
try std.testing.expect(pathEnds("") == null);
}
Loading
Loading