feat: agenthub start — one-command onboarding (no copy-paste prompts)
An agent runs ONE command and is working: it announces presence, claims the open task addressed to it, and prints the task + its handoff + the next step. Removes the per-agent kickoff copy-paste. - src/cli/commands/start.ts: `agenthub start --agent <name> --role <role>`. "Addressed to <agent>" = task title starts with "<agent>:" OR a handoff has toAgent=<agent>. Claims it, prints task body + handoff + the review gate reminder. No addressed task → lists open role tasks to pick. Works on local + --server paths. - templates: implementer guides (AGENTS/CODEX/KIMI.md) now lead with "On your first turn, RUN `agenthub start …` — do not just summarize", so a fresh session self-onboards from one trigger word. - tests: start auto-claims the addressed task, ignores others, no-ops on empty queue; templates test updated for the new commands. 116/116 green. Bump 0.2.2 -> 0.3.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b0973a878c
commit
7723360054
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"description": "Local coordination layer for AI coding agents",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
99
src/cli/commands/start.ts
Normal file
99
src/cli/commands/start.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { remoteClient } from '../remoteClient.js';
|
||||
import {
|
||||
listTasks as svcListTasks,
|
||||
getTask as svcGetTask,
|
||||
claimTask as svcClaimTask,
|
||||
} from '../../core/services/taskService.js';
|
||||
import { listHandoffs as svcListHandoffs, getHandoff as svcGetHandoff } from '../../core/services/handoffService.js';
|
||||
|
||||
/**
|
||||
* `agenthub start --agent <name> --role <role>` — one-command onboarding for an
|
||||
* agent. Announces presence, claims the open task addressed to this agent, and
|
||||
* prints the task body + its handoff + the next step, so the agent can begin
|
||||
* immediately without the human stitching commands together.
|
||||
*
|
||||
* "Addressed to <agent>" = task title starts with "<agent>:" (the delegation
|
||||
* convention) OR a handoff for that task has toAgent === <agent>.
|
||||
*/
|
||||
export async function startAgent(opts: {
|
||||
serverUrl?: string;
|
||||
projectCwd: string;
|
||||
agent: string;
|
||||
role: string;
|
||||
}): Promise<void> {
|
||||
const { serverUrl, projectCwd, agent, role } = opts;
|
||||
const a = agent.toLowerCase();
|
||||
|
||||
// 1. Announce (presence is best-effort — never block onboarding on it).
|
||||
if (serverUrl) {
|
||||
try {
|
||||
await remoteClient.announce(serverUrl, agent, role);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
console.log(`AgentHub: ${agent} joined (${role})`);
|
||||
|
||||
// 2. Gather open role tasks + handoffs (handoffs carry taskId + toAgent).
|
||||
const tasks = serverUrl
|
||||
? await remoteClient.listTasks(serverUrl, { role, status: 'open' })
|
||||
: svcListTasks(projectCwd, { role, status: 'open' });
|
||||
const handoffs = serverUrl ? await remoteClient.listHandoffs(serverUrl) : svcListHandoffs(projectCwd);
|
||||
|
||||
const addressedByHandoff = new Set(
|
||||
handoffs
|
||||
.filter((h) => h.toAgent && String(h.toAgent).toLowerCase() === a && h.taskId)
|
||||
.map((h) => String(h.taskId)),
|
||||
);
|
||||
const mine = tasks.filter(
|
||||
(t) => (t.title ?? '').toLowerCase().startsWith(`${a}:`) || addressedByHandoff.has(t.id),
|
||||
);
|
||||
|
||||
// 3. Nothing addressed → guide the agent instead of guessing.
|
||||
if (mine.length === 0) {
|
||||
if (tasks.length === 0) {
|
||||
console.log(`AgentHub: no open ${role} tasks — waiting for the architect to delegate.`);
|
||||
} else {
|
||||
console.log(`AgentHub: no task addressed to ${agent}. Open ${role} tasks:`);
|
||||
for (const t of tasks) console.log(` ${t.id} ${t.title ?? ''}`);
|
||||
console.log(`→ claim one yourself: agenthub task claim <id> --agent ${agent}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Claim the addressed task.
|
||||
const task = mine[0];
|
||||
if (serverUrl) await remoteClient.claimTask(serverUrl, task.id, agent);
|
||||
else svcClaimTask(projectCwd, task.id, agent);
|
||||
console.log(`AgentHub: Task claimed ${task.id} ${task.title ?? ''}`);
|
||||
|
||||
// 5. Print the task body.
|
||||
try {
|
||||
const detail = serverUrl ? await remoteClient.getTask(serverUrl, task.id) : svcGetTask(projectCwd, task.id);
|
||||
if (detail.body && detail.body.trim()) {
|
||||
console.log(`\n─ Task ${task.id} ──────────────`);
|
||||
console.log(detail.body.trim());
|
||||
}
|
||||
} catch {
|
||||
/* body is optional */
|
||||
}
|
||||
|
||||
// 6. Print the handoff for this task (scope + acceptance criteria).
|
||||
const hof = handoffs.find((h) => h.taskId === task.id);
|
||||
if (hof) {
|
||||
try {
|
||||
const hd = serverUrl ? await remoteClient.getHandoff(serverUrl, hof.id) : svcGetHandoff(projectCwd, hof.id);
|
||||
console.log(`\n─ Handoff ${hof.id} ──────────────`);
|
||||
console.log(hd.handoff.summary);
|
||||
if (hd.body && hd.body.trim()) console.log(hd.body.trim());
|
||||
} catch {
|
||||
/* handoff is optional */
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Next steps — the review gate, spelled out.
|
||||
console.log(`\n─ Next ──────────────`);
|
||||
console.log(`Implement the task, then submit for review: agenthub task review ${task.id}`);
|
||||
console.log(`Report what you did: agenthub memory add --title "${task.id} result" --category implementation --content "…"`);
|
||||
console.log(`Only the architect closes a task (\`done\`). If reopened, address the feedback and review again.`);
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { delegate } from './commands/delegate.js';
|
||||
import { serverStart } from './commands/server.js';
|
||||
import { update } from './commands/update.js';
|
||||
import { watchEvents } from './commands/watch.js';
|
||||
import { startAgent } from './commands/start.js';
|
||||
import { loadConfig, saveConfig } from '../core/config.js';
|
||||
import { findProjectRoot } from '../core/paths.js';
|
||||
import { discoverServer } from '../discovery.js';
|
||||
@ -112,7 +113,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.2')
|
||||
.version('0.3.0')
|
||||
.option('--server <url>', 'AgentHub server URL (env: AGENTHUB_SERVER)');
|
||||
|
||||
program
|
||||
@ -484,6 +485,21 @@ export function createProgram(cwd: string): Command {
|
||||
}
|
||||
});
|
||||
|
||||
// ─── start (onboarding) ────────────────────────────────────────────────────
|
||||
program
|
||||
.command('start')
|
||||
.description('Onboard: announce, claim the task addressed to you, print it + its handoff')
|
||||
.requiredOption('--agent <name>', 'Agent name')
|
||||
.option('--role <role>', 'Role (default: implementer)', 'implementer')
|
||||
.action(async (options) => {
|
||||
const { serverUrl, projectCwd } = await resolveContext(program, cwd);
|
||||
if (serverUrl) {
|
||||
await runRemote(serverUrl, () => startAgent({ serverUrl, projectCwd, agent: options.agent, role: options.role }));
|
||||
} else {
|
||||
await startAgent({ projectCwd, agent: options.agent, role: options.role });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── watch ───────────────────────────────────────────────────────────────
|
||||
program
|
||||
.command('watch')
|
||||
|
||||
@ -5,8 +5,13 @@ 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.
|
||||
|
||||
## Start here
|
||||
**If you are an implementer, RUN this on your first turn (don't just read it):**
|
||||
\`agenthub start --agent <you> --role implementer\` — it announces you, claims the
|
||||
task addressed to you, and prints it + its handoff so you can begin immediately.
|
||||
|
||||
## Golden rules (every agent)
|
||||
1. Announce yourself on start: \`agenthub hello --agent <you> --role <your-role>\`.
|
||||
1. Announce yourself on start (\`agenthub start …\`, or \`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\`.
|
||||
@ -38,23 +43,25 @@ On start: \`agenthub hello --agent <you> --role architect\`, then \`agenthub wat
|
||||
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:
|
||||
**On your first turn, RUN this and follow its output — do not just summarize:**
|
||||
|
||||
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>\`
|
||||
\`\`\`
|
||||
agenthub start --agent ${agentName} --role implementer
|
||||
\`\`\`
|
||||
|
||||
That one command announces you, claims the task addressed to you, and prints the
|
||||
task + its handoff. Then:
|
||||
|
||||
1. Implement the task.
|
||||
2. **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
|
||||
3. 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.
|
||||
You drive the AgentHub CLI yourself; the human does not type these commands for you.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
51
tests/start.test.ts
Normal file
51
tests/start.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Tests for `agenthub start` — one-command onboarding that announces and
|
||||
* auto-claims the task addressed to the agent (local path; the server path
|
||||
* uses the same logic via remoteClient).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, rmSync } from 'fs';
|
||||
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';
|
||||
|
||||
describe('agenthub start — onboarding auto-claim', () => {
|
||||
let cwd: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cwd = mkdtempSync(join(tmpdir(), 'ah-start-'));
|
||||
init(cwd, { projectName: 'start-test', yes: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('claims the open implementer task addressed to the agent (title prefix)', async () => {
|
||||
const mine = createTask(cwd, { title: 'kimi: backend tests', role: 'implementer' });
|
||||
createTask(cwd, { title: 'codex: magic audit', role: 'implementer' }); // addressed to someone else
|
||||
|
||||
await startAgent({ projectCwd: cwd, agent: 'kimi', role: 'implementer' });
|
||||
|
||||
const { task } = getTask(cwd, mine.id);
|
||||
expect(task.status).toBe('in_progress');
|
||||
expect(task.assignedTo).toBe('kimi');
|
||||
});
|
||||
|
||||
it('does NOT claim a task addressed to a different agent', async () => {
|
||||
const other = createTask(cwd, { title: 'codex: magic audit', role: 'implementer' });
|
||||
|
||||
await startAgent({ projectCwd: cwd, agent: 'kimi', role: 'implementer' });
|
||||
|
||||
const { task } = getTask(cwd, other.id);
|
||||
expect(task.status).toBe('open');
|
||||
expect(task.assignedTo).toBeUndefined();
|
||||
});
|
||||
|
||||
it('claims nothing when there are no open tasks (just announces)', async () => {
|
||||
// Should not throw; nothing to claim.
|
||||
await expect(startAgent({ projectCwd: cwd, agent: 'kimi', role: 'implementer' })).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -8,6 +8,9 @@ describe('templates', () => {
|
||||
|
||||
it('references agenthub commands', () => {
|
||||
expect(claudeMd()).toContain('agenthub decision create');
|
||||
expect(codexMd()).toContain('agenthub task claim');
|
||||
// Implementer guides now onboard via `agenthub start` (which auto-claims)
|
||||
// and submit via the review gate.
|
||||
expect(codexMd()).toContain('agenthub start');
|
||||
expect(codexMd()).toContain('agenthub task review');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user