// Workboard plugin module implements tools behavior. import { jsonResult, readStringParam } from "openclaw/plugin-sdk/core"; import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry"; import { Type } from "typebox"; import { WorkboardStore } from "./store.js"; import type { WorkboardCard } from "./types.js"; function contextOwner(ctx: OpenClawPluginToolContext | undefined): string { const record = (ctx ?? {}) as Record; return ( (typeof record.agentId === "string" && record.agentId) || (typeof record.sessionKey === "string" && record.sessionKey) || (typeof record.sessionId === "string" && record.sessionId) || "agent" ); } function canMutateCard(card: WorkboardCard, ownerId: string, token?: string): boolean { const claim = card.metadata?.claim; if (!claim) { return true; } return claim.ownerId === ownerId || (Boolean(token) && claim.token === token); } function readParentIds(value: unknown): string[] { if (value == null) { return []; } const entries = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : undefined; if (!entries) { throw new Error("parents must be an array or comma-separated string."); } const parents: string[] = []; for (const entry of entries) { if (typeof entry !== "string") { throw new Error("parents must contain only strings."); } const parent = entry.trim(); if (!parent || parents.includes(parent)) { continue; } if (parent.length > 120) { throw new Error("parents must be 120 characters or fewer."); } parents.push(parent); if (parents.length >= 20) { break; } } return parents; } async function requireScopedCard( store: WorkboardStore, cardId: string, ownerId: string, token?: string, ): Promise { const card = await store.get(cardId); if (!card) { throw new Error(`card not found: ${cardId}`); } if (!canMutateCard(card, ownerId, token)) { throw new Error(`card is claimed by ${card.metadata?.claim?.ownerId ?? "another agent"}.`); } return card; } async function requireClaimedCard( store: WorkboardStore, cardId: string, ownerId: string, token?: string, ): Promise { const card = await requireScopedCard(store, cardId, ownerId, token); if (!card.metadata?.claim) { throw new Error("card must be claimed before lifecycle completion."); } return card; } function summarizeCard(card: WorkboardCard) { return { id: card.id, title: card.title, status: card.status, priority: card.priority, agentId: card.agentId, tenant: card.metadata?.automation?.tenant, boardId: card.metadata?.automation?.boardId ?? "default", parents: card.metadata?.links ?.filter((link) => link.type === "parent" && link.targetCardId) .map((link) => link.targetCardId), children: card.metadata?.links ?.filter((link) => link.type === "child" && link.targetCardId) .map((link) => link.targetCardId), claim: card.metadata?.claim ? { ownerId: card.metadata.claim.ownerId, claimedAt: card.metadata.claim.claimedAt, lastHeartbeatAt: card.metadata.claim.lastHeartbeatAt, expiresAt: card.metadata.claim.expiresAt, } : undefined, diagnostics: card.metadata?.diagnostics, archivedAt: card.metadata?.archivedAt, updatedAt: card.updatedAt, }; } function redactClaimToken(card: WorkboardCard): WorkboardCard { const claim = card.metadata?.claim; if (!claim) { return card; } return { ...card, metadata: { ...card.metadata, claim: { ...claim, token: "[redacted]", }, }, }; } type WorkboardToolCardParams = { record: Record; id: string; token?: string; scope: { ownerId: string; token?: string }; }; type WorkboardToolCardParamsReader = (rawParams: unknown) => Promise; type WorkboardCardMutation = ( id: string, record: Record, scope: WorkboardToolCardParams["scope"], ) => Promise; function cardIdField() { return Type.String({ description: "Workboard card id." }); } function claimTokenField(description = "Claim token returned by workboard_claim.") { return Type.Optional(Type.String({ description })); } const ScopedClaimTokenField = claimTokenField("Claim token for claimed cards."); const OptionalNextStatusField = Type.Optional( Type.String({ description: "Optional next status." }), ); const OptionalOperatorNoteField = Type.Optional( Type.String({ description: "Optional operator note." }), ); function readCardToolParams(rawParams: unknown, ownerId: string): WorkboardToolCardParams { const record = rawParams as Record; const id = readStringParam(record, "id", { required: true }); const token = record.token as string | undefined; return { record, id, token, scope: { ownerId, token }, }; } function redactedCardResult(card: WorkboardCard) { return jsonResult({ card: redactClaimToken(card) }); } function redactedRawCardResult(card: WorkboardCard) { return jsonResult(redactClaimToken(card)); } const CardIdSchema = Type.Object( { id: cardIdField(), token: claimTokenField(), }, { additionalProperties: false }, ); export function createWorkboardTools(params: { api: OpenClawPluginApi; context?: OpenClawPluginToolContext; store?: WorkboardStore; }): AnyAgentTool[] { const store = params.store ?? WorkboardStore.openSqlite(); const ownerId = contextOwner(params.context); const readScopedCardToolParams = async (rawParams: unknown): Promise => { const input = readCardToolParams(rawParams, ownerId); await requireScopedCard(store, input.id, ownerId, input.token); return input; }; const readClaimedCardToolParams = async ( rawParams: unknown, ): Promise => { const input = readCardToolParams(rawParams, ownerId); await requireClaimedCard(store, input.id, ownerId, input.token); return input; }; const runCardMutation = async ( rawParams: unknown, readParams: WorkboardToolCardParamsReader, mutate: WorkboardCardMutation, ) => { const { record, id, scope } = await readParams(rawParams); return redactedCardResult(await mutate(id, record, scope)); }; const runScopedCardMutation = (rawParams: unknown, mutate: WorkboardCardMutation) => runCardMutation(rawParams, readScopedCardToolParams, mutate); const runClaimedCardMutation = (rawParams: unknown, mutate: WorkboardCardMutation) => runCardMutation(rawParams, readClaimedCardToolParams, mutate); return [ { name: "workboard_list", label: "Workboard List", description: "List Workboard cards with compact claim and diagnostic state. Use before choosing or routing board work.", parameters: Type.Object( { status: Type.Optional(Type.String({ description: "Optional card status filter." })), agentId: Type.Optional(Type.String({ description: "Optional agent id filter." })), tenant: Type.Optional(Type.String({ description: "Optional tenant filter." })), boardId: Type.Optional(Type.String({ description: "Optional board id filter." })), limit: Type.Optional( Type.Number({ description: "Maximum cards to return. Default 50." }), ), refreshDiagnostics: Type.Optional( Type.Boolean({ description: "Refresh stored diagnostics before listing." }), ), includeArchived: Type.Optional( Type.Boolean({ description: "Include archived cards. Default false." }), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; if (record.refreshDiagnostics === true) { await store.refreshDiagnostics(); } const status = typeof record.status === "string" ? record.status : undefined; const agentId = typeof record.agentId === "string" ? record.agentId : undefined; const tenant = typeof record.tenant === "string" ? record.tenant : undefined; const boardId = typeof record.boardId === "string" ? record.boardId : undefined; const limit = typeof record.limit === "number" && Number.isFinite(record.limit) ? Math.max(1, Math.min(200, Math.trunc(record.limit))) : 50; const cards = (await store.list({ boardId })) .filter((card) => record.includeArchived === true || !card.metadata?.archivedAt) .filter((card) => !status || card.status === status) .filter((card) => !agentId || card.agentId === agentId) .filter((card) => !tenant || card.metadata?.automation?.tenant === tenant) .slice(0, limit) .map(summarizeCard); return jsonResult({ cards }); }, }, { name: "workboard_create", label: "Workboard Create", description: "Create a Workboard card, optionally with parent dependencies, tenant, skills, workspace, and idempotency key.", parameters: Type.Object( { title: Type.String({ description: "Card title." }), notes: Type.Optional(Type.String({ description: "Card notes or acceptance criteria." })), status: Type.Optional(Type.String({ description: "Initial status." })), priority: Type.Optional(Type.String({ description: "low, normal, high, or urgent." })), labels: Type.Optional(Type.Array(Type.String(), { description: "Card labels." })), agentId: Type.Optional(Type.String({ description: "Assigned agent id." })), parents: Type.Optional(Type.Array(Type.String(), { description: "Parent card ids." })), token: Type.Optional( Type.String({ description: "Claim token for claimed parent cards." }), ), tenant: Type.Optional(Type.String({ description: "Soft tenant namespace." })), boardId: Type.Optional(Type.String({ description: "Soft board namespace." })), createdByCardId: Type.Optional( Type.String({ description: "Parent card that created this card." }), ), idempotencyKey: Type.Optional(Type.String({ description: "Idempotent create key." })), skills: Type.Optional(Type.Array(Type.String(), { description: "Suggested skills." })), workspace: Type.Optional( Type.Object( { kind: Type.String({ description: "scratch, dir, or worktree." }), path: Type.Optional(Type.String({ description: "Absolute dir/worktree path." })), branch: Type.Optional(Type.String({ description: "Suggested branch." })), }, { additionalProperties: false }, ), ), maxRuntimeSeconds: Type.Optional(Type.Number({ description: "Run timeout seconds." })), maxRetries: Type.Optional(Type.Number({ description: "Retry budget." })), scheduledAt: Type.Optional(Type.Number({ description: "Unix epoch milliseconds." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; readParentIds(record.parents); return jsonResult({ card: redactClaimToken( await store.create(record, { ownerId, token: record.token as string | undefined }), ), }); }, }, { name: "workboard_link", label: "Workboard Link", description: "Link a parent card to a child card so the child becomes ready only after parents are done.", parameters: Type.Object( { parentId: Type.String({ description: "Parent card id." }), childId: Type.String({ description: "Child card id." }), token: Type.Optional( Type.String({ description: "Claim token for claimed parent or child cards." }), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; const parentId = readStringParam(record, "parentId", { required: true }); const childId = readStringParam(record, "childId", { required: true }); const token = record.token as string | undefined; return jsonResult({ card: redactClaimToken(await store.linkCards(parentId, childId, { ownerId, token })), }); }, }, { name: "workboard_read", label: "Workboard Read", description: "Read one Workboard card and return bounded worker context with notes, attempts, comments, proof, links, and diagnostics.", parameters: CardIdSchema, execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; const id = readStringParam(record, "id", { required: true }); const card = await store.get(id); if (!card) { throw new Error(`card not found: ${id}`); } return jsonResult({ card: redactClaimToken(card), workerContext: await store.buildWorkerContext(id), }); }, }, { name: "workboard_claim", label: "Workboard Claim", description: "Claim a Workboard card for this agent and move backlog/todo cards into running. Returns a claim token for heartbeats and release.", parameters: Type.Object( { id: cardIdField(), ttlSeconds: Type.Optional(Type.Number({ description: "Claim TTL in seconds." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; const id = readStringParam(record, "id", { required: true }); const claimed = await store.claim(id, { ownerId, ttlSeconds: record.ttlSeconds, }); return jsonResult({ ...claimed, card: redactClaimToken(claimed.card) }); }, }, { name: "workboard_heartbeat", label: "Workboard Heartbeat", description: "Refresh this agent's Workboard claim heartbeat. Use during long-running card work so diagnostics do not mark it stale.", parameters: Type.Object( { id: cardIdField(), token: claimTokenField(), note: Type.Optional(Type.String({ description: "Optional compact progress note." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); return redactedRawCardResult( await store.heartbeat(id, { ...scope, note: record.note, }), ); }, }, { name: "workboard_release", label: "Workboard Release", description: "Release this agent's Workboard claim after finishing, pausing, or handing off card work.", parameters: Type.Object( { id: cardIdField(), token: claimTokenField(), status: Type.Optional( Type.String({ description: "Optional next card status after release." }), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); return redactedRawCardResult( await store.releaseClaim(id, { ...scope, status: record.status, }), ); }, }, { name: "workboard_comment", label: "Workboard Comment", description: "Append a compact comment to a Workboard card.", parameters: Type.Object( { id: cardIdField(), body: Type.String({ description: "Comment body." }), token: ScopedClaimTokenField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); return redactedRawCardResult(await store.addComment(id, { body: record.body }, scope)); }, }, { name: "workboard_proof", label: "Workboard Proof", description: "Attach proof or artifact metadata to a Workboard card after running tests, checks, or producing screenshots/logs.", parameters: Type.Object( { id: cardIdField(), status: Type.Optional( Type.String({ description: "passed, failed, skipped, or unknown." }), ), label: Type.Optional(Type.String({ description: "Proof label." })), command: Type.Optional(Type.String({ description: "Command or exact step run." })), url: Type.Optional(Type.String({ description: "Proof or artifact URL." })), note: Type.Optional(Type.String({ description: "Short proof note." })), artifactPath: Type.Optional( Type.String({ description: "Optional local artifact path." }), ), token: ScopedClaimTokenField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); const hasArtifact = (typeof record.artifactPath === "string" && record.artifactPath.trim() !== "") || (typeof record.url === "string" && record.url.trim() !== ""); const card = hasArtifact ? await store.addProofWithArtifact( id, record, { label: record.label, path: record.artifactPath, url: record.url, }, scope, ) : await store.addProof(id, record, scope); return redactedCardResult(card); }, }, { name: "workboard_complete", label: "Workboard Complete", description: "Complete a claimed Workboard card with a structured summary, proof, artifacts, and created-card manifest.", parameters: Type.Object( { id: cardIdField(), token: claimTokenField(), summary: Type.Optional(Type.String({ description: "Completion summary." })), proof: Type.Optional( Type.Object( { status: Type.Optional( Type.String({ description: "passed, failed, skipped, or unknown." }), ), label: Type.Optional(Type.String({ description: "Proof label." })), command: Type.Optional(Type.String({ description: "Command or step run." })), url: Type.Optional(Type.String({ description: "Proof URL." })), note: Type.Optional(Type.String({ description: "Proof note." })), }, { additionalProperties: false }, ), ), artifacts: Type.Optional( Type.Array( Type.Object( { label: Type.Optional(Type.String()), url: Type.Optional(Type.String()), path: Type.Optional(Type.String()), mimeType: Type.Optional(Type.String()), }, { additionalProperties: false }, ), ), ), createdCardIds: Type.Optional( Type.Array(Type.String(), { description: "Cards created during this run." }), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { return runClaimedCardMutation(rawParams, (id, record, scope) => store.complete(id, record, scope), ); }, }, { name: "workboard_attachment_add", label: "Workboard Attachment Add", description: "Store a small Workboard attachment in plugin SQLite KV and link it to the card.", parameters: Type.Object( { id: cardIdField(), fileName: Type.String({ description: "Attachment file name." }), contentBase64: Type.String({ description: "Base64 attachment content." }), mimeType: Type.Optional(Type.String({ description: "Attachment MIME type." })), note: Type.Optional(Type.String({ description: "Optional attachment note." })), token: ScopedClaimTokenField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); return redactedCardResult(await store.addAttachment(id, record, scope)); }, }, { name: "workboard_attachment_read", label: "Workboard Attachment Read", description: "Read one Workboard attachment from plugin SQLite KV.", parameters: Type.Object( { id: Type.String({ description: "Attachment id." }), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const id = readStringParam(rawParams as Record, "id", { required: true, }); const attachment = await store.getAttachment(id); if (!attachment) { throw new Error(`attachment not found: ${id}`); } return jsonResult(attachment); }, }, { name: "workboard_attachment_delete", label: "Workboard Attachment Delete", description: "Delete one Workboard attachment from plugin SQLite KV and the card index.", parameters: Type.Object( { id: cardIdField(), attachmentId: Type.String({ description: "Attachment id." }), token: ScopedClaimTokenField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); const attachmentId = readStringParam(record, "attachmentId", { required: true }); return redactedCardResult(await store.deleteAttachment(id, attachmentId, scope)); }, }, { name: "workboard_block", label: "Workboard Block", description: "Block a claimed Workboard card with a durable reason and release the claim.", parameters: Type.Object( { id: cardIdField(), token: claimTokenField(), reason: Type.Optional(Type.String({ description: "Blocker summary." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { return runClaimedCardMutation(rawParams, (id, record, scope) => store.block(id, record, scope), ); }, }, { name: "workboard_unblock", label: "Workboard Unblock", description: "Move a blocked Workboard card back to todo after adding enough context.", parameters: CardIdSchema, execute: async (_toolCallId, rawParams) => { const { id, scope } = await readScopedCardToolParams(rawParams); return redactedRawCardResult(await store.unblock(id, scope)); }, }, { name: "workboard_boards", label: "Workboard Boards", description: "List Workboard board namespaces with active, archived, and status counts.", parameters: Type.Object({}, { additionalProperties: false }), execute: async () => jsonResult(await store.listBoards()), }, { name: "workboard_board_create", label: "Workboard Board Create", description: "Create or update a Workboard board namespace with persisted SQLite metadata.", parameters: Type.Object( { id: Type.String({ description: "Board id." }), name: Type.Optional(Type.String({ description: "Display name." })), description: Type.Optional(Type.String({ description: "Board description." })), icon: Type.Optional(Type.String({ description: "Short icon or label." })), color: Type.Optional(Type.String({ description: "Display color token." })), defaultWorkspace: Type.Optional( Type.Object( { kind: Type.String({ description: "scratch, dir, or worktree." }), path: Type.Optional(Type.String({ description: "Absolute dir/worktree path." })), branch: Type.Optional(Type.String({ description: "Suggested branch." })), }, { additionalProperties: false }, ), ), orchestration: Type.Optional( Type.Object( { autoDecompose: Type.Optional( Type.Boolean({ description: "Mark ready triage cards for decomposition." }), ), autoDecomposePerDispatch: Type.Optional( Type.Number({ description: "Maximum orchestration candidates per dispatch." }), ), defaultAssignee: Type.Optional(Type.String({ description: "Default assignee." })), orchestratorProfile: Type.Optional( Type.String({ description: "Orchestrator profile id." }), ), }, { additionalProperties: false }, ), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => jsonResult({ board: await store.upsertBoard(rawParams as Record) }), }, { name: "workboard_board_archive", label: "Workboard Board Archive", description: "Archive or restore persisted Workboard board metadata.", parameters: Type.Object( { id: Type.String({ description: "Board id." }), archived: Type.Optional(Type.Boolean({ description: "Archive when true." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; return jsonResult({ board: await store.archiveBoard(record.id, record.archived) }); }, }, { name: "workboard_board_delete", label: "Workboard Board Delete", description: "Delete an empty non-default Workboard board metadata record.", parameters: Type.Object( { id: Type.String({ description: "Board id." }) }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => jsonResult(await store.deleteBoard((rawParams as Record).id)), }, { name: "workboard_stats", label: "Workboard Stats", description: "Summarize Workboard counts by status and assignee for one board or all boards.", parameters: Type.Object( { boardId: Type.Optional(Type.String({ description: "Optional board id filter." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; return jsonResult(await store.stats({ boardId: record.boardId })); }, }, { name: "workboard_runs", label: "Workboard Runs", description: "List persisted Workboard run attempts for one card.", parameters: CardIdSchema, execute: async (_toolCallId, rawParams) => { const id = readStringParam(rawParams as Record, "id", { required: true }); const result = await store.runs(id); return jsonResult({ ...result, card: redactClaimToken(result.card) }); }, }, { name: "workboard_specify", label: "Workboard Specify", description: "Turn a rough triage/backlog Workboard card into a specified todo card after reasoning through the requirements.", parameters: Type.Object( { id: Type.String({ description: "Workboard card id." }), title: Type.Optional(Type.String({ description: "Clarified title." })), notes: Type.Optional( Type.String({ description: "Clarified notes or acceptance criteria." }), ), agentId: Type.Optional(Type.String({ description: "Assigned agent id." })), priority: Type.Optional(Type.String({ description: "low, normal, high, or urgent." })), labels: Type.Optional(Type.Array(Type.String(), { description: "Card labels." })), boardId: Type.Optional(Type.String({ description: "Board id." })), tenant: Type.Optional(Type.String({ description: "Tenant or routing namespace." })), skills: Type.Optional(Type.Array(Type.String(), { description: "Suggested skills." })), workspace: Type.Optional( Type.Object( { kind: Type.String({ description: "scratch, dir, or worktree." }), path: Type.Optional(Type.String({ description: "Absolute dir/worktree path." })), branch: Type.Optional(Type.String({ description: "Suggested branch." })), }, { additionalProperties: false }, ), ), maxRuntimeSeconds: Type.Optional(Type.Number({ description: "Runtime budget." })), maxRetries: Type.Optional(Type.Number({ description: "Retry budget." })), summary: Type.Optional(Type.String({ description: "Specification summary comment." })), token: Type.Optional(Type.String({ description: "Claim token for claimed cards." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; const id = readStringParam(record, "id", { required: true }); await requireScopedCard(store, id, ownerId, record.token as string | undefined); return jsonResult({ card: redactClaimToken(await store.specify(id, record, { ownerId, token: record.token })), }); }, }, { name: "workboard_decompose", label: "Workboard Decompose", description: "Fan out a Workboard card into linked child cards and optionally complete the parent orchestration card.", parameters: Type.Object( { id: Type.String({ description: "Parent Workboard card id." }), token: Type.Optional(Type.String({ description: "Claim token for claimed cards." })), summary: Type.Optional(Type.String({ description: "Decomposition summary." })), completeParent: Type.Optional( Type.Boolean({ description: "Complete the parent after child creation. Default true.", }), ), children: Type.Array( Type.Object( { title: Type.String({ description: "Child title." }), notes: Type.Optional(Type.String({ description: "Child notes." })), agentId: Type.Optional(Type.String({ description: "Assigned agent id." })), priority: Type.Optional( Type.String({ description: "low, normal, high, or urgent." }), ), labels: Type.Optional(Type.Array(Type.String())), boardId: Type.Optional(Type.String()), tenant: Type.Optional(Type.String()), skills: Type.Optional(Type.Array(Type.String())), workspace: Type.Optional( Type.Object( { kind: Type.String({ description: "scratch, dir, or worktree." }), path: Type.Optional( Type.String({ description: "Absolute dir/worktree path." }), ), branch: Type.Optional(Type.String({ description: "Suggested branch." })), }, { additionalProperties: false }, ), ), maxRuntimeSeconds: Type.Optional(Type.Number()), maxRetries: Type.Optional(Type.Number()), idempotencyKey: Type.Optional(Type.String()), }, { additionalProperties: false }, ), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams as Record; const id = readStringParam(record, "id", { required: true }); await requireScopedCard(store, id, ownerId, record.token as string | undefined); const result = await store.decompose(id, record, { ownerId, token: record.token }); return jsonResult({ parent: redactClaimToken(result.parent), children: result.children.map(redactClaimToken), }); }, }, { name: "workboard_notify_subscribe", label: "Workboard Notify Subscribe", description: "Persist a Workboard notification subscription in the plugin SQLite store.", parameters: Type.Object( { boardId: Type.Optional(Type.String({ description: "Board id. Default default." })), cardId: Type.Optional(Type.String({ description: "Card id." })), sessionKey: Type.Optional(Type.String({ description: "Session key." })), runId: Type.Optional(Type.String({ description: "Run id." })), target: Type.Optional(Type.String({ description: "Human-readable target." })), eventKinds: Type.Optional( Type.Array(Type.String(), { description: "completed, failed, stale." }), ), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => jsonResult({ subscription: await store.subscribeNotifications(rawParams as Record), }), }, { name: "workboard_notify_list", label: "Workboard Notify List", description: "List persisted Workboard notification subscriptions.", parameters: Type.Object( { boardId: Type.Optional(Type.String({ description: "Board id." })), cardId: Type.Optional(Type.String({ description: "Card id." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => jsonResult(await store.listNotificationSubscriptions(rawParams as Record)), }, { name: "workboard_notify_events", label: "Workboard Notify Events", description: "Read replay-safe Workboard notification events without advancing cursors.", parameters: Type.Object( { subscriptionId: Type.Optional(Type.String({ description: "Subscription id." })), boardId: Type.Optional(Type.String({ description: "Board id." })), cardId: Type.Optional(Type.String({ description: "Card id." })), limit: Type.Optional(Type.Number({ description: "Maximum events. Default 50." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => jsonResult(await store.notificationEvents(rawParams as Record)), }, { name: "workboard_notify_advance", label: "Workboard Notify Advance", description: "Read Workboard notification events and advance the subscription cursor.", parameters: Type.Object( { subscriptionId: Type.String({ description: "Subscription id." }), limit: Type.Optional(Type.Number({ description: "Maximum events. Default 50." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => jsonResult(await store.advanceNotificationEvents(rawParams as Record)), }, { name: "workboard_notify_unsubscribe", label: "Workboard Notify Unsubscribe", description: "Delete a persisted Workboard notification subscription.", parameters: Type.Object( { id: Type.String({ description: "Subscription id." }) }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const id = readStringParam(rawParams as Record, "id", { required: true }); return jsonResult(await store.deleteNotificationSubscription(id)); }, }, { name: "workboard_promote", label: "Workboard Promote", description: "Promote a dependency-ready card into ready, optionally forcing past holds for operator recovery.", parameters: Type.Object( { id: cardIdField(), token: ScopedClaimTokenField, force: Type.Optional( Type.Boolean({ description: "Bypass dependency or schedule holds." }), ), reason: OptionalOperatorNoteField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { return runScopedCardMutation(rawParams, (id, record, scope) => store.promote(id, record, scope), ); }, }, { name: "workboard_reassign", label: "Workboard Reassign", description: "Change a card assignee and optionally reset failure state during recovery.", parameters: Type.Object( { id: cardIdField(), token: ScopedClaimTokenField, agentId: Type.Optional(Type.String({ description: "New assignee id." })), status: OptionalNextStatusField, resetFailures: Type.Optional(Type.Boolean({ description: "Reset failure count." })), reason: OptionalOperatorNoteField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { return runScopedCardMutation(rawParams, (id, record, scope) => store.reassign(id, record, scope), ); }, }, { name: "workboard_reclaim", label: "Workboard Reclaim", description: "Release a stale claim and stop running attempts so another agent can pick it up.", parameters: Type.Object( { id: cardIdField(), token: ScopedClaimTokenField, status: OptionalNextStatusField, reason: OptionalOperatorNoteField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { return runScopedCardMutation(rawParams, (id, record, scope) => store.reclaim(id, record, scope), ); }, }, { name: "workboard_dispatch", label: "Workboard Dispatch", description: "Run one Workboard dispatcher pass: promote unblocked cards, reclaim expired claims, and block timed-out runs.", parameters: Type.Object( { boardId: Type.Optional(Type.String({ description: "Optional board id filter." })), }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const record = rawParams && typeof rawParams === "object" && !Array.isArray(rawParams) ? (rawParams as Record) : {}; const result = await store.dispatch({ boardId: record.boardId }); return jsonResult({ ...result, promoted: result.promoted.map(redactClaimToken), reclaimed: result.reclaimed.map(redactClaimToken), blocked: result.blocked.map(redactClaimToken), orchestrated: result.orchestrated.map(redactClaimToken), }); }, }, { name: "workboard_worker_log", label: "Workboard Worker Log", description: "Append a persisted worker log entry to a Workboard card.", parameters: Type.Object( { id: cardIdField(), level: Type.Optional(Type.String({ description: "info, warning, or error." })), message: Type.String({ description: "Worker log message." }), sessionKey: Type.Optional(Type.String({ description: "Linked session key." })), runId: Type.Optional(Type.String({ description: "Linked run id." })), token: ScopedClaimTokenField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readScopedCardToolParams(rawParams); return redactedCardResult(await store.addWorkerLog(id, record, scope)); }, }, { name: "workboard_protocol_violation", label: "Workboard Protocol Violation", description: "Block a card and record a worker protocol violation when work stops without complete/block.", parameters: Type.Object( { id: cardIdField(), detail: Type.Optional(Type.String({ description: "Violation detail." })), sessionKey: Type.Optional(Type.String({ description: "Linked session key." })), runId: Type.Optional(Type.String({ description: "Linked run id." })), token: ScopedClaimTokenField, }, { additionalProperties: false }, ), execute: async (_toolCallId, rawParams) => { const { record, id, scope } = await readClaimedCardToolParams(rawParams); return redactedCardResult(await store.recordProtocolViolation(id, record, scope)); }, }, ]; }