Skip to content

Commit a341ae2

Browse files
authored
feat(workboard): add orchestration primitives
Adds Workboard orchestration statuses, dependency links, idempotent child creation, dispatch, and complete/block lifecycle operations backed by the plugin SQLite keyed store. Persists tenant, skills, workspace, schedule, runtime, retry, dispatch, and handoff metadata in card records, with claim scoping and token redaction. Surfaces the new states and metadata in the Control UI, horizontal board layout, localized strings, and Workboard docs. Verification: - pnpm test extensions/workboard/src/store.test.ts extensions/workboard/src/tools.test.ts extensions/workboard/src/gateway.test.ts ui/src/ui/controllers/workboard.test.ts ui/src/styles/workboard.test.ts ui/src/ui/views/workboard.test.ts -- --reporter=verbose - pnpm ui:i18n:check - /Users/steipete/Projects/agent-scripts/skills/autoreview/scripts/autoreview --mode branch --base origin/main, followed by focused clean local autoreview loops for final fixes - env -u OPENCLAW_TESTBOX pnpm check:changed - git diff --check
1 parent 18f94fc commit a341ae2

53 files changed

Lines changed: 3116 additions & 192 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/plugins/workboard.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,19 @@ view shows a plugin-unavailable state instead of local card data.
4242
Each card stores:
4343

4444
- title and notes
45-
- status: `backlog`, `todo`, `running`, `review`, `blocked`, or `done`
45+
- status: `triage`, `backlog`, `todo`, `scheduled`, `ready`, `running`,
46+
`review`, `blocked`, or `done`
4647
- priority: `low`, `normal`, `high`, or `urgent`
4748
- labels
4849
- optional agent id
4950
- optional linked session, run, task, or source URL
5051
- optional execution metadata for a Codex or Claude session started from the card
51-
- compact metadata for attempts, comments, links, proof, artifacts, claims, diagnostics, notifications, templates, archive state, and stale-session detection
52-
- recent card events such as created, moved, linked, claimed, heartbeat, attempt, proof, artifact, diagnostic, notification, archive, stale, or agent-updated changes
52+
- compact metadata for attempts, comments, links, proof, artifacts, automation,
53+
claims, diagnostics, notifications, templates, archive state, and
54+
stale-session detection
55+
- recent card events such as created, moved, linked, claimed, heartbeat,
56+
attempt, proof, artifact, diagnostic, notification, dispatch, archive, stale,
57+
or agent-updated changes
5358

5459
Cards are stored in the plugin's Gateway state. They are local to the Gateway
5560
state directory and move with the rest of that Gateway's OpenClaw state.
@@ -87,14 +92,23 @@ Workboard also exposes optional agent tools for board-aware workflows:
8792
- `workboard_list` lists compact cards with claim and diagnostic state.
8893
- `workboard_read` returns one card plus bounded worker context built from notes,
8994
attempts, comments, links, proof, artifacts, and active diagnostics.
90-
- `workboard_claim` claims a card for the calling agent and moves backlog or todo
91-
cards into `running`.
95+
- `workboard_create` creates a card with optional parents, tenant, skills,
96+
workspace metadata, idempotency key, runtime limit, and retry budget.
97+
- `workboard_link` links a parent card to a child card. Children stay in `todo`
98+
until every parent reaches `done`; then dispatch promotion moves them to
99+
`ready`.
100+
- `workboard_claim` claims a card for the calling agent and moves backlog, todo,
101+
or ready cards into `running`.
92102
- `workboard_heartbeat` refreshes the claim heartbeat during longer runs.
93103
- `workboard_release` releases the claim after completion, pause, or handoff and
94104
can move the card to a next status.
95-
- `workboard_comment`, `workboard_proof`, and `workboard_unblock` let an agent
96-
add handoff notes, attach proof or artifact references, and move blocked work
97-
back to `todo`.
105+
- `workboard_complete` and `workboard_block` are structured lifecycle tools for
106+
final summaries, proof, artifacts, created-card manifests, and blocker
107+
reasons.
108+
- `workboard_comment`, `workboard_proof`, `workboard_unblock`, and
109+
`workboard_dispatch` let an agent add handoff notes, attach proof or artifact
110+
references, move blocked work back to `todo`, and nudge dependency promotion or
111+
stale-claim cleanup.
98112

