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: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,20 @@ Requires Node.js 22+ and pnpm.
```bash
pnpm install
pnpm build # pnpm -r build (build before running the CLI/daemon)
pnpm daemon # HTTP API on http://127.0.0.1:9473
pnpm daemon # explicit daemon start; init/work auto-start it
pnpm seed # optional demo projects (Beacon, Silo, Forge)
pnpm dashboard # dev UI on http://127.0.0.1:5173
pnpm dashboard:preview # production build + preview
pnpm coord <args> # run the built CLI (node packages/cli/dist/index.js)
pnpm coord init # profile + folder-named project
pnpm coord work <track> # create/join worktree and launch the agent
pnpm coord <args> # run the built CLI (node packages/cli/dist/index.js)
pnpm test # per-package unit tests (node --test)
pnpm test:integration # builds, then runs tests/integration/*.test.mjs
```

The CLI runs against the built output, so `pnpm build` after changing CLI or
daemon code before exercising a flow.
daemon code before exercising a flow. Run `npm link` from this repository to
make the development build available as `coord` in other repositories.

## Conventions

Expand Down
23 changes: 14 additions & 9 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ digest; the authoritative checklist stays in [`todo.md`](./todo.md), and the
per-phase build plan lives in
[`report/team-implementation-plan.md`](./report/team-implementation-plan.md).

_Last updated: 2026-07-12._
_Last updated: 2026-07-25._

## At a glance

