diff --git a/src/cli/commands/startAgent.ts b/src/cli/commands/startAgent.ts index 1a17506..560de66 100644 --- a/src/cli/commands/startAgent.ts +++ b/src/cli/commands/startAgent.ts @@ -201,6 +201,11 @@ export async function startAgentSession(options: StartAgentOptions): Promise) : resolvePending(projectCwd, agent); const lines: string[] = []; + // Zuerst und am deutlichsten: eine bereits gehaltene Task. Sonst wartet der + // Agent nach einem Neustart auf neue Arbeit, waehrend seine eigene liegt. + if (pending.heldTask) { + lines.push(`⚠️ DU HAELTST BEREITS ${pending.heldTask} — arbeite DORT weiter und reiche sie ein, bevor du Neues anfaengst.`); + } if (pending.waitingTasks.length) lines.push(`Offen für dich: ${pending.waitingTasks.join(', ')}`); if (pending.awaitingReview.length) lines.push(`Wartet auf Architekten-Review: ${pending.awaitingReview.join(', ')} — du bist so lange gebunden.`); if (pending.unreadCount) lines.push(`Ungelesene Nachrichten: ${pending.unreadCount}`); diff --git a/src/cli/commands/work.ts b/src/cli/commands/work.ts index 8098170..835649a 100644 --- a/src/cli/commands/work.ts +++ b/src/cli/commands/work.ts @@ -1,5 +1,6 @@ import { parseSSEBuffer } from './watch.js'; import { announceAgent, findAddressedOpenTask, claimAndPrintTask, type AgentContext } from './start.js'; +import { resolvePending } from '../../core/services/checkinService.js'; import { discoverServer as discoverHubServer } from '../../discovery.js'; import { remoteClient } from '../remoteClient.js'; @@ -61,6 +62,21 @@ async function drainAgentMessages(ctx: WorkAgentContext): Promise { export async function workAgent(ctx: WorkAgentContext): Promise { await announceAgent(ctx.serverUrl, ctx.agent, ctx.role); + // Haelt der Agent schon eine Task? Der Finder unten sucht nur `open` — nach + // einem Session-Neustart wartet der Worker deshalb auf neue Arbeit, waehrend + // die eigene, bereits geclaimte Task unbearbeitet liegt. Das war ein echter + // Stillstand: Task auf in_progress, Agent tut nichts, niemand sieht warum. + try { + const held = resolvePending(ctx.projectCwd, ctx.agent).heldTask; + if (held) { + console.log( + `AgentHub: du haeltst bereits ${held}. Arbeite dort weiter und reiche sie mit ` + + `\`agenthub task review ${held}\` ein — erst danach kommt Neues.`, + ); + return; + } + } catch { /* ohne Projekt-Kontext weiter wie bisher */ } + // Already-waiting task? const found = await findAddressedOpenTask(ctx); if (found) { diff --git a/src/core/services/checkinService.ts b/src/core/services/checkinService.ts index b6c41b2..d59ca76 100644 --- a/src/core/services/checkinService.ts +++ b/src/core/services/checkinService.ts @@ -49,6 +49,18 @@ export interface PendingForAgent { }; /** Offene Tasks, die an den Agenten adressiert sind (inkl. Reopens). */ waitingTasks: string[]; + /** + * Die Task, die der Agent GERADE HAELT (in_progress, von ihm geclaimt). + * + * Klingt trivial, war aber eine echte Sackgasse: der Work-Loop sucht nur + * Tasks mit Status `open`. Haelt ein Agent bereits eine in_progress-Task — + * etwa nach einem Session-Neustart —, greift der Busy-Guard und der Loop + * liefert NICHTS zurueck. Der Agent wartet dann auf neue Arbeit, die nie + * kommt, waehrend seine eigene Task unbearbeitet liegt. Deshalb muss der + * gehaltene Task ueberall auftauchen, wo ein Agent nach "was ist zu tun" + * fragt. + */ + heldTask?: string; /** Vom Agenten eingereichte Tasks, die auf das Architekten-Review warten. */ awaitingReview: string[]; /** @@ -130,7 +142,8 @@ export function resolvePending( isMine(task.assignedTo); for (const task of tasks) { - if (task.status === 'open' && addressedToMe(task)) pending.waitingTasks.push(task.id); + if (task.status === 'in_progress' && isMine(task.claimedBy ?? task.assignedTo)) pending.heldTask = task.id; + else if (task.status === 'open' && addressedToMe(task)) pending.waitingTasks.push(task.id); else if (task.status === 'review' && isMine(task.claimedBy ?? task.assignedTo)) pending.awaitingReview.push(task.id); } @@ -174,6 +187,9 @@ export function resolvePending( const parts: string[] = []; if (pending.interrupted) parts.push(pending.interrupted.action); + if (pending.heldTask && !pending.interrupted) { + parts.push(`Du haeltst bereits ${pending.heldTask} — arbeite dort weiter und reiche sie ein, bevor du Neues anfaengst.`); + } if (pending.unreadCount > 0) parts.push(`${pending.unreadCount} unbeantwortete Nachricht(en) — agenthub_inbox lesen, beantworten und mit agenthub_message quittieren.`); if (pending.answeredAsks.length > 0) parts.push(`Deine Frage(n) ${pending.answeredAsks.map((a) => a.id).join(', ')} sind BEANTWORTET — lies die Antwort, bevor du weiterbaust.`); if (pending.waitingTasks.length > 0) parts.push(`Offen für dich: ${pending.waitingTasks.join(', ')}.`); @@ -186,6 +202,7 @@ export function resolvePending( export function hasPending(pending: PendingForAgent): boolean { return ( pending.interrupted !== undefined || + pending.heldTask !== undefined || pending.unreadCount > 0 || pending.answeredAsks.length > 0 || pending.waitingTasks.length > 0 diff --git a/src/mcp/server.ts b/src/mcp/server.ts index bee2bb1..5022fc2 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -311,6 +311,21 @@ export async function startMcpServer(cwd: string, options: { agent?: string } = LOOP += ' UNATTENDED MODE: never pause for human input — when you need a decision, ' + 'call agenthub_ask (it routes to the architect) and await the answer instead of stalling.'; } + // Haelt der Agent bereits eine Task, wartet der Finder ins Leere (er sucht + // nur `open`). Das nennen wir jetzt beim Namen, statt ihn blockieren zu + // lassen — sonst wartet er auf Arbeit, die er laengst hat. + if (!reviewer) { + const held = resolvePending(root, agent).heldTask; + if (held) { + const detail = remote ? await remoteClient.getTask(serverUrl!, held) : getTask(root, held); + return asText({ + held, + body: (detail as { body?: string }).body, + note: `Du haeltst bereits ${held}. Arbeite dort weiter und reiche sie mit agenthub_task_review ein — erst danach bekommst du Neues.`, + loop: LOOP, + }); + } + } const immediate = await finder(); if (immediate) { await leave('work delivered'); diff --git a/tests/checkinService.test.ts b/tests/checkinService.test.ts index a8fc35e..f4c7397 100644 --- a/tests/checkinService.test.ts +++ b/tests/checkinService.test.ts @@ -188,3 +188,24 @@ describe('resolvePending — Adressierung wie im Work-Loop', () => { expect(resolvePending(cwd, 'codex').waitingTasks).toHaveLength(0); }); }); + +describe('resolvePending — bereits gehaltene Task', () => { + it('nennt die in_progress-Task, die der Agent haelt', () => { + const id = openTaskFor('codex'); + claimTask(cwd, id, 'codex'); + + const pending = resolvePending(cwd, 'codex'); + + // Der Work-Loop sucht nur `open` — ohne diesen Hinweis wartet ein Agent + // nach dem Neustart auf neue Arbeit, waehrend seine eigene liegt. + expect(pending.heldTask).toBe(id); + expect(pending.note).toMatch(/haeltst bereits/i); + expect(hasPending(pending)).toBe(true); + }); + + it('meldet keine fremde in_progress-Task als gehalten', () => { + const id = openTaskFor('kimi'); + claimTask(cwd, id, 'kimi'); + expect(resolvePending(cwd, 'codex').heldTask).toBeUndefined(); + }); +});