internaltool-mcp 1.6.25 → 1.6.27
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 +127 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1537,6 +1537,125 @@ The log is visible in InternalTool under the task's Session tab — gives the hu
|
|
|
1537
1537
|
return text({ logged: true, type, name, summary })
|
|
1538
1538
|
}
|
|
1539
1539
|
)
|
|
1540
|
+
|
|
1541
|
+
// ── get_parallel_kickoffs ─────────────────────────────────────────────────────
|
|
1542
|
+
server.tool(
|
|
1543
|
+
'get_parallel_kickoffs',
|
|
1544
|
+
`Read the decomposition plan and return ready-to-paste Cursor Agent prompts for the next group of parallel subtasks.
|
|
1545
|
+
|
|
1546
|
+
COORDINATOR WORKFLOW:
|
|
1547
|
+
1. Call decompose_task to create the plan
|
|
1548
|
+
2. Call get_parallel_kickoffs to get one paste-ready prompt per parallel subtask
|
|
1549
|
+
3. Tell the user exactly how many new Cursor Agent windows to open
|
|
1550
|
+
4. Give them each prompt to paste — one window per subtask
|
|
1551
|
+
|
|
1552
|
+
Cursor cannot spawn agent windows automatically. This tool generates the prompts;
|
|
1553
|
+
the human opens the windows. Each builder agent then runs independently.`,
|
|
1554
|
+
{
|
|
1555
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1556
|
+
},
|
|
1557
|
+
async ({ taskId }) => {
|
|
1558
|
+
trackTaskActivity(taskId, 'get_parallel_kickoffs')
|
|
1559
|
+
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
1560
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
1561
|
+
const task = taskRes.data.task
|
|
1562
|
+
|
|
1563
|
+
if (!task.decomposition?.trim()) {
|
|
1564
|
+
return text({
|
|
1565
|
+
error: true,
|
|
1566
|
+
message: 'No decomposition plan found. Call decompose_task first to break the task into subtasks.',
|
|
1567
|
+
})
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
let dec
|
|
1571
|
+
try { dec = JSON.parse(task.decomposition) } catch {
|
|
1572
|
+
return errorText('Decomposition JSON is malformed — call decompose_task again.')
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const subtasks = dec.subtasks || []
|
|
1576
|
+
const execOrder = dec.executionOrder || []
|
|
1577
|
+
|
|
1578
|
+
// Determine which group to run next: first group that has at least one
|
|
1579
|
+
// subtask not yet reflected in the board subtasks as done.
|
|
1580
|
+
const boardSubtasks = task.subtasks || []
|
|
1581
|
+
// Board subtask titles may carry a role prefix like "[BUILDER] Auth hardening..."
|
|
1582
|
+
// Match on bare title OR prefixed title
|
|
1583
|
+
const isDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
1584
|
+
|
|
1585
|
+
let nextGroup = null
|
|
1586
|
+
let nextGroupIndex = -1
|
|
1587
|
+
for (let i = 0; i < execOrder.length; i++) {
|
|
1588
|
+
const group = execOrder[i]
|
|
1589
|
+
const allDone = group.every(t => isDone(t))
|
|
1590
|
+
if (!allDone) {
|
|
1591
|
+
// Also check all prior groups are fully done (respects dependsOn)
|
|
1592
|
+
const priorAllDone = execOrder.slice(0, i).every(g => g.every(t => isDone(t)))
|
|
1593
|
+
if (priorAllDone) {
|
|
1594
|
+
nextGroup = group
|
|
1595
|
+
nextGroupIndex = i + 1
|
|
1596
|
+
break
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (!nextGroup) {
|
|
1602
|
+
return text({
|
|
1603
|
+
allDone: true,
|
|
1604
|
+
message: 'All subtask groups are complete. Move to the final review or run coverage tests.',
|
|
1605
|
+
})
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Only kick off subtasks that aren't done yet
|
|
1609
|
+
const pendingInGroup = nextGroup.filter(t => !isDone(t))
|
|
1610
|
+
const parallelCount = pendingInGroup.length
|
|
1611
|
+
const isParallel = parallelCount > 1
|
|
1612
|
+
|
|
1613
|
+
if (parallelCount === 0) {
|
|
1614
|
+
return text({
|
|
1615
|
+
group: nextGroupIndex,
|
|
1616
|
+
allDone: true,
|
|
1617
|
+
message: `All subtasks in Group ${nextGroupIndex} are already done. Call get_parallel_kickoffs again to get the next group.`,
|
|
1618
|
+
})
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Build one ready-to-paste prompt per pending subtask in this group
|
|
1622
|
+
const kickoffs = pendingInGroup.map((subtaskTitle, i) => {
|
|
1623
|
+
const st = subtasks.find(s => s.title === subtaskTitle) || { title: subtaskTitle, role: 'builder', files: [], description: '' }
|
|
1624
|
+
const fileList = (st.files || []).map(f => ` - ${f}`).join('\n')
|
|
1625
|
+
const prompt = [
|
|
1626
|
+
`You are a ${st.role || 'builder'} agent for TASK-011 (${task.key}).`,
|
|
1627
|
+
``,
|
|
1628
|
+
`## Your subtask (${i + 1} of ${parallelCount} running in parallel)`,
|
|
1629
|
+
`**Title:** ${st.title}`,
|
|
1630
|
+
`**Description:** ${st.description || ''}`,
|
|
1631
|
+
`**Your files:**`,
|
|
1632
|
+
fileList || ' (to be determined)',
|
|
1633
|
+
``,
|
|
1634
|
+
`## Steps`,
|
|
1635
|
+
`1. Call \`get_agent_context\` with taskId \`${taskId}\``,
|
|
1636
|
+
`2. Call \`claim_files\` with taskId \`${taskId}\` and files: [${(st.files || []).map(f => `"${f}"`).join(', ')}]`,
|
|
1637
|
+
`3. Implement the subtask — only modify your claimed files`,
|
|
1638
|
+
`4. Run tests to verify nothing is broken`,
|
|
1639
|
+
`5. Commit your changes with a clear message referencing ${task.key}`,
|
|
1640
|
+
`6. Mark the subtask done in InternalTool by calling \`update_task\` to tick off "${st.title}" in the subtasks list`,
|
|
1641
|
+
``,
|
|
1642
|
+
`Do NOT touch files owned by other parallel agents.`,
|
|
1643
|
+
].join('\n')
|
|
1644
|
+
return { subtask: st.title, role: st.role, files: st.files, prompt }
|
|
1645
|
+
})
|
|
1646
|
+
|
|
1647
|
+
return text({
|
|
1648
|
+
group: nextGroupIndex,
|
|
1649
|
+
totalGroups: execOrder.length,
|
|
1650
|
+
isParallel,
|
|
1651
|
+
parallelCount,
|
|
1652
|
+
instruction: isParallel
|
|
1653
|
+
? `⚡ GROUP ${nextGroupIndex} runs ${parallelCount} subtasks IN PARALLEL.\n\nTell the user:\n"Please open ${parallelCount} new Cursor Agent windows (Cmd+Shift+P → New Agent Session or open a new Composer tab set to Agent mode).\nPaste prompt 1 in window 1, prompt 2 in window 2. They will run independently."`
|
|
1654
|
+
: `GROUP ${nextGroupIndex} has 1 sequential subtask. Paste the prompt below in a new Agent window (or handle it yourself).`,
|
|
1655
|
+
kickoffs,
|
|
1656
|
+
})
|
|
1657
|
+
}
|
|
1658
|
+
)
|
|
1540
1659
|
}
|
|
1541
1660
|
|
|
1542
1661
|
// ── Standup activity formatter ────────────────────────────────────────────────
|
|
@@ -2967,7 +3086,7 @@ function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
|
2967
3086
|
const BUILT_IN = {
|
|
2968
3087
|
scout: `# Scout Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- READ ONLY — do not write or modify any file\n- You CAN read files, run tests, and use MCP tools\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Read every source file systematically\n3. Call \`scout_task\` with confirmed=false, then confirmed=true + your report\n`,
|
|
2969
3088
|
builder: `# Builder Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n**Claimed files:**\n${fileLine}\n\n${scoutLine}\n${decLine}\n\n## Constraints\n- Only modify your claimed files\n- Every change needs a test update\n- Run tests before marking done\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Implement the feature\n3. Use the \`run-tests\` skill to verify\n4. Call \`commit_helper\` then \`raise_pr\`\n`,
|
|
2970
|
-
coordinator: `# Coordinator Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Do NOT write code — plan and delegate only\n- Each subtask must have exclusive file ownership\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}
|
|
3089
|
+
coordinator: `# Coordinator Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Do NOT write code — plan and delegate only\n- Each subtask must have exclusive file ownership\n- Cursor CANNOT auto-spawn agents — you generate prompts, the human opens windows\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\` to read the scout report\n2. Call \`decompose_task\` with taskId \`${taskId}\` to break work into parallel groups\n3. Call \`get_parallel_kickoffs\` with taskId \`${taskId}\` — this returns one ready-to-paste prompt per parallel subtask\n4. Tell the user exactly:\n - How many new Cursor Agent windows to open\n - Which prompt to paste in each window (copy verbatim from the kickoffs output)\n5. After all groups in a parallel batch are done, call \`get_parallel_kickoffs\` again for the next group\n\n## Important\nParallel = multiple Cursor Agent windows open at the same time.\nEach window is an independent builder — it claims its own files, implements, commits.\nYou (coordinator) do not implement anything — only plan and hand off.\n`,
|
|
2971
3090
|
reviewer: `# Reviewer Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Read the full diff before approving\n- Post specific, file-level comments\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Call \`review_pr\` to fetch the diff\n3. Call \`post_pr_review\` with your verdict\n`,
|
|
2972
3091
|
}
|
|
2973
3092
|
agentBody = BUILT_IN[role] || `# ${role} Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n`
|
|
@@ -3663,6 +3782,7 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
3663
3782
|
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
3664
3783
|
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
3665
3784
|
}
|
|
3785
|
+
trackTaskActivity(taskId, 'create_branch', { summary: confirmed ? 'Branch created on GitHub' : 'Branch creation preview' })
|
|
3666
3786
|
|
|
3667
3787
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
3668
3788
|
if (!taskRes?.success) return errorText('Task not found')
|
|
@@ -3926,6 +4046,7 @@ Set confirmed=false first to preview, then confirmed=true to park the task.`,
|
|
|
3926
4046
|
confirmed: z.boolean().optional().default(false).describe('Set true to park the task after reviewing the preview'),
|
|
3927
4047
|
},
|
|
3928
4048
|
async ({ taskId, summary = '', reason = '', confirmed = false }) => {
|
|
4049
|
+
trackTaskActivity(taskId, 'stash_changes', { summary: confirmed ? 'Task parked + stash instructions provided' : 'Stash preview' })
|
|
3929
4050
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
3930
4051
|
const task = taskRes?.data?.task
|
|
3931
4052
|
|
|
@@ -3985,6 +4106,7 @@ Set confirmed=false first to review the park note, then confirmed=true to unpark
|
|
|
3985
4106
|
confirmed: z.boolean().optional().default(false).describe('Set true to unpark the task after reviewing'),
|
|
3986
4107
|
},
|
|
3987
4108
|
async ({ taskId, stashIndex = 0, confirmed = false }) => {
|
|
4109
|
+
trackTaskActivity(taskId, 'pop_stash', { summary: confirmed ? `Stash[${stashIndex}] popped + task unparked` : 'Pop stash preview' })
|
|
3988
4110
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
3989
4111
|
if (!taskRes?.success) return errorText('Task not found')
|
|
3990
4112
|
const task = taskRes.data.task
|
|
@@ -4041,6 +4163,8 @@ Set confirmed=false first to preview the full plan, then confirmed=true to save
|
|
|
4041
4163
|
confirmed: z.boolean().optional().default(false).describe('Set true to park Task B and get the full command sequence'),
|
|
4042
4164
|
},
|
|
4043
4165
|
async ({ taskAId, taskBId, confirmed = false }) => {
|
|
4166
|
+
trackTaskActivity(taskAId, 'fix_pr_feedback', { summary: confirmed ? 'PR feedback fix plan confirmed' : 'PR feedback fix preview' })
|
|
4167
|
+
if (taskBId) trackTaskActivity(taskBId, 'fix_pr_feedback', { summary: 'Paused to fix PR feedback on another task' })
|
|
4044
4168
|
const taskARes = await api.get(`/api/tasks/${taskAId}`)
|
|
4045
4169
|
if (!taskARes?.success) return errorText('Task A not found')
|
|
4046
4170
|
const taskA = taskARes.data.task
|
|
@@ -4140,6 +4264,7 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
4140
4264
|
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
4141
4265
|
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
4142
4266
|
}
|
|
4267
|
+
trackTaskActivity(taskId, 'raise_pr', { summary: confirmed ? `PR raised: ${headBranch}${draft ? ' (draft)' : ''}` : 'PR preview' })
|
|
4143
4268
|
|
|
4144
4269
|
let taskRes
|
|
4145
4270
|
try { taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`)) }
|
|
@@ -4285,6 +4410,7 @@ branchAction values (only needed when current branch ≠ task branch):
|
|
|
4285
4410
|
.describe('Required when confirmed=true and current branch ≠ task branch'),
|
|
4286
4411
|
},
|
|
4287
4412
|
async ({ taskId, repoPath, confirmed = false, branchAction }) => {
|
|
4413
|
+
if (taskId) trackTaskActivity(taskId, 'commit_helper', { summary: confirmed ? 'Commit command generated' : 'Commit preview' })
|
|
4288
4414
|
const cwd = repoPath || process.cwd()
|
|
4289
4415
|
|
|
4290
4416
|
// ── Read local git state ──────────────────────────────────────────────────
|
package/package.json
CHANGED