Expand All @@ -22,7 +22,8 @@ Runnable end-to-end on one machine:
```bash
pnpm install
pnpm build
pnpm daemon # HTTP API on http://127.0.0.1:9473
pnpm coord init # auto-start daemon, profile, folder-named project
pnpm coord work my-track # create/join worktree and launch the coding agent
pnpm seed # optional demo projects: Beacon, Silo, Forge
pnpm dashboard # React UI on http://127.0.0.1:5173
pnpm test:integration # full CLI + daemon flow, incl. vault-flow.test.mjs
Expand All @@ -32,12 +33,14 @@ pnpm test:integration # full CLI + daemon flow, incl. vault-flow.test.mjs
SQLite state, projects/tracks, participants, worktrees, publish events,
vault materialization + rebuild, `vault context`, FTS5 vault search, avatars,
repo context, and Supabase relay mirroring/sync.
- **CLI** (`packages/cli`, `pnpm coord`) — `init` (with `--relay
local|supabase`), `status`, `project create|list`, `start`, `join`, `enter`,
- **CLI** (`packages/cli`, `pnpm coord`) — simplified onboarding through
`init` (auto-start daemon + folder-named project) and `work` (create/join a
worktree + launch Claude Code, Codex, Cursor, Ghost, or a shell). Bare
`coord` selects a track and launches the default agent. Lower-level commands
remain available: `status`, `project create|list`, `start`, `join`, `enter`,
`publish`, `vault read|context|search`, `context` (compact vault context +
teammate deltas), `hook install|uninstall|status` (Claude Code
auto-injection), `ws show|who|branches`, plus relay commands `login`,
`sessions`, `list`, `sync`, `status relay`.
teammate deltas), `hook install|uninstall|status`, `ws show|who|branches`,
plus relay commands `login`, `sessions`, `list`, `sync`, `status relay`.
- **Vault** (`packages/vault`) — flat Phase 1 files (`decisions.md`,
`observations.md`, `blockers.md`, `test-results.md`, `attempts.md`)
materialized from `events.jsonl`; row annotations (`[tb color= assign=]`)
Expand All @@ -55,8 +58,10 @@ pnpm test:integration # full CLI + daemon flow, incl. vault-flow.test.mjs
`participants`, `vault/context`, `inbox`, `conflicts`) and six tools
(`team_publish`, `vault_search`, `vault_read`, `workspace_status`,
`team_ask`, `team_reply`) — all calling the daemon. Workspace resolution
from explicit params, local `state.sqlite` worktree mapping, or
`.coord/.active` fallback. Start with `coord mcp`. Integration tests
from explicit params, launcher environment, local `state.sqlite` worktree
mapping, or `.coord/.active` fallback. `coord work` configures the MCP server
for Claude Code and Codex automatically; it can also be started directly
with `coord mcp`. Integration tests
spawn the server over stdio and verify JSON-RPC handshake, resource/tool
lists, live resource reads, and full ask/reply/conflict flows.

Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,36 @@ Requires Node.js 22+ and pnpm.
```bash
pnpm install
pnpm build
pnpm daemon # API on http://127.0.0.1:9473
```

To use the development build as `coord` from any repository:

```bash
npm link
cd /path/to/your/project
coord init
coord work my-track
```

`coord init` starts the local daemon when needed, creates your profile, and
creates a project named after the repository folder. `coord work` creates or
joins the track worktree and launches your configured agent there. Passing
`--claude`, `--codex`, `--cursor`, or `--shell` overrides the agent for one
run. After setup, bare `coord` selects a track and launches the default agent.

For dashboard development:

```bash
pnpm seed # Optional demo projects: Beacon, Silo, Forge
pnpm dashboard # UI on http://127.0.0.1:5173
```

**Your own data (no seed):**
When running without the global development link, prefix commands with
`pnpm`, for example:

```bash
pnpm coord init
pnpm coord project create --name "My App"
pnpm coord start my-track
pnpm coord work my-track
```

Optional: add `PEXELS_API_KEY` to `.env` for flower profile photos.
Expand Down
33 changes: 30 additions & 3 deletions docs/cli-worktrees.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CLI Worktrees (`start` / `join`)
# CLI Worktrees (`work` / `start` / `join`)

How the `@coord/cli` creates and manages git worktrees. Owner: Kushagra.

Expand All @@ -13,6 +13,29 @@ How the `@coord/cli` creates and manages git worktrees. Owner: Kushagra.

- **Branch:** `coord/<sessionName>/<safeName>` — byte-for-byte the daemon's `branchForParticipant` (index.ts:778). The session name is used **raw** (validated `^[A-Za-z0-9._-]+$`); the path segment is sanitized. `<safeName>` mirrors the daemon's `safeDisplayName`.

## Recommended flow: `coord work [TRACK]`

`coord work` is the user-facing entry point over the lower-level `start`,
`join`, and `enter` operations:

1. Start the local daemon if it is not already healthy.
2. Run interactive initialization when the repository has no profile.
3. Resolve the requested track from the argument, current Coord branch, only
available track, or an interactive picker.
4. Create a new track or join an existing local/relay track, repairing a
missing local worktree pointer when needed.
5. Mark the track active for MCP resolution.
6. Launch the configured agent inside the participant worktree.

Claude Code is launched with a generated project MCP config and its Coord
context hook installed. Codex is launched with temporary `-c
mcp_servers.coord.*` overrides, so the user's global Codex config is not
modified. Use `--no-launch` to prepare the worktree only, or `--claude`,
`--codex`, `--cursor`, `--ghost`, and `--shell` for a one-run override.
These launch arguments follow the official
[Claude Code CLI reference](https://docs.anthropic.com/en/docs/claude-code/cli-usage)
and [Codex CLI reference](https://developers.openai.com/codex/cli/reference/).

## `coord join [NAME] [--as DISPLAY_NAME]`

Flow (git-first, then daemon, so the daemon never records a row for a worktree that failed to materialize):
Expand All @@ -24,7 +47,7 @@ Flow (git-first, then daemon, so the daemon never records a row for a worktree t
5. `POST /workspaces/join` with `{ repoRoot, sessionName, displayName, agent, worktreePath }`.
6. Write a local pointer `.coord/workspaces/<session>/.worktree.<safeName>.json` (so a future `enter` resolves the path without reading SQLite).

**Enter the worktree:** `cd "$(... printed path)" && claude` (printed by the command).
**Continue in the worktree:** `coord work <session>` (printed by the command).

### Edge cases handled

Expand All @@ -47,9 +70,13 @@ packages/cli/src/lib/worktree.ts # prepareParticipantWorktree / rollbackPartic
packages/cli/src/lib/pointers.ts # .worktree.<name>.json read/write
packages/cli/src/commands/start.ts # runStart
packages/cli/src/commands/track.ts # shared join/registration helpers
packages/cli/src/commands/work.ts # high-level worktree + agent launcher
packages/cli/src/lib/agent.ts # agent aliases, detection, commands
```

