Skip to content

Commit f0752d8

Browse files
authored
feat(regent): integrate re_gent audit trail for AI agents (#73)
* feat: integrate regent for claude agents * docs(regent): document
1 parent 4078d79 commit f0752d8

29 files changed

Lines changed: 3416 additions & 161 deletions

ARCHITECTURE.md

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,23 @@ cmd/biomelab/
1414
1515
internal/
1616
gui/
17-
app.go FyneApp: window, HSplit layout, multi-repo management, mode switching
18-
dashboard.go Right panel: main card + scrollable linked cards grid, refresh timestamps
19-
card.go Worktree card rendering: branch, path, PR, agents, IDEs, status
20-
repo_panel.go Left panel: tappable VBox of repo/mode items (NOT widget.Tree)
21-
repo_panel_drag.go Drag-handle widget + reorder math for repo panel
22-
shortcuts.go All keyboard handling: handleKeyName + handleRune, navigation, operations
23-
keycapture.go desktop.Canvas.SetOnKeyDown setup, zoom shortcuts
24-
dialogs.go Confirmation dialogs (delete, sandbox create/remove, send PR flow)
25-
input_dialogs.go Input dialogs (branch, PR ref, repo path, agent select)
26-
refresh.go RefreshManager: goroutine tickers for local (5s) and network refresh
27-
state.go RepoState: domain + UI state, worktree sorting
28-
theme.go Dark theme with zoom support (Ctrl+/Ctrl-)
29-
icon.go AppIcon resource (set from embedded icon at startup)
30-
systray.go System tray: Show/Hide toggle, Quit
17+
app.go FyneApp: window, HSplit layout, multi-repo management, mode switching
18+
dashboard.go Right panel: main card + scrollable linked cards grid, refresh timestamps
19+
card.go Worktree card rendering: branch, path, PR, agents, IDEs, status
20+
repo_panel.go Left panel: tappable VBox of repo/mode items (NOT widget.Tree)
21+
repo_panel_drag.go Drag-handle widget + reorder math for repo panel
22+
shortcuts.go All keyboard handling: handleKeyName + handleRune, navigation, operations
23+
keycapture.go desktop.Canvas.SetOnKeyDown setup, zoom shortcuts
24+
dialogs.go Confirmation dialogs (delete, sandbox create/remove, send PR flow)
25+
input_dialogs.go Input dialogs (branch, PR ref, repo path, agent select)
26+
refresh.go RefreshManager: goroutine tickers for local (5s) and network refresh
27+
state.go RepoState: domain + UI state, worktree sorting
28+
theme.go Dark theme with zoom support (Ctrl+/Ctrl-)
29+
icon.go AppIcon resource (set from embedded icon at startup)
30+
systray.go System tray: Show/Hide toggle, Quit, Dependencies summary
31+
sysdeps_dialog.go System Dependencies modal + first-run banner
32+
regent_log_dialog.go Regent activity window (single instance, resizable, JSON export)
33+
save_file.go OS-native save dialog (osascript/zenity/PowerShell) + Fyne fallback
3134
3235
ops/
3336
refresh.go QuickRefresh, LocalRefresh, NetworkRefresh, CardRefresh
@@ -36,6 +39,7 @@ internal/
3639
3740
config/config.go Repo list persistence (~/.config/biomelab/repos.json)
3841
git/worktree.go Go-git v6 wrapper: list, create, remove, pull, fetch, sync status
42+
git/exclude.go Per-worktree git info/exclude writer (used by notes + regent)
3943
git/credential.go Git credential helper protocol (git credential fill)
4044
agent/ Agent kind registry + process detection
4145
ide/ IDE kind registry + process detection
@@ -44,6 +48,9 @@ internal/
4448
sandbox/sandbox.go Docker Sandbox (sbx) CLI wrapper
4549
terminal/terminal.go Open new terminal window (macOS .command / Linux x-terminal-emulator)
4650
github/pr.go GitHub-specific PR helpers (ParsePRRef, ValidatePR)
51+
notes/notes.go Per-worktree Markdown notes (.biomelab/note.md, pr-title.md)
52+
regent/ re_gent (rgt) integration: detection, init, hook install, log fetch
53+
sysdeps/ External CLI dependency checks (gh/glab/sbx/rgt) + cache
4754
```
4855

4956
## Key dependencies
@@ -176,6 +183,80 @@ skill (and similar tools) target: write the generated title and description
176183
to those two paths, and biomelab turns them into the actual PR on the next
177184
`Shift+P`.
178185

186+
## re_gent integration
187+
188+
[re_gent](https://github.com/regent-vcs/re_gent) captures every agent turn
189+
(prompt, reply, tool calls) into a content-addressed `.regent/` directory
190+
per worktree. Biomelab wraps it so the integration is invisible until the
191+
user installs `rgt`:
192+
193+
- **Auto-init on host worktrees.** `ops.CreateWorktree` runs
194+
`regent.EnsureInit` after every new worktree creation, and
195+
`app.buildRepoEntry` walks existing worktrees on startup
196+
(`migrateRegentForRepo`) so an `rgt install` retroactively wires up
197+
every regular-mode worktree on the next refresh. Sandbox worktrees
198+
skip this path — rgt belongs inside the container, installed via the
199+
regent kit.
200+
- **`EnsureInit` runs `rgt init --skip-hook --skip-skills`** with
201+
`cmd.Dir = wtPath`. `rgt init` ignores positional path args and always
202+
operates on `cwd`, so `cmd.Dir` is mandatory. The `--skip-hook` flag is
203+
used because rgt's interactive installer needs a TTY biomelab can't
204+
provide; we install Claude hooks ourselves.
205+
- **`EnsureClaudeHooks` writes `.claude/settings.json`** with three event
206+
hooks (`UserPromptSubmit`, `Stop`, `PostToolBatch`) pointing at
207+
`rgt message-hook` / `rgt tool-batch-hook`. Idempotent: rgt-related
208+
entries are deduplicated on each call, non-rgt entries are preserved.
209+
The JSON shape is ported from rgt's own installer
210+
(`internal/cli/init.go`, Apache-2.0, attributed in source).
211+
- **Git exclude.** `git.EnsureExcluded` writes `/.regent/` (and
212+
`/.biomelab/`) to the worktree's common `info/exclude` so neither
213+
directory shows up in `git status`. Extracted to `internal/git` so
214+
both `notes` and `regent` share one helper (used to live in `notes`,
215+
caused a `git → notes` import that prevented `notes → git`).
216+
- **Log viewer.** `l` shortcut opens `regent_log_dialog.go`, a single
217+
top-level resizable window. Content comes from `rgt log --json`
218+
(parsed in `internal/regent/log_json.go`) and renders structured rows:
219+
`sha · timestamp · origin`, `Human:` prompt, `Agent:` reply, then a
220+
`▶ N tools` toggle that reveals one row per tool call with the
221+
primary arg inline (file_path / command / query) and the rest below.
222+
Full file paths, no truncation. Single window: pressing `l` on a
223+
different card reuses the open window (`App.regentLogWindow` +
224+
`regentLogReload`).
225+
- **JSON export.** The window's `Export JSON…` button calls
226+
`regent.LogJSONRaw` and saves the bytes via the OS-native save dialog
227+
helper (`gui/save_file.go`: osascript on macOS, zenity/kdialog on
228+
Linux, PowerShell on Windows; falls back to `dialog.NewFileSave`).
229+
230+
## System dependencies
231+
232+
`internal/sysdeps` is the registry of external CLIs biomelab cares
233+
about. Each `Check` has a `Probe` that returns `Result{Status, Version,
234+
Note}`; a `Cache` (default TTL 30s) memoizes results across the systray
235+
menu, the dialog, and the first-run banner.
236+
237+
Two filtering layers apply before rendering:
238+
239+
- `ApplySuppression` drops `Missing` entries whose `SuppressIfAny` list
240+
names another check that is currently `OK` or `Degraded`. Used so
241+
`glab missing` doesn't appear when `gh` is installed.
242+
- `ApplyVisibility` drops `Missing` entries whose `Applies(cfg)` callback
243+
says the tool isn't relevant. Used so `sbx` doesn't appear at all
244+
when the user has no sandbox-mode repo. **Important:** `Applies`
245+
gates visibility of missing entries only — installed tools always
246+
show as green even when the user's config doesn't strictly need them.
247+
That way `sbx v0.29.0` is reported correctly on machines where the
248+
user just happens to have it.
249+
- `Partition` splits the surviving entries into a primary list and an
250+
optional list (`Optional && Missing`). The dialog renders the
251+
primary list at the top and the optional list (currently just `rgt`)
252+
under an "Optional tools" heading.
253+
254+
The systray label is `Dependencies: N/M ✓` (or
255+
`Dependencies: N/M (X need attention)`), where N/M counts only the
256+
visible primary entries. Clicking opens the dialog; the dialog's
257+
**Re-check** button invalidates the cache and refreshes the systray
258+
label in one call.
259+
179260
## Pitfalls
180261

181262
- go-git v6 is a pseudo-version. Do NOT use a `replace` directive.
@@ -185,3 +266,7 @@ to those two paths, and biomelab turns them into the actual PR on the next
185266
- `widget.Button` implements Focusable — don't put buttons in the main content.
186267
- IDE `ProcessPatterns` order matters: specific before broad (`"nvim"` before `"vim"`).
187268
- Always bounds-check `a.active < len(a.repos)` before accessing repos.
269+
- `rgt init` ignores positional path args and operates on `cwd`. Use `cmd.Dir`, not `rgt init <path>`.
270+
- `rgt init` hook installer needs a TTY; biomelab writes `.claude/settings.json` itself via `regent.EnsureClaudeHooks`. Don't rely on `--agent claude` to skip the prompt — it doesn't.
271+
- `widget.Accordion` misbehaves inside `container.NewVScroll` (clicks don't toggle). Use a button + visibility toggle instead — see the regent log dialog's tools collapsible.
272+
- The shared regent log window is keyed by `App.regentLogWindow` (single instance). Don't spawn a new window per worktree; reuse via `regentLogReload`.

CLAUDE.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,29 @@ Go version: 1.25+ (check `go env GOROOT` if you hit version mismatches).
3939
- **go-git v6** (unreleased, from main branch) -- All git operations.
4040
- **gopsutil** -- Cross-platform process detection.
4141
- **gh CLI / glab CLI** -- External tools for PR/MR status.
42+
- **rgt CLI** ([re_gent](https://github.com/regent-vcs/re_gent)) -- Optional. When installed, biomelab auto-initializes `.regent/` per worktree and wires Claude Code hooks.
4243

4344
## Package layout
4445

4546
```
4647
cmd/biomelab/ Entry point, icon embedding, PATH expansion
4748
internal/
4849
gui/ Fyne GUI: app, dashboard, cards, repo panel, dialogs,
49-
keyboard handling, theme, refresh manager, system tray
50+
keyboard handling, theme, refresh manager, system tray,
51+
sysdeps dialog + banner, regent log window, OS-native
52+
save dialog helper
5053
ops/ Shared business operations (refresh, worktree CRUD,
5154
sandbox ops) — extracted from old TUI for reuse
5255
config/ Repo list persistence (~/.config/biomelab/repos.json)
53-
git/ Go-git v6 wrapper
56+
git/ Go-git v6 wrapper + EnsureExcluded helper
5457
agent/ Agent process detection
5558
ide/ IDE process detection
5659
process/ Shared process enumeration
5760
provider/ PR/MR provider abstraction (GitHub, GitLab)
5861
sandbox/ Docker Sandbox (sbx) CLI wrapper
62+
notes/ Per-worktree Markdown notes (.biomelab/note.md)
63+
regent/ re_gent (rgt) integration: detect, init, hooks, log
64+
sysdeps/ External CLI dependency checks (gh/glab/sbx/rgt)
5965
terminal/ Open new terminal window
6066
github/ GitHub-specific PR helpers
6167
```
@@ -88,6 +94,17 @@ Always run `go test -race ./...`.
8894
prefix truncation (`truncatePath`), PR text uses suffix truncation.
8995
- **Card grid navigation** — up/down jump by column count, left/right by 1.
9096
Column count computed from `dashSlot.Size().Width / cardCellSize().Width`.
97+
- **`rgt init` ignores positional path args** — it always operates on `cwd`.
98+
Use `cmd.Dir = wtPath`, not `rgt init <wtPath>`.
99+
- **rgt hook installer needs a TTY**`--agent claude` doesn't skip the
100+
prompt in a non-TTY environment. Biomelab writes `.claude/settings.json`
101+
itself via `regent.EnsureClaudeHooks` (ported from rgt upstream, Apache-2.0).
102+
- **`widget.Accordion` misbehaves inside `VScroll`** — clicks don't toggle.
103+
Use a `widget.Button` that flips the inner container's visibility instead;
104+
see the regent log dialog's tools collapsible for the pattern.
105+
- **One regent log window at a time** — keyed by `App.regentLogWindow` plus
106+
a `regentLogReload` closure. Different cards reuse the same window via
107+
reload; don't spawn a new one per worktree.
91108

92109
## Release process
93110

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
- **Open in terminal** -- Press `Enter` to open a worktree in a terminal. If a terminal is already detected for that worktree, it is brought to the foreground instead of opening a new one. On macOS, activation uses TTY matching via AppleScript (requires Automation permission on first use).
2323
- **Open in editor** -- Press `e` to open in `$BIOME_EDITOR` (defaults to VS Code).
2424
- **Task notes** -- Press `m` (or right-click a card) to open a Markdown editor with a live preview, scoped to that worktree. Notes are stored at `<worktree>/.biomelab/note.md` (description) and `<worktree>/.biomelab/pr-title.md` (single-line title), auto-excluded from git, and mounted into the sandbox alongside the source so agents can read them. When you `Shift+P` to send a PR, biomelab offers to use the prepared title and description in place of the commit-derived defaults. External tools that write to those two paths become contributors to the next PR.
25+
- **Agent audit trail** ([re_gent](https://github.com/regent-vcs/re_gent)) -- When `rgt` is installed, biomelab auto-initializes `.regent/` in every regular-mode worktree and writes Claude Code hooks into `.claude/settings.json` — no terminal step. Press `l` (or systray → Dependencies) to open the activity window: one resizable view per session with `Human` / `Agent` rows, collapsible tool lists (full file paths, no truncation), and an **Export JSON…** button that writes the raw `rgt log --json` via the OS-native save dialog.
26+
- **System dependencies dialog** -- Systray entry (`Dependencies: N/M ✓`) opens a modal listing every external CLI biomelab relies on (`gh`, `glab`, `sbx`, `rgt`) with status dot, version, install hint, and docs link. A first-run banner above the dashboard nags only when a primary tool is missing or degraded; redundant CLIs (e.g. `glab` when `gh` is fine) are suppressed.
2527
- **Zoom** -- `Ctrl+=` / `Ctrl+-` / `Ctrl+0` to scale the UI font.
2628
- **System tray** -- Closing the window hides to system tray. Tray menu toggles Show/Hide.
2729
- **Auto-refresh** -- Local state refreshes every 5s, network state every 30s (configurable).
@@ -126,6 +128,7 @@ Launch `biomelab` from any directory, or open `Biomelab.app` from Spotlight/Find
126128
| `Enter` | Activate existing terminal or open new | Any card |
127129
| `e` | Open in editor | Any card |
128130
| `m` | Open note editor (right-click also works) | Any card |
131+
| `l` | Open regent activity log | Any card |
129132
| `c` | Create worktree | Main card |
130133
| `f` | Fetch PR/MR | Main card |
131134
| `d` | Delete worktree / remove sandbox | Linked: delete; Main+sandbox: remove |
@@ -136,6 +139,9 @@ Launch `biomelab` from any directory, or open `Biomelab.app` from Spotlight/Find
136139
| `s` | Start stopped sandbox | Main card |
137140
| `Shift+S` | Stop running sandbox | Main card |
138141
| `k` | Pick kits → create (if missing) or recreate sandbox with `--kit` flags | Sandbox mode |
142+
| `g` | Toggle kanban / grid view | Global |
143+
| `Tab` | Toggle focus between panels | Global |
144+
| `Ctrl+T` | Toggle dark / light theme | Global |
139145
| `Ctrl+=` | Zoom in | Global |
140146
| `Ctrl+-` | Zoom out | Global |
141147
| `Ctrl+0` | Reset zoom | Global |

internal/git/exclude.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package git
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
// excludeDirPerm and excludeFilePerm are the standard git info/exclude
13+
// file permissions (mirrors what `git worktree add` produces).
14+
const (
15+
excludeDirPerm = 0o755
16+
excludeFilePerm = 0o644
17+
)
18+
19+
// EnsureExcluded appends line to the worktree's info/exclude file if it
20+
// isn't already present. Idempotent. Used by sidecar packages (notes,
21+
// regent) to hide per-worktree directories from `git status`.
22+
//
23+
// Git reads info/exclude from the common gitdir for both the main worktree
24+
// and any linked worktrees — the per-worktree
25+
// .git/worktrees/<name>/info/exclude is created by `git worktree add` but
26+
// is not honored by `git status` (verified empirically). This function
27+
// resolves the right file in both cases:
28+
//
29+
// - main worktree: .git is a directory → <wt>/.git/info/exclude
30+
// - linked worktree: .git is a file with "gitdir: <wt-gitdir>" → read
31+
// <wt-gitdir>/commondir to find the common gitdir, then
32+
// <common-gitdir>/info/exclude.
33+
//
34+
// As a side effect, all worktrees of a repo share the same exclude file.
35+
// Patterns are anchored to the worktree root at match time (e.g.
36+
// "/.regent/"), so each worktree's own sidecar dir stays hidden.
37+
func EnsureExcluded(worktreeDir, line string) error {
38+
excludePath, err := excludeFilePath(worktreeDir)
39+
if err != nil {
40+
return err
41+
}
42+
if err := os.MkdirAll(filepath.Dir(excludePath), excludeDirPerm); err != nil {
43+
return fmt.Errorf("create info dir: %w", err)
44+
}
45+
existing, err := os.ReadFile(excludePath)
46+
if err != nil && !errors.Is(err, os.ErrNotExist) {
47+
return fmt.Errorf("read exclude: %w", err)
48+
}
49+
if hasExcludeLine(existing, line) {
50+
return nil
51+
}
52+
f, err := os.OpenFile(excludePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, excludeFilePerm)
53+
if err != nil {
54+
return fmt.Errorf("open exclude: %w", err)
55+
}
56+
var buf strings.Builder
57+
if len(existing) > 0 && !strings.HasSuffix(string(existing), "\n") {
58+
buf.WriteString("\n")
59+
}
60+
buf.WriteString(line)
61+
buf.WriteString("\n")
62+
if _, werr := f.WriteString(buf.String()); werr != nil {
63+
_ = f.Close()
64+
return fmt.Errorf("write exclude: %w", werr)
65+
}
66+
if cerr := f.Close(); cerr != nil {
67+
return fmt.Errorf("close exclude: %w", cerr)
68+
}
69+
return nil
70+
}
71+
72+
func excludeFilePath(worktreeDir string) (string, error) {
73+
gitPath := filepath.Join(worktreeDir, ".git")
74+
info, err := os.Stat(gitPath)
75+
if err != nil {
76+
return "", fmt.Errorf("stat .git: %w", err)
77+
}
78+
if info.IsDir() {
79+
return filepath.Join(gitPath, "info", "exclude"), nil
80+
}
81+
data, err := os.ReadFile(gitPath)
82+
if err != nil {
83+
return "", fmt.Errorf("read .git: %w", err)
84+
}
85+
const prefix = "gitdir:"
86+
line := strings.TrimSpace(string(data))
87+
if !strings.HasPrefix(line, prefix) {
88+
return "", fmt.Errorf(".git file missing %q prefix", prefix)
89+
}
90+
wtGitDir := strings.TrimSpace(strings.TrimPrefix(line, prefix))
91+
92+
commonDir := wtGitDir
93+
if raw, rerr := os.ReadFile(filepath.Join(wtGitDir, "commondir")); rerr == nil {
94+
cd := strings.TrimSpace(string(raw))
95+
if !filepath.IsAbs(cd) {
96+
cd = filepath.Join(wtGitDir, cd)
97+
}
98+
commonDir = filepath.Clean(cd)
99+
}
100+
return filepath.Join(commonDir, "info", "exclude"), nil
101+
}
102+
103+
func hasExcludeLine(content []byte, line string) bool {
104+
scanner := bufio.NewScanner(strings.NewReader(string(content)))
105+
for scanner.Scan() {
106+
if strings.TrimSpace(scanner.Text()) == line {
107+
return true
108+
}
109+
}
110+
return false
111+
}

internal/git/worktree.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ import (
1818
"github.com/go-git/go-git/v6/plumbing"
1919
githttp "github.com/go-git/go-git/v6/plumbing/transport/http"
2020
xworktree "github.com/go-git/go-git/v6/x/plumbing/worktree"
21+
)
2122

22-
"github.com/mdelapenya/biomelab/internal/notes"
23+
// biomelabDir is the per-worktree sidecar directory that biomelab uses
24+
// to store notes, PR drafts, and other transient state. Kept under the
25+
// worktree so it travels with the sandbox bind mount.
26+
const (
27+
biomelabDir = ".biomelab"
28+
biomelabExcludeLine = "/.biomelab/"
2329
)
2430

2531
// SyncStatus indicates whether a branch is up-to-date with its remote tracking branch.
@@ -991,10 +997,15 @@ func (r *Repository) HasStash() (bool, error) {
991997
}
992998

993999
// ensureBiomelabDir ensures the .biomelab directory exists in the given
994-
// worktree path so external tools can write files without creating it themselves.
1000+
// worktree path and that it's git-excluded. External tools (pr-scribe,
1001+
// note editor) write files inside without needing to create the directory
1002+
// themselves, and the entries never show up in `git status`.
9951003
// Errors are silently ignored as this is a best-effort initialization.
9961004
func (r *Repository) ensureBiomelabDir(wtPath string) error {
997-
return notes.EnsureDir(wtPath)
1005+
if err := os.MkdirAll(filepath.Join(wtPath, biomelabDir), 0o755); err != nil {
1006+
return err
1007+
}
1008+
return EnsureExcluded(wtPath, biomelabExcludeLine)
9981009
}
9991010

10001011
// MigrateWorktreeDirs ensures .biomelab exists for all worktrees in the

0 commit comments

Comments
 (0)