diff --git a/package.json b/package.json index fa1e9df..865037e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "agenthub", - "version": "0.2.0", + "version": "0.2.1", "description": "Local coordination layer for AI coding agents", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "files": ["dist", "bin"], + "files": [ + "dist", + "bin" + ], "bin": { "agenthub": "./bin/agenthub.js" }, @@ -32,9 +35,18 @@ "typescript": "^5.7.0", "vitest": "^3.0.0" }, - "keywords": ["ai", "agents", "claude", "codex", "kimi", "collaboration"], + "keywords": [ + "ai", + "agents", + "claude", + "codex", + "kimi", + "collaboration" + ], "license": "MIT", "pnpm": { - "onlyBuiltDependencies": ["better-sqlite3"] + "onlyBuiltDependencies": [ + "better-sqlite3" + ] } } diff --git a/src/cli/commands/task.ts b/src/cli/commands/task.ts index eea1c45..3d36f2c 100644 --- a/src/cli/commands/task.ts +++ b/src/cli/commands/task.ts @@ -1,5 +1,5 @@ import { input, select } from '@inquirer/prompts'; -import { createTask, listTasks, getTask, claimTask, doneTask } from '../../core/services/taskService.js'; +import { createTask, listTasks, getTask, claimTask, doneTask, reviewTask, reopenTask } from '../../core/services/taskService.js'; import type { Task } from '../../core/schema.js'; export async function taskCreate(cwd: string, options: Partial = {}): Promise { @@ -54,3 +54,13 @@ export function taskDone( }); console.log(`AgentHub: Task done ${id}`); } + +export function taskReview(cwd: string, id: string): void { + reviewTask(cwd, id); + console.log(`AgentHub: Task review ${id} (awaiting architect review)`); +} + +export function taskReopen(cwd: string, id: string): void { + reopenTask(cwd, id); + console.log(`AgentHub: Task reopened ${id}`); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 72bdff4..afbb2bc 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,7 +2,7 @@ import { Command } from 'commander'; import { init } from './commands/init.js'; import { status } from './commands/status.js'; import { memoryAdd, memorySearch, memoryList, type MemoryAddOptions } from './commands/memory.js'; -import { taskCreate, taskList, taskShow, taskClaim, taskDone } from './commands/task.js'; +import { taskCreate, taskList, taskShow, taskClaim, taskDone, taskReview, taskReopen } from './commands/task.js'; import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js'; import { decisionCreate, decisionList } from './commands/decision.js'; import { delegate } from './commands/delegate.js'; @@ -112,7 +112,7 @@ async function runRemote(serverUrl: string, fn: () => Promise): Promise', 'AgentHub server URL (env: AGENTHUB_SERVER)'); program @@ -315,6 +315,34 @@ export function createProgram(cwd: string): Command { taskDone(projectCwd, id, meta); } }); + taskCmd + .command('review ') + .description('Submit a task for architect review (implementer: use this instead of done)') + .action(async (id) => { + const { serverUrl, projectCwd } = await resolveContext(program, cwd); + if (serverUrl) { + await runRemote(serverUrl, async () => { + await remoteClient.reviewTask(serverUrl, id); + console.log(`AgentHub: Task review ${id} (awaiting architect review)`); + }); + } else { + taskReview(projectCwd, id); + } + }); + taskCmd + .command('reopen ') + .description('Re-trigger a task after review (architect: send back to implementer)') + .action(async (id) => { + const { serverUrl, projectCwd } = await resolveContext(program, cwd); + if (serverUrl) { + await runRemote(serverUrl, async () => { + await remoteClient.reopenTask(serverUrl, id); + console.log(`AgentHub: Task reopened ${id}`); + }); + } else { + taskReopen(projectCwd, id); + } + }); program.addCommand(taskCmd); const handoffCmd = new Command('handoff').description('Manage handoffs'); diff --git a/src/cli/remoteClient.ts b/src/cli/remoteClient.ts index 22dca19..b7adbb3 100644 --- a/src/cli/remoteClient.ts +++ b/src/cli/remoteClient.ts @@ -75,6 +75,14 @@ export const remoteClient = { }); }, + async reviewTask(baseUrl: string, id: string): Promise { + return request(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'review' }); + }, + + async reopenTask(baseUrl: string, id: string): Promise { + return request(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'open' }); + }, + async getTaskActivity(baseUrl: string, id: string): Promise { return request(baseUrl, 'GET', `/tasks/${id}/activity`); }, diff --git a/src/core/templates.ts b/src/core/templates.ts index b94dbc1..4aaa88f 100644 --- a/src/core/templates.ts +++ b/src/core/templates.ts @@ -1,58 +1,67 @@ export function agentsMd(projectName: string): string { - return `# AgentHub Instructions — ${projectName} + return `# AgentHub — ${projectName} -This project uses AgentHub for shared team memory. +This project coordinates multiple AI agents via AgentHub. The hub server runs on +the team's hub machine; you connect **automatically** (the project config's +\`serverUrl\`, or LAN auto-discovery) — no manual setup, no \`--server\` flag. -Before you start working: -1. Read \`.agenthub/status/latest.md\`. -2. Check \`.agenthub/tasks/\` for open tasks assigned to your role. -3. Read any handoff files relevant to your current task. +## Golden rules (every agent) +1. Announce yourself on start: \`agenthub hello --agent --role \`. +2. Read \`.agenthub/status/latest.md\` and the handoff(s) for your task first. +3. **Only the architect marks a task \`done\`.** Implementers submit finished work + with \`agenthub task review \` — never \`agenthub task done\`. +4. Report results as memory: \`agenthub memory add --title " result" --content "…"\`. +5. Never delete or overwrite files in \`.agenthub/\` unless explicitly asked. -While working: -- Create decision records for architectural or technical choices (\`agenthub decision create\`). -- Update task status (\`agenthub task done TSK-0001\`). - -When handing off: -- Create a handoff file (\`agenthub handoff create\`). -- Include context, open questions, and next steps. - -Do not delete or overwrite files in \`.agenthub/\` unless explicitly asked. +Drive the AgentHub CLI yourself — the human does not type these commands for you. `; } export function claudeMd(): string { - return `# Claude Code — Team Role + return `# Claude Code — AgentHub role: architect -You are part of the AgentHub team in this project. -Your default roles: architect, reviewer. +Read \`AGENTS.md\` first. You are the **architect**: you delegate work and you hold +final say. Implementers cannot close their own tasks. -Read \`AGENTS.md\` and \`.agenthub/status/latest.md\` before making decisions. -Use \`agenthub decision create\` for architecture choices. -Use \`agenthub handoff create\` when passing work to the implementer. +On start: \`agenthub hello --agent --role architect\`, then \`agenthub watch\`. + +- **Delegate:** \`agenthub task create --role implementer …\` + \`agenthub handoff create + --toRole implementer …\` with scope + acceptance criteria. +- **Review gate — only you close tasks.** When the stream shows + \`AgentHub: Task review … by \`, inspect the work + its memory, then: + - Satisfied → \`agenthub task done \`. + - Not satisfied → \`agenthub task reopen \` + a feedback handoff (what to fix). +- Record architecture choices: \`agenthub decision create\`. +`; +} + +function implementerMd(cliName: string, agentName: string, roles: string): string { + return `# ${cliName} — AgentHub roles: ${roles} + +Read \`AGENTS.md\` first. As an **implementer**, drive this loop yourself on open — +the human does not type AgentHub commands for you: + +1. Announce: \`agenthub hello --agent ${agentName} --role implementer\` +2. Find your task: \`agenthub task list --role implementer --status open\` + → pick the one addressed to you (title / handoff), then + \`agenthub task claim --agent ${agentName}\` +3. Read its handoff: \`agenthub handoff read \` +4. Implement it. +5. **Submit for review (NOT done):** \`agenthub task review \` + and report: \`agenthub memory add --title " result" --category implementation + --content ""\` +6. Wait for the architect's verdict. If the task is **reopened** (status back to + \`open\`), read the new feedback handoff, address it, and \`agenthub task review \` + again. + +⚠️ NEVER run \`agenthub task done\` — only the architect approves and closes tasks. `; } export function codexMd(): string { - return `# Codex CLI — Team Role - -You are part of the AgentHub team in this project. -Your default roles: implementer, tester. - -Read \`AGENTS.md\` and \`.agenthub/status/latest.md\` before implementing. -Use \`agenthub task claim TSK-0001\` to reserve a task. -Use \`agenthub task done TSK-0001\` when implementation is complete. -Use \`agenthub handoff create\` when passing work to the reviewer. -`; + return implementerMd('Codex CLI', 'codex', 'implementer, tester'); } export function kimiMd(): string { - return `# Kimi Code CLI — Team Role - -You are part of the AgentHub team in this project. -Your default roles: architect, implementer, reviewer. - -Read \`AGENTS.md\` and \`.agenthub/status/latest.md\` before making decisions. -Use \`agenthub decision create\` for architecture choices. -Use \`agenthub handoff create\` when passing work to another agent. -`; + return implementerMd('Kimi Code CLI', 'kimi', 'implementer'); } diff --git a/tests/sse.test.ts b/tests/sse.test.ts index 7a871a2..5d46704 100644 --- a/tests/sse.test.ts +++ b/tests/sse.test.ts @@ -131,6 +131,16 @@ describe('formatEvent (AgentHub-branded)', () => { const ev: AgentHubEvent = { type: 'agent', action: 'joined', id: 'kimi' }; expect(formatEvent(ev)).toBe('AgentHub: kimi joined'); }); + + it('formats a review submission as "Task review by "', () => { + const ev: AgentHubEvent = { type: 'task', action: 'updated', id: 'TSK-0019', status: 'review', assignedTo: 'codex' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Task review\s+TSK-0019\s+by codex$/); + }); + + it('formats a reopened task as "Task reopened "', () => { + const ev: AgentHubEvent = { type: 'task', action: 'updated', id: 'TSK-0019', status: 'open' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Task reopened\s+TSK-0019$/); + }); }); // ─── 3. Integration: eventBus fires on REST mutations ──────────────────────── @@ -178,6 +188,23 @@ describe('eventBus mutations', () => { expect(collected[0]).toMatchObject({ type: 'task', action: 'updated', status: 'in_progress', assignedTo: 'mac-claude' }); }); + it('emits task/updated review when an implementer submits for review', async () => { + await app.inject({ method: 'POST', url: '/tasks', payload: { title: 'T', role: 'implementer' } }); + collected.length = 0; + await app.inject({ method: 'PATCH', url: '/tasks/TSK-0001', payload: { status: 'review' } }); + expect(collected).toHaveLength(1); + expect(collected[0]).toMatchObject({ type: 'task', action: 'updated', status: 'review' }); + }); + + it('emits task/updated open when the architect reopens after review', async () => { + await app.inject({ method: 'POST', url: '/tasks', payload: { title: 'T', role: 'implementer' } }); + await app.inject({ method: 'PATCH', url: '/tasks/TSK-0001', payload: { status: 'review' } }); + collected.length = 0; + await app.inject({ method: 'PATCH', url: '/tasks/TSK-0001', payload: { status: 'open' } }); + expect(collected).toHaveLength(1); + expect(collected[0]).toMatchObject({ type: 'task', action: 'updated', status: 'open' }); + }); + it('emits handoff/created when POST /handoffs succeeds', async () => { await app.inject({ method: 'POST',