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
12 changes: 0 additions & 12 deletions .changeset/colony-package-namespace.md

This file was deleted.

7 changes: 0 additions & 7 deletions .changeset/colony-session-cwd-binding.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/hivemind-mcp-tool.md

This file was deleted.

7 changes: 0 additions & 7 deletions .changeset/pheromone-trails.md

This file was deleted.

8 changes: 0 additions & 8 deletions .changeset/proposal-system.md

This file was deleted.

8 changes: 0 additions & 8 deletions .changeset/response-thresholds.md

This file was deleted.

26 changes: 26 additions & 0 deletions apps/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# cavemem

## 0.5.0

### Minor Changes

- Sync linked release with the 0.4.0 MCP heartbeat bump so `@imdeadpool/colony`
and the supporting `@colony/*` workspace packages publish together.

## 0.3.0

### Patch Changes

- eb4dad9: Rename the public CLI package and workspace package/import namespace from cavemem to Colony. The CLI binary is now `colony`, workspace imports use `@colony/*`, release scripts pack `colony`, and installed hook scripts call `colony`.
- f1d036a: Bind hook-created sessions back to their repository cwd so colony views can see live Codex/Claude work instead of orphan `cwd: null` sessions.
- Fix `colony mcp` never starting the stdio server.

`apps/mcp-server/src/server.ts` gated `main()` behind an `isMainEntry()` check
so it only ran when executed directly. The CLI `mcp` command invoked it via
`await import('@colony/mcp-server')`, which triggered the guard (entry was
the CLI binary, not `server.js`) and skipped `main()` — the process exited
immediately after the dynamic import, causing IDE clients (Codex, Claude
Code) to fail the MCP handshake with "connection closed: initialize
response".

`main` is now exported from the server module and invoked explicitly by the
CLI command.

## 0.2.0

### Minor Changes
Expand Down
9 changes: 7 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@imdeadpool/colony",
"version": "0.2.0",
"version": "0.5.0",
"license": "MIT",
"description": "Local-first memory and coordination for Claude Code, Gemini CLI, OpenCode, Codex, and Cursor.",
"keywords": [
Expand Down Expand Up @@ -32,7 +32,12 @@
"colony": "./dist/index.js"
},
"main": "./dist/index.js",
"files": ["dist", "hooks-scripts", "README.md", "LICENSE"],
"files": [
"dist",
"hooks-scripts",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch --onSuccess \"node dist/index.js\"",
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function registerMcpCommand(program: Command): void {
.command('mcp')
.description('Run the MCP stdio server (typically invoked by the IDE)')
.action(async () => {
// Delegate: importing runs main() via the server module.
await import('@colony/mcp-server');
const { main } = await import('@colony/mcp-server');
await main();
});
}
64 changes: 64 additions & 0 deletions apps/mcp-server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
# @colony/mcp-server

## 0.5.0

### Patch Changes

- Updated dependencies
- @colony/compress@0.5.0
- @colony/config@0.5.0
- @colony/core@0.5.0
- @colony/embedding@0.5.0
- @colony/hooks@0.5.0

## 0.4.0

### Minor Changes

- Register the MCP caller in hivemind on startup and on every tool call. When a
client (e.g. codex) attaches to the colony stdio server without ever running
colony's lifecycle hooks, the server now writes / refreshes
`.omx/state/active-sessions/<session_id>.json` using the caller's cwd plus a
session id derived from `CODEX_SESSION_ID`, `CLAUDECODE_SESSION_ID`,
`CLAUDE_SESSION_ID`, `COLONY_CLIENT_SESSION_ID`, or a per-parent-process
fallback. Existing hook-written heartbeats are preserved — the writer never
overwrites a richer task preview with a blank one.

Exposes `upsertActiveSession` / `removeActiveSession` from `@colony/hooks` so
other non-hook runtimes can reuse the same writer.

### Patch Changes

- Updated dependencies
- @colony/hooks@0.4.0

## 0.3.0

### Minor Changes

- f853481: Add a compact `hivemind` MCP tool that maps active proxy-runtime agent sessions to their current tasks.
- 4076133: Add proposal system: pre-tasks that auto-promote via collective reinforcement. Agents call `task_propose` to surface a candidate improvement; other agents call `task_reinforce` (kind `explicit` or `rediscovered`), and PostToolUse adds weak `adjacent` reinforcement whenever an edit touches a file listed in a pending proposal's `touches_files`. Total decayed strength (1-hour half-life, weights 1.0 / 0.7 / 0.3 by kind) is recomputed on every read; when it crosses `PROMOTION_THRESHOLD` (2.5), the proposal is auto-promoted to a real `TaskThread` on a synthetic branch `{branch}/proposal-{id}`. The new `task_foraging_report` MCP tool lists pending (above the 0.3 noise floor) and promoted proposals; `SessionStart` surfaces the same report in-preface. Schema bumped 4 → 5: adds `proposals` and `proposal_reinforcements`.
- 42dd222: Add response-threshold routing for broadcast (`to_agent: 'any'`) handoffs. Each agent identity (Claude, Codex, …) can register a capability profile (`ui_work`, `api_work`, `test_work`, `infra_work`, `doc_work`, each `0..1`) via the new `agent_upsert_profile` MCP tool; unknown agents default to `0.5` across all dimensions. When `TaskThread.handOff` runs with `to_agent: 'any'`, it snapshots a keyword-weighted ranking of every non-sender participant into `HandoffMetadata.suggested_candidates`. `SessionStart` preface surfaces the top match and the viewing agent's own score inline with each pending broadcast handoff, so receivers can see at a glance whether they are the best fit. New `agent_get_profile` MCP tool exposes read-only inspection. Schema bumped 5 → 6: adds `agent_profiles` table.

### Patch Changes

- Fix `colony mcp` never starting the stdio server.

`apps/mcp-server/src/server.ts` gated `main()` behind an `isMainEntry()` check
so it only ran when executed directly. The CLI `mcp` command invoked it via
`await import('@colony/mcp-server')`, which triggered the guard (entry was
the CLI binary, not `server.js`) and skipped `main()` — the process exited
immediately after the dynamic import, causing IDE clients (Codex, Claude
Code) to fail the MCP handshake with "connection closed: initialize
response".

`main` is now exported from the server module and invoked explicitly by the
CLI command.

- Updated dependencies [eb4dad9]
- Updated dependencies [5f37e75]
- Updated dependencies [4076133]
- Updated dependencies [42dd222]
- @colony/compress@0.3.0
- @colony/config@0.3.0
- @colony/core@0.3.0
- @colony/embedding@0.3.0

## 0.2.0

### Minor Changes
Expand Down
3 changes: 2 additions & 1 deletion apps/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@colony/mcp-server",
"version": "0.2.0",
"version": "0.5.0",
"license": "MIT",
"private": true,
"type": "module",
Expand All @@ -19,6 +19,7 @@
"@colony/config": "workspace:*",
"@colony/core": "workspace:*",
"@colony/embedding": "workspace:*",
"@colony/hooks": "workspace:*",
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.23.8"
},
Expand Down
67 changes: 66 additions & 1 deletion apps/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
saveProfile,
} from '@colony/core';
import { createEmbedder } from '@colony/embedding';
import { type HookInput, type HookName, upsertActiveSession } from '@colony/hooks';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
Expand All @@ -40,6 +41,13 @@ export function buildServer(store: MemoryStore, settings: Settings): McpServer {
version: '0.1.0',
});

