feat(task): agenthub task assign — architect addresses an open task to an agent
Closes the addressing gap that left a waiting `agenthub work` blocked: the remaining Win tasks were titled "Win L2: …" / unassigned, so they matched no agent and the daemon waited forever. `agenthub task assign <id> --agent <name>` sets assignedTo WITHOUT claiming (status stays open) and fires task/updated — so an agent's blocked `work` re-checks, matches via assignedTo, and auto-claims it. The architect can now route a specific open task to a specific agent and have its daemon pick it up. - taskService.assignTask; PATCH /tasks/:id handles assignedTo-without-status; remoteClient.assignTask; CLI `task assign`. - test: assign keeps status open + sets assignedTo; start/work then claims it via the assignedTo match. 122/122 green. Bump 0.5.0 -> 0.6.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ec378131e4
commit
78bbebf435
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Local coordination layer for AI coding agents",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { input, select } from '@inquirer/prompts';
|
||||
import { createTask, listTasks, getTask, claimTask, doneTask, reviewTask, reopenTask } from '../../core/services/taskService.js';
|
||||
import { createTask, listTasks, getTask, claimTask, doneTask, reviewTask, reopenTask, assignTask } from '../../core/services/taskService.js';
|
||||
import type { Task } from '../../core/schema.js';
|
||||
|
||||
export async function taskCreate(cwd: string, options: Partial<Task> = {}): Promise<void> {
|
||||
@ -64,3 +64,8 @@ export function taskReopen(cwd: string, id: string): void {
|
||||
reopenTask(cwd, id);
|
||||
console.log(`AgentHub: Task reopened ${id}`);
|
||||
}
|
||||
|
||||
export function taskAssign(cwd: string, id: string, agentName: string): void {
|
||||
assignTask(cwd, id, agentName);
|
||||
console.log(`AgentHub: Task assigned ${id} → ${agentName}`);
|
||||
}
|
||||
|
||||
@ -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, taskReview, taskReopen } from './commands/task.js';
|
||||
import { taskCreate, taskList, taskShow, taskClaim, taskDone, taskReview, taskReopen, taskAssign } 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';
|
||||
@ -114,7 +114,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.5.0')
|
||||
.version('0.6.0')
|
||||
.option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)');
|
||||
|
||||
program
|
||||
@ -317,6 +317,21 @@ export function createProgram(cwd: string): Command {
|
||||
taskDone(projectCwd, id, meta);
|
||||
}
|
||||
});
|
||||
taskCmd
|
||||
.command('assign <id>')
|
||||
.description('Address an open task to an agent (architect): it stays open and the agent\'s `work` auto-claims it')
|
||||
.requiredOption('--agent <agent>', 'Agent name')
|
||||
.action(async (id, options) => {
|
||||
const { serverUrl, projectCwd } = await resolveContext(program, cwd);
|
||||
if (serverUrl) {
|
||||
await runRemote(serverUrl, async () => {
|
||||
await remoteClient.assignTask(serverUrl, id, options.agent);
|
||||
console.log(`AgentHub: Task assigned ${id} → ${options.agent}`);
|
||||
});
|
||||
} else {
|
||||
taskAssign(projectCwd, id, options.agent);
|
||||
}
|
||||
});
|
||||
taskCmd
|
||||
.command('review <id>')
|
||||
.description('Submit a task for architect review (implementer: use this instead of done)')
|
||||
|
||||
@ -75,6 +75,10 @@ export const remoteClient = {
|
||||
});
|
||||
},
|
||||
|
||||
async assignTask(baseUrl: string, id: string, agentName: string): Promise<Task> {
|
||||
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { assignedTo: agentName });
|
||||
},
|
||||
|
||||
async reviewTask(baseUrl: string, id: string): Promise<Task> {
|
||||
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'review' });
|
||||
},
|
||||
|
||||
@ -84,6 +84,16 @@ export function reopenTask(cwd: string, id: string): Task {
|
||||
return updateTask(cwd, id, { status: 'open' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Address an OPEN task to an agent without claiming it: sets assignedTo but
|
||||
* leaves the status untouched (open). The agent's `agenthub work` then auto-
|
||||
* claims it (assignedTo match), so the architect can route a specific task to
|
||||
* a specific agent and have a waiting daemon pick it up.
|
||||
*/
|
||||
export function assignTask(cwd: string, id: string, agentName: string): Task {
|
||||
return updateTask(cwd, id, { assignedTo: agentName });
|
||||
}
|
||||
|
||||
function toIndexEntry(task: Task, filePath: string) {
|
||||
return {
|
||||
id: task.id,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import { createTask, listTasks, getTask, claimTask, doneTask, reviewTask, cancelTask, reopenTask } from '../core/services/taskService.js';
|
||||
import { createTask, listTasks, getTask, claimTask, doneTask, reviewTask, cancelTask, reopenTask, assignTask } from '../core/services/taskService.js';
|
||||
import { getTaskActivity } from '../core/services/activityService.js';
|
||||
import { createHandoff, listHandoffs, getHandoff } from '../core/services/handoffService.js';
|
||||
import { createDecision, listDecisions } from '../core/services/decisionService.js';
|
||||
@ -148,6 +148,25 @@ export async function registerRoutes(app: FastifyInstance, cwd: string): Promise
|
||||
const { id } = request.params as { id: string };
|
||||
const patch = request.body as Partial<Task>;
|
||||
|
||||
// Assign without claiming: address an open task to an agent (no status
|
||||
// change). Fires task/updated so a waiting `agenthub work` auto-claims it.
|
||||
if (patch.assignedTo !== undefined && patch.status === undefined) {
|
||||
const assigned = assignTask(cwd, id, patch.assignedTo);
|
||||
emitChange(
|
||||
{
|
||||
type: 'task',
|
||||
action: 'updated',
|
||||
id: assigned.id,
|
||||
title: assigned.title,
|
||||
status: assigned.status,
|
||||
role: assigned.role,
|
||||
assignedTo: assigned.assignedTo,
|
||||
},
|
||||
assigned.updatedAt,
|
||||
);
|
||||
return assigned;
|
||||
}
|
||||
|
||||
let task: Task;
|
||||
switch (patch.status) {
|
||||
case 'in_progress':
|
||||
|
||||
@ -9,7 +9,7 @@ import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { init } from '../src/cli/commands/init.js';
|
||||
import { startAgent } from '../src/cli/commands/start.js';
|
||||
import { createTask, getTask } from '../src/core/services/taskService.js';
|
||||
import { createTask, getTask, assignTask } from '../src/core/services/taskService.js';
|
||||
|
||||
describe('agenthub start — onboarding auto-claim', () => {
|
||||
let cwd: string;
|
||||
@ -48,4 +48,19 @@ describe('agenthub start — onboarding auto-claim', () => {
|
||||
// Should not throw; nothing to claim.
|
||||
await expect(startAgent({ projectCwd: cwd, agent: 'kimi', role: 'implementer' })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('assignTask addresses an open task; start then claims it via assignedTo', async () => {
|
||||
// A generically-titled task, NOT name-addressed to the agent.
|
||||
const t = createTask(cwd, { title: 'Win L2: generic bypass task', role: 'implementer' });
|
||||
|
||||
assignTask(cwd, t.id, 'windows-claude');
|
||||
const afterAssign = getTask(cwd, t.id).task;
|
||||
expect(afterAssign.status).toBe('open'); // assign does NOT claim
|
||||
expect(afterAssign.assignedTo).toBe('windows-claude');
|
||||
|
||||
// Now the agent's start/work finds it via the assignedTo match and claims it.
|
||||
await startAgent({ projectCwd: cwd, agent: 'windows-claude', role: 'implementer' });
|
||||
expect(getTask(cwd, t.id).task.status).toBe('in_progress');
|
||||
expect(getTask(cwd, t.id).task.assignedTo).toBe('windows-claude');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user