feat: review-gate (task review/reopen) + autonomous role guides

Implements the architect-controlled workflow: implementers submit work
for review, only the architect closes tasks.

- CLI: `agenthub task review <id>` (implementer submits → status review)
  and `agenthub task reopen <id>` (architect re-triggers → status open).
  Server already supported both statuses; now exposed on local + --server
  paths, with remoteClient.reviewTask/reopenTask.
- templates: AGENTS.md + role guides rewritten so agents self-drive the
  CLI on open (hello → claim → implement → task review → wait), with the
  hard rule that ONLY the architect runs `task done`. Implementer guides
  (codex/kimi) never call done; architect guide documents done/reopen.
- tests: review/reopen SSE events + formatEvent "Task review"/"Task
  reopened" lines. 113/113 green.

Bump 0.2.0 -> 0.2.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-27 16:48:42 +02:00
parent 45070222d0
commit 353c139796
6 changed files with 141 additions and 47 deletions

View File

@ -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"
]
}
}

View File

@ -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<Task> = {}): Promise<void> {
@ -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}`);
}

View File

@ -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<void>): Promise<vo
export function createProgram(cwd: string): Command {
const program = new Command('agenthub')
.description('Local coordination layer for AI coding agents')
.version('0.2.0')
.version('0.2.1')
.option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)');
program
@ -315,6 +315,34 @@ export function createProgram(cwd: string): Command {
taskDone(projectCwd, id, meta);
}
});
taskCmd
.command('review <id>')
.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 <id>')
.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');

View File

@ -75,6 +75,14 @@ export const remoteClient = {
});
},
async reviewTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'review' });
},
async reopenTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'open' });
},
async getTaskActivity(baseUrl: string, id: string): Promise<ActivityItem[]> {
return request<ActivityItem[]>(baseUrl, 'GET', `/tasks/${id}/activity`);
},

View File

@ -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 <you> --role <your-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 <id>\` — never \`agenthub task done\`.
4. Report results as memory: \`agenthub memory add --title "<TSK> 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 <you> --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 <agent>\`, inspect the work + its memory, then:
- Satisfied \`agenthub task done <id>\`.
- Not satisfied \`agenthub task reopen <id>\` + 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 <id> --agent ${agentName}\`
3. Read its handoff: \`agenthub handoff read <HOF-id>\`
4. Implement it.
5. **Submit for review (NOT done):** \`agenthub task review <id>\`
and report: \`agenthub memory add --title "<id> result" --category implementation
--content "<what you did / how to verify it>"\`
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 <id>\`
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');
}

View File

@ -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 <id> by <agent>"', () => {
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 <id>"', () => {
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',