internaltool-mcp 1.6.29 → 1.6.31

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 +217 -45
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -760,8 +760,20 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
760
760
  }
761
761
 
762
762
  if (!confirmed) {
763
+ // Include any project-level decomposition templates as hints
764
+ const projectRes = task.project?._id || task.project
765
+ let decompositionTemplates = []
766
+ try {
767
+ const projRes = await apiWithRetry(() => api.get(`/api/projects/${projectRes}`))
768
+ decompositionTemplates = projRes?.data?.project?.agentConfig?.decompositionTemplates || []
769
+ } catch { /* non-fatal */ }
770
+
763
771
  return text({
764
772
  decomposition: executionPlan,
773
+ decompositionTemplates: decompositionTemplates.length > 0 ? {
774
+ note: 'These project templates may match your task. Use them as a starting point for subtaskPlan.',
775
+ templates: decompositionTemplates,
776
+ } : null,
765
777
  warnings: [
766
778
  !executionPlan.scoutReportPresent ? '⚠️ No scout report on this task. Consider running a scout agent first to map the codebase before builders start.' : null,
767
779
  !executionPlan.readmePresent ? '⚠️ No implementation plan (README). Builders need a spec to work from.' : null,
@@ -798,8 +810,8 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
798
810
  subtasksCreated: subtaskPlan.length,
799
811
  message: `Decomposition saved. ${subtaskPlan.length} subtask(s) added to the board.`,
800
812
  nextStep: parallelGroups.length > 0
801
- ? `⚡ COORDINATOR: Call get_parallel_kickoffs with taskId="${taskId}" NOW. It returns ready-to-paste prompts for each builder window. DO NOT implement code yourself your job is to coordinate only.`
802
- : `Call get_parallel_kickoffs with taskId="${taskId}" to get the builder prompt. DO NOT implement code yourself.`,
813
+ ? `⚡ COORDINATOR: Call get_parallel_kickoffs with taskId="${taskId}" NOW. It writes Cursor Background Agent files for each parallel builder automatically. Then tell the user to open Background Agents panel (⌘⇧J) and click Start. DO NOT implement code yourself.`
814
+ : `Call get_parallel_kickoffs with taskId="${taskId}". It writes a Cursor Background Agent file for the builder. Tell the user to open Background Agents panel (⌘⇧J) and start it.`,
803
815
  })
804
816
  }
805
817
  )
@@ -927,17 +939,32 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
927
939
  } catch { /* non-fatal */ }
928
940
  }
929
941
 
942
+ // Suggest relevant skills based on role and available project skills
943
+ const availableSkills = ctx.project?.agentConfig?.skills || []
944
+ const suggestedSkills = availableSkills.length > 0
945
+ ? availableSkills.map(s => ({
946
+ name: s.name,
947
+ description: s.description,
948
+ path: `.cursor/skills/${s.name}.md`,
949
+ }))
950
+ : effectiveRole === 'builder'
951
+ ? [{ name: 'run-tests', description: 'How to run and validate tests', path: '.cursor/skills/run-tests.md' }]
952
+ : effectiveRole === 'scout'
953
+ ? [{ name: 'scout-codebase', description: 'How to systematically explore and map the codebase', path: '.cursor/skills/scout-codebase.md' }]
954
+ : []
955
+
930
956
  return text({
931
- role: effectiveRole,
932
- taskKey: ctx.taskKey,
933
- taskTitle: ctx.taskTitle,
934
- systemPrompt: parts.join('\n\n'),
935
- allowedTools: ctx.customAllowedTools || null,
936
- claimedFiles: ctx.task?.claimedFiles || [],
937
- warnings: ctx.warnings || [],
938
- task: ctx.task,
939
- project: ctx.project,
940
- usage: 'Use systemPrompt as your Claude system prompt. If allowedTools is set, restrict your MCP tool calls to that list.',
957
+ role: effectiveRole,
958
+ taskKey: ctx.taskKey,
959
+ taskTitle: ctx.taskTitle,
960
+ systemPrompt: parts.join('\n\n'),
961
+ allowedTools: ctx.customAllowedTools || null,
962
+ claimedFiles: ctx.task?.claimedFiles || [],
963
+ warnings: ctx.warnings || [],
964
+ suggestedSkills,
965
+ task: ctx.task,
966
+ project: ctx.project,
967
+ usage: 'Use systemPrompt as your Claude system prompt. If allowedTools is set, restrict your MCP tool calls to that list. Read and follow each skill in suggestedSkills before starting work.',
941
968
  })
