Skip to content

Commit 96e80c1

Browse files
joshwilhelmiclaude
andcommitted
[gobby-cli-#120] chore(release): Prep gcode 0.6.0, gsqz 0.4.1, gcore 0.1.0, ghook 0.1.0
Bump crate versions, add new ghook user/dev guides and gcore dev guide, note gobby-core dependency in gcode dev guide and low-savings marker suppression in gsqz user guide, add four versioned CHANGELOG sections, and surface ghook + gcore in the top-level README. Adds version="0.1" to gcode's path dep on gobby-core so the manifest is valid for crates.io upload. Staged release order: tag gcore-v0.1.0 first (so gobby-core lands on the registry), then tag gcode-v0.6.0, gsqz-v0.4.1, ghook-v0.1.0 together. Verified: cargo build/test/clippy/fmt all clean across the workspace (267 tests passing). cargo package -p gobby-core succeeded; gobby-code packaging waits on gobby-core being on the registry (expected -- release ordering handles this). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1f57bbd commit 96e80c1

10 files changed

Lines changed: 777 additions & 7 deletions

CHANGELOG.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,63 @@ All notable changes to gobby-cli are documented in this file.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [0.6.0] — gcode
11+
12+
### Changed
13+
14+
#### gcode
15+
16+
- **gobby-core integration** — Migrated to consume the new `gobby-core` crate for project-root walk-up, bootstrap-config resolution, and daemon-URL construction. Inline implementations in `src/project.rs` removed (-109 lines); `src/config.rs` and `src/commands/index.rs` now use the shared helpers. No user-visible behavior change. (#115)
17+
18+
### Fixed
19+
20+
#### gcode
21+
22+
- **FTS LIKE escape** — Hardened FTS5 LIKE-clause escape in `src/search/fts.rs` against patterns containing `%`, `_`, or `\`. Prevents pathological queries from matching unintended rows. (#118)
23+
- **graph.rs dedup** — Deduplicated unresolved-symbol response building in `src/commands/graph.rs` (-63 lines net). No behavior change. (#118)
24+
25+
#### CI/CD
26+
27+
- **Binary-specific tag prefixes** — Release workflows now trigger on `gcode-v*`, `gsqz-v*`, `gloc-v*`, `gcore-v*`, and `ghook-v*` tags so each crate releases independently. (#110)
28+
- **Release gating** — Added `release-gobby-core` workflow; GitHub releases for binary crates are gated on successful crates.io publish. (#116)
29+
30+
## [0.4.1] — gsqz
31+
32+
### Fixed
33+
34+
#### gsqz
35+
36+
- **Low-savings marker** — Suppress `[gsqz:low-savings]` marker when prepending it would grow the output beyond the original. The marker now only annotates when the annotation itself doesn't make things worse. (#111)
37+
38+
## [0.1.0] — gobby-hooks
39+
40+
### Added
41+
42+
#### gobby-hooks
43+
44+
- **Initial release** — Sandbox-tolerant hook dispatcher binary `ghook`. Spools envelopes to `~/.gobby/hooks/inbox/` *before* POSTing to the local Gobby daemon, so the daemon's drain worker can replay deliveries lost to sandbox FS-read denials, network blips, or daemon restarts. (#114)
45+
- **Dispatch mode**`ghook --gobby-owned --cli=<c> --type=<t> [--critical] [--detach]` reads stdin, enriches with terminal context where applicable, atomically writes the envelope, then attempts the daemon POST.
46+
- **Diagnose mode**`ghook --diagnose --cli=<c> --type=<t>` prints a JSON snapshot of what would happen — daemon URL, project root/id, recognized CLI, critical flag, terminal-context preview. No network, no envelope write. Output validated against `schemas/diagnose-output.v1.schema.json`.
47+
- **Version mode**`ghook --version` prints the version and writes `~/.gobby/bin/.ghook-compatibility` so the daemon can detect schema-version mismatches.
48+
- **Exit codes**`0` for delivered or non-critical failure (envelope still enqueued); `2` for critical failure (envelope enqueued; signals the host CLI to abort).
49+
- **Schemas**`inbox-envelope.v1.schema.json` and `diagnose-output.v1.schema.json`, both validated against the Rust types in unit tests.
50+
- **Host CLI registry** — Out-of-the-box support for `claude`, `codex`, `gemini`, `qwen` (per-CLI critical-hooks set + terminal-context-hooks set). Unknown CLIs are tolerated — envelope still spools, just without enrichment.
51+
- **Quarantine** — Malformed stdin lands in `~/.gobby/hooks/inbox/quarantine/` as a body+meta pair. The drain never replays quarantined envelopes; they surface via `gobby status` and daemon logs.
52+
- **Atomic spool writes** — Envelopes use write-temp + `fsync` + rename, so the drain only ever sees fully-written files.
53+
- **Renamed for consistency** — Crate renamed from `gobby-hook` to `gobby-hooks`; binary stays `ghook`. (#117)
54+
55+
## [0.1.0] — gobby-core
56+
57+
### Added
58+
59+
#### gobby-core
60+
61+
- **Initial release** — Shared-primitives crate consumed by `gcode`, `gsqz`, `gloc`, and `ghook`. Three modules:
62+
- `project` — walk up from cwd to find `.gobby/project.json` (or legacy `.gobby/gcode.json`), read `id`/`project_id`.
63+
- `bootstrap` — resolve `~/.gobby/bootstrap.yaml` into a `DaemonEndpoint` (host + port). Falls back to `127.0.0.1:60887` on any failure.
64+
- `daemon_url` — compose a dial URL from a `DaemonEndpoint`, normalizing wildcard listen addresses (`0.0.0.0`, `::`, `::0`) to `127.0.0.1`.
65+
- **Extracted from inline implementations** in the binary crates so behavior changes propagate with one PR instead of four. (#112, #113, #117)
66+
1067
## [0.4.0] — gsqz
1168

1269
### Added

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
## What's Inside
2323

24-
This workspace contains three Gobby CLI tools:
24+
This workspace contains four Gobby CLI tools plus a shared library:
2525

2626
### gcode — Code Search & Navigation
2727

@@ -35,11 +35,18 @@ Squeezes CLI output before it eats your context window. 28 built-in pipelines fo
3535

3636
One command to launch Claude Code or Codex against a local LLM backend. Auto-detects LM Studio and Ollama, manages Ollama model lifecycle (pull, load, warmup), sets the right env vars, and `exec`s into your CLI of choice. YAML-configurable with aliases, per-client env templates, and ordered backend priority.
3737

38+
### ghook — Hook Dispatcher
39+
40+
Sandbox-tolerant hook dispatcher invoked by host AI CLIs (Claude Code, Codex, Gemini CLI, Qwen CLI) on lifecycle and tool-use events. Spools envelopes to `~/.gobby/hooks/inbox/` *before* POSTing to the local Gobby daemon, so the daemon's drain worker can replay any delivery lost to a sandbox FS-read denial, network blip, or daemon restart. You don't usually invoke it directly — Gobby wires it into your AI CLI for you.
41+
42+
`gobby-core` underpins them all — a small shared-primitives library (project root walk-up, bootstrap config, daemon URL). Not a standalone tool.
43+
3844
## Documentation
3945

4046
- [gcode User Guide](docs/guides/gcode-user-guide.md) — search, symbols, dependency graphs, project management
4147
- [gsqz User Guide](docs/guides/gsqz-user-guide.md) — pipelines, step types, configuration, debugging
4248
- [gloc User Guide](docs/guides/gloc-user-guide.md) — backends, clients, model management, configuration
49+
- [ghook User Guide](docs/guides/ghook-user-guide.md) — hook wiring, diagnose mode, inbox/replay, troubleshooting
4350
- [Changelog](CHANGELOG.md) — release history
4451
- [gcode README](crates/gcode/README.md) — architecture and build details
4552
- [gsqz README](crates/gsqz/README.md) — architecture and build details
@@ -69,6 +76,9 @@ cargo install gobby-squeeze
6976

7077
# gloc
7178
cargo install gobby-local
79+
80+
# ghook
81+
cargo install gobby-hooks
7282
```
7383

7484
On macOS, Metal GPU acceleration is enabled automatically. On Linux/Windows, embeddings use CPU inference by default — add a GPU feature flag for hardware acceleration.
@@ -81,6 +91,7 @@ cd gobby-cli
8191
cargo install --path crates/gcode
8292
cargo install --path crates/gsqz
8393
cargo install --path crates/gloc
94+
cargo install --path crates/ghook
8495
```
8596

8697
## Development

crates/gcode/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gobby-code"
3-
version = "0.5.3"
3+
version = "0.6.0"
44
edition = "2024"
55
rust-version = "1.85"
66
authors = ["Josh Wilhelmi <hello@gobby.ai>"]
@@ -18,7 +18,7 @@ path = "src/main.rs"
1818

1919
[dependencies]
2020
# Internal
21-
gobby-core = { path = "../gcore" }
21+
gobby-core = { path = "../gcore", version = "0.1" }
2222

2323
# CLI
2424
clap = { version = "4", features = ["derive"] }

crates/gsqz/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gobby-squeeze"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
edition = "2024"
55
rust-version = "1.85"
66
authors = ["Josh Wilhelmi <hello@gobby.ai>"]

docs/guides/gcode-development-guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ On exit, `semantic::shutdown()` explicitly drops the embedding model to prevent
3838
3. Otherwise, walk up from cwd looking for `.gobby/project.json` (Gobby-managed) or `.gobby/gcode.json` (standalone)
3939
4. Fall back to VCS root markers (`.git`, `.hg`, `.svn`) or cwd
4040

41+
The walk-up and `project.json` reading steps use `gobby_core::project::find_project_root` and `gobby_core::project::read_project_id` (extracted from `gcode/src/project.rs` so `ghook` and other binaries can share the same logic). Bootstrap-config and daemon-URL helpers also come from `gobby-core`. See [gobby-core Development Guide](gcore-development-guide.md) for the shared API.
42+
4143
### Database Path Selection
4244

4345
1. Check `~/.gobby/bootstrap.yaml` for `database_path` key
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# gobby-core Development Guide
2+
3+
Technical internals for developers and agents working in the `gobby-core` crate (`crates/gcore/`).
4+
5+
## What gobby-core Is
6+
7+
`gobby-core` is a small, dependency-light shared-primitives crate consumed by every Gobby CLI binary (`gcode`, `gsqz`, `gloc`, `ghook`). It exists so the binaries don't reimplement the same project-discovery and daemon-addressing logic four times — and so a behavior change (e.g. how the daemon URL is normalized) propagates with one PR instead of four.
8+
9+
It has no CLI. It has no public state. It's a library — that's the whole shape.
10+
11+
## Module Map
12+
13+
`crates/gcore/src/`:
14+
15+
| Module | Responsibility |
16+
|--------|----------------|
17+
| `project` | Walk up from a starting directory to find a `.gobby/` directory containing `project.json` or `gcode.json`. Read the `id` (or legacy `project_id`) field from `project.json`. |
18+
| `bootstrap` | Read `~/.gobby/bootstrap.yaml` to get the daemon's listen endpoint (`bind_host`, `daemon_port`). Falls back to `127.0.0.1:60887` when the file is missing or malformed. |
19+
| `daemon_url` | Compose a dial URL from a `DaemonEndpoint`, normalizing wildcard listen addresses (`0.0.0.0`, `::`, `::0`) to `127.0.0.1`. |
20+
21+
Roughly 250 lines of source total. Adding a fourth module should require justification.
22+
23+
## Public API
24+
25+
### `project`
26+
27+
```rust
28+
pub fn find_project_root(start: &Path) -> Option<PathBuf>;
29+
pub fn read_project_id(project_root: &Path) -> anyhow::Result<String>;
30+
```
31+
32+
`find_project_root` walks up from `start` looking for a `.gobby/project.json` (Gobby-managed) or `.gobby/gcode.json` (gcode-standalone). Returns the directory *containing* `.gobby/`, not `.gobby/` itself. Returns `None` when neither marker is found before hitting the filesystem root.
33+
34+
`read_project_id` reads `<root>/.gobby/project.json` and extracts the `id` field, falling back to the legacy `project_id` key. Errors if the file is missing, malformed, or the field isn't present.
35+
36+
```rust
37+
let cwd = std::env::current_dir()?;
38+
if let Some(root) = gobby_core::project::find_project_root(&cwd) {
39+
let id = gobby_core::project::read_project_id(&root)?;
40+
println!("project {id} at {}", root.display());
41+
}
42+
```
43+
44+
### `bootstrap`
45+
46+
```rust
47+
pub const DEFAULT_DAEMON_PORT: u16 = 60887;
48+
pub const DEFAULT_BIND_HOST: &str = "127.0.0.1";
49+
50+
pub struct DaemonEndpoint { pub host: String, pub port: u16 }
51+
52+
pub fn bootstrap_path() -> Option<PathBuf>;
53+
pub fn read_daemon_endpoint() -> DaemonEndpoint;
54+
pub fn read_daemon_endpoint_at(path: &Path) -> DaemonEndpoint;
55+
```
56+
57+
`read_daemon_endpoint` is the lookup callers want. `read_daemon_endpoint_at` exists for tests and for callers who already know the path. Both return `DaemonEndpoint::default()` (loopback + 60887) on any failure — missing file, unreadable, malformed YAML, missing fields, no home directory. **No errors are surfaced**; clients should always get *something* usable.
58+
59+
`DaemonEndpoint` returns the raw endpoint as written. `0.0.0.0` and `::` are valid listen addresses but invalid dial addresses — normalization is the caller's job, or the `daemon_url` module's, not this one's.
60+
61+
### `daemon_url`
62+
63+
```rust
64+
pub fn daemon_url() -> String;
65+
pub fn daemon_url_at(path: &Path) -> String;
66+
```
67+
68+
Composes `http://{host}:{port}` from a bootstrap-derived endpoint, with one rewrite: wildcard listen hosts (`0.0.0.0`, `::`, `::0`) become `127.0.0.1`. Hostnames, named interfaces, and explicit IPv4/IPv6 literals pass through unchanged.
69+
70+
```rust
71+
let url = gobby_core::daemon_url::daemon_url();
72+
// "http://127.0.0.1:60887" for default bootstrap
73+
// "http://10.0.0.5:61234" if bootstrap has bind_host: 10.0.0.5
74+
// "http://127.0.0.1:60887" if bootstrap has bind_host: 0.0.0.0
75+
ureq::post(&format!("{url}/api/hooks/execute")).send_string(body)?;
76+
```
77+
78+
Bracketing IPv6 literals for URL embedding is **not** handled here — in practice `bootstrap.yaml` is always `localhost`, an IPv4 literal, or a wildcard. If that ever stops being true, this is the place to add it.
79+
80+
## Why These Three Modules Specifically
81+
82+
Each module exists because at least two binaries need exactly this logic, and getting it slightly wrong in one of them would silently misbehave:
83+
84+
| Module | Consumers (today) | What goes wrong if duplicated |
85+
|--------|-------------------|-------------------------------|
86+
| `project` | `gcode`, `ghook` (and `gsqz`/`gloc` could use it) | Project discovery walks up across mounts, weird symlink loops, race conditions with `.gobby/` creation. One implementation = one set of edge cases. |
87+
| `bootstrap` | `gcode`, `ghook` | YAML field naming, fallback semantics. Easy for two implementations to disagree on whether a missing field is fatal. |
88+
| `daemon_url` | `ghook` (and `gcode` daemon RPC) | Wildcard-host normalization is non-obvious. A binary that POSTs to `0.0.0.0` will hang for the connect timeout instead of failing fast. |
89+
90+
## Versioning Policy
91+
92+
`gobby-core` is `0.x`. The contract:
93+
94+
- **Patch bumps (0.1.x)** — bug fixes, doc changes, internal refactors with no public API change.
95+
- **Minor bumps (0.x.0)** — additive public API (new functions, new fields). Existing consumers stay compatible.
96+
- **Pre-1.0 breaking changes** — bump the minor and bump *every* consumer crate's gobby-core dep in the same release. Don't strand consumers on an old gobby-core.
97+
98+
Consumers pin to a minor version (`gobby-core = "0.1"`) so patch updates are picked up automatically but additive changes require a coordinated bump.
99+
100+
## How to Consume
101+
102+
### In-tree (workspace crates)
103+
104+
```toml
105+
[dependencies]
106+
gobby-core = { path = "../gcore", version = "0.1" }
107+
```
108+
109+
The `path` is for local workspace builds; `version` is required by `cargo publish` and gets used when consumers install the crate from crates.io. Don't drop the `version` field — `cargo publish` will reject the consumer's manifest.
110+
111+
### Out-of-tree
112+
113+
```toml
114+
[dependencies]
115+
gobby-core = "0.1"
116+
```
117+
118+
Resolves against crates.io. The crate has no opinionated dependencies — `anyhow`, `dirs`, `serde_json`, `serde_yaml`, and `tempfile` (dev-only). It will not pull in tokio, reqwest, tracing, or anything else heavy.
119+
120+
## Adding a New Helper
121+
122+
Before adding a module or function to `gobby-core`, check:
123+
124+
1. **Do at least two binaries need it?** If only one does, keep it in that binary.
125+
2. **Is it dependency-light?** New deps in `gobby-core` propagate to *every* binary. Adding `tokio` here would 5x the binary size of `ghook` for zero benefit. If the helper needs heavy deps, it probably belongs in a separate shared crate.
126+
3. **Is it stateless or near-stateless?** `gobby-core` functions are pure or do narrow I/O (read one file, return result). A module that holds connection pools or background workers belongs elsewhere.
127+
4. **Is the public surface small?** Three functions + a `DaemonEndpoint` struct is the right order of magnitude. If you find yourself adding a builder, a config object, and an `init()` function, reconsider.
128+
129+
If yes to all four, add the module:
130+
131+
1. Create `crates/gcore/src/<name>.rs` with `//!` module docs.
132+
2. Add `pub mod <name>;` to `crates/gcore/src/lib.rs`.
133+
3. Write tests that pin behavior under the failure modes the consumer cares about (missing input, malformed input, edge-case values).
134+
4. Update this guide's module map.
135+
5. Bump `gobby-core` to the next minor version (`0.2.0`) since you're adding public API.
136+
6. Update consumer crates to use the new helper, replacing any duplicated implementation. Bump their versions too.
137+
138+
## Testing
139+
140+
Each module has `#[cfg(test)] mod tests` with `tempfile::tempdir()` for filesystem isolation:
141+
142+
- **project**: implicitly tested via consumer binaries (`gcode`, `ghook`); the module mirrors `gcode/src/project.rs` line-for-line.
143+
- **bootstrap**: missing/malformed/empty files all return defaults; custom port/host parsing; out-of-range port falls back to default.
144+
- **daemon_url**: wildcard IPv4/IPv6 normalize to loopback; localhost passes through; custom host+port composes correctly.
145+
146+
```bash
147+
cargo test -p gobby-core
148+
```
149+
150+
Fast, no I/O outside `tempdir()`, no network. Should run in well under a second.
151+
152+
## Design Decisions
153+
154+
### Why Infallible Defaults Instead of `Result`
155+
156+
`read_daemon_endpoint` and friends return `DaemonEndpoint` (not `Result<DaemonEndpoint>`). The reasoning:
157+
158+
- Every consumer wants *some* endpoint to dial. Erroring at startup because `~/.gobby/bootstrap.yaml` doesn't exist would force every binary to handle the error identically (fall back to loopback + 60887). Centralizing that fallback here is the right move.
159+
- The daemon defaults are well-known and stable. There's no "right" error message to surface — "use loopback" is always the answer.
160+
- If a binary genuinely needs to know whether the file existed (e.g. for a setup-wizard prompt), it can call `bootstrap_path()` and `Path::exists()` directly.
161+
162+
`read_project_id` *does* return `Result` because there's no sane default for "I asked for a project ID and there isn't one" — the caller has to decide what that means.
163+
164+
### Why Listen-Address Normalization Lives in `daemon_url`, Not `bootstrap`
165+
166+
`bootstrap` returns the raw endpoint as written so callers can distinguish "user configured `0.0.0.0` for LAN exposure" from "user configured `127.0.0.1`." `daemon_url` is the layer concerned with *dialing*, so that's where the rewrite happens. Diagnostic tooling that wants to display the actual `bind_host` (e.g. `ghook --diagnose`) reads from `bootstrap` directly.
167+
168+
### Why Not Re-Export from a Prelude
169+
170+
There's no `gobby_core::prelude`. The crate is small enough that explicit imports (`use gobby_core::project::find_project_root`) are clearer than a glob. Keep it that way until the public surface grows past ~10 items.

0 commit comments

Comments
 (0)