99113
Claimed cards reject agent-tool mutations from other agents unless the caller
100114
has the claim token returned by `workboard_claim`. Dashboard operators still use
@@ -105,6 +119,12 @@ flag assigned cards that wait too long, running cards without recent heartbeat,
105119
blocked cards that need attention, repeated failures, done cards without proof,
106120
and running cards that only have a loose session link.
107121

122+
Dispatch is intentionally Gateway-local. It does not spawn arbitrary operating
123+
system processes; normal OpenClaw sessions still own execution. A dispatch nudge
124+
promotes dependency-ready cards, records dispatch metadata on ready cards, and
125+
blocks expired claims or timed-out runs so operators can recover them from the
126+
board.
127+
108128
## Session lifecycle sync
109129

110130
Cards can be linked to existing dashboard sessions or to the session created
@@ -163,8 +183,9 @@ The plugin registers Gateway RPC methods under the `workboard.*` namespace:
163183
- `workboard.cards.export` requires `operator.read`
164184
- `workboard.cards.diagnostics` requires `operator.read`
165185
- `workboard.cards.diagnostics.refresh` requires `operator.write`
166-
- create, update, move, delete, comment, link, proof, artifact, claim, heartbeat,
167-
release, unblock, bulk, and archive methods require `operator.write`
186+
- create, update, move, delete, comment, link, dependency link, proof, artifact,
187+
claim, heartbeat, release, complete, block, unblock, dispatch, bulk, and
188+
archive methods require `operator.write`
168189

169190
Browsers connected with read-only operator access can inspect the board but
170191
cannot mutate cards.

