- MCP create-backlog-item for natural language Plan/Epic/Task authoring - Worker non-code: optional worktree + per-harness allowlist + doc_review→published - Harness registry: harness_providers/role_harness_routes, WorkerAdapter (minimal|crewai|langgraph|remote-ag-ui), AG-UI POST /api/ag-ui, Settings → Harnesses with Test - Plans tree: backlog.listTree + createPlan/Epic/Task + Start→Convoy, web CRUD without CLI - Fleet: /fleet rollup across workspaces + SSE /api/fleet/events Decision: docs/decisions/2026-08-25-second-brain-os-pivot.md Convoy: .convoys/second-brain-os-v01 Co-authored-by: orchestrator
218 lines
6.1 KiB
TypeScript
218 lines
6.1 KiB
TypeScript
import { and, eq, isNull } from "drizzle-orm";
|
|
|
|
import type { db as dbInstance } from "@tasks/database";
|
|
import { agentRuns, auditLog, markdownBacklogItems } from "@tasks/database/schema";
|
|
import { exportBacklogItemToMarkdown, resolveWorkflowPrompt } from "@tasks/database/markdown-backlog";
|
|
|
|
type Db = typeof dbInstance;
|
|
|
|
export async function findBacklogItemByRepoPath(
|
|
db: Db,
|
|
workspaceId: string,
|
|
repoPath: string,
|
|
) {
|
|
const [row] = await db
|
|
.select({
|
|
id: markdownBacklogItems.id,
|
|
title: markdownBacklogItems.title,
|
|
status: markdownBacklogItems.status,
|
|
repoPath: markdownBacklogItems.repoPath,
|
|
})
|
|
.from(markdownBacklogItems)
|
|
.where(
|
|
and(
|
|
eq(markdownBacklogItems.workspaceId, workspaceId),
|
|
eq(markdownBacklogItems.repoPath, repoPath),
|
|
),
|
|
)
|
|
.limit(1);
|
|
|
|
return row ?? null;
|
|
}
|
|
|
|
export function pickTaskRepoPath(files: string[]): string | null {
|
|
const taskFile = files.find((file) => /Task-[^/]+\.md$/i.test(file));
|
|
return taskFile ?? files[0] ?? null;
|
|
}
|
|
|
|
export async function claimTask(
|
|
db: Db,
|
|
input: {
|
|
workspaceId: string;
|
|
backlogItemId: string;
|
|
client?: string;
|
|
model?: string;
|
|
},
|
|
): Promise<{ runId: string; workflowPrompt: string }> {
|
|
const [item] = await db
|
|
.select({
|
|
id: markdownBacklogItems.id,
|
|
workspaceId: markdownBacklogItems.workspaceId,
|
|
status: markdownBacklogItems.status,
|
|
})
|
|
.from(markdownBacklogItems)
|
|
.where(eq(markdownBacklogItems.id, input.backlogItemId))
|
|
.limit(1);
|
|
|
|
if (!item || item.workspaceId !== input.workspaceId) {
|
|
throw new Error(`Backlog item ${input.backlogItemId} not found in workspace`);
|
|
}
|
|
|
|
const [existing] = await db
|
|
.select({ id: agentRuns.id })
|
|
.from(agentRuns)
|
|
.where(
|
|
and(eq(agentRuns.backlogItemId, input.backlogItemId), isNull(agentRuns.finishedAt)),
|
|
)
|
|
.limit(1);
|
|
|
|
if (existing) {
|
|
const resolved = await resolveWorkflowPrompt(db, {
|
|
workspaceId: input.workspaceId,
|
|
backlogItemId: input.backlogItemId,
|
|
});
|
|
return { runId: existing.id, workflowPrompt: resolved.prompt };
|
|
}
|
|
|
|
const priorStatus = item.status;
|
|
const shouldTransition = priorStatus === "ready" || priorStatus === "draft";
|
|
const metadata =
|
|
input.client || input.model
|
|
? {
|
|
...(input.client ? { client: input.client } : {}),
|
|
...(input.model ? { model: input.model } : {}),
|
|
}
|
|
: null;
|
|
|
|
const runId = await db.transaction(async (tx) => {
|
|
const [created] = await tx
|
|
.insert(agentRuns)
|
|
.values({
|
|
workspaceId: input.workspaceId,
|
|
backlogItemId: input.backlogItemId,
|
|
actorUserId: null,
|
|
metadata,
|
|
})
|
|
.returning({ id: agentRuns.id });
|
|
|
|
if (shouldTransition) {
|
|
await tx
|
|
.update(markdownBacklogItems)
|
|
.set({ status: "in_progress", updatedAt: new Date() })
|
|
.where(eq(markdownBacklogItems.id, input.backlogItemId));
|
|
}
|
|
|
|
await tx.insert(auditLog).values({
|
|
workspaceId: input.workspaceId,
|
|
actorUserId: null,
|
|
action: "task.claimed",
|
|
targetType: "agent_run",
|
|
targetId: created.id,
|
|
metadata: {
|
|
backlog_item_id: input.backlogItemId,
|
|
prior_status: priorStatus,
|
|
transitioned: shouldTransition,
|
|
system_actor: "echodo-worker",
|
|
...(metadata ?? {}),
|
|
},
|
|
});
|
|
|
|
return created.id;
|
|
});
|
|
|
|
const resolved = await resolveWorkflowPrompt(db, {
|
|
workspaceId: input.workspaceId,
|
|
backlogItemId: input.backlogItemId,
|
|
});
|
|
|
|
return { runId, workflowPrompt: resolved.prompt };
|
|
}
|
|
|
|
export async function completeTask(
|
|
db: Db,
|
|
input: {
|
|
runId: string;
|
|
outcome: "succeeded" | "failed" | "cancelled" | "stalled";
|
|
tokensTotal?: number;
|
|
tokensInput?: number;
|
|
tokensOutput?: number;
|
|
notes?: string;
|
|
error?: string;
|
|
finalStatus?: "done" | "blocked" | "ready" | "in_progress" | "cancelled";
|
|
modelAlias?: string;
|
|
modelTier?: string;
|
|
providerType?: string;
|
|
modelId?: string;
|
|
harnessAlias?: string | null;
|
|
repoRoot?: string | null;
|
|
},
|
|
) {
|
|
const [run] = await db.select().from(agentRuns).where(eq(agentRuns.id, input.runId)).limit(1);
|
|
if (!run) throw new Error("Run not found");
|
|
if (run.finishedAt !== null) throw new Error("Run already finished");
|
|
|
|
const finalStatus =
|
|
input.finalStatus ??
|
|
(input.outcome === "succeeded" ? "done" : input.outcome === "failed" ? "blocked" : null);
|
|
|
|
const tokensTotal =
|
|
input.tokensTotal ??
|
|
(typeof input.tokensInput === "number" && typeof input.tokensOutput === "number"
|
|
? input.tokensInput + input.tokensOutput
|
|
: null);
|
|
|
|
await db.transaction(async (tx) => {
|
|
await tx
|
|
.update(agentRuns)
|
|
.set({
|
|
finishedAt: new Date(),
|
|
outcome: input.outcome,
|
|
tokensInput: input.tokensInput ?? null,
|
|
tokensOutput: input.tokensOutput ?? null,
|
|
tokensTotal,
|
|
notes: input.notes ?? null,
|
|
error: input.error ?? null,
|
|
modelAlias: input.modelAlias ?? null,
|
|
modelTier: input.modelTier ?? null,
|
|
providerType: input.providerType ?? null,
|
|
modelId: input.modelId ?? null,
|
|
harnessAlias: input.harnessAlias ?? null,
|
|
metadata: {
|
|
...(run.metadata ?? {}),
|
|
client: "echodo-worker",
|
|
harnessAlias: input.harnessAlias ?? null,
|
|
},
|
|
})
|
|
.where(eq(agentRuns.id, input.runId));
|
|
|
|
if (finalStatus) {
|
|
await tx
|
|
.update(markdownBacklogItems)
|
|
.set({ status: finalStatus, updatedAt: new Date() })
|
|
.where(eq(markdownBacklogItems.id, run.backlogItemId));
|
|
}
|
|
|
|
await tx.insert(auditLog).values({
|
|
workspaceId: run.workspaceId,
|
|
actorUserId: null,
|
|
action: "task.completed",
|
|
targetType: "agent_run",
|
|
targetId: input.runId,
|
|
metadata: {
|
|
outcome: input.outcome,
|
|
finalStatus,
|
|
backlogItemId: run.backlogItemId,
|
|
tokensTotal,
|
|
system_actor: "echodo-worker",
|
|
},
|
|
});
|
|
});
|
|
|
|
if (input.repoRoot && finalStatus) {
|
|
await exportBacklogItemToMarkdown(db, {
|
|
workspaceId: run.workspaceId,
|
|
backlogItemId: run.backlogItemId,
|
|
repoRootAbs: input.repoRoot,
|
|
});
|
|
}
|
|
}
|