942
969
  }
943
970
  )
@@ -1541,16 +1568,15 @@ The log is visible in InternalTool under the task's Session tab — gives the hu
1541
1568
  // ── get_parallel_kickoffs ─────────────────────────────────────────────────────
1542
1569
  server.tool(
1543
1570
  'get_parallel_kickoffs',
1544
- `Read the decomposition plan and return ready-to-paste Cursor Agent prompts for the next group of parallel subtasks.
1571
+ `Read the decomposition plan, write Cursor Background Agent files for each parallel subtask, and return instructions.
1545
1572
 
1546
1573
  COORDINATOR WORKFLOW:
1547
1574
  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
1575
+ 2. Call get_parallel_kickoffs it writes .cursor/agents/builder-N.md files automatically
1576
+ 3. Open the Cursor Background Agents panel (⌘⇧J or ⌘⇧P "Background Agents")
1577
+ 4. Click "Start" for each builder agentthey run in parallel in the background
1551
1578
 
1552
- Cursor cannot spawn agent windows automatically. This tool generates the prompts;
1553
- the human opens the windows. Each builder agent then runs independently.`,
1579
+ The agent files are fully self-contained: each builder knows exactly what MCP tools to call.`,
1554
1580
  {
1555
1581
  taskId: z.string().describe("Task's MongoDB ObjectId"),
1556
1582
  },
@@ -1635,15 +1661,19 @@ the human opens the windows. Each builder agent then runs independently.`,
1635
1661
  ``,
1636
1662
  `Do these steps immediately in order — do not ask for confirmation, do not skip any step:`,
1637
1663
  ``,
1638
- `1. Call kickoff_task with taskId "${taskId}", agentRole "builder", confirmed true`,
1639
- `2. Call claim_files with taskId "${taskId}" and files [${filesArg}]`,
1640
- `3. Call get_agent_context with taskId "${taskId}" to read the full task plan`,
1641
- `4. Implement the subtask: ${st.description || st.title}`,
1664
+ `1. Call log_session_event with taskId "${taskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} started: ${st.title}"`,
1665
+ `2. Call kickoff_task with taskId "${taskId}", agentRole "builder", confirmed true`,
1666
+ `3. Call claim_files with taskId "${taskId}" and files [${filesArg}]`,
1667
+ `4. Call get_agent_context with taskId "${taskId}" to read the full task plan and suggested skills`,
1668
+ `5. Implement the subtask: ${st.description || st.title}`,
1642
1669
  ` - You may ONLY modify these files: ${fileListPlain}`,
1643
1670
  ` - Do NOT touch any other file`,
1644
- `5. Run npm test all tests must pass before continuing`,
1645
- `6. Call commit_helper with taskId "${taskId}"`,
1646
- `7. Call update_task with taskId "${taskId}" and mark the subtask "${st.title}" as done`,
1671
+ ` - Follow any skills listed in get_agent_context suggestedSkills`,
1672
+ `6. Run npm test all tests must pass before continuing`,
1673
+ `7. Call commit_helper with taskId "${taskId}"`,
1674
+ `8. Call update_task with taskId "${taskId}" and mark the subtask "${st.title}" as done`,
1675
+ `9. Call resume_task with taskId "${taskId}", agentRole "reviewer", confirmed true — transition to reviewer role`,
1676
+ `10. Call log_session_event with taskId "${taskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} finished: ${st.title}"`,
1647
1677
  ``,
1648
1678
  `You are builder ${i + 1} of ${parallelCount} running in parallel. Another builder is working on different files at the same time.`,
1649
1679
  ].join('\n')
@@ -1651,19 +1681,45 @@ the human opens the windows. Each builder agent then runs independently.`,
1651
1681
  return { subtask: st.title, role: st.role || 'builder', files: st.files || [], prompt }
1652
1682
  })
1653
1683
 
