@compilr-dev/sdk 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Work Item Tools — CRUD + workflow operations for work items.
3
+ *
4
+ * 9 tools: workitem_query, workitem_add, workitem_update, workitem_next,
5
+ * workitem_delete, workitem_status_counts, workitem_advance_step,
6
+ * workitem_claim, workitem_handoff
7
+ *
8
+ * Ported from CLI's src/tools/workitem-db.ts. Key adaptations:
9
+ * - getActiveProject()?.id → ctx.currentProjectId
10
+ * - getActiveSharedContext() "self" resolution → hooks?.resolveOwner?.('self')
11
+ * - awardWorkItemCompletion() → hooks?.onWorkItemCompleted?.(item)
12
+ * - workitem_advance_step imports from ../workflow.js (SDK's module)
13
+ */
14
+ import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
15
+ import { getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, STEP_ORDER, } from '../workflow.js';
16
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
17
+ export function createWorkItemTools(config) {
18
+ const { context: ctx, hooks } = config;
19
+ // ---------------------------------------------------------------------------
20
+ // workitem_query
21
+ // ---------------------------------------------------------------------------
22
+ const workitemQueryTool = defineTool({
23
+ name: 'workitem_query',
24
+ description: 'Query PERSISTENT work items from the project backlog (stored in database). ' +
25
+ 'For session todos shown in footer, use todo_read. ' +
26
+ 'Supports filtering by status, type, priority, owner, and search.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ project_id: {
31
+ type: 'number',
32
+ description: 'Project ID (uses active project if not provided)',
33
+ },
34
+ status: {
35
+ type: 'string',
36
+ enum: ['backlog', 'in_progress', 'completed', 'skipped', 'all'],
37
+ description: 'Filter by status (default: all)',
38
+ },
39
+ type: {
40
+ type: 'string',
41
+ enum: ['feature', 'bug', 'tech-debt', 'chore', 'all'],
42
+ description: 'Filter by type (default: all)',
43
+ },
44
+ priority: {
45
+ type: 'string',
46
+ enum: ['critical', 'high', 'medium', 'low', 'all'],
47
+ description: 'Filter by priority (default: all)',
48
+ },
49
+ owner: {
50
+ type: 'string',
51
+ description: 'Filter by owner: agent ID, "self" for your own items, "unassigned" for unowned, or "all" (default: all)',
52
+ },
53
+ search: {
54
+ type: 'string',
55
+ description: 'Search in title and description',
56
+ },
57
+ limit: {
58
+ type: 'number',
59
+ description: 'Maximum items to return (default: 50)',
60
+ },
61
+ offset: {
62
+ type: 'number',
63
+ description: 'Offset for pagination (default: 0)',
64
+ },
65
+ },
66
+ required: [],
67
+ },
68
+ execute: async (input) => {
69
+ try {
70
+ const projectId = input.project_id ?? ctx.currentProjectId;
71
+ if (!projectId) {
72
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
73
+ }
74
+ // Resolve "self" to the active agent ID via hook
75
+ let resolvedOwner = input.owner;
76
+ if (input.owner === 'self') {
77
+ resolvedOwner = hooks?.resolveOwner?.('self') ?? undefined;
78
+ }
79
+ const queryInput = {
80
+ project_id: projectId,
81
+ status: input.status,
82
+ type: input.type,
83
+ priority: input.priority,
84
+ owner: resolvedOwner,
85
+ search: input.search,
86
+ limit: input.limit ?? 50,
87
+ offset: input.offset ?? 0,
88
+ };
89
+ const result = await ctx.workItems.query(queryInput);
90
+ const items = result.items.map((item) => ({
91
+ id: item.id,
92
+ itemId: item.itemId,
93
+ type: item.type,
94
+ status: item.status,
95
+ priority: item.priority,
96
+ owner: item.owner,
97
+ guidedStep: item.guidedStep,
98
+ title: item.title,
99
+ description: item.description,
100
+ estimatedEffort: item.estimatedEffort,
101
+ completedAt: item.completedAt?.toISOString(),
102
+ commitHash: item.commitHash,
103
+ createdAt: item.createdAt.toISOString(),
104
+ }));
105
+ return createSuccessResult({
106
+ success: true,
107
+ items,
108
+ total: result.total,
109
+ hasMore: result.hasMore,
110
+ projectId,
111
+ });
112
+ }
113
+ catch (error) {
114
+ return createErrorResult(`Failed to query work items: ${error instanceof Error ? error.message : String(error)}`);
115
+ }
116
+ },
117
+ });
118
+ // ---------------------------------------------------------------------------
119
+ // workitem_add
120
+ // ---------------------------------------------------------------------------
121
+ const workitemAddTool = defineTool({
122
+ name: 'workitem_add',
123
+ description: 'Add a PERSISTENT work item to the project backlog (stored in database). ' +
124
+ 'For session-level tasks shown in footer, use todo_write.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ project_id: {
129
+ type: 'number',
130
+ description: 'Project ID (uses active project if not provided)',
131
+ },
132
+ type: {
133
+ type: 'string',
134
+ enum: ['feature', 'bug', 'tech-debt', 'chore'],
135
+ description: 'Work item type',
136
+ },
137
+ title: {
138
+ type: 'string',
139
+ description: 'Title of the work item',
140
+ },
141
+ description: {
142
+ type: 'string',
143
+ description: 'Detailed description',
144
+ },
145
+ priority: {
146
+ type: 'string',
147
+ enum: ['critical', 'high', 'medium', 'low'],
148
+ description: 'Priority (default: medium)',
149
+ },
150
+ estimated_effort: {
151
+ type: 'string',
152
+ enum: ['low', 'medium', 'high'],
153
+ description: 'Estimated effort',
154
+ },
155
+ owner: {
156
+ type: 'string',
157
+ description: 'Owner agent ID. Omit for unassigned.',
158
+ },
159
+ },
160
+ required: ['type', 'title'],
161
+ },
162
+ execute: async (input) => {
163
+ try {
164
+ const projectId = input.project_id ?? ctx.currentProjectId;
165
+ if (!projectId) {
166
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
167
+ }
168
+ const createInput = {
169
+ project_id: projectId,
170
+ type: input.type,
171
+ title: input.title,
172
+ description: input.description,
173
+ priority: input.priority,
174
+ estimated_effort: input.estimated_effort,
175
+ owner: input.owner,
176
+ };
177
+ const item = await ctx.workItems.create(createInput);
178
+ return createSuccessResult({
179
+ success: true,
180
+ message: `Work item ${item.itemId} created: "${item.title}"${item.owner ? ` (owner: ${item.owner})` : ''}`,
181
+ item: {
182
+ id: item.id,
183
+ itemId: item.itemId,
184
+ type: item.type,
185
+ status: item.status,
186
+ priority: item.priority,
187
+ owner: item.owner,
188
+ title: item.title,
189
+ },
190
+ });
191
+ }
192
+ catch (error) {
193
+ return createErrorResult(`Failed to add work item: ${error instanceof Error ? error.message : String(error)}`);
194
+ }
195
+ },
196
+ });
197
+ // ---------------------------------------------------------------------------
198
+ // workitem_update
199
+ // ---------------------------------------------------------------------------
200
+ const workitemUpdateTool = defineTool({
201
+ name: 'workitem_update',
202
+ description: 'Update a PERSISTENT work item in the project backlog (stored in database). ' +
203
+ 'Can update status, priority, owner, title, description, and more.',
204
+ inputSchema: {
205
+ type: 'object',
206
+ properties: {
207
+ item_id: {
208
+ type: 'string',
209
+ description: 'Work item ID (e.g., "REQ-001", "BUG-002")',
210
+ },
211
+ project_id: {
212
+ type: 'number',
213
+ description: 'Project ID (uses active project if not provided)',
214
+ },
215
+ status: {
216
+ type: 'string',
217
+ enum: ['backlog', 'in_progress', 'completed', 'skipped'],
218
+ description: 'Work item status',
219
+ },
220
+ priority: {
221
+ type: 'string',
222
+ enum: ['critical', 'high', 'medium', 'low'],
223
+ description: 'Priority',
224
+ },
225
+ owner: {
226
+ type: 'string',
227
+ description: 'Owner agent ID. Set to empty string to unassign.',
228
+ },
229
+ guided_step: {
230
+ type: 'string',
231
+ enum: ['plan', 'implement', 'test', 'commit', 'review'],
232
+ description: 'Current step in guided mode workflow',
233
+ },
234
+ title: {
235
+ type: 'string',
236
+ description: 'Updated title',
237
+ },
238
+ description: {
239
+ type: 'string',
240
+ description: 'Updated description',
241
+ },
242
+ commit_hash: {
243
+ type: 'string',
244
+ description: 'Git commit hash when completing the item',
245
+ },
246
+ },
247
+ required: ['item_id'],
248
+ },
249
+ execute: async (input) => {
250
+ try {
251
+ const projectId = input.project_id ?? ctx.currentProjectId;
252
+ if (!projectId) {
253
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
254
+ }
255
+ const existingItem = await ctx.workItems.getByItemId(projectId, input.item_id);
256
+ if (!existingItem) {
257
+ return createErrorResult(`Work item "${input.item_id}" not found in current project`);
258
+ }
259
+ const updateInput = {};
260
+ if (input.status)
261
+ updateInput.status = input.status;
262
+ if (input.priority)
263
+ updateInput.priority = input.priority;
264
+ if (input.owner !== undefined) {
265
+ updateInput.owner = input.owner === '' ? null : input.owner;
266
+ }
267
+ if (input.guided_step !== undefined)
268
+ updateInput.guided_step = input.guided_step;
269
+ if (input.title)
270
+ updateInput.title = input.title;
271
+ if (input.description !== undefined)
272
+ updateInput.description = input.description;
273
+ if (input.commit_hash)
274
+ updateInput.commit_hash = input.commit_hash;
275
+ const item = await ctx.workItems.update(existingItem.id, updateInput);
276
+ if (!item) {
277
+ return createErrorResult(`Failed to update work item "${input.item_id}"`);
278
+ }
279
+ // Fire hook if work item was just completed
280
+ if (input.status === 'completed' && existingItem.status !== 'completed') {
281
+ hooks?.onWorkItemCompleted?.({
282
+ id: item.id,
283
+ itemId: item.itemId,
284
+ title: item.title,
285
+ });
286
+ }
287
+ return createSuccessResult({
288
+ success: true,
289
+ message: `Work item ${item.itemId} updated`,
290
+ item: {
291
+ id: item.id,
292
+ itemId: item.itemId,
293
+ type: item.type,
294
+ status: item.status,
295
+ priority: item.priority,
296
+ owner: item.owner,
297
+ guidedStep: item.guidedStep,
298
+ title: item.title,
299
+ completedAt: item.completedAt?.toISOString(),
300
+ commitHash: item.commitHash,
301
+ },
302
+ });
303
+ }
304
+ catch (error) {
305
+ return createErrorResult(`Failed to update work item: ${error instanceof Error ? error.message : String(error)}`);
306
+ }
307
+ },
308
+ });
309
+ // ---------------------------------------------------------------------------
310
+ // workitem_next
311
+ // ---------------------------------------------------------------------------
312
+ const workitemNextTool = defineTool({
313
+ name: 'workitem_next',
314
+ description: 'Get the next work item to work on (highest priority backlog item). Useful for guided mode to pick the next task.',
315
+ inputSchema: {
316
+ type: 'object',
317
+ properties: {
318
+ project_id: {
319
+ type: 'number',
320
+ description: 'Project ID (uses active project if not provided)',
321
+ },
322
+ type: {
323
+ type: 'string',
324
+ enum: ['feature', 'bug', 'tech-debt', 'chore'],
325
+ description: 'Filter by type (optional)',
326
+ },
327
+ },
328
+ required: [],
329
+ },
330
+ execute: async (input) => {
331
+ try {
332
+ const projectId = input.project_id ?? ctx.currentProjectId;
333
+ if (!projectId) {
334
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
335
+ }
336
+ const item = await ctx.workItems.getNext(projectId, input.type);
337
+ if (!item) {
338
+ return createSuccessResult({
339
+ success: true,
340
+ item: null,
341
+ message: 'No backlog items found. The backlog is empty or all items are completed.',
342
+ });
343
+ }
344
+ return createSuccessResult({
345
+ success: true,
346
+ item: {
347
+ id: item.id,
348
+ itemId: item.itemId,
349
+ type: item.type,
350
+ status: item.status,
351
+ priority: item.priority,
352
+ title: item.title,
353
+ description: item.description,
354
+ estimatedEffort: item.estimatedEffort,
355
+ },
356
+ message: `Next item: ${item.itemId} "${item.title}" (${item.priority} priority)`,
357
+ });
358
+ }
359
+ catch (error) {
360
+ return createErrorResult(`Failed to get next work item: ${error instanceof Error ? error.message : String(error)}`);
361
+ }
362
+ },
363
+ });
364
+ // ---------------------------------------------------------------------------
365
+ // workitem_delete
366
+ // ---------------------------------------------------------------------------
367
+ const workitemDeleteTool = defineTool({
368
+ name: 'workitem_delete',
369
+ description: 'Delete a work item from the backlog.',
370
+ inputSchema: {
371
+ type: 'object',
372
+ properties: {
373
+ item_id: {
374
+ type: 'string',
375
+ description: 'Work item ID (e.g., "REQ-001", "BUG-002")',
376
+ },
377
+ project_id: {
378
+ type: 'number',
379
+ description: 'Project ID (uses active project if not provided)',
380
+ },
381
+ },
382
+ required: ['item_id'],
383
+ },
384
+ execute: async (input) => {
385
+ try {
386
+ const projectId = input.project_id ?? ctx.currentProjectId;
387
+ if (!projectId) {
388
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
389
+ }
390
+ const existingItem = await ctx.workItems.getByItemId(projectId, input.item_id);
391
+ if (!existingItem) {
392
+ return createErrorResult(`Work item "${input.item_id}" not found in current project`);
393
+ }
394
+ const deleted = await ctx.workItems.delete(existingItem.id);
395
+ if (!deleted) {
396
+ return createErrorResult(`Failed to delete work item "${input.item_id}"`);
397
+ }
398
+ return createSuccessResult({
399
+ success: true,
400
+ message: `Work item ${input.item_id} deleted`,
401
+ });
402
+ }
403
+ catch (error) {
404
+ return createErrorResult(`Failed to delete work item: ${error instanceof Error ? error.message : String(error)}`);
405
+ }
406
+ },
407
+ });
408
+ // ---------------------------------------------------------------------------
409
+ // workitem_status_counts
410
+ // ---------------------------------------------------------------------------
411
+ const workitemStatusCountsTool = defineTool({
412
+ name: 'workitem_status_counts',
413
+ description: 'Get counts of work items by status for the current project. Useful for showing progress.',
414
+ inputSchema: {
415
+ type: 'object',
416
+ properties: {
417
+ project_id: {
418
+ type: 'number',
419
+ description: 'Project ID (uses active project if not provided)',
420
+ },
421
+ },
422
+ required: [],
423
+ },
424
+ execute: async (input) => {
425
+ try {
426
+ const projectId = input.project_id ?? ctx.currentProjectId;
427
+ if (!projectId) {
428
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
429
+ }
430
+ const counts = await ctx.workItems.getStatusCounts(projectId);
431
+ const total = counts.backlog + counts.in_progress + counts.completed + counts.skipped;
432
+ const progress = total > 0 ? Math.round((counts.completed / total) * 100) : 0;
433
+ return createSuccessResult({
434
+ success: true,
435
+ counts,
436
+ total,
437
+ progress,
438
+ summary: `${String(counts.completed)}/${String(total)} completed (${String(progress)}%)`,
439
+ });
440
+ }
441
+ catch (error) {
442
+ return createErrorResult(`Failed to get status counts: ${error instanceof Error ? error.message : String(error)}`);
443
+ }
444
+ },
445
+ });
446
+ // ---------------------------------------------------------------------------
447
+ // workitem_advance_step
448
+ // ---------------------------------------------------------------------------
449
+ const workitemAdvanceStepTool = defineTool({
450
+ name: 'workitem_advance_step',
451
+ description: `Advance a work item to the next guided workflow step.\n` +
452
+ `Steps progress in order: ${STEP_ORDER.join(' → ')} → complete.\n` +
453
+ `Use this tool when the criteria for the current step are met.\n` +
454
+ `The agent should explicitly call this to advance - it provides visibility and audit trail.`,
455
+ inputSchema: {
456
+ type: 'object',
457
+ properties: {
458
+ item_id: {
459
+ type: 'string',
460
+ description: 'Work item ID (e.g., "REQ-001", "BUG-002")',
461
+ },
462
+ reason: {
463
+ type: 'string',
464
+ description: 'Why the step criteria was met (shown to user for visibility)',
465
+ },
466
+ force: {
467
+ type: 'boolean',
468
+ description: 'Skip validation and force advance (e.g., user override after test failures)',
469
+ },
470
+ project_id: {
471
+ type: 'number',
472
+ description: 'Project ID (uses active project if not provided)',
473
+ },
474
+ },
475
+ required: ['item_id', 'reason'],
476
+ },
477
+ execute: async (input) => {
478
+ try {
479
+ const projectId = input.project_id ?? ctx.currentProjectId;
480
+ if (!projectId) {
481
+ return createErrorResult('No project specified and no active project. Use project_get or /projects to select a project first.');
482
+ }
483
+ const item = await ctx.workItems.getByItemId(projectId, input.item_id);
484
+ if (!item) {
485
+ return createErrorResult(`Work item "${input.item_id}" not found in current project`);
486
+ }
487
+ if (item.status !== 'in_progress') {
488
+ return createErrorResult(`Work item "${input.item_id}" is not in progress (status: ${item.status}). ` +
489
+ 'Use workitem_update to set status to in_progress first.');
490
+ }
491
+ const currentStep = item.guidedStep;
492
+ const nextStep = getNextStep(currentStep);
493
+ // Check for valid transition (unless forced)
494
+ if (!input.force && currentStep !== null) {
495
+ if (!isValidTransition(currentStep, nextStep)) {
496
+ return createErrorResult(`Cannot advance from ${currentStep} to ${nextStep}. ` +
497
+ `Valid next step: ${getNextStep(currentStep)}. ` +
498
+ 'Use force=true to override.');
499
+ }
500
+ }
501
+ // Handle completion
502
+ if (nextStep === 'complete') {
503
+ const updatedItem = await ctx.workItems.update(item.id, {
504
+ status: 'completed',
505
+ guided_step: null,
506
+ });
507
+ if (!updatedItem) {
508
+ return createErrorResult('Failed to update work item');
509
+ }
510
+ hooks?.onWorkItemCompleted?.({
511
+ id: updatedItem.id,
512
+ itemId: updatedItem.itemId,
513
+ title: updatedItem.title,
514
+ });
515
+ return createSuccessResult({
516
+ success: true,
517
+ item: {
518
+ id: updatedItem.id,
519
+ itemId: updatedItem.itemId,
520
+ type: updatedItem.type,
521
+ status: updatedItem.status,
522
+ priority: updatedItem.priority,
523
+ guidedStep: updatedItem.guidedStep,
524
+ title: updatedItem.title,
525
+ completedAt: updatedItem.completedAt?.toISOString(),
526
+ commitHash: updatedItem.commitHash,
527
+ },
528
+ previous_step: currentStep,
529
+ current_step: null,
530
+ message: currentStep
531
+ ? `✓ ${input.item_id}: ${formatStepDisplay(currentStep)} → COMPLETED`
532
+ : `✓ ${input.item_id}: COMPLETED`,
533
+ completed: true,
534
+ reason: input.reason,
535
+ next_action: 'Work item complete! Use workitem_next to pick the next item from backlog.',
536
+ });
537
+ }
538
+ // Advance to next step
539
+ const nextGuidedStep = nextStep;
540
+ const updatedItem = await ctx.workItems.update(item.id, {
541
+ guided_step: nextGuidedStep,
542
+ });
543
+ if (!updatedItem) {
544
+ return createErrorResult('Failed to update work item');
545
+ }
546
+ const nextCriteria = getStepCriteria(nextGuidedStep);
547
+ return createSuccessResult({
548
+ success: true,
549
+ item: {
550
+ id: updatedItem.id,
551
+ itemId: updatedItem.itemId,
552
+ type: updatedItem.type,
553
+ status: updatedItem.status,
554
+ priority: updatedItem.priority,
555
+ guidedStep: updatedItem.guidedStep,
556
+ title: updatedItem.title,
557
+ },
558
+ previous_step: currentStep,
559
+ current_step: nextGuidedStep,
560
+ message: currentStep
561
+ ? `✓ ${input.item_id}: ${formatStepDisplay(currentStep)} → ${formatStepDisplay(nextGuidedStep)}`
562
+ : `✓ ${input.item_id}: Starting ${formatStepDisplay(nextGuidedStep)}`,
563
+ completed: false,
564
+ reason: input.reason,
565
+ next_action: nextCriteria.nextActionHint,
566
+ exit_criteria: nextCriteria.exitCriteria,
567
+ });
568
+ }
569
+ catch (error) {
570
+ return createErrorResult(`Failed to advance step: ${error instanceof Error ? error.message : String(error)}`);
571
+ }
572
+ },
573
+ });
574
+ // ---------------------------------------------------------------------------
575
+ // workitem_claim
576
+ // ---------------------------------------------------------------------------
577
+ const workitemClaimTool = defineTool({
578
+ name: 'workitem_claim',
579
+ description: 'Claim an unassigned PERSISTENT work item from project backlog. ' +
580
+ 'For session todos in footer, use todo_claim.',
581
+ inputSchema: {
582
+ type: 'object',
583
+ properties: {
584
+ item_id: {
585
+ type: 'string',
586
+ description: 'Work item ID (e.g., "REQ-001", "BUG-002")',
587
+ },
588
+ agent_id: {
589
+ type: 'string',
590
+ description: 'The agent ID claiming the work item',
591
+ },
592
+ project_id: {
593
+ type: 'number',
594
+ description: 'Project ID (uses active project if not provided)',
595
+ },
596
+ },
597
+ required: ['item_id', 'agent_id'],
598
+ },
599
+ execute: async (input) => {
600
+ try {
601
+ const projectId = input.project_id ?? ctx.currentProjectId;
602
+ if (!projectId) {
603
+ return createErrorResult('No project specified and no active project.');
604
+ }
605
+ const existingItem = await ctx.workItems.getByItemId(projectId, input.item_id);
606
+ if (!existingItem) {
607
+ return createErrorResult(`Work item "${input.item_id}" not found in current project`);
608
+ }
609
+ if (existingItem.owner) {
610
+ return createErrorResult(`Work item "${input.item_id}" is already owned by "${existingItem.owner}". Use workitem_handoff to transfer ownership.`);
611
+ }
612
+ const item = await ctx.workItems.update(existingItem.id, {
613
+ owner: input.agent_id,
614
+ });
615
+ if (!item) {
616
+ return createErrorResult(`Failed to claim work item "${input.item_id}"`);
617
+ }
618
+ return createSuccessResult({
619
+ success: true,
620
+ message: `Work item ${item.itemId} claimed by ${input.agent_id}`,
621
+ item: {
622
+ id: item.id,
623
+ itemId: item.itemId,
624
+ type: item.type,
625
+ status: item.status,
626
+ priority: item.priority,
627
+ owner: item.owner,
628
+ title: item.title,
629
+ },
630
+ });
631
+ }
632
+ catch (error) {
633
+ return createErrorResult(`Failed to claim work item: ${error instanceof Error ? error.message : String(error)}`);
634
+ }
635
+ },
636
+ });
637
+ // ---------------------------------------------------------------------------
638
+ // workitem_handoff
639
+ // ---------------------------------------------------------------------------
640
+ const workitemHandoffTool = defineTool({
641
+ name: 'workitem_handoff',
642
+ description: 'Hand off a PERSISTENT work item (from project backlog) to another agent. ' +
643
+ 'For session todos in footer, use todo_handoff.',
644
+ inputSchema: {
645
+ type: 'object',
646
+ properties: {
647
+ item_id: {
648
+ type: 'string',
649
+ description: 'Work item ID (e.g., "REQ-001", "BUG-002")',
650
+ },
651
+ to_agent_id: {
652
+ type: 'string',
653
+ description: 'The agent ID to hand off to',
654
+ },
655
+ notes: {
656
+ type: 'string',
657
+ description: 'Optional notes about the handoff (context, status, blockers)',
658
+ },
659
+ project_id: {
660
+ type: 'number',
661
+ description: 'Project ID (uses active project if not provided)',
662
+ },
663
+ },
664
+ required: ['item_id', 'to_agent_id'],
665
+ },
666
+ execute: async (input) => {
667
+ try {
668
+ const projectId = input.project_id ?? ctx.currentProjectId;
669
+ if (!projectId) {
670
+ return createErrorResult('No project specified and no active project.');
671
+ }
672
+ const existingItem = await ctx.workItems.getByItemId(projectId, input.item_id);
673
+ if (!existingItem) {
674
+ return createErrorResult(`Work item "${input.item_id}" not found in current project`);
675
+ }
676
+ const previousOwner = existingItem.owner;
677
+ const item = await ctx.workItems.update(existingItem.id, {
678
+ owner: input.to_agent_id,
679
+ });
680
+ if (!item) {
681
+ return createErrorResult(`Failed to hand off work item "${input.item_id}"`);
682
+ }
683
+ return createSuccessResult({
684
+ success: true,
685
+ message: `Work item ${item.itemId} handed off from "${previousOwner ?? 'unassigned'}" to "${input.to_agent_id}"`,
686
+ item: {
687
+ id: item.id,
688
+ itemId: item.itemId,
689
+ type: item.type,
690
+ status: item.status,
691
+ priority: item.priority,
692
+ owner: item.owner,
693
+ title: item.title,
694
+ },
695
+ previousOwner,
696
+ notes: input.notes,
697
+ });
698
+ }
699
+ catch (error) {
700
+ return createErrorResult(`Failed to hand off work item: ${error instanceof Error ? error.message : String(error)}`);
701
+ }
702
+ },
703
+ });
704
+ return [
705
+ workitemQueryTool,
706
+ workitemAddTool,
707
+ workitemUpdateTool,
708
+ workitemNextTool,
709
+ workitemDeleteTool,
710
+ workitemStatusCountsTool,
711
+ workitemAdvanceStepTool,
712
+ workitemClaimTool,
713
+ workitemHandoffTool,
714
+ ];
715
+ }