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
9 changes: 4 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ All code changes follow Red-Green-Refactor:

### Database

Single global database at `~/.config/swarm-tools/swarm.db`.
Schema is compatible with cyborg-swarm's libSQL database.
Single global database at `~/.config/uf/replicator/replicator.db`.
Use in-memory databases for tests (`db.OpenMemory()`).

### MCP Protocol
Expand Down Expand Up @@ -228,8 +227,8 @@ make install # Install to GOPATH/bin

| Command | Purpose |
|---------|---------|
| `replicator init` | Per-repo setup: creates `.hive/` with empty `cells.json` |
| `replicator setup` | Per-machine setup: creates `~/.config/swarm-tools/` + SQLite DB |
| `replicator init` | Per-repo setup: creates `.uf/replicator/` with empty `cells.json` |
| `replicator setup` | Per-machine setup: creates `~/.config/uf/replicator/` + SQLite DB |
| `replicator serve` | Start MCP JSON-RPC server on stdio |
| `replicator cells` | List hive cells (work items) |
| `replicator doctor` | Check environment health |
Expand Down Expand Up @@ -271,7 +270,7 @@ originally by [Joel Hooks](https://github.com/joelhooks).

## Active Technologies
- Go 1.25+ + `cobra` (CLI), `modernc.org/sqlite` (pure Go SQLite), stdlib `encoding/json` (MCP JSON-RPC), stdlib `os/exec` (git operations) (001-go-rewrite-phases)
- SQLite at `~/.config/swarm-tools/swarm.db` (WAL mode, compatible with cyborg-swarm) (001-go-rewrite-phases)
- SQLite at `~/.config/uf/replicator/replicator.db` (WAL mode) (001-go-rewrite-phases)
- Go 1.25+ + `charmbracelet/lipgloss v1.1.0`, `charmbracelet/log v1.0.0`, `muesli/termenv v0.16.0`, `charmbracelet/lipgloss/table` (sub-package of lipgloss) (002-charm-ux)
- SQLite via `modernc.org/sqlite` (unchanged) (002-charm-ux)

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ Download from [GitHub Releases](https://github.com/unbound-force/replicator/rele
## Usage

```bash
# Per-repo setup (creates .hive/ directory)
# Per-repo setup (creates .uf/replicator/ directory)
replicator init

# Per-machine setup (creates ~/.config/swarm-tools/ + SQLite DB)
# Per-machine setup (creates ~/.config/uf/replicator/ + SQLite DB)
replicator setup

# Start MCP server (AI agents connect via stdio)
Expand Down Expand Up @@ -116,7 +116,7 @@ For Claude Code, add to `mcp_servers` in your config:

| Variable | Default | Purpose |
|----------|---------|---------|
| `REPLICATOR_DB` | `~/.config/swarm-tools/swarm.db` | SQLite database path |
| `REPLICATOR_DB` | `~/.config/uf/replicator/replicator.db` | SQLite database path |
| `DEWEY_MCP_URL` | `http://localhost:3333/mcp/` | Dewey semantic memory endpoint |
| `ZEN_API_KEY` | *(none)* | OpenCode Zen gateway for LLM calls |

Expand Down
14 changes: 7 additions & 7 deletions cmd/replicator/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func initCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize a project directory for swarm operations",
Long: `Creates a .hive/ directory with an empty cells.json in the target
Long: `Creates a .uf/replicator/ directory with an empty cells.json in the target
directory. Idempotent — safe to run multiple times.

This is the per-repo initialization command. It does not require the
Expand All @@ -23,25 +23,25 @@ global database (replicator setup) or any external services.`,
return runInit(pathFlag)
},
}
cmd.Flags().StringVar(&pathFlag, "path", ".", "Target directory for .hive/ initialization")
cmd.Flags().StringVar(&pathFlag, "path", ".", "Target directory for .uf/replicator/ initialization")
return cmd
}

// runInit creates the .hive/ directory and seeds cells.json.
// runInit creates the .uf/replicator/ directory and seeds cells.json.
// Uses styled output: green for success, dim for already-initialized.
func runInit(targetDir string) error {
styles := ui.NewStyles(os.Stdout)
hiveDir := filepath.Join(targetDir, ".hive")
hiveDir := filepath.Join(targetDir, ".uf", "replicator")

// Check if already initialized.
if info, err := os.Stat(hiveDir); err == nil && info.IsDir() {
fmt.Println(styles.Dim.Render("already initialized"))
return nil
}

// Create .hive/ directory.
// Create .uf/replicator/ directory.
if err := os.MkdirAll(hiveDir, 0o755); err != nil {
return fmt.Errorf("create .hive directory: %w", err)
return fmt.Errorf("create .uf/replicator directory: %w", err)
}

// Write empty cells.json.
Expand All @@ -50,6 +50,6 @@ func runInit(targetDir string) error {
return fmt.Errorf("write cells.json: %w", err)
}

fmt.Println(styles.Pass.Render("initialized .hive/"))
fmt.Println(styles.Pass.Render("initialized .uf/replicator/"))
return nil
}
10 changes: 5 additions & 5 deletions cmd/replicator/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ func TestRunInit_FreshDirectory(t *testing.T) {
t.Fatalf("runInit: %v", err)
}

hiveDir := filepath.Join(dir, ".hive")
hiveDir := filepath.Join(dir, ".uf", "replicator")
info, err := os.Stat(hiveDir)
if err != nil {
t.Fatalf(".hive/ not created: %v", err)
t.Fatalf(".uf/replicator/ not created: %v", err)
}
if !info.IsDir() {
t.Fatal(".hive/ is not a directory")
t.Fatal(".uf/replicator/ is not a directory")
}

cellsPath := filepath.Join(hiveDir, "cells.json")
Expand All @@ -40,7 +40,7 @@ func TestRunInit_AlreadyInitialized(t *testing.T) {
}

// Write something to cells.json to verify it's not overwritten.
cellsPath := filepath.Join(dir, ".hive", "cells.json")
cellsPath := filepath.Join(dir, ".uf", "replicator", "cells.json")
os.WriteFile(cellsPath, []byte(`[{"id":"test"}]`), 0o644)

// Second init — should be idempotent.
Expand All @@ -64,7 +64,7 @@ func TestRunInit_CustomPath(t *testing.T) {
t.Fatalf("runInit with custom path: %v", err)
}

cellsPath := filepath.Join(target, ".hive", "cells.json")
cellsPath := filepath.Join(target, ".uf", "replicator", "cells.json")
if _, err := os.Stat(cellsPath); err != nil {
t.Fatalf("cells.json not created at custom path: %v", err)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/replicator/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ func serveMCP() error {
}

// setupLogger creates a charmbracelet/log logger that writes to both
// stderr and .unbound-force/replicator.log. If the log file cannot be
// stderr and .uf/replicator/replicator.log. If the log file cannot be
// created, logging falls back to stderr only and a warning is printed.
// The returned io.Closer should be deferred by the caller; it may be nil.
func setupLogger() (*charmlog.Logger, io.Closer) {
logDir := filepath.Join(".", ".unbound-force")
logDir := filepath.Join(".", ".uf", "replicator")
if err := os.MkdirAll(logDir, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "warning: cannot create log directory: %v (logging to stderr only)\n", err)
return charmlog.NewWithOptions(os.Stderr, charmlog.Options{
Expand Down
10 changes: 5 additions & 5 deletions cmd/replicator/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func TestSetupLogger_CreatesLogFile(t *testing.T) {
// Run setupLogger in a temp directory so it creates
// .unbound-force/replicator.log there.
// .uf/replicator/replicator.log there.
dir := t.TempDir()
orig, err := os.Getwd()
if err != nil {
Expand All @@ -27,7 +27,7 @@ func TestSetupLogger_CreatesLogFile(t *testing.T) {
t.Fatal("expected non-nil logger")
}

logPath := filepath.Join(dir, ".unbound-force", "replicator.log")
logPath := filepath.Join(dir, ".uf", "replicator", "replicator.log")
if _, err := os.Stat(logPath); err != nil {
t.Fatalf("log file not created: %v", err)
}
Expand All @@ -45,7 +45,7 @@ func TestSetupLogger_Truncates(t *testing.T) {
t.Cleanup(func() { os.Chdir(orig) })

// First call: write a marker to the log file.
logDir := filepath.Join(dir, ".unbound-force")
logDir := filepath.Join(dir, ".uf", "replicator")
os.MkdirAll(logDir, 0o755)
logPath := filepath.Join(logDir, "replicator.log")
os.WriteFile(logPath, []byte("MARKER_SHOULD_BE_GONE"), 0o644)
Expand Down Expand Up @@ -74,8 +74,8 @@ func TestSetupLogger_ReadOnlyDir(t *testing.T) {
t.Fatalf("Getwd: %v", err)
}

// Create the .unbound-force dir as read-only so file creation fails.
logDir := filepath.Join(dir, ".unbound-force")
// Create the .uf/replicator dir as read-only so file creation fails.
logDir := filepath.Join(dir, ".uf", "replicator")
os.MkdirAll(logDir, 0o755)
os.Chmod(logDir, 0o444)
t.Cleanup(func() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/replicator/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func runSetup() error {
return fmt.Errorf("determine home directory: %w", err)
}

configDir := filepath.Join(home, ".config", "swarm-tools")
configDir := filepath.Join(home, ".config", "uf", "replicator")
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("create config directory: %w", err)
}
Expand Down
9 changes: 4 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Package config manages replicator configuration.
//
// The global database lives at ~/.config/swarm-tools/swarm.db
// (shared with the TypeScript cyborg-swarm for migration compatibility).
// The global database lives at ~/.config/uf/replicator/replicator.db.
package config

import (
Expand Down Expand Up @@ -33,11 +32,11 @@ func Load() *Config {
func defaultDatabasePath() string {
home, err := os.UserHomeDir()
if err != nil {
return "swarm.db"
return "replicator.db"
}
dir := filepath.Join(home, ".config", "swarm-tools")
dir := filepath.Join(home, ".config", "uf", "replicator")
_ = os.MkdirAll(dir, 0o755)
return filepath.Join(dir, "swarm.db")
return filepath.Join(dir, "replicator.db")
}

func envOr(key, fallback string) string {
Expand Down
2 changes: 1 addition & 1 deletion internal/doctor/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func checkConfigDir() CheckResult {
}
}

configDir := home + "/.config/swarm-tools"
configDir := home + "/.config/uf/replicator"
info, err := os.Stat(configDir)
elapsed := time.Since(start)

Expand Down
12 changes: 6 additions & 6 deletions internal/hive/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import (

// Sync serializes all cells to JSON and commits them to git.
//
// Writes cells to <projectPath>/.hive/cells.json, then runs
// `git add .hive/ && git commit -m "hive sync"` in the project directory.
// Writes cells to <projectPath>/.uf/replicator/cells.json, then runs
// `git add .uf/replicator/ && git commit -m "hive sync"` in the project directory.
func Sync(store *db.Store, projectPath string) error {
cells, err := QueryCells(store, CellQuery{Limit: 10000})
if err != nil {
return fmt.Errorf("query cells for sync: %w", err)
}

hiveDir := filepath.Join(projectPath, ".hive")
hiveDir := filepath.Join(projectPath, ".uf", "replicator")
if err := os.MkdirAll(hiveDir, 0o755); err != nil {
return fmt.Errorf("create .hive dir: %w", err)
return fmt.Errorf("create .uf/replicator dir: %w", err)
}

data, err := json.MarshalIndent(cells, "", " ")
Expand All @@ -35,8 +35,8 @@ func Sync(store *db.Store, projectPath string) error {
return fmt.Errorf("write cells.json: %w", err)
}

// Stage and commit the .hive directory.
addCmd := exec.Command("git", "add", ".hive/")
// Stage and commit the .uf/replicator directory.
addCmd := exec.Command("git", "add", ".uf/replicator/")
addCmd.Dir = projectPath
if out, err := addCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git add: %w\n%s", err, out)
Expand Down
8 changes: 4 additions & 4 deletions internal/hive/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestSync(t *testing.T) {
}

// Verify cells.json was created.
cellsPath := filepath.Join(dir, ".hive", "cells.json")
cellsPath := filepath.Join(dir, ".uf", "replicator", "cells.json")
data, err := os.ReadFile(cellsPath)
if err != nil {
t.Fatalf("read cells.json: %v", err)
Expand Down Expand Up @@ -83,14 +83,14 @@ func TestSync_CreatesHiveDir(t *testing.T) {
}
}

// Sync with no cells -- should still create .hive/cells.json.
// Sync with no cells -- should still create .uf/replicator/cells.json.
if err := Sync(store, dir); err != nil {
t.Fatalf("Sync: %v", err)
}

hiveDir := filepath.Join(dir, ".hive")
hiveDir := filepath.Join(dir, ".uf", "replicator")
if _, err := os.Stat(hiveDir); os.IsNotExist(err) {
t.Error(".hive directory was not created")
t.Error(".uf/replicator directory was not created")
}
}

Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/add-init-command/design.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Context

Replicator has `setup` (per-machine: `~/.config/swarm-tools/` + SQLite) but no `init` (per-repo). The `uf` CLI needs `replicator init` to exist so it can delegate to it during `uf init`, following the same pattern as `dewey init`.
Replicator has `setup` (per-machine: `~/.config/uf/replicator/` + SQLite) but no `init` (per-repo). The `uf` CLI needs `replicator init` to exist so it can delegate to it during `uf init`, following the same pattern as `dewey init`.

The existing `setup.go` pattern: cobra command → `runSetup()` function → filesystem ops → print status. Follow this exactly.

Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/rename-paths/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: unbound-force
created: 2026-04-06
38 changes: 38 additions & 0 deletions openspec/changes/rename-paths/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Context

Three different directory names from the upstream fork need consolidation:

| Current | New | Purpose |
|---------|-----|---------|
| `[repo]/.hive/cells.json` | `[repo]/.uf/replicator/cells.json` | Per-repo cell state |
| `[repo]/.unbound-force/replicator.log` | `[repo]/.uf/replicator/replicator.log` | Per-repo MCP session log |
| `~/.config/swarm-tools/swarm.db` | `~/.config/uf/replicator/replicator.db` | Per-machine SQLite database |

## Goals / Non-Goals

### Goals
- Consolidate all replicator paths under `.uf/replicator/` (per-repo) and `~/.config/uf/replicator/` (per-machine)
- Rename database file from `swarm.db` to `replicator.db`
- Update all Go source, tests, documentation, and spec artifacts

### Non-Goals
- Auto-migration from old paths (clean break)
- Changing the `.unbound-force/config.yaml` ecosystem config (that's UF-level, not replicator-specific)
- Changing `.opencode/` or `.specify/` directory structures
- Modifying the LICENSE attribution text

## Decisions

**D1: Pure find-and-replace.** Every change is a string constant swap. No logic modifications, no new functions, no behavioral changes.

**D2: No migration path.** The old `~/.config/swarm-tools/swarm.db` is abandoned. Users run `replicator setup` to create the new directory. This is a clean break -- the cyborg-swarm TypeScript version can no longer share the same database.

**D3: Update spec artifacts.** Historical spec files will be updated to reflect the new paths for consistency, since the old paths will cause confusion if someone reads them.

**D4: Database renamed to `replicator.db`.** Complete naming break from `swarm.db`. Matches the tool name.

## Risks / Trade-offs

**Risk: Existing users lose data.** Anyone with cells/events in `~/.config/swarm-tools/swarm.db` will start fresh. Mitigation: this is a pre-release tool with no external users yet. The old database can be manually copied if needed.

**Trade-off: Breaks cyborg-swarm compatibility.** The Go and TypeScript versions can no longer share a database. This is intentional -- replicator is replacing cyborg-swarm, not coexisting with it long-term.
38 changes: 38 additions & 0 deletions openspec/changes/rename-paths/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Why

Replicator uses three different directory names inherited from the upstream fork: `.hive/` (per-repo cells), `.unbound-force/` (per-repo logs), and `~/.config/swarm-tools/` (per-machine database). These names are inconsistent, carry baggage from the TypeScript origin, and don't align with the `uf` CLI ecosystem where `uf` is the common root namespace.

Consolidating under `.uf/replicator/` (per-repo) and `~/.config/uf/replicator/` (per-machine) gives every replicator artifact a predictable, discoverable location within the UF namespace.

## What Changes

### Modified Capabilities
- Per-repo hive state moves from `[repo]/.hive/cells.json` to `[repo]/.uf/replicator/cells.json`
- Per-repo MCP log moves from `[repo]/.unbound-force/replicator.log` to `[repo]/.uf/replicator/replicator.log`
- Per-machine database moves from `~/.config/swarm-tools/swarm.db` to `~/.config/uf/replicator/replicator.db`
- Per-machine config directory moves from `~/.config/swarm-tools/` to `~/.config/uf/replicator/`

### No Migration
Clean break. Users run `replicator setup` to create the new directories. Old paths remain until manually removed. No auto-detection, no symlinks, no data copying.

## Impact

- 6 Go source files (path constants)
- 3 Go test files (assertion strings)
- 5 active documentation files (README, AGENTS.md, etc.)
- 10 completed spec artifact files (historical path references)
- Zero behavioral changes beyond the path locations

## Constitution Alignment

### I. Autonomous Collaboration
**PASS**: Path naming is a local concern. No inter-hero protocol changes.

### II. Composability First
**PASS**: Replicator remains independently installable. The new paths don't depend on other tools being present. The `~/.config/uf/` parent directory may be shared with other `uf` tools but each tool manages its own subdirectory.

### III. Observable Quality
**PASS**: The `doctor` command checks for the config directory at the new path. The path is documented in README and AGENTS.md.

### IV. Testability
**PASS**: All tests use `t.TempDir()` for filesystem operations. Path changes are string constant swaps with no logic changes.
Loading
Loading