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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,19 @@ make release # GoReleaser dry-run
make install # Install to GOPATH/bin
```

### CLI Commands

| 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 serve` | Start MCP JSON-RPC server on stdio |
| `replicator cells` | List hive cells (work items) |
| `replicator doctor` | Check environment health |
| `replicator stats` | Display activity summary |
| `replicator query` | Run preset SQL analytics queries |
| `replicator version` | Print version, commit, build date |

## Project Structure

```
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ go install github.com/unbound-force/replicator/cmd/replicator@latest
## Usage

```bash
# Initialize a project for swarm operations
replicator init

# Start MCP server (for AI agent connections)
replicator serve

Expand Down
52 changes: 52 additions & 0 deletions cmd/replicator/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
)

func initCmd() *cobra.Command {
var pathFlag string
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
directory. Idempotent — safe to run multiple times.

This is the per-repo initialization command. It does not require the
global database (replicator setup) or any external services.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(pathFlag)
},
}
cmd.Flags().StringVar(&pathFlag, "path", ".", "Target directory for .hive/ initialization")
return cmd
}

// runInit creates the .hive/ directory and seeds cells.json.
func runInit(targetDir string) error {
hiveDir := filepath.Join(targetDir, ".hive")

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

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

// Write empty cells.json.
cellsPath := filepath.Join(hiveDir, "cells.json")
if err := os.WriteFile(cellsPath, []byte("[]\n"), 0o644); err != nil {
return fmt.Errorf("write cells.json: %w", err)
}

fmt.Println("initialized .hive/")
return nil
}
78 changes: 78 additions & 0 deletions cmd/replicator/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"os"
"path/filepath"
"testing"
)

func TestRunInit_FreshDirectory(t *testing.T) {
dir := t.TempDir()
if err := runInit(dir); err != nil {
t.Fatalf("runInit: %v", err)
}

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

cellsPath := filepath.Join(hiveDir, "cells.json")
data, err := os.ReadFile(cellsPath)
if err != nil {
t.Fatalf("cells.json not created: %v", err)
}
if string(data) != "[]\n" {
t.Errorf("cells.json content = %q, want %q", string(data), "[]\n")
}
}

func TestRunInit_AlreadyInitialized(t *testing.T) {
dir := t.TempDir()

// First init.
if err := runInit(dir); err != nil {
t.Fatalf("first runInit: %v", err)
}

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

// Second init — should be idempotent.
if err := runInit(dir); err != nil {
t.Fatalf("second runInit: %v", err)
}

// Verify cells.json was NOT overwritten.
data, _ := os.ReadFile(cellsPath)
if string(data) != `[{"id":"test"}]` {
t.Errorf("cells.json was overwritten: got %q", string(data))
}
}

func TestRunInit_CustomPath(t *testing.T) {
parent := t.TempDir()
target := filepath.Join(parent, "myproject")
os.MkdirAll(target, 0o755)

if err := runInit(target); err != nil {
t.Fatalf("runInit with custom path: %v", err)
}

cellsPath := filepath.Join(target, ".hive", "cells.json")
if _, err := os.Stat(cellsPath); err != nil {
t.Fatalf("cells.json not created at custom path: %v", err)
}
}

func TestRunInit_InvalidPath(t *testing.T) {
err := runInit("/nonexistent/path/that/cannot/exist")
if err == nil {
t.Fatal("expected error for invalid path")
}
}
1 change: 1 addition & 0 deletions cmd/replicator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ via Dewey).`,
root.AddCommand(statsCmd())
root.AddCommand(queryCmd())
root.AddCommand(setupCmd())
root.AddCommand(initCmd())

if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-init-command/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: unbound-force
created: 2026-04-06
29 changes: 29 additions & 0 deletions openspec/changes/add-init-command/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## 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`.

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

## Goals / Non-Goals

### Goals
- Add `replicator init` command that creates `.hive/cells.json` in the target directory
- Support `--path` flag for specifying a non-CWD target
- Idempotent (exit 0 whether creating or already exists)
- No database, no git, no network