// Make this MCP client visible to hivemind even when the IDE never ran
// colony's lifecycle hooks (codex, custom MCP clients, background tools).
// The stdio MCP server is spawned per client session, so env + cwd
// identify the caller; upsertActiveSession merges with whatever a hook
// writer may have produced and preserves richer task previews.
installActiveSessionHeartbeat(server);

// tri-state: undefined = not yet attempted; null = unavailable (provider=none or load failed)
let embedder: Embedder | null | undefined = undefined;
const resolveEmbedder = async (): Promise<Embedder | null> => {
Expand Down Expand Up @@ -654,7 +662,64 @@ function nextAction(lanes: HivemindContextLane[], memoryHits: SearchResult[]): s
return 'No live lanes or matching memory found.';
}

async function main(): Promise<void> {
interface McpClientIdentity {
sessionId: string;
ide: string;
}

function detectMcpClientIdentity(env: NodeJS.ProcessEnv = process.env): McpClientIdentity {
const codexId = env.CODEX_SESSION_ID?.trim();
if (codexId) return { sessionId: codexId, ide: 'codex' };
const claudeId = env.CLAUDECODE_SESSION_ID?.trim() ?? env.CLAUDE_SESSION_ID?.trim();
if (claudeId) return { sessionId: claudeId, ide: 'claude-code' };
const override = env.COLONY_CLIENT_SESSION_ID?.trim();
if (override) return { sessionId: override, ide: env.COLONY_CLIENT_IDE?.trim() ?? 'unknown' };
// Fallback: stable per parent-process so the lane coalesces across tool calls.
return { sessionId: `mcp-${process.ppid}`, ide: env.COLONY_CLIENT_IDE?.trim() ?? 'unknown' };
}

function installActiveSessionHeartbeat(server: McpServer): void {
const client = detectMcpClientIdentity();
const cwd = process.cwd();

const touch = (hook: HookName, extras: Partial<HookInput> = {}): void => {
try {
upsertActiveSession(
{ session_id: client.sessionId, ide: client.ide, cwd, ...extras },
hook,
);
} catch {
// Heartbeat is best-effort; never fail a tool call because the JSON
// sidecar cannot be written.
}
};

// Register the client the moment the server is built — before any tool
// call — so the lane is visible on the very first hivemind query.
touch('session-start', { source: 'mcp-connect' });

// Wrap every subsequent `server.tool(...)` registration so each invocation
// bumps lastHeartbeatAt and reports the invoked tool name as the current
// task preview. The SDK overloads this method; we only care that the last
// argument is the handler.
type ToolRegister = McpServer['tool'];
const originalTool = server.tool.bind(server) as ToolRegister;
(server as { tool: ToolRegister }).tool = ((...toolArgs: unknown[]) => {
const name = typeof toolArgs[0] === 'string' ? toolArgs[0] : 'unknown';
const handlerIndex = toolArgs.length - 1;
const handler = toolArgs[handlerIndex];
if (typeof handler === 'function') {
const original = handler as (...handlerArgs: unknown[]) => unknown;
toolArgs[handlerIndex] = async (...handlerArgs: unknown[]) => {
touch('post-tool-use', { tool_name: `colony.${name}` });
return original(...handlerArgs);
};
}
return (originalTool as (...a: unknown[]) => ReturnType<ToolRegister>)(...toolArgs);
}) as ToolRegister;
}

export async function main(): Promise<void> {
const settings = loadSettings();
const dbPath = join(resolveDataDir(settings.dataDir), 'data.db');
const store = new MemoryStore({ dbPath, settings });
Expand Down
59 changes: 58 additions & 1 deletion apps/mcp-server/test/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { defaultSettings } from '@colony/config';
Expand Down Expand Up @@ -358,4 +358,61 @@ describe('MCP server', () => {
});
expect(res.isError).toBe(true);
});

