diff --git a/README.md b/README.md index bdd793b2..50a1aea0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ A new approach to task management with AI agent teams. - **Sit back and watch** — tasks change status on the kanban board while agents handle everything on their own - **Review changes like in Cursor** — see what code each task changed, then approve, reject, or comment - **Full tool visibility** — inspect exactly which tools an agent used to complete each task +- **Task-specific logs and messages** — clearly see all Claude logs and messages in isolation for each individual task, making it easy to trace what happened for any assignment - **Live process section** — see which agents are running processes and open URLs directly in the browser - **Stay in control** — send a direct message to any agent, drop a comment on a task, or pick a quick action right on the kanban card whenever you want to clarify something or add new work - **Solo mode** — one-member team: a single agent that creates its own tasks and shows live progress. Saves tokens; can expand to a full team anytime @@ -38,7 +39,7 @@ A new approach to task management with AI agent teams. More features
- +- **Task creation with attachments** — Simply send a message to the team lead with any attached images (planed all files). The lead will automatically create a fully described task and attach your files directly to the task for complete context. - **Deep session analysis** — detailed breakdown of what happened in each Claude session: bash commands, reasoning, subprocesses - **Smart task-to-log/changes matching** — automatically links Claude session logs/changes to specific tasks - **Advanced context monitoring system** — comprehensive breakdown of what consumes tokens at every step: user messages, Claude.md instructions, tool outputs, thinking text, and team coordination. Token usage, percentage of context window, and session cost are displayed for each category, with detailed views by category or size. @@ -47,10 +48,11 @@ A new approach to task management with AI agent teams. - **Built-in code editor** — edit project files with Git support without leaving the app - **Branch strategy** — choose via prompt: single branch or git worktree per agent - **Team member stats** — global performance statistics per member -- **Attach code context** — reference files or snippets in messages, like in Cursor +- **Attach code context** — reference files or snippets in messages, like in Cursor. You can also mention tasks using `#task-id`, or refer to another team with `@team-name` in your messages. - **Notification system** — configurable alerts when tasks complete, agents need attention, or errors occur - **MCP integration** — supports the built-in `mcp-server` (see [mcp-server folder](./mcp-server)) for integrating external tools and extensible agent plugins out of the box - **Post-compact context recovery** — when Claude compresses its context, the app restores the key team-management instructions so kanban/task-board coordination stays consistent and important operational context is not lost +- **Task context is preserved** — thanks to task descriptions, comments, and attachments, all essential information about each task remains available for ongoing work and future reference ## Installation @@ -221,6 +223,8 @@ pnpm dist # macOS + Windows + Linux - [ ] Planning mode to organize agent plans before execution - [ ] Curate what context each agent sees (files, docs, MCP servers, skills) - [ ] Multi-model support: proxy layer to use other popular LLMs (GPT, Gemini, DeepSeek, Llama, etc.), including offline/local models +- [ ] Attach any files to messages/comments/tasks +- [ ] Slash commands --- diff --git a/agent-teams-controller/src/controller.js b/agent-teams-controller/src/controller.js index 4a4ba19c..ece6bafc 100644 --- a/agent-teams-controller/src/controller.js +++ b/agent-teams-controller/src/controller.js @@ -7,6 +7,7 @@ const processes = require('./internal/processes.js'); const maintenance = require('./internal/maintenance.js'); const crossTeam = require('./internal/crossTeam.js'); const runtime = require('./internal/runtime.js'); +const agentBlocks = require('./internal/agentBlocks.js'); function bindModule(context, moduleApi) { return Object.fromEntries( @@ -36,6 +37,7 @@ function createController(options) { module.exports = { createController, createControllerContext, + agentBlocks, tasks, kanban, review, diff --git a/agent-teams-controller/src/internal/agentBlocks.js b/agent-teams-controller/src/internal/agentBlocks.js index 9913af91..395eafdd 100644 --- a/agent-teams-controller/src/internal/agentBlocks.js +++ b/agent-teams-controller/src/internal/agentBlocks.js @@ -1,6 +1,7 @@ const AGENT_BLOCK_TAG = 'info_for_agent'; const AGENT_BLOCK_OPEN = `<${AGENT_BLOCK_TAG}>`; const AGENT_BLOCK_CLOSE = ``; +const AGENT_BLOCK_RE = new RegExp(`<${AGENT_BLOCK_TAG}>[\\s\\S]*?`, 'g'); function wrapAgentBlock(text) { const trimmed = typeof text === 'string' ? text.trim() : ''; @@ -10,9 +11,20 @@ function wrapAgentBlock(text) { return `${AGENT_BLOCK_OPEN}\n${trimmed}\n${AGENT_BLOCK_CLOSE}`; } +/** + * Strip all agent-only blocks from text. + * Returns text with `...` blocks removed and trimmed. + */ +function stripAgentBlocks(text) { + if (typeof text !== 'string') return ''; + return text.replace(AGENT_BLOCK_RE, '').trim(); +} + module.exports = { AGENT_BLOCK_TAG, AGENT_BLOCK_OPEN, AGENT_BLOCK_CLOSE, + AGENT_BLOCK_RE, + stripAgentBlocks, wrapAgentBlock, }; diff --git a/agent-teams-controller/src/internal/messageStore.js b/agent-teams-controller/src/internal/messageStore.js index ea40a088..701f8204 100644 --- a/agent-teams-controller/src/internal/messageStore.js +++ b/agent-teams-controller/src/internal/messageStore.js @@ -180,19 +180,24 @@ function lookupMessage(paths, messageId) { throw new Error('Missing messageId'); } - const matches = []; + let match = null; + let matchCount = 0; // 1. Search sentMessages.json const sentRows = readJson(getSentMessagesPath(paths), []); if (Array.isArray(sentRows)) { for (const row of sentRows) { if (row && row.messageId === id) { - matches.push({ message: row, store: 'sent' }); + match = { message: row, store: 'sent' }; + matchCount++; + if (matchCount > 1) { + throw new Error(`Ambiguous messageId: ${id} found in multiple stores`); + } } } } - // 2. Search all inbox files + // 2. Search all inbox files (early-exit on ambiguity) const inboxDir = path.join(paths.teamDir, 'inboxes'); let inboxFiles = []; try { @@ -206,20 +211,20 @@ function lookupMessage(paths, messageId) { if (!Array.isArray(rows)) continue; for (const row of rows) { if (row && row.messageId === id) { - matches.push({ message: row, store: `inbox:${file.replace('.json', '')}` }); + matchCount++; + if (matchCount > 1) { + throw new Error(`Ambiguous messageId: ${id} found in multiple stores`); + } + match = { message: row, store: `inbox:${file.replace('.json', '')}` }; } } } - if (matches.length === 0) { + if (matchCount === 0) { throw new Error(`Message not found: ${id}`); } - if (matches.length > 1) { - throw new Error(`Ambiguous messageId: ${id} found in ${matches.length} stores`); - } - - return matches[0]; + return match; } module.exports = { diff --git a/agent-teams-controller/test/controller.test.js b/agent-teams-controller/test/controller.test.js index dbce5f19..9f9fd4f6 100644 --- a/agent-teams-controller/test/controller.test.js +++ b/agent-teams-controller/test/controller.test.js @@ -1047,7 +1047,7 @@ describe('agent-teams-controller API', () => { fs.writeFileSync(inboxPath, JSON.stringify([{ messageId: dupeId, text: 'copy-2' }])); expect(() => controller.messages.lookupMessage(dupeId)).toThrow( - 'Ambiguous messageId: dupe-message-id found in 2 stores' + 'Ambiguous messageId: dupe-message-id found in multiple stores' ); }); }); diff --git a/mcp-server/src/controller.ts b/mcp-server/src/controller.ts index 26dce9b6..0cfa581a 100644 --- a/mcp-server/src/controller.ts +++ b/mcp-server/src/controller.ts @@ -8,6 +8,9 @@ const controllerModule = (agentTeamsControllerModule as ControllerModule).default ?? agentTeamsControllerModule; const { createController } = controllerModule; +/** Re-export agentBlocks utilities (stripAgentBlocks, wrapAgentBlock, etc.) */ +export const agentBlocks = controllerModule.agentBlocks; + export function getController(teamName: string, claudeDir?: string) { return createController({ teamName, diff --git a/mcp-server/src/tools/taskTools.ts b/mcp-server/src/tools/taskTools.ts index 84c22e2d..c805da27 100644 --- a/mcp-server/src/tools/taskTools.ts +++ b/mcp-server/src/tools/taskTools.ts @@ -1,9 +1,12 @@ import type { FastMCP } from 'fastmcp'; import { z } from 'zod'; -import { getController } from '../controller'; +import { agentBlocks, getController } from '../controller'; import { jsonTextContent } from '../utils/format'; +/** stripAgentBlocks from canonical agentBlocks module — single source of truth for the tag format. */ +const { stripAgentBlocks } = agentBlocks; + const toolContextSchema = { teamName: z.string().min(1), claudeDir: z.string().min(1).optional(), @@ -11,11 +14,8 @@ const toolContextSchema = { const relationshipTypeSchema = z.enum(['blocked-by', 'blocks', 'related']); -/** Agent-only block tag used for hidden instructions — must be stripped from provenance snapshots. */ -const AGENT_BLOCK_RE = /[\s\S]*?<\/info_for_agent>/g; - /** Allowed message source types for task_create_from_message provenance. Fail closed — only explicit user-originated sources. */ -const USER_ORIGINATED_SOURCES = new Set(['user_sent', 'inbox']); +const USER_ORIGINATED_SOURCES = new Set(['user_sent']); /** * Shared payload builder for both task_create and task_create_from_message. @@ -49,14 +49,6 @@ function buildCreateTaskPayload(params: { }; } -/** - * Strip agent-only `` blocks from message text. - * Returns trimmed text with agent blocks removed. - */ -function stripAgentBlocks(text: string): string { - return text.replace(AGENT_BLOCK_RE, '').trim(); -} - export function registerTaskTools(server: Pick) { server.addTool({ name: 'task_create', @@ -117,6 +109,7 @@ export function registerTaskTools(server: Pick) { subject: z.string().min(1), description: z.string().optional(), owner: z.string().optional(), + createdBy: z.string().optional(), blockedBy: z.array(z.string().min(1)).optional(), related: z.array(z.string().min(1)).optional(), prompt: z.string().optional(), @@ -129,6 +122,7 @@ export function registerTaskTools(server: Pick) { subject, description, owner, + createdBy, blockedBy, related, prompt, @@ -143,7 +137,7 @@ export function registerTaskTools(server: Pick) { const source = typeof message.source === 'string' ? message.source : ''; if (!USER_ORIGINATED_SOURCES.has(source)) { throw new Error( - `Message source "${source}" is not user-originated. Only user_sent and inbox messages are eligible.` + `Message source "${source}" is not user-originated. Only user_sent messages are eligible.` ); } @@ -191,6 +185,7 @@ export function registerTaskTools(server: Pick) { subject, description, owner, + createdBy, blockedBy, related, prompt, diff --git a/mcp-server/test/tools.test.ts b/mcp-server/test/tools.test.ts index d8742c24..dd631492 100644 --- a/mcp-server/test/tools.test.ts +++ b/mcp-server/test/tools.test.ts @@ -1175,7 +1175,7 @@ describe('agent-teams-mcp tools', () => { to: 'lead', text: 'See attached screenshot', timestamp: '2026-03-15T14:00:00.000Z', - source: 'inbox', + source: 'user_sent', attachments: [ { id: 'att-1', filename: 'screenshot.png', mimeType: 'image/png', size: 42000 }, ], diff --git a/src/main/workers/team-fs-worker.ts b/src/main/workers/team-fs-worker.ts index 17bbe3e9..3dbdd0ac 100644 --- a/src/main/workers/team-fs-worker.ts +++ b/src/main/workers/team-fs-worker.ts @@ -135,6 +135,8 @@ interface ParsedTask { workIntervals?: unknown; historyEvents?: unknown; attachments?: unknown; + sourceMessageId?: unknown; + sourceMessage?: unknown; } interface RawWorkInterval { @@ -699,6 +701,18 @@ async function readTasksDirForTeam( attachments: Array.isArray(parsed.attachments) ? (parsed.attachments as unknown[]) : undefined, + sourceMessageId: + typeof parsed.sourceMessageId === 'string' && (parsed.sourceMessageId as string).trim() + ? (parsed.sourceMessageId as string).trim() + : undefined, + sourceMessage: + parsed.sourceMessage && + typeof parsed.sourceMessage === 'object' && + typeof (parsed.sourceMessage as Record).text === 'string' && + typeof (parsed.sourceMessage as Record).from === 'string' && + typeof (parsed.sourceMessage as Record).timestamp === 'string' + ? (parsed.sourceMessage as Record) + : undefined, teamName, }); } catch (error) { diff --git a/src/types/agent-teams-controller.d.ts b/src/types/agent-teams-controller.d.ts index 411551ad..c5b8a8d9 100644 --- a/src/types/agent-teams-controller.d.ts +++ b/src/types/agent-teams-controller.d.ts @@ -67,6 +67,15 @@ declare module 'agent-teams-controller' { getCrossTeamOutbox(): unknown; } + export interface AgentBlocksApi { + AGENT_BLOCK_TAG: string; + AGENT_BLOCK_OPEN: string; + AGENT_BLOCK_CLOSE: string; + AGENT_BLOCK_RE: RegExp; + stripAgentBlocks(text: string): string; + wrapAgentBlock(text: string): string; + } + export interface AgentTeamsController { tasks: ControllerTaskApi; kanban: ControllerKanbanApi; @@ -78,4 +87,6 @@ declare module 'agent-teams-controller' { } export function createController(options: ControllerContextOptions): AgentTeamsController; + + export const agentBlocks: AgentBlocksApi; }