### Non-Goals
- Creating `.hive/memories.jsonl` or other hive files (those are created by specific tools when needed)
- Running `git add` or `git commit` (that's `hive_sync`)
- Initializing the SQLite database (that's `replicator setup`)
- Adding `.hive/` to `.gitignore` (user decides tracking policy)

## Decisions

**D1: Follow the `setup.go` pattern.** New file `cmd/replicator/init.go` with `initCmd() *cobra.Command` and `runInit(targetDir string) error`. Register in `main.go` via `root.AddCommand(initCmd())`.

**D2: Seed file is `cells.json` with `[]`.** This matches the format `hive_sync` writes. An empty JSON array signals "initialized but no cells yet" rather than the ambiguity of a missing file.

**D3: `--path` defaults to `.` (current directory).** Same UX as `dewey init` which operates on CWD by default.

**D4: Testable via `t.TempDir()`.** The `runInit` function takes a directory path, making it directly testable without mocking.
33 changes: 33 additions & 0 deletions openspec/changes/add-init-command/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Why

The `uf init` command delegates per-repo initialization to each sub-tool (`dewey init`, `gaze init`). Replicator lacks an `init` command, so `uf init` cannot prepare a repository for swarm operations. Without it, the `.hive/` directory only appears organically on first `hive_sync` call, leaving new projects in an ambiguous state where the directory structure doesn't signal that swarm coordination is available.

This blocks `unbound-force/unbound-force#82` (replace Swarm plugin with Replicator in the `uf` CLI).

## What Changes

### New Capabilities
- `replicator init` creates a `.hive/` directory with an empty `cells.json` in the current working directory
- `replicator init --path /some/dir` creates `.hive/` at a specified location
- Idempotent: safe to run multiple times (prints "already initialized" if `.hive/` exists)

## Impact

- `cmd/replicator/init.go` (new file, ~40 lines)
- `cmd/replicator/main.go` (add `initCmd()` registration)
- `README.md` (add `init` to usage section)
- `AGENTS.md` (add `init` to commands table)

## Constitution Alignment

### I. Autonomous Collaboration
**PASS**: `init` creates a well-known directory (`.hive/`) that other tools can discover without coordination.

### II. Composability First
**PASS**: No database or external service required. Works standalone before `replicator setup` has run.

### III. Observable Quality
**PASS**: Prints machine-parseable status (`initialized .hive/` or `already initialized`). Exit 0 on success, exit 1 on error.

### IV. Testability
**PASS**: Uses `t.TempDir()` for filesystem tests. No external dependencies.
34 changes: 34 additions & 0 deletions openspec/changes/add-init-command/specs/init-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## ADDED Requirements

### Requirement: Per-repo initialization

`replicator init` MUST create a `.hive/` directory with an empty `cells.json` file in the target directory.

#### Scenario: Fresh directory
- **GIVEN** a directory without `.hive/`
- **WHEN** `replicator init` is run
- **THEN** `.hive/cells.json` is created containing `[]`
- **AND** stdout prints `initialized .hive/`
- **AND** exit code is 0

#### Scenario: Already initialized
- **GIVEN** a directory with existing `.hive/`
- **WHEN** `replicator init` is run
- **THEN** no files are modified
- **AND** stdout prints `already initialized`
- **AND** exit code is 0

#### Scenario: Custom path
- **GIVEN** a valid directory at `/some/path`
- **WHEN** `replicator init --path /some/path` is run
- **THEN** `.hive/cells.json` is created at `/some/path/.hive/cells.json`

#### Scenario: Invalid path
- **GIVEN** a path that does not exist and cannot be created
- **WHEN** `replicator init --path /nonexistent/path` is run
- **THEN** an error is printed
- **AND** exit code is 1

### Requirement: No external dependencies

`replicator init` MUST NOT require the SQLite database, git, network access, or any other external service to function.
19 changes: 19 additions & 0 deletions openspec/changes/add-init-command/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## 1. Implement init command

- [x] 1.1 Create `cmd/replicator/init.go` with `initCmd() *cobra.Command` (--path flag, default ".") and `runInit(targetDir string) error`
- [x] 1.2 In `runInit`: check if `.hive/` exists (print "already initialized", return nil), otherwise `os.MkdirAll` + `os.WriteFile("cells.json", "[]")`
- [x] 1.3 Register `initCmd()` in `cmd/replicator/main.go` via `root.AddCommand(initCmd())`

## 2. Tests

- [x] 2.1 Create `cmd/replicator/init_test.go` with tests: fresh dir creates `.hive/cells.json`, already-initialized is idempotent, custom --path works, cells.json content is `[]`

## 3. Documentation

- [x] 3.1 Add `replicator init` to the Usage section in `README.md`
- [x] 3.2 Add `make init` is NOT needed (clarify `init` is a CLI command, not a Makefile target) -- update AGENTS.md commands section to include `replicator init`

## 4. Verify

- [x] 4.1 Run `make check` -- all tests pass
- [x] 4.2 Run `./bin/replicator init` in a temp dir -- verify `.hive/cells.json` created
Loading