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:
parent
45070222d0
commit
353c139796
20
package.json
20
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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`);
|
||||
},
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user