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
65 changes: 65 additions & 0 deletions docs/branch-office.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,71 @@ tps mail check flint

The branch daemon log (`~/.tps/branch.log`) shows `MAIL: Received message for reed` and `SYNC: Heartbeat received — drained outbox` events.

## Flair spoke (ops-209a)

After a successful join with `--tunnel-via`, `tps office join` can automatically provision a **Flair spoke** on the remote branch. Flair is the TPS local memory engine (Harper). A spoke gives the branch its own persistent memory that can optionally federate with the team's Flair hub.

### Plan inference

Before touching the remote, `tps office join` reads `~/.tps/flair.json` (set by `tps flair set-hub`, see [commands.md](commands.md)) and determines one of three plans:

| Plan | Condition | Behavior |
|---|---|---|
| **hub-less** | `hub` is null (no team hub configured) | Install Flair on the branch. Branch memories are an island — not synced anywhere. |
| **spoke** | `hub` + `auth` both set and valid | Install Flair + configure periodic fed-sync to the hub + run one-shot validation. |
| **error** | `hub` is set but `auth` is missing or invalid | Abort Flair provisioning entirely. The join still succeeds — just no Flair. Fix with `tps flair set-hub --auth-mode admin-pass-file --auth-path <path>`. |

### Remote install flow

When proceeding (hub-less or spoke), `tps office join` executes over SSH:

1. **Install package:** `ssh <tunnel-via> 'mkdir -p ~/.flair && cd ~/.flair && npm install @tpsdev-ai/flair'`
2. **Generate admin pass:** `openssl rand -base64 24` locally, `scp` to `~/.flair/admin-pass` on the branch (mode 0600). The pass is never written to a local temp file.
3. **Install as a service:** OS-adaptive —
- **Linux (systemd):** Write a `~/.config/systemd/user/tps-flair-<name>.service` unit, run `systemctl --user daemon-reload && enable --now`.
- **macOS (launchd):** Write a `~/Library/LaunchAgents/ai.tpsdev.flair-<name>.plist`, `launchctl load` it.

Harper runs on port 9926 (default Flair port) with its data at `~/.harper/flair`.

### Fed-sync (spoke mode only)

In spoke mode, after Flair is running, `tps office join` configures periodic memory federation from the branch back to the hub:

1. **Config:** Write `~/.tps/flair-sync.json` on the branch with `localUrl`, `remoteUrl` (hub), `agentId`, and the hub's `admin-pass` auth.
2. **Timer + service:** On Linux, install `~/.config/systemd/user/tps-fed-sync-<name>.{service,timer}`. The timer triggers every 30s (with a 30s randomized delay to avoid thundering-herd). The service is `Type=oneshot` running `tps flair sync --once`.
3. **Validate:** Run a one-shot sync immediately. Success writes the timestamp to the manifest; failure leaves the branch hub-less until the sync is working.

### Opt-outs and re-provisioning

| Flag | Effect |
|---|---|
| `--no-flair` | Skip Flair spoke provisioning entirely. Join still completes with just supervision. |
| `--force-reinstall-flair` | If Flair is already installed on the remote, tear down and reinstall (preserves data unless `--purge-flair`). Without this flag, rejoin with an existing Flair install errors out. |

### Teardown on revoke

`tps office revoke <name>` tears down the Flair spoke by:
1. Stopping and disabling the fed-sync timer + service, removing their unit files.
2. Stopping and disabling the Flair service, removing its unit/plist.

Pass `--purge-flair` to also `rm -rf ~/.flair ~/.harper/flair` on the branch.

### Status reporting

`tps office status <name>` shows Flair spoke health when the branch has a supervision manifest:

```
🔒 Supervision (launchd):
🟢 Tunnel: ai.tpsdev.tunnel-reed → port 33744 via tps-reed (PID 12345)
🟢 Office: ai.tpsdev.office-reed (PID 12346)
Installed: 2026-05-17T12:00:00.000Z

🧠 Flair spoke:
Flair: 🟢 ~/.flair (port 9926)
API: ✅ reachable
Fed-Sync: 🟢 → hub (last: 2026-05-17T12:30:05.000Z)
```