extensions/workboard/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ export default definePluginEntry({
1515
api.registerTool((context) => createWorkboardTools({ api, context, store }), {
1616
names: [
1717
"workboard_list",
18+
"workboard_create",
19+
"workboard_link",
1820
"workboard_read",
1921
"workboard_claim",
2022
"workboard_heartbeat",
23+
"workboard_complete",
24+
"workboard_block",
25+
"workboard_dispatch",
2126
"workboard_release",
2227
"workboard_comment",
2328
"workboard_proof",

extensions/workboard/openclaw.plugin.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
"contracts": {
1010
"tools": [
1111
"workboard_list",
12+
"workboard_create",
13+
"workboard_link",
1214
"workboard_read",
1315
"workboard_claim",
1416
"workboard_heartbeat",
17+
"workboard_complete",
18+
"workboard_block",
19+
"workboard_dispatch",
1520
"workboard_release",
1621
"workboard_comment",
1722
"workboard_proof",
@@ -22,6 +27,12 @@
2227
"workboard_list": {
2328
"optional": true
2429
},
30+
"workboard_create": {
31+
"optional": true
32+
},
33+
"workboard_link": {
34+
"optional": true
35+
},
2536
"workboard_read": {
2637
"optional": true
2738
},
@@ -31,6 +42,15 @@
3142
"workboard_heartbeat": {
3243
"optional": true
3344
},
45+
"workboard_complete": {
46+
"optional": true
47+
},
48+
"workboard_block": {
49+
"optional": true
50+
},
51+
"workboard_dispatch": {
52+
"optional": true
53+
},
3454
"workboard_release": {
3555
"optional": true
3656
},

extensions/workboard/src/gateway.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,19 @@ describe("workboard gateway methods", () => {
5151
"workboard.cards.delete",
5252
"workboard.cards.comment",
5353
"workboard.cards.link",
54+
"workboard.cards.linkDependency",
5455
"workboard.cards.proof",
5556
"workboard.cards.artifact",
5657
"workboard.cards.claim",
5758
"workboard.cards.heartbeat",
5859
"workboard.cards.release",
60+
"workboard.cards.complete",
61+
"workboard.cards.block",
5962
"workboard.cards.unblock",
6063
"workboard.cards.bulk",
6164
"workboard.cards.diagnostics",
6265
"workboard.cards.diagnostics.refresh",
66+
"workboard.cards.dispatch",
6367
"workboard.cards.archive",
6468
"workboard.cards.export",
6569
]);
@@ -221,5 +225,40 @@ describe("workboard gateway methods", () => {
221225
expect(bulkRespond.mock.calls[0]?.[1]).toMatchObject({
222226
cards: [expect.objectContaining({ priority: "urgent" })],
223227
});
228+
229+
const completeRespond = vi.fn();
230+
await methods.get("workboard.cards.complete")?.handler({
231+
params: { id: cardId, summary: "Operator closed it." },
232+
respond: completeRespond,
233+
} as never);
234+
expect(completeRespond.mock.calls[0]?.[1]).toMatchObject({
235+
card: {
236+
status: "done",
237+
metadata: {
238+
comments: expect.arrayContaining([
239+
expect.objectContaining({ body: "Operator closed it." }),
240+
]),
241+
},
242+
},
243+
});
244+
245+
const blockedCreateRespond = vi.fn();
246+
await methods.get("workboard.cards.create")?.handler({
247+
params: { title: "Block me" },
248+
respond: blockedCreateRespond,
249+
} as never);
250+
const blockedCardId = blockedCreateRespond.mock.calls[0]?.[1]?.card.id;
251+
await methods.get("workboard.cards.claim")?.handler({
252+
params: { id: blockedCardId, ownerId: "main" },
253+
respond: vi.fn(),
254+
} as never);
255+
const blockRespond = vi.fn();
256+
await methods.get("workboard.cards.block")?.handler({
257+
params: { id: blockedCardId, reason: "Operator blocked it." },
258+
respond: blockRespond,
259+
} as never);
260+
expect(blockRespond.mock.calls[0]?.[1]).toMatchObject({
261+
card: { status: "blocked" },
262+
});
224263
});
225264
});

extensions/workboard/src/gateway.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,25 @@ export function registerWorkboardGatewayMethods(params: {
168168
{ scope: WRITE_SCOPE },
169169
);
170170

171+
api.registerGatewayMethod(
172+
"workboard.cards.linkDependency",
173+
async ({ params: requestParams, respond }) => {
174+
try {
175+
const parentId = requestParams.parentId;
176+
const childId = requestParams.childId;
177+
if (typeof parentId !== "string" || typeof childId !== "string") {
178+
throw new Error("parentId and childId are required.");
179+
}
180+
respond(true, {
181+
card: redactClaimToken(await store.linkCards(parentId, childId)),
182+
});
183+
} catch (error) {
184+
respondError(respond, error);
185+
}
186+
},
187+
{ scope: WRITE_SCOPE },
188+
);
189+
171190
api.registerGatewayMethod(
172191
"workboard.cards.proof",
173192
async ({ params: requestParams, respond }) => {
@@ -237,6 +256,34 @@ export function registerWorkboardGatewayMethods(params: {
237256
{ scope: WRITE_SCOPE },
238257
);
239258

259+
api.registerGatewayMethod(
260+
"workboard.cards.complete",
261+
async ({ params: requestParams, respond }) => {
262+
try {
263+
respond(true, {
264+
card: redactClaimToken(await store.complete(readId(requestParams), requestParams, null)),
265+
});
266+
} catch (error) {
267+
respondError(respond, error);
268+
}
269+
},
270+
{ scope: WRITE_SCOPE },
271+
);
272+
273+
api.registerGatewayMethod(
274+
"workboard.cards.block",
275+
async ({ params: requestParams, respond }) => {
276+
try {
277+
respond(true, {
278+
card: redactClaimToken(await store.block(readId(requestParams), requestParams, null)),
279+
});
280+
} catch (error) {
281+
respondError(respond, error);
282+
}
283+
},
284+
{ scope: WRITE_SCOPE },
285+
);
286+
240287
api.registerGatewayMethod(
241288
"workboard.cards.unblock",
242289
async ({ params: requestParams, respond }) => {
@@ -288,6 +335,24 @@ export function registerWorkboardGatewayMethods(params: {
288335
{ scope: WRITE_SCOPE },
289336
);
290337

338+
api.registerGatewayMethod(
339+
"workboard.cards.dispatch",
340+
async ({ respond }) => {
341+
try {
342+
const result = await store.dispatch();
343+
respond(true, {
344+
...result,
345+
promoted: result.promoted.map(redactClaimToken),
346+
reclaimed: result.reclaimed.map(redactClaimToken),
347+
blocked: result.blocked.map(redactClaimToken),
348+
});
349+
} catch (error) {
350+
respondError(respond, error);
351+
}
352+
},
353+
{ scope: WRITE_SCOPE },
354+
);
355+
291356
api.registerGatewayMethod(
292357
"workboard.cards.archive",
293358
async ({ params: requestParams, respond }) => {

0 commit comments

Comments
 (0)