it('registers the MCP caller as an active session on connect and tool use', async () => {
// The fixture's pre-wired server was built before we set env + cwd, so
// build an isolated one here to drive the heartbeat path end-to-end.
const repoRoot = mkdtempSync(join(tmpdir(), 'colony-mcp-hb-'));
mkdirSync(join(repoRoot, '.git'), { recursive: true });
writeFileSync(join(repoRoot, '.git', 'HEAD'), 'ref: refs/heads/hb-branch\n', 'utf8');

const prevCwd = process.cwd();
const prevCodexId = process.env.CODEX_SESSION_ID;
process.chdir(repoRoot);
process.env.CODEX_SESSION_ID = 'hb-session-1';

const isolatedStore = new MemoryStore({
dbPath: join(repoRoot, 'data.db'),
settings: defaultSettings,
});
const isolatedServer = buildServer(isolatedStore, defaultSettings);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const isolatedClient = new Client({ name: 'hb-test', version: '0.0.0' });

try {
await Promise.all([
isolatedServer.connect(serverTransport),
isolatedClient.connect(clientTransport),
]);

const sessionFile = join(
repoRoot,
'.omx',
'state',
'active-sessions',
'hb-session-1.json',
);
const afterConnect = JSON.parse(readFileSync(sessionFile, 'utf8'));
expect(afterConnect.sessionKey).toBe('hb-session-1');
expect(afterConnect.branch).toBe('hb-branch');
expect(afterConnect.cliName).toBe('codex');
expect(afterConnect.state).toBe('working');
const connectHeartbeat = afterConnect.lastHeartbeatAt;

await new Promise((r) => setTimeout(r, 5));

await isolatedClient.callTool({ name: 'list_sessions', arguments: { limit: 1 } });

const afterTool = JSON.parse(readFileSync(sessionFile, 'utf8'));
expect(afterTool.lastHeartbeatAt >= connectHeartbeat).toBe(true);
expect(afterTool.latestTaskPreview).toContain('colony.list_sessions');
} finally {
await isolatedClient.close();
isolatedStore.close();
process.chdir(prevCwd);
if (prevCodexId === undefined) delete process.env.CODEX_SESSION_ID;
else process.env.CODEX_SESSION_ID = prevCodexId;
rmSync(repoRoot, { recursive: true, force: true });
}
});
});
26 changes: 26 additions & 0 deletions apps/worker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# @colony/worker

## 0.5.0

### Patch Changes

- Updated dependencies
- @colony/compress@0.5.0
- @colony/config@0.5.0
- @colony/core@0.5.0
- @colony/embedding@0.5.0
- @colony/storage@0.5.0

## 0.3.0

### Patch Changes

- Updated dependencies [eb4dad9]
- Updated dependencies [f1d036a]
- Updated dependencies [5f37e75]
- Updated dependencies [4076133]
- Updated dependencies [42dd222]
- @colony/compress@0.3.0
- @colony/config@0.3.0
- @colony/core@0.3.0
- @colony/embedding@0.3.0
- @colony/storage@0.3.0

## 0.2.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@colony/worker",
"version": "0.2.0",
"version": "0.5.0",
"license": "MIT",
"private": true,
"type": "module",
Expand Down
Loading
Loading