mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(workboard): add manual card movement surfaces (#96554)
Adds consistent manual Workboard movement across CLI, slash commands, and agent tools while preserving operator authority and agent claim isolation. Co-authored-by: Ross I4JU <ross@i4ju.ch>
This commit is contained in:
+13
-2
@@ -22,6 +22,7 @@ openclaw gateway restart
|
||||
openclaw workboard list [--board <id>] [--status <status>] [--include-archived] [--json]
|
||||
openclaw workboard create <title...> [--notes <text>] [--status <status>] [--priority <priority>] [--agent <id>] [--board <id>] [--labels <items>] [--json]
|
||||
openclaw workboard show <id> [--json]
|
||||
openclaw workboard move <id> --status <status> [--json]
|
||||
openclaw workboard dispatch [--board <id>] [--max-starts <count>] [--admin] [--url <url>] [--token <token>] [--timeout <ms>] [--json]
|
||||
```
|
||||
|
||||
@@ -82,6 +83,15 @@ openclaw workboard show 7f4a2c10 --json
|
||||
|
||||
Text output prints the compact card line and notes. JSON output returns the full card record, including execution metadata, attempts, comments, links, proof, artifacts, worker logs, protocol state, diagnostics, and automation metadata.
|
||||
|
||||
## `move`
|
||||
|
||||
```bash
|
||||
openclaw workboard move 7f4a2c10 --status review
|
||||
openclaw workboard move 7f4a2c10 --status done --json
|
||||
```
|
||||
|
||||
`move` changes the card's status using the same manual-operator path as dragging a card in the dashboard. It accepts a full card id or an unambiguous prefix. Active dependency and schedule holds still apply. Operators may move a claimed card without its agent claim token; claim tokens remain scoped to agent-tool mutations and are redacted from JSON output.
|
||||
|
||||
## `dispatch`
|
||||
|
||||
```bash
|
||||
@@ -134,18 +144,19 @@ Command-capable channels can use the matching slash command:
|
||||
/workboard list
|
||||
/workboard show 7f4a2c10
|
||||
/workboard create Fix stale worker heartbeat
|
||||
/workboard move 7f4a2c10 --status review
|
||||
/workboard dispatch
|
||||
```
|
||||
|
||||
Slash command dispatch also uses the Gateway subagent runtime, so it follows the same claim, worker-start, and failure behavior as the dashboard and CLI Gateway path.
|
||||
|
||||
`/workboard list` and `/workboard show` are read commands for authorized command senders. `/workboard create` and `/workboard dispatch` mutate board state and require owner status on chat surfaces or a Gateway client with `operator.write` or `operator.admin`.
|
||||
`/workboard list` and `/workboard show` are read commands for authorized command senders. `/workboard create`, `/workboard move`, and `/workboard dispatch` mutate board state and require owner status on chat surfaces or a Gateway client with `operator.write` or `operator.admin`.
|
||||
|
||||
## Permissions
|
||||
|
||||
The CLI dispatch path normally requests Gateway `operator.write` and `operator.read` scopes. Workspace-bound cards run directly in an exact configured agent workspace; a worktree request is narrowed to that directory instead of letting the host materialize repository-controlled code. The selected worker must have writable, non-shared Docker sandbox access to that exact workspace, a live container hash matching the requested mounts and policy, and no host escape capability. Pass `--admin` to explicitly request `operator.admin`, allow another host checkout, and use normal managed-worktree setup; the connection fails if that scope is not approved for the client. A read-only Gateway token can inspect Workboard data through read methods, but it cannot create cards or dispatch workers. Workspace limits do not otherwise change manual card movement for callers with Workboard mutation permission.
|
||||
|
||||
Local `list`, `create`, and `show` commands operate on the local OpenClaw state directory used by the current profile. Use `--dev` or `--profile <name>` on the top-level `openclaw` command when you need a different state root.
|
||||
Local `list`, `create`, `show`, and `move` commands operate on the local OpenClaw state directory used by the current profile. Use `--dev` or `--profile <name>` on the top-level `openclaw` command when you need a different state root.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -2164,6 +2164,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: list
|
||||
- H2: create
|
||||
- H2: show
|
||||
- H2: move
|
||||
- H2: dispatch
|
||||
- H2: Slash command parity
|
||||
- H2: Permissions
|
||||
|
||||
@@ -136,6 +136,7 @@ rule as linked sessions (see [Session lifecycle sync](#session-lifecycle-sync)).
|
||||
| `workboard_promote` / `workboard_reassign` / `workboard_reclaim` | Recover or hand off stuck work. |
|
||||
| `workboard_comment` / `workboard_proof` | Add handoff notes or attach proof/artifact references. |
|
||||
| `workboard_unblock` | Move blocked work back to `todo`. |
|
||||
| `workboard_move` | Move a card to another status; claimed cards require the caller's agent claim scope. |
|
||||
| `workboard_dispatch` | Nudge dependency promotion or stale-claim cleanup without launching workers; worker launch uses Gateway or slash-command dispatch. |
|
||||
|
||||
Claimed cards reject agent-tool mutations from other agents unless the caller
|
||||
@@ -241,25 +242,28 @@ through the normal Workboard tools.
|
||||
openclaw workboard list [--board <id>] [--status <status>] [--include-archived] [--json]
|
||||
openclaw workboard create "Fix stale card lifecycle" --priority high --labels bug,workboard
|
||||
openclaw workboard show <card-id> [--json]
|
||||
openclaw workboard move <card-id> --status <status> [--json]
|
||||
openclaw workboard dispatch [--board <id>] [--json]
|
||||
```
|
||||
|
||||
`list` text output hides archived cards by default (`--include-archived`
|
||||
overrides); `--json` always includes archived cards, matching the full-card
|
||||
contract used by existing scripts. `show` accepts an unambiguous id prefix.
|
||||
`list`, `create`, and `show` always read/write local plugin state directly.
|
||||
Only `dispatch` calls the running Gateway, with the fallback described above.
|
||||
contract used by existing scripts. `show` and `move` accept an unambiguous id
|
||||
prefix. `list`, `create`, `show`, and `move` always read/write local plugin
|
||||
state directly. Only `dispatch` calls the running Gateway, with the fallback
|
||||
described above.
|
||||
|
||||
See [Workboard CLI](/cli/workboard) for full flags, JSON output, Gateway
|
||||
fallback behavior, id-prefix handling, dispatch selection rules, and
|
||||
troubleshooting.
|
||||
|
||||
`/workboard list`, `/workboard show <card-id>`, `/workboard create <title>`,
|
||||
and `/workboard dispatch` mirror the CLI. List and show are read operations
|
||||
for any authorized command sender. Create and dispatch require owner status on
|
||||
chat surfaces, or a Gateway client with `operator.write`/`operator.admin`.
|
||||
Their worktree access still follows the same workspace boundary described
|
||||
above.
|
||||
`/workboard move <card-id> --status <status>`, and `/workboard dispatch` mirror
|
||||
the CLI. List and show are read operations for any authorized command sender.
|
||||
Create, move, and dispatch require owner status on chat surfaces, or a Gateway
|
||||
client with `operator.write`/`operator.admin`. Manual operator moves use the
|
||||
same claim-override behavior as dashboard drag-and-drop. Their worktree access
|
||||
still follows the same workspace boundary described above.
|
||||
|
||||
## Session lifecycle sync
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"workboard_proof",
|
||||
"workboard_worker_log",
|
||||
"workboard_protocol_violation",
|
||||
"workboard_unblock"
|
||||
"workboard_unblock",
|
||||
"workboard_move"
|
||||
]
|
||||
},
|
||||
"commandAliases": [
|
||||
@@ -155,6 +156,9 @@
|
||||
},
|
||||
"workboard_unblock": {
|
||||
"optional": true
|
||||
},
|
||||
"workboard_move": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
|
||||
@@ -277,4 +277,36 @@ describe("registerWorkboardCli", () => {
|
||||
program.parseAsync(["workboard", "show", prefix], { from: "user" }),
|
||||
).rejects.toThrow("Ambiguous card id prefix");
|
||||
});
|
||||
|
||||
it("moves claimed cards with operator authority and redacts JSON output", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const card = await store.create({ title: "Claimed card", status: "todo" });
|
||||
await store.claim(card.id, { ownerId: "worker", token: "secret-token" });
|
||||
const program = createProgram(store);
|
||||
|
||||
const output = await captureStdout(async () => {
|
||||
await program.parseAsync(
|
||||
["workboard", "move", card.id.slice(0, 8), "--status", "review", "--json"],
|
||||
{ from: "user" },
|
||||
);
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(output);
|
||||
expect(parsed).toMatchObject({ card: { id: card.id, status: "review" } });
|
||||
expect(parsed.card.metadata.claim.token).toBe("[redacted]");
|
||||
expect(output).not.toContain("secret-token");
|
||||
});
|
||||
|
||||
it("rejects an invalid move status", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const card = await store.create({ title: "Invalid move" });
|
||||
const program = createProgram(store);
|
||||
|
||||
await expect(
|
||||
program.parseAsync(["workboard", "move", card.id, "--status", "later"], {
|
||||
from: "user",
|
||||
}),
|
||||
).rejects.toThrow("--status must be one of");
|
||||
await expect(store.get(card.id)).resolves.toMatchObject({ status: "todo" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolveWorkboardCardByIdOrPrefix } from "./card-lookup.js";
|
||||
import type { WorkboardDispatchResult, WorkboardStore } from "./store.js";
|
||||
import type { WorkboardCard } from "./types.js";
|
||||
import { WORKBOARD_STATUSES, type WorkboardCard, type WorkboardStatus } from "./types.js";
|
||||
|
||||
type JsonOptions = {
|
||||
json?: boolean;
|
||||
@@ -57,6 +57,10 @@ function splitLabels(value: string | undefined): string[] | undefined {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function isWorkboardStatus(value: string): value is WorkboardStatus {
|
||||
return (WORKBOARD_STATUSES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
function formatCardLine(card: WorkboardCard): string {
|
||||
const boardId = card.metadata?.automation?.boardId ?? "default";
|
||||
const agent = card.agentId ? ` ${card.agentId}` : "";
|
||||
@@ -241,6 +245,29 @@ export function registerWorkboardCli(params: { program: Command; store: Workboar
|
||||
}
|
||||
});
|
||||
|
||||
workboard
|
||||
.command("move")
|
||||
.argument("<id>", "Card id or prefix")
|
||||
.description("Move a Workboard card to another status")
|
||||
.requiredOption("--status <status>", "Target status")
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (id: string, options: JsonOptions & { status: string }) => {
|
||||
if (!isWorkboardStatus(options.status)) {
|
||||
throw new Error(`--status must be one of: ${WORKBOARD_STATUSES.join(", ")}.`);
|
||||
}
|
||||
const cards = await params.store.list();
|
||||
const { card, error } = resolveWorkboardCardByIdOrPrefix(cards, id);
|
||||
if (!card) {
|
||||
throw new Error(error);
|
||||
}
|
||||
const updated = await params.store.move(card.id, options.status, undefined);
|
||||
if (options.json) {
|
||||
writeJson({ card: redactClaimToken(updated) });
|
||||
} else {
|
||||
writeLine(formatCardLine(updated));
|
||||
}
|
||||
});
|
||||
|
||||
addGatewayClientOptions(
|
||||
workboard
|
||||
.command("dispatch")
|
||||
|
||||
@@ -242,10 +242,55 @@ describe("handleWorkboardCommand", () => {
|
||||
text: expect.stringContaining("operator.write"),
|
||||
}),
|
||||
);
|
||||
await expect(
|
||||
handleWorkboardCommand({ api, store, args: `move ${card.id} --status running` }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isError: true,
|
||||
text: expect.stringContaining("operator.write"),
|
||||
}),
|
||||
);
|
||||
expect(api.runtime.subagent.run).not.toHaveBeenCalled();
|
||||
await expect(store.get(card.id)).resolves.toMatchObject({ status: "ready" });
|
||||
});
|
||||
|
||||
it("moves claimed cards for operators on slash-command surfaces", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const api = createApi();
|
||||
const card = await store.create({ title: "Claimed slash card", status: "todo" });
|
||||
await store.claim(card.id, { ownerId: "worker", token: "secret-token" });
|
||||
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: `move ${card.id.slice(0, 8)} --status review`,
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
}),
|
||||
).resolves.toEqual(expect.objectContaining({ text: expect.stringContaining("review") }));
|
||||
await expect(store.get(card.id)).resolves.toMatchObject({
|
||||
status: "review",
|
||||
metadata: { claim: { ownerId: "worker", token: "secret-token" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid slash-command move statuses", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const api = createApi();
|
||||
const card = await store.create({ title: "Invalid slash move" });
|
||||
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: `move ${card.id} --status later`,
|
||||
senderIsOwner: true,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({ isError: true, text: expect.stringContaining("status must be") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the slash caller's workspace access for worktree materialization", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const api = createApi();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type WorkboardWorktreeRuntime,
|
||||
} from "./dispatcher.js";
|
||||
import type { WorkboardStore } from "./store.js";
|
||||
import type { WorkboardCard } from "./types.js";
|
||||
import { WORKBOARD_STATUSES, type WorkboardCard, type WorkboardStatus } from "./types.js";
|
||||
import {
|
||||
canonicalizeWorkboardWorkspaceAccess,
|
||||
resolveAgentWorkboardWorkspaceRuntime,
|
||||
@@ -64,6 +64,10 @@ function normalizeTitle(tokens: string[]): string {
|
||||
return tokens.join(" ").trim();
|
||||
}
|
||||
|
||||
function isWorkboardStatus(value: string): value is WorkboardStatus {
|
||||
return (WORKBOARD_STATUSES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
function canMutateWorkboard(params: {
|
||||
senderIsOwner?: boolean;
|
||||
gatewayClientScopes?: readonly string[];
|
||||
@@ -111,6 +115,7 @@ export async function handleWorkboardCommand(params: {
|
||||
"/workboard list",
|
||||
"/workboard show <card-id>",
|
||||
"/workboard create <title>",
|
||||
"/workboard move <card-id> --status <status>",
|
||||
"/workboard dispatch",
|
||||
].join("\n"),
|
||||
};
|
||||
@@ -144,6 +149,33 @@ export async function handleWorkboardCommand(params: {
|
||||
const card = await params.store.create({ title, workspaceAccess });
|
||||
return { text: `Created ${card.id.slice(0, 8)} ${card.title}` };
|
||||
}
|
||||
if (action === "move") {
|
||||
const accessError = requireWriteAccess(params);
|
||||
if (accessError) {
|
||||
return accessError;
|
||||
}
|
||||
const id = rest[0];
|
||||
const statusIndex = rest.indexOf("--status");
|
||||
const status = statusIndex >= 0 ? rest[statusIndex + 1] : undefined;
|
||||
if (!id || !status) {
|
||||
return {
|
||||
text: "Usage: /workboard move <card-id> --status <status>",
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
if (!isWorkboardStatus(status)) {
|
||||
return {
|
||||
text: `status must be one of: ${WORKBOARD_STATUSES.join(", ")}.`,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const cards = await params.store.list();
|
||||
const { card, error } = resolveWorkboardCardByIdOrPrefix(cards, id);
|
||||
if (!card) {
|
||||
return { text: error, isError: true };
|
||||
}
|
||||
return { text: formatCardLine(await params.store.move(card.id, status, undefined)) };
|
||||
}
|
||||
if (action === "dispatch") {
|
||||
const accessError = requireWriteAccess(params);
|
||||
if (accessError) {
|
||||
|
||||
@@ -627,13 +627,6 @@ export class WorkboardCoreStore {
|
||||
}
|
||||
}
|
||||
|
||||
async move(id: string, status: unknown, position: unknown): Promise<WorkboardCard> {
|
||||
return await this.update(id, {
|
||||
status,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<{ deleted: boolean }> {
|
||||
return await this.enqueueMutation(async () => await this.deleteDirect(id));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,31 @@ import { clearDiagnostics, normalizeBoundedString } from "./store-normalizers.js
|
||||
import type { WorkboardCard } from "./types.js";
|
||||
|
||||
export class WorkboardPromoteStore extends WorkboardEnrichmentStore {
|
||||
async move(
|
||||
id: string,
|
||||
status: unknown,
|
||||
position: unknown,
|
||||
scope?: WorkboardMutationScope,
|
||||
): Promise<WorkboardCard> {
|
||||
return await this.enqueueMutation(async () => {
|
||||
const existing = await this.get(id);
|
||||
if (!existing) {
|
||||
throw new Error(`card not found: ${id}`);
|
||||
}
|
||||
// Operator surfaces omit scope and may override claims. Agent tools pass scope so a
|
||||
// worker cannot move another worker's claimed card between the preflight and this write.
|
||||
assertCanMutateClaimedCard(existing, scope);
|
||||
return await this.updateCard(
|
||||
id,
|
||||
{ status, position },
|
||||
{
|
||||
allowMetadataDependencyLinks: false,
|
||||
enforceStatusHolds: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async promote(
|
||||
id: string,
|
||||
input: WorkboardPromoteInput = {},
|
||||
|
||||
@@ -1931,6 +1931,21 @@ describe("WorkboardStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets operators override claims while enforcing agent-scoped moves", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const card = await store.create({ title: "Scoped move", status: "todo" });
|
||||
await store.claim(card.id, { ownerId: "agent-a", token: "test-auth-token" });
|
||||
|
||||
await expect(store.move(card.id, "review", undefined, { ownerId: "agent-b" })).rejects.toThrow(
|
||||
"card is claimed by agent-a",
|
||||
);
|
||||
await expect(store.get(card.id)).resolves.toMatchObject({ status: "running" });
|
||||
|
||||
await expect(store.move(card.id, "review", undefined)).resolves.toMatchObject({
|
||||
status: "review",
|
||||
});
|
||||
});
|
||||
|
||||
it("checks matching claim tokens inside queued card writes", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const card = await store.create({ title: "Token-scoped mutation" });
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { AnyAgentTool } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { AgentToolResult } from "openclaw/plugin-sdk/tool-results";
|
||||
import { Type } from "typebox";
|
||||
import type { WorkboardMutationScope } from "./store-inputs.js";
|
||||
import type { WorkboardStore } from "./store.js";
|
||||
import { WORKBOARD_STATUSES, type WorkboardCard } from "./types.js";
|
||||
|
||||
type ScopedMoveParams = {
|
||||
record: Record<string, unknown>;
|
||||
id: string;
|
||||
scope: WorkboardMutationScope;
|
||||
};
|
||||
|
||||
const ClaimTokenFieldName = "token" as const;
|
||||
|
||||
export function cardIdField() {
|
||||
return Type.String({ description: "Workboard card id." });
|
||||
}
|
||||
|
||||
export function claimTokenField(description = "Claim token returned by workboard_claim.") {
|
||||
return Type.Optional(Type.String({ description }));
|
||||
}
|
||||
|
||||
export function createWorkboardMoveTool(params: {
|
||||
store: WorkboardStore;
|
||||
readScopedCardToolParams: (rawParams: unknown) => Promise<ScopedMoveParams>;
|
||||
redactedCardResult: (card: WorkboardCard) => AgentToolResult<{ card: WorkboardCard }>;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
name: "workboard_move",
|
||||
label: "Workboard Move",
|
||||
description:
|
||||
"Move a Workboard card to another status. Claimed cards require matching claim scope.",
|
||||
parameters: Type.Object(
|
||||
{
|
||||
id: cardIdField(),
|
||||
status: Type.Union(
|
||||
WORKBOARD_STATUSES.map((status) => Type.Literal(status)),
|
||||
{ description: "Target Workboard status." },
|
||||
),
|
||||
[ClaimTokenFieldName]: claimTokenField("Claim token for claimed cards."),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
execute: async (_toolCallId, rawParams) => {
|
||||
const { record, id, scope } = await params.readScopedCardToolParams(rawParams);
|
||||
return params.redactedCardResult(
|
||||
await params.store.move(id, record.status, undefined, scope),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -590,4 +590,41 @@ describe("workboard tools", () => {
|
||||
);
|
||||
expect(Buffer.from(attachment.contentBase64 as string, "base64").toString("utf8")).toBe("done");
|
||||
});
|
||||
|
||||
it("moves cards with agent claim scope", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const api = { runtime: {} } as unknown as OpenClawPluginApi;
|
||||
const tools = new Map(
|
||||
createWorkboardTools({ api, store, context: { agentId: "agent-b" } as never }).map((tool) => [
|
||||
tool.name,
|
||||
tool,
|
||||
]),
|
||||
);
|
||||
const card = await store.create({ title: "Move tool card", status: "todo" });
|
||||
|
||||
const unclaimed = readPayload(
|
||||
await tools.get("workboard_move")?.execute("move-unclaimed", {
|
||||
id: card.id,
|
||||
status: "ready",
|
||||
}),
|
||||
);
|
||||
expect(unclaimed.card).toMatchObject({ status: "ready" });
|
||||
|
||||
await store.claim(card.id, { ownerId: "agent-a", token: "test-auth-token" });
|
||||
await expect(
|
||||
tools.get("workboard_move")?.execute("move-denied", {
|
||||
id: card.id,
|
||||
status: "review",
|
||||
}),
|
||||
).rejects.toThrow("card is claimed by agent-a");
|
||||
|
||||
const claimed = readPayload(
|
||||
await tools.get("workboard_move")?.execute("move-claimed", {
|
||||
id: card.id,
|
||||
status: "review",
|
||||
token: "test-auth-token",
|
||||
}),
|
||||
);
|
||||
expect(claimed.card).toMatchObject({ status: "review" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { Type } from "typebox";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
import { cardIdField, claimTokenField, createWorkboardMoveTool } from "./tools-card-mutations.js";
|
||||
import type { WorkboardCard } from "./types.js";
|
||||
|
||||
function contextOwner(ctx: OpenClawPluginToolContext | undefined): string {
|
||||
@@ -139,14 +140,6 @@ type WorkboardCardMutation = (
|
||||
scope: WorkboardToolCardParams["scope"],
|
||||
) => Promise<WorkboardCard>;
|
||||
|
||||
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." }),
|
||||
@@ -621,6 +614,7 @@ export function createWorkboardTools(params: {
|
||||
return redactedRawCardResult(await store.unblock(id, scope));
|
||||
},
|
||||
},
|
||||
createWorkboardMoveTool({ store, readScopedCardToolParams, redactedCardResult }),
|
||||
{
|
||||
name: "workboard_boards",
|
||||
label: "Workboard Boards",
|
||||
|
||||
@@ -59,6 +59,7 @@ export const WORKBOARD_TOOL_NAMES = [
|
||||
"workboard_worker_log",
|
||||
"workboard_protocol_violation",
|
||||
"workboard_unblock",
|
||||
"workboard_move",
|
||||
] as const;
|
||||
|
||||
const WORKBOARD_REQUIRED_WORKER_TOOLS = [
|
||||
|
||||
Reference in New Issue
Block a user