1684
+ // Write each builder prompt as a Cursor Background Agent file in .cursor/agents/
1685
+ // Cursor Background Agents panel reads these files and lets you start each one independently.
1686
+ const repoRoot = process.cwd()
1687
+ const agentsDir = join(repoRoot, '.cursor', 'agents')
1688
+ mkdirSync(agentsDir, { recursive: true })
1689
+
1690
+ const writtenFiles = []
1691
+ kickoffs.forEach((k, i) => {
1692
+ const safeName = k.subtask.replace(/[^a-zA-Z0-9-_]/g, '-').replace(/-+/g, '-').slice(0, 40)
1693
+ const fileName = `builder-${i + 1}-${safeName}.md`
1694
+ const filePath = join(agentsDir, fileName)
1695
+
1696
+ const agentFileContent = [
1697
+ `---`,
1698
+ `description: Builder ${i + 1} of ${parallelCount} — ${k.subtask} (${task.key})`,
1699
+ `---`,
1700
+ ``,
1701
+ k.prompt,
1702
+ ].join('\n')
1703
+
1704
+ writeFileSync(filePath, agentFileContent, 'utf8')
1705
+ writtenFiles.push({ index: i + 1, file: `.cursor/agents/${fileName}`, subtask: k.subtask })
1706
+ })
1707
+
1654
1708
  // Build the human-facing instruction block
