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.
Files changed (2) hide show
  1. package/index.js +127 -1
  2. 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}\`\n2. Break work into subtasks with role + files + description\n3. Call \`decompose_task\` with taskId \`${taskId}\` to save the plan\n`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.25",
3
+ "version": "1.6.27",
4
4
  "description": "MCP server for InternalTool — connect AI assistants (Claude Code, Cursor) to your project and task management platform",
5
5
  "type": "module",
6
6
  "main": "index.js",