internaltool-mcp 1.4.0 → 1.5.1
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.
- package/index.js +619 -54
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -264,14 +264,30 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
264
264
|
|
|
265
265
|
server.tool(
|
|
266
266
|
'move_task',
|
|
267
|
-
'Move a task to a different column. Requires approved README to move from planning to execution columns.',
|
|
267
|
+
'Move a task to a different column. Requires approved README to move from planning to execution columns. Set confirmed=false first to preview.',
|
|
268
268
|
{
|
|
269
|
-
taskId:
|
|
270
|
-
column:
|
|
271
|
-
toIndex:
|
|
269
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
270
|
+
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']),
|
|
271
|
+
toIndex: z.number().int().min(0).default(0).describe('Position in column (0 = top)'),
|
|
272
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
272
273
|
},
|
|
273
|
-
async ({ taskId, column, toIndex }) =>
|
|
274
|
-
|
|
274
|
+
async ({ taskId, column, toIndex, confirmed = false }) => {
|
|
275
|
+
if (!confirmed) {
|
|
276
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
277
|
+
const task = taskRes?.data?.task
|
|
278
|
+
return text({
|
|
279
|
+
preview: {
|
|
280
|
+
action: 'move_task',
|
|
281
|
+
task: task ? { key: task.key, title: task.title, currentColumn: task.column } : { taskId },
|
|
282
|
+
moveTo: column,
|
|
283
|
+
atPosition: toIndex,
|
|
284
|
+
},
|
|
285
|
+
requiresConfirmation: true,
|
|
286
|
+
message: `This will move the task to "${column}". Call move_task again with confirmed=true to proceed.`,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
return call(() => api.post(`/api/tasks/${taskId}/move`, { column, toIndex }))
|
|
290
|
+
}
|
|
275
291
|
)
|
|
276
292
|
|
|
277
293
|
server.tool(
|
|
@@ -286,22 +302,56 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
286
302
|
`Pause a task and save your current work context.
|
|
287
303
|
IMPORTANT: Before calling this, run "git diff HEAD" and "git status" in the terminal to see what has changed.
|
|
288
304
|
Use that output to write precise summary/remaining/blockers fields — not generic text.
|
|
289
|
-
This is the developer's saved mental state; make it detailed enough to resume cold tomorrow
|
|
305
|
+
This is the developer's saved mental state; make it detailed enough to resume cold tomorrow.
|
|
306
|
+
|
|
307
|
+
Set confirmed=false first to preview, then confirmed=true to actually save.`,
|
|
290
308
|
{
|
|
291
309
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
292
310
|
summary: z.string().optional().describe('What was built/changed — include file names and what was done'),
|
|
293
311
|
remaining: z.string().optional().describe('Specific next steps to complete this task'),
|
|
294
312
|
blockers: z.string().optional().describe('Anything blocking progress'),
|
|
313
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
295
314
|
},
|
|
296
|
-
async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
|
|
297
|
-
|
|
315
|
+
async ({ taskId, summary = '', remaining = '', blockers = '', confirmed = false }) => {
|
|
316
|
+
if (!confirmed) {
|
|
317
|
+
return text({
|
|
318
|
+
preview: {
|
|
319
|
+
action: 'park_task',
|
|
320
|
+
taskId,
|
|
321
|
+
willSave: { summary: summary || '(empty)', remaining: remaining || '(empty)', blockers: blockers || '(empty)' },
|
|
322
|
+
},
|
|
323
|
+
requiresConfirmation: true,
|
|
324
|
+
message: 'Review the park note above. Call park_task again with confirmed=true to save it.',
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
return call(() => api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers }))
|
|
328
|
+
}
|
|
298
329
|
)
|
|
299
330
|
|
|
300
331
|
server.tool(
|
|
301
332
|
'unpark_task',
|
|
302
|
-
'Resume a parked task.',
|
|
303
|
-
{
|
|
304
|
-
|
|
333
|
+
'Resume a parked task. Set confirmed=false first to preview, then confirmed=true to execute.',
|
|
334
|
+
{
|
|
335
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
336
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
337
|
+
},
|
|
338
|
+
async ({ taskId, confirmed = false }) => {
|
|
339
|
+
if (!confirmed) {
|
|
340
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
341
|
+
const task = taskRes?.data?.task
|
|
342
|
+
return text({
|
|
343
|
+
preview: {
|
|
344
|
+
action: 'unpark_task',
|
|
345
|
+
taskId,
|
|
346
|
+
task: task ? { key: task.key, title: task.title, parkNote: task.parkNote } : null,
|
|
347
|
+
willDo: 'Clear park note and mark task as active again',
|
|
348
|
+
},
|
|
349
|
+
requiresConfirmation: true,
|
|
350
|
+
message: 'Call unpark_task again with confirmed=true to resume this task.',
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
return call(() => api.patch(`/api/tasks/${taskId}/unpark`, {}))
|
|
354
|
+
}
|
|
305
355
|
)
|
|
306
356
|
}
|
|
307
357
|
|
|
@@ -398,62 +448,100 @@ Use this when the developer says "wrap up", "end of day", or "I'm done for today
|
|
|
398
448
|
|
|
399
449
|
server.tool(
|
|
400
450
|
'kickoff_task',
|
|
401
|
-
`Kick off a task — brief the developer
|
|
402
|
-
|
|
403
|
-
|
|
451
|
+
`Kick off a task — read the implementation plan (README) and brief the developer, then move the task to in_progress.
|
|
452
|
+
|
|
453
|
+
The README/implementation plan IS the brief. Always read it fully and present it to the developer
|
|
454
|
+
before they write a single line of code. If no README exists, warn the developer that the plan is
|
|
455
|
+
missing and suggest they write one before starting.
|
|
456
|
+
|
|
457
|
+
Workflow:
|
|
458
|
+
1. confirmed=false → show the full README brief + subtask checklist so developer can read the plan
|
|
459
|
+
2. confirmed=true → move task to in_progress, return branch name and recent repo commits
|
|
460
|
+
|
|
404
461
|
Use this when a developer says "start task", "brief me on", or "what do I need to do for TASK-X".`,
|
|
405
462
|
{
|
|
406
|
-
taskId:
|
|
463
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
464
|
+
confirmed: z.boolean().optional().default(false).describe('Set true after reading the plan to move the task to in_progress'),
|
|
407
465
|
},
|
|
408
|
-
async ({ taskId }) => {
|
|
409
|
-
// Get full task
|
|
466
|
+
async ({ taskId, confirmed = false }) => {
|
|
410
467
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
411
468
|
if (!taskRes?.success) return errorText('Task not found')
|
|
412
469
|
const task = taskRes.data.task
|
|
413
470
|
|
|
414
|
-
|
|
471
|
+
const suggestedBranch = `feature/${task.key?.toLowerCase()}-${task.title
|
|
472
|
+
.toLowerCase()
|
|
473
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
474
|
+
.slice(0, 40)}`
|
|
475
|
+
|
|
476
|
+
const hasReadme = !!(task.readmeMarkdown && task.readmeMarkdown.trim().length > 0)
|
|
477
|
+
const subtasks = (task.subtasks || []).map(s => ({
|
|
478
|
+
title: s.title,
|
|
479
|
+
done: s.done,
|
|
480
|
+
status: s.done ? '✅' : '⬜',
|
|
481
|
+
}))
|
|
482
|
+
const subtasksDone = subtasks.filter(s => s.done).length
|
|
483
|
+
const subtasksTotal = subtasks.length
|
|
484
|
+
|
|
485
|
+
// ── Preview: show the full plan before touching anything ──
|
|
486
|
+
if (!confirmed) {
|
|
487
|
+
return text({
|
|
488
|
+
brief: {
|
|
489
|
+
key: task.key,
|
|
490
|
+
title: task.title,
|
|
491
|
+
priority: task.priority,
|
|
492
|
+
description: task.description || '(no description)',
|
|
493
|
+
// The implementation plan — Claude must read and present this to the developer
|
|
494
|
+
implementationPlan: hasReadme
|
|
495
|
+
? task.readmeMarkdown
|
|
496
|
+
: '⚠️ No implementation plan (README) found for this task. The developer should write one before starting work.',
|
|
497
|
+
subtasks: subtasksTotal > 0
|
|
498
|
+
? { items: subtasks, progress: `${subtasksDone}/${subtasksTotal} done` }
|
|
499
|
+
: null,
|
|
500
|
+
approvalState: task.approval?.state || 'none',
|
|
501
|
+
},
|
|
502
|
+
meta: {
|
|
503
|
+
currentColumn: task.column,
|
|
504
|
+
willMoveTo: 'in_progress',
|
|
505
|
+
suggestedBranch,
|
|
506
|
+
},
|
|
507
|
+
requiresConfirmation: true,
|
|
508
|
+
message: hasReadme
|
|
509
|
+
? `Read the implementation plan above carefully, then call kickoff_task again with confirmed=true to start the task.`
|
|
510
|
+
: `⚠️ This task has no implementation plan. Consider writing one first (use update_task with readmeMarkdown), then confirm to start anyway.`,
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── Confirmed: move to in_progress and fetch recent commits ──
|
|
415
515
|
let recentCommits = []
|
|
416
516
|
try {
|
|
417
|
-
const commitsRes = await api.get(
|
|
418
|
-
`/api/projects/${task.project}/github/commits?per_page=10`
|
|
419
|
-
)
|
|
517
|
+
const commitsRes = await api.get(`/api/projects/${task.project}/github/commits?per_page=10`)
|
|
420
518
|
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
421
519
|
} catch { /* GitHub may not be linked */ }
|
|
422
520
|
|
|
423
|
-
// Move to in_progress
|
|
424
521
|
let moved = false
|
|
425
522
|
try {
|
|
426
|
-
const moveRes = await api.post(`/api/tasks/${taskId}/move`, {
|
|
427
|
-
column: 'in_progress',
|
|
428
|
-
toIndex: 0,
|
|
429
|
-
})
|
|
523
|
+
const moveRes = await api.post(`/api/tasks/${taskId}/move`, { column: 'in_progress', toIndex: 0 })
|
|
430
524
|
moved = moveRes?.success ?? false
|
|
431
525
|
} catch { /* might already be in_progress */ }
|
|
432
526
|
|
|
433
527
|
return text({
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
column: moved ? 'in_progress' : task.column,
|
|
441
|
-
readme: task.readmeMarkdown,
|
|
442
|
-
subtasks: task.subtasks,
|
|
443
|
-
parkNote: task.parkNote,
|
|
444
|
-
approval: task.approval,
|
|
528
|
+
started: {
|
|
529
|
+
key: task.key,
|
|
530
|
+
title: task.title,
|
|
531
|
+
priority: task.priority,
|
|
532
|
+
column: moved ? 'in_progress' : task.column,
|
|
533
|
+
subtasks,
|
|
445
534
|
},
|
|
446
|
-
|
|
535
|
+
implementationPlan: hasReadme ? task.readmeMarkdown : null,
|
|
536
|
+
recentCommits: recentCommits.slice(0, 5).map(c => ({
|
|
447
537
|
sha: c.sha?.slice(0, 7),
|
|
448
538
|
message: c.commit?.message?.split('\n')[0],
|
|
449
539
|
author: c.commit?.author?.name,
|
|
450
540
|
date: c.commit?.author?.date,
|
|
451
541
|
})),
|
|
452
542
|
movedToInProgress: moved,
|
|
453
|
-
suggestedBranch
|
|
454
|
-
|
|
455
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
456
|
-
.slice(0, 40)}`,
|
|
543
|
+
suggestedBranch,
|
|
544
|
+
nextStep: `Use create_branch to create "${suggestedBranch}" on GitHub, then run:\n git fetch origin\n git checkout ${suggestedBranch}`,
|
|
457
545
|
})
|
|
458
546
|
}
|
|
459
547
|
)
|
|
@@ -509,14 +597,17 @@ Call this when the developer asks "what have I been neglecting" or "any stale ta
|
|
|
509
597
|
'link_pr_to_task',
|
|
510
598
|
`Link a pull request to a task, post a comment, and move the task to in_review.
|
|
511
599
|
Use this when a developer pushes a branch and opens a PR.
|
|
512
|
-
Keeps the board in sync with git automatically
|
|
600
|
+
Keeps the board in sync with git automatically.
|
|
601
|
+
|
|
602
|
+
Set confirmed=false first to preview, then confirmed=true to execute.`,
|
|
513
603
|
{
|
|
514
604
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
515
605
|
prUrl: z.string().describe('Full GitHub PR URL'),
|
|
516
606
|
prTitle: z.string().optional().describe('PR title'),
|
|
517
607
|
branch: z.string().optional().describe('Branch name'),
|
|
608
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
518
609
|
},
|
|
519
|
-
async ({ taskId, prUrl, prTitle, branch }) => {
|
|
610
|
+
async ({ taskId, prUrl, prTitle, branch, confirmed = false }) => {
|
|
520
611
|
const commentBody = [
|
|
521
612
|
`## Pull Request Opened`,
|
|
522
613
|
`**PR:** [${prTitle || prUrl}](${prUrl})`,
|
|
@@ -524,16 +615,23 @@ Keeps the board in sync with git automatically.`,
|
|
|
524
615
|
`**Status:** Ready for review`,
|
|
525
616
|
].filter(Boolean).join('\n')
|
|
526
617
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
618
|
+
if (!confirmed) {
|
|
619
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
620
|
+
const task = taskRes?.data?.task
|
|
621
|
+
return text({
|
|
622
|
+
preview: {
|
|
623
|
+
action: 'link_pr_to_task',
|
|
624
|
+
task: task ? { key: task.key, title: task.title } : { taskId },
|
|
625
|
+
willPost: commentBody,
|
|
626
|
+
willMoveTo: 'in_review',
|
|
627
|
+
},
|
|
628
|
+
requiresConfirmation: true,
|
|
629
|
+
message: 'This will post the comment above and move the task to in_review. Call link_pr_to_task again with confirmed=true to proceed.',
|
|
630
|
+
})
|
|
631
|
+
}
|
|
531
632
|
|
|
532
|
-
|
|
533
|
-
const moveRes
|
|
534
|
-
column: 'in_review',
|
|
535
|
-
toIndex: 0,
|
|
536
|
-
})
|
|
633
|
+
const commentRes = await api.post(`/api/tasks/${taskId}/comments`, { body: commentBody })
|
|
634
|
+
const moveRes = await api.post(`/api/tasks/${taskId}/move`, { column: 'in_review', toIndex: 0 })
|
|
537
635
|
|
|
538
636
|
return text({
|
|
539
637
|
commented: commentRes?.success ?? false,
|
|
@@ -677,6 +775,472 @@ function registerGithubTools(server, { scopedProjectId }) {
|
|
|
677
775
|
)
|
|
678
776
|
}
|
|
679
777
|
|
|
778
|
+
// ── Git Workflow Tools ─────────────────────────────────────────────────────────
|
|
779
|
+
// All write operations require confirmed=true after showing a preview.
|
|
780
|
+
// This gives developers full visibility before anything is executed.
|
|
781
|
+
|
|
782
|
+
function registerGitWorkflowTools(server, { scopedProjectId } = {}) {
|
|
783
|
+
|
|
784
|
+
// ── list_my_tasks ────────────────────────────────────────────────────────────
|
|
785
|
+
server.tool(
|
|
786
|
+
'list_my_tasks',
|
|
787
|
+
`List your assigned tasks sorted by priority (critical → high → medium → low).
|
|
788
|
+
Each task includes a suggested next git action so Claude can guide you step by step.
|
|
789
|
+
Use this at the start of a session or when switching tasks.`,
|
|
790
|
+
{
|
|
791
|
+
includeColumns: z.array(
|
|
792
|
+
z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done'])
|
|
793
|
+
).optional().default(['todo', 'in_progress', 'in_review'])
|
|
794
|
+
.describe('Which columns to include. Default excludes backlog and done.'),
|
|
795
|
+
},
|
|
796
|
+
async ({ includeColumns = ['todo', 'in_progress', 'in_review'] } = {}) => {
|
|
797
|
+
const res = await api.get('/api/users/me/tasks')
|
|
798
|
+
if (!res?.success) return errorText('Could not fetch tasks')
|
|
799
|
+
|
|
800
|
+
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
801
|
+
const tasks = (res.data.tasks || [])
|
|
802
|
+
.filter(t => includeColumns.includes(t.column))
|
|
803
|
+
.sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99))
|
|
804
|
+
.map(t => {
|
|
805
|
+
let suggestedAction = null
|
|
806
|
+
if (t.column === 'todo') {
|
|
807
|
+
suggestedAction = 'Use kickoff_task to start — it will brief you and move the task to in_progress'
|
|
808
|
+
} else if (t.column === 'in_progress' && !t.github?.headBranch) {
|
|
809
|
+
suggestedAction = 'No branch yet — use create_branch to create one before coding'
|
|
810
|
+
} else if (t.column === 'in_progress' && t.github?.headBranch) {
|
|
811
|
+
suggestedAction = `Branch exists (${t.github.headBranch}). Pushed commits? Use raise_pr to open a PR`
|
|
812
|
+
} else if (t.column === 'in_review') {
|
|
813
|
+
suggestedAction = `PR #${t.github?.prNumber || '?'} is open — waiting for reviewer feedback`
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
id: t._id,
|
|
817
|
+
key: t.key,
|
|
818
|
+
title: t.title,
|
|
819
|
+
column: t.column,
|
|
820
|
+
priority: t.priority,
|
|
821
|
+
project: t.project?.name || t.project,
|
|
822
|
+
github: t.github ? { headBranch: t.github.headBranch, prNumber: t.github.prNumber, mergedAt: t.github.mergedAt } : null,
|
|
823
|
+
parked: !!t.parkNote?.parkedAt,
|
|
824
|
+
suggestedAction,
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
return text({ tasks, count: tasks.length })
|
|
829
|
+
}
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
// ── get_task_context ─────────────────────────────────────────────────────────
|
|
833
|
+
server.tool(
|
|
834
|
+
'get_task_context',
|
|
835
|
+
`Get everything needed to resume a task: full details, park notes, recent activity, and git state.
|
|
836
|
+
Use this when returning to a task after a break, or when Claude needs the full picture before suggesting next steps.`,
|
|
837
|
+
{
|
|
838
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
839
|
+
},
|
|
840
|
+
async ({ taskId }) => {
|
|
841
|
+
const [taskRes, activityRes] = await Promise.all([
|
|
842
|
+
api.get(`/api/tasks/${taskId}`),
|
|
843
|
+
api.get(`/api/tasks/${taskId}/activity`).catch(() => null),
|
|
844
|
+
])
|
|
845
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
846
|
+
const task = taskRes.data.task
|
|
847
|
+
const recentActivity = (activityRes?.data?.activity || []).slice(-10)
|
|
848
|
+
|
|
849
|
+
let coachingPrompt = null
|
|
850
|
+
if (task.column === 'in_progress' && !task.github?.headBranch) {
|
|
851
|
+
coachingPrompt = 'No branch created yet. Use create_branch before writing code.'
|
|
852
|
+
} else if (task.column === 'in_progress' && task.github?.headBranch) {
|
|
853
|
+
coachingPrompt = `Branch: ${task.github.headBranch}. When commits are pushed, use raise_pr to open a PR.`
|
|
854
|
+
} else if (task.column === 'in_review') {
|
|
855
|
+
coachingPrompt = `PR #${task.github?.prNumber} is open. Waiting for reviewer feedback.`
|
|
856
|
+
} else if (task.parkNote?.parkedAt) {
|
|
857
|
+
coachingPrompt = `Task was parked. Park note: "${task.parkNote.summary}". Use pop_stash if you stashed changes.`
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return text({
|
|
861
|
+
task: {
|
|
862
|
+
id: task._id,
|
|
863
|
+
key: task.key,
|
|
864
|
+
title: task.title,
|
|
865
|
+
description: task.description,
|
|
866
|
+
priority: task.priority,
|
|
867
|
+
column: task.column,
|
|
868
|
+
assignees: (task.assignees || []).map(a => ({ id: a._id, name: a.name })),
|
|
869
|
+
reviewer: task.reviewer ? { id: task.reviewer._id, name: task.reviewer.name } : null,
|
|
870
|
+
parkNote: task.parkNote,
|
|
871
|
+
github: task.github,
|
|
872
|
+
subtasks: task.subtasks,
|
|
873
|
+
readmeMarkdown: task.readmeMarkdown,
|
|
874
|
+
},
|
|
875
|
+
recentActivity: recentActivity.map(a => ({ action: a.action, createdAt: a.createdAt, meta: a.meta })),
|
|
876
|
+
coachingPrompt,
|
|
877
|
+
})
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
// ── create_branch ────────────────────────────────────────────────────────────
|
|
882
|
+
server.tool(
|
|
883
|
+
'create_branch',
|
|
884
|
+
`Create a Git branch on GitHub for a task using the naming convention:
|
|
885
|
+
feature/TASK-XXX-short-description (for new features)
|
|
886
|
+
fix/TASK-XXX-short-description (for bug fixes / patches)
|
|
887
|
+
|
|
888
|
+
WHEN TO USE: Immediately after a task moves to in_progress.
|
|
889
|
+
Claude will suggest this automatically after kickoff_task.
|
|
890
|
+
|
|
891
|
+
Set confirmed=false first to preview the branch name, then confirmed=true to create it.
|
|
892
|
+
After creation, run locally:
|
|
893
|
+
git fetch origin
|
|
894
|
+
git checkout <branchName>`,
|
|
895
|
+
{
|
|
896
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
897
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
898
|
+
fromRef: z.string().optional().describe("Base ref to branch from (default: project's default branch)"),
|
|
899
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to create the branch after reviewing the preview'),
|
|
900
|
+
},
|
|
901
|
+
async ({ taskId, projectId, fromRef, confirmed = false }) => {
|
|
902
|
+
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
903
|
+
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
907
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
908
|
+
const task = taskRes.data.task
|
|
909
|
+
|
|
910
|
+
const isFix = /\b(fix|bug|hotfix|patch)\b/i.test(task.title + ' ' + (task.description || ''))
|
|
911
|
+
const prefix = isFix ? 'fix' : 'feature'
|
|
912
|
+
const slug = task.title
|
|
913
|
+
.toLowerCase()
|
|
914
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
915
|
+
.replace(/^-|-$/g, '')
|
|
916
|
+
.slice(0, 35)
|
|
917
|
+
const branchName = `${prefix}/${task.key.toLowerCase()}-${slug}`
|
|
918
|
+
|
|
919
|
+
// Preview
|
|
920
|
+
if (!confirmed) {
|
|
921
|
+
return text({
|
|
922
|
+
preview: {
|
|
923
|
+
action: 'create_branch',
|
|
924
|
+
branchName,
|
|
925
|
+
fromRef: fromRef || '(project default branch)',
|
|
926
|
+
repo: '(resolved from project)',
|
|
927
|
+
task: { key: task.key, title: task.title },
|
|
928
|
+
},
|
|
929
|
+
requiresConfirmation: true,
|
|
930
|
+
message: `Will create branch "${branchName}" on GitHub. Call create_branch again with confirmed=true to proceed.`,
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
const res = await api.post(`/api/projects/${projectId}/github/branches`, { branchName, fromRef })
|
|
936
|
+
if (!res?.success) return errorText(res?.message || 'Could not create branch')
|
|
937
|
+
return text({
|
|
938
|
+
branchName,
|
|
939
|
+
url: res.data.url,
|
|
940
|
+
message: 'Branch created on GitHub.',
|
|
941
|
+
gitSteps: [
|
|
942
|
+
'git fetch origin',
|
|
943
|
+
`git checkout ${branchName}`,
|
|
944
|
+
],
|
|
945
|
+
nextStep: 'Start coding! When your commits are ready to review, use raise_pr to open a pull request.',
|
|
946
|
+
})
|
|
947
|
+
} catch (e) {
|
|
948
|
+
return errorText(e.message)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
// ── stash_changes ────────────────────────────────────────────────────────────
|
|
954
|
+
server.tool(
|
|
955
|
+
'stash_changes',
|
|
956
|
+
`Save your current uncommitted work before switching to a higher-priority task.
|
|
957
|
+
|
|
958
|
+
WHEN TO USE: When an urgent task arrives while you are mid-work on another task.
|
|
959
|
+
Claude suggests this automatically when you say "I need to switch to TASK-XYZ".
|
|
960
|
+
|
|
961
|
+
This tool parks the current task in InternalTool (saves your context) and returns the
|
|
962
|
+
exact git stash command to run in your terminal — MCP cannot run git commands directly.
|
|
963
|
+
|
|
964
|
+
Set confirmed=false first to preview, then confirmed=true to park the task.`,
|
|
965
|
+
{
|
|
966
|
+
taskId: z.string().describe("Task's MongoDB ObjectId of the task you are pausing"),
|
|
967
|
+
summary: z.string().optional().describe('What you were doing — include file names and what changed'),
|
|
968
|
+
reason: z.string().optional().describe('Why you are switching (e.g. "urgent fix for TASK-042")'),
|
|
969
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to park the task after reviewing the preview'),
|
|
970
|
+
},
|
|
971
|
+
async ({ taskId, summary = '', reason = '', confirmed = false }) => {
|
|
972
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
973
|
+
const task = taskRes?.data?.task
|
|
974
|
+
|
|
975
|
+
const stashMsg = summary
|
|
976
|
+
? `wip: ${summary.slice(0, 60).replace(/"/g, "'")}`
|
|
977
|
+
: 'wip: pausing for priority switch'
|
|
978
|
+
|
|
979
|
+
const parkNote = {
|
|
980
|
+
summary: summary || 'Work in progress — stashing before switching tasks',
|
|
981
|
+
remaining: reason ? `Switched to: ${reason}` : 'Resume after priority task is done',
|
|
982
|
+
blockers: '',
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (!confirmed) {
|
|
986
|
+
return text({
|
|
987
|
+
preview: {
|
|
988
|
+
action: 'stash_changes',
|
|
989
|
+
task: task ? { key: task.key, title: task.title } : { taskId },
|
|
990
|
+
willPark: parkNote,
|
|
991
|
+
gitCommand: `git stash push -m "${stashMsg}"`,
|
|
992
|
+
},
|
|
993
|
+
requiresConfirmation: true,
|
|
994
|
+
message: 'This will park the task in InternalTool. Run the git stash command yourself in the terminal. Call stash_changes again with confirmed=true to save the park note.',
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
await api.patch(`/api/tasks/${taskId}/park`, parkNote)
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
return errorText(`Failed to park task: ${e.message}`)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return text({
|
|
1005
|
+
taskParked: true,
|
|
1006
|
+
gitCommand: `git stash push -m "${stashMsg}"`,
|
|
1007
|
+
message: 'Task parked. Run the git command above in your terminal to stash your changes.',
|
|
1008
|
+
nextStep: 'Use kickoff_task or create_branch to start the priority task. Use pop_stash to return here.',
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
// ── pop_stash ────────────────────────────────────────────────────────────────
|
|
1014
|
+
server.tool(
|
|
1015
|
+
'pop_stash',
|
|
1016
|
+
`Restore stashed work when returning to a previously paused task.
|
|
1017
|
+
|
|
1018
|
+
WHEN TO USE: When returning to a task you paused with stash_changes.
|
|
1019
|
+
Claude suggests this automatically when you resume a parked task.
|
|
1020
|
+
|
|
1021
|
+
Returns the git commands to run in your terminal and unparks the task in InternalTool.
|
|
1022
|
+
|
|
1023
|
+
Set confirmed=false first to review the park note, then confirmed=true to unpark.`,
|
|
1024
|
+
{
|
|
1025
|
+
taskId: z.string().describe("Task's MongoDB ObjectId of the task you are resuming"),
|
|
1026
|
+
stashIndex: z.number().int().min(0).optional().default(0)
|
|
1027
|
+
.describe("Stash index to pop (0 = most recent). Run 'git stash list' to see all."),
|
|
1028
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to unpark the task after reviewing'),
|
|
1029
|
+
},
|
|
1030
|
+
async ({ taskId, stashIndex = 0, confirmed = false }) => {
|
|
1031
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
1032
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
1033
|
+
const task = taskRes.data.task
|
|
1034
|
+
|
|
1035
|
+
const stashCmd = stashIndex === 0 ? 'git stash pop' : `git stash pop stash@{${stashIndex}}`
|
|
1036
|
+
const gitSteps = ['git fetch origin', 'git rebase origin/main', stashCmd]
|
|
1037
|
+
|
|
1038
|
+
if (!confirmed) {
|
|
1039
|
+
return text({
|
|
1040
|
+
preview: {
|
|
1041
|
+
action: 'pop_stash',
|
|
1042
|
+
task: { key: task.key, title: task.title, column: task.column },
|
|
1043
|
+
parkNote: task.parkNote || null,
|
|
1044
|
+
gitCommands: gitSteps,
|
|
1045
|
+
willDo: 'Clear park note and mark task active in InternalTool',
|
|
1046
|
+
},
|
|
1047
|
+
requiresConfirmation: true,
|
|
1048
|
+
message: 'Review your park note above. Call pop_stash again with confirmed=true to unpark and get the git commands.',
|
|
1049
|
+
})
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
await api.patch(`/api/tasks/${taskId}/unpark`, {})
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
return errorText(`Failed to unpark task: ${e.message}`)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return text({
|
|
1059
|
+
taskUnparked: true,
|
|
1060
|
+
parkNote: task.parkNote || null,
|
|
1061
|
+
gitCommands: gitSteps,
|
|
1062
|
+
message: 'Task unparked. Run the git commands above in your terminal to restore your work.',
|
|
1063
|
+
nextStep: 'Review your park note for context, then continue where you left off.',
|
|
1064
|
+
})
|
|
1065
|
+
}
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
// ── fix_pr_feedback ──────────────────────────────────────────────────────────
|
|
1069
|
+
server.tool(
|
|
1070
|
+
'fix_pr_feedback',
|
|
1071
|
+
`Guide the developer through fixing a PR that has "changes requested" or was closed,
|
|
1072
|
+
while they are mid-work on a different task.
|
|
1073
|
+
|
|
1074
|
+
WHEN TO USE: When a notification says "PR needs changes" on Task A while working on Task B.
|
|
1075
|
+
Claude will suggest this automatically when it detects a task with github.changesRequestedAt set.
|
|
1076
|
+
|
|
1077
|
+
The tool shows exactly what to do — stash Task B, switch to Task A's branch, fix, push, return.
|
|
1078
|
+
All git commands are returned for the developer to run in their terminal.
|
|
1079
|
+
|
|
1080
|
+
Set confirmed=false first to preview the full plan, then confirmed=true to save the stash note.`,
|
|
1081
|
+
{
|
|
1082
|
+
taskAId: z.string().describe("Task's MongoDB ObjectId of the task whose PR needs fixing (Task A)"),
|
|
1083
|
+
taskBId: z.string().optional().describe("Task's MongoDB ObjectId of the task currently being worked on (Task B), if any"),
|
|
1084
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to park Task B and get the full command sequence'),
|
|
1085
|
+
},
|
|
1086
|
+
async ({ taskAId, taskBId, confirmed = false }) => {
|
|
1087
|
+
const taskARes = await api.get(`/api/tasks/${taskAId}`)
|
|
1088
|
+
if (!taskARes?.success) return errorText('Task A not found')
|
|
1089
|
+
const taskA = taskARes.data.task
|
|
1090
|
+
|
|
1091
|
+
let taskB = null
|
|
1092
|
+
if (taskBId) {
|
|
1093
|
+
const taskBRes = await api.get(`/api/tasks/${taskBId}`)
|
|
1094
|
+
taskB = taskBRes?.data?.task || null
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const taskABranch = taskA.github?.headBranch || `feature/${taskA.key?.toLowerCase()}-${taskA.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30)}`
|
|
1098
|
+
const taskBBranch = taskB?.github?.headBranch || (taskB ? `feature/${taskB.key?.toLowerCase()}` : null)
|
|
1099
|
+
|
|
1100
|
+
const plan = {
|
|
1101
|
+
situation: taskB
|
|
1102
|
+
? `PR for Task A (${taskA.key}) needs changes while you're working on Task B (${taskB.key}).`
|
|
1103
|
+
: `PR for Task A (${taskA.key}) needs changes.`,
|
|
1104
|
+
taskA: { key: taskA.key, title: taskA.title, branch: taskABranch, prNumber: taskA.github?.prNumber, prUrl: taskA.github?.prUrl },
|
|
1105
|
+
taskB: taskB ? { key: taskB.key, title: taskB.title, branch: taskBBranch } : null,
|
|
1106
|
+
steps: [
|
|
1107
|
+
taskB ? `1. Stash Task B work: git stash push -m "wip: ${taskBBranch} — switching to fix ${taskA.key} PR"` : null,
|
|
1108
|
+
`${taskB ? 2 : 1}. Switch to Task A branch: git checkout ${taskABranch}`,
|
|
1109
|
+
`${taskB ? 3 : 2}. Fetch and rebase: git fetch origin && git rebase origin/main`,
|
|
1110
|
+
`${taskB ? 4 : 3}. Read reviewer feedback on GitHub: ${taskA.github?.prUrl || 'open the PR'}`,
|
|
1111
|
+
`${taskB ? 5 : 4}. Fix issues, commit: git add . && git commit -m "fix(${taskA.key?.toLowerCase()}): address review feedback"`,
|
|
1112
|
+
`${taskB ? 6 : 5}. Push to update PR: git push origin ${taskABranch}`,
|
|
1113
|
+
taskB ? `7. Return to Task B: git checkout ${taskBBranch} && git stash pop` : null,
|
|
1114
|
+
].filter(Boolean),
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (!confirmed) {
|
|
1118
|
+
return text({
|
|
1119
|
+
preview: plan,
|
|
1120
|
+
requiresConfirmation: true,
|
|
1121
|
+
message: taskB
|
|
1122
|
+
? `This will park Task B and guide you to fix the PR for Task A. Call fix_pr_feedback again with confirmed=true to proceed.`
|
|
1123
|
+
: `Here's the plan to fix the PR. Call fix_pr_feedback again with confirmed=true to proceed.`,
|
|
1124
|
+
})
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Park Task B if provided
|
|
1128
|
+
if (taskB && taskBId) {
|
|
1129
|
+
try {
|
|
1130
|
+
await api.patch(`/api/tasks/${taskBId}/park`, {
|
|
1131
|
+
summary: `Working on fix for ${taskA.key} PR review`,
|
|
1132
|
+
remaining: `Resume ${taskB.key} after PR fix is pushed`,
|
|
1133
|
+
blockers: '',
|
|
1134
|
+
})
|
|
1135
|
+
} catch { /* non-fatal — developer can park manually */ }
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return text({
|
|
1139
|
+
plan,
|
|
1140
|
+
taskBParked: !!(taskB && taskBId),
|
|
1141
|
+
gitCommands: [
|
|
1142
|
+
taskBBranch ? `git stash push -m "wip: ${taskBBranch} — switching to fix ${taskA.key} PR"` : null,
|
|
1143
|
+
`git checkout ${taskABranch}`,
|
|
1144
|
+
'git fetch origin',
|
|
1145
|
+
'git rebase origin/main',
|
|
1146
|
+
`# Read feedback at: ${taskA.github?.prUrl || '(open the PR on GitHub)'}`,
|
|
1147
|
+
`# Make your fixes, then:`,
|
|
1148
|
+
`git add .`,
|
|
1149
|
+
`git commit -m "fix(${taskA.key?.toLowerCase()}): address review feedback — <describe what changed>"`,
|
|
1150
|
+
`git push origin ${taskABranch}`,
|
|
1151
|
+
taskBBranch ? `# When done — return to Task B:` : null,
|
|
1152
|
+
taskBBranch ? `git checkout ${taskBBranch}` : null,
|
|
1153
|
+
taskBBranch ? `git stash pop` : null,
|
|
1154
|
+
].filter(Boolean),
|
|
1155
|
+
nextStep: `Fix the issues on Task A's branch, push, then return to Task B. The PR updates automatically when you push.`,
|
|
1156
|
+
})
|
|
1157
|
+
}
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
// ── raise_pr ─────────────────────────────────────────────────────────────────
|
|
1161
|
+
server.tool(
|
|
1162
|
+
'raise_pr',
|
|
1163
|
+
`Create a GitHub Pull Request for a task with auto-generated title and description.
|
|
1164
|
+
|
|
1165
|
+
WHEN TO USE: After pushing commits on a feature/fix branch and ready for review.
|
|
1166
|
+
Claude suggests this automatically when a task is in_progress and you indicate commits are pushed.
|
|
1167
|
+
|
|
1168
|
+
The PR title and body are built from the task data (title, key, description, park notes).
|
|
1169
|
+
After the PR is created, the GitHub webhook will automatically move the task to in_review
|
|
1170
|
+
and notify the assigned reviewer.
|
|
1171
|
+
|
|
1172
|
+
Set confirmed=false first to preview the full PR content, then confirmed=true to create it.`,
|
|
1173
|
+
{
|
|
1174
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1175
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
1176
|
+
headBranch: z.string().describe('Your feature/fix branch (e.g. feature/task-001-my-feature)'),
|
|
1177
|
+
additionalNotes: z.string().optional().describe('Extra context to add to the PR body'),
|
|
1178
|
+
draft: z.boolean().optional().default(false).describe('Open as a draft PR (not yet ready for review)'),
|
|
1179
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to create the PR after reviewing the preview'),
|
|
1180
|
+
},
|
|
1181
|
+
async ({ taskId, projectId, headBranch, additionalNotes = '', draft = false, confirmed = false }) => {
|
|
1182
|
+
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
1183
|
+
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
1187
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
1188
|
+
const task = taskRes.data.task
|
|
1189
|
+
|
|
1190
|
+
const prTitle = `[${task.key}] ${task.title}`
|
|
1191
|
+
const bodyParts = [
|
|
1192
|
+
`## ${task.key}: ${task.title}`,
|
|
1193
|
+
'',
|
|
1194
|
+
task.description ? `### Description\n${task.description}` : null,
|
|
1195
|
+
task.readmeMarkdown ? `### Implementation Notes\n${task.readmeMarkdown.slice(0, 600)}` : null,
|
|
1196
|
+
task.parkNote?.summary ? `### Work Summary\n${task.parkNote.summary}` : null,
|
|
1197
|
+
task.parkNote?.remaining ? `### What Remains\n${task.parkNote.remaining}` : null,
|
|
1198
|
+
additionalNotes ? `### Additional Notes\n${additionalNotes}` : null,
|
|
1199
|
+
'',
|
|
1200
|
+
'---',
|
|
1201
|
+
`*Auto-generated by InternalTool MCP — task: ${task.key}*`,
|
|
1202
|
+
].filter(v => v !== null).join('\n')
|
|
1203
|
+
|
|
1204
|
+
if (!confirmed) {
|
|
1205
|
+
return text({
|
|
1206
|
+
preview: {
|
|
1207
|
+
action: 'raise_pr',
|
|
1208
|
+
prTitle,
|
|
1209
|
+
prBody: bodyParts,
|
|
1210
|
+
headBranch,
|
|
1211
|
+
draft,
|
|
1212
|
+
task: { key: task.key, title: task.title },
|
|
1213
|
+
},
|
|
1214
|
+
requiresConfirmation: true,
|
|
1215
|
+
message: `Will open a${draft ? ' draft' : ''} PR titled "${prTitle}". Call raise_pr again with confirmed=true to create it.`,
|
|
1216
|
+
})
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
try {
|
|
1220
|
+
const res = await api.post(`/api/projects/${projectId}/github/pull-requests`, {
|
|
1221
|
+
title: prTitle,
|
|
1222
|
+
body: bodyParts,
|
|
1223
|
+
head: headBranch,
|
|
1224
|
+
draft,
|
|
1225
|
+
})
|
|
1226
|
+
if (!res?.success) return errorText(res?.message || 'Could not create PR')
|
|
1227
|
+
return text({
|
|
1228
|
+
prNumber: res.data.prNumber,
|
|
1229
|
+
prUrl: res.data.prUrl,
|
|
1230
|
+
title: prTitle,
|
|
1231
|
+
draft,
|
|
1232
|
+
message: `PR #${res.data.prNumber} created.`,
|
|
1233
|
+
nextStep: draft
|
|
1234
|
+
? 'PR is a draft. Mark it ready for review on GitHub when you want reviewer notifications to fire.'
|
|
1235
|
+
: 'PR is live. The GitHub webhook will move the task to in_review and notify the reviewer within seconds.',
|
|
1236
|
+
})
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
return errorText(e.message)
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
)
|
|
1242
|
+
}
|
|
1243
|
+
|
|
680
1244
|
function registerAdminTools(server) {
|
|
681
1245
|
server.tool(
|
|
682
1246
|
'admin_list_users',
|
|
@@ -728,6 +1292,7 @@ async function main() {
|
|
|
728
1292
|
registerCommentTools(server)
|
|
729
1293
|
registerNotificationTools(server)
|
|
730
1294
|
registerGithubTools(server, ctx)
|
|
1295
|
+
registerGitWorkflowTools(server, ctx)
|
|
731
1296
|
|
|
732
1297
|
if (isAdmin) {
|
|
733
1298
|
registerAdminTools(server)
|
package/package.json
CHANGED