1655
- const howToOpen = [
1656
- `In Cursor, open ${parallelCount} new Agent windows:`,
1657
- ` • Mac: Cmd+Shift+P → "New Agent" (or click "+" in the Composer panel)`,
1658
- ` Windows: Ctrl+Shift+P "New Agent"`,
1709
+ const howToStart = [
1710
+ `✅ ${parallelCount} builder agent file(s) written to .cursor/agents/`,
1711
+ ``,
1712
+ `TO START PARALLEL BUILDERS IN CURSOR:`,
1713
+ ` 1. Open Background Agents panel: ⌘⇧J (or ⌘⇧P → "Background Agents")`,
1714
+ ` 2. You'll see ${parallelCount} new builder agent(s) listed`,
1715
+ ` 3. Click "Start" for each one — they run in parallel automatically`,
1659
1716
  ``,
1660
- ...kickoffs.map((k, i) => `Window ${i + 1}: paste the prompt for subtask "${k.subtask}"`),
1717
+ ...writtenFiles.map(f => ` Builder ${f.index}: "${f.subtask}" → ${f.file}`),
1661
1718
  ``,
1662
- `Each window will independently call kickoff_task → claim_files → implementcommit.`,
1663
- `They run at the same timeyou don't need to wait for one to finish before starting the other.`,
1719
+ `Each builder calls kickoff_task → claim_files → implementsruns tests → commits.`,
1720
+ `They work on different files simultaneouslyno need to wait for one before starting the other.`,
1664
1721
  ``,
1665
- `After BOTH windows finish and commit, come back to this coordinator window and call:`,
1666
- ` get_parallel_kickoffs(taskId="${taskId}")`,
1722
+ `After all builders finish, come back here and call get_parallel_kickoffs again`,
1667
1723
  `to get the next group's prompts.`,
1668
1724
  ].join('\n')
1669
1725
 
@@ -1672,17 +1728,104 @@ the human opens the windows. Each builder agent then runs independently.`,
1672
1728
  totalGroups: execOrder.length,
1673
1729
  isParallel,
1674
1730
  parallelCount,
1675
- READ_THIS_FIRST: '👇 READ THIS BEFORE DOING ANYTHING',
1731
+ READ_THIS_FIRST: ' BUILDER AGENT FILES WRITTEN — SEE INSTRUCTIONS BELOW',
1676
1732
  COORDINATOR_INSTRUCTION: isParallel
1677
- ? `⚡ GROUP ${nextGroupIndex} of ${execOrder.length}: ${parallelCount} subtasks run IN PARALLEL.\n\n${howToOpen}`
1678
- : `GROUP ${nextGroupIndex} of ${execOrder.length}: 1 sequential subtask.\nOpen 1 new Agent window and paste the prompt below.`,
1679
- kickoffs: kickoffs.map((k, i) => ({
1680
- window: i + 1,
1681
- subtask: k.subtask,
1682
- role: k.role,
1683
- files: k.files,
1684
- PASTE_THIS_IN_NEW_AGENT_WINDOW: k.prompt,
1685
- })),
1733
+ ? `⚡ GROUP ${nextGroupIndex} of ${execOrder.length}: ${parallelCount} builders written as Cursor Background Agents.\n\n${howToStart}`
1734
+ : `GROUP ${nextGroupIndex} of ${execOrder.length}: 1 sequential builder written as a Cursor Background Agent.\n\n${howToStart}`,
1735
+ agentFilesWritten: writtenFiles,
1736
+ })
1737
+ }
1738
+ )
1739
+
1740
+ // ── wait_for_group ─────────────────────────────────────────────────────────────
1741
+ server.tool(
1742
+ 'wait_for_group',
1743
+ `Poll until all subtasks in a decomposition group are marked done. Use after get_parallel_kickoffs.
1744
+
1745
+ COORDINATOR WORKFLOW:
1746
+ 1. Call get_parallel_kickoffs — writes builder agent files, user starts them
1747
+ 2. Call wait_for_group with the groupIndex returned — blocks until all builders finish
1748
+ 3. Call get_parallel_kickoffs again for the next group
1749
+
1750
+ Polls every 10 s, times out after 30 min by default. Returns immediately if already done.`,
1751
+ {
1752
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
1753
+ groupIndex: z.number().int().min(1).describe('1-based group number to wait for (matches group from get_parallel_kickoffs)'),
1754
+ pollIntervalMs: z.number().optional().default(10000).describe('Poll interval in ms (default 10 s)'),
1755
+ timeoutMs: z.number().optional().default(1800000).describe('Max wait in ms (default 30 min)'),
1756
+ },
1757
+ async ({ taskId, groupIndex, pollIntervalMs = 10000, timeoutMs = 1800000 }) => {
1758
+ trackTaskActivity(taskId, 'wait_for_group', { summary: `Waiting for Group ${groupIndex}` })
1759
+ const deadline = Date.now() + timeoutMs
1760
+ let attempts = 0
1761
+
1762
+ while (Date.now() < deadline) {
1763
+ attempts++
1764
+ const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
1765
+ if (!taskRes?.success) return errorText('Task not found')
1766
+ const task = taskRes.data.task
1767
+
1768
+ if (!task.decomposition?.trim()) return errorText('No decomposition found. Call decompose_task first.')
1769
+ let dec
1770
+ try { dec = JSON.parse(task.decomposition) } catch {
1771
+ return errorText('Decomposition JSON is malformed.')
1772
+ }
1773
+
1774
+ const execOrder = dec.executionOrder || []
1775
+ const targetGroup = execOrder[groupIndex - 1]
1776
+ if (!targetGroup) return errorText(`Group ${groupIndex} does not exist. Decomposition has ${execOrder.length} group(s).`)
1777
+
1778
+ const boardSubtasks = task.subtasks || []
1779
+ const isDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
1780
+
1781
+ const doneTitles = targetGroup.filter(t => isDone(t))
1782
+ const pendingTitles = targetGroup.filter(t => !isDone(t))
1783
+
1784
+ if (pendingTitles.length === 0) {
1785
+ return text({
1786
+ done: true,
1787
+ group: groupIndex,
1788
+ subtasks: targetGroup,
1789
+ done_count: doneTitles.length,
1790
+ attempts,
1791
+ message: `✅ All ${targetGroup.length} subtask(s) in Group ${groupIndex} are done after ${attempts} poll(s).`,
1792
+ nextStep: groupIndex < execOrder.length
1793
+ ? `Call get_parallel_kickoffs with taskId="${taskId}" to get Group ${groupIndex + 1}'s builder prompts.`
1794
+ : 'All groups complete. Run final tests then call raise_pr.',
1795
+ })
1796
+ }
1797
+
1798
+ // Log progress every 6 polls (~1 min)
1799
+ if (attempts % 6 === 0) {
1800
+ trackTaskActivity(taskId, 'wait_for_group', {
1801
+ summary: `Still waiting for Group ${groupIndex}: ${pendingTitles.length} pending (${doneTitles.length}/${targetGroup.length} done)`,
1802
+ })
1803
+ }
1804
+
1805
+ if (Date.now() + pollIntervalMs < deadline) {
1806
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
1807
+ } else {
1808
+ break
1809
+ }
1810
+ }
1811
+
1812
+ // Timed out — fetch final state
1813
+ const finalRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
1814
+ const finalTask = finalRes?.data?.task
1815
+ let finalDec
1816
+ try { finalDec = JSON.parse(finalTask?.decomposition || '{}') } catch { finalDec = {} }
1817
+ const finalGroup = (finalDec.executionOrder || [])[groupIndex - 1] || []
1818
+ const finalBoard = finalTask?.subtasks || []
1819
+ const finalIsDone = (t) => finalBoard.some(s => s.done && (s.title === t || s.title.endsWith(t)))
1820
+ const stillPending = finalGroup.filter(t => !finalIsDone(t))
1821
+
1822
+ return text({
1823
+ timedOut: true,
1824
+ group: groupIndex,
1825
+ pending: stillPending,
1826
+ attempts,
1827
+ message: `⏱ Timed out after ${attempts} poll(s). ${stillPending.length} subtask(s) still pending: ${stillPending.join(', ')}`,
1828
+ hint: 'Check the Background Agents panel — a builder may have crashed. Call wait_for_group again to resume waiting, or proceed manually.',
1686
1829
  })