Tests: `packages/cli/test/worktree.test.cjs` (naming parity + worktree orchestration with a fake `GitRunner`).
Tests: `packages/cli/test/worktree.test.cjs` (naming parity + worktree
orchestration with a fake `GitRunner`) and `packages/cli/test/work.test.cjs`
(Claude Code, Codex, Cursor, and shell launch plans).

## Known limitations (pending daemon asks — see `nihal-daemon-requests.md`)

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.0.0",
"private": true,
"description": "Coord — shared workspace for teams building with AI agents.",
"bin": {
"coord": "packages/cli/dist/index.js"
},
"scripts": {
"build": "pnpm -r build",
"dashboard": "pnpm --filter @coord/dashboard dev --host 127.0.0.1",
Expand Down
109 changes: 98 additions & 11 deletions packages/cli/src/commands/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, openSync, c
import { join, resolve } from 'node:path';
import type { ClientOptions } from '../daemon-client';

const DEFAULT_DAEMON_PORT = 9473;
const DAEMON_START_TIMEOUT_MS = 5_000;

type DaemonState = {
pid: number;
port: number;
Expand Down Expand Up @@ -45,24 +48,75 @@ function daemonEntryPoint(): string {
function parsePort(argv: string[], fallback: string | undefined): number {
const index = argv.indexOf('--port');
const value = index >= 0 ? argv[index + 1] : fallback;
const port = Number(value ?? 9473);
const port = Number(value ?? DEFAULT_DAEMON_PORT);
if (!Number.isInteger(port) || port <= 0) {
throw new Error('Daemon port must be a positive integer.');
}
return port;
}

async function runDaemonStart(argv: string[], options: ClientOptions): Promise<void> {
function portFromBaseUrl(baseUrl: string | undefined): number {
if (!baseUrl) return parsePort([], process.env.COORD_DAEMON_PORT);

let url: URL;
try {
url = new URL(baseUrl);
} catch {
throw new Error(`Invalid Coord daemon URL: ${baseUrl}`);
}

if (url.protocol !== 'http:' || (url.hostname !== '127.0.0.1' && url.hostname !== 'localhost')) {
throw new Error(
`Cannot auto-start a remote daemon at ${baseUrl}. Start it separately or unset COORD_DAEMON_URL.`
);
}

return Number(url.port || 80);
}

async function daemonIsHealthy(baseUrl: string): Promise<boolean> {
try {
const response = await fetch(new URL('/health', baseUrl), {
signal: AbortSignal.timeout(750)
});
if (!response.ok) return false;
const body = await response.json() as { ok?: boolean };
return body.ok === true;
} catch {
return false;
}
}

async function waitForDaemon(baseUrl: string): Promise<void> {
const startedAt = Date.now();
while (Date.now() - startedAt < DAEMON_START_TIMEOUT_MS) {
if (await daemonIsHealthy(baseUrl)) return;
await new Promise((resolvePromise) => setTimeout(resolvePromise, 75));
}
throw new Error(`Coord daemon did not become ready at ${baseUrl}. Check .coord/daemon/daemon.log.`);
}

async function startManagedDaemon(
options: ClientOptions,
port: number,
quiet: boolean
): Promise<{ started: boolean; baseUrl: string }> {
const baseUrl = `http://127.0.0.1:${port}`;
const existing = readState(options.repoRoot);
if (existing && isProcessAlive(existing.pid)) {
console.log(`Coord daemon already running on ${existing.baseUrl}`);
console.log(`PID: ${existing.pid}`);
console.log(`Log: ${existing.logPath}`);
return;
if (await daemonIsHealthy(baseUrl)) {
if (!quiet) {
console.log(`Coord daemon already running on ${baseUrl}`);
console.log(`PID: ${existing.pid}`);
console.log(`Log: ${existing.logPath}`);
}
return { started: false, baseUrl };
}
throw new Error(
`A managed Coord daemon is running as PID ${existing.pid}, but ${baseUrl} is not healthy. Check ${existing.logPath}.`
);
}

const port = parsePort(argv, process.env.COORD_DAEMON_PORT);
const baseUrl = `http://127.0.0.1:${port}`;
const dir = stateDir(options.repoRoot);
mkdirSync(dir, { recursive: true });

Expand Down Expand Up @@ -95,9 +149,42 @@ async function runDaemonStart(argv: string[], options: ClientOptions): Promise<v
};
writeFileSync(statePath(options.repoRoot), `${JSON.stringify(state, null, 2)}\n`);

console.log(`Started Coord daemon on ${baseUrl}`);
console.log(`PID: ${state.pid}`);
console.log(`Log: ${logPath}`);
try {
await waitForDaemon(baseUrl);
} catch (error) {
if (state.pid && isProcessAlive(state.pid)) {
process.kill(state.pid, 'SIGTERM');
}
rmSync(statePath(options.repoRoot), { force: true });
throw error;
}

if (!quiet) {
console.log(`Started Coord daemon on ${baseUrl}`);
console.log(`PID: ${state.pid}`);
console.log(`Log: ${logPath}`);
}
return { started: true, baseUrl };
}

/**
* Make normal commands self-starting while preserving explicit remote daemon
* configurations. Returns true only when this call launched the daemon.
*/
export async function ensureDaemonRunning(options: ClientOptions): Promise<boolean> {
const configuredBaseUrl = options.baseUrl ?? `http://127.0.0.1:${parsePort([], process.env.COORD_DAEMON_PORT)}`;
if (await daemonIsHealthy(configuredBaseUrl)) return false;

const port = portFromBaseUrl(configuredBaseUrl);
const result = await startManagedDaemon(options, port, true);
return result.started;
}

async function runDaemonStart(argv: string[], options: ClientOptions): Promise<void> {
const port = argv.includes('--port')
? parsePort(argv, process.env.COORD_DAEMON_PORT)
: portFromBaseUrl(options.baseUrl);
await startManagedDaemon(options, port, false);
}

async function runDaemonStatus(options: ClientOptions): Promise<void> {
Expand Down
25 changes: 18 additions & 7 deletions packages/cli/src/commands/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ function isInstalled(settings: ClaudeSettings): boolean {
return (settings.hooks?.SessionStart ?? []).some(entryTargetsCoord);
}

async function runHookInstall(argv: string[], options: ClientOptions, cwd: string): Promise<void> {
export async function installClaudeHook(
argv: string[],
options: ClientOptions,
cwd: string,
quiet = false
): Promise<void> {
const command = parseFlag(argv, '--command') ?? HOOK_COMMAND;
const path = settingsPath(cwd);
const settings = readSettings(path);
Expand All @@ -68,18 +73,24 @@ async function runHookInstall(argv: string[], options: ClientOptions, cwd: strin
}
}
writeSettings(path, settings);
console.log(`Coord SessionStart hook already present — refreshed in ${path}`);
if (!quiet) {
console.log(`Coord SessionStart hook already present — refreshed in ${path}`);
}
} else {
sessionStart.push({ hooks: [{ type: 'command', command }] });
writeSettings(path, settings);
console.log(`Installed Coord SessionStart hook in ${path}`);
console.log(`Command: ${command}`);
if (!quiet) {
console.log(`Installed Coord SessionStart hook in ${path}`);
console.log(`Command: ${command}`);
}
}

if (!autoInject) {
if (!autoInject && !quiet) {
console.log('Note: config.autoInject is false — the hook is installed but you disabled auto-injection in .coord/config.json.');
}
console.log('New Claude Code sessions in this worktree will now receive shared context automatically.');
if (!quiet) {
console.log('New Claude Code sessions in this worktree will now receive shared context automatically.');
}
}

function runHookUninstall(cwd: string): void {
Expand Down Expand Up @@ -133,7 +144,7 @@ export async function runHook(argv: string[], options: ClientOptions): Promise<v
const cwd = process.cwd();

if (sub === 'install') {
await runHookInstall(argv.slice(1), options, cwd);
await installClaudeHook(argv.slice(1), options, cwd);
return;
}
if (sub === 'uninstall') {
Expand Down
Loading