## Operational commands

### On the branch
Expand Down
7 changes: 5 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,23 @@ tps office list
tps office status [agent]

tps office join <name> <join-token> # Remote-relay model: pair a remote branch
[--tunnel-via <ssh-host>] [--port <n>] [--force]
[--no-flair] [--force-reinstall-flair]
tps office connect <name> # Long-running connection (use under KeepAlive)
tps office sync <name> # One-shot connect+drain
tps office revoke <name> # Drop a paired branch from the registry
[--keep-units] [--purge-flair]
```

**Docker-sandbox commands:**
- `start <agent>`: Create and start a sandbox for the agent. Installs OpenClaw and TPS inside.
- `stop <agent>`: Stop the sandbox.

**Remote-relay commands:**
- `join <name> <join-token>`: Register a remote branch using the `tps://join?…` token printed by `tps branch init` on the branch.
- `join <name> <join-token>`: Register a remote branch using the `tps://join?…` token printed by `tps branch init` on the branch. With `--tunnel-via <ssh-host>`, also provisions macOS launchd supervision (ops-7x9y) and a Flair spoke on the remote (ops-209a, opt-out with `--no-flair`). Use `--force-reinstall-flair` to re-provision an existing Flair install.
- `connect <name>`: Open a persistent encrypted channel to the named branch. Designed for KeepAlive (launchd/systemd).
- `sync <name>`: One-shot connect, drain inbound/outbound mail, disconnect. Useful for catch-up.
- `revoke <name>`: Remove the branch from `~/.tps/registry/`. Does not remove launchd/systemd units — clean those up separately.
- `revoke <name>`: Remove the branch from `~/.tps/registry/`. Tears down supervision units (unless `--keep-units`) and Flair spoke (unless `--keep-units`). Pass `--purge-flair` to also `rm -rf ~/.flair ~/.harper/flair` on the branch.

**Common to both:**
- `list`: List entries from `~/.tps/branch-office/` (one row per known branch alias, with sandbox-presence flag). Output is local registry state, not live connection health — use `status` for that.
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/bin/tps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ const cli = meow(
// ops-7x9y: office join supervision flags (--force is already declared above)
tunnelVia: { type: "string" },
keepUnits: { type: "boolean", default: false },
// ops-209a: Flair spoke provisioning flags
noFlair: { type: "boolean", default: false },
forceReinstallFlair: { type: "boolean", default: false },
purgeFlair: { type: "boolean", default: false },
// Task envelope flags (mail send --task)
task: { type: "string" },
taskId: { type: "string" },
Expand Down Expand Up @@ -565,7 +569,7 @@ async function main() {
} else if (action === "join") {
const joinToken = rest[2];
if (!rest[1] || !joinToken) {
console.error("Usage: tps office join <name> <join-token-url> [--tunnel-via <ssh-host>] [--port <n>] [--force]");
console.error("Usage: tps office join <name> <join-token-url> [--tunnel-via <ssh-host>] [--port <n>] [--force] [--no-flair] [--force-reinstall-flair]");
process.exit(1);
}
await runOffice({
Expand All @@ -575,16 +579,19 @@ async function main() {
tunnelVia: cli.flags.tunnelVia as string | undefined,
port: cli.flags.port as number | undefined,
force: cli.flags.force as boolean,
noFlair: cli.flags.noFlair as boolean,
forceReinstallFlair: cli.flags.forceReinstallFlair as boolean,
});
} else if (action === "revoke") {
if (!rest[1]) {
console.error("Usage: tps office revoke <name> [--keep-units]");
console.error("Usage: tps office revoke <name> [--keep-units] [--purge-flair]");
process.exit(1);
}
await runOffice({
action: "revoke",
agent: rest[1],
keepUnits: cli.flags.keepUnits as boolean,
purgeFlair: cli.flags.purgeFlair as boolean,
});
} else if (action === "setup") {
const dryRun = process.argv.includes("--dry-run") || process.argv.includes("--dry");
Expand Down
Loading
Loading