1687
1830
  }
1688
1831
  )
@@ -3009,11 +3152,18 @@ You are a COORDINATOR agent. Your behavioral constraints for this session:
3009
3152
  - Bypass the decomposition step — always decompose before builders start
3010
3153
  - Start coding without a scout report when the codebase is unfamiliar
3011
3154
 
3155
+ **PARALLEL BUILDER WORKFLOW:**
3156
+ 1. Call decompose_task — creates subtasks with file ownership
3157
+ 2. Call get_parallel_kickoffs — writes .cursor/agents/builder-N.md files automatically
3158
+ 3. Tell the user: "Open Background Agents panel (⌘⇧J) and click Start for each builder"
3159
+ 4. Wait for builders to finish, then call get_parallel_kickoffs again for the next group
3160
+
3012
3161
  **WORK STYLE:**
3013
3162
  - Start with decompose_task to get the structured execution plan
3014
3163
  - Ensure no two builder subtasks claim the same file
3015
3164
  - Scout report must exist before builders begin
3016
- - Each builder subtask must have explicit file ownership and a clear scope`,
3165
+ - Each builder subtask must have explicit file ownership and a clear scope
3166
+ - NEVER implement code yourself — your job ends after get_parallel_kickoffs writes the agent files`,
3017
3167
  }
3018
3168
 
3019
3169
  /** Write task-specific cursor rules to .cursor/rules/<taskKey>.mdc in the local repo root.
@@ -4528,6 +4678,19 @@ branchAction values (only needed when current branch ≠ task branch):
4528
4678
  ...unsafeUntracked.map(f => `⚠️ SKIP: ${f} ← do not commit this`),
4529
4679
  ]
4530
4680
 
4681
+ // ── Claims validation ─────────────────────────────────────────────────────
4682
+ // If this task has claimed files, warn if staged changes include unclaimed files.
4683
+ let claimsViolations = []
4684
+ if (taskId) {
4685
+ try {
4686
+ const claimRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
4687
+ const claimedFiles = claimRes?.data?.task?.claimedFiles || []
4688
+ if (claimedFiles.length > 0) {
4689
+ claimsViolations = changedFilesList.filter(f => !claimedFiles.some(cf => f.endsWith(cf) || cf.endsWith(f)))
4690
+ }
4691
+ } catch { /* non-fatal */ }
4692
+ }
4693
+
4531
4694
  // ── Block if task is parked — prevent Dev A pushing after handoff ────────
4532
4695
  if (task) {
4533
4696
  const isParked = !!(task.parkNote?.parkedAt)
@@ -4640,6 +4803,11 @@ branchAction values (only needed when current branch ≠ task branch):
4640
4803
  : null,
4641
4804
  commands: [addCmd, `git commit -m "${commitMsg}"`, pushCmd].filter(Boolean),
4642
4805
  requiresConfirmation: true,
4806
+ claimsViolations: claimsViolations.length > 0 ? {
4807
+ warning: `⚠️ ${claimsViolations.length} file(s) in your staged changes are NOT in your claimed files list. You may only edit claimed files.`,
4808
+ unclaimed: claimsViolations,
4809
+ claimedFiles: changedFilesList.filter(f => !claimsViolations.includes(f)),
4810
+ } : null,
4643
4811
  message: `Suggested: "${commitMsg}". Call commit_helper again with confirmed=true to get the final commands.`,
4644
4812
  })
4645
4813
  }
@@ -4706,6 +4874,10 @@ branchAction values (only needed when current branch ≠ task branch):
4706
4874
  unsafeUntrackedWarning: unsafeUntracked.length
4707
4875
  ? `These were excluded from git add — add them to .gitignore: ${unsafeUntracked.join(', ')}`
4708
4876
  : null,
4877
+ claimsViolations: claimsViolations.length > 0 ? {
4878
+ warning: `⚠️ ${claimsViolations.length} file(s) modified that are NOT in your claimed files. Review before committing.`,
4879
+ unclaimed: claimsViolations,
4880
+ } : null,
4709
4881
  message: `Copy-paste these commands in order.`,
4710
4882
  nextStep: taskBranch && !branchMismatch ? `After pushing, call raise_pr to open the pull request.` : null,
4711
4883
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.29",
3
+ "version": "1.6.31",
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",